Self-Hosted Servers Experiment
the build was successful Details

This commit is contained in:
Sarah Jamie Lewis 2020-11-04 13:41:10 -08:00
parent fd45f72a09
commit a276d5732a
10 changed files with 375 additions and 40 deletions

2
go.mod
View File

@ -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

4
go.sum
View File

@ -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=

View File

@ -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")
)

View File

@ -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
})
}

View File

@ -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:

View File

@ -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)

View File

@ -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
}

View File

@ -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
}
}

View File

@ -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() }
}
}
}

View File

@ -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)
}
}