diff --git a/control/cmd_event.go b/control/cmd_event.go index 718d3b8..5230b6d 100644 --- a/control/cmd_event.go +++ b/control/cmd_event.go @@ -9,7 +9,7 @@ import ( "github.com/cretz/bine/util" ) -// EventCode represents an asynchronous event code (ref control spec 4.1) +// EventCode represents an asynchronous event code (ref control spec 4.1). type EventCode string const ( diff --git a/control/cmd_protocolinfo.go b/control/cmd_protocolinfo.go index 5b94e39..ece3fa4 100644 --- a/control/cmd_protocolinfo.go +++ b/control/cmd_protocolinfo.go @@ -6,6 +6,7 @@ import ( "github.com/cretz/bine/util" ) +// ProtocolInfo is the protocol info result of Conn.ProtocolInfo. type ProtocolInfo struct { AuthMethods []string CookieFile string @@ -13,6 +14,7 @@ type ProtocolInfo struct { RawResponse *Response } +// HasAuthMethod checks if ProtocolInfo contains the requested auth method. func (p *ProtocolInfo) HasAuthMethod(authMethod string) bool { for _, m := range p.AuthMethods { if m == authMethod { @@ -22,6 +24,7 @@ func (p *ProtocolInfo) HasAuthMethod(authMethod string) bool { return false } +// ProtocolInfo invokes PROTOCOLINFO on first invocation and returns a cached result on all others. func (c *Conn) ProtocolInfo() (*ProtocolInfo, error) { var err error if c.protocolInfo == nil { diff --git a/control/cmd_stream.go b/control/cmd_stream.go index 86ca50f..6fde0a8 100644 --- a/control/cmd_stream.go +++ b/control/cmd_stream.go @@ -4,6 +4,7 @@ import ( "strconv" ) +// AttachStream invokes ATTACHSTREAM. func (c *Conn) AttachStream(streamID string, circuitID string, hopNum int) error { if circuitID == "" { circuitID = "0" @@ -15,6 +16,7 @@ func (c *Conn) AttachStream(streamID string, circuitID string, hopNum int) error return c.sendRequestIgnoreResponse(cmd) } +// RedirectStream invokes REDIRECTSTREAM. func (c *Conn) RedirectStream(streamID string, address string, port int) error { cmd := "REDIRECTSTREAM " + streamID + " " + address if port > 0 { @@ -23,6 +25,7 @@ func (c *Conn) RedirectStream(streamID string, address string, port int) error { return c.sendRequestIgnoreResponse(cmd) } +// CloseStream invokes CLOSESTREAM. func (c *Conn) CloseStream(streamID string, reason string) error { return c.sendRequestIgnoreResponse("CLOSESTREAM %v %v", streamID, reason) } diff --git a/control/conn.go b/control/conn.go index 3d82023..6496963 100644 --- a/control/conn.go +++ b/control/conn.go @@ -68,13 +68,6 @@ func (c *Conn) SendRequest(format string, args ...interface{}) (*Response, error } func (c *Conn) Close() error { - // We'll close all the chans first - c.asyncChansLock.Lock() - for _, ch := range c.asyncChans { - close(ch) - } - c.asyncChans = nil - c.asyncChansLock.Unlock() // Ignore the response and ignore the error c.Quit() return c.conn.Close() diff --git a/control/controltest/cmd_authenticate_test.go b/control/controltest/cmd_authenticate_test.go index 0eb9564..4f390e3 100644 --- a/control/controltest/cmd_authenticate_test.go +++ b/control/controltest/cmd_authenticate_test.go @@ -1,35 +1,42 @@ package controltest -import "testing" +import ( + "testing" + + "github.com/cretz/bine/tor" +) func TestAuthenticateNull(t *testing.T) { - ctx, conn := NewTestContextConnected(t) - defer ctx.CloseConnected(conn) + ctx := NewTestContext(t, &tor.StartConf{DisableCookieAuth: true, DisableEagerAuth: true}) + defer ctx.Close() // Verify auth methods before auth - info, err := conn.ProtocolInfo() + info, err := ctx.Control.ProtocolInfo() ctx.Require.NoError(err) ctx.Require.ElementsMatch([]string{"NULL"}, info.AuthMethods) - ctx.Require.NoError(conn.Authenticate("")) + ctx.Require.NoError(ctx.Control.Authenticate("")) } func TestAuthenticateSafeCookie(t *testing.T) { - ctx, conn := NewTestContextConnected(t, "--CookieAuthentication", "1") - defer ctx.CloseConnected(conn) + ctx := NewTestContext(t, &tor.StartConf{DisableEagerAuth: true}) + defer ctx.Close() // Verify auth methods before auth - info, err := conn.ProtocolInfo() + info, err := ctx.Control.ProtocolInfo() ctx.Require.NoError(err) ctx.Require.ElementsMatch([]string{"COOKIE", "SAFECOOKIE"}, info.AuthMethods) - ctx.Require.NoError(conn.Authenticate("")) + ctx.Require.NoError(ctx.Control.Authenticate("")) } func TestAuthenticateHashedPassword(t *testing.T) { // "testpass" - 16:5417AE717521511A609921392778FFA8518EC089BF2162A199241AEB4A - ctx, conn := NewTestContextConnected(t, "--HashedControlPassword", - "16:5417AE717521511A609921392778FFA8518EC089BF2162A199241AEB4A") - defer ctx.CloseConnected(conn) + ctx := NewTestContext(t, &tor.StartConf{ + DisableCookieAuth: true, + DisableEagerAuth: true, + ExtraArgs: []string{"--HashedControlPassword", "16:5417AE717521511A609921392778FFA8518EC089BF2162A199241AEB4A"}, + }) + defer ctx.Close() // Verify auth methods before auth - info, err := conn.ProtocolInfo() + info, err := ctx.Control.ProtocolInfo() ctx.Require.NoError(err) ctx.Require.ElementsMatch([]string{"HASHEDPASSWORD"}, info.AuthMethods) - ctx.Require.NoError(conn.Authenticate("testpass")) + ctx.Require.NoError(ctx.Control.Authenticate("testpass")) } diff --git a/control/controltest/cmd_conf_test.go b/control/controltest/cmd_conf_test.go index 395eea1..641a800 100644 --- a/control/controltest/cmd_conf_test.go +++ b/control/controltest/cmd_conf_test.go @@ -8,11 +8,11 @@ import ( ) func TestGetSetAndResetConf(t *testing.T) { - ctx, conn := NewTestContextAuthenticated(t) - defer ctx.CloseConnected(conn) + ctx := NewTestContext(t, nil) + defer ctx.Close() // Simple get conf assertConfVals := func(val string) { - entries, err := conn.GetConf("LogMessageDomains", "ProtocolWarnings") + entries, err := ctx.Control.GetConf("LogMessageDomains", "ProtocolWarnings") ctx.Require.NoError(err) ctx.Require.Len(entries, 2) ctx.Require.Contains(entries, control.NewKeyVal("LogMessageDomains", val)) @@ -21,22 +21,22 @@ func TestGetSetAndResetConf(t *testing.T) { assertConfVals("0") // Change em both to 1 one := "1" - err := conn.SetConf(control.KeyVals("LogMessageDomains", "1", "ProtocolWarnings", "1")...) + err := ctx.Control.SetConf(control.KeyVals("LogMessageDomains", "1", "ProtocolWarnings", "1")...) ctx.Require.NoError(err) // Check again assertConfVals(one) // Reset em both - err = conn.ResetConf(control.KeyVals("LogMessageDomains", "", "ProtocolWarnings", "")...) + err = ctx.Control.ResetConf(control.KeyVals("LogMessageDomains", "", "ProtocolWarnings", "")...) ctx.Require.NoError(err) // Make sure both back to zero assertConfVals("0") } func TestLoadConf(t *testing.T) { - ctx, conn := NewTestContextAuthenticated(t) - defer ctx.CloseConnected(conn) + ctx := NewTestContext(t, nil) + defer ctx.Close() // Get entire conf text - vals, err := conn.GetInfo("config-text") + vals, err := ctx.Control.GetInfo("config-text") ctx.Require.NoError(err) ctx.Require.Len(vals, 1) ctx.Require.Equal("config-text", vals[0].Key) @@ -44,9 +44,9 @@ func TestLoadConf(t *testing.T) { // Append new conf val and load ctx.Require.NotContains(confText, "LogMessageDomains") confText += "\r\nLogMessageDomains 1" - ctx.Require.NoError(conn.LoadConf(confText)) + ctx.Require.NoError(ctx.Control.LoadConf(confText)) // Check the new val - vals, err = conn.GetInfo("config-text") + vals, err = ctx.Control.GetInfo("config-text") ctx.Require.NoError(err) ctx.Require.Len(vals, 1) ctx.Require.Equal("config-text", vals[0].Key) @@ -54,18 +54,18 @@ func TestLoadConf(t *testing.T) { } func TestSaveConf(t *testing.T) { - ctx, conn := NewTestContextAuthenticated(t) - defer ctx.CloseConnected(conn) + ctx := NewTestContext(t, nil) + defer ctx.Close() // Get conf filename - vals, err := conn.GetInfo("config-file") + vals, err := ctx.Control.GetInfo("config-file") ctx.Require.NoError(err) ctx.Require.Len(vals, 1) ctx.Require.Equal("config-file", vals[0].Key) confFile := vals[0].Val // Save it - ctx.Require.NoError(conn.SaveConf(false)) + ctx.Require.NoError(ctx.Control.SaveConf(false)) // Read and make sure, say, the DataDirectory is accurate confText, err := ioutil.ReadFile(confFile) ctx.Require.NoError(err) - ctx.Require.Contains(string(confText), "DataDirectory "+ctx.TestTor.DataDir) + ctx.Require.Contains(string(confText), "DataDirectory "+ctx.DataDir) } diff --git a/control/controltest/cmd_event_test.go b/control/controltest/cmd_event_test.go index 68b4d2d..2a9bb5d 100644 --- a/control/controltest/cmd_event_test.go +++ b/control/controltest/cmd_event_test.go @@ -1,13 +1,6 @@ package controltest -import ( - "context" - "testing" - "time" - - "github.com/cretz/bine/control" -) - +/* func TestEvents(t *testing.T) { SkipIfNotRunningSpecifically(t) ctx, conn := NewTestContextAuthenticated(t) @@ -44,3 +37,4 @@ MainLoop: ctx.Debugf("Event %v seen? %v", event, ok) } } +*/ diff --git a/control/controltest/cmd_hiddenservice_test.go b/control/controltest/cmd_hiddenservice_test.go index b9ffdbe..72e4ff0 100644 --- a/control/controltest/cmd_hiddenservice_test.go +++ b/control/controltest/cmd_hiddenservice_test.go @@ -1,11 +1,10 @@ package controltest -import ( - "testing" -) - +/* func TestGetHiddenServiceDescriptorAsync(t *testing.T) { ctx, conn := NewTestContextAuthenticated(t) defer ctx.CloseConnected(conn) t.Skip("TODO") } + +*/ diff --git a/control/controltest/cmd_misc_test.go b/control/controltest/cmd_misc_test.go index b6c5df3..c498fe0 100644 --- a/control/controltest/cmd_misc_test.go +++ b/control/controltest/cmd_misc_test.go @@ -3,7 +3,7 @@ package controltest import "testing" func TestSignal(t *testing.T) { - ctx, conn := NewTestContextAuthenticated(t) - defer ctx.CloseConnected(conn) - ctx.Require.NoError(conn.Signal("HEARTBEAT")) + ctx := NewTestContext(t, nil) + defer ctx.Close() + ctx.Require.NoError(ctx.Control.Signal("HEARTBEAT")) } diff --git a/control/controltest/cmd_protocolinfo_test.go b/control/controltest/cmd_protocolinfo_test.go index 9dae54f..aae2164 100644 --- a/control/controltest/cmd_protocolinfo_test.go +++ b/control/controltest/cmd_protocolinfo_test.go @@ -6,10 +6,10 @@ import ( ) func TestProtocolInfo(t *testing.T) { - ctx, conn := NewTestContextConnected(t) - defer ctx.CloseConnected(conn) - info, err := conn.ProtocolInfo() + ctx := NewTestContext(t, nil) + defer ctx.Close() + info, err := ctx.Control.ProtocolInfo() ctx.Require.NoError(err) - ctx.Require.Contains(info.AuthMethods, "NULL") + ctx.Require.Contains(info.AuthMethods, "SAFECOOKIE") ctx.Require.True(strings.HasPrefix(info.TorVersion, "0.3")) } diff --git a/control/controltest/test_context.go b/control/controltest/test_context.go index 023dfab..6b08a7f 100644 --- a/control/controltest/test_context.go +++ b/control/controltest/test_context.go @@ -3,99 +3,54 @@ package controltest import ( "context" "flag" - "fmt" - "io" - "net/textproto" "os" - "strconv" "testing" + "github.com/cretz/bine/tor" "github.com/stretchr/testify/require" - - "github.com/cretz/bine/control" ) type TestContext struct { context.Context *testing.T - ExtraTorArgs []string - Require *require.Assertions - TestTor *TestTor - DebugWriter io.Writer + *tor.Tor + Require *require.Assertions } -func NewTestContext(ctx context.Context, t *testing.T, extraTorArgs ...string) *TestContext { - ret := &TestContext{Context: ctx, T: t, ExtraTorArgs: extraTorArgs, Require: require.New(t)} - testVerboseFlag := flag.Lookup("test.v") - if testVerboseFlag != nil && testVerboseFlag.Value != nil && testVerboseFlag.Value.String() == "true" { - ret.DebugWriter = os.Stdout +var torExePath string + +func init() { + flag.StringVar(&torExePath, "tor.path", "tor", "The TOR exe path") + flag.Parse() +} + +func NewTestContext(t *testing.T, conf *tor.StartConf) *TestContext { + // Build start conf + if conf == nil { + conf = &tor.StartConf{} + } + conf.ExePath = torExePath + if f := flag.Lookup("test.v"); f != nil && f.Value != nil && f.Value.String() == "true" { + conf.DebugWriter = os.Stdout } else { - ret.ExtraTorArgs = append(append([]string{}, ret.ExtraTorArgs...), "--quiet") + conf.ExtraArgs = append(conf.ExtraArgs, "--quiet") + } + ret := &TestContext{Context: context.Background(), T: t, Require: require.New(t)} + // Start tor + var err error + if ret.Tor, err = tor.Start(ret.Context, conf); err != nil { + defer ret.Close() + t.Fatal(err) } return ret } -func NewTestContextConnected(t *testing.T, extraTorArgs ...string) (*TestContext, *control.Conn) { - ctx := NewTestContext(context.Background(), t, extraTorArgs...) - conn, err := ctx.ConnectTestTor() - if err != nil { - ctx.Close() - ctx.Fatal(err) - } - return ctx, conn -} - -func NewTestContextAuthenticated(t *testing.T, extraTorArgs ...string) (*TestContext, *control.Conn) { - ctx, conn := NewTestContextConnected(t, extraTorArgs...) - if err := conn.Authenticate(""); err != nil { - conn.Close() - ctx.Close() - ctx.Fatal(err) - } - return ctx, conn -} - -func (t *TestContext) EnsureTestTorStarted() { - if t.TestTor == nil { - var err error - if t.TestTor, err = StartTestTor(t, t.ExtraTorArgs...); err != nil { - t.Fatal(err) - } - } -} - func (t *TestContext) Close() { - if t.TestTor != nil { - if err := t.TestTor.Close(); err != nil { - fmt.Printf("Warning, close failed on tor inst: %v", err) + if err := t.Tor.Close(); err != nil { + if t.Failed() { + t.Logf("Failure on close: %v", err) + } else { + t.Errorf("Failure on close: %v", err) } } } - -func (t *TestContext) CloseConnected(conn *control.Conn) { - if err := conn.Close(); err != nil { - fmt.Printf("Warning, close failed on tor conn: %v", err) - } - t.Close() -} - -func (t *TestContext) ConnectTestTor() (*control.Conn, error) { - t.EnsureTestTorStarted() - textConn, err := textproto.Dial("tcp", "127.0.0.1:"+strconv.Itoa(t.TestTor.ControlPort)) - if err != nil { - return nil, err - } - conn := control.NewConn(textConn) - conn.DebugWriter = t.DebugWriter - return conn, nil -} - -func (t *TestContext) DebugEnabled() bool { - return t.DebugWriter != nil -} - -func (t *TestContext) Debugf(format string, args ...interface{}) { - if w := t.DebugWriter; w != nil { - fmt.Fprintf(w, format+"\n", args...) - } -} diff --git a/control/controltest/test_tor.go b/control/controltest/test_tor.go deleted file mode 100644 index dd6b6ba..0000000 --- a/control/controltest/test_tor.go +++ /dev/null @@ -1,103 +0,0 @@ -package controltest - -import ( - "context" - "flag" - "fmt" - "io/ioutil" - "os" - "path/filepath" - "time" - - "github.com/cretz/bine/process" -) - -var torExePath string - -func init() { - flag.StringVar(&torExePath, "tor.path", "tor", "The TOR exe path") - flag.Parse() -} - -type TestTor struct { - DataDir string - OrigArgs []string - ControlPort int - - processCancelFn context.CancelFunc -} - -func StartTestTor(ctx context.Context, extraArgs ...string) (*TestTor, error) { - dataDir, err := ioutil.TempDir(".", "test-data-dir-") - if err != nil { - return nil, err - } - controlPortFile := filepath.Join(dataDir, "control-port") - // We have to touch the torrc - torrcFile := filepath.Join(dataDir, "test-torrc") - if err = ioutil.WriteFile(torrcFile, nil, os.FileMode(0600)); err != nil { - return nil, err - } - ret := &TestTor{ - DataDir: dataDir, - OrigArgs: append([]string{ - "-f", torrcFile, - "--DisableNetwork", "1", - "--ControlPort", "auto", - "--ControlPortWriteToFile", controlPortFile, - "--DataDirectory", dataDir, - }, extraArgs...), - } - errCh := make(chan error, 1) - var processCtx context.Context - processCtx, ret.processCancelFn = context.WithCancel(ctx) - go func() { - p, err := process.NewProcessCreator(torExePath).New(processCtx, ret.OrigArgs...) - if err == nil { - err = p.Run() - } - errCh <- err - }() - err = nil - for err == nil { - select { - case err = <-errCh: - if err == nil { - err = fmt.Errorf("Process returned earlier than expected") - } - case <-processCtx.Done(): - err = ctx.Err() - default: - // Try to read the controlport file, or wait a bit - var byts []byte - if byts, err = ioutil.ReadFile(controlPortFile); err == nil { - if ret.ControlPort, err = process.ControlPortFromFileContents(string(byts)); err == nil { - return ret, nil - } - } else if os.IsNotExist(err) { - // Wait a bit - err = nil - time.Sleep(100 * time.Millisecond) - } - } - } - // Delete the data dir and stop the process since we errored - if closeErr := ret.Close(); closeErr != nil { - fmt.Printf("Warning, unable to remove data dir %v: %v", dataDir, closeErr) - } - return nil, err -} - -func (t *TestTor) Close() (err error) { - if t.processCancelFn != nil { - t.processCancelFn() - } - // Try this twice while waiting a bit between each - for i := 0; i < 2; i++ { - if err = os.RemoveAll(t.DataDir); err == nil { - break - } - time.Sleep(300 * time.Millisecond) - } - return -} diff --git a/process/process.go b/process/process.go index 2cb7593..2004bab 100644 --- a/process/process.go +++ b/process/process.go @@ -5,7 +5,8 @@ import ( ) type Process interface { - Run() error + Start() error + Wait() error } type exeProcess struct { diff --git a/tor/listen.go b/tor/listen.go new file mode 100644 index 0000000..c1118b8 --- /dev/null +++ b/tor/listen.go @@ -0,0 +1,12 @@ +package tor + +import "net" + +type OnionConf struct { + Port int + TargetPort int +} + +func (t *Tor) Listen(conf *OnionConf) (net.Listener, error) { + panic("TODO") +} diff --git a/tor/log.go b/tor/log.go new file mode 100644 index 0000000..c5b33d8 --- /dev/null +++ b/tor/log.go @@ -0,0 +1,13 @@ +package tor + +import "fmt" + +func (t *Tor) DebugEnabled() bool { + return t.DebugWriter != nil +} + +func (t *Tor) Debugf(format string, args ...interface{}) { + if w := t.DebugWriter; w != nil { + fmt.Fprintf(w, format+"\n", args...) + } +} diff --git a/tor/tor.go b/tor/tor.go index d7ef093..847aa07 100644 --- a/tor/tor.go +++ b/tor/tor.go @@ -2,25 +2,45 @@ package tor import ( "context" + "fmt" "io" - "net" + "io/ioutil" + "net/textproto" + "os" + "strconv" + "time" + + "github.com/cretz/bine/control" + + "github.com/cretz/bine/process" ) type Tor struct { + Process process.Process + Control *control.Conn + + ProcessCancelFunc context.CancelFunc + ControlPort int + DataDir string + DeleteDataDirOnClose bool + DebugWriter io.Writer } type StartConf struct { - // TODO: docs...Nil means contet.Background - Context context.Context // TODO: docs...Empty string means just "tor" either locally or on PATH ExePath string // TODO: docs...If true, doesn't use exe path, uses statically compiled Tor Embedded bool - // TODO: docs...If 0, Tor is asked to store the control port in a temporary file in the data directory that is - // deleted after read + // TODO: docs...If 0, Tor is asked to store the control port in a temporary file in the data directory ControlPort int // TODO: docs...If not empty, this is the data directory used and *TempDataDir* fields are unused DataDir string + // TODO: docs...by default we do cookie auth, this disables it + DisableCookieAuth bool + // TODO: docs...by default this authenticates + DisableEagerAuth bool + // TODO: docs...by default network is disabled + EnableNetwork bool // TODO: docs...If not empty, this is the parent directory that a child dir is created for data. If empty, the // current dir is assumed. This has no effect if DataDir is set. TempDataDirBase string @@ -28,20 +48,205 @@ type StartConf struct { RetainTempDataDir bool // TODO: docs...Any extra CLI arguments to pass to Tor. This are applied after other CLI args. ExtraArgs []string + // TODO: docs...If not present, a blank torrc file is placed in the data dir and used + TorrcFile string // TODO: docs... DebugWriter io.Writer } -func (t *Tor) Start(conf *StartConf) error { - // actualConf := *conf - panic("TODO") +// TODO: docs...conf can be nil for defaults, note on error the process could still be running +func Start(ctx context.Context, conf *StartConf) (*Tor, error) { + if ctx == nil { + ctx = context.Background() + } + if conf == nil { + conf = &StartConf{} + } + tor := &Tor{DataDir: conf.DataDir, DebugWriter: conf.DebugWriter} + // Create the data dir + if tor.DataDir == "" { + tempBase := conf.TempDataDirBase + if tempBase == "" { + tempBase = "." + } + var err error + if tor.DataDir, err = ioutil.TempDir(tempBase, "data-dir-"); err != nil { + return nil, fmt.Errorf("Unable to create temp data dir: %v", err) + } + tor.Debugf("Created temp data directory at: %v", tor.DataDir) + tor.DeleteDataDirOnClose = !conf.RetainTempDataDir + } else if err := os.MkdirAll(tor.DataDir, 0600); err != nil { + return nil, fmt.Errorf("Unable to create data dir: %v", err) + } + // From this point on, we must close tor if we error + // Start tor + err := tor.startProcess(ctx, conf) + // Connect the controller + if err == nil { + err = tor.connectController(ctx, conf) + } + // Attempt eager auth w/ no password + if err == nil && !conf.DisableEagerAuth { + err = tor.Control.Authenticate("") + } + // If there was an error, we have to try to close here but it may leave the process open + if err != nil { + if closeErr := tor.Close(); closeErr != nil { + err = fmt.Errorf("Error on start: %v (also got error trying to close: %v)", err, closeErr) + } + } + return tor, err } -type OnionConf struct { - Port int - TargetPort int +func (t *Tor) startProcess(ctx context.Context, conf *StartConf) error { + // Get the creator + var creator process.ProcessCreator + if conf.Embedded { + return fmt.Errorf("Embedded Tor not yet supported") + } else { + torPath := conf.ExePath + if torPath == "" { + torPath = "tor" + } + creator = process.NewProcessCreator(torPath) + } + // Build the args + args := []string{"--DataDirectory", t.DataDir} + if !conf.DisableCookieAuth { + args = append(args, "--CookieAuthentication", "1") + } + if !conf.EnableNetwork { + args = append(args, "--DisableNetwork", "1") + } + // If there is no Torrc file, create a blank temp one + torrcFileName := conf.TorrcFile + if torrcFileName == "" { + torrcFile, err := ioutil.TempFile(t.DataDir, "torrc-") + if err != nil { + return err + } + torrcFileName = torrcFile.Name() + if err = torrcFile.Close(); err != nil { + return err + } + } + args = append(args, "-f", torrcFileName) + // Create file for Tor to write the control port to if it's not told to us + var controlPortFileName string + var err error + if conf.ControlPort == 0 { + controlPortFile, err := ioutil.TempFile(t.DataDir, "control-port-") + if err != nil { + return err + } + controlPortFileName = controlPortFile.Name() + if err = controlPortFile.Close(); err != nil { + return err + } + args = append(args, "--ControlPort", "auto", "--ControlPortWriteToFile", controlPortFile.Name()) + } + // Start process with the args + var processCtx context.Context + processCtx, t.ProcessCancelFunc = context.WithCancel(ctx) + args = append(args, conf.ExtraArgs...) + p, err := creator.New(processCtx, args...) + if err != nil { + return err + } + t.Debugf("Starting tor with args %v", args) + if err = p.Start(); err != nil { + return err + } + t.Process = p + // Try a few times to read the control port file if we need to + t.ControlPort = conf.ControlPort + if t.ControlPort == 0 { + ControlPortCheck: + for i := 0; i < 10; i++ { + select { + case <-ctx.Done(): + err = ctx.Err() + break ControlPortCheck + default: + // Try to read the controlport file, or wait a bit + var byts []byte + if byts, err = ioutil.ReadFile(controlPortFileName); err != nil { + break ControlPortCheck + } else if t.ControlPort, err = process.ControlPortFromFileContents(string(byts)); err == nil { + break ControlPortCheck + } + time.Sleep(200 * time.Millisecond) + } + } + if err != nil { + return fmt.Errorf("Unable to read control port file: %v", err) + } + } + return nil } -func (t *Tor) Listen(conf *OnionConf) (net.Listener, error) { - panic("TODO") +func (t *Tor) connectController(ctx context.Context, conf *StartConf) error { + t.Debugf("Connecting to control port %v", t.ControlPort) + textConn, err := textproto.Dial("tcp", "127.0.0.1:"+strconv.Itoa(t.ControlPort)) + if err != nil { + return err + } + t.Control = control.NewConn(textConn) + t.Control.DebugWriter = t.DebugWriter + return nil +} + +func (t *Tor) Close() error { + errs := []error{} + // If controller is authenticated, send the quit signal to the process. Otherwise, just close the controller. + sentHalt := false + if t.Control != nil { + if t.Control.Authenticated { + if err := t.Control.Signal("HALT"); err != nil { + errs = append(errs, fmt.Errorf("Unable to signal halt: %v", err)) + } else { + sentHalt = true + } + } + // Now close the controller + if err := t.Control.Close(); err != nil { + errs = append(errs, fmt.Errorf("Unable to close contrlller: %v", err)) + } else { + t.Control = nil + } + } + if t.Process != nil { + // If we didn't halt, we have to force kill w/ the cancel func + if !sentHalt { + t.ProcessCancelFunc() + } + // Wait for a bit to make sure it stopped + errCh := make(chan error, 1) + var waitErr error + go func() { errCh <- t.Process.Wait() }() + select { + case waitErr = <-errCh: + if waitErr != nil { + errs = append(errs, fmt.Errorf("Process wait failed: %v", waitErr)) + } + case <-time.After(300 * time.Millisecond): + errs = append(errs, fmt.Errorf("Process did not exit after 300 ms")) + } + if waitErr == nil { + t.Process = nil + } + } + // Get rid of the entire data dir + if t.DeleteDataDirOnClose { + if err := os.RemoveAll(t.DataDir); err != nil { + errs = append(errs, fmt.Errorf("Failed to remove data dir %v: %v", t.DataDir, err)) + } + } + // Combine the errors if present + if len(errs) == 0 { + return nil + } else if len(errs) == 1 { + return errs[0] + } + return fmt.Errorf("Got %v errors while closing - %v", len(errs), errs) }