package tor import ( "errors" "fmt" "github.com/yawning/bulb" "log" "net" "net/http" "net/url" "os" "os/exec" "os/user" "path" "strings" "time" ) // Manager checks connectivity of the Tor process used to support Cwtch/ type Manager struct { socksPort int controlPort int process *exec.Cmd } // NewTorManager Instantiates a new connection manager, returns non-nil error if it fails to connect to a tor daemon on the given ports. func NewTorManager(socksPort int, controlPort int) (*Manager, error) { torManager := new(Manager) torManager.socksPort = socksPort torManager.controlPort = controlPort err := torManager.TestConnection() if err != nil { usr, err := user.Current() if err != nil { return nil, err } torrc := path.Join(usr.HomeDir, ".cwtch", "torrc") if _, err := os.Stat(torrc); os.IsNotExist(err) { os.MkdirAll(path.Join(usr.HomeDir, ".cwtch"), 0700) file, err := os.Create(torrc) if err != nil { return nil, err } fmt.Fprintf(file, "SOCKSPort %d\nControlPort %d\n", socksPort, controlPort) file.Close() } cmd := exec.Command("tor", "-f", torrc) err = cmd.Start() if err != nil { return nil, err } fmt.Printf("\nWaiting to connect to Tor Proxy...\n") time.Sleep(time.Second * 5) torManager.process = cmd err = torManager.TestConnection() return torManager, err } return torManager, err } type proxyStatus int const ( proxyStatusOK proxyStatus = iota proxyStatusWrongType proxyStatusCannotConnect proxyStatusTimeout ) // Shutdown kills the managed Tor Process func (tm *Manager) Shutdown() { if tm.process != nil { if err := tm.process.Process.Kill(); err != nil { log.Fatal("failed to kill process: ", err) } } } // Detect whether a proxy is connectable and is a Tor proxy func checkTorProxy(proxyAddress string) proxyStatus { // A trick to do this without making an outward connection is, // paradoxically, to try to open it as http. // This is documented in section 4 here: https://github.com/torproject/torspec/blob/master/socks-extensions.txt client := &http.Client{Timeout: 2 * time.Second} response, err := client.Get("http://" + proxyAddress + "/") if err != nil { switch t := err.(type) { case *url.Error: switch t.Err.(type) { case *net.OpError: // Network-level error. Will in turn contain a os.SyscallError return proxyStatusCannotConnect default: // http.error unfortunately not exported, need to match on string // net/http: request canceled if strings.Index(t.Err.Error(), "request canceled") != -1 { return proxyStatusTimeout } } } // Protocol-level errors mean that http failed, so it's not Tor return proxyStatusWrongType } defer response.Body.Close() if response.Status != "501 Tor is not an HTTP Proxy" { return proxyStatusWrongType } return proxyStatusOK } func proxyStatusMessage(status proxyStatus) string { switch status { case proxyStatusWrongType: return "Proxy specified is not a Tor proxy" case proxyStatusCannotConnect: return "Cannot connect to Tor proxy" case proxyStatusTimeout: return "Proxy timeout" default: return "Unknown proxy error" } } // TestConnection returns nil if both the socks and control ports of the Tor connection are active, otherwise it returns an error. func (tm *Manager) TestConnection() error { proxyStatus := checkTorProxy(fmt.Sprintf("127.0.0.1:%d", tm.socksPort)) controlAddress := fmt.Sprintf("127.0.0.1:%d", tm.controlPort) if proxyStatus == proxyStatusOK { c, err := bulb.Dial("tcp4", controlAddress) if c != nil { c.Close() } if err == nil { return nil } return fmt.Errorf("could not connect to Tor Control Port %v %v", tm.controlPort, err) } return errors.New(proxyStatusMessage(proxyStatus)) }