Compare commits

..

No commits in common. "master" and "font-setting" have entirely different histories.

49 changed files with 450 additions and 2526 deletions

View File

@ -5,29 +5,27 @@ name: linux-test
steps:
- name: fetch
image: golang:1.21.5
image: golang:1.19.1
volumes:
- name: deps
path: /go
commands:
- go install honnef.co/go/tools/cmd/staticcheck@latest
- go install go.uber.org/nilaway/cmd/nilaway@latest
- wget https://git.openprivacy.ca/openprivacy/buildfiles/raw/branch/master/tor/tor-0.4.8.9-linux-x86_64.tar.gz -O tor.tar.gz
- tar -xzf tor.tar.gz
- chmod a+x Tor/tor
- export PATH=$PWD/Tor/:$PATH
- export LD_LIBRARY_PATH=$PWD/Tor/
- tor --version
- wget https://git.openprivacy.ca/openprivacy/buildfiles/raw/master/tor/tor
- wget https://git.openprivacy.ca/openprivacy/buildfiles/raw/master/tor/torrc
- chmod a+x tor
- go get -u golang.org/x/lint/golint
- export GO111MODULE=on
- go mod vendor
- name: quality
image: golang:1.21.5
image: golang:1.19.1
volumes:
- name: deps
path: /go
commands:
- ./testing/quality.sh
- staticcheck ./...
- name: units-tests
image: golang:1.21.5
image: golang:1.19.1
volumes:
- name: deps
path: /go
@ -35,32 +33,28 @@ steps:
- export PATH=`pwd`:$PATH
- sh testing/tests.sh
- name: integ-test
image: golang:1.21.5
image: golang:1.19.1
volumes:
- name: deps
path: /go
commands:
- export PATH=$PWD/Tor/:$PATH
- export LD_LIBRARY_PATH=$PWD/Tor/
- tor --version
- export PATH=`pwd`:$PATH
- go test -timeout=30m -race -v cwtch.im/cwtch/testing/
- name: filesharing-integ-test
image: golang:1.21.5
image: golang:1.19.1
volumes:
- name: deps
path: /go
commands:
- export PATH=$PWD/Tor/:$PATH
- export LD_LIBRARY_PATH=$PWD/Tor/
- export PATH=`pwd`:$PATH
- go test -timeout=20m -race -v cwtch.im/cwtch/testing/filesharing
- name: filesharing-autodownload-integ-test
image: golang:1.21.5
image: golang:1.19.1
volumes:
- name: deps
path: /go
commands:
- export PATH=$PWD/Tor/:$PATH
- export LD_LIBRARY_PATH=$PWD/Tor/
- export PATH=`pwd`:$PATH
- go test -timeout=20m -race -v cwtch.im/cwtch/testing/autodownload
- name: notify-gogs
image: openpriv/drone-gogs

2
.gitignore vendored
View File

@ -34,5 +34,3 @@ tokens
tordir/
testing/autodownload/download_dir
testing/autodownload/storage
*.swp
testing/managerstorage/*

View File

@ -1,17 +1,10 @@
package app
import (
"os"
path "path/filepath"
"strconv"
"sync"
"cwtch.im/cwtch/app/plugins"
"cwtch.im/cwtch/event"
"cwtch.im/cwtch/extensions"
"cwtch.im/cwtch/functionality/filesharing"
"cwtch.im/cwtch/functionality/hybrid"
"cwtch.im/cwtch/functionality/servers"
"cwtch.im/cwtch/model"
"cwtch.im/cwtch/model/attr"
"cwtch.im/cwtch/model/constants"
@ -21,6 +14,10 @@ import (
"cwtch.im/cwtch/storage"
"git.openprivacy.ca/openprivacy/connectivity"
"git.openprivacy.ca/openprivacy/log"
"os"
path "path/filepath"
"strconv"
"sync"
)
type application struct {
@ -65,7 +62,7 @@ type Application interface {
QueryACNStatus()
QueryACNVersion()
ConfigureConnections(onion string, doListn, doPeers, doServers bool)
ActivateEngines(doListn, doPeers, doServers bool)
ActivatePeerEngine(onion string)
DeactivatePeerEngine(onion string)
@ -219,7 +216,7 @@ func (app *application) setupPeer(profile peer.CwtchPeer) {
// Initialize the Peer with the Given Event Bus
app.peers[profile.GetOnion()] = profile
profile.Init(eventBus)
profile.Init(app.eventBuses[profile.GetOnion()])
// Update the Peer with the Most Recent Experiment State...
globalSettings := app.settings.ReadGlobalSettings()
@ -261,8 +258,7 @@ func (app *application) DeleteProfile(onion string, password string) {
defer app.appmutex.Unlock()
// short circuit to prevent nil-pointer panic if this function is called twice (or incorrectly)
peer := app.peers[onion]
if peer == nil {
if app.peers[onion] == nil {
log.Errorf("shutdownPeer called with invalid onion %v", onion)
return
}
@ -272,11 +268,11 @@ func (app *application) DeleteProfile(onion string, password string) {
password = DefactoPasswordForUnencryptedProfiles
}
if peer.CheckPassword(password) {
if app.peers[onion].CheckPassword(password) {
// soft-shutdown
peer.Shutdown()
app.peers[onion].Shutdown()
// delete the underlying storage
peer.Delete()
app.peers[onion].Delete()
// hard shutdown / remove from app
app.shutdownPeer(onion)
@ -344,7 +340,6 @@ func (app *application) LoadProfiles(password string) {
cps, err := peer.CreateEncryptedStore(profileDirectory, password)
if err != nil {
log.Errorf("error creating encrypted store: %v", err)
continue
}
profile := peer.ImportLegacyProfile(legacyProfile, cps)
loaded = app.installProfile(profile)
@ -365,12 +360,8 @@ 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))
profile.RegisterHook(new(hybrid.ManagedGroupFunctionality))
profile.RegisterHook(new(hybrid.GroupManagerFunctionality)) // will only be activated if GroupManagerExperiment is enabled...
// Ensure that Profiles have the Most Up to Date Settings...
profile.NotifySettingsUpdate(app.settings.ReadGlobalSettings())
}
@ -392,62 +383,45 @@ func (app *application) installProfile(profile peer.CwtchPeer) bool {
return false
}
// ActivateEngines launches all peer engines
func (app *application) ActivateEngines(doListen, doPeers, doServers bool) {
log.Debugf("ActivateEngines")
for _, profile := range app.peers {
app.engines[profile.GetOnion()], _ = profile.GenerateProtocolEngine(app.acn, app.eventBuses[profile.GetOnion()], app.engineHooks)
app.eventBuses[profile.GetOnion()].Publish(event.NewEventList(event.ProtocolEngineCreated))
}
app.QueryACNStatus()
if doListen {
for _, profile := range app.peers {
log.Debugf(" Listen for %v", profile.GetOnion())
profile.Listen()
}
}
if doPeers || doServers {
for _, profile := range app.peers {
log.Debugf(" Start Connections for %v doPeers:%v doServers:%v", profile.GetOnion(), doPeers, doServers)
profile.StartConnections(doPeers, doServers)
}
}
}
// ActivatePeerEngine creates a peer engine for use with an ACN, should be called once the underlying ACN is online
func (app *application) ActivatePeerEngine(onion string) {
profile := app.GetPeer(onion)
if profile != nil {
app.appmutex.Lock()
if _, exists := app.engines[onion]; !exists {
eventBus, exists := app.eventBuses[profile.GetOnion()]
app.engines[profile.GetOnion()], _ = profile.GenerateProtocolEngine(app.acn, app.eventBuses[profile.GetOnion()], app.engineHooks)
if !exists {
// todo handle this case?
log.Errorf("cannot activate peer engine without an event bus")
app.appmutex.Unlock()
return
}
engine, err := profile.GenerateProtocolEngine(app.acn, eventBus, app.engineHooks)
if err == nil {
log.Debugf("restartFlow: Creating a New Protocol Engine...")
app.engines[profile.GetOnion()] = engine
eventBus.Publish(event.NewEventList(event.ProtocolEngineCreated))
} else {
log.Errorf("corrupted profile detected for %v", onion)
}
}
app.appmutex.Unlock()
}
app.eventBuses[profile.GetOnion()].Publish(event.NewEventList(event.ProtocolEngineCreated))
app.QueryACNStatus()
}
// ConfigureConnections autostarts the given kinds of connections.
func (app *application) ConfigureConnections(onion string, listen bool, peers bool, servers bool) {
profile := app.GetPeer(onion)
if profile != nil {
app.appmutex.Lock()
profileBus, exists := app.eventBuses[profile.GetOnion()]
app.appmutex.Unlock()
if exists {
// if we are making a decision to ignore
if !peers || !servers {
profileBus.Publish(event.NewEventList(event.PurgeRetries))
}
// enable the engine if it doesn't exist...
// note: this function is idempotent
app.ActivatePeerEngine(onion)
if listen {
if true {
profile.Listen()
}
profileBus.Publish(event.NewEventList(event.ResumeRetries))
// do this in the background, for large contact lists it can take a long time...
go profile.StartConnections(peers, servers)
profile.StartConnections(true, true)
}
} else {
log.Errorf("profile does not exist %v", onion)
}
}
@ -518,18 +492,10 @@ func (app *application) eventHandler() {
profile := app.GetPeer(onion)
if profile != nil {
autostart, exists := profile.GetScopedZonedAttribute(attr.LocalScope, attr.ProfileZone, constants.PeerAutostart)
appearOffline, appearOfflineExists := profile.GetScopedZonedAttribute(attr.LocalScope, attr.ProfileZone, constants.PeerAppearOffline)
if !exists || autostart == "true" {
if appearOfflineExists && appearOffline == "true" {
// don't configure any connections...
log.Infof("peer appearing offline, not launching listen threads or connecting jobs")
app.ConfigureConnections(onion, false, false, false)
} else {
app.ConfigureConnections(onion, true, true, true)
app.ActivatePeerEngine(onion)
}
}
}
}
}
} else {
@ -560,26 +526,21 @@ func (app *application) ShutdownPeer(onion string) {
}
// shutdownPeer mutex unlocked helper shutdown peer
//
//nolint:nilaway
func (app *application) shutdownPeer(onion string) {
// short circuit to prevent nil-pointer panic if this function is called twice (or incorrectly)
onionEventBus := app.eventBuses[onion]
onionPeer := app.peers[onion]
if onionEventBus == nil || onionPeer == nil {
if app.eventBuses[onion] == nil || app.peers[onion] == nil {
log.Errorf("shutdownPeer called with invalid onion %v", onion)
return
}
// we are an internal locked method, app.eventBuses[onion] cannot fail...
onionEventBus.Publish(event.NewEventList(event.ShutdownPeer, event.Identity, onion))
onionEventBus.Shutdown()
app.eventBuses[onion].Publish(event.NewEventList(event.ShutdownPeer, event.Identity, onion))
app.eventBuses[onion].Shutdown()
delete(app.eventBuses, onion)
onionPeer.Shutdown()
app.peers[onion].Shutdown()
delete(app.peers, onion)
if onionEngine, ok := app.engines[onion]; ok {
onionEngine.Shutdown()
if _, ok := app.engines[onion]; ok {
app.engines[onion].Shutdown()
delete(app.engines, onion)
}
log.Debugf("shutting down plugins for %v", onion)

View File

@ -3,7 +3,6 @@ package plugins
import (
"cwtch.im/cwtch/event"
"cwtch.im/cwtch/protocol/connections"
"git.openprivacy.ca/openprivacy/connectivity/tor"
"git.openprivacy.ca/openprivacy/log"
"math"
"strconv"
@ -105,10 +104,6 @@ func (cq *connectionQueue) dequeue() *contact {
return c
}
func (cq *connectionQueue) len() int {
return len(cq.queue)
}
type contactRetry struct {
bus event.Manager
queue event.Queue
@ -122,15 +117,14 @@ type contactRetry struct {
acnProgress int
connections sync.Map //[string]*contact
connCount int
pendingQueue *connectionQueue
priorityQueue *connectionQueue
authorizedPeers sync.Map
stallRetries bool
}
// NewConnectionRetry returns a Plugin that when started will retry connecting to contacts with a failedCount timing
func NewConnectionRetry(bus event.Manager, onion string) Plugin {
cr := &contactRetry{bus: bus, queue: event.NewQueue(), breakChan: make(chan bool, 1), authorizedPeers: sync.Map{}, connections: sync.Map{}, stallRetries: true, ACNUp: false, ACNUpTime: time.Now(), protocolEngine: false, onion: onion, pendingQueue: newConnectionQueue(), priorityQueue: newConnectionQueue()}
cr := &contactRetry{bus: bus, queue: event.NewQueue(), breakChan: make(chan bool, 1), connections: sync.Map{}, connCount: 0, ACNUp: false, ACNUpTime: time.Now(), protocolEngine: false, onion: onion, pendingQueue: newConnectionQueue(), priorityQueue: newConnectionQueue()}
return cr
}
@ -178,21 +172,14 @@ func (cr *contactRetry) run() {
cr.bus.Subscribe(event.ServerStateChange, cr.queue)
cr.bus.Subscribe(event.QueuePeerRequest, cr.queue)
cr.bus.Subscribe(event.QueueJoinServer, cr.queue)
cr.bus.Subscribe(event.DisconnectPeerRequest, cr.queue)
cr.bus.Subscribe(event.DisconnectServerRequest, cr.queue)
cr.bus.Subscribe(event.ProtocolEngineShutdown, cr.queue)
cr.bus.Subscribe(event.ProtocolEngineCreated, cr.queue)
cr.bus.Subscribe(event.DeleteContact, cr.queue)
cr.bus.Subscribe(event.UpdateConversationAuthorization, cr.queue)
cr.bus.Subscribe(event.PurgeRetries, cr.queue)
cr.bus.Subscribe(event.ResumeRetries, cr.queue)
for {
// Only attempt connection if both the ACN and the Protocol Engines are Online...
log.Debugf("restartFlow checking state")
if cr.ACNUp && cr.protocolEngine && !cr.stallRetries {
log.Debugf("restartFlow time to queue!!")
if cr.ACNUp {
cr.requeueReady()
connectingCount := cr.connectingCount()
log.Debugf("checking queues (priority len: %v) (pending len: %v) of total conns watched: %v, with current connecingCount: %v", len(cr.priorityQueue.queue), len(cr.pendingQueue.queue), cr.connCount, connectingCount)
// do priority connections first...
for connectingCount < cr.maxTorCircuitsPending() && len(cr.priorityQueue.queue) > 0 {
@ -220,67 +207,20 @@ func (cr *contactRetry) run() {
}
cr.lastCheck = time.Now()
}
// regardless of if we're up, run manual force deconnectiong of timed out connections
cr.connections.Range(func(k, v interface{}) bool {
p := v.(*contact)
if p.state == connections.CONNECTING && time.Since(p.lastAttempt) > time.Duration(circuitTimeoutSecs)*time.Second*2 {
// we have been "connecting" for twice the circuttimeout so it's failed, we just didn't learn about it, manually disconnect
cr.handleEvent(p.id, connections.DISCONNECTED, p.ctype)
log.Errorf("had to manually set peer %v of profile %v to DISCONNECTED due to assumed circuit timeout (%v) seconds", p.id, cr.onion, circuitTimeoutSecs*2)
}
return true
})
select {
case e := <-cr.queue.OutChan():
switch e.EventType {
case event.PurgeRetries:
// Purge All Authorized Peers
cr.authorizedPeers.Range(func(key interface{}, value interface{}) bool {
cr.authorizedPeers.Delete(key)
return true
})
// Purge All Connection States
cr.connections.Range(func(key interface{}, value interface{}) bool {
cr.connections.Delete(key)
return true
})
case event.ResumeRetries:
log.Infof("resuming retries...")
cr.stallRetries = false
case event.DisconnectPeerRequest:
peer := e.Data[event.RemotePeer]
cr.authorizedPeers.Delete(peer)
case event.DisconnectServerRequest:
peer := e.Data[event.GroupServer]
cr.authorizedPeers.Delete(peer)
case event.DeleteContact:
// this case covers both servers and peers (servers are peers, and go through the
// same delete conversation flow)
peer := e.Data[event.RemotePeer]
cr.authorizedPeers.Delete(peer)
case event.UpdateConversationAuthorization:
// if we update the conversation authorization then we need to check if
// we need to remove blocked conversations from the regular flow.
peer := e.Data[event.RemotePeer]
blocked := e.Data[event.Blocked]
if blocked == "true" {
cr.authorizedPeers.Delete(peer)
}
case event.PeerStateChange:
state := connections.ConnectionStateToType()[e.Data[event.ConnectionState]]
peer := e.Data[event.RemotePeer]
// only handle state change events from pre-authorized peers;
if _, exists := cr.authorizedPeers.Load(peer); exists {
cr.handleEvent(peer, state, peerConn)
}
case event.ServerStateChange:
state := connections.ConnectionStateToType()[e.Data[event.ConnectionState]]
server := e.Data[event.GroupServer]
// only handle state change events from pre-authorized servers;
if _, exists := cr.authorizedPeers.Load(server); exists {
cr.handleEvent(server, state, serverConn)
}
case event.QueueJoinServer:
fallthrough
case event.QueuePeerRequest:
@ -297,12 +237,11 @@ func (cr *contactRetry) run() {
id = server
cr.addConnection(server, connections.DISCONNECTED, serverConn, lastSeen)
}
// this was an authorized event, and so we store this peer.
log.Debugf("authorizing id: %v", id)
cr.authorizedPeers.Store(id, true)
if c, ok := cr.connections.Load(id); ok {
contact := c.(*contact)
if contact.state == connections.DISCONNECTED {
if contact.state == connections.DISCONNECTED && !contact.queued {
// prioritize connections made in the last week
if time.Since(contact.lastSeen).Hours() < PriorityQueueTimeSinceQualifierHours {
cr.priorityQueue.insert(contact)
@ -315,7 +254,7 @@ func (cr *contactRetry) run() {
case event.ProtocolEngineShutdown:
cr.ACNUp = false
cr.protocolEngine = false
cr.stallRetries = true
cr.connections.Range(func(k, v interface{}) bool {
p := v.(*contact)
if p.state == connections.AUTHENTICATED || p.state == connections.SYNCED {
@ -353,43 +292,15 @@ func (cr *contactRetry) processStatus() {
return
}
if cr.acnProgress == 100 && !cr.ACNUp {
// ACN is up...at this point we need to completely reset our state
// as there is no guarantee that the tor daemon shares our state anymore...
cr.ACNUp = true
cr.ACNUpTime = time.Now()
// reset all of the queues...
cr.priorityQueue = newConnectionQueue()
cr.pendingQueue = newConnectionQueue()
// Loop through connections. Reset state, and requeue...
cr.connections.Range(func(k, v interface{}) bool {
p := v.(*contact)
// only reload connections if they are on the authorized peers list
if _, exists := cr.authorizedPeers.Load(p.id); exists {
p.queued = true
// prioritize connections made recently...
log.Debugf("adding %v to queue", p.id)
if time.Since(p.lastSeen).Hours() < PriorityQueueTimeSinceQualifierHours {
cr.priorityQueue.insert(p)
} else {
cr.pendingQueue.insert(p)
}
}
return true
})
} else if cr.acnProgress != 100 {
cr.ACNUp = false
cr.connections.Range(func(k, v interface{}) bool {
p := v.(*contact)
p.failedCount = 0
p.queued = false
p.state = connections.DISCONNECTED
return true
})
} else if cr.acnProgress != 100 {
cr.ACNUp = false
}
}
@ -400,14 +311,8 @@ func (cr *contactRetry) requeueReady() {
var retryable []*contact
throughPutPerMin := int((float64(cr.maxTorCircuitsPending()) / float64(circuitTimeoutSecs)) * 60.0)
queueCount := cr.priorityQueue.len() + cr.pendingQueue.len()
// adjustedBaseTimeout = basetimeoust * (queuedItemsCount / throughPutPerMin)
// when less items are queued than through put it'll lower adjustedBaseTimeOut, but that'll be reset in the next block
// when more items are queued it will increase the timeout, to a max of MaxBaseTimeoutSec (enforced in the next block)
adjustedBaseTimeout := circuitTimeoutSecs * (queueCount / throughPutPerMin)
// circuitTimeoutSecs (120s) < adjustedBaseTimeout < MaxBaseTimeoutSec (300s)
throughPutPerMin := cr.maxTorCircuitsPending() / (circuitTimeoutSecs / 60)
adjustedBaseTimeout := cr.connCount / throughPutPerMin * 60
if adjustedBaseTimeout < circuitTimeoutSecs {
adjustedBaseTimeout = circuitTimeoutSecs
} else if adjustedBaseTimeout > MaxBaseTimeoutSec {
@ -416,16 +321,12 @@ func (cr *contactRetry) requeueReady() {
cr.connections.Range(func(k, v interface{}) bool {
p := v.(*contact)
// Don't retry anyone who isn't on the authorized peers list
if _, exists := cr.authorizedPeers.Load(p.id); exists {
if p.state == connections.DISCONNECTED && !p.queued {
timeout := time.Duration((math.Pow(2, float64(p.failedCount)))*float64(adjustedBaseTimeout /*baseTimeoutSec*/)) * time.Second
if time.Since(p.lastAttempt) > timeout {
retryable = append(retryable, p)
}
}
}
return true
})
for _, contact := range retryable {
@ -438,7 +339,6 @@ func (cr *contactRetry) requeueReady() {
}
func (cr *contactRetry) publishConnectionRequest(contact *contact) {
log.Debugf("RestartFlow Publish Connection Request listener %v", contact)
if contact.ctype == peerConn {
cr.bus.Publish(event.NewEvent(event.PeerRequest, map[event.Field]string{event.RemotePeer: contact.id}))
}
@ -458,12 +358,14 @@ func (cr *contactRetry) addConnection(id string, state connections.ConnectionSta
if _, exists := cr.connections.Load(id); !exists {
p := &contact{id: id, state: state, failedCount: 0, lastAttempt: event.CwtchEpoch, ctype: ctype, lastSeen: lastSeen, queued: false}
cr.connections.Store(id, p)
cr.connCount += 1
return
} else {
// we have rerequested this connnection, probably via an explicit ask, update it's state
if c, ok := cr.connections.Load(id); ok {
contact := c.(*contact)
contact.state = state
// we have rerequested this connnection. Force set the queued parameter to true.
p, _ := cr.connections.Load(id)
if !p.(*contact).queued {
p.(*contact).queued = true
cr.connCount += 1
}
}
}
@ -476,12 +378,6 @@ func (cr *contactRetry) handleEvent(id string, state connections.ConnectionState
return
}
// reject events that contain invalid hostnames...we cannot connect to them
// and they could result in spurious connection attempts...
if !tor.IsValidHostname(id) {
return
}
if _, exists := cr.connections.Load(id); !exists {
// We have an event for something we don't know about...
// The only reason this should happen is if a *new* Peer/Server connection has changed.

View File

@ -14,48 +14,39 @@ import (
// We are invasively checking the internal state of the retry plugin and accessing pointers from another
// thread.
// We could build an entire thread safe monitoring functonality, but that would dramatically expand the scope of this test.
func TestContactRetryQueue(t *testing.T) {
log.SetLevel(log.LevelDebug)
bus := event.NewEventManager()
cr := NewConnectionRetry(bus, "").(*contactRetry)
cr.ACNUp = true // fake an ACN connection...
cr.protocolEngine = true // fake protocol engine
cr.stallRetries = false // fake not being in offline mode...
go cr.run()
testOnion := "2wgvbza2mbuc72a4u6r6k4hc2blcvrmk4q26bfvlwbqxv2yq5k52fcqd"
t.Logf("contact plugin up and running..sending peer connection...")
// Assert that there is a peer connection identified as "test"
bus.Publish(event.NewEvent(event.QueuePeerRequest, map[event.Field]string{event.RemotePeer: testOnion, event.LastSeen: "test"}))
bus.Publish(event.NewEvent(event.QueuePeerRequest, map[event.Field]string{event.RemotePeer: "test", event.LastSeen: "test"}))
// Wait until the test actually exists, and is queued
// This is the worst part of this test setup. Ideally we would sleep, or some other yielding, but
// go test scheduling doesn't like that and even sleeping long periods won't cause the event thread to make
// progress...
setup := false
for !setup {
if _, exists := cr.connections.Load(testOnion); exists {
if _, exists := cr.authorizedPeers.Load(testOnion); exists {
t.Logf("authorized")
setup = true
for {
if pinf, exists := cr.connections.Load("test"); exists {
if pinf.(*contact).queued {
break
}
}
}
// We should very quickly become connecting...
time.Sleep(time.Second)
pinf, _ := cr.connections.Load(testOnion)
if pinf.(*contact).state != 1 {
t.Fatalf("test connection should be in connecting after update, actually: %v", pinf.(*contact).state)
pinf, _ := cr.connections.Load("test")
if pinf.(*contact).queued == false {
t.Fatalf("test connection should be queued, actually: %v", pinf.(*contact).queued)
}
// Asset that "test" is authenticated
cr.handleEvent(testOnion, connections.AUTHENTICATED, peerConn)
cr.handleEvent("test", connections.AUTHENTICATED, peerConn)
// Assert that "test has a valid state"
pinf, _ = cr.connections.Load(testOnion)
pinf, _ = cr.connections.Load("test")
if pinf.(*contact).state != 3 {
t.Fatalf("test connection should be in authenticated after update, actually: %v", pinf.(*contact).state)
}
@ -63,66 +54,20 @@ func TestContactRetryQueue(t *testing.T) {
// Publish an unrelated event to trigger the Plugin to go through a queuing cycle
// If we didn't do this we would have to wait 30 seconds for a check-in
bus.Publish(event.NewEvent(event.PeerStateChange, map[event.Field]string{event.RemotePeer: "test2", event.ConnectionState: "Disconnected"}))
bus.Publish(event.NewEvent(event.QueuePeerRequest, map[event.Field]string{event.RemotePeer: testOnion, event.LastSeen: time.Now().Format(time.RFC3339Nano)}))
time.Sleep(time.Second)
pinf, _ = cr.connections.Load(testOnion)
if pinf.(*contact).state != 1 {
t.Fatalf("test connection should be in connecting after update, actually: %v", pinf.(*contact).state)
if pinf.(*contact).queued != false {
t.Fatalf("test connection should not be queued, actually: %v", pinf.(*contact).queued)
}
// Publish a new peer request...
bus.Publish(event.NewEvent(event.QueuePeerRequest, map[event.Field]string{event.RemotePeer: "test"}))
time.Sleep(time.Second) // yield for a second so the event can catch up...
// Peer test should be forced to queue....
pinf, _ = cr.connections.Load("test")
if pinf.(*contact).queued != true {
t.Fatalf("test connection should be forced to queue after new queue peer request")
}
cr.Shutdown()
}
// Takes around 4 min unless you adjust the consts for tickTimeSec and circuitTimeoutSecs
/*
func TestRetryEmission(t *testing.T) {
log.SetLevel(log.LevelDebug)
log.Infof("*** Starting TestRetryEmission! ***")
bus := event.NewEventManager()
testQueue := event.NewQueue()
bus.Subscribe(event.PeerRequest, testQueue)
cr := NewConnectionRetry(bus, "").(*contactRetry)
cr.Start()
time.Sleep(100 * time.Millisecond)
bus.Publish(event.NewEventList(event.ACNStatus, event.Progress, "100"))
bus.Publish(event.NewEventList(event.ProtocolEngineCreated))
pub, _, _ := ed25519.GenerateKey(rand.Reader)
peerAddr := tor.GetTorV3Hostname(pub)
bus.Publish(event.NewEventList(event.QueuePeerRequest, event.RemotePeer, peerAddr, event.LastSeen, time.Now().Format(time.RFC3339Nano)))
log.Infof("Fetching 1st event")
ev := testQueue.Next()
if ev.EventType != event.PeerRequest {
t.Errorf("1st event emitted was %v, expected %v", ev.EventType, event.PeerRequest)
}
log.Infof("1st event: %v", ev)
bus.Publish(event.NewEventList(event.PeerStateChange, event.RemotePeer, peerAddr, event.ConnectionState, connections.ConnectionStateName[connections.DISCONNECTED]))
log.Infof("fetching 2nd event")
ev = testQueue.Next()
log.Infof("2nd event: %v", ev)
if ev.EventType != event.PeerRequest {
t.Errorf("2nd event emitted was %v, expected %v", ev.EventType, event.PeerRequest)
}
bus.Publish(event.NewEventList(event.PeerStateChange, event.RemotePeer, peerAddr, event.ConnectionState, connections.ConnectionStateName[connections.CONNECTED]))
time.Sleep(100 * time.Millisecond)
bus.Publish(event.NewEventList(event.PeerStateChange, event.RemotePeer, peerAddr, event.ConnectionState, connections.ConnectionStateName[connections.DISCONNECTED]))
log.Infof("fetching 3rd event")
ev = testQueue.Next()
log.Infof("3nd event: %v", ev)
if ev.EventType != event.PeerRequest {
t.Errorf("3nd event emitted was %v, expected %v", ev.EventType, event.PeerRequest)
}
cr.Shutdown()
}
*/

View File

@ -15,9 +15,6 @@ func WaitGetPeer(app Application, name string) peer.CwtchPeer {
for {
for _, handle := range app.ListProfiles() {
peer := app.GetPeer(handle)
if peer == nil {
continue
}
localName, _ := peer.GetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants.Name)
if localName == name {
return peer

View File

@ -25,15 +25,6 @@ const (
// GroupServer
QueuePeerRequest = Type("QueuePeerRequest")
// Disconnect*Request
// Close active connections and prevent new connections
DisconnectPeerRequest = Type("DisconnectPeerRequest")
DisconnectServerRequest = Type("DisconnectServerRequest")
// Events to Manage Retry Contacts
PurgeRetries = Type("PurgeRetries")
ResumeRetries = Type("ResumeRetries")
// RetryServerRequest
// Asks CwtchPeer to retry a server connection...
// GroupServer: [eg "chpr7qm6op5vfcg2pi4vllco3h6aa7exexc4rqwnlupqhoogx2zgd6qd"
@ -226,10 +217,6 @@ const (
// Heartbeat is used to trigger actions that need to happen every so often...
Heartbeat = Type("Heartbeat")
// Conversation Search
SearchResult = Type("SearchResult")
SearchCancelled = Type("SearchCancelled")
)
// Field defines common event attributes
@ -284,9 +271,7 @@ const (
Status = Field("Status")
EventID = Field("EventID")
EventContext = Field("EventContext")
Channel = Field("Channel")
Index = Field("Index")
RowIndex = Field("RowIndex")
ContentHash = Field("ContentHash")
// Handle denotes a contact handle of any type.
@ -313,8 +298,6 @@ const (
FilePath = Field("FilePath")
FileDownloadFinished = Field("FileDownloadFinished")
NameSuggestion = Field("NameSuggestion")
SearchID = Field("SearchID")
)
// Defining Common errors
@ -337,25 +320,19 @@ const (
ContextSendFile = "im.cwtch.file.send.chunk"
)
// Define Attribute Keys related to history preservation
// Define Default Attribute Keys
const (
PreserveHistoryDefaultSettingKey = "SaveHistoryDefault" // profile level default
SaveHistoryKey = "SavePeerHistory" // peer level setting
SaveHistoryKey = "SavePeerHistory"
)
// Define Default Attribute Values
const (
// Save History has 3 distinct states. By default we refer to the profile level
// attribute PreserveHistoryDefaultSettingKey ( default: false i.e. DefaultDeleteHistory),
// For each contact, if the profile owner confirms deletion we change to DeleteHistoryConfirmed,
// if the profile owner confirms they want to save history then this becomes SaveHistoryConfirmed
// These settings are set at the UI level using Get/SetScopeZoneAttribute with scoped zone: local.profile.*
// Save History has 3 distinct states. By default we don't save history (DefaultDeleteHistory), if the peer confirms this
// we change to DeleteHistoryConfirmed, if they confirm they want to save then this becomes SaveHistoryConfirmed
// We use this distinction between default and confirmed to drive UI
DeleteHistoryDefault = "DefaultDeleteHistory"
SaveHistoryConfirmed = "SaveHistory"
DeleteHistoryConfirmed = "DeleteHistoryConfirmed"
// NOTE: While this says "[DeleteHistory]Default", The actual behaviour will now depend on the
// global app/profile value of PreserveHistoryDefaultSettingKey
DeleteHistoryDefault = "DefaultDeleteHistory"
)
// Bool strings

View File

@ -46,8 +46,6 @@ func NewEventList(eventType Type, args ...interface{}) Event {
val, vok := args[i+1].(string)
if kok && vok {
data[key] = val
} else {
log.Errorf("attempted to send a field that could not be parsed to a string: %v %v", args[i], args[i+1])
}
}
return Event{EventType: eventType, EventID: GetRandNumber().String(), Data: data}
@ -95,11 +93,6 @@ 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

@ -1,4 +1,3 @@
// nolint:nilaway - the infiniteBuffer function causes issues with static analysis because it is very unidomatic.
package event
/*

View File

@ -104,15 +104,6 @@ func (pne ProfileValueExtension) OnContactRequestValue(profile peer.CwtchPeer, c
val, exists = profile.GetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants.Name)
}
// NOTE: Cwtch 1.15+ requires that profiles be able to restrict file downloading to specific contacts. As such we need an ACL check here
// on the fileshareing zone.
// TODO: Split this functionality into FilesharingFunctionality, and restrict this function to only considering Profile zoned attributes?
if zone == attr.FilesharingZone {
if !conversation.GetPeerAC().ShareFiles {
return
}
}
// Construct a Response
resp := event.NewEvent(event.SendRetValMessageToPeer, map[event.Field]string{event.ConversationID: strconv.Itoa(conversation.ID), event.RemotePeer: conversation.Handle, event.Exists: strconv.FormatBool(exists)})
resp.EventID = eventID

View File

@ -1,91 +0,0 @@
package extensions
import (
"slices"
"strconv"
"time"
"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 {
log.Infof("Sending Offline Messages to %s", ci.Handle)
// Check the last 100 messages, if any of them are pending, then send them now...
messsages, _ := profile.GetMostRecentMessages(ci.ID, constants.CHANNEL_CHAT, 0, uint(100))
slices.Reverse(messsages)
for _, message := range messsages {
if message.Attr[constants.AttrAck] == constants.False {
sent, timeparseerr := time.Parse(time.RFC3339, message.Attr[constants.AttrSentTimestamp])
if timeparseerr != nil {
continue
}
if time.Since(sent) > time.Hour*24*7 {
continue
}
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.Infof("resending message that was sent when peer was offline")
profile.PublishEvent(ev)
}
}
if ci.HasChannel(constants.CHANNEL_MANAGER) {
messsages, _ = profile.GetMostRecentMessages(ci.ID, constants.CHANNEL_MANAGER, 0, uint(100))
slices.Reverse(messsages)
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

@ -45,8 +45,6 @@ func (f *Functionality) ExperimentsToRegister() []string {
func (f *Functionality) OnEvent(ev event.Event, profile peer.CwtchPeer) {
if profile.IsFeatureEnabled(constants.FileSharingExperiment) {
switch ev.EventType {
case event.ProtocolEngineCreated:
f.ReShareFiles(profile)
case event.ManifestReceived:
log.Debugf("Manifest Received Event!: %v", ev)
handle := ev.Data[event.Handle]
@ -195,9 +193,6 @@ func (f *Functionality) VerifyOrResumeDownload(profile peer.CwtchPeer, conversat
// Assert the filename...this is technically not necessary, but is here for completeness
manifest.FileName = downloadfilepath
if manifest.VerifyFile() == nil {
// Send a FileDownloaded Event. Usually when VerifyOrResumeDownload is triggered it's because some UI is awaiting the results of a
// Download.
profile.PublishEvent(event.NewEvent(event.FileDownloaded, map[event.Field]string{event.FileKey: fileKey, event.FilePath: downloadfilepath, event.TempFile: downloadfilepath}))
// File is verified and there is nothing else to do...
return nil
} else {
@ -210,7 +205,7 @@ func (f *Functionality) VerifyOrResumeDownload(profile peer.CwtchPeer, conversat
return errors.New("file download metadata does not exist, or is corrupted")
}
func (f *Functionality) CheckDownloadStatus(profile peer.CwtchPeer, fileKey string) error {
func (f *Functionality) CheckDownloadStatus(profile peer.CwtchPeer, fileKey string) {
path, _ := profile.GetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.path", fileKey))
if value, exists := profile.GetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.complete", fileKey)); exists && value == event.True {
profile.PublishEvent(event.NewEvent(event.FileDownloaded, map[event.Field]string{
@ -229,7 +224,6 @@ func (f *Functionality) CheckDownloadStatus(profile peer.CwtchPeer, fileKey stri
event.FilePath: path,
}))
}
return nil // cannot fail
}
func (f *Functionality) EnhancedShareFile(profile peer.CwtchPeer, conversationID int, sharefilepath string) string {
@ -272,12 +266,6 @@ func (f *Functionality) DownloadFile(profile peer.CwtchPeer, conversationID int,
}
// Don't download files if the download file 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")
}
@ -286,7 +274,7 @@ func (f *Functionality) DownloadFile(profile peer.CwtchPeer, conversationID int,
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)
@ -303,10 +291,9 @@ func (f *Functionality) DownloadFile(profile peer.CwtchPeer, conversationID int,
}
// startFileShare is a private method used to finalize a file share and publish it to the protocol engine for processing.
// if force is set to true, this function will ignore timestamp checks...
func (f *Functionality) startFileShare(profile peer.CwtchPeer, filekey string, manifest string, force bool) error {
func (f *Functionality) startFileShare(profile peer.CwtchPeer, filekey string, manifest string) error {
tsStr, exists := profile.GetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.ts", filekey))
if exists && !force {
if exists {
ts, err := strconv.ParseInt(tsStr, 10, 64)
if err != nil || ts < time.Now().Unix()-2592000 {
log.Errorf("ignoring request to download a file offered more than 30 days ago")
@ -316,22 +303,12 @@ func (f *Functionality) startFileShare(profile peer.CwtchPeer, filekey string, m
// set the filekey status to active
profile.SetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.active", filekey), constants.True)
// reset the timestamp...
profile.SetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.ts", filekey), strconv.FormatInt(time.Now().Unix(), 10))
// share the manifest
profile.PublishEvent(event.NewEvent(event.ShareManifest, map[event.Field]string{event.FileKey: filekey, event.SerializedManifest: manifest}))
return nil
}
// RestartFileShare takes in an existing filekey and, assuming the manifest exists, restarts sharing of the manifest
// by default this function always forces a file share, even if the file has timed out.
func (f *Functionality) RestartFileShare(profile peer.CwtchPeer, filekey string) error {
return f.restartFileShareAdvanced(profile, filekey, true)
}
// RestartFileShareAdvanced takes in an existing filekey and, assuming the manifest exists, restarts sharing of the manifest in addition
// to a set of parameters
func (f *Functionality) restartFileShareAdvanced(profile peer.CwtchPeer, filekey string, force bool) error {
// assert that we are allowed to restart filesharing
if !profile.IsFeatureEnabled(constants.FileSharingExperiment) {
@ -343,7 +320,7 @@ func (f *Functionality) restartFileShareAdvanced(profile peer.CwtchPeer, filekey
if manifestExists {
// everything is in order, so reshare this file with the engine
log.Debugf("restarting file share: %v", filekey)
return f.startFileShare(profile, filekey, manifest, force)
return f.startFileShare(profile, filekey, manifest)
}
return fmt.Errorf("manifest does not exist for filekey: %v", filekey)
}
@ -377,10 +354,12 @@ func (f *Functionality) ReShareFiles(profile peer.CwtchPeer) error {
filekey := strings.Join(keyparts[:2], ".")
sharedFile, err := f.GetFileShareInfo(profile, filekey)
// If we haven't explicitly stopped sharing the file then attempt a reshare
// If we haven't explicitly stopped sharing the file AND
// If fewer than 30 days have passed since we originally shared this file,
// Then attempt to share this file again...
// TODO: In the future this would be the point to change the timestamp and reshare the file...
if err == nil && sharedFile.Active {
// this reshare can fail because we don't force sharing of files older than 30 days...
err := f.restartFileShareAdvanced(profile, filekey, false)
err := f.RestartFileShare(profile, filekey)
if err != nil {
log.Debugf("could not reshare file: %v", err)
}
@ -474,7 +453,7 @@ func (f *Functionality) ShareFile(filepath string, profile peer.CwtchPeer) (stri
profile.SetScopedZonedAttribute(attr.ConversationScope, attr.FilesharingZone, fmt.Sprintf("%s.manifest", key), string(serializedManifest))
profile.SetScopedZonedAttribute(attr.ConversationScope, attr.FilesharingZone, fmt.Sprintf("%s.manifest.size", key), strconv.Itoa(int(math.Ceil(float64(len(serializedManifest)-lenDiff)/float64(files.DefaultChunkSize)))))
err = f.startFileShare(profile, key, string(serializedManifest), false)
err = f.startFileShare(profile, key, string(serializedManifest))
return key, string(wrapperJSON), err
}
@ -576,12 +555,11 @@ func GenerateDownloadPath(basePath, fileName string, overwrite bool) (filePath,
}
// StopFileShare sends a message to the ProtocolEngine to cease sharing a particular file
func (f *Functionality) StopFileShare(profile peer.CwtchPeer, fileKey string) error {
func (f *Functionality) StopFileShare(profile peer.CwtchPeer, fileKey string) {
// Note we do not do a permissions check here, as we are *always* permitted to stop sharing files.
// set the filekey status to inactive
profile.SetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.active", fileKey), constants.False)
profile.PublishEvent(event.NewEvent(event.StopFileShare, map[event.Field]string{event.FileKey: fileKey}))
return nil // cannot fail
}
// StopAllFileShares sends a message to the ProtocolEngine to cease sharing all files

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.GetPeerAC().RenderImages {
if ci.Accepted {
i.handleImagePreviews(profile, &ev, ci.ID, ci.ID)
}
}
case event.NewMessageFromGroup:
ci, err := profile.FetchConversationInfo(ev.Data["RemotePeer"])
if err == nil {
if ci.GetPeerAC().RenderImages {
if ci.Accepted {
i.handleImagePreviews(profile, &ev, ci.ID, ci.ID)
}
}
@ -62,18 +62,10 @@ func (i *ImagePreviewsFunctionality) OnEvent(ev event.Event, profile peer.CwtchP
if err == nil {
for _, ci := range conversations {
if profile.GetPeerState(ci.Handle) == connections.AUTHENTICATED {
// if we have enabled file shares for this contact, then send them our profile image
// NOTE: In the past, Cwtch treated "profile image" as a public file share. As such, anyone with the file key and who is able
// to authenticate with the profile (i.e. non-blocked peers) can download the file (if the global profile images experiment is enabled)
// To better allow for fine-grained permissions (and to support hybrid group permissions), we want to enable per-conversation file
// sharing permissions. As such, profile images are now only shared with contacts with that permission enabled.
// (i.e. all previous accepted contacts, new accepted contacts, and contacts who have this toggle set explictly)
if ci.GetPeerAC().ShareFiles {
profile.SendScopedZonedGetValToContact(ci.ID, attr.PublicScope, attr.ProfileZone, constants.CustomProfileImageKey)
}
}
}
}
case event.ProtocolEngineCreated:
// Now that the Peer Engine is Activated, Reshare Profile Images
key, exists := profile.GetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants.CustomProfileImageKey)
@ -83,9 +75,10 @@ func (i *ImagePreviewsFunctionality) OnEvent(ev event.Event, profile peer.CwtchP
// we reset the profile image here so that it is always available.
profile.SetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.ts", key), strconv.FormatInt(time.Now().Unix(), 10))
log.Debugf("Custom Profile Image: %v %s", key, serializedManifest)
f := Functionality{}
f.RestartFileShare(profile, key)
}
// If file sharing is enabled then reshare all active files...
fsf := FunctionalityGate()
_ = fsf.ReShareFiles(profile)
}
}
}
@ -98,7 +91,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.GetPeerAC().RenderImages {
if conversation.Accepted {
fileKey := value
basepath := i.downloadFolder
fsf := FunctionalityGate()
@ -109,7 +102,6 @@ func (i *ImagePreviewsFunctionality) OnContactReceiveValue(profile peer.CwtchPee
if value, exists := profile.GetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.complete", fileKey)); exists && value == event.True {
if _, err := os.Stat(fp); err == nil {
// file is marked as completed downloaded and exists...
// Note: this will also resend the FileDownloaded event if successful...
if fsf.VerifyOrResumeDownload(profile, conversation.ID, fileKey, constants.ImagePreviewMaxSizeInBytes) == nil {
return
}
@ -131,16 +123,6 @@ 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.
@ -160,7 +142,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

View File

@ -1,106 +0,0 @@
package hybrid
import (
"crypto/ed25519"
"encoding/base32"
"encoding/json"
"fmt"
"strings"
"cwtch.im/cwtch/event"
"cwtch.im/cwtch/model"
"cwtch.im/cwtch/model/attr"
"git.openprivacy.ca/openprivacy/log"
)
const ManagedGroupOpen = "managed-group-open"
type GroupEventType int
const (
MemberGroupIDKey = "member_group_id_key"
MemberMessageIDKey = "member_group_messge_id"
)
const (
AddMember = GroupEventType(0x1000)
RemoveMember = GroupEventType(0x2000)
RotateKey = GroupEventType(0x3000)
NewMessage = GroupEventType(0x4000)
NewClearMessage = GroupEventType(0x5000)
SyncRequest = GroupEventType(0x6000)
)
type ManageGroupEvent struct {
EventType GroupEventType `json:"t"`
Data string `json:"d"` // json encoded data
}
type AddMemberEvent struct {
Handle string `json:"h"`
}
type RemoveMemberEvent struct {
Handle string `json:"h"`
}
type RotateKeyEvent struct {
Key []byte `json:"k"`
}
type NewMessageEvent struct {
EncryptedHybridGroupMessage []byte `json:"m"`
}
type NewClearMessageEvent struct {
HybridGroupMessage HybridGroupMessage `json:"m"`
}
type SyncRequestMessage struct {
// a map of MemberGroupID: MemberMessageID
LastSeen map[int]int `json:"l"`
}
// This file contains code for the Hybrid Group / Managed Group types..
type HybridGroupMessage struct {
Author string `json:"a"` // the authors cwtch address
MemberGroupID uint32 `json:"g"`
MemberMessageID uint32 `json:"m"`
MessageBody string `json:"b"`
Sent uint64 `json:"t"` // milliseconds since epoch
Signature []byte `json:"s"` // of json-encoded content (including empty sig)
}
// AuthenticateMessage returns true if the Author of the message produced the Signature over the message
func AuthenticateMessage(message HybridGroupMessage) bool {
messageCopy := message
messageCopy.Signature = []byte{}
// Otherwise we derive the public key from the sender and check it against that.
decodedPub, err := base32.StdEncoding.DecodeString(strings.ToUpper(message.Author))
if err == nil {
data, err := json.Marshal(messageCopy)
if err == nil && len(decodedPub) >= 32 {
return ed25519.Verify(decodedPub[:32], data, message.Signature)
}
}
return false
}
func CheckACL(handle string, group *model.Conversation) (*model.AccessControl, error) {
if isOpen, exists := group.Attributes[attr.LocalScope.ConstructScopedZonedPath(attr.ConversationZone.ConstructZonedPath(ManagedGroupOpen)).ToString()]; !exists {
return nil, fmt.Errorf("group has not been setup correctly - ManagedGroupOpen does not exist ")
} else if isOpen == event.True {
// We don't need to do a membership check
defaultACL := group.GetPeerAC()
return &defaultACL, nil
}
// If this is a closed group. Check if we have an ACL entry for this member
// If we don't OR that member has been blocked, then close the connection.
if acl, inGroup := group.ACL[handle]; !inGroup || acl.Blocked {
log.Infof("ACL Check Failed: %v %v %v", handle, acl, inGroup)
return nil, fmt.Errorf("peer is not a member of this group")
} else {
return &acl, nil
}
}

View File

@ -1,224 +0,0 @@
// This file contains all code related to how a Group Manager operates over a group.
// Managed groups are canonically controlled by members setting
// the ManageGroup permission in the conversation ACL; allowing the manager to
// take control of how this group is structured, see OnEvent below...
// TODO: This file represents stage 1 of the roll out which de-risks most of the
// integration into cwtch peer, new interfaces, and UI integration
// The following functionality is not yet implemented:
// - group-level encryption
// - key rotation / membership ACL
// Cwtch Hybrid Groups are still very experimental functionality and should
// only be used for testing purposes.
package hybrid
import (
"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"
"encoding/json"
"fmt"
"git.openprivacy.ca/openprivacy/log"
)
// MANAGED_GROUP_HANDLE denotes the nominal name that the managed group is given, for easier handling
// Note: we could use id here, as the managed group should technically always be the first group
// But we don't want to assume that, and also allow conversations to be moved around without
// constantly referring to a magic id.
const MANAGED_GROUP_HANDLE = "managed:000"
type GroupManagerFunctionality struct {
}
func (f *GroupManagerFunctionality) NotifySettingsUpdate(settings settings.GlobalSettings) {
}
func (f *GroupManagerFunctionality) EventsToRegister() []event.Type {
return []event.Type{event.PeerStateChange, event.NewMessageFromPeerEngine}
}
func (f *GroupManagerFunctionality) ExperimentsToRegister() []string {
return []string{constants.GroupManagerExperiment, constants.GroupsExperiment}
}
// OnEvent handles File Sharing Hooks like Manifest Received and FileDownloaded
func (f *GroupManagerFunctionality) OnEvent(ev event.Event, profile peer.CwtchPeer) {
// We only want to engage this functionality if the peer is managing a group.
// In that case ALL peer connections and messages need to be routed through
// the management logic
// For now, we assume that a manager is a peer with a special management group.
// In the future we may want to make this a profile-level switch/attribute.
isManager := false
if ci, err := profile.FetchConversationInfo(MANAGED_GROUP_HANDLE); ci != nil && err == nil {
isManager = true
}
if isManager {
switch ev.EventType {
case event.PeerStateChange:
handle := ev.Data["RemotePeer"]
// check that we have authenticated with this peer
if connections.ConnectionStateToType()[ev.Data[event.ConnectionState]] == connections.AUTHENTICATED {
mg, err := f.GetManagedGroup(profile)
if err != nil {
log.Infof("group manager received peer connections but no suitable group has been found: %v %v", handle, err)
profile.DisconnectFromPeer(handle)
break
}
if _, err := CheckACL(handle, mg); err != nil {
log.Infof("received managed group connection from unauthorized peer: %v %v", handle, err)
profile.DisconnectFromPeer(handle)
break
}
}
// This is where most of the magic happens for managed groups. A few notes:
// - CwtchPeer has already taken care of storing this for us, we don't need to worry about that
// - Group Managers **only** speak overlays and **always** wrap their messages in a ManageGroupEvent anything else is fast-rejected.
case event.NewMessageFromPeerEngine:
log.Infof("received new message from peer: manager")
ci, err := f.GetManagedGroup(profile)
if err != nil {
log.Errorf("unknown conversation %v", err)
break // we don't care about unknown conversations...
}
var cm model.MessageWrapper
err = json.Unmarshal([]byte(ev.Data[event.Data]), &cm)
if err != nil {
log.Errorf("could not deserialize json %s %v", ev.Data[event.Data], err)
break
}
// The overlay type of this message **must** be ManageGroupEvent
if cm.Overlay == model.OverlayManageGroupEvent {
var mge ManageGroupEvent
err = json.Unmarshal([]byte(cm.Data), &mge)
if err == nil {
f.handleEvent(profile, *ci, mge, ev.Data[event.Data])
}
}
}
}
}
// handleEvent takes in a high level ManageGroupEvent message, transforms it into the proper type, and passes it on for handling
// assumes we are called after an event provided by an authorized peer (i.e. ManageGroup == true)
func (f *GroupManagerFunctionality) handleEvent(profile peer.CwtchPeer, conversation model.Conversation, mge ManageGroupEvent, original string) {
switch mge.EventType {
case NewClearMessage:
var nme NewClearMessageEvent
err := json.Unmarshal([]byte(mge.Data), &nme)
if err == nil {
f.handleNewMessageEvent(profile, conversation, nme, original)
}
}
}
func (f *GroupManagerFunctionality) handleNewMessageEvent(profile peer.CwtchPeer, conversation model.Conversation, nme NewClearMessageEvent, original string) {
log.Infof("handling new clear message event")
hgm := nme.HybridGroupMessage
if AuthenticateMessage(hgm) {
log.Infof("authenticated message")
group, err := f.GetManagedGroup(profile)
if err != nil {
log.Infof("received fraudulant hybrid message from group: %v", err)
return
}
if acl, err := CheckACL(hgm.Author, group); err != nil {
log.Infof("received fraudulant hybrid message from group: %v", err)
return
} else if !acl.Append {
log.Infof("received fraudulant hybrid message from group: peer does not have append privileges")
return
} else {
// TODO - Store this message locally in a format that makes it easier to
// do assurance later on
// forward the message to everyone who the server has added as a contact
// and who are represented in the ACL...
allConversations, _ := profile.FetchConversations()
for _, ci := range allConversations {
// NOTE: This check works for Open Groups too as CheckACL will return the default ACL
// for the group....
if ci.Handle != MANAGED_GROUP_HANDLE { // don't send to ourselves...
if acl, err := CheckACL(hgm.Author, group); err == nil && acl.Read {
log.Infof("forwarding group message to: %v", ci.Handle)
profile.SendMessage(ci.ID, original)
}
}
}
}
} else {
log.Errorf("received fraudulant hybrid message fom group")
}
}
// GetManagedGroup is a convieniance function that looks up the managed group
func (f *GroupManagerFunctionality) GetManagedGroup(profile peer.CwtchPeer) (*model.Conversation, error) {
return profile.FetchConversationInfo(MANAGED_GROUP_HANDLE)
}
// Establish a new Managed Group and return its conversation id
func (f *GroupManagerFunctionality) ManageNewGroup(profile peer.CwtchPeer) (int, error) {
// note: a manager can only manage one group. This will (probably) always be true and has a few benefits
// and downsides.
// The main downside is that it requires a new manager per group (and thus an onion service per group)
// However, it means that we can lean on p2p functionality like profile images / metadata / name
// etc. for group metadata and effectively get that for-free in the client.
// HOWEVER: hedging our bets here by giving this group a numeric handle...
if _, err := profile.FetchConversationInfo(MANAGED_GROUP_HANDLE); err == nil {
return -1, fmt.Errorf("manager is already managing a group")
}
ac := model.DefaultP2PAccessControl()
// by setting the ManageGroup permission in this ACL we are allowing the manager to
// take control of how this group is structured, see OnEvent above...
ac.ManageGroup = true
acl := model.AccessControlList{}
acl[profile.GetOnion()] = ac
ci, err := profile.NewConversation(MANAGED_GROUP_HANDLE, acl)
if err != nil {
return -1, err
}
profile.SetConversationAttribute(ci, attr.LocalScope.ConstructScopedZonedPath(attr.ConversationZone.ConstructZonedPath(ManagedGroupOpen)), event.False)
return ci, nil
}
// AddHybridContact is a wrapper arround NewContactConversation which sets the contact
// up for Hybrid Group channel messages...
// TODO this function assumes that authorization has been done at a higher level..
func (f *GroupManagerFunctionality) AddHybridContact(profile peer.CwtchPeer, handle string) error {
ac := model.DefaultP2PAccessControl()
ac.ManageGroup = false
ci, err := profile.NewContactConversation(handle, ac, true)
if err != nil {
return err
}
mg, err := f.GetManagedGroup(profile)
if err != nil {
return err
}
// Update the ACL list to add this contact...
acl := mg.ACL
acl[handle] = model.DefaultP2PAccessControl()
profile.UpdateConversationAccessControlList(mg.ID, acl)
// enable channel 2 on this conversation (hybrid groups management channel)
profile.InitChannel(ci, constants.CHANNEL_MANAGER)
key := fmt.Sprintf("channel.%d", constants.CHANNEL_MANAGER)
profile.SetConversationAttribute(ci, attr.LocalScope.ConstructScopedZonedPath(attr.ConversationZone.ConstructZonedPath(key)), constants.True)
return nil
}
func (f *GroupManagerFunctionality) OnContactRequestValue(profile peer.CwtchPeer, conversation model.Conversation, eventID string, path attr.ScopedZonedPath) {
// nop hybrid group conversations do not exchange contact requests
}
func (f *GroupManagerFunctionality) OnContactReceiveValue(profile peer.CwtchPeer, conversation model.Conversation, path attr.ScopedZonedPath, value string, exists bool) {
// nop hybrid group conversations do not exchange contact requests
}

View File

@ -1,330 +0,0 @@
package hybrid
import (
"crypto/rand"
"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/settings"
"encoding/base64"
"encoding/json"
"fmt"
"git.openprivacy.ca/openprivacy/log"
"golang.org/x/crypto/nacl/secretbox"
"math"
"math/big"
"strconv"
"time"
)
type ManagedGroupFunctionality struct {
}
func (f ManagedGroupFunctionality) NotifySettingsUpdate(settings settings.GlobalSettings) {
}
func (f ManagedGroupFunctionality) EventsToRegister() []event.Type {
return []event.Type{event.NewMessageFromPeerEngine}
}
func (f ManagedGroupFunctionality) ExperimentsToRegister() []string {
return []string{constants.GroupsExperiment}
}
// OnEvent handles File Sharing Hooks like Manifest Received and FileDownloaded
func (f *ManagedGroupFunctionality) OnEvent(ev event.Event, profile peer.CwtchPeer) {
switch ev.EventType {
// This is where most of the magic happens for managed groups. A few notes:
// - CwtchPeer has already taken care of storing this for us, we don't need to worry about that
// - Group Managers **only** speak overlays and **always** wrap their messages in a ManageGroupEvent anything else is fast-rejected.
case event.NewMessageFromPeerEngine:
handle := ev.Data[event.RemotePeer]
ci, err := profile.FetchConversationInfo(handle)
if err != nil {
break // we don't care about unknown conversations...
}
// We reject managed group requests for groups not setup as managed groups...
if ci.ACL[handle].ManageGroup {
var cm model.MessageWrapper
err = json.Unmarshal([]byte(ev.Data[event.Data]), &cm)
if err != nil {
break
}
// The overlay type of this message **must** be ManageGroupEvent
if cm.Overlay == model.OverlayManageGroupEvent {
var mge ManageGroupEvent
err = json.Unmarshal([]byte(cm.Data), &mge)
if err == nil {
cid, err := profile.FetchConversationInfo(handle)
if err == nil {
f.handleEvent(profile, *cid, mge)
}
}
}
}
}
}
// handleEvent takes in a high level ManageGroupEvent message, transforms it into the proper type, and passes it on for handling
// assumes we are called after an event provided by an authorized peer (i.e. ManageGroup == true)
func (f *ManagedGroupFunctionality) handleEvent(profile peer.CwtchPeer, conversation model.Conversation, mge ManageGroupEvent) {
switch mge.EventType {
case AddMember:
var ame AddMemberEvent
err := json.Unmarshal([]byte(mge.Data), &ame)
if err == nil {
f.handleAddMemberEvent(profile, conversation, ame)
}
case RemoveMember:
var rme RemoveMemberEvent
err := json.Unmarshal([]byte(mge.Data), &rme)
if err == nil {
f.handleRemoveMemberEvent(profile, conversation, rme)
}
case NewMessage:
var nme NewMessageEvent
err := json.Unmarshal([]byte(mge.Data), &nme)
if err == nil {
f.handleNewMessageEvent(profile, conversation, nme)
}
case NewClearMessage:
var nme NewClearMessageEvent
err := json.Unmarshal([]byte(mge.Data), &nme)
if err == nil {
f.handleNewClearMessageEvent(profile, conversation, nme)
}
case RotateKey:
var rke RotateKeyEvent
err := json.Unmarshal([]byte(mge.Data), &rke)
if err == nil {
f.handleRotateKeyEvent(profile, conversation, rke)
}
}
}
// handleAddMemberEvent adds a group member to the conversation ACL
// assumes we are called after an event provided by an authorized peer (i.e. ManageGroup == true)
func (f *ManagedGroupFunctionality) handleAddMemberEvent(profile peer.CwtchPeer, conversation model.Conversation, ame AddMemberEvent) {
acl := conversation.ACL
acl[ame.Handle] = model.DefaultP2PAccessControl()
profile.UpdateConversationAccessControlList(conversation.ID, acl)
}
// handleRemoveMemberEvent removes a group member from the conversation ACL
// assumes we are called after an event provided by an authorized peer (i.e. ManageGroup == true)
func (f *ManagedGroupFunctionality) handleRemoveMemberEvent(profile peer.CwtchPeer, conversation model.Conversation, rme RemoveMemberEvent) {
acl := conversation.ACL
delete(acl, rme.Handle)
profile.UpdateConversationAccessControlList(conversation.ID, acl)
}
// handleRotateKeyEvent rotates the encryption key for a given group
// assumes we are called after an event provided by an authorized peer (i.e. ManageGroup == true)
// TODO this currently is a noop as group levle encryption is unimplemented
func (f *ManagedGroupFunctionality) handleRotateKeyEvent(profile peer.CwtchPeer, conversation model.Conversation, rke RotateKeyEvent) {
keyScope := attr.LocalScope.ConstructScopedZonedPath(attr.ConversationZone.ConstructZonedPath("key"))
keyB64 := base64.StdEncoding.EncodeToString(rke.Key)
profile.SetConversationAttribute(conversation.ID, keyScope, keyB64)
}
// TODO this is a sketch implementation that is not yet complete.
func (f *ManagedGroupFunctionality) handleNewMessageEvent(profile peer.CwtchPeer, conversation model.Conversation, nme NewMessageEvent) {
keyScope := attr.LocalScope.ConstructScopedZonedPath(attr.ConversationZone.ConstructZonedPath("key"))
if keyB64, err := profile.GetConversationAttribute(conversation.ID, keyScope); err == nil {
key, err := base64.StdEncoding.DecodeString(keyB64)
if err != nil || len(key) != 32 {
log.Errorf("hybrid group key is corrupted")
return
}
// decrypt the message with key...
hgm, err := f.decryptMessage(key, nme.EncryptedHybridGroupMessage)
if hgm == nil || err != nil {
log.Errorf("unable to decrypt hybrid group message: %v", err)
return
}
f.handleNewClearMessageEvent(profile, conversation, NewClearMessageEvent{HybridGroupMessage: *hgm})
}
}
func (f *ManagedGroupFunctionality) handleNewClearMessageEvent(profile peer.CwtchPeer, conversation model.Conversation, nme NewClearMessageEvent) {
hgm := nme.HybridGroupMessage
if AuthenticateMessage(hgm) {
// TODO Closed Group Membership Check - right now we only support open groups...
if profile.GetOnion() == hgm.Author {
// ack
signatureB64 := base64.StdEncoding.EncodeToString(hgm.Signature)
id, err := profile.GetChannelMessageBySignature(conversation.ID, constants.CHANNEL_CHAT, signatureB64)
if err == nil {
profile.UpdateMessageAttribute(conversation.ID, constants.CHANNEL_CHAT, id, constants.AttrAck, constants.True)
profile.PublishEvent(event.NewEvent(event.IndexedAcknowledgement, map[event.Field]string{event.ConversationID: strconv.Itoa(conversation.ID), event.Index: strconv.Itoa(id)}))
}
} else {
mgidstr := strconv.Itoa(int(nme.HybridGroupMessage.MemberGroupID)) // we need both MemberGroupId and MemberMessageId for attestation later on...
newmmidstr := strconv.Itoa(int(nme.HybridGroupMessage.MemberMessageID))
// Set the attributes of this message...
attr := model.Attributes{MemberGroupIDKey: mgidstr, MemberMessageIDKey: newmmidstr,
constants.AttrAuthor: hgm.Author,
constants.AttrAck: event.True,
constants.AttrSentTimestamp: time.UnixMilli(int64(hgm.Sent)).Format(time.RFC3339Nano)}
// Note: The Channel here is 0...this is the main channel that UIs understand as the default, so this message is
// becomes part of the conversation...
mid, err := profile.InternalInsertMessage(conversation.ID, constants.CHANNEL_CHAT, hgm.Author, hgm.MessageBody, attr, hgm.Signature)
contenthash := model.CalculateContentHash(hgm.Author, hgm.MessageBody)
if err == nil {
profile.PublishEvent(event.NewEvent(event.NewMessageFromGroup, map[event.Field]string{event.ConversationID: strconv.Itoa(conversation.ID), event.TimestampSent: time.UnixMilli(int64(hgm.Sent)).Format(time.RFC3339Nano), event.RemotePeer: hgm.Author, event.Index: strconv.Itoa(mid), event.Data: hgm.MessageBody, event.ContentHash: contenthash}))
}
}
// TODO need to send an event here...
} else {
log.Errorf("received fraudulant hybrid message fom group")
}
}
// todo sketch function
func (f *ManagedGroupFunctionality) decryptMessage(key []byte, ciphertext []byte) (*HybridGroupMessage, error) {
if len(ciphertext) > 24 {
var decryptNonce [24]byte
copy(decryptNonce[:], ciphertext[:24])
var fixedSizeKey [32]byte
copy(fixedSizeKey[:], key[:32])
decrypted, ok := secretbox.Open(nil, ciphertext[24:], &decryptNonce, &fixedSizeKey)
if ok {
var hgm HybridGroupMessage
err := json.Unmarshal(decrypted, &hgm)
return &hgm, err
}
}
return nil, fmt.Errorf("invalid ciphertext/key error")
}
// Define a new managed group, managed by the manager...
func (f *ManagedGroupFunctionality) NewManagedGroup(profile peer.CwtchPeer, manager string) error {
// generate a truely random member id for this group in [0..2^32)
nBig, err := rand.Int(rand.Reader, big.NewInt(math.MaxUint32))
if err != nil {
return err // if there is a problem with random we want to exit now rather than have to clean up group setup...
}
ac := model.DefaultP2PAccessControl()
ac.ManageGroup = true // by setting the ManageGroup permission in this ACL we are allowing the manager to control of how this group is structured
ci, err := profile.NewContactConversation(manager, ac, true)
if err != nil {
return err
}
// enable channel 2 on this conversation (hybrid groups management channel)
key := fmt.Sprintf("channel.%d", 2)
err = profile.SetConversationAttribute(ci, attr.LocalScope.ConstructScopedZonedPath(attr.ConversationZone.ConstructZonedPath(key)), constants.True)
if err != nil {
return fmt.Errorf("could not enable channel 2 on hybrid group: %v", err) // likely a catestrophic error...fail
}
err = profile.InitChannel(ci, 2)
if err != nil {
return fmt.Errorf("could not enable channel 2 on hybrid group: %v", err) // likely a catestrophic error...fail
}
// finally, set the member group id on this group...
mgidkey := attr.LocalScope.ConstructScopedZonedPath(attr.ConversationZone.ConstructZonedPath(MemberGroupIDKey))
err = profile.SetConversationAttributeInt(ci, mgidkey, int(nBig.Uint64()))
if err != nil {
return fmt.Errorf("could not set group id on hybrid group: %v", err) // likely a catestrophic error...fail
}
return nil
}
// SendMessageToManagedGroup acts like SendMessage(ToPeer), but with a few additional bookkeeping steps for Hybrid Groups
func (f *ManagedGroupFunctionality) SendMessageToManagedGroup(profile peer.CwtchPeer, conversation int, message string) (int, error) {
mgidkey := attr.LocalScope.ConstructScopedZonedPath(attr.ConversationZone.ConstructZonedPath(MemberGroupIDKey))
mgid, err := profile.GetConversationAttributeInt(conversation, mgidkey)
if err != nil {
return -1, err
}
mmidkey := attr.LocalScope.ConstructScopedZonedPath(attr.ConversationZone.ConstructZonedPath(MemberMessageIDKey))
mmid, err := profile.GetConversationAttributeInt(conversation, mmidkey)
if err != nil {
mmid = 0 // first message
}
mmid += 1
// Now time to package this whole thing in layers of JSON...
hgm := HybridGroupMessage{
MemberGroupID: uint32(mgid),
MemberMessageID: uint32(mmid),
Sent: uint64(time.Now().UnixMilli()),
Author: profile.GetOnion(),
MessageBody: message,
Signature: []byte{}, // Leave blank so we can sign this message...
}
data, err := json.Marshal(hgm)
if err != nil {
return -1, err
}
// Don't forget to sign the message...
sig, err := profile.SignMessage(data)
if err != nil {
return -1, err
}
hgm.Signature = sig
ncm := NewClearMessageEvent{
HybridGroupMessage: hgm,
}
signedData, err := json.Marshal(ncm)
if err != nil {
return -1, err
}
mgm := ManageGroupEvent{
EventType: NewClearMessage,
Data: string(signedData),
}
odata, err := json.Marshal(mgm)
if err != nil {
return -1, err
}
overlay := model.MessageWrapper{
Overlay: model.OverlayManageGroupEvent,
Data: string(odata),
}
ojson, err := json.Marshal(overlay)
if err != nil {
return -1, err
}
// send the message to the manager and update our message is string for tracking...
_, err = profile.SendMessage(conversation, string(ojson))
if err != nil {
return -1, err
}
profile.SetConversationAttributeInt(conversation, mmidkey, mmid)
// ok there is still one more thing we need to do...
// insert this message as part of our group log, for members of the group
// this exists in channel 0 of the conversation with the group manager...
mgidstr := strconv.Itoa(mgid) // we need both MemberGroupId and MemberMessageId for attestation later on...
newmmidstr := strconv.Itoa(mmid)
attr := model.Attributes{MemberGroupIDKey: mgidstr, MemberMessageIDKey: newmmidstr, constants.AttrAuthor: profile.GetOnion(), constants.AttrAck: event.False, constants.AttrSentTimestamp: time.Now().Format(time.RFC3339Nano)}
return profile.InternalInsertMessage(conversation, 0, hgm.Author, message, attr, hgm.Signature)
}
func (f ManagedGroupFunctionality) OnContactRequestValue(profile peer.CwtchPeer, conversation model.Conversation, eventID string, path attr.ScopedZonedPath) {
// nop hybrid group conversations do not exchange contact requests
}
func (f ManagedGroupFunctionality) OnContactReceiveValue(profile peer.CwtchPeer, conversation model.Conversation, path attr.ScopedZonedPath, value string, exists bool) {
// nop hybrid group conversations do not exchange contact requests
}

View File

@ -1,69 +0,0 @@
package inter
import (
"strings"
"cwtch.im/cwtch/functionality/hybrid"
"cwtch.im/cwtch/peer"
)
// This functionality is a little different. It's not functionality per-se. It's a wrapper around
// CwtchProfile function that combines some core-functionalities like Hybrid Groups so that
// they can be transparently exposed in autobindings.
// DEV NOTE: consider moving other cross-cutting interface functions here to simplfy CwtchPeer
type InterfaceFunctionality struct {
}
// FunctionalityGate returns filesharing functionality - gates now happen on function calls.
func FunctionalityGate() *InterfaceFunctionality {
return new(InterfaceFunctionality)
}
func (i InterfaceFunctionality) ImportBundle(profile peer.CwtchPeer, uri string) error {
// check if this is a managed group. Note: managed groups do not comply with the server bundle format.
if strings.HasPrefix(uri, "managed:") {
uri = uri[len("managed:"):]
mgf := hybrid.ManagedGroupFunctionality{}
return mgf.NewManagedGroup(profile, uri)
}
// DEV NOTE: we may want to eventually move Server Import code to ServerFunctionality and add a hook here...
// DEV NOTE: consider making ImportBundle a high-level functionality interface? to support different kinds of contacts?
return profile.ImportBundle(uri)
}
// EnhancedImportBundle is identical to EnhancedImportBundle in CwtchPeer but instead of wrapping CwtchPeer.ImportBundle it instead
// wraps InterfaceFunctionality.ImportBundle
func (i InterfaceFunctionality) EnhancedImportBundle(profile peer.CwtchPeer, uri string) string {
err := i.ImportBundle(profile, uri)
if err == nil {
return "importBundle.success"
}
return err.Error()
}
// SendMessage sends a message to a conversation.
// NOTE: Unlike CwtchPeer.SendMessage this interface makes no guarentees about the raw-ness of the message sent to peer contacts.
// If the conversation is a hybrid groups then the message may be wrapped in multiple layers of overlay messages / encryption
// prior to being send. To send a raw message to a peer then use peer.CwtchPeer
// DEV NOTE: Move Legacy Group message send here...
func (i InterfaceFunctionality) SendMessage(profile peer.CwtchPeer, conversation int, message string) (int, error) {
ci, err := profile.GetConversationInfo(conversation)
if err != nil {
return -1, err
}
if ci.ACL[ci.Handle].ManageGroup {
mgf := hybrid.ManagedGroupFunctionality{}
return mgf.SendMessageToManagedGroup(profile, conversation, message)
}
return profile.SendMessage(conversation, message)
}
// EnhancedSendMessage Attempts to Send a Message and Immediately Attempts to Lookup the Message in the Database
// this wraps InterfaceFunctionality.SendMessage to support HybridGroups
func (i InterfaceFunctionality) EnhancedSendMessage(profile peer.CwtchPeer, conversation int, message string) string {
mid, err := i.SendMessage(profile, conversation, message)
if err != nil {
return ""
}
return profile.EnhancedGetMessageById(conversation, mid)
}

View File

@ -1,150 +0,0 @@
package servers
import (
"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"
"encoding/json"
"errors"
"git.openprivacy.ca/openprivacy/log"
)
const (
// ServerList is a json encoded list of servers
ServerList = event.Field("ServerList")
)
const (
// UpdateServerInfo is an event containing a ProfileOnion and a ServerList
UpdateServerInfo = event.Type("UpdateServerInfo")
)
// Functionality groups some common UI triggered functions for contacts...
type Functionality struct {
}
func (f *Functionality) NotifySettingsUpdate(settings settings.GlobalSettings) {
}
func (f *Functionality) EventsToRegister() []event.Type {
return []event.Type{event.QueueJoinServer}
}
func (f *Functionality) ExperimentsToRegister() []string {
return []string{constants.GroupsExperiment}
}
// OnEvent handles File Sharing Hooks like Manifest Received and FileDownloaded
func (f *Functionality) OnEvent(ev event.Event, profile peer.CwtchPeer) {
if profile.IsFeatureEnabled(constants.GroupsExperiment) {
switch ev.EventType {
// keep the UI in sync with the current backend server updates...
// queue join server gets triggered on load and on new servers so it's a nice
// low-noise event to hook into...
case event.QueueJoinServer:
f.PublishServerUpdate(profile)
}
}
}
func (f *Functionality) OnContactRequestValue(profile peer.CwtchPeer, conversation model.Conversation, eventID string, path attr.ScopedZonedPath) {
// nop
}
func (f *Functionality) OnContactReceiveValue(profile peer.CwtchPeer, conversation model.Conversation, path attr.ScopedZonedPath, value string, exists bool) {
// nopt
}
// FunctionalityGate returns filesharing functionality - gates now happen on function calls.
func FunctionalityGate() *Functionality {
return new(Functionality)
}
// ServerKey packages up key information...
// TODO: Can this be merged with KeyBundle?
type ServerKey struct {
Type string `json:"type"`
Key string `json:"key"`
}
// SyncStatus packages up server sync information...
type SyncStatus struct {
StartTime string `json:"startTime"`
LastMessageTime string `json:"lastMessageTime"`
}
// Server encapsulates the information needed to represent a server...
type Server struct {
Onion string `json:"onion"`
Identifier int `json:"identifier"`
Status string `json:"status"`
Description string `json:"description"`
Keys []ServerKey `json:"keys"`
SyncProgress SyncStatus `json:"syncProgress"`
}
// PublishServerUpdate serializes the current list of group servers and publishes an event with this information
func (f *Functionality) PublishServerUpdate(profile peer.CwtchPeer) error {
serverListForOnion := f.GetServerInfoList(profile)
serversListBytes, err := json.Marshal(serverListForOnion)
profile.PublishEvent(event.NewEvent(UpdateServerInfo, map[event.Field]string{"ProfileOnion": profile.GetOnion(), ServerList: string(serversListBytes)}))
return err
}
// GetServerInfoList compiles all the information the UI might need regarding all servers..
func (f *Functionality) GetServerInfoList(profile peer.CwtchPeer) []Server {
var servers []Server
for _, server := range profile.GetServers() {
server, err := f.GetServerInfo(profile, server)
if err != nil {
log.Errorf("profile server list is corrupted: %v", err)
continue
}
servers = append(servers, server)
}
return servers
}
// DeleteServer purges a server and all related keys from a profile
func (f *Functionality) DeleteServerInfo(profile peer.CwtchPeer, serverOnion string) error {
// Servers are stores as special conversations
ci, err := profile.FetchConversationInfo(serverOnion)
if err != nil {
return err
}
// Purge keys...
// NOTE: This will leave some groups in the state of being unable to connect to a particular
// server.
profile.DeleteConversation(ci.ID)
f.PublishServerUpdate(profile)
return nil
}
// GetServerInfo compiles all the information the UI might need regarding a particular server including any verified
// cryptographic keys
func (f *Functionality) GetServerInfo(profile peer.CwtchPeer, serverOnion string) (Server, error) {
serverInfo, err := profile.FetchConversationInfo(serverOnion)
if err != nil {
return Server{}, errors.New("server not found")
}
keyTypes := []model.KeyType{model.KeyTypeServerOnion, model.KeyTypeTokenOnion, model.KeyTypePrivacyPass}
var serverKeys []ServerKey
for _, keyType := range keyTypes {
if key, has := serverInfo.GetAttribute(attr.PublicScope, attr.ServerKeyZone, string(keyType)); has {
serverKeys = append(serverKeys, ServerKey{Type: string(keyType), Key: key})
}
}
description, _ := serverInfo.GetAttribute(attr.LocalScope, attr.ServerZone, constants.Description)
startTimeStr := serverInfo.Attributes[attr.LocalScope.ConstructScopedZonedPath(attr.LegacyGroupZone.ConstructZonedPath(constants.SyncPreLastMessageTime)).ToString()]
recentTimeStr := serverInfo.Attributes[attr.LocalScope.ConstructScopedZonedPath(attr.LegacyGroupZone.ConstructZonedPath(constants.SyncMostRecentMessageTime)).ToString()]
syncStatus := SyncStatus{startTimeStr, recentTimeStr}
return Server{Onion: serverOnion, Identifier: serverInfo.ID, Status: connections.ConnectionStateName[profile.GetPeerState(serverInfo.Handle)], Keys: serverKeys, Description: description, SyncProgress: syncStatus}, nil
}

6
go.mod
View File

@ -1,10 +1,10 @@
module cwtch.im/cwtch
go 1.20
go 1.17
require (
git.openprivacy.ca/cwtch.im/tapir v0.6.0
git.openprivacy.ca/openprivacy/connectivity v1.11.0
git.openprivacy.ca/openprivacy/connectivity v1.8.6
git.openprivacy.ca/openprivacy/log v1.0.3
github.com/gtank/ristretto255 v0.1.3-0.20210930101514-6bb39798585c
github.com/mutecomm/go-sqlcipher/v4 v4.4.2
@ -15,7 +15,7 @@ require (
require (
filippo.io/edwards25519 v1.0.0 // indirect
git.openprivacy.ca/openprivacy/bine v0.0.5 // indirect
git.openprivacy.ca/openprivacy/bine v0.0.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

103
go.sum
View File

@ -1,23 +1,46 @@
filippo.io/edwards25519 v1.0.0-rc.1/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns=
filippo.io/edwards25519 v1.0.0 h1:0wAIcmJUqRdI8IJ/3eGi5/HwXZWPujYXXlkrQogz0Ek=
filippo.io/edwards25519 v1.0.0/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns=
git.openprivacy.ca/cwtch.im/tapir v0.6.0 h1:TtnKjxitkIDMM7Qn0n/u+mOHRLJzuQUYjYRu5n0/QFY=
git.openprivacy.ca/cwtch.im/tapir v0.6.0/go.mod h1:iQIq4y7N+DuP3CxyG66WNEC/d6vzh+wXvvOmelB+KoY=
git.openprivacy.ca/openprivacy/bine v0.0.5 h1:DJs5gqw3SkvLSgRDvroqJxZ7F+YsbxbBRg5t0rU5gYE=
git.openprivacy.ca/openprivacy/bine v0.0.5/go.mod h1:fwdeq6RO08WDkV0k7HfArsjRvurVULoUQmT//iaABZM=
git.openprivacy.ca/openprivacy/connectivity v1.11.0 h1:roASjaFtQLu+HdH5fa2wx6F00NL3YsUTlmXBJh8aLZk=
git.openprivacy.ca/openprivacy/connectivity v1.11.0/go.mod h1:OQO1+7OIz/jLxDrorEMzvZA6SEbpbDyLGpjoFqT3z1Y=
git.openprivacy.ca/openprivacy/bine v0.0.4 h1:CO7EkGyz+jegZ4ap8g5NWRuDHA/56KKvGySR6OBPW+c=
git.openprivacy.ca/openprivacy/bine v0.0.4/go.mod h1:13ZqhKyqakDsN/ZkQkIGNULsmLyqtXc46XBcnuXm/mU=
git.openprivacy.ca/openprivacy/connectivity v1.8.6 h1:g74PyDGvpMZ3+K0dXy3mlTJh+e0rcwNk0XF8owzkmOA=
git.openprivacy.ca/openprivacy/connectivity v1.8.6/go.mod h1:Hn1gpOx/bRZp5wvCtPQVJPXrfeUH0EGiG/Aoa0vjGLg=
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/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
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=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/gtank/merlin v0.1.1 h1:eQ90iG7K9pOhtereWsmyRJ6RAwcP4tHTDBHXNg+u5is=
github.com/gtank/merlin v0.1.1/go.mod h1:T86dnYJhcGOh5BjZFCJWTDeTK7XW8uE+E21Cy/bIQ+s=
github.com/gtank/ristretto255 v0.1.3-0.20210930101514-6bb39798585c h1:gkfmnY4Rlt3VINCo4uKdpvngiibQyoENVj5Q88sxXhE=
github.com/gtank/ristretto255 v0.1.3-0.20210930101514-6bb39798585c/go.mod h1:tDPFhGdt3hJWqtKwx57i9baiB1Cj0yAg22VOPUqm5vY=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
@ -28,43 +51,115 @@ github.com/mimoo/StrobeGo v0.0.0-20220103164710-9a04d6ca976b h1:QrHweqAtyJ9EwCaG
github.com/mimoo/StrobeGo v0.0.0-20220103164710-9a04d6ca976b/go.mod h1:xxLb2ip6sSUts3g1irPVHyk/DGslwQsNOo9I7smJfNU=
github.com/mutecomm/go-sqlcipher/v4 v4.4.2 h1:eM10bFtI4UvibIsKr10/QT7Yfz+NADfjZYh0GKrXUNc=
github.com/mutecomm/go-sqlcipher/v4 v4.4.2/go.mod h1:mF2UmIpBnzFeBdu/ypTDb/LdbS0nk0dfSN1WUsWTjMA=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc=
github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c=
github.com/onsi/ginkgo/v2 v2.1.4 h1:GNapqRSid3zijZ9H77KrgVG4/8KqiyRsxcSxe+7ApXY=
github.com/onsi/ginkgo/v2 v2.1.4/go.mod h1:um6tUpWM/cxCK3/FK8BXqEiUMUwRgSM4JXG47RKZmLU=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro=
github.com/onsi/gomega v1.20.1 h1:PA/3qinGoukvymdIDV8pii6tiZgC8kbmJO6Z5+b002Q=
github.com/onsi/gomega v1.20.1/go.mod h1:DtrZpjmvpn2mPm4YWQa0/ALMDj9v4YxLgojwPeREyVo=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU=
go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d h1:3qF+Z8Hkrw9sOhrFHti9TlB1Hkac1x+DNRkv0XQiFjo=
golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b h1:ZmngSVLe/wycRns9MKikG9OWIEjGcGAkacif7oYQaUY=
golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64 h1:UiNENfZ8gDvpiWw7IpOMQ27spWmThO1RwwdQVbJahJM=
golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw=
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@ -20,9 +20,6 @@ const (
// LegacyGroupZone for attributes related to legacy group experiment
LegacyGroupZone = Zone("legacygroup")
// ConversationZone for attributes related to structure of the conversation
ConversationZone = Zone("conversation")
// FilesharingZone for attributes related to file sharing
FilesharingZone = Zone("filesharing")
@ -68,8 +65,6 @@ func ParseZone(path string) (Zone, string) {
return ServerKeyZone, parts[1]
case ServerZone:
return ServerZone, parts[1]
case ConversationZone:
return ConversationZone, parts[1]
default:
return UnknownZone, parts[1]
}

View File

@ -57,18 +57,9 @@ const SyncMostRecentMessageTime = "SyncMostRecentMessageTime"
const AttrLastConnectionTime = "last-connection-time"
const PeerAutostart = "autostart"
const PeerAppearOffline = "appear-offline"
const Archived = "archived"
const ProfileStatus = "profile-status"
const ProfileAttribute1 = "profile-attribute-1"
const ProfileAttribute2 = "profile-attribute-2"
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,4 +0,0 @@
package constants
const CHANNEL_CHAT = 0
const CHANNEL_MANAGER = 2

View File

@ -1,7 +1,5 @@
package constants
const GroupsExperiment = "tapir-groups-experiment"
// FileSharingExperiment Allows file sharing
const FileSharingExperiment = "filesharing"
@ -19,6 +17,3 @@ var AutoDLFileExts = [...]string{".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp
// BlodeuweddExperiment enables the Blodeuwedd Assistant
const BlodeuweddExperiment = "blodeuwedd"
// Enables the Hybrid Group Manager Extension
const GroupManagerExperiment = "group-manager"

View File

@ -4,35 +4,19 @@ import (
"cwtch.im/cwtch/model/attr"
"cwtch.im/cwtch/model/constants"
"encoding/json"
"fmt"
"git.openprivacy.ca/openprivacy/connectivity/tor"
"git.openprivacy.ca/openprivacy/log"
"time"
)
// 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 overrides all other settings
// Basic Conversation Rights
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
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
ManageGroup bool // Allows this conversation to be managed by hybrid groups
}
// DefaultP2PAccessControl defaults to a semi-trusted peer with no access to special extensions.
// DefaultP2PAccessControl - because in the year 2021, go does not support constant structs...
func DefaultP2PAccessControl() AccessControl {
return AccessControl{Read: true, Append: true, ExchangeAttributes: true, Blocked: false,
AutoConnect: true, ShareFiles: false, RenderImages: false}
return AccessControl{Read: true, Append: true, Blocked: false}
}
// AccessControlList represents an access control list for a conversation. Mapping handles to conversation
@ -46,10 +30,10 @@ func (acl *AccessControlList) Serialize() []byte {
}
// DeserializeAccessControlList takes in JSON and returns an AccessControlList
func DeserializeAccessControlList(data []byte) (AccessControlList, error) {
func DeserializeAccessControlList(data []byte) AccessControlList {
var acl AccessControlList
err := json.Unmarshal(data, &acl)
return acl, err
json.Unmarshal(data, &acl)
return acl
}
// Attributes a type-driven encapsulation of an Attribute map.
@ -63,12 +47,8 @@ func (a *Attributes) Serialize() []byte {
// DeserializeAttributes converts a JSON struct into an Attributes map
func DeserializeAttributes(data []byte) 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)
}
var attributes Attributes
json.Unmarshal(data, &attributes)
return attributes
}
@ -80,8 +60,6 @@ type Conversation struct {
Handle string
Attributes Attributes
ACL AccessControlList
// Deprecated, please use ACL for permissions related functions
Accepted bool
}
@ -93,36 +71,6 @@ 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()
}
// HasChannel returns true if the requested channel has been setup for this conversation
func (ci *Conversation) HasChannel(requestedChannel int) bool {
if requestedChannel == 0 {
return true
}
if requestedChannel == 1 {
return false // channel 1 is mapped to channel 0 for backwards compatibility
}
key := fmt.Sprintf("channel.%d", requestedChannel)
if value, exists := ci.Attributes[attr.LocalScope.ConstructScopedZonedPath(attr.ConversationZone.ConstructZonedPath(key)).ToString()]; exists {
return value == constants.True
}
return false
}
// 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

@ -6,13 +6,13 @@ import "sync"
// examples of experiments include File Sharing, Profile Images and Groups.
type Experiments struct {
enabled bool
experiments *sync.Map
experiments sync.Map
}
// InitExperiments encapsulates a set of experiments separate from their storage in GlobalSettings.
func InitExperiments(enabled bool, experiments map[string]bool) Experiments {
syncExperiments := new(sync.Map)
var syncExperiments sync.Map
for experiment, set := range experiments {
syncExperiments.Store(experiment, set)
}

View File

@ -59,26 +59,21 @@ func NewGroup(server string) (*Group, error) {
// Derive Group ID from the group key and the server public key. This binds the group to a particular server
// and key.
var err error
group.GroupID, err = deriveGroupID(groupKey[:], server)
return group, err
group.GroupID = deriveGroupID(groupKey[:], server)
return group, nil
}
// CheckGroup returns true only if the ID of the group is cryptographically valid.
func (g *Group) CheckGroup() bool {
id, _ := deriveGroupID(g.GroupKey[:], g.GroupServer)
return g.GroupID == id
return g.GroupID == deriveGroupID(g.GroupKey[:], g.GroupServer)
}
// deriveGroupID hashes together the key and the hostname to create a bound identifier that can later
// be referenced and checked by profiles when they receive invites and messages.
func deriveGroupID(groupKey []byte, serverHostname string) (string, error) {
data, err := base32.StdEncoding.DecodeString(strings.ToUpper(serverHostname))
if err != nil {
return "", err
}
func deriveGroupID(groupKey []byte, serverHostname string) string {
data, _ := base32.StdEncoding.DecodeString(strings.ToUpper(serverHostname))
pubkey := data[0:ed25519.PublicKeySize]
return hex.EncodeToString(pbkdf2.Key(groupKey, pubkey, 4096, 16, sha512.New)), nil
return hex.EncodeToString(pbkdf2.Key(groupKey, pubkey, 4096, 16, sha512.New))
}
// Invite generates a invitation that can be sent to a cwtch peer
@ -153,7 +148,7 @@ func ValidateInvite(invite string) (*groups.GroupInvite, error) {
// Derive the servers public key (we can ignore the error checking here because it's already been
// done by IsValidHostname, and check that we derive the same groupID...
derivedGroupID, _ := deriveGroupID(gci.SharedKey, gci.ServerHost)
derivedGroupID := deriveGroupID(gci.SharedKey, gci.ServerHost)
if derivedGroupID != gci.GroupID {
return nil, errors.New("group id is invalid")
}
@ -171,9 +166,7 @@ func ValidateInvite(invite string) (*groups.GroupInvite, error) {
// If successful, adds the message to the group's timeline
func (g *Group) AttemptDecryption(ciphertext []byte, signature []byte) (bool, *groups.DecryptedGroupMessage) {
success, dgm := g.DecryptMessage(ciphertext)
// the second check here is not needed, but DecryptMessage violates the usual
// go calling convention and we want static analysis tools to pick it up
if success && dgm != nil {
if success {
// Attempt to serialize this message
serialized, err := json.Marshal(dgm)

View File

@ -9,10 +9,7 @@ import (
)
func TestGroup(t *testing.T) {
g, err := NewGroup("2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd")
if err != nil {
t.Fatalf("Group with real group server should not fail")
}
g, _ := NewGroup("2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd")
dgm := &groups.DecryptedGroupMessage{
Onion: "onion",
Text: "Hello World!",
@ -40,7 +37,7 @@ func TestGroup(t *testing.T) {
encMessage, _ := g.EncryptMessage(dgm)
ok, message := g.DecryptMessage(encMessage)
if (!ok || message == nil) || message.Text != "Hello World!" {
if !ok || message.Text != "Hello World!" {
t.Errorf("group encryption was invalid, or returned wrong message decrypted:%v message:%v", ok, message)
return
}
@ -76,10 +73,7 @@ func TestGroupValidation(t *testing.T) {
t.Logf("Error: %v", err)
// Generate a valid group but replace the group server...
group, err = NewGroup("2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd")
if err != nil {
t.Fatalf("Group with real group server should not fail")
}
group, _ = NewGroup("2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd")
group.GroupServer = "tcnkoch4nyr3cldkemejtkpqok342rbql6iclnjjs3ndgnjgufzyxvqd"
invite, _ = group.Invite()
_, err = ValidateInvite(invite)
@ -90,10 +84,7 @@ func TestGroupValidation(t *testing.T) {
t.Logf("Error: %v", err)
// Generate a valid group but replace the group key...
group, err = NewGroup("2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd")
if err != nil {
t.Fatalf("Group with real group server should not fail")
}
group, _ = NewGroup("2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd")
group.GroupKey = sha256.Sum256([]byte{})
invite, _ = group.Invite()
_, err = ValidateInvite(invite)

View File

@ -3,7 +3,6 @@ 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
@ -13,13 +12,3 @@ 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,41 +1,9 @@
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 {
// 1024 / 0x400 is the start of new channel overlays
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
@ -49,6 +17,3 @@ const OverlayInviteGroup = 101
// OverlayFileSharing is the canonical identifier for the file sharing overlay
const OverlayFileSharing = 200
// ManageGroupEvent is the canonical identifier for the manage group overlay
const OverlayManageGroupEvent = 0x402

View File

@ -54,11 +54,13 @@ const MaxGroupMessageLength = 1800
func getRandomness(arr *[]byte) {
if _, err := io.ReadFull(rand.Reader, (*arr)[:]); err != nil {
if err != nil {
// If we can't do randomness, just crash something is very very wrong and we are not going
// to resolve it here....
panic(err.Error())
}
}
}
// GenerateRandomID generates a random 16 byte hex id code
func GenerateRandomID() string {
@ -78,19 +80,11 @@ func (p *Profile) GetCopy(timeline bool) *Profile {
if timeline {
for groupID := range newp.Groups {
if group, exists := newp.Groups[groupID]; exists {
if pGroup, exists := p.Groups[groupID]; exists {
group.Timeline = *(pGroup).Timeline.GetCopy()
}
}
newp.Groups[groupID].Timeline = *p.Groups[groupID].Timeline.GetCopy()
}
for peerID := range newp.Contacts {
if peer, exists := newp.Contacts[peerID]; exists {
if pPeer, exists := p.Contacts[peerID]; exists {
peer.Timeline = *(pPeer).Timeline.GetCopy()
}
}
newp.Contacts[peerID].Timeline = *p.Contacts[peerID].Timeline.GetCopy()
}
}

View File

@ -1,7 +1,6 @@
package peer
import (
"context"
"crypto/rand"
"encoding/base64"
"encoding/hex"
@ -16,7 +15,6 @@ import (
"sync"
"time"
"cwtch.im/cwtch/model"
"cwtch.im/cwtch/model/constants"
"cwtch.im/cwtch/protocol/groups"
"cwtch.im/cwtch/settings"
@ -27,6 +25,7 @@ import (
"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"
@ -76,8 +75,6 @@ type cwtchPeer struct {
extensionLock sync.Mutex // we don't want to hold up all of cwtch for managing thread safe access to extensions
experiments model.Experiments
experimentsLock sync.Mutex
cancelSearchContext context.CancelFunc
}
// EnhancedSendInviteMessage encapsulates attempting to send an invite to a conversation and looking up the enhanced message
@ -94,7 +91,7 @@ func (cp *cwtchPeer) EnhancedImportBundle(importString string) string {
return cp.ImportBundle(importString).Error()
}
func (cp *cwtchPeer) EnhancedGetMessages(conversation int, index int, count uint) string {
func (cp *cwtchPeer) EnhancedGetMessages(conversation int, index int, count int) string {
var emessages = make([]EnhancedMessage, count)
messages, err := cp.GetMostRecentMessages(conversation, 0, index, count)
@ -145,7 +142,7 @@ func (cp *cwtchPeer) EnhancedGetMessageByContentHash(conversation int, contentHa
offset, err := cp.GetChannelMessageByContentHash(conversation, 0, contentHash)
if err == nil {
messages, err := cp.GetMostRecentMessages(conversation, 0, offset, 1)
if len(messages) > 0 && err == nil {
if err == nil {
sentTime, _ := time.Parse(time.RFC3339Nano, messages[0].Attr[constants.AttrSentTimestamp])
message.Message = model.Message{
Message: messages[0].Body,
@ -194,16 +191,10 @@ func (cp *cwtchPeer) UpdateExperiments(enabled bool, experiments map[string]bool
cp.experiments = model.InitExperiments(enabled, experiments)
}
// NotifySettingsUpdate notifies a Cwtch profile of a change in the nature of global settings.
// The Cwtch Profile uses this information to update registered extensions in addition
// to updating internal settings.
// NotifySettingsUpdate notifies a Cwtch profile of a change in the nature of global experiments. The Cwtch Profile uses
// this information to update registered extensions.
func (cp *cwtchPeer) NotifySettingsUpdate(settings settings.GlobalSettings) {
log.Debugf("Cwtch Profile Settings Update: %v", settings)
// update the save history default...
cp.SetScopedZonedAttribute(attr.LocalScope, attr.ProfileZone, event.PreserveHistoryDefaultSettingKey, strconv.FormatBool(settings.DefaultSaveHistory))
// pass these seetings updates
cp.extensionLock.Lock()
defer cp.extensionLock.Unlock()
for _, extension := range cp.extensions {
@ -310,33 +301,7 @@ func (cp *cwtchPeer) GenerateProtocolEngine(acn connectivity.ACN, bus event.Mana
authorizations := make(map[string]model.Authorization)
for _, conversation := range conversations {
// 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 tor.IsValidHostname(conversation.Handle) {
if conversation.ACL[conversation.Handle].Blocked {
authorizations[conversation.Handle] = model.AuthBlocked
} else {
@ -436,19 +401,12 @@ 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
// 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, cm.Channel(), 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))
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 {
@ -703,19 +661,11 @@ func (cp *cwtchPeer) ImportGroup(exportedInvite string) (int, error) {
cp.SetConversationAttribute(groupConversationID, attr.LocalScope.ConstructScopedZonedPath(attr.LegacyGroupZone.ConstructZonedPath(constants.GroupKey)), base64.StdEncoding.EncodeToString(gci.SharedKey))
cp.SetConversationAttribute(groupConversationID, attr.LocalScope.ConstructScopedZonedPath(attr.ProfileZone.ConstructZonedPath(constants.Name)), gci.GroupName)
cp.eventBus.Publish(event.NewEvent(event.NewGroup, map[event.Field]string{event.ConversationID: strconv.Itoa(groupConversationID), event.GroupServer: gci.ServerHost, event.GroupInvite: exportedInvite, event.GroupName: gci.GroupName}))
cp.QueueJoinServer(gci.ServerHost)
cp.JoinServer(gci.ServerHost)
}
return groupConversationID, err
}
// NewConversation create a new multi-peer conversation.
func (cp *cwtchPeer) NewConversation(handle string, acl model.AccessControlList) (int, error) {
conversationID, err := cp.storage.NewConversation(handle, model.Attributes{event.SaveHistoryKey: event.DeleteHistoryDefault}, acl, true)
cp.SetConversationAttribute(conversationID, attr.LocalScope.ConstructScopedZonedPath(attr.ProfileZone.ConstructZonedPath(constants.ACLVersion)), constants.ACLVersionOne)
cp.SetConversationAttribute(conversationID, attr.LocalScope.ConstructScopedZonedPath(attr.ProfileZone.ConstructZonedPath(constants.AttrLastConnectionTime)), time.Now().Format(time.RFC3339Nano))
return conversationID, err
}
// NewContactConversation create a new p2p conversation with the given acl applied to the handle.
func (cp *cwtchPeer) NewContactConversation(handle string, acl model.AccessControl, accepted bool) (int, error) {
cp.mutex.Lock()
@ -723,60 +673,13 @@ 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 {
@ -789,21 +692,6 @@ 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)
@ -851,7 +739,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.GetPeerAC().AutoConnect {
if !ci.IsGroup() && !ci.IsServer() && ci.Accepted {
cp.PeerWithOnion(ci.Handle)
}
@ -881,8 +769,7 @@ func (cp *cwtchPeer) DeleteConversation(id int) error {
defer cp.mutex.Unlock()
ci, err := cp.storage.GetConversation(id)
if err == nil && ci != nil {
log.Debugf("deleting %v", ci)
cp.eventBus.Publish(event.NewEventList(event.DeleteContact, event.RemotePeer, ci.Handle, event.ConversationID, strconv.Itoa(id)))
cp.eventBus.Publish(event.NewEventList(event.DeleteContact, event.RemotePeer, ci.Handle))
return cp.storage.DeleteConversation(id)
}
return fmt.Errorf("could not delete conversation, did not exist")
@ -906,26 +793,6 @@ func (cp *cwtchPeer) GetConversationAttribute(id int, path attr.ScopedZonedPath)
return val, nil
}
// SetConversationAttributeInt sets the conversation attribute at path to an integer value
func (cp *cwtchPeer) SetConversationAttributeInt(id int, path attr.ScopedZonedPath, value int) error {
strvalue := strconv.Itoa(value)
return cp.storage.SetConversationAttribute(id, path, strvalue)
}
// GetConversationAttributeInt is a method for retrieving an integer value of a given conversation
func (cp *cwtchPeer) GetConversationAttributeInt(id int, path attr.ScopedZonedPath) (int, error) {
ci, err := cp.storage.GetConversation(id)
if err != nil {
return 0, err
}
val, exists := ci.Attributes[path.ToString()]
if !exists {
return 0, fmt.Errorf("%v does not exist for conversation %v", path.ToString(), id)
}
intvalue, err := strconv.Atoi(val)
return intvalue, err
}
// GetChannelMessage returns a message from a conversation channel referenced by the absolute ID.
// Note: This should note be used to index a list as the ID is not expected to be tied to absolute position
// in the table (e.g. deleted messages, expired messages, etc.)
@ -933,85 +800,13 @@ func (cp *cwtchPeer) GetChannelMessage(conversation int, channel int, id int) (s
return cp.storage.GetChannelMessage(conversation, channel, id)
}
func (cp *cwtchPeer) doSearch(ctx context.Context, searchID string, pattern string) {
// do not allow trivial searches that would match a wide variety of messages...
if len(pattern) <= 5 {
return
}
conversations, _ := cp.FetchConversations()
maxCount := 0
conversationCount := map[int]int{}
for _, conversation := range conversations {
count, err := cp.storage.GetChannelMessageCount(conversation.ID, 0)
if err != nil {
log.Errorf("could not fetch channel count for conversation %d:%d: %s", conversation.ID, 0, err)
}
if count > maxCount {
maxCount = count
}
conversationCount[conversation.ID] = count
}
log.Debugf("searching messages..%v", conversationCount)
for offset := 0; offset < (maxCount + 10); offset += 10 {
select {
case <-ctx.Done():
cp.PublishEvent(event.NewEvent(event.SearchCancelled, map[event.Field]string{event.SearchID: searchID}))
return
case <-time.After(time.Millisecond * 100):
for _, conversation := range conversations {
ccount := conversationCount[conversation.ID]
if offset > ccount {
continue
}
log.Debugf("searching messages..%v: %v offset: %v", conversation.ID, pattern, offset)
matchingMessages, err := cp.storage.SearchMessages(conversation.ID, 0, pattern, offset, 10)
if err != nil {
log.Errorf("could not fetch matching messages for conversation %d:%d: %s", conversation.ID, 0, err)
}
for _, matchingMessage := range matchingMessages {
// publish this search result...
index, _ := cp.storage.GetRowNumberByMessageID(conversation.ID, 0, matchingMessage.ID)
cp.PublishEvent(event.NewEvent(event.SearchResult, map[event.Field]string{event.SearchID: searchID, event.RowIndex: strconv.Itoa(index), event.ConversationID: strconv.Itoa(conversation.ID), event.Index: strconv.Itoa(matchingMessage.ID)}))
log.Debugf("found matching message: %q", matchingMessage)
}
}
}
}
}
// SearchConversation returns a message from a conversation channel referenced by the absolute ID.
// Note: This should note be used to index a list as the ID is not expected to be tied to absolute position
// in the table (e.g. deleted messages, expired messages, etc.)
func (cp *cwtchPeer) SearchConversations(pattern string) string {
// TODO: For now, we simply surround the pattern with the sqlite LIKE syntax for matching any prefix, and any suffix
// At some point we would like to extend this patternt to support e.g. searching a specific conversation, or
// searching for particular types of message.
pattern = fmt.Sprintf("%%%v%%", pattern)
// we need this lock here to prevent weirdness happening when reassigning cp.cancelSearchContext
cp.mutex.Lock()
defer cp.mutex.Unlock()
if cp.cancelSearchContext != nil {
cp.cancelSearchContext() // Cancel any current searches...
}
ctx, cancel := context.WithCancel(context.Background()) // create a new cancellable contexts...
cp.cancelSearchContext = cancel // save the cancel function...
searchID := event.GetRandNumber().String() // generate a new search id
go cp.doSearch(ctx, searchID, pattern) // perform the search in a new goroutine
return searchID // return the search id so any clients listening to the event bus can associate SearchResult events with this search
}
// GetChannelMessageCount returns the absolute number of messages in a given conversation channel
func (cp *cwtchPeer) GetChannelMessageCount(conversation int, channel int) (int, error) {
return cp.storage.GetChannelMessageCount(conversation, channel)
}
// GetMostRecentMessages returns a selection of messages, ordered by most recently inserted
func (cp *cwtchPeer) GetMostRecentMessages(conversation int, channel int, offset int, limit uint) ([]model.ConversationMessage, error) {
func (cp *cwtchPeer) GetMostRecentMessages(conversation int, channel int, offset int, limit int) ([]model.ConversationMessage, error) {
return cp.storage.GetMostRecentMessages(conversation, channel, offset, limit)
}
@ -1110,13 +905,13 @@ func (cp *cwtchPeer) AddServer(serverSpecification string) (string, error) {
// we haven't seen this key associated with the server before
}
// If we have gotten to this point we can assume this is a safe key bundle signed by the
// server with no conflicting keys. So we are going to save all the keys
// // If we have gotten to this point we can assume this is a safe key bundle signed by the
// // server with no conflicting keys. So we are going to save all the keys
for k, v := range ab {
cp.SetConversationAttribute(conversationInfo.ID, attr.PublicScope.ConstructScopedZonedPath(attr.ServerKeyZone.ConstructZonedPath(k)), v)
}
cp.SetConversationAttribute(conversationInfo.ID, attr.PublicScope.ConstructScopedZonedPath(attr.ServerKeyZone.ConstructZonedPath(string(model.BundleType))), serverSpecification)
cp.QueueJoinServer(onion)
cp.JoinServer(onion)
return onion, err
}
return "", err
@ -1165,23 +960,16 @@ func (cp *cwtchPeer) PeerWithOnion(onion string) {
cp.eventBus.Publish(event.NewEvent(event.QueuePeerRequest, map[event.Field]string{event.RemotePeer: onion, event.LastSeen: lastSeen.Format(time.RFC3339Nano)}))
}
func (cp *cwtchPeer) DisconnectFromPeer(onion string) {
cp.eventBus.Publish(event.NewEvent(event.DisconnectPeerRequest, map[event.Field]string{event.RemotePeer: onion}))
}
func (cp *cwtchPeer) DisconnectFromServer(onion string) {
cp.eventBus.Publish(event.NewEvent(event.DisconnectServerRequest, map[event.Field]string{event.GroupServer: onion}))
}
// QueuePeeringWithOnion sends the request to peer with an onion directly to the contact retry queue; this is a mechanism to not flood tor with circuit requests
// Status: Ready for 1.10
func (cp *cwtchPeer) QueuePeeringWithOnion(handle string) {
lastSeen := event.CwtchEpoch
ci, err := cp.FetchConversationInfo(handle)
if err == nil {
lastSeen := cp.GetConversationLastSeenTime(ci.ID)
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)}))
lastSeen = cp.GetConversationLastSeenTime(ci.ID)
}
if !ci.ACL[ci.Handle].Blocked && ci.Accepted {
cp.eventBus.Publish(event.NewEvent(event.QueuePeerRequest, map[event.Field]string{event.RemotePeer: handle, event.LastSeen: lastSeen.Format(time.RFC3339Nano)}))
}
}
@ -1297,9 +1085,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())
@ -1309,14 +1097,6 @@ func (cp *cwtchPeer) ImportBundle(importString string) error {
// JoinServer manages a new server connection with the given onion address
func (cp *cwtchPeer) JoinServer(onion string) error {
// only connect to servers if the group experiment is enabled.
// note: there are additional checks throughout the app that minimize server interaction
// regardless, and we can only reach this point if groups experiment was at one point enabled
// TODO: this really belongs in an extension, but for legacy reasons groups are more tightly
// integrated into Cwtch. At some point, probably during hybrid groups implementation this
// API should be deprecated in favor of one with much stronger protections.
if cp.IsFeatureEnabled(constants.GroupsExperiment) {
ci, err := cp.FetchConversationInfo(onion)
if ci == nil || err != nil {
return errors.New("no keys found for server connection")
@ -1340,8 +1120,6 @@ func (cp *cwtchPeer) JoinServer(onion string) error {
}
return errors.New("no keys found for server connection")
}
return errors.New("group experiment is not enabled")
}
// MakeAntispamPayment allows a peer to retrigger antispam, important if the initial connection somehow fails...
// TODO in the future we might want to expose this in CwtchPeer interface
@ -1439,7 +1217,7 @@ func (cp *cwtchPeer) getConnectionsSortedByLastSeen(doPeers, doServers bool) []*
continue
}
} else {
if !doPeers {
if !doPeers || !conversation.Accepted {
continue
}
}
@ -1456,16 +1234,13 @@ func (cp *cwtchPeer) StartConnections(doPeers, doServers bool) {
byRecent := cp.getConnectionsSortedByLastSeen(doPeers, doServers)
log.Infof("StartConnections for %v", cp.GetOnion())
for _, conversation := range byRecent {
// only bother tracking servers if the experiment is enabled...
if conversation.model.IsServer() && cp.IsFeatureEnabled(constants.GroupsExperiment) {
if conversation.model.IsServer() {
log.Debugf(" QueueJoinServer(%v)", conversation.model.Handle)
cp.QueueJoinServer(conversation.model.Handle)
} else {
log.Debugf(" QueuePeerWithOnion(%v)", conversation.model.Handle)
if conversation.model.GetPeerAC().AutoConnect {
cp.QueuePeeringWithOnion(conversation.model.Handle)
}
}
time.Sleep(50 * time.Millisecond)
}
}
@ -1528,22 +1303,9 @@ func (cp *cwtchPeer) storeMessage(handle string, message string, sent time.Time)
}
}
// Don't store messages in channel 7
channel := 0
if cm, err := model.DeserializeMessage(message); err == nil {
if cm.IsStream() {
return -1, nil
}
channel = cm.Channel()
}
// Generate a random number and use it as the signature
signature := event.GetRandNumber().String()
return cp.storage.InsertMessage(ci.ID, channel, message, model.Attributes{constants.AttrAuthor: handle, constants.AttrAck: event.True, constants.AttrSentTimestamp: sent.Format(time.RFC3339Nano)}, signature, model.CalculateContentHash(handle, message))
}
func (cp *cwtchPeer) InitChannel(conversation int, channel int) error {
return cp.storage.InitChannelOnConversation(conversation, channel)
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))
}
// eventHandler process events from other subsystems
@ -1558,7 +1320,6 @@ func (cp *cwtchPeer) eventHandler() {
onion, _ := cp.storage.LoadProfileKeyValue(TypeAttribute, attr.PublicScope.ConstructScopedZonedPath(attr.ProfileZone.ConstructZonedPath(constants.Onion)).ToString())
log.Infof("Protocol engine for %s has stopped listening: %v", onion, ev.Data[event.Error])
cp.mutex.Unlock()
cp.processExtensionsEvent(ev)
case event.EncryptedGroupMessage:
// If successful, a side effect is the message is added to the group's timeline
@ -1603,7 +1364,6 @@ func (cp *cwtchPeer) eventHandler() {
case event.NewMessageFromPeerEngine: //event.TimestampReceived, event.RemotePeer, event.Data
ts, _ := time.Parse(time.RFC3339Nano, ev.Data[event.TimestampReceived])
id, err := cp.storeMessage(ev.Data[event.RemotePeer], ev.Data[event.Data], ts)
cp.processExtensionsEvent(ev)
if err == nil {
// Republish as NewMessageFromPeer
ev.EventType = event.NewMessageFromPeer
@ -1647,7 +1407,8 @@ func (cp *cwtchPeer) eventHandler() {
conversationInfo, err := cp.FetchConversationInfo(onion)
log.Debugf("confo info lookup newgetval %v %v %v", onion, conversationInfo, err)
if conversationInfo != nil && conversationInfo.GetPeerAC().ExchangeAttributes {
// only accepted contacts can look up information
if conversationInfo != nil && conversationInfo.Accepted {
// Type Safe Scoped/Zoned Path
zscope := attr.IntoScope(scope)
zone, zpath := attr.ParseZone(zpath)
@ -1679,7 +1440,7 @@ func (cp *cwtchPeer) eventHandler() {
conversationInfo, _ := cp.FetchConversationInfo(handle)
// only accepted contacts can look up information
if conversationInfo != nil && conversationInfo.GetPeerAC().ExchangeAttributes {
if conversationInfo != nil && conversationInfo.Accepted {
// Type Safe Scoped/Zoned Path
zscope := attr.IntoScope(scope)
zone, zpath := attr.ParseZone(zpath)
@ -1700,13 +1461,6 @@ func (cp *cwtchPeer) eventHandler() {
}
case event.PeerStateChange:
handle := ev.Data[event.RemotePeer]
// we need to do this first because calls in the rest of this block may result in
// events that result the UI or bindings fetching new data.
cp.mutex.Lock()
cp.state[handle] = connections.ConnectionStateToType()[ev.Data[event.ConnectionState]]
cp.mutex.Unlock()
if connections.ConnectionStateToType()[ev.Data[event.ConnectionState]] == connections.AUTHENTICATED {
ci, err := cp.FetchConversationInfo(handle)
var cid int
@ -1719,7 +1473,6 @@ 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 {
@ -1734,8 +1487,23 @@ func (cp *cwtchPeer) eventHandler() {
}
// Safe Access to Extensions
cp.processExtensionsEvent(ev)
cp.extensionLock.Lock()
for _, extension := range cp.extensions {
log.Debugf("checking extension...%v", extension)
// check if the current map of experiments satisfies the extension requirements
if !cp.checkExtensionExperiment(extension) {
log.Debugf("skipping extension (%s) ..not all experiments satisfied", extension)
continue
}
if cp.checkEventExperiment(extension, ev.EventType) {
extension.extension.OnEvent(ev, cp)
}
}
cp.extensionLock.Unlock()
cp.mutex.Lock()
cp.state[ev.Data[event.RemotePeer]] = connections.ConnectionStateToType()[ev.Data[event.ConnectionState]]
cp.mutex.Unlock()
case event.ServerStateChange:
cp.mutex.Lock()
prevState := cp.state[ev.Data[event.GroupServer]]
@ -1873,21 +1641,6 @@ func (cp *cwtchPeer) attemptInsertOrAcknowledgeLegacyGroupConversation(conversat
return err
}
// finds a message by a signature by searching across all possible channels returns a channel and a message id
func (cp *cwtchPeer) findChannelMessageBySignature(ci *model.Conversation, signature string) (int, int, error) {
id, err := cp.GetChannelMessageBySignature(ci.ID, constants.CHANNEL_CHAT, signature)
if err == nil {
return constants.CHANNEL_CHAT, id, nil
}
if ci.HasChannel(constants.CHANNEL_MANAGER) {
id, err = cp.GetChannelMessageBySignature(ci.ID, constants.CHANNEL_MANAGER, signature)
if err == nil {
return constants.CHANNEL_CHAT, id, nil
}
}
return -1, -1, err
}
// attemptAcknowledgeP2PConversation is a convenience method that looks up the conversation
// by the given handle and attempts to mark the message as acknowledged. returns error on failure
// to either find the contact or the associated message
@ -1895,19 +1648,21 @@ func (cp *cwtchPeer) attemptAcknowledgeP2PConversation(handle string, signature
ci, err := cp.FetchConversationInfo(handle)
// We should *never* received a peer acknowledgement for a conversation that doesn't exist...
if ci != nil && err == nil {
chid, mid, err := cp.findChannelMessageBySignature(ci, signature)
// for p2p messages the randomly generated event ID is the "signature"
id, err := cp.GetChannelMessageBySignature(ci.ID, 0, signature)
if err == nil {
_, attributes, err := cp.GetChannelMessage(ci.ID, chid, mid)
_, attributes, err := cp.GetChannelMessage(ci.ID, 0, id)
if err == nil {
cp.mutex.Lock()
attributes[constants.AttrAck] = constants.True
cp.mutex.Unlock()
cp.storage.UpdateMessageAttributes(ci.ID, chid, mid, attributes)
cp.eventBus.Publish(event.NewEvent(event.IndexedAcknowledgement, map[event.Field]string{event.ConversationID: strconv.Itoa(ci.ID), event.RemotePeer: ci.Handle, event.Channel: strconv.Itoa(chid), event.Index: strconv.Itoa(mid)}))
cp.storage.UpdateMessageAttributes(ci.ID, 0, id, attributes)
cp.eventBus.Publish(event.NewEvent(event.IndexedAcknowledgement, map[event.Field]string{event.ConversationID: strconv.Itoa(ci.ID), event.RemotePeer: handle, event.Index: strconv.Itoa(id)}))
return nil
}
return err
}
return fmt.Errorf("no such signature error: %x", signature)
return err
}
return err
}
@ -1921,16 +1676,16 @@ func (cp *cwtchPeer) attemptErrorConversationMessage(handle string, signature st
// We should *never* received an error for a conversation that doesn't exist...
if ci != nil && err == nil {
// "signature" here is event ID for peer messages...
chid, mid, err := cp.findChannelMessageBySignature(ci, signature)
id, err := cp.GetChannelMessageBySignature(ci.ID, 0, signature)
if err == nil {
_, attributes, err := cp.GetChannelMessage(ci.ID, chid, mid)
_, attributes, err := cp.GetChannelMessage(ci.ID, 0, id)
if err == nil {
cp.mutex.Lock()
attributes[constants.AttrErr] = constants.True
cp.storage.UpdateMessageAttributes(ci.ID, chid, mid, attributes)
cp.storage.UpdateMessageAttributes(ci.ID, 0, id, attributes)
cp.mutex.Unlock()
// Send a generic indexed failure...
cp.eventBus.Publish(event.NewEvent(event.IndexedFailure, map[event.Field]string{event.ConversationID: strconv.Itoa(ci.ID), event.Handle: handle, event.Error: error, event.Channel: strconv.Itoa(chid), event.Index: strconv.Itoa(mid)}))
cp.eventBus.Publish(event.NewEvent(event.IndexedFailure, map[event.Field]string{event.ConversationID: strconv.Itoa(ci.ID), event.Handle: handle, event.Error: error, event.Index: strconv.Itoa(id)}))
return nil
}
return err
@ -1969,45 +1724,3 @@ func (cp *cwtchPeer) constructGroupFromConversation(conversationInfo *model.Conv
}
return &group, nil
}
// Insert a message directly into a conversation-channel, with the given attributes.
// NOTE: This should only be used by cwtch-extensions. Regular API uses should use more direct methods
// like SendMessage
func (cp *cwtchPeer) InternalInsertMessage(conversation int, channel int, author string, body string, attrs model.Attributes, signature []byte) (int, error) {
signatureB64 := base64.StdEncoding.EncodeToString(signature)
return cp.storage.InsertMessage(conversation, channel, body, attrs, signatureB64, model.CalculateContentHash(author, body))
}
func (cp *cwtchPeer) SignMessage(blob []byte) ([]byte, error) {
privateKey, err := cp.storage.LoadProfileKeyValue(TypePrivateKey, "Ed25519PrivateKey")
if err != nil {
log.Errorf("error loading private key from storage")
return nil, err
}
publicKey, err := cp.storage.LoadProfileKeyValue(TypePublicKey, "Ed25519PublicKey")
if err != nil {
log.Errorf("error loading public key from storage")
return nil, err
}
identity := primitives.InitializeIdentity("", (*ed25519.PrivateKey)(&privateKey), (*ed25519.PublicKey)(&publicKey))
return identity.Sign(blob), nil
}
func (cp *cwtchPeer) processExtensionsEvent(ev event.Event) {
cp.extensionLock.Lock()
for _, extension := range cp.extensions {
// check if the current map of experiments satisfies the extension requirements
if !cp.checkExtensionExperiment(extension) {
log.Debugf("skipping extension (%v) ..not all experiments satisfied", extension)
continue
}
// if the extension is registered for this event type then process
if _, contains := extension.events[ev.EventType]; contains {
extension.extension.OnEvent(ev, cp)
}
}
cp.extensionLock.Unlock()
}

View File

@ -13,7 +13,6 @@ import (
"io"
"os"
"path/filepath"
"strconv"
"strings"
"sync"
)
@ -62,7 +61,6 @@ type CwtchProfileStorage struct {
channelGetMostRecentMessagesStmts map[ChannelID]*sql.Stmt
channelGetMessageByContentHashStmts map[ChannelID]*sql.Stmt
channelRowNumberStmts map[ChannelID]*sql.Stmt
channelSearchConversationSQLStmt map[ChannelID]*sql.Stmt
ProfileDirectory string
db *sql.DB
}
@ -87,7 +85,7 @@ const setConversationACLSQLStmt = `update conversations set ACL=(?) where ID=(?)
const deleteConversationSQLStmt = `delete from conversations where ID=(?);`
// createTableConversationMessagesSQLStmt is a template for creating conversation based tables...
const createTableConversationMessagesSQLStmt = `create table if not exists channel_%d_%d_chat (ID integer unique primary key autoincrement, Body text, Attributes []byte, Expiry datetime, Signature text unique, ContentHash blob text);`
const createTableConversationMessagesSQLStmt = `create table if not exists channel_%d_0_chat (ID integer unique primary key autoincrement, Body text, Attributes []byte, Expiry datetime, Signature text unique, ContentHash blob text);`
// insertMessageIntoConversationSQLStmt is a template for creating conversation based tables...
const insertMessageIntoConversationSQLStmt = `insert into channel_%d_%d_chat (Body, Attributes, Signature, ContentHash) values(?,?,?,?);`
@ -116,9 +114,6 @@ const getMessageCountFromConversationSQLStmt = `select count(*) from channel_%d_
// getMostRecentMessagesSQLStmt is a template for fetching the most recent N messages in a conversation channel
const getMostRecentMessagesSQLStmt = `select ID, Body, Attributes, Signature, ContentHash from channel_%d_%d_chat order by ID desc limit (?) offset (?);`
// searchConversationSQLStmt is a template for search a conversation for the most recent N messages matching a given pattern
const searchConversationSQLStmt = `select ID, Body, Attributes, Signature, ContentHash from (select ID, Body, Attributes, Signature, ContentHash from channel_%d_%d_chat order by ID desc limit (?) offset (?)) where BODY like (?)`
// NewCwtchProfileStorage constructs a new CwtchProfileStorage from a database. It is also responsible for
// Preparing commonly used SQL Statements
func NewCwtchProfileStorage(db *sql.DB, profileDirectory string) (*CwtchProfileStorage, error) {
@ -227,7 +222,6 @@ func NewCwtchProfileStorage(db *sql.DB, profileDirectory string) (*CwtchProfileS
channelGetMostRecentMessagesStmts: map[ChannelID]*sql.Stmt{},
channelGetCountStmts: map[ChannelID]*sql.Stmt{},
channelRowNumberStmts: map[ChannelID]*sql.Stmt{},
channelSearchConversationSQLStmt: map[ChannelID]*sql.Stmt{},
},
nil
}
@ -324,7 +318,7 @@ func (cps *CwtchProfileStorage) NewConversation(handle string, attributes model.
return -1, tx.Rollback()
}
result, err = tx.Exec(fmt.Sprintf(createTableConversationMessagesSQLStmt, id, 0))
result, err = tx.Exec(fmt.Sprintf(createTableConversationMessagesSQLStmt, id))
if err != nil {
log.Errorf("error executing transaction: %v", err)
return -1, tx.Rollback()
@ -345,27 +339,6 @@ func (cps *CwtchProfileStorage) NewConversation(handle string, attributes model.
return int(conversationID), nil
}
func (cps *CwtchProfileStorage) InitChannelOnConversation(id int, channel int) error {
cps.mutex.Lock()
defer cps.mutex.Unlock()
tx, err := cps.db.Begin()
if err != nil {
log.Errorf("error executing transaction: %v", err)
return tx.Rollback()
}
_, err = tx.Exec(fmt.Sprintf(createTableConversationMessagesSQLStmt, id, channel))
if err != nil {
log.Errorf("error executing transaction: %v", err)
return tx.Rollback()
}
err = tx.Commit()
if err != nil {
log.Errorf("error executing transaction: %v", err)
return tx.Rollback()
}
return nil
}
// GetConversationByHandle is a convenience method to fetch an active conversation by a handle
// Usage Notes: This should **only** be used to look up p2p conversations by convention.
// Ideally this function should not exist, and all lookups should happen by ID (this is currently
@ -397,12 +370,7 @@ func (cps *CwtchProfileStorage) GetConversationByHandle(handle string) (*model.C
}
rows.Close()
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
return &model.Conversation{ID: id, Handle: handle, ACL: model.DeserializeAccessControlList(acl), Attributes: model.DeserializeAttributes(attributes), Accepted: accepted}, nil
}
// FetchConversations returns *all* active conversations. This method should only be called
@ -438,13 +406,7 @@ func (cps *CwtchProfileStorage) FetchConversations() ([]*model.Conversation, err
rows.Close()
return nil, err
}
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})
conversations = append(conversations, &model.Conversation{ID: id, Handle: handle, ACL: model.DeserializeAccessControlList(acl), Attributes: model.DeserializeAttributes(attributes), Accepted: accepted})
}
}
@ -477,12 +439,7 @@ func (cps *CwtchProfileStorage) GetConversation(id int) (*model.Conversation, er
}
rows.Close()
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
return &model.Conversation{ID: id, Handle: handle, ACL: model.DeserializeAccessControlList(acl), Attributes: model.DeserializeAttributes(attributes), Accepted: accepted}, nil
}
// AcceptConversation sets the accepted status of a conversation to true in the backing datastore
@ -778,47 +735,8 @@ func (cps *CwtchProfileStorage) GetChannelMessageCount(conversation int, channel
return count, nil
}
func (cps *CwtchProfileStorage) SearchMessages(conversation int, channel int, pattern string, offset int, limit int) ([]model.ConversationMessage, error) {
channelID := ChannelID{Conversation: conversation, Channel: channel}
cps.mutex.Lock()
defer cps.mutex.Unlock()
_, exists := cps.channelSearchConversationSQLStmt[channelID]
if !exists {
conversationStmt, err := cps.db.Prepare(fmt.Sprintf(searchConversationSQLStmt, conversation, channel))
if err != nil {
log.Errorf("error executing transaction: %v", err)
return nil, err
}
cps.channelSearchConversationSQLStmt[channelID] = conversationStmt
}
rows, err := cps.channelSearchConversationSQLStmt[channelID].Query(limit, offset, pattern)
if err != nil {
log.Errorf("error executing prepared stmt: %v", err)
return nil, err
}
var conversationMessages []model.ConversationMessage
defer rows.Close()
for {
result := rows.Next()
if !result {
return conversationMessages, nil
}
var id int
var body string
var attributes []byte
var sig string
var contenthash string
err = rows.Scan(&id, &body, &attributes, &sig, &contenthash)
if err != nil {
return conversationMessages, err
}
conversationMessages = append(conversationMessages, model.ConversationMessage{ID: id, Body: body, Attr: model.DeserializeAttributes(attributes), Signature: sig, ContentHash: contenthash})
}
}
// GetMostRecentMessages returns the most recent messages in a channel up to a given limit at a given offset
func (cps *CwtchProfileStorage) GetMostRecentMessages(conversation int, channel int, offset int, limit uint) ([]model.ConversationMessage, error) {
func (cps *CwtchProfileStorage) GetMostRecentMessages(conversation int, channel int, offset int, limit int) ([]model.ConversationMessage, error) {
channelID := ChannelID{Conversation: conversation, Channel: channel}
cps.mutex.Lock()
@ -873,30 +791,12 @@ func (cps *CwtchProfileStorage) PurgeConversationChannel(conversation int, chann
// PurgeNonSavedMessages deletes all message conversations that are not explicitly set to saved.
func (cps *CwtchProfileStorage) PurgeNonSavedMessages() {
// check to see if the profile global setting has been explicitly set to save (peer) conversations by default.
defaultSave := false
key, err := cps.LoadProfileKeyValue(TypeAttribute, attr.LocalScope.ConstructScopedZonedPath(attr.ProfileZone.ConstructZonedPath(event.PreserveHistoryDefaultSettingKey)).ToString())
if err == nil {
if defaultSaveSetting, err := strconv.ParseBool(string(key)); err == nil {
defaultSave = defaultSaveSetting
}
}
// For each conversation, all that is not explicitly saved will be lost...
// Purge Messages that are not stored...
ci, err := cps.FetchConversations()
if err == nil {
for _, conversation := range ci {
// unless this is a server or a group...for which we default save always (for legacy reasons)
// FIXME: revisit this for hybrid groups.
if !conversation.IsGroup() && !conversation.IsServer() {
// Note that we only check for confirmed status here...if it is set to any other value we will fallthrough to the default.
saveHistoryConfirmed := conversation.Attributes[attr.LocalScope.ConstructScopedZonedPath(attr.ProfileZone.ConstructZonedPath(event.SaveHistoryKey)).ToString()] == event.SaveHistoryConfirmed
deleteHistoryConfirmed := conversation.Attributes[attr.LocalScope.ConstructScopedZonedPath(attr.ProfileZone.ConstructZonedPath(event.SaveHistoryKey)).ToString()] == event.DeleteHistoryConfirmed
// we purge conversation history in two specific instances...
// if the conversation has been explicitly marked as delete history confirmed OR
// if save history hasn't been confirmed and default save history is false - i.e. in all other cases
if deleteHistoryConfirmed || (!saveHistoryConfirmed && !defaultSave) {
if conversation.Attributes[attr.LocalScope.ConstructScopedZonedPath(attr.ProfileZone.ConstructZonedPath(event.SaveHistoryKey)).ToString()] != event.SaveHistoryConfirmed {
log.Debugf("purging conversation...")
// TODO: At some point in the future this needs to iterate over channels and make a decision for each on..
cps.PurgeConversationChannel(conversation.ID, 0)

View File

@ -20,9 +20,7 @@ type ModifyPeeringState interface {
BlockUnknownConnections()
AllowUnknownConnections()
PeerWithOnion(string)
QueueJoinServer(string)
DisconnectFromPeer(string)
DisconnectFromServer(string)
JoinServer(string) error
}
// ModifyContactsAndPeers is a meta-interface intended to restrict a call to reading and modifying contacts
@ -50,8 +48,6 @@ type ModifyServers interface {
// SendMessages enables a caller to sender messages to a contact
type SendMessages interface {
// SendMessage sends a raw message to the conversation.
// SendMessage is a deprecated public API. Use EnhancedSendMessage instead
SendMessage(conversation int, message string) (int, error)
// EnhancedSendMessage Attempts to Send a Message and Immediately Attempts to Lookup the Message in the Database
@ -117,40 +113,24 @@ type CwtchPeer interface {
EnhancedImportBundle(string) string
// New Unified Conversation Interfaces
NewConversation(handle string, acl model.AccessControlList) (int, error)
InitChannel(conversation int, channel int) error
NewContactConversation(handle string, acl model.AccessControl, accepted bool) (int, error)
FetchConversations() ([]*model.Conversation, error)
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)
SetConversationAttributeInt(conversation int, path attr.ScopedZonedPath, value int) error
GetConversationAttributeInt(conversation int, path attr.ScopedZonedPath) (int, error)
DeleteConversation(conversation int) error
// New Unified Conversation Channel Interfaces
GetChannelMessage(conversation int, channel int, id int) (string, model.Attributes, error)
GetChannelMessageCount(conversation int, channel int) (int, error)
GetChannelMessageByContentHash(conversation int, channel int, contenthash string) (int, error)
GetChannelMessageBySignature(conversationID int, channelID int, signature string) (int, error)
GetMostRecentMessages(conversation int, channel int, offset int, limit uint) ([]model.ConversationMessage, error)
GetMostRecentMessages(conversation int, channel int, offset int, limit int) ([]model.ConversationMessage, error)
UpdateMessageAttribute(conversation int, channel int, id int, key string, value string) error
SearchConversations(pattern string) string
// EnhancedGetMessageById returns a json-encoded enhanced message, suitable for rendering in a UI
EnhancedGetMessageById(conversation int, mid int) string
@ -159,7 +139,7 @@ type CwtchPeer interface {
EnhancedGetMessageByContentHash(conversation int, hash string) string
// EnhancedGetMessages returns a set of json-encoded enhanced messages, suitable for rendering in a UI
EnhancedGetMessages(conversation int, index int, count uint) string
EnhancedGetMessages(conversation int, index int, count int) string
// Server Token APIS
// TODO move these to feature protected interfaces
@ -175,10 +155,6 @@ type CwtchPeer interface {
UpdateExperiments(enabled bool, experiments map[string]bool)
NotifySettingsUpdate(settings settings.GlobalSettings)
IsFeatureEnabled(featureName string) bool
SignMessage(blob []byte) ([]byte, error)
// Used for Internal Bookkeeping by Extensions, **do not expose in autobindings**
InternalInsertMessage(conversation int, channel int, author string, body string, attributes model.Attributes, signature []byte) (int, error)
}
// EnhancedMessage wraps a Cwtch model.Message with some additional data to reduce calls from the UI.

View File

@ -122,8 +122,6 @@ func NewProtocolEngine(identity primitives.Identity, privateKey ed25519.PrivateK
engine.eventManager.Subscribe(event.UpdateConversationAuthorization, engine.queue)
engine.eventManager.Subscribe(event.BlockUnknownPeers, engine.queue)
engine.eventManager.Subscribe(event.AllowUnknownPeers, engine.queue)
engine.eventManager.Subscribe(event.DisconnectPeerRequest, engine.queue)
engine.eventManager.Subscribe(event.DisconnectServerRequest, engine.queue)
// File Handling
engine.eventManager.Subscribe(event.ShareManifest, engine.queue)
@ -151,7 +149,6 @@ func (e *engine) EventManager() event.Manager {
// eventHandler process events from other subsystems
func (e *engine) eventHandler() {
log.Debugf("restartFlow Launching ProtocolEngine listener")
for {
ev := e.queue.Next()
// optimistic shutdown...
@ -162,7 +159,6 @@ func (e *engine) eventHandler() {
case event.StatusRequest:
e.eventManager.Publish(event.Event{EventType: event.ProtocolEngineStatus, EventID: ev.EventID})
case event.PeerRequest:
log.Debugf("restartFlow Handling Peer Request")
if torProvider.IsValidHostname(ev.Data[event.RemotePeer]) {
go e.peerWithOnion(ev.Data[event.RemotePeer])
}
@ -196,10 +192,6 @@ func (e *engine) eventHandler() {
// We remove this peer from out blocklist which will prevent them from contacting us if we have "block unknown peers" turned on.
e.authorizations.Delete(ev.Data[event.RemotePeer])
e.deleteConnection(onion)
case event.DisconnectPeerRequest:
e.deleteConnection(ev.Data[event.RemotePeer])
case event.DisconnectServerRequest:
e.leaveServer(ev.Data[event.GroupServer])
case event.SendMessageToGroup:
ciphertext, _ := base64.StdEncoding.DecodeString(ev.Data[event.Ciphertext])
signature, _ := base64.StdEncoding.DecodeString(ev.Data[event.Signature])
@ -341,7 +333,6 @@ func (e *engine) listenFn() {
func (e *engine) Shutdown() {
// don't accept any more events...
e.queue.Publish(event.NewEvent(event.ProtocolEngineShutdown, map[event.Field]string{}))
e.eventManager.Publish(event.NewEvent(event.ProtocolEngineShutdown, map[event.Field]string{}))
e.service.Shutdown()
e.shuttingDown.Store(true)
e.ephemeralServicesLock.Lock()
@ -350,11 +341,10 @@ func (e *engine) Shutdown() {
log.Infof("shutting down ephemeral service")
// work around: service.shutdown() can block for a long time if it is Open()ing a new connection, putting it in a
// goroutine means we can perform this operation and let the per service shutdown in their own time or until the app exits
conn := connection // don't capture loop variable
go func() {
conn.connectingLock.Lock()
conn.service.Shutdown()
conn.connectingLock.Unlock()
connection.connectingLock.Lock()
connection.service.Shutdown()
connection.connectingLock.Unlock()
}()
}
@ -368,32 +358,25 @@ func (e *engine) peerWithOnion(onion string) {
if !e.isBlocked(onion) {
e.ignoreOnShutdown(e.peerConnecting)(onion)
connected, err := e.service.Connect(onion, e.createPeerTemplate())
if connected && err == nil {
// on success CwtchPeer will handle Auth and other status updates
// early exit from this function...
return
}
// If we are already connected...check if we are authed and issue an auth event
// (This allows the ui to be stateless)
if connected && err != nil {
conn, err := e.service.WaitForCapabilityOrClose(onion, cwtchCapability)
conn, err := e.service.GetConnection(onion)
if err == nil {
if conn.HasCapability(cwtchCapability) {
e.ignoreOnShutdown(e.peerAuthed)(onion)
return
}
log.Errorf("PeerWithOnion something went very wrong...%v %v", onion, err)
if conn != nil {
conn.Close()
}
e.ignoreOnShutdown(e.peerDisconnected)(onion)
} else {
}
// Only issue a disconnected error if we are disconnected (Connect will fail if a connection already exists)
if !connected && err != nil {
e.ignoreOnShutdown(e.peerDisconnected)(onion)
}
}
}
e.ignoreOnShutdown(e.peerDisconnected)(onion)
}
func (e *engine) makeAntispamPayment(onion string) {
log.Debugf("making antispam payment")
@ -470,10 +453,6 @@ func (e *engine) peerWithTokenServer(onion string, tokenServerOnion string, toke
e.ignoreOnShutdown(e.serverAuthed)(onion)
return
}
// if we are not authed or synced then we are stuck...
e.ignoreOnShutdown(e.serverConnecting)(onion)
log.Errorf("server connection attempt issued to active connection")
}
}
@ -749,16 +728,6 @@ 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,14 +2,12 @@ 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")
@ -135,14 +133,6 @@ 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)
}
}
@ -158,15 +148,6 @@ 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

@ -13,7 +13,7 @@ type ChunkSpec []uint64
// CreateChunkSpec given a full list of chunks with their downloaded status (true for downloaded, false otherwise)
// derives a list of identifiers of chunks that have not been downloaded yet
func CreateChunkSpec(progress []bool) ChunkSpec {
chunks := ChunkSpec{}
var chunks ChunkSpec
for i, p := range progress {
if !p {
chunks = append(chunks, uint64(i))

View File

@ -93,12 +93,7 @@ func TestManifestLarge(t *testing.T) {
}
// Prepare Download
cwtchPngOutManifest, err := LoadManifest("testdata/cwtch.png.manifest")
if err != nil {
t.Fatalf("could not prepare download %v", err)
}
cwtchPngOutManifest, _ := LoadManifest("testdata/cwtch.png.manifest")
cwtchPngOutManifest.FileName = "testdata/cwtch.out.png"
defer cwtchPngOutManifest.Close()

View File

@ -35,7 +35,6 @@ type GlobalSettings struct {
Locale string
Theme string
ThemeMode string
ThemeImages bool
PreviousPid int64
ExperimentsEnabled bool
Experiments map[string]bool
@ -58,14 +57,11 @@ type GlobalSettings struct {
TorCacheDir string
BlodeuweddPath string
FontScaling float64
DefaultSaveHistory bool
}
var DefaultGlobalSettings = GlobalSettings{
Locale: "en",
Theme: "cwtch",
ThemeMode: "dark",
ThemeImages: false,
Theme: "dark",
PreviousPid: -1,
ExperimentsEnabled: false,
Experiments: map[string]bool{constants.MessageFormattingExperiment: true},
@ -87,7 +83,6 @@ var DefaultGlobalSettings = GlobalSettings{
TorCacheDir: "",
BlodeuweddPath: "",
FontScaling: 1.0, // use the system pixel scaling default
DefaultSaveHistory: false,
}
func InitGlobalSettingsFile(directory string, password string) (*GlobalSettingsFile, error) {
@ -136,8 +131,6 @@ func (globalSettingsFile *GlobalSettingsFile) ReadGlobalSettings() GlobalSetting
return settings //firstTime = true
}
// note: by giving json.Unmarshal settings we are providing it defacto defaults
// from DefaultGlobalSettings
err = json.Unmarshal(settingsBytes, &settings)
if err != nil {
log.Errorf("Could not parse global ui settings: %v\n", err)

View File

@ -67,9 +67,7 @@ func (ps *ProfileStoreV1) load() error {
if contact.Attributes[event.SaveHistoryKey] == event.SaveHistoryConfirmed {
ss := NewStreamStore(ps.directory, contact.LocalID, ps.key)
if contact, exists := cp.Contacts[contact.Onion]; exists {
contact.Timeline.SetMessages(ss.Read())
}
cp.Contacts[contact.Onion].Timeline.SetMessages(ss.Read())
}
}
@ -80,10 +78,8 @@ func (ps *ProfileStoreV1) load() error {
continue
}
ss := NewStreamStore(ps.directory, group.LocalID, ps.key)
if group, exists := cp.Groups[gid]; exists {
group.Timeline.SetMessages(ss.Read())
group.Timeline.Sort()
}
cp.Groups[gid].Timeline.SetMessages(ss.Read())
cp.Groups[gid].Timeline.Sort()
}
}

View File

@ -64,6 +64,7 @@ func TestFileSharing(t *testing.T) {
os.MkdirAll(dataDir, 0700)
// we don't need real randomness for the port, just to avoid a possible conflict...
mrand.Seed(int64(time.Now().Nanosecond()))
socksPort := mrand.Intn(1000) + 9051
controlPort := mrand.Intn(1000) + 9052
@ -98,10 +99,7 @@ func TestFileSharing(t *testing.T) {
app := app2.NewApp(acn, "./storage", app2.LoadAppSettings("./storage"))
usr, err := user.Current()
if err != nil {
t.Fatalf("current user is undefined")
}
usr, _ := user.Current()
cwtchDir := path.Join(usr.HomeDir, ".cwtch")
os.Mkdir(cwtchDir, 0700)
os.RemoveAll(path.Join(cwtchDir, "testing"))
@ -116,10 +114,8 @@ func TestFileSharing(t *testing.T) {
t.Logf("** Waiting for Alice, Bob...")
alice := app2.WaitGetPeer(app, "alice")
app.ActivatePeerEngine(alice.GetOnion())
app.ConfigureConnections(alice.GetOnion(), true, true, true)
bob := app2.WaitGetPeer(app, "bob")
app.ActivatePeerEngine(bob.GetOnion())
app.ConfigureConnections(bob.GetOnion(), true, true, true)
alice.AutoHandleEvents([]event.Type{event.PeerStateChange, event.NewRetValMessageFromPeer})
bob.AutoHandleEvents([]event.Type{event.PeerStateChange, event.NewRetValMessageFromPeer})
@ -145,23 +141,10 @@ 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)
err = alice.AcceptConversation(1)
if err != nil {
t.Fatalf("Error!: %v", err)
}
err = bob.AcceptConversation(1)
if err != nil {
t.Fatalf("Error!: %v", err)
}
alice.AcceptConversation(1)
bob.AcceptConversation(1)
t.Logf("Alice and Bob are Connected!!")
filesharingFunctionality := filesharing.FunctionalityGate()
@ -178,11 +161,11 @@ func TestFileSharing(t *testing.T) {
// testBobDownloadFile(t, bob, filesharingFunctionality, queueOracle)
// Wait for say...
time.Sleep(30 * time.Second)
time.Sleep(10 * time.Second)
if _, err := os.Stat(path.Join(settings.DownloadPath, "cwtch.png")); errors.Is(err, os.ErrNotExist) {
// path/to/whatever does not exist
t.Fatalf("cwtch.png should have been automatically downloaded...")
t.Fatalf("cwthc.png should have been automatically downloadeded...")
}
app.Shutdown()

View File

@ -34,6 +34,26 @@ var (
carolLines = []string{"Howdy, thanks!"}
)
func waitForConnection(t *testing.T, peer peer.CwtchPeer, addr string, target connections.ConnectionState) {
peerName, _ := peer.GetScopedZonedAttribute(attr.LocalScope, attr.ProfileZone, constants.Name)
for {
log.Infof("%v checking connection...\n", peerName)
state := peer.GetPeerState(addr)
log.Infof("Waiting for Peer %v to %v - state: %v\n", peerName, addr, connections.ConnectionStateName[state])
if state == connections.FAILED {
t.Fatalf("%v could not connect to %v", peer.GetOnion(), addr)
}
if state != target {
log.Infof("peer %v %v waiting connect %v, currently: %v\n", peerName, peer.GetOnion(), addr, connections.ConnectionStateName[state])
time.Sleep(time.Second * 5)
continue
} else {
log.Infof("peer %v %v CONNECTED to %v\n", peerName, peer.GetOnion(), addr)
break
}
}
}
func waitForRetVal(peer peer.CwtchPeer, convId int, szp attr.ScopedZonedPath) {
for {
_, err := peer.GetConversationAttribute(convId, szp)
@ -79,6 +99,7 @@ func TestCwtchPeerIntegration(t *testing.T) {
os.MkdirAll(dataDir, 0700)
// we don't need real randomness for the port, just to avoid a possible conflict...
mrand.Seed(int64(time.Now().Nanosecond()))
socksPort := mrand.Intn(1000) + 9051
controlPort := mrand.Intn(1000) + 9052
@ -129,11 +150,6 @@ func TestCwtchPeerIntegration(t *testing.T) {
numGoRoutinesPostAppStart := runtime.NumGoroutine()
// ***** cwtchPeer setup *****
// Turn on Groups Experiment...
settings := app.ReadSettings()
settings.ExperimentsEnabled = true
settings.Experiments[constants.GroupsExperiment] = true
app.UpdateSettings(settings)
log.Infoln("Creating Alice...")
app.CreateProfile("Alice", "asdfasdf", true)
@ -147,7 +163,6 @@ func TestCwtchPeerIntegration(t *testing.T) {
alice := app2.WaitGetPeer(app, "Alice")
aliceBus := app.GetEventBus(alice.GetOnion())
app.ActivatePeerEngine(alice.GetOnion())
app.ConfigureConnections(alice.GetOnion(), true, true, true)
log.Infoln("Alice created:", alice.GetOnion())
// alice.SetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants.Name, "Alice") <- This is now done automatically by ProfileValueExtension, keeping this here for clarity
alice.AutoHandleEvents([]event.Type{event.PeerStateChange, event.ServerStateChange, event.NewGroupInvite, event.NewRetValMessageFromPeer})
@ -155,7 +170,6 @@ func TestCwtchPeerIntegration(t *testing.T) {
bob := app2.WaitGetPeer(app, "Bob")
bobBus := app.GetEventBus(bob.GetOnion())
app.ActivatePeerEngine(bob.GetOnion())
app.ConfigureConnections(bob.GetOnion(), true, true, true)
log.Infoln("Bob created:", bob.GetOnion())
// bob.SetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants.Name, "Bob") <- This is now done automatically by ProfileValueExtension, keeping this here for clarity
bob.AutoHandleEvents([]event.Type{event.PeerStateChange, event.ServerStateChange, event.NewGroupInvite, event.NewRetValMessageFromPeer})
@ -163,7 +177,6 @@ func TestCwtchPeerIntegration(t *testing.T) {
carol := app2.WaitGetPeer(app, "Carol")
carolBus := app.GetEventBus(carol.GetOnion())
app.ActivatePeerEngine(carol.GetOnion())
app.ConfigureConnections(carol.GetOnion(), true, true, true)
log.Infoln("Carol created:", carol.GetOnion())
// carol.SetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants.Name, "Carol") <- This is now done automatically by ProfileValueExtension, keeping this here for clarity
carol.AutoHandleEvents([]event.Type{event.PeerStateChange, event.ServerStateChange, event.NewGroupInvite, event.NewRetValMessageFromPeer})
@ -216,10 +229,10 @@ func TestCwtchPeerIntegration(t *testing.T) {
t.Fatalf("Alice password did not change...")
}
WaitForConnection(t, alice, bob.GetOnion(), connections.AUTHENTICATED)
WaitForConnection(t, alice, carol.GetOnion(), connections.AUTHENTICATED)
WaitForConnection(t, bob, alice.GetOnion(), connections.AUTHENTICATED)
WaitForConnection(t, carol, alice.GetOnion(), connections.AUTHENTICATED)
waitForConnection(t, alice, bob.GetOnion(), connections.AUTHENTICATED)
waitForConnection(t, alice, carol.GetOnion(), connections.AUTHENTICATED)
waitForConnection(t, bob, alice.GetOnion(), connections.AUTHENTICATED)
waitForConnection(t, carol, alice.GetOnion(), connections.AUTHENTICATED)
log.Infof("Alice and Bob getVal public.name...")
@ -303,9 +316,9 @@ func TestCwtchPeerIntegration(t *testing.T) {
}
log.Infof("Waiting for alice to join server...")
WaitForConnection(t, alice, ServerAddr, connections.SYNCED)
waitForConnection(t, alice, ServerAddr, connections.SYNCED)
log.Infof("Waiting for Bob to join connect to group server...")
WaitForConnection(t, bob, ServerAddr, connections.SYNCED)
waitForConnection(t, bob, ServerAddr, connections.SYNCED)
// 1 = Alice
// 2 = Server
@ -331,7 +344,7 @@ func TestCwtchPeerIntegration(t *testing.T) {
if len(cachedTokens) > (usedTokens + len(carolLines)) {
carol.StoreCachedTokens(ServerAddr, cachedTokens[usedTokens:usedTokens+len(carolLines)])
}
WaitForConnection(t, carol, ServerAddr, connections.SYNCED)
waitForConnection(t, carol, ServerAddr, connections.SYNCED)
numGoRoutinesPostCarolConnect := runtime.NumGoroutine()
// Check Alice Timeline
@ -370,10 +383,6 @@ func TestCwtchPeerIntegration(t *testing.T) {
checkMessage(t, carol, carolGroupConversationID, 5, carolLines[0])
checkMessage(t, carol, carolGroupConversationID, 6, bobLines[2])
// Have bob clean up some conversations...
log.Infof("Bob cleanup conversation")
bob.DeleteConversation(1)
log.Infof("Shutting down Bob...")
app.ShutdownPeer(bob.GetOnion())
time.Sleep(time.Second * 3)

View File

@ -29,6 +29,7 @@ func TestEncryptedStorage(t *testing.T) {
os.MkdirAll(dataDir, 0700)
// we don't need real randomness for the port, just to avoid a possible conflict...
mrand.Seed(int64(time.Now().Nanosecond()))
socksPort := mrand.Intn(1000) + 9051
controlPort := mrand.Intn(1000) + 9052
@ -98,10 +99,6 @@ func TestEncryptedStorage(t *testing.T) {
ci, err = bob.FetchConversationInfo(alice.GetOnion())
}
if ci == nil {
t.Fatalf("could not fetch bobs conversation")
}
body, _, err := bob.GetChannelMessage(ci.ID, 0, 1)
if body != "Hello Bob" || err != nil {
t.Fatalf("unexpected message in conversation channel %v %v", body, err)

View File

@ -2,12 +2,6 @@ package filesharing
import (
"crypto/rand"
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
"path/filepath"
app2 "cwtch.im/cwtch/app"
"cwtch.im/cwtch/event"
"cwtch.im/cwtch/functionality/filesharing"
@ -18,8 +12,13 @@ import (
"cwtch.im/cwtch/protocol/connections"
"cwtch.im/cwtch/protocol/files"
utils2 "cwtch.im/cwtch/utils"
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
"git.openprivacy.ca/openprivacy/connectivity/tor"
"git.openprivacy.ca/openprivacy/log"
"path/filepath"
// Import SQL Cipher
mrand "math/rand"
@ -59,13 +58,13 @@ func TestFileSharing(t *testing.T) {
os.RemoveAll("cwtch.out.png.manifest")
log.SetLevel(log.LevelDebug)
log.ExcludeFromPattern("tapir")
os.Mkdir("tordir", 0700)
dataDir := path.Join("tordir", "tor")
os.MkdirAll(dataDir, 0700)
// we don't need real randomness for the port, just to avoid a possible conflict...
mrand.Seed(int64(time.Now().Nanosecond()))
socksPort := mrand.Intn(1000) + 9051
controlPort := mrand.Intn(1000) + 9052
@ -100,10 +99,7 @@ func TestFileSharing(t *testing.T) {
app := app2.NewApp(acn, "./storage", app2.LoadAppSettings("./storage"))
usr, err := user.Current()
if err != nil {
t.Fatalf("current user is undefined")
}
usr, _ := user.Current()
cwtchDir := path.Join(usr.HomeDir, ".cwtch")
os.Mkdir(cwtchDir, 0700)
os.RemoveAll(path.Join(cwtchDir, "testing"))
@ -118,25 +114,14 @@ func TestFileSharing(t *testing.T) {
t.Logf("** Waiting for Alice, Bob...")
alice := app2.WaitGetPeer(app, "alice")
app.ActivatePeerEngine(alice.GetOnion())
app.ConfigureConnections(alice.GetOnion(), true, true, true)
bob := app2.WaitGetPeer(app, "bob")
app.ActivatePeerEngine(bob.GetOnion())
app.ConfigureConnections(bob.GetOnion(), true, true, true)
alice.AutoHandleEvents([]event.Type{event.PeerStateChange, event.NewRetValMessageFromPeer})
bob.AutoHandleEvents([]event.Type{event.PeerStateChange, event.NewRetValMessageFromPeer})
aliceQueueOracle := event.NewQueue()
aliceEb := app.GetEventBus(alice.GetOnion())
if aliceEb == nil {
t.Fatalf("alice's eventbus is undefined")
}
aliceEb.Subscribe(event.SearchResult, aliceQueueOracle)
queueOracle := event.NewQueue()
bobEb := app.GetEventBus(bob.GetOnion())
if bobEb == nil {
t.Fatalf("bob's eventbus is undefined")
}
bobEb.Subscribe(event.FileDownloaded, queueOracle)
app.GetEventBus(bob.GetOnion()).Subscribe(event.FileDownloaded, queueOracle)
// Turn on File Sharing Experiment...
settings := app.ReadSettings()
@ -151,39 +136,25 @@ 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()
_, fileSharingMessage, err := filesharingFunctionality.ShareFile("cwtch.png", alice)
alice.SendMessage(1, fileSharingMessage)
if err != nil {
t.Fatalf("Error!: %v", err)
}
alice.SendMessage(1, fileSharingMessage)
// 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")
// Wait for the messages to arrive...
time.Sleep(time.Second * 20)
alice.SearchConversations("test")
results := 0
for {
ev := aliceQueueOracle.Next()
if ev.EventType != event.SearchResult {
t.Fatalf("Expected a search result vent")
}
results += 1
t.Logf("found search result (%d)....%v", results, ev)
if results == 2 {
break
}
}
time.Sleep(time.Second * 10)
// test that bob can download and verify the file
testBobDownloadFile(t, bob, filesharingFunctionality, queueOracle)
@ -209,7 +180,6 @@ func TestFileSharing(t *testing.T) {
// test that we can delete bob...
app.DeleteProfile(bob.GetOnion(), "asdfasdf")
aliceQueueOracle.Shutdown()
queueOracle.Shutdown()
app.Shutdown()
acn.Close()
@ -231,6 +201,7 @@ func testBobDownloadFile(t *testing.T, bob peer.CwtchPeer, filesharingFunctional
os.RemoveAll("cwtch.out.png")
os.RemoveAll("cwtch.out.png.manifest")
bob.AcceptConversation(1)
message, _, err := bob.GetChannelMessage(1, 0, 1)
if err != nil {
t.Fatalf("could not find file sharing message: %v", err)

View File

@ -1,214 +0,0 @@
package testing
import (
"crypto/rand"
"encoding/base64"
"fmt"
mrand "math/rand"
"os"
"path"
"path/filepath"
"runtime"
"runtime/pprof"
"testing"
"time"
app2 "cwtch.im/cwtch/app"
"cwtch.im/cwtch/event"
"cwtch.im/cwtch/functionality/hybrid"
"cwtch.im/cwtch/functionality/inter"
"cwtch.im/cwtch/model/constants"
"cwtch.im/cwtch/peer"
"cwtch.im/cwtch/protocol/connections"
"git.openprivacy.ca/openprivacy/connectivity/tor"
"git.openprivacy.ca/openprivacy/log"
_ "github.com/mutecomm/go-sqlcipher/v4"
)
func TestHyrbidGroupIntegration(t *testing.T) {
t.Logf("Starting Hybrid Groups Test")
os.RemoveAll("./storage")
os.RemoveAll("./managerstorage")
// Goroutine Monitoring Start..
numGoRoutinesStart := runtime.NumGoroutine()
log.AddEverythingFromPattern("connectivity")
log.SetLevel(log.LevelInfo)
log.ExcludeFromPattern("connection/connection")
log.ExcludeFromPattern("outbound/3dhauthchannel")
log.ExcludeFromPattern("event/eventmanager")
log.ExcludeFromPattern("tapir")
os.Mkdir("tordir", 0700)
dataDir := path.Join("tordir", "tor")
os.MkdirAll(dataDir, 0700)
// we don't need real randomness for the port, just to avoid a possible conflict...
socksPort := mrand.Intn(1000) + 9051
controlPort := mrand.Intn(1000) + 9052
// generate a random password
key := make([]byte, 64)
_, err := rand.Read(key)
if err != nil {
panic(err)
}
useCache := os.Getenv("TORCACHE") == "true"
torDataDir := ""
if useCache {
log.Infof("using tor cache")
torDataDir = filepath.Join(dataDir, "data-dir-torcache")
os.MkdirAll(torDataDir, 0700)
} else {
log.Infof("using clean tor data dir")
if torDataDir, err = os.MkdirTemp(dataDir, "data-dir-"); err != nil {
t.Fatalf("could not create data dir")
}
}
tor.NewTorrc().WithSocksPort(socksPort).WithOnionTrafficOnly().WithHashedPassword(base64.StdEncoding.EncodeToString(key)).WithControlPort(controlPort).Build("tordir/tor/torrc")
acn, err := tor.NewTorACNWithAuth("./tordir", path.Join("..", "tor"), torDataDir, controlPort, tor.HashedPasswordAuthenticator{Password: base64.StdEncoding.EncodeToString(key)})
if err != nil {
t.Fatalf("Could not start Tor: %v", err)
}
log.Infof("Waiting for tor to bootstrap...")
acn.WaitTillBootstrapped()
defer acn.Close()
// ***** Cwtch Server management *****
app := app2.NewApp(acn, "./storage", app2.LoadAppSettings("./storage"))
// ***** cwtchPeer setup *****
// Turn on Groups Experiment...
settings := app.ReadSettings()
settings.ExperimentsEnabled = true
settings.Experiments[constants.GroupsExperiment] = true
settings.Experiments[constants.GroupManagerExperiment] = true
app.UpdateSettings(settings)
alice := MakeProfile(app, "Alice")
bob := MakeProfile(app, "Bob")
manager := MakeProfile(app, "Manager")
waitTime := time.Duration(60) * time.Second
log.Infof("** Waiting for Alice, Bob, and Carol to register their onion hidden service on the network... (%v)\n", waitTime)
time.Sleep(waitTime)
log.Infof("** Wait Done!")
// Ok Lets Start By Creating a Hybrid Group...
hgmf := hybrid.GroupManagerFunctionality{}
ci, err := hgmf.ManageNewGroup(manager)
if err != nil {
t.Fatalf("could not create hybrid group: %v", err)
}
log.Infof("created a hybrid group: %d. moving onto adding hybrid contacts...", ci)
err = hgmf.AddHybridContact(manager, alice.GetOnion())
if err != nil {
t.Fatalf("could not create hybrid contact (alice): %v", err)
}
err = hgmf.AddHybridContact(manager, bob.GetOnion())
if err != nil {
t.Fatalf("could not create hybrid contact (bob): %v", err)
}
// Now we can allow alice, bob and carol to create a new hybrid group...
log.Infof("now we can allow alice bob and carol to join the hybrid group")
inter := inter.InterfaceFunctionality{}
err = inter.ImportBundle(alice, "managed:"+manager.GetOnion())
if err != nil {
t.Fatalf("could not create hybrid group contact (carol): %v", err)
}
alice.PeerWithOnion(manager.GetOnion()) // explictly trigger a peer request
err = inter.ImportBundle(bob, "managed:"+manager.GetOnion())
if err != nil {
t.Fatalf("could not create hybrid group contact (carol): %v", err)
}
bob.PeerWithOnion(manager.GetOnion())
log.Infof("waiting for alice and manager to connect")
WaitForConnection(t, alice, manager.GetOnion(), connections.AUTHENTICATED)
log.Infof("waiting for bob and manager to connect")
WaitForConnection(t, bob, manager.GetOnion(), connections.AUTHENTICATED)
// at this pont we should be able to send messages to the group, and receive them in the timeline
log.Infof("sending message to group")
_, err = inter.SendMessage(alice, 1, "hello everyone!!!")
if err != nil {
t.Fatalf("hybrid group sending failed... %v", err)
}
// Note: From this point onwards there are no managed-group specific calls. Everything happens
// transparently with respect to the receiver.
time.Sleep(time.Second * 10)
bobMessages, err := bob.GetMostRecentMessages(1, constants.CHANNEL_CHAT, 0, 1)
if err != nil || len(bobMessages) != 1 {
t.Fatalf("hybrid group receipt failed... %v %v ", err, len(bobMessages))
}
if bobMessages[0].Body != "hello everyone!!!" {
t.Fatalf("hybrid group receipt failed...message does not match")
}
aliceMessages, err := alice.GetMostRecentMessages(1, constants.CHANNEL_CHAT, 0, 1)
if err != nil || len(aliceMessages) != 1 {
t.Fatalf("hybrid group receipt failed... %v", err)
}
if aliceMessages[0].Attr[constants.AttrAck] != constants.True {
t.Fatalf("hybrid group receipt failed...alice's message was not ack'd")
}
// Time to Clean Up....
log.Infof("Shutting down Alice...")
app.ShutdownPeer(alice.GetOnion())
time.Sleep(time.Second * 3)
log.Infof("Shutting down Bob...")
app.ShutdownPeer(bob.GetOnion())
time.Sleep(time.Second * 3)
log.Infof("Shutting fown Manager...")
app.ShutdownPeer(manager.GetOnion())
time.Sleep(time.Second * 3)
log.Infof("Shutting down apps...")
log.Infof("app Shutdown: %v\n", runtime.NumGoroutine())
app.Shutdown()
time.Sleep(2 * time.Second)
log.Infof("Done shutdown: %v\n", runtime.NumGoroutine())
log.Infof("Shutting down ACN...")
acn.Close()
time.Sleep(time.Second * 60) // the network status / heartbeat plugin might keep goroutines alive for a minute before killing them
numGoRoutinesPostAppShutdown := runtime.NumGoroutine()
// Printing out the current goroutines
// Very useful if we are leaking any.
pprof.Lookup("goroutine").WriteTo(os.Stdout, 1)
fmt.Println("")
if numGoRoutinesStart != numGoRoutinesPostAppShutdown {
t.Errorf("Number of GoRoutines at start (%v) does not match number of goRoutines after cleanup of peers and servers (%v), clean up failed, v detected!", numGoRoutinesStart, numGoRoutinesPostAppShutdown)
}
}
func MakeProfile(application app2.Application, name string) peer.CwtchPeer {
application.CreateProfile(name, "asdfasdf", true)
p := app2.WaitGetPeer(application, name)
application.ConfigureConnections(p.GetOnion(), true, true, false)
log.Infof("%s created: %s", name, p.GetOnion())
// bob.SetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants.Name, "Bob") <- This is now done automatically by ProfileValueExtension, keeping this here for clarity
p.AutoHandleEvents([]event.Type{event.PeerStateChange, event.ServerStateChange, event.NewGroupInvite, event.NewRetValMessageFromPeer})
return p
}

View File

@ -4,25 +4,18 @@ echo "Checking code quality (you want to see no output here)"
echo ""
echo ""
echo "Running staticcheck..."
echo "Linting:"
staticcheck ./...
# In the future we should remove include-pkgs. However, there are a few false positives in the overall go stdlib that make this
# too noisy right now, specifically assigning nil to initialize slices (safe), and using go internal context channels assigned
# nil (also safe).
# We also have one file infinite_channel.go written in a way that static analysis cannot reason about easily. So it is explictly
# ignored.
echo "Running nilaway..."
nilaway -include-pkgs="cwtch.im/cwtch,cwtch.im/tapir,git.openprivacy.ca/openprivacy/connectivity" -exclude-file-docstrings="nolint:nilaway" ./...
echo "Time to format"
gofmt -l -s -w .
# ineffassign (https://github.com/gordonklaus/ineffassign)
# echo "Checking for ineffectual assignment of errors (unchecked errors...)"
# ineffassign .
echo "Checking for ineffectual assignment of errors (unchecked errors...)"
ineffassign ./..
# misspell (https://github.com/client9/misspell/cmd/misspell)
# echo "Checking for misspelled words..."
# misspell . | grep -v "testing/" | grep -v "vendor/" | grep -v "go.sum" | grep -v ".idea"
echo "Checking for misspelled words..."
misspell . | grep -v "testing/" | grep -v "vendor/" | grep -v "go.sum" | grep -v ".idea"

View File

@ -1,32 +0,0 @@
package testing
import (
"cwtch.im/cwtch/model/attr"
"cwtch.im/cwtch/model/constants"
"cwtch.im/cwtch/peer"
"cwtch.im/cwtch/protocol/connections"
"git.openprivacy.ca/openprivacy/log"
_ "github.com/mutecomm/go-sqlcipher/v4"
"testing"
"time"
)
func WaitForConnection(t *testing.T, peer peer.CwtchPeer, addr string, target connections.ConnectionState) {
peerName, _ := peer.GetScopedZonedAttribute(attr.LocalScope, attr.ProfileZone, constants.Name)
for {
log.Infof("%v checking connection...\n", peerName)
state := peer.GetPeerState(addr)
log.Infof("Waiting for Peer %v to %v - state: %v\n", peerName, addr, connections.ConnectionStateName[state])
if state == connections.FAILED {
t.Fatalf("%v could not connect to %v", peer.GetOnion(), addr)
}
if state != target {
log.Infof("peer %v %v waiting connect %v, currently: %v\n", peerName, peer.GetOnion(), addr, connections.ConnectionStateName[state])
time.Sleep(time.Second * 5)
continue
} else {
log.Infof("peer %v %v CONNECTED to %v\n", peerName, peer.GetOnion(), addr)
break
}
}
}

View File

@ -18,6 +18,7 @@ import (
"os"
path "path/filepath"
"strings"
"time"
)
var tool = flag.String("tool", "", "the tool to use")
@ -85,6 +86,7 @@ func getTokens(bundle string) {
os.MkdirAll(dataDir, 0700)
// we don't need real randomness for the port, just to avoid a possible conflict...
mrand.Seed(int64(time.Now().Nanosecond()))
socksPort := mrand.Intn(1000) + 9051
controlPort := mrand.Intn(1000) + 9052

View File

@ -1,4 +1,3 @@
// nolint:nilaway - the context timeout here is reported as an error, even though it is a by-the-doc example
package utils
import (