diff --git a/.drone.yml b/.drone.yml index 5a4de5c..c55c25d 100644 --- a/.drone.yml +++ b/.drone.yml @@ -10,10 +10,10 @@ pipeline: event: [ push, pull_request ] image: golang commands: + - go install honnef.co/go/tools/cmd/staticcheck@latest - wget https://git.openprivacy.ca/openprivacy/buildfiles/raw/master/tor/tor -P tmp/ - chmod a+x tmp/tor - go mod download - - go get -u golang.org/x/lint/golint quality: when: repo: openprivacy/connectivity @@ -21,8 +21,7 @@ pipeline: event: [ push, pull_request ] image: golang commands: - - go list ./... | xargs go vet - - go list ./... | xargs golint -set_exit_status + - staticcheck ./... units-tests: when: repo: openprivacy/connectivity diff --git a/.gitignore b/.gitignore index 9e85f5a..abeeb44 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ tor/tor/ vendor/ *.cover.out tmp/ +testing/tor/* \ No newline at end of file diff --git a/acn.go b/acn.go index 3da4f52..5afabb8 100644 --- a/acn.go +++ b/acn.go @@ -21,9 +21,6 @@ type PrivateKey interface{} // ListenService is an address that was opened with Listen() and can Accept() new connections type ListenService interface { - // AddressIdentity is the core "identity" part of an address, ex: rsjeuxzlexy4fvo75vrdtj37nrvlmvbw57n5mhypcjpzv3xkka3l4yyd - AddressIdentity() string - // AddressFull is the full network address, ex: rsjeuxzlexy4fvo75vrdtj37nrvlmvbw57n5mhypcjpzv3xkka3l4yyd.onion:9878 AddressFull() string @@ -38,7 +35,7 @@ type ACN interface { // On ACN error state it returns -2 GetBootstrapStatus() (int, string) // WaitTillBootstrapped Blocks until underlying network is bootstrapped - WaitTillBootstrapped() + WaitTillBootstrapped() error // Sets the callback function to be called when ACN status changes SetStatusCallback(callback func(int, string)) @@ -58,5 +55,7 @@ type ACN interface { // GetVersion returns a string of what the ACN returns when asked for a version GetVersion() string + Callback() func(int, string) + Close() } diff --git a/error_acn.go b/error_acn.go new file mode 100644 index 0000000..d9dbb2b --- /dev/null +++ b/error_acn.go @@ -0,0 +1,51 @@ +package connectivity + +import ( + "errors" + "fmt" + "net" +) + +// ErrorACN - a status-callback safe errored ACN. Use this when ACN construction goes wrong +// and you need a safe substitute that can later be replaced with a working ACN without impacting calling clients. +type ErrorACN struct { + statusCallbackCache func(int, string) +} + +func (e ErrorACN) Callback() func(int, string) { + return e.statusCallbackCache +} + +func (e ErrorACN) GetBootstrapStatus() (int, string) { + return -1, "error initializing tor" +} + +func (e ErrorACN) WaitTillBootstrapped() error { + return errors.New("error initializing tor") +} + +func (e *ErrorACN) SetStatusCallback(callback func(int, string)) { + e.statusCallbackCache = callback +} + +func (e ErrorACN) Restart() { +} + +func (e ErrorACN) Open(hostname string) (net.Conn, string, error) { + return nil, "", fmt.Errorf("error initializing tor") +} + +func (e ErrorACN) Listen(identity PrivateKey, port int) (ListenService, error) { + return nil, fmt.Errorf("error initializing tor") +} + +func (e ErrorACN) GetPID() (int, error) { + return -1, fmt.Errorf("error initializing tor") +} + +func (e ErrorACN) GetVersion() string { + return "Error Initializing Tor" +} + +func (e ErrorACN) Close() { +} diff --git a/localProvider.go b/localProvider.go index 5a08baa..656d0ba 100644 --- a/localProvider.go +++ b/localProvider.go @@ -18,6 +18,10 @@ func NewLocalACN() ACN { return &localProvider{} } +func (lp *localProvider) Callback() func(int, string) { + return func(int, string) {} +} + func (ls *localListenService) AddressFull() string { return ls.l.Addr().String() } @@ -52,7 +56,8 @@ func (lp *localProvider) GetVersion() string { } // WaitTillBootstrapped Blocks until underlying network is bootstrapped -func (lp *localProvider) WaitTillBootstrapped() { +func (lp *localProvider) WaitTillBootstrapped() error { + return nil } func (lp *localProvider) Listen(identity PrivateKey, port int) (ListenService, error) { diff --git a/proxy_acn.go b/proxy_acn.go new file mode 100644 index 0000000..0161085 --- /dev/null +++ b/proxy_acn.go @@ -0,0 +1,74 @@ +package connectivity + +import ( + "net" + "sync" +) + +// ProxyACN because there is rarely a problem that can't be solved by another layer of indirection. +// ACN is a core resource that many parts of a system may need access too e.g. all clients and servers need an instance +// and a UI may also need status information and a configuration interface. +// We want to allow configuration and replacement of an ACN without impacting the API of all downstream systems - introducing +// ProxyACN - a wrapper around an ACN that allows safe replacement of a running ACN that is transparent to callers. +type ProxyACN struct { + acn ACN + + // All operations on the underlying acn are assumed to be thread safe, however changing the actual + // acn in ReplaceACN will lock to force an ordering of Close and Callback + lock sync.Mutex +} + +func NewProxyACN(acn ACN) ProxyACN { + return ProxyACN{ + acn: acn, + } +} + +// ReplaceACN closes down the current ACN and replaces it with a new ACN. +func (p *ProxyACN) ReplaceACN(acn ACN) { + p.lock.Lock() + defer p.lock.Unlock() + p.acn.Close() + acn.SetStatusCallback(p.acn.Callback()) + p.acn = acn +} + +func (p *ProxyACN) GetBootstrapStatus() (int, string) { + return p.acn.GetBootstrapStatus() +} + +func (p *ProxyACN) WaitTillBootstrapped() error { + return p.acn.WaitTillBootstrapped() +} + +func (p *ProxyACN) SetStatusCallback(callback func(int, string)) { + p.acn.SetStatusCallback(callback) +} + +func (p *ProxyACN) Restart() { + p.acn.Restart() +} + +func (p *ProxyACN) Open(hostname string) (net.Conn, string, error) { + return p.acn.Open(hostname) +} + +func (p *ProxyACN) Listen(identity PrivateKey, port int) (ListenService, error) { + return p.acn.Listen(identity, port) +} + +func (p *ProxyACN) GetPID() (int, error) { + return p.acn.GetPID() +} + +func (p *ProxyACN) GetVersion() string { + return p.acn.GetVersion() +} + +func (p *ProxyACN) Close() { + p.acn.Close() +} + +func (p *ProxyACN) Callback() func(int, string) { + return p.acn.Callback() +} diff --git a/testing/launch_tor_integration_test.go b/testing/launch_tor_integration_test.go index da031b1..7611c74 100644 --- a/testing/launch_tor_integration_test.go +++ b/testing/launch_tor_integration_test.go @@ -32,7 +32,11 @@ func TestLaunchTor(t *testing.T) { if err != nil { t.Fatalf("tor failed to start: %v", err) } else { - acn.WaitTillBootstrapped() + err := acn.WaitTillBootstrapped() + if err != nil { + t.Fatalf("error bootstrapping tor %v", err) + } + if pid, err := acn.GetPID(); err == nil { t.Logf("tor pid: %v", pid) } else { diff --git a/testing/quality.sh b/testing/quality.sh index 0599b46..5262cc6 100755 --- a/testing/quality.sh +++ b/testing/quality.sh @@ -1,9 +1,7 @@ #!/bin/sh echo "Checking code quality (you want to see no output here)" - -echo "Formatting:" -gofmt -s -w -l . +echo "" echo "Vetting:" go list ./... | xargs go vet @@ -11,12 +9,16 @@ go list ./... | xargs go vet echo "" echo "Linting:" -go list ./... | xargs golint +staticcheck ./... + + +echo "Time to format" +gofmt -l -s -w . # ineffassign (https://github.com/gordonklaus/ineffassign) echo "Checking for ineffectual assignment of errors (unchecked errors...)" ineffassign . -# misspell (https://github.com/client9/misspell) +# misspell (https://github.com/client9/misspell/cmd/misspell) echo "Checking for misspelled words..." -go list ./... | xargs misspell +misspell . | grep -v "testing/" | grep -v "vendor/" | grep -v "go.sum" | grep -v ".idea" diff --git a/testing/tor/torrc b/testing/tor/torrc deleted file mode 100644 index 459089c..0000000 --- a/testing/tor/torrc +++ /dev/null @@ -1,4 +0,0 @@ -SOCKSPort 9050 -ControlPort 9051 -# "examplehashedpassword" - used for testing -HashedControlPassword 16:C15305F97789414B601259E3EC5E76B8E55FC56A9F562B713F3D2BA257 diff --git a/tor/torProvider.go b/tor/torProvider.go index d45effc..2fe156c 100644 --- a/tor/torProvider.go +++ b/tor/torProvider.go @@ -55,9 +55,9 @@ func (l *logWriter) Write(p []byte) (int, error) { } type onionListenService struct { - lock sync.Mutex - os *tor.OnionService - tp *torProvider + lock sync.Mutex + os *tor.OnionService + tp *torProvider } type torProvider struct { @@ -72,6 +72,7 @@ type torProvider struct { statusCallback func(int, string) lastRestartTime time.Time authenticator tor.Authenticator + isClosed bool } func (ols *onionListenService) AddressFull() string { @@ -80,21 +81,13 @@ func (ols *onionListenService) AddressFull() string { return ols.os.Addr().String() } -func (ols *onionListenService) AddressIdentity() string { - ols.lock.Lock() - defer ols.lock.Unlock() - return ols.os.Addr().String()[:56] -} - func (ols *onionListenService) Accept() (net.Conn, error) { return ols.os.Accept() } func (ols *onionListenService) Close() { - address := ols.AddressIdentity() ols.lock.Lock() defer ols.lock.Unlock() - ols.tp.unregisterListener(address) ols.os.Close() } @@ -156,21 +149,25 @@ func (tp *torProvider) GetVersion() string { return "No Tor" } +func (tp *torProvider) closed() bool { + tp.lock.Lock() + defer tp.lock.Unlock() + return tp.isClosed +} + // WaitTillBootstrapped Blocks until underlying network is bootstrapped -func (tp *torProvider) WaitTillBootstrapped() { - for { +func (tp *torProvider) WaitTillBootstrapped() error { + for !tp.closed() { progress, _ := tp.GetBootstrapStatus() if progress == 100 { - break + return nil } time.Sleep(100 * time.Millisecond) } + return errors.New("close called before bootstrap") } func (tp *torProvider) Listen(identity connectivity.PrivateKey, port int) (connectivity.ListenService, error) { - var onion = "" - var privkey ed25519.PrivateKey - tp.lock.Lock() defer tp.lock.Unlock() @@ -178,6 +175,9 @@ func (tp *torProvider) Listen(identity connectivity.PrivateKey, port int) (conne return nil, errors.New("tor provider closed") } + var onion string + var privkey ed25519.PrivateKey + switch pk := identity.(type) { case ed25519.PrivateKey: privkey = pk @@ -185,7 +185,11 @@ func (tp *torProvider) Listen(identity connectivity.PrivateKey, port int) (conne switch pubk := gpubk.(type) { case ed25519.PublicKey: onion = GetTorV3Hostname(pubk) + default: + return nil, fmt.Errorf("unknown public key type %v", pubk) } + default: + return nil, fmt.Errorf("unknown private key type %v", pk) } // Hack around tor detached onions not having a more obvious resume mechanism @@ -221,7 +225,7 @@ func (tp *torProvider) Listen(identity connectivity.PrivateKey, port int) (conne os.CloseLocalListenerOnClose = true ols := &onionListenService{os: os, tp: tp} - tp.childListeners[ols.AddressIdentity()] = ols + tp.childListeners[ols.AddressFull()] = ols return ols, nil } @@ -284,16 +288,24 @@ func (tp *torProvider) Open(hostname string) (net.Conn, string, error) { } func (tp *torProvider) Close() { - for _, child := range tp.childListeners { - child.Close() - } - tp.lock.Lock() defer tp.lock.Unlock() - tp.breakChan <- true - if tp.t != nil { - tp.t.Close() - tp.t = nil + + // Unregister Child Listeners + for addr, child := range tp.childListeners { + child.Close() + delete(tp.childListeners, addr) + } + + if !tp.isClosed { + // Break out of any background checks and close + // the underlying tor connection + tp.isClosed = true + tp.breakChan <- true + if tp.t != nil { + tp.t.Close() + tp.t = nil + } } } @@ -303,6 +315,12 @@ func (tp *torProvider) SetStatusCallback(callback func(int, string)) { tp.statusCallback = callback } +func (tp *torProvider) Callback() func(int, string) { + tp.lock.Lock() + defer tp.lock.Unlock() + return tp.statusCallback +} + func (tp *torProvider) callStatusCallback(prog int, status string) { tp.lock.Lock() if tp.statusCallback != nil { @@ -410,7 +428,7 @@ func startTor(appDirectory string, bundledTorPath string, controlPort int, authe tp.t = t } else { log.Debugln("Could not find a viable tor running or to run") - return nil, fmt.Errorf("Could not connect to or start Tor that met requirements (min Tor version 0.3.5.x)") + return nil, fmt.Errorf("could not connect to or start Tor that met requirements (min Tor version 0.3.5.x)") } err = tp.checkVersion() @@ -430,12 +448,6 @@ func (tp *torProvider) GetPID() (int, error) { return 0, err } -func (tp *torProvider) unregisterListener(id string) { - tp.lock.Lock() - defer tp.lock.Unlock() - delete(tp.childListeners, id) -} - func (tp *torProvider) monitorRestart() { lastBootstrapProgress := networkUnknown interval := minStatusIntervalMs diff --git a/tor/torProvider_test.go b/tor/torProvider_test.go index 7400517..e7104a1 100644 --- a/tor/torProvider_test.go +++ b/tor/torProvider_test.go @@ -3,7 +3,7 @@ package tor import ( "fmt" "git.openprivacy.ca/openprivacy/log" - "path" + path "path/filepath" "testing" ) @@ -18,6 +18,9 @@ func TestTorProvider(t *testing.T) { progChan := make(chan int) log.SetLevel(log.LevelDebug) torpath := path.Join("..", "tmp/tor") + + NewTorrc().WithControlPort(9051).WithHashedPassword("examplehashedpassword").Build(path.Join("..", "testing", "tor", "torrc")) + log.Debugf("setting tor path %v", torpath) acn, err := NewTorACNWithAuth(path.Join("../testing/"), torpath, 9051, HashedPasswordAuthenticator{"examplehashedpassword"}) if err != nil { diff --git a/tor/torUtils_test.go b/tor/torUtils_test.go index 0416c2a..2e7c60d 100644 --- a/tor/torUtils_test.go +++ b/tor/torUtils_test.go @@ -98,3 +98,11 @@ func TestGenerateTorrc(t *testing.T) { } os.Remove(path) } + +func TestPreviewTorrc(t *testing.T) { + expected := "SocksPort 9050 OnionTrafficOnly\nControlPort 9061" + torrc := NewTorrc().WithCustom([]string{"SocksPort 9050"}).WithControlPort(9061).WithOnionTrafficOnly().Preview() + if torrc != expected { + t.Fatalf("unexpected torrc generated: [%v] [%v]", expected, torrc) + } +} diff --git a/tor/torrcBuilder.go b/tor/torrcBuilder.go index 1b4ea2f..6937d7c 100644 --- a/tor/torrcBuilder.go +++ b/tor/torrcBuilder.go @@ -34,6 +34,13 @@ func (tb *TorrcBuilder) WithControlPort(port int) *TorrcBuilder { return tb } +// WithCustom clobbers the torrc builder and allows the client to set any option they want, while benefiting +// from other configuration options. +func (tb *TorrcBuilder) WithCustom(lines []string) *TorrcBuilder { + tb.lines = lines + return tb +} + // WithOnionTrafficOnly ensures that the tor process only routes tor onion traffic. func (tb *TorrcBuilder) WithOnionTrafficOnly() *TorrcBuilder { for i, line := range tb.lines { @@ -61,6 +68,11 @@ func (tb *TorrcBuilder) Build(path string) error { return ioutil.WriteFile(path, []byte(strings.Join(tb.lines, "\n")), 0600) } +// Preview provides a string representation of the torrc file without writing it to a file location. +func (tb *TorrcBuilder) Preview() string { + return strings.Join(tb.lines, "\n") +} + // GenerateHashedPassword calculates a hash in the same way tha tor --hash-password does // this function takes a salt as input which is not great from an api-misuse perspective, but // we make it private.