cwtch/app/app.go

600 lines
19 KiB
Go
Raw Permalink Normal View History

2018-04-30 21:47:21 +00:00
package app
import (
2022-10-03 20:05:36 +00:00
"cwtch.im/cwtch/app/plugins"
"cwtch.im/cwtch/event"
2023-01-05 21:52:43 +00:00
"cwtch.im/cwtch/extensions"
"cwtch.im/cwtch/functionality/filesharing"
"cwtch.im/cwtch/functionality/servers"
2022-10-03 20:05:36 +00:00
"cwtch.im/cwtch/model"
"cwtch.im/cwtch/model/attr"
"cwtch.im/cwtch/model/constants"
"cwtch.im/cwtch/peer"
"cwtch.im/cwtch/protocol/connections"
"cwtch.im/cwtch/settings"
2022-10-03 20:05:36 +00:00
"cwtch.im/cwtch/storage"
"git.openprivacy.ca/openprivacy/connectivity"
"git.openprivacy.ca/openprivacy/log"
"os"
path "path/filepath"
"strconv"
"sync"
2018-04-30 21:47:21 +00:00
)
type application struct {
2022-10-03 20:05:36 +00:00
eventBuses map[string]event.Manager
directory string
peers map[string]peer.CwtchPeer
acn connectivity.ACN
plugins sync.Map //map[string] []plugins.Plugin
engines map[string]connections.Engine
appBus event.Manager
eventQueue event.Queue
appmutex sync.Mutex
engineHooks connections.EngineHooks
settings *settings.GlobalSettingsFile
}
func (app *application) IsFeatureEnabled(experiment string) bool {
2023-04-17 19:05:02 +00:00
globalSettings := app.ReadSettings()
if globalSettings.ExperimentsEnabled {
if status, exists := globalSettings.Experiments[experiment]; exists {
return status
}
}
return false
}
// Application is a full cwtch peer application. It allows management, usage and storage of multiple peers
type Application interface {
2022-10-03 20:05:36 +00:00
LoadProfiles(password string)
CreateProfile(name string, password string, autostart bool)
2023-04-20 20:32:36 +00:00
InstallEngineHooks(engineHooks connections.EngineHooks)
2022-10-03 20:05:36 +00:00
ImportProfile(exportedCwtchFile string, password string) (peer.CwtchPeer, error)
2023-02-27 22:05:52 +00:00
EnhancedImportProfile(exportedCwtchFile string, password string) string
DeleteProfile(onion string, currentPassword string)
2022-10-03 20:05:36 +00:00
AddPeerPlugin(onion string, pluginID plugins.PluginID)
2022-10-03 20:05:36 +00:00
GetPrimaryBus() event.Manager
GetEventBus(onion string) event.Manager
QueryACNStatus()
QueryACNVersion()
ConfigureConnections(onion string, doListn, doPeers, doServers bool)
ActivatePeerEngine(onion string)
2022-10-03 20:05:36 +00:00
DeactivatePeerEngine(onion string)
ReadSettings() settings.GlobalSettings
UpdateSettings(settings settings.GlobalSettings)
IsFeatureEnabled(experiment string) bool
2022-10-03 20:05:36 +00:00
ShutdownPeer(string)
Shutdown()
2022-10-03 20:05:36 +00:00
GetPeer(onion string) peer.CwtchPeer
ListProfiles() []string
}
// LoadProfileFn is the function signature for a function in an app that loads a profile
type LoadProfileFn func(profile peer.CwtchPeer)
func LoadAppSettings(appDirectory string) *settings.GlobalSettingsFile {
2022-10-03 20:05:36 +00:00
log.Debugf("NewApp(%v)\n", appDirectory)
os.MkdirAll(path.Join(appDirectory, "profiles"), 0700)
// Note: we basically presume this doesn't fail. If the file doesn't exist we create it, and as such the
// only plausible error conditions are related to file create e.g. low disk space. If that is the case then
// many other parts of Cwtch are likely to fail also.
2023-04-17 19:05:02 +00:00
globalSettingsFile, err := settings.InitGlobalSettingsFile(appDirectory, DefactoPasswordForUnencryptedProfiles)
if err != nil {
2023-04-17 19:33:31 +00:00
log.Errorf("error initializing global globalSettingsFile file %s. Global globalSettingsFile might not be loaded or saved", err)
}
2023-04-17 19:05:02 +00:00
return globalSettingsFile
}
// NewApp creates a new app with some environment awareness and initializes a Tor Manager
2023-04-20 20:32:36 +00:00
func NewApp(acn connectivity.ACN, appDirectory string, settings *settings.GlobalSettingsFile) Application {
app := &application{engines: make(map[string]connections.Engine), eventBuses: make(map[string]event.Manager), directory: appDirectory, appBus: event.NewEventManager(), settings: settings, eventQueue: event.NewQueue()}
2022-10-03 20:05:36 +00:00
app.peers = make(map[string]peer.CwtchPeer)
2023-04-20 20:36:43 +00:00
app.engineHooks = connections.DefaultEngineHooks{}
2022-10-03 20:05:36 +00:00
app.acn = acn
statusHandler := app.getACNStatusHandler()
acn.SetStatusCallback(statusHandler)
acn.SetVersionCallback(app.getACNVersionHandler())
prog, status := acn.GetBootstrapStatus()
statusHandler(prog, status)
app.GetPrimaryBus().Subscribe(event.ACNStatus, app.eventQueue)
go app.eventHandler()
2022-10-03 20:05:36 +00:00
return app
2018-04-30 21:47:21 +00:00
}
2023-04-20 20:32:36 +00:00
func (app *application) InstallEngineHooks(engineHooks connections.EngineHooks) {
2023-04-20 20:38:54 +00:00
app.appmutex.Lock()
defer app.appmutex.Unlock()
2023-04-20 20:32:36 +00:00
app.engineHooks = engineHooks
}
func (app *application) ReadSettings() settings.GlobalSettings {
app.appmutex.Lock()
defer app.appmutex.Unlock()
return app.settings.ReadGlobalSettings()
}
func (app *application) UpdateSettings(settings settings.GlobalSettings) {
// don't allow any other application changes while settings update
app.appmutex.Lock()
defer app.appmutex.Unlock()
app.settings.WriteGlobalSettings(settings)
for _, profile := range app.peers {
profile.UpdateExperiments(settings.ExperimentsEnabled, settings.Experiments)
2023-02-27 21:31:32 +00:00
// Explicitly toggle blocking/unblocking of unknown connections for profiles
// that have been loaded.
if settings.BlockUnknownConnections {
profile.BlockUnknownConnections()
} else {
profile.AllowUnknownConnections()
}
profile.NotifySettingsUpdate(settings)
}
}
// ListProfiles returns a map of onions to their profile's Name
func (app *application) ListProfiles() []string {
2022-10-03 20:05:36 +00:00
var keys []string
app.appmutex.Lock()
defer app.appmutex.Unlock()
2022-10-03 20:05:36 +00:00
for handle := range app.peers {
keys = append(keys, handle)
}
return keys
}
// GetPeer returns a cwtchPeer for a given onion address
func (app *application) GetPeer(onion string) peer.CwtchPeer {
2023-05-02 20:45:19 +00:00
app.appmutex.Lock()
defer app.appmutex.Unlock()
2023-04-17 19:05:02 +00:00
if profile, ok := app.peers[onion]; ok {
return profile
2022-10-03 20:05:36 +00:00
}
return nil
}
2023-04-17 19:05:02 +00:00
func (app *application) AddPlugin(peerid string, id plugins.PluginID, bus event.Manager, acn connectivity.ACN) {
if _, exists := app.plugins.Load(peerid); !exists {
app.plugins.Store(peerid, []plugins.Plugin{})
2022-10-03 20:05:36 +00:00
}
2023-04-17 19:05:02 +00:00
pluginsinf, _ := app.plugins.Load(peerid)
2022-10-03 20:05:36 +00:00
peerPlugins := pluginsinf.([]plugins.Plugin)
for _, plugin := range peerPlugins {
if plugin.Id() == id {
log.Errorf("trying to add second instance of plugin %v to peer %v", id, peerid)
return
}
}
2022-10-03 20:05:36 +00:00
newp, err := plugins.Get(id, bus, acn, peerid)
if err == nil {
newp.Start()
peerPlugins = append(peerPlugins, newp)
log.Debugf("storing plugin for %v %v", peerid, peerPlugins)
2023-04-17 19:05:02 +00:00
app.plugins.Store(peerid, peerPlugins)
2022-10-03 20:05:36 +00:00
} else {
log.Errorf("error adding plugin: %v", err)
}
}
func (app *application) CreateProfile(name string, password string, autostart bool) {
autostartVal := constants.True
if !autostart {
autostartVal = constants.False
}
tagVal := constants.ProfileTypeV1Password
if password == DefactoPasswordForUnencryptedProfiles {
tagVal = constants.ProfileTypeV1DefaultPassword
}
app.CreatePeer(name, password, map[attr.ZonedPath]string{
attr.ProfileZone.ConstructZonedPath(constants.Tag): tagVal,
attr.ProfileZone.ConstructZonedPath(constants.PeerAutostart): autostartVal,
})
}
2023-02-27 21:31:32 +00:00
func (app *application) setupPeer(profile peer.CwtchPeer) {
eventBus := event.NewEventManager()
app.eventBuses[profile.GetOnion()] = eventBus
// Initialize the Peer with the Given Event Bus
app.peers[profile.GetOnion()] = profile
profile.Init(eventBus)
2023-02-27 21:31:32 +00:00
// Update the Peer with the Most Recent Experiment State...
2023-04-17 19:05:02 +00:00
globalSettings := app.settings.ReadGlobalSettings()
profile.UpdateExperiments(globalSettings.ExperimentsEnabled, globalSettings.Experiments)
2023-02-27 21:31:32 +00:00
app.registerHooks(profile)
// Register the Peer With Application Plugins..
app.AddPeerPlugin(profile.GetOnion(), plugins.CONNECTIONRETRY) // Now Mandatory
app.AddPeerPlugin(profile.GetOnion(), plugins.HEARTBEAT) // Now Mandatory
2023-02-27 21:31:32 +00:00
}
2022-12-10 19:50:22 +00:00
func (app *application) CreatePeer(name string, password string, attributes map[attr.ZonedPath]string) {
2022-10-03 20:05:36 +00:00
app.appmutex.Lock()
defer app.appmutex.Unlock()
2022-10-03 20:05:36 +00:00
profileDirectory := path.Join(app.directory, "profiles", model.GenerateRandomID())
2022-10-03 20:05:36 +00:00
profile, err := peer.CreateEncryptedStorePeer(profileDirectory, name, password)
if err != nil {
log.Errorf("Error Creating Peer: %v", err)
app.appBus.Publish(event.NewEventList(event.PeerError, event.Error, err.Error()))
return
}
2023-02-27 21:31:32 +00:00
app.setupPeer(profile)
2022-12-10 19:50:22 +00:00
for zp, val := range attributes {
zone, key := attr.ParseZone(zp.ToString())
profile.SetScopedZonedAttribute(attr.LocalScope, zone, key, val)
2022-10-03 20:05:36 +00:00
}
2022-12-10 19:50:22 +00:00
2022-10-03 20:05:36 +00:00
app.appBus.Publish(event.NewEvent(event.NewPeer, map[event.Field]string{event.Identity: profile.GetOnion(), event.Created: event.True}))
2019-12-10 23:45:43 +00:00
}
func (app *application) DeleteProfile(onion string, password string) {
log.Debugf("DeleteProfile called on %v\n", onion)
2022-10-03 20:05:36 +00:00
app.appmutex.Lock()
defer app.appmutex.Unlock()
2019-12-10 23:45:43 +00:00
// short circuit to prevent nil-pointer panic if this function is called twice (or incorrectly)
peer := app.peers[onion]
if peer == nil {
log.Errorf("shutdownPeer called with invalid onion %v", onion)
return
}
2023-02-27 21:31:32 +00:00
// allow a blank password to delete "unencrypted" accounts...
if password == "" {
password = DefactoPasswordForUnencryptedProfiles
}
if peer.CheckPassword(password) {
2022-10-25 20:59:05 +00:00
// soft-shutdown
peer.Shutdown()
2022-10-25 20:59:05 +00:00
// delete the underlying storage
peer.Delete()
2022-10-25 20:59:05 +00:00
// hard shutdown / remove from app
app.shutdownPeer(onion)
2022-10-03 20:05:36 +00:00
// Shutdown and Remove the Engine
log.Debugf("Delete peer for %v Done\n", onion)
app.appBus.Publish(event.NewEventList(event.PeerDeleted, event.Identity, onion))
return
}
app.appBus.Publish(event.NewEventList(event.AppError, event.Error, event.PasswordMatchError, event.Identity, onion))
2019-12-10 23:45:43 +00:00
}
func (app *application) AddPeerPlugin(onion string, pluginID plugins.PluginID) {
2022-10-03 20:05:36 +00:00
app.AddPlugin(onion, pluginID, app.eventBuses[onion], app.acn)
}
2022-03-08 21:45:26 +00:00
func (app *application) ImportProfile(exportedCwtchFile string, password string) (peer.CwtchPeer, error) {
2022-10-03 20:05:36 +00:00
profileDirectory := path.Join(app.directory, "profiles")
profile, err := peer.ImportProfile(exportedCwtchFile, profileDirectory, password)
if profile != nil || err == nil {
app.installProfile(profile)
}
return profile, err
2022-03-08 21:45:26 +00:00
}
2023-02-27 22:05:52 +00:00
func (app *application) EnhancedImportProfile(exportedCwtchFile string, password string) string {
_, err := app.ImportProfile(exportedCwtchFile, password)
if err == nil {
return ""
}
return err.Error()
}
// LoadProfiles takes a password and attempts to load any profiles it can from storage with it and create Peers for them
func (app *application) LoadProfiles(password string) {
2022-10-03 20:05:36 +00:00
count := 0
migrating := false
files, err := os.ReadDir(path.Join(app.directory, "profiles"))
if err != nil {
log.Errorf("error: cannot read profiles directory: %v", err)
return
}
for _, file := range files {
// Attempt to load an encrypted database
profileDirectory := path.Join(app.directory, "profiles", file.Name())
profile, err := peer.FromEncryptedDatabase(profileDirectory, password)
loaded := false
if err == nil {
// return the load the profile...
log.Infof("loading profile from new-type storage database...")
loaded = app.installProfile(profile)
} else { // On failure attempt to load a legacy profile
profileStore, err := storage.LoadProfileWriterStore(profileDirectory, password)
if err != nil {
continue
}
log.Infof("found legacy profile. importing to new database structure...")
legacyProfile := profileStore.GetProfileCopy(true)
if !migrating {
migrating = true
app.appBus.Publish(event.NewEventList(event.StartingStorageMiragtion))
}
cps, err := peer.CreateEncryptedStore(profileDirectory, password)
if err != nil {
log.Errorf("error creating encrypted store: %v", err)
continue
2022-10-03 20:05:36 +00:00
}
profile := peer.ImportLegacyProfile(legacyProfile, cps)
loaded = app.installProfile(profile)
}
if loaded {
count++
}
}
if count == 0 {
message := event.NewEventList(event.AppError, event.Error, event.AppErrLoaded0)
app.appBus.Publish(message)
}
if migrating {
app.appBus.Publish(event.NewEventList(event.DoneStorageMigration))
}
}
2023-01-05 21:52:43 +00:00
func (app *application) registerHooks(profile peer.CwtchPeer) {
// Register Hooks
profile.RegisterHook(extensions.ProfileValueExtension{})
2024-02-26 21:16:31 +00:00
profile.RegisterHook(extensions.SendWhenOnlineExtension{})
2023-04-17 19:05:02 +00:00
profile.RegisterHook(new(filesharing.Functionality))
profile.RegisterHook(new(filesharing.ImagePreviewsFunctionality))
profile.RegisterHook(new(servers.Functionality))
// Ensure that Profiles have the Most Up to Date Settings...
profile.NotifySettingsUpdate(app.settings.ReadGlobalSettings())
2023-01-05 21:52:43 +00:00
}
// installProfile takes a profile and if it isn't loaded in the app, installs it and returns true
func (app *application) installProfile(profile peer.CwtchPeer) bool {
2022-10-03 20:05:36 +00:00
app.appmutex.Lock()
defer app.appmutex.Unlock()
// Only attempt to finalize the profile if we don't have one loaded...
if app.peers[profile.GetOnion()] == nil {
2023-02-27 21:31:32 +00:00
app.setupPeer(profile)
2023-02-27 20:07:19 +00:00
// Finalize the Creation of Peer / Notify any Interfaces..
2022-10-03 20:05:36 +00:00
app.appBus.Publish(event.NewEvent(event.NewPeer, map[event.Field]string{event.Identity: profile.GetOnion(), event.Created: event.False}))
return true
}
// Otherwise shutdown the connections
profile.Shutdown()
return false
}
2023-04-17 19:05:02 +00:00
// ActivatePeerEngine creates a peer engine for use with an ACN, should be called once the underlying ACN is online
func (app *application) ActivatePeerEngine(onion string) {
2022-10-03 20:05:36 +00:00
profile := app.GetPeer(onion)
if profile != nil {
2022-12-05 04:08:41 +00:00
if _, exists := app.engines[onion]; !exists {
eventBus, exists := app.eventBuses[profile.GetOnion()]
if !exists {
// todo handle this case?
log.Errorf("cannot activate peer engine without an event bus")
return
}
engine, err := profile.GenerateProtocolEngine(app.acn, eventBus, app.engineHooks)
if err == nil {
log.Debugf("restartFlow: Creating a New Protocol Engine...")
app.engines[profile.GetOnion()] = engine
eventBus.Publish(event.NewEventList(event.ProtocolEngineCreated))
app.QueryACNStatus()
} else {
log.Errorf("corrupted profile detected for %v", onion)
}
2022-10-03 20:05:36 +00:00
}
}
}
// ConfigureConnections autostarts the given kinds of connections.
func (app *application) ConfigureConnections(onion string, listen bool, peers bool, servers bool) {
profile := app.GetPeer(onion)
if profile != nil {
2023-09-19 22:37:26 +00:00
profileBus, exists := app.eventBuses[profile.GetOnion()]
if exists {
// if we are making a decision to ignore
if !peers || !servers {
profileBus.Publish(event.NewEventList(event.PurgeRetries))
}
2023-09-19 22:37:26 +00:00
// enable the engine if it doesn't exist...
// note: this function is idempotent
app.ActivatePeerEngine(onion)
if listen {
profile.Listen()
}
profileBus.Publish(event.NewEventList(event.ResumeRetries))
// do this in the background, for large contact lists it can take a long time...
go profile.StartConnections(peers, servers)
}
2023-09-26 20:04:30 +00:00
} else {
log.Errorf("profile does not exist %v", onion)
}
}
2022-09-10 17:36:28 +00:00
// DeactivatePeerEngine shutsdown and cleans up a peer engine, should be called when an underlying ACN goes offline
func (app *application) DeactivatePeerEngine(onion string) {
2022-10-03 20:05:36 +00:00
if engine, exists := app.engines[onion]; exists {
engine.Shutdown()
delete(app.engines, onion)
}
}
// GetPrimaryBus returns the bus the Application uses for events that aren't peer specific
func (app *application) GetPrimaryBus() event.Manager {
2022-10-03 20:05:36 +00:00
return app.appBus
2018-04-30 21:47:21 +00:00
}
// GetEventBus returns a cwtchPeer's event bus
func (app *application) GetEventBus(onion string) event.Manager {
2022-10-03 20:05:36 +00:00
if manager, ok := app.eventBuses[onion]; ok {
return manager
}
return nil
}
2018-10-05 23:27:57 +00:00
func (app *application) getACNStatusHandler() func(int, string) {
2022-10-03 20:05:36 +00:00
return func(progress int, status string) {
progStr := strconv.Itoa(progress)
app.appmutex.Lock()
app.appBus.Publish(event.NewEventList(event.ACNStatus, event.Progress, progStr, event.Status, status))
for _, bus := range app.eventBuses {
bus.Publish(event.NewEventList(event.ACNStatus, event.Progress, progStr, event.Status, status))
}
app.appmutex.Unlock()
}
}
func (app *application) getACNVersionHandler() func(string) {
2022-10-03 20:05:36 +00:00
return func(version string) {
app.appmutex.Lock()
defer app.appmutex.Unlock()
app.appBus.Publish(event.NewEventList(event.ACNVersion, event.Data, version))
}
}
func (app *application) QueryACNStatus() {
2022-10-03 20:05:36 +00:00
prog, status := app.acn.GetBootstrapStatus()
app.getACNStatusHandler()(prog, status)
}
2020-12-01 03:25:17 +00:00
func (app *application) QueryACNVersion() {
2022-10-03 20:05:36 +00:00
version := app.acn.GetVersion()
app.appBus.Publish(event.NewEventList(event.ACNVersion, event.Data, version))
2020-12-01 03:25:17 +00:00
}
func (app *application) eventHandler() {
acnStatus := -1
for {
e := app.eventQueue.Next()
switch e.EventType {
case event.ACNStatus:
newAcnStatus, err := strconv.Atoi(e.Data[event.Progress])
if err != nil {
break
}
if newAcnStatus == 100 {
if acnStatus != 100 {
for _, onion := range app.ListProfiles() {
profile := app.GetPeer(onion)
if profile != nil {
autostart, exists := profile.GetScopedZonedAttribute(attr.LocalScope, attr.ProfileZone, constants.PeerAutostart)
appearOffline, appearOfflineExists := profile.GetScopedZonedAttribute(attr.LocalScope, attr.ProfileZone, constants.PeerAppearOffline)
if !exists || autostart == "true" {
if appearOfflineExists && appearOffline == "true" {
// don't configure any connections...
log.Infof("peer appearing offline, not launching listen threads or connecting jobs")
app.ConfigureConnections(onion, false, false, false)
} else {
app.ConfigureConnections(onion, true, true, true)
}
}
}
}
}
} else {
if acnStatus == 100 {
// just fell offline
for _, onion := range app.ListProfiles() {
app.DeactivatePeerEngine(onion)
}
}
}
acnStatus = newAcnStatus
default:
// invalid event, signifies shutdown
if e.EventType == "" {
return
}
}
}
}
// ShutdownPeer shuts down a peer and removes it from the app's management
func (app *application) ShutdownPeer(onion string) {
2022-10-03 20:05:36 +00:00
app.appmutex.Lock()
defer app.appmutex.Unlock()
app.shutdownPeer(onion)
}
2022-09-10 17:36:28 +00:00
// shutdownPeer mutex unlocked helper shutdown peer
//
//nolint:nilaway
func (app *application) shutdownPeer(onion string) {
// short circuit to prevent nil-pointer panic if this function is called twice (or incorrectly)
onionEventBus := app.eventBuses[onion]
onionPeer := app.peers[onion]
if onionEventBus == nil || onionPeer == nil {
log.Errorf("shutdownPeer called with invalid onion %v", onion)
return
}
// we are an internal locked method, app.eventBuses[onion] cannot fail...
onionEventBus.Publish(event.NewEventList(event.ShutdownPeer, event.Identity, onion))
onionEventBus.Shutdown()
2022-10-03 20:05:36 +00:00
delete(app.eventBuses, onion)
onionPeer.Shutdown()
2022-10-03 20:05:36 +00:00
delete(app.peers, onion)
if onionEngine, ok := app.engines[onion]; ok {
onionEngine.Shutdown()
2022-10-03 20:05:36 +00:00
delete(app.engines, onion)
}
log.Debugf("shutting down plugins for %v", onion)
pluginsI, ok := app.plugins.Load(onion)
if ok {
2023-04-17 19:05:02 +00:00
appPlugins := pluginsI.([]plugins.Plugin)
for _, plugin := range appPlugins {
2022-10-03 20:05:36 +00:00
plugin.Shutdown()
}
}
app.plugins.Delete(onion)
}
// Shutdown shutsdown all peers of an app
func (app *application) Shutdown() {
2022-10-03 20:05:36 +00:00
app.appmutex.Lock()
defer app.appmutex.Unlock()
for id := range app.peers {
log.Debugf("Shutting Down Peer %v", id)
app.shutdownPeer(id)
}
log.Debugf("Shutting Down App")
app.eventQueue.Shutdown()
2022-10-03 20:05:36 +00:00
app.appBus.Shutdown()
log.Debugf("Shut Down Complete")
2018-04-30 21:47:21 +00:00
}