diff --git a/app/app.go b/app/app.go index 3f98604..b4ab679 100644 --- a/app/app.go +++ b/app/app.go @@ -1,6 +1,7 @@ package app import ( + "cwtch.im/cwtch/connectivity/tor" "cwtch.im/cwtch/peer" "log" ) @@ -16,6 +17,12 @@ func (app *Application) NewProfile(name string, filename string) error { app.Peer = profile err := profile.Save(filename) if err == nil { + + _, err := tor.NewTorManager(9050, 9051) + if err != nil { + return err + } + go func() { err := app.Peer.Listen() if err != nil { @@ -31,6 +38,12 @@ func (app *Application) SetProfile(filename string) error { profile, err := peer.LoadCwtchPeer(filename) app.Peer = profile if err == nil { + + _, err := tor.NewTorManager(9050, 9051) + if err != nil { + return err + } + go func() { err := app.Peer.Listen() if err != nil { diff --git a/connectivity/tor/tormanager.go b/connectivity/tor/tormanager.go new file mode 100644 index 0000000..04bb85e --- /dev/null +++ b/connectivity/tor/tormanager.go @@ -0,0 +1,94 @@ +package tor + +import ( + "errors" + "fmt" + "github.com/yawning/bulb" + "net" + "net/http" + "net/url" + "strings" + "time" +) + +// Manager checks connectivity of the Tor process used to support Cwtch/ +type Manager struct { + socksPort int + controlPort int +} + +// 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() + return torManager, err +} + +type proxyStatus int + +const ( + proxyStatusOK proxyStatus = iota + proxyStatusWrongType + proxyStatusCannotConnect + proxyStatusTimeout +) + +// 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() + } + return fmt.Errorf("could not connect to Tor Control Port %v %v", tm.controlPort, err) + } + return errors.New(proxyStatusMessage(proxyStatus)) +}