From d50f210e3523a0d7c1e207e36e0df10eb01964ec Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Mon, 6 Mar 2023 13:06:15 -0800 Subject: [PATCH 1/6] Port Autodownload / Image Previews / Profile Image Experiment to Cwtch --- .drone.yml | 8 + app/app.go | 20 +- extensions/profile_value.go | 4 + .../filesharing/filesharing_functionality.go | 16 +- functionality/filesharing/image_previews.go | 139 +++++++++++++ peer/cwtch_peer.go | 17 +- peer/cwtchprofilestorage.go | 30 ++- peer/hooks.go | 4 + peer/profile_interface.go | 2 + protocol/files/filesharing_subsystem.go | 2 +- {app => settings}/settings.go | 2 +- testing/autodownload/cwtch.png | Bin 0 -> 51791 bytes .../file_sharing_integration_test.go | 186 ++++++++++++++++++ testing/cwtch_peer_server_integration_test.go | 2 +- .../file_sharing_integration_test.go | 2 +- 15 files changed, 408 insertions(+), 26 deletions(-) create mode 100644 functionality/filesharing/image_previews.go rename {app => settings}/settings.go (99%) create mode 100644 testing/autodownload/cwtch.png create mode 100644 testing/autodownload/file_sharing_integration_test.go diff --git a/.drone.yml b/.drone.yml index 4ee878e..75f1322 100644 --- a/.drone.yml +++ b/.drone.yml @@ -48,6 +48,14 @@ steps: commands: - export PATH=`pwd`:$PATH - go test -timeout=20m -race -v cwtch.im/cwtch/testing/filesharing + - name: filesharing-autodownload-integ-test + image: golang:1.19.1 + volumes: + - name: deps + path: /go + commands: + - export PATH=`pwd`:$PATH + - go test -timeout=20m -race -v cwtch.im/cwtch/testing/autodownload - name: notify-gogs image: openpriv/drone-gogs pull: if-not-exists diff --git a/app/app.go b/app/app.go index 6198d27..3042c5e 100644 --- a/app/app.go +++ b/app/app.go @@ -10,6 +10,7 @@ import ( "cwtch.im/cwtch/model/constants" "cwtch.im/cwtch/peer" "cwtch.im/cwtch/protocol/connections" + "cwtch.im/cwtch/settings" "cwtch.im/cwtch/storage" "git.openprivacy.ca/openprivacy/connectivity" "git.openprivacy.ca/openprivacy/log" @@ -32,7 +33,7 @@ type application struct { appBus event.Manager appmutex sync.Mutex - settings *GlobalSettingsFile + settings *settings.GlobalSettingsFile } func (app *application) IsFeatureEnabled(experiment string) bool { @@ -64,8 +65,8 @@ type Application interface { ActivatePeerEngine(onion string) DeactivatePeerEngine(onion string) - ReadSettings() GlobalSettings - UpdateSettings(settings GlobalSettings) + ReadSettings() settings.GlobalSettings + UpdateSettings(settings settings.GlobalSettings) IsFeatureEnabled(experiment string) bool ShutdownPeer(string) @@ -78,14 +79,14 @@ type Application interface { // 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) *GlobalSettingsFile { +func LoadAppSettings(appDirectory string) *settings.GlobalSettingsFile { 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. - settings, err := InitGlobalSettingsFile(appDirectory, DefactoPasswordForUnencryptedProfiles) + settings, err := settings.InitGlobalSettingsFile(appDirectory, DefactoPasswordForUnencryptedProfiles) if err != nil { log.Errorf("error initializing global settings file %. Global settings might not be loaded or saves", err) } @@ -93,7 +94,7 @@ func LoadAppSettings(appDirectory string) *GlobalSettingsFile { } // NewApp creates a new app with some environment awareness and initializes a Tor Manager -func NewApp(acn connectivity.ACN, appDirectory string, settings *GlobalSettingsFile) Application { +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} app.peers = make(map[string]peer.CwtchPeer) @@ -108,13 +109,13 @@ func NewApp(acn connectivity.ACN, appDirectory string, settings *GlobalSettingsF return app } -func (app *application) ReadSettings() GlobalSettings { +func (app *application) ReadSettings() settings.GlobalSettings { app.appmutex.Lock() defer app.appmutex.Unlock() return app.settings.ReadGlobalSettings() } -func (app *application) UpdateSettings(settings GlobalSettings) { +func (app *application) UpdateSettings(settings settings.GlobalSettings) { // don't allow any other application changes while settings update app.appmutex.Lock() defer app.appmutex.Unlock() @@ -133,6 +134,8 @@ func (app *application) UpdateSettings(settings GlobalSettings) { } else { profile.AllowUnknownConnections() } + + profile.NotifySettingsUpdate(settings) } } @@ -347,6 +350,7 @@ func (app *application) registerHooks(profile peer.CwtchPeer) { // Register Hooks profile.RegisterHook(extensions.ProfileValueExtension{}) profile.RegisterHook(filesharing.Functionality{}) + profile.RegisterHook(new(filesharing.ImagePreviewsFunctionality)) } // installProfile takes a profile and if it isn't loaded in the app, installs it and returns true diff --git a/extensions/profile_value.go b/extensions/profile_value.go index 3855807..0ac8b3b 100644 --- a/extensions/profile_value.go +++ b/extensions/profile_value.go @@ -6,6 +6,7 @@ import ( "cwtch.im/cwtch/model/attr" "cwtch.im/cwtch/model/constants" "cwtch.im/cwtch/peer" + "cwtch.im/cwtch/settings" "git.openprivacy.ca/openprivacy/log" "strconv" ) @@ -14,6 +15,9 @@ import ( type ProfileValueExtension struct { } +func (pne ProfileValueExtension) NotifySettingsUpdate(settings settings.GlobalSettings) { +} + func (pne ProfileValueExtension) EventsToRegister() []event.Type { return nil } diff --git a/functionality/filesharing/filesharing_functionality.go b/functionality/filesharing/filesharing_functionality.go index 80a0346..4ec1620 100644 --- a/functionality/filesharing/filesharing_functionality.go +++ b/functionality/filesharing/filesharing_functionality.go @@ -3,6 +3,7 @@ package filesharing import ( "crypto/rand" "cwtch.im/cwtch/event" + "cwtch.im/cwtch/settings" "encoding/hex" "encoding/json" "errors" @@ -30,8 +31,11 @@ import ( type Functionality struct { } +func (f Functionality) NotifySettingsUpdate(settings settings.GlobalSettings) { +} + func (f Functionality) EventsToRegister() []event.Type { - return []event.Type{event.ManifestReceived, event.FileDownloaded} + return []event.Type{event.ProtocolEngineCreated, event.ManifestReceived, event.FileDownloaded} } func (f Functionality) ExperimentsToRegister() []string { @@ -133,13 +137,9 @@ func (f Functionality) OnContactReceiveValue(profile peer.CwtchPeer, conversatio } } -// FunctionalityGate returns filesharing if enabled in the given experiment map -// Note: Experiment maps are currently in libcwtch-go -func FunctionalityGate(experimentMap map[string]bool) (*Functionality, error) { - if experimentMap[constants.FileSharingExperiment] { - return new(Functionality), nil - } - return nil, errors.New("filesharing is not enabled") +// FunctionalityGate returns filesharing functionality - gates now happen on function calls. +func FunctionalityGate() *Functionality { + return new(Functionality) } // PreviewFunctionalityGate returns filesharing if image previews are enabled diff --git a/functionality/filesharing/image_previews.go b/functionality/filesharing/image_previews.go new file mode 100644 index 0000000..4b7b911 --- /dev/null +++ b/functionality/filesharing/image_previews.go @@ -0,0 +1,139 @@ +package filesharing + +import ( + "cwtch.im/cwtch/event" + "cwtch.im/cwtch/model" + "cwtch.im/cwtch/model/attr" + "cwtch.im/cwtch/model/constants" + "cwtch.im/cwtch/peer" + "cwtch.im/cwtch/settings" + "encoding/json" + "fmt" + "git.openprivacy.ca/openprivacy/log" + "os" + "strconv" + "time" +) + +type ImagePreviewsFunctionality struct { + downloadFolder string +} + +func (i *ImagePreviewsFunctionality) NotifySettingsUpdate(settings settings.GlobalSettings) { + i.downloadFolder = settings.DownloadPath +} + +func (i ImagePreviewsFunctionality) EventsToRegister() []event.Type { + return []event.Type{event.ProtocolEngineCreated, event.NewMessageFromPeer, event.NewMessageFromGroup} +} + +func (i ImagePreviewsFunctionality) ExperimentsToRegister() []string { + return []string{constants.FileSharingExperiment, constants.ImagePreviewsExperiment} +} + +func (i *ImagePreviewsFunctionality) OnEvent(ev event.Event, profile peer.CwtchPeer) { + if profile.IsFeatureEnabled(constants.FileSharingExperiment) && profile.IsFeatureEnabled(constants.ImagePreviewsExperiment) { + switch ev.EventType { + case event.NewMessageFromPeer: + ci, err := profile.FetchConversationInfo(ev.Data["RemotePeer"]) + if err == nil { + if ci.Accepted { + i.handleImagePreviews(profile, &ev, ci.ID, ci.ID) + } + } + case event.NewMessageFromGroup: + ci, err := profile.FetchConversationInfo(ev.Data["RemotePeer"]) + if err == nil { + if ci.Accepted { + i.handleImagePreviews(profile, &ev, ci.ID, ci.ID) + } + } + case event.ProtocolEngineCreated: + // Now that the Peer Engine is Activated, Reshare Profile Images + key, exists := profile.GetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants.CustomProfileImageKey) + if exists { + serializedManifest, _ := profile.GetScopedZonedAttribute(attr.ConversationScope, attr.FilesharingZone, fmt.Sprintf("%s.manifest", key)) + // reset the share timestamp, currently file shares are hardcoded to expire after 30 days... + // we reset the profile image here so that it is always available. + profile.SetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.ts", key), strconv.FormatInt(time.Now().Unix(), 10)) + log.Debugf("Custom Profile Image: %v %s", key, serializedManifest) + } + // If file sharing is enabled then reshare all active files... + fsf := FunctionalityGate() + fsf.ReShareFiles(profile) + } + } +} + +func (i ImagePreviewsFunctionality) OnContactRequestValue(profile peer.CwtchPeer, conversation model.Conversation, eventID string, path attr.ScopedZonedPath) { +} + +func (i *ImagePreviewsFunctionality) OnContactReceiveValue(profile peer.CwtchPeer, conversation model.Conversation, path attr.ScopedZonedPath, value string, exists bool) { + if profile.IsFeatureEnabled(constants.FileSharingExperiment) && profile.IsFeatureEnabled(constants.ImagePreviewsExperiment) { + _, zone, path := path.GetScopeZonePath() + if zone == attr.ProfileZone && path == constants.CustomProfileImageKey { + fileKey := value + + if conversation.Accepted { + fsf := FunctionalityGate() + basepath := i.downloadFolder + fp, mp := GenerateDownloadPath(basepath, fileKey, true) + + if value, exists := profile.GetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.complete", fileKey)); exists && value == event.True { + if _, err := os.Stat(fp); err == nil { + // file is marked as completed downloaded and exists... + } else { + // the user probably deleted the file, mark completed as false... + profile.SetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.complete", fileKey), event.False) + } + } + + log.Debugf("Downloading Profile Image %v %v %v", fp, mp, fileKey) + // ev.Event.Data[event.FilePath] = fp + fsf.DownloadFile(profile, conversation.ID, fp, mp, value, constants.ImagePreviewMaxSizeInBytes) + } + } + } +} + +// handleImagePreviews checks settings and, if appropriate, auto-downloads any images +func (i *ImagePreviewsFunctionality) handleImagePreviews(profile peer.CwtchPeer, ev *event.Event, conversationID, senderID int) { + if profile.IsFeatureEnabled(constants.FileSharingExperiment) && profile.IsFeatureEnabled(constants.ImagePreviewsExperiment) { + + // Short-circuit failures + // Don't autodownload images if the download path does not exist. + if i.downloadFolder == "" { + log.Debugf("download folder %v is not set", i.downloadFolder) + return + } + + // Don't autodownload images if the download path does not exist. + if _, err := os.Stat(i.downloadFolder); os.IsNotExist(err) { + log.Debugf("download folder %v does not exist", i.downloadFolder) + return + } + + // If file sharing is enabled then reshare all active files... + fsf := FunctionalityGate() + + // Now look at the image preview experiment + var cm model.MessageWrapper + err := json.Unmarshal([]byte(ev.Data[event.Data]), &cm) + if err == nil && cm.Overlay == model.OverlayFileSharing { + log.Debugf("Received File Sharing Message") + var fm OverlayMessage + err = json.Unmarshal([]byte(cm.Data), &fm) + if err == nil { + if fm.ShouldAutoDL() { + basepath := i.downloadFolder + fp, mp := GenerateDownloadPath(basepath, fm.Name, false) + log.Debugf("autodownloading file!") + ev.Data["Auto"] = constants.True + mID, _ := strconv.Atoi(ev.Data["Index"]) + profile.UpdateMessageAttribute(conversationID, 0, mID, constants.AttrDownloaded, constants.True) + fsf.DownloadFile(profile, senderID, fp, mp, fm.FileKey(), constants.ImagePreviewMaxSizeInBytes) + } + } + } + } +} diff --git a/peer/cwtch_peer.go b/peer/cwtch_peer.go index 7037fd2..7fbbc35 100644 --- a/peer/cwtch_peer.go +++ b/peer/cwtch_peer.go @@ -4,6 +4,7 @@ import ( "crypto/rand" "cwtch.im/cwtch/model/constants" "cwtch.im/cwtch/protocol/groups" + "cwtch.im/cwtch/settings" "encoding/base64" "encoding/hex" "encoding/json" @@ -189,6 +190,16 @@ func (cp *cwtchPeer) UpdateExperiments(enabled bool, experiments map[string]bool cp.experiments = model.InitExperiments(enabled, experiments) } +// NotifySettingsUpdate 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) NotifySettingsUpdate(settings settings.GlobalSettings) { + cp.extensionLock.Lock() + defer cp.extensionLock.Unlock() + for _, extension := range cp.extensions { + extension.extension.NotifySettingsUpdate(settings) + } +} + func (cp *cwtchPeer) PublishEvent(resp event.Event) { log.Debugf("Publishing Event: %v %v", resp.EventType, resp.Data) cp.eventBus.Publish(resp) @@ -1408,7 +1419,7 @@ func (cp *cwtchPeer) eventHandler() { 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") + log.Debugf("skipping extension (%s) ..not all experiments satisfied", extension) continue } @@ -1439,7 +1450,7 @@ func (cp *cwtchPeer) eventHandler() { 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") + log.Debugf("skipping extension (%s) ..not all experiments satisfied", extension) continue } extension.extension.OnContactReceiveValue(cp, *conversationInfo, scopedZonedPath, val, exists) @@ -1538,7 +1549,7 @@ func (cp *cwtchPeer) eventHandler() { // check if the current map of experiments satisfies the extension requirements if !cp.checkExtensionExperiment(extension) { - log.Debugf("skipping extension...not all experiments satisfied") + log.Debugf("skipping extension (%s) ..not all experiments satisfied", extension) continue } diff --git a/peer/cwtchprofilestorage.go b/peer/cwtchprofilestorage.go index ce79a6e..886896a 100644 --- a/peer/cwtchprofilestorage.go +++ b/peer/cwtchprofilestorage.go @@ -228,6 +228,8 @@ func NewCwtchProfileStorage(db *sql.DB, profileDirectory string) (*CwtchProfileS // StoreProfileKeyValue allows storing of typed Key/Value attribute in the Storage Engine func (cps *CwtchProfileStorage) StoreProfileKeyValue(keyType StorageKeyType, key string, value []byte) error { + cps.mutex.Lock() + defer cps.mutex.Unlock() _, err := cps.insertProfileKeyValueStmt.Exec(keyType, key, value) if err != nil { log.Errorf("error executing query: %v", err) @@ -238,6 +240,8 @@ func (cps *CwtchProfileStorage) StoreProfileKeyValue(keyType StorageKeyType, key // FindProfileKeysByPrefix allows fetching of typed values via a known Key from the Storage Engine func (cps *CwtchProfileStorage) FindProfileKeysByPrefix(keyType StorageKeyType, prefix string) ([]string, error) { + cps.mutex.Lock() + defer cps.mutex.Unlock() rows, err := cps.findProfileKeySQLStmt.Query(keyType, prefix+"%") if err != nil { log.Errorf("error executing query: %v", err) @@ -266,6 +270,8 @@ func (cps *CwtchProfileStorage) FindProfileKeysByPrefix(keyType StorageKeyType, // LoadProfileKeyValue allows fetching of typed values via a known Key from the Storage Engine func (cps *CwtchProfileStorage) LoadProfileKeyValue(keyType StorageKeyType, key string) ([]byte, error) { + cps.mutex.Lock() + defer cps.mutex.Unlock() rows, err := cps.selectProfileKeyValueStmt.Query(keyType, key) if err != nil { log.Errorf("error executing query: %v", err) @@ -291,6 +297,8 @@ func (cps *CwtchProfileStorage) LoadProfileKeyValue(keyType StorageKeyType, key // NewConversation stores a new conversation in the data store func (cps *CwtchProfileStorage) NewConversation(handle string, attributes model.Attributes, acl model.AccessControlList, accepted bool) (int, error) { + cps.mutex.Lock() + defer cps.mutex.Unlock() tx, err := cps.db.Begin() if err != nil { @@ -336,6 +344,8 @@ func (cps *CwtchProfileStorage) NewConversation(handle string, attributes model. // Ideally this function should not exist, and all lookups should happen by ID (this is currently // unavoidable in some circumstances because the event bus references conversations by handle, not by id) func (cps *CwtchProfileStorage) GetConversationByHandle(handle string) (*model.Conversation, error) { + cps.mutex.Lock() + defer cps.mutex.Unlock() rows, err := cps.selectConversationByHandleStmt.Query(handle) if err != nil { log.Errorf("error executing query: %v", err) @@ -367,6 +377,8 @@ func (cps *CwtchProfileStorage) GetConversationByHandle(handle string) (*model.C // on app start up to build a summary of conversations for the UI. Any further updates should be integrated // through the event bus. func (cps *CwtchProfileStorage) FetchConversations() ([]*model.Conversation, error) { + cps.mutex.Lock() + defer cps.mutex.Unlock() rows, err := cps.fetchAllConversationsStmt.Query() if err != nil { log.Errorf("error executing query: %v", err) @@ -401,6 +413,8 @@ func (cps *CwtchProfileStorage) FetchConversations() ([]*model.Conversation, err // GetConversation looks up a particular conversation by id func (cps *CwtchProfileStorage) GetConversation(id int) (*model.Conversation, error) { + cps.mutex.Lock() + defer cps.mutex.Unlock() rows, err := cps.selectConversationStmt.Query(id) if err != nil { log.Errorf("error executing query: %v", err) @@ -430,6 +444,8 @@ func (cps *CwtchProfileStorage) GetConversation(id int) (*model.Conversation, er // AcceptConversation sets the accepted status of a conversation to true in the backing datastore func (cps *CwtchProfileStorage) AcceptConversation(id int) error { + cps.mutex.Lock() + defer cps.mutex.Unlock() _, err := cps.acceptConversationStmt.Exec(id) if err != nil { log.Errorf("error executing query: %v", err) @@ -440,6 +456,8 @@ func (cps *CwtchProfileStorage) AcceptConversation(id int) error { // DeleteConversation purges the conversation and any associated message history from the conversation store. func (cps *CwtchProfileStorage) DeleteConversation(id int) error { + cps.mutex.Lock() + defer cps.mutex.Unlock() _, err := cps.deleteConversationStmt.Exec(id) if err != nil { log.Errorf("error executing query: %v", err) @@ -450,6 +468,8 @@ func (cps *CwtchProfileStorage) DeleteConversation(id int) error { // SetConversationACL sets a new ACL on a given conversation. func (cps *CwtchProfileStorage) SetConversationACL(id int, acl model.AccessControlList) error { + cps.mutex.Lock() + defer cps.mutex.Unlock() _, err := cps.setConversationACLStmt.Exec(acl.Serialize(), id) if err != nil { log.Errorf("error executing query: %v", err) @@ -464,6 +484,8 @@ func (cps *CwtchProfileStorage) SetConversationAttribute(id int, path attr.Scope if err != nil { return err } + cps.mutex.Lock() + defer cps.mutex.Unlock() ci.Attributes[path.ToString()] = value _, err = cps.setConversationAttributesStmt.Exec(ci.Attributes.Serialize(), id) if err != nil { @@ -475,7 +497,6 @@ func (cps *CwtchProfileStorage) SetConversationAttribute(id int, path attr.Scope // InsertMessage appends a message to a conversation channel, with a given set of attributes func (cps *CwtchProfileStorage) InsertMessage(conversation int, channel int, body string, attributes model.Attributes, signature string, contentHash string) (int, error) { - channelID := ChannelID{Conversation: conversation, Channel: channel} cps.mutex.Lock() @@ -757,6 +778,8 @@ func (cps *CwtchProfileStorage) GetMostRecentMessages(conversation int, channel // PurgeConversationChannel deletes all message for a conversation channel. func (cps *CwtchProfileStorage) PurgeConversationChannel(conversation int, channel int) error { + cps.mutex.Lock() + defer cps.mutex.Unlock() conversationStmt, err := cps.db.Prepare(fmt.Sprintf(purgeMessagesFromConversationSQLStmt, conversation, channel)) if err != nil { log.Errorf("error executing transaction: %v", err) @@ -785,12 +808,13 @@ func (cps *CwtchProfileStorage) PurgeNonSavedMessages() { // Close closes the underlying database and prepared statements func (cps *CwtchProfileStorage) Close(purgeAllNonSavedMessages bool) { - cps.mutex.Lock() - defer cps.mutex.Unlock() if cps.db != nil { if purgeAllNonSavedMessages { cps.PurgeNonSavedMessages() } + // We can't lock before this.. + cps.mutex.Lock() + defer cps.mutex.Unlock() cps.insertProfileKeyValueStmt.Close() cps.selectProfileKeyValueStmt.Close() diff --git a/peer/hooks.go b/peer/hooks.go index 33eec53..6a0924c 100644 --- a/peer/hooks.go +++ b/peer/hooks.go @@ -4,6 +4,7 @@ import ( "cwtch.im/cwtch/event" "cwtch.im/cwtch/model" "cwtch.im/cwtch/model/attr" + "cwtch.im/cwtch/settings" ) type ProfileHooks interface { @@ -21,6 +22,9 @@ type ProfileHooks interface { // OnContactReceiveValue is Hooked after a profile receives a response to a Get/Val Request OnContactReceiveValue(profile CwtchPeer, conversation model.Conversation, path attr.ScopedZonedPath, value string, exists bool) + + // NotifySettingsUpdate allow profile hooks to access configs e.g. download folder + NotifySettingsUpdate(settings settings.GlobalSettings) } type ProfileHook struct { diff --git a/peer/profile_interface.go b/peer/profile_interface.go index 6e6b830..fe28871 100644 --- a/peer/profile_interface.go +++ b/peer/profile_interface.go @@ -5,6 +5,7 @@ import ( "cwtch.im/cwtch/model" "cwtch.im/cwtch/model/attr" "cwtch.im/cwtch/protocol/connections" + "cwtch.im/cwtch/settings" "git.openprivacy.ca/cwtch.im/tapir/primitives/privacypass" "git.openprivacy.ca/openprivacy/connectivity" ) @@ -152,6 +153,7 @@ type CwtchPeer interface { PublishEvent(resp event.Event) RegisterHook(hook ProfileHooks) UpdateExperiments(enabled bool, experiments map[string]bool) + NotifySettingsUpdate(settings settings.GlobalSettings) IsFeatureEnabled(featureName string) bool } diff --git a/protocol/files/filesharing_subsystem.go b/protocol/files/filesharing_subsystem.go index 011d015..2d63b81 100644 --- a/protocol/files/filesharing_subsystem.go +++ b/protocol/files/filesharing_subsystem.go @@ -34,7 +34,7 @@ func (fsss *FileSharingSubSystem) ShareFile(fileKey string, serializedManifest s log.Errorf("could not share file %v", err) return } - log.Infof("sharing file: %v %v", fileKey, serializedManifest) + log.Debugf("sharing file: %v %v", fileKey, serializedManifest) fsss.activeShares.Store(fileKey, &manifest) } diff --git a/app/settings.go b/settings/settings.go similarity index 99% rename from app/settings.go rename to settings/settings.go index baf7f13..08cd263 100644 --- a/app/settings.go +++ b/settings/settings.go @@ -1,4 +1,4 @@ -package app +package settings import ( "cwtch.im/cwtch/event" diff --git a/testing/autodownload/cwtch.png b/testing/autodownload/cwtch.png new file mode 100644 index 0000000000000000000000000000000000000000..e50812f7eb4da511c0964c3a9a061ee0ae140181 GIT binary patch literal 51791 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 z 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 literal 0 HcmV?d00001 diff --git a/testing/autodownload/file_sharing_integration_test.go b/testing/autodownload/file_sharing_integration_test.go new file mode 100644 index 0000000..94feda7 --- /dev/null +++ b/testing/autodownload/file_sharing_integration_test.go @@ -0,0 +1,186 @@ +package filesharing + +import ( + "crypto/rand" + app2 "cwtch.im/cwtch/app" + "cwtch.im/cwtch/event" + "cwtch.im/cwtch/functionality/filesharing" + "cwtch.im/cwtch/model" + "cwtch.im/cwtch/model/attr" + "cwtch.im/cwtch/model/constants" + "cwtch.im/cwtch/peer" + "cwtch.im/cwtch/protocol/connections" + "encoding/base64" + "errors" + "fmt" + "git.openprivacy.ca/openprivacy/connectivity/tor" + "git.openprivacy.ca/openprivacy/log" + "path/filepath" + + // Import SQL Cipher + mrand "math/rand" + "os" + "os/user" + "path" + "runtime" + "runtime/pprof" + "testing" + "time" + + _ "github.com/mutecomm/go-sqlcipher/v4" +) + +func waitForPeerPeerConnection(t *testing.T, peera peer.CwtchPeer, peerb peer.CwtchPeer) { + for { + state := peera.GetPeerState(peerb.GetOnion()) + if state == connections.FAILED { + t.Fatalf("%v could not connect to %v", peera.GetOnion(), peerb.GetOnion()) + } + if state != connections.AUTHENTICATED { + fmt.Printf("peer %v waiting connect to peer %v, currently: %v\n", peera.GetOnion(), peerb.GetOnion(), connections.ConnectionStateName[state]) + time.Sleep(time.Second * 5) + continue + } else { + peerAName, _ := peera.GetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants.Name) + peerBName, _ := peerb.GetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants.Name) + fmt.Printf("%v CONNECTED and AUTHED to %v\n", peerAName, peerBName) + break + } + } +} + +func TestFileSharing(t *testing.T) { + numGoRoutinesStart := runtime.NumGoroutine() + os.RemoveAll("cwtch.out.png") + os.RemoveAll("cwtch.out.png.manifest") + os.RemoveAll("storage") + os.RemoveAll("tordir") + + log.SetLevel(log.LevelDebug) + + os.Mkdir("tordir", 0700) + dataDir := path.Join("tordir", "tor") + os.MkdirAll(dataDir, 0700) + + // we don't need real randomness for the port, just to avoid a possible conflict... + mrand.Seed(int64(time.Now().Nanosecond())) + socksPort := mrand.Intn(1000) + 9051 + controlPort := mrand.Intn(1000) + 9052 + + // generate a random password + key := make([]byte, 64) + _, err := rand.Read(key) + if err != nil { + panic(err) + } + + useCache := os.Getenv("TORCACHE") == "true" + + torDataDir := "" + if useCache { + log.Infof("using tor cache") + torDataDir = filepath.Join(dataDir, "data-dir-torcache") + os.MkdirAll(torDataDir, 0700) + } else { + log.Infof("using clean tor data dir") + if torDataDir, err = os.MkdirTemp(dataDir, "data-dir-"); err != nil { + t.Fatalf("could not create data dir") + } + } + + tor.NewTorrc().WithSocksPort(socksPort).WithOnionTrafficOnly().WithHashedPassword(base64.StdEncoding.EncodeToString(key)).WithControlPort(controlPort).Build("tordir/tor/torrc") + acn, err := tor.NewTorACNWithAuth("./tordir", path.Join("..", "tor"), torDataDir, controlPort, tor.HashedPasswordAuthenticator{Password: base64.StdEncoding.EncodeToString(key)}) + if err != nil { + t.Fatalf("Could not start Tor: %v", err) + } + acn.WaitTillBootstrapped() + defer acn.Close() + + app := app2.NewApp(acn, "./storage", app2.LoadAppSettings("./storage")) + + usr, _ := user.Current() + cwtchDir := path.Join(usr.HomeDir, ".cwtch") + os.Mkdir(cwtchDir, 0700) + os.RemoveAll(path.Join(cwtchDir, "testing")) + os.Mkdir(path.Join(cwtchDir, "testing"), 0700) + + t.Logf("Creating Alice...") + app.CreateProfile("alice", "asdfasdf", true) + + t.Logf("Creating Bob...") + app.CreateProfile("bob", "asdfasdf", true) + + t.Logf("** Waiting for Alice, Bob...") + alice := app2.WaitGetPeer(app, "alice") + app.ActivatePeerEngine(alice.GetOnion()) + bob := app2.WaitGetPeer(app, "bob") + app.ActivatePeerEngine(bob.GetOnion()) + + alice.AutoHandleEvents([]event.Type{event.PeerStateChange, event.NewRetValMessageFromPeer}) + bob.AutoHandleEvents([]event.Type{event.PeerStateChange, event.NewRetValMessageFromPeer}) + + queueOracle := event.NewQueue() + app.GetEventBus(bob.GetOnion()).Subscribe(event.FileDownloaded, queueOracle) + + // Turn on File Sharing Experiment... + settings := app.ReadSettings() + settings.ExperimentsEnabled = true + settings.DownloadPath = "./download_dir" + os.RemoveAll(path.Join(settings.DownloadPath, "cwtch.png")) + os.RemoveAll(path.Join(settings.DownloadPath, "cwtch.png.manifest")) + settings.Experiments[constants.FileSharingExperiment] = true + // Turn Auto Downloading On... (Part of the Image Previews / Profile Images Experiment) + settings.Experiments[constants.ImagePreviewsExperiment] = 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) + + bob.NewContactConversation(alice.GetOnion(), model.DefaultP2PAccessControl(), true) + alice.NewContactConversation(bob.GetOnion(), model.DefaultP2PAccessControl(), true) + alice.PeerWithOnion(bob.GetOnion()) + + t.Logf("Waiting for alice and Bob to peer...") + waitForPeerPeerConnection(t, alice, bob) + alice.AcceptConversation(1) + bob.AcceptConversation(1) + t.Logf("Alice and Bob are Connected!!") + + filesharingFunctionality := filesharing.FunctionalityGate() + + _, fileSharingMessage, err := filesharingFunctionality.ShareFile("cwtch.png", alice) + alice.SendMessage(1, fileSharingMessage) + + if err != nil { + t.Fatalf("Error!: %v", err) + } + + // test that bob can download and verify the file + // The main difference here is that bob doesn't need to do anything... + // testBobDownloadFile(t, bob, filesharingFunctionality, queueOracle) + + // Wait for say... + time.Sleep(10 * time.Second) + + if _, err := os.Stat(path.Join(settings.DownloadPath, "cwtch.png")); errors.Is(err, os.ErrNotExist) { + // path/to/whatever does not exist + t.Fatalf("cwthc.png should have been automatically downloadeded...") + } + + queueOracle.Shutdown() + app.Shutdown() + acn.Close() + time.Sleep(10 * time.Second) + numGoRoutinesPostACN := runtime.NumGoroutine() + + // Printing out the current goroutines + // Very useful if we are leaking any. + pprof.Lookup("goroutine").WriteTo(os.Stdout, 1) + + if numGoRoutinesStart != numGoRoutinesPostACN { + t.Errorf("Number of GoRoutines at start (%v) does not match number of goRoutines after cleanup of peers and servers (%v), clean up failed, leak detected!", numGoRoutinesStart, numGoRoutinesPostACN) + } + +} diff --git a/testing/cwtch_peer_server_integration_test.go b/testing/cwtch_peer_server_integration_test.go index 816290d..a7421c3 100644 --- a/testing/cwtch_peer_server_integration_test.go +++ b/testing/cwtch_peer_server_integration_test.go @@ -412,7 +412,7 @@ func TestCwtchPeerIntegration(t *testing.T) { pprof.Lookup("goroutine").WriteTo(os.Stdout, 1) fmt.Println("") log.Infof("numGoRoutinesStart: %v\nnumGoRoutinesPostAppStart: %v\nnumGoRoutinesPostPeerStart: %v\nnumGoRoutinesPostPeerAndServerConnect: %v\n"+ - "numGoRoutinesPostAlice: %v\nnumGoRoutinesPostCarolConnect: %v\nnumGoRoutinesPostBob: %v\nnumGoRoutinesPostCarol: %v\nnumGoRoutinesPostAppShutdown: %v", + "numGoRoutinesPostAlice: %v\nnumGoRoutinesPostCarolConnect: %v\nnumGoRoutinesPostBob: %v\nnumGoRoutinesPostCarol: %v\nnumGoRoutinesPostAppShutdown: %v", numGoRoutinesStart, numGoRoutinesPostAppStart, numGoRoutinesPostPeerStart, numGoRoutinesPostServerConnect, numGoRoutinesPostAlice, numGoRoutinesPostCarolConnect, numGoRoutinesPostBob, numGoRoutinesPostCarol, numGoRoutinesPostAppShutdown) diff --git a/testing/filesharing/file_sharing_integration_test.go b/testing/filesharing/file_sharing_integration_test.go index f151cd6..5287acb 100644 --- a/testing/filesharing/file_sharing_integration_test.go +++ b/testing/filesharing/file_sharing_integration_test.go @@ -144,7 +144,7 @@ func TestFileSharing(t *testing.T) { t.Logf("Alice and Bob are Connected!!") - filesharingFunctionality, _ := filesharing.FunctionalityGate(map[string]bool{constants.FileSharingExperiment: true}) + filesharingFunctionality := filesharing.FunctionalityGate() _, fileSharingMessage, err := filesharingFunctionality.ShareFile("cwtch.png", alice) alice.SendMessage(1, fileSharingMessage) From 0139f7a5a9bd7f148b059f48645600ff0e6e5c14 Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Mon, 6 Mar 2023 13:19:52 -0800 Subject: [PATCH 2/6] Skip processed error if an experiment *might* have flagged this event --- peer/cwtch_peer.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/peer/cwtch_peer.go b/peer/cwtch_peer.go index 7fbbc35..ddc2791 100644 --- a/peer/cwtch_peer.go +++ b/peer/cwtch_peer.go @@ -1550,6 +1550,11 @@ func (cp *cwtchPeer) eventHandler() { // check if the current map of experiments satisfies the extension requirements if !cp.checkExtensionExperiment(extension) { log.Debugf("skipping extension (%s) ..not all experiments satisfied", extension) + if cp.checkEventExperiment(extension, ev.EventType) { + // If this experiment was enabled...we might have processed this event... + // To avoid flagging an error later on in this method we set processed to true. + processed = true + } continue } @@ -1568,6 +1573,17 @@ func (cp *cwtchPeer) eventHandler() { } } +func (cp *cwtchPeer) checkEventExperiment(hook ProfileHook, event event.Type) bool { + cp.experimentsLock.Lock() + defer cp.experimentsLock.Unlock() + for hookEvent := range hook.events { + if event == hookEvent { + return true + } + } + return false +} + func (cp *cwtchPeer) checkExtensionExperiment(hook ProfileHook) bool { cp.experimentsLock.Lock() defer cp.experimentsLock.Unlock() From 186a33deb6250a15077c421d60c01202a99ed6cb Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Mon, 6 Mar 2023 13:27:45 -0800 Subject: [PATCH 3/6] Autocreate Download Folder in Test --- testing/autodownload/file_sharing_integration_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/testing/autodownload/file_sharing_integration_test.go b/testing/autodownload/file_sharing_integration_test.go index 94feda7..549db81 100644 --- a/testing/autodownload/file_sharing_integration_test.go +++ b/testing/autodownload/file_sharing_integration_test.go @@ -55,6 +55,7 @@ func TestFileSharing(t *testing.T) { os.RemoveAll("cwtch.out.png.manifest") os.RemoveAll("storage") os.RemoveAll("tordir") + os.RemoveAll("./download_dir") log.SetLevel(log.LevelDebug) @@ -128,6 +129,7 @@ func TestFileSharing(t *testing.T) { settings.DownloadPath = "./download_dir" os.RemoveAll(path.Join(settings.DownloadPath, "cwtch.png")) os.RemoveAll(path.Join(settings.DownloadPath, "cwtch.png.manifest")) + os.MkdirAll(settings.DownloadPath, 0700) settings.Experiments[constants.FileSharingExperiment] = true // Turn Auto Downloading On... (Part of the Image Previews / Profile Images Experiment) settings.Experiments[constants.ImagePreviewsExperiment] = true From de32ae240a2bbc7fd79e6e412d2b56fabff24171 Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Mon, 6 Mar 2023 13:43:57 -0800 Subject: [PATCH 4/6] Remove Queue Oracle --- testing/autodownload/file_sharing_integration_test.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/testing/autodownload/file_sharing_integration_test.go b/testing/autodownload/file_sharing_integration_test.go index 549db81..51cd2a4 100644 --- a/testing/autodownload/file_sharing_integration_test.go +++ b/testing/autodownload/file_sharing_integration_test.go @@ -120,9 +120,6 @@ func TestFileSharing(t *testing.T) { alice.AutoHandleEvents([]event.Type{event.PeerStateChange, event.NewRetValMessageFromPeer}) bob.AutoHandleEvents([]event.Type{event.PeerStateChange, event.NewRetValMessageFromPeer}) - queueOracle := event.NewQueue() - app.GetEventBus(bob.GetOnion()).Subscribe(event.FileDownloaded, queueOracle) - // Turn on File Sharing Experiment... settings := app.ReadSettings() settings.ExperimentsEnabled = true @@ -171,7 +168,6 @@ func TestFileSharing(t *testing.T) { t.Fatalf("cwthc.png should have been automatically downloadeded...") } - queueOracle.Shutdown() app.Shutdown() acn.Close() time.Sleep(10 * time.Second) From fcb07042d7ce9904c403ab279b319f2f82875b7c Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Mon, 6 Mar 2023 13:44:12 -0800 Subject: [PATCH 5/6] Extend Test Clean Up Time --- testing/autodownload/file_sharing_integration_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/autodownload/file_sharing_integration_test.go b/testing/autodownload/file_sharing_integration_test.go index 51cd2a4..bdd5835 100644 --- a/testing/autodownload/file_sharing_integration_test.go +++ b/testing/autodownload/file_sharing_integration_test.go @@ -170,7 +170,7 @@ func TestFileSharing(t *testing.T) { app.Shutdown() acn.Close() - time.Sleep(10 * time.Second) + time.Sleep(20 * time.Second) numGoRoutinesPostACN := runtime.NumGoroutine() // Printing out the current goroutines From 264b8b936323d6eff437d0e38ee5f7a1da4144ee Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Mon, 13 Mar 2023 12:46:15 -0700 Subject: [PATCH 6/6] Ensure Settings Updates are Applied to Experiments --- app/app.go | 2 ++ functionality/filesharing/filesharing_functionality.go | 6 +++--- functionality/filesharing/image_previews.go | 6 +++--- peer/cwtch_peer.go | 1 + peer/storage.go | 2 +- 5 files changed, 10 insertions(+), 7 deletions(-) diff --git a/app/app.go b/app/app.go index 3042c5e..871b32a 100644 --- a/app/app.go +++ b/app/app.go @@ -351,6 +351,8 @@ func (app *application) registerHooks(profile peer.CwtchPeer) { profile.RegisterHook(extensions.ProfileValueExtension{}) profile.RegisterHook(filesharing.Functionality{}) profile.RegisterHook(new(filesharing.ImagePreviewsFunctionality)) + // Ensure that Profiles have the Most Up to Date Settings... + profile.NotifySettingsUpdate(app.settings.ReadGlobalSettings()) } // installProfile takes a profile and if it isn't loaded in the app, installs it and returns true diff --git a/functionality/filesharing/filesharing_functionality.go b/functionality/filesharing/filesharing_functionality.go index 4ec1620..16dd892 100644 --- a/functionality/filesharing/filesharing_functionality.go +++ b/functionality/filesharing/filesharing_functionality.go @@ -68,7 +68,7 @@ func (f Functionality) OnEvent(ev event.Event, profile peer.CwtchPeer) { 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) + log.Debugf("could not download file, size %v greater than limit %v", manifest.FileSizeInBytes, fileSizeLimitValue) } else { manifest.Title = manifest.FileName manifest.FileName = downloadFilePath @@ -349,10 +349,10 @@ func (f *Functionality) ReShareFiles(profile peer.CwtchPeer) error { if err == nil && sharedFile.Active { err := f.RestartFileShare(profile, filekey) if err != nil { - log.Errorf("could not reshare file: %v", err) + log.Debugf("could not reshare file: %v", err) } } else { - log.Errorf("could not get fileshare info %v", err) + log.Debugf("could not get fileshare info %v", err) } } } diff --git a/functionality/filesharing/image_previews.go b/functionality/filesharing/image_previews.go index 4b7b911..7deb959 100644 --- a/functionality/filesharing/image_previews.go +++ b/functionality/filesharing/image_previews.go @@ -103,13 +103,13 @@ func (i *ImagePreviewsFunctionality) handleImagePreviews(profile peer.CwtchPeer, // Short-circuit failures // Don't autodownload images if the download path does not exist. if i.downloadFolder == "" { - log.Debugf("download folder %v is not set", i.downloadFolder) + log.Errorf("download folder %v is not set", i.downloadFolder) return } // Don't autodownload images if the download path does not exist. if _, err := os.Stat(i.downloadFolder); os.IsNotExist(err) { - log.Debugf("download folder %v does not exist", i.downloadFolder) + log.Errorf("download folder %v does not exist", i.downloadFolder) return } @@ -127,7 +127,7 @@ func (i *ImagePreviewsFunctionality) handleImagePreviews(profile peer.CwtchPeer, if fm.ShouldAutoDL() { basepath := i.downloadFolder fp, mp := GenerateDownloadPath(basepath, fm.Name, false) - log.Debugf("autodownloading file!") + log.Debugf("autodownloading file! %v %v %v", basepath, fp, i.downloadFolder) ev.Data["Auto"] = constants.True mID, _ := strconv.Atoi(ev.Data["Index"]) profile.UpdateMessageAttribute(conversationID, 0, mID, constants.AttrDownloaded, constants.True) diff --git a/peer/cwtch_peer.go b/peer/cwtch_peer.go index ddc2791..e8dc432 100644 --- a/peer/cwtch_peer.go +++ b/peer/cwtch_peer.go @@ -193,6 +193,7 @@ func (cp *cwtchPeer) UpdateExperiments(enabled bool, experiments map[string]bool // NotifySettingsUpdate 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) NotifySettingsUpdate(settings settings.GlobalSettings) { + log.Debugf("Cwtch Profile Settings Update: %v", settings) cp.extensionLock.Lock() defer cp.extensionLock.Unlock() for _, extension := range cp.extensions { diff --git a/peer/storage.go b/peer/storage.go index 42a5872..197cb16 100644 --- a/peer/storage.go +++ b/peer/storage.go @@ -155,7 +155,7 @@ func CreateEncryptedStore(profileDirectory string, password string) (*CwtchProfi // FromEncryptedDatabase constructs a Cwtch Profile from an existing Encrypted Database func FromEncryptedDatabase(profileDirectory string, password string) (CwtchPeer, error) { - log.Infof("Loading Encrypted Profile: %v", profileDirectory) + log.Debugf("Loading Encrypted Profile: %v", profileDirectory) db, err := openEncryptedDatabase(profileDirectory, password, false) if db == nil || err != nil { return nil, fmt.Errorf("unable to open encrypted database: error: %v", err)