controller.go 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688
  1. package tor
  2. import (
  3. "bytes"
  4. "crypto/hmac"
  5. "crypto/rand"
  6. "crypto/sha256"
  7. "encoding/hex"
  8. "errors"
  9. "fmt"
  10. "net/textproto"
  11. "os"
  12. "regexp"
  13. "strconv"
  14. "strings"
  15. "sync/atomic"
  16. )
  17. const (
  18. // success is the Tor Control response code representing a successful
  19. // request.
  20. success = 250
  21. // invalidNumOfArguments is the Tor Control response code representing
  22. // there being an invalid number of arguments.
  23. invalidNumOfArguments = 512
  24. // serviceIDNotRecognized is the Tor Control response code representing
  25. // the specified ServiceID is not recognized.
  26. serviceIDNotRecognized = 552
  27. // nonceLen is the length of a nonce generated by either the controller
  28. // or the Tor server
  29. nonceLen = 32
  30. // cookieLen is the length of the authentication cookie.
  31. cookieLen = 32
  32. // ProtocolInfoVersion is the `protocolinfo` version currently supported
  33. // by the Tor server.
  34. ProtocolInfoVersion = 1
  35. // MinTorVersion is the minimum supported version that the Tor server
  36. // must be running on. This is needed in order to create v3 onion
  37. // services through Tor's control port.
  38. MinTorVersion = "0.3.3.6"
  39. // authSafeCookie is the name of the SAFECOOKIE authentication method.
  40. authSafeCookie = "SAFECOOKIE"
  41. // authHashedPassword is the name of the HASHEDPASSWORD authentication
  42. // method.
  43. authHashedPassword = "HASHEDPASSWORD"
  44. // authNull is the name of the NULL authentication method.
  45. authNull = "NULL"
  46. )
  47. var (
  48. // serverKey is the key used when computing the HMAC-SHA256 of a message
  49. // from the server.
  50. serverKey = []byte("Tor safe cookie authentication " +
  51. "server-to-controller hash")
  52. // controllerKey is the key used when computing the HMAC-SHA256 of a
  53. // message from the controller.
  54. controllerKey = []byte("Tor safe cookie authentication " +
  55. "controller-to-server hash")
  56. // errCodeNotMatch is used when an expected response code is not
  57. // returned.
  58. errCodeNotMatch = errors.New("unexpected code")
  59. // errTCNotStarted is used when we require the tor controller to be
  60. // started while it's not.
  61. errTCNotStarted = errors.New("tor controller must be started")
  62. // errTCNotStarted is used when we require the tor controller to be
  63. // not stopped while it is.
  64. errTCStopped = errors.New("tor controller must not be stopped")
  65. // replyFieldRegexp is the regular expression used to find fields in a
  66. // reply. Parameters within a reply should be of the form KEY=VALUE or
  67. // KEY="VALUE", where quoted values might contain spaces, newlines and
  68. // quoted pairs. If the parameter doesn't contain "=", then we can
  69. // assume it doesn't provide any relevant information that isn't already
  70. // known. Read more on this topic:
  71. // https://gitweb.torproject.org/torspec.git/tree/control-spec.txt#n188
  72. replyFieldRegexp = regexp.MustCompile(
  73. `[^" \r\n]+=(?:"(?:[^"\\]|\\[\0-\x7F])*"|[^" \r\n]*)`,
  74. )
  75. )
  76. // Controller is an implementation of the Tor Control protocol. This is used in
  77. // order to communicate with a Tor server. Its only supported method of
  78. // authentication is the SAFECOOKIE method.
  79. //
  80. // NOTE: The connection to the Tor server must be authenticated before
  81. // proceeding to send commands. Otherwise, the connection will be closed.
  82. //
  83. // TODO:
  84. // - if adding support for more commands, extend this with a command queue?
  85. // - place under sub-package?
  86. // - support async replies from the server
  87. type Controller struct {
  88. // started is used atomically in order to prevent multiple calls to
  89. // Start.
  90. started int32
  91. // stopped is used atomically in order to prevent multiple calls to
  92. // Stop.
  93. stopped int32
  94. // conn is the underlying connection between the controller and the
  95. // Tor server. It provides read and write methods to simplify the
  96. // text-based messages within the connection.
  97. conn *textproto.Conn
  98. // controlAddr is the host:port the Tor server is listening locally for
  99. // controller connections on.
  100. controlAddr string
  101. // password, if non-empty, signals that the controller should attempt to
  102. // authenticate itself with the backing Tor daemon through the
  103. // HASHEDPASSWORD authentication method with this value.
  104. password string
  105. // version is the current version of the Tor server.
  106. version string
  107. // targetIPAddress is the IP address which we tell the Tor server to use
  108. // to connect to the LND node. This is required when the Tor server
  109. // runs on another host, otherwise the service will not be reachable.
  110. targetIPAddress string
  111. // activeServiceID is the Onion ServiceID created by ADD_ONION.
  112. activeServiceID string
  113. }
  114. // NewController returns a new Tor controller that will be able to interact with
  115. // a Tor server.
  116. func NewController(controlAddr string, targetIPAddress string,
  117. password string) *Controller {
  118. return &Controller{
  119. controlAddr: controlAddr,
  120. targetIPAddress: targetIPAddress,
  121. password: password,
  122. }
  123. }
  124. // Start establishes and authenticates the connection between the controller
  125. // and a Tor server. Once done, the controller will be able to send commands
  126. // and expect responses.
  127. func (c *Controller) Start() error {
  128. if !atomic.CompareAndSwapInt32(&c.started, 0, 1) {
  129. return nil
  130. }
  131. log.Info("Starting tor controller")
  132. conn, err := textproto.Dial("tcp", c.controlAddr)
  133. if err != nil {
  134. return fmt.Errorf("unable to connect to Tor server: %w", err)
  135. }
  136. c.conn = conn
  137. return c.authenticate()
  138. }
  139. // Stop closes the connection between the controller and the Tor server.
  140. func (c *Controller) Stop() error {
  141. if !atomic.CompareAndSwapInt32(&c.stopped, 0, 1) {
  142. return nil
  143. }
  144. log.Info("Stopping tor controller")
  145. // Remove the onion service.
  146. if err := c.DelOnion(c.activeServiceID); err != nil {
  147. log.Errorf("DEL_ONION got error: %v", err)
  148. return err
  149. }
  150. // Reset service ID.
  151. c.activeServiceID = ""
  152. return c.conn.Close()
  153. }
  154. // Reconnect makes a new socket connection between the tor controller and
  155. // daemon. It will attempt to close the old connection, make a new connection
  156. // and authenticate, and finally reset the activeServiceID that the controller
  157. // is aware of.
  158. //
  159. // NOTE: Any old onion services will be removed once this function is called.
  160. // In the case of a Tor daemon restart, previously created onion services will
  161. // no longer be there. If the function is called without a Tor daemon restart,
  162. // because the control connection is reset, all the onion services belonging to
  163. // the old connection will be removed.
  164. func (c *Controller) Reconnect() error {
  165. // Require the tor controller to be running when we want to reconnect.
  166. // This means the started flag must be 1 and the stopped flag must be
  167. // 0.
  168. if c.started != 1 {
  169. return errTCNotStarted
  170. }
  171. if c.stopped != 0 {
  172. return errTCStopped
  173. }
  174. log.Info("Re-connectting tor controller")
  175. // If we have an old connection, try to close it. We might receive an
  176. // error if the connection has already been closed by Tor daemon(ie,
  177. // daemon restarted), so we ignore the error here.
  178. if c.conn != nil {
  179. if err := c.conn.Close(); err != nil {
  180. log.Debugf("closing old conn got err: %v", err)
  181. }
  182. }
  183. // Make a new connection and authenticate.
  184. conn, err := textproto.Dial("tcp", c.controlAddr)
  185. if err != nil {
  186. return fmt.Errorf("unable to connect to Tor server: %w", err)
  187. }
  188. c.conn = conn
  189. // Authenticate the connection between the controller and Tor daemon.
  190. if err := c.authenticate(); err != nil {
  191. return err
  192. }
  193. // Reset the activeServiceID. This value would only be set if a
  194. // previous onion service was created. Because the old connection has
  195. // been closed at this point, the old onion service is no longer
  196. // active.
  197. c.activeServiceID = ""
  198. return nil
  199. }
  200. // sendCommand sends a command to the Tor server and returns its response, as a
  201. // single space-delimited string, and code.
  202. func (c *Controller) sendCommand(command string) (int, string, error) {
  203. id, err := c.conn.Cmd(command)
  204. if err != nil {
  205. return 0, "", err
  206. }
  207. // Make sure our reader only process the response returned from the
  208. // above command.
  209. c.conn.StartResponse(id)
  210. defer c.conn.EndResponse(id)
  211. code, reply, err := c.readResponse(success)
  212. if err != nil {
  213. log.Debugf("sendCommand:%s got err:%v, reply:%v",
  214. command, err, reply)
  215. return code, reply, err
  216. }
  217. return code, reply, nil
  218. }
  219. // readResponse reads the replies from Tor to the controller. The reply has the
  220. // following format,
  221. //
  222. // Reply = SyncReply / AsyncReply
  223. // SyncReply = *(MidReplyLine / DataReplyLine) EndReplyLine
  224. // AsyncReply = *(MidReplyLine / DataReplyLine) EndReplyLine
  225. //
  226. // MidReplyLine = StatusCode "-" ReplyLine
  227. // DataReplyLine = StatusCode "+" ReplyLine CmdData
  228. // EndReplyLine = StatusCode SP ReplyLine
  229. // ReplyLine = [ReplyText] CRLF
  230. // ReplyText = XXXX
  231. // StatusCode = 3DIGIT
  232. //
  233. // Unless specified otherwise, multiple lines in a single reply from Tor daemon
  234. // to the controller are guaranteed to share the same status code. Read more on
  235. // this topic:
  236. //
  237. // https://gitweb.torproject.org/torspec.git/tree/control-spec.txt#n158
  238. //
  239. // NOTE: this code is influenced by https://github.com/Yawning/bulb.
  240. func (c *Controller) readResponse(expected int) (int, string, error) {
  241. // Clean the buffer inside the conn. This is needed when we encountered
  242. // an error while reading the response, the remaining lines need to be
  243. // cleaned before next read.
  244. defer func() {
  245. if _, err := c.conn.R.Discard(c.conn.R.Buffered()); err != nil {
  246. log.Errorf("clean read buffer failed: %v", err)
  247. }
  248. }()
  249. reply, code := "", 0
  250. hasMoreLines := true
  251. for hasMoreLines {
  252. line, err := c.conn.Reader.ReadLine()
  253. if err != nil {
  254. return 0, reply, err
  255. }
  256. log.Tracef("Reading line: %v", line)
  257. // Line being shortter than 4 is not allowed.
  258. if len(line) < 4 {
  259. err = textproto.ProtocolError("short line: " + line)
  260. return 0, reply, err
  261. }
  262. // Parse the status code.
  263. code, err = strconv.Atoi(line[0:3])
  264. if err != nil {
  265. return code, reply, err
  266. }
  267. switch line[3] {
  268. // EndReplyLine = StatusCode SP ReplyLine.
  269. // Example: 250 OK
  270. // This is the end of the response, so we mark hasMoreLines to
  271. // be false to exit the loop.
  272. case ' ':
  273. reply += line[4:]
  274. hasMoreLines = false
  275. // MidReplyLine = StatusCode "-" ReplyLine.
  276. // Example: 250-version=...
  277. // This is a continued response, so we keep reading the next
  278. // line.
  279. case '-':
  280. reply += line[4:]
  281. // DataReplyLine = StatusCode "+" ReplyLine CmdData.
  282. // Example: 250+config-text=
  283. // line1
  284. // line2
  285. // more lines...
  286. // .
  287. // This is a data response, meaning the following multiple
  288. // lines are the actual data, and a dot(.) in the end means the
  289. // end of the data response. The response will be formatted as,
  290. // key=line1,line2,...
  291. // The above example will then be,
  292. // config-text=line1,line2,...
  293. case '+':
  294. // Add the key(config-text=)
  295. reply += line[4:]
  296. // Add the values.
  297. resp, err := c.conn.Reader.ReadDotLines()
  298. if err != nil {
  299. return code, reply, err
  300. }
  301. reply += strings.Join(resp, ",")
  302. // Invalid line separator found.
  303. default:
  304. err = textproto.ProtocolError("invalid line: " + line)
  305. return code, reply, err
  306. }
  307. // We check the code here so that the error message is parsed
  308. // from the line.
  309. if code != expected {
  310. return code, reply, errCodeNotMatch
  311. }
  312. // Separate each line using "\n".
  313. if hasMoreLines {
  314. reply += "\n"
  315. }
  316. }
  317. log.Tracef("Parsed reply: %v", reply)
  318. return code, reply, nil
  319. }
  320. // unescapeValue removes escape codes from the value in the Tor reply. A
  321. // backslash followed by any character represents that character, so we remove
  322. // any backslash not preceded by another backslash.
  323. func unescapeValue(value string) string {
  324. newString := ""
  325. justRemovedBackslash := false
  326. for _, char := range value {
  327. if char == '\\' && !justRemovedBackslash {
  328. justRemovedBackslash = true
  329. continue
  330. }
  331. newString += string(char)
  332. justRemovedBackslash = false
  333. }
  334. return newString
  335. }
  336. // parseTorReply parses the reply from the Tor server after receiving a command
  337. // from a controller. This will parse the relevant reply parameters into a map
  338. // of keys and values.
  339. func parseTorReply(reply string) map[string]string {
  340. params := make(map[string]string)
  341. // Find all fields of a reply. The -1 indicates that we want this to
  342. // find all instances of the regexp.
  343. contents := replyFieldRegexp.FindAllString(reply, -1)
  344. for _, content := range contents {
  345. // Each parameter within the reply should be of the form
  346. // KEY=VALUE or KEY="VALUE".
  347. keyValue := strings.SplitN(content, "=", 2)
  348. key := keyValue[0]
  349. value := keyValue[1]
  350. // Quoted strings need extra processing.
  351. if strings.HasPrefix(value, `"`) {
  352. // Remove quotes around the value.
  353. value = value[1 : len(value)-1]
  354. // Unescape the value.
  355. value = unescapeValue(value)
  356. }
  357. params[key] = value
  358. }
  359. return params
  360. }
  361. // authenticate authenticates the connection between the controller and the
  362. // Tor server using either of the following supported authentication methods
  363. // depending on its configuration: SAFECOOKIE, HASHEDPASSWORD, and NULL.
  364. func (c *Controller) authenticate() error {
  365. protocolInfo, err := c.protocolInfo()
  366. if err != nil {
  367. return err
  368. }
  369. log.Debugf("received protocol info: %v", protocolInfo)
  370. // With the version retrieved, we'll cache it now in case it needs to be
  371. // used later on.
  372. c.version = protocolInfo.version()
  373. switch {
  374. // If a password was provided, then we should attempt to use the
  375. // HASHEDPASSWORD authentication method.
  376. case c.password != "":
  377. if !protocolInfo.supportsAuthMethod(authHashedPassword) {
  378. return fmt.Errorf("%v authentication method not "+
  379. "supported", authHashedPassword)
  380. }
  381. return c.authenticateViaHashedPassword()
  382. // Otherwise, attempt to authentication via the SAFECOOKIE method as it
  383. // provides the most security.
  384. case protocolInfo.supportsAuthMethod(authSafeCookie):
  385. return c.authenticateViaSafeCookie(protocolInfo)
  386. // Fallback to the NULL method if any others aren't supported.
  387. case protocolInfo.supportsAuthMethod(authNull):
  388. return c.authenticateViaNull()
  389. // No supported authentication methods, fail.
  390. default:
  391. return errors.New("the Tor server must be configured with " +
  392. "NULL, SAFECOOKIE, or HASHEDPASSWORD authentication")
  393. }
  394. }
  395. // authenticateViaNull authenticates the controller with the Tor server using
  396. // the NULL authentication method.
  397. func (c *Controller) authenticateViaNull() error {
  398. _, _, err := c.sendCommand("AUTHENTICATE")
  399. return err
  400. }
  401. // authenticateViaHashedPassword authenticates the controller with the Tor
  402. // server using the HASHEDPASSWORD authentication method.
  403. func (c *Controller) authenticateViaHashedPassword() error {
  404. cmd := fmt.Sprintf("AUTHENTICATE \"%s\"", c.password)
  405. _, _, err := c.sendCommand(cmd)
  406. return err
  407. }
  408. // authenticateViaSafeCookie authenticates the controller with the Tor server
  409. // using the SAFECOOKIE authentication method.
  410. func (c *Controller) authenticateViaSafeCookie(info protocolInfo) error {
  411. // Before proceeding to authenticate the connection, we'll retrieve
  412. // the authentication cookie of the Tor server. This will be used
  413. // throughout the authentication routine. We do this before as once the
  414. // authentication routine has begun, it is not possible to retrieve it
  415. // mid-way.
  416. cookie, err := c.getAuthCookie(info)
  417. if err != nil {
  418. return fmt.Errorf("unable to retrieve authentication cookie: "+
  419. "%v", err)
  420. }
  421. // Authenticating using the SAFECOOKIE authentication method is a two
  422. // step process. We'll kick off the authentication routine by sending
  423. // the AUTHCHALLENGE command followed by a hex-encoded 32-byte nonce.
  424. clientNonce := make([]byte, nonceLen)
  425. if _, err := rand.Read(clientNonce); err != nil {
  426. return fmt.Errorf("unable to generate client nonce: %w", err)
  427. }
  428. cmd := fmt.Sprintf("AUTHCHALLENGE SAFECOOKIE %x", clientNonce)
  429. _, reply, err := c.sendCommand(cmd)
  430. if err != nil {
  431. return err
  432. }
  433. // If successful, the reply from the server should be of the following
  434. // format:
  435. //
  436. // "250 AUTHCHALLENGE"
  437. // SP "SERVERHASH=" ServerHash
  438. // SP "SERVERNONCE=" ServerNonce
  439. // CRLF
  440. //
  441. // We're interested in retrieving the SERVERHASH and SERVERNONCE
  442. // parameters, so we'll parse our reply to do so.
  443. replyParams := parseTorReply(reply)
  444. // Once retrieved, we'll ensure these values are of proper length when
  445. // decoded.
  446. serverHash, ok := replyParams["SERVERHASH"]
  447. if !ok {
  448. return errors.New("server hash not found in reply")
  449. }
  450. decodedServerHash, err := hex.DecodeString(serverHash)
  451. if err != nil {
  452. return fmt.Errorf("unable to decode server hash: %w", err)
  453. }
  454. if len(decodedServerHash) != sha256.Size {
  455. return errors.New("invalid server hash length")
  456. }
  457. serverNonce, ok := replyParams["SERVERNONCE"]
  458. if !ok {
  459. return errors.New("server nonce not found in reply")
  460. }
  461. decodedServerNonce, err := hex.DecodeString(serverNonce)
  462. if err != nil {
  463. return fmt.Errorf("unable to decode server nonce: %w", err)
  464. }
  465. if len(decodedServerNonce) != nonceLen {
  466. return errors.New("invalid server nonce length")
  467. }
  468. // The server hash above was constructed by computing the HMAC-SHA256
  469. // of the message composed of the cookie, client nonce, and server
  470. // nonce. We'll redo this computation ourselves to ensure the integrity
  471. // and authentication of the message.
  472. hmacMessage := bytes.Join(
  473. [][]byte{cookie, clientNonce, decodedServerNonce}, []byte{},
  474. )
  475. computedServerHash := computeHMAC256(serverKey, hmacMessage)
  476. if !hmac.Equal(computedServerHash, decodedServerHash) {
  477. return fmt.Errorf("expected server hash %x, got %x",
  478. decodedServerHash, computedServerHash)
  479. }
  480. // If the MAC check was successful, we'll proceed with the last step of
  481. // the authentication routine. We'll now send the AUTHENTICATE command
  482. // followed by a hex-encoded client hash constructed by computing the
  483. // HMAC-SHA256 of the same message, but this time using the controller's
  484. // key.
  485. clientHash := computeHMAC256(controllerKey, hmacMessage)
  486. if len(clientHash) != sha256.Size {
  487. return errors.New("invalid client hash length")
  488. }
  489. cmd = fmt.Sprintf("AUTHENTICATE %x", clientHash)
  490. if _, _, err := c.sendCommand(cmd); err != nil {
  491. return err
  492. }
  493. return nil
  494. }
  495. // getAuthCookie retrieves the authentication cookie in bytes from the Tor
  496. // server. Cookie authentication must be enabled for this to work.
  497. func (c *Controller) getAuthCookie(info protocolInfo) ([]byte, error) {
  498. // Retrieve the cookie file path from the PROTOCOLINFO reply.
  499. cookieFilePath, ok := info["COOKIEFILE"]
  500. if !ok {
  501. return nil, errors.New("COOKIEFILE not found in PROTOCOLINFO " +
  502. "reply")
  503. }
  504. cookieFilePath = strings.Trim(cookieFilePath, "\"")
  505. // Read the cookie from the file and ensure it has the correct length.
  506. cookie, err := os.ReadFile(cookieFilePath)
  507. if err != nil {
  508. return nil, err
  509. }
  510. if len(cookie) != cookieLen {
  511. return nil, errors.New("invalid authentication cookie length")
  512. }
  513. return cookie, nil
  514. }
  515. // computeHMAC256 computes the HMAC-SHA256 of a key and message.
  516. func computeHMAC256(key, message []byte) []byte {
  517. mac := hmac.New(sha256.New, key)
  518. mac.Write(message)
  519. return mac.Sum(nil)
  520. }
  521. // supportsV3 is a helper function that parses the current version of the Tor
  522. // server and determines whether it supports creating v3 onion services through
  523. // Tor's control port. The version string should be of the format:
  524. //
  525. // major.minor.revision.build
  526. func supportsV3(version string) error {
  527. // We'll split the minimum Tor version that's supported and the given
  528. // version in order to individually compare each number.
  529. parts := strings.Split(version, ".")
  530. if len(parts) != 4 {
  531. return errors.New("version string is not of the format " +
  532. "major.minor.revision.build")
  533. }
  534. // It's possible that the build number (the last part of the version
  535. // string) includes a pre-release string, e.g. rc, beta, etc., so we'll
  536. // parse that as well.
  537. build := strings.Split(parts[len(parts)-1], "-")
  538. parts[len(parts)-1] = build[0]
  539. // Ensure that each part of the version string corresponds to a number.
  540. for _, part := range parts {
  541. if _, err := strconv.Atoi(part); err != nil {
  542. return err
  543. }
  544. }
  545. // Once we've determined we have a proper version string of the format
  546. // major.minor.revision.build, we can just do a string comparison to
  547. // determine if it satisfies the minimum version supported.
  548. if version < MinTorVersion {
  549. return fmt.Errorf("version %v below minimum version supported "+
  550. "%v", version, MinTorVersion)
  551. }
  552. return nil
  553. }
  554. // protocolInfo is encompasses the details of a response to a PROTOCOLINFO
  555. // command.
  556. type protocolInfo map[string]string
  557. // version returns the Tor version as reported by the server.
  558. func (i protocolInfo) version() string {
  559. version := i["Tor"]
  560. return strings.Trim(version, "\"")
  561. }
  562. // supportsAuthMethod determines whether the Tor server supports the given
  563. // authentication method.
  564. func (i protocolInfo) supportsAuthMethod(method string) bool {
  565. methods, ok := i["METHODS"]
  566. if !ok {
  567. return false
  568. }
  569. return strings.Contains(methods, method)
  570. }
  571. // protocolInfo sends a "PROTOCOLINFO" command to the Tor server and returns its
  572. // response.
  573. func (c *Controller) protocolInfo() (protocolInfo, error) {
  574. cmd := fmt.Sprintf("PROTOCOLINFO %d", ProtocolInfoVersion)
  575. _, reply, err := c.sendCommand(cmd)
  576. if err != nil {
  577. return nil, err
  578. }
  579. return protocolInfo(parseTorReply(reply)), nil
  580. }