diff --git a/app/app.go b/app/app.go index d18c076..b516468 100644 --- a/app/app.go +++ b/app/app.go @@ -119,7 +119,7 @@ func (app *application) CreatePeer(name string, password string) { } func (app *application) AddPeerPlugin(onion string, pluginID plugins.PluginID) { - app.AddPlugin(onion, pluginID, app.eventBuses[onion]) + app.AddPlugin(onion, pluginID, app.eventBuses[onion], app.acn) } // LoadProfiles takes a password and attempts to load any profiles it can from storage with it and create Peers for them @@ -220,15 +220,16 @@ func (app *application) ShutdownPeer(onion string) { delete(app.engines, onion) app.storage[onion].Shutdown() delete(app.storage, onion) + app.appletPlugins.Shutdown() } // Shutdown shutsdown all peers of an app and then the tormanager func (app *application) Shutdown() { for id, peer := range app.peers { peer.Shutdown() + app.appletPlugins.ShutdownPeer(id) app.engines[id].Shutdown() app.storage[id].Shutdown() - app.appletPlugins.Shutdown() app.eventBuses[id].Shutdown() } app.appBus.Shutdown() diff --git a/app/appClient.go b/app/appClient.go index cfe901d..1117afc 100644 --- a/app/appClient.go +++ b/app/appClient.go @@ -123,7 +123,7 @@ func (ac *applicationClient) ShutdownPeer(onion string) { ac.bridge.Write(&message) } -// Shutdown shuts down the application lcienr and all front end peer components +// Shutdown shuts down the application client and all front end peer components func (ac *applicationClient) Shutdown() { for id := range ac.peers { ac.ShutdownPeer(id) diff --git a/app/appService.go b/app/appService.go index 08d1f15..cb6af8f 100644 --- a/app/appService.go +++ b/app/appService.go @@ -50,7 +50,7 @@ func (as *applicationService) handleEvent(ev *event.Event) { case event.AddPeerPlugin: onion := ev.Data[event.Identity] pluginID, _ := strconv.Atoi(ev.Data[event.Data]) - as.AddPlugin(onion, plugins.PluginID(pluginID), as.eventBuses[onion]) + as.AddPlugin(onion, plugins.PluginID(pluginID), as.eventBuses[onion], as.acn) case event.LoadProfiles: password := ev.Data[event.Password] as.loadProfiles(password) @@ -147,8 +147,10 @@ func (as *applicationService) ShutdownPeer(onion string) { // Shutdown shuts down the application Service and all peer related backend parts func (as *applicationService) Shutdown() { + log.Debugf("shutting down application service...") + as.appletPlugins.Shutdown() for id := range as.engines { - as.appletPlugins.Shutdown() + log.Debugf("shutting down application service peer engine %v", id) as.ShutdownPeer(id) } } diff --git a/app/applets.go b/app/applets.go index 507c023..751ef0a 100644 --- a/app/applets.go +++ b/app/applets.go @@ -77,16 +77,27 @@ func (ap *appletPeers) GetPeer(onion string) peer.CwtchPeer { // ***** applet Plugins func (ap *appletPlugins) Shutdown() { + log.Debugf("shutting down applet plugins...") ap.plugins.Range(func(k, v interface{}) bool { - plugins := v.([]plugins.Plugin) - for _, plugin := range plugins { - plugin.Shutdown() - } + log.Debugf("shutting down plugins for %v", k) + ap.ShutdownPeer(k.(string)) return true }) } -func (ap *appletPlugins) AddPlugin(peerid string, id plugins.PluginID, bus event.Manager) { +func (ap *appletPlugins) ShutdownPeer(peerid string) { + log.Debugf("shutting down plugins for %v", peerid) + pluginsI, ok := ap.plugins.Load(peerid) + if ok { + plugins := pluginsI.([]plugins.Plugin) + for _, plugin := range plugins { + log.Debugf("shutting down plugin: %v", plugin) + plugin.Shutdown() + } + } +} + +func (ap *appletPlugins) AddPlugin(peerid string, id plugins.PluginID, bus event.Manager, acn connectivity.ACN) { if _, exists := ap.plugins.Load(peerid); !exists { ap.plugins.Store(peerid, []plugins.Plugin{}) } @@ -94,8 +105,9 @@ func (ap *appletPlugins) AddPlugin(peerid string, id plugins.PluginID, bus event pluginsinf, _ := ap.plugins.Load(peerid) peerPlugins := pluginsinf.([]plugins.Plugin) - newp := plugins.Get(id, bus) + newp := plugins.Get(id, bus, acn) newp.Start() peerPlugins = append(peerPlugins, newp) + log.Debugf("storing plugin for %v %v", peerid, peerPlugins) ap.plugins.Store(peerid, peerPlugins) } diff --git a/app/plugins/networkCheck.go b/app/plugins/networkCheck.go new file mode 100644 index 0000000..ac8a0ca --- /dev/null +++ b/app/plugins/networkCheck.go @@ -0,0 +1,97 @@ +package plugins + +import ( + "cwtch.im/cwtch/event" + "git.openprivacy.ca/openprivacy/libricochet-go/connectivity" + "git.openprivacy.ca/openprivacy/libricochet-go/log" + "git.openprivacy.ca/openprivacy/libricochet-go/policies" + "time" +) + +// networkCheck is a convenience plugin for testing high level availability of onion services +type networkCheck struct { + bus event.Manager + queue event.Queue + acn connectivity.ACN + onionsToCheck []string + breakChan chan bool + running bool +} + +// NewNetworkCheck returns a Plugin that when started will attempt various network tests +func NewNetworkCheck(bus event.Manager, acn connectivity.ACN) Plugin { + nc := &networkCheck{bus: bus, acn: acn, queue: event.NewQueue(), breakChan: make(chan bool, 1)} + return nc +} + +func (nc *networkCheck) Start() { + go nc.run() +} + +func (nc *networkCheck) run() { + nc.running = true + nc.bus.Subscribe(event.ProtocolEngineStartListen, nc.queue) + nc.bus.Subscribe(event.NewMessageFromPeer, nc.queue) + nc.bus.Subscribe(event.PeerAcknowledgement, nc.queue) + nc.bus.Subscribe(event.EncryptedGroupMessage, nc.queue) + var lastMessageReceived time.Time + for { + select { + case <-nc.breakChan: + nc.running = false + return + case e := <-nc.queue.OutChan(): + switch e.EventType { + // On receipt of a Listen request for an onion service we will add the onion to our list + // and then we will wait a minute and check the connection for the first time (the onion should be up) + // under normal operating circumstances + case event.ProtocolEngineStartListen: + log.Debugf("initiating connection check for %v", e.Data[event.Onion]) + nc.onionsToCheck = append(nc.onionsToCheck, e.Data[event.Onion]) + default: + // if we receive either an encrypted group message or a peer acknowledgement we can assume the network + // is up and running (our onion service might still not be available, but we would aim to detect that + // through other actions + // we reset out timer + lastMessageReceived = time.Now() + } + case <-time.After(tickTime): + // if we haven't received an action in the last minute...kick off a set of testing + if time.Now().Sub(lastMessageReceived) > time.Minute { + for _, onion := range nc.onionsToCheck { + go nc.checkConnection(onion) + } + } + } + } +} + +func (nc *networkCheck) Shutdown() { + if nc.running { + nc.queue.Shutdown() + log.Debugf("shutting down network status plugin") + nc.breakChan <- true + } +} + +// +func (nc *networkCheck) checkConnection(onion string) { + // we want to definitively time these actions out faster than tor will, because these onions should definitely be + // online + ClientTimeout := policies.TimeoutPolicy(time.Second * 60) + err := ClientTimeout.ExecuteAction(func() error { + conn, _, err := nc.acn.Open(onion) + if err == nil { + conn.Close() + } + return err + }) + // regardless of the outcome we want to report a status to let anyone who might care know that we did do a check + if err != nil { + log.Debugf("publishing network error for %v", onion) + nc.bus.Publish(event.NewEvent(event.NetworkStatus, map[event.Field]string{event.Onion: onion, event.Error: err.Error(), event.Status: "Error"})) + } else { + log.Debugf("publishing network success for %v", onion) + nc.bus.Publish(event.NewEvent(event.NetworkStatus, map[event.Field]string{event.Onion: onion, event.Error: "", event.Status: "Success"})) + } +} diff --git a/app/plugins/plugin.go b/app/plugins/plugin.go index 9614394..93d4358 100644 --- a/app/plugins/plugin.go +++ b/app/plugins/plugin.go @@ -2,6 +2,7 @@ package plugins import ( "cwtch.im/cwtch/event" + "git.openprivacy.ca/openprivacy/libricochet-go/connectivity" ) // PluginID is used as an ID for signaling plugin activities @@ -10,6 +11,7 @@ type PluginID int // These are the plugin IDs for the supplied plugins const ( CONNECTIONRETRY PluginID = iota + NETWORKCHECK ) // Plugin is the interface for a plugin @@ -19,10 +21,12 @@ type Plugin interface { } // Get is a plugin factory for the requested plugin -func Get(id PluginID, bus event.Manager) Plugin { +func Get(id PluginID, bus event.Manager, acn connectivity.ACN) Plugin { switch id { case CONNECTIONRETRY: return NewConnectionRetry(bus) + case NETWORKCHECK: + return NewNetworkCheck(bus, acn) } return nil diff --git a/event/common.go b/event/common.go index dceec48..443062a 100644 --- a/event/common.go +++ b/event/common.go @@ -165,6 +165,12 @@ const ( // Progress, Status ACNStatus = Type("ACNStatus") + + // Network Status + // Status: Success || Error + // Error: Description of the Error + // Onion: the local onion we attempt to check + NetworkStatus = Type("NetworkError") ) // Field defines common event attributes @@ -172,6 +178,10 @@ type Field string // Defining Common Field Types const ( + + // A peers local onion address + Onion = Field("Onion") + RemotePeer = Field("RemotePeer") Ciphertext = Field("Ciphertext") Signature = Field("Signature") diff --git a/go.mod b/go.mod index a1f5094..f706204 100644 --- a/go.mod +++ b/go.mod @@ -4,12 +4,18 @@ require ( cwtch.im/tapir v0.1.11 git.openprivacy.ca/openprivacy/libricochet-go v1.0.6 github.com/c-bata/go-prompt v0.2.3 + github.com/client9/misspell v0.3.4 // indirect github.com/golang/protobuf v1.3.2 + github.com/gordonklaus/ineffassign v0.0.0-20190601041439-ed7b1b5ee0f8 // indirect github.com/mattn/go-colorable v0.1.2 // indirect github.com/mattn/go-runewidth v0.0.4 // indirect github.com/mattn/go-tty v0.0.0-20190424173100-523744f04859 // indirect github.com/pkg/term v0.0.0-20190109203006-aa71e9d9e942 // indirect github.com/struCoder/pidusage v0.1.2 golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 + golang.org/x/lint v0.0.0-20190930215403-16217165b5de // indirect golang.org/x/net v0.0.0-20190628185345-da137c7871d7 + golang.org/x/tools v0.0.0-20191031220737-6d8f1af9ccc0 // indirect ) + +go 1.13 diff --git a/go.sum b/go.sum index 54100c9..c54c71f 100644 --- a/go.sum +++ b/go.sum @@ -10,6 +10,8 @@ github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412 h1:w1UutsfOrms1J05zt7I github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412/go.mod h1:WPjqKcmVOxf0XSf3YxCJs6N6AOSrOx3obionmG7T0y0= github.com/c-bata/go-prompt v0.2.3 h1:jjCS+QhG/sULBhAaBdjb2PlMRVaKXQgn+4yzaauvs2s= github.com/c-bata/go-prompt v0.2.3/go.mod h1:VzqtzE2ksDBcdln8G7mk2RX9QyGjH+OVqOCSiVIqS34= +github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cretz/bine v0.1.0 h1:1/fvhLE+fk0bPzjdO5Ci+0ComYxEMuB1JhM4X5skT3g= github.com/cretz/bine v0.1.0/go.mod h1:6PF6fWAvYtwjRGkAuDEJeWNOv3a2hUouSP/yRYXmvHw= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= @@ -18,6 +20,8 @@ github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/gordonklaus/ineffassign v0.0.0-20190601041439-ed7b1b5ee0f8 h1:ehVe1P3MbhHjeN/Rn66N2fGLrP85XXO1uxpLhv0jtX8= +github.com/gordonklaus/ineffassign v0.0.0-20190601041439-ed7b1b5ee0f8/go.mod h1:cuNKsD1zp2v6XfE/orVX2QE1LC+i254ceGcVeDT3pTU= github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE= @@ -41,15 +45,25 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 h1:HuIa8hRrWRSrqYzx1qI49NNxhdi2PrY7gxVSq1JjLDc= golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3 h1:ulvT7fqt0yHWzpJwI57MezWnYDVpCAYBVuYst/L+fAY= golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190628185345-da137c7871d7 h1:rTIdg5QFRR7XCaK4LCjBiPbx8j4DQRpdYMnGn/bJUEU= golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd h1:/e+gpKk9r3dJobndpTytxS2gOy6m5uvpg+ISQoEcusQ= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20191031220737-6d8f1af9ccc0 h1:+o3suEKE/4hCUt6qjV8SDcVZhz2dO8UWlHliCa+4bvg= +golang.org/x/tools v0.0.0-20191031220737-6d8f1af9ccc0/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/peer/cwtch_peer.go b/peer/cwtch_peer.go index 7a70eeb..6101713 100644 --- a/peer/cwtch_peer.go +++ b/peer/cwtch_peer.go @@ -320,7 +320,7 @@ func (cp *cwtchPeer) RejectInvite(groupID string) { // Listen makes the peer open a listening port to accept incoming connections (and be detactably online) func (cp *cwtchPeer) Listen() { log.Debugf("cwtchPeer Listen sending ProtocolEngineStartListen\n") - cp.eventBus.Publish(event.NewEvent(event.ProtocolEngineStartListen, map[event.Field]string{})) + cp.eventBus.Publish(event.NewEvent(event.ProtocolEngineStartListen, map[event.Field]string{event.Onion: cp.Profile.Onion})) } // StartGroupConnections attempts to connect to all group servers (thus initiating reconnect attempts in the conectionsmanager) diff --git a/testing/cwtch_peer_server_integration_test.go b/testing/cwtch_peer_server_integration_test.go index 2f690c6..6d01de7 100644 --- a/testing/cwtch_peer_server_integration_test.go +++ b/testing/cwtch_peer_server_integration_test.go @@ -2,6 +2,7 @@ package testing import ( app2 "cwtch.im/cwtch/app" + "cwtch.im/cwtch/app/plugins" "cwtch.im/cwtch/app/utils" "cwtch.im/cwtch/event" "cwtch.im/cwtch/event/bridge" @@ -163,6 +164,7 @@ func TestCwtchPeerIntegration(t *testing.T) { appClient.CreatePeer("carol", "asdfasdf") alice := utils.WaitGetPeer(app, "alice") + app.AddPeerPlugin(alice.GetProfile().Onion, plugins.NETWORKCHECK) fmt.Println("Alice created:", alice.GetProfile().Onion) alice.AutoHandleEvents([]event.Type{event.PeerStateChange, event.ServerStateChange, event.NewGroupInvite}) @@ -409,6 +411,7 @@ func TestCwtchPeerIntegration(t *testing.T) { fmt.Println("Shutting down ACN...") acn.Close() time.Sleep(time.Second * 2) // Server ^^ has a 5 second loop attempting reconnect before exiting + time.Sleep(time.Second * 30) // the network status plugin might keep goroutines alive for a minute before killing them numGoRoutinesPostACN := runtime.NumGoroutine() // Printing out the current goroutines