Compare commits
72 Commits
crForceDis
...
master
Author | SHA1 | Date |
---|---|---|
Sarah Jamie Lewis | c5aa6905a4 | |
Sarah Jamie Lewis | 74d2aec96a | |
Sarah Jamie Lewis | 4bce08dc00 | |
Sarah Jamie Lewis | 77c6139792 | |
Sarah Jamie Lewis | a35374f200 | |
Sarah Jamie Lewis | e14044e404 | |
Sarah Jamie Lewis | fdec3302af | |
Sarah Jamie Lewis | d61dc30bb2 | |
Sarah Jamie Lewis | a7b885166a | |
Sarah Jamie Lewis | b32b11c711 | |
Sarah Jamie Lewis | 0e96539f22 | |
Sarah Jamie Lewis | e55f342324 | |
Sarah Jamie Lewis | 89aca91b37 | |
Sarah Jamie Lewis | cd918c02ea | |
Sarah Jamie Lewis | 05a198c89f | |
Sarah Jamie Lewis | 1d9202ff93 | |
Sarah Jamie Lewis | 0907af57d5 | |
Sarah Jamie Lewis | 826ac40a5c | |
Sarah Jamie Lewis | 1a034953df | |
Sarah Jamie Lewis | 3124f7b7c4 | |
Sarah Jamie Lewis | 792e79dceb | |
Sarah Jamie Lewis | 3e0680943a | |
Sarah Jamie Lewis | 9cb62d269e | |
Sarah Jamie Lewis | ec71e56d23 | |
Sarah Jamie Lewis | aaabb12b6c | |
Sarah Jamie Lewis | b0a87ee8d0 | |
Sarah Jamie Lewis | d66beb95e5 | |
Sarah Jamie Lewis | 41b3e20aff | |
Sarah Jamie Lewis | 1c7003fb96 | |
Dan Ballard | cb3b0b4c46 | |
Sarah Jamie Lewis | a18c19bbf2 | |
Sarah Jamie Lewis | be4230d16e | |
Sarah Jamie Lewis | 34957f809b | |
Sarah Jamie Lewis | 456a5f5c4d | |
Sarah Jamie Lewis | 657fb76b04 | |
Sarah Jamie Lewis | c0bc3b0803 | |
Sarah Jamie Lewis | 7a962359b3 | |
Sarah Jamie Lewis | 935b4a1103 | |
Sarah Jamie Lewis | 51d146fb5c | |
Sarah Jamie Lewis | 6d9e892408 | |
Sarah Jamie Lewis | 44856003d6 | |
Sarah Jamie Lewis | f16eeb1922 | |
Sarah Jamie Lewis | 13583f3e8c | |
Sarah Jamie Lewis | 58b1008cae | |
Sarah Jamie Lewis | 45d6d76a7d | |
Sarah Jamie Lewis | f42e25e926 | |
Sarah Jamie Lewis | 7538f1a531 | |
Sarah Jamie Lewis | a5cea1ca7b | |
Sarah Jamie Lewis | e311301d72 | |
Sarah Jamie Lewis | 7464e3922d | |
Sarah Jamie Lewis | 298a8d8aea | |
Sarah Jamie Lewis | 75a3c14285 | |
Sarah Jamie Lewis | 407902b8ee | |
Sarah Jamie Lewis | 6d29ca322e | |
Sarah Jamie Lewis | fb164b104b | |
Sarah Jamie Lewis | 048effc91a | |
Sarah Jamie Lewis | ca63205934 | |
Sarah Jamie Lewis | 0997406e51 | |
Sarah Jamie Lewis | 602041d1c2 | |
Sarah Jamie Lewis | 95527f8978 | |
Sarah Jamie Lewis | d5c3795f13 | |
Sarah Jamie Lewis | 51f993973c | |
Sarah Jamie Lewis | 5b2b839865 | |
Sarah Jamie Lewis | 151e25b607 | |
Sarah Jamie Lewis | fac34ad814 | |
Sarah Jamie Lewis | aae8a7fc03 | |
Sarah Jamie Lewis | e1877d69b7 | |
Sarah Jamie Lewis | 066ed86598 | |
Sarah Jamie Lewis | 4db041f850 | |
Sarah Jamie Lewis | 546180d65e | |
Sarah Jamie Lewis | 9dbc398690 | |
Sarah Jamie Lewis | b27229091a |
36
.drone.yml
36
.drone.yml
|
@ -5,27 +5,29 @@ name: linux-test
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: fetch
|
- name: fetch
|
||||||
image: golang:1.19.1
|
image: golang:1.21.5
|
||||||
volumes:
|
volumes:
|
||||||
- name: deps
|
- name: deps
|
||||||
path: /go
|
path: /go
|
||||||
commands:
|
commands:
|
||||||
- go install honnef.co/go/tools/cmd/staticcheck@latest
|
- go install honnef.co/go/tools/cmd/staticcheck@latest
|
||||||
- wget https://git.openprivacy.ca/openprivacy/buildfiles/raw/master/tor/tor
|
- go install go.uber.org/nilaway/cmd/nilaway@latest
|
||||||
- wget https://git.openprivacy.ca/openprivacy/buildfiles/raw/master/tor/torrc
|
- 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
|
||||||
- chmod a+x tor
|
- tar -xzf tor.tar.gz
|
||||||
- go get -u golang.org/x/lint/golint
|
- chmod a+x Tor/tor
|
||||||
|
- export PATH=$PWD/Tor/:$PATH
|
||||||
|
- export LD_LIBRARY_PATH=$PWD/Tor/
|
||||||
|
- tor --version
|
||||||
- export GO111MODULE=on
|
- export GO111MODULE=on
|
||||||
- go mod vendor
|
|
||||||
- name: quality
|
- name: quality
|
||||||
image: golang:1.19.1
|
image: golang:1.21.5
|
||||||
volumes:
|
volumes:
|
||||||
- name: deps
|
- name: deps
|
||||||
path: /go
|
path: /go
|
||||||
commands:
|
commands:
|
||||||
- staticcheck ./...
|
- ./testing/quality.sh
|
||||||
- name: units-tests
|
- name: units-tests
|
||||||
image: golang:1.19.1
|
image: golang:1.21.5
|
||||||
volumes:
|
volumes:
|
||||||
- name: deps
|
- name: deps
|
||||||
path: /go
|
path: /go
|
||||||
|
@ -33,28 +35,32 @@ steps:
|
||||||
- export PATH=`pwd`:$PATH
|
- export PATH=`pwd`:$PATH
|
||||||
- sh testing/tests.sh
|
- sh testing/tests.sh
|
||||||
- name: integ-test
|
- name: integ-test
|
||||||
image: golang:1.19.1
|
image: golang:1.21.5
|
||||||
volumes:
|
volumes:
|
||||||
- name: deps
|
- name: deps
|
||||||
path: /go
|
path: /go
|
||||||
commands:
|
commands:
|
||||||
- export PATH=`pwd`:$PATH
|
- export PATH=$PWD/Tor/:$PATH
|
||||||
|
- export LD_LIBRARY_PATH=$PWD/Tor/
|
||||||
|
- tor --version
|
||||||
- go test -timeout=30m -race -v cwtch.im/cwtch/testing/
|
- go test -timeout=30m -race -v cwtch.im/cwtch/testing/
|
||||||
- name: filesharing-integ-test
|
- name: filesharing-integ-test
|
||||||
image: golang:1.19.1
|
image: golang:1.21.5
|
||||||
volumes:
|
volumes:
|
||||||
- name: deps
|
- name: deps
|
||||||
path: /go
|
path: /go
|
||||||
commands:
|
commands:
|
||||||
- export PATH=`pwd`:$PATH
|
- export PATH=$PWD/Tor/:$PATH
|
||||||
|
- export LD_LIBRARY_PATH=$PWD/Tor/
|
||||||
- go test -timeout=20m -race -v cwtch.im/cwtch/testing/filesharing
|
- go test -timeout=20m -race -v cwtch.im/cwtch/testing/filesharing
|
||||||
- name: filesharing-autodownload-integ-test
|
- name: filesharing-autodownload-integ-test
|
||||||
image: golang:1.19.1
|
image: golang:1.21.5
|
||||||
volumes:
|
volumes:
|
||||||
- name: deps
|
- name: deps
|
||||||
path: /go
|
path: /go
|
||||||
commands:
|
commands:
|
||||||
- export PATH=`pwd`:$PATH
|
- export PATH=$PWD/Tor/:$PATH
|
||||||
|
- export LD_LIBRARY_PATH=$PWD/Tor/
|
||||||
- go test -timeout=20m -race -v cwtch.im/cwtch/testing/autodownload
|
- go test -timeout=20m -race -v cwtch.im/cwtch/testing/autodownload
|
||||||
- name: notify-gogs
|
- name: notify-gogs
|
||||||
image: openpriv/drone-gogs
|
image: openpriv/drone-gogs
|
||||||
|
|
|
@ -33,4 +33,6 @@ data-dir-cwtchtool/
|
||||||
tokens
|
tokens
|
||||||
tordir/
|
tordir/
|
||||||
testing/autodownload/download_dir
|
testing/autodownload/download_dir
|
||||||
testing/autodownload/storage
|
testing/autodownload/storage
|
||||||
|
*.swp
|
||||||
|
testing/managerstorage/*
|
133
app/app.go
133
app/app.go
|
@ -1,10 +1,16 @@
|
||||||
package app
|
package app
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"os"
|
||||||
|
path "path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"cwtch.im/cwtch/app/plugins"
|
"cwtch.im/cwtch/app/plugins"
|
||||||
"cwtch.im/cwtch/event"
|
"cwtch.im/cwtch/event"
|
||||||
"cwtch.im/cwtch/extensions"
|
"cwtch.im/cwtch/extensions"
|
||||||
"cwtch.im/cwtch/functionality/filesharing"
|
"cwtch.im/cwtch/functionality/filesharing"
|
||||||
|
"cwtch.im/cwtch/functionality/hybrid"
|
||||||
"cwtch.im/cwtch/functionality/servers"
|
"cwtch.im/cwtch/functionality/servers"
|
||||||
"cwtch.im/cwtch/model"
|
"cwtch.im/cwtch/model"
|
||||||
"cwtch.im/cwtch/model/attr"
|
"cwtch.im/cwtch/model/attr"
|
||||||
|
@ -15,10 +21,6 @@ import (
|
||||||
"cwtch.im/cwtch/storage"
|
"cwtch.im/cwtch/storage"
|
||||||
"git.openprivacy.ca/openprivacy/connectivity"
|
"git.openprivacy.ca/openprivacy/connectivity"
|
||||||
"git.openprivacy.ca/openprivacy/log"
|
"git.openprivacy.ca/openprivacy/log"
|
||||||
"os"
|
|
||||||
path "path/filepath"
|
|
||||||
"strconv"
|
|
||||||
"sync"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type application struct {
|
type application struct {
|
||||||
|
@ -63,7 +65,7 @@ type Application interface {
|
||||||
QueryACNStatus()
|
QueryACNStatus()
|
||||||
QueryACNVersion()
|
QueryACNVersion()
|
||||||
|
|
||||||
ActivateEngines(doListn, doPeers, doServers bool)
|
ConfigureConnections(onion string, doListn, doPeers, doServers bool)
|
||||||
ActivatePeerEngine(onion string)
|
ActivatePeerEngine(onion string)
|
||||||
DeactivatePeerEngine(onion string)
|
DeactivatePeerEngine(onion string)
|
||||||
|
|
||||||
|
@ -217,7 +219,7 @@ func (app *application) setupPeer(profile peer.CwtchPeer) {
|
||||||
|
|
||||||
// Initialize the Peer with the Given Event Bus
|
// Initialize the Peer with the Given Event Bus
|
||||||
app.peers[profile.GetOnion()] = profile
|
app.peers[profile.GetOnion()] = profile
|
||||||
profile.Init(app.eventBuses[profile.GetOnion()])
|
profile.Init(eventBus)
|
||||||
|
|
||||||
// Update the Peer with the Most Recent Experiment State...
|
// Update the Peer with the Most Recent Experiment State...
|
||||||
globalSettings := app.settings.ReadGlobalSettings()
|
globalSettings := app.settings.ReadGlobalSettings()
|
||||||
|
@ -259,7 +261,8 @@ func (app *application) DeleteProfile(onion string, password string) {
|
||||||
defer app.appmutex.Unlock()
|
defer app.appmutex.Unlock()
|
||||||
|
|
||||||
// short circuit to prevent nil-pointer panic if this function is called twice (or incorrectly)
|
// short circuit to prevent nil-pointer panic if this function is called twice (or incorrectly)
|
||||||
if app.peers[onion] == nil {
|
peer := app.peers[onion]
|
||||||
|
if peer == nil {
|
||||||
log.Errorf("shutdownPeer called with invalid onion %v", onion)
|
log.Errorf("shutdownPeer called with invalid onion %v", onion)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -269,11 +272,11 @@ func (app *application) DeleteProfile(onion string, password string) {
|
||||||
password = DefactoPasswordForUnencryptedProfiles
|
password = DefactoPasswordForUnencryptedProfiles
|
||||||
}
|
}
|
||||||
|
|
||||||
if app.peers[onion].CheckPassword(password) {
|
if peer.CheckPassword(password) {
|
||||||
// soft-shutdown
|
// soft-shutdown
|
||||||
app.peers[onion].Shutdown()
|
peer.Shutdown()
|
||||||
// delete the underlying storage
|
// delete the underlying storage
|
||||||
app.peers[onion].Delete()
|
peer.Delete()
|
||||||
// hard shutdown / remove from app
|
// hard shutdown / remove from app
|
||||||
app.shutdownPeer(onion)
|
app.shutdownPeer(onion)
|
||||||
|
|
||||||
|
@ -341,6 +344,7 @@ func (app *application) LoadProfiles(password string) {
|
||||||
cps, err := peer.CreateEncryptedStore(profileDirectory, password)
|
cps, err := peer.CreateEncryptedStore(profileDirectory, password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("error creating encrypted store: %v", err)
|
log.Errorf("error creating encrypted store: %v", err)
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
profile := peer.ImportLegacyProfile(legacyProfile, cps)
|
profile := peer.ImportLegacyProfile(legacyProfile, cps)
|
||||||
loaded = app.installProfile(profile)
|
loaded = app.installProfile(profile)
|
||||||
|
@ -361,9 +365,12 @@ func (app *application) LoadProfiles(password string) {
|
||||||
func (app *application) registerHooks(profile peer.CwtchPeer) {
|
func (app *application) registerHooks(profile peer.CwtchPeer) {
|
||||||
// Register Hooks
|
// Register Hooks
|
||||||
profile.RegisterHook(extensions.ProfileValueExtension{})
|
profile.RegisterHook(extensions.ProfileValueExtension{})
|
||||||
|
profile.RegisterHook(extensions.SendWhenOnlineExtension{})
|
||||||
profile.RegisterHook(new(filesharing.Functionality))
|
profile.RegisterHook(new(filesharing.Functionality))
|
||||||
profile.RegisterHook(new(filesharing.ImagePreviewsFunctionality))
|
profile.RegisterHook(new(filesharing.ImagePreviewsFunctionality))
|
||||||
profile.RegisterHook(new(servers.Functionality))
|
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...
|
// Ensure that Profiles have the Most Up to Date Settings...
|
||||||
profile.NotifySettingsUpdate(app.settings.ReadGlobalSettings())
|
profile.NotifySettingsUpdate(app.settings.ReadGlobalSettings())
|
||||||
}
|
}
|
||||||
|
@ -385,45 +392,62 @@ func (app *application) installProfile(profile peer.CwtchPeer) bool {
|
||||||
return false
|
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
|
// 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) {
|
func (app *application) ActivatePeerEngine(onion string) {
|
||||||
profile := app.GetPeer(onion)
|
profile := app.GetPeer(onion)
|
||||||
if profile != nil {
|
if profile != nil {
|
||||||
|
app.appmutex.Lock()
|
||||||
if _, exists := app.engines[onion]; !exists {
|
if _, exists := app.engines[onion]; !exists {
|
||||||
log.Debugf("restartFlow: Creating a New Protocol Engine...")
|
eventBus, exists := app.eventBuses[profile.GetOnion()]
|
||||||
app.engines[profile.GetOnion()], _ = profile.GenerateProtocolEngine(app.acn, app.eventBuses[profile.GetOnion()], app.engineHooks)
|
|
||||||
app.eventBuses[profile.GetOnion()].Publish(event.NewEventList(event.ProtocolEngineCreated))
|
if !exists {
|
||||||
app.QueryACNStatus()
|
// todo handle this case?
|
||||||
if true {
|
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.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 {
|
||||||
profile.Listen()
|
profile.Listen()
|
||||||
}
|
}
|
||||||
profile.StartConnections(true, true)
|
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
log.Errorf("profile does not exist %v", onion)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -494,9 +518,17 @@ func (app *application) eventHandler() {
|
||||||
profile := app.GetPeer(onion)
|
profile := app.GetPeer(onion)
|
||||||
if profile != nil {
|
if profile != nil {
|
||||||
autostart, exists := profile.GetScopedZonedAttribute(attr.LocalScope, attr.ProfileZone, constants.PeerAutostart)
|
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 !exists || autostart == "true" {
|
||||||
app.ActivatePeerEngine(onion)
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -528,21 +560,26 @@ func (app *application) ShutdownPeer(onion string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// shutdownPeer mutex unlocked helper shutdown peer
|
// shutdownPeer mutex unlocked helper shutdown peer
|
||||||
|
//
|
||||||
|
//nolint:nilaway
|
||||||
func (app *application) shutdownPeer(onion string) {
|
func (app *application) shutdownPeer(onion string) {
|
||||||
|
|
||||||
// short circuit to prevent nil-pointer panic if this function is called twice (or incorrectly)
|
// short circuit to prevent nil-pointer panic if this function is called twice (or incorrectly)
|
||||||
if app.eventBuses[onion] == nil || app.peers[onion] == nil {
|
onionEventBus := app.eventBuses[onion]
|
||||||
|
onionPeer := app.peers[onion]
|
||||||
|
if onionEventBus == nil || onionPeer == nil {
|
||||||
log.Errorf("shutdownPeer called with invalid onion %v", onion)
|
log.Errorf("shutdownPeer called with invalid onion %v", onion)
|
||||||
return
|
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)
|
delete(app.eventBuses, onion)
|
||||||
app.peers[onion].Shutdown()
|
onionPeer.Shutdown()
|
||||||
delete(app.peers, onion)
|
delete(app.peers, onion)
|
||||||
if _, ok := app.engines[onion]; ok {
|
if onionEngine, ok := app.engines[onion]; ok {
|
||||||
app.engines[onion].Shutdown()
|
onionEngine.Shutdown()
|
||||||
delete(app.engines, onion)
|
delete(app.engines, onion)
|
||||||
}
|
}
|
||||||
log.Debugf("shutting down plugins for %v", onion)
|
log.Debugf("shutting down plugins for %v", onion)
|
||||||
|
|
|
@ -3,6 +3,7 @@ package plugins
|
||||||
import (
|
import (
|
||||||
"cwtch.im/cwtch/event"
|
"cwtch.im/cwtch/event"
|
||||||
"cwtch.im/cwtch/protocol/connections"
|
"cwtch.im/cwtch/protocol/connections"
|
||||||
|
"git.openprivacy.ca/openprivacy/connectivity/tor"
|
||||||
"git.openprivacy.ca/openprivacy/log"
|
"git.openprivacy.ca/openprivacy/log"
|
||||||
"math"
|
"math"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
@ -120,14 +121,16 @@ type contactRetry struct {
|
||||||
lastCheck time.Time
|
lastCheck time.Time
|
||||||
acnProgress int
|
acnProgress int
|
||||||
|
|
||||||
connections sync.Map //[string]*contact
|
connections sync.Map //[string]*contact
|
||||||
pendingQueue *connectionQueue
|
pendingQueue *connectionQueue
|
||||||
priorityQueue *connectionQueue
|
priorityQueue *connectionQueue
|
||||||
|
authorizedPeers sync.Map
|
||||||
|
stallRetries bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewConnectionRetry returns a Plugin that when started will retry connecting to contacts with a failedCount timing
|
// NewConnectionRetry returns a Plugin that when started will retry connecting to contacts with a failedCount timing
|
||||||
func NewConnectionRetry(bus event.Manager, onion string) Plugin {
|
func NewConnectionRetry(bus event.Manager, onion string) Plugin {
|
||||||
cr := &contactRetry{bus: bus, queue: event.NewQueue(), breakChan: make(chan bool, 1), connections: sync.Map{}, 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), authorizedPeers: sync.Map{}, connections: sync.Map{}, stallRetries: true, ACNUp: false, ACNUpTime: time.Now(), protocolEngine: false, onion: onion, pendingQueue: newConnectionQueue(), priorityQueue: newConnectionQueue()}
|
||||||
return cr
|
return cr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -175,13 +178,18 @@ func (cr *contactRetry) run() {
|
||||||
cr.bus.Subscribe(event.ServerStateChange, cr.queue)
|
cr.bus.Subscribe(event.ServerStateChange, cr.queue)
|
||||||
cr.bus.Subscribe(event.QueuePeerRequest, cr.queue)
|
cr.bus.Subscribe(event.QueuePeerRequest, cr.queue)
|
||||||
cr.bus.Subscribe(event.QueueJoinServer, 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.ProtocolEngineShutdown, cr.queue)
|
||||||
cr.bus.Subscribe(event.ProtocolEngineCreated, 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 {
|
for {
|
||||||
// Only attempt connection if both the ACN and the Protocol Engines are Online...
|
// Only attempt connection if both the ACN and the Protocol Engines are Online...
|
||||||
log.Debugf("restartFlow checking state")
|
log.Debugf("restartFlow checking state")
|
||||||
if cr.ACNUp && cr.protocolEngine {
|
if cr.ACNUp && cr.protocolEngine && !cr.stallRetries {
|
||||||
log.Debugf("restartFlow time to queue!!")
|
log.Debugf("restartFlow time to queue!!")
|
||||||
cr.requeueReady()
|
cr.requeueReady()
|
||||||
connectingCount := cr.connectingCount()
|
connectingCount := cr.connectingCount()
|
||||||
|
@ -226,16 +234,53 @@ func (cr *contactRetry) run() {
|
||||||
select {
|
select {
|
||||||
case e := <-cr.queue.OutChan():
|
case e := <-cr.queue.OutChan():
|
||||||
switch e.EventType {
|
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:
|
case event.PeerStateChange:
|
||||||
state := connections.ConnectionStateToType()[e.Data[event.ConnectionState]]
|
state := connections.ConnectionStateToType()[e.Data[event.ConnectionState]]
|
||||||
peer := e.Data[event.RemotePeer]
|
peer := e.Data[event.RemotePeer]
|
||||||
cr.handleEvent(peer, state, peerConn)
|
// only handle state change events from pre-authorized peers;
|
||||||
|
if _, exists := cr.authorizedPeers.Load(peer); exists {
|
||||||
|
cr.handleEvent(peer, state, peerConn)
|
||||||
|
}
|
||||||
case event.ServerStateChange:
|
case event.ServerStateChange:
|
||||||
state := connections.ConnectionStateToType()[e.Data[event.ConnectionState]]
|
state := connections.ConnectionStateToType()[e.Data[event.ConnectionState]]
|
||||||
server := e.Data[event.GroupServer]
|
server := e.Data[event.GroupServer]
|
||||||
cr.handleEvent(server, state, serverConn)
|
// only handle state change events from pre-authorized servers;
|
||||||
|
if _, exists := cr.authorizedPeers.Load(server); exists {
|
||||||
|
cr.handleEvent(server, state, serverConn)
|
||||||
|
}
|
||||||
case event.QueueJoinServer:
|
case event.QueueJoinServer:
|
||||||
fallthrough
|
fallthrough
|
||||||
case event.QueuePeerRequest:
|
case event.QueuePeerRequest:
|
||||||
|
@ -252,11 +297,12 @@ func (cr *contactRetry) run() {
|
||||||
id = server
|
id = server
|
||||||
cr.addConnection(server, connections.DISCONNECTED, serverConn, lastSeen)
|
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 {
|
if c, ok := cr.connections.Load(id); ok {
|
||||||
contact := c.(*contact)
|
contact := c.(*contact)
|
||||||
if contact.state == connections.DISCONNECTED && !contact.queued {
|
if contact.state == connections.DISCONNECTED {
|
||||||
|
|
||||||
// prioritize connections made in the last week
|
// prioritize connections made in the last week
|
||||||
if time.Since(contact.lastSeen).Hours() < PriorityQueueTimeSinceQualifierHours {
|
if time.Since(contact.lastSeen).Hours() < PriorityQueueTimeSinceQualifierHours {
|
||||||
cr.priorityQueue.insert(contact)
|
cr.priorityQueue.insert(contact)
|
||||||
|
@ -269,7 +315,7 @@ func (cr *contactRetry) run() {
|
||||||
case event.ProtocolEngineShutdown:
|
case event.ProtocolEngineShutdown:
|
||||||
cr.ACNUp = false
|
cr.ACNUp = false
|
||||||
cr.protocolEngine = false
|
cr.protocolEngine = false
|
||||||
|
cr.stallRetries = true
|
||||||
cr.connections.Range(func(k, v interface{}) bool {
|
cr.connections.Range(func(k, v interface{}) bool {
|
||||||
p := v.(*contact)
|
p := v.(*contact)
|
||||||
if p.state == connections.AUTHENTICATED || p.state == connections.SYNCED {
|
if p.state == connections.AUTHENTICATED || p.state == connections.SYNCED {
|
||||||
|
@ -319,14 +365,17 @@ func (cr *contactRetry) processStatus() {
|
||||||
// Loop through connections. Reset state, and requeue...
|
// Loop through connections. Reset state, and requeue...
|
||||||
cr.connections.Range(func(k, v interface{}) bool {
|
cr.connections.Range(func(k, v interface{}) bool {
|
||||||
p := v.(*contact)
|
p := v.(*contact)
|
||||||
p.queued = true
|
|
||||||
|
|
||||||
// prioritize connections made recently...
|
// only reload connections if they are on the authorized peers list
|
||||||
log.Debugf("adding %v to queue", p.id)
|
if _, exists := cr.authorizedPeers.Load(p.id); exists {
|
||||||
if time.Since(p.lastSeen).Hours() < PriorityQueueTimeSinceQualifierHours {
|
p.queued = true
|
||||||
cr.priorityQueue.insert(p)
|
// prioritize connections made recently...
|
||||||
} else {
|
log.Debugf("adding %v to queue", p.id)
|
||||||
cr.pendingQueue.insert(p)
|
if time.Since(p.lastSeen).Hours() < PriorityQueueTimeSinceQualifierHours {
|
||||||
|
cr.priorityQueue.insert(p)
|
||||||
|
} else {
|
||||||
|
cr.pendingQueue.insert(p)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
|
@ -367,10 +416,14 @@ func (cr *contactRetry) requeueReady() {
|
||||||
|
|
||||||
cr.connections.Range(func(k, v interface{}) bool {
|
cr.connections.Range(func(k, v interface{}) bool {
|
||||||
p := v.(*contact)
|
p := v.(*contact)
|
||||||
if p.state == connections.DISCONNECTED && !p.queued {
|
|
||||||
timeout := time.Duration((math.Pow(2, float64(p.failedCount)))*float64(adjustedBaseTimeout /*baseTimeoutSec*/)) * time.Second
|
// Don't retry anyone who isn't on the authorized peers list
|
||||||
if time.Since(p.lastAttempt) > timeout {
|
if _, exists := cr.authorizedPeers.Load(p.id); exists {
|
||||||
retryable = append(retryable, p)
|
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
|
return true
|
||||||
|
@ -407,10 +460,10 @@ func (cr *contactRetry) addConnection(id string, state connections.ConnectionSta
|
||||||
cr.connections.Store(id, p)
|
cr.connections.Store(id, p)
|
||||||
return
|
return
|
||||||
} else {
|
} else {
|
||||||
// we have rerequested this connnection. Force set the queued parameter to true.
|
// we have rerequested this connnection, probably via an explicit ask, update it's state
|
||||||
p, _ := cr.connections.Load(id)
|
if c, ok := cr.connections.Load(id); ok {
|
||||||
if !p.(*contact).queued {
|
contact := c.(*contact)
|
||||||
p.(*contact).queued = true
|
contact.state = state
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -423,6 +476,12 @@ func (cr *contactRetry) handleEvent(id string, state connections.ConnectionState
|
||||||
return
|
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 {
|
if _, exists := cr.connections.Load(id); !exists {
|
||||||
// We have an event for something we don't know about...
|
// 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.
|
// The only reason this should happen is if a *new* Peer/Server connection has changed.
|
||||||
|
|
|
@ -21,34 +21,41 @@ func TestContactRetryQueue(t *testing.T) {
|
||||||
cr := NewConnectionRetry(bus, "").(*contactRetry)
|
cr := NewConnectionRetry(bus, "").(*contactRetry)
|
||||||
cr.ACNUp = true // fake an ACN connection...
|
cr.ACNUp = true // fake an ACN connection...
|
||||||
cr.protocolEngine = true // fake protocol engine
|
cr.protocolEngine = true // fake protocol engine
|
||||||
|
cr.stallRetries = false // fake not being in offline mode...
|
||||||
go cr.run()
|
go cr.run()
|
||||||
|
|
||||||
|
testOnion := "2wgvbza2mbuc72a4u6r6k4hc2blcvrmk4q26bfvlwbqxv2yq5k52fcqd"
|
||||||
|
|
||||||
t.Logf("contact plugin up and running..sending peer connection...")
|
t.Logf("contact plugin up and running..sending peer connection...")
|
||||||
// Assert that there is a peer connection identified as "test"
|
// Assert that there is a peer connection identified as "test"
|
||||||
bus.Publish(event.NewEvent(event.QueuePeerRequest, map[event.Field]string{event.RemotePeer: "test", event.LastSeen: "test"}))
|
bus.Publish(event.NewEvent(event.QueuePeerRequest, map[event.Field]string{event.RemotePeer: testOnion, event.LastSeen: "test"}))
|
||||||
|
|
||||||
// Wait until the test actually exists, and is queued
|
// 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
|
// 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
|
// go test scheduling doesn't like that and even sleeping long periods won't cause the event thread to make
|
||||||
// progress...
|
// progress...
|
||||||
for {
|
setup := false
|
||||||
if pinf, exists := cr.connections.Load("test"); exists {
|
for !setup {
|
||||||
if pinf.(*contact).queued {
|
if _, exists := cr.connections.Load(testOnion); exists {
|
||||||
break
|
if _, exists := cr.authorizedPeers.Load(testOnion); exists {
|
||||||
|
t.Logf("authorized")
|
||||||
|
setup = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pinf, _ := cr.connections.Load("test")
|
// We should very quickly become connecting...
|
||||||
if pinf.(*contact).queued == false {
|
time.Sleep(time.Second)
|
||||||
t.Fatalf("test connection should be queued, actually: %v", pinf.(*contact).queued)
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Asset that "test" is authenticated
|
// Asset that "test" is authenticated
|
||||||
cr.handleEvent("test", connections.AUTHENTICATED, peerConn)
|
cr.handleEvent(testOnion, connections.AUTHENTICATED, peerConn)
|
||||||
|
|
||||||
// Assert that "test has a valid state"
|
// Assert that "test has a valid state"
|
||||||
pinf, _ = cr.connections.Load("test")
|
pinf, _ = cr.connections.Load(testOnion)
|
||||||
if pinf.(*contact).state != 3 {
|
if pinf.(*contact).state != 3 {
|
||||||
t.Fatalf("test connection should be in authenticated after update, actually: %v", pinf.(*contact).state)
|
t.Fatalf("test connection should be in authenticated after update, actually: %v", pinf.(*contact).state)
|
||||||
}
|
}
|
||||||
|
@ -56,19 +63,12 @@ func TestContactRetryQueue(t *testing.T) {
|
||||||
// Publish an unrelated event to trigger the Plugin to go through a queuing cycle
|
// 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
|
// 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.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)
|
time.Sleep(time.Second)
|
||||||
if pinf.(*contact).queued != false {
|
pinf, _ = cr.connections.Load(testOnion)
|
||||||
t.Fatalf("test connection should not be queued, actually: %v", pinf.(*contact).queued)
|
if pinf.(*contact).state != 1 {
|
||||||
}
|
t.Fatalf("test connection should be in connecting after update, actually: %v", pinf.(*contact).state)
|
||||||
|
|
||||||
// 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()
|
cr.Shutdown()
|
||||||
|
|
|
@ -15,6 +15,9 @@ func WaitGetPeer(app Application, name string) peer.CwtchPeer {
|
||||||
for {
|
for {
|
||||||
for _, handle := range app.ListProfiles() {
|
for _, handle := range app.ListProfiles() {
|
||||||
peer := app.GetPeer(handle)
|
peer := app.GetPeer(handle)
|
||||||
|
if peer == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
localName, _ := peer.GetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants.Name)
|
localName, _ := peer.GetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants.Name)
|
||||||
if localName == name {
|
if localName == name {
|
||||||
return peer
|
return peer
|
||||||
|
|
|
@ -25,6 +25,15 @@ const (
|
||||||
// GroupServer
|
// GroupServer
|
||||||
QueuePeerRequest = Type("QueuePeerRequest")
|
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
|
// RetryServerRequest
|
||||||
// Asks CwtchPeer to retry a server connection...
|
// Asks CwtchPeer to retry a server connection...
|
||||||
// GroupServer: [eg "chpr7qm6op5vfcg2pi4vllco3h6aa7exexc4rqwnlupqhoogx2zgd6qd"
|
// GroupServer: [eg "chpr7qm6op5vfcg2pi4vllco3h6aa7exexc4rqwnlupqhoogx2zgd6qd"
|
||||||
|
@ -275,7 +284,9 @@ const (
|
||||||
Status = Field("Status")
|
Status = Field("Status")
|
||||||
EventID = Field("EventID")
|
EventID = Field("EventID")
|
||||||
EventContext = Field("EventContext")
|
EventContext = Field("EventContext")
|
||||||
|
Channel = Field("Channel")
|
||||||
Index = Field("Index")
|
Index = Field("Index")
|
||||||
|
RowIndex = Field("RowIndex")
|
||||||
ContentHash = Field("ContentHash")
|
ContentHash = Field("ContentHash")
|
||||||
|
|
||||||
// Handle denotes a contact handle of any type.
|
// Handle denotes a contact handle of any type.
|
||||||
|
@ -326,19 +337,25 @@ const (
|
||||||
ContextSendFile = "im.cwtch.file.send.chunk"
|
ContextSendFile = "im.cwtch.file.send.chunk"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Define Default Attribute Keys
|
// Define Attribute Keys related to history preservation
|
||||||
const (
|
const (
|
||||||
SaveHistoryKey = "SavePeerHistory"
|
PreserveHistoryDefaultSettingKey = "SaveHistoryDefault" // profile level default
|
||||||
|
SaveHistoryKey = "SavePeerHistory" // peer level setting
|
||||||
)
|
)
|
||||||
|
|
||||||
// Define Default Attribute Values
|
// Define Default Attribute Values
|
||||||
const (
|
const (
|
||||||
// Save History has 3 distinct states. By default we don't save history (DefaultDeleteHistory), if the peer confirms this
|
// Save History has 3 distinct states. By default we refer to the profile level
|
||||||
// we change to DeleteHistoryConfirmed, if they confirm they want to save then this becomes SaveHistoryConfirmed
|
// attribute PreserveHistoryDefaultSettingKey ( default: false i.e. DefaultDeleteHistory),
|
||||||
// We use this distinction between default and confirmed to drive UI
|
// For each contact, if the profile owner confirms deletion we change to DeleteHistoryConfirmed,
|
||||||
DeleteHistoryDefault = "DefaultDeleteHistory"
|
// 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.*
|
||||||
SaveHistoryConfirmed = "SaveHistory"
|
SaveHistoryConfirmed = "SaveHistory"
|
||||||
DeleteHistoryConfirmed = "DeleteHistoryConfirmed"
|
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
|
// Bool strings
|
||||||
|
|
|
@ -95,6 +95,11 @@ func (em *manager) initialize() {
|
||||||
func (em *manager) Subscribe(eventType Type, queue Queue) {
|
func (em *manager) Subscribe(eventType Type, queue Queue) {
|
||||||
em.mapMutex.Lock()
|
em.mapMutex.Lock()
|
||||||
defer em.mapMutex.Unlock()
|
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)
|
em.subscribers[eventType] = append(em.subscribers[eventType], queue)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
// nolint:nilaway - the infiniteBuffer function causes issues with static analysis because it is very unidomatic.
|
||||||
package event
|
package event
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
|
@ -104,6 +104,15 @@ func (pne ProfileValueExtension) OnContactRequestValue(profile peer.CwtchPeer, c
|
||||||
val, exists = profile.GetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants.Name)
|
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
|
// 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 := 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
|
resp.EventID = eventID
|
||||||
|
|
|
@ -0,0 +1,91 @@
|
||||||
|
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) {
|
||||||
|
|
||||||
|
}
|
|
@ -45,6 +45,8 @@ func (f *Functionality) ExperimentsToRegister() []string {
|
||||||
func (f *Functionality) OnEvent(ev event.Event, profile peer.CwtchPeer) {
|
func (f *Functionality) OnEvent(ev event.Event, profile peer.CwtchPeer) {
|
||||||
if profile.IsFeatureEnabled(constants.FileSharingExperiment) {
|
if profile.IsFeatureEnabled(constants.FileSharingExperiment) {
|
||||||
switch ev.EventType {
|
switch ev.EventType {
|
||||||
|
case event.ProtocolEngineCreated:
|
||||||
|
f.ReShareFiles(profile)
|
||||||
case event.ManifestReceived:
|
case event.ManifestReceived:
|
||||||
log.Debugf("Manifest Received Event!: %v", ev)
|
log.Debugf("Manifest Received Event!: %v", ev)
|
||||||
handle := ev.Data[event.Handle]
|
handle := ev.Data[event.Handle]
|
||||||
|
@ -208,7 +210,7 @@ func (f *Functionality) VerifyOrResumeDownload(profile peer.CwtchPeer, conversat
|
||||||
return errors.New("file download metadata does not exist, or is corrupted")
|
return errors.New("file download metadata does not exist, or is corrupted")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Functionality) CheckDownloadStatus(profile peer.CwtchPeer, fileKey string) {
|
func (f *Functionality) CheckDownloadStatus(profile peer.CwtchPeer, fileKey string) error {
|
||||||
path, _ := profile.GetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.path", fileKey))
|
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 {
|
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{
|
profile.PublishEvent(event.NewEvent(event.FileDownloaded, map[event.Field]string{
|
||||||
|
@ -227,6 +229,7 @@ func (f *Functionality) CheckDownloadStatus(profile peer.CwtchPeer, fileKey stri
|
||||||
event.FilePath: path,
|
event.FilePath: path,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
return nil // cannot fail
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Functionality) EnhancedShareFile(profile peer.CwtchPeer, conversationID int, sharefilepath string) string {
|
func (f *Functionality) EnhancedShareFile(profile peer.CwtchPeer, conversationID int, sharefilepath string) string {
|
||||||
|
@ -269,15 +272,21 @@ func (f *Functionality) DownloadFile(profile peer.CwtchPeer, conversationID int,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Don't download files if the download file directory does not exist
|
// Don't download files if the download file directory does not exist
|
||||||
if _, err := os.Stat(path.Dir(downloadFilePath)); os.IsNotExist(err) {
|
// Unless we are on Android where the kernel wishes to keep us ignorant of the
|
||||||
return errors.New("download directory does not exist")
|
// actual path and/or existence of the file. We handle this case further down
|
||||||
}
|
// the line when the manifest is received and protocol engine and the Android layer
|
||||||
|
// negotiate a temporary local file -> final file copy. We don't want to worry
|
||||||
|
// about that here...
|
||||||
|
if runtime.GOOS != "android" {
|
||||||
|
if _, err := os.Stat(path.Dir(downloadFilePath)); os.IsNotExist(err) {
|
||||||
|
return errors.New("download directory does not exist")
|
||||||
|
}
|
||||||
|
|
||||||
// Don't download files if the manifest file directory does not exist
|
// Don't download files if the manifest file directory does not exist
|
||||||
if _, err := os.Stat(path.Dir(manifestFilePath)); os.IsNotExist(err) {
|
if _, err := os.Stat(path.Dir(manifestFilePath)); os.IsNotExist(err) {
|
||||||
return errors.New("manifest directory does not exist")
|
return errors.New("manifest directory does not exist")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store local.filesharing.filekey.manifest as the location of the manifest
|
// Store local.filesharing.filekey.manifest as the location of the manifest
|
||||||
profile.SetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.manifest", key), manifestFilePath)
|
profile.SetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.manifest", key), manifestFilePath)
|
||||||
|
|
||||||
|
@ -294,9 +303,10 @@ 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.
|
// startFileShare is a private method used to finalize a file share and publish it to the protocol engine for processing.
|
||||||
func (f *Functionality) startFileShare(profile peer.CwtchPeer, filekey string, manifest string) error {
|
// 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 {
|
||||||
tsStr, exists := profile.GetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.ts", filekey))
|
tsStr, exists := profile.GetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.ts", filekey))
|
||||||
if exists {
|
if exists && !force {
|
||||||
ts, err := strconv.ParseInt(tsStr, 10, 64)
|
ts, err := strconv.ParseInt(tsStr, 10, 64)
|
||||||
if err != nil || ts < time.Now().Unix()-2592000 {
|
if err != nil || ts < time.Now().Unix()-2592000 {
|
||||||
log.Errorf("ignoring request to download a file offered more than 30 days ago")
|
log.Errorf("ignoring request to download a file offered more than 30 days ago")
|
||||||
|
@ -306,12 +316,22 @@ func (f *Functionality) startFileShare(profile peer.CwtchPeer, filekey string, m
|
||||||
|
|
||||||
// set the filekey status to active
|
// set the filekey status to active
|
||||||
profile.SetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.active", filekey), constants.True)
|
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}))
|
profile.PublishEvent(event.NewEvent(event.ShareManifest, map[event.Field]string{event.FileKey: filekey, event.SerializedManifest: manifest}))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// RestartFileShare takes in an existing filekey and, assuming the manifest exists, restarts sharing of the manifest
|
// 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 {
|
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
|
// assert that we are allowed to restart filesharing
|
||||||
if !profile.IsFeatureEnabled(constants.FileSharingExperiment) {
|
if !profile.IsFeatureEnabled(constants.FileSharingExperiment) {
|
||||||
|
@ -323,7 +343,7 @@ func (f *Functionality) RestartFileShare(profile peer.CwtchPeer, filekey string)
|
||||||
if manifestExists {
|
if manifestExists {
|
||||||
// everything is in order, so reshare this file with the engine
|
// everything is in order, so reshare this file with the engine
|
||||||
log.Debugf("restarting file share: %v", filekey)
|
log.Debugf("restarting file share: %v", filekey)
|
||||||
return f.startFileShare(profile, filekey, manifest)
|
return f.startFileShare(profile, filekey, manifest, force)
|
||||||
}
|
}
|
||||||
return fmt.Errorf("manifest does not exist for filekey: %v", filekey)
|
return fmt.Errorf("manifest does not exist for filekey: %v", filekey)
|
||||||
}
|
}
|
||||||
|
@ -357,12 +377,10 @@ func (f *Functionality) ReShareFiles(profile peer.CwtchPeer) error {
|
||||||
filekey := strings.Join(keyparts[:2], ".")
|
filekey := strings.Join(keyparts[:2], ".")
|
||||||
sharedFile, err := f.GetFileShareInfo(profile, filekey)
|
sharedFile, err := f.GetFileShareInfo(profile, filekey)
|
||||||
|
|
||||||
// If we haven't explicitly stopped sharing the file AND
|
// If we haven't explicitly stopped sharing the file then attempt a reshare
|
||||||
// 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 {
|
if err == nil && sharedFile.Active {
|
||||||
err := f.RestartFileShare(profile, filekey)
|
// this reshare can fail because we don't force sharing of files older than 30 days...
|
||||||
|
err := f.restartFileShareAdvanced(profile, filekey, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Debugf("could not reshare file: %v", err)
|
log.Debugf("could not reshare file: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -456,7 +474,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", 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)))))
|
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))
|
err = f.startFileShare(profile, key, string(serializedManifest), false)
|
||||||
|
|
||||||
return key, string(wrapperJSON), err
|
return key, string(wrapperJSON), err
|
||||||
}
|
}
|
||||||
|
@ -558,11 +576,12 @@ func GenerateDownloadPath(basePath, fileName string, overwrite bool) (filePath,
|
||||||
}
|
}
|
||||||
|
|
||||||
// StopFileShare sends a message to the ProtocolEngine to cease sharing a particular file
|
// StopFileShare sends a message to the ProtocolEngine to cease sharing a particular file
|
||||||
func (f *Functionality) StopFileShare(profile peer.CwtchPeer, fileKey string) {
|
func (f *Functionality) StopFileShare(profile peer.CwtchPeer, fileKey string) error {
|
||||||
// Note we do not do a permissions check here, as we are *always* permitted to stop sharing files.
|
// Note we do not do a permissions check here, as we are *always* permitted to stop sharing files.
|
||||||
// set the filekey status to inactive
|
// set the filekey status to inactive
|
||||||
profile.SetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.active", fileKey), constants.False)
|
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}))
|
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
|
// StopAllFileShares sends a message to the ProtocolEngine to cease sharing all files
|
||||||
|
|
|
@ -38,14 +38,14 @@ func (i *ImagePreviewsFunctionality) OnEvent(ev event.Event, profile peer.CwtchP
|
||||||
case event.NewMessageFromPeer:
|
case event.NewMessageFromPeer:
|
||||||
ci, err := profile.FetchConversationInfo(ev.Data["RemotePeer"])
|
ci, err := profile.FetchConversationInfo(ev.Data["RemotePeer"])
|
||||||
if err == nil {
|
if err == nil {
|
||||||
if ci.Accepted {
|
if ci.GetPeerAC().RenderImages {
|
||||||
i.handleImagePreviews(profile, &ev, ci.ID, ci.ID)
|
i.handleImagePreviews(profile, &ev, ci.ID, ci.ID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case event.NewMessageFromGroup:
|
case event.NewMessageFromGroup:
|
||||||
ci, err := profile.FetchConversationInfo(ev.Data["RemotePeer"])
|
ci, err := profile.FetchConversationInfo(ev.Data["RemotePeer"])
|
||||||
if err == nil {
|
if err == nil {
|
||||||
if ci.Accepted {
|
if ci.GetPeerAC().RenderImages {
|
||||||
i.handleImagePreviews(profile, &ev, ci.ID, ci.ID)
|
i.handleImagePreviews(profile, &ev, ci.ID, ci.ID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -62,7 +62,15 @@ func (i *ImagePreviewsFunctionality) OnEvent(ev event.Event, profile peer.CwtchP
|
||||||
if err == nil {
|
if err == nil {
|
||||||
for _, ci := range conversations {
|
for _, ci := range conversations {
|
||||||
if profile.GetPeerState(ci.Handle) == connections.AUTHENTICATED {
|
if profile.GetPeerState(ci.Handle) == connections.AUTHENTICATED {
|
||||||
profile.SendScopedZonedGetValToContact(ci.ID, attr.PublicScope, attr.ProfileZone, constants.CustomProfileImageKey)
|
// 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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -75,10 +83,9 @@ func (i *ImagePreviewsFunctionality) OnEvent(ev event.Event, profile peer.CwtchP
|
||||||
// we reset the profile image here so that it is always available.
|
// 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))
|
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)
|
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -91,7 +98,7 @@ func (i *ImagePreviewsFunctionality) OnContactReceiveValue(profile peer.CwtchPee
|
||||||
_, zone, path := path.GetScopeZonePath()
|
_, zone, path := path.GetScopeZonePath()
|
||||||
if exists && zone == attr.ProfileZone && path == constants.CustomProfileImageKey {
|
if exists && zone == attr.ProfileZone && path == constants.CustomProfileImageKey {
|
||||||
// We only download from accepted conversations
|
// We only download from accepted conversations
|
||||||
if conversation.Accepted {
|
if conversation.GetPeerAC().RenderImages {
|
||||||
fileKey := value
|
fileKey := value
|
||||||
basepath := i.downloadFolder
|
basepath := i.downloadFolder
|
||||||
fsf := FunctionalityGate()
|
fsf := FunctionalityGate()
|
||||||
|
@ -124,6 +131,16 @@ func (i *ImagePreviewsFunctionality) OnContactReceiveValue(profile peer.CwtchPee
|
||||||
// handleImagePreviews checks settings and, if appropriate, auto-downloads any images
|
// handleImagePreviews checks settings and, if appropriate, auto-downloads any images
|
||||||
func (i *ImagePreviewsFunctionality) handleImagePreviews(profile peer.CwtchPeer, ev *event.Event, conversationID, senderID int) {
|
func (i *ImagePreviewsFunctionality) handleImagePreviews(profile peer.CwtchPeer, ev *event.Event, conversationID, senderID int) {
|
||||||
if profile.IsFeatureEnabled(constants.FileSharingExperiment) && profile.IsFeatureEnabled(constants.ImagePreviewsExperiment) {
|
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
|
// Short-circuit failures
|
||||||
// Don't auto-download images if the download path does not exist.
|
// Don't auto-download images if the download path does not exist.
|
||||||
|
@ -143,7 +160,7 @@ func (i *ImagePreviewsFunctionality) handleImagePreviews(profile peer.CwtchPeer,
|
||||||
|
|
||||||
// Now look at the image preview experiment
|
// Now look at the image preview experiment
|
||||||
var cm model.MessageWrapper
|
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 {
|
if err == nil && cm.Overlay == model.OverlayFileSharing {
|
||||||
log.Debugf("Received File Sharing Message")
|
log.Debugf("Received File Sharing Message")
|
||||||
var fm OverlayMessage
|
var fm OverlayMessage
|
||||||
|
|
|
@ -0,0 +1,106 @@
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,224 @@
|
||||||
|
// 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
|
||||||
|
}
|
|
@ -0,0 +1,330 @@
|
||||||
|
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
|
||||||
|
}
|
|
@ -0,0 +1,69 @@
|
||||||
|
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)
|
||||||
|
}
|
|
@ -9,6 +9,8 @@ import (
|
||||||
"cwtch.im/cwtch/protocol/connections"
|
"cwtch.im/cwtch/protocol/connections"
|
||||||
"cwtch.im/cwtch/settings"
|
"cwtch.im/cwtch/settings"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"git.openprivacy.ca/openprivacy/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -30,7 +32,7 @@ func (f *Functionality) NotifySettingsUpdate(settings settings.GlobalSettings) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Functionality) EventsToRegister() []event.Type {
|
func (f *Functionality) EventsToRegister() []event.Type {
|
||||||
return []event.Type{event.ProtocolEngineCreated, event.ManifestReceived, event.FileDownloaded}
|
return []event.Type{event.QueueJoinServer}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Functionality) ExperimentsToRegister() []string {
|
func (f *Functionality) ExperimentsToRegister() []string {
|
||||||
|
@ -42,8 +44,9 @@ func (f *Functionality) OnEvent(ev event.Event, profile peer.CwtchPeer) {
|
||||||
if profile.IsFeatureEnabled(constants.GroupsExperiment) {
|
if profile.IsFeatureEnabled(constants.GroupsExperiment) {
|
||||||
switch ev.EventType {
|
switch ev.EventType {
|
||||||
// keep the UI in sync with the current backend server updates...
|
// keep the UI in sync with the current backend server updates...
|
||||||
// TODO: do we need a secondary heartbeat for less common updates?
|
// queue join server gets triggered on load and on new servers so it's a nice
|
||||||
case event.Heartbeat:
|
// low-noise event to hook into...
|
||||||
|
case event.QueueJoinServer:
|
||||||
f.PublishServerUpdate(profile)
|
f.PublishServerUpdate(profile)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -86,25 +89,49 @@ type Server struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// PublishServerUpdate serializes the current list of group servers and publishes an event with this information
|
// PublishServerUpdate serializes the current list of group servers and publishes an event with this information
|
||||||
func (f *Functionality) PublishServerUpdate(profile peer.CwtchPeer) {
|
func (f *Functionality) PublishServerUpdate(profile peer.CwtchPeer) error {
|
||||||
serverListForOnion := f.GetServerInfoList(profile)
|
serverListForOnion := f.GetServerInfoList(profile)
|
||||||
serversListBytes, _ := json.Marshal(serverListForOnion)
|
serversListBytes, err := json.Marshal(serverListForOnion)
|
||||||
profile.PublishEvent(event.NewEvent(UpdateServerInfo, map[event.Field]string{"ProfileOnion": profile.GetOnion(), ServerList: string(serversListBytes)}))
|
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..
|
// GetServerInfoList compiles all the information the UI might need regarding all servers..
|
||||||
func (f *Functionality) GetServerInfoList(profile peer.CwtchPeer) []Server {
|
func (f *Functionality) GetServerInfoList(profile peer.CwtchPeer) []Server {
|
||||||
var servers []Server
|
var servers []Server
|
||||||
for _, server := range profile.GetServers() {
|
for _, server := range profile.GetServers() {
|
||||||
servers = append(servers, f.GetServerInfo(profile, server))
|
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
|
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
|
// GetServerInfo compiles all the information the UI might need regarding a particular server including any verified
|
||||||
// cryptographic keys
|
// cryptographic keys
|
||||||
func (f *Functionality) GetServerInfo(profile peer.CwtchPeer, serverOnion string) Server {
|
func (f *Functionality) GetServerInfo(profile peer.CwtchPeer, serverOnion string) (Server, error) {
|
||||||
serverInfo, _ := profile.FetchConversationInfo(serverOnion)
|
serverInfo, err := profile.FetchConversationInfo(serverOnion)
|
||||||
|
if err != nil {
|
||||||
|
return Server{}, errors.New("server not found")
|
||||||
|
}
|
||||||
keyTypes := []model.KeyType{model.KeyTypeServerOnion, model.KeyTypeTokenOnion, model.KeyTypePrivacyPass}
|
keyTypes := []model.KeyType{model.KeyTypeServerOnion, model.KeyTypeTokenOnion, model.KeyTypePrivacyPass}
|
||||||
var serverKeys []ServerKey
|
var serverKeys []ServerKey
|
||||||
|
|
||||||
|
@ -119,5 +146,5 @@ func (f *Functionality) GetServerInfo(profile peer.CwtchPeer, serverOnion string
|
||||||
recentTimeStr := serverInfo.Attributes[attr.LocalScope.ConstructScopedZonedPath(attr.LegacyGroupZone.ConstructZonedPath(constants.SyncMostRecentMessageTime)).ToString()]
|
recentTimeStr := serverInfo.Attributes[attr.LocalScope.ConstructScopedZonedPath(attr.LegacyGroupZone.ConstructZonedPath(constants.SyncMostRecentMessageTime)).ToString()]
|
||||||
syncStatus := SyncStatus{startTimeStr, recentTimeStr}
|
syncStatus := SyncStatus{startTimeStr, recentTimeStr}
|
||||||
|
|
||||||
return Server{Onion: serverOnion, Identifier: serverInfo.ID, Status: connections.ConnectionStateName[profile.GetPeerState(serverInfo.Handle)], Keys: serverKeys, Description: description, SyncProgress: syncStatus}
|
return Server{Onion: serverOnion, Identifier: serverInfo.ID, Status: connections.ConnectionStateName[profile.GetPeerState(serverInfo.Handle)], Keys: serverKeys, Description: description, SyncProgress: syncStatus}, nil
|
||||||
}
|
}
|
||||||
|
|
6
go.mod
6
go.mod
|
@ -1,10 +1,10 @@
|
||||||
module cwtch.im/cwtch
|
module cwtch.im/cwtch
|
||||||
|
|
||||||
go 1.17
|
go 1.20
|
||||||
|
|
||||||
require (
|
require (
|
||||||
git.openprivacy.ca/cwtch.im/tapir v0.6.0
|
git.openprivacy.ca/cwtch.im/tapir v0.6.0
|
||||||
git.openprivacy.ca/openprivacy/connectivity v1.8.6
|
git.openprivacy.ca/openprivacy/connectivity v1.11.0
|
||||||
git.openprivacy.ca/openprivacy/log v1.0.3
|
git.openprivacy.ca/openprivacy/log v1.0.3
|
||||||
github.com/gtank/ristretto255 v0.1.3-0.20210930101514-6bb39798585c
|
github.com/gtank/ristretto255 v0.1.3-0.20210930101514-6bb39798585c
|
||||||
github.com/mutecomm/go-sqlcipher/v4 v4.4.2
|
github.com/mutecomm/go-sqlcipher/v4 v4.4.2
|
||||||
|
@ -15,7 +15,7 @@ require (
|
||||||
|
|
||||||
require (
|
require (
|
||||||
filippo.io/edwards25519 v1.0.0 // indirect
|
filippo.io/edwards25519 v1.0.0 // indirect
|
||||||
git.openprivacy.ca/openprivacy/bine v0.0.4 // indirect
|
git.openprivacy.ca/openprivacy/bine v0.0.5 // indirect
|
||||||
github.com/google/go-cmp v0.5.8 // indirect
|
github.com/google/go-cmp v0.5.8 // indirect
|
||||||
github.com/gtank/merlin v0.1.1 // indirect
|
github.com/gtank/merlin v0.1.1 // indirect
|
||||||
github.com/mimoo/StrobeGo v0.0.0-20220103164710-9a04d6ca976b // indirect
|
github.com/mimoo/StrobeGo v0.0.0-20220103164710-9a04d6ca976b // indirect
|
||||||
|
|
103
go.sum
103
go.sum
|
@ -1,46 +1,23 @@
|
||||||
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 h1:0wAIcmJUqRdI8IJ/3eGi5/HwXZWPujYXXlkrQogz0Ek=
|
||||||
filippo.io/edwards25519 v1.0.0/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns=
|
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 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/cwtch.im/tapir v0.6.0/go.mod h1:iQIq4y7N+DuP3CxyG66WNEC/d6vzh+wXvvOmelB+KoY=
|
||||||
git.openprivacy.ca/openprivacy/bine v0.0.4 h1:CO7EkGyz+jegZ4ap8g5NWRuDHA/56KKvGySR6OBPW+c=
|
git.openprivacy.ca/openprivacy/bine v0.0.5 h1:DJs5gqw3SkvLSgRDvroqJxZ7F+YsbxbBRg5t0rU5gYE=
|
||||||
git.openprivacy.ca/openprivacy/bine v0.0.4/go.mod h1:13ZqhKyqakDsN/ZkQkIGNULsmLyqtXc46XBcnuXm/mU=
|
git.openprivacy.ca/openprivacy/bine v0.0.5/go.mod h1:fwdeq6RO08WDkV0k7HfArsjRvurVULoUQmT//iaABZM=
|
||||||
git.openprivacy.ca/openprivacy/connectivity v1.8.6 h1:g74PyDGvpMZ3+K0dXy3mlTJh+e0rcwNk0XF8owzkmOA=
|
git.openprivacy.ca/openprivacy/connectivity v1.11.0 h1:roASjaFtQLu+HdH5fa2wx6F00NL3YsUTlmXBJh8aLZk=
|
||||||
git.openprivacy.ca/openprivacy/connectivity v1.8.6/go.mod h1:Hn1gpOx/bRZp5wvCtPQVJPXrfeUH0EGiG/Aoa0vjGLg=
|
git.openprivacy.ca/openprivacy/connectivity v1.11.0/go.mod h1:OQO1+7OIz/jLxDrorEMzvZA6SEbpbDyLGpjoFqT3z1Y=
|
||||||
git.openprivacy.ca/openprivacy/log v1.0.3 h1:E/PMm4LY+Q9s3aDpfySfEDq/vYQontlvNj/scrPaga0=
|
git.openprivacy.ca/openprivacy/log v1.0.3 h1:E/PMm4LY+Q9s3aDpfySfEDq/vYQontlvNj/scrPaga0=
|
||||||
git.openprivacy.ca/openprivacy/log v1.0.3/go.mod h1:gGYK8xHtndRLDymFtmjkG26GaMQNgyhioNS82m812Iw=
|
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.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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
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 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 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
|
||||||
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
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 h1:eQ90iG7K9pOhtereWsmyRJ6RAwcP4tHTDBHXNg+u5is=
|
||||||
github.com/gtank/merlin v0.1.1/go.mod h1:T86dnYJhcGOh5BjZFCJWTDeTK7XW8uE+E21Cy/bIQ+s=
|
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 h1:gkfmnY4Rlt3VINCo4uKdpvngiibQyoENVj5Q88sxXhE=
|
||||||
github.com/gtank/ristretto255 v0.1.3-0.20210930101514-6bb39798585c/go.mod h1:tDPFhGdt3hJWqtKwx57i9baiB1Cj0yAg22VOPUqm5vY=
|
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 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
|
||||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
|
@ -51,115 +28,43 @@ 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/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 h1:eM10bFtI4UvibIsKr10/QT7Yfz+NADfjZYh0GKrXUNc=
|
||||||
github.com/mutecomm/go-sqlcipher/v4 v4.4.2/go.mod h1:mF2UmIpBnzFeBdu/ypTDb/LdbS0nk0dfSN1WUsWTjMA=
|
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 h1:GNapqRSid3zijZ9H77KrgVG4/8KqiyRsxcSxe+7ApXY=
|
||||||
github.com/onsi/ginkgo/v2 v2.1.4/go.mod h1:um6tUpWM/cxCK3/FK8BXqEiUMUwRgSM4JXG47RKZmLU=
|
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 h1:PA/3qinGoukvymdIDV8pii6tiZgC8kbmJO6Z5+b002Q=
|
||||||
github.com/onsi/gomega v1.20.1/go.mod h1:DtrZpjmvpn2mPm4YWQa0/ALMDj9v4YxLgojwPeREyVo=
|
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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
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/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.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.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
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 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU=
|
||||||
go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4=
|
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-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-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-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 h1:3qF+Z8Hkrw9sOhrFHti9TlB1Hkac1x+DNRkv0XQiFjo=
|
||||||
golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
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-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-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 h1:ZmngSVLe/wycRns9MKikG9OWIEjGcGAkacif7oYQaUY=
|
||||||
golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
|
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-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-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-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-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 h1:UiNENfZ8gDvpiWw7IpOMQ27spWmThO1RwwdQVbJahJM=
|
||||||
golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
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.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.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 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
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-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 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 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 h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
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.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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|
|
@ -20,6 +20,9 @@ const (
|
||||||
// LegacyGroupZone for attributes related to legacy group experiment
|
// LegacyGroupZone for attributes related to legacy group experiment
|
||||||
LegacyGroupZone = Zone("legacygroup")
|
LegacyGroupZone = Zone("legacygroup")
|
||||||
|
|
||||||
|
// ConversationZone for attributes related to structure of the conversation
|
||||||
|
ConversationZone = Zone("conversation")
|
||||||
|
|
||||||
// FilesharingZone for attributes related to file sharing
|
// FilesharingZone for attributes related to file sharing
|
||||||
FilesharingZone = Zone("filesharing")
|
FilesharingZone = Zone("filesharing")
|
||||||
|
|
||||||
|
@ -65,6 +68,8 @@ func ParseZone(path string) (Zone, string) {
|
||||||
return ServerKeyZone, parts[1]
|
return ServerKeyZone, parts[1]
|
||||||
case ServerZone:
|
case ServerZone:
|
||||||
return ServerZone, parts[1]
|
return ServerZone, parts[1]
|
||||||
|
case ConversationZone:
|
||||||
|
return ConversationZone, parts[1]
|
||||||
default:
|
default:
|
||||||
return UnknownZone, parts[1]
|
return UnknownZone, parts[1]
|
||||||
}
|
}
|
||||||
|
|
|
@ -57,6 +57,7 @@ const SyncMostRecentMessageTime = "SyncMostRecentMessageTime"
|
||||||
|
|
||||||
const AttrLastConnectionTime = "last-connection-time"
|
const AttrLastConnectionTime = "last-connection-time"
|
||||||
const PeerAutostart = "autostart"
|
const PeerAutostart = "autostart"
|
||||||
|
const PeerAppearOffline = "appear-offline"
|
||||||
const Archived = "archived"
|
const Archived = "archived"
|
||||||
|
|
||||||
const ProfileStatus = "profile-status"
|
const ProfileStatus = "profile-status"
|
||||||
|
@ -66,3 +67,8 @@ const ProfileAttribute3 = "profile-attribute-3"
|
||||||
|
|
||||||
// Description is used on server contacts,
|
// Description is used on server contacts,
|
||||||
const Description = "description"
|
const Description = "description"
|
||||||
|
|
||||||
|
// Used to store the status of acl migrations
|
||||||
|
const ACLVersion = "acl-version"
|
||||||
|
const ACLVersionOne = "acl-v1"
|
||||||
|
const ACLVersionTwo = "acl-v2"
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
package constants
|
||||||
|
|
||||||
|
const CHANNEL_CHAT = 0
|
||||||
|
const CHANNEL_MANAGER = 2
|
|
@ -19,3 +19,6 @@ var AutoDLFileExts = [...]string{".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp
|
||||||
|
|
||||||
// BlodeuweddExperiment enables the Blodeuwedd Assistant
|
// BlodeuweddExperiment enables the Blodeuwedd Assistant
|
||||||
const BlodeuweddExperiment = "blodeuwedd"
|
const BlodeuweddExperiment = "blodeuwedd"
|
||||||
|
|
||||||
|
// Enables the Hybrid Group Manager Extension
|
||||||
|
const GroupManagerExperiment = "group-manager"
|
||||||
|
|
|
@ -4,19 +4,35 @@ import (
|
||||||
"cwtch.im/cwtch/model/attr"
|
"cwtch.im/cwtch/model/attr"
|
||||||
"cwtch.im/cwtch/model/constants"
|
"cwtch.im/cwtch/model/constants"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"git.openprivacy.ca/openprivacy/connectivity/tor"
|
||||||
|
"git.openprivacy.ca/openprivacy/log"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AccessControl is a type determining client assigned authorization to a peer
|
// AccessControl is a type determining client assigned authorization to a peer
|
||||||
|
// for a given conversation
|
||||||
type AccessControl struct {
|
type AccessControl struct {
|
||||||
Blocked bool // Any attempts from this handle to connect are blocked
|
Blocked bool // Any attempts from this handle to connect are blocked overrides all other settings
|
||||||
Read bool // Allows a handle to access the conversation
|
|
||||||
Append bool // Allows a handle to append new messages to the conversation
|
// Basic Conversation Rights
|
||||||
|
Read bool // Allows a handle to access the conversation
|
||||||
|
Append bool // Allows a handle to append new messages to the conversation
|
||||||
|
|
||||||
|
AutoConnect bool // Profile should automatically try to connect with peer
|
||||||
|
ExchangeAttributes bool // Profile should automatically exchange attributes like Name, Profile Image, etc.
|
||||||
|
|
||||||
|
// Extension Related Permissions
|
||||||
|
ShareFiles bool // Allows a handle to share files to a conversation
|
||||||
|
RenderImages bool // Indicates that certain filetypes should be autodownloaded and rendered when shared by this contact
|
||||||
|
|
||||||
|
ManageGroup bool // Allows this conversation to be managed by hybrid groups
|
||||||
}
|
}
|
||||||
|
|
||||||
// DefaultP2PAccessControl - because in the year 2021, go does not support constant structs...
|
// DefaultP2PAccessControl defaults to a semi-trusted peer with no access to special extensions.
|
||||||
func DefaultP2PAccessControl() AccessControl {
|
func DefaultP2PAccessControl() AccessControl {
|
||||||
return AccessControl{Read: true, Append: true, Blocked: false}
|
return AccessControl{Read: true, Append: true, ExchangeAttributes: true, Blocked: false,
|
||||||
|
AutoConnect: true, ShareFiles: false, RenderImages: false}
|
||||||
}
|
}
|
||||||
|
|
||||||
// AccessControlList represents an access control list for a conversation. Mapping handles to conversation
|
// AccessControlList represents an access control list for a conversation. Mapping handles to conversation
|
||||||
|
@ -30,10 +46,10 @@ func (acl *AccessControlList) Serialize() []byte {
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeserializeAccessControlList takes in JSON and returns an AccessControlList
|
// DeserializeAccessControlList takes in JSON and returns an AccessControlList
|
||||||
func DeserializeAccessControlList(data []byte) AccessControlList {
|
func DeserializeAccessControlList(data []byte) (AccessControlList, error) {
|
||||||
var acl AccessControlList
|
var acl AccessControlList
|
||||||
json.Unmarshal(data, &acl)
|
err := json.Unmarshal(data, &acl)
|
||||||
return acl
|
return acl, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Attributes a type-driven encapsulation of an Attribute map.
|
// Attributes a type-driven encapsulation of an Attribute map.
|
||||||
|
@ -47,8 +63,12 @@ func (a *Attributes) Serialize() []byte {
|
||||||
|
|
||||||
// DeserializeAttributes converts a JSON struct into an Attributes map
|
// DeserializeAttributes converts a JSON struct into an Attributes map
|
||||||
func DeserializeAttributes(data []byte) Attributes {
|
func DeserializeAttributes(data []byte) Attributes {
|
||||||
var attributes Attributes
|
attributes := make(Attributes)
|
||||||
json.Unmarshal(data, &attributes)
|
err := json.Unmarshal(data, &attributes)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("error deserializing attributes (this is likely a programming error): %v", err)
|
||||||
|
return make(Attributes)
|
||||||
|
}
|
||||||
return attributes
|
return attributes
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -60,7 +80,9 @@ type Conversation struct {
|
||||||
Handle string
|
Handle string
|
||||||
Attributes Attributes
|
Attributes Attributes
|
||||||
ACL AccessControlList
|
ACL AccessControlList
|
||||||
Accepted bool
|
|
||||||
|
// Deprecated, please use ACL for permissions related functions
|
||||||
|
Accepted bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAttribute is a helper function that fetches a conversation attribute by scope, zone and key
|
// GetAttribute is a helper function that fetches a conversation attribute by scope, zone and key
|
||||||
|
@ -71,6 +93,36 @@ func (ci *Conversation) GetAttribute(scope attr.Scope, zone attr.Zone, key strin
|
||||||
return "", false
|
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
|
// IsGroup is a helper attribute that identifies whether a conversation is a legacy group
|
||||||
func (ci *Conversation) IsGroup() bool {
|
func (ci *Conversation) IsGroup() bool {
|
||||||
if _, exists := ci.Attributes[attr.LocalScope.ConstructScopedZonedPath(attr.LegacyGroupZone.ConstructZonedPath(constants.GroupID)).ToString()]; exists {
|
if _, exists := ci.Attributes[attr.LocalScope.ConstructScopedZonedPath(attr.LegacyGroupZone.ConstructZonedPath(constants.GroupID)).ToString()]; exists {
|
||||||
|
|
|
@ -6,13 +6,13 @@ import "sync"
|
||||||
// examples of experiments include File Sharing, Profile Images and Groups.
|
// examples of experiments include File Sharing, Profile Images and Groups.
|
||||||
type Experiments struct {
|
type Experiments struct {
|
||||||
enabled bool
|
enabled bool
|
||||||
experiments sync.Map
|
experiments *sync.Map
|
||||||
}
|
}
|
||||||
|
|
||||||
// InitExperiments encapsulates a set of experiments separate from their storage in GlobalSettings.
|
// InitExperiments encapsulates a set of experiments separate from their storage in GlobalSettings.
|
||||||
func InitExperiments(enabled bool, experiments map[string]bool) Experiments {
|
func InitExperiments(enabled bool, experiments map[string]bool) Experiments {
|
||||||
|
|
||||||
var syncExperiments sync.Map
|
syncExperiments := new(sync.Map)
|
||||||
for experiment, set := range experiments {
|
for experiment, set := range experiments {
|
||||||
syncExperiments.Store(experiment, set)
|
syncExperiments.Store(experiment, set)
|
||||||
}
|
}
|
||||||
|
|
|
@ -59,21 +59,26 @@ 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
|
// Derive Group ID from the group key and the server public key. This binds the group to a particular server
|
||||||
// and key.
|
// and key.
|
||||||
group.GroupID = deriveGroupID(groupKey[:], server)
|
var err error
|
||||||
return group, nil
|
group.GroupID, err = deriveGroupID(groupKey[:], server)
|
||||||
|
return group, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// CheckGroup returns true only if the ID of the group is cryptographically valid.
|
// CheckGroup returns true only if the ID of the group is cryptographically valid.
|
||||||
func (g *Group) CheckGroup() bool {
|
func (g *Group) CheckGroup() bool {
|
||||||
return g.GroupID == deriveGroupID(g.GroupKey[:], g.GroupServer)
|
id, _ := deriveGroupID(g.GroupKey[:], g.GroupServer)
|
||||||
|
return g.GroupID == id
|
||||||
}
|
}
|
||||||
|
|
||||||
// deriveGroupID hashes together the key and the hostname to create a bound identifier that can later
|
// 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.
|
// be referenced and checked by profiles when they receive invites and messages.
|
||||||
func deriveGroupID(groupKey []byte, serverHostname string) string {
|
func deriveGroupID(groupKey []byte, serverHostname string) (string, error) {
|
||||||
data, _ := base32.StdEncoding.DecodeString(strings.ToUpper(serverHostname))
|
data, err := base32.StdEncoding.DecodeString(strings.ToUpper(serverHostname))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
pubkey := data[0:ed25519.PublicKeySize]
|
pubkey := data[0:ed25519.PublicKeySize]
|
||||||
return hex.EncodeToString(pbkdf2.Key(groupKey, pubkey, 4096, 16, sha512.New))
|
return hex.EncodeToString(pbkdf2.Key(groupKey, pubkey, 4096, 16, sha512.New)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Invite generates a invitation that can be sent to a cwtch peer
|
// Invite generates a invitation that can be sent to a cwtch peer
|
||||||
|
@ -148,7 +153,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
|
// 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...
|
// 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 {
|
if derivedGroupID != gci.GroupID {
|
||||||
return nil, errors.New("group id is invalid")
|
return nil, errors.New("group id is invalid")
|
||||||
}
|
}
|
||||||
|
@ -166,7 +171,9 @@ func ValidateInvite(invite string) (*groups.GroupInvite, error) {
|
||||||
// If successful, adds the message to the group's timeline
|
// If successful, adds the message to the group's timeline
|
||||||
func (g *Group) AttemptDecryption(ciphertext []byte, signature []byte) (bool, *groups.DecryptedGroupMessage) {
|
func (g *Group) AttemptDecryption(ciphertext []byte, signature []byte) (bool, *groups.DecryptedGroupMessage) {
|
||||||
success, dgm := g.DecryptMessage(ciphertext)
|
success, dgm := g.DecryptMessage(ciphertext)
|
||||||
if success {
|
// 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 {
|
||||||
|
|
||||||
// Attempt to serialize this message
|
// Attempt to serialize this message
|
||||||
serialized, err := json.Marshal(dgm)
|
serialized, err := json.Marshal(dgm)
|
||||||
|
|
|
@ -9,7 +9,10 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestGroup(t *testing.T) {
|
func TestGroup(t *testing.T) {
|
||||||
g, _ := NewGroup("2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd")
|
g, err := NewGroup("2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Group with real group server should not fail")
|
||||||
|
}
|
||||||
dgm := &groups.DecryptedGroupMessage{
|
dgm := &groups.DecryptedGroupMessage{
|
||||||
Onion: "onion",
|
Onion: "onion",
|
||||||
Text: "Hello World!",
|
Text: "Hello World!",
|
||||||
|
@ -37,7 +40,7 @@ func TestGroup(t *testing.T) {
|
||||||
|
|
||||||
encMessage, _ := g.EncryptMessage(dgm)
|
encMessage, _ := g.EncryptMessage(dgm)
|
||||||
ok, message := g.DecryptMessage(encMessage)
|
ok, message := g.DecryptMessage(encMessage)
|
||||||
if !ok || message.Text != "Hello World!" {
|
if (!ok || message == nil) || message.Text != "Hello World!" {
|
||||||
t.Errorf("group encryption was invalid, or returned wrong message decrypted:%v message:%v", ok, message)
|
t.Errorf("group encryption was invalid, or returned wrong message decrypted:%v message:%v", ok, message)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -73,7 +76,10 @@ func TestGroupValidation(t *testing.T) {
|
||||||
t.Logf("Error: %v", err)
|
t.Logf("Error: %v", err)
|
||||||
|
|
||||||
// Generate a valid group but replace the group server...
|
// Generate a valid group but replace the group server...
|
||||||
group, _ = NewGroup("2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd")
|
group, err = NewGroup("2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Group with real group server should not fail")
|
||||||
|
}
|
||||||
group.GroupServer = "tcnkoch4nyr3cldkemejtkpqok342rbql6iclnjjs3ndgnjgufzyxvqd"
|
group.GroupServer = "tcnkoch4nyr3cldkemejtkpqok342rbql6iclnjjs3ndgnjgufzyxvqd"
|
||||||
invite, _ = group.Invite()
|
invite, _ = group.Invite()
|
||||||
_, err = ValidateInvite(invite)
|
_, err = ValidateInvite(invite)
|
||||||
|
@ -84,7 +90,10 @@ func TestGroupValidation(t *testing.T) {
|
||||||
t.Logf("Error: %v", err)
|
t.Logf("Error: %v", err)
|
||||||
|
|
||||||
// Generate a valid group but replace the group key...
|
// Generate a valid group but replace the group key...
|
||||||
group, _ = NewGroup("2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd")
|
group, err = NewGroup("2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Group with real group server should not fail")
|
||||||
|
}
|
||||||
group.GroupKey = sha256.Sum256([]byte{})
|
group.GroupKey = sha256.Sum256([]byte{})
|
||||||
invite, _ = group.Invite()
|
invite, _ = group.Invite()
|
||||||
_, err = ValidateInvite(invite)
|
_, err = ValidateInvite(invite)
|
||||||
|
|
|
@ -3,6 +3,7 @@ package model
|
||||||
import (
|
import (
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CalculateContentHash derives a hash using the author and the message body. It is intended to be
|
// CalculateContentHash derives a hash using the author and the message body. It is intended to be
|
||||||
|
@ -12,3 +13,13 @@ func CalculateContentHash(author string, messageBody string) string {
|
||||||
contentBasedHash := sha256.Sum256(content)
|
contentBasedHash := sha256.Sum256(content)
|
||||||
return base64.StdEncoding.EncodeToString(contentBasedHash[:])
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -1,9 +1,41 @@
|
||||||
package model
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
// MessageWrapper is the canonical Cwtch overlay wrapper
|
// MessageWrapper is the canonical Cwtch overlay wrapper
|
||||||
type MessageWrapper struct {
|
type MessageWrapper struct {
|
||||||
Overlay int `json:"o"`
|
Overlay int `json:"o"`
|
||||||
Data string `json:"d"`
|
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
|
// OverlayChat is the canonical identifier for chat overlays
|
||||||
|
@ -17,3 +49,6 @@ const OverlayInviteGroup = 101
|
||||||
|
|
||||||
// OverlayFileSharing is the canonical identifier for the file sharing overlay
|
// OverlayFileSharing is the canonical identifier for the file sharing overlay
|
||||||
const OverlayFileSharing = 200
|
const OverlayFileSharing = 200
|
||||||
|
|
||||||
|
// ManageGroupEvent is the canonical identifier for the manage group overlay
|
||||||
|
const OverlayManageGroupEvent = 0x402
|
||||||
|
|
|
@ -54,11 +54,9 @@ const MaxGroupMessageLength = 1800
|
||||||
|
|
||||||
func getRandomness(arr *[]byte) {
|
func getRandomness(arr *[]byte) {
|
||||||
if _, err := io.ReadFull(rand.Reader, (*arr)[:]); err != nil {
|
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
|
||||||
// If we can't do randomness, just crash something is very very wrong and we are not going
|
// to resolve it here....
|
||||||
// to resolve it here....
|
panic(err.Error())
|
||||||
panic(err.Error())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -80,11 +78,19 @@ func (p *Profile) GetCopy(timeline bool) *Profile {
|
||||||
|
|
||||||
if timeline {
|
if timeline {
|
||||||
for groupID := range newp.Groups {
|
for groupID := range newp.Groups {
|
||||||
newp.Groups[groupID].Timeline = *p.Groups[groupID].Timeline.GetCopy()
|
if group, exists := newp.Groups[groupID]; exists {
|
||||||
|
if pGroup, exists := p.Groups[groupID]; exists {
|
||||||
|
group.Timeline = *(pGroup).Timeline.GetCopy()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for peerID := range newp.Contacts {
|
for peerID := range newp.Contacts {
|
||||||
newp.Contacts[peerID].Timeline = *p.Contacts[peerID].Timeline.GetCopy()
|
if peer, exists := newp.Contacts[peerID]; exists {
|
||||||
|
if pPeer, exists := p.Contacts[peerID]; exists {
|
||||||
|
peer.Timeline = *(pPeer).Timeline.GetCopy()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,6 +16,7 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"cwtch.im/cwtch/model"
|
||||||
"cwtch.im/cwtch/model/constants"
|
"cwtch.im/cwtch/model/constants"
|
||||||
"cwtch.im/cwtch/protocol/groups"
|
"cwtch.im/cwtch/protocol/groups"
|
||||||
"cwtch.im/cwtch/settings"
|
"cwtch.im/cwtch/settings"
|
||||||
|
@ -26,7 +27,6 @@ import (
|
||||||
"golang.org/x/crypto/ed25519"
|
"golang.org/x/crypto/ed25519"
|
||||||
|
|
||||||
"cwtch.im/cwtch/event"
|
"cwtch.im/cwtch/event"
|
||||||
"cwtch.im/cwtch/model"
|
|
||||||
"cwtch.im/cwtch/model/attr"
|
"cwtch.im/cwtch/model/attr"
|
||||||
"cwtch.im/cwtch/protocol/connections"
|
"cwtch.im/cwtch/protocol/connections"
|
||||||
"git.openprivacy.ca/openprivacy/log"
|
"git.openprivacy.ca/openprivacy/log"
|
||||||
|
@ -94,7 +94,7 @@ func (cp *cwtchPeer) EnhancedImportBundle(importString string) string {
|
||||||
return cp.ImportBundle(importString).Error()
|
return cp.ImportBundle(importString).Error()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cp *cwtchPeer) EnhancedGetMessages(conversation int, index int, count int) string {
|
func (cp *cwtchPeer) EnhancedGetMessages(conversation int, index int, count uint) string {
|
||||||
var emessages = make([]EnhancedMessage, count)
|
var emessages = make([]EnhancedMessage, count)
|
||||||
|
|
||||||
messages, err := cp.GetMostRecentMessages(conversation, 0, index, count)
|
messages, err := cp.GetMostRecentMessages(conversation, 0, index, count)
|
||||||
|
@ -145,7 +145,7 @@ func (cp *cwtchPeer) EnhancedGetMessageByContentHash(conversation int, contentHa
|
||||||
offset, err := cp.GetChannelMessageByContentHash(conversation, 0, contentHash)
|
offset, err := cp.GetChannelMessageByContentHash(conversation, 0, contentHash)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
messages, err := cp.GetMostRecentMessages(conversation, 0, offset, 1)
|
messages, err := cp.GetMostRecentMessages(conversation, 0, offset, 1)
|
||||||
if err == nil {
|
if len(messages) > 0 && err == nil {
|
||||||
sentTime, _ := time.Parse(time.RFC3339Nano, messages[0].Attr[constants.AttrSentTimestamp])
|
sentTime, _ := time.Parse(time.RFC3339Nano, messages[0].Attr[constants.AttrSentTimestamp])
|
||||||
message.Message = model.Message{
|
message.Message = model.Message{
|
||||||
Message: messages[0].Body,
|
Message: messages[0].Body,
|
||||||
|
@ -194,10 +194,16 @@ func (cp *cwtchPeer) UpdateExperiments(enabled bool, experiments map[string]bool
|
||||||
cp.experiments = model.InitExperiments(enabled, experiments)
|
cp.experiments = model.InitExperiments(enabled, experiments)
|
||||||
}
|
}
|
||||||
|
|
||||||
// NotifySettingsUpdate notifies a Cwtch profile of a change in the nature of global experiments. The Cwtch Profile uses
|
// NotifySettingsUpdate notifies a Cwtch profile of a change in the nature of global settings.
|
||||||
// this information to update registered extensions.
|
// The Cwtch Profile uses this information to update registered extensions in addition
|
||||||
|
// to updating internal settings.
|
||||||
func (cp *cwtchPeer) NotifySettingsUpdate(settings settings.GlobalSettings) {
|
func (cp *cwtchPeer) NotifySettingsUpdate(settings settings.GlobalSettings) {
|
||||||
log.Debugf("Cwtch Profile Settings Update: %v", settings)
|
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()
|
cp.extensionLock.Lock()
|
||||||
defer cp.extensionLock.Unlock()
|
defer cp.extensionLock.Unlock()
|
||||||
for _, extension := range cp.extensions {
|
for _, extension := range cp.extensions {
|
||||||
|
@ -304,7 +310,33 @@ func (cp *cwtchPeer) GenerateProtocolEngine(acn connectivity.ACN, bus event.Mana
|
||||||
|
|
||||||
authorizations := make(map[string]model.Authorization)
|
authorizations := make(map[string]model.Authorization)
|
||||||
for _, conversation := range conversations {
|
for _, conversation := range conversations {
|
||||||
if tor.IsValidHostname(conversation.Handle) {
|
|
||||||
|
// Only perform the following actions for Peer-type Conversaions...
|
||||||
|
if conversation.IsCwtchPeer() {
|
||||||
|
// if this profile does not have an ACL version, and the profile is accepted (OR the acl version is v1 and the profile is accepted...)
|
||||||
|
// then migrate the permissions to the v2 ACL
|
||||||
|
// migrate the old accepted AC to a new fine-grained one
|
||||||
|
// we only do this for previously trusted connections
|
||||||
|
// NOTE: this does not supercede global cwthch experiments settings
|
||||||
|
// if share files is turned off globally then acl.ShareFiles will be ignored
|
||||||
|
// Note: There was a bug in the original EP code that meant that some acl-v1 profiles did not get ShareFiles or RenderImages - this corrects that.
|
||||||
|
if version, exists := conversation.GetAttribute(attr.LocalScope, attr.ProfileZone, constants.ACLVersion); !exists || version == constants.ACLVersionOne {
|
||||||
|
if conversation.Accepted {
|
||||||
|
if ac, exists := conversation.ACL[conversation.Handle]; exists {
|
||||||
|
ac.ShareFiles = true
|
||||||
|
ac.RenderImages = true
|
||||||
|
ac.AutoConnect = true
|
||||||
|
ac.ExchangeAttributes = true
|
||||||
|
conversation.ACL[conversation.Handle] = ac
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the ACL Version
|
||||||
|
cp.storage.SetConversationAttribute(conversation.ID, attr.LocalScope.ConstructScopedZonedPath(attr.ProfileZone.ConstructZonedPath(constants.ACLVersion)), constants.ACLVersionTwo)
|
||||||
|
// Store the updated ACL
|
||||||
|
cp.storage.SetConversationACL(conversation.ID, conversation.ACL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if conversation.ACL[conversation.Handle].Blocked {
|
if conversation.ACL[conversation.Handle].Blocked {
|
||||||
authorizations[conversation.Handle] = model.AuthBlocked
|
authorizations[conversation.Handle] = model.AuthBlocked
|
||||||
} else {
|
} else {
|
||||||
|
@ -404,12 +436,19 @@ func (cp *cwtchPeer) SendMessage(conversation int, message string) (int, error)
|
||||||
if tor.IsValidHostname(conversationInfo.Handle) {
|
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})
|
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())
|
onion, _ := cp.storage.LoadProfileKeyValue(TypeAttribute, attr.PublicScope.ConstructScopedZonedPath(attr.ProfileZone.ConstructZonedPath(constants.Onion)).ToString())
|
||||||
|
id := -1
|
||||||
|
|
||||||
// For p2p messages we store the event id of the message as the "signature" we can then look this up in the database later for acks
|
// check if we should store this message locally...
|
||||||
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 cm, err := model.DeserializeMessage(message); err == nil {
|
||||||
if err != nil {
|
if !cm.IsStream() {
|
||||||
return -1, err
|
// 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))
|
||||||
|
if err != nil {
|
||||||
|
return -1, err
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cp.eventBus.Publish(ev)
|
cp.eventBus.Publish(ev)
|
||||||
return id, nil
|
return id, nil
|
||||||
} else {
|
} else {
|
||||||
|
@ -664,11 +703,19 @@ 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.LegacyGroupZone.ConstructZonedPath(constants.GroupKey)), base64.StdEncoding.EncodeToString(gci.SharedKey))
|
||||||
cp.SetConversationAttribute(groupConversationID, attr.LocalScope.ConstructScopedZonedPath(attr.ProfileZone.ConstructZonedPath(constants.Name)), gci.GroupName)
|
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.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.JoinServer(gci.ServerHost)
|
cp.QueueJoinServer(gci.ServerHost)
|
||||||
}
|
}
|
||||||
return groupConversationID, err
|
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.
|
// 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) {
|
func (cp *cwtchPeer) NewContactConversation(handle string, acl model.AccessControl, accepted bool) (int, error) {
|
||||||
cp.mutex.Lock()
|
cp.mutex.Lock()
|
||||||
|
@ -676,13 +723,60 @@ func (cp *cwtchPeer) NewContactConversation(handle string, acl model.AccessContr
|
||||||
conversationInfo, _ := cp.storage.GetConversationByHandle(handle)
|
conversationInfo, _ := cp.storage.GetConversationByHandle(handle)
|
||||||
if conversationInfo == nil {
|
if conversationInfo == nil {
|
||||||
conversationID, err := cp.storage.NewConversation(handle, model.Attributes{event.SaveHistoryKey: event.DeleteHistoryDefault}, model.AccessControlList{handle: acl}, accepted)
|
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))
|
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}))
|
cp.eventBus.Publish(event.NewEvent(event.ContactCreated, map[event.Field]string{event.ConversationID: strconv.Itoa(conversationID), event.RemotePeer: handle}))
|
||||||
return conversationID, err
|
return conversationID, err
|
||||||
}
|
}
|
||||||
return -1, fmt.Errorf("contact conversation already exists")
|
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`
|
// 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
|
// This will cause Cwtch to auto connect to this conversation on start up
|
||||||
func (cp *cwtchPeer) AcceptConversation(id int) error {
|
func (cp *cwtchPeer) AcceptConversation(id int) error {
|
||||||
|
@ -695,6 +789,21 @@ func (cp *cwtchPeer) AcceptConversation(id int) error {
|
||||||
log.Errorf("Could not get conversation for %v: %v", id, err)
|
log.Errorf("Could not get conversation for %v: %v", id, err)
|
||||||
return 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() {
|
if !ci.IsGroup() && !ci.IsServer() {
|
||||||
cp.sendUpdateAuth(id, ci.Handle, ci.Accepted, ci.ACL[ci.Handle].Blocked)
|
cp.sendUpdateAuth(id, ci.Handle, ci.Accepted, ci.ACL[ci.Handle].Blocked)
|
||||||
cp.PeerWithOnion(ci.Handle)
|
cp.PeerWithOnion(ci.Handle)
|
||||||
|
@ -742,7 +851,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
|
// 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)
|
cp.sendUpdateAuth(id, ci.Handle, ci.Accepted, ci.ACL[ci.Handle].Blocked)
|
||||||
|
|
||||||
if !ci.IsGroup() && !ci.IsServer() && ci.Accepted {
|
if !ci.IsGroup() && !ci.IsServer() && ci.GetPeerAC().AutoConnect {
|
||||||
cp.PeerWithOnion(ci.Handle)
|
cp.PeerWithOnion(ci.Handle)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -797,6 +906,26 @@ func (cp *cwtchPeer) GetConversationAttribute(id int, path attr.ScopedZonedPath)
|
||||||
return val, nil
|
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.
|
// 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
|
// 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.)
|
// in the table (e.g. deleted messages, expired messages, etc.)
|
||||||
|
@ -844,7 +973,8 @@ func (cp *cwtchPeer) doSearch(ctx context.Context, searchID string, pattern stri
|
||||||
}
|
}
|
||||||
for _, matchingMessage := range matchingMessages {
|
for _, matchingMessage := range matchingMessages {
|
||||||
// publish this search result...
|
// publish this search result...
|
||||||
cp.PublishEvent(event.NewEvent(event.SearchResult, map[event.Field]string{event.SearchID: searchID, event.ConversationID: strconv.Itoa(conversation.ID), event.Index: strconv.Itoa(matchingMessage.ID)}))
|
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)
|
log.Debugf("found matching message: %q", matchingMessage)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -881,7 +1011,7 @@ func (cp *cwtchPeer) GetChannelMessageCount(conversation int, channel int) (int,
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetMostRecentMessages returns a selection of messages, ordered by most recently inserted
|
// GetMostRecentMessages returns a selection of messages, ordered by most recently inserted
|
||||||
func (cp *cwtchPeer) GetMostRecentMessages(conversation int, channel int, offset int, limit int) ([]model.ConversationMessage, error) {
|
func (cp *cwtchPeer) GetMostRecentMessages(conversation int, channel int, offset int, limit uint) ([]model.ConversationMessage, error) {
|
||||||
return cp.storage.GetMostRecentMessages(conversation, channel, offset, limit)
|
return cp.storage.GetMostRecentMessages(conversation, channel, offset, limit)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -980,13 +1110,13 @@ func (cp *cwtchPeer) AddServer(serverSpecification string) (string, error) {
|
||||||
// we haven't seen this key associated with the server before
|
// 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
|
// 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
|
// server with no conflicting keys. So we are going to save all the keys
|
||||||
for k, v := range ab {
|
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(k)), v)
|
||||||
}
|
}
|
||||||
cp.SetConversationAttribute(conversationInfo.ID, attr.PublicScope.ConstructScopedZonedPath(attr.ServerKeyZone.ConstructZonedPath(string(model.BundleType))), serverSpecification)
|
cp.SetConversationAttribute(conversationInfo.ID, attr.PublicScope.ConstructScopedZonedPath(attr.ServerKeyZone.ConstructZonedPath(string(model.BundleType))), serverSpecification)
|
||||||
cp.JoinServer(onion)
|
cp.QueueJoinServer(onion)
|
||||||
return onion, err
|
return onion, err
|
||||||
}
|
}
|
||||||
return "", err
|
return "", err
|
||||||
|
@ -1035,16 +1165,23 @@ 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)}))
|
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
|
// 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
|
// Status: Ready for 1.10
|
||||||
func (cp *cwtchPeer) QueuePeeringWithOnion(handle string) {
|
func (cp *cwtchPeer) QueuePeeringWithOnion(handle string) {
|
||||||
lastSeen := event.CwtchEpoch
|
|
||||||
ci, err := cp.FetchConversationInfo(handle)
|
ci, err := cp.FetchConversationInfo(handle)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
lastSeen = cp.GetConversationLastSeenTime(ci.ID)
|
lastSeen := cp.GetConversationLastSeenTime(ci.ID)
|
||||||
}
|
if !ci.ACL[ci.Handle].Blocked {
|
||||||
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)}))
|
||||||
cp.eventBus.Publish(event.NewEvent(event.QueuePeerRequest, map[event.Field]string{event.RemotePeer: handle, event.LastSeen: lastSeen.Format(time.RFC3339Nano)}))
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1160,9 +1297,9 @@ func (cp *cwtchPeer) ImportBundle(importString string) error {
|
||||||
return ConstructResponse(constants.ImportBundlePrefix, "success")
|
return ConstructResponse(constants.ImportBundlePrefix, "success")
|
||||||
} else if tor.IsValidHostname(importString) {
|
} else if tor.IsValidHostname(importString) {
|
||||||
_, err := cp.NewContactConversation(importString, model.DefaultP2PAccessControl(), true)
|
_, 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 {
|
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, "success")
|
||||||
}
|
}
|
||||||
return ConstructResponse(constants.ImportBundlePrefix, err.Error())
|
return ConstructResponse(constants.ImportBundlePrefix, err.Error())
|
||||||
|
@ -1172,28 +1309,38 @@ func (cp *cwtchPeer) ImportBundle(importString string) error {
|
||||||
|
|
||||||
// JoinServer manages a new server connection with the given onion address
|
// JoinServer manages a new server connection with the given onion address
|
||||||
func (cp *cwtchPeer) JoinServer(onion string) error {
|
func (cp *cwtchPeer) JoinServer(onion string) error {
|
||||||
ci, err := cp.FetchConversationInfo(onion)
|
|
||||||
if ci == nil || err != nil {
|
// 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")
|
||||||
|
}
|
||||||
|
|
||||||
|
//if cp.GetContact(onion) != nil {
|
||||||
|
tokenY, yExists := ci.Attributes[attr.PublicScope.ConstructScopedZonedPath(attr.ServerKeyZone.ConstructZonedPath(string(model.KeyTypePrivacyPass))).ToString()]
|
||||||
|
tokenOnion, onionExists := ci.Attributes[attr.PublicScope.ConstructScopedZonedPath(attr.ServerKeyZone.ConstructZonedPath(string(model.KeyTypeTokenOnion))).ToString()]
|
||||||
|
if yExists && onionExists {
|
||||||
|
signature, exists := ci.Attributes[attr.LocalScope.ConstructScopedZonedPath(attr.ProfileZone.ConstructZonedPath(lastReceivedSignature)).ToString()]
|
||||||
|
if !exists {
|
||||||
|
signature = base64.StdEncoding.EncodeToString([]byte{})
|
||||||
|
}
|
||||||
|
cachedTokensJson, hasCachedTokens := ci.GetAttribute(attr.LocalScope, attr.ServerZone, "tokens")
|
||||||
|
if hasCachedTokens {
|
||||||
|
log.Debugf("using cached tokens for %v", ci.Handle)
|
||||||
|
}
|
||||||
|
|
||||||
|
cp.eventBus.Publish(event.NewEvent(event.JoinServer, map[event.Field]string{event.GroupServer: onion, event.ServerTokenY: tokenY, event.ServerTokenOnion: tokenOnion, event.Signature: signature, event.CachedTokens: cachedTokensJson}))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
return errors.New("no keys found for server connection")
|
return errors.New("no keys found for server connection")
|
||||||
}
|
}
|
||||||
|
return errors.New("group experiment is not enabled")
|
||||||
//if cp.GetContact(onion) != nil {
|
|
||||||
tokenY, yExists := ci.Attributes[attr.PublicScope.ConstructScopedZonedPath(attr.ServerKeyZone.ConstructZonedPath(string(model.KeyTypePrivacyPass))).ToString()]
|
|
||||||
tokenOnion, onionExists := ci.Attributes[attr.PublicScope.ConstructScopedZonedPath(attr.ServerKeyZone.ConstructZonedPath(string(model.KeyTypeTokenOnion))).ToString()]
|
|
||||||
if yExists && onionExists {
|
|
||||||
signature, exists := ci.Attributes[attr.LocalScope.ConstructScopedZonedPath(attr.ProfileZone.ConstructZonedPath(lastReceivedSignature)).ToString()]
|
|
||||||
if !exists {
|
|
||||||
signature = base64.StdEncoding.EncodeToString([]byte{})
|
|
||||||
}
|
|
||||||
cachedTokensJson, hasCachedTokens := ci.GetAttribute(attr.LocalScope, attr.ServerZone, "tokens")
|
|
||||||
if hasCachedTokens {
|
|
||||||
log.Debugf("using cached tokens for %v", ci.Handle)
|
|
||||||
}
|
|
||||||
|
|
||||||
cp.eventBus.Publish(event.NewEvent(event.JoinServer, map[event.Field]string{event.GroupServer: onion, event.ServerTokenY: tokenY, event.ServerTokenOnion: tokenOnion, event.Signature: signature, event.CachedTokens: cachedTokensJson}))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return errors.New("no keys found for server connection")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MakeAntispamPayment allows a peer to retrigger antispam, important if the initial connection somehow fails...
|
// MakeAntispamPayment allows a peer to retrigger antispam, important if the initial connection somehow fails...
|
||||||
|
@ -1292,7 +1439,7 @@ func (cp *cwtchPeer) getConnectionsSortedByLastSeen(doPeers, doServers bool) []*
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if !doPeers || !conversation.Accepted {
|
if !doPeers {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1309,12 +1456,15 @@ func (cp *cwtchPeer) StartConnections(doPeers, doServers bool) {
|
||||||
byRecent := cp.getConnectionsSortedByLastSeen(doPeers, doServers)
|
byRecent := cp.getConnectionsSortedByLastSeen(doPeers, doServers)
|
||||||
log.Infof("StartConnections for %v", cp.GetOnion())
|
log.Infof("StartConnections for %v", cp.GetOnion())
|
||||||
for _, conversation := range byRecent {
|
for _, conversation := range byRecent {
|
||||||
if conversation.model.IsServer() {
|
// only bother tracking servers if the experiment is enabled...
|
||||||
|
if conversation.model.IsServer() && cp.IsFeatureEnabled(constants.GroupsExperiment) {
|
||||||
log.Debugf(" QueueJoinServer(%v)", conversation.model.Handle)
|
log.Debugf(" QueueJoinServer(%v)", conversation.model.Handle)
|
||||||
cp.QueueJoinServer(conversation.model.Handle)
|
cp.QueueJoinServer(conversation.model.Handle)
|
||||||
} else {
|
} else {
|
||||||
log.Debugf(" QueuePeerWithOnion(%v)", conversation.model.Handle)
|
log.Debugf(" QueuePeerWithOnion(%v)", conversation.model.Handle)
|
||||||
cp.QueuePeeringWithOnion(conversation.model.Handle)
|
if conversation.model.GetPeerAC().AutoConnect {
|
||||||
|
cp.QueuePeeringWithOnion(conversation.model.Handle)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
time.Sleep(50 * time.Millisecond)
|
time.Sleep(50 * time.Millisecond)
|
||||||
}
|
}
|
||||||
|
@ -1378,9 +1528,22 @@ 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
|
// Generate a random number and use it as the signature
|
||||||
signature := event.GetRandNumber().String()
|
signature := event.GetRandNumber().String()
|
||||||
return cp.storage.InsertMessage(ci.ID, 0, message, model.Attributes{constants.AttrAuthor: handle, constants.AttrAck: event.True, constants.AttrSentTimestamp: sent.Format(time.RFC3339Nano)}, signature, model.CalculateContentHash(handle, message))
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
// eventHandler process events from other subsystems
|
// eventHandler process events from other subsystems
|
||||||
|
@ -1395,6 +1558,7 @@ func (cp *cwtchPeer) eventHandler() {
|
||||||
onion, _ := cp.storage.LoadProfileKeyValue(TypeAttribute, attr.PublicScope.ConstructScopedZonedPath(attr.ProfileZone.ConstructZonedPath(constants.Onion)).ToString())
|
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])
|
log.Infof("Protocol engine for %s has stopped listening: %v", onion, ev.Data[event.Error])
|
||||||
cp.mutex.Unlock()
|
cp.mutex.Unlock()
|
||||||
|
cp.processExtensionsEvent(ev)
|
||||||
case event.EncryptedGroupMessage:
|
case event.EncryptedGroupMessage:
|
||||||
|
|
||||||
// If successful, a side effect is the message is added to the group's timeline
|
// If successful, a side effect is the message is added to the group's timeline
|
||||||
|
@ -1439,6 +1603,7 @@ func (cp *cwtchPeer) eventHandler() {
|
||||||
case event.NewMessageFromPeerEngine: //event.TimestampReceived, event.RemotePeer, event.Data
|
case event.NewMessageFromPeerEngine: //event.TimestampReceived, event.RemotePeer, event.Data
|
||||||
ts, _ := time.Parse(time.RFC3339Nano, ev.Data[event.TimestampReceived])
|
ts, _ := time.Parse(time.RFC3339Nano, ev.Data[event.TimestampReceived])
|
||||||
id, err := cp.storeMessage(ev.Data[event.RemotePeer], ev.Data[event.Data], ts)
|
id, err := cp.storeMessage(ev.Data[event.RemotePeer], ev.Data[event.Data], ts)
|
||||||
|
cp.processExtensionsEvent(ev)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
// Republish as NewMessageFromPeer
|
// Republish as NewMessageFromPeer
|
||||||
ev.EventType = event.NewMessageFromPeer
|
ev.EventType = event.NewMessageFromPeer
|
||||||
|
@ -1482,8 +1647,7 @@ func (cp *cwtchPeer) eventHandler() {
|
||||||
conversationInfo, err := cp.FetchConversationInfo(onion)
|
conversationInfo, err := cp.FetchConversationInfo(onion)
|
||||||
|
|
||||||
log.Debugf("confo info lookup newgetval %v %v %v", onion, conversationInfo, err)
|
log.Debugf("confo info lookup newgetval %v %v %v", onion, conversationInfo, err)
|
||||||
// only accepted contacts can look up information
|
if conversationInfo != nil && conversationInfo.GetPeerAC().ExchangeAttributes {
|
||||||
if conversationInfo != nil && conversationInfo.Accepted {
|
|
||||||
// Type Safe Scoped/Zoned Path
|
// Type Safe Scoped/Zoned Path
|
||||||
zscope := attr.IntoScope(scope)
|
zscope := attr.IntoScope(scope)
|
||||||
zone, zpath := attr.ParseZone(zpath)
|
zone, zpath := attr.ParseZone(zpath)
|
||||||
|
@ -1515,7 +1679,7 @@ func (cp *cwtchPeer) eventHandler() {
|
||||||
|
|
||||||
conversationInfo, _ := cp.FetchConversationInfo(handle)
|
conversationInfo, _ := cp.FetchConversationInfo(handle)
|
||||||
// only accepted contacts can look up information
|
// only accepted contacts can look up information
|
||||||
if conversationInfo != nil && conversationInfo.Accepted {
|
if conversationInfo != nil && conversationInfo.GetPeerAC().ExchangeAttributes {
|
||||||
// Type Safe Scoped/Zoned Path
|
// Type Safe Scoped/Zoned Path
|
||||||
zscope := attr.IntoScope(scope)
|
zscope := attr.IntoScope(scope)
|
||||||
zone, zpath := attr.ParseZone(zpath)
|
zone, zpath := attr.ParseZone(zpath)
|
||||||
|
@ -1536,6 +1700,13 @@ func (cp *cwtchPeer) eventHandler() {
|
||||||
}
|
}
|
||||||
case event.PeerStateChange:
|
case event.PeerStateChange:
|
||||||
handle := ev.Data[event.RemotePeer]
|
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 {
|
if connections.ConnectionStateToType()[ev.Data[event.ConnectionState]] == connections.AUTHENTICATED {
|
||||||
ci, err := cp.FetchConversationInfo(handle)
|
ci, err := cp.FetchConversationInfo(handle)
|
||||||
var cid int
|
var cid int
|
||||||
|
@ -1548,6 +1719,7 @@ func (cp *cwtchPeer) eventHandler() {
|
||||||
|
|
||||||
timestamp := time.Now().Format(time.RFC3339Nano)
|
timestamp := time.Now().Format(time.RFC3339Nano)
|
||||||
cp.SetConversationAttribute(cid, attr.LocalScope.ConstructScopedZonedPath(attr.ProfileZone.ConstructZonedPath(constants.AttrLastConnectionTime)), timestamp)
|
cp.SetConversationAttribute(cid, attr.LocalScope.ConstructScopedZonedPath(attr.ProfileZone.ConstructZonedPath(constants.AttrLastConnectionTime)), timestamp)
|
||||||
|
|
||||||
} else if connections.ConnectionStateToType()[ev.Data[event.ConnectionState]] == connections.DISCONNECTED {
|
} else if connections.ConnectionStateToType()[ev.Data[event.ConnectionState]] == connections.DISCONNECTED {
|
||||||
ci, err := cp.FetchConversationInfo(handle)
|
ci, err := cp.FetchConversationInfo(handle)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
@ -1562,23 +1734,8 @@ func (cp *cwtchPeer) eventHandler() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Safe Access to Extensions
|
// Safe Access to Extensions
|
||||||
cp.extensionLock.Lock()
|
cp.processExtensionsEvent(ev)
|
||||||
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:
|
case event.ServerStateChange:
|
||||||
cp.mutex.Lock()
|
cp.mutex.Lock()
|
||||||
prevState := cp.state[ev.Data[event.GroupServer]]
|
prevState := cp.state[ev.Data[event.GroupServer]]
|
||||||
|
@ -1716,6 +1873,21 @@ func (cp *cwtchPeer) attemptInsertOrAcknowledgeLegacyGroupConversation(conversat
|
||||||
return err
|
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
|
// 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
|
// 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
|
// to either find the contact or the associated message
|
||||||
|
@ -1723,21 +1895,19 @@ func (cp *cwtchPeer) attemptAcknowledgeP2PConversation(handle string, signature
|
||||||
ci, err := cp.FetchConversationInfo(handle)
|
ci, err := cp.FetchConversationInfo(handle)
|
||||||
// We should *never* received a peer acknowledgement for a conversation that doesn't exist...
|
// We should *never* received a peer acknowledgement for a conversation that doesn't exist...
|
||||||
if ci != nil && err == nil {
|
if ci != nil && err == nil {
|
||||||
// for p2p messages the randomly generated event ID is the "signature"
|
chid, mid, err := cp.findChannelMessageBySignature(ci, signature)
|
||||||
id, err := cp.GetChannelMessageBySignature(ci.ID, 0, signature)
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
_, attributes, err := cp.GetChannelMessage(ci.ID, 0, id)
|
_, attributes, err := cp.GetChannelMessage(ci.ID, chid, mid)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
cp.mutex.Lock()
|
cp.mutex.Lock()
|
||||||
attributes[constants.AttrAck] = constants.True
|
attributes[constants.AttrAck] = constants.True
|
||||||
cp.mutex.Unlock()
|
cp.mutex.Unlock()
|
||||||
cp.storage.UpdateMessageAttributes(ci.ID, 0, id, attributes)
|
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: handle, event.Index: strconv.Itoa(id)}))
|
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)}))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
return err
|
return fmt.Errorf("no such signature error: %x", signature)
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -1751,16 +1921,16 @@ func (cp *cwtchPeer) attemptErrorConversationMessage(handle string, signature st
|
||||||
// We should *never* received an error for a conversation that doesn't exist...
|
// We should *never* received an error for a conversation that doesn't exist...
|
||||||
if ci != nil && err == nil {
|
if ci != nil && err == nil {
|
||||||
// "signature" here is event ID for peer messages...
|
// "signature" here is event ID for peer messages...
|
||||||
id, err := cp.GetChannelMessageBySignature(ci.ID, 0, signature)
|
chid, mid, err := cp.findChannelMessageBySignature(ci, signature)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
_, attributes, err := cp.GetChannelMessage(ci.ID, 0, id)
|
_, attributes, err := cp.GetChannelMessage(ci.ID, chid, mid)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
cp.mutex.Lock()
|
cp.mutex.Lock()
|
||||||
attributes[constants.AttrErr] = constants.True
|
attributes[constants.AttrErr] = constants.True
|
||||||
cp.storage.UpdateMessageAttributes(ci.ID, 0, id, attributes)
|
cp.storage.UpdateMessageAttributes(ci.ID, chid, mid, attributes)
|
||||||
cp.mutex.Unlock()
|
cp.mutex.Unlock()
|
||||||
// Send a generic indexed failure...
|
// 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.Index: strconv.Itoa(id)}))
|
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)}))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
|
@ -1799,3 +1969,45 @@ func (cp *cwtchPeer) constructGroupFromConversation(conversationInfo *model.Conv
|
||||||
}
|
}
|
||||||
return &group, nil
|
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()
|
||||||
|
}
|
||||||
|
|
|
@ -13,6 +13,7 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
)
|
)
|
||||||
|
@ -86,7 +87,7 @@ const setConversationACLSQLStmt = `update conversations set ACL=(?) where ID=(?)
|
||||||
const deleteConversationSQLStmt = `delete from conversations where ID=(?);`
|
const deleteConversationSQLStmt = `delete from conversations where ID=(?);`
|
||||||
|
|
||||||
// createTableConversationMessagesSQLStmt is a template for creating conversation based tables...
|
// createTableConversationMessagesSQLStmt is a template for creating conversation based tables...
|
||||||
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);`
|
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);`
|
||||||
|
|
||||||
// insertMessageIntoConversationSQLStmt is a template for creating conversation based tables...
|
// insertMessageIntoConversationSQLStmt is a template for creating conversation based tables...
|
||||||
const insertMessageIntoConversationSQLStmt = `insert into channel_%d_%d_chat (Body, Attributes, Signature, ContentHash) values(?,?,?,?);`
|
const insertMessageIntoConversationSQLStmt = `insert into channel_%d_%d_chat (Body, Attributes, Signature, ContentHash) values(?,?,?,?);`
|
||||||
|
@ -323,7 +324,7 @@ func (cps *CwtchProfileStorage) NewConversation(handle string, attributes model.
|
||||||
return -1, tx.Rollback()
|
return -1, tx.Rollback()
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err = tx.Exec(fmt.Sprintf(createTableConversationMessagesSQLStmt, id))
|
result, err = tx.Exec(fmt.Sprintf(createTableConversationMessagesSQLStmt, id, 0))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("error executing transaction: %v", err)
|
log.Errorf("error executing transaction: %v", err)
|
||||||
return -1, tx.Rollback()
|
return -1, tx.Rollback()
|
||||||
|
@ -344,6 +345,27 @@ func (cps *CwtchProfileStorage) NewConversation(handle string, attributes model.
|
||||||
return int(conversationID), nil
|
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
|
// 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.
|
// 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
|
// Ideally this function should not exist, and all lookups should happen by ID (this is currently
|
||||||
|
@ -375,7 +397,12 @@ func (cps *CwtchProfileStorage) GetConversationByHandle(handle string) (*model.C
|
||||||
}
|
}
|
||||||
rows.Close()
|
rows.Close()
|
||||||
|
|
||||||
return &model.Conversation{ID: id, Handle: handle, ACL: model.DeserializeAccessControlList(acl), Attributes: model.DeserializeAttributes(attributes), Accepted: accepted}, nil
|
cacl, err := model.DeserializeAccessControlList(acl)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("error deserializing ACL from database, database maybe corrupted: %v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &model.Conversation{ID: id, Handle: handle, ACL: cacl, Attributes: model.DeserializeAttributes(attributes), Accepted: accepted}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// FetchConversations returns *all* active conversations. This method should only be called
|
// FetchConversations returns *all* active conversations. This method should only be called
|
||||||
|
@ -411,7 +438,13 @@ func (cps *CwtchProfileStorage) FetchConversations() ([]*model.Conversation, err
|
||||||
rows.Close()
|
rows.Close()
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
conversations = append(conversations, &model.Conversation{ID: id, Handle: handle, ACL: model.DeserializeAccessControlList(acl), Attributes: model.DeserializeAttributes(attributes), Accepted: accepted})
|
|
||||||
|
cacl, err := model.DeserializeAccessControlList(acl)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("error deserializing ACL from database, database maybe corrupted: %v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
conversations = append(conversations, &model.Conversation{ID: id, Handle: handle, ACL: cacl, Attributes: model.DeserializeAttributes(attributes), Accepted: accepted})
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -444,7 +477,12 @@ func (cps *CwtchProfileStorage) GetConversation(id int) (*model.Conversation, er
|
||||||
}
|
}
|
||||||
rows.Close()
|
rows.Close()
|
||||||
|
|
||||||
return &model.Conversation{ID: id, Handle: handle, ACL: model.DeserializeAccessControlList(acl), Attributes: model.DeserializeAttributes(attributes), Accepted: accepted}, nil
|
cacl, err := model.DeserializeAccessControlList(acl)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("error deserializing ACL from database, database maybe corrupted: %v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &model.Conversation{ID: id, Handle: handle, ACL: cacl, Attributes: model.DeserializeAttributes(attributes), Accepted: accepted}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// AcceptConversation sets the accepted status of a conversation to true in the backing datastore
|
// AcceptConversation sets the accepted status of a conversation to true in the backing datastore
|
||||||
|
@ -780,7 +818,7 @@ func (cps *CwtchProfileStorage) SearchMessages(conversation int, channel int, pa
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetMostRecentMessages returns the most recent messages in a channel up to a given limit at a given offset
|
// 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 int) ([]model.ConversationMessage, error) {
|
func (cps *CwtchProfileStorage) GetMostRecentMessages(conversation int, channel int, offset int, limit uint) ([]model.ConversationMessage, error) {
|
||||||
channelID := ChannelID{Conversation: conversation, Channel: channel}
|
channelID := ChannelID{Conversation: conversation, Channel: channel}
|
||||||
|
|
||||||
cps.mutex.Lock()
|
cps.mutex.Lock()
|
||||||
|
@ -835,12 +873,30 @@ func (cps *CwtchProfileStorage) PurgeConversationChannel(conversation int, chann
|
||||||
|
|
||||||
// PurgeNonSavedMessages deletes all message conversations that are not explicitly set to saved.
|
// PurgeNonSavedMessages deletes all message conversations that are not explicitly set to saved.
|
||||||
func (cps *CwtchProfileStorage) PurgeNonSavedMessages() {
|
func (cps *CwtchProfileStorage) PurgeNonSavedMessages() {
|
||||||
// Purge Messages that are not stored...
|
|
||||||
|
// 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...
|
||||||
ci, err := cps.FetchConversations()
|
ci, err := cps.FetchConversations()
|
||||||
if err == nil {
|
if err == nil {
|
||||||
for _, conversation := range ci {
|
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() {
|
if !conversation.IsGroup() && !conversation.IsServer() {
|
||||||
if conversation.Attributes[attr.LocalScope.ConstructScopedZonedPath(attr.ProfileZone.ConstructZonedPath(event.SaveHistoryKey)).ToString()] != event.SaveHistoryConfirmed {
|
// 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) {
|
||||||
log.Debugf("purging conversation...")
|
log.Debugf("purging conversation...")
|
||||||
// TODO: At some point in the future this needs to iterate over channels and make a decision for each on..
|
// 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)
|
cps.PurgeConversationChannel(conversation.ID, 0)
|
||||||
|
|
|
@ -20,7 +20,9 @@ type ModifyPeeringState interface {
|
||||||
BlockUnknownConnections()
|
BlockUnknownConnections()
|
||||||
AllowUnknownConnections()
|
AllowUnknownConnections()
|
||||||
PeerWithOnion(string)
|
PeerWithOnion(string)
|
||||||
JoinServer(string) error
|
QueueJoinServer(string)
|
||||||
|
DisconnectFromPeer(string)
|
||||||
|
DisconnectFromServer(string)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ModifyContactsAndPeers is a meta-interface intended to restrict a call to reading and modifying contacts
|
// ModifyContactsAndPeers is a meta-interface intended to restrict a call to reading and modifying contacts
|
||||||
|
@ -48,6 +50,8 @@ type ModifyServers interface {
|
||||||
|
|
||||||
// SendMessages enables a caller to sender messages to a contact
|
// SendMessages enables a caller to sender messages to a contact
|
||||||
type SendMessages interface {
|
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)
|
SendMessage(conversation int, message string) (int, error)
|
||||||
|
|
||||||
// EnhancedSendMessage Attempts to Send a Message and Immediately Attempts to Lookup the Message in the Database
|
// EnhancedSendMessage Attempts to Send a Message and Immediately Attempts to Lookup the Message in the Database
|
||||||
|
@ -113,23 +117,38 @@ type CwtchPeer interface {
|
||||||
EnhancedImportBundle(string) string
|
EnhancedImportBundle(string) string
|
||||||
|
|
||||||
// New Unified Conversation Interfaces
|
// 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)
|
NewContactConversation(handle string, acl model.AccessControl, accepted bool) (int, error)
|
||||||
FetchConversations() ([]*model.Conversation, error)
|
FetchConversations() ([]*model.Conversation, error)
|
||||||
ArchiveConversation(conversation int)
|
ArchiveConversation(conversation int)
|
||||||
GetConversationInfo(conversation int) (*model.Conversation, error)
|
GetConversationInfo(conversation int) (*model.Conversation, error)
|
||||||
FetchConversationInfo(handle string) (*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
|
AcceptConversation(conversation int) error
|
||||||
BlockConversation(conversation int) error
|
BlockConversation(conversation int) error
|
||||||
UnblockConversation(conversation int) error
|
UnblockConversation(conversation int) error
|
||||||
|
|
||||||
SetConversationAttribute(conversation int, path attr.ScopedZonedPath, value string) error
|
SetConversationAttribute(conversation int, path attr.ScopedZonedPath, value string) error
|
||||||
GetConversationAttribute(conversation int, path attr.ScopedZonedPath) (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
|
DeleteConversation(conversation int) error
|
||||||
|
|
||||||
// New Unified Conversation Channel Interfaces
|
// New Unified Conversation Channel Interfaces
|
||||||
GetChannelMessage(conversation int, channel int, id int) (string, model.Attributes, error)
|
GetChannelMessage(conversation int, channel int, id int) (string, model.Attributes, error)
|
||||||
GetChannelMessageCount(conversation int, channel int) (int, error)
|
GetChannelMessageCount(conversation int, channel int) (int, error)
|
||||||
GetChannelMessageByContentHash(conversation int, channel int, contenthash string) (int, error)
|
GetChannelMessageByContentHash(conversation int, channel int, contenthash string) (int, error)
|
||||||
GetMostRecentMessages(conversation int, channel int, offset int, limit int) ([]model.ConversationMessage, error)
|
GetChannelMessageBySignature(conversationID int, channelID int, signature string) (int, error)
|
||||||
|
GetMostRecentMessages(conversation int, channel int, offset int, limit uint) ([]model.ConversationMessage, error)
|
||||||
UpdateMessageAttribute(conversation int, channel int, id int, key string, value string) error
|
UpdateMessageAttribute(conversation int, channel int, id int, key string, value string) error
|
||||||
SearchConversations(pattern string) string
|
SearchConversations(pattern string) string
|
||||||
|
|
||||||
|
@ -140,7 +159,7 @@ type CwtchPeer interface {
|
||||||
EnhancedGetMessageByContentHash(conversation int, hash string) string
|
EnhancedGetMessageByContentHash(conversation int, hash string) string
|
||||||
|
|
||||||
// EnhancedGetMessages returns a set of json-encoded enhanced messages, suitable for rendering in a UI
|
// EnhancedGetMessages returns a set of json-encoded enhanced messages, suitable for rendering in a UI
|
||||||
EnhancedGetMessages(conversation int, index int, count int) string
|
EnhancedGetMessages(conversation int, index int, count uint) string
|
||||||
|
|
||||||
// Server Token APIS
|
// Server Token APIS
|
||||||
// TODO move these to feature protected interfaces
|
// TODO move these to feature protected interfaces
|
||||||
|
@ -156,6 +175,10 @@ type CwtchPeer interface {
|
||||||
UpdateExperiments(enabled bool, experiments map[string]bool)
|
UpdateExperiments(enabled bool, experiments map[string]bool)
|
||||||
NotifySettingsUpdate(settings settings.GlobalSettings)
|
NotifySettingsUpdate(settings settings.GlobalSettings)
|
||||||
IsFeatureEnabled(featureName string) bool
|
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.
|
// EnhancedMessage wraps a Cwtch model.Message with some additional data to reduce calls from the UI.
|
||||||
|
|
|
@ -122,6 +122,8 @@ func NewProtocolEngine(identity primitives.Identity, privateKey ed25519.PrivateK
|
||||||
engine.eventManager.Subscribe(event.UpdateConversationAuthorization, engine.queue)
|
engine.eventManager.Subscribe(event.UpdateConversationAuthorization, engine.queue)
|
||||||
engine.eventManager.Subscribe(event.BlockUnknownPeers, engine.queue)
|
engine.eventManager.Subscribe(event.BlockUnknownPeers, engine.queue)
|
||||||
engine.eventManager.Subscribe(event.AllowUnknownPeers, 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
|
// File Handling
|
||||||
engine.eventManager.Subscribe(event.ShareManifest, engine.queue)
|
engine.eventManager.Subscribe(event.ShareManifest, engine.queue)
|
||||||
|
@ -194,6 +196,10 @@ 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.
|
// 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.authorizations.Delete(ev.Data[event.RemotePeer])
|
||||||
e.deleteConnection(onion)
|
e.deleteConnection(onion)
|
||||||
|
case event.DisconnectPeerRequest:
|
||||||
|
e.deleteConnection(ev.Data[event.RemotePeer])
|
||||||
|
case event.DisconnectServerRequest:
|
||||||
|
e.leaveServer(ev.Data[event.GroupServer])
|
||||||
case event.SendMessageToGroup:
|
case event.SendMessageToGroup:
|
||||||
ciphertext, _ := base64.StdEncoding.DecodeString(ev.Data[event.Ciphertext])
|
ciphertext, _ := base64.StdEncoding.DecodeString(ev.Data[event.Ciphertext])
|
||||||
signature, _ := base64.StdEncoding.DecodeString(ev.Data[event.Signature])
|
signature, _ := base64.StdEncoding.DecodeString(ev.Data[event.Signature])
|
||||||
|
@ -464,6 +470,10 @@ func (e *engine) peerWithTokenServer(onion string, tokenServerOnion string, toke
|
||||||
e.ignoreOnShutdown(e.serverAuthed)(onion)
|
e.ignoreOnShutdown(e.serverAuthed)(onion)
|
||||||
return
|
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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -739,6 +749,16 @@ func (e *engine) handlePeerMessage(hostname string, eventID string, context stri
|
||||||
// Fall through handler for the default text conversation.
|
// 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)}))
|
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
|
// 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
|
// 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 {
|
if err := e.sendPeerMessage(hostname, pmodel.PeerMessage{ID: eventID, Context: event.ContextAck, Data: []byte{}}); err != nil {
|
||||||
|
|
|
@ -2,12 +2,14 @@ package connections
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"cwtch.im/cwtch/event"
|
"cwtch.im/cwtch/event"
|
||||||
|
"cwtch.im/cwtch/model"
|
||||||
model2 "cwtch.im/cwtch/protocol/model"
|
model2 "cwtch.im/cwtch/protocol/model"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"git.openprivacy.ca/cwtch.im/tapir"
|
"git.openprivacy.ca/cwtch.im/tapir"
|
||||||
"git.openprivacy.ca/cwtch.im/tapir/applications"
|
"git.openprivacy.ca/cwtch.im/tapir/applications"
|
||||||
"git.openprivacy.ca/openprivacy/log"
|
"git.openprivacy.ca/openprivacy/log"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
const cwtchCapability = tapir.Capability("cwtchCapability")
|
const cwtchCapability = tapir.Capability("cwtchCapability")
|
||||||
|
@ -133,6 +135,14 @@ func (pa *PeerApp) listen() {
|
||||||
pa.version.Store(Version2)
|
pa.version.Store(Version2)
|
||||||
}
|
}
|
||||||
} else {
|
} 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)
|
pa.MessageHandler(pa.connection.Hostname(), packet.ID, packet.Context, packet.Data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -148,6 +158,15 @@ func (pa *PeerApp) SendMessage(message model2.PeerMessage) error {
|
||||||
var serialized []byte
|
var serialized []byte
|
||||||
var err error
|
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 {
|
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)
|
// 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()
|
serialized = message.Serialize()
|
||||||
|
|
|
@ -13,7 +13,7 @@ type ChunkSpec []uint64
|
||||||
// CreateChunkSpec given a full list of chunks with their downloaded status (true for downloaded, false otherwise)
|
// 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
|
// derives a list of identifiers of chunks that have not been downloaded yet
|
||||||
func CreateChunkSpec(progress []bool) ChunkSpec {
|
func CreateChunkSpec(progress []bool) ChunkSpec {
|
||||||
var chunks ChunkSpec
|
chunks := ChunkSpec{}
|
||||||
for i, p := range progress {
|
for i, p := range progress {
|
||||||
if !p {
|
if !p {
|
||||||
chunks = append(chunks, uint64(i))
|
chunks = append(chunks, uint64(i))
|
||||||
|
|
|
@ -93,7 +93,12 @@ func TestManifestLarge(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prepare Download
|
// Prepare Download
|
||||||
cwtchPngOutManifest, _ := LoadManifest("testdata/cwtch.png.manifest")
|
cwtchPngOutManifest, err := LoadManifest("testdata/cwtch.png.manifest")
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("could not prepare download %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
cwtchPngOutManifest.FileName = "testdata/cwtch.out.png"
|
cwtchPngOutManifest.FileName = "testdata/cwtch.out.png"
|
||||||
|
|
||||||
defer cwtchPngOutManifest.Close()
|
defer cwtchPngOutManifest.Close()
|
||||||
|
|
|
@ -35,6 +35,7 @@ type GlobalSettings struct {
|
||||||
Locale string
|
Locale string
|
||||||
Theme string
|
Theme string
|
||||||
ThemeMode string
|
ThemeMode string
|
||||||
|
ThemeImages bool
|
||||||
PreviousPid int64
|
PreviousPid int64
|
||||||
ExperimentsEnabled bool
|
ExperimentsEnabled bool
|
||||||
Experiments map[string]bool
|
Experiments map[string]bool
|
||||||
|
@ -57,11 +58,14 @@ type GlobalSettings struct {
|
||||||
TorCacheDir string
|
TorCacheDir string
|
||||||
BlodeuweddPath string
|
BlodeuweddPath string
|
||||||
FontScaling float64
|
FontScaling float64
|
||||||
|
DefaultSaveHistory bool
|
||||||
}
|
}
|
||||||
|
|
||||||
var DefaultGlobalSettings = GlobalSettings{
|
var DefaultGlobalSettings = GlobalSettings{
|
||||||
Locale: "en",
|
Locale: "en",
|
||||||
Theme: "dark",
|
Theme: "cwtch",
|
||||||
|
ThemeMode: "dark",
|
||||||
|
ThemeImages: false,
|
||||||
PreviousPid: -1,
|
PreviousPid: -1,
|
||||||
ExperimentsEnabled: false,
|
ExperimentsEnabled: false,
|
||||||
Experiments: map[string]bool{constants.MessageFormattingExperiment: true},
|
Experiments: map[string]bool{constants.MessageFormattingExperiment: true},
|
||||||
|
@ -83,6 +87,7 @@ var DefaultGlobalSettings = GlobalSettings{
|
||||||
TorCacheDir: "",
|
TorCacheDir: "",
|
||||||
BlodeuweddPath: "",
|
BlodeuweddPath: "",
|
||||||
FontScaling: 1.0, // use the system pixel scaling default
|
FontScaling: 1.0, // use the system pixel scaling default
|
||||||
|
DefaultSaveHistory: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
func InitGlobalSettingsFile(directory string, password string) (*GlobalSettingsFile, error) {
|
func InitGlobalSettingsFile(directory string, password string) (*GlobalSettingsFile, error) {
|
||||||
|
@ -131,6 +136,8 @@ func (globalSettingsFile *GlobalSettingsFile) ReadGlobalSettings() GlobalSetting
|
||||||
return settings //firstTime = true
|
return settings //firstTime = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// note: by giving json.Unmarshal settings we are providing it defacto defaults
|
||||||
|
// from DefaultGlobalSettings
|
||||||
err = json.Unmarshal(settingsBytes, &settings)
|
err = json.Unmarshal(settingsBytes, &settings)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("Could not parse global ui settings: %v\n", err)
|
log.Errorf("Could not parse global ui settings: %v\n", err)
|
||||||
|
|
|
@ -67,7 +67,9 @@ func (ps *ProfileStoreV1) load() error {
|
||||||
|
|
||||||
if contact.Attributes[event.SaveHistoryKey] == event.SaveHistoryConfirmed {
|
if contact.Attributes[event.SaveHistoryKey] == event.SaveHistoryConfirmed {
|
||||||
ss := NewStreamStore(ps.directory, contact.LocalID, ps.key)
|
ss := NewStreamStore(ps.directory, contact.LocalID, ps.key)
|
||||||
cp.Contacts[contact.Onion].Timeline.SetMessages(ss.Read())
|
if contact, exists := cp.Contacts[contact.Onion]; exists {
|
||||||
|
contact.Timeline.SetMessages(ss.Read())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -78,8 +80,10 @@ func (ps *ProfileStoreV1) load() error {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
ss := NewStreamStore(ps.directory, group.LocalID, ps.key)
|
ss := NewStreamStore(ps.directory, group.LocalID, ps.key)
|
||||||
cp.Groups[gid].Timeline.SetMessages(ss.Read())
|
if group, exists := cp.Groups[gid]; exists {
|
||||||
cp.Groups[gid].Timeline.Sort()
|
group.Timeline.SetMessages(ss.Read())
|
||||||
|
group.Timeline.Sort()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -64,7 +64,6 @@ func TestFileSharing(t *testing.T) {
|
||||||
os.MkdirAll(dataDir, 0700)
|
os.MkdirAll(dataDir, 0700)
|
||||||
|
|
||||||
// we don't need real randomness for the port, just to avoid a possible conflict...
|
// 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
|
socksPort := mrand.Intn(1000) + 9051
|
||||||
controlPort := mrand.Intn(1000) + 9052
|
controlPort := mrand.Intn(1000) + 9052
|
||||||
|
|
||||||
|
@ -99,7 +98,10 @@ func TestFileSharing(t *testing.T) {
|
||||||
|
|
||||||
app := app2.NewApp(acn, "./storage", app2.LoadAppSettings("./storage"))
|
app := app2.NewApp(acn, "./storage", app2.LoadAppSettings("./storage"))
|
||||||
|
|
||||||
usr, _ := user.Current()
|
usr, err := user.Current()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("current user is undefined")
|
||||||
|
}
|
||||||
cwtchDir := path.Join(usr.HomeDir, ".cwtch")
|
cwtchDir := path.Join(usr.HomeDir, ".cwtch")
|
||||||
os.Mkdir(cwtchDir, 0700)
|
os.Mkdir(cwtchDir, 0700)
|
||||||
os.RemoveAll(path.Join(cwtchDir, "testing"))
|
os.RemoveAll(path.Join(cwtchDir, "testing"))
|
||||||
|
@ -114,8 +116,10 @@ func TestFileSharing(t *testing.T) {
|
||||||
t.Logf("** Waiting for Alice, Bob...")
|
t.Logf("** Waiting for Alice, Bob...")
|
||||||
alice := app2.WaitGetPeer(app, "alice")
|
alice := app2.WaitGetPeer(app, "alice")
|
||||||
app.ActivatePeerEngine(alice.GetOnion())
|
app.ActivatePeerEngine(alice.GetOnion())
|
||||||
|
app.ConfigureConnections(alice.GetOnion(), true, true, true)
|
||||||
bob := app2.WaitGetPeer(app, "bob")
|
bob := app2.WaitGetPeer(app, "bob")
|
||||||
app.ActivatePeerEngine(bob.GetOnion())
|
app.ActivatePeerEngine(bob.GetOnion())
|
||||||
|
app.ConfigureConnections(bob.GetOnion(), true, true, true)
|
||||||
|
|
||||||
alice.AutoHandleEvents([]event.Type{event.PeerStateChange, event.NewRetValMessageFromPeer})
|
alice.AutoHandleEvents([]event.Type{event.PeerStateChange, event.NewRetValMessageFromPeer})
|
||||||
bob.AutoHandleEvents([]event.Type{event.PeerStateChange, event.NewRetValMessageFromPeer})
|
bob.AutoHandleEvents([]event.Type{event.PeerStateChange, event.NewRetValMessageFromPeer})
|
||||||
|
@ -141,10 +145,23 @@ func TestFileSharing(t *testing.T) {
|
||||||
alice.NewContactConversation(bob.GetOnion(), model.DefaultP2PAccessControl(), true)
|
alice.NewContactConversation(bob.GetOnion(), model.DefaultP2PAccessControl(), true)
|
||||||
alice.PeerWithOnion(bob.GetOnion())
|
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...")
|
t.Logf("Waiting for alice and Bob to peer...")
|
||||||
waitForPeerPeerConnection(t, alice, bob)
|
waitForPeerPeerConnection(t, alice, bob)
|
||||||
alice.AcceptConversation(1)
|
err = alice.AcceptConversation(1)
|
||||||
bob.AcceptConversation(1)
|
if err != nil {
|
||||||
|
t.Fatalf("Error!: %v", err)
|
||||||
|
}
|
||||||
|
err = bob.AcceptConversation(1)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error!: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
t.Logf("Alice and Bob are Connected!!")
|
t.Logf("Alice and Bob are Connected!!")
|
||||||
|
|
||||||
filesharingFunctionality := filesharing.FunctionalityGate()
|
filesharingFunctionality := filesharing.FunctionalityGate()
|
||||||
|
@ -161,11 +178,11 @@ func TestFileSharing(t *testing.T) {
|
||||||
// testBobDownloadFile(t, bob, filesharingFunctionality, queueOracle)
|
// testBobDownloadFile(t, bob, filesharingFunctionality, queueOracle)
|
||||||
|
|
||||||
// Wait for say...
|
// Wait for say...
|
||||||
time.Sleep(10 * time.Second)
|
time.Sleep(30 * time.Second)
|
||||||
|
|
||||||
if _, err := os.Stat(path.Join(settings.DownloadPath, "cwtch.png")); errors.Is(err, os.ErrNotExist) {
|
if _, err := os.Stat(path.Join(settings.DownloadPath, "cwtch.png")); errors.Is(err, os.ErrNotExist) {
|
||||||
// path/to/whatever does not exist
|
// path/to/whatever does not exist
|
||||||
t.Fatalf("cwthc.png should have been automatically downloadeded...")
|
t.Fatalf("cwtch.png should have been automatically downloaded...")
|
||||||
}
|
}
|
||||||
|
|
||||||
app.Shutdown()
|
app.Shutdown()
|
||||||
|
|
|
@ -34,26 +34,6 @@ var (
|
||||||
carolLines = []string{"Howdy, thanks!"}
|
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) {
|
func waitForRetVal(peer peer.CwtchPeer, convId int, szp attr.ScopedZonedPath) {
|
||||||
for {
|
for {
|
||||||
_, err := peer.GetConversationAttribute(convId, szp)
|
_, err := peer.GetConversationAttribute(convId, szp)
|
||||||
|
@ -99,7 +79,6 @@ func TestCwtchPeerIntegration(t *testing.T) {
|
||||||
os.MkdirAll(dataDir, 0700)
|
os.MkdirAll(dataDir, 0700)
|
||||||
|
|
||||||
// we don't need real randomness for the port, just to avoid a possible conflict...
|
// 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
|
socksPort := mrand.Intn(1000) + 9051
|
||||||
controlPort := mrand.Intn(1000) + 9052
|
controlPort := mrand.Intn(1000) + 9052
|
||||||
|
|
||||||
|
@ -150,6 +129,11 @@ func TestCwtchPeerIntegration(t *testing.T) {
|
||||||
numGoRoutinesPostAppStart := runtime.NumGoroutine()
|
numGoRoutinesPostAppStart := runtime.NumGoroutine()
|
||||||
|
|
||||||
// ***** cwtchPeer setup *****
|
// ***** cwtchPeer setup *****
|
||||||
|
// Turn on Groups Experiment...
|
||||||
|
settings := app.ReadSettings()
|
||||||
|
settings.ExperimentsEnabled = true
|
||||||
|
settings.Experiments[constants.GroupsExperiment] = true
|
||||||
|
app.UpdateSettings(settings)
|
||||||
|
|
||||||
log.Infoln("Creating Alice...")
|
log.Infoln("Creating Alice...")
|
||||||
app.CreateProfile("Alice", "asdfasdf", true)
|
app.CreateProfile("Alice", "asdfasdf", true)
|
||||||
|
@ -163,6 +147,7 @@ func TestCwtchPeerIntegration(t *testing.T) {
|
||||||
alice := app2.WaitGetPeer(app, "Alice")
|
alice := app2.WaitGetPeer(app, "Alice")
|
||||||
aliceBus := app.GetEventBus(alice.GetOnion())
|
aliceBus := app.GetEventBus(alice.GetOnion())
|
||||||
app.ActivatePeerEngine(alice.GetOnion())
|
app.ActivatePeerEngine(alice.GetOnion())
|
||||||
|
app.ConfigureConnections(alice.GetOnion(), true, true, true)
|
||||||
log.Infoln("Alice created:", alice.GetOnion())
|
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.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})
|
alice.AutoHandleEvents([]event.Type{event.PeerStateChange, event.ServerStateChange, event.NewGroupInvite, event.NewRetValMessageFromPeer})
|
||||||
|
@ -170,6 +155,7 @@ func TestCwtchPeerIntegration(t *testing.T) {
|
||||||
bob := app2.WaitGetPeer(app, "Bob")
|
bob := app2.WaitGetPeer(app, "Bob")
|
||||||
bobBus := app.GetEventBus(bob.GetOnion())
|
bobBus := app.GetEventBus(bob.GetOnion())
|
||||||
app.ActivatePeerEngine(bob.GetOnion())
|
app.ActivatePeerEngine(bob.GetOnion())
|
||||||
|
app.ConfigureConnections(bob.GetOnion(), true, true, true)
|
||||||
log.Infoln("Bob created:", bob.GetOnion())
|
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.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})
|
bob.AutoHandleEvents([]event.Type{event.PeerStateChange, event.ServerStateChange, event.NewGroupInvite, event.NewRetValMessageFromPeer})
|
||||||
|
@ -177,6 +163,7 @@ func TestCwtchPeerIntegration(t *testing.T) {
|
||||||
carol := app2.WaitGetPeer(app, "Carol")
|
carol := app2.WaitGetPeer(app, "Carol")
|
||||||
carolBus := app.GetEventBus(carol.GetOnion())
|
carolBus := app.GetEventBus(carol.GetOnion())
|
||||||
app.ActivatePeerEngine(carol.GetOnion())
|
app.ActivatePeerEngine(carol.GetOnion())
|
||||||
|
app.ConfigureConnections(carol.GetOnion(), true, true, true)
|
||||||
log.Infoln("Carol created:", carol.GetOnion())
|
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.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})
|
carol.AutoHandleEvents([]event.Type{event.PeerStateChange, event.ServerStateChange, event.NewGroupInvite, event.NewRetValMessageFromPeer})
|
||||||
|
@ -229,10 +216,10 @@ func TestCwtchPeerIntegration(t *testing.T) {
|
||||||
t.Fatalf("Alice password did not change...")
|
t.Fatalf("Alice password did not change...")
|
||||||
}
|
}
|
||||||
|
|
||||||
waitForConnection(t, alice, bob.GetOnion(), connections.AUTHENTICATED)
|
WaitForConnection(t, alice, bob.GetOnion(), connections.AUTHENTICATED)
|
||||||
waitForConnection(t, alice, carol.GetOnion(), connections.AUTHENTICATED)
|
WaitForConnection(t, alice, carol.GetOnion(), connections.AUTHENTICATED)
|
||||||
waitForConnection(t, bob, alice.GetOnion(), connections.AUTHENTICATED)
|
WaitForConnection(t, bob, alice.GetOnion(), connections.AUTHENTICATED)
|
||||||
waitForConnection(t, carol, alice.GetOnion(), connections.AUTHENTICATED)
|
WaitForConnection(t, carol, alice.GetOnion(), connections.AUTHENTICATED)
|
||||||
|
|
||||||
log.Infof("Alice and Bob getVal public.name...")
|
log.Infof("Alice and Bob getVal public.name...")
|
||||||
|
|
||||||
|
@ -316,9 +303,9 @@ func TestCwtchPeerIntegration(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Infof("Waiting for alice to join server...")
|
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...")
|
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
|
// 1 = Alice
|
||||||
// 2 = Server
|
// 2 = Server
|
||||||
|
@ -344,7 +331,7 @@ func TestCwtchPeerIntegration(t *testing.T) {
|
||||||
if len(cachedTokens) > (usedTokens + len(carolLines)) {
|
if len(cachedTokens) > (usedTokens + len(carolLines)) {
|
||||||
carol.StoreCachedTokens(ServerAddr, cachedTokens[usedTokens: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()
|
numGoRoutinesPostCarolConnect := runtime.NumGoroutine()
|
||||||
|
|
||||||
// Check Alice Timeline
|
// Check Alice Timeline
|
||||||
|
|
|
@ -29,7 +29,6 @@ func TestEncryptedStorage(t *testing.T) {
|
||||||
os.MkdirAll(dataDir, 0700)
|
os.MkdirAll(dataDir, 0700)
|
||||||
|
|
||||||
// we don't need real randomness for the port, just to avoid a possible conflict...
|
// 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
|
socksPort := mrand.Intn(1000) + 9051
|
||||||
controlPort := mrand.Intn(1000) + 9052
|
controlPort := mrand.Intn(1000) + 9052
|
||||||
|
|
||||||
|
@ -99,6 +98,10 @@ func TestEncryptedStorage(t *testing.T) {
|
||||||
ci, err = bob.FetchConversationInfo(alice.GetOnion())
|
ci, err = bob.FetchConversationInfo(alice.GetOnion())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ci == nil {
|
||||||
|
t.Fatalf("could not fetch bobs conversation")
|
||||||
|
}
|
||||||
|
|
||||||
body, _, err := bob.GetChannelMessage(ci.ID, 0, 1)
|
body, _, err := bob.GetChannelMessage(ci.ID, 0, 1)
|
||||||
if body != "Hello Bob" || err != nil {
|
if body != "Hello Bob" || err != nil {
|
||||||
t.Fatalf("unexpected message in conversation channel %v %v", body, err)
|
t.Fatalf("unexpected message in conversation channel %v %v", body, err)
|
||||||
|
|
|
@ -58,7 +58,7 @@ func TestFileSharing(t *testing.T) {
|
||||||
os.RemoveAll("cwtch.out.png")
|
os.RemoveAll("cwtch.out.png")
|
||||||
os.RemoveAll("cwtch.out.png.manifest")
|
os.RemoveAll("cwtch.out.png.manifest")
|
||||||
|
|
||||||
log.SetLevel(log.LevelInfo)
|
log.SetLevel(log.LevelDebug)
|
||||||
log.ExcludeFromPattern("tapir")
|
log.ExcludeFromPattern("tapir")
|
||||||
|
|
||||||
os.Mkdir("tordir", 0700)
|
os.Mkdir("tordir", 0700)
|
||||||
|
@ -66,7 +66,6 @@ func TestFileSharing(t *testing.T) {
|
||||||
os.MkdirAll(dataDir, 0700)
|
os.MkdirAll(dataDir, 0700)
|
||||||
|
|
||||||
// we don't need real randomness for the port, just to avoid a possible conflict...
|
// 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
|
socksPort := mrand.Intn(1000) + 9051
|
||||||
controlPort := mrand.Intn(1000) + 9052
|
controlPort := mrand.Intn(1000) + 9052
|
||||||
|
|
||||||
|
@ -101,7 +100,10 @@ func TestFileSharing(t *testing.T) {
|
||||||
|
|
||||||
app := app2.NewApp(acn, "./storage", app2.LoadAppSettings("./storage"))
|
app := app2.NewApp(acn, "./storage", app2.LoadAppSettings("./storage"))
|
||||||
|
|
||||||
usr, _ := user.Current()
|
usr, err := user.Current()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("current user is undefined")
|
||||||
|
}
|
||||||
cwtchDir := path.Join(usr.HomeDir, ".cwtch")
|
cwtchDir := path.Join(usr.HomeDir, ".cwtch")
|
||||||
os.Mkdir(cwtchDir, 0700)
|
os.Mkdir(cwtchDir, 0700)
|
||||||
os.RemoveAll(path.Join(cwtchDir, "testing"))
|
os.RemoveAll(path.Join(cwtchDir, "testing"))
|
||||||
|
@ -116,16 +118,25 @@ func TestFileSharing(t *testing.T) {
|
||||||
t.Logf("** Waiting for Alice, Bob...")
|
t.Logf("** Waiting for Alice, Bob...")
|
||||||
alice := app2.WaitGetPeer(app, "alice")
|
alice := app2.WaitGetPeer(app, "alice")
|
||||||
app.ActivatePeerEngine(alice.GetOnion())
|
app.ActivatePeerEngine(alice.GetOnion())
|
||||||
|
app.ConfigureConnections(alice.GetOnion(), true, true, true)
|
||||||
bob := app2.WaitGetPeer(app, "bob")
|
bob := app2.WaitGetPeer(app, "bob")
|
||||||
app.ActivatePeerEngine(bob.GetOnion())
|
app.ActivatePeerEngine(bob.GetOnion())
|
||||||
|
app.ConfigureConnections(bob.GetOnion(), true, true, true)
|
||||||
alice.AutoHandleEvents([]event.Type{event.PeerStateChange, event.NewRetValMessageFromPeer})
|
alice.AutoHandleEvents([]event.Type{event.PeerStateChange, event.NewRetValMessageFromPeer})
|
||||||
bob.AutoHandleEvents([]event.Type{event.PeerStateChange, event.NewRetValMessageFromPeer})
|
bob.AutoHandleEvents([]event.Type{event.PeerStateChange, event.NewRetValMessageFromPeer})
|
||||||
|
|
||||||
aliceQueueOracle := event.NewQueue()
|
aliceQueueOracle := event.NewQueue()
|
||||||
app.GetEventBus(alice.GetOnion()).Subscribe(event.SearchResult, aliceQueueOracle)
|
aliceEb := app.GetEventBus(alice.GetOnion())
|
||||||
|
if aliceEb == nil {
|
||||||
|
t.Fatalf("alice's eventbus is undefined")
|
||||||
|
}
|
||||||
|
aliceEb.Subscribe(event.SearchResult, aliceQueueOracle)
|
||||||
queueOracle := event.NewQueue()
|
queueOracle := event.NewQueue()
|
||||||
app.GetEventBus(bob.GetOnion()).Subscribe(event.FileDownloaded, queueOracle)
|
bobEb := app.GetEventBus(bob.GetOnion())
|
||||||
|
if bobEb == nil {
|
||||||
|
t.Fatalf("bob's eventbus is undefined")
|
||||||
|
}
|
||||||
|
bobEb.Subscribe(event.FileDownloaded, queueOracle)
|
||||||
|
|
||||||
// Turn on File Sharing Experiment...
|
// Turn on File Sharing Experiment...
|
||||||
settings := app.ReadSettings()
|
settings := app.ReadSettings()
|
||||||
|
@ -140,13 +151,6 @@ func TestFileSharing(t *testing.T) {
|
||||||
|
|
||||||
bob.NewContactConversation(alice.GetOnion(), model.DefaultP2PAccessControl(), true)
|
bob.NewContactConversation(alice.GetOnion(), model.DefaultP2PAccessControl(), true)
|
||||||
alice.NewContactConversation(bob.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()
|
filesharingFunctionality := filesharing.FunctionalityGate()
|
||||||
|
|
||||||
|
@ -156,10 +160,10 @@ func TestFileSharing(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
alice.SendMessage(1, fileSharingMessage)
|
alice.SendMessage(1, fileSharingMessage)
|
||||||
bob.AcceptConversation(1)
|
|
||||||
|
|
||||||
// Wait for the messages to arrive...
|
// Ok this is fun...we just Sent a Message we may not have a connection yet...
|
||||||
time.Sleep(time.Second * 10)
|
// 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 a test message")
|
||||||
bob.SendMessage(1, "this is another test message")
|
bob.SendMessage(1, "this is another test message")
|
||||||
|
|
|
@ -0,0 +1,214 @@
|
||||||
|
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
|
||||||
|
}
|
|
@ -4,18 +4,25 @@ echo "Checking code quality (you want to see no output here)"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "Linting:"
|
echo "Running staticcheck..."
|
||||||
|
|
||||||
staticcheck ./...
|
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"
|
echo "Time to format"
|
||||||
gofmt -l -s -w .
|
gofmt -l -s -w .
|
||||||
|
|
||||||
# ineffassign (https://github.com/gordonklaus/ineffassign)
|
# ineffassign (https://github.com/gordonklaus/ineffassign)
|
||||||
echo "Checking for ineffectual assignment of errors (unchecked errors...)"
|
# echo "Checking for ineffectual assignment of errors (unchecked errors...)"
|
||||||
ineffassign ./..
|
# ineffassign .
|
||||||
|
|
||||||
# misspell (https://github.com/client9/misspell/cmd/misspell)
|
# misspell (https://github.com/client9/misspell/cmd/misspell)
|
||||||
echo "Checking for misspelled words..."
|
# echo "Checking for misspelled words..."
|
||||||
misspell . | grep -v "testing/" | grep -v "vendor/" | grep -v "go.sum" | grep -v ".idea"
|
# misspell . | grep -v "testing/" | grep -v "vendor/" | grep -v "go.sum" | grep -v ".idea"
|
||||||
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,7 +18,6 @@ import (
|
||||||
"os"
|
"os"
|
||||||
path "path/filepath"
|
path "path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var tool = flag.String("tool", "", "the tool to use")
|
var tool = flag.String("tool", "", "the tool to use")
|
||||||
|
@ -86,7 +85,6 @@ func getTokens(bundle string) {
|
||||||
os.MkdirAll(dataDir, 0700)
|
os.MkdirAll(dataDir, 0700)
|
||||||
|
|
||||||
// we don't need real randomness for the port, just to avoid a possible conflict...
|
// 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
|
socksPort := mrand.Intn(1000) + 9051
|
||||||
controlPort := mrand.Intn(1000) + 9052
|
controlPort := mrand.Intn(1000) + 9052
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
// nolint:nilaway - the context timeout here is reported as an error, even though it is a by-the-doc example
|
||||||
package utils
|
package utils
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|
Loading…
Reference in New Issue