Compare commits

...

12 Commits

Author SHA1 Message Date
Sarah Jamie Lewis d2fd57853a Load App Settings Tests
continuous-integration/drone/pr Build is pending Details
2023-02-28 10:13:11 -08:00
Sarah Jamie Lewis e0adeac4a0 Logging Fixes / InitApp -> InitAppSettings 2023-02-28 10:00:32 -08:00
Sarah Jamie Lewis 0f83fb79f0 Support for Enhanced Import 2023-02-27 14:05:52 -08:00
Sarah Jamie Lewis dfaed356c4 Consolidating Profile Setup Logic 2023-02-27 13:31:32 -08:00
Sarah Jamie Lewis c90301bb4c Reorganize Peer Init 2023-02-27 12:07:19 -08:00
Sarah Jamie Lewis 03da50b850 Experiments Update on Load 2023-02-27 12:01:54 -08:00
Sarah Jamie Lewis b22b8f3297 Fix Map Panic 2023-02-25 07:19:12 -08:00
Sarah Jamie Lewis 49e0d849fa Large API Refactor in prep for autobindings 2023-02-21 15:55:14 -08:00
Sarah Jamie Lewis 84c8c8bc11 Rename API
continuous-integration/drone/pr Build is passing Details
2023-01-25 12:34:58 -08:00
Sarah Jamie Lewis 371dad5f90 FileSharing Experiments / Move Experiment Handling to App and Cwtch Peer
continuous-integration/drone/pr Build is pending Details
2023-01-25 12:32:26 -08:00
Sarah Jamie Lewis e86eb45d22 Initial Prototype of Event Hooks
continuous-integration/drone/pr Build is passing Details
2023-01-05 13:52:43 -08:00
Sarah Jamie Lewis 445355b0fe Log Errors related to Sharing Files 2023-01-05 11:21:08 -08:00
22 changed files with 971 additions and 214 deletions

View File

@ -3,6 +3,8 @@ package app
import (
"cwtch.im/cwtch/app/plugins"
"cwtch.im/cwtch/event"
"cwtch.im/cwtch/extensions"
"cwtch.im/cwtch/functionality/filesharing"
"cwtch.im/cwtch/model"
"cwtch.im/cwtch/model/attr"
"cwtch.im/cwtch/model/constants"
@ -29,16 +31,28 @@ type application struct {
engines map[string]connections.Engine
appBus event.Manager
appmutex sync.Mutex
settings *GlobalSettingsFile
}
func (app *application) IsFeatureEnabled(experiment string) bool {
settings := app.ReadSettings()
if settings.ExperimentsEnabled {
if status, exists := settings.Experiments[experiment]; exists {
return status
}
}
return false
}
// Application is a full cwtch peer application. It allows management, usage and storage of multiple peers
type Application interface {
LoadProfiles(password string)
CreatePeer(name string, password string, attributes map[attr.ZonedPath]string)
// Deprecated in 1.10
CreateTaggedPeer(name string, password string, tag string)
CreateProfile(name string, password string, autostart bool)
ImportProfile(exportedCwtchFile string, password string) (peer.CwtchPeer, error)
DeletePeer(onion string, currentPassword string)
EnhancedImportProfile(exportedCwtchFile string, password string) string
DeleteProfile(onion string, currentPassword string)
AddPeerPlugin(onion string, pluginID plugins.PluginID)
GetPrimaryBus() event.Manager
@ -47,9 +61,13 @@ type Application interface {
QueryACNVersion()
ActivateEngines(doListn, doPeers, doServers bool)
ActivatePeerEngine(onion string, doListen, doPeers, doServers bool)
ActivatePeerEngine(onion string)
DeactivatePeerEngine(onion string)
ReadSettings() GlobalSettings
UpdateSettings(settings GlobalSettings)
IsFeatureEnabled(experiment string) bool
ShutdownPeer(string)
Shutdown()
@ -60,12 +78,24 @@ type Application interface {
// LoadProfileFn is the function signature for a function in an app that loads a profile
type LoadProfileFn func(profile peer.CwtchPeer)
// NewApp creates a new app with some environment awareness and initializes a Tor Manager
func NewApp(acn connectivity.ACN, appDirectory string) Application {
func LoadAppSettings(appDirectory string) *GlobalSettingsFile {
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)
}
return settings
}
// NewApp creates a new app with some environment awareness and initializes a Tor Manager
func NewApp(acn connectivity.ACN, appDirectory string, 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)
app.acn = acn
@ -78,6 +108,34 @@ 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)
// Explicitly toggle blocking/unblocking of unknown connections for profiles
// that have been loaded.
if settings.BlockUnknownConnections {
profile.BlockUnknownConnections()
} else {
profile.AllowUnknownConnections()
}
}
}
// ListProfiles returns a map of onions to their profile's Name
func (app *application) ListProfiles() []string {
var keys []string
@ -124,11 +182,45 @@ func (ap *application) AddPlugin(peerid string, id plugins.PluginID, bus event.M
}
}
func (app *application) CreateProfile(name string, password string, autostart bool) {
autostartVal := constants.True
if !autostart {
autostartVal = constants.False
}
tagVal := constants.ProfileTypeV1Password
if password == DefactoPasswordForUnencryptedProfiles {
tagVal = constants.ProfileTypeV1DefaultPassword
}
app.CreatePeer(name, password, map[attr.ZonedPath]string{
attr.ProfileZone.ConstructZonedPath(constants.Tag): tagVal,
attr.ProfileZone.ConstructZonedPath(constants.PeerAutostart): autostartVal,
})
}
// Deprecated in 1.10
func (app *application) CreateTaggedPeer(name string, password string, tag string) {
app.CreatePeer(name, password, map[attr.ZonedPath]string{attr.ProfileZone.ConstructZonedPath(constants.Tag): tag})
}
func (app *application) setupPeer(profile peer.CwtchPeer) {
eventBus := event.NewEventManager()
app.eventBuses[profile.GetOnion()] = eventBus
// Initialize the Peer with the Given Event Bus
app.peers[profile.GetOnion()] = profile
profile.Init(app.eventBuses[profile.GetOnion()])
// Update the Peer with the Most Recent Experiment State...
settings := app.settings.ReadGlobalSettings()
profile.UpdateExperiments(settings.ExperimentsEnabled, settings.Experiments)
app.registerHooks(profile)
// Register the Peer With Application Plugins..
app.AddPeerPlugin(profile.GetOnion(), plugins.CONNECTIONRETRY) // Now Mandatory
}
func (app *application) CreatePeer(name string, password string, attributes map[attr.ZonedPath]string) {
app.appmutex.Lock()
defer app.appmutex.Unlock()
@ -142,25 +234,26 @@ func (app *application) CreatePeer(name string, password string, attributes map[
return
}
eventBus := event.NewEventManager()
app.eventBuses[profile.GetOnion()] = eventBus
profile.Init(app.eventBuses[profile.GetOnion()])
app.peers[profile.GetOnion()] = profile
app.setupPeer(profile)
for zp, val := range attributes {
zone, key := attr.ParseZone(zp.ToString())
profile.SetScopedZonedAttribute(attr.LocalScope, zone, key, val)
}
app.AddPeerPlugin(profile.GetOnion(), plugins.CONNECTIONRETRY) // Now Mandatory
app.appBus.Publish(event.NewEvent(event.NewPeer, map[event.Field]string{event.Identity: profile.GetOnion(), event.Created: event.True}))
}
func (app *application) DeletePeer(onion string, password string) {
log.Infof("DeletePeer called on %v\n", onion)
func (app *application) DeleteProfile(onion string, password string) {
log.Debugf("DeleteProfile called on %v\n", onion)
app.appmutex.Lock()
defer app.appmutex.Unlock()
// allow a blank password to delete "unencrypted" accounts...
if password == "" {
password = DefactoPasswordForUnencryptedProfiles
}
if app.peers[onion].CheckPassword(password) {
// soft-shutdown
app.peers[onion].Shutdown()
@ -190,6 +283,14 @@ func (app *application) ImportProfile(exportedCwtchFile string, password string)
return profile, err
}
func (app *application) EnhancedImportProfile(exportedCwtchFile string, password string) string {
_, err := app.ImportProfile(exportedCwtchFile, password)
if err == nil {
return ""
}
return err.Error()
}
// LoadProfiles takes a password and attempts to load any profiles it can from storage with it and create Peers for them
func (app *application) LoadProfiles(password string) {
count := 0
@ -242,6 +343,12 @@ func (app *application) LoadProfiles(password string) {
}
}
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
func (app *application) installProfile(profile peer.CwtchPeer) bool {
app.appmutex.Lock()
@ -249,11 +356,8 @@ func (app *application) installProfile(profile peer.CwtchPeer) bool {
// Only attempt to finalize the profile if we don't have one loaded...
if app.peers[profile.GetOnion()] == nil {
eventBus := event.NewEventManager()
app.eventBuses[profile.GetOnion()] = eventBus
profile.Init(app.eventBuses[profile.GetOnion()])
app.peers[profile.GetOnion()] = profile
app.AddPeerPlugin(profile.GetOnion(), plugins.CONNECTIONRETRY) // Now Mandatory
app.setupPeer(profile)
// Finalize the Creation of Peer / Notify any Interfaces..
app.appBus.Publish(event.NewEvent(event.NewPeer, map[event.Field]string{event.Identity: profile.GetOnion(), event.Created: event.False}))
return true
}
@ -288,7 +392,7 @@ func (app *application) ActivateEngines(doListen, doPeers, doServers bool) {
}
// ActivePeerEngine creates a peer engine for use with an ACN, should be called once the underlying ACN is online
func (app *application) ActivatePeerEngine(onion string, doListen, doPeers, doServers bool) {
func (app *application) ActivatePeerEngine(onion string) {
profile := app.GetPeer(onion)
if profile != nil {
if _, exists := app.engines[onion]; !exists {
@ -296,10 +400,10 @@ func (app *application) ActivatePeerEngine(onion string, doListen, doPeers, doSe
app.eventBuses[profile.GetOnion()].Publish(event.NewEventList(event.ProtocolEngineCreated))
app.QueryACNStatus()
if doListen {
if true {
profile.Listen()
}
profile.StartConnections(doPeers, doServers)
profile.StartConnections(true, true)
}
}
}

6
app/app_constants.go Normal file
View File

@ -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"

View File

@ -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):

146
app/settings.go Normal file
View File

@ -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)
}
}

View File

@ -228,7 +228,8 @@ type Field string
const (
// A peers local onion address
Onion = Field("Onion")
Onion = Field("Onion")
ProfileOnion = Field("ProfileOnion")
RemotePeer = Field("RemotePeer")
LastSeen = Field("LastSeen")

View File

@ -0,0 +1,65 @@
package extensions
import (
"cwtch.im/cwtch/event"
"cwtch.im/cwtch/model"
"cwtch.im/cwtch/model/attr"
"cwtch.im/cwtch/model/constants"
"cwtch.im/cwtch/peer"
"git.openprivacy.ca/openprivacy/log"
"strconv"
)
// ProfileValueExtension implements custom Profile Names over Cwtch
type ProfileValueExtension struct {
}
func (pne ProfileValueExtension) EventsToRegister() []event.Type {
return nil
}
func (pne ProfileValueExtension) ExperimentsToRegister() []string {
return nil
}
func (pne ProfileValueExtension) OnEvent(event event.Event, profile peer.CwtchPeer) {
// nop
}
// OnContactReceiveValue for ProfileValueExtension handles saving specific Public Profile Values like Profile Name
func (pne ProfileValueExtension) OnContactReceiveValue(profile peer.CwtchPeer, conversation model.Conversation, szp attr.ScopedZonedPath, value string, exists bool) {
// Allow public profile parameters to be added as contact specific attributes...
scope, zone, _ := szp.GetScopeZonePath()
if exists && scope.IsPublic() && zone == attr.ProfileZone {
err := profile.SetConversationAttribute(conversation.ID, szp, value)
if err != nil {
log.Errorf("error setting conversation attribute %v", err)
}
}
}
// 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.Debugf("Looking up public | conversation scope/zone %v", szp.ToString())
if scope.IsPublic() || scope.IsConversation() {
val, exists := profile.GetScopedZonedAttribute(scope, zone, zpath)
// NOTE: Temporary Override because UI currently wipes names if it can't find them...
if !exists && zone == attr.UnknownZone && zpath == constants.Name {
val, exists = profile.GetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants.Name)
}
// Construct a Response
resp := event.NewEvent(event.SendRetValMessageToPeer, map[event.Field]string{event.ConversationID: strconv.Itoa(conversation.ID), event.RemotePeer: conversation.Handle, event.Exists: strconv.FormatBool(exists)})
resp.EventID = eventID
if exists {
resp.Data[event.Data] = val
} else {
resp.Data[event.Data] = ""
}
log.Debugf("Responding with SendRetValMessageToPeer exists:%v data: %v\n", exists, val)
profile.PublishEvent(resp)
}
}

View File

@ -2,12 +2,14 @@ package filesharing
import (
"crypto/rand"
"cwtch.im/cwtch/event"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"math"
"math/bits"
"os"
path "path/filepath"
"regexp"
@ -28,6 +30,109 @@ import (
type Functionality struct {
}
func (f Functionality) EventsToRegister() []event.Type {
return []event.Type{event.ManifestReceived, event.FileDownloaded}
}
func (f Functionality) ExperimentsToRegister() []string {
return []string{constants.FileSharingExperiment}
}
// OnEvent handles File Sharing Hooks like Manifest Received and FileDownloaded
func (f Functionality) OnEvent(ev event.Event, profile peer.CwtchPeer) {
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))
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)
} 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,
}))
}
}
}
}
} else {
log.Errorf("error saving manifest: %v", err)
}
} else {
log.Errorf("found manifest path but not download path for %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")
}
} else {
log.Errorf("profile called filesharing experiment OnContactReceiveValue even though file sharing was not enabled. This is likely a programming error.")
}
}
func (f Functionality) OnContactRequestValue(profile peer.CwtchPeer, conversation model.Conversation, eventID string, path attr.ScopedZonedPath) {
// nop
}
func (f Functionality) OnContactReceiveValue(profile peer.CwtchPeer, conversation model.Conversation, path attr.ScopedZonedPath, value string, exists bool) {
// 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
// Note: Experiment maps are currently in libcwtch-go
func FunctionalityGate(experimentMap map[string]bool) (*Functionality, error) {
@ -73,10 +178,74 @@ func (om *OverlayMessage) ShouldAutoDL() bool {
return false
}
func (f *Functionality) VerifyOrResumeDownload(profile peer.CwtchPeer, conversation int, fileKey string) {
if manifestFilePath, exists := profile.GetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.manifest", fileKey)); exists {
if downloadfilepath, exists := profile.GetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.path", fileKey)); exists {
log.Debugf("resuming %s", fileKey)
f.DownloadFile(profile, conversation, downloadfilepath, manifestFilePath, fileKey, files.MaxManifestSize*files.DefaultChunkSize)
} else {
log.Errorf("found manifest path but not download path for %s", fileKey)
}
} else {
log.Errorf("no stored manifest path found for %s", fileKey)
}
}
func (f *Functionality) CheckDownloadStatus(profile peer.CwtchPeer, fileKey string) {
path, _ := profile.GetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.path", fileKey))
if value, exists := profile.GetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.complete", fileKey)); exists && value == event.True {
profile.PublishEvent(event.NewEvent(event.FileDownloaded, map[event.Field]string{
event.ProfileOnion: profile.GetOnion(),
event.FileKey: fileKey,
event.FilePath: path,
event.TempFile: "",
}))
} else {
log.Debugf("CheckDownloadStatus found .path but not .complete")
profile.PublishEvent(event.NewEvent(event.FileDownloadProgressUpdate, map[event.Field]string{
event.ProfileOnion: profile.GetOnion(),
event.FileKey: fileKey,
event.Progress: "-1",
event.FileSizeInChunks: "-1",
event.FilePath: path,
}))
}
}
func (f *Functionality) EnhancedShareFile(profile peer.CwtchPeer, conversationID int, sharefilepath string) string {
fileKey, overlay, err := f.ShareFile(sharefilepath, profile)
if err != nil {
log.Errorf("error sharing file: %v", err)
} else if conversationID == -1 {
// FIXME: At some point we might want to allow arbitrary public files, but for now this API will assume
// there is only one, and it is the custom profile image...
profile.SetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants.CustomProfileImageKey, fileKey)
} else {
// Set a new attribute so we can associate this download with this conversation...
profile.SetConversationAttribute(conversationID, attr.ConversationScope.ConstructScopedZonedPath(attr.FilesharingZone.ConstructZonedPath(fileKey)), "")
id, err := profile.SendMessage(conversationID, overlay)
if err == nil {
return profile.EnhancedGetMessageById(conversationID, id)
}
}
return ""
}
// DownloadFileDefaultLimit given a profile, a conversation handle and a file sharing key, start off a download process
// to downloadFilePath with a default filesize limit
func (f *Functionality) DownloadFileDefaultLimit(profile peer.CwtchPeer, conversationID int, downloadFilePath string, manifestFilePath string, key string) error {
return f.DownloadFile(profile, conversationID, downloadFilePath, manifestFilePath, key, files.MaxManifestSize*files.DefaultChunkSize)
}
// DownloadFile given a profile, a conversation handle and a file sharing key, start off a download process
// 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")
@ -109,14 +278,37 @@ func (f *Functionality) DownloadFile(profile peer.CwtchPeer, conversationID int,
return nil
}
// startFileShare is a private method used to finalize a file share and publish it to the protocol engine for processing.
func (f *Functionality) startFileShare(profile peer.CwtchPeer, filekey string, manifest string) error {
tsStr, exists := profile.GetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.ts", filekey))
if exists {
ts, err := strconv.ParseInt(tsStr, 10, 64)
if err != nil || ts < time.Now().Unix()-2592000 {
log.Errorf("ignoring request to download a file offered more than 30 days ago")
return err
}
}
// set the filekey status to active
profile.SetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.active", filekey), constants.True)
profile.PublishEvent(event.NewEvent(event.ShareManifest, map[event.Field]string{event.FileKey: filekey, event.SerializedManifest: manifest}))
return nil
}
// 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 {
// everything is in order, so reshare this file with the engine
profile.ShareFile(filekey, manifest)
return nil
log.Debugf("restarting file share: %v", filekey)
return f.startFileShare(profile, filekey, manifest)
}
return fmt.Errorf("manifest does not exist for filekey: %v", filekey)
}
@ -124,6 +316,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
@ -132,6 +330,7 @@ func (f *Functionality) ReShareFiles(profile peer.CwtchPeer) error {
for _, key := range keys {
// only look at timestamp keys
// this is an arbitrary choice
if strings.HasSuffix(key, ".ts") {
_, zonedpath := attr.ParseScope(key)
_, keypath := attr.ParseZone(zonedpath)
@ -148,7 +347,12 @@ func (f *Functionality) ReShareFiles(profile peer.CwtchPeer) error {
// Then attempt to share this file again...
// TODO: In the future this would be the point to change the timestamp and reshare the file...
if err == nil && sharedFile.Active {
f.RestartFileShare(profile, filekey)
err := f.RestartFileShare(profile, filekey)
if err != nil {
log.Errorf("could not reshare file: %v", err)
}
} else {
log.Errorf("could not get fileshare info %v", err)
}
}
}
@ -184,6 +388,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
@ -231,7 +441,7 @@ func (f *Functionality) ShareFile(filepath string, profile peer.CwtchPeer) (stri
profile.SetScopedZonedAttribute(attr.ConversationScope, attr.FilesharingZone, fmt.Sprintf("%s.manifest", key), string(serializedManifest))
profile.SetScopedZonedAttribute(attr.ConversationScope, attr.FilesharingZone, fmt.Sprintf("%s.manifest.size", key), strconv.Itoa(int(math.Ceil(float64(len(serializedManifest)-lenDiff)/float64(files.DefaultChunkSize)))))
profile.ShareFile(key, string(serializedManifest))
err = f.startFileShare(profile, key, string(serializedManifest))
return key, string(wrapperJSON), err
}
@ -258,6 +468,14 @@ type SharedFile struct {
Expired bool
}
func (f *Functionality) EnhancedGetSharedFiles(profile peer.CwtchPeer, conversationID int) string {
data, err := json.Marshal(f.GetSharedFiles(profile, conversationID))
if err == nil {
return string(data)
}
return ""
}
// GetSharedFiles returns all file shares associated with a given conversation
func (f *Functionality) GetSharedFiles(profile peer.CwtchPeer, conversationID int) []SharedFile {
sharedFiles := []SharedFile{}
@ -323,3 +541,17 @@ func GenerateDownloadPath(basePath, fileName string, overwrite bool) (filePath,
}
return
}
// 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}))
}
// 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{}))
}

View File

@ -23,6 +23,12 @@ type Scope string
// ScopedZonedPath typed path with a scope and a zone
type ScopedZonedPath string
func (szp ScopedZonedPath) GetScopeZonePath() (Scope, Zone, string) {
scope, path := ParseScope(string(szp))
zone, zpath := ParseZone(path)
return scope, zone, zpath
}
// scopes for attributes
const (
// on a peer, local and peer supplied data

View File

@ -56,3 +56,5 @@ const SyncPreLastMessageTime = "SyncPreLastMessageTime"
const SyncMostRecentMessageTime = "SyncMostRecentMessageTime"
const AttrLastConnectionTime = "last-connection-time"
const PeerAutostart = "autostart"
const Archived = "archived"

View File

@ -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"}

39
model/experiments.go Normal file
View File

@ -0,0 +1,39 @@
package model
import "sync"
// 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
lock sync.Mutex
}
// 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
}
// go will sometimes panic if we do not lock this read-only map...
e.lock.Lock()
defer e.lock.Unlock()
enabled, exists := e.experiments[experiment]
if !exists {
return false
}
return enabled
}

View File

@ -14,10 +14,8 @@ import (
"git.openprivacy.ca/openprivacy/connectivity"
"git.openprivacy.ca/openprivacy/connectivity/tor"
"golang.org/x/crypto/ed25519"
"math/bits"
"os"
path "path/filepath"
"runtime"
"sort"
"strconv"
"strings"
@ -28,7 +26,6 @@ import (
"cwtch.im/cwtch/model"
"cwtch.im/cwtch/model/attr"
"cwtch.im/cwtch/protocol/connections"
"cwtch.im/cwtch/protocol/files"
"git.openprivacy.ca/openprivacy/log"
)
@ -38,8 +35,7 @@ const lastReceivedSignature = "LastReceivedSignature"
var autoHandleableEvents = map[event.Type]bool{event.EncryptedGroupMessage: true, event.PeerStateChange: true,
event.ServerStateChange: true, event.NewGroupInvite: true, event.NewMessageFromPeerEngine: true,
event.PeerAcknowledgement: true, event.PeerError: true, event.SendMessageToPeerError: true, event.SendMessageToGroupError: true,
event.NewGetValMessageFromPeer: true, event.NewRetValMessageFromPeer: true, event.ProtocolEngineStopped: true, event.RetryServerRequest: true,
event.ManifestSizeReceived: true, event.ManifestReceived: true, event.FileDownloaded: true, event.TriggerAntispamCheck: true}
event.NewGetValMessageFromPeer: true, event.NewRetValMessageFromPeer: true, event.ProtocolEngineStopped: true, event.RetryServerRequest: true, event.TriggerAntispamCheck: true}
// DefaultEventsToHandle specifies which events will be subscribed to
//
@ -58,8 +54,6 @@ var DefaultEventsToHandle = []event.Type{
event.ServerStateChange,
event.SendMessageToPeerError,
event.NewRetValMessageFromPeer,
event.ManifestReceived,
event.FileDownloaded,
event.TriggerAntispamCheck,
}
@ -74,6 +68,132 @@ 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
experiments model.Experiments
experimentsLock sync.Mutex
}
func (cp *cwtchPeer) EnhancedImportBundle(importString string) string {
return cp.ImportBundle(importString).Error()
}
func (cp *cwtchPeer) EnhancedGetMessages(conversation int, index int, count int) string {
var emessages []EnhancedMessage = make([]EnhancedMessage, count)
messages, err := cp.GetMostRecentMessages(conversation, 0, index, count)
if err == nil {
for i, message := range messages {
time, _ := time.Parse(time.RFC3339Nano, message.Attr[constants.AttrSentTimestamp])
emessages[i].Message = model.Message{
Message: message.Body,
Acknowledged: message.Attr[constants.AttrAck] == constants.True,
Error: message.Attr[constants.AttrErr],
PeerID: message.Attr[constants.AttrAuthor],
Timestamp: time,
}
emessages[i].ID = message.ID
emessages[i].Attributes = message.Attr
emessages[i].ContentHash = model.CalculateContentHash(message.Attr[constants.AttrAuthor], message.Body)
}
}
bytes, _ := json.Marshal(emessages)
return string(bytes)
}
func (cp *cwtchPeer) EnhancedGetMessageById(conversation int, messageID int) string {
var message EnhancedMessage
dbmessage, attr, err := cp.GetChannelMessage(conversation, 0, messageID)
if err == nil {
time, _ := time.Parse(time.RFC3339Nano, attr[constants.AttrSentTimestamp])
message.Message = model.Message{
Message: dbmessage,
Acknowledged: attr[constants.AttrAck] == constants.True,
Error: attr[constants.AttrErr],
PeerID: attr[constants.AttrAuthor],
Timestamp: time,
}
message.ID = messageID
message.Attributes = attr
message.ContentHash = model.CalculateContentHash(attr[constants.AttrAuthor], dbmessage)
}
bytes, _ := json.Marshal(message)
return string(bytes)
}
func (cp *cwtchPeer) EnhancedGetMessageByContentHash(conversation int, contentHash string) string {
var message EnhancedMessage
offset, err := cp.GetChannelMessageByContentHash(conversation, 0, contentHash)
if err == nil {
messages, err := cp.GetMostRecentMessages(conversation, 0, offset, 1)
if err == nil {
time, _ := time.Parse(time.RFC3339Nano, messages[0].Attr[constants.AttrSentTimestamp])
message.Message = model.Message{
Message: messages[0].Body,
Acknowledged: messages[0].Attr[constants.AttrAck] == constants.True,
Error: messages[0].Attr[constants.AttrErr],
PeerID: messages[0].Attr[constants.AttrAuthor],
Timestamp: time,
}
message.ID = messages[0].ID
message.Attributes = messages[0].Attr
message.LocalIndex = offset
message.ContentHash = contentHash
} else {
log.Errorf("error fetching local index {} ", err)
}
}
bytes, _ := json.Marshal(message)
return string(bytes)
}
func (cp *cwtchPeer) EnhancedSendMessage(conversation int, message string) string {
mid, err := cp.SendMessage(conversation, message)
if err == nil {
return cp.EnhancedGetMessageById(conversation, mid)
}
return ""
}
func (cp *cwtchPeer) ArchiveConversation(conversationID int) {
cp.SetConversationAttribute(conversationID, attr.LocalScope.ConstructScopedZonedPath(attr.ProfileZone.ConstructZonedPath(constants.Archived)), constants.True)
}
// 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)
}
func (cp *cwtchPeer) RegisterHook(extension ProfileHooks) {
cp.extensionLock.Lock()
defer cp.extensionLock.Unlock()
// Register Requested Events
for _, event := range extension.EventsToRegister() {
cp.eventBus.Subscribe(event, cp.queue)
}
cp.extensions = append(cp.extensions, ConstructHook(extension))
}
func (cp *cwtchPeer) StoreCachedTokens(tokenServer string, tokens []*privacypass.Token) {
@ -87,7 +207,7 @@ func (cp *cwtchPeer) StoreCachedTokens(tokenServer string, tokens []*privacypass
}
}
func (cp *cwtchPeer) Export(file string) error {
func (cp *cwtchPeer) ExportProfile(file string) error {
cp.mutex.Lock()
defer cp.mutex.Unlock()
return cp.storage.Export(file)
@ -141,7 +261,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)
@ -1165,33 +1285,6 @@ func (cp *cwtchPeer) storeMessage(handle string, message string, sent time.Time)
return cp.storage.InsertMessage(ci.ID, 0, message, model.Attributes{constants.AttrAuthor: handle, constants.AttrAck: event.True, constants.AttrSentTimestamp: sent.Format(time.RFC3339Nano)}, signature, model.CalculateContentHash(handle, message))
}
// ShareFile begins hosting the given serialized manifest
func (cp *cwtchPeer) ShareFile(fileKey string, serializedManifest string) {
tsStr, exists := cp.GetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.ts", fileKey))
if exists {
ts, err := strconv.ParseInt(tsStr, 10, 64)
if err != nil || ts < time.Now().Unix()-2592000 {
log.Errorf("ignoring request to download a file offered more than 30 days ago")
return
}
}
// set the filekey status to active
cp.SetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.active", fileKey), constants.True)
cp.eventBus.Publish(event.NewEvent(event.ShareManifest, map[event.Field]string{event.FileKey: fileKey, event.SerializedManifest: serializedManifest}))
}
// StopFileShare sends a message to the ProtocolEngine to cease sharing a particular file
func (cp *cwtchPeer) StopFileShare(fileKey string) {
// set the filekey status to inactive
cp.SetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.active", fileKey), constants.False)
cp.eventBus.Publish(event.NewEvent(event.StopFileShare, map[event.Field]string{event.FileKey: fileKey}))
}
// StopAllFileShares sends a message to the ProtocolEngine to cease sharing all files
func (cp *cwtchPeer) StopAllFileShares() {
cp.eventBus.Publish(event.NewEvent(event.StopAllFileShares, map[event.Field]string{}))
}
// eventHandler process events from other subsystems
func (cp *cwtchPeer) eventHandler() {
for {
@ -1288,128 +1381,60 @@ func (cp *cwtchPeer) eventHandler() {
path := ev.Data[event.Path]
log.Debugf("NewGetValMessageFromPeer for %v.%v from %v\n", scope, path, onion)
conversationInfo, err := cp.FetchConversationInfo(onion)
log.Debugf("confo info lookup newgetval %v %v %v", onion, conversationInfo, err)
// only accepted contacts can look up information
if conversationInfo != nil && conversationInfo.Accepted {
scope := attr.IntoScope(scope)
if scope.IsPublic() || scope.IsConversation() {
zone, zpath := attr.ParseZone(path)
val, exists := cp.GetScopedZonedAttribute(scope, zone, zpath)
// Type Safe Scoped/Zoned Path
zscope := attr.IntoScope(scope)
zone, zpath := attr.ParseZone(path)
scopedZonedPath := zscope.ConstructScopedZonedPath(zone.ConstructZonedPath(zpath))
// NOTE: Temporary Override because UI currently wipes names if it can't find them...
if !exists && zone == attr.UnknownZone && path == constants.Name {
val, exists = cp.GetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants.Name)
// Safe Access to Extensions
cp.extensionLock.Lock()
log.Debugf("checking extension...%v", cp.extensions)
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
}
resp := event.NewEvent(event.SendRetValMessageToPeer, map[event.Field]string{event.ConversationID: strconv.Itoa(conversationInfo.ID), event.RemotePeer: onion, event.Exists: strconv.FormatBool(exists)})
resp.EventID = ev.EventID
if exists {
resp.Data[event.Data] = val
} else {
resp.Data[event.Data] = ""
}
log.Debugf("Responding with SendRetValMessageToPeer exists:%v data: %v\n", exists, val)
cp.eventBus.Publish(resp)
extension.extension.OnContactRequestValue(cp, *conversationInfo, ev.EventID, scopedZonedPath)
}
cp.extensionLock.Unlock()
}
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 := cp.GetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%v.manifest", fileKey))
if exists {
downloadFilePath, exists := cp.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 := cp.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)
} else {
tempFile := ""
if runtime.GOOS == "android" {
tempFile = manifestFilePath[0 : len(manifestFilePath)-len(".manifest")]
log.Debugf("derived android temp path: %v", tempFile)
}
cp.eventBus.Publish(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("found manifest path but not download path for %v", fileKey)
}
} else {
log.Errorf("no download path found for manifest: %v", fileKey)
}
case event.FileDownloaded:
fileKey := ev.Data[event.FileKey]
cp.SetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.complete", fileKey), "true")
case event.NewRetValMessageFromPeer:
onion := ev.Data[event.RemotePeer]
handle := ev.Data[event.RemotePeer]
scope := ev.Data[event.Scope]
path := ev.Data[event.Path]
val := ev.Data[event.Data]
exists, _ := strconv.ParseBool(ev.Data[event.Exists])
log.Debugf("NewRetValMessageFromPeer %v %v %v %v %v\n", onion, scope, path, exists, val)
if exists {
log.Debugf("NewRetValMessageFromPeer %v %v %v %v %v\n", handle, scope, path, exists, val)
// Handle File Sharing Metadata
// TODO This probably should be broken out to it's own code..
zone, path := attr.ParseZone(path)
if attr.Scope(scope).IsConversation() && zone == attr.FilesharingZone && strings.HasSuffix(path, ".manifest.size") {
fileKey := strings.Replace(path, ".manifest.size", "", 1)
size, err := strconv.Atoi(val)
// 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 {
cp.eventBus.Publish(event.NewEvent(event.ManifestSizeReceived, map[event.Field]string{event.FileKey: fileKey, event.ManifestSize: val, event.Handle: onion}))
} else {
cp.eventBus.Publish(event.NewEvent(event.ManifestError, map[event.Field]string{event.FileKey: fileKey, event.Handle: onion}))
}
}
conversationInfo, _ := cp.FetchConversationInfo(handle)
// only accepted contacts can look up information
if conversationInfo != nil && conversationInfo.Accepted {
// Type Safe Scoped/Zoned Path
zscope := attr.IntoScope(scope)
zone, zpath := attr.ParseZone(path)
scopedZonedPath := zscope.ConstructScopedZonedPath(zone.ConstructZonedPath(zpath))
// Allow public profile parameters to be added as peer specific attributes...
if attr.Scope(scope).IsPublic() && zone == attr.ProfileZone {
ci, err := cp.FetchConversationInfo(onion)
log.Debugf("fetch conversation info %v %v", ci, err)
if ci != nil && err == nil {
err := cp.SetConversationAttribute(ci.ID, attr.Scope(scope).ConstructScopedZonedPath(zone.ConstructZonedPath(path)), val)
if err != nil {
log.Errorf("error setting conversation attribute %v", err)
}
// 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()
}
case event.PeerStateChange:
handle := ev.Data[event.RemotePeer]
@ -1491,14 +1516,48 @@ func (cp *cwtchPeer) eventHandler() {
}
}
default:
if ev.EventType != "" {
log.Errorf("peer event handler received an event it was not subscribed for: %v", ev.EventType)
// invalid event, signifies shutdown
if ev.EventType == "" {
return
}
// Otherwise, obtain Safe Access to Extensions
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)
processed = true
}
}
cp.extensionLock.Unlock()
if !processed {
log.Errorf("cwtch profile received an event that it (or an extension) was unable to handle. this is very likely a programming error: %v", ev.EventType)
}
return
}
}
}
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

View File

@ -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)
}

48
peer/hooks.go Normal file
View File

@ -0,0 +1,48 @@
package peer
import (
"cwtch.im/cwtch/event"
"cwtch.im/cwtch/model"
"cwtch.im/cwtch/model/attr"
)
type ProfileHooks interface {
// EventsToRegister returns a set of events that the extension is interested hooking
EventsToRegister() []event.Type
// ExperimentsToRegister returns a set of experiments that the extension is interested in being notified about
ExperimentsToRegister() []string
// OnEvent is called whenever an event Registered with RegisterEvents is called
OnEvent(event event.Event, profile CwtchPeer)
// OnContactRequestValue is Hooked when a contact sends a request for the given path
OnContactRequestValue(profile CwtchPeer, conversation model.Conversation, eventID string, path attr.ScopedZonedPath)
// 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)
}
type ProfileHook struct {
extension ProfileHooks
events map[event.Type]bool
experiments map[string]bool
}
func ConstructHook(extension ProfileHooks) ProfileHook {
events := make(map[event.Type]bool)
for _, event := range extension.EventsToRegister() {
events[event] = true
}
experiments := make(map[string]bool)
for _, experiment := range extension.ExperimentsToRegister() {
experiments[experiment] = true
}
return ProfileHook{
extension,
events,
experiments,
}
}

View File

@ -48,6 +48,10 @@ type ModifyServers interface {
// SendMessages enables a caller to sender messages to a contact
type SendMessages interface {
SendMessage(conversation int, message string) (int, error)
// EnhancedSendMessage Attempts to Send a Message and Immediately Attempts to Lookup the Message in the Database
EnhancedSendMessage(conversation int, message string) string
SendInviteToConversation(conversationID int, inviteConversationID int) (int, error)
SendScopedZonedGetValToContact(conversationID int, scope attr.Scope, zone attr.Zone, key string)
}
@ -101,10 +105,12 @@ type CwtchPeer interface {
// Import Bundle
ImportBundle(string) error
EnhancedImportBundle(string) string
// New Unified Conversation Interfaces
NewContactConversation(handle string, acl model.AccessControl, accepted bool) (int, error)
FetchConversations() ([]*model.Conversation, error)
ArchiveConversation(conversation int)
GetConversationInfo(conversation int) (*model.Conversation, error)
FetchConversationInfo(handle string) (*model.Conversation, error)
AcceptConversation(conversation int) error
@ -121,11 +127,14 @@ type CwtchPeer interface {
GetMostRecentMessages(conversation int, channel int, offset int, limit int) ([]model.ConversationMessage, error)
UpdateMessageAttribute(conversation int, channel int, id int, key string, value string) error
// File Sharing APIS
// TODO move these to feature protected interfaces
ShareFile(fileKey string, serializedManifest string)
StopFileShare(fileKey string)
StopAllFileShares()
// EnhancedGetMessageById returns a json-encoded enhanced message, suitable for rendering in a UI
EnhancedGetMessageById(conversation int, mid int) string
// EnhancedGetMessageByContentHash returns a json-encoded enhanced message, suitable for rendering in a UI
EnhancedGetMessageByContentHash(conversation int, hash string) string
// EnhancedGetMessages returns a set of json-encoded enhanced messages, suitable for rendering in a UI
EnhancedGetMessages(conversation int, index int, count int) string
// Server Token APIS
// TODO move these to feature protected interfaces
@ -134,6 +143,20 @@ type CwtchPeer interface {
// Profile Management
CheckPassword(password string) bool
ChangePassword(oldpassword string, newpassword string, newpasswordAgain string) error
Export(file string) error
ExportProfile(file string) error
Delete()
PublishEvent(resp event.Event)
RegisterHook(hook ProfileHooks)
UpdateExperiments(enabled bool, experiments map[string]bool)
IsFeatureEnabled(featureName string) bool
}
// EnhancedMessage wraps a Cwtch model.Message with some additional data to reduce calls from the UI.
type EnhancedMessage struct {
model.Message
ID int // the actual ID of the message in the database (not the row number)
LocalIndex int // local index in the DB (row #). Can be empty (most calls supply it) but lookup by hash will fill it
ContentHash string
ContactImage string
Attributes map[string]string
}

View File

@ -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)

View File

@ -34,6 +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)
fsss.activeShares.Store(fileKey, &manifest)
}

View File

@ -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
}

View File

@ -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)

View File

@ -139,7 +139,7 @@ func TestCwtchPeerIntegration(t *testing.T) {
const ServerAddr = "nfhxzvzxinripgdh4t2m4xcy3crf6p4cbhectgckuj3idsjsaotgowad"
serverKeyBundle, _ := base64.StdEncoding.DecodeString(ServerKeyBundleBase64)
app := app2.NewApp(acn, "./storage")
app := app2.NewApp(acn, "./storage", app2.LoadAppSettings("./storage"))
usr, _ := user.Current()
cwtchDir := path.Join(usr.HomeDir, ".cwtch")
@ -152,31 +152,31 @@ func TestCwtchPeerIntegration(t *testing.T) {
// ***** cwtchPeer setup *****
log.Infoln("Creating Alice...")
app.CreateTaggedPeer("Alice", "asdfasdf", "test")
app.CreateProfile("Alice", "asdfasdf", true)
log.Infoln("Creating Bob...")
app.CreateTaggedPeer("Bob", "asdfasdf", "test")
app.CreateProfile("Bob", "asdfasdf", true)
log.Infoln("Creating Carol...")
app.CreateTaggedPeer("Carol", "asdfasdf", "test")
app.CreateProfile("Carol", "asdfasdf", true)
alice := app2.WaitGetPeer(app, "Alice")
aliceBus := app.GetEventBus(alice.GetOnion())
app.ActivatePeerEngine(alice.GetOnion(), true, true, true)
app.ActivatePeerEngine(alice.GetOnion())
log.Infoln("Alice created:", alice.GetOnion())
alice.SetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants.Name, "Alice")
alice.AutoHandleEvents([]event.Type{event.PeerStateChange, event.ServerStateChange, event.NewGroupInvite, event.NewRetValMessageFromPeer})
bob := app2.WaitGetPeer(app, "Bob")
bobBus := app.GetEventBus(bob.GetOnion())
app.ActivatePeerEngine(bob.GetOnion(), true, true, true)
app.ActivatePeerEngine(bob.GetOnion())
log.Infoln("Bob created:", bob.GetOnion())
bob.SetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants.Name, "Bob")
bob.AutoHandleEvents([]event.Type{event.PeerStateChange, event.ServerStateChange, event.NewGroupInvite, event.NewRetValMessageFromPeer})
carol := app2.WaitGetPeer(app, "Carol")
carolBus := app.GetEventBus(carol.GetOnion())
app.ActivatePeerEngine(carol.GetOnion(), true, true, true)
app.ActivatePeerEngine(carol.GetOnion())
log.Infoln("Carol created:", carol.GetOnion())
carol.SetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants.Name, "Carol")
carol.AutoHandleEvents([]event.Type{event.PeerStateChange, event.ServerStateChange, event.NewGroupInvite, event.NewRetValMessageFromPeer})
@ -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)

View File

@ -59,9 +59,9 @@ func TestEncryptedStorage(t *testing.T) {
defer acn.Close()
acn.WaitTillBootstrapped()
app := app2.NewApp(acn, cwtchDir)
app.CreateTaggedPeer("alice", "password", constants.ProfileTypeV1Password)
app.CreateTaggedPeer("bob", "password", constants.ProfileTypeV1Password)
app := app2.NewApp(acn, cwtchDir, app2.LoadAppSettings(cwtchDir))
app.CreateProfile("alice", "password", true)
app.CreateProfile("bob", "password", true)
alice := app2.WaitGetPeer(app, "alice")
bob := app2.WaitGetPeer(app, "bob")
@ -130,7 +130,7 @@ func TestEncryptedStorage(t *testing.T) {
t.Fatalf("expeced GetMostRecentMessages to return 1, instead returned: %v %v", len(messages), messages)
}
err = alice.Export("alice.tar.gz")
err = alice.ExportProfile("alice.tar.gz")
if err != nil {
t.Fatalf("could not export profile: %v", err)
}
@ -140,7 +140,7 @@ func TestEncryptedStorage(t *testing.T) {
t.Fatal("profile is already imported...this should fail")
}
app.DeletePeer(alice.GetOnion(), "password")
app.DeleteProfile(alice.GetOnion(), "password")
alice, err = app.ImportProfile("alice.tar.gz", "password")
if err != nil {
t.Fatalf("profile should have successfully imported: %s", err)

View File

@ -18,6 +18,7 @@ import (
"fmt"
"git.openprivacy.ca/openprivacy/connectivity/tor"
"git.openprivacy.ca/openprivacy/log"
"path/filepath"
// Import SQL Cipher
mrand "math/rand"
@ -56,7 +57,7 @@ func TestFileSharing(t *testing.T) {
os.RemoveAll("cwtch.out.png")
os.RemoveAll("cwtch.out.png.manifest")
log.SetLevel(log.LevelInfo)
log.SetLevel(log.LevelDebug)
os.Mkdir("tordir", 0700)
dataDir := path.Join("tordir", "tor")
@ -74,20 +75,29 @@ func TestFileSharing(t *testing.T) {
panic(err)
}
useCache := os.Getenv("TORCACHE") == "true"
torDataDir := ""
if torDataDir, err = os.MkdirTemp(dataDir, "data-dir-"); err != nil {
t.Fatalf("could not create data dir")
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)})
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")
app := app2.NewApp(acn, "./storage", app2.LoadAppSettings("./storage"))
usr, _ := user.Current()
cwtchDir := path.Join(usr.HomeDir, ".cwtch")
@ -96,25 +106,30 @@ func TestFileSharing(t *testing.T) {
os.Mkdir(path.Join(cwtchDir, "testing"), 0700)
t.Logf("Creating Alice...")
app.CreateTaggedPeer("alice", "asdfasdf", "testing")
app.CreateProfile("alice", "asdfasdf", true)
t.Logf("Creating Bob...")
app.CreateTaggedPeer("bob", "asdfasdf", "testing")
app.CreateProfile("bob", "asdfasdf", true)
t.Logf("** Waiting for Alice, Bob...")
alice := app2.WaitGetPeer(app, "alice")
app.ActivatePeerEngine(alice.GetOnion(), true, true, true)
app.ActivatePeerEngine(alice.GetOnion())
bob := app2.WaitGetPeer(app, "bob")
app.ActivatePeerEngine(bob.GetOnion(), true, true, true)
app.ActivatePeerEngine(bob.GetOnion())
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)
@ -125,6 +140,7 @@ func TestFileSharing(t *testing.T) {
t.Logf("Waiting for alice and Bob to peer...")
waitForPeerPeerConnection(t, alice, bob)
alice.AcceptConversation(1)
t.Logf("Alice and Bob are Connected!!")
@ -145,20 +161,24 @@ func TestFileSharing(t *testing.T) {
// Test stopping and restarting file shares
t.Logf("Stopping File Share")
alice.StopAllFileShares()
filesharingFunctionality.StopAllFileShares(alice)
// Allow time for the stop request to filter through Engine
time.Sleep(time.Second * 5)
// 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)
// test that we can delete bob...
app.DeletePeer(bob.GetOnion(), "asdfasdf")
app.DeleteProfile(bob.GetOnion(), "asdfasdf")
queueOracle.Shutdown()
app.Shutdown()
@ -181,6 +201,7 @@ func testBobDownloadFile(t *testing.T, bob peer.CwtchPeer, filesharingFunctional
os.RemoveAll("cwtch.out.png")
os.RemoveAll("cwtch.out.png.manifest")
bob.AcceptConversation(1)
message, _, err := bob.GetChannelMessage(1, 0, 1)
if err != nil {
t.Fatalf("could not find file sharing message: %v", err)
@ -194,19 +215,19 @@ func testBobDownloadFile(t *testing.T, bob peer.CwtchPeer, filesharingFunctional
err := json.Unmarshal([]byte(messageWrapper.Data), &fileMessageOverlay)
if err == nil {
t.Logf("bob attempting to download file with invalid download")
// try downloading with invalid download dir
err = filesharingFunctionality.DownloadFile(bob, 1, "/do/not/download/this/file/cwtch.out.png", "./cwtch.out.png.manifest", fmt.Sprintf("%s.%s", fileMessageOverlay.Hash, fileMessageOverlay.Nonce), constants.ImagePreviewMaxSizeInBytes)
if err == nil {
t.Fatalf("should not download file with invalid download dir")
}
t.Logf("bob attempting to download file with invalid manifest")
// try downloading with invalid manifest dir
err = filesharingFunctionality.DownloadFile(bob, 1, "./cwtch.out.png", "/do/not/download/this/file/cwtch.out.png.manifest", fmt.Sprintf("%s.%s", fileMessageOverlay.Hash, fileMessageOverlay.Nonce), constants.ImagePreviewMaxSizeInBytes)
if err == nil {
t.Fatalf("should not download file with invalid manifest dir")
}
t.Logf("bob attempting to download file")
err = filesharingFunctionality.DownloadFile(bob, 1, "./cwtch.out.png", "./cwtch.out.png.manifest", fmt.Sprintf("%s.%s", fileMessageOverlay.Hash, fileMessageOverlay.Nonce), constants.ImagePreviewMaxSizeInBytes)
if err != nil {
t.Fatalf("could not download file: %v", err)