diff --git a/app/app.go b/app/app.go index a1231b8..32b5e33 100644 --- a/app/app.go +++ b/app/app.go @@ -31,6 +31,8 @@ type application struct { engines map[string]connections.Engine appBus event.Manager appmutex sync.Mutex + + settings *GlobalSettingsFile } // Application is a full cwtch peer application. It allows management, usage and storage of multiple peers @@ -52,6 +54,9 @@ type Application interface { ActivatePeerEngine(onion string, doListen, doPeers, doServers bool) DeactivatePeerEngine(onion string) + ReadSettings() GlobalSettings + UpdateSettings(settings GlobalSettings) + ShutdownPeer(string) Shutdown() @@ -67,7 +72,15 @@ func NewApp(acn connectivity.ACN, appDirectory string) Application { log.Debugf("NewApp(%v)\n", appDirectory) os.MkdirAll(path.Join(appDirectory, "profiles"), 0700) - app := &application{engines: make(map[string]connections.Engine), eventBuses: make(map[string]event.Manager), directory: appDirectory, appBus: event.NewEventManager()} + // 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. + settings, err := InitGlobalSettingsFile(appDirectory, DefactoPasswordForUnencryptedProfiles) + if err != nil { + log.Errorf("error initializing global settings file %. Global settings might not be loaded or saves", err) + } + + app := &application{engines: make(map[string]connections.Engine), eventBuses: make(map[string]event.Manager), directory: appDirectory, appBus: event.NewEventManager(), settings: settings} app.peers = make(map[string]peer.CwtchPeer) app.acn = acn @@ -80,6 +93,26 @@ func NewApp(acn connectivity.ACN, appDirectory string) Application { return app } +func (app *application) ReadSettings() GlobalSettings { + app.appmutex.Lock() + defer app.appmutex.Unlock() + return app.settings.ReadGlobalSettings() +} + +func (app *application) UpdateSettings(settings GlobalSettings) { + // don't allow any other application changes while settings update + app.appmutex.Lock() + defer app.appmutex.Unlock() + app.settings.WriteGlobalSettings(settings) + + // we now need to propagate changes to all peers + app.peerLock.Lock() + defer app.peerLock.Unlock() + for _, profile := range app.peers { + profile.UpdateExperiments(settings.ExperimentsEnabled, settings.Experiments) + } +} + // ListProfiles returns a map of onions to their profile's Name func (app *application) ListProfiles() []string { var keys []string @@ -160,7 +193,7 @@ func (app *application) CreatePeer(name string, password string, attributes map[ } func (app *application) DeletePeer(onion string, password string) { - log.Infof("DeletePeer called on %v\n", onion) + log.Debugf("DeletePeer called on %v\n", onion) app.appmutex.Lock() defer app.appmutex.Unlock() @@ -249,7 +282,6 @@ func (app *application) registerHooks(profile peer.CwtchPeer) { // Register Hooks profile.RegisterHook(extensions.ProfileValueExtension{}) profile.RegisterHook(filesharing.Functionality{}) - } // installProfile takes a profile and if it isn't loaded in the app, installs it and returns true diff --git a/app/app_constants.go b/app/app_constants.go new file mode 100644 index 0000000..f564305 --- /dev/null +++ b/app/app_constants.go @@ -0,0 +1,6 @@ +package app + +// We offer "un-passworded" profiles but our storage encrypts everything with a password. We need an agreed upon +// password to use in that case, that the app case use behind the scenes to password and unlock with +// https://docs.openprivacy.ca/cwtch-security-handbook/profile_encryption_and_storage.html +const DefactoPasswordForUnencryptedProfiles = "be gay do crime" diff --git a/app/plugins/antispam.go b/app/plugins/antispam.go index 6fb1195..c2f309b 100644 --- a/app/plugins/antispam.go +++ b/app/plugins/antispam.go @@ -27,7 +27,7 @@ func (a antispam) Shutdown() { } func (a *antispam) run() { - log.Infof("running antispam trigger plugin") + log.Debugf("running antispam trigger plugin") for { select { case <-time.After(antispamTickTime): diff --git a/app/settings.go b/app/settings.go new file mode 100644 index 0000000..baf7f13 --- /dev/null +++ b/app/settings.go @@ -0,0 +1,146 @@ +package app + +import ( + "cwtch.im/cwtch/event" + "cwtch.im/cwtch/model/constants" + "cwtch.im/cwtch/storage/v1" + "encoding/json" + "git.openprivacy.ca/openprivacy/log" + "os" + path "path/filepath" +) + +const ( + CwtchStarted = event.Type("CwtchStarted") + CwtchStartError = event.Type("CwtchStartError") + UpdateGlobalSettings = event.Type("UpdateGlobalSettings") +) + +const GlobalSettingsFilename = "ui.globals" +const saltFile = "SALT" + +type NotificationPolicy string + +const ( + NotificationPolicyMute = NotificationPolicy("NotificationPolicy.Mute") + NotificationPolicyOptIn = NotificationPolicy("NotificationPolicy.OptIn") + NotificationPolicyDefaultAll = NotificationPolicy("NotificationPolicy.DefaultAll") +) + +type GlobalSettingsFile struct { + v1.FileStore +} + +type GlobalSettings struct { + Locale string + Theme string + ThemeMode string + PreviousPid int64 + ExperimentsEnabled bool + Experiments map[string]bool + BlockUnknownConnections bool + NotificationPolicy NotificationPolicy + NotificationContent string + StreamerMode bool + StateRootPane int + FirstTime bool + UIColumnModePortrait string + UIColumnModeLandscape string + DownloadPath string + AllowAdvancedTorConfig bool + CustomTorrc string + UseCustomTorrc bool + UseExternalTor bool + CustomSocksPort int + CustomControlPort int + UseTorCache bool + TorCacheDir string +} + +var DefaultGlobalSettings = GlobalSettings{ + Locale: "en", + Theme: "dark", + PreviousPid: -1, + ExperimentsEnabled: false, + Experiments: map[string]bool{constants.MessageFormattingExperiment: true}, + StateRootPane: 0, + FirstTime: true, + BlockUnknownConnections: false, + StreamerMode: false, + UIColumnModePortrait: "DualpaneMode.Single", + UIColumnModeLandscape: "DualpaneMode.CopyPortrait", + NotificationPolicy: "NotificationPolicy.Mute", + NotificationContent: "NotificationContent.SimpleEvent", + DownloadPath: "", + AllowAdvancedTorConfig: false, + CustomTorrc: "", + UseCustomTorrc: false, + CustomSocksPort: -1, + CustomControlPort: -1, + UseTorCache: false, + TorCacheDir: "", +} + +func InitGlobalSettingsFile(directory string, password string) (*GlobalSettingsFile, error) { + var key [32]byte + salt, err := os.ReadFile(path.Join(directory, saltFile)) + if err != nil { + log.Infof("Could not find salt file: %v (creating a new settings file)", err) + var newSalt [128]byte + key, newSalt, err = v1.CreateKeySalt(password) + if err != nil { + log.Errorf("Could not initialize salt: %v", err) + return nil, err + } + os.Mkdir(directory, 0700) + err := os.WriteFile(path.Join(directory, saltFile), newSalt[:], 0600) + if err != nil { + log.Errorf("Could not write salt file: %v", err) + return nil, err + } + } else { + key = v1.CreateKey(password, salt) + } + + gsFile := v1.NewFileStore(directory, GlobalSettingsFilename, key) + log.Infof("initialized global settings file: %v", gsFile) + globalSettingsFile := GlobalSettingsFile{ + gsFile, + } + return &globalSettingsFile, nil +} + +func (globalSettingsFile *GlobalSettingsFile) ReadGlobalSettings() GlobalSettings { + settings := DefaultGlobalSettings + + if globalSettingsFile == nil { + log.Errorf("Global Settings File was not Initialized Properly") + return settings + } + + settingsBytes, err := globalSettingsFile.Read() + if err != nil { + log.Infof("Could not read global ui settings: %v (assuming this is a first time app deployment...)", err) + return settings //firstTime = true + } + + err = json.Unmarshal(settingsBytes, &settings) + if err != nil { + log.Errorf("Could not parse global ui settings: %v\n", err) + // TODO if settings is corrupted, we probably want to alert the UI. + return settings //firstTime = true + } + + log.Debugf("Settings: %#v", settings) + return settings +} + +func (globalSettingsFile *GlobalSettingsFile) WriteGlobalSettings(globalSettings GlobalSettings) { + bytes, _ := json.Marshal(globalSettings) + // override first time setting + globalSettings.FirstTime = true + err := globalSettingsFile.Write(bytes) + if err != nil { + log.Errorf("Could not write global ui settings: %v\n", err) + } +} diff --git a/extensions/profile_value.go b/extensions/profile_value.go index 86f19fd..1a5327f 100644 --- a/extensions/profile_value.go +++ b/extensions/profile_value.go @@ -41,7 +41,7 @@ func (pne ProfileValueExtension) OnContactReceiveValue(profile peer.CwtchPeer, c // OnContactRequestValue for ProfileValueExtension handles returning Public Profile Values func (pne ProfileValueExtension) OnContactRequestValue(profile peer.CwtchPeer, conversation model.Conversation, eventID string, szp attr.ScopedZonedPath) { scope, zone, zpath := szp.GetScopeZonePath() - log.Infof("Looking up public | conversation scope/zone %v", szp.ToString()) + log.Debugf("Looking up public | conversation scope/zone %v", szp.ToString()) if scope.IsPublic() || scope.IsConversation() { val, exists := profile.GetScopedZonedAttribute(scope, zone, zpath) diff --git a/functionality/filesharing/filesharing_functionality.go b/functionality/filesharing/filesharing_functionality.go index 9e1ddc8..1f6625a 100644 --- a/functionality/filesharing/filesharing_functionality.go +++ b/functionality/filesharing/filesharing_functionality.go @@ -40,66 +40,70 @@ func (f Functionality) RegisterExperiments() []string { // OnEvent handles File Sharing Hooks like Manifest Received and FileDownloaded func (f Functionality) OnEvent(ev event.Event, profile peer.CwtchPeer) { - switch ev.EventType { - case event.ManifestReceived: - log.Debugf("Manifest Received Event!: %v", ev) - handle := ev.Data[event.Handle] - fileKey := ev.Data[event.FileKey] - serializedManifest := ev.Data[event.SerializedManifest] + if profile.IsFeatureEnabled(constants.FileSharingExperiment) { + switch ev.EventType { + case event.ManifestReceived: + log.Debugf("Manifest Received Event!: %v", ev) + handle := ev.Data[event.Handle] + fileKey := ev.Data[event.FileKey] + serializedManifest := ev.Data[event.SerializedManifest] - manifestFilePath, exists := profile.GetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%v.manifest", fileKey)) - if exists { - downloadFilePath, exists := profile.GetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%v.path", fileKey)) + manifestFilePath, exists := profile.GetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%v.manifest", fileKey)) if exists { - log.Debugf("downloading manifest to %v, file to %v", manifestFilePath, downloadFilePath) - var manifest files.Manifest - err := json.Unmarshal([]byte(serializedManifest), &manifest) + downloadFilePath, exists := profile.GetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%v.path", fileKey)) + if exists { + log.Debugf("downloading manifest to %v, file to %v", manifestFilePath, downloadFilePath) + var manifest files.Manifest + err := json.Unmarshal([]byte(serializedManifest), &manifest) - if err == nil { - // We only need to check the file size here, as manifest is sent to engine and the file created - // will be bound to the size advertised in manifest. - fileSizeLimitValue, fileSizeLimitExists := profile.GetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%v.limit", fileKey)) - if fileSizeLimitExists { - fileSizeLimit, err := strconv.ParseUint(fileSizeLimitValue, 10, bits.UintSize) - if err == nil { - if manifest.FileSizeInBytes >= fileSizeLimit { - log.Errorf("could not download file, size %v greater than limit %v", manifest.FileSizeInBytes, fileSizeLimitValue) - } else { - manifest.Title = manifest.FileName - manifest.FileName = downloadFilePath - log.Debugf("saving manifest") - err = manifest.Save(manifestFilePath) - if err != nil { - log.Errorf("could not save manifest: %v", err) + if err == nil { + // We only need to check the file size here, as manifest is sent to engine and the file created + // will be bound to the size advertised in manifest. + fileSizeLimitValue, fileSizeLimitExists := profile.GetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%v.limit", fileKey)) + if fileSizeLimitExists { + fileSizeLimit, err := strconv.ParseUint(fileSizeLimitValue, 10, bits.UintSize) + if err == nil { + if manifest.FileSizeInBytes >= fileSizeLimit { + log.Errorf("could not download file, size %v greater than limit %v", manifest.FileSizeInBytes, fileSizeLimitValue) } else { - tempFile := "" - if runtime.GOOS == "android" { - tempFile = manifestFilePath[0 : len(manifestFilePath)-len(".manifest")] - log.Debugf("derived android temp path: %v", tempFile) + manifest.Title = manifest.FileName + manifest.FileName = downloadFilePath + log.Debugf("saving manifest") + err = manifest.Save(manifestFilePath) + if err != nil { + log.Errorf("could not save manifest: %v", err) + } else { + tempFile := "" + if runtime.GOOS == "android" { + tempFile = manifestFilePath[0 : len(manifestFilePath)-len(".manifest")] + log.Debugf("derived android temp path: %v", tempFile) + } + profile.PublishEvent(event.NewEvent(event.ManifestSaved, map[event.Field]string{ + event.FileKey: fileKey, + event.Handle: handle, + event.SerializedManifest: string(manifest.Serialize()), + event.TempFile: tempFile, + event.NameSuggestion: manifest.Title, + })) } - profile.PublishEvent(event.NewEvent(event.ManifestSaved, map[event.Field]string{ - event.FileKey: fileKey, - event.Handle: handle, - event.SerializedManifest: string(manifest.Serialize()), - event.TempFile: tempFile, - event.NameSuggestion: manifest.Title, - })) } } } + } else { + log.Errorf("error saving manifest: %v", err) } } else { - log.Errorf("error saving manifest: %v", err) + log.Errorf("found manifest path but not download path for %v", fileKey) } } else { - log.Errorf("found manifest path but not download path for %v", fileKey) + log.Errorf("no download path found for manifest: %v", fileKey) } - } else { - log.Errorf("no download path found for manifest: %v", fileKey) + case event.FileDownloaded: + fileKey := ev.Data[event.FileKey] + profile.SetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.complete", fileKey), "true") } - case event.FileDownloaded: - fileKey := ev.Data[event.FileKey] - profile.SetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.complete", fileKey), "true") + } else { + log.Errorf("profile called filesharing experiment OnContactReceiveValue even though file sharing was not enabled. This is likely a programming error.") } } @@ -108,21 +112,25 @@ func (f Functionality) OnContactRequestValue(profile peer.CwtchPeer, conversatio } func (f Functionality) OnContactReceiveValue(profile peer.CwtchPeer, conversation model.Conversation, path attr.ScopedZonedPath, value string, exists bool) { - scope, zone, zpath := path.GetScopeZonePath() - log.Infof("file sharing contact receive value") - if exists && scope.IsConversation() && zone == attr.FilesharingZone && strings.HasSuffix(zpath, ".manifest.size") { - fileKey := strings.Replace(zpath, ".manifest.size", "", 1) - size, err := strconv.Atoi(value) - // if size is valid and below the maximum size for a manifest - // this is to prevent malicious sharers from using large amounts of memory when distributing - // a manifest as we reconstruct this in-memory - if err == nil && size < files.MaxManifestSize { - profile.PublishEvent(event.NewEvent(event.ManifestSizeReceived, map[event.Field]string{event.FileKey: fileKey, event.ManifestSize: value, event.Handle: conversation.Handle})) - } else { - profile.PublishEvent(event.NewEvent(event.ManifestError, map[event.Field]string{event.FileKey: fileKey, event.Handle: conversation.Handle})) + // Profile should not call us if FileSharing is disabled + if profile.IsFeatureEnabled(constants.FileSharingExperiment) { + scope, zone, zpath := path.GetScopeZonePath() + log.Debugf("file sharing contact receive value") + if exists && scope.IsConversation() && zone == attr.FilesharingZone && strings.HasSuffix(zpath, ".manifest.size") { + fileKey := strings.Replace(zpath, ".manifest.size", "", 1) + size, err := strconv.Atoi(value) + // if size is valid and below the maximum size for a manifest + // this is to prevent malicious sharers from using large amounts of memory when distributing + // a manifest as we reconstruct this in-memory + if err == nil && size < files.MaxManifestSize { + profile.PublishEvent(event.NewEvent(event.ManifestSizeReceived, map[event.Field]string{event.FileKey: fileKey, event.ManifestSize: value, event.Handle: conversation.Handle})) + } else { + profile.PublishEvent(event.NewEvent(event.ManifestError, map[event.Field]string{event.FileKey: fileKey, event.Handle: conversation.Handle})) + } } + } else { + log.Errorf("profile called filesharing experiment OnContactReceiveValue even though file sharing was not enabled. This is likely a programming error.") } - } // FunctionalityGate returns filesharing if enabled in the given experiment map @@ -174,6 +182,11 @@ func (om *OverlayMessage) ShouldAutoDL() bool { // to downloadFilePath func (f *Functionality) DownloadFile(profile peer.CwtchPeer, conversationID int, downloadFilePath string, manifestFilePath string, key string, limit uint64) error { + // assert that we are allowed to download the file + if !profile.IsFeatureEnabled(constants.FileSharingExperiment) { + return errors.New("filesharing functionality is not enabled") + } + // Don't download files if the download or manifest path is not set if downloadFilePath == "" || manifestFilePath == "" { return errors.New("download path or manifest path is empty") @@ -225,6 +238,12 @@ func (f *Functionality) startFileShare(profile peer.CwtchPeer, filekey string, m // RestartFileShare takes in an existing filekey and, assuming the manifest exists, restarts sharing of the manifest func (f *Functionality) RestartFileShare(profile peer.CwtchPeer, filekey string) error { + + // assert that we are allowed to restart filesharing + if !profile.IsFeatureEnabled(constants.FileSharingExperiment) { + return errors.New("filesharing functionality is not enabled") + } + // check that a manifest exists manifest, manifestExists := profile.GetScopedZonedAttribute(attr.ConversationScope, attr.FilesharingZone, fmt.Sprintf("%s.manifest", filekey)) if manifestExists { @@ -238,6 +257,12 @@ func (f *Functionality) RestartFileShare(profile peer.CwtchPeer, filekey string) // ReShareFiles given a profile we iterate through all existing fileshares and re-share them // if the time limit has not expired func (f *Functionality) ReShareFiles(profile peer.CwtchPeer) error { + + // assert that we are allowed to restart filesharing + if !profile.IsFeatureEnabled(constants.FileSharingExperiment) { + return errors.New("filesharing functionality is not enabled") + } + keys, err := profile.GetScopedZonedAttributeKeys(attr.LocalScope, attr.FilesharingZone) if err != nil { return err @@ -304,6 +329,12 @@ func (f *Functionality) GetFileShareInfo(profile peer.CwtchPeer, filekey string) // ShareFile given a profile and a conversation handle, sets up a file sharing process to share the file // at filepath func (f *Functionality) ShareFile(filepath string, profile peer.CwtchPeer) (string, string, error) { + + // assert that we are allowed to share files + if !profile.IsFeatureEnabled(constants.FileSharingExperiment) { + return "", "", errors.New("filesharing functionality is not enabled") + } + manifest, err := files.CreateManifest(filepath) if err != nil { return "", "", err @@ -446,6 +477,7 @@ func GenerateDownloadPath(basePath, fileName string, overwrite bool) (filePath, // StopFileShare sends a message to the ProtocolEngine to cease sharing a particular file func (f *Functionality) StopFileShare(profile peer.CwtchPeer, fileKey string) { + // Note we do not do a permissions check here, as we are *always* permitted to stop sharing files. // set the filekey status to inactive profile.SetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.active", fileKey), constants.False) profile.PublishEvent(event.NewEvent(event.StopFileShare, map[event.Field]string{event.FileKey: fileKey})) @@ -453,5 +485,6 @@ func (f *Functionality) StopFileShare(profile peer.CwtchPeer, fileKey string) { // StopAllFileShares sends a message to the ProtocolEngine to cease sharing all files func (f *Functionality) StopAllFileShares(profile peer.CwtchPeer) { + // Note we do not do a permissions check here, as we are *always* permitted to stop sharing files. profile.PublishEvent(event.NewEvent(event.StopAllFileShares, map[event.Field]string{})) } diff --git a/model/constants/experiments.go b/model/constants/experiments.go index 665a856..95486ff 100644 --- a/model/constants/experiments.go +++ b/model/constants/experiments.go @@ -10,5 +10,7 @@ const ImagePreviewsExperiment = "filesharing-images" // ImagePreviewMaxSizeInBytes Files up to this size will be autodownloaded using ImagePreviewsExperiment const ImagePreviewMaxSizeInBytes = 20971520 +const MessageFormattingExperiment = "message-formatting" + // AutoDLFileExts Files with these extensions will be autodownloaded using ImagePreviewsExperiment var AutoDLFileExts = [...]string{".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp"} diff --git a/model/experiments.go b/model/experiments.go new file mode 100644 index 0000000..36ca753 --- /dev/null +++ b/model/experiments.go @@ -0,0 +1,32 @@ +package model + +// Experiments are optional functionality that can be enabled/disabled by an application either completely or individually. +// examples of experiments include File Sharing, Profile Images and Groups. +type Experiments struct { + enabled bool + experiments map[string]bool +} + +// InitExperiments encapsulates a set of experiments separate from their storage in GlobalSettings. +func InitExperiments(enabled bool, experiments map[string]bool) Experiments { + return Experiments{ + enabled: enabled, + experiments: experiments, + } +} + +// IsEnabled is a convenience function that takes in an experiment and returns true if it is enabled. Experiments +// are only enabled if both global experiments are turned on and if the specific experiment is also turned on. +// The one exception to this is experiments that have been promoted to default functionality which may be turned on +// even if experiments turned off globally. These experiments are defined by DefaultEnabledFunctionality. +func (e *Experiments) IsEnabled(experiment string) bool { + if !e.enabled { + // todo handle default-enabled functionality + return false + } + enabled, exists := e.experiments[experiment] + if !exists { + return false + } + return enabled +} diff --git a/peer/cwtch_peer.go b/peer/cwtch_peer.go index 22dc43b..e62c2bd 100644 --- a/peer/cwtch_peer.go +++ b/peer/cwtch_peer.go @@ -69,11 +69,30 @@ type cwtchPeer struct { queue event.Queue eventBus event.Manager - extensions []ProfileHook - extensionLock sync.Mutex // we don't want to hold up all of cwtch for managing thread safe access to extensions + extensions []ProfileHook + extensionLock sync.Mutex // we don't want to hold up all of cwtch for managing thread safe access to extensions + experiments model.Experiments + experimentsLock sync.Mutex +} + +// IsFeatureEnabled returns true if the functionality defined by featureName has been enabled by the application, false otherwise. +// this function is intended to be used by ProfileHooks to determine if they should execute experimental functionality. +func (cp *cwtchPeer) IsFeatureEnabled(featureName string) bool { + cp.experimentsLock.Lock() + defer cp.experimentsLock.Unlock() + return cp.experiments.IsEnabled(featureName) +} + +// UpdateExperiments notifies a Cwtch profile of a change in the nature of global experiments. The Cwtch Profile uses +// this information to update registered extensions. +func (cp *cwtchPeer) UpdateExperiments(enabled bool, experiments map[string]bool) { + cp.experimentsLock.Lock() + defer cp.experimentsLock.Unlock() + cp.experiments = model.InitExperiments(enabled, experiments) } func (cp *cwtchPeer) PublishEvent(resp event.Event) { + log.Debugf("Publishing Event: %v %v", resp.EventType, resp.Data) cp.eventBus.Publish(resp) } @@ -154,7 +173,7 @@ func (cp *cwtchPeer) ChangePassword(password string, newpassword string, newpass // probably redundant but we like api safety if newpassword == newpasswordAgain { rekey := createKey(newpassword, salt) - log.Infof("rekeying database...") + log.Debugf("rekeying database...") return cp.storage.Rekey(rekey) } return errors.New(constants.PasswordsDoNotMatchError) @@ -1286,9 +1305,15 @@ func (cp *cwtchPeer) eventHandler() { // Safe Access to Extensions cp.extensionLock.Lock() - log.Infof("checking extension...%v", cp.extensions) + log.Debugf("checking extension...%v", cp.extensions) for _, extension := range cp.extensions { - log.Infof("checking extension...%v", extension) + log.Debugf("checking extension...%v", extension) + // check if the current map of experiments satisfies the extension requirements + if !cp.checkExtensionExperiment(extension) { + log.Debugf("skipping extension...not all experiments satisfied") + continue + } + extension.extension.OnContactRequestValue(cp, *conversationInfo, ev.EventID, scopedZonedPath) } cp.extensionLock.Unlock() @@ -1313,6 +1338,12 @@ func (cp *cwtchPeer) eventHandler() { // Safe Access to Extensions cp.extensionLock.Lock() for _, extension := range cp.extensions { + log.Debugf("checking extension...%v", extension) + // check if the current map of experiments satisfies the extension requirements + if !cp.checkExtensionExperiment(extension) { + log.Debugf("skipping extension...not all experiments satisfied") + continue + } extension.extension.OnContactReceiveValue(cp, *conversationInfo, scopedZonedPath, val, exists) } cp.extensionLock.Unlock() @@ -1406,6 +1437,13 @@ func (cp *cwtchPeer) eventHandler() { processed := false cp.extensionLock.Lock() for _, extension := range cp.extensions { + + // check if the current map of experiments satisfies the extension requirements + if !cp.checkExtensionExperiment(extension) { + log.Debugf("skipping extension...not all experiments satisfied") + continue + } + // if the extension is registered for this event type then process if _, contains := extension.events[ev.EventType]; contains { extension.extension.OnEvent(ev, cp) @@ -1421,6 +1459,17 @@ func (cp *cwtchPeer) eventHandler() { } } +func (cp *cwtchPeer) checkExtensionExperiment(hook ProfileHook) bool { + cp.experimentsLock.Lock() + defer cp.experimentsLock.Unlock() + for experiment := range hook.experiments { + if !cp.experiments.IsEnabled(experiment) { + return false + } + } + return true +} + // attemptInsertOrAcknowledgeLegacyGroupConversation is a convenience method that looks up the conversation // by the given handle and attempts to mark the message as acknowledged. returns error on failure // to either find the contact or the associated message diff --git a/peer/cwtchprofilestorage.go b/peer/cwtchprofilestorage.go index e7cca3f..ce79a6e 100644 --- a/peer/cwtchprofilestorage.go +++ b/peer/cwtchprofilestorage.go @@ -125,7 +125,9 @@ func NewCwtchProfileStorage(db *sql.DB, profileDirectory string) (*CwtchProfileS insertProfileKeyValueStmt, err := db.Prepare(insertProfileKeySQLStmt) if err != nil { db.Close() - log.Errorf("error preparing query: %v %v", insertProfileKeySQLStmt, err) + // note: this is debug because we expect failure here when opening an encrypted database with an + // incorrect password. The rest are errors because failure is not expected. + log.Debugf("error preparing query: %v %v", insertProfileKeySQLStmt, err) return nil, err } @@ -772,7 +774,7 @@ func (cps *CwtchProfileStorage) PurgeNonSavedMessages() { for _, conversation := range ci { if !conversation.IsGroup() && !conversation.IsServer() { if conversation.Attributes[attr.LocalScope.ConstructScopedZonedPath(attr.ProfileZone.ConstructZonedPath(event.SaveHistoryKey)).ToString()] != event.SaveHistoryConfirmed { - log.Infof("purging conversation...") + log.Debugf("purging conversation...") // TODO: At some point in the future this needs to iterate over channels and make a decision for each on.. cps.PurgeConversationChannel(conversation.ID, 0) } diff --git a/peer/hooks.go b/peer/hooks.go index d061c27..1f4b545 100644 --- a/peer/hooks.go +++ b/peer/hooks.go @@ -7,11 +7,10 @@ import ( ) type ProfileHooks interface { - // RegisterEvents returns a set of events that the extension is interested hooking RegisterEvents() []event.Type - // RegisterExperiments RegisterExperiments returns a set of experiments that the extension is interested in being notified about + // RegisterExperiments returns a set of experiments that the extension is interested in being notified about RegisterExperiments() []string // OnEvent is called whenever an event Registered with RegisterEvents is called diff --git a/peer/profile_interface.go b/peer/profile_interface.go index c9fa835..98c463d 100644 --- a/peer/profile_interface.go +++ b/peer/profile_interface.go @@ -132,4 +132,6 @@ type CwtchPeer interface { Delete() PublishEvent(resp event.Event) RegisterHook(hook ProfileHooks) + UpdateExperiments(enabled bool, experiments map[string]bool) + IsFeatureEnabled(featureName string) bool } diff --git a/peer/storage.go b/peer/storage.go index c69b8e1..42a5872 100644 --- a/peer/storage.go +++ b/peer/storage.go @@ -176,7 +176,7 @@ func ImportProfile(exportedCwtchFile string, profilesDir string, password string log.Errorf("%s is an invalid cwtch backup file: %s", profileID, err) return nil, err } - log.Infof("%s is a valid cwtch backup file", profileID) + log.Debugf("%s is a valid cwtch backup file", profileID) profileDBFile := filepath.Join(profilesDir, profileID, dbFile) log.Debugf("checking %v", profileDBFile) diff --git a/storage/v1/profile_store.go b/storage/v1/profile_store.go index 32110ba..98c6802 100644 --- a/storage/v1/profile_store.go +++ b/storage/v1/profile_store.go @@ -73,7 +73,7 @@ func (ps *ProfileStoreV1) load() error { for gid, group := range cp.Groups { if group.Version == 0 { - log.Infof("group %v is of unsupported version 0. dropping group...\n", group.GroupID) + log.Debugf("group %v is of unsupported version 0. dropping group...\n", group.GroupID) delete(cp.Groups, gid) continue } diff --git a/storage/v1/stream_store.go b/storage/v1/stream_store.go index c90c9ee..759104b 100644 --- a/storage/v1/stream_store.go +++ b/storage/v1/stream_store.go @@ -152,7 +152,7 @@ func (ss *streamStore) WriteN(messages []model.Message) { ss.lock.Lock() defer ss.lock.Unlock() - log.Infof("WriteN %v messages\n", len(messages)) + log.Debugf("WriteN %v messages\n", len(messages)) i := 0 for _, m := range messages { ss.updateBuffer(m) diff --git a/testing/filesharing/file_sharing_integration_test.go b/testing/filesharing/file_sharing_integration_test.go index 2574be0..a438868 100644 --- a/testing/filesharing/file_sharing_integration_test.go +++ b/testing/filesharing/file_sharing_integration_test.go @@ -118,13 +118,18 @@ func TestFileSharing(t *testing.T) { app.ActivatePeerEngine(bob.GetOnion(), true, true, true) alice.AutoHandleEvents([]event.Type{event.PeerStateChange, event.NewRetValMessageFromPeer}) - bob.AutoHandleEvents([]event.Type{event.PeerStateChange, event.NewRetValMessageFromPeer, event.ManifestReceived}) + bob.AutoHandleEvents([]event.Type{event.PeerStateChange, event.NewRetValMessageFromPeer}) queueOracle := event.NewQueue() app.GetEventBus(bob.GetOnion()).Subscribe(event.FileDownloaded, queueOracle) - t.Logf("** Launching Peers...") + // Turn on File Sharing Experiment... + settings := app.ReadSettings() + settings.ExperimentsEnabled = true + settings.Experiments[constants.FileSharingExperiment] = true + app.UpdateSettings(settings) + t.Logf("** Launching Peers...") waitTime := time.Duration(30) * time.Second t.Logf("** Waiting for Alice, Bob to connect with onion network... (%v)\n", waitTime) time.Sleep(waitTime) @@ -163,7 +168,11 @@ func TestFileSharing(t *testing.T) { // Restart t.Logf("Restarting File Share") - filesharingFunctionality.ReShareFiles(alice) + err = filesharingFunctionality.ReShareFiles(alice) + + if err != nil { + t.Fatalf("Error!: %v", err) + } // run the same download test again...to check that we can actually download the file testBobDownloadFile(t, bob, filesharingFunctionality, queueOracle)