Compare commits

...

20 Commits

Author SHA1 Message Date
Sarah Jamie Lewis 0e96539f22 Merge pull request 'Store Messages and Send when Online' (#553) from offline-messages into master
continuous-integration/drone/push Build is pending Details
Reviewed-on: #553
Reviewed-by: Dan Ballard <dan@openprivacy.ca>
2024-04-16 18:35:02 +00:00
Sarah Jamie Lewis e55f342324
Updating Logging -> Debug
continuous-integration/drone/pr Build is passing Details
2024-02-26 13:40:47 -08:00
Sarah Jamie Lewis 89aca91b37
Store Messages and Send when Online
continuous-integration/drone/pr Build is passing Details
2024-02-26 13:18:38 -08:00
Sarah Jamie Lewis cd918c02ea Merge pull request 'Fix Error in ACL-V1 that Prevented ShareFiles (for some)' (#552) from acl-v2 into master
continuous-integration/drone/push Build is passing Details
Reviewed-on: #552
Reviewed-by: Dan Ballard <dan@openprivacy.ca>
2024-02-26 17:26:17 +00:00
Sarah Jamie Lewis 05a198c89f
Fix Error in ACL-V1 that Prevented ShareFiles (for some)
continuous-integration/drone/pr Build is passing Details
Also aligns model.DeserializeAttributes to best practice
2024-02-24 12:51:19 -08:00
Sarah Jamie Lewis 1d9202ff93 Don't reject text messages
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is pending Details
2024-02-12 22:02:35 +00:00
Sarah Jamie Lewis 0907af57d5 Merge pull request 'Introduce Channel/Overlay Mappings' (#549) from overlays into master
continuous-integration/drone/push Build is pending Details
Reviewed-on: #549
Reviewed-by: Dan Ballard <dan@openprivacy.ca>
2024-02-11 23:10:59 +00:00
Sarah Jamie Lewis 826ac40a5c Stream check in engine
continuous-integration/drone/pr Build is pending Details
2024-02-11 14:45:11 -08:00
Sarah Jamie Lewis 1a034953df Util Functions for MW
continuous-integration/drone/pr Build is pending Details
2024-02-11 14:44:18 -08:00
Sarah Jamie Lewis 3124f7b7c4 MessageOverlay time to pointer
continuous-integration/drone/pr Build is pending Details
2024-02-11 13:56:19 -08:00
Sarah Jamie Lewis 792e79dceb Introduce Channel/Overlay Mappings
continuous-integration/drone/pr Build is failing Details
- Map channel 7 to ephemeral / no ack
- Create model methods
- Introduce optional latency measurements into Cwtch
2024-02-11 12:14:07 -08:00
Sarah Jamie Lewis 3e0680943a Prevent Duplicate Queue Subscription
continuous-integration/drone/pr Build is pending Details
continuous-integration/drone/push Build is failing Details
2024-02-09 13:16:23 -08:00
Sarah Jamie Lewis 9cb62d269e Merge pull request 'Fix non-image/preview downloads in Android' (#547) from android_file_download_fix into master
continuous-integration/drone/push Build is pending Details
Reviewed-on: #547
Reviewed-by: Dan Ballard <dan@openprivacy.ca>
2024-02-09 21:06:25 +00:00
Sarah Jamie Lewis ec71e56d23 Fix non-image/preview downloads in Android
continuous-integration/drone/pr Build is pending Details
2024-02-09 11:33:25 -08:00
Sarah Jamie Lewis aaabb12b6c Merge pull request 'First Cut of Enhanced Permissions' (#543) from enhanced-permissions into master
continuous-integration/drone/push Build is pending Details
Reviewed-on: #543
Reviewed-by: Dan Ballard <dan@openprivacy.ca>
2024-01-15 18:04:34 +00:00
Sarah Jamie Lewis b0a87ee8d0 Move comment for better understanding
continuous-integration/drone/pr Build is pending Details
2024-01-11 10:06:08 -08:00
Sarah Jamie Lewis d66beb95e5 Update APIs, Formatting
continuous-integration/drone/pr Build is pending Details
2024-01-11 10:02:27 -08:00
Sarah Jamie Lewis 41b3e20aff Remove Flakey Queued Check in Contact Retry Plugin Test
continuous-integration/drone/pr Build is passing Details
2024-01-08 13:25:53 -08:00
Sarah Jamie Lewis 1c7003fb96 First Draft of Enhanced Permissions API
continuous-integration/drone/pr Build is pending Details
2024-01-08 13:22:38 -08:00
Dan Ballard cb3b0b4c46 add new setting themeImages and fix default themeing
continuous-integration/drone/pr Build is pending Details
continuous-integration/drone/push Build is pending Details
2024-01-06 12:04:47 -08:00
21 changed files with 409 additions and 74 deletions

4
.gitignore vendored
View File

@ -33,4 +33,6 @@ data-dir-cwtchtool/
tokens
tordir/
testing/autodownload/download_dir
testing/autodownload/storage
testing/autodownload/storage
*.swp
testing/managerstorage/*

View File

@ -363,6 +363,7 @@ func (app *application) LoadProfiles(password string) {
func (app *application) registerHooks(profile peer.CwtchPeer) {
// Register Hooks
profile.RegisterHook(extensions.ProfileValueExtension{})
profile.RegisterHook(extensions.SendWhenOnlineExtension{})
profile.RegisterHook(new(filesharing.Functionality))
profile.RegisterHook(new(filesharing.ImagePreviewsFunctionality))
profile.RegisterHook(new(servers.Functionality))

View File

@ -36,12 +36,10 @@ func TestContactRetryQueue(t *testing.T) {
// progress...
setup := false
for !setup {
if pinf, exists := cr.connections.Load(testOnion); exists {
if pinf.(*contact).queued {
if _, exists := cr.authorizedPeers.Load(testOnion); exists {
t.Logf("authorized")
setup = true
}
if _, exists := cr.connections.Load(testOnion); exists {
if _, exists := cr.authorizedPeers.Load(testOnion); exists {
t.Logf("authorized")
setup = true
}
}
}

View File

@ -95,6 +95,11 @@ func (em *manager) initialize() {
func (em *manager) Subscribe(eventType Type, queue Queue) {
em.mapMutex.Lock()
defer em.mapMutex.Unlock()
for _, sub := range em.subscribers[eventType] {
if sub == queue {
return // don't add the same queue for the same event twice...
}
}
em.subscribers[eventType] = append(em.subscribers[eventType], queue)
}

View File

@ -0,0 +1,66 @@
package extensions
import (
"strconv"
"cwtch.im/cwtch/event"
"cwtch.im/cwtch/model"
"cwtch.im/cwtch/model/attr"
"cwtch.im/cwtch/model/constants"
"cwtch.im/cwtch/peer"
"cwtch.im/cwtch/protocol/connections"
"cwtch.im/cwtch/settings"
"git.openprivacy.ca/openprivacy/log"
)
// SendWhenOnlineExtension implements automatic sending
// Some Considerations:
// - There are race conditions inherant in this approach e.g. a peer could go offline just after recieving a message and never sending an ack
// - In that case the next time we connect we will send a duplicate message.
// - Currently we do not include metadata like sent time in raw peer protocols (however Overlay does now have support for that information)
type SendWhenOnlineExtension struct {
}
func (soe SendWhenOnlineExtension) NotifySettingsUpdate(_ settings.GlobalSettings) {
}
func (soe SendWhenOnlineExtension) EventsToRegister() []event.Type {
return []event.Type{event.PeerStateChange}
}
func (soe SendWhenOnlineExtension) ExperimentsToRegister() []string {
return nil
}
func (soe SendWhenOnlineExtension) OnEvent(ev event.Event, profile peer.CwtchPeer) {
switch ev.EventType {
case event.PeerStateChange:
ci, err := profile.FetchConversationInfo(ev.Data["RemotePeer"])
if err == nil {
// if we have re-authenticated with thie peer then request their profile image...
if connections.ConnectionStateToType()[ev.Data[event.ConnectionState]] == connections.AUTHENTICATED {
// Check the last 100 messages, if any of them are pending, then send them now...
messsages, _ := profile.GetMostRecentMessages(ci.ID, 0, 0, uint(100))
for _, message := range messsages {
if message.Attr[constants.AttrAck] == constants.False {
body := message.Body
ev := event.NewEvent(event.SendMessageToPeer, map[event.Field]string{event.ConversationID: strconv.Itoa(ci.ID), event.RemotePeer: ci.Handle, event.Data: body})
ev.EventID = message.Signature // we need this ensure that we correctly ack this in the db when it comes back
// TODO: The EventBus is becoming very noisy...we may want to consider a one-way shortcut to Engine i.e. profile.Engine.SendMessageToPeer
log.Debugf("resending message that was sent when peer was offline")
profile.PublishEvent(ev)
}
}
}
}
}
}
// OnContactReceiveValue is nop for SendWhenOnnlineExtension
func (soe SendWhenOnlineExtension) OnContactReceiveValue(profile peer.CwtchPeer, conversation model.Conversation, szp attr.ScopedZonedPath, value string, exists bool) {
}
// OnContactRequestValue is nop for SendWhenOnnlineExtension
func (soe SendWhenOnlineExtension) OnContactRequestValue(profile peer.CwtchPeer, conversation model.Conversation, eventID string, szp attr.ScopedZonedPath) {
}

View File

@ -272,15 +272,21 @@ func (f *Functionality) DownloadFile(profile peer.CwtchPeer, conversationID int,
}
// Don't download files if the download file directory does not exist
if _, err := os.Stat(path.Dir(downloadFilePath)); os.IsNotExist(err) {
return errors.New("download directory does not exist")
}
// Unless we are on Android where the kernel wishes to keep us ignorant of the
// actual path and/or existence of the file. We handle this case further down
// the line when the manifest is received and protocol engine and the Android layer
// negotiate a temporary local file -> final file copy. We don't want to worry
// about that here...
if runtime.GOOS != "android" {
if _, err := os.Stat(path.Dir(downloadFilePath)); os.IsNotExist(err) {
return errors.New("download directory does not exist")
}
// Don't download files if the manifest file directory does not exist
if _, err := os.Stat(path.Dir(manifestFilePath)); os.IsNotExist(err) {
return errors.New("manifest directory does not exist")
// Don't download files if the manifest file directory does not exist
if _, err := os.Stat(path.Dir(manifestFilePath)); os.IsNotExist(err) {
return errors.New("manifest directory does not exist")
}
}
// Store local.filesharing.filekey.manifest as the location of the manifest
profile.SetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.manifest", key), manifestFilePath)

View File

@ -38,14 +38,14 @@ func (i *ImagePreviewsFunctionality) OnEvent(ev event.Event, profile peer.CwtchP
case event.NewMessageFromPeer:
ci, err := profile.FetchConversationInfo(ev.Data["RemotePeer"])
if err == nil {
if ci.Accepted {
if ci.GetPeerAC().RenderImages {
i.handleImagePreviews(profile, &ev, ci.ID, ci.ID)
}
}
case event.NewMessageFromGroup:
ci, err := profile.FetchConversationInfo(ev.Data["RemotePeer"])
if err == nil {
if ci.Accepted {
if ci.GetPeerAC().RenderImages {
i.handleImagePreviews(profile, &ev, ci.ID, ci.ID)
}
}
@ -90,7 +90,7 @@ func (i *ImagePreviewsFunctionality) OnContactReceiveValue(profile peer.CwtchPee
_, zone, path := path.GetScopeZonePath()
if exists && zone == attr.ProfileZone && path == constants.CustomProfileImageKey {
// We only download from accepted conversations
if conversation.Accepted {
if conversation.GetPeerAC().RenderImages {
fileKey := value
basepath := i.downloadFolder
fsf := FunctionalityGate()
@ -123,6 +123,16 @@ func (i *ImagePreviewsFunctionality) OnContactReceiveValue(profile peer.CwtchPee
// handleImagePreviews checks settings and, if appropriate, auto-downloads any images
func (i *ImagePreviewsFunctionality) handleImagePreviews(profile peer.CwtchPeer, ev *event.Event, conversationID, senderID int) {
if profile.IsFeatureEnabled(constants.FileSharingExperiment) && profile.IsFeatureEnabled(constants.ImagePreviewsExperiment) {
ci, err := profile.GetConversationInfo(senderID)
if err != nil {
log.Errorf("attempted to call handleImagePreviews with unknown conversation: %v", senderID)
return
}
if !ci.GetPeerAC().ShareFiles || !ci.GetPeerAC().RenderImages {
log.Infof("refusing to autodownload files from sender: %v. conversation AC does not permit image rendering", senderID)
return
}
// Short-circuit failures
// Don't auto-download images if the download path does not exist.
@ -142,7 +152,7 @@ func (i *ImagePreviewsFunctionality) handleImagePreviews(profile peer.CwtchPeer,
// Now look at the image preview experiment
var cm model.MessageWrapper
err := json.Unmarshal([]byte(ev.Data[event.Data]), &cm)
err = json.Unmarshal([]byte(ev.Data[event.Data]), &cm)
if err == nil && cm.Overlay == model.OverlayFileSharing {
log.Debugf("Received File Sharing Message")
var fm OverlayMessage

1
go.mod
View File

@ -16,7 +16,6 @@ require (
require (
filippo.io/edwards25519 v1.0.0 // indirect
git.openprivacy.ca/openprivacy/bine v0.0.5 // indirect
github.com/client9/misspell v0.3.4 // indirect
github.com/google/go-cmp v0.5.8 // indirect
github.com/gtank/merlin v0.1.1 // indirect
github.com/mimoo/StrobeGo v0.0.0-20220103164710-9a04d6ca976b // indirect

2
go.sum
View File

@ -8,8 +8,6 @@ git.openprivacy.ca/openprivacy/connectivity v1.11.0 h1:roASjaFtQLu+HdH5fa2wx6F00
git.openprivacy.ca/openprivacy/connectivity v1.11.0/go.mod h1:OQO1+7OIz/jLxDrorEMzvZA6SEbpbDyLGpjoFqT3z1Y=
git.openprivacy.ca/openprivacy/log v1.0.3 h1:E/PMm4LY+Q9s3aDpfySfEDq/vYQontlvNj/scrPaga0=
git.openprivacy.ca/openprivacy/log v1.0.3/go.mod h1:gGYK8xHtndRLDymFtmjkG26GaMQNgyhioNS82m812Iw=
github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=

View File

@ -67,3 +67,8 @@ const ProfileAttribute3 = "profile-attribute-3"
// Description is used on server contacts,
const Description = "description"
// Used to store the status of acl migrations
const ACLVersion = "acl-version"
const ACLVersionOne = "acl-v1"
const ACLVersionTwo = "acl-v2"

View File

@ -1,22 +1,36 @@
package model
import (
"cwtch.im/cwtch/model/attr"
"cwtch.im/cwtch/model/constants"
"encoding/json"
"time"
"cwtch.im/cwtch/model/attr"
"cwtch.im/cwtch/model/constants"
"git.openprivacy.ca/openprivacy/connectivity/tor"
"git.openprivacy.ca/openprivacy/log"
)
// AccessControl is a type determining client assigned authorization to a peer
// for a given conversation
type AccessControl struct {
Blocked bool // Any attempts from this handle to connect are blocked
Read bool // Allows a handle to access the conversation
Append bool // Allows a handle to append new messages to the conversation
Blocked bool // Any attempts from this handle to connect are blocked overrides all other settings
// Basic Conversation Rights
Read bool // Allows a handle to access the conversation
Append bool // Allows a handle to append new messages to the conversation
AutoConnect bool // Profile should automatically try to connect with peer
ExchangeAttributes bool // Profile should automatically exchange attributes like Name, Profile Image, etc.
// Extension Related Permissions
ShareFiles bool // Allows a handle to share files to a conversation
RenderImages bool // Indicates that certain filetypes should be autodownloaded and rendered when shared by this contact
}
// DefaultP2PAccessControl - because in the year 2021, go does not support constant structs...
// DefaultP2PAccessControl defaults to a semi-trusted peer with no access to special extensions.
func DefaultP2PAccessControl() AccessControl {
return AccessControl{Read: true, Append: true, Blocked: false}
return AccessControl{Read: true, Append: true, ExchangeAttributes: true, Blocked: false,
AutoConnect: true, ShareFiles: false, RenderImages: false}
}
// AccessControlList represents an access control list for a conversation. Mapping handles to conversation
@ -30,10 +44,10 @@ func (acl *AccessControlList) Serialize() []byte {
}
// DeserializeAccessControlList takes in JSON and returns an AccessControlList
func DeserializeAccessControlList(data []byte) AccessControlList {
func DeserializeAccessControlList(data []byte) (AccessControlList, error) {
var acl AccessControlList
json.Unmarshal(data, &acl)
return acl
err := json.Unmarshal(data, &acl)
return acl, err
}
// Attributes a type-driven encapsulation of an Attribute map.
@ -47,8 +61,12 @@ func (a *Attributes) Serialize() []byte {
// DeserializeAttributes converts a JSON struct into an Attributes map
func DeserializeAttributes(data []byte) Attributes {
var attributes Attributes
json.Unmarshal(data, &attributes)
attributes := make(Attributes)
err := json.Unmarshal(data, &attributes)
if err != nil {
log.Error("error deserializing attributes (this is likely a programming error): %v", err)
return make(Attributes)
}
return attributes
}
@ -60,7 +78,9 @@ type Conversation struct {
Handle string
Attributes Attributes
ACL AccessControlList
Accepted bool
// Deprecated, please use ACL for permissions related functions
Accepted bool
}
// GetAttribute is a helper function that fetches a conversation attribute by scope, zone and key
@ -71,6 +91,21 @@ func (ci *Conversation) GetAttribute(scope attr.Scope, zone attr.Zone, key strin
return "", false
}
// GetPeerAC returns a suitable Access Control object for a the given peer conversation
// If this is called for a group conversation, this method will error and return a safe default AC.
func (ci *Conversation) GetPeerAC() AccessControl {
if acl, exists := ci.ACL[ci.Handle]; exists {
return acl
}
log.Errorf("attempted to access a Peer Access Control object from %v but peer ACL is undefined. This is likely a programming error", ci.Handle)
return DefaultP2PAccessControl()
}
// IsCwtchPeer is a helper attribute that identifies whether a conversation is a cwtch peer
func (ci *Conversation) IsCwtchPeer() bool {
return tor.IsValidHostname(ci.Handle)
}
// IsGroup is a helper attribute that identifies whether a conversation is a legacy group
func (ci *Conversation) IsGroup() bool {
if _, exists := ci.Attributes[attr.LocalScope.ConstructScopedZonedPath(attr.LegacyGroupZone.ConstructZonedPath(constants.GroupID)).ToString()]; exists {

View File

@ -3,6 +3,7 @@ package model
import (
"crypto/sha256"
"encoding/base64"
"encoding/json"
)
// CalculateContentHash derives a hash using the author and the message body. It is intended to be
@ -12,3 +13,13 @@ func CalculateContentHash(author string, messageBody string) string {
contentBasedHash := sha256.Sum256(content)
return base64.StdEncoding.EncodeToString(contentBasedHash[:])
}
func DeserializeMessage(message string) (*MessageWrapper, error) {
var cm MessageWrapper
err := json.Unmarshal([]byte(message), &cm)
if err != nil {
return nil, err
}
return &cm, err
}

View File

@ -1,9 +1,40 @@
package model
import (
"time"
)
// MessageWrapper is the canonical Cwtch overlay wrapper
type MessageWrapper struct {
Overlay int `json:"o"`
Data string `json:"d"`
// when the data was assembled
SendTime *time.Time `json:"s,omitempty"`
// when the data was transmitted (by protocol engine e.g. over Tor)
TransitTime *time.Time `json:"t,omitempty"`
// when the data was received
RecvTime *time.Time `json:"r,omitempty"`
}
// Channel is defined as being the last 3 bits of the overlay id
// Channel 0 is reserved for the main conversation
// Channel 2 is reserved for conversation admin (managed groups)
// Channel 7 is reserved for streams (no ack, no store)
func (mw MessageWrapper) Channel() int {
if mw.Overlay > 1024 {
return mw.Overlay & 0x07
}
// for backward compatibilty all overlays less than 0x400 i.e. 1024 are
// mapped to channel 0 regardless of their channel status.
return 0
}
// If Overlay is a Stream Message it should not be ackd, or stored.
func (mw MessageWrapper) IsStream() bool {
return mw.Channel() == 0x07
}
// OverlayChat is the canonical identifier for chat overlays

View File

@ -3,11 +3,20 @@ package peer
import (
"context"
"crypto/rand"
"cwtch.im/cwtch/model"
"cwtch.im/cwtch/model/constants"
"cwtch.im/cwtch/protocol/groups"
"cwtch.im/cwtch/settings"
"encoding/base64"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"git.openprivacy.ca/cwtch.im/tapir/primitives"
"git.openprivacy.ca/cwtch.im/tapir/primitives/privacypass"
"git.openprivacy.ca/openprivacy/connectivity"
"git.openprivacy.ca/openprivacy/connectivity/tor"
"golang.org/x/crypto/ed25519"
"os"
path "path/filepath"
"sort"
@ -16,17 +25,7 @@ import (
"sync"
"time"
"cwtch.im/cwtch/model/constants"
"cwtch.im/cwtch/protocol/groups"
"cwtch.im/cwtch/settings"
"git.openprivacy.ca/cwtch.im/tapir/primitives"
"git.openprivacy.ca/cwtch.im/tapir/primitives/privacypass"
"git.openprivacy.ca/openprivacy/connectivity"
"git.openprivacy.ca/openprivacy/connectivity/tor"
"golang.org/x/crypto/ed25519"
"cwtch.im/cwtch/event"
"cwtch.im/cwtch/model"
"cwtch.im/cwtch/model/attr"
"cwtch.im/cwtch/protocol/connections"
"git.openprivacy.ca/openprivacy/log"
@ -310,7 +309,33 @@ func (cp *cwtchPeer) GenerateProtocolEngine(acn connectivity.ACN, bus event.Mana
authorizations := make(map[string]model.Authorization)
for _, conversation := range conversations {
if tor.IsValidHostname(conversation.Handle) {
// Only perform the following actions for Peer-type Conversaions...
if conversation.IsCwtchPeer() {
// if this profile does not have an ACL version, and the profile is accepted (OR the acl version is v1 and the profile is accepted...)
// then migrate the permissions to the v2 ACL
// migrate the old accepted AC to a new fine-grained one
// we only do this for previously trusted connections
// NOTE: this does not supercede global cwthch experiments settings
// if share files is turned off globally then acl.ShareFiles will be ignored
// Note: There was a bug in the original EP code that meant that some acl-v1 profiles did not get ShareFiles or RenderImages - this corrects that.
if version, exists := conversation.GetAttribute(attr.LocalScope, attr.ProfileZone, constants.ACLVersion); !exists || version == constants.ACLVersionOne {
if conversation.Accepted {
if ac, exists := conversation.ACL[conversation.Handle]; exists {
ac.ShareFiles = true
ac.RenderImages = true
ac.AutoConnect = true
ac.ExchangeAttributes = true
conversation.ACL[conversation.Handle] = ac
}
// Update the ACL Version
cp.storage.SetConversationAttribute(conversation.ID, attr.LocalScope.ConstructScopedZonedPath(attr.ProfileZone.ConstructZonedPath(constants.ACLVersion)), constants.ACLVersionTwo)
// Store the updated ACL
cp.storage.SetConversationACL(conversation.ID, conversation.ACL)
}
}
if conversation.ACL[conversation.Handle].Blocked {
authorizations[conversation.Handle] = model.AuthBlocked
} else {
@ -410,12 +435,19 @@ func (cp *cwtchPeer) SendMessage(conversation int, message string) (int, error)
if tor.IsValidHostname(conversationInfo.Handle) {
ev := event.NewEvent(event.SendMessageToPeer, map[event.Field]string{event.ConversationID: strconv.Itoa(conversationInfo.ID), event.RemotePeer: conversationInfo.Handle, event.Data: message})
onion, _ := cp.storage.LoadProfileKeyValue(TypeAttribute, attr.PublicScope.ConstructScopedZonedPath(attr.ProfileZone.ConstructZonedPath(constants.Onion)).ToString())
id := -1
// For p2p messages we store the event id of the message as the "signature" we can then look this up in the database later for acks
id, err := cp.storage.InsertMessage(conversationInfo.ID, 0, message, model.Attributes{constants.AttrAuthor: string(onion), constants.AttrAck: event.False, constants.AttrSentTimestamp: time.Now().Format(time.RFC3339Nano)}, ev.EventID, model.CalculateContentHash(string(onion), message))
if err != nil {
return -1, err
// check if we should store this message locally...
if cm, err := model.DeserializeMessage(message); err == nil {
if !cm.IsStream() {
// For p2p messages we store the event id of the message as the "signature" we can then look this up in the database later for acks
id, err = cp.storage.InsertMessage(conversationInfo.ID, 0, message, model.Attributes{constants.AttrAuthor: string(onion), constants.AttrAck: event.False, constants.AttrSentTimestamp: time.Now().Format(time.RFC3339Nano)}, ev.EventID, model.CalculateContentHash(string(onion), message))
if err != nil {
return -1, err
}
}
}
cp.eventBus.Publish(ev)
return id, nil
} else {
@ -682,13 +714,60 @@ func (cp *cwtchPeer) NewContactConversation(handle string, acl model.AccessContr
conversationInfo, _ := cp.storage.GetConversationByHandle(handle)
if conversationInfo == nil {
conversationID, err := cp.storage.NewConversation(handle, model.Attributes{event.SaveHistoryKey: event.DeleteHistoryDefault}, model.AccessControlList{handle: acl}, accepted)
if err != nil {
log.Errorf("unable to create a new contact conversation: %v", err)
return -1, err
}
cp.SetConversationAttribute(conversationID, attr.LocalScope.ConstructScopedZonedPath(attr.ProfileZone.ConstructZonedPath(constants.AttrLastConnectionTime)), time.Now().Format(time.RFC3339Nano))
if accepted {
// If this call came from a trusted action (i.e. import bundle or accept button then accept the conversation)
// This assigns all permissions (and in v2 is currently the default state of trusted contacts)
// Accept conversation does PeerWithOnion
cp.AcceptConversation(conversationID)
}
cp.SetConversationAttribute(conversationID, attr.LocalScope.ConstructScopedZonedPath(attr.ProfileZone.ConstructZonedPath(constants.ACLVersion)), constants.ACLVersionTwo)
cp.eventBus.Publish(event.NewEvent(event.ContactCreated, map[event.Field]string{event.ConversationID: strconv.Itoa(conversationID), event.RemotePeer: handle}))
return conversationID, err
}
return -1, fmt.Errorf("contact conversation already exists")
}
// UpdateConversationAccessControlList is a genric ACL update method
func (cp *cwtchPeer) UpdateConversationAccessControlList(id int, acl model.AccessControlList) error {
return cp.storage.SetConversationACL(id, acl)
}
// EnhancedUpdateConversationAccessControlList wraps UpdateConversationAccessControlList and allows updating via a serialized JSON struct
func (cp *cwtchPeer) EnhancedUpdateConversationAccessControlList(id int, json string) error {
_, err := cp.GetConversationInfo(id)
if err == nil {
acl, err := model.DeserializeAccessControlList([]byte(json))
if err == nil {
return cp.UpdateConversationAccessControlList(id, acl)
}
return err
}
return err
}
// GetConversationAccessControlList returns the access control list associated with the conversation
func (cp *cwtchPeer) GetConversationAccessControlList(id int) (model.AccessControlList, error) {
ci, err := cp.GetConversationInfo(id)
if err == nil {
return ci.ACL, nil
}
return nil, err
}
// EnhancedGetConversationAccessControlList serialzies the access control list associated with the conversation
func (cp *cwtchPeer) EnhancedGetConversationAccessControlList(id int) (string, error) {
ci, err := cp.GetConversationInfo(id)
if err == nil {
return string(ci.ACL.Serialize()), nil
}
return "", err
}
// AcceptConversation looks up a conversation by `handle` and sets the Accepted status to `true`
// This will cause Cwtch to auto connect to this conversation on start up
func (cp *cwtchPeer) AcceptConversation(id int) error {
@ -701,6 +780,21 @@ func (cp *cwtchPeer) AcceptConversation(id int) error {
log.Errorf("Could not get conversation for %v: %v", id, err)
return err
}
if ac, exists := ci.ACL[ci.Handle]; exists {
ac.ShareFiles = true
ac.AutoConnect = true
ac.RenderImages = true
ac.ExchangeAttributes = true
ci.ACL[ci.Handle] = ac
}
err = cp.storage.SetConversationACL(id, ci.ACL)
if err != nil {
log.Errorf("Could not set conversation acl for %v: %v", id, err)
return err
}
if !ci.IsGroup() && !ci.IsServer() {
cp.sendUpdateAuth(id, ci.Handle, ci.Accepted, ci.ACL[ci.Handle].Blocked)
cp.PeerWithOnion(ci.Handle)
@ -748,7 +842,7 @@ func (cp *cwtchPeer) UnblockConversation(id int) error {
// TODO at some point in the future engine needs to understand ACLs not just legacy auth status
cp.sendUpdateAuth(id, ci.Handle, ci.Accepted, ci.ACL[ci.Handle].Blocked)
if !ci.IsGroup() && !ci.IsServer() && ci.Accepted {
if !ci.IsGroup() && !ci.IsServer() && ci.GetPeerAC().AutoConnect {
cp.PeerWithOnion(ci.Handle)
}
@ -1056,7 +1150,7 @@ func (cp *cwtchPeer) QueuePeeringWithOnion(handle string) {
ci, err := cp.FetchConversationInfo(handle)
if err == nil {
lastSeen := cp.GetConversationLastSeenTime(ci.ID)
if !ci.ACL[ci.Handle].Blocked && ci.Accepted {
if !ci.ACL[ci.Handle].Blocked {
cp.eventBus.Publish(event.NewEvent(event.QueuePeerRequest, map[event.Field]string{event.RemotePeer: handle, event.LastSeen: lastSeen.Format(time.RFC3339Nano)}))
}
}
@ -1174,9 +1268,9 @@ func (cp *cwtchPeer) ImportBundle(importString string) error {
return ConstructResponse(constants.ImportBundlePrefix, "success")
} else if tor.IsValidHostname(importString) {
_, err := cp.NewContactConversation(importString, model.DefaultP2PAccessControl(), true)
// NOTE: Not NewContactConversation implictly does AcceptConversation AND PeerWithOnion if relevant so
// we no longer need to do it here...
if err == nil {
// Assuming all is good, we should peer with this contact.
cp.PeerWithOnion(importString)
return ConstructResponse(constants.ImportBundlePrefix, "success")
}
return ConstructResponse(constants.ImportBundlePrefix, err.Error())
@ -1316,7 +1410,7 @@ func (cp *cwtchPeer) getConnectionsSortedByLastSeen(doPeers, doServers bool) []*
continue
}
} else {
if !doPeers || !conversation.Accepted {
if !doPeers {
continue
}
}
@ -1339,7 +1433,9 @@ func (cp *cwtchPeer) StartConnections(doPeers, doServers bool) {
cp.QueueJoinServer(conversation.model.Handle)
} else {
log.Debugf(" QueuePeerWithOnion(%v)", conversation.model.Handle)
cp.QueuePeeringWithOnion(conversation.model.Handle)
if conversation.model.GetPeerAC().AutoConnect {
cp.QueuePeeringWithOnion(conversation.model.Handle)
}
}
time.Sleep(50 * time.Millisecond)
}
@ -1403,6 +1499,13 @@ func (cp *cwtchPeer) storeMessage(handle string, message string, sent time.Time)
}
}
// Don't store messages in channel 7
if cm, err := model.DeserializeMessage(message); err == nil {
if cm.IsStream() {
return -1, nil
}
}
// Generate a random number and use it as the signature
signature := event.GetRandNumber().String()
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))
@ -1508,7 +1611,7 @@ func (cp *cwtchPeer) eventHandler() {
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.GetPeerAC().ExchangeAttributes {
// Type Safe Scoped/Zoned Path
zscope := attr.IntoScope(scope)
zone, zpath := attr.ParseZone(zpath)
@ -1540,7 +1643,7 @@ func (cp *cwtchPeer) eventHandler() {
conversationInfo, _ := cp.FetchConversationInfo(handle)
// only accepted contacts can look up information
if conversationInfo != nil && conversationInfo.Accepted {
if conversationInfo != nil && conversationInfo.GetPeerAC().ExchangeAttributes {
// Type Safe Scoped/Zoned Path
zscope := attr.IntoScope(scope)
zone, zpath := attr.ParseZone(zpath)
@ -1580,6 +1683,7 @@ func (cp *cwtchPeer) eventHandler() {
timestamp := time.Now().Format(time.RFC3339Nano)
cp.SetConversationAttribute(cid, attr.LocalScope.ConstructScopedZonedPath(attr.ProfileZone.ConstructZonedPath(constants.AttrLastConnectionTime)), timestamp)
} else if connections.ConnectionStateToType()[ev.Data[event.ConnectionState]] == connections.DISCONNECTED {
ci, err := cp.FetchConversationInfo(handle)
if err == nil {

View File

@ -376,7 +376,12 @@ func (cps *CwtchProfileStorage) GetConversationByHandle(handle string) (*model.C
}
rows.Close()
return &model.Conversation{ID: id, Handle: handle, ACL: model.DeserializeAccessControlList(acl), Attributes: model.DeserializeAttributes(attributes), Accepted: accepted}, nil
cacl, err := model.DeserializeAccessControlList(acl)
if err != nil {
log.Errorf("error deserializing ACL from database, database maybe corrupted: %v", err)
return nil, err
}
return &model.Conversation{ID: id, Handle: handle, ACL: cacl, Attributes: model.DeserializeAttributes(attributes), Accepted: accepted}, nil
}
// FetchConversations returns *all* active conversations. This method should only be called
@ -412,7 +417,13 @@ func (cps *CwtchProfileStorage) FetchConversations() ([]*model.Conversation, err
rows.Close()
return nil, err
}
conversations = append(conversations, &model.Conversation{ID: id, Handle: handle, ACL: model.DeserializeAccessControlList(acl), Attributes: model.DeserializeAttributes(attributes), Accepted: accepted})
cacl, err := model.DeserializeAccessControlList(acl)
if err != nil {
log.Errorf("error deserializing ACL from database, database maybe corrupted: %v", err)
return nil, err
}
conversations = append(conversations, &model.Conversation{ID: id, Handle: handle, ACL: cacl, Attributes: model.DeserializeAttributes(attributes), Accepted: accepted})
}
}
@ -445,7 +456,12 @@ func (cps *CwtchProfileStorage) GetConversation(id int) (*model.Conversation, er
}
rows.Close()
return &model.Conversation{ID: id, Handle: handle, ACL: model.DeserializeAccessControlList(acl), Attributes: model.DeserializeAttributes(attributes), Accepted: accepted}, nil
cacl, err := model.DeserializeAccessControlList(acl)
if err != nil {
log.Errorf("error deserializing ACL from database, database maybe corrupted: %v", err)
return nil, err
}
return &model.Conversation{ID: id, Handle: handle, ACL: cacl, Attributes: model.DeserializeAttributes(attributes), Accepted: accepted}, nil
}
// AcceptConversation sets the accepted status of a conversation to true in the backing datastore

View File

@ -120,9 +120,19 @@ type CwtchPeer interface {
ArchiveConversation(conversation int)
GetConversationInfo(conversation int) (*model.Conversation, error)
FetchConversationInfo(handle string) (*model.Conversation, error)
// API-level management of conversation access control
UpdateConversationAccessControlList(id int, acl model.AccessControlList) error
EnhancedUpdateConversationAccessControlList(conversation int, acjson string) error
GetConversationAccessControlList(conversation int) (model.AccessControlList, error)
EnhancedGetConversationAccessControlList(conversation int) (string, error)
// Convieniance Functions for ACL Management
AcceptConversation(conversation int) error
BlockConversation(conversation int) error
UnblockConversation(conversation int) error
SetConversationAttribute(conversation int, path attr.ScopedZonedPath, value string) error
GetConversationAttribute(conversation int, path attr.ScopedZonedPath) (string, error)
DeleteConversation(conversation int) error

View File

@ -749,6 +749,16 @@ func (e *engine) handlePeerMessage(hostname string, eventID string, context stri
// Fall through handler for the default text conversation.
e.eventManager.Publish(event.NewEvent(event.NewMessageFromPeerEngine, map[event.Field]string{event.TimestampReceived: time.Now().Format(time.RFC3339Nano), event.RemotePeer: hostname, event.Data: string(message)}))
// Don't ack messages in channel 7
// Note: this code explictly doesn't care about malformed messages, we deal with them
// later on...we still want to ack the original send...(as some "malformed" messages
// may be future-ok)
if cm, err := model.DeserializeMessage(string(message)); err == nil {
if cm.IsStream() {
return
}
}
// Send an explicit acknowledgement
// Every other protocol should have an explicit acknowledgement message e.g. value lookups have responses, and file handling has an explicit flow
if err := e.sendPeerMessage(hostname, pmodel.PeerMessage{ID: eventID, Context: event.ContextAck, Data: []byte{}}); err != nil {

View File

@ -2,12 +2,14 @@ package connections
import (
"cwtch.im/cwtch/event"
"cwtch.im/cwtch/model"
model2 "cwtch.im/cwtch/protocol/model"
"encoding/json"
"git.openprivacy.ca/cwtch.im/tapir"
"git.openprivacy.ca/cwtch.im/tapir/applications"
"git.openprivacy.ca/openprivacy/log"
"sync/atomic"
"time"
)
const cwtchCapability = tapir.Capability("cwtchCapability")
@ -133,6 +135,14 @@ func (pa *PeerApp) listen() {
pa.version.Store(Version2)
}
} else {
if cm, err := model.DeserializeMessage(string(packet.Data)); err == nil {
if cm.TransitTime != nil {
rt := time.Now().UTC()
cm.RecvTime = &rt
data, _ := json.Marshal(cm)
packet.Data = data
}
}
pa.MessageHandler(pa.connection.Hostname(), packet.ID, packet.Context, packet.Data)
}
}
@ -148,6 +158,15 @@ func (pa *PeerApp) SendMessage(message model2.PeerMessage) error {
var serialized []byte
var err error
if cm, err := model.DeserializeMessage(string(message.Data)); err == nil {
if cm.SendTime != nil {
tt := time.Now().UTC()
cm.TransitTime = &tt
data, _ := json.Marshal(cm)
message.Data = data
}
}
if pa.version.Load() == Version2 {
// treat data as a pre-serialized string, not as a byte array (which will be base64 encoded and bloat the packet size)
serialized = message.Serialize()

View File

@ -35,6 +35,7 @@ type GlobalSettings struct {
Locale string
Theme string
ThemeMode string
ThemeImages bool
PreviousPid int64
ExperimentsEnabled bool
Experiments map[string]bool
@ -62,7 +63,9 @@ type GlobalSettings struct {
var DefaultGlobalSettings = GlobalSettings{
Locale: "en",
Theme: "dark",
Theme: "cwtch",
ThemeMode: "dark",
ThemeImages: false,
PreviousPid: -1,
ExperimentsEnabled: false,
Experiments: map[string]bool{constants.MessageFormattingExperiment: true},

View File

@ -145,10 +145,23 @@ func TestFileSharing(t *testing.T) {
alice.NewContactConversation(bob.GetOnion(), model.DefaultP2PAccessControl(), true)
alice.PeerWithOnion(bob.GetOnion())
json, err := alice.EnhancedGetConversationAccessControlList(1)
if err != nil {
t.Fatalf("Error!: %v", err)
}
t.Logf("alice<->bob ACL: %s", json)
t.Logf("Waiting for alice and Bob to peer...")
waitForPeerPeerConnection(t, alice, bob)
alice.AcceptConversation(1)
bob.AcceptConversation(1)
err = alice.AcceptConversation(1)
if err != nil {
t.Fatalf("Error!: %v", err)
}
err = bob.AcceptConversation(1)
if err != nil {
t.Fatalf("Error!: %v", err)
}
t.Logf("Alice and Bob are Connected!!")
filesharingFunctionality := filesharing.FunctionalityGate()

View File

@ -58,7 +58,7 @@ func TestFileSharing(t *testing.T) {
os.RemoveAll("cwtch.out.png")
os.RemoveAll("cwtch.out.png.manifest")
log.SetLevel(log.LevelInfo)
log.SetLevel(log.LevelDebug)
log.ExcludeFromPattern("tapir")
os.Mkdir("tordir", 0700)
@ -151,13 +151,6 @@ func TestFileSharing(t *testing.T) {
bob.NewContactConversation(alice.GetOnion(), model.DefaultP2PAccessControl(), true)
alice.NewContactConversation(bob.GetOnion(), model.DefaultP2PAccessControl(), true)
alice.PeerWithOnion(bob.GetOnion())
t.Logf("Waiting for alice and Bob to peer...")
waitForPeerPeerConnection(t, alice, bob)
alice.AcceptConversation(1)
t.Logf("Alice and Bob are Connected!!")
filesharingFunctionality := filesharing.FunctionalityGate()
@ -167,10 +160,10 @@ func TestFileSharing(t *testing.T) {
}
alice.SendMessage(1, fileSharingMessage)
bob.AcceptConversation(1)
// Wait for the messages to arrive...
time.Sleep(time.Second * 10)
// Ok this is fun...we just Sent a Message we may not have a connection yet...
// so this test will only pass if sending offline works...
waitForPeerPeerConnection(t, bob, alice)
bob.SendMessage(1, "this is a test message")
bob.SendMessage(1, "this is another test message")