From a276d5732a2c6d16ab1d90b1e7e7d3cf0bd2fc3b Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Wed, 4 Nov 2020 13:41:10 -0800 Subject: [PATCH] Self-Hosted Servers Experiment --- go.mod | 2 +- go.sum | 4 +- go/constants/server_manager_events.go | 20 ++- go/features/servers/server_manager.go | 248 ++++++++++++++++++++++++-- go/handlers/peerHandler.go | 2 - go/ui/gcd.go | 46 ++++- main.go | 2 +- qml/panes/ServerAddEditPane.qml | 70 +++++++- qml/widgets/ServerList.qml | 12 +- qml/widgets/ServerRow.qml | 9 +- 10 files changed, 375 insertions(+), 40 deletions(-) diff --git a/go.mod b/go.mod index 42000d2f..e9acf02e 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module cwtch.im/ui go 1.12 require ( - cwtch.im/cwtch v0.4.6 + cwtch.im/cwtch v0.4.7 git.openprivacy.ca/openprivacy/connectivity v1.3.1 git.openprivacy.ca/openprivacy/log v1.0.1 github.com/c-bata/go-prompt v0.2.3 // indirect diff --git a/go.sum b/go.sum index 40e915e9..8c5b5771 100644 --- a/go.sum +++ b/go.sum @@ -9,8 +9,8 @@ cwtch.im/cwtch v0.4.4 h1:LC9SngDx+iLXFiDK8GUkSxaWqloXQcPijvMPmwt9h80= cwtch.im/cwtch v0.4.4/go.mod h1:10gBkMSqAH95Pz4jTx5mpIHE+dkn+4kRC4BFTxWuQK8= cwtch.im/cwtch v0.4.5 h1:BK4IMqCMf9xNmeLzaVDUbl2bnXdw5fOWXvEGBMTOjXM= cwtch.im/cwtch v0.4.5/go.mod h1:Mh7vQQ3z55+prpX6EuUkg4QNQkBACMoDcgCNBeAH2EY= -cwtch.im/cwtch v0.4.6 h1:jQT0WZY0BGS/EKZrtvL48kMYoed00/q1ycvI0u7Dez4= -cwtch.im/cwtch v0.4.6/go.mod h1:Mh7vQQ3z55+prpX6EuUkg4QNQkBACMoDcgCNBeAH2EY= +cwtch.im/cwtch v0.4.7 h1:y8Roq1L1PAs0FkBDdk+7EUVLCHwyzl+dOEfVu4VX0Ic= +cwtch.im/cwtch v0.4.7/go.mod h1:Mh7vQQ3z55+prpX6EuUkg4QNQkBACMoDcgCNBeAH2EY= cwtch.im/tapir v0.2.0 h1:7MkoR5+uEuPW34/O0GZRidnIjq/01Cfm8nl5IRuqpGc= cwtch.im/tapir v0.2.0/go.mod h1:xzzZ28adyUXNkYL1YodcHsAiTt3IJ8Loc29YVn9mIEQ= cwtch.im/tapir v0.2.1 h1:t1YJB9q5sV1A9xwiiwL6WVfw3dwQWLoecunuzT1PQtw= diff --git a/go/constants/server_manager_events.go b/go/constants/server_manager_events.go index d10e0954..44f688cb 100644 --- a/go/constants/server_manager_events.go +++ b/go/constants/server_manager_events.go @@ -2,7 +2,25 @@ package constants import "cwtch.im/cwtch/event" -// The server manage defines its own events... +// The server manager defines its own events, most should be self-explanatory: const ( + NewServer = event.Type("NewServer") + + // Force a UI update ListServers = event.Type("ListServers") + + // Takes an Onion, used to toggle off/on Server availability + StartServer = event.Type("StartServer") + StopServer = event.Type("StopServer") + + // Takes an Onion and a AutoStartEnabled boolean + AutoStart = event.Type("AutoStart") + + // Get the status of a particular server (takes an Onion) + CheckServerStatus = event.Type("CheckServerStatus") + ServerStatusUpdate = event.Type("ServerStatusUpdate") +) + +const ( + AutoStartEnabled = event.Field("AutoStartEnabled") ) \ No newline at end of file diff --git a/go/features/servers/server_manager.go b/go/features/servers/server_manager.go index a2c94563..e1679741 100644 --- a/go/features/servers/server_manager.go +++ b/go/features/servers/server_manager.go @@ -1,57 +1,267 @@ package servers import ( + "crypto/rand" "cwtch.im/cwtch/event" + "cwtch.im/cwtch/model" "cwtch.im/cwtch/server" "cwtch.im/ui/go/constants" "cwtch.im/ui/go/the" "cwtch.im/ui/go/ui" + "encoding/base64" + "fmt" + "git.openprivacy.ca/openprivacy/connectivity" + "git.openprivacy.ca/openprivacy/connectivity/tor" "git.openprivacy.ca/openprivacy/log" + "io/ioutil" + "math" + "math/big" + "os" + "path" + "sync" ) // ServerManager is responsible for managing user operated servers type ServerManager struct { - servers map[string]server.Server + servers sync.Map + configDir string + acn connectivity.ACN } -func LaunchServiceManager(gcd *ui.GrandCentralDispatcher) { +type serverStatusCache struct { + online bool + autostart bool + messages uint64 + bundle []byte +} + + +// LaunchServiceManager is responsible for setting up everything relating to managing servers in the UI. +func LaunchServiceManager(gcd *ui.GrandCentralDispatcher, acn connectivity.ACN, configDir string) { sm := new(ServerManager) + sm.configDir = configDir + sm.acn = acn sm.Init(gcd) } +// initializeServerCache sets up a new cache based on the config. Notably it stores a newly signed keybundle that +// the ui uses to allow people to share server key bundles. +func initializeServerCache(config server.Config) (*server.Server,serverStatusCache) { + newServer := new(server.Server) + newServer.Setup(config) + newServer.KeyBundle() + return newServer, serverStatusCache{ + false, + config.AutoStart, + 0, + newServer.KeyBundle().Serialize(), + } + +} func (sm *ServerManager) Init(gcd *ui.GrandCentralDispatcher) { - sm.servers = make(map[string]server.Server) - q := event.NewQueue() + the.AppBus.Subscribe(constants.NewServer, q) the.AppBus.Subscribe(constants.ListServers, q) + the.AppBus.Subscribe(constants.ServerStatusUpdate, q) + the.AppBus.Subscribe(event.Shutdown, q) + + // NOTE: Servers don't (yet?) benefit from any kind of encryption of their config (unlike peer profiles) + // FIXME: We assume servers can be malicious, a compromised server will not be able to derive any additional + // metadata from a hosted group (assuming that the server isn't also a group participant) - this logic doesn't + // really hold if the server being hosted as part of the UI - however a large pool of self hosted servers also mitigates + // many of risks e.g lack of server diversification / availability of servers. + // Like many parts of the metadata resistant risk model, it is a compromise + log.Debugf("Reading server directory: %v", sm.configDir) + os.MkdirAll(sm.configDir,0700) + items, _ := ioutil.ReadDir(sm.configDir) + for _, item := range items { + if item.IsDir() { + log.Debugf("Found Server Directory %v / %v", sm.configDir, item.Name()) + config := server.LoadConfig(path.Join(sm.configDir, item.Name()), "serverconfig") + identity := config.Identity() + log.Debugf("Launching Server goroutine for %v", identity.Hostname()) + s,cache := initializeServerCache(config) + sm.servers.Store(identity.Hostname(), cache) + go sm.runServer(s) + } else { + log.Debugf("Found non server directory: %v", item.Name()) + } + } + + // Update the UI with all know servers + sm.ListServers(gcd) for { e := q.Next() - switch e.EventType { - case constants.ListServers: { - sm.ListServers(gcd) - } + case constants.NewServer: + sm.NewServer() + sm.ListServers(gcd) + case constants.ListServers: + sm.ListServers(gcd) + case constants.ServerStatusUpdate: + sm.ListServers(gcd) + case event.Shutdown: + // we don't need to do anything else here, all the server goroutines will also subscribe to the + // shutdown event and return nicely. + return } } } -// TODO Replace with details from actual hosted servers. Right now these values are used to sketch / test out the -// UI QML + +// NewServer createa a new server +func (sm *ServerManager) NewServer() { + log.Debugf("Adding a new Server") + num, err := rand.Int(rand.Reader, big.NewInt(math.MaxUint32)) + if err == nil { + serverDir := path.Join(sm.configDir, num.String()); + os.MkdirAll(serverDir,0700) + config := server.LoadConfig(serverDir, "serverconfig") + identity := config.Identity() + s, cache := initializeServerCache(config) + sm.servers.Store(identity.Hostname(),cache) + go sm.runServer(s) + } +} + +// runServer sets up an event queue per server to allow them to manage their own state. +func (sm *ServerManager) runServer(s * server.Server) { + q := event.NewQueue() + the.AppBus.Subscribe(constants.StartServer, q) + the.AppBus.Subscribe(constants.StopServer, q) + the.AppBus.Subscribe(constants.CheckServerStatus, q) + the.AppBus.Subscribe(constants.AutoStart, q) + the.AppBus.Subscribe(event.Shutdown, q) + + + identity := s.Identity() + + cache,ok := sm.servers.Load(identity.Hostname()) + if ok { + serverStatusCache := cache.(serverStatusCache) + if serverStatusCache.autostart { + the.AppBus.Publish(event.NewEvent(constants.StartServer, map[event.Field]string{event.Onion: identity.Hostname()})) + } + } + + + log.Debugf("Launching Server %v", identity.Hostname()) + log.Debugf("Launching Event Bus for Server %v", identity.Hostname()) + for { + e := q.Next() + + switch e.EventType { + case constants.StartServer: + onion := e.Data[event.Onion] + if onion == identity.Hostname() { + if running,_ := s.CheckStatus(); running { + // we are already running + log.Debugf("Server %v Already Running", onion) + continue + } + log.Debugf("Running Server %v", onion) + s.Run(sm.acn) + + // TODO Remove this whole blob once we can actually create groups in the UI + // This is for developers who want to test their newly created server with a group. + group, _ := model.NewGroup(tor.GetTorV3Hostname(identity.PublicKey())) + group.SignGroup([]byte{}) + invite, err := group.Invite([]byte{}) + if err != nil { + panic(err) + } + fmt.Printf("Secret Debugging Group for Testing: %v\n", "torv3"+base64.StdEncoding.EncodeToString(invite)) + cache,ok := sm.servers.Load(onion) + if ok { + serverStatusCache := cache.(serverStatusCache) + serverStatusCache.online = true + sm.servers.Store(onion, serverStatusCache) + the.AppBus.Publish(event.NewEvent(constants.ServerStatusUpdate, map[event.Field]string{})) + } + } + case constants.StopServer: + onion := e.Data[event.Onion] + if onion == identity.Hostname() { + s.Shutdown() + cache,ok := sm.servers.Load(onion) + if ok { + serverStatusCache := cache.(serverStatusCache) + serverStatusCache.online = false + sm.servers.Store(onion, serverStatusCache) + the.AppBus.Publish(event.NewEvent(constants.ServerStatusUpdate, map[event.Field]string{})) + } + } + case constants.CheckServerStatus: + onion := e.Data[event.Onion] + if onion == identity.Hostname() { + // Kick off a restart + if _, err := s.CheckStatus(); err != nil { + s.Shutdown() + s.Run(sm.acn) + } + cache, ok := sm.servers.Load(onion) + if ok { + serverStatusCache := cache.(serverStatusCache) + serverStatusCache.messages = uint64(s.GetStatistics().TotalMessages) + log.Debugf("Server Statistics %v %v", onion, serverStatusCache.messages) + sm.servers.Store(onion, serverStatusCache) + the.AppBus.Publish(event.NewEvent(constants.ServerStatusUpdate, map[event.Field]string{})) + } + } + case constants.AutoStart: + onion := e.Data[event.Onion] + if onion == identity.Hostname() { + autostart := e.Data[constants.AutoStartEnabled] == event.True + s.ConfigureAutostart(autostart) + cache,ok := sm.servers.Load(onion) + if ok { + serverStatusCache := cache.(serverStatusCache) + serverStatusCache.autostart = autostart + sm.servers.Store(onion, serverStatusCache) + the.AppBus.Publish(event.NewEvent(constants.ServerStatusUpdate, map[event.Field]string{})) + } + } + case event.Shutdown: + s.Shutdown() + return + } + } +} + +// ListServers iterates through the current collection of servers and their associated +// cache and sends an update to the UI. func (sm *ServerManager) ListServers(gcd *ui.GrandCentralDispatcher) { log.Debugf("Listing Servers...") - gcd.AddServer("Server 1","Server 1","server",0) - gcd.AddServer("Server 2","Server 2","server",4) - gcd.AddServer("Server 3","Server 3","server",4) -} + sm.servers.Range(func(k interface{},v interface{}) bool { + serverOnion := k.(string) + statusCache := v.(serverStatusCache) + status := 0 + if statusCache.online { + status = 1 + } -func (sm *ServerManager) StartServer(handle string) { - // TODO Start the server with the given handle config -} + // TODO this doesn't allow for an expansion of key types, this whole flow needs rethinking... + key_types := []model.KeyType{model.KeyTypeServerOnion, model.KeyTypeTokenOnion, model.KeyTypePrivacyPass} + var keyNames []string + var keys []string -func (sm *ServerManager) StopServer(handle string) { - // TODO Stop the given server + keybundle,_ := model.DeserializeAndVerify(statusCache.bundle) + + for _, key_type := range key_types { + log.Debugf("Looking up %v %v", key_type, keyNames) + if keybundle.HasKeyType(key_type) { + key,_ := keybundle.GetKey(key_type) + keyNames = append(keyNames, string(key_type)) + keys = append(keys, string(key)) + } + } + + log.Debugf("Updating Server %v %v %v", serverOnion, status, statusCache.messages) + gcd.AddServer(serverOnion,serverOnion,serverOnion, status, statusCache.autostart, "server:"+base64.StdEncoding.EncodeToString(statusCache.bundle), int(statusCache.messages), keyNames, keys) + return true + }) } \ No newline at end of file diff --git a/go/handlers/peerHandler.go b/go/handlers/peerHandler.go index be94e766..826b7191 100644 --- a/go/handlers/peerHandler.go +++ b/go/handlers/peerHandler.go @@ -131,8 +131,6 @@ func PeerHandler(onion string, uiManager ui.Manager, subscribed chan bool) { } uiManager.UpdateContactStatus(groupID, int(state), loading) uiManager.UpdateContactStatus(serverOnion, int(state), loading) - } else { - log.Errorf("found group that is nil :/") } } case event.DeletePeer: diff --git a/go/ui/gcd.go b/go/ui/gcd.go index 85c208a4..8f853217 100644 --- a/go/ui/gcd.go +++ b/go/ui/gcd.go @@ -57,8 +57,13 @@ type GrandCentralDispatcher struct { _ func(failed bool) `signal:"ChangePasswordResponse"` // server management - _ func(handle, displayname, image string, status int) `signal:"AddServer"` + _ func(handle, displayname, image string, status int, autostart bool, bundle string, messages int, key_types []string, keys []string) `signal:"AddServer"` _ func() `signal:"requestServers,auto"` + _ func() `signal:"newServer,auto"` + _ func(server string) `signal:"startServer,auto"` + _ func(server string) `signal:"stopServer,auto"` + _ func(server string) `signal:"checkServer,auto"` + _ func(server string, enabled bool) `signal:"autostartServer",auto` // contact list stuff _ func(handle, displayName, image string, badge, status int, authorization string, loading bool, lastMsgTime int) `signal:"AddContact"` @@ -388,6 +393,45 @@ func (this *GrandCentralDispatcher) requestServers() { })) } +func (this *GrandCentralDispatcher) newServer() { + the.AppBus.Publish(event.NewEvent(constants.NewServer, map[event.Field]string{ + + })) +} + +func (this *GrandCentralDispatcher) startServer(onion string) { + log.Debugf("Requesting Start Server: %v", onion) + the.AppBus.Publish(event.NewEvent(constants.StartServer, map[event.Field]string{ + event.Onion: onion, + })) +} + +func (this *GrandCentralDispatcher) stopServer(onion string) { + log.Debugf("Requesting Stop Server: %v", onion) + the.AppBus.Publish(event.NewEvent(constants.StopServer, map[event.Field]string{ + event.Onion: onion, + })) +} + +func (this *GrandCentralDispatcher) checkServer(onion string) { + log.Debugf("Requesting Stop Server: %v", onion) + the.AppBus.Publish(event.NewEvent(constants.CheckServerStatus, map[event.Field]string{ + event.Onion: onion, + })) +} + +func (this *GrandCentralDispatcher) autostartServer(onion string, enabled bool) { + log.Debugf("Requesting Autostart Toggle: %v %v", onion, enabled) + value := event.False + if enabled { + value = event.True + } + the.AppBus.Publish(event.NewEvent(constants.AutoStart, map[event.Field]string{ + event.Onion: onion, + constants.AutoStartEnabled: value, + })) +} + func (this *GrandCentralDispatcher) requestGroupSettings(groupID string) { group := the.Peer.GetGroup(groupID) diff --git a/main.go b/main.go index 26f96895..652d1ed4 100644 --- a/main.go +++ b/main.go @@ -323,7 +323,7 @@ func loadNetworkingAndFiles(gcd *ui.GrandCentralDispatcher, service bool, client the.AppBus = the.CwtchApp.GetPrimaryBus() subscribed := make(chan bool) go handlers.App(gcd, subscribed, clientUI) - go servers.LaunchServiceManager(gcd) + go servers.LaunchServiceManager(gcd, the.ACN, path.Join(the.CwtchDir, "servers")) <-subscribed } diff --git a/qml/panes/ServerAddEditPane.qml b/qml/panes/ServerAddEditPane.qml index 342073ed..b992341c 100644 --- a/qml/panes/ServerAddEditPane.qml +++ b/qml/panes/ServerAddEditPane.qml @@ -8,9 +8,6 @@ import QtQuick.Window 2.11 import "../opaque" as Opaque import "../opaque/theme" -// import "../styles" - - Opaque.SettingsList { // Add Profile Pane id: serverAddEditPane @@ -19,15 +16,22 @@ Opaque.SettingsList { // Add Profile Pane function reset() { serverAddEditPane.server_name = ""; serverAddEditPane.server_available = false; + serverAddEditPane.server_bundle = "" } property string server_name; property bool server_available; + property string server_bundle; + property bool autostart_server; + property int server_messages; - function load(server_onion, server_name, server_available) { + function load(server_onion, server_name, server_available, autostart_server, server_messages, server_bundle) { reset(); serverAddEditPane.server_name = server_name; serverAddEditPane.server_available = server_available; + serverAddEditPane.server_bundle = server_bundle; + serverAddEditPane.autostart_server = autostart_server; + serverAddEditPane.server_messages = server_messages; } settings: Column { @@ -36,7 +40,7 @@ Opaque.SettingsList { // Add Profile Pane Opaque.ScalingLabel { text: server_name - size: 48 + size: 16 } Opaque.Setting { @@ -48,16 +52,64 @@ Opaque.SettingsList { // Add Profile Pane isToggled: serverAddEditPane.server_available onToggled: function() { - serverAddEditPane.serverAddEditPane = !serverAddEditPane + serverAddEditPane.server_available = !serverAddEditPane.server_available + if (serverAddEditPane.server_available) { + gcd.startServer(serverAddEditPane.server_name) + } else { + gcd.stopServer(serverAddEditPane.server_name) + } } } } + + Opaque.Setting { + label: qsTr("server-autostart") + + field: Opaque.ToggleSwitch { + anchors.right: parent.right + + isToggled: serverAddEditPane.autostart_server + onToggled: function() { + serverAddEditPane.autostart_server = !serverAddEditPane.autostart_server + gcd.autostartServer(serverAddEditPane.server_name, serverAddEditPane.autostart_server) + } + } + } + + Opaque.Setting { + inline: false + label: qsTr("server-num-messages") + + field: Opaque.TextField { + id: numMessages + readOnly: true + text: serverAddEditPane.server_messages; + } + } + + Opaque.Setting { + inline: false + label: qsTr("server-key-bundle") + + field: Opaque.ButtonTextField { + id: txtServerBundle + readOnly: true + text: serverAddEditPane.server_bundle; + button_text: qsTr("copy-btn") + dropShadowColor: Theme.dropShadowPaneColor + onClicked: { + //: notification: copied to clipboard + gcd.popup(qsTr("copied-to-clipboard-notification")) + txtServerBundle.selectAll() + txtServerBundle.copy() + } + } + } + } - Connections { // UPDATE UNREAD MESSAGES COUNTER + Connections { target: gcd - - } } diff --git a/qml/widgets/ServerList.qml b/qml/widgets/ServerList.qml index 5c596be4..769de778 100644 --- a/qml/widgets/ServerList.qml +++ b/qml/widgets/ServerList.qml @@ -40,11 +40,13 @@ ColumnLayout { gcd.requestServers(); } - onAddServer: function(handle, displayName, image, status) { + onAddServer: function(handle, displayName, image, status, autostart, bundle, messages, key_types, keys) { // don't add duplicates for (var i = 0; i < serversModel.count; i++) { if (serversModel.get(i)["_handle"] == handle) { + serversModel.get(i)["_status"] = status + serversModel.get(i)["_messages"] = messages return } } @@ -64,6 +66,9 @@ ColumnLayout { _displayName: displayName, _image: image, _status: status, + _bundle: bundle, + _autostart: autostart, + _messages: messages }) } @@ -84,6 +89,9 @@ ColumnLayout { displayName: _displayName image: _image status: _status + bundle: _bundle + autostart: _autostart + messages: _messages Layout.fillWidth: true } } @@ -105,7 +113,7 @@ ColumnLayout { } badgeColor: Theme.defaultButtonColor - onClicked: function(handle) { serverAddEditPane.reset(); parentStack.pane = parentStack.addEditServerPane } + onClicked: function(handle) { gcd.newServer() } } } } diff --git a/qml/widgets/ServerRow.qml b/qml/widgets/ServerRow.qml index 5cbc3bea..0585a13b 100644 --- a/qml/widgets/ServerRow.qml +++ b/qml/widgets/ServerRow.qml @@ -14,13 +14,16 @@ import "../opaque/theme" Opaque.PortraitRow { id: root property int status; + property string bundle; + property bool autostart; + property int messages; portraitBorderColor: Theme.portraitOnlineBorderColor portraitColor: Theme.portraitOnlineBackgroundColor nameColor: Theme.portraitOnlineTextColor onionColor: Theme.portraitOnlineTextColor - badgeColor: status == 4 ? Theme.portraitOnlineBadgeColor : Theme.portraitOfflineBadgeColor + badgeColor: status == 1 ? Theme.portraitOnlineBadgeColor : Theme.portraitOfflineBadgeColor badgeVisible: true Opaque.Icon {// Edit BUTTON @@ -42,12 +45,14 @@ Opaque.PortraitRow { size: parent.height * 0.5 onClicked: { - serverAddEditPane.load(handle, displayName, status) + gcd.checkServer(handle) + serverAddEditPane.load(handle, displayName, status, autostart, messages, bundle) parentStack.pane = parentStack.addEditServerPane } onHover: function (hover) { root.isHover = hover + gcd.checkServer(handle) } }