Compare commits
12 Commits
master
...
autobindin
Author | SHA1 | Date |
---|---|---|
Sarah Jamie Lewis | d2fd57853a | |
Sarah Jamie Lewis | e0adeac4a0 | |
Sarah Jamie Lewis | 0f83fb79f0 | |
Sarah Jamie Lewis | dfaed356c4 | |
Sarah Jamie Lewis | c90301bb4c | |
Sarah Jamie Lewis | 03da50b850 | |
Sarah Jamie Lewis | b22b8f3297 | |
Sarah Jamie Lewis | 49e0d849fa | |
Sarah Jamie Lewis | 84c8c8bc11 | |
Sarah Jamie Lewis | 371dad5f90 | |
Sarah Jamie Lewis | e86eb45d22 | |
Sarah Jamie Lewis | 445355b0fe |
150
app/app.go
150
app/app.go
|
@ -3,6 +3,8 @@ package app
|
||||||
import (
|
import (
|
||||||
"cwtch.im/cwtch/app/plugins"
|
"cwtch.im/cwtch/app/plugins"
|
||||||
"cwtch.im/cwtch/event"
|
"cwtch.im/cwtch/event"
|
||||||
|
"cwtch.im/cwtch/extensions"
|
||||||
|
"cwtch.im/cwtch/functionality/filesharing"
|
||||||
"cwtch.im/cwtch/model"
|
"cwtch.im/cwtch/model"
|
||||||
"cwtch.im/cwtch/model/attr"
|
"cwtch.im/cwtch/model/attr"
|
||||||
"cwtch.im/cwtch/model/constants"
|
"cwtch.im/cwtch/model/constants"
|
||||||
|
@ -29,16 +31,28 @@ type application struct {
|
||||||
engines map[string]connections.Engine
|
engines map[string]connections.Engine
|
||||||
appBus event.Manager
|
appBus event.Manager
|
||||||
appmutex sync.Mutex
|
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
|
// Application is a full cwtch peer application. It allows management, usage and storage of multiple peers
|
||||||
type Application interface {
|
type Application interface {
|
||||||
LoadProfiles(password string)
|
LoadProfiles(password string)
|
||||||
CreatePeer(name string, password string, attributes map[attr.ZonedPath]string)
|
CreateProfile(name string, password string, autostart bool)
|
||||||
// Deprecated in 1.10
|
|
||||||
CreateTaggedPeer(name string, password string, tag string)
|
|
||||||
ImportProfile(exportedCwtchFile string, password string) (peer.CwtchPeer, error)
|
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)
|
AddPeerPlugin(onion string, pluginID plugins.PluginID)
|
||||||
|
|
||||||
GetPrimaryBus() event.Manager
|
GetPrimaryBus() event.Manager
|
||||||
|
@ -47,9 +61,13 @@ type Application interface {
|
||||||
QueryACNVersion()
|
QueryACNVersion()
|
||||||
|
|
||||||
ActivateEngines(doListn, doPeers, doServers bool)
|
ActivateEngines(doListn, doPeers, doServers bool)
|
||||||
ActivatePeerEngine(onion string, doListen, doPeers, doServers bool)
|
ActivatePeerEngine(onion string)
|
||||||
DeactivatePeerEngine(onion string)
|
DeactivatePeerEngine(onion string)
|
||||||
|
|
||||||
|
ReadSettings() GlobalSettings
|
||||||
|
UpdateSettings(settings GlobalSettings)
|
||||||
|
IsFeatureEnabled(experiment string) bool
|
||||||
|
|
||||||
ShutdownPeer(string)
|
ShutdownPeer(string)
|
||||||
Shutdown()
|
Shutdown()
|
||||||
|
|
||||||
|
@ -60,12 +78,24 @@ type Application interface {
|
||||||
// LoadProfileFn is the function signature for a function in an app that loads a profile
|
// LoadProfileFn is the function signature for a function in an app that loads a profile
|
||||||
type LoadProfileFn func(profile peer.CwtchPeer)
|
type LoadProfileFn func(profile peer.CwtchPeer)
|
||||||
|
|
||||||
// NewApp creates a new app with some environment awareness and initializes a Tor Manager
|
func LoadAppSettings(appDirectory string) *GlobalSettingsFile {
|
||||||
func NewApp(acn connectivity.ACN, appDirectory string) Application {
|
|
||||||
log.Debugf("NewApp(%v)\n", appDirectory)
|
log.Debugf("NewApp(%v)\n", appDirectory)
|
||||||
os.MkdirAll(path.Join(appDirectory, "profiles"), 0700)
|
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.peers = make(map[string]peer.CwtchPeer)
|
||||||
|
|
||||||
app.acn = acn
|
app.acn = acn
|
||||||
|
@ -78,6 +108,34 @@ func NewApp(acn connectivity.ACN, appDirectory string) Application {
|
||||||
return app
|
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
|
// ListProfiles returns a map of onions to their profile's Name
|
||||||
func (app *application) ListProfiles() []string {
|
func (app *application) ListProfiles() []string {
|
||||||
var keys []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
|
// Deprecated in 1.10
|
||||||
func (app *application) CreateTaggedPeer(name string, password string, tag string) {
|
func (app *application) CreateTaggedPeer(name string, password string, tag string) {
|
||||||
app.CreatePeer(name, password, map[attr.ZonedPath]string{attr.ProfileZone.ConstructZonedPath(constants.Tag): tag})
|
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) {
|
func (app *application) CreatePeer(name string, password string, attributes map[attr.ZonedPath]string) {
|
||||||
app.appmutex.Lock()
|
app.appmutex.Lock()
|
||||||
defer app.appmutex.Unlock()
|
defer app.appmutex.Unlock()
|
||||||
|
@ -142,25 +234,26 @@ func (app *application) CreatePeer(name string, password string, attributes map[
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
eventBus := event.NewEventManager()
|
app.setupPeer(profile)
|
||||||
app.eventBuses[profile.GetOnion()] = eventBus
|
|
||||||
profile.Init(app.eventBuses[profile.GetOnion()])
|
|
||||||
app.peers[profile.GetOnion()] = profile
|
|
||||||
|
|
||||||
for zp, val := range attributes {
|
for zp, val := range attributes {
|
||||||
zone, key := attr.ParseZone(zp.ToString())
|
zone, key := attr.ParseZone(zp.ToString())
|
||||||
profile.SetScopedZonedAttribute(attr.LocalScope, zone, key, val)
|
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}))
|
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) {
|
func (app *application) DeleteProfile(onion string, password string) {
|
||||||
log.Infof("DeletePeer called on %v\n", onion)
|
log.Debugf("DeleteProfile called on %v\n", onion)
|
||||||
app.appmutex.Lock()
|
app.appmutex.Lock()
|
||||||
defer app.appmutex.Unlock()
|
defer app.appmutex.Unlock()
|
||||||
|
|
||||||
|
// allow a blank password to delete "unencrypted" accounts...
|
||||||
|
if password == "" {
|
||||||
|
password = DefactoPasswordForUnencryptedProfiles
|
||||||
|
}
|
||||||
|
|
||||||
if app.peers[onion].CheckPassword(password) {
|
if app.peers[onion].CheckPassword(password) {
|
||||||
// soft-shutdown
|
// soft-shutdown
|
||||||
app.peers[onion].Shutdown()
|
app.peers[onion].Shutdown()
|
||||||
|
@ -190,6 +283,14 @@ func (app *application) ImportProfile(exportedCwtchFile string, password string)
|
||||||
return profile, err
|
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
|
// 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) {
|
func (app *application) LoadProfiles(password string) {
|
||||||
count := 0
|
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
|
// 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 {
|
func (app *application) installProfile(profile peer.CwtchPeer) bool {
|
||||||
app.appmutex.Lock()
|
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...
|
// Only attempt to finalize the profile if we don't have one loaded...
|
||||||
if app.peers[profile.GetOnion()] == nil {
|
if app.peers[profile.GetOnion()] == nil {
|
||||||
eventBus := event.NewEventManager()
|
app.setupPeer(profile)
|
||||||
app.eventBuses[profile.GetOnion()] = eventBus
|
// Finalize the Creation of Peer / Notify any Interfaces..
|
||||||
profile.Init(app.eventBuses[profile.GetOnion()])
|
|
||||||
app.peers[profile.GetOnion()] = profile
|
|
||||||
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.False}))
|
app.appBus.Publish(event.NewEvent(event.NewPeer, map[event.Field]string{event.Identity: profile.GetOnion(), event.Created: event.False}))
|
||||||
return true
|
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
|
// 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)
|
profile := app.GetPeer(onion)
|
||||||
if profile != nil {
|
if profile != nil {
|
||||||
if _, exists := app.engines[onion]; !exists {
|
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.eventBuses[profile.GetOnion()].Publish(event.NewEventList(event.ProtocolEngineCreated))
|
||||||
app.QueryACNStatus()
|
app.QueryACNStatus()
|
||||||
if doListen {
|
if true {
|
||||||
profile.Listen()
|
profile.Listen()
|
||||||
}
|
}
|
||||||
profile.StartConnections(doPeers, doServers)
|
profile.StartConnections(true, 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() {
|
func (a *antispam) run() {
|
||||||
log.Infof("running antispam trigger plugin")
|
log.Debugf("running antispam trigger plugin")
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-time.After(antispamTickTime):
|
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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -228,7 +228,8 @@ type Field string
|
||||||
const (
|
const (
|
||||||
|
|
||||||
// A peers local onion address
|
// A peers local onion address
|
||||||
Onion = Field("Onion")
|
Onion = Field("Onion")
|
||||||
|
ProfileOnion = Field("ProfileOnion")
|
||||||
|
|
||||||
RemotePeer = Field("RemotePeer")
|
RemotePeer = Field("RemotePeer")
|
||||||
LastSeen = Field("LastSeen")
|
LastSeen = Field("LastSeen")
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,12 +2,14 @@ package filesharing
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
|
"cwtch.im/cwtch/event"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"math"
|
"math"
|
||||||
|
"math/bits"
|
||||||
"os"
|
"os"
|
||||||
path "path/filepath"
|
path "path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
@ -28,6 +30,109 @@ import (
|
||||||
type Functionality struct {
|
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
|
// FunctionalityGate returns filesharing if enabled in the given experiment map
|
||||||
// Note: Experiment maps are currently in libcwtch-go
|
// Note: Experiment maps are currently in libcwtch-go
|
||||||
func FunctionalityGate(experimentMap map[string]bool) (*Functionality, error) {
|
func FunctionalityGate(experimentMap map[string]bool) (*Functionality, error) {
|
||||||
|
@ -73,10 +178,74 @@ func (om *OverlayMessage) ShouldAutoDL() bool {
|
||||||
return false
|
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
|
// DownloadFile given a profile, a conversation handle and a file sharing key, start off a download process
|
||||||
// to downloadFilePath
|
// to downloadFilePath
|
||||||
func (f *Functionality) DownloadFile(profile peer.CwtchPeer, conversationID int, downloadFilePath string, manifestFilePath string, key string, limit uint64) error {
|
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
|
// Don't download files if the download or manifest path is not set
|
||||||
if downloadFilePath == "" || manifestFilePath == "" {
|
if downloadFilePath == "" || manifestFilePath == "" {
|
||||||
return errors.New("download path or manifest path is empty")
|
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
|
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
|
// 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 {
|
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
|
// check that a manifest exists
|
||||||
manifest, manifestExists := profile.GetScopedZonedAttribute(attr.ConversationScope, attr.FilesharingZone, fmt.Sprintf("%s.manifest", filekey))
|
manifest, manifestExists := profile.GetScopedZonedAttribute(attr.ConversationScope, attr.FilesharingZone, fmt.Sprintf("%s.manifest", filekey))
|
||||||
if manifestExists {
|
if manifestExists {
|
||||||
// everything is in order, so reshare this file with the engine
|
// everything is in order, so reshare this file with the engine
|
||||||
profile.ShareFile(filekey, manifest)
|
log.Debugf("restarting file share: %v", filekey)
|
||||||
return nil
|
return f.startFileShare(profile, filekey, manifest)
|
||||||
}
|
}
|
||||||
return fmt.Errorf("manifest does not exist for filekey: %v", filekey)
|
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
|
// ReShareFiles given a profile we iterate through all existing fileshares and re-share them
|
||||||
// if the time limit has not expired
|
// if the time limit has not expired
|
||||||
func (f *Functionality) ReShareFiles(profile peer.CwtchPeer) error {
|
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)
|
keys, err := profile.GetScopedZonedAttributeKeys(attr.LocalScope, attr.FilesharingZone)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -132,6 +330,7 @@ func (f *Functionality) ReShareFiles(profile peer.CwtchPeer) error {
|
||||||
for _, key := range keys {
|
for _, key := range keys {
|
||||||
// only look at timestamp keys
|
// only look at timestamp keys
|
||||||
// this is an arbitrary choice
|
// this is an arbitrary choice
|
||||||
|
|
||||||
if strings.HasSuffix(key, ".ts") {
|
if strings.HasSuffix(key, ".ts") {
|
||||||
_, zonedpath := attr.ParseScope(key)
|
_, zonedpath := attr.ParseScope(key)
|
||||||
_, keypath := attr.ParseZone(zonedpath)
|
_, keypath := attr.ParseZone(zonedpath)
|
||||||
|
@ -148,7 +347,12 @@ func (f *Functionality) ReShareFiles(profile peer.CwtchPeer) error {
|
||||||
// Then attempt to share this file again...
|
// Then attempt to share this file again...
|
||||||
// TODO: In the future this would be the point to change the timestamp and reshare the file...
|
// TODO: In the future this would be the point to change the timestamp and reshare the file...
|
||||||
if err == nil && sharedFile.Active {
|
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
|
// ShareFile given a profile and a conversation handle, sets up a file sharing process to share the file
|
||||||
// at filepath
|
// at filepath
|
||||||
func (f *Functionality) ShareFile(filepath string, profile peer.CwtchPeer) (string, string, error) {
|
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)
|
manifest, err := files.CreateManifest(filepath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", err
|
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", 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.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
|
return key, string(wrapperJSON), err
|
||||||
}
|
}
|
||||||
|
@ -258,6 +468,14 @@ type SharedFile struct {
|
||||||
Expired bool
|
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
|
// GetSharedFiles returns all file shares associated with a given conversation
|
||||||
func (f *Functionality) GetSharedFiles(profile peer.CwtchPeer, conversationID int) []SharedFile {
|
func (f *Functionality) GetSharedFiles(profile peer.CwtchPeer, conversationID int) []SharedFile {
|
||||||
sharedFiles := []SharedFile{}
|
sharedFiles := []SharedFile{}
|
||||||
|
@ -323,3 +541,17 @@ func GenerateDownloadPath(basePath, fileName string, overwrite bool) (filePath,
|
||||||
}
|
}
|
||||||
return
|
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{}))
|
||||||
|
}
|
||||||
|
|
|
@ -23,6 +23,12 @@ type Scope string
|
||||||
// ScopedZonedPath typed path with a scope and a zone
|
// ScopedZonedPath typed path with a scope and a zone
|
||||||
type ScopedZonedPath string
|
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
|
// scopes for attributes
|
||||||
const (
|
const (
|
||||||
// on a peer, local and peer supplied data
|
// on a peer, local and peer supplied data
|
||||||
|
|
|
@ -56,3 +56,5 @@ const SyncPreLastMessageTime = "SyncPreLastMessageTime"
|
||||||
const SyncMostRecentMessageTime = "SyncMostRecentMessageTime"
|
const SyncMostRecentMessageTime = "SyncMostRecentMessageTime"
|
||||||
|
|
||||||
const AttrLastConnectionTime = "last-connection-time"
|
const AttrLastConnectionTime = "last-connection-time"
|
||||||
|
const PeerAutostart = "autostart"
|
||||||
|
const Archived = "archived"
|
||||||
|
|
|
@ -10,5 +10,7 @@ const ImagePreviewsExperiment = "filesharing-images"
|
||||||
// ImagePreviewMaxSizeInBytes Files up to this size will be autodownloaded using ImagePreviewsExperiment
|
// ImagePreviewMaxSizeInBytes Files up to this size will be autodownloaded using ImagePreviewsExperiment
|
||||||
const ImagePreviewMaxSizeInBytes = 20971520
|
const ImagePreviewMaxSizeInBytes = 20971520
|
||||||
|
|
||||||
|
const MessageFormattingExperiment = "message-formatting"
|
||||||
|
|
||||||
// AutoDLFileExts Files with these extensions will be autodownloaded using ImagePreviewsExperiment
|
// AutoDLFileExts Files with these extensions will be autodownloaded using ImagePreviewsExperiment
|
||||||
var AutoDLFileExts = [...]string{".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp"}
|
var AutoDLFileExts = [...]string{".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp"}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -14,10 +14,8 @@ import (
|
||||||
"git.openprivacy.ca/openprivacy/connectivity"
|
"git.openprivacy.ca/openprivacy/connectivity"
|
||||||
"git.openprivacy.ca/openprivacy/connectivity/tor"
|
"git.openprivacy.ca/openprivacy/connectivity/tor"
|
||||||
"golang.org/x/crypto/ed25519"
|
"golang.org/x/crypto/ed25519"
|
||||||
"math/bits"
|
|
||||||
"os"
|
"os"
|
||||||
path "path/filepath"
|
path "path/filepath"
|
||||||
"runtime"
|
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -28,7 +26,6 @@ import (
|
||||||
"cwtch.im/cwtch/model"
|
"cwtch.im/cwtch/model"
|
||||||
"cwtch.im/cwtch/model/attr"
|
"cwtch.im/cwtch/model/attr"
|
||||||
"cwtch.im/cwtch/protocol/connections"
|
"cwtch.im/cwtch/protocol/connections"
|
||||||
"cwtch.im/cwtch/protocol/files"
|
|
||||||
"git.openprivacy.ca/openprivacy/log"
|
"git.openprivacy.ca/openprivacy/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -38,8 +35,7 @@ const lastReceivedSignature = "LastReceivedSignature"
|
||||||
var autoHandleableEvents = map[event.Type]bool{event.EncryptedGroupMessage: true, event.PeerStateChange: true,
|
var autoHandleableEvents = map[event.Type]bool{event.EncryptedGroupMessage: true, event.PeerStateChange: true,
|
||||||
event.ServerStateChange: true, event.NewGroupInvite: true, event.NewMessageFromPeerEngine: true,
|
event.ServerStateChange: true, event.NewGroupInvite: true, event.NewMessageFromPeerEngine: true,
|
||||||
event.PeerAcknowledgement: true, event.PeerError: true, event.SendMessageToPeerError: true, event.SendMessageToGroupError: 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.NewGetValMessageFromPeer: true, event.NewRetValMessageFromPeer: true, event.ProtocolEngineStopped: true, event.RetryServerRequest: true, event.TriggerAntispamCheck: true}
|
||||||
event.ManifestSizeReceived: true, event.ManifestReceived: true, event.FileDownloaded: true, event.TriggerAntispamCheck: true}
|
|
||||||
|
|
||||||
// DefaultEventsToHandle specifies which events will be subscribed to
|
// DefaultEventsToHandle specifies which events will be subscribed to
|
||||||
//
|
//
|
||||||
|
@ -58,8 +54,6 @@ var DefaultEventsToHandle = []event.Type{
|
||||||
event.ServerStateChange,
|
event.ServerStateChange,
|
||||||
event.SendMessageToPeerError,
|
event.SendMessageToPeerError,
|
||||||
event.NewRetValMessageFromPeer,
|
event.NewRetValMessageFromPeer,
|
||||||
event.ManifestReceived,
|
|
||||||
event.FileDownloaded,
|
|
||||||
event.TriggerAntispamCheck,
|
event.TriggerAntispamCheck,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -74,6 +68,132 @@ type cwtchPeer struct {
|
||||||
|
|
||||||
queue event.Queue
|
queue event.Queue
|
||||||
eventBus event.Manager
|
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) {
|
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()
|
cp.mutex.Lock()
|
||||||
defer cp.mutex.Unlock()
|
defer cp.mutex.Unlock()
|
||||||
return cp.storage.Export(file)
|
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
|
// probably redundant but we like api safety
|
||||||
if newpassword == newpasswordAgain {
|
if newpassword == newpasswordAgain {
|
||||||
rekey := createKey(newpassword, salt)
|
rekey := createKey(newpassword, salt)
|
||||||
log.Infof("rekeying database...")
|
log.Debugf("rekeying database...")
|
||||||
return cp.storage.Rekey(rekey)
|
return cp.storage.Rekey(rekey)
|
||||||
}
|
}
|
||||||
return errors.New(constants.PasswordsDoNotMatchError)
|
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))
|
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
|
// eventHandler process events from other subsystems
|
||||||
func (cp *cwtchPeer) eventHandler() {
|
func (cp *cwtchPeer) eventHandler() {
|
||||||
for {
|
for {
|
||||||
|
@ -1288,128 +1381,60 @@ func (cp *cwtchPeer) eventHandler() {
|
||||||
path := ev.Data[event.Path]
|
path := ev.Data[event.Path]
|
||||||
|
|
||||||
log.Debugf("NewGetValMessageFromPeer for %v.%v from %v\n", scope, path, onion)
|
log.Debugf("NewGetValMessageFromPeer for %v.%v from %v\n", scope, path, onion)
|
||||||
|
|
||||||
conversationInfo, err := cp.FetchConversationInfo(onion)
|
conversationInfo, err := cp.FetchConversationInfo(onion)
|
||||||
|
|
||||||
log.Debugf("confo info lookup newgetval %v %v %v", onion, conversationInfo, err)
|
log.Debugf("confo info lookup newgetval %v %v %v", onion, conversationInfo, err)
|
||||||
|
// only accepted contacts can look up information
|
||||||
if conversationInfo != nil && conversationInfo.Accepted {
|
if conversationInfo != nil && conversationInfo.Accepted {
|
||||||
scope := attr.IntoScope(scope)
|
// Type Safe Scoped/Zoned Path
|
||||||
if scope.IsPublic() || scope.IsConversation() {
|
zscope := attr.IntoScope(scope)
|
||||||
zone, zpath := attr.ParseZone(path)
|
zone, zpath := attr.ParseZone(path)
|
||||||
val, exists := cp.GetScopedZonedAttribute(scope, zone, zpath)
|
scopedZonedPath := zscope.ConstructScopedZonedPath(zone.ConstructZonedPath(zpath))
|
||||||
|
|
||||||
// NOTE: Temporary Override because UI currently wipes names if it can't find them...
|
// Safe Access to Extensions
|
||||||
if !exists && zone == attr.UnknownZone && path == constants.Name {
|
cp.extensionLock.Lock()
|
||||||
val, exists = cp.GetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants.Name)
|
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)})
|
extension.extension.OnContactRequestValue(cp, *conversationInfo, ev.EventID, scopedZonedPath)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
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:
|
case event.NewRetValMessageFromPeer:
|
||||||
onion := ev.Data[event.RemotePeer]
|
handle := ev.Data[event.RemotePeer]
|
||||||
scope := ev.Data[event.Scope]
|
scope := ev.Data[event.Scope]
|
||||||
path := ev.Data[event.Path]
|
path := ev.Data[event.Path]
|
||||||
val := ev.Data[event.Data]
|
val := ev.Data[event.Data]
|
||||||
exists, _ := strconv.ParseBool(ev.Data[event.Exists])
|
exists, _ := strconv.ParseBool(ev.Data[event.Exists])
|
||||||
log.Debugf("NewRetValMessageFromPeer %v %v %v %v %v\n", onion, scope, path, exists, val)
|
log.Debugf("NewRetValMessageFromPeer %v %v %v %v %v\n", handle, scope, path, exists, val)
|
||||||
if exists {
|
|
||||||
|
|
||||||
// Handle File Sharing Metadata
|
conversationInfo, _ := cp.FetchConversationInfo(handle)
|
||||||
// TODO This probably should be broken out to it's own code..
|
// only accepted contacts can look up information
|
||||||
zone, path := attr.ParseZone(path)
|
if conversationInfo != nil && conversationInfo.Accepted {
|
||||||
if attr.Scope(scope).IsConversation() && zone == attr.FilesharingZone && strings.HasSuffix(path, ".manifest.size") {
|
// Type Safe Scoped/Zoned Path
|
||||||
fileKey := strings.Replace(path, ".manifest.size", "", 1)
|
zscope := attr.IntoScope(scope)
|
||||||
size, err := strconv.Atoi(val)
|
zone, zpath := attr.ParseZone(path)
|
||||||
// if size is valid and below the maximum size for a manifest
|
scopedZonedPath := zscope.ConstructScopedZonedPath(zone.ConstructZonedPath(zpath))
|
||||||
// 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}))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Allow public profile parameters to be added as peer specific attributes...
|
// Safe Access to Extensions
|
||||||
if attr.Scope(scope).IsPublic() && zone == attr.ProfileZone {
|
cp.extensionLock.Lock()
|
||||||
ci, err := cp.FetchConversationInfo(onion)
|
for _, extension := range cp.extensions {
|
||||||
log.Debugf("fetch conversation info %v %v", ci, err)
|
log.Debugf("checking extension...%v", extension)
|
||||||
if ci != nil && err == nil {
|
// check if the current map of experiments satisfies the extension requirements
|
||||||
err := cp.SetConversationAttribute(ci.ID, attr.Scope(scope).ConstructScopedZonedPath(zone.ConstructZonedPath(path)), val)
|
if !cp.checkExtensionExperiment(extension) {
|
||||||
if err != nil {
|
log.Debugf("skipping extension...not all experiments satisfied")
|
||||||
log.Errorf("error setting conversation attribute %v", err)
|
continue
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
extension.extension.OnContactReceiveValue(cp, *conversationInfo, scopedZonedPath, val, exists)
|
||||||
}
|
}
|
||||||
|
cp.extensionLock.Unlock()
|
||||||
}
|
}
|
||||||
case event.PeerStateChange:
|
case event.PeerStateChange:
|
||||||
handle := ev.Data[event.RemotePeer]
|
handle := ev.Data[event.RemotePeer]
|
||||||
|
@ -1491,14 +1516,48 @@ func (cp *cwtchPeer) eventHandler() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
if ev.EventType != "" {
|
// invalid event, signifies shutdown
|
||||||
log.Errorf("peer event handler received an event it was not subscribed for: %v", ev.EventType)
|
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
|
// 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
|
// 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
|
// 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)
|
insertProfileKeyValueStmt, err := db.Prepare(insertProfileKeySQLStmt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
db.Close()
|
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
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -772,7 +774,7 @@ func (cps *CwtchProfileStorage) PurgeNonSavedMessages() {
|
||||||
for _, conversation := range ci {
|
for _, conversation := range ci {
|
||||||
if !conversation.IsGroup() && !conversation.IsServer() {
|
if !conversation.IsGroup() && !conversation.IsServer() {
|
||||||
if conversation.Attributes[attr.LocalScope.ConstructScopedZonedPath(attr.ProfileZone.ConstructZonedPath(event.SaveHistoryKey)).ToString()] != event.SaveHistoryConfirmed {
|
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..
|
// 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)
|
cps.PurgeConversationChannel(conversation.ID, 0)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
}
|
||||||
|
}
|
|
@ -48,6 +48,10 @@ type ModifyServers interface {
|
||||||
// SendMessages enables a caller to sender messages to a contact
|
// SendMessages enables a caller to sender messages to a contact
|
||||||
type SendMessages interface {
|
type SendMessages interface {
|
||||||
SendMessage(conversation int, message string) (int, error)
|
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)
|
SendInviteToConversation(conversationID int, inviteConversationID int) (int, error)
|
||||||
SendScopedZonedGetValToContact(conversationID int, scope attr.Scope, zone attr.Zone, key string)
|
SendScopedZonedGetValToContact(conversationID int, scope attr.Scope, zone attr.Zone, key string)
|
||||||
}
|
}
|
||||||
|
@ -101,10 +105,12 @@ type CwtchPeer interface {
|
||||||
|
|
||||||
// Import Bundle
|
// Import Bundle
|
||||||
ImportBundle(string) error
|
ImportBundle(string) error
|
||||||
|
EnhancedImportBundle(string) string
|
||||||
|
|
||||||
// New Unified Conversation Interfaces
|
// New Unified Conversation Interfaces
|
||||||
NewContactConversation(handle string, acl model.AccessControl, accepted bool) (int, error)
|
NewContactConversation(handle string, acl model.AccessControl, accepted bool) (int, error)
|
||||||
FetchConversations() ([]*model.Conversation, error)
|
FetchConversations() ([]*model.Conversation, error)
|
||||||
|
ArchiveConversation(conversation int)
|
||||||
GetConversationInfo(conversation int) (*model.Conversation, error)
|
GetConversationInfo(conversation int) (*model.Conversation, error)
|
||||||
FetchConversationInfo(handle string) (*model.Conversation, error)
|
FetchConversationInfo(handle string) (*model.Conversation, error)
|
||||||
AcceptConversation(conversation int) error
|
AcceptConversation(conversation int) error
|
||||||
|
@ -121,11 +127,14 @@ type CwtchPeer interface {
|
||||||
GetMostRecentMessages(conversation int, channel int, offset int, limit int) ([]model.ConversationMessage, error)
|
GetMostRecentMessages(conversation int, channel int, offset int, limit int) ([]model.ConversationMessage, error)
|
||||||
UpdateMessageAttribute(conversation int, channel int, id int, key string, value string) error
|
UpdateMessageAttribute(conversation int, channel int, id int, key string, value string) error
|
||||||
|
|
||||||
// File Sharing APIS
|
// EnhancedGetMessageById returns a json-encoded enhanced message, suitable for rendering in a UI
|
||||||
// TODO move these to feature protected interfaces
|
EnhancedGetMessageById(conversation int, mid int) string
|
||||||
ShareFile(fileKey string, serializedManifest string)
|
|
||||||
StopFileShare(fileKey string)
|
// EnhancedGetMessageByContentHash returns a json-encoded enhanced message, suitable for rendering in a UI
|
||||||
StopAllFileShares()
|
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
|
// Server Token APIS
|
||||||
// TODO move these to feature protected interfaces
|
// TODO move these to feature protected interfaces
|
||||||
|
@ -134,6 +143,20 @@ type CwtchPeer interface {
|
||||||
// Profile Management
|
// Profile Management
|
||||||
CheckPassword(password string) bool
|
CheckPassword(password string) bool
|
||||||
ChangePassword(oldpassword string, newpassword string, newpasswordAgain string) error
|
ChangePassword(oldpassword string, newpassword string, newpasswordAgain string) error
|
||||||
Export(file string) error
|
ExportProfile(file string) error
|
||||||
Delete()
|
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
|
||||||
}
|
}
|
||||||
|
|
|
@ -176,7 +176,7 @@ func ImportProfile(exportedCwtchFile string, profilesDir string, password string
|
||||||
log.Errorf("%s is an invalid cwtch backup file: %s", profileID, err)
|
log.Errorf("%s is an invalid cwtch backup file: %s", profileID, err)
|
||||||
return nil, 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)
|
profileDBFile := filepath.Join(profilesDir, profileID, dbFile)
|
||||||
log.Debugf("checking %v", profileDBFile)
|
log.Debugf("checking %v", profileDBFile)
|
||||||
|
|
|
@ -34,6 +34,7 @@ func (fsss *FileSharingSubSystem) ShareFile(fileKey string, serializedManifest s
|
||||||
log.Errorf("could not share file %v", err)
|
log.Errorf("could not share file %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
log.Infof("sharing file: %v %v", fileKey, serializedManifest)
|
||||||
fsss.activeShares.Store(fileKey, &manifest)
|
fsss.activeShares.Store(fileKey, &manifest)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -73,7 +73,7 @@ func (ps *ProfileStoreV1) load() error {
|
||||||
|
|
||||||
for gid, group := range cp.Groups {
|
for gid, group := range cp.Groups {
|
||||||
if group.Version == 0 {
|
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)
|
delete(cp.Groups, gid)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
|
@ -152,7 +152,7 @@ func (ss *streamStore) WriteN(messages []model.Message) {
|
||||||
ss.lock.Lock()
|
ss.lock.Lock()
|
||||||
defer ss.lock.Unlock()
|
defer ss.lock.Unlock()
|
||||||
|
|
||||||
log.Infof("WriteN %v messages\n", len(messages))
|
log.Debugf("WriteN %v messages\n", len(messages))
|
||||||
i := 0
|
i := 0
|
||||||
for _, m := range messages {
|
for _, m := range messages {
|
||||||
ss.updateBuffer(m)
|
ss.updateBuffer(m)
|
||||||
|
|
|
@ -139,7 +139,7 @@ func TestCwtchPeerIntegration(t *testing.T) {
|
||||||
const ServerAddr = "nfhxzvzxinripgdh4t2m4xcy3crf6p4cbhectgckuj3idsjsaotgowad"
|
const ServerAddr = "nfhxzvzxinripgdh4t2m4xcy3crf6p4cbhectgckuj3idsjsaotgowad"
|
||||||
serverKeyBundle, _ := base64.StdEncoding.DecodeString(ServerKeyBundleBase64)
|
serverKeyBundle, _ := base64.StdEncoding.DecodeString(ServerKeyBundleBase64)
|
||||||
|
|
||||||
app := app2.NewApp(acn, "./storage")
|
app := app2.NewApp(acn, "./storage", app2.LoadAppSettings("./storage"))
|
||||||
|
|
||||||
usr, _ := user.Current()
|
usr, _ := user.Current()
|
||||||
cwtchDir := path.Join(usr.HomeDir, ".cwtch")
|
cwtchDir := path.Join(usr.HomeDir, ".cwtch")
|
||||||
|
@ -152,31 +152,31 @@ func TestCwtchPeerIntegration(t *testing.T) {
|
||||||
// ***** cwtchPeer setup *****
|
// ***** cwtchPeer setup *****
|
||||||
|
|
||||||
log.Infoln("Creating Alice...")
|
log.Infoln("Creating Alice...")
|
||||||
app.CreateTaggedPeer("Alice", "asdfasdf", "test")
|
app.CreateProfile("Alice", "asdfasdf", true)
|
||||||
|
|
||||||
log.Infoln("Creating Bob...")
|
log.Infoln("Creating Bob...")
|
||||||
app.CreateTaggedPeer("Bob", "asdfasdf", "test")
|
app.CreateProfile("Bob", "asdfasdf", true)
|
||||||
|
|
||||||
log.Infoln("Creating Carol...")
|
log.Infoln("Creating Carol...")
|
||||||
app.CreateTaggedPeer("Carol", "asdfasdf", "test")
|
app.CreateProfile("Carol", "asdfasdf", true)
|
||||||
|
|
||||||
alice := app2.WaitGetPeer(app, "Alice")
|
alice := app2.WaitGetPeer(app, "Alice")
|
||||||
aliceBus := app.GetEventBus(alice.GetOnion())
|
aliceBus := app.GetEventBus(alice.GetOnion())
|
||||||
app.ActivatePeerEngine(alice.GetOnion(), true, true, true)
|
app.ActivatePeerEngine(alice.GetOnion())
|
||||||
log.Infoln("Alice created:", alice.GetOnion())
|
log.Infoln("Alice created:", alice.GetOnion())
|
||||||
alice.SetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants.Name, "Alice")
|
alice.SetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants.Name, "Alice")
|
||||||
alice.AutoHandleEvents([]event.Type{event.PeerStateChange, event.ServerStateChange, event.NewGroupInvite, event.NewRetValMessageFromPeer})
|
alice.AutoHandleEvents([]event.Type{event.PeerStateChange, event.ServerStateChange, event.NewGroupInvite, event.NewRetValMessageFromPeer})
|
||||||
|
|
||||||
bob := app2.WaitGetPeer(app, "Bob")
|
bob := app2.WaitGetPeer(app, "Bob")
|
||||||
bobBus := app.GetEventBus(bob.GetOnion())
|
bobBus := app.GetEventBus(bob.GetOnion())
|
||||||
app.ActivatePeerEngine(bob.GetOnion(), true, true, true)
|
app.ActivatePeerEngine(bob.GetOnion())
|
||||||
log.Infoln("Bob created:", bob.GetOnion())
|
log.Infoln("Bob created:", bob.GetOnion())
|
||||||
bob.SetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants.Name, "Bob")
|
bob.SetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants.Name, "Bob")
|
||||||
bob.AutoHandleEvents([]event.Type{event.PeerStateChange, event.ServerStateChange, event.NewGroupInvite, event.NewRetValMessageFromPeer})
|
bob.AutoHandleEvents([]event.Type{event.PeerStateChange, event.ServerStateChange, event.NewGroupInvite, event.NewRetValMessageFromPeer})
|
||||||
|
|
||||||
carol := app2.WaitGetPeer(app, "Carol")
|
carol := app2.WaitGetPeer(app, "Carol")
|
||||||
carolBus := app.GetEventBus(carol.GetOnion())
|
carolBus := app.GetEventBus(carol.GetOnion())
|
||||||
app.ActivatePeerEngine(carol.GetOnion(), true, true, true)
|
app.ActivatePeerEngine(carol.GetOnion())
|
||||||
log.Infoln("Carol created:", carol.GetOnion())
|
log.Infoln("Carol created:", carol.GetOnion())
|
||||||
carol.SetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants.Name, "Carol")
|
carol.SetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants.Name, "Carol")
|
||||||
carol.AutoHandleEvents([]event.Type{event.PeerStateChange, event.ServerStateChange, event.NewGroupInvite, event.NewRetValMessageFromPeer})
|
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)
|
pprof.Lookup("goroutine").WriteTo(os.Stdout, 1)
|
||||||
fmt.Println("")
|
fmt.Println("")
|
||||||
log.Infof("numGoRoutinesStart: %v\nnumGoRoutinesPostAppStart: %v\nnumGoRoutinesPostPeerStart: %v\nnumGoRoutinesPostPeerAndServerConnect: %v\n"+
|
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,
|
numGoRoutinesStart, numGoRoutinesPostAppStart, numGoRoutinesPostPeerStart, numGoRoutinesPostServerConnect,
|
||||||
numGoRoutinesPostAlice, numGoRoutinesPostCarolConnect, numGoRoutinesPostBob, numGoRoutinesPostCarol, numGoRoutinesPostAppShutdown)
|
numGoRoutinesPostAlice, numGoRoutinesPostCarolConnect, numGoRoutinesPostBob, numGoRoutinesPostCarol, numGoRoutinesPostAppShutdown)
|
||||||
|
|
||||||
|
|
|
@ -59,9 +59,9 @@ func TestEncryptedStorage(t *testing.T) {
|
||||||
|
|
||||||
defer acn.Close()
|
defer acn.Close()
|
||||||
acn.WaitTillBootstrapped()
|
acn.WaitTillBootstrapped()
|
||||||
app := app2.NewApp(acn, cwtchDir)
|
app := app2.NewApp(acn, cwtchDir, app2.LoadAppSettings(cwtchDir))
|
||||||
app.CreateTaggedPeer("alice", "password", constants.ProfileTypeV1Password)
|
app.CreateProfile("alice", "password", true)
|
||||||
app.CreateTaggedPeer("bob", "password", constants.ProfileTypeV1Password)
|
app.CreateProfile("bob", "password", true)
|
||||||
|
|
||||||
alice := app2.WaitGetPeer(app, "alice")
|
alice := app2.WaitGetPeer(app, "alice")
|
||||||
bob := app2.WaitGetPeer(app, "bob")
|
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)
|
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 {
|
if err != nil {
|
||||||
t.Fatalf("could not export profile: %v", err)
|
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")
|
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")
|
alice, err = app.ImportProfile("alice.tar.gz", "password")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("profile should have successfully imported: %s", err)
|
t.Fatalf("profile should have successfully imported: %s", err)
|
||||||
|
|
|
@ -18,6 +18,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"git.openprivacy.ca/openprivacy/connectivity/tor"
|
"git.openprivacy.ca/openprivacy/connectivity/tor"
|
||||||
"git.openprivacy.ca/openprivacy/log"
|
"git.openprivacy.ca/openprivacy/log"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
// Import SQL Cipher
|
// Import SQL Cipher
|
||||||
mrand "math/rand"
|
mrand "math/rand"
|
||||||
|
@ -56,7 +57,7 @@ func TestFileSharing(t *testing.T) {
|
||||||
os.RemoveAll("cwtch.out.png")
|
os.RemoveAll("cwtch.out.png")
|
||||||
os.RemoveAll("cwtch.out.png.manifest")
|
os.RemoveAll("cwtch.out.png.manifest")
|
||||||
|
|
||||||
log.SetLevel(log.LevelInfo)
|
log.SetLevel(log.LevelDebug)
|
||||||
|
|
||||||
os.Mkdir("tordir", 0700)
|
os.Mkdir("tordir", 0700)
|
||||||
dataDir := path.Join("tordir", "tor")
|
dataDir := path.Join("tordir", "tor")
|
||||||
|
@ -74,20 +75,29 @@ func TestFileSharing(t *testing.T) {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useCache := os.Getenv("TORCACHE") == "true"
|
||||||
|
|
||||||
torDataDir := ""
|
torDataDir := ""
|
||||||
if torDataDir, err = os.MkdirTemp(dataDir, "data-dir-"); err != nil {
|
if useCache {
|
||||||
t.Fatalf("could not create data dir")
|
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")
|
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 {
|
if err != nil {
|
||||||
t.Fatalf("Could not start Tor: %v", err)
|
t.Fatalf("Could not start Tor: %v", err)
|
||||||
}
|
}
|
||||||
acn.WaitTillBootstrapped()
|
acn.WaitTillBootstrapped()
|
||||||
defer acn.Close()
|
defer acn.Close()
|
||||||
|
|
||||||
app := app2.NewApp(acn, "./storage")
|
app := app2.NewApp(acn, "./storage", app2.LoadAppSettings("./storage"))
|
||||||
|
|
||||||
usr, _ := user.Current()
|
usr, _ := user.Current()
|
||||||
cwtchDir := path.Join(usr.HomeDir, ".cwtch")
|
cwtchDir := path.Join(usr.HomeDir, ".cwtch")
|
||||||
|
@ -96,25 +106,30 @@ func TestFileSharing(t *testing.T) {
|
||||||
os.Mkdir(path.Join(cwtchDir, "testing"), 0700)
|
os.Mkdir(path.Join(cwtchDir, "testing"), 0700)
|
||||||
|
|
||||||
t.Logf("Creating Alice...")
|
t.Logf("Creating Alice...")
|
||||||
app.CreateTaggedPeer("alice", "asdfasdf", "testing")
|
app.CreateProfile("alice", "asdfasdf", true)
|
||||||
|
|
||||||
t.Logf("Creating Bob...")
|
t.Logf("Creating Bob...")
|
||||||
app.CreateTaggedPeer("bob", "asdfasdf", "testing")
|
app.CreateProfile("bob", "asdfasdf", true)
|
||||||
|
|
||||||
t.Logf("** Waiting for Alice, Bob...")
|
t.Logf("** Waiting for Alice, Bob...")
|
||||||
alice := app2.WaitGetPeer(app, "alice")
|
alice := app2.WaitGetPeer(app, "alice")
|
||||||
app.ActivatePeerEngine(alice.GetOnion(), true, true, true)
|
app.ActivatePeerEngine(alice.GetOnion())
|
||||||
bob := app2.WaitGetPeer(app, "bob")
|
bob := app2.WaitGetPeer(app, "bob")
|
||||||
app.ActivatePeerEngine(bob.GetOnion(), true, true, true)
|
app.ActivatePeerEngine(bob.GetOnion())
|
||||||
|
|
||||||
alice.AutoHandleEvents([]event.Type{event.PeerStateChange, event.NewRetValMessageFromPeer})
|
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()
|
queueOracle := event.NewQueue()
|
||||||
app.GetEventBus(bob.GetOnion()).Subscribe(event.FileDownloaded, queueOracle)
|
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
|
waitTime := time.Duration(30) * time.Second
|
||||||
t.Logf("** Waiting for Alice, Bob to connect with onion network... (%v)\n", waitTime)
|
t.Logf("** Waiting for Alice, Bob to connect with onion network... (%v)\n", waitTime)
|
||||||
time.Sleep(waitTime)
|
time.Sleep(waitTime)
|
||||||
|
@ -125,6 +140,7 @@ func TestFileSharing(t *testing.T) {
|
||||||
|
|
||||||
t.Logf("Waiting for alice and Bob to peer...")
|
t.Logf("Waiting for alice and Bob to peer...")
|
||||||
waitForPeerPeerConnection(t, alice, bob)
|
waitForPeerPeerConnection(t, alice, bob)
|
||||||
|
alice.AcceptConversation(1)
|
||||||
|
|
||||||
t.Logf("Alice and Bob are Connected!!")
|
t.Logf("Alice and Bob are Connected!!")
|
||||||
|
|
||||||
|
@ -145,20 +161,24 @@ func TestFileSharing(t *testing.T) {
|
||||||
|
|
||||||
// Test stopping and restarting file shares
|
// Test stopping and restarting file shares
|
||||||
t.Logf("Stopping File Share")
|
t.Logf("Stopping File Share")
|
||||||
alice.StopAllFileShares()
|
filesharingFunctionality.StopAllFileShares(alice)
|
||||||
|
|
||||||
// Allow time for the stop request to filter through Engine
|
// Allow time for the stop request to filter through Engine
|
||||||
time.Sleep(time.Second * 5)
|
time.Sleep(time.Second * 5)
|
||||||
|
|
||||||
// Restart
|
// Restart
|
||||||
t.Logf("Restarting File Share")
|
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
|
// run the same download test again...to check that we can actually download the file
|
||||||
testBobDownloadFile(t, bob, filesharingFunctionality, queueOracle)
|
testBobDownloadFile(t, bob, filesharingFunctionality, queueOracle)
|
||||||
|
|
||||||
// test that we can delete bob...
|
// test that we can delete bob...
|
||||||
app.DeletePeer(bob.GetOnion(), "asdfasdf")
|
app.DeleteProfile(bob.GetOnion(), "asdfasdf")
|
||||||
|
|
||||||
queueOracle.Shutdown()
|
queueOracle.Shutdown()
|
||||||
app.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")
|
||||||
os.RemoveAll("cwtch.out.png.manifest")
|
os.RemoveAll("cwtch.out.png.manifest")
|
||||||
|
|
||||||
|
bob.AcceptConversation(1)
|
||||||
message, _, err := bob.GetChannelMessage(1, 0, 1)
|
message, _, err := bob.GetChannelMessage(1, 0, 1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("could not find file sharing message: %v", err)
|
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)
|
err := json.Unmarshal([]byte(messageWrapper.Data), &fileMessageOverlay)
|
||||||
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
t.Logf("bob attempting to download file with invalid download")
|
||||||
// try downloading with invalid download dir
|
// 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)
|
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 {
|
if err == nil {
|
||||||
t.Fatalf("should not download file with invalid download dir")
|
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
|
// 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)
|
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 {
|
if err == nil {
|
||||||
t.Fatalf("should not download file with invalid manifest dir")
|
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)
|
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 {
|
if err != nil {
|
||||||
t.Fatalf("could not download file: %v", err)
|
t.Fatalf("could not download file: %v", err)
|
||||||
|
|
Loading…
Reference in New Issue