Initial Prototype of Event Hooks

This commit is contained in:
Sarah Jamie Lewis 2023-01-05 13:52:43 -08:00
parent 697b3df54c
commit 26c5c11216
8 changed files with 350 additions and 162 deletions

View File

@ -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"
@ -145,6 +147,7 @@ func (app *application) CreatePeer(name string, password string, attributes map[
eventBus := event.NewEventManager() eventBus := event.NewEventManager()
app.eventBuses[profile.GetOnion()] = eventBus app.eventBuses[profile.GetOnion()] = eventBus
profile.Init(app.eventBuses[profile.GetOnion()]) profile.Init(app.eventBuses[profile.GetOnion()])
app.registerHooks(profile)
app.peers[profile.GetOnion()] = profile app.peers[profile.GetOnion()] = profile
for zp, val := range attributes { for zp, val := range attributes {
@ -242,6 +245,13 @@ 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()
@ -252,8 +262,11 @@ func (app *application) installProfile(profile peer.CwtchPeer) bool {
eventBus := event.NewEventManager() eventBus := event.NewEventManager()
app.eventBuses[profile.GetOnion()] = eventBus app.eventBuses[profile.GetOnion()] = eventBus
profile.Init(app.eventBuses[profile.GetOnion()]) profile.Init(app.eventBuses[profile.GetOnion()])
app.registerHooks(profile)
app.peers[profile.GetOnion()] = profile app.peers[profile.GetOnion()] = profile
app.AddPeerPlugin(profile.GetOnion(), plugins.CONNECTIONRETRY) // Now Mandatory 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
} }

View File

@ -0,0 +1,65 @@
package extensions
import (
"cwtch.im/cwtch/event"
"cwtch.im/cwtch/model"
"cwtch.im/cwtch/model/attr"
"cwtch.im/cwtch/model/constants"
"cwtch.im/cwtch/peer"
"git.openprivacy.ca/openprivacy/log"
"strconv"
)
// ProfileValueExtension implements custom Profile Names over Cwtch
type ProfileValueExtension struct {
}
func (pne ProfileValueExtension) RegisterEvents() []event.Type {
return nil
}
func (pne ProfileValueExtension) RegisterExperiments() []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.Infof("Looking up public | conversation scope/zone %v", szp.ToString())
if scope.IsPublic() || scope.IsConversation() {
val, exists := profile.GetScopedZonedAttribute(scope, zone, zpath)
// NOTE: Temporary Override because UI currently wipes names if it can't find them...
if !exists && zone == attr.UnknownZone && zpath == constants.Name {
val, exists = profile.GetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants.Name)
}
// Construct a Response
resp := event.NewEvent(event.SendRetValMessageToPeer, map[event.Field]string{event.ConversationID: strconv.Itoa(conversation.ID), event.RemotePeer: conversation.Handle, event.Exists: strconv.FormatBool(exists)})
resp.EventID = eventID
if exists {
resp.Data[event.Data] = val
} else {
resp.Data[event.Data] = ""
}
log.Debugf("Responding with SendRetValMessageToPeer exists:%v data: %v\n", exists, val)
profile.PublishEvent(resp)
}
}

View File

@ -2,12 +2,14 @@ package filesharing
import ( 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,101 @@ import (
type Functionality struct { type Functionality struct {
} }
func (f Functionality) RegisterEvents() []event.Type {
return []event.Type{event.ManifestReceived, event.FileDownloaded}
}
func (f Functionality) RegisterExperiments() []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) {
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")
}
}
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) {
scope, zone, zpath := path.GetScopeZonePath()
log.Infof("file sharing contact receive value")
if exists && scope.IsConversation() && zone == attr.FilesharingZone && strings.HasSuffix(zpath, ".manifest.size") {
fileKey := strings.Replace(zpath, ".manifest.size", "", 1)
size, err := strconv.Atoi(value)
// if size is valid and below the maximum size for a manifest
// this is to prevent malicious sharers from using large amounts of memory when distributing
// a manifest as we reconstruct this in-memory
if err == nil && size < files.MaxManifestSize {
profile.PublishEvent(event.NewEvent(event.ManifestSizeReceived, map[event.Field]string{event.FileKey: fileKey, event.ManifestSize: value, event.Handle: conversation.Handle}))
} else {
profile.PublishEvent(event.NewEvent(event.ManifestError, map[event.Field]string{event.FileKey: fileKey, event.Handle: conversation.Handle}))
}
}
}
// 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) {
@ -109,6 +206,23 @@ 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 {
// check that a manifest exists // check that a manifest exists
@ -116,8 +230,7 @@ func (f *Functionality) RestartFileShare(profile peer.CwtchPeer, filekey string)
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
log.Debugf("restarting file share: %v", filekey) log.Debugf("restarting file share: %v", filekey)
profile.ShareFile(filekey, manifest) return f.startFileShare(profile, filekey, manifest)
return nil
} }
return fmt.Errorf("manifest does not exist for filekey: %v", filekey) return fmt.Errorf("manifest does not exist for filekey: %v", filekey)
} }
@ -238,7 +351,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
} }
@ -330,3 +443,15 @@ 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) {
// 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) {
profile.PublishEvent(event.NewEvent(event.StopAllFileShares, map[event.Field]string{}))
}

View File

@ -23,6 +23,12 @@ type Scope string
// ScopedZonedPath typed path with a scope and a zone // 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

View File

@ -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,25 @@ 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
}
func (cp *cwtchPeer) PublishEvent(resp event.Event) {
cp.eventBus.Publish(resp)
}
func (cp *cwtchPeer) RegisterHook(extension ProfileHooks) {
cp.extensionLock.Lock()
defer cp.extensionLock.Unlock()
// Register Requested Events
for _, event := range extension.RegisterEvents() {
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) {
@ -1165,33 +1178,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 +1274,48 @@ 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.Infof("checking extension...%v", cp.extensions)
} for _, extension := range cp.extensions {
log.Infof("checking extension...%v", extension)
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) extension.extension.OnContactReceiveValue(cp, *conversationInfo, scopedZonedPath, val, exists)
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)
}
}
} }
cp.extensionLock.Unlock()
} }
case event.PeerStateChange: case event.PeerStateChange:
handle := ev.Data[event.RemotePeer] handle := ev.Data[event.RemotePeer]
@ -1491,10 +1397,26 @@ 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 {
// 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
} }
} }
} }

49
peer/hooks.go Normal file
View File

@ -0,0 +1,49 @@
package peer
import (
"cwtch.im/cwtch/event"
"cwtch.im/cwtch/model"
"cwtch.im/cwtch/model/attr"
)
type ProfileHooks interface {
// RegisterEvents returns a set of events that the extension is interested hooking
RegisterEvents() []event.Type
// RegisterExperiments RegisterExperiments returns a set of experiments that the extension is interested in being notified about
RegisterExperiments() []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.RegisterEvents() {
events[event] = true
}
experiments := make(map[string]bool)
for _, experiment := range extension.RegisterExperiments() {
experiments[experiment] = true
}
return ProfileHook{
extension,
events,
experiments,
}
}

View File

@ -121,12 +121,6 @@ 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
// TODO move these to feature protected interfaces
ShareFile(fileKey string, serializedManifest string)
StopFileShare(fileKey string)
StopAllFileShares()
// Server Token APIS // Server Token APIS
// TODO move these to feature protected interfaces // TODO move these to feature protected interfaces
StoreCachedTokens(tokenServer string, tokens []*privacypass.Token) StoreCachedTokens(tokenServer string, tokens []*privacypass.Token)
@ -136,4 +130,6 @@ type CwtchPeer interface {
ChangePassword(oldpassword string, newpassword string, newpasswordAgain string) error ChangePassword(oldpassword string, newpassword string, newpasswordAgain string) error
Export(file string) error Export(file string) error
Delete() Delete()
PublishEvent(resp event.Event)
RegisterHook(hook ProfileHooks)
} }

View File

@ -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,13 +75,22 @@ 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)
} }
@ -125,6 +135,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,7 +156,7 @@ 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)
@ -181,6 +192,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 +206,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)