From aec3c401808075aef6ae6f10cd4940e187568198 Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Thu, 7 Oct 2021 15:40:25 -0700 Subject: [PATCH] Enforced Zoned Attribute Lookups --- app/app.go | 2 +- app/appService.go | 2 +- .../filesharing/filesharing_functionality.go | 14 ++- model/attr/scope.go | 55 ++++++----- model/attr/zone.go | 52 ++++++++++ peer/cwtch_peer.go | 95 +++++++++++++++---- testing/cwtch_peer_server_integration_test.go | 14 +-- .../file_sharing_integration_test.go | 4 +- 8 files changed, 179 insertions(+), 59 deletions(-) create mode 100644 model/attr/zone.go diff --git a/app/app.go b/app/app.go index a828266..4598a96 100644 --- a/app/app.go +++ b/app/app.go @@ -13,7 +13,7 @@ import ( "git.openprivacy.ca/openprivacy/log" "io/ioutil" "os" - "path" + path "path/filepath" "strconv" "sync" ) diff --git a/app/appService.go b/app/appService.go index 7d0bf21..b83f303 100644 --- a/app/appService.go +++ b/app/appService.go @@ -9,7 +9,7 @@ import ( "git.openprivacy.ca/cwtch.im/tapir/primitives" "git.openprivacy.ca/openprivacy/connectivity" "git.openprivacy.ca/openprivacy/log" - "path" + path "path/filepath" "strconv" "sync" ) diff --git a/functionality/filesharing/filesharing_functionality.go b/functionality/filesharing/filesharing_functionality.go index 84d5ed8..ab4daf2 100644 --- a/functionality/filesharing/filesharing_functionality.go +++ b/functionality/filesharing/filesharing_functionality.go @@ -41,9 +41,15 @@ type OverlayMessage struct { // 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, handle string, downloadFilePath string, manifestFilePath string, key string) { - profile.SetAttribute(attr.GetLocalScope(fmt.Sprintf("%s.manifest", key)), manifestFilePath) - profile.SetAttribute(attr.GetLocalScope(key), downloadFilePath) - profile.SendGetValToPeer(handle, attr.PublicScope, fmt.Sprintf("%s.manifest.size", key)) + + // Store local.filesharing.filekey.manifest as the location of the manifest + profile.SetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.manifest", key), manifestFilePath) + + // Store local.filesharing.filekey as the location of the download + profile.SetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, key, downloadFilePath) + + // Get the value of conversation.filesharing.filekey.manifest.size from `handle` + profile.SendScopedZonedGetValToContact(handle, attr.ConversationScope, attr.FilesharingZone, fmt.Sprintf("%s.manifest.size", key)) } // ShareFile given a profile and a conversation handle, sets up a file sharing process to share the file @@ -83,7 +89,7 @@ func (f *Functionality) ShareFile(filepath string, profile peer.CwtchPeer, handl // manifest.FileName gets redacted in filesharing_subsystem (to remove the system-specific file hierarchy), // but we need to *store* the full path because the sender also uses it to locate the file lenDiff := len(filepath) - len(path.Base(filepath)) - profile.SetAttribute(attr.GetPublicScope(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)) diff --git a/model/attr/scope.go b/model/attr/scope.go index 60d40cc..90e4d3e 100644 --- a/model/attr/scope.go +++ b/model/attr/scope.go @@ -1,9 +1,5 @@ package attr -import ( - "strings" -) - /* Scope model for peer attributes and requests @@ -16,45 +12,54 @@ values stored in the LocalScope. */ +// Scope strongly types Scope strings +type Scope string + // scopes for attributes const ( // on a peer, local and peer supplied data - LocalScope = "local" - PeerScope = "peer" + LocalScope = Scope("local") + PeerScope = Scope("peer") + ConversationScope = Scope("conversation") // on a local profile, public data and private settings - PublicScope = "public" - SettingsScope = "settings" + PublicScope = Scope("public") ) // Separator for scope and the rest of path const Separator = "." -// GetPublicScope takes a path and attaches the pubic scope to it -func GetPublicScope(path string) string { - return PublicScope + Separator + path +// IntoScope converts a string to a Scope +func IntoScope(scope string) Scope { + return Scope(scope) } -// GetSettingsScope takes a path and attaches the settings scope to it -func GetSettingsScope(path string) string { - return SettingsScope + Separator + path +// ConstructScopedZonedPath enforces a scope over a zoned path +func (scope Scope) ConstructScopedZonedPath(zonedPath ZonedPath) string { + return string(scope) + Separator + string(zonedPath) +} + +// IsPublic returns true if the scope is a public scope +func (scope Scope) IsPublic() bool { + return scope == PublicScope +} + +// IsConversation returns true if the scope is a conversation scope +func (scope Scope) IsConversation() bool { + return scope == ConversationScope +} + +// GetPublicScope takes a path and attaches the pubic scope to it +func GetPublicScope(path string) string { + return string(PublicScope) + Separator + path } // GetLocalScope takes a path and attaches the local scope to it func GetLocalScope(path string) string { - return LocalScope + Separator + path + return string(LocalScope) + Separator + path } // GetPeerScope takes a path and attaches the peer scope to it func GetPeerScope(path string) string { - return PeerScope + Separator + path -} - -// GetScopePath take a full path and returns the scope and the scope-less path -func GetScopePath(fullPath string) (string, string) { - parts := strings.SplitN(fullPath, Separator, 1) - if len(parts) != 2 { - return "", "" - } - return parts[0], parts[1] + return string(PeerScope) + Separator + path } diff --git a/model/attr/zone.go b/model/attr/zone.go new file mode 100644 index 0000000..2146497 --- /dev/null +++ b/model/attr/zone.go @@ -0,0 +1,52 @@ +package attr + +import ( + "git.openprivacy.ca/openprivacy/log" + "strings" +) + +// Zone forces attributes to belong to a given subsystem e.g profile or filesharing +// Note: Zone is different from Scope which deals with public visibility of a given attribute +type Zone string + +// ZonedPath explicitly types paths that contain a zone for strongly typed APIs +type ZonedPath string + +const ( + + // ProfileZone for attributes related to profile details like name and profile image + ProfileZone = Zone("profile") + + // FilesharingZone for attributes related to file sharing + FilesharingZone = Zone("filesharing") + + // UnknownZone is a catch all useful for error handling + UnknownZone = Zone("unknown") +) + +// EnforceZone takes a path and attaches a zone to it. +// Note that this returns a ZonedPath which isn't directly usable, it must be given to ConstructScopedZonedPath +// in order to be realized into an actual attribute path. +func (zone Zone) EnforceZone(path string) ZonedPath { + return ZonedPath(string(zone) + Separator + path) +} + +// ParseZone takes in an untyped string and returns an explicit Zone along with the rest of the untyped path +func ParseZone(path string) (Zone, string) { + parts := strings.SplitN(path, Separator, 2) + + log.Debugf("parsed zone: %v %v", parts, path) + + if len(parts) != 2 { + return UnknownZone, "" + } + + switch Zone(parts[0]) { + case ProfileZone: + return ProfileZone, parts[1] + case FilesharingZone: + return FilesharingZone, parts[1] + default: + return UnknownZone, parts[1] + } +} diff --git a/peer/cwtch_peer.go b/peer/cwtch_peer.go index c6f6e97..de47ce8 100644 --- a/peer/cwtch_peer.go +++ b/peer/cwtch_peer.go @@ -54,6 +54,41 @@ type cwtchPeer struct { eventBus event.Manager } +func (cp *cwtchPeer) SendScopedZonedGetValToContact(handle string, scope attr.Scope, zone attr.Zone, path string) { + event := event.NewEventList(event.SendGetValMessageToPeer, event.RemotePeer, handle, event.Scope, string(scope), event.Path, string(zone.EnforceZone(path))) + cp.eventBus.Publish(event) +} + +func (cp *cwtchPeer) GetScopedZonedAttribute(scope attr.Scope, zone attr.Zone, key string) (string, bool) { + cp.mutex.Lock() + defer cp.mutex.Unlock() + scopedZonedKey := scope.ConstructScopedZonedPath(zone.EnforceZone(key)) + + log.Debugf("looking up attribute %v %v %v (%v)", scope, zone, key, scopedZonedKey) + + if val, exists := cp.Profile.GetAttribute(scopedZonedKey); exists { + return val, true + } + + if key == attr.GetLocalScope("name") { + return cp.Profile.Name, true + } + + return "", false +} + +func (cp *cwtchPeer) SetScopedZonedAttribute(scope attr.Scope, zone attr.Zone, key string, value string) { + cp.mutex.Lock() + scopedZonedKey := scope.ConstructScopedZonedPath(zone.EnforceZone(key)) + log.Debugf("storing attribute: %v = %v", scopedZonedKey, value) + cp.Profile.SetAttribute(scopedZonedKey, value) + defer cp.mutex.Unlock() + cp.eventBus.Publish(event.NewEvent(event.SetAttribute, map[event.Field]string{ + event.Key: scopedZonedKey, + event.Data: value, + })) +} + // SendMessage is a higher level that merges sending messages to contacts and group handles // If you try to send a message to a handle that doesn't exist, malformed or an incorrect type then // this function will error @@ -163,15 +198,20 @@ type ModifyServers interface { type SendMessages interface { SendMessage(handle string, message string) error + // Deprecated: is unsafe SendGetValToPeer(string, string, string) + SendScopedZonedGetValToContact(handle string, scope attr.Scope, zone attr.Zone, key string) + // Deprecated SendMessageToPeer(string, string) string // TODO This should probably not be exposed + // Deprecated do not use this to store messages StoreMessage(onion string, messageTxt string, sent time.Time) - // TODO Extract once groups are stable + // TODO + // Deprecated use overlays instead InviteOnionToGroup(string, string) error } @@ -202,9 +242,18 @@ type CwtchPeer interface { // Relating to local attributes GetOnion() string + + // Deprecated: SetAttribute is Unsafe used SetScopedZonedAttribute Instead SetAttribute(string, string) + + // Deprecated: GetAttribute is Unsafe used GetScopedZonedAttribute Instead GetAttribute(string) (string, bool) + // scope.zone.key = value + SetScopedZonedAttribute(scope attr.Scope, zone attr.Zone, key string, value string) + // scope.zone.key = value + GetScopedZonedAttribute(scope attr.Scope, zone attr.Zone, key string) (string, bool) + ReadContacts ModifyContacts @@ -651,6 +700,7 @@ func (cp *cwtchPeer) SetAttribute(key string, val string) { })) } +// Deprecated // GetAttribute gets an attribute for the profile func (cp *cwtchPeer) GetAttribute(key string) (string, bool) { cp.mutex.Lock() @@ -811,12 +861,14 @@ func (cp *cwtchPeer) eventHandler() { scope := ev.Data[event.Scope] 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) remotePeer := cp.GetContact(onion) if remotePeer != nil && remotePeer.Authorization == model.AuthApproved { - if scope == attr.PublicScope { - val, exists := cp.GetAttribute(attr.GetPublicScope(path)) + scope := attr.IntoScope(scope) + if scope.IsPublic() || scope.IsConversation() { + zone, path := attr.ParseZone(path) + val, exists := cp.GetScopedZonedAttribute(scope, zone, path) resp := event.NewEvent(event.SendRetValMessageToPeer, map[event.Field]string{event.RemotePeer: onion, event.Exists: strconv.FormatBool(exists)}) resp.EventID = ev.EventID if exists { @@ -838,9 +890,9 @@ func (cp *cwtchPeer) eventHandler() { fileKey := ev.Data[event.FileKey] serializedManifest := ev.Data[event.SerializedManifest] - manifestFilePath, exists := cp.GetAttribute(attr.GetLocalScope(fmt.Sprintf("%v.manifest", fileKey))) + manifestFilePath, exists := cp.GetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%v.manifest", fileKey)) if exists { - downloadFilePath, exists := cp.GetAttribute(attr.GetLocalScope(fileKey)) + downloadFilePath, exists := cp.GetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fileKey) if exists { log.Debugf("downloading manifest to %v, file to %v", manifestFilePath, downloadFilePath) var manifest files.Manifest @@ -884,22 +936,27 @@ func (cp *cwtchPeer) eventHandler() { exists, _ := strconv.ParseBool(ev.Data[event.Exists]) log.Debugf("NewRetValMessageFromPeer %v %v%v %v %v\n", onion, scope, path, exists, val) if exists { - if scope == attr.PublicScope { - if 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})) - } + + // 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.SetContactAttribute(onion, attr.GetPeerScope(path), val) + 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... + if attr.Scope(scope).IsPublic() && zone == attr.ProfileZone { + cp.SetContactAttribute(onion, attr.GetPeerScope(path), val) + } } case event.PeerStateChange: cp.mutex.Lock() diff --git a/testing/cwtch_peer_server_integration_test.go b/testing/cwtch_peer_server_integration_test.go index 0e17c35..745eb79 100644 --- a/testing/cwtch_peer_server_integration_test.go +++ b/testing/cwtch_peer_server_integration_test.go @@ -154,17 +154,17 @@ func TestCwtchPeerIntegration(t *testing.T) { alice := utils.WaitGetPeer(app, "alice") fmt.Println("Alice created:", alice.GetOnion()) - alice.SetAttribute(attr.GetPublicScope("name"), "Alice") + alice.SetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, "name", "Alice") alice.AutoHandleEvents([]event.Type{event.PeerStateChange, event.ServerStateChange, event.NewGroupInvite, event.NewRetValMessageFromPeer}) bob := utils.WaitGetPeer(app, "bob") fmt.Println("Bob created:", bob.GetOnion()) - bob.SetAttribute(attr.GetPublicScope("name"), "Bob") + bob.SetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, "name", "Bob") bob.AutoHandleEvents([]event.Type{event.PeerStateChange, event.ServerStateChange, event.NewGroupInvite, event.NewRetValMessageFromPeer}) carol := utils.WaitGetPeer(appClient, "carol") fmt.Println("Carol created:", carol.GetOnion()) - carol.SetAttribute(attr.GetPublicScope("name"), "Carol") + carol.SetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, "name", "Carol") carol.AutoHandleEvents([]event.Type{event.PeerStateChange, event.ServerStateChange, event.NewGroupInvite, event.NewRetValMessageFromPeer}) app.LaunchPeers() @@ -217,11 +217,11 @@ func TestCwtchPeerIntegration(t *testing.T) { fmt.Println("Alice and Bob getVal public.name...") - alice.SendGetValToPeer(bob.GetOnion(), attr.PublicScope, "name") - bob.SendGetValToPeer(alice.GetOnion(), attr.PublicScope, "name") + alice.SendScopedZonedGetValToContact(bob.GetOnion(), attr.PublicScope, attr.ProfileZone, "name") + bob.SendScopedZonedGetValToContact(alice.GetOnion(), attr.PublicScope, attr.ProfileZone, "name") - alice.SendGetValToPeer(carol.GetOnion(), attr.PublicScope, "name") - carol.SendGetValToPeer(alice.GetOnion(), attr.PublicScope, "name") + alice.SendScopedZonedGetValToContact(carol.GetOnion(), attr.PublicScope, attr.ProfileZone, "name") + carol.SendScopedZonedGetValToContact(alice.GetOnion(), attr.PublicScope, attr.ProfileZone, "name") time.Sleep(10 * time.Second) diff --git a/testing/filesharing/file_sharing_integration_test.go b/testing/filesharing/file_sharing_integration_test.go index 6fcf3cf..0f3b313 100644 --- a/testing/filesharing/file_sharing_integration_test.go +++ b/testing/filesharing/file_sharing_integration_test.go @@ -40,8 +40,8 @@ func waitForPeerPeerConnection(t *testing.T, peera peer.CwtchPeer, peerb peer.Cw time.Sleep(time.Second * 5) continue } else { - peerAName, _ := peera.GetAttribute(attr.GetLocalScope("name")) - peerBName, _ := peerb.GetAttribute(attr.GetLocalScope("name")) + peerAName, _ := peera.GetScopedZonedAttribute(attr.LocalScope, attr.ProfileZone, "name") + peerBName, _ := peerb.GetScopedZonedAttribute(attr.LocalScope, attr.ProfileZone, "name") fmt.Printf("%v CONNECTED and AUTHED to %v\n", peerAName, peerBName) break }