bine/tor/tor.go

302 lines
8.8 KiB
Go
Raw Normal View History

package tor
import (
"context"
2018-05-14 18:11:18 +00:00
"fmt"
"io"
2018-05-14 18:11:18 +00:00
"io/ioutil"
"net/textproto"
"os"
"strconv"
"time"
"github.com/cretz/bine/control"
2018-05-14 20:18:32 +00:00
"github.com/cretz/bine/process/embedded"
2018-05-14 18:11:18 +00:00
"github.com/cretz/bine/process"
)
2018-05-14 20:36:29 +00:00
// Tor is the wrapper around the Tor process and control port connection. It
// should be created with Start and developers should always call Close when
// done.
type Tor struct {
2018-05-14 20:18:32 +00:00
// Process is the Tor instance that is running.
2018-05-14 18:11:18 +00:00
Process process.Process
2018-05-14 20:18:32 +00:00
// Control is the Tor controller connection.
2018-05-14 18:11:18 +00:00
Control *control.Conn
2018-05-14 20:36:29 +00:00
// ProcessCancelFunc is the context cancellation func for the Tor process.
// It is used by Close and should not be called directly. This can be nil.
2018-05-14 20:18:32 +00:00
ProcessCancelFunc context.CancelFunc
// ControlPort is the port that Control is connected on.
ControlPort int
// DataDir is the path to the data directory that Tor is using.
DataDir string
2018-05-14 20:36:29 +00:00
// DeleteDataDirOnClose is true if, when Close is invoked, the entire
// directory will be deleted.
2018-05-14 18:11:18 +00:00
DeleteDataDirOnClose bool
2018-05-14 20:18:32 +00:00
2018-05-14 20:36:29 +00:00
// DebugWriter is the writer used for debug logs, or nil if debug logs
// should not be emitted.
2018-05-14 20:18:32 +00:00
DebugWriter io.Writer
// StopProcessOnClose, if true, will attempt to halt the process on close.
StopProcessOnClose bool
}
2018-05-14 20:36:29 +00:00
// StartConf is the configuration used for Start when starting a Tor instance. A
// default instance with no fields set is the default used for Start.
type StartConf struct {
2018-05-14 20:36:29 +00:00
// ExePath is the path to the Tor executable. If it is not present, "tor" is
// used either locally or on the PATH.
ExePath string
2018-05-14 20:18:32 +00:00
2018-05-14 20:36:29 +00:00
// Embedded is true if Tor is statically compiled. If true, ExePath is
// ignored.
Embedded bool
2018-05-14 20:18:32 +00:00
2018-05-14 20:36:29 +00:00
// ControlPort is the port to use for the Tor controller. If it is 0, Tor
// picks a port for use.
ControlPort int
2018-05-14 20:18:32 +00:00
2018-05-14 20:36:29 +00:00
// DataDir is the directory used by Tor. If it is empty, a temporary
// directory is created in TempDataDirBase.
DataDir string
2018-05-14 20:18:32 +00:00
2018-05-14 20:36:29 +00:00
// TempDataDirBase is the parent directory that a temporary data directory
// will be created under for use by Tor. This is ignored if DataDir is not
// empty. If empty it is assumed to be the current working directory.
2018-05-14 20:18:32 +00:00
TempDataDirBase string
2018-05-14 20:36:29 +00:00
// RetainTempDataDir, if true, will not set the created temporary data
// directory to be deleted on close. This is ignored if DataDir is not
// empty.
2018-05-14 20:18:32 +00:00
RetainTempDataDir bool
2018-05-14 20:36:29 +00:00
// DisableCookieAuth, if true, will not use the default SAFECOOKIE
// authentication mechanism for the Tor controller.
2018-05-14 18:11:18 +00:00
DisableCookieAuth bool
2018-05-14 20:18:32 +00:00
// DisableEagerAuth, if true, will not authenticate on Start.
2018-05-14 18:11:18 +00:00
DisableEagerAuth bool
2018-05-14 20:18:32 +00:00
// EnableNetwork, if true, will connect to the wider Tor network on start.
2018-05-14 18:11:18 +00:00
EnableNetwork bool
2018-05-14 20:18:32 +00:00
2018-05-14 20:36:29 +00:00
// ExtraArgs is the set of extra args passed to the Tor instance when
// started.
ExtraArgs []string
2018-05-14 20:18:32 +00:00
2018-05-14 20:36:29 +00:00
// TorrcFile is the torrc file to on start. If empty, a blank torrc is
// created in the data directory and is used instead.
2018-05-14 18:11:18 +00:00
TorrcFile string
2018-05-14 20:18:32 +00:00
2018-05-14 20:36:29 +00:00
// DebugWriter is the writer to use for debug logs, or nil for no debug
// logs.
DebugWriter io.Writer
}
2018-05-14 20:36:29 +00:00
// Start a Tor instance and connect to it. If ctx is nil, context.Background()
// is used. If conf is nil, a default instance is used.
2018-05-14 18:11:18 +00:00
func Start(ctx context.Context, conf *StartConf) (*Tor, error) {
if ctx == nil {
ctx = context.Background()
}
if conf == nil {
conf = &StartConf{}
}
2018-05-14 20:18:32 +00:00
tor := &Tor{DataDir: conf.DataDir, DebugWriter: conf.DebugWriter, StopProcessOnClose: true}
2018-05-14 18:11:18 +00:00
// 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
}
func (t *Tor) startProcess(ctx context.Context, conf *StartConf) error {
// Get the creator
2018-05-14 19:47:05 +00:00
var creator process.Creator
2018-05-14 18:11:18 +00:00
if conf.Embedded {
2018-05-14 20:18:32 +00:00
creator = embedded.NewCreator()
2018-05-14 18:11:18 +00:00
} else {
torPath := conf.ExePath
if torPath == "" {
torPath = "tor"
}
2018-05-14 19:47:05 +00:00
creator = process.NewCreator(torPath)
2018-05-14 18:11:18 +00:00
}
// 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
}
2018-05-14 18:11:18 +00:00
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
}
2018-05-14 20:36:29 +00:00
// Close sends a halt to the Tor process if it can, closes the controller
// connection, and stops the process.
2018-05-14 18:11:18 +00:00
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 {
2018-05-14 20:18:32 +00:00
if t.Control.Authenticated && t.StopProcessOnClose {
2018-05-14 18:11:18 +00:00
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
2018-05-14 20:18:32 +00:00
if !sentHalt && t.StopProcessOnClose {
2018-05-14 18:11:18 +00:00
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)
}