diff --git a/acn.go b/acn.go deleted file mode 100644 index 50ada0a..0000000 --- a/acn.go +++ /dev/null @@ -1,103 +0,0 @@ -package main - -import ( - "crypto/rand" - "cwtch.im/cwtch/app" - "cwtch.im/cwtch/event" - "encoding/base64" - "fmt" - "git.openprivacy.ca/openprivacy/connectivity" - "git.openprivacy.ca/openprivacy/connectivity/tor" - "git.openprivacy.ca/openprivacy/log" - mrand "math/rand" - "os" - path "path/filepath" - "strings" - "time" -) - -const ( - CwtchStarted = event.Type("CwtchStarted") - CwtchStartError = event.Type("CwtchStartError") - UpdateGlobalSettings = event.Type("UpdateGlobalSettings") -) - -func buildACN(settings app.GlobalSettings, torPath string, appDir string) (connectivity.ACN, app.GlobalSettings) { - - mrand.Seed(int64(time.Now().Nanosecond())) - socksPort := mrand.Intn(1000) + 9600 - controlPort := socksPort + 1 - - // generate a random password (actually random, stored in memory, for the control port) - key := make([]byte, 64) - _, err := rand.Read(key) - if err != nil { - panic(err) - } - - log.Infof("making directory %v", appDir) - err = os.MkdirAll(path.Join(appDir, "tor"), 0700) - - if err != nil { - log.Errorf("error creating tor data directory: %v. Aborting app start up", err) - eventHandler.Push(event.NewEventList(CwtchStartError, event.Error, fmt.Sprintf("Error connecting to Tor: %v", err))) - return &connectivity.ErrorACN{}, settings - } - - if settings.AllowAdvancedTorConfig { - controlPort = settings.CustomControlPort - socksPort = settings.CustomSocksPort - } - - torrc := tor.NewTorrc().WithSocksPort(socksPort).WithOnionTrafficOnly().WithControlPort(controlPort).WithHashedPassword(base64.StdEncoding.EncodeToString(key)) - // torrc.WithLog(path.Join(appDir, "tor", "tor.log"), tor.TorLogLevelNotice) - if settings.UseCustomTorrc { - customTorrc := settings.CustomTorrc - torrc.WithCustom(strings.Split(customTorrc, "\n")) - } else { - // Fallback to showing the freshly generated torrc for this session. - settings.CustomTorrc = torrc.Preview() - settings.CustomControlPort = controlPort - settings.CustomSocksPort = socksPort - } - - err = torrc.Build(path.Join(appDir, "tor", "torrc")) - - if err != nil { - log.Errorf("error constructing torrc: %v", err) - eventHandler.Push(event.NewEventList(CwtchStartError, event.Error, fmt.Sprintf("Error connecting to Tor: %v", err))) - return &connectivity.ErrorACN{}, settings - } - - dataDir := settings.TorCacheDir - if !settings.UseTorCache { - - // purge data dir directories if we are not using them for a cache - torDir := path.Join(appDir, "tor") - files, err := path.Glob(path.Join(torDir, "data-dir-*")) - if err != nil { - log.Errorf("could not construct filesystem glob: %v", err) - } - for _, f := range files { - if err := os.RemoveAll(f); err != nil { - log.Errorf("could not remove data-dir: %v", err) - } - } - - if dataDir, err = os.MkdirTemp(torDir, "data-dir-"); err != nil { - eventHandler.Push(event.NewEventList(CwtchStartError, event.Error, fmt.Sprintf("Error connecting to Tor: %v", err))) - return &connectivity.ErrorACN{}, settings - } - } - - // Persist Current Data Dir as Tor Cache... - settings.TorCacheDir = dataDir - - acn, err := tor.NewTorACNWithAuth(appDir, torPath, dataDir, controlPort, tor.HashedPasswordAuthenticator{Password: base64.StdEncoding.EncodeToString(key)}) - if err != nil { - log.Errorf("Error connecting to Tor replacing with ErrorACN: %v\n", err) - eventHandler.Push(event.NewEventList(CwtchStartError, event.Error, fmt.Sprintf("Error connecting to Tor: %v", err))) - acn = &connectivity.ErrorACN{} - } - return acn, settings -} diff --git a/attributes.go b/attributes.go deleted file mode 100644 index f3bdaa7..0000000 --- a/attributes.go +++ /dev/null @@ -1,84 +0,0 @@ -package main - -import "C" -import ( - "cwtch.im/cwtch/model/attr" - "encoding/json" -) - -// TODO: At some point these functions should also be autogenerated - -// Attribute is a struct to return the dual values of an attempt at a Get*Attribute API call, meant to be json serialized -type Attribute struct { - Exists bool - Value string -} - -//export c_GetProfileAttribute -func c_GetProfileAttribute(profile_ptr *C.char, profile_len C.int, key_ptr *C.char, key_len C.int) *C.char { - profileOnion := C.GoStringN(profile_ptr, profile_len) - key := C.GoStringN(key_ptr, key_len) - return C.CString(GetProfileAttribute(profileOnion, key)) -} - -// GetProfileAttribute provides a wrapper around profile.GetScopedZonedAttribute -// Key must have the format zone.key where Zone is defined in Cwtch. Unknown zones are not permitted. -// Currently forcing the Public Scope -// Returns json of Attribute -func GetProfileAttribute(profileOnion string, key string) string { - profile := application.GetPeer(profileOnion) - if profile != nil { - zone, key := attr.ParseZone(key) - - res, exists := profile.GetScopedZonedAttribute(attr.PublicScope, zone, key) - attr := Attribute{exists, res} - json, _ := json.Marshal(attr) - return string(json) - - } - empty := Attribute{false, ""} - json, _ := json.Marshal(empty) - return (string(json)) -} - -//export c_SetConversationAttribute -func c_SetConversationAttribute(profile_ptr *C.char, profile_len C.int, conversation_id C.int, key_ptr *C.char, key_len C.int, val_ptr *C.char, val_len C.int) { - profileOnion := C.GoStringN(profile_ptr, profile_len) - key := C.GoStringN(key_ptr, key_len) - value := C.GoStringN(val_ptr, val_len) - SetConversationAttribute(profileOnion, int(conversation_id), key, value) -} - -// SetConversationAttribute provides a wrapper around profile.SetProfileAttribute -// key is of format Zone.Key, and the API forces the Local Scope -func SetConversationAttribute(profileOnion string, conversationID int, key string, value string) { - profile := application.GetPeer(profileOnion) - zone, key := attr.ParseZone(key) - profile.SetConversationAttribute(conversationID, attr.LocalScope.ConstructScopedZonedPath(zone.ConstructZonedPath(key)), value) -} - -//export c_GetConversationAttribute -func c_GetConversationAttribute(profile_ptr *C.char, profile_len C.int, conversation_id C.int, key_ptr *C.char, key_len C.int) *C.char { - profileOnion := C.GoStringN(profile_ptr, profile_len) - key := C.GoStringN(key_ptr, key_len) - return C.CString(GetConversationAttribute(profileOnion, int(conversation_id), key)) -} - -// GetGonversationAttribute provides a wrapper around profile.GetGonversationAttribute -// key is of format Scope.Zone.Key -// Returns json of an Attribute -func GetConversationAttribute(profileOnion string, conversationID int, key string) string { - profile := application.GetPeer(profileOnion) - if profile != nil { - scope, zonekey := attr.ParseScope(key) - zone, key := attr.ParseZone(zonekey) - - res, err := profile.GetConversationAttribute(conversationID, scope.ConstructScopedZonedPath(zone.ConstructZonedPath(key))) - attr := Attribute{err == nil, res} - json, _ := json.Marshal(attr) - return string(json) - } - empty := Attribute{false, ""} - json, _ := json.Marshal(empty) - return (string(json)) -} diff --git a/constants/globals.go b/constants/globals.go index f4669f4..e5d41ee 100644 --- a/constants/globals.go +++ b/constants/globals.go @@ -11,6 +11,13 @@ const ( NotificationConversation = NotificationType("ContactInfo") ) +const ( + // StatusSuccess is an event response for event.Status signifying a call succeeded + StatusSuccess = "success" + // StatusError is an event response for event.Status signifying a call failed in error, ideally accompanied by a event.Error + StatusError = "error" +) + const ( // ConversationNotificationPolicyDefault enum for conversations indicating to use global notification policy ConversationNotificationPolicyDefault = "ConversationNotificationPolicy.Default" diff --git a/experiments/servers/servers_functionality.go b/experiments/servers/servers_functionality.go new file mode 100644 index 0000000..72f54bc --- /dev/null +++ b/experiments/servers/servers_functionality.go @@ -0,0 +1,337 @@ +package servers + +import ( + "cwtch.im/cwtch/app" + "cwtch.im/cwtch/event" + "git.openprivacy.ca/cwtch.im/cwtch-autobindings/constants" + "git.openprivacy.ca/cwtch.im/server" + "git.openprivacy.ca/openprivacy/connectivity" + "git.openprivacy.ca/openprivacy/log" + "os" + "path/filepath" + "strconv" + "sync" + "time" +) + +const serversExperiment = "servers-experiment" + +const ( + ZeroServersLoaded = event.Type("ZeroServersLoaded") + NewServer = event.Type("NewServer") + ServerIntentUpdate = event.Type("ServerIntentUpdate") + ServerDeleted = event.Type("ServerDeleted") + ServerStatsUpdate = event.Type("ServerStatsUpdate") +) + +const ( + Intent = event.Field("Intent") + TotalMessages = event.Field("TotalMessages") + Connections = event.Field("Connections") +) + +const ( + IntentRunning = "running" + IntentStopped = "stopped" +) + +type ServerInfo struct { + Onion string + ServerBundle string + Autostart bool + Running bool + Description string + StorageType string +} + +type PublishFn func(event.Event) + +var lock sync.Mutex +var appServers server.Servers +var killStatsUpdate chan bool = make(chan bool, 1) +var enabled bool = false + +func InitServers(acn connectivity.ACN, appdir string, ) *ServersFunctionality { + lock.Lock() + defer lock.Unlock() + if appServers == nil { + serversDir := filepath.Join(appdir, "servers") + err := os.MkdirAll(serversDir, 0700) + if err != nil { + log.Errorf("Could not init servers directory: %s", err) + } + appServers = server.NewServers(acn, serversDir) + } + return new(ServersFunctionality) +} + +// track acnStatus across events +var acnStatus = -1 + +func (sf *ServersFunctionality) OnACNStatusEvent(appl app.Application, e *event.Event) { + newAcnStatus, err := strconv.Atoi(e.Data[event.Progress]) + if err != nil { + return + } + if newAcnStatus == 100 { + if acnStatus != 100 { + // just came online + } + } else { + if acnStatus == 100 { + for _, onion := range sf.ListServers() { + sf.StopServer(appl, onion) + } + } + } + acnStatus = newAcnStatus +} + +func (sh *ServersFunctionality) Disable() { + lock.Lock() + defer lock.Unlock() + if appServers != nil { + appServers.Stop() + } + if enabled { + enabled = false + killStatsUpdate <- true + } +} + +func (sh *ServersFunctionality) Enabled() bool { + lock.Lock() + defer lock.Unlock() + return enabled +} + +// ServersFunctionality provides experiment gated server functionality +type ServersFunctionality struct { +} + +func (sh *ServersFunctionality) UpdateSettings(appl app.Application, acn connectivity.ACN) { + if appl.IsFeatureEnabled(serversExperiment) { + sh.Disable() + } else { + sh.LoadServers(appl, acn, app.DefactoPasswordForUnencryptedProfiles) + if !sh.Enabled() { + sh.Enable(appl) + serversList := sh.ListServers() + for _, server := range serversList { + serverInfo := sh.GetServerInfo(server) + ev := event.NewEvent(NewServer, make(map[event.Field]string)) + serverInfo.EnrichEvent(&ev) + appl.GetPrimaryBus().Publish(ev) + } + sh.LaunchServers(appl, acn) + } + } +} + +func (sh *ServersFunctionality) SetServerAttribute(appl app.Application, handle string, key string, val string) { + if appl.IsFeatureEnabled(serversExperiment) { + server := sh.GetServer(handle) + if server != nil { + server.SetAttribute(key, val) + } + } +} + +func (sh *ServersFunctionality) StopServers(appl app.Application) { + // we are always permitted to stop servers + for _, onion := range sh.ListServers() { + sh.StopServer(appl, onion) + } +} + +func (sh *ServersFunctionality) LaunchServers(appl app.Application, acn connectivity.ACN) { + if appl.IsFeatureEnabled(serversExperiment) { + acnStatus, _ := acn.GetBootstrapStatus() + if acnStatus == 100 { + for _, onion := range sh.ListServers() { + autostart := false + if s := sh.GetServer(onion); s != nil { + autostart = s.GetAttribute(server.AttrAutostart) == "true" + } + if autostart { + sh.LaunchServer(appl, onion) + } + } + } + } +} + +func (sf *ServersFunctionality) Enable(application app.Application) { + lock.Lock() + defer lock.Unlock() + if appServers != nil && !enabled { + enabled = true + go cacheForwardServerMetricUpdates(application) + } +} + +func (sf *ServersFunctionality) LoadServers(appl app.Application, acn connectivity.ACN, password string) { + serversList, err := appServers.LoadServers(password) + if err != nil { + log.Errorf("Error attempting to load servers :%s\n", err) + appl.GetPrimaryBus().Publish(event.NewEventList(ZeroServersLoaded)) + } else if len(serversList) == 0 { + log.Debugln("Loaded 0 servers") + appl.GetPrimaryBus().Publish(event.NewEventList(ZeroServersLoaded)) + } else { + acnStatus, _ := acn.GetBootstrapStatus() + for _, serverOnion := range serversList { + serverInfo := sf.GetServerInfo(serverOnion) + log.Debugf("Load Server NewServer event: %s", serverInfo) + ev := event.NewEvent(NewServer, make(map[event.Field]string)) + serverInfo.EnrichEvent(&ev) + appl.GetPrimaryBus().Publish(ev) + if serverInfo.Autostart && acnStatus == 100 { + sf.LaunchServer(appl, serverOnion) + } + } + } + + // server:1.3/libcwtch-go:1.4 accidentally enabled monitor logging by default. make sure it's turned off + for _, onion := range serversList { + server := appServers.GetServer(onion) + server.SetMonitorLogging(false) + } +} + +func (sf *ServersFunctionality) CreateServer(appl app.Application, password string, description string, autostart bool) { + s, err := appServers.CreateServer(password) + if err != nil { + log.Errorf("Could not create new server: %s\n", err) + } else { + s.SetAttribute(server.AttrDescription, description) + if autostart { + s.SetAttribute(server.AttrAutostart, "true") + } else { + s.SetAttribute(server.AttrAutostart, "false") + } + if password == app.DefactoPasswordForUnencryptedProfiles { + s.SetAttribute(server.AttrStorageType, server.StorageTypeDefaultPassword) + } else { + s.SetAttribute(server.AttrStorageType, server.StorageTypePassword) + } + serverInfo := sf.GetServerInfo(s.Onion()) + log.Debugf("Creating Server NewServer event: %s", serverInfo) + ev := event.NewEvent(NewServer, make(map[event.Field]string)) + serverInfo.EnrichEvent(&ev) + appl.GetPrimaryBus().Publish(ev) + if autostart { + sf.LaunchServer(appl, s.Onion()) + } + } +} + +func (sf *ServersFunctionality) GetServer(onion string) server.Server { + return appServers.GetServer(onion) +} + +func (sf *ServersFunctionality) GetServerStatistics(onion string) server.Statistics { + s := appServers.GetServer(onion) + if s != nil { + return s.GetStatistics() + } + return server.Statistics{} +} + +func (sf *ServersFunctionality) ListServers() []string { + return appServers.ListServers() +} + +func (sf *ServersFunctionality) DeleteServer(appl app.Application, onion string, currentPassword string) error { + sf.StopServer(appl, onion) + appl.GetPrimaryBus().Publish(event.NewEventList(ServerIntentUpdate, event.Identity, onion, Intent, IntentStopped)) + err := appServers.DeleteServer(onion, currentPassword) + if err == nil { + appl.GetPrimaryBus().Publish(event.NewEventList(ServerDeleted, event.Status, constants.StatusSuccess, event.Identity, onion)) + } else { + appl.GetPrimaryBus().Publish(event.NewEventList(ServerDeleted, event.Status, constants.StatusError, event.Error, err.Error(), event.Identity, onion)) + } + return err +} + +func (sf *ServersFunctionality) LaunchServer(appl app.Application, onion string) { + if appl.IsFeatureEnabled(serversExperiment) { + appServers.LaunchServer(onion) + server := appServers.GetServer(onion) + if server != nil { + newStats := server.GetStatistics() + appl.GetPrimaryBus().Publish(event.NewEventList(ServerStatsUpdate, event.Identity, onion, TotalMessages, strconv.Itoa(newStats.TotalMessages), Connections, strconv.Itoa(newStats.TotalConnections))) + } + } +} + +func (sf *ServersFunctionality) StopServer(appl app.Application, onion string) { + // we are always permitted to stop servers + appServers.StopServer(onion) + appl.GetPrimaryBus().Publish(event.NewEventList(ServerIntentUpdate, event.Identity, onion, Intent, IntentStopped)) +} + +func (sf *ServersFunctionality) DestroyServers() { + // we are always permitted to destroy servers + appServers.Destroy() +} + +func (sf *ServersFunctionality) GetServerInfo(onion string) *ServerInfo { + s := sf.GetServer(onion) + var serverInfo ServerInfo + serverInfo.Onion = s.Onion() + serverInfo.ServerBundle = s.ServerBundle() + serverInfo.Autostart = s.GetAttribute(server.AttrAutostart) == "true" + running, _ := s.CheckStatus() + serverInfo.Running = running + serverInfo.Description = s.GetAttribute(server.AttrDescription) + serverInfo.StorageType = s.GetAttribute(server.AttrStorageType) + return &serverInfo +} + +func (si *ServerInfo) EnrichEvent(e *event.Event) { + e.Data["Onion"] = si.Onion + e.Data["ServerBundle"] = si.ServerBundle + e.Data["Description"] = si.Description + e.Data["StorageType"] = si.StorageType + if si.Autostart { + e.Data["Autostart"] = "true" + } else { + e.Data["Autostart"] = "false" + } + if si.Running { + e.Data["Running"] = "true" + } else { + e.Data["Running"] = "false" + } +} + +// cacheForwardServerMetricUpdates every minute gets metrics for all servers, and if they have changed, sends events to the UI +func cacheForwardServerMetricUpdates(appl app.Application) { + var cache map[string]server.Statistics = make(map[string]server.Statistics) + duration := time.Second // allow first load + for { + select { + case <-time.After(duration): + duration = time.Minute + serverList := appServers.ListServers() + for _, serverOnion := range serverList { + server := appServers.GetServer(serverOnion) + if running, err := server.CheckStatus(); running && err == nil { + newStats := server.GetStatistics() + if stats, ok := cache[serverOnion]; !ok || stats.TotalConnections != newStats.TotalConnections || stats.TotalMessages != newStats.TotalMessages { + cache[serverOnion] = newStats + appl.GetPrimaryBus().Publish(event.NewEventList(ServerStatsUpdate, event.Identity, serverOnion, TotalMessages, strconv.Itoa(newStats.TotalMessages), Connections, strconv.Itoa(newStats.TotalConnections))) + } + } + } + case <-killStatsUpdate: + return + } + } +} + +func (sh *ServersFunctionality) Shutdown() { + sh.Disable() + appServers.Destroy() +} diff --git a/features/servers/servers_functionality.go b/features/servers/servers_functionality.go deleted file mode 100644 index 6b25c1a..0000000 --- a/features/servers/servers_functionality.go +++ /dev/null @@ -1,220 +0,0 @@ -package servers - -import ( - "cwtch.im/cwtch/event" - "fmt" - "git.openprivacy.ca/cwtch.im/server" - "git.openprivacy.ca/openprivacy/connectivity" - "git.openprivacy.ca/openprivacy/log" - "os" - "path/filepath" - "strconv" - "sync" - "time" -) - -const serversExperiment = "servers-experiment" - -const ( - ZeroServersLoaded = event.Type("ZeroServersLoaded") - NewServer = event.Type("NewServer") - ServerIntentUpdate = event.Type("ServerIntentUpdate") - ServerDeleted = event.Type("ServerDeleted") - ServerStatsUpdate = event.Type("ServerStatsUpdate") -) - -const ( - Intent = event.Field("Intent") - TotalMessages = event.Field("TotalMessages") - Connections = event.Field("Connections") -) - -const ( - IntentRunning = "running" - IntentStopped = "stopped" -) - -// TODO: move into Cwtch model/attr - -type ServerInfo struct { - Onion string - ServerBundle string - Autostart bool - Running bool - Description string - StorageType string -} - -type PublishFn func(event.Event) - -var lock sync.Mutex -var appServers server.Servers -var publishFn PublishFn -var killStatsUpdate chan bool = make(chan bool, 1) -var enabled bool = false - -func InitServers(acn connectivity.ACN, appdir string, pfn PublishFn) { - lock.Lock() - defer lock.Unlock() - if appServers == nil { - serversDir := filepath.Join(appdir, "servers") - err := os.MkdirAll(serversDir, 0700) - if err != nil { - log.Errorf("Could not init servers directory: %s", err) - } - appServers = server.NewServers(acn, serversDir) - publishFn = pfn - } -} - -func Disable() { - lock.Lock() - defer lock.Unlock() - if appServers != nil { - appServers.Stop() - } - if enabled { - enabled = false - killStatsUpdate <- true - } -} - -func Enabled() bool { - lock.Lock() - defer lock.Unlock() - return enabled -} - -// ServersFunctionality provides experiment gated server functionality -type ServersFunctionality struct { -} - -// ExperimentGate returns ServersFunctionality if the experiment is enabled, and an error otherwise. -func ExperimentGate(experimentMap map[string]bool) (*ServersFunctionality, error) { - if experimentMap[serversExperiment] { - lock.Lock() - defer lock.Unlock() - return &ServersFunctionality{}, nil - } - return nil, fmt.Errorf("gated by %v", serversExperiment) -} - -func (sf *ServersFunctionality) Enable() { - lock.Lock() - defer lock.Unlock() - if appServers != nil && !enabled { - enabled = true - go cacheForwardServerMetricUpdates() - } -} - -func (sf *ServersFunctionality) LoadServers(password string) ([]string, error) { - servers, err := appServers.LoadServers(password) - // server:1.3/libcwtch-go:1.4 accidentally enabled monitor logging by default. make sure it's turned off - for _, onion := range servers { - server := appServers.GetServer(onion) - server.SetMonitorLogging(false) - } - return servers, err -} - -func (sf *ServersFunctionality) CreateServer(password string) (server.Server, error) { - return appServers.CreateServer(password) -} - -func (sf *ServersFunctionality) GetServer(onion string) server.Server { - return appServers.GetServer(onion) -} - -func (sf *ServersFunctionality) GetServerStatistics(onion string) server.Statistics { - s := appServers.GetServer(onion) - if s != nil { - return s.GetStatistics() - } - return server.Statistics{} -} - -func (sf *ServersFunctionality) ListServers() []string { - return appServers.ListServers() -} - -func (sf *ServersFunctionality) DeleteServer(onion string, currentPassword string) error { - return appServers.DeleteServer(onion, currentPassword) -} - -func (sf *ServersFunctionality) LaunchServer(onion string) { - appServers.LaunchServer(onion) - server := appServers.GetServer(onion) - if server != nil { - newStats := server.GetStatistics() - publishFn(event.NewEventList(ServerStatsUpdate, event.Identity, onion, TotalMessages, strconv.Itoa(newStats.TotalMessages), Connections, strconv.Itoa(newStats.TotalConnections))) - } -} - -func (sf *ServersFunctionality) StopServer(onion string) { - appServers.StopServer(onion) -} - -func (sf *ServersFunctionality) DestroyServers() { - appServers.Destroy() -} - -func (sf *ServersFunctionality) GetServerInfo(onion string) *ServerInfo { - s := sf.GetServer(onion) - var serverInfo ServerInfo - serverInfo.Onion = s.Onion() - serverInfo.ServerBundle = s.ServerBundle() - serverInfo.Autostart = s.GetAttribute(server.AttrAutostart) == "true" - running, _ := s.CheckStatus() - serverInfo.Running = running - serverInfo.Description = s.GetAttribute(server.AttrDescription) - serverInfo.StorageType = s.GetAttribute(server.AttrStorageType) - return &serverInfo -} - -func (si *ServerInfo) EnrichEvent(e *event.Event) { - e.Data["Onion"] = si.Onion - e.Data["ServerBundle"] = si.ServerBundle - e.Data["Description"] = si.Description - e.Data["StorageType"] = si.StorageType - if si.Autostart { - e.Data["Autostart"] = "true" - } else { - e.Data["Autostart"] = "false" - } - if si.Running { - e.Data["Running"] = "true" - } else { - e.Data["Running"] = "false" - } -} - -// cacheForwardServerMetricUpdates every minute gets metrics for all servers, and if they have changed, sends events to the UI -func cacheForwardServerMetricUpdates() { - var cache map[string]server.Statistics = make(map[string]server.Statistics) - duration := time.Second // allow first load - for { - select { - case <-time.After(duration): - duration = time.Minute - serverList := appServers.ListServers() - for _, serverOnion := range serverList { - server := appServers.GetServer(serverOnion) - if running, err := server.CheckStatus(); running && err == nil { - newStats := server.GetStatistics() - if stats, ok := cache[serverOnion]; !ok || stats.TotalConnections != newStats.TotalConnections || stats.TotalMessages != newStats.TotalMessages { - cache[serverOnion] = newStats - publishFn(event.NewEventList(ServerStatsUpdate, event.Identity, serverOnion, TotalMessages, strconv.Itoa(newStats.TotalMessages), Connections, strconv.Itoa(newStats.TotalConnections))) - } - } - } - case <-killStatsUpdate: - return - } - } -} - -func Shutdown() { - Disable() - appServers.Destroy() -} diff --git a/generate/generate_bindings.go b/generate/generate_bindings.go index ed0c7c1..21bfe98 100644 --- a/generate/generate_bindings.go +++ b/generate/generate_bindings.go @@ -49,6 +49,8 @@ func main() { case "app": generatedBindings = generateAppFunction(generatedBindings, fName, parts[2:]) + case "(json)app": + generatedBindings = generateJsonAppFunction(generatedBindings, fName, parts[2:]) case "profile": generatedBindings = generateProfileFunction(generatedBindings, fName, parts[2:]) case "(json)profile": @@ -233,6 +235,30 @@ func {{FNAME}}({{GO_ARGS_SPEC}}) { return bindings } +func generateJsonAppFunction(bindings string, name string, argsTypes []string) string { + appPrototype := ` +//export c_{{FNAME}} +func c_{{FNAME}}({{C_ARGS}}) *C.char { + return C.CString({{FNAME}}({{C2GO_ARGS}})) +} + +func {{FNAME}}({{GO_ARGS_SPEC}}) string { + return application.{{LIBNAME}}({{GO_ARG}}) +} +` + + cArgs, c2GoArgs, goSpec, gUse := mapArgs(argsTypes) + appPrototype = strings.ReplaceAll(appPrototype, "{{FNAME}}", strings.TrimPrefix(name, "Enhanced")) + appPrototype = strings.ReplaceAll(appPrototype, "{{LIBNAME}}", name) + appPrototype = strings.ReplaceAll(appPrototype, "{{C_ARGS}}", cArgs) + appPrototype = strings.ReplaceAll(appPrototype, "{{C2GO_ARGS}}", c2GoArgs) + appPrototype = strings.ReplaceAll(appPrototype, "{{GO_ARGS_SPEC}}", goSpec) + appPrototype = strings.ReplaceAll(appPrototype, "{{GO_ARG}}", gUse) + + bindings += appPrototype + return bindings +} + func generateProfileFunction(bindings string, name string, argsTypes []string) string { appPrototype := ` //export c_{{FNAME}} diff --git a/go.mod b/go.mod index c5e3bf7..cd9c6a4 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,8 @@ require ( github.com/mutecomm/go-sqlcipher/v4 v4.4.2 ) +replace cwtch.im/cwtch => /home/sarah/workspace/src/cwtch.im/cwtch + require ( filippo.io/edwards25519 v1.0.0 // indirect git.openprivacy.ca/cwtch.im/tapir v0.6.0 // indirect diff --git a/go.sum b/go.sum index cb757c8..562ce03 100644 --- a/go.sum +++ b/go.sum @@ -1,16 +1,8 @@ cwtch.im/cwtch v0.18.0/go.mod h1:StheazFFY7PKqBbEyDVLhzWW6WOat41zV0ckC240c5Y= -cwtch.im/cwtch v0.18.10-0.20230221235514-49e0d849fa3e h1:5Gu6fKJNcTR7zzNj/JBtdrZqtkGh4eP/FQPwrF6sY5A= -cwtch.im/cwtch v0.18.10-0.20230221235514-49e0d849fa3e/go.mod h1:h8S7EgEM+8pE1k+XLB5jAFdIPlOzwoXEY0GH5mQye5A= -cwtch.im/cwtch v0.18.10-0.20230225151912-b22b8f329771 h1:Rc+3E0FGDbia35//hI/dH4feTww/w2gz4/2nHYI8sq0= -cwtch.im/cwtch v0.18.10-0.20230225151912-b22b8f329771/go.mod h1:h8S7EgEM+8pE1k+XLB5jAFdIPlOzwoXEY0GH5mQye5A= -cwtch.im/cwtch v0.18.10-0.20230227200154-03da50b85054 h1:PeEVr0+KSqJlL41UkCRLKIo8DJRTWKlbxsvXacp/Z+U= -cwtch.im/cwtch v0.18.10-0.20230227200154-03da50b85054/go.mod h1:h8S7EgEM+8pE1k+XLB5jAFdIPlOzwoXEY0GH5mQye5A= -cwtch.im/cwtch v0.18.10-0.20230227200719-c90301bb4cfa h1:U+zbOAapOb1SAaoFuBywi9wb3e8/xcpZSNBry6EPfj4= -cwtch.im/cwtch v0.18.10-0.20230227200719-c90301bb4cfa/go.mod h1:h8S7EgEM+8pE1k+XLB5jAFdIPlOzwoXEY0GH5mQye5A= cwtch.im/cwtch v0.18.10-0.20230227213132-dfaed356c491 h1:pzzWnyXQHsnKDNHhG+JRTlDg3Eit62grfkM6KeUiv8g= cwtch.im/cwtch v0.18.10-0.20230227213132-dfaed356c491/go.mod h1:h8S7EgEM+8pE1k+XLB5jAFdIPlOzwoXEY0GH5mQye5A= -cwtch.im/cwtch v0.18.10 h1:iTzLzlms1mgn8kLfClU/yAWIVWVRRT8UmfbDNli9dzE= -cwtch.im/cwtch v0.18.10/go.mod h1:h8S7EgEM+8pE1k+XLB5jAFdIPlOzwoXEY0GH5mQye5A= +cwtch.im/cwtch v0.18.10-0.20230227220552-0f83fb79f0c7 h1:hHKB6bPgYPwjOBGg3JR/3ovmSys1rKs6nsnpkrXslbU= +cwtch.im/cwtch v0.18.10-0.20230227220552-0f83fb79f0c7/go.mod h1:h8S7EgEM+8pE1k+XLB5jAFdIPlOzwoXEY0GH5mQye5A= filippo.io/edwards25519 v1.0.0-rc.1/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns= filippo.io/edwards25519 v1.0.0 h1:0wAIcmJUqRdI8IJ/3eGi5/HwXZWPujYXXlkrQogz0Ek= filippo.io/edwards25519 v1.0.0/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns= @@ -100,8 +92,12 @@ golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d h1:3qF+Z8Hkrw9sOhrFHti9TlB1Hkac1x+DNRkv0XQiFjo= golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/mobile v0.0.0-20221110043201-43a038452099 h1:aIu0lKmfdgtn2uTj7JI2oN4TUrQvgB+wzTPO23bCKt8= +golang.org/x/mobile v0.0.0-20221110043201-43a038452099/go.mod h1:aAjjkJNdrh3PMckS4B10TGS2nag27cbKR1y2BpUxsiY= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 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= @@ -152,6 +148,8 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= +golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/spec b/spec index 1fb0330..eb6d28b 100644 --- a/spec +++ b/spec @@ -8,7 +8,7 @@ app DeactivatePeerEngine profile app CreateProfile name password bool:autostart app LoadProfiles password app DeleteProfile profile password -app ImportProfile string:file password +(json)app EnhancedImportProfile string:file password profile ChangePassword string:current string:newPassword string:newPasswordAgain profile ExportProfile string:file @@ -40,3 +40,7 @@ import "cwtch.im/cwtch/functionality/filesharing" @profile-experiment VerifyOrResumeDownload filesharing conversation string:filekey @(json)profile-experiment EnhancedShareFile filesharing conversation string:filepath @(json)profile-experiment EnhancedGetSharedFiles filesharing conversation + + +# Server Hosting Experiment +# app-experiment ServerFunctionality \ No newline at end of file diff --git a/templates/lib_template.go b/templates/lib_template.go index d8235f3..3669f23 100644 --- a/templates/lib_template.go +++ b/templates/lib_template.go @@ -8,9 +8,14 @@ import "C" import ( "cwtch.im/cwtch/event" + "cwtch.im/cwtch/model/attr" + "cwtch.im/cwtch/model/constants" "encoding/json" "fmt" + "git.openprivacy.ca/cwtch.im/cwtch-autobindings/experiments/servers" "git.openprivacy.ca/cwtch.im/cwtch-autobindings/utils" + _ "git.openprivacy.ca/cwtch.im/server" + "git.openprivacy.ca/openprivacy/connectivity/tor" "git.openprivacy.ca/openprivacy/log" "os" "os/user" @@ -18,11 +23,11 @@ import ( "runtime" "strings" "time" + mrand "math/rand" + "crypto/rand" + "encoding/base64" "unsafe" - - // Import SQL Cipher _ "github.com/mutecomm/go-sqlcipher/v4" - "cwtch.im/cwtch/app" "git.openprivacy.ca/openprivacy/connectivity" @@ -121,9 +126,9 @@ func _startCwtch(appDir string, torPath string) { } log.Infof("Creating new EventHandler()") - eventHandler = utils.NewEventHandler() + // Exclude Tapir wire Messages //(We need a TRACE level) log.ExcludeFromPattern("service.go") @@ -187,6 +192,11 @@ func _startCwtch(appDir string, torPath string) { application.GetPrimaryBus().Publish(event.NewEvent(app.CwtchStarted, map[event.Field]string{})) application.QueryACNVersion() application.LoadProfiles(app.DefactoPasswordForUnencryptedProfiles) + + serverExperiment = servers.InitServers(&globalACN, appDir) + eventHandler.AddModule(serverExperiment) + serverExperiment.Enable(application) + serverExperiment.LoadServers(application, &globalACN, app.DefactoPasswordForUnencryptedProfiles) } // the pointer returned from this function **must** be freed using c_Free @@ -255,6 +265,114 @@ func ShutdownCwtch() { } } +// TODO: At some point these functions should also be autogenerated + +// Attribute is a struct to return the dual values of an attempt at a Get*Attribute API call, meant to be json serialized +type Attribute struct { + Exists bool + Value string +} + +//export c_SetProfileAttribute +func c_SetProfileAttribute(profile_ptr *C.char, profile_len C.int, key_ptr *C.char, key_len C.int, val_ptr *C.char, val_len C.int) { + profileOnion := C.GoStringN(profile_ptr, profile_len) + key := C.GoStringN(key_ptr, key_len) + value := C.GoStringN(val_ptr, val_len) + SetProfileAttribute(profileOnion, key, value) +} + +// SetProfileAttribute provides a wrapper around profile.SetScopedZonedAttribute +// WARNING: Because this function is potentially dangerous all keys and zones must be added +// explicitly. If you are attempting to added behaviour to the UI that requires the existence of new keys +// you probably want to be building out functionality/subsystem in the UI itself. +// Key must have the format zone.key where Zone is defined in Cwtch. Unknown zones are not permitted. +func SetProfileAttribute(profileOnion string, key string, value string) { + profile := application.GetPeer(profileOnion) + if profile != nil { + zone, key := attr.ParseZone(key) + + // TODO We only allow public.profile.key to be set for now. + // All other scopes and zones need to be added explicitly or handled by Cwtch. + if zone == attr.ProfileZone && key == constants.Name { + profile.SetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants.Name, value) + } else if zone == attr.ProfileZone && key == constants.PeerAutostart { + profile.SetScopedZonedAttribute(attr.LocalScope, attr.ProfileZone, constants.PeerAutostart, value) + } else { + log.Errorf("attempted to set an attribute with an unknown zone: %v", key) + } + } +} + +//export c_GetProfileAttribute +func c_GetProfileAttribute(profile_ptr *C.char, profile_len C.int, key_ptr *C.char, key_len C.int) *C.char { + profileOnion := C.GoStringN(profile_ptr, profile_len) + key := C.GoStringN(key_ptr, key_len) + return C.CString(GetProfileAttribute(profileOnion, key)) +} + +// GetProfileAttribute provides a wrapper around profile.GetScopedZonedAttribute +// Key must have the format zone.key where Zone is defined in Cwtch. Unknown zones are not permitted. +// Currently forcing the Public Scope +// Returns json of Attribute +func GetProfileAttribute(profileOnion string, key string) string { + profile := application.GetPeer(profileOnion) + if profile != nil { + zone, key := attr.ParseZone(key) + + res, exists := profile.GetScopedZonedAttribute(attr.PublicScope, zone, key) + attr := Attribute{exists, res} + json, _ := json.Marshal(attr) + return string(json) + + } + empty := Attribute{false, ""} + json, _ := json.Marshal(empty) + return (string(json)) +} + +//export c_SetConversationAttribute +func c_SetConversationAttribute(profile_ptr *C.char, profile_len C.int, conversation_id C.int, key_ptr *C.char, key_len C.int, val_ptr *C.char, val_len C.int) { + profileOnion := C.GoStringN(profile_ptr, profile_len) + key := C.GoStringN(key_ptr, key_len) + value := C.GoStringN(val_ptr, val_len) + SetConversationAttribute(profileOnion, int(conversation_id), key, value) +} + +// SetConversationAttribute provides a wrapper around profile.SetProfileAttribute +// key is of format Zone.Key, and the API forces the Local Scope +func SetConversationAttribute(profileOnion string, conversationID int, key string, value string) { + profile := application.GetPeer(profileOnion) + zone, key := attr.ParseZone(key) + profile.SetConversationAttribute(conversationID, attr.LocalScope.ConstructScopedZonedPath(zone.ConstructZonedPath(key)), value) +} + +//export c_GetConversationAttribute +func c_GetConversationAttribute(profile_ptr *C.char, profile_len C.int, conversation_id C.int, key_ptr *C.char, key_len C.int) *C.char { + profileOnion := C.GoStringN(profile_ptr, profile_len) + key := C.GoStringN(key_ptr, key_len) + return C.CString(GetConversationAttribute(profileOnion, int(conversation_id), key)) +} + +// GetGonversationAttribute provides a wrapper around profile.GetGonversationAttribute +// key is of format Scope.Zone.Key +// Returns json of an Attribute +func GetConversationAttribute(profileOnion string, conversationID int, key string) string { + profile := application.GetPeer(profileOnion) + if profile != nil { + scope, zonekey := attr.ParseScope(key) + zone, key := attr.ParseZone(zonekey) + + res, err := profile.GetConversationAttribute(conversationID, scope.ConstructScopedZonedPath(zone.ConstructZonedPath(key))) + attr := Attribute{err == nil, res} + json, _ := json.Marshal(attr) + return string(json) + } + empty := Attribute{false, ""} + json, _ := json.Marshal(empty) + return (string(json)) +} + + //export c_ResetTor func c_ResetTor() { ResetTor() @@ -277,6 +395,93 @@ func ResetTor() { log.Infof("Restarted") } +const ( + CwtchStarted = event.Type("CwtchStarted") + CwtchStartError = event.Type("CwtchStartError") + UpdateGlobalSettings = event.Type("UpdateGlobalSettings") +) + +func buildACN(settings app.GlobalSettings, torPath string, appDir string) (connectivity.ACN, app.GlobalSettings) { + + mrand.Seed(int64(time.Now().Nanosecond())) + socksPort := mrand.Intn(1000) + 9600 + controlPort := socksPort + 1 + + // generate a random password (actually random, stored in memory, for the control port) + key := make([]byte, 64) + _, err := rand.Read(key) + if err != nil { + panic(err) + } + + log.Infof("making directory %v", appDir) + err = os.MkdirAll(path.Join(appDir, "tor"), 0700) + + if err != nil { + log.Errorf("error creating tor data directory: %v. Aborting app start up", err) + eventHandler.Push(event.NewEventList(CwtchStartError, event.Error, fmt.Sprintf("Error connecting to Tor: %v", err))) + return &connectivity.ErrorACN{}, settings + } + + if settings.AllowAdvancedTorConfig { + controlPort = settings.CustomControlPort + socksPort = settings.CustomSocksPort + } + + torrc := tor.NewTorrc().WithSocksPort(socksPort).WithOnionTrafficOnly().WithControlPort(controlPort).WithHashedPassword(base64.StdEncoding.EncodeToString(key)) + // torrc.WithLog(path.Join(appDir, "tor", "tor.log"), tor.TorLogLevelNotice) + if settings.UseCustomTorrc { + customTorrc := settings.CustomTorrc + torrc.WithCustom(strings.Split(customTorrc, "\n")) + } else { + // Fallback to showing the freshly generated torrc for this session. + settings.CustomTorrc = torrc.Preview() + settings.CustomControlPort = controlPort + settings.CustomSocksPort = socksPort + } + + err = torrc.Build(path.Join(appDir, "tor", "torrc")) + + if err != nil { + log.Errorf("error constructing torrc: %v", err) + eventHandler.Push(event.NewEventList(CwtchStartError, event.Error, fmt.Sprintf("Error connecting to Tor: %v", err))) + return &connectivity.ErrorACN{}, settings + } + + dataDir := settings.TorCacheDir + if !settings.UseTorCache { + + // purge data dir directories if we are not using them for a cache + torDir := path.Join(appDir, "tor") + files, err := path.Glob(path.Join(torDir, "data-dir-*")) + if err != nil { + log.Errorf("could not construct filesystem glob: %v", err) + } + for _, f := range files { + if err := os.RemoveAll(f); err != nil { + log.Errorf("could not remove data-dir: %v", err) + } + } + + if dataDir, err = os.MkdirTemp(torDir, "data-dir-"); err != nil { + eventHandler.Push(event.NewEventList(CwtchStartError, event.Error, fmt.Sprintf("Error connecting to Tor: %v", err))) + return &connectivity.ErrorACN{}, settings + } + } + + // Persist Current Data Dir as Tor Cache... + settings.TorCacheDir = dataDir + + acn, err := tor.NewTorACNWithAuth(appDir, torPath, dataDir, controlPort, tor.HashedPasswordAuthenticator{Password: base64.StdEncoding.EncodeToString(key)}) + if err != nil { + log.Errorf("Error connecting to Tor replacing with ErrorACN: %v\n", err) + eventHandler.Push(event.NewEventList(CwtchStartError, event.Error, fmt.Sprintf("Error connecting to Tor: %v", err))) + acn = &connectivity.ErrorACN{} + } + return acn, settings +} + + //export c_UpdateSettings func c_UpdateSettings(json_ptr *C.char, json_len C.int) { settingsJson := C.GoStringN(json_ptr, json_len) @@ -289,6 +494,92 @@ func UpdateSettings(settingsJson string) { application.UpdateSettings(newSettings) } +//***** Server APIs ***** + +var serverExperiment *servers.ServersFunctionality + +//export c_LoadServers +func c_LoadServers(passwordPtr *C.char, passwordLen C.int) { + LoadServers(C.GoStringN(passwordPtr, passwordLen)) +} + +func LoadServers(password string) { + serverExperiment.LoadServers(application, &globalACN, password) +} + +//export c_CreateServer +func c_CreateServer(passwordPtr *C.char, passwordLen C.int, descPtr *C.char, descLen C.int, autostart C.char) { + CreateServer(C.GoStringN(passwordPtr, passwordLen), C.GoStringN(descPtr, descLen), autostart == 1) +} + +func CreateServer(password string, description string, autostart bool) { + serverExperiment.CreateServer(application, password, description, autostart) +} + +//export c_DeleteServer +func c_DeleteServer(onionPtr *C.char, onionLen C.int, currentPasswordPtr *C.char, currentPasswordLen C.int) { + DeleteServer(C.GoStringN(onionPtr, onionLen), C.GoStringN(currentPasswordPtr, currentPasswordLen)) +} + +func DeleteServer(onion string, currentPassword string) { + serverExperiment.DeleteServer(application, onion, currentPassword) +} + +//export c_LaunchServers +func c_LaunchServers() { + LaunchServers() +} + +func LaunchServers() { + serverExperiment.LaunchServers(application, &globalACN) +} + +//export c_LaunchServer +func c_LaunchServer(onionPtr *C.char, onionLen C.int) { + LaunchServer(C.GoStringN(onionPtr, onionLen)) +} + +func LaunchServer(onion string) { + serverExperiment.LaunchServer(application, onion) +} + +//export c_StopServer +func c_StopServer(onionPtr *C.char, onionLen C.int) { + StopServer(C.GoStringN(onionPtr, onionLen)) +} + +func StopServer(onion string) { + serverExperiment.StopServer(application, onion) +} + +//export c_StopServers +func c_StopServers() { + StopServers() +} + +func StopServers() { + serverExperiment.StopServers(application) +} + +//export c_DestroyServers +func c_DestroyServers() { + DestroyServers() +} + +func DestroyServers() { + serverExperiment.DestroyServers() +} + +//export c_SetServerAttribute +func c_SetServerAttribute(onionPtr *C.char, onionLen C.int, keyPtr *C.char, keyLen C.int, valPtr *C.char, valLen C.int) { + SetServerAttribute(C.GoStringN(onionPtr, onionLen), C.GoStringN(keyPtr, keyLen), C.GoStringN(valPtr, valLen)) +} + +func SetServerAttribute(onion string, key string, val string) { + serverExperiment.SetServerAttribute(application, onion, key, val) +} + + {{BINDINGS}} // Leave as is, needed by ffi diff --git a/utils/eventHandler.go b/utils/eventHandler.go index a0e4f26..72fc91c 100644 --- a/utils/eventHandler.go +++ b/utils/eventHandler.go @@ -3,7 +3,7 @@ package utils import ( "encoding/json" "fmt" - "git.openprivacy.ca/cwtch.im/cwtch-autobindings/features/servers" + "git.openprivacy.ca/cwtch.im/cwtch-autobindings/experiments/servers" "os" "strconv" @@ -38,16 +38,25 @@ type EventHandler struct { app app.Application appBusQueue event.Queue profileEvents chan EventProfileEnvelope + modules []EventHandlerInterface +} + +type EventHandlerInterface interface { + OnACNStatusEvent(appl app.Application, e *event.Event) } // We should be reading from profile events pretty quickly, but we make this buffer fairly large... const profileEventsBufferSize = 512 func NewEventHandler() *EventHandler { - eh := &EventHandler{app: nil, appBusQueue: event.NewQueue(), profileEvents: make(chan EventProfileEnvelope, profileEventsBufferSize)} + eh := &EventHandler{app: nil, appBusQueue: event.NewQueue(), profileEvents: make(chan EventProfileEnvelope, profileEventsBufferSize), modules: nil} return eh } +func (eh *EventHandler) AddModule(ehi EventHandlerInterface) { + eh.modules = append(eh.modules, ehi) +} + func (eh *EventHandler) HandleApp(application app.Application) { eh.app = application application.GetPrimaryBus().Subscribe(event.NewPeer, eh.appBusQueue) @@ -119,6 +128,11 @@ func (eh *EventHandler) handleAppBusEvent(e *event.Event) string { } } acnStatus = newAcnStatus + + for _, module := range eh.modules { + module.OnACNStatusEvent(eh.app, e) + } + case event.NewPeer: onion := e.Data[event.Identity] profile := eh.app.GetPeer(e.Data[event.Identity])