Initial import.
This commit is contained in:
commit
90a4be2923
|
@ -0,0 +1,3 @@
|
||||||
|
*.swp
|
||||||
|
*~
|
||||||
|
examples/basic/basic
|
|
@ -0,0 +1,122 @@
|
||||||
|
Creative Commons Legal Code
|
||||||
|
|
||||||
|
CC0 1.0 Universal
|
||||||
|
|
||||||
|
CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
|
||||||
|
LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
|
||||||
|
ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
|
||||||
|
INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
|
||||||
|
REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
|
||||||
|
PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
|
||||||
|
THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
|
||||||
|
HEREUNDER.
|
||||||
|
|
||||||
|
Statement of Purpose
|
||||||
|
|
||||||
|
The laws of most jurisdictions throughout the world automatically confer
|
||||||
|
exclusive Copyright and Related Rights (defined below) upon the creator
|
||||||
|
and subsequent owner(s) (each and all, an "owner") of an original work of
|
||||||
|
authorship and/or a database (each, a "Work").
|
||||||
|
|
||||||
|
Certain owners wish to permanently relinquish those rights to a Work for
|
||||||
|
the purpose of contributing to a commons of creative, cultural and
|
||||||
|
scientific works ("Commons") that the public can reliably and without fear
|
||||||
|
of later claims of infringement build upon, modify, incorporate in other
|
||||||
|
works, reuse and redistribute as freely as possible in any form whatsoever
|
||||||
|
and for any purposes, including without limitation commercial purposes.
|
||||||
|
These owners may contribute to the Commons to promote the ideal of a free
|
||||||
|
culture and the further production of creative, cultural and scientific
|
||||||
|
works, or to gain reputation or greater distribution for their Work in
|
||||||
|
part through the use and efforts of others.
|
||||||
|
|
||||||
|
For these and/or other purposes and motivations, and without any
|
||||||
|
expectation of additional consideration or compensation, the person
|
||||||
|
associating CC0 with a Work (the "Affirmer"), to the extent that he or she
|
||||||
|
is an owner of Copyright and Related Rights in the Work, voluntarily
|
||||||
|
elects to apply CC0 to the Work and publicly distribute the Work under its
|
||||||
|
terms, with knowledge of his or her Copyright and Related Rights in the
|
||||||
|
Work and the meaning and intended legal effect of CC0 on those rights.
|
||||||
|
|
||||||
|
1. Copyright and Related Rights. A Work made available under CC0 may be
|
||||||
|
protected by copyright and related or neighboring rights ("Copyright and
|
||||||
|
Related Rights"). Copyright and Related Rights include, but are not
|
||||||
|
limited to, the following:
|
||||||
|
|
||||||
|
i. the right to reproduce, adapt, distribute, perform, display,
|
||||||
|
communicate, and translate a Work;
|
||||||
|
ii. moral rights retained by the original author(s) and/or performer(s);
|
||||||
|
iii. publicity and privacy rights pertaining to a person's image or
|
||||||
|
likeness depicted in a Work;
|
||||||
|
iv. rights protecting against unfair competition in regards to a Work,
|
||||||
|
subject to the limitations in paragraph 4(a), below;
|
||||||
|
v. rights protecting the extraction, dissemination, use and reuse of data
|
||||||
|
in a Work;
|
||||||
|
vi. database rights (such as those arising under Directive 96/9/EC of the
|
||||||
|
European Parliament and of the Council of 11 March 1996 on the legal
|
||||||
|
protection of databases, and under any national implementation
|
||||||
|
thereof, including any amended or successor version of such
|
||||||
|
directive); and
|
||||||
|
vii. other similar, equivalent or corresponding rights throughout the
|
||||||
|
world based on applicable law or treaty, and any national
|
||||||
|
implementations thereof.
|
||||||
|
|
||||||
|
2. Waiver. To the greatest extent permitted by, but not in contravention
|
||||||
|
of, applicable law, Affirmer hereby overtly, fully, permanently,
|
||||||
|
irrevocably and unconditionally waives, abandons, and surrenders all of
|
||||||
|
Affirmer's Copyright and Related Rights and associated claims and causes
|
||||||
|
of action, whether now known or unknown (including existing as well as
|
||||||
|
future claims and causes of action), in the Work (i) in all territories
|
||||||
|
worldwide, (ii) for the maximum duration provided by applicable law or
|
||||||
|
treaty (including future time extensions), (iii) in any current or future
|
||||||
|
medium and for any number of copies, and (iv) for any purpose whatsoever,
|
||||||
|
including without limitation commercial, advertising or promotional
|
||||||
|
purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
|
||||||
|
member of the public at large and to the detriment of Affirmer's heirs and
|
||||||
|
successors, fully intending that such Waiver shall not be subject to
|
||||||
|
revocation, rescission, cancellation, termination, or any other legal or
|
||||||
|
equitable action to disrupt the quiet enjoyment of the Work by the public
|
||||||
|
as contemplated by Affirmer's express Statement of Purpose.
|
||||||
|
|
||||||
|
3. Public License Fallback. Should any part of the Waiver for any reason
|
||||||
|
be judged legally invalid or ineffective under applicable law, then the
|
||||||
|
Waiver shall be preserved to the maximum extent permitted taking into
|
||||||
|
account Affirmer's express Statement of Purpose. In addition, to the
|
||||||
|
extent the Waiver is so judged Affirmer hereby grants to each affected
|
||||||
|
person a royalty-free, non transferable, non sublicensable, non exclusive,
|
||||||
|
irrevocable and unconditional license to exercise Affirmer's Copyright and
|
||||||
|
Related Rights in the Work (i) in all territories worldwide, (ii) for the
|
||||||
|
maximum duration provided by applicable law or treaty (including future
|
||||||
|
time extensions), (iii) in any current or future medium and for any number
|
||||||
|
of copies, and (iv) for any purpose whatsoever, including without
|
||||||
|
limitation commercial, advertising or promotional purposes (the
|
||||||
|
"License"). The License shall be deemed effective as of the date CC0 was
|
||||||
|
applied by Affirmer to the Work. Should any part of the License for any
|
||||||
|
reason be judged legally invalid or ineffective under applicable law, such
|
||||||
|
partial invalidity or ineffectiveness shall not invalidate the remainder
|
||||||
|
of the License, and in such case Affirmer hereby affirms that he or she
|
||||||
|
will not (i) exercise any of his or her remaining Copyright and Related
|
||||||
|
Rights in the Work or (ii) assert any associated claims and causes of
|
||||||
|
action with respect to the Work, in either case contrary to Affirmer's
|
||||||
|
express Statement of Purpose.
|
||||||
|
|
||||||
|
4. Limitations and Disclaimers.
|
||||||
|
|
||||||
|
a. No trademark or patent rights held by Affirmer are waived, abandoned,
|
||||||
|
surrendered, licensed or otherwise affected by this document.
|
||||||
|
b. Affirmer offers the Work as-is and makes no representations or
|
||||||
|
warranties of any kind concerning the Work, express, implied,
|
||||||
|
statutory or otherwise, including without limitation warranties of
|
||||||
|
title, merchantability, fitness for a particular purpose, non
|
||||||
|
infringement, or the absence of latent or other defects, accuracy, or
|
||||||
|
the present or absence of errors, whether or not discoverable, all to
|
||||||
|
the greatest extent permissible under applicable law.
|
||||||
|
c. Affirmer disclaims responsibility for clearing rights of other persons
|
||||||
|
that may apply to the Work or any use thereof, including without
|
||||||
|
limitation any person's Copyright and Related Rights in the Work.
|
||||||
|
Further, Affirmer disclaims responsibility for obtaining any necessary
|
||||||
|
consents, permissions or other rights required for any use of the
|
||||||
|
Work.
|
||||||
|
d. Affirmer understands and acknowledges that Creative Commons is not a
|
||||||
|
party to this document and has no duty or obligation with respect to
|
||||||
|
this CC0 or use of the Work.
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
## bulb - Is not stem
|
||||||
|
### Yawning Angel (yawning at torproject dot org)
|
||||||
|
|
||||||
|
bulb is a Go language interface to the Tor control port. It is considerably
|
||||||
|
lighter in functionality than stem and other controller libraries, and is
|
||||||
|
intended to be used in combination with`control-spec.txt`.
|
||||||
|
|
||||||
|
It was written primarily as a not-invented-here hack, and the base interface is
|
||||||
|
more than likely to stay fairly low level, though useful helpers will be added
|
||||||
|
as I need them.
|
||||||
|
|
||||||
|
Things you should probably use instead:
|
||||||
|
* [stem](https://stem.torproject.org)
|
||||||
|
* [orc](https://github.com/sycamoreone/orc)
|
||||||
|
|
||||||
|
Bugs:
|
||||||
|
* bulb does not send the 'QUIT' command before closing the connection.
|
|
@ -0,0 +1,124 @@
|
||||||
|
// cmd_authenticate.go - AUTHENTICATE/AUTHCHALLENGE commands.
|
||||||
|
//
|
||||||
|
// To the extent possible under law, Yawning Angel waived all copyright
|
||||||
|
// and related or neighboring rights to bulb, using the creative
|
||||||
|
// commons "cc0" public domain dedication. See LICENSE or
|
||||||
|
// <http://creativecommons.org/publicdomain/zero/1.0/> for full details.
|
||||||
|
|
||||||
|
package bulb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/hmac"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
|
"io/ioutil"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Authenticate authenticates with the Tor instance using the "best" possible
|
||||||
|
// authentication method.
|
||||||
|
func (c *Conn) Authenticate() error {
|
||||||
|
if c.isAuthenticated {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine the supported authentication methods, and the cookie path.
|
||||||
|
pi, err := c.ProtocolInfo()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Add support for password authentication. "COOKIE" auth is
|
||||||
|
// superceded by "SAFECOOKIE" on all reasonable versions of Tor.
|
||||||
|
const (
|
||||||
|
cmdAuthenticate = "AUTHENTICATE"
|
||||||
|
authMethodNull = "NULL"
|
||||||
|
authMethodSafeCookie = "SAFECOOKIE"
|
||||||
|
)
|
||||||
|
if pi.AuthMethods[authMethodNull] {
|
||||||
|
_, err = c.Request(cmdAuthenticate)
|
||||||
|
c.isAuthenticated = err == nil
|
||||||
|
return err
|
||||||
|
} else if pi.AuthMethods[authMethodSafeCookie] {
|
||||||
|
const (
|
||||||
|
authCookieLength = 32
|
||||||
|
authNonceLength = 32
|
||||||
|
authHashLength = 32
|
||||||
|
|
||||||
|
authServerHashKey = "Tor safe cookie authentication server-to-controller hash"
|
||||||
|
authClientHashKey = "Tor safe cookie authentication controller-to-server hash"
|
||||||
|
)
|
||||||
|
|
||||||
|
if pi.CookieFile == "" {
|
||||||
|
return newProtocolError("invalid (empty) COOKIEFILE")
|
||||||
|
}
|
||||||
|
cookie, err := ioutil.ReadFile(pi.CookieFile)
|
||||||
|
if err != nil {
|
||||||
|
return newProtocolError("failed to read COOKIEFILE: %v", err)
|
||||||
|
} else if len(cookie) != authCookieLength {
|
||||||
|
return newProtocolError("invalid cookie file length: %d", len(cookie))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send an AUTHCHALLENGE command, and parse the response.
|
||||||
|
var clientNonce [authNonceLength]byte
|
||||||
|
if _, err := rand.Read(clientNonce[:]); err != nil {
|
||||||
|
return newProtocolError("failed to generate clientNonce: %v", err)
|
||||||
|
}
|
||||||
|
clientNonceStr := hex.EncodeToString(clientNonce[:])
|
||||||
|
resp, err := c.Request("AUTHCHALLENGE %s %s", authMethodSafeCookie, clientNonceStr)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
splitResp := strings.Split(resp.Reply, " ")
|
||||||
|
if len(splitResp) != 3 {
|
||||||
|
return newProtocolError("invalid AUTHCHALLENGE response")
|
||||||
|
}
|
||||||
|
serverHashStr := strings.TrimPrefix(splitResp[1], "SERVERHASH=")
|
||||||
|
if serverHashStr == splitResp[1] {
|
||||||
|
return newProtocolError("missing SERVERHASH")
|
||||||
|
}
|
||||||
|
serverHash, err := hex.DecodeString(serverHashStr)
|
||||||
|
if err != nil {
|
||||||
|
return newProtocolError("failed to decode ServerHash: %v", err)
|
||||||
|
}
|
||||||
|
if len(serverHash) != authHashLength {
|
||||||
|
return newProtocolError("invalid ServerHash length: %d", len(serverHash))
|
||||||
|
}
|
||||||
|
serverNonceStr := strings.TrimPrefix(splitResp[2], "SERVERNONCE=")
|
||||||
|
if serverNonceStr == splitResp[2] {
|
||||||
|
return newProtocolError("missing SERVERNONCE")
|
||||||
|
}
|
||||||
|
serverNonce, err := hex.DecodeString(serverNonceStr)
|
||||||
|
if err != nil {
|
||||||
|
return newProtocolError("failed to decode ServerNonce: %v", err)
|
||||||
|
}
|
||||||
|
if len(serverNonce) != authNonceLength {
|
||||||
|
return newProtocolError("invalid ServerNonce length: %d", len(serverNonce))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate the ServerHash.
|
||||||
|
m := hmac.New(sha256.New, []byte(authServerHashKey))
|
||||||
|
m.Write(cookie)
|
||||||
|
m.Write(clientNonce[:])
|
||||||
|
m.Write(serverNonce)
|
||||||
|
dervServerHash := m.Sum(nil)
|
||||||
|
if !hmac.Equal(serverHash, dervServerHash) {
|
||||||
|
return newProtocolError("invalid ServerHash: mismatch")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate the ClientHash, and issue the AUTHENTICATE.
|
||||||
|
m = hmac.New(sha256.New, []byte(authClientHashKey))
|
||||||
|
m.Write(cookie)
|
||||||
|
m.Write(clientNonce[:])
|
||||||
|
m.Write(serverNonce)
|
||||||
|
clientHash := m.Sum(nil)
|
||||||
|
clientHashStr := hex.EncodeToString(clientHash)
|
||||||
|
|
||||||
|
_, err = c.Request("%s %s", cmdAuthenticate, clientHashStr)
|
||||||
|
c.isAuthenticated = err == nil
|
||||||
|
return err
|
||||||
|
} else {
|
||||||
|
return newProtocolError("no supported authentication methods")
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,83 @@
|
||||||
|
// cmd_protocolinfo.go - PROTOCOLINFO command.
|
||||||
|
//
|
||||||
|
// To the extent possible under law, Yawning Angel waived all copyright
|
||||||
|
// and related or neighboring rights to bulb, using the creative
|
||||||
|
// commons "cc0" public domain dedication. See LICENSE or
|
||||||
|
// <http://creativecommons.org/publicdomain/zero/1.0/> for full details.
|
||||||
|
|
||||||
|
package bulb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ProtocolInfo is the result of the ProtocolInfo command.
|
||||||
|
type ProtocolInfo struct {
|
||||||
|
AuthMethods map[string]bool
|
||||||
|
CookieFile string
|
||||||
|
TorVersion string
|
||||||
|
|
||||||
|
RawResponse *Response
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProtocolInfo issues a PROTOCOLINFO command and returns the parsed response.
|
||||||
|
func (c *Conn) ProtocolInfo() (*ProtocolInfo, error) {
|
||||||
|
resp, err := c.Request("PROTOCOLINFO")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse out the PIVERSION to make sure it speaks something we understand.
|
||||||
|
if len(resp.Data) < 1 {
|
||||||
|
return nil, newProtocolError("missing PIVERSION")
|
||||||
|
}
|
||||||
|
switch resp.Data[0] {
|
||||||
|
case "1":
|
||||||
|
return nil, newProtocolError("invalid PIVERSION: '%s'", resp.Reply)
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse out the rest of the lines.
|
||||||
|
pi := new(ProtocolInfo)
|
||||||
|
pi.RawResponse = resp
|
||||||
|
pi.AuthMethods = make(map[string]bool)
|
||||||
|
for i := 1; i < len(resp.Data); i++ {
|
||||||
|
splitLine := strings.Split(resp.Data[i], " ")
|
||||||
|
switch splitLine[0] {
|
||||||
|
case "AUTH":
|
||||||
|
// Parse an AuthLine detailing how to authenticate.
|
||||||
|
if len(splitLine) < 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
methods := strings.TrimPrefix(splitLine[1], "METHODS=")
|
||||||
|
if methods == splitLine[1] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, meth := range strings.Split(methods, ",") {
|
||||||
|
pi.AuthMethods[meth] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(splitLine) < 3 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
cookiePath := strings.TrimPrefix(splitLine[2], "COOKIEFILE=")
|
||||||
|
if cookiePath == splitLine[2] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
pi.CookieFile, _ = strconv.Unquote(cookiePath)
|
||||||
|
case "VERSION":
|
||||||
|
// Parse a VersionLine detailing the Tor version.
|
||||||
|
if len(splitLine) < 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
torVersion := strings.TrimPrefix(splitLine[1], "Tor=")
|
||||||
|
if torVersion == splitLine[1] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
pi.TorVersion, _ = strconv.Unquote(torVersion)
|
||||||
|
default: // MUST ignore unsupported InfoLines.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return pi, nil
|
||||||
|
}
|
|
@ -0,0 +1,228 @@
|
||||||
|
// conn.go - Controller connection instance.
|
||||||
|
//
|
||||||
|
// To the extent possible under law, Yawning Angel waived all copyright
|
||||||
|
// and related or neighboring rights to bulb, using the creative
|
||||||
|
// commons "cc0" public domain dedication. See LICENSE or
|
||||||
|
// <http://creativecommons.org/publicdomain/zero/1.0/> for full details.
|
||||||
|
|
||||||
|
// Package bulb is a Go language interface to a Tor control port.
|
||||||
|
package bulb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
gofmt "fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"net/textproto"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
maxEventBacklog = 16
|
||||||
|
maxResponseBacklog = 16
|
||||||
|
)
|
||||||
|
|
||||||
|
// ErrNoAsyncReader is the error returned when the asynchronous event handling
|
||||||
|
// is requested, but the helper go routine has not been started.
|
||||||
|
var ErrNoAsyncReader = errors.New("event requested without an async reader")
|
||||||
|
|
||||||
|
// Conn is a control port connection instance.
|
||||||
|
type Conn struct {
|
||||||
|
conn *textproto.Conn
|
||||||
|
isAuthenticated bool
|
||||||
|
debugLog bool
|
||||||
|
|
||||||
|
asyncReaderLock sync.Mutex
|
||||||
|
asyncReaderRunning bool
|
||||||
|
eventChan chan *Response
|
||||||
|
respChan chan *Response
|
||||||
|
closeWg sync.WaitGroup
|
||||||
|
|
||||||
|
rdErrLock sync.Mutex
|
||||||
|
rdErr error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) setRdErr(err error, force bool) {
|
||||||
|
c.rdErrLock.Lock()
|
||||||
|
defer c.rdErrLock.Unlock()
|
||||||
|
if c.rdErr == nil || force {
|
||||||
|
c.rdErr = err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) getRdErr() error {
|
||||||
|
c.rdErrLock.Lock()
|
||||||
|
defer c.rdErrLock.Unlock()
|
||||||
|
return c.rdErr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) isAsyncReaderRunning() bool {
|
||||||
|
c.asyncReaderLock.Lock()
|
||||||
|
defer c.asyncReaderLock.Unlock()
|
||||||
|
return c.asyncReaderRunning
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) asyncReader() {
|
||||||
|
for {
|
||||||
|
resp, err := c.readResponse()
|
||||||
|
if err != nil {
|
||||||
|
c.setRdErr(err, false)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if resp.IsAsync() {
|
||||||
|
c.eventChan <- resp
|
||||||
|
} else {
|
||||||
|
c.respChan <- resp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
close(c.eventChan)
|
||||||
|
close(c.respChan)
|
||||||
|
c.closeWg.Done()
|
||||||
|
|
||||||
|
// In theory, we would lock and set asyncReaderRunning to false here, but
|
||||||
|
// once it's started, the only way it returns is if there is a catastrophic
|
||||||
|
// failure, or a graceful shutdown. Changing this will require redoing how
|
||||||
|
// Close() works.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debug enables/disables debug logging of control port chatter.
|
||||||
|
func (c *Conn) Debug(enable bool) {
|
||||||
|
c.debugLog = enable
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close closes the connection.
|
||||||
|
func (c *Conn) Close() error {
|
||||||
|
c.asyncReaderLock.Lock()
|
||||||
|
defer c.asyncReaderLock.Unlock()
|
||||||
|
|
||||||
|
err := c.conn.Close()
|
||||||
|
if err != nil && c.asyncReaderRunning {
|
||||||
|
c.closeWg.Wait()
|
||||||
|
}
|
||||||
|
c.setRdErr(io.ErrClosedPipe, true)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// StartAsyncReader starts the asynchronous reader go routine that allows
|
||||||
|
// asynchronous events to be handled. It must not be called simultaniously
|
||||||
|
// with Request or undefined behavior will occur.
|
||||||
|
func (c *Conn) StartAsyncReader() {
|
||||||
|
c.asyncReaderLock.Lock()
|
||||||
|
defer c.asyncReaderLock.Unlock()
|
||||||
|
if c.asyncReaderRunning {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allocate the channels and kick off the read worker.
|
||||||
|
c.eventChan = make(chan *Response, maxEventBacklog)
|
||||||
|
c.respChan = make(chan *Response, maxResponseBacklog)
|
||||||
|
c.closeWg.Add(1)
|
||||||
|
go c.asyncReader()
|
||||||
|
c.asyncReaderRunning = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// NextEvent returns the next asynchronous event received, blocking if
|
||||||
|
// neccecary. In order to enable asynchronous event handling, StartAsyncReader
|
||||||
|
// must be called first.
|
||||||
|
func (c *Conn) NextEvent() (*Response, error) {
|
||||||
|
if err := c.getRdErr(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !c.isAsyncReaderRunning() {
|
||||||
|
return nil, ErrNoAsyncReader
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, ok := <-c.eventChan
|
||||||
|
if resp != nil {
|
||||||
|
return resp, nil
|
||||||
|
} else if !ok {
|
||||||
|
return nil, io.ErrClosedPipe
|
||||||
|
}
|
||||||
|
panic("BUG: NextEvent() returned a nil response and error")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request sends a raw control port request and returns the response.
|
||||||
|
// If the async. reader is not currently running, events received while waiting
|
||||||
|
// for the response will be silently dropped. Calling StartAsyncReader
|
||||||
|
// simultaniously with Request will lead to undefined behavior.
|
||||||
|
func (c *Conn) Request(fmt string, args ...interface{}) (*Response, error) {
|
||||||
|
if err := c.getRdErr(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
asyncResp := c.isAsyncReaderRunning()
|
||||||
|
|
||||||
|
if c.debugLog {
|
||||||
|
log.Printf("C: %s", gofmt.Sprintf(fmt, args...))
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err := c.conn.Cmd(fmt, args...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
c.conn.StartResponse(id)
|
||||||
|
defer c.conn.EndResponse(id)
|
||||||
|
var resp *Response
|
||||||
|
if asyncResp {
|
||||||
|
var ok bool
|
||||||
|
resp, ok = <-c.respChan
|
||||||
|
if resp == nil && !ok {
|
||||||
|
return nil, io.ErrClosedPipe
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Event handing requires the asyncReader() goroutine, try to get a
|
||||||
|
// response, while silently swallowing events.
|
||||||
|
for resp == nil || resp.IsAsync() {
|
||||||
|
resp, err = c.readResponse()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if resp == nil {
|
||||||
|
panic("BUG: SendRawRequest() returned a nil response and error")
|
||||||
|
}
|
||||||
|
if resp.IsOk() {
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
return resp, resp.Err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read reads directly from the control port connection. Mixing this call
|
||||||
|
// with Request, or asynchronous events will lead to undefined behavior.
|
||||||
|
func (c *Conn) Read(p []byte) (int, error) {
|
||||||
|
return c.conn.R.Read(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write writes directly from the control port connection. Mixing this call
|
||||||
|
// with Request will lead to undefined behavior.
|
||||||
|
func (c *Conn) Write(p []byte) (int, error) {
|
||||||
|
n, err := c.conn.W.Write(p)
|
||||||
|
if err == nil {
|
||||||
|
// If the write succeeds, but the flush fails, n will be incorrect...
|
||||||
|
return n, c.conn.W.Flush()
|
||||||
|
}
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dial connects to a given network/address and returns a new Conn for the
|
||||||
|
// connection.
|
||||||
|
func Dial(network, addr string) (*Conn, error) {
|
||||||
|
c, err := net.Dial(network, addr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return NewConn(c), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewConn returns a new Conn using c for I/O.
|
||||||
|
func NewConn(c io.ReadWriteCloser) *Conn {
|
||||||
|
conn := new(Conn)
|
||||||
|
conn.conn = textproto.NewConn(c)
|
||||||
|
return conn
|
||||||
|
}
|
||||||
|
|
||||||
|
func newProtocolError(fmt string, args ...interface{}) textproto.ProtocolError {
|
||||||
|
return textproto.ProtocolError(gofmt.Sprintf(fmt, args...))
|
||||||
|
}
|
|
@ -0,0 +1,56 @@
|
||||||
|
// Basic example.
|
||||||
|
//
|
||||||
|
// To the extent possible under law, Yawning Angel waived all copyright
|
||||||
|
// and related or neighboring rights to bulb, using the creative
|
||||||
|
// commons "cc0" public domain dedication. See LICENSE or
|
||||||
|
// <http://creativecommons.org/publicdomain/zero/1.0/> for full details.
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/yawning/bulb"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// Connect to a running tor instance.
|
||||||
|
// * TCP: c, err := bulb.Dial("tcp4", "127.0.0.1:9051")
|
||||||
|
c, err := bulb.Dial("unix", "/var/run/tor/control")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to connect to control port: %v", err)
|
||||||
|
}
|
||||||
|
defer c.Close()
|
||||||
|
|
||||||
|
// See what's really going on under the hood.
|
||||||
|
// Do not enable in production.
|
||||||
|
c.Debug(true)
|
||||||
|
|
||||||
|
// Authenticate with the control port.
|
||||||
|
if err := c.Authenticate(); err != nil {
|
||||||
|
log.Fatalf("Authentication failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// At this point, c.Request() can be used to issue requests.
|
||||||
|
resp, err := c.Request("GETINFO version")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("GETINFO version failed: %v", err)
|
||||||
|
}
|
||||||
|
log.Printf("GETINFO version: %v", resp)
|
||||||
|
|
||||||
|
// If you want to use events, then you need to start up the async reader,
|
||||||
|
// which demultiplexes responses and events.
|
||||||
|
c.StartAsyncReader()
|
||||||
|
|
||||||
|
// For example, watch circuit events till the app is killed.
|
||||||
|
if _, err := c.Request("SETEVENTS CIRC"); err != nil {
|
||||||
|
log.Fatalf("SETEVENTS CIRC failed: %v", err)
|
||||||
|
}
|
||||||
|
for {
|
||||||
|
ev, err := c.NextEvent()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("NextEvent() failed: %v", err)
|
||||||
|
}
|
||||||
|
log.Printf("Circuit event: %v", ev)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,107 @@
|
||||||
|
// response.go - Generic response handler
|
||||||
|
//
|
||||||
|
// To the extent possible under law, Yawning Angel waived all copyright
|
||||||
|
// and related or neighboring rights to bulb, using the creative
|
||||||
|
// commons "cc0" public domain dedication. See LICENSE or
|
||||||
|
// <http://creativecommons.org/publicdomain/zero/1.0/> for full details.
|
||||||
|
|
||||||
|
package bulb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"net/textproto"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Response is a response to a control port command, or an asyncrhonous event.
|
||||||
|
type Response struct {
|
||||||
|
// Err is the status code and string representation associated with a
|
||||||
|
// response. Responses that have completed successfully will also have
|
||||||
|
// Err set to indicate such.
|
||||||
|
Err *textproto.Error
|
||||||
|
|
||||||
|
// Reply is the text on the EndReplyLine of the response.
|
||||||
|
Reply string
|
||||||
|
|
||||||
|
// Data is the MidReplyLines/DataReplyLines of the response. Dot encoded
|
||||||
|
// data is "decoded" and presented as a single string (terminal ".CRLF"
|
||||||
|
// removed, all intervening CRs stripped).
|
||||||
|
Data []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsOk returns true if the response status code indicates success or
|
||||||
|
// an asynchronous event.
|
||||||
|
func (r *Response) IsOk() bool {
|
||||||
|
switch r.Err.Code {
|
||||||
|
case StatusOk, StatusOkUnneccecary, StatusAsyncEvent:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsAsync returns true if the response is an asyncrhonous event.
|
||||||
|
func (r *Response) IsAsync() bool {
|
||||||
|
return r.Err.Code == StatusAsyncEvent
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) readResponse() (*Response, error) {
|
||||||
|
var resp *Response
|
||||||
|
var statusCode int
|
||||||
|
for {
|
||||||
|
line, err := c.conn.ReadLine()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if c.debugLog {
|
||||||
|
log.Printf("S: %s", line)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the line that was just read.
|
||||||
|
if len(line) < 4 {
|
||||||
|
return nil, newProtocolError("truncated response: '%s'", line)
|
||||||
|
}
|
||||||
|
if code, err := strconv.Atoi(line[0:3]); err != nil {
|
||||||
|
return nil, newProtocolError("invalid status code: '%s'", line[0:3])
|
||||||
|
} else if code < 100 {
|
||||||
|
return nil, newProtocolError("invalid status code: '%s'", line[0:3])
|
||||||
|
} else if resp == nil {
|
||||||
|
resp = new(Response)
|
||||||
|
statusCode = code
|
||||||
|
} else if code != statusCode {
|
||||||
|
// The status code should stay fixed for all lines of the
|
||||||
|
// response, since events can't be interleaved with response
|
||||||
|
// lines.
|
||||||
|
return nil, newProtocolError("status code changed: %03d != %03d", code, statusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
if line[3] == ' ' {
|
||||||
|
// Final line in the response.
|
||||||
|
resp.Reply = line[4:]
|
||||||
|
resp.Err = statusCodeToError(statusCode, resp.Reply)
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.Data == nil {
|
||||||
|
resp.Data = make([]string, 0, 1)
|
||||||
|
}
|
||||||
|
switch line[3] {
|
||||||
|
case '-':
|
||||||
|
// Continuation, keep reading.
|
||||||
|
resp.Data = append(resp.Data, line[4:])
|
||||||
|
case '+':
|
||||||
|
// A "dot-encoded" payload follows.
|
||||||
|
resp.Data = append(resp.Data, line[4:])
|
||||||
|
dotBody, err := c.conn.ReadDotBytes()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if c.debugLog {
|
||||||
|
log.Printf("S: [dot encoded data]")
|
||||||
|
}
|
||||||
|
resp.Data = append(resp.Data, string(dotBody))
|
||||||
|
default:
|
||||||
|
return nil, newProtocolError("invalid separator: '%c'", line[3])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,69 @@
|
||||||
|
// status.go - Status codes.
|
||||||
|
//
|
||||||
|
// To the extent possible under law, Yawning Angel waived all copyright
|
||||||
|
// and related or neighboring rights to bulb, using the creative
|
||||||
|
// commons "cc0" public domain dedication. See LICENSE or
|
||||||
|
// <http://creativecommons.org/publicdomain/zero/1.0/> for full details.
|
||||||
|
|
||||||
|
package bulb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/textproto"
|
||||||
|
)
|
||||||
|
|
||||||
|
// The various control port StatusCode constants.
|
||||||
|
const (
|
||||||
|
StatusOk = 250
|
||||||
|
StatusOkUnneccecary = 251
|
||||||
|
|
||||||
|
StatusErrResourceExhausted = 451
|
||||||
|
StatusErrSyntaxError = 500
|
||||||
|
StatusErrUnrecognizedCmd = 510
|
||||||
|
StatusErrUnimplementedCmd = 511
|
||||||
|
StatusErrSyntaxErrorArg = 512
|
||||||
|
StatusErrUnrecognizedCmdArg = 513
|
||||||
|
StatusErrAuthenticationRequired = 514
|
||||||
|
StatusErrBadAuthentication = 515
|
||||||
|
StatusErrUnspecifiedTorError = 550
|
||||||
|
StatusErrInternalError = 551
|
||||||
|
StatusErrUnrecognizedEntity = 552
|
||||||
|
StatusErrInvalidConfigValue = 553
|
||||||
|
StatusErrInvalidDescriptor = 554
|
||||||
|
StatusErrUnmanagedEntity = 555
|
||||||
|
|
||||||
|
StatusAsyncEvent = 650
|
||||||
|
)
|
||||||
|
|
||||||
|
var statusCodeStringMap = map[int]string{
|
||||||
|
StatusOk: "OK",
|
||||||
|
StatusOkUnneccecary: "Operation was unnecessary",
|
||||||
|
|
||||||
|
StatusErrResourceExhausted: "Resource exhausted",
|
||||||
|
StatusErrSyntaxError: "Syntax error: protocol",
|
||||||
|
StatusErrUnrecognizedCmd: "Unrecognized command",
|
||||||
|
StatusErrUnimplementedCmd: "Unimplemented command",
|
||||||
|
StatusErrSyntaxErrorArg: "Syntax error in command argument",
|
||||||
|
StatusErrUnrecognizedCmdArg: "Unrecognized command argument",
|
||||||
|
StatusErrAuthenticationRequired: "Authentication required",
|
||||||
|
StatusErrBadAuthentication: "Bad authentication",
|
||||||
|
StatusErrUnspecifiedTorError: "Unspecified Tor error",
|
||||||
|
StatusErrInternalError: "Internal error",
|
||||||
|
StatusErrUnrecognizedEntity: "Unrecognized entity",
|
||||||
|
StatusErrInvalidConfigValue: "Invalid configuration value",
|
||||||
|
StatusErrInvalidDescriptor: "Invalid descriptor",
|
||||||
|
StatusErrUnmanagedEntity: "Unmanaged entity",
|
||||||
|
|
||||||
|
StatusAsyncEvent: "Asynchronous event notification",
|
||||||
|
}
|
||||||
|
|
||||||
|
func statusCodeToError(code int, reply string) *textproto.Error {
|
||||||
|
err := new(textproto.Error)
|
||||||
|
err.Code = code
|
||||||
|
if msg, ok := statusCodeStringMap[code]; ok {
|
||||||
|
err.Msg = fmt.Sprintf("%s: %s", msg, reply)
|
||||||
|
} else {
|
||||||
|
err.Msg = fmt.Sprintf("Unknown status code (%03d): %s", code, reply)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
Reference in New Issue