forked from cwtch.im/cwtch
FileSharing Experiments / Move Experiment Handling to App and Cwtch Peer
This commit is contained in:
parent
26c5c11216
commit
f246ea1e40
38
app/app.go
38
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
|
||||
|
|
|
@ -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"
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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{}))
|
||||
}
|
||||
|
|
|
@ -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"}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue