Enforced Zoned Attribute Lookups #394

Merged
dan merged 2 commits from zoned_attributes into master 2021-10-08 20:54:07 +00:00
8 changed files with 205 additions and 64 deletions

View File

@ -13,7 +13,7 @@ import (
"git.openprivacy.ca/openprivacy/log"
"io/ioutil"
"os"
"path"
path "path/filepath"
"strconv"
"sync"
)

View File

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

View File

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

View File

@ -1,9 +1,5 @@
package attr
import (
"strings"
)
/*
Scope model for peer attributes and requests
@ -16,45 +12,79 @@ values stored in the LocalScope.
*/
// Scope strongly types Scope strings
type Scope string
// ScopedZonedPath typed path with a scope and a zone
type ScopedZonedPath 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")
UnknownScope = Scope("unknown")
)
// 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 {
sarah marked this conversation as resolved
Review

if we're going type heavy dont we want it to return a like ScopedZonedPath?

if we're going type heavy dont we want it to return a like ScopedZonedPath?
switch scope {
case "local":
return LocalScope
case "peer":
return PeerScope
case "conversation":
return ConversationScope
case "public":
return PublicScope
}
return UnknownScope
}
sarah marked this conversation as resolved
Review

IsLocal() bool ?

IsLocal() bool ?
// 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) ScopedZonedPath {
return ScopedZonedPath(string(scope) + Separator + string(zonedPath))
}
// ToString converts a ScopedZonedPath to a string
func (szp ScopedZonedPath) ToString() string {
return string(szp)
}
// IsLocal returns true if the scope is a local scope
func (scope Scope) IsLocal() bool {
return scope == LocalScope
}
// IsPeer returns true if the scope is a peer scope
func (scope Scope) IsPeer() bool {
return scope == PeerScope
}
// 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
}
// 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
}

52
model/attr/zone.go Normal file
View File

@ -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")
)
// ConstructZonedPath 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) ConstructZonedPath(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]
}
}

View File

@ -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.ConstructZonedPath(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.ConstructZonedPath(key))
log.Debugf("looking up attribute %v %v %v (%v)", scope, zone, key, scopedZonedKey)
if val, exists := cp.Profile.GetAttribute(scopedZonedKey.ToString()); 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.ConstructZonedPath(key))
log.Debugf("storing attribute: %v = %v", scopedZonedKey, value)
cp.Profile.SetAttribute(scopedZonedKey.ToString(), value)
defer cp.mutex.Unlock()
cp.eventBus.Publish(event.NewEvent(event.SetAttribute, map[event.Field]string{
event.Key: scopedZonedKey.ToString(),
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,16 @@ 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
Review

this looks like it has lots of use? we want to create a issue to start on that?

this looks like it has lots of use? we want to create a issue to start on that?
Review

Created: #395

Created: https://git.openprivacy.ca/cwtch.im/cwtch/issues/395
// TODO This should probably not be exposed
StoreMessage(onion string, messageTxt string, sent time.Time)
// TODO Extract once groups are stable
// TODO
// Deprecated use overlays instead
InviteOnionToGroup(string, string) error
sarah marked this conversation as resolved
Review

What is still using this?

case event.NewMessageFromPeer: is but it can at least be dropped from the interface than as that's internal only. it looks like libcwtch-go doesn't use it. so i think it can be droped from the interface?

What is still using this? ```case event.NewMessageFromPeer:``` is but it can at least be dropped from the interface than as that's internal only. it looks like libcwtch-go doesn't use it. so i think it can be droped from the interface?
}
@ -202,9 +238,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 +696,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()
@ -726,7 +772,7 @@ func (cp *cwtchPeer) Shutdown() {
cp.queue.Shutdown()
}
func (cp *cwtchPeer) StoreMessage(onion string, messageTxt string, sent time.Time) {
func (cp *cwtchPeer) storeMessage(onion string, messageTxt string, sent time.Time) {
if cp.GetContact(onion) == nil {
cp.AddContact(onion, onion, model.AuthUnknown)
}
@ -779,7 +825,7 @@ func (cp *cwtchPeer) eventHandler() {
}
case event.NewMessageFromPeer: //event.TimestampReceived, event.RemotePeer, event.Data
ts, _ := time.Parse(time.RFC3339Nano, ev.Data[event.TimestampReceived])
cp.StoreMessage(ev.Data[event.RemotePeer], ev.Data[event.Data], ts)
cp.storeMessage(ev.Data[event.RemotePeer], ev.Data[event.Data], ts)
case event.PeerAcknowledgement:
cp.mutex.Lock()
@ -811,12 +857,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 +886,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 +932,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()

View File

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

View File

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