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" "git.openprivacy.ca/openprivacy/log"
"io/ioutil" "io/ioutil"
"os" "os"
"path" path "path/filepath"
"strconv" "strconv"
"sync" "sync"
) )

View File

@ -9,7 +9,7 @@ import (
"git.openprivacy.ca/cwtch.im/tapir/primitives" "git.openprivacy.ca/cwtch.im/tapir/primitives"
"git.openprivacy.ca/openprivacy/connectivity" "git.openprivacy.ca/openprivacy/connectivity"
"git.openprivacy.ca/openprivacy/log" "git.openprivacy.ca/openprivacy/log"
"path" path "path/filepath"
"strconv" "strconv"
"sync" "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 // DownloadFile given a profile, a conversation handle and a file sharing key, start off a download process
// to downloadFilePath // to downloadFilePath
func (f *Functionality) DownloadFile(profile peer.CwtchPeer, handle string, downloadFilePath string, manifestFilePath string, key string) { 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) // Store local.filesharing.filekey.manifest as the location of the manifest
profile.SendGetValToPeer(handle, attr.PublicScope, fmt.Sprintf("%s.manifest.size", key)) 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 // 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), // 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 // 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)) 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)) profile.ShareFile(key, string(serializedManifest))

View File

@ -1,9 +1,5 @@
package attr package attr
import (
"strings"
)
/* /*
Scope model for peer attributes and requests 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 // scopes for attributes
const ( const (
// on a peer, local and peer supplied data // on a peer, local and peer supplied data
LocalScope = "local" LocalScope = Scope("local")
PeerScope = "peer" PeerScope = Scope("peer")
ConversationScope = Scope("conversation")
// on a local profile, public data and private settings // on a local profile, public data and private settings
PublicScope = "public" PublicScope = Scope("public")
SettingsScope = "settings"
UnknownScope = Scope("unknown")
) )
// Separator for scope and the rest of path // Separator for scope and the rest of path
const Separator = "." const Separator = "."
// GetPublicScope takes a path and attaches the pubic scope to it // IntoScope converts a string to a Scope
func GetPublicScope(path string) string { 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?
return PublicScope + Separator + path 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 // ConstructScopedZonedPath enforces a scope over a zoned path
func GetSettingsScope(path string) string { func (scope Scope) ConstructScopedZonedPath(zonedPath ZonedPath) ScopedZonedPath {
return SettingsScope + Separator + path 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 // GetLocalScope takes a path and attaches the local scope to it
func GetLocalScope(path string) string { 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 // GetPeerScope takes a path and attaches the peer scope to it
func GetPeerScope(path string) string { func GetPeerScope(path string) string {
return PeerScope + Separator + path return string(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]
} }

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 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 // 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 // If you try to send a message to a handle that doesn't exist, malformed or an incorrect type then
// this function will error // this function will error
@ -163,15 +198,16 @@ type ModifyServers interface {
type SendMessages interface { type SendMessages interface {
SendMessage(handle string, message string) error SendMessage(handle string, message string) error
// Deprecated: is unsafe
SendGetValToPeer(string, string, string) SendGetValToPeer(string, string, string)
SendScopedZonedGetValToContact(handle string, scope attr.Scope, zone attr.Zone, key string)
// Deprecated // Deprecated
SendMessageToPeer(string, string) string 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 // TODO
StoreMessage(onion string, messageTxt string, sent time.Time) // Deprecated use overlays instead
// TODO Extract once groups are stable
InviteOnionToGroup(string, string) error 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 // Relating to local attributes
GetOnion() string GetOnion() string
// Deprecated: SetAttribute is Unsafe used SetScopedZonedAttribute Instead
SetAttribute(string, string) SetAttribute(string, string)
// Deprecated: GetAttribute is Unsafe used GetScopedZonedAttribute Instead
GetAttribute(string) (string, bool) 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 ReadContacts
ModifyContacts ModifyContacts
@ -651,6 +696,7 @@ func (cp *cwtchPeer) SetAttribute(key string, val string) {
})) }))
} }
// Deprecated
// GetAttribute gets an attribute for the profile // GetAttribute gets an attribute for the profile
func (cp *cwtchPeer) GetAttribute(key string) (string, bool) { func (cp *cwtchPeer) GetAttribute(key string) (string, bool) {
cp.mutex.Lock() cp.mutex.Lock()
@ -726,7 +772,7 @@ func (cp *cwtchPeer) Shutdown() {
cp.queue.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 { if cp.GetContact(onion) == nil {
cp.AddContact(onion, onion, model.AuthUnknown) cp.AddContact(onion, onion, model.AuthUnknown)
} }
@ -779,7 +825,7 @@ func (cp *cwtchPeer) eventHandler() {
} }
case event.NewMessageFromPeer: //event.TimestampReceived, event.RemotePeer, event.Data case event.NewMessageFromPeer: //event.TimestampReceived, event.RemotePeer, event.Data
ts, _ := time.Parse(time.RFC3339Nano, ev.Data[event.TimestampReceived]) 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: case event.PeerAcknowledgement:
cp.mutex.Lock() cp.mutex.Lock()
@ -811,12 +857,14 @@ func (cp *cwtchPeer) eventHandler() {
scope := ev.Data[event.Scope] scope := ev.Data[event.Scope]
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)
remotePeer := cp.GetContact(onion) remotePeer := cp.GetContact(onion)
if remotePeer != nil && remotePeer.Authorization == model.AuthApproved { if remotePeer != nil && remotePeer.Authorization == model.AuthApproved {
if scope == attr.PublicScope { scope := attr.IntoScope(scope)
val, exists := cp.GetAttribute(attr.GetPublicScope(path)) 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 := event.NewEvent(event.SendRetValMessageToPeer, map[event.Field]string{event.RemotePeer: onion, event.Exists: strconv.FormatBool(exists)})
resp.EventID = ev.EventID resp.EventID = ev.EventID
if exists { if exists {
@ -838,9 +886,9 @@ func (cp *cwtchPeer) eventHandler() {
fileKey := ev.Data[event.FileKey] fileKey := ev.Data[event.FileKey]
serializedManifest := ev.Data[event.SerializedManifest] 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 { if exists {
downloadFilePath, exists := cp.GetAttribute(attr.GetLocalScope(fileKey)) downloadFilePath, exists := cp.GetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fileKey)
if exists { if exists {
log.Debugf("downloading manifest to %v, file to %v", manifestFilePath, downloadFilePath) log.Debugf("downloading manifest to %v, file to %v", manifestFilePath, downloadFilePath)
var manifest files.Manifest var manifest files.Manifest
@ -884,22 +932,27 @@ func (cp *cwtchPeer) eventHandler() {
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", onion, scope, path, exists, val)
if exists { if exists {
if scope == attr.PublicScope {
if strings.HasSuffix(path, ".manifest.size") { // Handle File Sharing Metadata
fileKey := strings.Replace(path, ".manifest.size", "", 1) // TODO This probably should be broken out to it's own code..
size, err := strconv.Atoi(val) zone, path := attr.ParseZone(path)
// if size is valid and below the maximum size for a manifest if attr.Scope(scope).IsConversation() && zone == attr.FilesharingZone && strings.HasSuffix(path, ".manifest.size") {
// this is to prevent malicious sharers from using large amounts of memory when distributing fileKey := strings.Replace(path, ".manifest.size", "", 1)
// a manifest as we reconstruct this in-memory size, err := strconv.Atoi(val)
if err == nil && size < files.MaxManifestSize { // if size is valid and below the maximum size for a manifest
cp.eventBus.Publish(event.NewEvent(event.ManifestSizeReceived, map[event.Field]string{event.FileKey: fileKey, event.ManifestSize: val, event.Handle: onion})) // this is to prevent malicious sharers from using large amounts of memory when distributing
} else { // a manifest as we reconstruct this in-memory
cp.eventBus.Publish(event.NewEvent(event.ManifestError, map[event.Field]string{event.FileKey: fileKey, event.Handle: onion})) 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 { } 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: case event.PeerStateChange:
cp.mutex.Lock() cp.mutex.Lock()

View File

@ -154,17 +154,17 @@ func TestCwtchPeerIntegration(t *testing.T) {
alice := utils.WaitGetPeer(app, "alice") alice := utils.WaitGetPeer(app, "alice")
fmt.Println("Alice created:", alice.GetOnion()) 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}) alice.AutoHandleEvents([]event.Type{event.PeerStateChange, event.ServerStateChange, event.NewGroupInvite, event.NewRetValMessageFromPeer})
bob := utils.WaitGetPeer(app, "bob") bob := utils.WaitGetPeer(app, "bob")
fmt.Println("Bob created:", bob.GetOnion()) 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}) bob.AutoHandleEvents([]event.Type{event.PeerStateChange, event.ServerStateChange, event.NewGroupInvite, event.NewRetValMessageFromPeer})
carol := utils.WaitGetPeer(appClient, "carol") carol := utils.WaitGetPeer(appClient, "carol")
fmt.Println("Carol created:", carol.GetOnion()) 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}) carol.AutoHandleEvents([]event.Type{event.PeerStateChange, event.ServerStateChange, event.NewGroupInvite, event.NewRetValMessageFromPeer})
app.LaunchPeers() app.LaunchPeers()
@ -217,11 +217,11 @@ func TestCwtchPeerIntegration(t *testing.T) {
fmt.Println("Alice and Bob getVal public.name...") fmt.Println("Alice and Bob getVal public.name...")
alice.SendGetValToPeer(bob.GetOnion(), attr.PublicScope, "name") alice.SendScopedZonedGetValToContact(bob.GetOnion(), attr.PublicScope, attr.ProfileZone, "name")
bob.SendGetValToPeer(alice.GetOnion(), attr.PublicScope, "name") bob.SendScopedZonedGetValToContact(alice.GetOnion(), attr.PublicScope, attr.ProfileZone, "name")
alice.SendGetValToPeer(carol.GetOnion(), attr.PublicScope, "name") alice.SendScopedZonedGetValToContact(carol.GetOnion(), attr.PublicScope, attr.ProfileZone, "name")
carol.SendGetValToPeer(alice.GetOnion(), attr.PublicScope, "name") carol.SendScopedZonedGetValToContact(alice.GetOnion(), attr.PublicScope, attr.ProfileZone, "name")
time.Sleep(10 * time.Second) 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) time.Sleep(time.Second * 5)
continue continue
} else { } else {
peerAName, _ := peera.GetAttribute(attr.GetLocalScope("name")) peerAName, _ := peera.GetScopedZonedAttribute(attr.LocalScope, attr.ProfileZone, "name")
peerBName, _ := peerb.GetAttribute(attr.GetLocalScope("name")) peerBName, _ := peerb.GetScopedZonedAttribute(attr.LocalScope, attr.ProfileZone, "name")
fmt.Printf("%v CONNECTED and AUTHED to %v\n", peerAName, peerBName) fmt.Printf("%v CONNECTED and AUTHED to %v\n", peerAName, peerBName)
break break
} }