forked from cwtch.im/cwtch
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 (
|
||||
"cwtch.im/cwtch/app/plugins"
|
||||
"cwtch.im/cwtch/event"
|
||||
"cwtch.im/cwtch/extensions"
|
||||
"cwtch.im/cwtch/functionality/filesharing"
|
||||
"cwtch.im/cwtch/model"
|
||||
"cwtch.im/cwtch/model/attr"
|
||||
"cwtch.im/cwtch/model/constants"
|
||||
|
@ -29,16 +31,28 @@ type application struct {
|
|||
engines map[string]connections.Engine
|
||||
appBus event.Manager
|
||||
appmutex sync.Mutex
|
||||
|
||||
settings *GlobalSettingsFile
|
||||
}
|
||||
|
||||
func (app *application) IsFeatureEnabled(experiment string) bool {
|
||||
settings := app.ReadSettings()
|
||||
if settings.ExperimentsEnabled {
|
||||
if status, exists := settings.Experiments[experiment]; exists {
|
||||
return status
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Application is a full cwtch peer application. It allows management, usage and storage of multiple peers
|
||||
type Application interface {
|
||||
LoadProfiles(password string)
|
||||
CreatePeer(name string, password string, attributes map[attr.ZonedPath]string)
|
||||
// Deprecated in 1.10
|
||||
CreateTaggedPeer(name string, password string, tag string)
|
||||
CreateProfile(name string, password string, autostart bool)
|
||||
|
||||
ImportProfile(exportedCwtchFile string, password string) (peer.CwtchPeer, error)
|
||||
DeletePeer(onion string, currentPassword string)
|
||||
EnhancedImportProfile(exportedCwtchFile string, password string) string
|
||||
DeleteProfile(onion string, currentPassword string)
|
||||
AddPeerPlugin(onion string, pluginID plugins.PluginID)
|
||||
|
||||
GetPrimaryBus() event.Manager
|
||||
|
@ -47,9 +61,13 @@ type Application interface {
|
|||
QueryACNVersion()
|
||||
|
||||
ActivateEngines(doListn, doPeers, doServers bool)
|
||||
ActivatePeerEngine(onion string, doListen, doPeers, doServers bool)
|
||||
ActivatePeerEngine(onion string)
|
||||
DeactivatePeerEngine(onion string)
|
||||
|
||||
ReadSettings() GlobalSettings
|
||||
UpdateSettings(settings GlobalSettings)
|
||||
IsFeatureEnabled(experiment string) bool
|
||||
|
||||
ShutdownPeer(string)
|
||||
Shutdown()
|
||||
|
||||
|
@ -60,12 +78,24 @@ type Application interface {
|
|||
// LoadProfileFn is the function signature for a function in an app that loads a profile
|
||||
type LoadProfileFn func(profile peer.CwtchPeer)
|
||||
|
||||
// NewApp creates a new app with some environment awareness and initializes a Tor Manager
|
||||
func NewApp(acn connectivity.ACN, appDirectory string) Application {
|
||||
func LoadAppSettings(appDirectory string) *GlobalSettingsFile {
|
||||
log.Debugf("NewApp(%v)\n", appDirectory)
|
||||
os.MkdirAll(path.Join(appDirectory, "profiles"), 0700)
|
||||
|
||||
app := &application{engines: make(map[string]connections.Engine), eventBuses: make(map[string]event.Manager), directory: appDirectory, appBus: event.NewEventManager()}
|
||||
// Note: we basically presume this doesn't fail. If the file doesn't exist we create it, and as such the
|
||||
// only plausible error conditions are related to file create e.g. low disk space. If that is the case then
|
||||
// many other parts of Cwtch are likely to fail also.
|
||||
settings, err := InitGlobalSettingsFile(appDirectory, DefactoPasswordForUnencryptedProfiles)
|
||||
if err != nil {
|
||||
log.Errorf("error initializing global settings file %. Global settings might not be loaded or saves", err)
|
||||
}
|
||||
return settings
|
||||
}
|
||||
|
||||
// NewApp creates a new app with some environment awareness and initializes a Tor Manager
|
||||
func NewApp(acn connectivity.ACN, appDirectory string, settings *GlobalSettingsFile) Application {
|
||||
|
||||
app := &application{engines: make(map[string]connections.Engine), eventBuses: make(map[string]event.Manager), directory: appDirectory, appBus: event.NewEventManager(), settings: settings}
|
||||
app.peers = make(map[string]peer.CwtchPeer)
|
||||
|
||||
app.acn = acn
|
||||
|
@ -78,6 +108,34 @@ func NewApp(acn connectivity.ACN, appDirectory string) Application {
|
|||
return app
|
||||
}
|
||||
|
||||
func (app *application) ReadSettings() GlobalSettings {
|
||||
app.appmutex.Lock()
|
||||
defer app.appmutex.Unlock()
|
||||
return app.settings.ReadGlobalSettings()
|
||||
}
|
||||
|
||||
func (app *application) UpdateSettings(settings GlobalSettings) {
|
||||
// don't allow any other application changes while settings update
|
||||
app.appmutex.Lock()
|
||||
defer app.appmutex.Unlock()
|
||||
app.settings.WriteGlobalSettings(settings)
|
||||
|
||||
// we now need to propagate changes to all peers
|
||||
app.peerLock.Lock()
|
||||
defer app.peerLock.Unlock()
|
||||
for _, profile := range app.peers {
|
||||
profile.UpdateExperiments(settings.ExperimentsEnabled, settings.Experiments)
|
||||
|
||||
// Explicitly toggle blocking/unblocking of unknown connections for profiles
|
||||
// that have been loaded.
|
||||
if settings.BlockUnknownConnections {
|
||||
profile.BlockUnknownConnections()
|
||||
} else {
|
||||
profile.AllowUnknownConnections()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ListProfiles returns a map of onions to their profile's Name
|
||||
func (app *application) ListProfiles() []string {
|
||||
var keys []string
|
||||
|
@ -124,11 +182,45 @@ func (ap *application) AddPlugin(peerid string, id plugins.PluginID, bus event.M
|
|||
}
|
||||
}
|
||||
|
||||
func (app *application) CreateProfile(name string, password string, autostart bool) {
|
||||
autostartVal := constants.True
|
||||
if !autostart {
|
||||
autostartVal = constants.False
|
||||
}
|
||||
tagVal := constants.ProfileTypeV1Password
|
||||
if password == DefactoPasswordForUnencryptedProfiles {
|
||||
tagVal = constants.ProfileTypeV1DefaultPassword
|
||||
}
|
||||
|
||||
app.CreatePeer(name, password, map[attr.ZonedPath]string{
|
||||
attr.ProfileZone.ConstructZonedPath(constants.Tag): tagVal,
|
||||
attr.ProfileZone.ConstructZonedPath(constants.PeerAutostart): autostartVal,
|
||||
})
|
||||
}
|
||||
|
||||
// Deprecated in 1.10
|
||||
func (app *application) CreateTaggedPeer(name string, password string, tag string) {
|
||||
app.CreatePeer(name, password, map[attr.ZonedPath]string{attr.ProfileZone.ConstructZonedPath(constants.Tag): tag})
|
||||
}
|
||||
|
||||
func (app *application) setupPeer(profile peer.CwtchPeer) {
|
||||
eventBus := event.NewEventManager()
|
||||
app.eventBuses[profile.GetOnion()] = eventBus
|
||||
|
||||
// Initialize the Peer with the Given Event Bus
|
||||
app.peers[profile.GetOnion()] = profile
|
||||
profile.Init(app.eventBuses[profile.GetOnion()])
|
||||
|
||||
// Update the Peer with the Most Recent Experiment State...
|
||||
settings := app.settings.ReadGlobalSettings()
|
||||
profile.UpdateExperiments(settings.ExperimentsEnabled, settings.Experiments)
|
||||
app.registerHooks(profile)
|
||||
|
||||
// Register the Peer With Application Plugins..
|
||||
app.AddPeerPlugin(profile.GetOnion(), plugins.CONNECTIONRETRY) // Now Mandatory
|
||||
|
||||
}
|
||||
|
||||
func (app *application) CreatePeer(name string, password string, attributes map[attr.ZonedPath]string) {
|
||||
app.appmutex.Lock()
|
||||
defer app.appmutex.Unlock()
|
||||
|
@ -142,25 +234,26 @@ func (app *application) CreatePeer(name string, password string, attributes map[
|
|||
return
|
||||
}
|
||||
|
||||
eventBus := event.NewEventManager()
|
||||
app.eventBuses[profile.GetOnion()] = eventBus
|
||||
profile.Init(app.eventBuses[profile.GetOnion()])
|
||||
app.peers[profile.GetOnion()] = profile
|
||||
app.setupPeer(profile)
|
||||
|
||||
for zp, val := range attributes {
|
||||
zone, key := attr.ParseZone(zp.ToString())
|
||||
profile.SetScopedZonedAttribute(attr.LocalScope, zone, key, val)
|
||||
}
|
||||
|
||||
app.AddPeerPlugin(profile.GetOnion(), plugins.CONNECTIONRETRY) // Now Mandatory
|
||||
app.appBus.Publish(event.NewEvent(event.NewPeer, map[event.Field]string{event.Identity: profile.GetOnion(), event.Created: event.True}))
|
||||
}
|
||||
|
||||
func (app *application) DeletePeer(onion string, password string) {
|
||||
log.Infof("DeletePeer called on %v\n", onion)
|
||||
func (app *application) DeleteProfile(onion string, password string) {
|
||||
log.Debugf("DeleteProfile called on %v\n", onion)
|
||||
app.appmutex.Lock()
|
||||
defer app.appmutex.Unlock()
|
||||
|
||||
// allow a blank password to delete "unencrypted" accounts...
|
||||
if password == "" {
|
||||
password = DefactoPasswordForUnencryptedProfiles
|
||||
}
|
||||
|
||||
if app.peers[onion].CheckPassword(password) {
|
||||
// soft-shutdown
|
||||
app.peers[onion].Shutdown()
|
||||
|
@ -190,6 +283,14 @@ func (app *application) ImportProfile(exportedCwtchFile string, password string)
|
|||
return profile, err
|
||||
}
|
||||
|
||||
func (app *application) EnhancedImportProfile(exportedCwtchFile string, password string) string {
|
||||
_, err := app.ImportProfile(exportedCwtchFile, password)
|
||||
if err == nil {
|
||||
return ""
|
||||
}
|
||||
return err.Error()
|
||||
}
|
||||
|
||||
// LoadProfiles takes a password and attempts to load any profiles it can from storage with it and create Peers for them
|
||||
func (app *application) LoadProfiles(password string) {
|
||||
count := 0
|
||||
|
@ -242,6 +343,12 @@ func (app *application) LoadProfiles(password string) {
|
|||
}
|
||||
}
|
||||
|
||||
func (app *application) registerHooks(profile peer.CwtchPeer) {
|
||||
// Register Hooks
|
||||
profile.RegisterHook(extensions.ProfileValueExtension{})
|
||||
profile.RegisterHook(filesharing.Functionality{})
|
||||
}
|
||||
|
||||
// installProfile takes a profile and if it isn't loaded in the app, installs it and returns true
|
||||
func (app *application) installProfile(profile peer.CwtchPeer) bool {
|
||||
app.appmutex.Lock()
|
||||
|
@ -249,11 +356,8 @@ func (app *application) installProfile(profile peer.CwtchPeer) bool {
|
|||
|
||||
// Only attempt to finalize the profile if we don't have one loaded...
|
||||
if app.peers[profile.GetOnion()] == nil {
|
||||
eventBus := event.NewEventManager()
|
||||
app.eventBuses[profile.GetOnion()] = eventBus
|
||||
profile.Init(app.eventBuses[profile.GetOnion()])
|
||||
app.peers[profile.GetOnion()] = profile
|
||||
app.AddPeerPlugin(profile.GetOnion(), plugins.CONNECTIONRETRY) // Now Mandatory
|
||||
app.setupPeer(profile)
|
||||
// Finalize the Creation of Peer / Notify any Interfaces..
|
||||
app.appBus.Publish(event.NewEvent(event.NewPeer, map[event.Field]string{event.Identity: profile.GetOnion(), event.Created: event.False}))
|
||||
return true
|
||||
}
|
||||
|
@ -288,7 +392,7 @@ func (app *application) ActivateEngines(doListen, doPeers, doServers bool) {
|
|||
}
|
||||
|
||||
// ActivePeerEngine creates a peer engine for use with an ACN, should be called once the underlying ACN is online
|
||||
func (app *application) ActivatePeerEngine(onion string, doListen, doPeers, doServers bool) {
|
||||
func (app *application) ActivatePeerEngine(onion string) {
|
||||
profile := app.GetPeer(onion)
|
||||
if profile != nil {
|
||||
if _, exists := app.engines[onion]; !exists {
|
||||
|
@ -296,10 +400,10 @@ func (app *application) ActivatePeerEngine(onion string, doListen, doPeers, doSe
|
|||
|
||||
app.eventBuses[profile.GetOnion()].Publish(event.NewEventList(event.ProtocolEngineCreated))
|
||||
app.QueryACNStatus()
|
||||
if doListen {
|
||||
if true {
|
||||
profile.Listen()
|
||||
}
|
||||
profile.StartConnections(doPeers, doServers)
|
||||
profile.StartConnections(true, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
package app
|
||||
|
||||
// We offer "un-passworded" profiles but our storage encrypts everything with a password. We need an agreed upon
|
||||
// password to use in that case, that the app case use behind the scenes to password and unlock with
|
||||
// https://docs.openprivacy.ca/cwtch-security-handbook/profile_encryption_and_storage.html
|
||||
const DefactoPasswordForUnencryptedProfiles = "be gay do crime"
|
|
@ -27,7 +27,7 @@ func (a antispam) Shutdown() {
|
|||
}
|
||||
|
||||
func (a *antispam) run() {
|
||||
log.Infof("running antispam trigger plugin")
|
||||
log.Debugf("running antispam trigger plugin")
|
||||
for {
|
||||
select {
|
||||
case <-time.After(antispamTickTime):
|
||||
|
|
|
@ -0,0 +1,146 @@
|
|||
package app
|
||||
|
||||
import (
|
||||
"cwtch.im/cwtch/event"
|
||||
"cwtch.im/cwtch/model/constants"
|
||||
"cwtch.im/cwtch/storage/v1"
|
||||
"encoding/json"
|
||||
"git.openprivacy.ca/openprivacy/log"
|
||||
"os"
|
||||
path "path/filepath"
|
||||
)
|
||||
|
||||
const (
|
||||
CwtchStarted = event.Type("CwtchStarted")
|
||||
CwtchStartError = event.Type("CwtchStartError")
|
||||
UpdateGlobalSettings = event.Type("UpdateGlobalSettings")
|
||||
)
|
||||
|
||||
const GlobalSettingsFilename = "ui.globals"
|
||||
const saltFile = "SALT"
|
||||
|
||||
type NotificationPolicy string
|
||||
|
||||
const (
|
||||
NotificationPolicyMute = NotificationPolicy("NotificationPolicy.Mute")
|
||||
NotificationPolicyOptIn = NotificationPolicy("NotificationPolicy.OptIn")
|
||||
NotificationPolicyDefaultAll = NotificationPolicy("NotificationPolicy.DefaultAll")
|
||||
)
|
||||
|
||||
type GlobalSettingsFile struct {
|
||||
v1.FileStore
|
||||
}
|
||||
|
||||
type GlobalSettings struct {
|
||||
Locale string
|
||||
Theme string
|
||||
ThemeMode string
|
||||
PreviousPid int64
|
||||
ExperimentsEnabled bool
|
||||
Experiments map[string]bool
|
||||
BlockUnknownConnections bool
|
||||
NotificationPolicy NotificationPolicy
|
||||
NotificationContent string
|
||||
StreamerMode bool
|
||||
StateRootPane int
|
||||
FirstTime bool
|
||||
UIColumnModePortrait string
|
||||
UIColumnModeLandscape string
|
||||
DownloadPath string
|
||||
AllowAdvancedTorConfig bool
|
||||
CustomTorrc string
|
||||
UseCustomTorrc bool
|
||||
UseExternalTor bool
|
||||
CustomSocksPort int
|
||||
CustomControlPort int
|
||||
UseTorCache bool
|
||||
TorCacheDir string
|
||||
}
|
||||
|
||||
var DefaultGlobalSettings = GlobalSettings{
|
||||
Locale: "en",
|
||||
Theme: "dark",
|
||||
PreviousPid: -1,
|
||||
ExperimentsEnabled: false,
|
||||
Experiments: map[string]bool{constants.MessageFormattingExperiment: true},
|
||||
StateRootPane: 0,
|
||||
FirstTime: true,
|
||||
BlockUnknownConnections: false,
|
||||
StreamerMode: false,
|
||||
UIColumnModePortrait: "DualpaneMode.Single",
|
||||
UIColumnModeLandscape: "DualpaneMode.CopyPortrait",
|
||||
NotificationPolicy: "NotificationPolicy.Mute",
|
||||
NotificationContent: "NotificationContent.SimpleEvent",
|
||||
DownloadPath: "",
|
||||
AllowAdvancedTorConfig: false,
|
||||
CustomTorrc: "",
|
||||
UseCustomTorrc: false,
|
||||
CustomSocksPort: -1,
|
||||
CustomControlPort: -1,
|
||||
UseTorCache: false,
|
||||
TorCacheDir: "",
|
||||
}
|
||||
|
||||
func InitGlobalSettingsFile(directory string, password string) (*GlobalSettingsFile, error) {
|
||||
var key [32]byte
|
||||
salt, err := os.ReadFile(path.Join(directory, saltFile))
|
||||
if err != nil {
|
||||
log.Infof("Could not find salt file: %v (creating a new settings file)", err)
|
||||
var newSalt [128]byte
|
||||
key, newSalt, err = v1.CreateKeySalt(password)
|
||||
if err != nil {
|
||||
log.Errorf("Could not initialize salt: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
os.Mkdir(directory, 0700)
|
||||
err := os.WriteFile(path.Join(directory, saltFile), newSalt[:], 0600)
|
||||
if err != nil {
|
||||
log.Errorf("Could not write salt file: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
key = v1.CreateKey(password, salt)
|
||||
}
|
||||
|
||||
gsFile := v1.NewFileStore(directory, GlobalSettingsFilename, key)
|
||||
log.Infof("initialized global settings file: %v", gsFile)
|
||||
globalSettingsFile := GlobalSettingsFile{
|
||||
gsFile,
|
||||
}
|
||||
return &globalSettingsFile, nil
|
||||
}
|
||||
|
||||
func (globalSettingsFile *GlobalSettingsFile) ReadGlobalSettings() GlobalSettings {
|
||||
settings := DefaultGlobalSettings
|
||||
|
||||
if globalSettingsFile == nil {
|
||||
log.Errorf("Global Settings File was not Initialized Properly")
|
||||
return settings
|
||||
}
|
||||
|
||||
settingsBytes, err := globalSettingsFile.Read()
|
||||
if err != nil {
|
||||
log.Infof("Could not read global ui settings: %v (assuming this is a first time app deployment...)", err)
|
||||
return settings //firstTime = true
|
||||
}
|
||||
|
||||
err = json.Unmarshal(settingsBytes, &settings)
|
||||
if err != nil {
|
||||
log.Errorf("Could not parse global ui settings: %v\n", err)
|
||||
// TODO if settings is corrupted, we probably want to alert the UI.
|
||||
return settings //firstTime = true
|
||||
}
|
||||
|
||||
log.Debugf("Settings: %#v", settings)
|
||||
return settings
|
||||
}
|
||||
|
||||
func (globalSettingsFile *GlobalSettingsFile) WriteGlobalSettings(globalSettings GlobalSettings) {
|
||||
bytes, _ := json.Marshal(globalSettings)
|
||||
// override first time setting
|
||||
globalSettings.FirstTime = true
|
||||
err := globalSettingsFile.Write(bytes)
|
||||
if err != nil {
|
||||
log.Errorf("Could not write global ui settings: %v\n", err)
|
||||
}
|
||||
}
|
|
@ -228,7 +228,8 @@ type Field string
|
|||
const (
|
||||
|
||||
// A peers local onion address
|
||||
Onion = Field("Onion")
|
||||
Onion = Field("Onion")
|
||||
ProfileOnion = Field("ProfileOnion")
|
||||
|
||||
RemotePeer = Field("RemotePeer")
|
||||
LastSeen = Field("LastSeen")
|
||||
|
|
|
@ -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 (
|
||||
"crypto/rand"
|
||||
"cwtch.im/cwtch/event"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"math/bits"
|
||||
"os"
|
||||
path "path/filepath"
|
||||
"regexp"
|
||||
|
@ -28,6 +30,109 @@ import (
|
|||
type Functionality struct {
|
||||
}
|
||||
|
||||
func (f Functionality) EventsToRegister() []event.Type {
|
||||
return []event.Type{event.ManifestReceived, event.FileDownloaded}
|
||||
}
|
||||
|
||||
func (f Functionality) ExperimentsToRegister() []string {
|
||||
return []string{constants.FileSharingExperiment}
|
||||
}
|
||||
|
||||
// OnEvent handles File Sharing Hooks like Manifest Received and FileDownloaded
|
||||
func (f Functionality) OnEvent(ev event.Event, profile peer.CwtchPeer) {
|
||||
if profile.IsFeatureEnabled(constants.FileSharingExperiment) {
|
||||
switch ev.EventType {
|
||||
case event.ManifestReceived:
|
||||
log.Debugf("Manifest Received Event!: %v", ev)
|
||||
handle := ev.Data[event.Handle]
|
||||
fileKey := ev.Data[event.FileKey]
|
||||
serializedManifest := ev.Data[event.SerializedManifest]
|
||||
|
||||
manifestFilePath, exists := profile.GetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%v.manifest", fileKey))
|
||||
if exists {
|
||||
downloadFilePath, exists := profile.GetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%v.path", fileKey))
|
||||
if exists {
|
||||
log.Debugf("downloading manifest to %v, file to %v", manifestFilePath, downloadFilePath)
|
||||
var manifest files.Manifest
|
||||
err := json.Unmarshal([]byte(serializedManifest), &manifest)
|
||||
|
||||
if err == nil {
|
||||
// We only need to check the file size here, as manifest is sent to engine and the file created
|
||||
// will be bound to the size advertised in manifest.
|
||||
fileSizeLimitValue, fileSizeLimitExists := profile.GetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%v.limit", fileKey))
|
||||
if fileSizeLimitExists {
|
||||
fileSizeLimit, err := strconv.ParseUint(fileSizeLimitValue, 10, bits.UintSize)
|
||||
if err == nil {
|
||||
if manifest.FileSizeInBytes >= fileSizeLimit {
|
||||
log.Errorf("could not download file, size %v greater than limit %v", manifest.FileSizeInBytes, fileSizeLimitValue)
|
||||
} else {
|
||||
manifest.Title = manifest.FileName
|
||||
manifest.FileName = downloadFilePath
|
||||
log.Debugf("saving manifest")
|
||||
err = manifest.Save(manifestFilePath)
|
||||
if err != nil {
|
||||
log.Errorf("could not save manifest: %v", err)
|
||||
} else {
|
||||
tempFile := ""
|
||||
if runtime.GOOS == "android" {
|
||||
tempFile = manifestFilePath[0 : len(manifestFilePath)-len(".manifest")]
|
||||
log.Debugf("derived android temp path: %v", tempFile)
|
||||
}
|
||||
profile.PublishEvent(event.NewEvent(event.ManifestSaved, map[event.Field]string{
|
||||
event.FileKey: fileKey,
|
||||
event.Handle: handle,
|
||||
event.SerializedManifest: string(manifest.Serialize()),
|
||||
event.TempFile: tempFile,
|
||||
event.NameSuggestion: manifest.Title,
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.Errorf("error saving manifest: %v", err)
|
||||
}
|
||||
} else {
|
||||
log.Errorf("found manifest path but not download path for %v", fileKey)
|
||||
}
|
||||
} else {
|
||||
log.Errorf("no download path found for manifest: %v", fileKey)
|
||||
}
|
||||
case event.FileDownloaded:
|
||||
fileKey := ev.Data[event.FileKey]
|
||||
profile.SetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.complete", fileKey), "true")
|
||||
}
|
||||
} else {
|
||||
log.Errorf("profile called filesharing experiment OnContactReceiveValue even though file sharing was not enabled. This is likely a programming error.")
|
||||
}
|
||||
}
|
||||
|
||||
func (f Functionality) OnContactRequestValue(profile peer.CwtchPeer, conversation model.Conversation, eventID string, path attr.ScopedZonedPath) {
|
||||
// nop
|
||||
}
|
||||
|
||||
func (f Functionality) OnContactReceiveValue(profile peer.CwtchPeer, conversation model.Conversation, path attr.ScopedZonedPath, value string, exists bool) {
|
||||
// Profile should not call us if FileSharing is disabled
|
||||
if profile.IsFeatureEnabled(constants.FileSharingExperiment) {
|
||||
scope, zone, zpath := path.GetScopeZonePath()
|
||||
log.Debugf("file sharing contact receive value")
|
||||
if exists && scope.IsConversation() && zone == attr.FilesharingZone && strings.HasSuffix(zpath, ".manifest.size") {
|
||||
fileKey := strings.Replace(zpath, ".manifest.size", "", 1)
|
||||
size, err := strconv.Atoi(value)
|
||||
// if size is valid and below the maximum size for a manifest
|
||||
// this is to prevent malicious sharers from using large amounts of memory when distributing
|
||||
// a manifest as we reconstruct this in-memory
|
||||
if err == nil && size < files.MaxManifestSize {
|
||||
profile.PublishEvent(event.NewEvent(event.ManifestSizeReceived, map[event.Field]string{event.FileKey: fileKey, event.ManifestSize: value, event.Handle: conversation.Handle}))
|
||||
} else {
|
||||
profile.PublishEvent(event.NewEvent(event.ManifestError, map[event.Field]string{event.FileKey: fileKey, event.Handle: conversation.Handle}))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.Errorf("profile called filesharing experiment OnContactReceiveValue even though file sharing was not enabled. This is likely a programming error.")
|
||||
}
|
||||
}
|
||||
|
||||
// FunctionalityGate returns filesharing if enabled in the given experiment map
|
||||
// Note: Experiment maps are currently in libcwtch-go
|
||||
func FunctionalityGate(experimentMap map[string]bool) (*Functionality, error) {
|
||||
|
@ -73,10 +178,74 @@ func (om *OverlayMessage) ShouldAutoDL() bool {
|
|||
return false
|
||||
}
|
||||
|
||||
func (f *Functionality) VerifyOrResumeDownload(profile peer.CwtchPeer, conversation int, fileKey string) {
|
||||
if manifestFilePath, exists := profile.GetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.manifest", fileKey)); exists {
|
||||
if downloadfilepath, exists := profile.GetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.path", fileKey)); exists {
|
||||
log.Debugf("resuming %s", fileKey)
|
||||
f.DownloadFile(profile, conversation, downloadfilepath, manifestFilePath, fileKey, files.MaxManifestSize*files.DefaultChunkSize)
|
||||
} else {
|
||||
log.Errorf("found manifest path but not download path for %s", fileKey)
|
||||
}
|
||||
} else {
|
||||
log.Errorf("no stored manifest path found for %s", fileKey)
|
||||
}
|
||||
}
|
||||
|
||||
func (f *Functionality) CheckDownloadStatus(profile peer.CwtchPeer, fileKey string) {
|
||||
path, _ := profile.GetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.path", fileKey))
|
||||
if value, exists := profile.GetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.complete", fileKey)); exists && value == event.True {
|
||||
profile.PublishEvent(event.NewEvent(event.FileDownloaded, map[event.Field]string{
|
||||
event.ProfileOnion: profile.GetOnion(),
|
||||
event.FileKey: fileKey,
|
||||
event.FilePath: path,
|
||||
event.TempFile: "",
|
||||
}))
|
||||
} else {
|
||||
log.Debugf("CheckDownloadStatus found .path but not .complete")
|
||||
profile.PublishEvent(event.NewEvent(event.FileDownloadProgressUpdate, map[event.Field]string{
|
||||
event.ProfileOnion: profile.GetOnion(),
|
||||
event.FileKey: fileKey,
|
||||
event.Progress: "-1",
|
||||
event.FileSizeInChunks: "-1",
|
||||
event.FilePath: path,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
func (f *Functionality) EnhancedShareFile(profile peer.CwtchPeer, conversationID int, sharefilepath string) string {
|
||||
fileKey, overlay, err := f.ShareFile(sharefilepath, profile)
|
||||
if err != nil {
|
||||
log.Errorf("error sharing file: %v", err)
|
||||
} else if conversationID == -1 {
|
||||
// FIXME: At some point we might want to allow arbitrary public files, but for now this API will assume
|
||||
// there is only one, and it is the custom profile image...
|
||||
profile.SetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants.CustomProfileImageKey, fileKey)
|
||||
} else {
|
||||
// Set a new attribute so we can associate this download with this conversation...
|
||||
profile.SetConversationAttribute(conversationID, attr.ConversationScope.ConstructScopedZonedPath(attr.FilesharingZone.ConstructZonedPath(fileKey)), "")
|
||||
id, err := profile.SendMessage(conversationID, overlay)
|
||||
if err == nil {
|
||||
return profile.EnhancedGetMessageById(conversationID, id)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// DownloadFileDefaultLimit given a profile, a conversation handle and a file sharing key, start off a download process
|
||||
// to downloadFilePath with a default filesize limit
|
||||
func (f *Functionality) DownloadFileDefaultLimit(profile peer.CwtchPeer, conversationID int, downloadFilePath string, manifestFilePath string, key string) error {
|
||||
return f.DownloadFile(profile, conversationID, downloadFilePath, manifestFilePath, key, files.MaxManifestSize*files.DefaultChunkSize)
|
||||
}
|
||||
|
||||
// DownloadFile given a profile, a conversation handle and a file sharing key, start off a download process
|
||||
// to downloadFilePath
|
||||
func (f *Functionality) DownloadFile(profile peer.CwtchPeer, conversationID int, downloadFilePath string, manifestFilePath string, key string, limit uint64) error {
|
||||
|
||||
// assert that we are allowed to download the file
|
||||
if !profile.IsFeatureEnabled(constants.FileSharingExperiment) {
|
||||
return errors.New("filesharing functionality is not enabled")
|
||||
}
|
||||
|
||||
// Don't download files if the download or manifest path is not set
|
||||
if downloadFilePath == "" || manifestFilePath == "" {
|
||||
return errors.New("download path or manifest path is empty")
|
||||
|
@ -109,14 +278,37 @@ func (f *Functionality) DownloadFile(profile peer.CwtchPeer, conversationID int,
|
|||
return nil
|
||||
}
|
||||
|
||||
// startFileShare is a private method used to finalize a file share and publish it to the protocol engine for processing.
|
||||
func (f *Functionality) startFileShare(profile peer.CwtchPeer, filekey string, manifest string) error {
|
||||
tsStr, exists := profile.GetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.ts", filekey))
|
||||
if exists {
|
||||
ts, err := strconv.ParseInt(tsStr, 10, 64)
|
||||
if err != nil || ts < time.Now().Unix()-2592000 {
|
||||
log.Errorf("ignoring request to download a file offered more than 30 days ago")
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// set the filekey status to active
|
||||
profile.SetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.active", filekey), constants.True)
|
||||
profile.PublishEvent(event.NewEvent(event.ShareManifest, map[event.Field]string{event.FileKey: filekey, event.SerializedManifest: manifest}))
|
||||
return nil
|
||||
}
|
||||
|
||||
// RestartFileShare takes in an existing filekey and, assuming the manifest exists, restarts sharing of the manifest
|
||||
func (f *Functionality) RestartFileShare(profile peer.CwtchPeer, filekey string) error {
|
||||
|
||||
// assert that we are allowed to restart filesharing
|
||||
if !profile.IsFeatureEnabled(constants.FileSharingExperiment) {
|
||||
return errors.New("filesharing functionality is not enabled")
|
||||
}
|
||||
|
||||
// check that a manifest exists
|
||||
manifest, manifestExists := profile.GetScopedZonedAttribute(attr.ConversationScope, attr.FilesharingZone, fmt.Sprintf("%s.manifest", filekey))
|
||||
if manifestExists {
|
||||
// everything is in order, so reshare this file with the engine
|
||||
profile.ShareFile(filekey, manifest)
|
||||
return nil
|
||||
log.Debugf("restarting file share: %v", filekey)
|
||||
return f.startFileShare(profile, filekey, manifest)
|
||||
}
|
||||
return fmt.Errorf("manifest does not exist for filekey: %v", filekey)
|
||||
}
|
||||
|
@ -124,6 +316,12 @@ func (f *Functionality) RestartFileShare(profile peer.CwtchPeer, filekey string)
|
|||
// ReShareFiles given a profile we iterate through all existing fileshares and re-share them
|
||||
// if the time limit has not expired
|
||||
func (f *Functionality) ReShareFiles(profile peer.CwtchPeer) error {
|
||||
|
||||
// assert that we are allowed to restart filesharing
|
||||
if !profile.IsFeatureEnabled(constants.FileSharingExperiment) {
|
||||
return errors.New("filesharing functionality is not enabled")
|
||||
}
|
||||
|
||||
keys, err := profile.GetScopedZonedAttributeKeys(attr.LocalScope, attr.FilesharingZone)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -132,6 +330,7 @@ func (f *Functionality) ReShareFiles(profile peer.CwtchPeer) error {
|
|||
for _, key := range keys {
|
||||
// only look at timestamp keys
|
||||
// this is an arbitrary choice
|
||||
|
||||
if strings.HasSuffix(key, ".ts") {
|
||||
_, zonedpath := attr.ParseScope(key)
|
||||
_, keypath := attr.ParseZone(zonedpath)
|
||||
|
@ -148,7 +347,12 @@ func (f *Functionality) ReShareFiles(profile peer.CwtchPeer) error {
|
|||
// Then attempt to share this file again...
|
||||
// TODO: In the future this would be the point to change the timestamp and reshare the file...
|
||||
if err == nil && sharedFile.Active {
|
||||
f.RestartFileShare(profile, filekey)
|
||||
err := f.RestartFileShare(profile, filekey)
|
||||
if err != nil {
|
||||
log.Errorf("could not reshare file: %v", err)
|
||||
}
|
||||
} else {
|
||||
log.Errorf("could not get fileshare info %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -184,6 +388,12 @@ func (f *Functionality) GetFileShareInfo(profile peer.CwtchPeer, filekey string)
|
|||
// ShareFile given a profile and a conversation handle, sets up a file sharing process to share the file
|
||||
// at filepath
|
||||
func (f *Functionality) ShareFile(filepath string, profile peer.CwtchPeer) (string, string, error) {
|
||||
|
||||
// assert that we are allowed to share files
|
||||
if !profile.IsFeatureEnabled(constants.FileSharingExperiment) {
|
||||
return "", "", errors.New("filesharing functionality is not enabled")
|
||||
}
|
||||
|
||||
manifest, err := files.CreateManifest(filepath)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
|
@ -231,7 +441,7 @@ func (f *Functionality) ShareFile(filepath string, profile peer.CwtchPeer) (stri
|
|||
profile.SetScopedZonedAttribute(attr.ConversationScope, attr.FilesharingZone, fmt.Sprintf("%s.manifest", key), string(serializedManifest))
|
||||
profile.SetScopedZonedAttribute(attr.ConversationScope, attr.FilesharingZone, fmt.Sprintf("%s.manifest.size", key), strconv.Itoa(int(math.Ceil(float64(len(serializedManifest)-lenDiff)/float64(files.DefaultChunkSize)))))
|
||||
|
||||
profile.ShareFile(key, string(serializedManifest))
|
||||
err = f.startFileShare(profile, key, string(serializedManifest))
|
||||
|
||||
return key, string(wrapperJSON), err
|
||||
}
|
||||
|
@ -258,6 +468,14 @@ type SharedFile struct {
|
|||
Expired bool
|
||||
}
|
||||
|
||||
func (f *Functionality) EnhancedGetSharedFiles(profile peer.CwtchPeer, conversationID int) string {
|
||||
data, err := json.Marshal(f.GetSharedFiles(profile, conversationID))
|
||||
if err == nil {
|
||||
return string(data)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// GetSharedFiles returns all file shares associated with a given conversation
|
||||
func (f *Functionality) GetSharedFiles(profile peer.CwtchPeer, conversationID int) []SharedFile {
|
||||
sharedFiles := []SharedFile{}
|
||||
|
@ -323,3 +541,17 @@ func GenerateDownloadPath(basePath, fileName string, overwrite bool) (filePath,
|
|||
}
|
||||
return
|
||||
}
|
||||
|
||||
// StopFileShare sends a message to the ProtocolEngine to cease sharing a particular file
|
||||
func (f *Functionality) StopFileShare(profile peer.CwtchPeer, fileKey string) {
|
||||
// Note we do not do a permissions check here, as we are *always* permitted to stop sharing files.
|
||||
// set the filekey status to inactive
|
||||
profile.SetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.active", fileKey), constants.False)
|
||||
profile.PublishEvent(event.NewEvent(event.StopFileShare, map[event.Field]string{event.FileKey: fileKey}))
|
||||
}
|
||||
|
||||
// StopAllFileShares sends a message to the ProtocolEngine to cease sharing all files
|
||||
func (f *Functionality) StopAllFileShares(profile peer.CwtchPeer) {
|
||||
// Note we do not do a permissions check here, as we are *always* permitted to stop sharing files.
|
||||
profile.PublishEvent(event.NewEvent(event.StopAllFileShares, map[event.Field]string{}))
|
||||
}
|
||||
|
|
|
@ -23,6 +23,12 @@ type Scope string
|
|||
// ScopedZonedPath typed path with a scope and a zone
|
||||
type ScopedZonedPath string
|
||||
|
||||
func (szp ScopedZonedPath) GetScopeZonePath() (Scope, Zone, string) {
|
||||
scope, path := ParseScope(string(szp))
|
||||
zone, zpath := ParseZone(path)
|
||||
return scope, zone, zpath
|
||||
}
|
||||
|
||||
// scopes for attributes
|
||||
const (
|
||||
// on a peer, local and peer supplied data
|
||||
|
|
|
@ -56,3 +56,5 @@ const SyncPreLastMessageTime = "SyncPreLastMessageTime"
|
|||
const SyncMostRecentMessageTime = "SyncMostRecentMessageTime"
|
||||
|
||||
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
|
||||
const ImagePreviewMaxSizeInBytes = 20971520
|
||||
|
||||
const MessageFormattingExperiment = "message-formatting"
|
||||
|
||||
// AutoDLFileExts Files with these extensions will be autodownloaded using ImagePreviewsExperiment
|
||||
var AutoDLFileExts = [...]string{".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp"}
|
||||
|
|
|
@ -0,0 +1,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/tor"
|
||||
"golang.org/x/crypto/ed25519"
|
||||
"math/bits"
|
||||
"os"
|
||||
path "path/filepath"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
@ -28,7 +26,6 @@ import (
|
|||
"cwtch.im/cwtch/model"
|
||||
"cwtch.im/cwtch/model/attr"
|
||||
"cwtch.im/cwtch/protocol/connections"
|
||||
"cwtch.im/cwtch/protocol/files"
|
||||
"git.openprivacy.ca/openprivacy/log"
|
||||
)
|
||||
|
||||
|
@ -38,8 +35,7 @@ const lastReceivedSignature = "LastReceivedSignature"
|
|||
var autoHandleableEvents = map[event.Type]bool{event.EncryptedGroupMessage: true, event.PeerStateChange: true,
|
||||
event.ServerStateChange: true, event.NewGroupInvite: true, event.NewMessageFromPeerEngine: true,
|
||||
event.PeerAcknowledgement: true, event.PeerError: true, event.SendMessageToPeerError: true, event.SendMessageToGroupError: true,
|
||||
event.NewGetValMessageFromPeer: true, event.NewRetValMessageFromPeer: true, event.ProtocolEngineStopped: true, event.RetryServerRequest: true,
|
||||
event.ManifestSizeReceived: true, event.ManifestReceived: true, event.FileDownloaded: true, event.TriggerAntispamCheck: true}
|
||||
event.NewGetValMessageFromPeer: true, event.NewRetValMessageFromPeer: true, event.ProtocolEngineStopped: true, event.RetryServerRequest: true, event.TriggerAntispamCheck: true}
|
||||
|
||||
// DefaultEventsToHandle specifies which events will be subscribed to
|
||||
//
|
||||
|
@ -58,8 +54,6 @@ var DefaultEventsToHandle = []event.Type{
|
|||
event.ServerStateChange,
|
||||
event.SendMessageToPeerError,
|
||||
event.NewRetValMessageFromPeer,
|
||||
event.ManifestReceived,
|
||||
event.FileDownloaded,
|
||||
event.TriggerAntispamCheck,
|
||||
}
|
||||
|
||||
|
@ -74,6 +68,132 @@ type cwtchPeer struct {
|
|||
|
||||
queue event.Queue
|
||||
eventBus event.Manager
|
||||
|
||||
extensions []ProfileHook
|
||||
extensionLock sync.Mutex // we don't want to hold up all of cwtch for managing thread safe access to extensions
|
||||
experiments model.Experiments
|
||||
experimentsLock sync.Mutex
|
||||
}
|
||||
|
||||
func (cp *cwtchPeer) EnhancedImportBundle(importString string) string {
|
||||
return cp.ImportBundle(importString).Error()
|
||||
}
|
||||
|
||||
func (cp *cwtchPeer) EnhancedGetMessages(conversation int, index int, count int) string {
|
||||
var emessages []EnhancedMessage = make([]EnhancedMessage, count)
|
||||
|
||||
messages, err := cp.GetMostRecentMessages(conversation, 0, index, count)
|
||||
if err == nil {
|
||||
|
||||
for i, message := range messages {
|
||||
|
||||
time, _ := time.Parse(time.RFC3339Nano, message.Attr[constants.AttrSentTimestamp])
|
||||
emessages[i].Message = model.Message{
|
||||
Message: message.Body,
|
||||
Acknowledged: message.Attr[constants.AttrAck] == constants.True,
|
||||
Error: message.Attr[constants.AttrErr],
|
||||
PeerID: message.Attr[constants.AttrAuthor],
|
||||
Timestamp: time,
|
||||
}
|
||||
emessages[i].ID = message.ID
|
||||
emessages[i].Attributes = message.Attr
|
||||
emessages[i].ContentHash = model.CalculateContentHash(message.Attr[constants.AttrAuthor], message.Body)
|
||||
}
|
||||
}
|
||||
|
||||
bytes, _ := json.Marshal(emessages)
|
||||
return string(bytes)
|
||||
}
|
||||
|
||||
func (cp *cwtchPeer) EnhancedGetMessageById(conversation int, messageID int) string {
|
||||
var message EnhancedMessage
|
||||
dbmessage, attr, err := cp.GetChannelMessage(conversation, 0, messageID)
|
||||
if err == nil {
|
||||
time, _ := time.Parse(time.RFC3339Nano, attr[constants.AttrSentTimestamp])
|
||||
message.Message = model.Message{
|
||||
Message: dbmessage,
|
||||
Acknowledged: attr[constants.AttrAck] == constants.True,
|
||||
Error: attr[constants.AttrErr],
|
||||
PeerID: attr[constants.AttrAuthor],
|
||||
Timestamp: time,
|
||||
}
|
||||
message.ID = messageID
|
||||
message.Attributes = attr
|
||||
message.ContentHash = model.CalculateContentHash(attr[constants.AttrAuthor], dbmessage)
|
||||
}
|
||||
bytes, _ := json.Marshal(message)
|
||||
return string(bytes)
|
||||
}
|
||||
|
||||
func (cp *cwtchPeer) EnhancedGetMessageByContentHash(conversation int, contentHash string) string {
|
||||
var message EnhancedMessage
|
||||
offset, err := cp.GetChannelMessageByContentHash(conversation, 0, contentHash)
|
||||
if err == nil {
|
||||
messages, err := cp.GetMostRecentMessages(conversation, 0, offset, 1)
|
||||
if err == nil {
|
||||
time, _ := time.Parse(time.RFC3339Nano, messages[0].Attr[constants.AttrSentTimestamp])
|
||||
message.Message = model.Message{
|
||||
Message: messages[0].Body,
|
||||
Acknowledged: messages[0].Attr[constants.AttrAck] == constants.True,
|
||||
Error: messages[0].Attr[constants.AttrErr],
|
||||
PeerID: messages[0].Attr[constants.AttrAuthor],
|
||||
Timestamp: time,
|
||||
}
|
||||
message.ID = messages[0].ID
|
||||
message.Attributes = messages[0].Attr
|
||||
message.LocalIndex = offset
|
||||
message.ContentHash = contentHash
|
||||
} else {
|
||||
log.Errorf("error fetching local index {} ", err)
|
||||
}
|
||||
}
|
||||
bytes, _ := json.Marshal(message)
|
||||
return string(bytes)
|
||||
}
|
||||
|
||||
func (cp *cwtchPeer) EnhancedSendMessage(conversation int, message string) string {
|
||||
mid, err := cp.SendMessage(conversation, message)
|
||||
if err == nil {
|
||||
return cp.EnhancedGetMessageById(conversation, mid)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (cp *cwtchPeer) ArchiveConversation(conversationID int) {
|
||||
cp.SetConversationAttribute(conversationID, attr.LocalScope.ConstructScopedZonedPath(attr.ProfileZone.ConstructZonedPath(constants.Archived)), constants.True)
|
||||
}
|
||||
|
||||
// IsFeatureEnabled returns true if the functionality defined by featureName has been enabled by the application, false otherwise.
|
||||
// this function is intended to be used by ProfileHooks to determine if they should execute experimental functionality.
|
||||
func (cp *cwtchPeer) IsFeatureEnabled(featureName string) bool {
|
||||
cp.experimentsLock.Lock()
|
||||
defer cp.experimentsLock.Unlock()
|
||||
return cp.experiments.IsEnabled(featureName)
|
||||
}
|
||||
|
||||
// UpdateExperiments notifies a Cwtch profile of a change in the nature of global experiments. The Cwtch Profile uses
|
||||
// this information to update registered extensions.
|
||||
func (cp *cwtchPeer) UpdateExperiments(enabled bool, experiments map[string]bool) {
|
||||
cp.experimentsLock.Lock()
|
||||
defer cp.experimentsLock.Unlock()
|
||||
cp.experiments = model.InitExperiments(enabled, experiments)
|
||||
}
|
||||
|
||||
func (cp *cwtchPeer) PublishEvent(resp event.Event) {
|
||||
log.Debugf("Publishing Event: %v %v", resp.EventType, resp.Data)
|
||||
cp.eventBus.Publish(resp)
|
||||
}
|
||||
|
||||
func (cp *cwtchPeer) RegisterHook(extension ProfileHooks) {
|
||||
cp.extensionLock.Lock()
|
||||
defer cp.extensionLock.Unlock()
|
||||
|
||||
// Register Requested Events
|
||||
for _, event := range extension.EventsToRegister() {
|
||||
cp.eventBus.Subscribe(event, cp.queue)
|
||||
}
|
||||
|
||||
cp.extensions = append(cp.extensions, ConstructHook(extension))
|
||||
}
|
||||
|
||||
func (cp *cwtchPeer) StoreCachedTokens(tokenServer string, tokens []*privacypass.Token) {
|
||||
|
@ -87,7 +207,7 @@ func (cp *cwtchPeer) StoreCachedTokens(tokenServer string, tokens []*privacypass
|
|||
}
|
||||
}
|
||||
|
||||
func (cp *cwtchPeer) Export(file string) error {
|
||||
func (cp *cwtchPeer) ExportProfile(file string) error {
|
||||
cp.mutex.Lock()
|
||||
defer cp.mutex.Unlock()
|
||||
return cp.storage.Export(file)
|
||||
|
@ -141,7 +261,7 @@ func (cp *cwtchPeer) ChangePassword(password string, newpassword string, newpass
|
|||
// probably redundant but we like api safety
|
||||
if newpassword == newpasswordAgain {
|
||||
rekey := createKey(newpassword, salt)
|
||||
log.Infof("rekeying database...")
|
||||
log.Debugf("rekeying database...")
|
||||
return cp.storage.Rekey(rekey)
|
||||
}
|
||||
return errors.New(constants.PasswordsDoNotMatchError)
|
||||
|
@ -1165,33 +1285,6 @@ func (cp *cwtchPeer) storeMessage(handle string, message string, sent time.Time)
|
|||
return cp.storage.InsertMessage(ci.ID, 0, message, model.Attributes{constants.AttrAuthor: handle, constants.AttrAck: event.True, constants.AttrSentTimestamp: sent.Format(time.RFC3339Nano)}, signature, model.CalculateContentHash(handle, message))
|
||||
}
|
||||
|
||||
// ShareFile begins hosting the given serialized manifest
|
||||
func (cp *cwtchPeer) ShareFile(fileKey string, serializedManifest string) {
|
||||
tsStr, exists := cp.GetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.ts", fileKey))
|
||||
if exists {
|
||||
ts, err := strconv.ParseInt(tsStr, 10, 64)
|
||||
if err != nil || ts < time.Now().Unix()-2592000 {
|
||||
log.Errorf("ignoring request to download a file offered more than 30 days ago")
|
||||
return
|
||||
}
|
||||
}
|
||||
// set the filekey status to active
|
||||
cp.SetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.active", fileKey), constants.True)
|
||||
cp.eventBus.Publish(event.NewEvent(event.ShareManifest, map[event.Field]string{event.FileKey: fileKey, event.SerializedManifest: serializedManifest}))
|
||||
}
|
||||
|
||||
// StopFileShare sends a message to the ProtocolEngine to cease sharing a particular file
|
||||
func (cp *cwtchPeer) StopFileShare(fileKey string) {
|
||||
// set the filekey status to inactive
|
||||
cp.SetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.active", fileKey), constants.False)
|
||||
cp.eventBus.Publish(event.NewEvent(event.StopFileShare, map[event.Field]string{event.FileKey: fileKey}))
|
||||
}
|
||||
|
||||
// StopAllFileShares sends a message to the ProtocolEngine to cease sharing all files
|
||||
func (cp *cwtchPeer) StopAllFileShares() {
|
||||
cp.eventBus.Publish(event.NewEvent(event.StopAllFileShares, map[event.Field]string{}))
|
||||
}
|
||||
|
||||
// eventHandler process events from other subsystems
|
||||
func (cp *cwtchPeer) eventHandler() {
|
||||
for {
|
||||
|
@ -1288,128 +1381,60 @@ func (cp *cwtchPeer) eventHandler() {
|
|||
path := ev.Data[event.Path]
|
||||
|
||||
log.Debugf("NewGetValMessageFromPeer for %v.%v from %v\n", scope, path, onion)
|
||||
|
||||
conversationInfo, err := cp.FetchConversationInfo(onion)
|
||||
|
||||
log.Debugf("confo info lookup newgetval %v %v %v", onion, conversationInfo, err)
|
||||
// only accepted contacts can look up information
|
||||
if conversationInfo != nil && conversationInfo.Accepted {
|
||||
scope := attr.IntoScope(scope)
|
||||
if scope.IsPublic() || scope.IsConversation() {
|
||||
zone, zpath := attr.ParseZone(path)
|
||||
val, exists := cp.GetScopedZonedAttribute(scope, zone, zpath)
|
||||
// Type Safe Scoped/Zoned Path
|
||||
zscope := attr.IntoScope(scope)
|
||||
zone, zpath := attr.ParseZone(path)
|
||||
scopedZonedPath := zscope.ConstructScopedZonedPath(zone.ConstructZonedPath(zpath))
|
||||
|
||||
// NOTE: Temporary Override because UI currently wipes names if it can't find them...
|
||||
if !exists && zone == attr.UnknownZone && path == constants.Name {
|
||||
val, exists = cp.GetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants.Name)
|
||||
// Safe Access to Extensions
|
||||
cp.extensionLock.Lock()
|
||||
log.Debugf("checking extension...%v", cp.extensions)
|
||||
for _, extension := range cp.extensions {
|
||||
log.Debugf("checking extension...%v", extension)
|
||||
// check if the current map of experiments satisfies the extension requirements
|
||||
if !cp.checkExtensionExperiment(extension) {
|
||||
log.Debugf("skipping extension...not all experiments satisfied")
|
||||
continue
|
||||
}
|
||||
|
||||
resp := event.NewEvent(event.SendRetValMessageToPeer, map[event.Field]string{event.ConversationID: strconv.Itoa(conversationInfo.ID), event.RemotePeer: onion, event.Exists: strconv.FormatBool(exists)})
|
||||
resp.EventID = ev.EventID
|
||||
if exists {
|
||||
resp.Data[event.Data] = val
|
||||
} else {
|
||||
resp.Data[event.Data] = ""
|
||||
}
|
||||
log.Debugf("Responding with SendRetValMessageToPeer exists:%v data: %v\n", exists, val)
|
||||
|
||||
cp.eventBus.Publish(resp)
|
||||
extension.extension.OnContactRequestValue(cp, *conversationInfo, ev.EventID, scopedZonedPath)
|
||||
}
|
||||
cp.extensionLock.Unlock()
|
||||
|
||||
}
|
||||
|
||||
case event.ManifestReceived:
|
||||
log.Debugf("Manifest Received Event!: %v", ev)
|
||||
handle := ev.Data[event.Handle]
|
||||
fileKey := ev.Data[event.FileKey]
|
||||
serializedManifest := ev.Data[event.SerializedManifest]
|
||||
|
||||
manifestFilePath, exists := cp.GetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%v.manifest", fileKey))
|
||||
if exists {
|
||||
downloadFilePath, exists := cp.GetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%v.path", fileKey))
|
||||
if exists {
|
||||
log.Debugf("downloading manifest to %v, file to %v", manifestFilePath, downloadFilePath)
|
||||
var manifest files.Manifest
|
||||
err := json.Unmarshal([]byte(serializedManifest), &manifest)
|
||||
|
||||
if err == nil {
|
||||
// We only need to check the file size here, as manifest is sent to engine and the file created
|
||||
// will be bound to the size advertised in manifest.
|
||||
fileSizeLimitValue, fileSizeLimitExists := cp.GetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%v.limit", fileKey))
|
||||
if fileSizeLimitExists {
|
||||
fileSizeLimit, err := strconv.ParseUint(fileSizeLimitValue, 10, bits.UintSize)
|
||||
if err == nil {
|
||||
if manifest.FileSizeInBytes >= fileSizeLimit {
|
||||
log.Errorf("could not download file, size %v greater than limit %v", manifest.FileSizeInBytes, fileSizeLimitValue)
|
||||
} else {
|
||||
manifest.Title = manifest.FileName
|
||||
manifest.FileName = downloadFilePath
|
||||
log.Debugf("saving manifest")
|
||||
err = manifest.Save(manifestFilePath)
|
||||
if err != nil {
|
||||
log.Errorf("could not save manifest: %v", err)
|
||||
} else {
|
||||
tempFile := ""
|
||||
if runtime.GOOS == "android" {
|
||||
tempFile = manifestFilePath[0 : len(manifestFilePath)-len(".manifest")]
|
||||
log.Debugf("derived android temp path: %v", tempFile)
|
||||
}
|
||||
cp.eventBus.Publish(event.NewEvent(event.ManifestSaved, map[event.Field]string{
|
||||
event.FileKey: fileKey,
|
||||
event.Handle: handle,
|
||||
event.SerializedManifest: string(manifest.Serialize()),
|
||||
event.TempFile: tempFile,
|
||||
event.NameSuggestion: manifest.Title,
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.Errorf("error saving manifest: %v", err)
|
||||
}
|
||||
} else {
|
||||
log.Errorf("found manifest path but not download path for %v", fileKey)
|
||||
}
|
||||
} else {
|
||||
log.Errorf("no download path found for manifest: %v", fileKey)
|
||||
}
|
||||
case event.FileDownloaded:
|
||||
fileKey := ev.Data[event.FileKey]
|
||||
cp.SetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.complete", fileKey), "true")
|
||||
case event.NewRetValMessageFromPeer:
|
||||
onion := ev.Data[event.RemotePeer]
|
||||
handle := ev.Data[event.RemotePeer]
|
||||
scope := ev.Data[event.Scope]
|
||||
path := ev.Data[event.Path]
|
||||
val := ev.Data[event.Data]
|
||||
exists, _ := strconv.ParseBool(ev.Data[event.Exists])
|
||||
log.Debugf("NewRetValMessageFromPeer %v %v %v %v %v\n", onion, scope, path, exists, val)
|
||||
if exists {
|
||||
log.Debugf("NewRetValMessageFromPeer %v %v %v %v %v\n", handle, scope, path, exists, val)
|
||||
|
||||
// Handle File Sharing Metadata
|
||||
// TODO This probably should be broken out to it's own code..
|
||||
zone, path := attr.ParseZone(path)
|
||||
if attr.Scope(scope).IsConversation() && zone == attr.FilesharingZone && strings.HasSuffix(path, ".manifest.size") {
|
||||
fileKey := strings.Replace(path, ".manifest.size", "", 1)
|
||||
size, err := strconv.Atoi(val)
|
||||
// if size is valid and below the maximum size for a manifest
|
||||
// this is to prevent malicious sharers from using large amounts of memory when distributing
|
||||
// a manifest as we reconstruct this in-memory
|
||||
if err == nil && size < files.MaxManifestSize {
|
||||
cp.eventBus.Publish(event.NewEvent(event.ManifestSizeReceived, map[event.Field]string{event.FileKey: fileKey, event.ManifestSize: val, event.Handle: onion}))
|
||||
} else {
|
||||
cp.eventBus.Publish(event.NewEvent(event.ManifestError, map[event.Field]string{event.FileKey: fileKey, event.Handle: onion}))
|
||||
}
|
||||
}
|
||||
conversationInfo, _ := cp.FetchConversationInfo(handle)
|
||||
// only accepted contacts can look up information
|
||||
if conversationInfo != nil && conversationInfo.Accepted {
|
||||
// Type Safe Scoped/Zoned Path
|
||||
zscope := attr.IntoScope(scope)
|
||||
zone, zpath := attr.ParseZone(path)
|
||||
scopedZonedPath := zscope.ConstructScopedZonedPath(zone.ConstructZonedPath(zpath))
|
||||
|
||||
// Allow public profile parameters to be added as peer specific attributes...
|
||||
if attr.Scope(scope).IsPublic() && zone == attr.ProfileZone {
|
||||
ci, err := cp.FetchConversationInfo(onion)
|
||||
log.Debugf("fetch conversation info %v %v", ci, err)
|
||||
if ci != nil && err == nil {
|
||||
err := cp.SetConversationAttribute(ci.ID, attr.Scope(scope).ConstructScopedZonedPath(zone.ConstructZonedPath(path)), val)
|
||||
if err != nil {
|
||||
log.Errorf("error setting conversation attribute %v", err)
|
||||
}
|
||||
// Safe Access to Extensions
|
||||
cp.extensionLock.Lock()
|
||||
for _, extension := range cp.extensions {
|
||||
log.Debugf("checking extension...%v", extension)
|
||||
// check if the current map of experiments satisfies the extension requirements
|
||||
if !cp.checkExtensionExperiment(extension) {
|
||||
log.Debugf("skipping extension...not all experiments satisfied")
|
||||
continue
|
||||
}
|
||||
extension.extension.OnContactReceiveValue(cp, *conversationInfo, scopedZonedPath, val, exists)
|
||||
}
|
||||
cp.extensionLock.Unlock()
|
||||
}
|
||||
case event.PeerStateChange:
|
||||
handle := ev.Data[event.RemotePeer]
|
||||
|
@ -1491,14 +1516,48 @@ func (cp *cwtchPeer) eventHandler() {
|
|||
}
|
||||
}
|
||||
default:
|
||||
if ev.EventType != "" {
|
||||
log.Errorf("peer event handler received an event it was not subscribed for: %v", ev.EventType)
|
||||
// invalid event, signifies shutdown
|
||||
if ev.EventType == "" {
|
||||
return
|
||||
}
|
||||
|
||||
// Otherwise, obtain Safe Access to Extensions
|
||||
processed := false
|
||||
cp.extensionLock.Lock()
|
||||
for _, extension := range cp.extensions {
|
||||
|
||||
// check if the current map of experiments satisfies the extension requirements
|
||||
if !cp.checkExtensionExperiment(extension) {
|
||||
log.Debugf("skipping extension...not all experiments satisfied")
|
||||
continue
|
||||
}
|
||||
|
||||
// if the extension is registered for this event type then process
|
||||
if _, contains := extension.events[ev.EventType]; contains {
|
||||
extension.extension.OnEvent(ev, cp)
|
||||
processed = true
|
||||
}
|
||||
}
|
||||
cp.extensionLock.Unlock()
|
||||
|
||||
if !processed {
|
||||
log.Errorf("cwtch profile received an event that it (or an extension) was unable to handle. this is very likely a programming error: %v", ev.EventType)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (cp *cwtchPeer) checkExtensionExperiment(hook ProfileHook) bool {
|
||||
cp.experimentsLock.Lock()
|
||||
defer cp.experimentsLock.Unlock()
|
||||
for experiment := range hook.experiments {
|
||||
if !cp.experiments.IsEnabled(experiment) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// attemptInsertOrAcknowledgeLegacyGroupConversation is a convenience method that looks up the conversation
|
||||
// by the given handle and attempts to mark the message as acknowledged. returns error on failure
|
||||
// to either find the contact or the associated message
|
||||
|
|
|
@ -125,7 +125,9 @@ func NewCwtchProfileStorage(db *sql.DB, profileDirectory string) (*CwtchProfileS
|
|||
insertProfileKeyValueStmt, err := db.Prepare(insertProfileKeySQLStmt)
|
||||
if err != nil {
|
||||
db.Close()
|
||||
log.Errorf("error preparing query: %v %v", insertProfileKeySQLStmt, err)
|
||||
// note: this is debug because we expect failure here when opening an encrypted database with an
|
||||
// incorrect password. The rest are errors because failure is not expected.
|
||||
log.Debugf("error preparing query: %v %v", insertProfileKeySQLStmt, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
@ -772,7 +774,7 @@ func (cps *CwtchProfileStorage) PurgeNonSavedMessages() {
|
|||
for _, conversation := range ci {
|
||||
if !conversation.IsGroup() && !conversation.IsServer() {
|
||||
if conversation.Attributes[attr.LocalScope.ConstructScopedZonedPath(attr.ProfileZone.ConstructZonedPath(event.SaveHistoryKey)).ToString()] != event.SaveHistoryConfirmed {
|
||||
log.Infof("purging conversation...")
|
||||
log.Debugf("purging conversation...")
|
||||
// TODO: At some point in the future this needs to iterate over channels and make a decision for each on..
|
||||
cps.PurgeConversationChannel(conversation.ID, 0)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
type SendMessages interface {
|
||||
SendMessage(conversation int, message string) (int, error)
|
||||
|
||||
// EnhancedSendMessage Attempts to Send a Message and Immediately Attempts to Lookup the Message in the Database
|
||||
EnhancedSendMessage(conversation int, message string) string
|
||||
|
||||
SendInviteToConversation(conversationID int, inviteConversationID int) (int, error)
|
||||
SendScopedZonedGetValToContact(conversationID int, scope attr.Scope, zone attr.Zone, key string)
|
||||
}
|
||||
|
@ -101,10 +105,12 @@ type CwtchPeer interface {
|
|||
|
||||
// Import Bundle
|
||||
ImportBundle(string) error
|
||||
EnhancedImportBundle(string) string
|
||||
|
||||
// New Unified Conversation Interfaces
|
||||
NewContactConversation(handle string, acl model.AccessControl, accepted bool) (int, error)
|
||||
FetchConversations() ([]*model.Conversation, error)
|
||||
ArchiveConversation(conversation int)
|
||||
GetConversationInfo(conversation int) (*model.Conversation, error)
|
||||
FetchConversationInfo(handle string) (*model.Conversation, error)
|
||||
AcceptConversation(conversation int) error
|
||||
|
@ -121,11 +127,14 @@ type CwtchPeer interface {
|
|||
GetMostRecentMessages(conversation int, channel int, offset int, limit int) ([]model.ConversationMessage, error)
|
||||
UpdateMessageAttribute(conversation int, channel int, id int, key string, value string) error
|
||||
|
||||
// File Sharing APIS
|
||||
// TODO move these to feature protected interfaces
|
||||
ShareFile(fileKey string, serializedManifest string)
|
||||
StopFileShare(fileKey string)
|
||||
StopAllFileShares()
|
||||
// EnhancedGetMessageById returns a json-encoded enhanced message, suitable for rendering in a UI
|
||||
EnhancedGetMessageById(conversation int, mid int) string
|
||||
|
||||
// EnhancedGetMessageByContentHash returns a json-encoded enhanced message, suitable for rendering in a UI
|
||||
EnhancedGetMessageByContentHash(conversation int, hash string) string
|
||||
|
||||
// EnhancedGetMessages returns a set of json-encoded enhanced messages, suitable for rendering in a UI
|
||||
EnhancedGetMessages(conversation int, index int, count int) string
|
||||
|
||||
// Server Token APIS
|
||||
// TODO move these to feature protected interfaces
|
||||
|
@ -134,6 +143,20 @@ type CwtchPeer interface {
|
|||
// Profile Management
|
||||
CheckPassword(password string) bool
|
||||
ChangePassword(oldpassword string, newpassword string, newpasswordAgain string) error
|
||||
Export(file string) error
|
||||
ExportProfile(file string) error
|
||||
Delete()
|
||||
PublishEvent(resp event.Event)
|
||||
RegisterHook(hook ProfileHooks)
|
||||
UpdateExperiments(enabled bool, experiments map[string]bool)
|
||||
IsFeatureEnabled(featureName string) bool
|
||||
}
|
||||
|
||||
// EnhancedMessage wraps a Cwtch model.Message with some additional data to reduce calls from the UI.
|
||||
type EnhancedMessage struct {
|
||||
model.Message
|
||||
ID int // the actual ID of the message in the database (not the row number)
|
||||
LocalIndex int // local index in the DB (row #). Can be empty (most calls supply it) but lookup by hash will fill it
|
||||
ContentHash string
|
||||
ContactImage string
|
||||
Attributes map[string]string
|
||||
}
|
||||
|
|
|
@ -176,7 +176,7 @@ func ImportProfile(exportedCwtchFile string, profilesDir string, password string
|
|||
log.Errorf("%s is an invalid cwtch backup file: %s", profileID, err)
|
||||
return nil, err
|
||||
}
|
||||
log.Infof("%s is a valid cwtch backup file", profileID)
|
||||
log.Debugf("%s is a valid cwtch backup file", profileID)
|
||||
|
||||
profileDBFile := filepath.Join(profilesDir, profileID, dbFile)
|
||||
log.Debugf("checking %v", profileDBFile)
|
||||
|
|
|
@ -34,6 +34,7 @@ func (fsss *FileSharingSubSystem) ShareFile(fileKey string, serializedManifest s
|
|||
log.Errorf("could not share file %v", err)
|
||||
return
|
||||
}
|
||||
log.Infof("sharing file: %v %v", fileKey, serializedManifest)
|
||||
fsss.activeShares.Store(fileKey, &manifest)
|
||||
}
|
||||
|
||||
|
|
|
@ -73,7 +73,7 @@ func (ps *ProfileStoreV1) load() error {
|
|||
|
||||
for gid, group := range cp.Groups {
|
||||
if group.Version == 0 {
|
||||
log.Infof("group %v is of unsupported version 0. dropping group...\n", group.GroupID)
|
||||
log.Debugf("group %v is of unsupported version 0. dropping group...\n", group.GroupID)
|
||||
delete(cp.Groups, gid)
|
||||
continue
|
||||
}
|
||||
|
|
|
@ -152,7 +152,7 @@ func (ss *streamStore) WriteN(messages []model.Message) {
|
|||
ss.lock.Lock()
|
||||
defer ss.lock.Unlock()
|
||||
|
||||
log.Infof("WriteN %v messages\n", len(messages))
|
||||
log.Debugf("WriteN %v messages\n", len(messages))
|
||||
i := 0
|
||||
for _, m := range messages {
|
||||
ss.updateBuffer(m)
|
||||
|
|
|
@ -139,7 +139,7 @@ func TestCwtchPeerIntegration(t *testing.T) {
|
|||
const ServerAddr = "nfhxzvzxinripgdh4t2m4xcy3crf6p4cbhectgckuj3idsjsaotgowad"
|
||||
serverKeyBundle, _ := base64.StdEncoding.DecodeString(ServerKeyBundleBase64)
|
||||
|
||||
app := app2.NewApp(acn, "./storage")
|
||||
app := app2.NewApp(acn, "./storage", app2.LoadAppSettings("./storage"))
|
||||
|
||||
usr, _ := user.Current()
|
||||
cwtchDir := path.Join(usr.HomeDir, ".cwtch")
|
||||
|
@ -152,31 +152,31 @@ func TestCwtchPeerIntegration(t *testing.T) {
|
|||
// ***** cwtchPeer setup *****
|
||||
|
||||
log.Infoln("Creating Alice...")
|
||||
app.CreateTaggedPeer("Alice", "asdfasdf", "test")
|
||||
app.CreateProfile("Alice", "asdfasdf", true)
|
||||
|
||||
log.Infoln("Creating Bob...")
|
||||
app.CreateTaggedPeer("Bob", "asdfasdf", "test")
|
||||
app.CreateProfile("Bob", "asdfasdf", true)
|
||||
|
||||
log.Infoln("Creating Carol...")
|
||||
app.CreateTaggedPeer("Carol", "asdfasdf", "test")
|
||||
app.CreateProfile("Carol", "asdfasdf", true)
|
||||
|
||||
alice := app2.WaitGetPeer(app, "Alice")
|
||||
aliceBus := app.GetEventBus(alice.GetOnion())
|
||||
app.ActivatePeerEngine(alice.GetOnion(), true, true, true)
|
||||
app.ActivatePeerEngine(alice.GetOnion())
|
||||
log.Infoln("Alice created:", alice.GetOnion())
|
||||
alice.SetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants.Name, "Alice")
|
||||
alice.AutoHandleEvents([]event.Type{event.PeerStateChange, event.ServerStateChange, event.NewGroupInvite, event.NewRetValMessageFromPeer})
|
||||
|
||||
bob := app2.WaitGetPeer(app, "Bob")
|
||||
bobBus := app.GetEventBus(bob.GetOnion())
|
||||
app.ActivatePeerEngine(bob.GetOnion(), true, true, true)
|
||||
app.ActivatePeerEngine(bob.GetOnion())
|
||||
log.Infoln("Bob created:", bob.GetOnion())
|
||||
bob.SetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants.Name, "Bob")
|
||||
bob.AutoHandleEvents([]event.Type{event.PeerStateChange, event.ServerStateChange, event.NewGroupInvite, event.NewRetValMessageFromPeer})
|
||||
|
||||
carol := app2.WaitGetPeer(app, "Carol")
|
||||
carolBus := app.GetEventBus(carol.GetOnion())
|
||||
app.ActivatePeerEngine(carol.GetOnion(), true, true, true)
|
||||
app.ActivatePeerEngine(carol.GetOnion())
|
||||
log.Infoln("Carol created:", carol.GetOnion())
|
||||
carol.SetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants.Name, "Carol")
|
||||
carol.AutoHandleEvents([]event.Type{event.PeerStateChange, event.ServerStateChange, event.NewGroupInvite, event.NewRetValMessageFromPeer})
|
||||
|
@ -412,7 +412,7 @@ func TestCwtchPeerIntegration(t *testing.T) {
|
|||
pprof.Lookup("goroutine").WriteTo(os.Stdout, 1)
|
||||
fmt.Println("")
|
||||
log.Infof("numGoRoutinesStart: %v\nnumGoRoutinesPostAppStart: %v\nnumGoRoutinesPostPeerStart: %v\nnumGoRoutinesPostPeerAndServerConnect: %v\n"+
|
||||
"numGoRoutinesPostAlice: %v\nnumGoRoutinesPostCarolConnect: %v\nnumGoRoutinesPostBob: %v\nnumGoRoutinesPostCarol: %v\nnumGoRoutinesPostAppShutdown: %v",
|
||||
"numGoRoutinesPostAlice: %v\nnumGoRoutinesPostCarolConnect: %v\nnumGoRoutinesPostBob: %v\nnumGoRoutinesPostCarol: %v\nnumGoRoutinesPostAppShutdown: %v",
|
||||
numGoRoutinesStart, numGoRoutinesPostAppStart, numGoRoutinesPostPeerStart, numGoRoutinesPostServerConnect,
|
||||
numGoRoutinesPostAlice, numGoRoutinesPostCarolConnect, numGoRoutinesPostBob, numGoRoutinesPostCarol, numGoRoutinesPostAppShutdown)
|
||||
|
||||
|
|
|
@ -59,9 +59,9 @@ func TestEncryptedStorage(t *testing.T) {
|
|||
|
||||
defer acn.Close()
|
||||
acn.WaitTillBootstrapped()
|
||||
app := app2.NewApp(acn, cwtchDir)
|
||||
app.CreateTaggedPeer("alice", "password", constants.ProfileTypeV1Password)
|
||||
app.CreateTaggedPeer("bob", "password", constants.ProfileTypeV1Password)
|
||||
app := app2.NewApp(acn, cwtchDir, app2.LoadAppSettings(cwtchDir))
|
||||
app.CreateProfile("alice", "password", true)
|
||||
app.CreateProfile("bob", "password", true)
|
||||
|
||||
alice := app2.WaitGetPeer(app, "alice")
|
||||
bob := app2.WaitGetPeer(app, "bob")
|
||||
|
@ -130,7 +130,7 @@ func TestEncryptedStorage(t *testing.T) {
|
|||
t.Fatalf("expeced GetMostRecentMessages to return 1, instead returned: %v %v", len(messages), messages)
|
||||
}
|
||||
|
||||
err = alice.Export("alice.tar.gz")
|
||||
err = alice.ExportProfile("alice.tar.gz")
|
||||
if err != nil {
|
||||
t.Fatalf("could not export profile: %v", err)
|
||||
}
|
||||
|
@ -140,7 +140,7 @@ func TestEncryptedStorage(t *testing.T) {
|
|||
t.Fatal("profile is already imported...this should fail")
|
||||
}
|
||||
|
||||
app.DeletePeer(alice.GetOnion(), "password")
|
||||
app.DeleteProfile(alice.GetOnion(), "password")
|
||||
alice, err = app.ImportProfile("alice.tar.gz", "password")
|
||||
if err != nil {
|
||||
t.Fatalf("profile should have successfully imported: %s", err)
|
||||
|
|
|
@ -18,6 +18,7 @@ import (
|
|||
"fmt"
|
||||
"git.openprivacy.ca/openprivacy/connectivity/tor"
|
||||
"git.openprivacy.ca/openprivacy/log"
|
||||
"path/filepath"
|
||||
|
||||
// Import SQL Cipher
|
||||
mrand "math/rand"
|
||||
|
@ -56,7 +57,7 @@ func TestFileSharing(t *testing.T) {
|
|||
os.RemoveAll("cwtch.out.png")
|
||||
os.RemoveAll("cwtch.out.png.manifest")
|
||||
|
||||
log.SetLevel(log.LevelInfo)
|
||||
log.SetLevel(log.LevelDebug)
|
||||
|
||||
os.Mkdir("tordir", 0700)
|
||||
dataDir := path.Join("tordir", "tor")
|
||||
|
@ -74,20 +75,29 @@ func TestFileSharing(t *testing.T) {
|
|||
panic(err)
|
||||
}
|
||||
|
||||
useCache := os.Getenv("TORCACHE") == "true"
|
||||
|
||||
torDataDir := ""
|
||||
if torDataDir, err = os.MkdirTemp(dataDir, "data-dir-"); err != nil {
|
||||
t.Fatalf("could not create data dir")
|
||||
if useCache {
|
||||
log.Infof("using tor cache")
|
||||
torDataDir = filepath.Join(dataDir, "data-dir-torcache")
|
||||
os.MkdirAll(torDataDir, 0700)
|
||||
} else {
|
||||
log.Infof("using clean tor data dir")
|
||||
if torDataDir, err = os.MkdirTemp(dataDir, "data-dir-"); err != nil {
|
||||
t.Fatalf("could not create data dir")
|
||||
}
|
||||
}
|
||||
|
||||
tor.NewTorrc().WithSocksPort(socksPort).WithOnionTrafficOnly().WithHashedPassword(base64.StdEncoding.EncodeToString(key)).WithControlPort(controlPort).Build("tordir/tor/torrc")
|
||||
acn, err := tor.NewTorACNWithAuth("./tordir", path.Join("..", "..", "tor"), torDataDir, controlPort, tor.HashedPasswordAuthenticator{Password: base64.StdEncoding.EncodeToString(key)})
|
||||
acn, err := tor.NewTorACNWithAuth("./tordir", path.Join("..", "tor"), torDataDir, controlPort, tor.HashedPasswordAuthenticator{Password: base64.StdEncoding.EncodeToString(key)})
|
||||
if err != nil {
|
||||
t.Fatalf("Could not start Tor: %v", err)
|
||||
}
|
||||
acn.WaitTillBootstrapped()
|
||||
defer acn.Close()
|
||||
|
||||
app := app2.NewApp(acn, "./storage")
|
||||
app := app2.NewApp(acn, "./storage", app2.LoadAppSettings("./storage"))
|
||||
|
||||
usr, _ := user.Current()
|
||||
cwtchDir := path.Join(usr.HomeDir, ".cwtch")
|
||||
|
@ -96,25 +106,30 @@ func TestFileSharing(t *testing.T) {
|
|||
os.Mkdir(path.Join(cwtchDir, "testing"), 0700)
|
||||
|
||||
t.Logf("Creating Alice...")
|
||||
app.CreateTaggedPeer("alice", "asdfasdf", "testing")
|
||||
app.CreateProfile("alice", "asdfasdf", true)
|
||||
|
||||
t.Logf("Creating Bob...")
|
||||
app.CreateTaggedPeer("bob", "asdfasdf", "testing")
|
||||
app.CreateProfile("bob", "asdfasdf", true)
|
||||
|
||||
t.Logf("** Waiting for Alice, Bob...")
|
||||
alice := app2.WaitGetPeer(app, "alice")
|
||||
app.ActivatePeerEngine(alice.GetOnion(), true, true, true)
|
||||
app.ActivatePeerEngine(alice.GetOnion())
|
||||
bob := app2.WaitGetPeer(app, "bob")
|
||||
app.ActivatePeerEngine(bob.GetOnion(), true, true, true)
|
||||
app.ActivatePeerEngine(bob.GetOnion())
|
||||
|
||||
alice.AutoHandleEvents([]event.Type{event.PeerStateChange, event.NewRetValMessageFromPeer})
|
||||
bob.AutoHandleEvents([]event.Type{event.PeerStateChange, event.NewRetValMessageFromPeer, event.ManifestReceived})
|
||||
bob.AutoHandleEvents([]event.Type{event.PeerStateChange, event.NewRetValMessageFromPeer})
|
||||
|
||||
queueOracle := event.NewQueue()
|
||||
app.GetEventBus(bob.GetOnion()).Subscribe(event.FileDownloaded, queueOracle)
|
||||
|
||||
t.Logf("** Launching Peers...")
|
||||
// Turn on File Sharing Experiment...
|
||||
settings := app.ReadSettings()
|
||||
settings.ExperimentsEnabled = true
|
||||
settings.Experiments[constants.FileSharingExperiment] = true
|
||||
app.UpdateSettings(settings)
|
||||
|
||||
t.Logf("** Launching Peers...")
|
||||
waitTime := time.Duration(30) * time.Second
|
||||
t.Logf("** Waiting for Alice, Bob to connect with onion network... (%v)\n", waitTime)
|
||||
time.Sleep(waitTime)
|
||||
|
@ -125,6 +140,7 @@ func TestFileSharing(t *testing.T) {
|
|||
|
||||
t.Logf("Waiting for alice and Bob to peer...")
|
||||
waitForPeerPeerConnection(t, alice, bob)
|
||||
alice.AcceptConversation(1)
|
||||
|
||||
t.Logf("Alice and Bob are Connected!!")
|
||||
|
||||
|
@ -145,20 +161,24 @@ func TestFileSharing(t *testing.T) {
|
|||
|
||||
// Test stopping and restarting file shares
|
||||
t.Logf("Stopping File Share")
|
||||
alice.StopAllFileShares()
|
||||
filesharingFunctionality.StopAllFileShares(alice)
|
||||
|
||||
// Allow time for the stop request to filter through Engine
|
||||
time.Sleep(time.Second * 5)
|
||||
|
||||
// Restart
|
||||
t.Logf("Restarting File Share")
|
||||
filesharingFunctionality.ReShareFiles(alice)
|
||||
err = filesharingFunctionality.ReShareFiles(alice)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Error!: %v", err)
|
||||
}
|
||||
|
||||
// run the same download test again...to check that we can actually download the file
|
||||
testBobDownloadFile(t, bob, filesharingFunctionality, queueOracle)
|
||||
|
||||
// test that we can delete bob...
|
||||
app.DeletePeer(bob.GetOnion(), "asdfasdf")
|
||||
app.DeleteProfile(bob.GetOnion(), "asdfasdf")
|
||||
|
||||
queueOracle.Shutdown()
|
||||
app.Shutdown()
|
||||
|
@ -181,6 +201,7 @@ func testBobDownloadFile(t *testing.T, bob peer.CwtchPeer, filesharingFunctional
|
|||
os.RemoveAll("cwtch.out.png")
|
||||
os.RemoveAll("cwtch.out.png.manifest")
|
||||
|
||||
bob.AcceptConversation(1)
|
||||
message, _, err := bob.GetChannelMessage(1, 0, 1)
|
||||
if err != nil {
|
||||
t.Fatalf("could not find file sharing message: %v", err)
|
||||
|
@ -194,19 +215,19 @@ func testBobDownloadFile(t *testing.T, bob peer.CwtchPeer, filesharingFunctional
|
|||
err := json.Unmarshal([]byte(messageWrapper.Data), &fileMessageOverlay)
|
||||
|
||||
if err == nil {
|
||||
|
||||
t.Logf("bob attempting to download file with invalid download")
|
||||
// try downloading with invalid download dir
|
||||
err = filesharingFunctionality.DownloadFile(bob, 1, "/do/not/download/this/file/cwtch.out.png", "./cwtch.out.png.manifest", fmt.Sprintf("%s.%s", fileMessageOverlay.Hash, fileMessageOverlay.Nonce), constants.ImagePreviewMaxSizeInBytes)
|
||||
if err == nil {
|
||||
t.Fatalf("should not download file with invalid download dir")
|
||||
}
|
||||
|
||||
t.Logf("bob attempting to download file with invalid manifest")
|
||||
// try downloading with invalid manifest dir
|
||||
err = filesharingFunctionality.DownloadFile(bob, 1, "./cwtch.out.png", "/do/not/download/this/file/cwtch.out.png.manifest", fmt.Sprintf("%s.%s", fileMessageOverlay.Hash, fileMessageOverlay.Nonce), constants.ImagePreviewMaxSizeInBytes)
|
||||
if err == nil {
|
||||
t.Fatalf("should not download file with invalid manifest dir")
|
||||
}
|
||||
|
||||
t.Logf("bob attempting to download file")
|
||||
err = filesharingFunctionality.DownloadFile(bob, 1, "./cwtch.out.png", "./cwtch.out.png.manifest", fmt.Sprintf("%s.%s", fileMessageOverlay.Hash, fileMessageOverlay.Nonce), constants.ImagePreviewMaxSizeInBytes)
|
||||
if err != nil {
|
||||
t.Fatalf("could not download file: %v", err)
|
||||
|
|
Loading…
Reference in New Issue