Merge branch 'master' of git.openprivacy.ca:cwtch.im/cwtch into ipreview
continuous-integration/drone/push Build is passing
Details
continuous-integration/drone/push Build is passing
Details
This commit is contained in:
commit
9d34b7ef57
|
@ -25,3 +25,6 @@ testing/tordir/
|
|||
tokens-bak.db
|
||||
tokens.db
|
||||
tokens1.db
|
||||
arch/
|
||||
testing/encryptedstorage/encrypted_storage_profiles
|
||||
testing/encryptedstorage/tordir
|
145
app/app.go
145
app/app.go
|
@ -10,7 +10,6 @@ import (
|
|||
"cwtch.im/cwtch/protocol/connections"
|
||||
"cwtch.im/cwtch/storage"
|
||||
"fmt"
|
||||
"git.openprivacy.ca/cwtch.im/tapir/primitives"
|
||||
"git.openprivacy.ca/openprivacy/connectivity"
|
||||
"git.openprivacy.ca/openprivacy/log"
|
||||
"io/ioutil"
|
||||
|
@ -32,7 +31,6 @@ type application struct {
|
|||
appletPeers
|
||||
appletACN
|
||||
appletPlugins
|
||||
storage map[string]storage.ProfileStore
|
||||
engines map[string]connections.Engine
|
||||
appBus event.Manager
|
||||
appmutex sync.Mutex
|
||||
|
@ -41,7 +39,6 @@ type application struct {
|
|||
// Application is a full cwtch peer application. It allows management, usage and storage of multiple peers
|
||||
type Application interface {
|
||||
LoadProfiles(password string)
|
||||
CreatePeer(name string, password string)
|
||||
CreateTaggedPeer(name string, password string, tag string)
|
||||
DeletePeer(onion string, currentPassword string)
|
||||
AddPeerPlugin(onion string, pluginID plugins.PluginID)
|
||||
|
@ -61,7 +58,7 @@ type Application interface {
|
|||
}
|
||||
|
||||
// LoadProfileFn is the function signature for a function in an app that loads a profile
|
||||
type LoadProfileFn func(profile *model.Profile, store storage.ProfileStore)
|
||||
type LoadProfileFn func(profile peer.CwtchPeer)
|
||||
|
||||
func newAppCore(appDirectory string) *applicationCore {
|
||||
appCore := &applicationCore{eventBuses: make(map[string]event.Manager), directory: appDirectory}
|
||||
|
@ -72,33 +69,13 @@ func newAppCore(appDirectory string) *applicationCore {
|
|||
// NewApp creates a new app with some environment awareness and initializes a Tor Manager
|
||||
func NewApp(acn connectivity.ACN, appDirectory string) Application {
|
||||
log.Debugf("NewApp(%v)\n", appDirectory)
|
||||
app := &application{storage: make(map[string]storage.ProfileStore), engines: make(map[string]connections.Engine), applicationCore: *newAppCore(appDirectory), appBus: event.NewEventManager()}
|
||||
app := &application{engines: make(map[string]connections.Engine), applicationCore: *newAppCore(appDirectory), appBus: event.NewEventManager()}
|
||||
app.appletPeers.init()
|
||||
|
||||
app.appletACN.init(acn, app.getACNStatusHandler())
|
||||
return app
|
||||
}
|
||||
|
||||
// CreatePeer creates a new Peer with a given name and core required accessories (eventbus)
|
||||
func (ac *applicationCore) CreatePeer(name string) (*model.Profile, error) {
|
||||
log.Debugf("CreatePeer(%v)\n", name)
|
||||
|
||||
profile := storage.NewProfile(name)
|
||||
|
||||
ac.coremutex.Lock()
|
||||
defer ac.coremutex.Unlock()
|
||||
|
||||
_, exists := ac.eventBuses[profile.Onion]
|
||||
if exists {
|
||||
return nil, fmt.Errorf("error: profile for onion %v already exists", profile.Onion)
|
||||
}
|
||||
|
||||
eventBus := event.NewEventManager()
|
||||
ac.eventBuses[profile.Onion] = eventBus
|
||||
|
||||
return profile, nil
|
||||
}
|
||||
|
||||
func (ac *applicationCore) DeletePeer(onion string) {
|
||||
ac.coremutex.Lock()
|
||||
defer ac.coremutex.Unlock()
|
||||
|
@ -108,37 +85,29 @@ func (ac *applicationCore) DeletePeer(onion string) {
|
|||
}
|
||||
|
||||
func (app *application) CreateTaggedPeer(name string, password string, tag string) {
|
||||
profile, err := app.applicationCore.CreatePeer(name)
|
||||
app.appmutex.Lock()
|
||||
defer app.appmutex.Unlock()
|
||||
|
||||
profileDirectory := path.Join(app.directory, "profiles", model.GenerateRandomID())
|
||||
|
||||
profile, err := peer.CreateEncryptedStorePeer(profileDirectory, name, password)
|
||||
if err != nil {
|
||||
log.Errorf("Error Creating Peer: %v", err)
|
||||
app.appBus.Publish(event.NewEventList(event.PeerError, event.Error, err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
profileStore := storage.CreateProfileWriterStore(app.eventBuses[profile.Onion], path.Join(app.directory, "profiles", profile.LocalID), password, profile)
|
||||
app.storage[profile.Onion] = profileStore
|
||||
|
||||
pc := app.storage[profile.Onion].GetProfileCopy(true)
|
||||
p := peer.FromProfile(pc)
|
||||
p.Init(app.eventBuses[profile.Onion])
|
||||
|
||||
peerAuthorizations := profile.ContactsAuthorizations()
|
||||
// TODO: Would be nice if ProtocolEngine did not need to explicitly be given the Private Key.
|
||||
identity := primitives.InitializeIdentity(profile.Name, &profile.Ed25519PrivateKey, &profile.Ed25519PublicKey)
|
||||
engine := connections.NewProtocolEngine(identity, profile.Ed25519PrivateKey, app.acn, app.eventBuses[profile.Onion], peerAuthorizations)
|
||||
|
||||
app.peers[profile.Onion] = p
|
||||
app.engines[profile.Onion] = engine
|
||||
eventBus := event.NewEventManager()
|
||||
app.eventBuses[profile.GetOnion()] = eventBus
|
||||
profile.Init(app.eventBuses[profile.GetOnion()])
|
||||
app.peers[profile.GetOnion()] = profile
|
||||
app.engines[profile.GetOnion()], _ = profile.GenerateProtocolEngine(app.acn, app.eventBuses[profile.GetOnion()])
|
||||
|
||||
if tag != "" {
|
||||
p.SetScopedZonedAttribute(attr.LocalScope, attr.ProfileZone, constants.Tag, tag)
|
||||
profile.SetScopedZonedAttribute(attr.LocalScope, attr.ProfileZone, constants.Tag, tag)
|
||||
}
|
||||
|
||||
app.appBus.Publish(event.NewEvent(event.NewPeer, map[event.Field]string{event.Identity: profile.Onion, event.Created: event.True}))
|
||||
}
|
||||
|
||||
// CreatePeer creates a new Peer with the given name and required accessories (eventbus, storage, protocol engine)
|
||||
func (app *application) CreatePeer(name string, password string) {
|
||||
app.CreateTaggedPeer(name, password, "")
|
||||
app.appBus.Publish(event.NewEvent(event.NewPeer, map[event.Field]string{event.Identity: profile.GetOnion(), event.Created: event.True}))
|
||||
}
|
||||
|
||||
func (app *application) DeletePeer(onion string, password string) {
|
||||
|
@ -146,23 +115,20 @@ func (app *application) DeletePeer(onion string, password string) {
|
|||
app.appmutex.Lock()
|
||||
defer app.appmutex.Unlock()
|
||||
|
||||
if app.storage[onion].CheckPassword(password) {
|
||||
if app.peers[onion].CheckPassword(password) {
|
||||
app.appletPlugins.ShutdownPeer(onion)
|
||||
app.plugins.Delete(onion)
|
||||
|
||||
app.peers[onion].Shutdown()
|
||||
delete(app.peers, onion)
|
||||
|
||||
// Shutdown and Remove the Engine
|
||||
app.engines[onion].Shutdown()
|
||||
delete(app.engines, onion)
|
||||
|
||||
app.storage[onion].Shutdown()
|
||||
app.storage[onion].Delete()
|
||||
delete(app.storage, onion)
|
||||
|
||||
app.peers[onion].Shutdown()
|
||||
app.peers[onion].Delete()
|
||||
delete(app.peers, onion)
|
||||
app.eventBuses[onion].Publish(event.NewEventList(event.ShutdownPeer, event.Identity, onion))
|
||||
|
||||
app.applicationCore.DeletePeer(onion)
|
||||
|
||||
log.Debugf("Delete peer for %v Done\n", onion)
|
||||
app.appBus.Publish(event.NewEventList(event.PeerDeleted, event.Identity, onion))
|
||||
return
|
||||
|
@ -186,27 +152,28 @@ func (ac *applicationCore) LoadProfiles(password string, timeline bool, loadProf
|
|||
}
|
||||
|
||||
for _, file := range files {
|
||||
eventBus := event.NewEventManager()
|
||||
profileStore, err := storage.LoadProfileWriterStore(eventBus, path.Join(ac.directory, "profiles", file.Name()), password)
|
||||
// Attempt to load an encrypted database
|
||||
profileDirectory := path.Join(ac.directory, "profiles", file.Name())
|
||||
profile, err := peer.FromEncryptedDatabase(profileDirectory, password)
|
||||
if err == nil {
|
||||
// return the load the profile...
|
||||
log.Infof("loading profile from new-type storage database...")
|
||||
loadProfileFn(profile)
|
||||
} else { // On failure attempt to load a legacy profile
|
||||
profileStore, err := storage.LoadProfileWriterStore(profileDirectory, password)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
log.Infof("found legacy profile. importing to new database structure...")
|
||||
legacyProfile := profileStore.GetProfileCopy(timeline)
|
||||
|
||||
profile := profileStore.GetProfileCopy(timeline)
|
||||
|
||||
_, exists := ac.eventBuses[profile.Onion]
|
||||
if exists {
|
||||
profileStore.Shutdown()
|
||||
eventBus.Shutdown()
|
||||
log.Errorf("profile for onion %v already exists", profile.Onion)
|
||||
continue
|
||||
cps, err := peer.CreateEncryptedStore(profileDirectory, password)
|
||||
if err != nil {
|
||||
log.Errorf("error creating encrypted store: %v", err)
|
||||
}
|
||||
profile := peer.ImportLegacyProfile(legacyProfile, cps)
|
||||
loadProfileFn(profile)
|
||||
}
|
||||
|
||||
ac.coremutex.Lock()
|
||||
ac.eventBuses[profile.Onion] = eventBus
|
||||
ac.coremutex.Unlock()
|
||||
|
||||
loadProfileFn(profile, profileStore)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -214,20 +181,22 @@ func (ac *applicationCore) LoadProfiles(password string, timeline bool, loadProf
|
|||
// LoadProfiles takes a password and attempts to load any profiles it can from storage with it and create Peers for them
|
||||
func (app *application) LoadProfiles(password string) {
|
||||
count := 0
|
||||
app.applicationCore.LoadProfiles(password, true, func(profile *model.Profile, profileStore storage.ProfileStore) {
|
||||
peer := peer.FromProfile(profile)
|
||||
peer.Init(app.eventBuses[profile.Onion])
|
||||
|
||||
peerAuthorizations := profile.ContactsAuthorizations()
|
||||
identity := primitives.InitializeIdentity(profile.Name, &profile.Ed25519PrivateKey, &profile.Ed25519PublicKey)
|
||||
engine := connections.NewProtocolEngine(identity, profile.Ed25519PrivateKey, app.acn, app.eventBuses[profile.Onion], peerAuthorizations)
|
||||
app.applicationCore.LoadProfiles(password, true, func(profile peer.CwtchPeer) {
|
||||
app.appmutex.Lock()
|
||||
app.peers[profile.Onion] = peer
|
||||
app.storage[profile.Onion] = profileStore
|
||||
app.engines[profile.Onion] = engine
|
||||
app.appmutex.Unlock()
|
||||
app.appBus.Publish(event.NewEvent(event.NewPeer, map[event.Field]string{event.Identity: profile.Onion, event.Created: event.False}))
|
||||
// Only attempt to finalize the profile if we don't have one loaded...
|
||||
if app.peers[profile.GetOnion()] == nil {
|
||||
eventBus := event.NewEventManager()
|
||||
app.eventBuses[profile.GetOnion()] = eventBus
|
||||
profile.Init(app.eventBuses[profile.GetOnion()])
|
||||
app.peers[profile.GetOnion()] = profile
|
||||
app.engines[profile.GetOnion()], _ = profile.GenerateProtocolEngine(app.acn, app.eventBuses[profile.GetOnion()])
|
||||
app.appBus.Publish(event.NewEvent(event.NewPeer, map[event.Field]string{event.Identity: profile.GetOnion(), event.Created: event.False}))
|
||||
count++
|
||||
} else {
|
||||
// Otherwise shutdown the connections
|
||||
profile.Shutdown()
|
||||
}
|
||||
app.appmutex.Unlock()
|
||||
})
|
||||
if count == 0 {
|
||||
message := event.NewEventList(event.AppError, event.Error, event.AppErrLoaded0)
|
||||
|
@ -251,12 +220,12 @@ func (ac *applicationCore) GetEventBus(onion string) event.Manager {
|
|||
func (app *application) getACNStatusHandler() func(int, string) {
|
||||
return func(progress int, status string) {
|
||||
progStr := strconv.Itoa(progress)
|
||||
app.peerLock.Lock()
|
||||
app.appmutex.Lock()
|
||||
app.appBus.Publish(event.NewEventList(event.ACNStatus, event.Progress, progStr, event.Status, status))
|
||||
for _, bus := range app.eventBuses {
|
||||
bus.Publish(event.NewEventList(event.ACNStatus, event.Progress, progStr, event.Status, status))
|
||||
}
|
||||
app.peerLock.Unlock()
|
||||
app.appmutex.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -280,8 +249,6 @@ func (app *application) ShutdownPeer(onion string) {
|
|||
delete(app.peers, onion)
|
||||
app.engines[onion].Shutdown()
|
||||
delete(app.engines, onion)
|
||||
app.storage[onion].Shutdown()
|
||||
delete(app.storage, onion)
|
||||
app.appletPlugins.Shutdown()
|
||||
}
|
||||
|
||||
|
@ -293,8 +260,6 @@ func (app *application) Shutdown() {
|
|||
app.appletPlugins.ShutdownPeer(id)
|
||||
log.Debugf("Shutting Down Engines for %v", id)
|
||||
app.engines[id].Shutdown()
|
||||
log.Debugf("Shutting Down Storage for %v", id)
|
||||
app.storage[id].Shutdown()
|
||||
log.Debugf("Shutting Down Bus for %v", id)
|
||||
app.eventBuses[id].Shutdown()
|
||||
}
|
||||
|
|
|
@ -1,39 +0,0 @@
|
|||
package app
|
||||
|
||||
import "cwtch.im/cwtch/event"
|
||||
import "git.openprivacy.ca/openprivacy/log"
|
||||
|
||||
const (
|
||||
// DestApp should be used as a destination for IPC messages that are for the application itself an not a peer
|
||||
DestApp = "app"
|
||||
)
|
||||
|
||||
type applicationBridge struct {
|
||||
applicationCore
|
||||
|
||||
bridge event.IPCBridge
|
||||
handle func(*event.Event)
|
||||
}
|
||||
|
||||
func (ab *applicationBridge) listen() {
|
||||
log.Infoln("ab.listen()")
|
||||
for {
|
||||
ipcMessage, ok := ab.bridge.Read()
|
||||
log.Debugf("listen() got %v for %v\n", ipcMessage.Message.EventType, ipcMessage.Dest)
|
||||
if !ok {
|
||||
log.Debugln("exiting appBridge.listen()")
|
||||
return
|
||||
}
|
||||
|
||||
if ipcMessage.Dest == DestApp {
|
||||
ab.handle(&ipcMessage.Message)
|
||||
} else {
|
||||
if eventBus, exists := ab.eventBuses[ipcMessage.Dest]; exists {
|
||||
eventBus.PublishLocal(ipcMessage.Message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (ab *applicationBridge) Shutdown() {
|
||||
}
|
177
app/appClient.go
177
app/appClient.go
|
@ -1,177 +0,0 @@
|
|||
package app
|
||||
|
||||
import (
|
||||
"cwtch.im/cwtch/app/plugins"
|
||||
"cwtch.im/cwtch/event"
|
||||
"cwtch.im/cwtch/peer"
|
||||
"cwtch.im/cwtch/storage"
|
||||
"fmt"
|
||||
"git.openprivacy.ca/openprivacy/log"
|
||||
"path"
|
||||
"strconv"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type applicationClient struct {
|
||||
applicationBridge
|
||||
appletPeers
|
||||
|
||||
appBus event.Manager
|
||||
acmutex sync.Mutex
|
||||
}
|
||||
|
||||
// NewAppClient returns an Application that acts as a client to a AppService, connected by the IPCBridge supplied
|
||||
func NewAppClient(appDirectory string, bridge event.IPCBridge) Application {
|
||||
appClient := &applicationClient{appletPeers: appletPeers{peers: make(map[string]peer.CwtchPeer)}, applicationBridge: applicationBridge{applicationCore: *newAppCore(appDirectory), bridge: bridge}, appBus: event.NewEventManager()}
|
||||
appClient.handle = appClient.handleEvent
|
||||
|
||||
go appClient.listen()
|
||||
|
||||
appClient.bridge.Write(&event.IPCMessage{Dest: DestApp, Message: event.NewEventList(event.ReloadClient)})
|
||||
|
||||
log.Infoln("Created new App Client")
|
||||
return appClient
|
||||
}
|
||||
|
||||
// GetPrimaryBus returns the bus the Application uses for events that aren't peer specific
|
||||
func (ac *applicationClient) GetPrimaryBus() event.Manager {
|
||||
return ac.appBus
|
||||
}
|
||||
|
||||
func (ac *applicationClient) handleEvent(ev *event.Event) {
|
||||
switch ev.EventType {
|
||||
case event.NewPeer:
|
||||
localID := ev.Data[event.Identity]
|
||||
key := ev.Data[event.Key]
|
||||
salt := ev.Data[event.Salt]
|
||||
reload := ev.Data[event.Status] == event.StorageRunning
|
||||
created := ev.Data[event.Created]
|
||||
ac.newPeer(localID, key, salt, reload, created)
|
||||
case event.PeerDeleted:
|
||||
onion := ev.Data[event.Identity]
|
||||
ac.handleDeletedPeer(onion)
|
||||
case event.PeerError:
|
||||
ac.appBus.Publish(*ev)
|
||||
case event.AppError:
|
||||
ac.appBus.Publish(*ev)
|
||||
case event.ACNStatus:
|
||||
ac.appBus.Publish(*ev)
|
||||
case event.ACNVersion:
|
||||
ac.appBus.Publish(*ev)
|
||||
case event.ReloadDone:
|
||||
ac.appBus.Publish(*ev)
|
||||
}
|
||||
}
|
||||
|
||||
func (ac *applicationClient) newPeer(localID, key, salt string, reload bool, created string) {
|
||||
var keyBytes [32]byte
|
||||
var saltBytes [128]byte
|
||||
copy(keyBytes[:], key)
|
||||
copy(saltBytes[:], salt)
|
||||
profile, err := storage.ReadProfile(path.Join(ac.directory, "profiles", localID), keyBytes, saltBytes)
|
||||
if err != nil {
|
||||
log.Errorf("Could not read profile for NewPeer event: %v\n", err)
|
||||
ac.appBus.Publish(event.NewEventList(event.PeerError, event.Error, fmt.Sprintf("Could not read profile for NewPeer event: %v\n", err)))
|
||||
return
|
||||
}
|
||||
|
||||
_, exists := ac.peers[profile.Onion]
|
||||
if exists {
|
||||
log.Errorf("profile for onion %v already exists", profile.Onion)
|
||||
ac.appBus.Publish(event.NewEventList(event.PeerError, event.Error, fmt.Sprintf("profile for onion %v already exists", profile.Onion)))
|
||||
return
|
||||
}
|
||||
|
||||
eventBus := event.NewIPCEventManager(ac.bridge, profile.Onion)
|
||||
peer := peer.FromProfile(profile)
|
||||
peer.Init(eventBus)
|
||||
|
||||
ac.peerLock.Lock()
|
||||
defer ac.peerLock.Unlock()
|
||||
ac.peers[profile.Onion] = peer
|
||||
ac.eventBuses[profile.Onion] = eventBus
|
||||
npEvent := event.NewEvent(event.NewPeer, map[event.Field]string{event.Identity: profile.Onion, event.Created: created})
|
||||
if reload {
|
||||
npEvent.Data[event.Status] = event.StorageRunning
|
||||
}
|
||||
ac.appBus.Publish(npEvent)
|
||||
|
||||
if reload {
|
||||
ac.bridge.Write(&event.IPCMessage{Dest: DestApp, Message: event.NewEventList(event.ReloadPeer, event.Identity, profile.Onion)})
|
||||
}
|
||||
}
|
||||
|
||||
// CreatePeer messages the service to create a new Peer with the given name
|
||||
func (ac *applicationClient) CreatePeer(name string, password string) {
|
||||
ac.CreateTaggedPeer(name, password, "")
|
||||
}
|
||||
|
||||
func (ac *applicationClient) CreateTaggedPeer(name, password, tag string) {
|
||||
log.Infof("appClient CreatePeer %v\n", name)
|
||||
message := event.IPCMessage{Dest: DestApp, Message: event.NewEvent(event.CreatePeer, map[event.Field]string{event.ProfileName: name, event.Password: password, event.Data: tag})}
|
||||
ac.bridge.Write(&message)
|
||||
}
|
||||
|
||||
// DeletePeer messages the service to delete a peer
|
||||
func (ac *applicationClient) DeletePeer(onion string, password string) {
|
||||
message := event.IPCMessage{Dest: DestApp, Message: event.NewEvent(event.DeletePeer, map[event.Field]string{event.Identity: onion, event.Password: password})}
|
||||
ac.bridge.Write(&message)
|
||||
}
|
||||
|
||||
func (ac *applicationClient) ChangePeerPassword(onion, oldpass, newpass string) {
|
||||
message := event.IPCMessage{Dest: onion, Message: event.NewEventList(event.ChangePassword, event.Password, oldpass, event.NewPassword, newpass)}
|
||||
ac.bridge.Write(&message)
|
||||
}
|
||||
|
||||
func (ac *applicationClient) handleDeletedPeer(onion string) {
|
||||
ac.acmutex.Lock()
|
||||
defer ac.acmutex.Unlock()
|
||||
ac.peers[onion].Shutdown()
|
||||
delete(ac.peers, onion)
|
||||
ac.eventBuses[onion].Publish(event.NewEventList(event.ShutdownPeer, event.Identity, onion))
|
||||
|
||||
ac.applicationCore.DeletePeer(onion)
|
||||
ac.appBus.Publish(event.NewEventList(event.PeerDeleted, event.Identity, onion))
|
||||
}
|
||||
|
||||
func (ac *applicationClient) AddPeerPlugin(onion string, pluginID plugins.PluginID) {
|
||||
message := event.IPCMessage{Dest: DestApp, Message: event.NewEvent(event.AddPeerPlugin, map[event.Field]string{event.Identity: onion, event.Data: strconv.Itoa(int(pluginID))})}
|
||||
ac.bridge.Write(&message)
|
||||
}
|
||||
|
||||
// LoadProfiles messages the service to load any profiles for the given password
|
||||
func (ac *applicationClient) LoadProfiles(password string) {
|
||||
message := event.IPCMessage{Dest: DestApp, Message: event.NewEvent(event.LoadProfiles, map[event.Field]string{event.Password: password})}
|
||||
ac.bridge.Write(&message)
|
||||
}
|
||||
|
||||
func (ac *applicationClient) QueryACNStatus() {
|
||||
message := event.IPCMessage{Dest: DestApp, Message: event.NewEvent(event.GetACNStatus, map[event.Field]string{})}
|
||||
ac.bridge.Write(&message)
|
||||
}
|
||||
|
||||
func (ac *applicationClient) QueryACNVersion() {
|
||||
message := event.IPCMessage{Dest: DestApp, Message: event.NewEvent(event.GetACNVersion, map[event.Field]string{})}
|
||||
ac.bridge.Write(&message)
|
||||
}
|
||||
|
||||
// ShutdownPeer shuts down a peer and removes it from the app's management
|
||||
func (ac *applicationClient) ShutdownPeer(onion string) {
|
||||
ac.acmutex.Lock()
|
||||
defer ac.acmutex.Unlock()
|
||||
ac.eventBuses[onion].Shutdown()
|
||||
delete(ac.eventBuses, onion)
|
||||
ac.peers[onion].Shutdown()
|
||||
delete(ac.peers, onion)
|
||||
message := event.IPCMessage{Dest: DestApp, Message: event.NewEvent(event.ShutdownPeer, map[event.Field]string{event.Identity: onion})}
|
||||
ac.bridge.Write(&message)
|
||||
}
|
||||
|
||||
// Shutdown shuts down the application client and all front end peer components
|
||||
func (ac *applicationClient) Shutdown() {
|
||||
for id := range ac.peers {
|
||||
ac.ShutdownPeer(id)
|
||||
}
|
||||
ac.applicationBridge.Shutdown()
|
||||
ac.appBus.Shutdown()
|
||||
}
|
|
@ -1,209 +0,0 @@
|
|||
package app
|
||||
|
||||
import (
|
||||
"cwtch.im/cwtch/app/plugins"
|
||||
"cwtch.im/cwtch/event"
|
||||
"cwtch.im/cwtch/model"
|
||||
"cwtch.im/cwtch/protocol/connections"
|
||||
"cwtch.im/cwtch/storage"
|
||||
"git.openprivacy.ca/cwtch.im/tapir/primitives"
|
||||
"git.openprivacy.ca/openprivacy/connectivity"
|
||||
"git.openprivacy.ca/openprivacy/log"
|
||||
path "path/filepath"
|
||||
"strconv"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type applicationService struct {
|
||||
applicationBridge
|
||||
appletACN
|
||||
appletPlugins
|
||||
|
||||
storage map[string]storage.ProfileStore
|
||||
engines map[string]connections.Engine
|
||||
asmutex sync.Mutex
|
||||
}
|
||||
|
||||
// ApplicationService is the back end of an application that manages engines and writing storage and communicates to an ApplicationClient by an IPCBridge
|
||||
type ApplicationService interface {
|
||||
Shutdown()
|
||||
}
|
||||
|
||||
// NewAppService returns an ApplicationService that runs the backend of an app and communicates with a client by the supplied IPCBridge
|
||||
func NewAppService(acn connectivity.ACN, appDirectory string, bridge event.IPCBridge) ApplicationService {
|
||||
appService := &applicationService{storage: make(map[string]storage.ProfileStore), engines: make(map[string]connections.Engine), applicationBridge: applicationBridge{applicationCore: *newAppCore(appDirectory), bridge: bridge}}
|
||||
|
||||
appService.appletACN.init(acn, appService.getACNStatusHandler())
|
||||
appService.handle = appService.handleEvent
|
||||
|
||||
go appService.listen()
|
||||
|
||||
log.Infoln("Created new App Service")
|
||||
return appService
|
||||
}
|
||||
|
||||
func (as *applicationService) handleEvent(ev *event.Event) {
|
||||
log.Infof("app Service handleEvent %v\n", ev.EventType)
|
||||
switch ev.EventType {
|
||||
case event.CreatePeer:
|
||||
profileName := ev.Data[event.ProfileName]
|
||||
password := ev.Data[event.Password]
|
||||
tag := ev.Data[event.Data]
|
||||
as.createPeer(profileName, password, tag)
|
||||
case event.DeletePeer:
|
||||
onion := ev.Data[event.Identity]
|
||||
password := ev.Data[event.Password]
|
||||
as.deletePeer(onion, password)
|
||||
|
||||
message := event.IPCMessage{Dest: DestApp, Message: *ev}
|
||||
as.bridge.Write(&message)
|
||||
case event.AddPeerPlugin:
|
||||
onion := ev.Data[event.Identity]
|
||||
pluginID, _ := strconv.Atoi(ev.Data[event.Data])
|
||||
as.AddPlugin(onion, plugins.PluginID(pluginID), as.eventBuses[onion], as.acn)
|
||||
case event.LoadProfiles:
|
||||
password := ev.Data[event.Password]
|
||||
as.loadProfiles(password)
|
||||
case event.ReloadClient:
|
||||
for _, storage := range as.storage {
|
||||
peerMsg := *storage.GetNewPeerMessage()
|
||||
peerMsg.Data[event.Status] = event.StorageRunning
|
||||
peerMsg.Data[event.Created] = event.False
|
||||
message := event.IPCMessage{Dest: DestApp, Message: peerMsg}
|
||||
as.bridge.Write(&message)
|
||||
}
|
||||
|
||||
message := event.IPCMessage{Dest: DestApp, Message: event.NewEventList(event.ReloadDone)}
|
||||
as.bridge.Write(&message)
|
||||
case event.ReloadPeer:
|
||||
onion := ev.Data[event.Identity]
|
||||
events := as.storage[onion].GetStatusMessages()
|
||||
|
||||
for _, ev := range events {
|
||||
message := event.IPCMessage{Dest: onion, Message: *ev}
|
||||
as.bridge.Write(&message)
|
||||
}
|
||||
case event.GetACNStatus:
|
||||
prog, status := as.acn.GetBootstrapStatus()
|
||||
as.getACNStatusHandler()(prog, status)
|
||||
case event.GetACNVersion:
|
||||
version := as.acn.GetVersion()
|
||||
as.bridge.Write(&event.IPCMessage{Dest: DestApp, Message: event.NewEventList(event.ACNVersion, event.Data, version)})
|
||||
case event.ShutdownPeer:
|
||||
onion := ev.Data[event.Identity]
|
||||
as.ShutdownPeer(onion)
|
||||
}
|
||||
}
|
||||
|
||||
func (as *applicationService) createPeer(name, password, tag string) {
|
||||
log.Infof("app Service create peer %v %v\n", name, password)
|
||||
profile, err := as.applicationCore.CreatePeer(name)
|
||||
as.eventBuses[profile.Onion] = event.IPCEventManagerFrom(as.bridge, profile.Onion, as.eventBuses[profile.Onion])
|
||||
if err != nil {
|
||||
log.Errorf("Could not create Peer: %v\n", err)
|
||||
message := event.IPCMessage{Dest: DestApp, Message: event.NewEventList(event.PeerError, event.Error, err.Error())}
|
||||
as.bridge.Write(&message)
|
||||
return
|
||||
}
|
||||
|
||||
profileStore := storage.CreateProfileWriterStore(as.eventBuses[profile.Onion], path.Join(as.directory, "profiles", profile.LocalID), password, profile)
|
||||
|
||||
peerAuthorizations := profile.ContactsAuthorizations()
|
||||
// TODO: Would be nice if ProtocolEngine did not need to explicitly be given the Private Key.
|
||||
identity := primitives.InitializeIdentity(profile.Name, &profile.Ed25519PrivateKey, &profile.Ed25519PublicKey)
|
||||
engine := connections.NewProtocolEngine(identity, profile.Ed25519PrivateKey, as.acn, as.eventBuses[profile.Onion], peerAuthorizations)
|
||||
|
||||
as.storage[profile.Onion] = profileStore
|
||||
as.engines[profile.Onion] = engine
|
||||
|
||||
peerMsg := *profileStore.GetNewPeerMessage()
|
||||
peerMsg.Data[event.Created] = event.True
|
||||
peerMsg.Data[event.Status] = event.StorageNew
|
||||
message := event.IPCMessage{Dest: DestApp, Message: peerMsg}
|
||||
as.bridge.Write(&message)
|
||||
}
|
||||
|
||||
func (as *applicationService) loadProfiles(password string) {
|
||||
count := 0
|
||||
as.applicationCore.LoadProfiles(password, false, func(profile *model.Profile, profileStore storage.ProfileStore) {
|
||||
as.eventBuses[profile.Onion] = event.IPCEventManagerFrom(as.bridge, profile.Onion, as.eventBuses[profile.Onion])
|
||||
|
||||
peerAuthorizations := profile.ContactsAuthorizations()
|
||||
identity := primitives.InitializeIdentity(profile.Name, &profile.Ed25519PrivateKey, &profile.Ed25519PublicKey)
|
||||
engine := connections.NewProtocolEngine(identity, profile.Ed25519PrivateKey, as.acn, as.eventBuses[profile.Onion], peerAuthorizations)
|
||||
as.asmutex.Lock()
|
||||
as.storage[profile.Onion] = profileStore
|
||||
as.engines[profile.Onion] = engine
|
||||
as.asmutex.Unlock()
|
||||
|
||||
peerMsg := *profileStore.GetNewPeerMessage()
|
||||
peerMsg.Data[event.Created] = event.False
|
||||
peerMsg.Data[event.Status] = event.StorageNew
|
||||
message := event.IPCMessage{Dest: DestApp, Message: peerMsg}
|
||||
as.bridge.Write(&message)
|
||||
count++
|
||||
})
|
||||
if count == 0 {
|
||||
message := event.IPCMessage{Dest: DestApp, Message: event.NewEventList(event.AppError, event.Error, event.AppErrLoaded0)}
|
||||
as.bridge.Write(&message)
|
||||
}
|
||||
}
|
||||
|
||||
func (as *applicationService) getACNStatusHandler() func(int, string) {
|
||||
return func(progress int, status string) {
|
||||
progStr := strconv.Itoa(progress)
|
||||
as.bridge.Write(&event.IPCMessage{Dest: DestApp, Message: event.NewEventList(event.ACNStatus, event.Progress, progStr, event.Status, status)})
|
||||
as.applicationCore.coremutex.Lock()
|
||||
defer as.applicationCore.coremutex.Unlock()
|
||||
for _, bus := range as.eventBuses {
|
||||
bus.Publish(event.NewEventList(event.ACNStatus, event.Progress, progStr, event.Status, status))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (as *applicationService) deletePeer(onion, password string) {
|
||||
as.asmutex.Lock()
|
||||
defer as.asmutex.Unlock()
|
||||
|
||||
if as.storage[onion].CheckPassword(password) {
|
||||
as.appletPlugins.ShutdownPeer(onion)
|
||||
as.plugins.Delete(onion)
|
||||
|
||||
as.engines[onion].Shutdown()
|
||||
delete(as.engines, onion)
|
||||
|
||||
as.storage[onion].Shutdown()
|
||||
as.storage[onion].Delete()
|
||||
delete(as.storage, onion)
|
||||
|
||||
as.eventBuses[onion].Publish(event.NewEventList(event.ShutdownPeer, event.Identity, onion))
|
||||
|
||||
as.applicationCore.DeletePeer(onion)
|
||||
log.Debugf("Delete peer for %v Done\n", onion)
|
||||
|
||||
message := event.IPCMessage{Dest: DestApp, Message: event.NewEventList(event.PeerDeleted, event.Identity, onion)}
|
||||
as.bridge.Write(&message)
|
||||
return
|
||||
}
|
||||
message := event.IPCMessage{Dest: DestApp, Message: event.NewEventList(event.AppError, event.Error, event.PasswordMatchError, event.Identity, onion)}
|
||||
as.bridge.Write(&message)
|
||||
}
|
||||
|
||||
func (as *applicationService) ShutdownPeer(onion string) {
|
||||
as.engines[onion].Shutdown()
|
||||
delete(as.engines, onion)
|
||||
as.storage[onion].Shutdown()
|
||||
delete(as.storage, onion)
|
||||
as.eventBuses[onion].Shutdown()
|
||||
delete(as.eventBuses, onion)
|
||||
}
|
||||
|
||||
// Shutdown shuts down the application Service and all peer related backend parts
|
||||
func (as *applicationService) Shutdown() {
|
||||
log.Debugf("shutting down application service...")
|
||||
as.appletPlugins.Shutdown()
|
||||
for id := range as.engines {
|
||||
log.Debugf("shutting down application service peer engine %v", id)
|
||||
as.ShutdownPeer(id)
|
||||
}
|
||||
}
|
|
@ -113,9 +113,13 @@ func (ap *appletPlugins) AddPlugin(peerid string, id plugins.PluginID, bus event
|
|||
pluginsinf, _ := ap.plugins.Load(peerid)
|
||||
peerPlugins := pluginsinf.([]plugins.Plugin)
|
||||
|
||||
newp := plugins.Get(id, bus, acn, peerid)
|
||||
newp, err := plugins.Get(id, bus, acn, peerid)
|
||||
if err == nil {
|
||||
newp.Start()
|
||||
peerPlugins = append(peerPlugins, newp)
|
||||
log.Debugf("storing plugin for %v %v", peerid, peerPlugins)
|
||||
ap.plugins.Store(peerid, peerPlugins)
|
||||
} else {
|
||||
log.Errorf("error adding plugin: %v", err)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ package plugins
|
|||
|
||||
import (
|
||||
"cwtch.im/cwtch/event"
|
||||
"fmt"
|
||||
"git.openprivacy.ca/openprivacy/connectivity"
|
||||
)
|
||||
|
||||
|
@ -21,13 +22,13 @@ type Plugin interface {
|
|||
}
|
||||
|
||||
// Get is a plugin factory for the requested plugin
|
||||
func Get(id PluginID, bus event.Manager, acn connectivity.ACN, onion string) Plugin {
|
||||
func Get(id PluginID, bus event.Manager, acn connectivity.ACN, onion string) (Plugin, error) {
|
||||
switch id {
|
||||
case CONNECTIONRETRY:
|
||||
return NewConnectionRetry(bus, onion)
|
||||
return NewConnectionRetry(bus, onion), nil
|
||||
case NETWORKCHECK:
|
||||
return NewNetworkCheck(bus, acn)
|
||||
return NewNetworkCheck(bus, acn), nil
|
||||
}
|
||||
|
||||
return nil
|
||||
return nil, fmt.Errorf("plugin not defined %v", id)
|
||||
}
|
||||
|
|
|
@ -1,57 +0,0 @@
|
|||
package bridge
|
||||
|
||||
import (
|
||||
"cwtch.im/cwtch/event"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type goChanBridge struct {
|
||||
in chan event.IPCMessage
|
||||
out chan event.IPCMessage
|
||||
closedChan chan bool
|
||||
closed bool
|
||||
lock sync.Mutex
|
||||
}
|
||||
|
||||
// MakeGoChanBridge returns a simple testing IPCBridge made from inprocess go channels
|
||||
func MakeGoChanBridge() (b1, b2 event.IPCBridge) {
|
||||
chan1 := make(chan event.IPCMessage)
|
||||
chan2 := make(chan event.IPCMessage)
|
||||
closed := make(chan bool)
|
||||
|
||||
a := &goChanBridge{in: chan1, out: chan2, closedChan: closed, closed: false}
|
||||
b := &goChanBridge{in: chan2, out: chan1, closedChan: closed, closed: false}
|
||||
|
||||
go monitor(a, b)
|
||||
|
||||
return a, b
|
||||
}
|
||||
|
||||
func monitor(a, b *goChanBridge) {
|
||||
<-a.closedChan
|
||||
a.closed = true
|
||||
b.closed = true
|
||||
a.closedChan <- true
|
||||
}
|
||||
|
||||
func (pb *goChanBridge) Read() (*event.IPCMessage, bool) {
|
||||
message, ok := <-pb.in
|
||||
return &message, ok
|
||||
}
|
||||
|
||||
func (pb *goChanBridge) Write(message *event.IPCMessage) {
|
||||
pb.lock.Lock()
|
||||
defer pb.lock.Unlock()
|
||||
if !pb.closed {
|
||||
pb.out <- *message
|
||||
}
|
||||
}
|
||||
|
||||
func (pb *goChanBridge) Shutdown() {
|
||||
if !pb.closed {
|
||||
close(pb.in)
|
||||
close(pb.out)
|
||||
pb.closedChan <- true
|
||||
<-pb.closedChan
|
||||
}
|
||||
}
|
|
@ -1,72 +0,0 @@
|
|||
package bridge
|
||||
|
||||
/* Todo: When go generics ships, refactor this and event.infiniteChannel into one */
|
||||
|
||||
// InfiniteChannel implements the Channel interface with an infinite buffer between the input and the output.
|
||||
type InfiniteChannel struct {
|
||||
input, output chan interface{}
|
||||
length chan int
|
||||
buffer *InfiniteQueue
|
||||
}
|
||||
|
||||
func newInfiniteChannel() *InfiniteChannel {
|
||||
ch := &InfiniteChannel{
|
||||
input: make(chan interface{}),
|
||||
output: make(chan interface{}),
|
||||
length: make(chan int),
|
||||
buffer: newInfiniteQueue(),
|
||||
}
|
||||
go ch.infiniteBuffer()
|
||||
return ch
|
||||
}
|
||||
|
||||
// In returns the input channel
|
||||
func (ch *InfiniteChannel) In() chan<- interface{} {
|
||||
return ch.input
|
||||
}
|
||||
|
||||
// Out returns the output channel
|
||||
func (ch *InfiniteChannel) Out() <-chan interface{} {
|
||||
return ch.output
|
||||
}
|
||||
|
||||
// Len returns the length of items in queue
|
||||
func (ch *InfiniteChannel) Len() int {
|
||||
return <-ch.length
|
||||
}
|
||||
|
||||
// Close closes the InfiniteChanel
|
||||
func (ch *InfiniteChannel) Close() {
|
||||
close(ch.input)
|
||||
}
|
||||
|
||||
func (ch *InfiniteChannel) infiniteBuffer() {
|
||||
var input, output chan interface{}
|
||||
var next interface{}
|
||||
input = ch.input
|
||||
|
||||
for input != nil || output != nil {
|
||||
select {
|
||||
case elem, open := <-input:
|
||||
if open {
|
||||
ch.buffer.Add(elem)
|
||||
} else {
|
||||
input = nil
|
||||
}
|
||||
case output <- next:
|
||||
ch.buffer.Remove()
|
||||
case ch.length <- ch.buffer.Length():
|
||||
}
|
||||
|
||||
if ch.buffer.Length() > 0 {
|
||||
output = ch.output
|
||||
next = ch.buffer.Peek()
|
||||
} else {
|
||||
output = nil
|
||||
next = nil
|
||||
}
|
||||
}
|
||||
|
||||
close(ch.output)
|
||||
close(ch.length)
|
||||
}
|
|
@ -1,105 +0,0 @@
|
|||
package bridge
|
||||
|
||||
/* Todo: When go generics ships, refactor this and event.infinitQueue channel into one */
|
||||
|
||||
/*
|
||||
Package queue provides a fast, ring-buffer queue based on the version suggested by Dariusz Górecki.
|
||||
Using this instead of other, simpler, queue implementations (slice+append or linked list) provides
|
||||
substantial memory and time benefits, and fewer GC pauses.
|
||||
|
||||
The queue implemented here is as fast as it is for an additional reason: it is *not* thread-safe.
|
||||
*/
|
||||
|
||||
// minQueueLen is smallest capacity that queue may have.
|
||||
// Must be power of 2 for bitwise modulus: x % n == x & (n - 1).
|
||||
const minQueueLen = 16
|
||||
|
||||
// InfiniteQueue represents a single instance of the queue data structure.
|
||||
type InfiniteQueue struct {
|
||||
buf []interface{}
|
||||
head, tail, count int
|
||||
}
|
||||
|
||||
// New constructs and returns a new Queue.
|
||||
func newInfiniteQueue() *InfiniteQueue {
|
||||
return &InfiniteQueue{
|
||||
buf: make([]interface{}, minQueueLen),
|
||||
}
|
||||
}
|
||||
|
||||
// Length returns the number of elements currently stored in the queue.
|
||||
func (q *InfiniteQueue) Length() int {
|
||||
return q.count
|
||||
}
|
||||
|
||||
// resizes the queue to fit exactly twice its current contents
|
||||
// this can result in shrinking if the queue is less than half-full
|
||||
func (q *InfiniteQueue) resize() {
|
||||
newBuf := make([]interface{}, q.count<<1)
|
||||
|
||||
if q.tail > q.head {
|
||||
copy(newBuf, q.buf[q.head:q.tail])
|
||||
} else {
|
||||
n := copy(newBuf, q.buf[q.head:])
|
||||
copy(newBuf[n:], q.buf[:q.tail])
|
||||
}
|
||||
|
||||
q.head = 0
|
||||
q.tail = q.count
|
||||
q.buf = newBuf
|
||||
}
|
||||
|
||||
// Add puts an element on the end of the queue.
|
||||
func (q *InfiniteQueue) Add(elem interface{}) {
|
||||
if q.count == len(q.buf) {
|
||||
q.resize()
|
||||
}
|
||||
|
||||
q.buf[q.tail] = elem
|
||||
// bitwise modulus
|
||||
q.tail = (q.tail + 1) & (len(q.buf) - 1)
|
||||
q.count++
|
||||
}
|
||||
|
||||
// Peek returns the element at the head of the queue. This call panics
|
||||
// if the queue is empty.
|
||||
func (q *InfiniteQueue) Peek() interface{} {
|
||||
if q.count <= 0 {
|
||||
panic("queue: Peek() called on empty queue")
|
||||
}
|
||||
return q.buf[q.head]
|
||||
}
|
||||
|
||||
// Get returns the element at index i in the queue. If the index is
|
||||
// invalid, the call will panic. This method accepts both positive and
|
||||
// negative index values. Index 0 refers to the first element, and
|
||||
// index -1 refers to the last.
|
||||
func (q *InfiniteQueue) Get(i int) interface{} {
|
||||
// If indexing backwards, convert to positive index.
|
||||
if i < 0 {
|
||||
i += q.count
|
||||
}
|
||||
if i < 0 || i >= q.count {
|
||||
panic("queue: Get() called with index out of range")
|
||||
}
|
||||
// bitwise modulus
|
||||
return q.buf[(q.head+i)&(len(q.buf)-1)]
|
||||
}
|
||||
|
||||
// Remove removes and returns the element from the front of the queue. If the
|
||||
// queue is empty, the call will panic.
|
||||
func (q *InfiniteQueue) Remove() interface{} {
|
||||
if q.count <= 0 {
|
||||
panic("queue: Remove() called on empty queue")
|
||||
}
|
||||
ret := q.buf[q.head]
|
||||
q.buf[q.head] = nil
|
||||
// bitwise modulus
|
||||
q.head = (q.head + 1) & (len(q.buf) - 1)
|
||||
q.count--
|
||||
// Resize down if buffer 1/4 full.
|
||||
if len(q.buf) > minQueueLen && (q.count<<2) == len(q.buf) {
|
||||
q.resize()
|
||||
}
|
||||
return ret
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
// +build windows
|
||||
|
||||
package bridge
|
||||
|
||||
import (
|
||||
"cwtch.im/cwtch/event"
|
||||
"log"
|
||||
)
|
||||
|
||||
func NewPipeBridgeClient(inFilename, outFilename string) event.IPCBridge {
|
||||
log.Fatal("Not supported on windows")
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewPipeBridgeService returns a pipe backed IPCBridge for a service
|
||||
func NewPipeBridgeService(inFilename, outFilename string) event.IPCBridge {
|
||||
log.Fatal("Not supported on windows")
|
||||
return nil
|
||||
}
|
|
@ -1,357 +0,0 @@
|
|||
// +build !windows
|
||||
|
||||
package bridge
|
||||
|
||||
import (
|
||||
"cwtch.im/cwtch/event"
|
||||
"cwtch.im/cwtch/protocol/connections"
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"git.openprivacy.ca/openprivacy/log"
|
||||
"os"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
/* pipeBridge creates a pair of named pipes
|
||||
Needs a call to new client and service to fully successfully open
|
||||
*/
|
||||
|
||||
const maxBufferSize = 1000
|
||||
|
||||
const serviceName = "service"
|
||||
const clientName = "client"
|
||||
|
||||
const syn = "SYN"
|
||||
const synack = "SYNACK"
|
||||
const ack = "ACK"
|
||||
|
||||
type pipeBridge struct {
|
||||
infile, outfile string
|
||||
in, out *os.File
|
||||
read chan event.IPCMessage
|
||||
write *InfiniteChannel
|
||||
closedChan chan bool
|
||||
state connections.ConnectionState
|
||||
lock sync.Mutex
|
||||
threeShake func() bool
|
||||
|
||||
// For logging / debugging purposes
|
||||
name string
|
||||
}
|
||||
|
||||
func newPipeBridge(inFilename, outFilename string) *pipeBridge {
|
||||
syscall.Mkfifo(inFilename, 0600)
|
||||
syscall.Mkfifo(outFilename, 0600)
|
||||
pb := &pipeBridge{infile: inFilename, outfile: outFilename, state: connections.DISCONNECTED}
|
||||
pb.read = make(chan event.IPCMessage, maxBufferSize)
|
||||
pb.write = newInfiniteChannel() //make(chan event.IPCMessage, maxBufferSize)
|
||||
return pb
|
||||
}
|
||||
|
||||
// NewPipeBridgeClient returns a pipe backed IPCBridge for a client
|
||||
func NewPipeBridgeClient(inFilename, outFilename string) event.IPCBridge {
|
||||
log.Debugf("Making new PipeBridge Client...\n")
|
||||
pb := newPipeBridge(inFilename, outFilename)
|
||||
pb.name = clientName
|
||||
pb.threeShake = pb.threeShakeClient
|
||||
go pb.connectionManager()
|
||||
|
||||
return pb
|
||||
}
|
||||
|
||||
// NewPipeBridgeService returns a pipe backed IPCBridge for a service
|
||||
func NewPipeBridgeService(inFilename, outFilename string) event.IPCBridge {
|
||||
log.Debugf("Making new PipeBridge Service...\n")
|
||||
pb := newPipeBridge(inFilename, outFilename)
|
||||
pb.name = serviceName
|
||||
pb.threeShake = pb.threeShakeService
|
||||
|
||||
go pb.connectionManager()
|
||||
|
||||
log.Debugf("Successfully created new PipeBridge Service!\n")
|
||||
return pb
|
||||
}
|
||||
|
||||
func (pb *pipeBridge) setState(state connections.ConnectionState) {
|
||||
pb.lock.Lock()
|
||||
defer pb.lock.Unlock()
|
||||
|
||||
pb.state = state
|
||||
}
|
||||
|
||||
func (pb *pipeBridge) getState() connections.ConnectionState {
|
||||
pb.lock.Lock()
|
||||
defer pb.lock.Unlock()
|
||||
|
||||
return pb.state
|
||||
}
|
||||
|
||||
func (pb *pipeBridge) connectionManager() {
|
||||
for pb.getState() != connections.KILLED {
|
||||
log.Debugf("clientConnManager loop start init\n")
|
||||
pb.setState(connections.CONNECTING)
|
||||
|
||||
var err error
|
||||
log.Debugf("%v open file infile\n", pb.name)
|
||||
pb.in, err = os.OpenFile(pb.infile, os.O_RDWR, 0600)
|
||||
if err != nil {
|
||||
pb.setState(connections.DISCONNECTED)
|
||||
continue
|
||||
}
|
||||
|
||||
log.Debugf("%v open file outfile\n", pb.name)
|
||||
pb.out, err = os.OpenFile(pb.outfile, os.O_RDWR, 0600)
|
||||
if err != nil {
|
||||
pb.setState(connections.DISCONNECTED)
|
||||
continue
|
||||
}
|
||||
|
||||
log.Debugf("Successfully connected PipeBridge %v!\n", pb.name)
|
||||
|
||||
pb.handleConns()
|
||||
}
|
||||
log.Debugf("exiting %v ConnectionManager\n", pb.name)
|
||||
|
||||
}
|
||||
|
||||
// threeShake performs a 3way handshake sync up
|
||||
func (pb *pipeBridge) threeShakeService() bool {
|
||||
synacked := false
|
||||
|
||||
for {
|
||||
resp, err := pb.readString()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if string(resp) == syn {
|
||||
if !synacked {
|
||||
err = pb.writeString([]byte(synack))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
synacked = true
|
||||
}
|
||||
} else if string(resp) == ack {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (pb *pipeBridge) synLoop(stop chan bool) {
|
||||
delay := time.Duration(0)
|
||||
for {
|
||||
select {
|
||||
case <-time.After(delay):
|
||||
err := pb.writeString([]byte(syn))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
delay = time.Second
|
||||
case <-stop:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (pb *pipeBridge) threeShakeClient() bool {
|
||||
stop := make(chan bool)
|
||||
go pb.synLoop(stop)
|
||||
for {
|
||||
resp, err := pb.readString()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if string(resp) == synack {
|
||||
stop <- true
|
||||
err := pb.writeString([]byte(ack))
|
||||
return err == nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (pb *pipeBridge) handleConns() {
|
||||
|
||||
if !pb.threeShake() {
|
||||
pb.setState(connections.FAILED)
|
||||
pb.closeReset()
|
||||
return
|
||||
}
|
||||
|
||||
pb.setState(connections.AUTHENTICATED)
|
||||
|
||||
pb.closedChan = make(chan bool, 5)
|
||||
|
||||
log.Debugf("handleConns authed, %v 2xgo\n", pb.name)
|
||||
|
||||
go pb.handleRead()
|
||||
go pb.handleWrite()
|
||||
|
||||
<-pb.closedChan
|
||||
log.Debugf("handleConns <-closedChan (%v)\n", pb.name)
|
||||
if pb.getState() != connections.KILLED {
|
||||
pb.setState(connections.FAILED)
|
||||
}
|
||||
pb.closeReset()
|
||||
log.Debugf("handleConns done for %v, exit\n", pb.name)
|
||||
}
|
||||
|
||||
func (pb *pipeBridge) closeReset() {
|
||||
pb.in.Close()
|
||||
pb.out.Close()
|
||||
close(pb.read)
|
||||
pb.write.Close()
|
||||
|
||||
if pb.getState() != connections.KILLED {
|
||||
pb.read = make(chan event.IPCMessage, maxBufferSize)
|
||||
pb.write = newInfiniteChannel()
|
||||
}
|
||||
}
|
||||
|
||||
func (pb *pipeBridge) handleWrite() {
|
||||
log.Debugf("handleWrite() %v\n", pb.name)
|
||||
defer log.Debugf("exiting handleWrite() %v\n", pb.name)
|
||||
|
||||
for {
|
||||
select {
|
||||
case messageInf := <-pb.write.output:
|
||||
if messageInf == nil {
|
||||
pb.closedChan <- true
|
||||
return
|
||||
}
|
||||
message := messageInf.(event.IPCMessage)
|
||||
if message.Message.EventType == event.EncryptedGroupMessage || message.Message.EventType == event.SendMessageToGroup || message.Message.EventType == event.NewMessageFromGroup {
|
||||
log.Debugf("handleWrite <- message: %v %v ...\n", message.Dest, message.Message.EventType)
|
||||
} else {
|
||||
log.Debugf("handleWrite <- message: %v\n", message)
|
||||
}
|
||||
if pb.getState() == connections.AUTHENTICATED {
|
||||
encMessage := &event.IPCMessage{Dest: message.Dest, Message: event.Event{EventType: message.Message.EventType, EventID: message.Message.EventID, Data: make(map[event.Field]string)}}
|
||||
for k, v := range message.Message.Data {
|
||||
encMessage.Message.Data[k] = base64.StdEncoding.EncodeToString([]byte(v))
|
||||
}
|
||||
|
||||
messageJSON, _ := json.Marshal(encMessage)
|
||||
err := pb.writeString(messageJSON)
|
||||
if err != nil {
|
||||
pb.closedChan <- true
|
||||
return
|
||||
}
|
||||
} else {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (pb *pipeBridge) handleRead() {
|
||||
log.Debugf("handleRead() %v\n", pb.name)
|
||||
defer log.Debugf("exiting handleRead() %v", pb.name)
|
||||
|
||||
for {
|
||||
log.Debugf("Waiting to handleRead()...\n")
|
||||
|
||||
buffer, err := pb.readString()
|
||||
if err != nil {
|
||||
pb.closedChan <- true
|
||||
return
|
||||
}
|
||||
|
||||
var message event.IPCMessage
|
||||
err = json.Unmarshal(buffer, &message)
|
||||
if err != nil {
|
||||
log.Errorf("Read error: '%v', value: '%v'", err, buffer)
|
||||
pb.closedChan <- true
|
||||
return // probably new connection trying to initialize
|
||||
}
|
||||
for k, v := range message.Message.Data {
|
||||
val, _ := base64.StdEncoding.DecodeString(v)
|
||||
message.Message.Data[k] = string(val)
|
||||
}
|
||||
if message.Message.EventType == event.EncryptedGroupMessage || message.Message.EventType == event.SendMessageToGroup || message.Message.EventType == event.NewMessageFromGroup {
|
||||
log.Debugf("handleRead read<-: %v %v ...\n", message.Dest, message.Message.EventType)
|
||||
} else {
|
||||
log.Debugf("handleRead read<-: %v\n", message)
|
||||
}
|
||||
pb.read <- message
|
||||
log.Debugf("handleRead wrote\n")
|
||||
}
|
||||
}
|
||||
|
||||
func (pb *pipeBridge) Read() (*event.IPCMessage, bool) {
|
||||
log.Debugf("Read() %v...\n", pb.name)
|
||||
var ok = false
|
||||
var message event.IPCMessage
|
||||
for !ok && pb.getState() != connections.KILLED {
|
||||
message, ok = <-pb.read
|
||||
if message.Message.EventType == event.EncryptedGroupMessage || message.Message.EventType == event.SendMessageToGroup || message.Message.EventType == event.NewMessageFromGroup {
|
||||
log.Debugf("Read %v: %v %v ...\n", pb.name, message.Dest, message.Message.EventType)
|
||||
} else {
|
||||
log.Debugf("Read %v: %v\n", pb.name, message)
|
||||
}
|
||||
}
|
||||
return &message, pb.getState() != connections.KILLED
|
||||
}
|
||||
|
||||
func (pb *pipeBridge) Write(message *event.IPCMessage) {
|
||||
if message.Message.EventType == event.EncryptedGroupMessage || message.Message.EventType == event.SendMessageToGroup || message.Message.EventType == event.NewMessageFromGroup {
|
||||
log.Debugf("Write %v: %v %v ...\n", pb.name, message.Dest, message.Message.EventType)
|
||||
} else {
|
||||
log.Debugf("Write %v: %v\n", pb.name, message)
|
||||
}
|
||||
pb.write.input <- *message
|
||||
log.Debugf("Wrote\n")
|
||||
}
|
||||
|
||||
func (pb *pipeBridge) Shutdown() {
|
||||
log.Debugf("pb.Shutdown() for %v currently in state: %v\n", pb.name, connections.ConnectionStateName[pb.getState()])
|
||||
pb.state = connections.KILLED
|
||||
pb.closedChan <- true
|
||||
log.Debugf("Done Shutdown for %v\n", pb.name)
|
||||
}
|
||||
|
||||
func (pb *pipeBridge) writeString(message []byte) error {
|
||||
size := make([]byte, 2)
|
||||
binary.LittleEndian.PutUint16(size, uint16(len(message)))
|
||||
pb.out.Write(size)
|
||||
|
||||
for pos := 0; pos < len(message); {
|
||||
n, err := pb.out.Write(message[pos:])
|
||||
if err != nil {
|
||||
log.Errorf("Writing out on pipeBridge: %v\n", err)
|
||||
return err
|
||||
}
|
||||
pos += n
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (pb *pipeBridge) readString() ([]byte, error) {
|
||||
var n int
|
||||
size := make([]byte, 2)
|
||||
var err error
|
||||
|
||||
n, err = pb.in.Read(size)
|
||||
if err != nil || n != 2 {
|
||||
log.Errorf("Could not read len int from stream: %v\n", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
n = int(binary.LittleEndian.Uint16(size))
|
||||
pos := 0
|
||||
buffer := make([]byte, n)
|
||||
for n > 0 {
|
||||
m, err := pb.in.Read(buffer[pos:])
|
||||
if err != nil {
|
||||
log.Errorf("Reading into buffer from pipe: %v\n", err)
|
||||
return nil, err
|
||||
}
|
||||
n -= m
|
||||
pos += m
|
||||
}
|
||||
return buffer, nil
|
||||
}
|
|
@ -1,131 +0,0 @@
|
|||
package bridge
|
||||
|
||||
import (
|
||||
"cwtch.im/cwtch/event"
|
||||
"git.openprivacy.ca/openprivacy/log"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
clientPipe = "./client"
|
||||
servicePipe = "./service"
|
||||
)
|
||||
|
||||
func clientHelper(t *testing.T, in, out string, messageOrig *event.IPCMessage, done chan bool) {
|
||||
client := NewPipeBridgeClient(in, out)
|
||||
|
||||
messageAfter, ok := client.Read()
|
||||
if !ok {
|
||||
t.Errorf("Reading from client IPCBridge failed")
|
||||
done <- true
|
||||
return
|
||||
}
|
||||
|
||||
if messageOrig.Dest != messageAfter.Dest {
|
||||
t.Errorf("Dest's value differs expected: %v actaul: %v", messageOrig.Dest, messageAfter.Dest)
|
||||
}
|
||||
|
||||
if messageOrig.Message.EventType != messageAfter.Message.EventType {
|
||||
t.Errorf("EventTypes's value differs expected: %v actaul: %v", messageOrig.Message.EventType, messageAfter.Message.EventType)
|
||||
}
|
||||
|
||||
if messageOrig.Message.Data[event.Identity] != messageAfter.Message.Data[event.Identity] {
|
||||
t.Errorf("Data[Identity]'s value differs expected: %v actaul: %v", messageOrig.Message.Data[event.Identity], messageAfter.Message.Data[event.Identity])
|
||||
}
|
||||
|
||||
done <- true
|
||||
}
|
||||
|
||||
func serviceHelper(t *testing.T, in, out string, messageOrig *event.IPCMessage, done chan bool) {
|
||||
service := NewPipeBridgeService(in, out)
|
||||
|
||||
service.Write(messageOrig)
|
||||
|
||||
done <- true
|
||||
}
|
||||
|
||||
func TestPipeBridge(t *testing.T) {
|
||||
os.Remove(servicePipe)
|
||||
os.Remove(clientPipe)
|
||||
|
||||
messageOrig := &event.IPCMessage{Dest: "ABC", Message: event.NewEventList(event.NewPeer, event.Identity, "It is I")}
|
||||
serviceDone := make(chan bool)
|
||||
clientDone := make(chan bool)
|
||||
|
||||
go clientHelper(t, clientPipe, servicePipe, messageOrig, clientDone)
|
||||
go serviceHelper(t, servicePipe, clientPipe, messageOrig, serviceDone)
|
||||
|
||||
<-serviceDone
|
||||
<-clientDone
|
||||
}
|
||||
|
||||
func restartingClient(t *testing.T, in, out string, done chan bool) {
|
||||
client := NewPipeBridgeClient(in, out)
|
||||
|
||||
message1 := &event.IPCMessage{Dest: "ABC", Message: event.NewEventList(event.NewPeer)}
|
||||
log.Infoln("client writing message 1")
|
||||
client.Write(message1)
|
||||
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
log.Infoln("client shutdown")
|
||||
client.Shutdown()
|
||||
|
||||
log.Infoln("client new client")
|
||||
client = NewPipeBridgeClient(in, out)
|
||||
message2 := &event.IPCMessage{Dest: "ABC", Message: event.NewEventList(event.DeleteContact)}
|
||||
log.Infoln("client2 write message2")
|
||||
client.Write(message2)
|
||||
|
||||
done <- true
|
||||
}
|
||||
|
||||
func stableService(t *testing.T, in, out string, done chan bool) {
|
||||
service := NewPipeBridgeService(in, out)
|
||||
|
||||
log.Infoln("service wait read 1")
|
||||
message1, ok := service.Read()
|
||||
log.Infof("service read 1 %v ok:%v\n", message1, ok)
|
||||
if !ok {
|
||||
t.Errorf("Reading from client IPCBridge 1st time failed")
|
||||
done <- true
|
||||
return
|
||||
}
|
||||
if message1.Message.EventType != event.NewPeer {
|
||||
t.Errorf("Wrong message received, expected NewPeer\n")
|
||||
done <- true
|
||||
return
|
||||
}
|
||||
|
||||
log.Infoln("service wait read 2")
|
||||
message2, ok := service.Read()
|
||||
log.Infof("service read 2 got %v ok:%v\n", message2, ok)
|
||||
if !ok {
|
||||
t.Errorf("Reading from client IPCBridge 2nd time failed")
|
||||
done <- true
|
||||
return
|
||||
}
|
||||
if message2.Message.EventType != event.DeleteContact {
|
||||
t.Errorf("Wrong message received, expected DeleteContact, got %v\n", message2)
|
||||
done <- true
|
||||
return
|
||||
}
|
||||
|
||||
done <- true
|
||||
}
|
||||
|
||||
func TestReconnect(t *testing.T) {
|
||||
log.Infoln("TestReconnect")
|
||||
os.Remove(servicePipe)
|
||||
os.Remove(clientPipe)
|
||||
|
||||
serviceDone := make(chan bool)
|
||||
clientDone := make(chan bool)
|
||||
|
||||
go restartingClient(t, clientPipe, servicePipe, clientDone)
|
||||
go stableService(t, servicePipe, clientPipe, serviceDone)
|
||||
|
||||
<-serviceDone
|
||||
<-clientDone
|
||||
}
|
|
@ -79,6 +79,7 @@ const (
|
|||
|
||||
SendMessageToPeer = Type("SendMessageToPeer")
|
||||
NewMessageFromPeer = Type("NewMessageFromPeer")
|
||||
NewMessageFromPeerEngine = Type("NewMessageFromPeerEngine")
|
||||
|
||||
// RemotePeer, scope, path
|
||||
NewGetValMessageFromPeer = Type("NewGetValMessageFromPeer")
|
||||
|
@ -126,8 +127,7 @@ const (
|
|||
// a peer contact has been added
|
||||
// attributes:
|
||||
// RemotePeer [eg ""]
|
||||
// Authorization
|
||||
PeerCreated = Type("PeerCreated")
|
||||
ContactCreated = Type("ContactCreated")
|
||||
|
||||
// Password, NewPassword
|
||||
ChangePassword = Type("ChangePassword")
|
||||
|
@ -253,6 +253,9 @@ const (
|
|||
FileDownloadProgressUpdate = Type("FileDownloadProgressUpdate")
|
||||
FileDownloaded = Type("FileDownloaded")
|
||||
FileVerificationFailed = Type("FileVerificationFailed")
|
||||
|
||||
// Profile Attribute Event
|
||||
UpdatedProfileAttribute = Type("UpdatedProfileAttribute")
|
||||
)
|
||||
|
||||
// Field defines common event attributes
|
||||
|
@ -273,6 +276,7 @@ const (
|
|||
|
||||
Identity = Field("Identity")
|
||||
|
||||
ConversationID = Field("ConversationID")
|
||||
GroupID = Field("GroupID")
|
||||
GroupServer = Field("GroupServer")
|
||||
ServerTokenY = Field("ServerTokenY")
|
||||
|
|
|
@ -62,11 +62,9 @@ type manager struct {
|
|||
}
|
||||
|
||||
// Manager is an interface for an event bus
|
||||
// FIXME this interface lends itself to race conditions around channels
|
||||
type Manager interface {
|
||||
Subscribe(Type, Queue)
|
||||
Publish(Event)
|
||||
PublishLocal(Event)
|
||||
Shutdown()
|
||||
}
|
||||
|
||||
|
@ -123,11 +121,6 @@ func (em *manager) Publish(event Event) {
|
|||
}
|
||||
}
|
||||
|
||||
// Publish an event only locally, not going over an IPC bridge if there is one
|
||||
func (em *manager) PublishLocal(event Event) {
|
||||
em.Publish(event)
|
||||
}
|
||||
|
||||
// eventBus is an internal function that is used to distribute events to all subscribers
|
||||
func (em *manager) eventBus() {
|
||||
for {
|
||||
|
|
|
@ -1,38 +0,0 @@
|
|||
package event
|
||||
|
||||
type ipcManager struct {
|
||||
manager Manager
|
||||
|
||||
onion string
|
||||
ipcBridge IPCBridge
|
||||
}
|
||||
|
||||
// NewIPCEventManager returns an EvenetManager that also pipes events over and supplied IPCBridge
|
||||
func NewIPCEventManager(bridge IPCBridge, onion string) Manager {
|
||||
em := &ipcManager{onion: onion, ipcBridge: bridge, manager: NewEventManager()}
|
||||
return em
|
||||
}
|
||||
|
||||
// IPCEventManagerFrom returns an IPCEventManger from the supplied manager and IPCBridge
|
||||
func IPCEventManagerFrom(bridge IPCBridge, onion string, manager Manager) Manager {
|
||||
em := &ipcManager{onion: onion, ipcBridge: bridge, manager: manager}
|
||||
return em
|
||||
}
|
||||
|
||||
func (ipcm *ipcManager) Publish(ev Event) {
|
||||
ipcm.manager.Publish(ev)
|
||||
message := &IPCMessage{Dest: ipcm.onion, Message: ev}
|
||||
ipcm.ipcBridge.Write(message)
|
||||
}
|
||||
|
||||
func (ipcm *ipcManager) PublishLocal(ev Event) {
|
||||
ipcm.manager.Publish(ev)
|
||||
}
|
||||
|
||||
func (ipcm *ipcManager) Subscribe(eventType Type, queue Queue) {
|
||||
ipcm.manager.Subscribe(eventType, queue)
|
||||
}
|
||||
|
||||
func (ipcm *ipcManager) Shutdown() {
|
||||
ipcm.manager.Shutdown()
|
||||
}
|
14
event/ipc.go
14
event/ipc.go
|
@ -1,14 +0,0 @@
|
|||
package event
|
||||
|
||||
// IPCMessage is a wrapper for a regular eventMessage with a destination (onion|AppDest) so the other side of the bridge can route appropriately
|
||||
type IPCMessage struct {
|
||||
Dest string
|
||||
Message Event
|
||||
}
|
||||
|
||||
// IPCBridge is an interface to a IPC construct used to communicate IPCMessages
|
||||
type IPCBridge interface {
|
||||
Read() (*IPCMessage, bool)
|
||||
Write(message *IPCMessage)
|
||||
Shutdown()
|
||||
}
|
|
@ -25,9 +25,10 @@ import (
|
|||
type Functionality struct {
|
||||
}
|
||||
|
||||
// FunctionalityGate returns contact.Functionality always
|
||||
// FunctionalityGate returns filesharing if enabled in the given experiment map
|
||||
// Note: Experiment maps are currently in libcwtch-go
|
||||
func FunctionalityGate(experimentMap map[string]bool) (*Functionality, error) {
|
||||
if experimentMap["filesharing"] == true {
|
||||
if experimentMap["filesharing"] {
|
||||
return new(Functionality), nil
|
||||
}
|
||||
return nil, errors.New("filesharing is not enabled")
|
||||
|
@ -55,7 +56,8 @@ func (om *OverlayMessage) FileKey() string {
|
|||
|
||||
// DownloadFile given a profile, a conversation handle and a file sharing key, start off a download process
|
||||
// to downloadFilePath
|
||||
func (f *Functionality) DownloadFile(profile peer.CwtchPeer, handle string, downloadFilePath string, manifestFilePath string, key string) {
|
||||
func (f *Functionality) DownloadFile(profile peer.CwtchPeer, conversationID int, downloadFilePath string, manifestFilePath string, key string) {
|
||||
|
||||
// Store local.filesharing.filekey.manifest as the location of the manifest
|
||||
profile.SetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.manifest", key), manifestFilePath)
|
||||
|
||||
|
@ -63,12 +65,12 @@ func (f *Functionality) DownloadFile(profile peer.CwtchPeer, handle string, down
|
|||
profile.SetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%s.path", key), downloadFilePath)
|
||||
|
||||
// Get the value of conversation.filesharing.filekey.manifest.size from `handle`
|
||||
profile.SendScopedZonedGetValToContact(handle, attr.ConversationScope, attr.FilesharingZone, fmt.Sprintf("%s.manifest.size", key))
|
||||
profile.SendScopedZonedGetValToContact(conversationID, attr.ConversationScope, attr.FilesharingZone, fmt.Sprintf("%s.manifest.size", key))
|
||||
}
|
||||
|
||||
// ShareFile given a profile and a conversation handle, sets up a file sharing process to share the file
|
||||
// at filepath
|
||||
func (f *Functionality) ShareFile(filepath string, profile peer.CwtchPeer, handle string) error {
|
||||
func (f *Functionality) ShareFile(filepath string, profile peer.CwtchPeer, conversationID int) error {
|
||||
manifest, err := files.CreateManifest(filepath)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -110,7 +112,7 @@ func (f *Functionality) ShareFile(filepath string, profile peer.CwtchPeer, handl
|
|||
|
||||
profile.ShareFile(key, string(serializedManifest))
|
||||
|
||||
profile.SendMessage(handle, string(wrapperJSON))
|
||||
profile.SendMessage(conversationID, string(wrapperJSON))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
7
go.mod
7
go.mod
|
@ -6,9 +6,12 @@ require (
|
|||
git.openprivacy.ca/cwtch.im/tapir v0.4.9
|
||||
git.openprivacy.ca/openprivacy/connectivity v1.5.0
|
||||
git.openprivacy.ca/openprivacy/log v1.0.3
|
||||
github.com/cucumber/godog v0.12.0
|
||||
github.com/gtank/ristretto255 v0.1.2
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
|
||||
github.com/mutecomm/go-sqlcipher/v4 v4.4.2
|
||||
github.com/onsi/ginkgo v1.16.5 // indirect
|
||||
github.com/onsi/ginkgo/v2 v2.0.0-rc2
|
||||
github.com/onsi/gomega v1.17.0
|
||||
golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
|
||||
)
|
||||
|
|
387
go.sum
387
go.sum
|
@ -1,3 +1,16 @@
|
|||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
|
||||
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
|
||||
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
|
||||
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
|
||||
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
|
||||
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
|
||||
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
|
||||
cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=
|
||||
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
|
||||
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
filippo.io/edwards25519 v1.0.0-rc.1 h1:m0VOOB23frXZvAOK44usCgLWvtsxIoMCTBGJZlpmGfU=
|
||||
filippo.io/edwards25519 v1.0.0-rc.1/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns=
|
||||
git.openprivacy.ca/cwtch.im/tapir v0.4.9 h1:LXonlztwvI1F1++0IyomIcDH1/Bxzo+oN8YjGonNvjM=
|
||||
|
@ -9,51 +22,411 @@ git.openprivacy.ca/openprivacy/connectivity v1.5.0/go.mod h1:UjQiGBnWbotmBzIw59B
|
|||
git.openprivacy.ca/openprivacy/log v1.0.2/go.mod h1:gGYK8xHtndRLDymFtmjkG26GaMQNgyhioNS82m812Iw=
|
||||
git.openprivacy.ca/openprivacy/log v1.0.3 h1:E/PMm4LY+Q9s3aDpfySfEDq/vYQontlvNj/scrPaga0=
|
||||
git.openprivacy.ca/openprivacy/log v1.0.3/go.mod h1:gGYK8xHtndRLDymFtmjkG26GaMQNgyhioNS82m812Iw=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
|
||||
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
|
||||
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
|
||||
github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
|
||||
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||
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/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
|
||||
github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/cucumber/gherkin-go/v19 v19.0.3 h1:mMSKu1077ffLbTJULUfM5HPokgeBcIGboyeNUof1MdE=
|
||||
github.com/cucumber/gherkin-go/v19 v19.0.3/go.mod h1:jY/NP6jUtRSArQQJ5h1FXOUgk5fZK24qtE7vKi776Vw=
|
||||
github.com/cucumber/godog v0.12.0 h1:xVOc9ML+1joT0CqcdQTpfXiT7G1hOLbCmlUnYOyJ80w=
|
||||
github.com/cucumber/godog v0.12.0/go.mod h1:u6SD7IXC49dLpPN35kal0oYEjsXZWee4pW6Tm9t5pIc=
|
||||
github.com/cucumber/messages-go/v16 v16.0.0/go.mod h1:EJcyR5Mm5ZuDsKJnT2N9KRnBK30BGjtYotDKpwQ0v6g=
|
||||
github.com/cucumber/messages-go/v16 v16.0.1 h1:fvkpwsLgnIm0qugftrw2YwNlio+ABe2Iu94Ap8GMYIY=
|
||||
github.com/cucumber/messages-go/v16 v16.0.1/go.mod h1:EJcyR5Mm5ZuDsKJnT2N9KRnBK30BGjtYotDKpwQ0v6g=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
|
||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
|
||||
github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw=
|
||||
github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
|
||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
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 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
|
||||
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
|
||||
github.com/gtank/merlin v0.1.1 h1:eQ90iG7K9pOhtereWsmyRJ6RAwcP4tHTDBHXNg+u5is=
|
||||
github.com/gtank/merlin v0.1.1/go.mod h1:T86dnYJhcGOh5BjZFCJWTDeTK7XW8uE+E21Cy/bIQ+s=
|
||||
github.com/gtank/ristretto255 v0.1.2 h1:JEqUCPA1NvLq5DwYtuzigd7ss8fwbYay9fi4/5uMzcc=
|
||||
github.com/gtank/ristretto255 v0.1.2/go.mod h1:Ph5OpO6c7xKUGROZfWVLiJf9icMDwUeIvY4OmlYW69o=
|
||||
github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
|
||||
github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
|
||||
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
|
||||
github.com/hashicorp/go-immutable-radix v1.3.0 h1:8exGP7ego3OmkfksihtSouGMZ+hQrhxx+FVELeXpVPE=
|
||||
github.com/hashicorp/go-immutable-radix v1.3.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
|
||||
github.com/hashicorp/go-memdb v1.3.0 h1:xdXq34gBOMEloa9rlGStLxmfX/dyIK8htOv36dQUwHU=
|
||||
github.com/hashicorp/go-memdb v1.3.0/go.mod h1:Mluclgwib3R93Hk5fxEfiRhB+6Dar64wWh71LpNSe3g=
|
||||
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
|
||||
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
|
||||
github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
|
||||
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
|
||||
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
|
||||
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go-uuid v1.0.2 h1:cfejS+Tpcp13yd5nYHWDI6qVCny6wyX2Mt5SGur2IGE=
|
||||
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
|
||||
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
|
||||
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
|
||||
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
|
||||
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
|
||||
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/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
||||
github.com/mimoo/StrobeGo v0.0.0-20181016162300-f8f6d4d2b643 h1:hLDRPB66XQT/8+wG9WsDpiCvZf1yKO7sz7scAjSlBa0=
|
||||
github.com/mimoo/StrobeGo v0.0.0-20181016162300-f8f6d4d2b643/go.mod h1:43+3pMjjKimDBf5Kr4ZFNGbLql1zKkbImw+fZbw3geM=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
|
||||
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
|
||||
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
|
||||
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
|
||||
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
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/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
|
||||
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
||||
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
||||
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/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
|
||||
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
|
||||
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
|
||||
github.com/onsi/ginkgo/v2 v2.0.0-rc2 h1:2ukZwTHG/SAlJe4mm5xTdcUYH7IRvldIXhukE1pQBeY=
|
||||
github.com/onsi/ginkgo/v2 v2.0.0-rc2/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c=
|
||||
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 h1:9Luw4uT5HTjHTN8+aNcSThgH1vdXnmdJ8xIfZ4wyTRE=
|
||||
github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
|
||||
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
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/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
|
||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
|
||||
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
|
||||
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
|
||||
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
|
||||
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/spf13/cobra v1.1.1/go.mod h1:WnodtKOvamDL/PwE2M4iKs8aMDBZ5Q5klgD3qfVJQMI=
|
||||
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||
go.etcd.io/bbolt v1.3.4 h1:hi1bXHMVrlQh6WwxAy+qZCV/SYIlqo+Ushwdpa4tAKg=
|
||||
go.etcd.io/bbolt v1.3.4/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
|
||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee h1:4yd7jl+vXjalO5ztz6Vc1VADv+S/80LGJmyl1ROJ2AI=
|
||||
golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
|
||||
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
|
||||
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
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-20210405180319-a5a99cb37ef4 h1:4nGaVu0QrbjT/AK2PRLuQfQuh6DJve+pELhqTdAj3x0=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781 h1:DzZ89McO9/gWPsQXS/FVKAlG02ZjaQ6AlZRBimEYOd0=
|
||||
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/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/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/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-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/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-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/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-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007 h1:gG67DSER+11cZvqIMb8S8bt0vZtiN6xWYARwirrOSfE=
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/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-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
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/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 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
||||
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
|
||||
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
|
||||
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
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 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||
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/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
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.2.8/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 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
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-20200615113413-eeeca48fe776 h1:tQIYjPdBoyREyB9XMu+nnTclpTYkz2zFM+lzLJFO4gQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
package cwtch
|
||||
|
||||
import (
|
||||
"cwtch.im/cwtch/model"
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
import "github.com/cucumber/godog"
|
||||
|
||||
type legacyGroupTest struct {
|
||||
group *model.Group
|
||||
invite string
|
||||
validationError error
|
||||
}
|
||||
|
||||
func (lgt *legacyGroupTest) aGroupOn(server string) error {
|
||||
var err error
|
||||
lgt.group, err = model.NewGroup(server)
|
||||
return err
|
||||
}
|
||||
|
||||
func (lgt *legacyGroupTest) theGroupIDShouldBeCryptographicallyBoundTo(server string) error {
|
||||
if lgt.group.GroupServer != server {
|
||||
return errors.New("group server does not match")
|
||||
}
|
||||
if lgt.group.CheckGroup() == false {
|
||||
return errors.New("group failed cryptographic validation")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (lgt *legacyGroupTest) iGenerateAnInvite() error {
|
||||
var err error
|
||||
lgt.invite, err = lgt.group.Invite()
|
||||
return err
|
||||
}
|
||||
|
||||
func (lgt *legacyGroupTest) theInviteShouldValidate() error {
|
||||
_, err := model.ValidateInvite(lgt.invite)
|
||||
return err
|
||||
}
|
||||
|
||||
func (lgt *legacyGroupTest) iValidateTheInvite(invite string) error {
|
||||
_, err := model.ValidateInvite(invite)
|
||||
lgt.validationError = err
|
||||
return nil
|
||||
}
|
||||
|
||||
func (lgt *legacyGroupTest) iShouldGetAValidationError(expectedError string) error {
|
||||
if lgt.validationError == nil || expectedError != lgt.validationError.Error() {
|
||||
return fmt.Errorf("unexpected validation error: %v", expectedError)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func InitializeScenario(ctx *godog.ScenarioContext) {
|
||||
lgt := legacyGroupTest{}
|
||||
ctx.Step(`^a group on "([^"]*)"$`, lgt.aGroupOn)
|
||||
ctx.Step(`^the GroupID should be cryptographically bound to "([^"]*)"$`, lgt.theGroupIDShouldBeCryptographicallyBoundTo)
|
||||
|
||||
ctx.Step(`^I generate an invite$`, lgt.iGenerateAnInvite)
|
||||
ctx.Step(`^the invite should validate$`, lgt.theInviteShouldValidate)
|
||||
|
||||
ctx.Step(`^I validate the invite "([^"]*)"$`, lgt.iValidateTheInvite)
|
||||
ctx.Step(`^I should get a validation error "([^"]*)"$`, lgt.iShouldGetAValidationError)
|
||||
|
||||
}
|
|
@ -78,21 +78,3 @@ func (scope Scope) IsPublic() bool {
|
|||
func (scope Scope) IsConversation() bool {
|
||||
return scope == ConversationScope
|
||||
}
|
||||
|
||||
// GetLocalScope takes a path and attaches the local scope to it
|
||||
// Deprecated: Use ConstructScopedZonedPath
|
||||
func GetLocalScope(path string) string {
|
||||
return string(LocalScope) + Separator + path
|
||||
}
|
||||
|
||||
// GetPublicScope takes a path and attaches the local scope to it
|
||||
// Deprecated: Use ConstructScopedZonedPath
|
||||
func GetPublicScope(path string) string {
|
||||
return string(PublicScope) + Separator + path
|
||||
}
|
||||
|
||||
// GetPeerScope takes a path and attaches the peer scope to it
|
||||
// Deprecated: Use ConstructScopedZonedPath
|
||||
func GetPeerScope(path string) string {
|
||||
return string(PeerScope) + Separator + path
|
||||
}
|
||||
|
|
|
@ -17,9 +17,18 @@ const (
|
|||
// ProfileZone for attributes related to profile details like name and profile image
|
||||
ProfileZone = Zone("profile")
|
||||
|
||||
// LegacyGroupZone for attributes related to legacy group experiment
|
||||
LegacyGroupZone = Zone("legacygroup")
|
||||
|
||||
// FilesharingZone for attributes related to file sharing
|
||||
FilesharingZone = Zone("filesharing")
|
||||
|
||||
// ServerKeyZone for attributes related to Server Keys
|
||||
ServerKeyZone = Zone("serverkey")
|
||||
|
||||
// ServerZone is for attributes related to the server
|
||||
ServerZone = Zone("server")
|
||||
|
||||
// UnknownZone is a catch all useful for error handling
|
||||
UnknownZone = Zone("unknown")
|
||||
)
|
||||
|
@ -44,8 +53,14 @@ func ParseZone(path string) (Zone, string) {
|
|||
switch Zone(parts[0]) {
|
||||
case ProfileZone:
|
||||
return ProfileZone, parts[1]
|
||||
case LegacyGroupZone:
|
||||
return LegacyGroupZone, parts[1]
|
||||
case FilesharingZone:
|
||||
return FilesharingZone, parts[1]
|
||||
case ServerKeyZone:
|
||||
return ServerKeyZone, parts[1]
|
||||
case ServerZone:
|
||||
return ServerZone, parts[1]
|
||||
default:
|
||||
return UnknownZone, parts[1]
|
||||
}
|
||||
|
|
|
@ -3,6 +3,9 @@ package constants
|
|||
// Name refers to a Profile Name
|
||||
const Name = "name"
|
||||
|
||||
// Onion refers the Onion address of the profile
|
||||
const Onion = "onion"
|
||||
|
||||
// Tag describes the type of a profile e.g. default password / encrypted etc.
|
||||
const Tag = "tag"
|
||||
|
||||
|
@ -11,3 +14,38 @@ const ProfileTypeV1DefaultPassword = "v1-defaultPassword"
|
|||
|
||||
// ProfileTypeV1Password is a tag describing a profile encrypted derived from a user-provided password.
|
||||
const ProfileTypeV1Password = "v1-userPassword"
|
||||
|
||||
// GroupID is the ID of a group
|
||||
const GroupID = "groupid"
|
||||
|
||||
// GroupServer identifies the Server the legacy group is hosted on
|
||||
const GroupServer = "groupserver"
|
||||
|
||||
// GroupKey is the name of the group key attribute...
|
||||
const GroupKey = "groupkey"
|
||||
|
||||
// True - true
|
||||
const True = "true"
|
||||
|
||||
// False - false
|
||||
const False = "false"
|
||||
|
||||
// AttrAuthor - conversation attribute for author of the message - referenced by pub key rather than conversation id because of groups.
|
||||
const AttrAuthor = "author"
|
||||
|
||||
// AttrAck - conversation attribute for acknowledgement status
|
||||
const AttrAck = "ack"
|
||||
|
||||
// AttrErr - conversation attribute for errored status
|
||||
const AttrErr = "error"
|
||||
|
||||
// AttrSentTimestamp - conversation attribute for the time the message was (nominally) sent
|
||||
const AttrSentTimestamp = "sent"
|
||||
|
||||
// Legacy MessageFlags
|
||||
|
||||
// AttrRejected - conversation attribute for storing rejected prompts (for invites)
|
||||
const AttrRejected = "rejected-invite"
|
||||
|
||||
// AttrDownloaded - conversation attribute for storing downloaded prompts (for file downloads)
|
||||
const AttrDownloaded = "file-downloaded"
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
package constants
|
||||
|
||||
// ServerPrefix precedes a server import statement
|
||||
const ServerPrefix = "server:"
|
||||
|
||||
// TofuBundlePrefix precedes a server and a group import statement
|
||||
const TofuBundlePrefix = "tofubundle:"
|
||||
|
||||
// GroupPrefix precedes a group import statement
|
||||
const GroupPrefix = "torv3"
|
||||
|
||||
// ImportBundlePrefix is an error api constant for import bundle error messages
|
||||
const ImportBundlePrefix = "importBundle"
|
|
@ -0,0 +1,96 @@
|
|||
package model
|
||||
|
||||
import (
|
||||
"cwtch.im/cwtch/model/attr"
|
||||
"cwtch.im/cwtch/model/constants"
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
// AccessControl is a type determining client assigned authorization to a peer
|
||||
type AccessControl struct {
|
||||
Blocked bool // Any attempts from this handle to connect are blocked
|
||||
Read bool // Allows a handle to access the conversation
|
||||
Append bool // Allows a handle to append new messages to the conversation
|
||||
}
|
||||
|
||||
// DefaultP2PAccessControl - because in the year 2021, go does not support constant structs...
|
||||
func DefaultP2PAccessControl() AccessControl {
|
||||
return AccessControl{Read: true, Append: true, Blocked: false}
|
||||
}
|
||||
|
||||
// AccessControlList represents an access control list for a conversation. Mapping handles to conversation
|
||||
// functions
|
||||
type AccessControlList map[string]AccessControl
|
||||
|
||||
// Serialize transforms the ACL into json.
|
||||
func (acl *AccessControlList) Serialize() []byte {
|
||||
data, _ := json.Marshal(acl)
|
||||
return data
|
||||
}
|
||||
|
||||
// DeserializeAccessControlList takes in JSON and returns an AccessControlList
|
||||
func DeserializeAccessControlList(data []byte) AccessControlList {
|
||||
var acl AccessControlList
|
||||
json.Unmarshal(data, &acl)
|
||||
return acl
|
||||
}
|
||||
|
||||
// Attributes a type-driven encapsulation of an Attribute map.
|
||||
type Attributes map[string]string
|
||||
|
||||
// Serialize transforms an Attributes map into a JSON struct
|
||||
func (a *Attributes) Serialize() []byte {
|
||||
data, _ := json.Marshal(a)
|
||||
return data
|
||||
}
|
||||
|
||||
// DeserializeAttributes converts a JSON struct into an Attributes map
|
||||
func DeserializeAttributes(data []byte) Attributes {
|
||||
var attributes Attributes
|
||||
json.Unmarshal(data, &attributes)
|
||||
return attributes
|
||||
}
|
||||
|
||||
// Conversation encapsulates high-level information about a conversation, including the
|
||||
// handle, any set attributes, the access control list associated with the message tree and the
|
||||
// accepted status of the conversation (whether the user has consented into the conversation).
|
||||
type Conversation struct {
|
||||
ID int
|
||||
Handle string
|
||||
Attributes Attributes
|
||||
ACL AccessControlList
|
||||
Accepted bool
|
||||
}
|
||||
|
||||
// GetAttribute is a helper function that fetches a conversation attribute by scope, zone and key
|
||||
func (ci *Conversation) GetAttribute(scope attr.Scope, zone attr.Zone, key string) (string, bool) {
|
||||
if value, exists := ci.Attributes[scope.ConstructScopedZonedPath(zone.ConstructZonedPath(key)).ToString()]; exists {
|
||||
return value, true
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
// IsGroup is a helper attribute that identifies whether a conversation is a legacy group
|
||||
func (ci *Conversation) IsGroup() bool {
|
||||
if _, exists := ci.Attributes[attr.LocalScope.ConstructScopedZonedPath(attr.LegacyGroupZone.ConstructZonedPath(constants.GroupID)).ToString()]; exists {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// IsServer is a helper attribute that identifies whether a conversation is with a server
|
||||
func (ci *Conversation) IsServer() bool {
|
||||
if _, exists := ci.Attributes[attr.PublicScope.ConstructScopedZonedPath(attr.ServerKeyZone.ConstructZonedPath(string(BundleType))).ToString()]; exists {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ConversationMessage bundles an instance of a conversation message row
|
||||
type ConversationMessage struct {
|
||||
ID int
|
||||
Body string
|
||||
Attr Attributes
|
||||
Signature string
|
||||
ContentHash string
|
||||
}
|
223
model/group.go
223
model/group.go
|
@ -4,8 +4,6 @@ import (
|
|||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"crypto/sha512"
|
||||
"cwtch.im/cwtch/model/attr"
|
||||
"cwtch.im/cwtch/model/constants"
|
||||
"cwtch.im/cwtch/protocol/groups"
|
||||
"encoding/base32"
|
||||
"encoding/base64"
|
||||
|
@ -13,13 +11,13 @@ import (
|
|||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"git.openprivacy.ca/cwtch.im/tapir/primitives"
|
||||
"git.openprivacy.ca/openprivacy/connectivity/tor"
|
||||
"git.openprivacy.ca/openprivacy/log"
|
||||
"golang.org/x/crypto/nacl/secretbox"
|
||||
"golang.org/x/crypto/pbkdf2"
|
||||
"io"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
|
@ -34,24 +32,18 @@ const GroupInvitePrefix = "torv3"
|
|||
type Group struct {
|
||||
// GroupID is now derived from the GroupKey and the GroupServer
|
||||
GroupID string
|
||||
GroupName string
|
||||
GroupKey [32]byte
|
||||
GroupServer string
|
||||
Timeline Timeline `json:"-"`
|
||||
Accepted bool
|
||||
IsCompromised bool
|
||||
Attributes map[string]string
|
||||
lock sync.Mutex
|
||||
LocalID string
|
||||
State string `json:"-"`
|
||||
Attributes map[string]string //legacy to not use
|
||||
Version int
|
||||
Timeline Timeline `json:"-"`
|
||||
LocalID string
|
||||
}
|
||||
|
||||
// NewGroup initializes a new group associated with a given CwtchServer
|
||||
func NewGroup(server string) (*Group, error) {
|
||||
group := new(Group)
|
||||
group.Version = CurrentGroupVersion
|
||||
group.LocalID = GenerateRandomID()
|
||||
group.Accepted = true // we are starting a group, so we assume we want to connect to it...
|
||||
if !tor.IsValidHostname(server) {
|
||||
return nil, errors.New("server is not a valid v3 onion")
|
||||
}
|
||||
|
@ -68,11 +60,6 @@ func NewGroup(server string) (*Group, error) {
|
|||
// Derive Group ID from the group key and the server public key. This binds the group to a particular server
|
||||
// and key.
|
||||
group.GroupID = deriveGroupID(groupKey[:], server)
|
||||
|
||||
group.Attributes = make(map[string]string)
|
||||
// By default we set the "name" of the group to a random string, we can override this later, but to simplify the
|
||||
// codes around invite, we assume that this is always set.
|
||||
group.Attributes[attr.GetLocalScope(constants.Name)] = group.GroupID
|
||||
return group, nil
|
||||
}
|
||||
|
||||
|
@ -89,17 +76,12 @@ func deriveGroupID(groupKey []byte, serverHostname string) string {
|
|||
return hex.EncodeToString(pbkdf2.Key(groupKey, pubkey, 4096, 16, sha512.New))
|
||||
}
|
||||
|
||||
// Compromised should be called if we detect a groupkey leak
|
||||
func (g *Group) Compromised() {
|
||||
g.IsCompromised = true
|
||||
}
|
||||
|
||||
// Invite generates a invitation that can be sent to a cwtch peer
|
||||
func (g *Group) Invite() (string, error) {
|
||||
|
||||
gci := &groups.GroupInvite{
|
||||
GroupID: g.GroupID,
|
||||
GroupName: g.Attributes[attr.GetLocalScope(constants.Name)],
|
||||
GroupName: g.GroupName,
|
||||
SharedKey: g.GroupKey[:],
|
||||
ServerHost: g.GroupServer,
|
||||
}
|
||||
|
@ -109,75 +91,6 @@ func (g *Group) Invite() (string, error) {
|
|||
return serializedInvite, err
|
||||
}
|
||||
|
||||
// AddSentMessage takes a DecryptedGroupMessage and adds it to the Groups Timeline
|
||||
func (g *Group) AddSentMessage(message *groups.DecryptedGroupMessage, sig []byte) Message {
|
||||
g.lock.Lock()
|
||||
defer g.lock.Unlock()
|
||||
timelineMessage := Message{
|
||||
Message: message.Text,
|
||||
Timestamp: time.Unix(int64(message.Timestamp), 0),
|
||||
Received: time.Unix(0, 0),
|
||||
Signature: sig,
|
||||
PeerID: message.Onion,
|
||||
PreviousMessageSig: message.PreviousMessageSig,
|
||||
ReceivedByServer: false,
|
||||
}
|
||||
g.Timeline.Insert(&timelineMessage)
|
||||
return timelineMessage
|
||||
}
|
||||
|
||||
// ErrorSentMessage removes a sent message from the unacknowledged list and sets its error flag if found, otherwise returns false
|
||||
func (g *Group) ErrorSentMessage(sig []byte, error string) bool {
|
||||
g.lock.Lock()
|
||||
defer g.lock.Unlock()
|
||||
|
||||
return g.Timeline.SetSendError(sig, error)
|
||||
}
|
||||
|
||||
// GetMessage returns the message at index `index` if it exists. Otherwise returns false.
|
||||
// This routine also returns the length of the timeline
|
||||
// If go has an optional type this would return Option<Message>...
|
||||
func (g *Group) GetMessage(index int) (bool, Message, int) {
|
||||
g.lock.Lock()
|
||||
defer g.lock.Unlock()
|
||||
|
||||
length := len(g.Timeline.Messages)
|
||||
|
||||
if length > index {
|
||||
return true, g.Timeline.Messages[index], length
|
||||
}
|
||||
return false, Message{}, length
|
||||
}
|
||||
|
||||
// AddMessage takes a DecryptedGroupMessage and adds it to the Groups Timeline
|
||||
func (g *Group) AddMessage(message *groups.DecryptedGroupMessage, sig []byte) (*Message, int) {
|
||||
|
||||
g.lock.Lock()
|
||||
defer g.lock.Unlock()
|
||||
|
||||
timelineMessage := &Message{
|
||||
Message: message.Text,
|
||||
Timestamp: time.Unix(int64(message.Timestamp), 0),
|
||||
Received: time.Now(),
|
||||
Signature: sig,
|
||||
PeerID: message.Onion,
|
||||
PreviousMessageSig: message.PreviousMessageSig,
|
||||
ReceivedByServer: true,
|
||||
Error: "",
|
||||
Acknowledged: true,
|
||||
}
|
||||
index := g.Timeline.Insert(timelineMessage)
|
||||
|
||||
return timelineMessage, index
|
||||
}
|
||||
|
||||
// GetTimeline provides a safe copy of the timeline
|
||||
func (g *Group) GetTimeline() (timeline []Message) {
|
||||
g.lock.Lock()
|
||||
defer g.lock.Unlock()
|
||||
return g.Timeline.GetMessages()
|
||||
}
|
||||
|
||||
//EncryptMessage takes a message and encrypts the message under the group key.
|
||||
func (g *Group) EncryptMessage(message *groups.DecryptedGroupMessage) ([]byte, error) {
|
||||
var nonce [24]byte
|
||||
|
@ -211,21 +124,6 @@ func (g *Group) DecryptMessage(ciphertext []byte) (bool, *groups.DecryptedGroupM
|
|||
return false, nil
|
||||
}
|
||||
|
||||
// SetAttribute allows applications to store arbitrary configuration info at the group level.
|
||||
func (g *Group) SetAttribute(name string, value string) {
|
||||
g.lock.Lock()
|
||||
defer g.lock.Unlock()
|
||||
g.Attributes[name] = value
|
||||
}
|
||||
|
||||
// GetAttribute returns the value of a value set with SetAttribute. If no such value has been set exists is set to false.
|
||||
func (g *Group) GetAttribute(name string) (value string, exists bool) {
|
||||
g.lock.Lock()
|
||||
defer g.lock.Unlock()
|
||||
value, exists = g.Attributes[name]
|
||||
return
|
||||
}
|
||||
|
||||
// ValidateInvite takes in a serialized invite and returns the invite structure if it is cryptographically valid
|
||||
// and an error if it is not
|
||||
func ValidateInvite(invite string) (*groups.GroupInvite, error) {
|
||||
|
@ -263,3 +161,112 @@ func ValidateInvite(invite string) (*groups.GroupInvite, error) {
|
|||
}
|
||||
return nil, errors.New("invite has invalid structure")
|
||||
}
|
||||
|
||||
// AttemptDecryption takes a ciphertext and signature and attempts to decrypt it under known groups.
|
||||
// If successful, adds the message to the group's timeline
|
||||
func (g *Group) AttemptDecryption(ciphertext []byte, signature []byte) (bool, *groups.DecryptedGroupMessage) {
|
||||
success, dgm := g.DecryptMessage(ciphertext)
|
||||
if success {
|
||||
|
||||
// Attempt to serialize this message
|
||||
serialized, err := json.Marshal(dgm)
|
||||
|
||||
// Someone send a message that isn't a valid Decrypted Group Message. Since we require this struct in orer
|
||||
// to verify the message, we simply ignore it.
|
||||
if err != nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// This now requires knowledge of the Sender, the Onion and the Specific Decrypted Group Message (which should only
|
||||
// be derivable from the cryptographic key) which contains many unique elements such as the time and random padding
|
||||
verified := g.VerifyGroupMessage(dgm.Onion, g.GroupID, base64.StdEncoding.EncodeToString(serialized), signature)
|
||||
|
||||
if !verified {
|
||||
// An earlier version of this protocol mistakenly signed the ciphertext of the message
|
||||
// instead of the serialized decrypted group message.
|
||||
// This has 2 issues:
|
||||
// 1. A server with knowledge of group members public keys AND the Group ID would be able to detect valid messages
|
||||
// 2. It made the metadata-security of a group dependent on keeping the cryptographically derived Group ID secret.
|
||||
// While not awful, it also isn't good. For Version 3 groups only we permit Cwtch to check this older signature
|
||||
// structure in a backwards compatible way for the duration of the Groups Experiment.
|
||||
// TODO: Delete this check when Groups are no long Experimental
|
||||
if g.Version == 3 {
|
||||
verified = g.VerifyGroupMessage(dgm.Onion, g.GroupID, string(ciphertext), signature)
|
||||
}
|
||||
}
|
||||
|
||||
// So we have a message that has a valid group key, but the signature can't be verified.
|
||||
// The most obvious explanation for this is that the group key has been compromised (or we are in an open group and the server is being malicious)
|
||||
// Either way, someone who has the private key is being detectably bad so we are just going to throw this message away and mark the group as Compromised.
|
||||
if !verified {
|
||||
return false, nil
|
||||
}
|
||||
return true, dgm
|
||||
}
|
||||
|
||||
// If we couldn't find a group to decrypt the message with we just return false. This is an expected case
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// VerifyGroupMessage confirms the authenticity of a message given an sender onion, message and signature.
|
||||
// The goal of this function is 2-fold:
|
||||
// 1. We confirm that the sender referenced in the group text is the actual sender of the message (or at least
|
||||
// knows the senders private key)
|
||||
// 2. Secondly, we confirm that the sender sent the message to a particular group id on a specific server (it doesn't
|
||||
// matter if we actually received this message from the server or from a hybrid protocol, all that matters is
|
||||
// that the sender and receivers agree that this message was intended for the group
|
||||
// The 2nd point is important as it prevents an attack documented in the original Cwtch paper (and later at
|
||||
// https://docs.openprivacy.ca/cwtch-security-handbook/groups.html) in which a malicious profile sets up 2 groups
|
||||
// on two different servers with the same key and then forwards messages between them to convince the parties in
|
||||
// each group that they are actually in one big group (with the intent to later censor and/or selectively send messages
|
||||
// to each group).
|
||||
func (g *Group) VerifyGroupMessage(onion string, groupID string, message string, signature []byte) bool {
|
||||
// We use our group id, a known reference server and the ciphertext of the message.
|
||||
m := groupID + g.GroupServer + message
|
||||
|
||||
// Otherwise we derive the public key from the sender and check it against that.
|
||||
decodedPub, err := base32.StdEncoding.DecodeString(strings.ToUpper(onion))
|
||||
if err == nil && len(decodedPub) >= 32 {
|
||||
return ed25519.Verify(decodedPub[:32], []byte(m), signature)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// EncryptMessageToGroup when given a message and a group, encrypts and signs the message under the group and
|
||||
// profile
|
||||
func EncryptMessageToGroup(message string, author primitives.Identity, group *Group, prevSig string) ([]byte, []byte, *groups.DecryptedGroupMessage, error) {
|
||||
if len(message) > MaxGroupMessageLength {
|
||||
return nil, nil, nil, errors.New("group message is too long")
|
||||
}
|
||||
timestamp := time.Now().Unix()
|
||||
|
||||
lenPadding := MaxGroupMessageLength - len(message)
|
||||
padding := make([]byte, lenPadding)
|
||||
getRandomness(&padding)
|
||||
hexGroupID, err := hex.DecodeString(group.GroupID)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
prevSigBytes, err := base64.StdEncoding.DecodeString(prevSig)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
dm := &groups.DecryptedGroupMessage{
|
||||
Onion: author.Hostname(),
|
||||
Text: message,
|
||||
SignedGroupID: hexGroupID,
|
||||
Timestamp: uint64(timestamp),
|
||||
PreviousMessageSig: prevSigBytes,
|
||||
Padding: padding[:],
|
||||
}
|
||||
|
||||
ciphertext, err := group.EncryptMessage(dm)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
serialized, _ := json.Marshal(dm)
|
||||
signature := author.Sign([]byte(group.GroupID + group.GroupServer + base64.StdEncoding.EncodeToString(serialized)))
|
||||
return ciphertext, signature, dm, nil
|
||||
}
|
||||
|
|
|
@ -4,7 +4,6 @@ import (
|
|||
"crypto/sha256"
|
||||
"cwtch.im/cwtch/protocol/groups"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
@ -42,11 +41,7 @@ func TestGroup(t *testing.T) {
|
|||
t.Errorf("group encryption was invalid, or returned wrong message decrypted:%v message:%v", ok, message)
|
||||
return
|
||||
}
|
||||
g.SetAttribute("test", "test_value")
|
||||
value, exists := g.GetAttribute("test")
|
||||
if !exists || value != "test_value" {
|
||||
t.Errorf("Custom Attribute Should have been set, instead %v %v", exists, value)
|
||||
}
|
||||
|
||||
t.Logf("Got message %v", message)
|
||||
}
|
||||
|
||||
|
@ -65,12 +60,7 @@ func TestGroupValidation(t *testing.T) {
|
|||
GroupKey: [32]byte{},
|
||||
GroupServer: "",
|
||||
Timeline: Timeline{},
|
||||
Accepted: false,
|
||||
IsCompromised: false,
|
||||
Attributes: nil,
|
||||
lock: sync.Mutex{},
|
||||
LocalID: "",
|
||||
State: "",
|
||||
Version: 0,
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,110 @@
|
|||
package model_test
|
||||
|
||||
import (
|
||||
"cwtch.im/cwtch/model"
|
||||
"cwtch.im/cwtch/protocol/groups"
|
||||
"encoding/base64"
|
||||
"git.openprivacy.ca/cwtch.im/tapir/primitives"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("group models", func() {
|
||||
var (
|
||||
newgroup *model.Group
|
||||
anothergroup *model.Group
|
||||
dgm groups.DecryptedGroupMessage
|
||||
alice primitives.Identity
|
||||
)
|
||||
|
||||
BeforeEach(func() {
|
||||
newgroup, _ = model.NewGroup("iikv7tizbyxc42rsagnjxss65h3nfiwrkkoiikh7ui27r5xkav7gzuid")
|
||||
anothergroup, _ = model.NewGroup("iikv7tizbyxc42rsagnjxss65h3nfiwrkkoiikh7ui27r5xkav7gzuid")
|
||||
|
||||
alice, _ = primitives.InitializeEphemeralIdentity()
|
||||
|
||||
dgm = groups.DecryptedGroupMessage{
|
||||
Text: "hello world",
|
||||
Onion: "some random onion",
|
||||
Timestamp: 0,
|
||||
SignedGroupID: nil,
|
||||
PreviousMessageSig: nil,
|
||||
Padding: nil,
|
||||
}
|
||||
})
|
||||
|
||||
Context("on creation of a group", func() {
|
||||
It("should pass the cryptographic check", func() {
|
||||
Expect(newgroup.CheckGroup()).To(Equal(true))
|
||||
})
|
||||
})
|
||||
|
||||
Context("after generating an invite", func() {
|
||||
It("should validate", func() {
|
||||
invite, err := newgroup.Invite()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
anotherGroup, err := model.ValidateInvite(invite)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
Expect(anotherGroup.GroupID).To(Equal(newgroup.GroupID))
|
||||
Expect(anotherGroup.GroupName).To(Equal(newgroup.GroupName))
|
||||
Expect(anotherGroup.SharedKey).To(Equal(newgroup.GroupKey[:]))
|
||||
})
|
||||
})
|
||||
|
||||
Context("when encrypting a message", func() {
|
||||
Context("decrypting with the same group", func() {
|
||||
It("should succeed", func() {
|
||||
ciphertext, err := newgroup.EncryptMessage(&dgm)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
success, decryptedMessage := newgroup.DecryptMessage(ciphertext)
|
||||
Expect(success).To(Equal(true))
|
||||
Expect(decryptedMessage.Text).To(Equal(dgm.Text))
|
||||
Expect(decryptedMessage.Onion).To(Equal(dgm.Onion))
|
||||
})
|
||||
})
|
||||
|
||||
Context("decrypting with a different group", func() {
|
||||
It("should fail", func() {
|
||||
ciphertext, err := newgroup.EncryptMessage(&dgm)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
success, decryptedMessage := anothergroup.DecryptMessage(ciphertext)
|
||||
Expect(success).To(Equal(false))
|
||||
Expect(decryptedMessage).To(BeNil())
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Context("when alice encrypts a message to new group", func() {
|
||||
It("should succeed and bob should succeed in decrypting it", func() {
|
||||
ciphertext, sign, _, err := model.EncryptMessageToGroup("hello world", alice, newgroup, base64.StdEncoding.EncodeToString([]byte("hello world")))
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
success, dgm := newgroup.AttemptDecryption(ciphertext, sign)
|
||||
Expect(success).To(BeTrue())
|
||||
Expect(dgm.Text).To(Equal("hello world"))
|
||||
})
|
||||
})
|
||||
|
||||
Context("when alice encrypts a message to new group", func() {
|
||||
It("should succeed and eve should fail in decrypting it", func() {
|
||||
ciphertext, sign, _, err := model.EncryptMessageToGroup("hello world", alice, newgroup, base64.StdEncoding.EncodeToString([]byte("hello world")))
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
success, dgm := anothergroup.AttemptDecryption(ciphertext, sign)
|
||||
Expect(success).To(BeFalse())
|
||||
Expect(dgm).To(BeNil())
|
||||
})
|
||||
})
|
||||
|
||||
Context("when alice encrypts a message to new group", func() {
|
||||
Context("and the server messes with the signature", func() {
|
||||
It("bob should be unable to verify the message with the wrong signature", func() {
|
||||
ciphertext, _, _, err := model.EncryptMessageToGroup("hello world", alice, newgroup, base64.StdEncoding.EncodeToString([]byte("hello world")))
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
success, dgm := newgroup.AttemptDecryption(ciphertext, []byte("bad signature"))
|
||||
Expect(success).To(BeFalse())
|
||||
Expect(dgm).To(BeNil())
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
})
|
|
@ -1,127 +0,0 @@
|
|||
package model
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestMessagePadding(t *testing.T) {
|
||||
|
||||
// Setup the Group
|
||||
sarah := GenerateNewProfile("Sarah")
|
||||
alice := GenerateNewProfile("Alice")
|
||||
sarah.AddContact(alice.Onion, &alice.PublicProfile)
|
||||
alice.AddContact(sarah.Onion, &sarah.PublicProfile)
|
||||
|
||||
gid, invite, _ := alice.StartGroup("2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd")
|
||||
|
||||
sarah.ProcessInvite(invite)
|
||||
|
||||
group := alice.GetGroup(gid)
|
||||
|
||||
c1, s1, err := sarah.EncryptMessageToGroup("Hello World 1", group.GroupID)
|
||||
t.Logf("Length of Encrypted Message: %v %v", len(c1), err)
|
||||
alice.AttemptDecryption(c1, s1)
|
||||
|
||||
c2, s2, _ := alice.EncryptMessageToGroup("Hello World 2", group.GroupID)
|
||||
t.Logf("Length of Encrypted Message: %v", len(c2))
|
||||
alice.AttemptDecryption(c2, s2)
|
||||
|
||||
c3, s3, _ := alice.EncryptMessageToGroup("Hello World 3", group.GroupID)
|
||||
t.Logf("Length of Encrypted Message: %v", len(c3))
|
||||
alice.AttemptDecryption(c3, s3)
|
||||
|
||||
c4, s4, _ := alice.EncryptMessageToGroup("Hello World this is a much longer message 3", group.GroupID)
|
||||
t.Logf("Length of Encrypted Message: %v", len(c4))
|
||||
alice.AttemptDecryption(c4, s4)
|
||||
|
||||
}
|
||||
|
||||
func TestTranscriptConsistency(t *testing.T) {
|
||||
timeline := new(Timeline)
|
||||
|
||||
// Setup the Group
|
||||
sarah := GenerateNewProfile("Sarah")
|
||||
alice := GenerateNewProfile("Alice")
|
||||
sarah.AddContact(alice.Onion, &alice.PublicProfile)
|
||||
alice.AddContact(sarah.Onion, &sarah.PublicProfile)
|
||||
|
||||
// The lightest weight server entry possible (usually we would import a key bundle...)
|
||||
sarah.AddContact("2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd", &PublicProfile{Attributes: map[string]string{string(KeyTypeServerOnion): "2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd"}})
|
||||
|
||||
gid, invite, _ := alice.StartGroup("2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd")
|
||||
|
||||
sarah.ProcessInvite(invite)
|
||||
|
||||
group := alice.GetGroup(gid)
|
||||
|
||||
t.Logf("group: %v, sarah %v", group, sarah)
|
||||
|
||||
c1, s1, _ := alice.EncryptMessageToGroup("Hello World 1", group.GroupID)
|
||||
t.Logf("Length of Encrypted Message: %v", len(c1))
|
||||
alice.AttemptDecryption(c1, s1)
|
||||
|
||||
c2, s2, _ := alice.EncryptMessageToGroup("Hello World 2", group.GroupID)
|
||||
t.Logf("Length of Encrypted Message: %v", len(c2))
|
||||
alice.AttemptDecryption(c2, s2)
|
||||
|
||||
c3, s3, _ := alice.EncryptMessageToGroup("Hello World 3", group.GroupID)
|
||||
t.Logf("Length of Encrypted Message: %v", len(c3))
|
||||
alice.AttemptDecryption(c3, s3)
|
||||
|
||||
time.Sleep(time.Second * 1)
|
||||
|
||||
c4, s4, _ := alice.EncryptMessageToGroup("Hello World 4", group.GroupID)
|
||||
t.Logf("Length of Encrypted Message: %v", len(c4))
|
||||
alice.AttemptDecryption(c4, s4)
|
||||
|
||||
c5, s5, _ := alice.EncryptMessageToGroup("Hello World 5", group.GroupID)
|
||||
t.Logf("Length of Encrypted Message: %v", len(c5))
|
||||
|
||||
_, _, m1, _ := sarah.AttemptDecryption(c1, s1)
|
||||
sarah.AttemptDecryption(c1, s1) // Try a duplicate
|
||||
_, _, m2, _ := sarah.AttemptDecryption(c2, s2)
|
||||
_, _, m3, _ := sarah.AttemptDecryption(c3, s3)
|
||||
_, _, m4, _ := sarah.AttemptDecryption(c4, s4)
|
||||
_, _, m5, _ := sarah.AttemptDecryption(c5, s5)
|
||||
|
||||
// Now we simulate a client receiving these Messages completely out of order
|
||||
timeline.Insert(m1)
|
||||
timeline.Insert(m5)
|
||||
timeline.Insert(m4)
|
||||
timeline.Insert(m3)
|
||||
timeline.Insert(m2)
|
||||
|
||||
for i, m := range group.GetTimeline() {
|
||||
if m.Message != "Hello World "+strconv.Itoa(i+1) {
|
||||
t.Fatalf("Timeline Out of Order!: %v %v", i, m)
|
||||
}
|
||||
|
||||
t.Logf("Messages %v: %v %x %x", i, m.Message, m.Signature, m.PreviousMessageSig)
|
||||
}
|
||||
|
||||
// Test message by hash lookup...
|
||||
hash := timeline.calculateHash(*m5)
|
||||
|
||||
t.Logf("Looking up %v ", hash)
|
||||
|
||||
for key, msgs := range timeline.hashCache {
|
||||
t.Logf("%v %v", key, msgs)
|
||||
}
|
||||
|
||||
// check a real message..
|
||||
msgs, err := timeline.GetMessagesByHash(hash)
|
||||
if err != nil || len(msgs) != 1 {
|
||||
t.Fatalf("looking up message by hash %v should have not errored: %v", hash, err)
|
||||
} else if msgs[0].Message.Message != m5.Message {
|
||||
t.Fatalf("%v != %v", msgs[0].Message, m5.Message)
|
||||
}
|
||||
|
||||
// Check a non existed hash... error if there is no error
|
||||
_, err = timeline.GetMessagesByHash("not a real hash")
|
||||
if err == nil {
|
||||
t.Fatalf("looking up message by hash %v should have errored: %v", hash, err)
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
package model
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
)
|
||||
|
||||
// CalculateContentHash derives a hash using the author and the message body. It is intended to be
|
||||
// globally referencable in the context of a single conversation
|
||||
func CalculateContentHash(author string, messageBody string) string {
|
||||
content := []byte(author + messageBody)
|
||||
contentBasedHash := sha256.Sum256(content)
|
||||
return base64.StdEncoding.EncodeToString(contentBasedHash[:])
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
package model_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
func TestModel(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "Model Suite")
|
||||
}
|
479
model/profile.go
479
model/profile.go
|
@ -2,25 +2,16 @@ package model
|
|||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"cwtch.im/cwtch/model/attr"
|
||||
"cwtch.im/cwtch/model/constants"
|
||||
"cwtch.im/cwtch/protocol/groups"
|
||||
"encoding/base32"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"git.openprivacy.ca/openprivacy/connectivity/tor"
|
||||
"golang.org/x/crypto/ed25519"
|
||||
"io"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Authorization is a type determining client assigned authorization to a peer
|
||||
// Deprecated - Only used for Importing legacy profile formats
|
||||
// Still used in some APIs in UI but will be replaced prior to full deprecation
|
||||
type Authorization string
|
||||
|
||||
const (
|
||||
|
@ -33,6 +24,7 @@ const (
|
|||
)
|
||||
|
||||
// PublicProfile is a local copy of a CwtchIdentity
|
||||
// Deprecated - Only used for Importing legacy profile formats
|
||||
type PublicProfile struct {
|
||||
Name string
|
||||
Ed25519PublicKey ed25519.PublicKey
|
||||
|
@ -48,6 +40,7 @@ type PublicProfile struct {
|
|||
}
|
||||
|
||||
// Profile encapsulates all the attributes necessary to be a Cwtch Peer.
|
||||
// Deprecated - Only used for Importing legacy profile formats
|
||||
type Profile struct {
|
||||
PublicProfile
|
||||
Contacts map[string]*PublicProfile
|
||||
|
@ -59,418 +52,6 @@ type Profile struct {
|
|||
// TODO: Should this be per server?
|
||||
const MaxGroupMessageLength = 1800
|
||||
|
||||
// GenerateRandomID generates a random 16 byte hex id code
|
||||
func GenerateRandomID() string {
|
||||
randBytes := make([]byte, 16)
|
||||
rand.Read(randBytes)
|
||||
return filepath.Join(hex.EncodeToString(randBytes))
|
||||
}
|
||||
|
||||
func (p *PublicProfile) init() {
|
||||
if p.Attributes == nil {
|
||||
p.Attributes = make(map[string]string)
|
||||
}
|
||||
p.UnacknowledgedMessages = make(map[string]int)
|
||||
p.LocalID = GenerateRandomID()
|
||||
}
|
||||
|
||||
// SetAttribute allows applications to store arbitrary configuration info at the profile level.
|
||||
func (p *PublicProfile) SetAttribute(name string, value string) {
|
||||
p.lock.Lock()
|
||||
defer p.lock.Unlock()
|
||||
p.Attributes[name] = value
|
||||
}
|
||||
|
||||
// IsServer returns true if the profile is associated with a server.
|
||||
func (p *PublicProfile) IsServer() (isServer bool) {
|
||||
_, isServer = p.GetAttribute(string(KeyTypeServerOnion))
|
||||
return
|
||||
}
|
||||
|
||||
// GetAttribute returns the value of a value set with SetCustomAttribute. If no such value has been set exists is set to false.
|
||||
func (p *PublicProfile) GetAttribute(name string) (value string, exists bool) {
|
||||
p.lock.Lock()
|
||||
defer p.lock.Unlock()
|
||||
value, exists = p.Attributes[name]
|
||||
return
|
||||
}
|
||||
|
||||
// GenerateNewProfile creates a new profile, with new encryption and signing keys, and a profile name.
|
||||
func GenerateNewProfile(name string) *Profile {
|
||||
p := new(Profile)
|
||||
p.init()
|
||||
p.Name = name
|
||||
pub, priv, _ := ed25519.GenerateKey(rand.Reader)
|
||||
p.Ed25519PublicKey = pub
|
||||
p.Ed25519PrivateKey = priv
|
||||
p.Onion = tor.GetTorV3Hostname(pub)
|
||||
|
||||
p.Contacts = make(map[string]*PublicProfile)
|
||||
p.Contacts[p.Onion] = &p.PublicProfile
|
||||
p.Groups = make(map[string]*Group)
|
||||
return p
|
||||
}
|
||||
|
||||
// AddContact allows direct manipulation of cwtch contacts
|
||||
func (p *Profile) AddContact(onion string, profile *PublicProfile) {
|
||||
p.lock.Lock()
|
||||
profile.init()
|
||||
// We expect callers to verify addresses before we get to this point, so if this isn't a
|
||||
// valid address this is a noop.
|
||||
if tor.IsValidHostname(onion) {
|
||||
decodedPub, err := base32.StdEncoding.DecodeString(strings.ToUpper(onion[:56]))
|
||||
if err == nil {
|
||||
profile.Ed25519PublicKey = ed25519.PublicKey(decodedPub[:32])
|
||||
p.Contacts[onion] = profile
|
||||
}
|
||||
}
|
||||
p.lock.Unlock()
|
||||
}
|
||||
|
||||
// UpdateMessageFlags updates the flags stored with a message
|
||||
func (p *Profile) UpdateMessageFlags(handle string, mIdx int, flags uint64) {
|
||||
p.lock.Lock()
|
||||
defer p.lock.Unlock()
|
||||
if contact, exists := p.Contacts[handle]; exists {
|
||||
if len(contact.Timeline.Messages) > mIdx {
|
||||
contact.Timeline.Messages[mIdx].Flags = flags
|
||||
}
|
||||
} else if group, exists := p.Groups[handle]; exists {
|
||||
if len(group.Timeline.Messages) > mIdx {
|
||||
group.Timeline.Messages[mIdx].Flags = flags
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeleteContact deletes a peer contact
|
||||
func (p *Profile) DeleteContact(onion string) {
|
||||
p.lock.Lock()
|
||||
defer p.lock.Unlock()
|
||||
delete(p.Contacts, onion)
|
||||
}
|
||||
|
||||
// DeleteGroup deletes a group
|
||||
func (p *Profile) DeleteGroup(groupID string) {
|
||||
p.lock.Lock()
|
||||
defer p.lock.Unlock()
|
||||
delete(p.Groups, groupID)
|
||||
}
|
||||
|
||||
// RejectInvite rejects and removes a group invite
|
||||
func (p *Profile) RejectInvite(groupID string) {
|
||||
p.lock.Lock()
|
||||
delete(p.Groups, groupID)
|
||||
p.lock.Unlock()
|
||||
}
|
||||
|
||||
// AddSentMessageToContactTimeline allows the saving of a message sent via a direct connection chat to the profile.
|
||||
func (p *Profile) AddSentMessageToContactTimeline(onion string, messageTxt string, sent time.Time, eventID string) *Message {
|
||||
p.lock.Lock()
|
||||
defer p.lock.Unlock()
|
||||
|
||||
contact, ok := p.Contacts[onion]
|
||||
if ok {
|
||||
now := time.Now()
|
||||
sig := p.SignMessage(onion + messageTxt + sent.String() + now.String())
|
||||
|
||||
message := &Message{PeerID: p.Onion, Message: messageTxt, Timestamp: sent, Received: now, Signature: sig, Acknowledged: false}
|
||||
if contact.UnacknowledgedMessages == nil {
|
||||
contact.UnacknowledgedMessages = make(map[string]int)
|
||||
}
|
||||
contact.Timeline.Insert(message)
|
||||
contact.UnacknowledgedMessages[eventID] = contact.Timeline.Len() - 1
|
||||
return message
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddMessageToContactTimeline allows the saving of a message sent via a direct connection chat to the profile.
|
||||
func (p *Profile) AddMessageToContactTimeline(onion string, messageTxt string, sent time.Time) (message *Message) {
|
||||
p.lock.Lock()
|
||||
defer p.lock.Unlock()
|
||||
contact, ok := p.Contacts[onion]
|
||||
|
||||
// We don't really need a Signature here, but we use it to maintain order
|
||||
now := time.Now()
|
||||
sig := p.SignMessage(onion + messageTxt + sent.String() + now.String())
|
||||
if ok {
|
||||
message = &Message{PeerID: onion, Message: messageTxt, Timestamp: sent, Received: now, Signature: sig, Acknowledged: true}
|
||||
contact.Timeline.Insert(message)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// ErrorSentMessageToPeer sets a sent message's error message and removes it from the unacknowledged list
|
||||
func (p *Profile) ErrorSentMessageToPeer(onion string, eventID string, error string) int {
|
||||
p.lock.Lock()
|
||||
defer p.lock.Unlock()
|
||||
|
||||
contact, ok := p.Contacts[onion]
|
||||
if ok {
|
||||
mIdx, ok := contact.UnacknowledgedMessages[eventID]
|
||||
if ok {
|
||||
contact.Timeline.Messages[mIdx].Error = error
|
||||
delete(contact.UnacknowledgedMessages, eventID)
|
||||
return mIdx
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// AckSentMessageToPeer sets mesage to a peer as acknowledged
|
||||
func (p *Profile) AckSentMessageToPeer(onion string, eventID string) int {
|
||||
p.lock.Lock()
|
||||
defer p.lock.Unlock()
|
||||
|
||||
contact, ok := p.Contacts[onion]
|
||||
if ok {
|
||||
mIdx, ok := contact.UnacknowledgedMessages[eventID]
|
||||
if ok {
|
||||
contact.Timeline.Messages[mIdx].Acknowledged = true
|
||||
delete(contact.UnacknowledgedMessages, eventID)
|
||||
return mIdx
|
||||
}
|
||||
}
|
||||
|
||||
return -1
|
||||
}
|
||||
|
||||
// AddGroupSentMessageError searches matching groups for the message by sig and marks it as an error
|
||||
func (p *Profile) AddGroupSentMessageError(groupID string, signature []byte, error string) {
|
||||
p.lock.Lock()
|
||||
defer p.lock.Unlock()
|
||||
group, exists := p.Groups[groupID]
|
||||
if exists {
|
||||
group.ErrorSentMessage(signature, error)
|
||||
}
|
||||
}
|
||||
|
||||
// AcceptInvite accepts a group invite
|
||||
func (p *Profile) AcceptInvite(groupID string) (err error) {
|
||||
p.lock.Lock()
|
||||
defer p.lock.Unlock()
|
||||
group, ok := p.Groups[groupID]
|
||||
if ok {
|
||||
group.Accepted = true
|
||||
} else {
|
||||
err = errors.New("group does not exist")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// GetGroups returns an unordered list of group IDs associated with this profile.
|
||||
func (p *Profile) GetGroups() []string {
|
||||
p.lock.Lock()
|
||||
defer p.lock.Unlock()
|
||||
var keys []string
|
||||
for onion := range p.Groups {
|
||||
keys = append(keys, onion)
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
// GetContacts returns an unordered list of contact onions associated with this profile.
|
||||
func (p *Profile) GetContacts() []string {
|
||||
p.lock.Lock()
|
||||
defer p.lock.Unlock()
|
||||
var keys []string
|
||||
for onion := range p.Contacts {
|
||||
if onion != p.Onion {
|
||||
keys = append(keys, onion)
|
||||
}
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
// SetContactAuthorization sets the authoirization level of a peer
|
||||
func (p *Profile) SetContactAuthorization(onion string, auth Authorization) (err error) {
|
||||
p.lock.Lock()
|
||||
defer p.lock.Unlock()
|
||||
contact, ok := p.Contacts[onion]
|
||||
if ok {
|
||||
contact.Authorization = auth
|
||||
} else {
|
||||
err = errors.New("peer does not exist")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// GetContactAuthorization returns the contact's authorization level
|
||||
func (p *Profile) GetContactAuthorization(onion string) Authorization {
|
||||
p.lock.Lock()
|
||||
defer p.lock.Unlock()
|
||||
contact, ok := p.Contacts[onion]
|
||||
if ok {
|
||||
return contact.Authorization
|
||||
}
|
||||
return AuthUnknown
|
||||
}
|
||||
|
||||
// ContactsAuthorizations calculates a list of Peers who are at the supplied auth levels
|
||||
func (p *Profile) ContactsAuthorizations(authorizationFilter ...Authorization) map[string]Authorization {
|
||||
authorizations := map[string]Authorization{}
|
||||
for _, contact := range p.GetContacts() {
|
||||
c, _ := p.GetContact(contact)
|
||||
authorizations[c.Onion] = c.Authorization
|
||||
}
|
||||
return authorizations
|
||||
}
|
||||
|
||||
// GetContact returns a contact if the profile has it
|
||||
func (p *Profile) GetContact(onion string) (*PublicProfile, bool) {
|
||||
p.lock.Lock()
|
||||
defer p.lock.Unlock()
|
||||
contact, ok := p.Contacts[onion]
|
||||
return contact, ok
|
||||
}
|
||||
|
||||
// VerifyGroupMessage confirms the authenticity of a message given an sender onion, message and signature.
|
||||
// The goal of this function is 2-fold:
|
||||
// 1. We confirm that the sender referenced in the group text is the actual sender of the message (or at least
|
||||
// knows the senders private key)
|
||||
// 2. Secondly, we confirm that the sender sent the message to a particular group id on a specific server (it doesn't
|
||||
// matter if we actually received this message from the server or from a hybrid protocol, all that matters is
|
||||
// that the sender and receivers agree that this message was intended for the group
|
||||
// The 2nd point is important as it prevents an attack documented in the original Cwtch paper (and later at
|
||||
// https://docs.openprivacy.ca/cwtch-security-handbook/groups.html) in which a malicious profile sets up 2 groups
|
||||
// on two different servers with the same key and then forwards messages between them to convince the parties in
|
||||
// each group that they are actually in one big group (with the intent to later censor and/or selectively send messages
|
||||
// to each group).
|
||||
func (p *Profile) VerifyGroupMessage(onion string, groupID string, message string, signature []byte) bool {
|
||||
|
||||
group := p.GetGroup(groupID)
|
||||
if group == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// We use our group id, a known reference server and the ciphertext of the message.
|
||||
m := groupID + group.GroupServer + message
|
||||
|
||||
// If the message is ostensibly from us then we check it against our public key...
|
||||
if onion == p.Onion {
|
||||
return ed25519.Verify(p.Ed25519PublicKey, []byte(m), signature)
|
||||
}
|
||||
|
||||
// Otherwise we derive the public key from the sender and check it against that.
|
||||
decodedPub, err := base32.StdEncoding.DecodeString(strings.ToUpper(onion))
|
||||
if err == nil && len(decodedPub) >= 32 {
|
||||
return ed25519.Verify(decodedPub[:32], []byte(m), signature)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// SignMessage takes a given message and returns an Ed21159 signature
|
||||
func (p *Profile) SignMessage(message string) []byte {
|
||||
sig := ed25519.Sign(p.Ed25519PrivateKey, []byte(message))
|
||||
return sig
|
||||
}
|
||||
|
||||
// StartGroup when given a server, creates a new Group under this profile and returns the group id an a precomputed
|
||||
// invite which can be sent on the wire.
|
||||
func (p *Profile) StartGroup(server string) (groupID string, invite string, err error) {
|
||||
group, err := NewGroup(server)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
groupID = group.GroupID
|
||||
invite, err = group.Invite()
|
||||
p.lock.Lock()
|
||||
defer p.lock.Unlock()
|
||||
p.Groups[group.GroupID] = group
|
||||
return
|
||||
}
|
||||
|
||||
// GetGroup a pointer to a Group by the group Id, returns nil if no group found.
|
||||
func (p *Profile) GetGroup(groupID string) (g *Group) {
|
||||
p.lock.Lock()
|
||||
defer p.lock.Unlock()
|
||||
g = p.Groups[groupID]
|
||||
return
|
||||
}
|
||||
|
||||
// ProcessInvite validates a group invite and adds a new group invite to the profile if it is valid.
|
||||
// returns the new group ID on success, error on fail.
|
||||
func (p *Profile) ProcessInvite(invite string) (string, error) {
|
||||
gci, err := ValidateInvite(invite)
|
||||
if err == nil {
|
||||
if server, exists := p.GetContact(gci.ServerHost); !exists || !server.IsServer() {
|
||||
return "", fmt.Errorf("unknown server. a server key bundle needs to be imported before this group can be verified")
|
||||
}
|
||||
group := new(Group)
|
||||
group.Version = CurrentGroupVersion
|
||||
group.GroupID = gci.GroupID
|
||||
group.LocalID = GenerateRandomID()
|
||||
copy(group.GroupKey[:], gci.SharedKey[:])
|
||||
group.GroupServer = gci.ServerHost
|
||||
group.Accepted = false
|
||||
group.Attributes = make(map[string]string)
|
||||
group.Attributes[attr.GetLocalScope(constants.Name)] = gci.GroupName
|
||||
p.AddGroup(group)
|
||||
return gci.GroupID, nil
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
|
||||
// AddGroup is a convenience method for adding a group to a profile.
|
||||
func (p *Profile) AddGroup(group *Group) {
|
||||
p.lock.Lock()
|
||||
defer p.lock.Unlock()
|
||||
_, exists := p.Groups[group.GroupID]
|
||||
if !exists {
|
||||
p.Groups[group.GroupID] = group
|
||||
}
|
||||
}
|
||||
|
||||
// AttemptDecryption takes a ciphertext and signature and attempts to decrypt it under known groups.
|
||||
// If successful, adds the message to the group's timeline
|
||||
func (p *Profile) AttemptDecryption(ciphertext []byte, signature []byte) (bool, string, *Message, int) {
|
||||
for _, group := range p.Groups {
|
||||
success, dgm := group.DecryptMessage(ciphertext)
|
||||
if success {
|
||||
|
||||
// Attempt to serialize this message
|
||||
serialized, err := json.Marshal(dgm)
|
||||
|
||||
// Someone send a message that isn't a valid Decrypted Group Message. Since we require this struct in orer
|
||||
// to verify the message, we simply ignore it.
|
||||
if err != nil {
|
||||
return false, group.GroupID, nil, -1
|
||||
}
|
||||
|
||||
// This now requires knowledge of the Sender, the Onion and the Specific Decrypted Group Message (which should only
|
||||
// be derivable from the cryptographic key) which contains many unique elements such as the time and random padding
|
||||
verified := p.VerifyGroupMessage(dgm.Onion, group.GroupID, base64.StdEncoding.EncodeToString(serialized), signature)
|
||||
|
||||
if !verified {
|
||||
// An earlier version of this protocol mistakenly signed the ciphertext of the message
|
||||
// instead of the serialized decrypted group message.
|
||||
// This has 2 issues:
|
||||
// 1. A server with knowledge of group members public keys AND the Group ID would be able to detect valid messages
|
||||
// 2. It made the metadata-security of a group dependent on keeping the cryptographically derived Group ID secret.
|
||||
// While not awful, it also isn't good. For Version 3 groups only we permit Cwtch to check this older signature
|
||||
// structure in a backwards compatible way for the duration of the Groups Experiment.
|
||||
// TODO: Delete this check when Groups are no long Experimental
|
||||
if group.Version == 3 {
|
||||
verified = p.VerifyGroupMessage(dgm.Onion, group.GroupID, string(ciphertext), signature)
|
||||
}
|
||||
}
|
||||
|
||||
// So we have a message that has a valid group key, but the signature can't be verified.
|
||||
// The most obvious explanation for this is that the group key has been compromised (or we are in an open group and the server is being malicious)
|
||||
// Either way, someone who has the private key is being detectably bad so we are just going to throw this message away and mark the group as Compromised.
|
||||
if !verified {
|
||||
group.Compromised()
|
||||
return false, group.GroupID, nil, -1
|
||||
}
|
||||
message, index := group.AddMessage(dgm, signature)
|
||||
return true, group.GroupID, message, index
|
||||
}
|
||||
}
|
||||
|
||||
// If we couldn't find a group to decrypt the message with we just return false. This is an expected case
|
||||
return false, "", nil, -1
|
||||
}
|
||||
|
||||
func getRandomness(arr *[]byte) {
|
||||
if _, err := io.ReadFull(rand.Reader, (*arr)[:]); err != nil {
|
||||
if err != nil {
|
||||
|
@ -481,53 +62,11 @@ func getRandomness(arr *[]byte) {
|
|||
}
|
||||
}
|
||||
|
||||
// EncryptMessageToGroup when given a message and a group, encrypts and signs the message under the group and
|
||||
// profile
|
||||
func (p *Profile) EncryptMessageToGroup(message string, groupID string) ([]byte, []byte, error) {
|
||||
|
||||
if len(message) > MaxGroupMessageLength {
|
||||
return nil, nil, errors.New("group message is too long")
|
||||
}
|
||||
|
||||
group := p.GetGroup(groupID)
|
||||
if group != nil {
|
||||
timestamp := time.Now().Unix()
|
||||
|
||||
// Select the latest message from the timeline as a reference point.
|
||||
var prevSig []byte
|
||||
if len(group.Timeline.Messages) > 0 {
|
||||
prevSig = group.Timeline.Messages[len(group.Timeline.Messages)-1].Signature
|
||||
} else {
|
||||
prevSig = []byte(group.GroupID)
|
||||
}
|
||||
|
||||
lenPadding := MaxGroupMessageLength - len(message)
|
||||
padding := make([]byte, lenPadding)
|
||||
getRandomness(&padding)
|
||||
hexGroupID, err := hex.DecodeString(group.GroupID)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
dm := &groups.DecryptedGroupMessage{
|
||||
Onion: p.Onion,
|
||||
Text: message,
|
||||
SignedGroupID: hexGroupID,
|
||||
Timestamp: uint64(timestamp),
|
||||
PreviousMessageSig: prevSig,
|
||||
Padding: padding[:],
|
||||
}
|
||||
|
||||
ciphertext, err := group.EncryptMessage(dm)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
serialized, _ := json.Marshal(dm)
|
||||
signature := p.SignMessage(groupID + group.GroupServer + base64.StdEncoding.EncodeToString(serialized))
|
||||
group.AddSentMessage(dm, signature)
|
||||
return ciphertext, signature, nil
|
||||
}
|
||||
return nil, nil, errors.New("group does not exist")
|
||||
// GenerateRandomID generates a random 16 byte hex id code
|
||||
func GenerateRandomID() string {
|
||||
randBytes := make([]byte, 16)
|
||||
rand.Read(randBytes)
|
||||
return hex.EncodeToString(randBytes)
|
||||
}
|
||||
|
||||
// GetCopy returns a full deep copy of the Profile struct and its members (timeline inclusion control by arg)
|
||||
|
|
|
@ -1,136 +0,0 @@
|
|||
package model
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestProfileIdentity(t *testing.T) {
|
||||
sarah := GenerateNewProfile("Sarah")
|
||||
alice := GenerateNewProfile("Alice")
|
||||
|
||||
alice.AddContact(sarah.Onion, &sarah.PublicProfile)
|
||||
if alice.Contacts[sarah.Onion].Name != "Sarah" {
|
||||
t.Errorf("alice should have added sarah as a contact %v", alice.Contacts)
|
||||
}
|
||||
|
||||
if len(alice.GetContacts()) != 1 {
|
||||
t.Errorf("alice should be only contact: %v", alice.GetContacts())
|
||||
}
|
||||
|
||||
alice.SetAttribute("test", "hello world")
|
||||
value, _ := alice.GetAttribute("test")
|
||||
if value != "hello world" {
|
||||
t.Errorf("value from custom attribute should have been 'hello world', instead was: %v", value)
|
||||
}
|
||||
|
||||
t.Logf("%v", alice)
|
||||
}
|
||||
|
||||
func TestTrustPeer(t *testing.T) {
|
||||
sarah := GenerateNewProfile("Sarah")
|
||||
alice := GenerateNewProfile("Alice")
|
||||
sarah.AddContact(alice.Onion, &alice.PublicProfile)
|
||||
alice.AddContact(sarah.Onion, &sarah.PublicProfile)
|
||||
alice.SetContactAuthorization(sarah.Onion, AuthApproved)
|
||||
if alice.GetContactAuthorization(sarah.Onion) != AuthApproved {
|
||||
t.Errorf("peer should be approved")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBlockPeer(t *testing.T) {
|
||||
sarah := GenerateNewProfile("Sarah")
|
||||
alice := GenerateNewProfile("Alice")
|
||||
sarah.AddContact(alice.Onion, &alice.PublicProfile)
|
||||
alice.AddContact(sarah.Onion, &sarah.PublicProfile)
|
||||
alice.SetContactAuthorization(sarah.Onion, AuthBlocked)
|
||||
if alice.GetContactAuthorization(sarah.Onion) != AuthBlocked {
|
||||
t.Errorf("peer should be blocked")
|
||||
}
|
||||
|
||||
if alice.SetContactAuthorization("", AuthUnknown) == nil {
|
||||
t.Errorf("Seting Auth level of a non existent peer should error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAcceptNonExistentGroup(t *testing.T) {
|
||||
sarah := GenerateNewProfile("Sarah")
|
||||
sarah.AcceptInvite("doesnotexist")
|
||||
}
|
||||
|
||||
func TestRejectGroupInvite(t *testing.T) {
|
||||
sarah := GenerateNewProfile("Sarah")
|
||||
alice := GenerateNewProfile("Alice")
|
||||
sarah.AddContact(alice.Onion, &alice.PublicProfile)
|
||||
alice.AddContact(sarah.Onion, &sarah.PublicProfile)
|
||||
// The lightest weight server entry possible (usually we would import a key bundle...)
|
||||
sarah.AddContact("2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd", &PublicProfile{Attributes: map[string]string{string(KeyTypeServerOnion): "2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd"}})
|
||||
|
||||
gid, invite, _ := alice.StartGroup("2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd")
|
||||
sarah.ProcessInvite(invite)
|
||||
group := alice.GetGroup(gid)
|
||||
if len(sarah.Groups) == 1 {
|
||||
if sarah.GetGroup(group.GroupID).Accepted {
|
||||
t.Errorf("Group should not be accepted")
|
||||
}
|
||||
sarah.RejectInvite(group.GroupID)
|
||||
if len(sarah.Groups) != 0 {
|
||||
t.Errorf("Group %v should have been deleted", group.GroupID)
|
||||
}
|
||||
return
|
||||
}
|
||||
t.Errorf("Group should exist in map")
|
||||
}
|
||||
|
||||
func TestProfileGroup(t *testing.T) {
|
||||
sarah := GenerateNewProfile("Sarah")
|
||||
alice := GenerateNewProfile("Alice")
|
||||
sarah.AddContact(alice.Onion, &alice.PublicProfile)
|
||||
alice.AddContact(sarah.Onion, &sarah.PublicProfile)
|
||||
|
||||
gid, invite, _ := alice.StartGroup("2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd")
|
||||
|
||||
// The lightest weight server entry possible (usually we would import a key bundle...)
|
||||
sarah.AddContact("2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd", &PublicProfile{Attributes: map[string]string{string(KeyTypeServerOnion): "2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd"}})
|
||||
sarah.ProcessInvite(invite)
|
||||
if len(sarah.GetGroups()) != 1 {
|
||||
t.Errorf("sarah should only be in 1 group instead: %v", sarah.GetGroups())
|
||||
}
|
||||
|
||||
group := alice.GetGroup(gid)
|
||||
sarah.AcceptInvite(group.GroupID)
|
||||
c, s1, _ := sarah.EncryptMessageToGroup("Hello World", group.GroupID)
|
||||
alice.AttemptDecryption(c, s1)
|
||||
|
||||
gid2, invite2, _ := alice.StartGroup("2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd")
|
||||
sarah.ProcessInvite(invite2)
|
||||
group2 := alice.GetGroup(gid2)
|
||||
c2, s2, _ := sarah.EncryptMessageToGroup("Hello World", group2.GroupID)
|
||||
alice.AttemptDecryption(c2, s2)
|
||||
|
||||
_, _, err := sarah.EncryptMessageToGroup(string(make([]byte, MaxGroupMessageLength*2)), group2.GroupID)
|
||||
if err == nil {
|
||||
t.Errorf("Overly long message should have returned an error")
|
||||
}
|
||||
|
||||
bob := GenerateNewProfile("bob")
|
||||
bob.AddContact(alice.Onion, &alice.PublicProfile)
|
||||
// The lightest weight server entry possible (usually we would import a key bundle...)
|
||||
bob.AddContact("2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd", &PublicProfile{Attributes: map[string]string{string(KeyTypeServerOnion): "2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd"}})
|
||||
|
||||
bob.ProcessInvite(invite2)
|
||||
c3, s3, err := bob.EncryptMessageToGroup("Bobs Message", group2.GroupID)
|
||||
if err == nil {
|
||||
ok, _, message, _ := alice.AttemptDecryption(c3, s3)
|
||||
if !ok {
|
||||
t.Errorf("Bobs message to the group should be decrypted %v %v", message, ok)
|
||||
}
|
||||
|
||||
eve := GenerateNewProfile("eve")
|
||||
ok, _, _, _ = eve.AttemptDecryption(c3, s3)
|
||||
if ok {
|
||||
t.Errorf("Eves hould not be able to decrypt Messages!")
|
||||
}
|
||||
} else {
|
||||
t.Errorf("Bob failed to encrypt a message to the group")
|
||||
}
|
||||
}
|
1374
peer/cwtch_peer.go
1374
peer/cwtch_peer.go
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,764 @@
|
|||
package peer
|
||||
|
||||
import (
|
||||
"cwtch.im/cwtch/event"
|
||||
"cwtch.im/cwtch/model"
|
||||
"cwtch.im/cwtch/model/attr"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"git.openprivacy.ca/openprivacy/log"
|
||||
"os"
|
||||
)
|
||||
|
||||
// StorageKeyType is an interface wrapper around storage key types
|
||||
type StorageKeyType string
|
||||
|
||||
const (
|
||||
// TypeAttribute for Profile Scoped and Zoned Attributes
|
||||
TypeAttribute = StorageKeyType("Attribute")
|
||||
|
||||
// TypePrivateKey for Profile Private Keys
|
||||
TypePrivateKey = StorageKeyType("PrivateKey")
|
||||
|
||||
// TypePublicKey for Profile Public Keys
|
||||
TypePublicKey = StorageKeyType("PublicKey")
|
||||
)
|
||||
|
||||
// CwtchProfileStorage encapsulates common datastore requests so as to not pollute the main cwtch profile
|
||||
// struct with database knowledge
|
||||
type CwtchProfileStorage struct {
|
||||
|
||||
// Note: Statements are thread safe..
|
||||
|
||||
// Profile related statements
|
||||
insertProfileKeyValueStmt *sql.Stmt
|
||||
selectProfileKeyValueStmt *sql.Stmt
|
||||
|
||||
// Conversation related statements
|
||||
insertConversationStmt *sql.Stmt
|
||||
fetchAllConversationsStmt *sql.Stmt
|
||||
selectConversationStmt *sql.Stmt
|
||||
selectConversationByHandleStmt *sql.Stmt
|
||||
acceptConversationStmt *sql.Stmt
|
||||
deleteConversationStmt *sql.Stmt
|
||||
setConversationAttributesStmt *sql.Stmt
|
||||
setConversationACLStmt *sql.Stmt
|
||||
|
||||
channelInsertStmts map[ChannelID]*sql.Stmt
|
||||
channelUpdateMessageStmts map[ChannelID]*sql.Stmt
|
||||
channelGetMessageStmts map[ChannelID]*sql.Stmt
|
||||
channelGetMessageBySignatureStmts map[ChannelID]*sql.Stmt
|
||||
channelGetCountStmts map[ChannelID]*sql.Stmt
|
||||
channelGetMostRecentMessagesStmts map[ChannelID]*sql.Stmt
|
||||
channelGetMessageByContentHashStmts map[ChannelID]*sql.Stmt
|
||||
channelRowNumberStmts map[ChannelID]*sql.Stmt
|
||||
ProfileDirectory string
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
// ChannelID encapsulates the data necessary to reference a channel structure.
|
||||
type ChannelID struct {
|
||||
Conversation int
|
||||
Channel int
|
||||
}
|
||||
|
||||
const insertProfileKeySQLStmt = `insert or replace into profile_kv(KeyType, KeyName, KeyValue) values(?,?,?);`
|
||||
const selectProfileKeySQLStmt = `select KeyValue from profile_kv where KeyType=(?) and KeyName=(?);`
|
||||
|
||||
const insertConversationSQLStmt = `insert into conversations(Handle, Attributes, ACL, Accepted) values(?,?,?,?);`
|
||||
const fetchAllConversationsSQLStmt = `select ID, Handle, Attributes, ACL, Accepted from conversations;`
|
||||
const selectConversationSQLStmt = `select ID, Handle, Attributes, ACL, Accepted from conversations where ID=(?);`
|
||||
const selectConversationByHandleSQLStmt = `select ID, Handle, Attributes, ACL, Accepted from conversations where Handle=(?);`
|
||||
const acceptConversationSQLStmt = `update conversations set Accepted=true where ID=(?);`
|
||||
const setConversationAttributesSQLStmt = `update conversations set Attributes=(?) where ID=(?) ;`
|
||||
const setConversationACLSQLStmt = `update conversations set ACL=(?) where ID=(?) ;`
|
||||
const deleteConversationSQLStmt = `delete from conversations where ID=(?);`
|
||||
|
||||
// createTableConversationMessagesSQLStmt is a template for creating conversation based tables...
|
||||
const createTableConversationMessagesSQLStmt = `create table if not exists channel_%d_0_chat (ID integer unique primary key autoincrement, Body text, Attributes []byte, Expiry datetime, Signature text unique, ContentHash blob text);`
|
||||
|
||||
// insertMessageIntoConversationSQLStmt is a template for creating conversation based tables...
|
||||
const insertMessageIntoConversationSQLStmt = `insert into channel_%d_%d_chat (Body, Attributes, Signature, ContentHash) values(?,?,?,?);`
|
||||
|
||||
// updateMessageIntoConversationSQLStmt is a template for updating attributes of a message in a conversation
|
||||
const updateMessageIntoConversationSQLStmt = `update channel_%d_%d_chat set Attributes=(?) where ID=(?);`
|
||||
|
||||
// purgeMessagesFromConversationSQLStmt is a template for updating attributes of a message in a conversation
|
||||
const purgeMessagesFromConversationSQLStmt = `delete from channel_%d_%d_chat;`
|
||||
|
||||
// getMessageFromConversationSQLStmt is a template for fetching a message by ID from a conversation
|
||||
const getMessageFromConversationSQLStmt = `select Body, Attributes from channel_%d_%d_chat where ID=(?);`
|
||||
|
||||
// getMessageBySignatureFromConversationSQLStmt is a template for selecting conversation messages by signature
|
||||
const getMessageBySignatureFromConversationSQLStmt = `select ID from channel_%d_%d_chat where Signature=(?);`
|
||||
|
||||
// getMessageByContentHashFromConversationSQLStmt is a template for selecting conversation messages by content hash
|
||||
const getMessageByContentHashFromConversationSQLStmt = `select ID from channel_%d_%d_chat where ContentHash=(?) order by ID desc limit 1;`
|
||||
|
||||
// getLocalIndexOfMessageIDSQLStmt is a template for fetching the offset of a message from the bottom of the database.
|
||||
const getLocalIndexOfMessageIDSQLStmt = `select count (*) from channel_%d_%d_chat where ID >= (?) order by ID desc;`
|
||||
|
||||
// getMessageCountFromConversationSQLStmt is a template for fetching the count of a messages in a conversation channel
|
||||
const getMessageCountFromConversationSQLStmt = `select count(*) from channel_%d_%d_chat;`
|
||||
|
||||
// getMostRecentMessagesSQLStmt is a template for fetching the most recent N messages in a conversation channel
|
||||
const getMostRecentMessagesSQLStmt = `select ID, Body, Attributes, Signature, ContentHash from channel_%d_%d_chat order by ID desc limit (?) offset (?);`
|
||||
|
||||
// NewCwtchProfileStorage constructs a new CwtchProfileStorage from a database. It is also responsible for
|
||||
// Preparing commonly used SQL Statements
|
||||
func NewCwtchProfileStorage(db *sql.DB, profileDirectory string) (*CwtchProfileStorage, error) {
|
||||
|
||||
if db == nil {
|
||||
return nil, errors.New("cannot construct cwtch profile storage with a nil database")
|
||||
}
|
||||
|
||||
insertProfileKeyValueStmt, err := db.Prepare(insertProfileKeySQLStmt)
|
||||
if err != nil {
|
||||
log.Errorf("error preparing query: %v %v", insertProfileKeySQLStmt, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
selectProfileKeyStmt, err := db.Prepare(selectProfileKeySQLStmt)
|
||||
if err != nil {
|
||||
log.Errorf("error preparing query: %v %v", selectProfileKeySQLStmt, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
insertConversationStmt, err := db.Prepare(insertConversationSQLStmt)
|
||||
if err != nil {
|
||||
log.Errorf("error preparing query: %v %v", insertConversationSQLStmt, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fetchAllConversationsStmt, err := db.Prepare(fetchAllConversationsSQLStmt)
|
||||
if err != nil {
|
||||
log.Errorf("error preparing query: %v %v", fetchAllConversationsSQLStmt, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
selectConversationStmt, err := db.Prepare(selectConversationSQLStmt)
|
||||
if err != nil {
|
||||
log.Errorf("error preparing query: %v %v", selectConversationSQLStmt, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
selectConversationByHandleStmt, err := db.Prepare(selectConversationByHandleSQLStmt)
|
||||
if err != nil {
|
||||
log.Errorf("error preparing query: %v %v", selectConversationByHandleSQLStmt, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
acceptConversationStmt, err := db.Prepare(acceptConversationSQLStmt)
|
||||
if err != nil {
|
||||
log.Errorf("error preparing query: %v %v", acceptConversationSQLStmt, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
deleteConversationStmt, err := db.Prepare(deleteConversationSQLStmt)
|
||||
if err != nil {
|
||||
log.Errorf("error preparing query: %v %v", deleteConversationSQLStmt, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
setConversationAttributesStmt, err := db.Prepare(setConversationAttributesSQLStmt)
|
||||
if err != nil {
|
||||
log.Errorf("error preparing query: %v %v", setConversationAttributesSQLStmt, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
setConversationACLStmt, err := db.Prepare(setConversationACLSQLStmt)
|
||||
if err != nil {
|
||||
log.Errorf("error preparing query: %v %v", setConversationACLSQLStmt, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &CwtchProfileStorage{db: db,
|
||||
ProfileDirectory: profileDirectory,
|
||||
insertProfileKeyValueStmt: insertProfileKeyValueStmt,
|
||||
selectProfileKeyValueStmt: selectProfileKeyStmt,
|
||||
fetchAllConversationsStmt: fetchAllConversationsStmt,
|
||||
insertConversationStmt: insertConversationStmt,
|
||||
selectConversationStmt: selectConversationStmt,
|
||||
selectConversationByHandleStmt: selectConversationByHandleStmt,
|
||||
acceptConversationStmt: acceptConversationStmt,
|
||||
deleteConversationStmt: deleteConversationStmt,
|
||||
setConversationAttributesStmt: setConversationAttributesStmt,
|
||||
setConversationACLStmt: setConversationACLStmt,
|
||||
channelInsertStmts: map[ChannelID]*sql.Stmt{},
|
||||
channelUpdateMessageStmts: map[ChannelID]*sql.Stmt{},
|
||||
channelGetMessageStmts: map[ChannelID]*sql.Stmt{},
|
||||
channelGetMessageBySignatureStmts: map[ChannelID]*sql.Stmt{},
|
||||
channelGetMessageByContentHashStmts: map[ChannelID]*sql.Stmt{},
|
||||
channelGetMostRecentMessagesStmts: map[ChannelID]*sql.Stmt{},
|
||||
channelGetCountStmts: map[ChannelID]*sql.Stmt{},
|
||||
channelRowNumberStmts: map[ChannelID]*sql.Stmt{},
|
||||
},
|
||||
nil
|
||||
}
|
||||
|
||||
// StoreProfileKeyValue allows storing of typed Key/Value attribute in the Storage Engine
|
||||
func (cps *CwtchProfileStorage) StoreProfileKeyValue(keyType StorageKeyType, key string, value []byte) error {
|
||||
_, err := cps.insertProfileKeyValueStmt.Exec(keyType, key, value)
|
||||
if err != nil {
|
||||
log.Errorf("error executing query: %v", err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadProfileKeyValue allows fetching of typed values via a known Key from the Storage Engine
|
||||
func (cps *CwtchProfileStorage) LoadProfileKeyValue(keyType StorageKeyType, key string) ([]byte, error) {
|
||||
rows, err := cps.selectProfileKeyValueStmt.Query(keyType, key)
|
||||
if err != nil {
|
||||
log.Errorf("error executing query: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := rows.Next()
|
||||
|
||||
if !result {
|
||||
return nil, errors.New("no result found")
|
||||
}
|
||||
|
||||
var keyValue []byte
|
||||
err = rows.Scan(&keyValue)
|
||||
if err != nil {
|
||||
log.Errorf("error fetching rows: %v", err)
|
||||
rows.Close()
|
||||
return nil, err
|
||||
}
|
||||
rows.Close()
|
||||
return keyValue, nil
|
||||
}
|
||||
|
||||
// NewConversation stores a new conversation in the data store
|
||||
func (cps *CwtchProfileStorage) NewConversation(handle string, attributes model.Attributes, acl model.AccessControlList, accepted bool) (int, error) {
|
||||
tx, err := cps.db.Begin()
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("error executing transaction: %v", err)
|
||||
return -1, err
|
||||
}
|
||||
|
||||
result, err := tx.Stmt(cps.insertConversationStmt).Exec(handle, attributes.Serialize(), acl.Serialize(), accepted)
|
||||
if err != nil {
|
||||
log.Errorf("error executing transaction: %v", err)
|
||||
return -1, tx.Rollback()
|
||||
}
|
||||
|
||||
id, err := result.LastInsertId()
|
||||
if err != nil {
|
||||
log.Errorf("error executing transaction: %v", err)
|
||||
return -1, tx.Rollback()
|
||||
}
|
||||
|
||||
result, err = tx.Exec(fmt.Sprintf(createTableConversationMessagesSQLStmt, id))
|
||||
if err != nil {
|
||||
log.Errorf("error executing transaction: %v", err)
|
||||
return -1, tx.Rollback()
|
||||
}
|
||||
|
||||
conversationID, err := result.LastInsertId()
|
||||
if err != nil {
|
||||
log.Errorf("error executing transaction: %v", err)
|
||||
return -1, tx.Rollback()
|
||||
}
|
||||
|
||||
err = tx.Commit()
|
||||
if err != nil {
|
||||
log.Errorf("error executing transaction: %v", err)
|
||||
return -1, tx.Rollback()
|
||||
}
|
||||
|
||||
return int(conversationID), nil
|
||||
}
|
||||
|
||||
// GetConversationByHandle is a convenience method to fetch an active conversation by a handle
|
||||
// Usage Notes: This should **only** be used to look up p2p conversations by convention.
|
||||
// Ideally this function should not exist, and all lookups should happen by ID (this is currently
|
||||
// unavoidable in some circumstances because the event bus references conversations by handle, not by id)
|
||||
func (cps *CwtchProfileStorage) GetConversationByHandle(handle string) (*model.Conversation, error) {
|
||||
rows, err := cps.selectConversationByHandleStmt.Query(handle)
|
||||
if err != nil {
|
||||
log.Errorf("error executing query: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := rows.Next()
|
||||
|
||||
if !result {
|
||||
return nil, errors.New("no result found")
|
||||
}
|
||||
|
||||
var id int
|
||||
var acl []byte
|
||||
var attributes []byte
|
||||
var accepted bool
|
||||
err = rows.Scan(&id, &handle, &attributes, &acl, &accepted)
|
||||
if err != nil {
|
||||
log.Errorf("error fetching rows: %v", err)
|
||||
rows.Close()
|
||||
return nil, err
|
||||
}
|
||||
rows.Close()
|
||||
|
||||
return &model.Conversation{ID: id, Handle: handle, ACL: model.DeserializeAccessControlList(acl), Attributes: model.DeserializeAttributes(attributes), Accepted: accepted}, nil
|
||||
}
|
||||
|
||||
// FetchConversations returns *all* active conversations. This method should only be called
|
||||
// on app start up to build a summary of conversations for the UI. Any further updates should be integrated
|
||||
// through the event bus.
|
||||
func (cps *CwtchProfileStorage) FetchConversations() ([]*model.Conversation, error) {
|
||||
rows, err := cps.fetchAllConversationsStmt.Query()
|
||||
if err != nil {
|
||||
log.Errorf("error executing query: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var conversations []*model.Conversation
|
||||
|
||||
defer rows.Close()
|
||||
for {
|
||||
result := rows.Next()
|
||||
|
||||
if !result {
|
||||
return conversations, nil
|
||||
}
|
||||
|
||||
var id int
|
||||
var handle string
|
||||
var acl []byte
|
||||
var attributes []byte
|
||||
var accepted bool
|
||||
err = rows.Scan(&id, &handle, &attributes, &acl, &accepted)
|
||||
if err != nil {
|
||||
log.Errorf("error fetching rows: %v", err)
|
||||
rows.Close()
|
||||
return nil, err
|
||||
}
|
||||
conversations = append(conversations, &model.Conversation{ID: id, Handle: handle, ACL: model.DeserializeAccessControlList(acl), Attributes: model.DeserializeAttributes(attributes), Accepted: accepted})
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// GetConversation looks up a particular conversation by id
|
||||
func (cps *CwtchProfileStorage) GetConversation(id int) (*model.Conversation, error) {
|
||||
rows, err := cps.selectConversationStmt.Query(id)
|
||||
if err != nil {
|
||||
log.Errorf("error executing query: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := rows.Next()
|
||||
|
||||
if !result {
|
||||
return nil, errors.New("no result found")
|
||||
}
|
||||
|
||||
var handle string
|
||||
var acl []byte
|
||||
var attributes []byte
|
||||
var accepted bool
|
||||
err = rows.Scan(&id, &handle, &attributes, &acl, &accepted)
|
||||
if err != nil {
|
||||
log.Errorf("error fetching rows: %v", err)
|
||||
rows.Close()
|
||||
return nil, err
|
||||
}
|
||||
rows.Close()
|
||||
|
||||
return &model.Conversation{ID: id, Handle: handle, ACL: model.DeserializeAccessControlList(acl), Attributes: model.DeserializeAttributes(attributes), Accepted: accepted}, nil
|
||||
}
|
||||
|
||||
// AcceptConversation sets the accepted status of a conversation to true in the backing datastore
|
||||
func (cps *CwtchProfileStorage) AcceptConversation(id int) error {
|
||||
_, err := cps.acceptConversationStmt.Exec(id)
|
||||
if err != nil {
|
||||
log.Errorf("error executing query: %v", err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteConversation purges the conversation and any associated message history from the conversation store.
|
||||
func (cps *CwtchProfileStorage) DeleteConversation(id int) error {
|
||||
_, err := cps.deleteConversationStmt.Exec(id)
|
||||
if err != nil {
|
||||
log.Errorf("error executing query: %v", err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetConversationACL sets a new ACL on a given conversation.
|
||||
func (cps *CwtchProfileStorage) SetConversationACL(id int, acl model.AccessControlList) error {
|
||||
_, err := cps.setConversationACLStmt.Exec(acl, id)
|
||||
if err != nil {
|
||||
log.Errorf("error executing query: %v", err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetConversationAttribute sets a new attribute on a given conversation.
|
||||
func (cps *CwtchProfileStorage) SetConversationAttribute(id int, path attr.ScopedZonedPath, value string) error {
|
||||
ci, err := cps.GetConversation(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ci.Attributes[path.ToString()] = value
|
||||
_, err = cps.setConversationAttributesStmt.Exec(ci.Attributes.Serialize(), id)
|
||||
if err != nil {
|
||||
log.Errorf("error executing query: %v", err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// InsertMessage appends a message to a conversation channel, with a given set of attributes
|
||||
func (cps *CwtchProfileStorage) InsertMessage(conversation int, channel int, body string, attributes model.Attributes, signature string, contentHash string) (int, error) {
|
||||
|
||||
channelID := ChannelID{Conversation: conversation, Channel: channel}
|
||||
|
||||
_, exists := cps.channelInsertStmts[channelID]
|
||||
if !exists {
|
||||
conversationStmt, err := cps.db.Prepare(fmt.Sprintf(insertMessageIntoConversationSQLStmt, conversation, channel))
|
||||
if err != nil {
|
||||
log.Errorf("error executing transaction: %v", err)
|
||||
return -1, err
|
||||
}
|
||||
cps.channelInsertStmts[channelID] = conversationStmt
|
||||
}
|
||||
|
||||
result, err := cps.channelInsertStmts[channelID].Exec(body, attributes.Serialize(), signature, contentHash)
|
||||
if err != nil {
|
||||
log.Errorf("error inserting message: %v %v", signature, err)
|
||||
return -1, err
|
||||
}
|
||||
|
||||
id, err := result.LastInsertId()
|
||||
return int(id), err
|
||||
}
|
||||
|
||||
// UpdateMessageAttributes updates the attributes associated with a message of a given conversation
|
||||
func (cps *CwtchProfileStorage) UpdateMessageAttributes(conversation int, channel int, messageID int, attributes model.Attributes) error {
|
||||
|
||||
channelID := ChannelID{Conversation: conversation, Channel: channel}
|
||||
|
||||
_, exists := cps.channelUpdateMessageStmts[channelID]
|
||||
if !exists {
|
||||
conversationStmt, err := cps.db.Prepare(fmt.Sprintf(updateMessageIntoConversationSQLStmt, conversation, channel))
|
||||
if err != nil {
|
||||
log.Errorf("error executing transaction: %v", err)
|
||||
return err
|
||||
}
|
||||
cps.channelUpdateMessageStmts[channelID] = conversationStmt
|
||||
}
|
||||
|
||||
_, err := cps.channelUpdateMessageStmts[channelID].Exec(attributes.Serialize(), messageID)
|
||||
if err != nil {
|
||||
log.Errorf("error updating message: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetChannelMessageBySignature looks up a conversation message by signature instead of identifier. Both are unique but
|
||||
// signatures are common between conversation participants (in groups) and so are a more useful message to index.
|
||||
func (cps *CwtchProfileStorage) GetChannelMessageBySignature(conversation int, channel int, signature string) (int, error) {
|
||||
channelID := ChannelID{Conversation: conversation, Channel: channel}
|
||||
|
||||
_, exists := cps.channelGetMessageBySignatureStmts[channelID]
|
||||
if !exists {
|
||||
conversationStmt, err := cps.db.Prepare(fmt.Sprintf(getMessageBySignatureFromConversationSQLStmt, conversation, channel))
|
||||
if err != nil {
|
||||
log.Errorf("error executing transaction: %v", err)
|
||||
return -1, err
|
||||
}
|
||||
cps.channelGetMessageBySignatureStmts[channelID] = conversationStmt
|
||||
}
|
||||
|
||||
rows, err := cps.channelGetMessageBySignatureStmts[channelID].Query(signature)
|
||||
if err != nil {
|
||||
log.Errorf("error executing query: %v", err)
|
||||
return -1, err
|
||||
}
|
||||
|
||||
result := rows.Next()
|
||||
|
||||
if !result {
|
||||
return -1, errors.New("no result found")
|
||||
}
|
||||
|
||||
var id int
|
||||
err = rows.Scan(&id)
|
||||
if err != nil {
|
||||
log.Errorf("error fetching rows: %v", err)
|
||||
rows.Close()
|
||||
return -1, err
|
||||
}
|
||||
rows.Close()
|
||||
return id, nil
|
||||
}
|
||||
|
||||
// GetChannelMessageByContentHash looks up a conversation message by hash instead of identifier.
|
||||
func (cps *CwtchProfileStorage) GetChannelMessageByContentHash(conversation int, channel int, hash string) (int, error) {
|
||||
channelID := ChannelID{Conversation: conversation, Channel: channel}
|
||||
|
||||
_, exists := cps.channelGetMessageByContentHashStmts[channelID]
|
||||
if !exists {
|
||||
conversationStmt, err := cps.db.Prepare(fmt.Sprintf(getMessageByContentHashFromConversationSQLStmt, conversation, channel))
|
||||
if err != nil {
|
||||
log.Errorf("error executing transaction: %v", err)
|
||||
return -1, err
|
||||
}
|
||||
cps.channelGetMessageByContentHashStmts[channelID] = conversationStmt
|
||||
}
|
||||
|
||||
rows, err := cps.channelGetMessageByContentHashStmts[channelID].Query(hash)
|
||||
if err != nil {
|
||||
log.Errorf("error executing query: %v", err)
|
||||
return -1, err
|
||||
}
|
||||
|
||||
result := rows.Next()
|
||||
|
||||
if !result {
|
||||
return -1, errors.New("no result found")
|
||||
}
|
||||
|
||||
var id int
|
||||
err = rows.Scan(&id)
|
||||
if err != nil {
|
||||
log.Errorf("error fetching rows: %v", err)
|
||||
rows.Close()
|
||||
return -1, err
|
||||
}
|
||||
rows.Close()
|
||||
|
||||
return id, nil
|
||||
}
|
||||
|
||||
// GetRowNumberByMessageID looks up the row number of a message by the message ID
|
||||
func (cps *CwtchProfileStorage) GetRowNumberByMessageID(conversation int, channel int, id int) (int, error) {
|
||||
channelID := ChannelID{Conversation: conversation, Channel: channel}
|
||||
|
||||
_, exists := cps.channelRowNumberStmts[channelID]
|
||||
if !exists {
|
||||
conversationStmt, err := cps.db.Prepare(fmt.Sprintf(getLocalIndexOfMessageIDSQLStmt, conversation, channel))
|
||||
if err != nil {
|
||||
log.Errorf("error executing transaction: %v", err)
|
||||
return -1, err
|
||||
}
|
||||
cps.channelRowNumberStmts[channelID] = conversationStmt
|
||||
}
|
||||
|
||||
rows, err := cps.channelRowNumberStmts[channelID].Query(id)
|
||||
if err != nil {
|
||||
log.Errorf("error executing query: %v", err)
|
||||
return -1, err
|
||||
}
|
||||
|
||||
result := rows.Next()
|
||||
|
||||
if !result {
|
||||
return -1, errors.New("no result found")
|
||||
}
|
||||
|
||||
var rownum int
|
||||
err = rows.Scan(&rownum)
|
||||
if err != nil {
|
||||
log.Errorf("error fetching rows: %v", err)
|
||||
rows.Close()
|
||||
return -1, err
|
||||
}
|
||||
rows.Close()
|
||||
// Return the offset **not** the count
|
||||
return rownum - 1, nil
|
||||
}
|
||||
|
||||
// GetChannelMessage looks up a channel message by conversation, channel and message id. On success it
|
||||
// returns the message body and the attributes associated with the message. Otherwise an error is returned.
|
||||
func (cps *CwtchProfileStorage) GetChannelMessage(conversation int, channel int, messageID int) (string, model.Attributes, error) {
|
||||
channelID := ChannelID{Conversation: conversation, Channel: channel}
|
||||
|
||||
_, exists := cps.channelGetMessageStmts[channelID]
|
||||
if !exists {
|
||||
conversationStmt, err := cps.db.Prepare(fmt.Sprintf(getMessageFromConversationSQLStmt, conversation, channel))
|
||||
if err != nil {
|
||||
log.Errorf("error executing transaction: %v", err)
|
||||
return "", nil, err
|
||||
}
|
||||
cps.channelGetMessageStmts[channelID] = conversationStmt
|
||||
}
|
||||
|
||||
rows, err := cps.channelGetMessageStmts[channelID].Query(messageID)
|
||||
if err != nil {
|
||||
log.Errorf("error executing query: %v", err)
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
result := rows.Next()
|
||||
|
||||
if !result {
|
||||
return "", nil, errors.New("no result found")
|
||||
}
|
||||
|
||||
// Deserialize the Row
|
||||
var body string
|
||||
var attributes []byte
|
||||
err = rows.Scan(&body, &attributes)
|
||||
if err != nil {
|
||||
log.Errorf("error fetching rows: %v", err)
|
||||
rows.Close()
|
||||
return "", nil, err
|
||||
}
|
||||
rows.Close()
|
||||
|
||||
return body, model.DeserializeAttributes(attributes), nil
|
||||
}
|
||||
|
||||
// GetChannelMessageCount returns the number of messages in a channel
|
||||
func (cps *CwtchProfileStorage) GetChannelMessageCount(conversation int, channel int) (int, error) {
|
||||
channelID := ChannelID{Conversation: conversation, Channel: channel}
|
||||
|
||||
_, exists := cps.channelGetCountStmts[channelID]
|
||||
if !exists {
|
||||
conversationStmt, err := cps.db.Prepare(fmt.Sprintf(getMessageCountFromConversationSQLStmt, conversation, channel))
|
||||
if err != nil {
|
||||
log.Errorf("error executing transaction: %v", err)
|
||||
return -1, err
|
||||
}
|
||||
cps.channelGetCountStmts[channelID] = conversationStmt
|
||||
}
|
||||
|
||||
var count int
|
||||
err := cps.channelGetCountStmts[channelID].QueryRow().Scan(&count)
|
||||
if err != nil {
|
||||
log.Errorf("error executing query: %v", err)
|
||||
return -1, err
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
|
||||
// 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) {
|
||||
channelID := ChannelID{Conversation: conversation, Channel: channel}
|
||||
|
||||
_, exists := cps.channelGetMostRecentMessagesStmts[channelID]
|
||||
if !exists {
|
||||
conversationStmt, err := cps.db.Prepare(fmt.Sprintf(getMostRecentMessagesSQLStmt, conversation, channel))
|
||||
if err != nil {
|
||||
log.Errorf("error executing transaction: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
cps.channelGetMostRecentMessagesStmts[channelID] = conversationStmt
|
||||
}
|
||||
|
||||
rows, err := cps.channelGetMostRecentMessagesStmts[channelID].Query(limit, offset)
|
||||
if err != nil {
|
||||
log.Errorf("error executing query: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
var conversationMessages []model.ConversationMessage
|
||||
defer rows.Close()
|
||||
for {
|
||||
result := rows.Next()
|
||||
if !result {
|
||||
return conversationMessages, nil
|
||||
}
|
||||
var id int
|
||||
var body string
|
||||
var attributes []byte
|
||||
var sig string
|
||||
var contenthash string
|
||||
err = rows.Scan(&id, &body, &attributes, &sig, &contenthash)
|
||||
if err != nil {
|
||||
return conversationMessages, err
|
||||
}
|
||||
conversationMessages = append(conversationMessages, model.ConversationMessage{ID: id, Body: body, Attr: model.DeserializeAttributes(attributes), Signature: sig, ContentHash: contenthash})
|
||||
}
|
||||
}
|
||||
|
||||
// PurgeConversationChannel deletes all message for a conversation channel.
|
||||
func (cps *CwtchProfileStorage) PurgeConversationChannel(conversation int, channel int) error {
|
||||
conversationStmt, err := cps.db.Prepare(fmt.Sprintf(purgeMessagesFromConversationSQLStmt, conversation, channel))
|
||||
if err != nil {
|
||||
log.Errorf("error executing transaction: %v", err)
|
||||
return err
|
||||
}
|
||||
conversationStmt.Exec()
|
||||
return conversationStmt.Close()
|
||||
}
|
||||
|
||||
// PurgeNonSavedMessages deletes all message conversations that are not explicitly set to saved.
|
||||
func (cps *CwtchProfileStorage) PurgeNonSavedMessages() {
|
||||
// Purge Messages that are not stored...
|
||||
ci, err := cps.FetchConversations()
|
||||
if err == nil {
|
||||
for _, conversation := range ci {
|
||||
if !conversation.IsGroup() && !conversation.IsServer() {
|
||||
if conversation.Attributes[attr.LocalScope.ConstructScopedZonedPath(attr.ProfileZone.ConstructZonedPath(event.SaveHistoryKey)).ToString()] != event.SaveHistoryConfirmed {
|
||||
log.Infof("purging conversation...")
|
||||
// TODO: At some point in the future this needs to iterate over channels and make a decision for each on..
|
||||
cps.PurgeConversationChannel(conversation.ID, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Close closes the underlying database and prepared statements
|
||||
func (cps *CwtchProfileStorage) Close() {
|
||||
if cps.db != nil {
|
||||
|
||||
cps.PurgeNonSavedMessages()
|
||||
|
||||
cps.insertProfileKeyValueStmt.Close()
|
||||
cps.selectProfileKeyValueStmt.Close()
|
||||
|
||||
cps.insertConversationStmt.Close()
|
||||
cps.fetchAllConversationsStmt.Close()
|
||||
cps.selectConversationStmt.Close()
|
||||
cps.selectConversationByHandleStmt.Close()
|
||||
cps.acceptConversationStmt.Close()
|
||||
cps.deleteConversationStmt.Close()
|
||||
cps.setConversationAttributesStmt.Close()
|
||||
cps.setConversationACLStmt.Close()
|
||||
|
||||
for _, v := range cps.channelInsertStmts {
|
||||
v.Close()
|
||||
}
|
||||
for _, v := range cps.channelUpdateMessageStmts {
|
||||
v.Close()
|
||||
}
|
||||
for _, v := range cps.channelGetMessageStmts {
|
||||
v.Close()
|
||||
}
|
||||
for _, v := range cps.channelGetMessageBySignatureStmts {
|
||||
v.Close()
|
||||
}
|
||||
for _, v := range cps.channelGetCountStmts {
|
||||
v.Close()
|
||||
}
|
||||
for _, v := range cps.channelGetMostRecentMessagesStmts {
|
||||
v.Close()
|
||||
}
|
||||
for _, v := range cps.channelGetMessageByContentHashStmts {
|
||||
v.Close()
|
||||
}
|
||||
|
||||
cps.db.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// Delete unconditionally destroys the profile directory associated with the store.
|
||||
// This is unrecoverable.
|
||||
func (cps *CwtchProfileStorage) Delete() {
|
||||
err := os.RemoveAll(cps.ProfileDirectory)
|
||||
if err != nil {
|
||||
log.Errorf("error deleting profile directory", err)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,118 @@
|
|||
package peer
|
||||
|
||||
import (
|
||||
"cwtch.im/cwtch/event"
|
||||
"cwtch.im/cwtch/model"
|
||||
"cwtch.im/cwtch/model/attr"
|
||||
"cwtch.im/cwtch/protocol/connections"
|
||||
"git.openprivacy.ca/openprivacy/connectivity"
|
||||
)
|
||||
|
||||
// AccessPeeringState provides access to functions relating to the underlying connections of a peer.
|
||||
type AccessPeeringState interface {
|
||||
GetPeerState(string) connections.ConnectionState
|
||||
}
|
||||
|
||||
// ModifyPeeringState is a meta-interface intended to restrict callers to modify-only access to connection peers
|
||||
type ModifyPeeringState interface {
|
||||
BlockUnknownConnections()
|
||||
AllowUnknownConnections()
|
||||
PeerWithOnion(string)
|
||||
JoinServer(string) error
|
||||
}
|
||||
|
||||
// ModifyContactsAndPeers is a meta-interface intended to restrict a call to reading and modifying contacts
|
||||
// and peers.
|
||||
type ModifyContactsAndPeers interface {
|
||||
ModifyPeeringState
|
||||
}
|
||||
|
||||
// ReadServers provides access to the servers
|
||||
type ReadServers interface {
|
||||
GetServers() []string
|
||||
}
|
||||
|
||||
// ModifyGroups provides write-only access add/edit/remove new groups
|
||||
type ModifyGroups interface {
|
||||
ImportGroup(string) (int, error)
|
||||
StartGroup(string, string) (int, error)
|
||||
}
|
||||
|
||||
// ModifyServers provides write-only access to servers
|
||||
type ModifyServers interface {
|
||||
AddServer(string) (string, error)
|
||||
ResyncServer(onion string) error
|
||||
}
|
||||
|
||||
// SendMessages enables a caller to sender messages to a contact
|
||||
type SendMessages interface {
|
||||
SendMessage(conversation int, message string) error
|
||||
SendInviteToConversation(conversationID int, inviteConversationID int) error
|
||||
SendScopedZonedGetValToContact(conversationID int, scope attr.Scope, zone attr.Zone, key string)
|
||||
}
|
||||
|
||||
// CwtchPeer provides us with a way of testing systems built on top of cwtch without having to
|
||||
// directly implement a cwtchPeer.
|
||||
type CwtchPeer interface {
|
||||
|
||||
// Core Cwtch Peer Functions that should not be exposed to
|
||||
// most functions
|
||||
Init(event.Manager)
|
||||
|
||||
GenerateProtocolEngine(acn connectivity.ACN, bus event.Manager) (connections.Engine, error)
|
||||
|
||||
AutoHandleEvents(events []event.Type)
|
||||
Listen()
|
||||
StartPeersConnections()
|
||||
StartServerConnections()
|
||||
Shutdown()
|
||||
|
||||
// GetOnion is deprecated. If you find yourself needing to rely on this method it is time
|
||||
// to consider replacing this with a GetAddress(es) function that can fully expand cwtch beyond the boundaries
|
||||
// of tor v3 onion services.
|
||||
// Deprecated
|
||||
GetOnion() string
|
||||
|
||||
// SetScopedZonedAttribute allows the setting of an attribute by scope and zone
|
||||
// scope.zone.key = value
|
||||
SetScopedZonedAttribute(scope attr.Scope, zone attr.Zone, key string, value string)
|
||||
|
||||
// GetScopedZonedAttribute allows the retrieval of an attribute by scope and zone
|
||||
// scope.zone.key = value
|
||||
GetScopedZonedAttribute(scope attr.Scope, zone attr.Zone, key string) (string, bool)
|
||||
|
||||
AccessPeeringState
|
||||
ModifyPeeringState
|
||||
|
||||
ModifyGroups
|
||||
|
||||
ReadServers
|
||||
ModifyServers
|
||||
|
||||
SendMessages
|
||||
|
||||
// Import Bundle
|
||||
ImportBundle(string) error
|
||||
|
||||
// New Unified Conversation Interfaces
|
||||
NewContactConversation(handle string, acl model.AccessControl, accepted bool) (int, error)
|
||||
FetchConversations() ([]*model.Conversation, error)
|
||||
GetConversationInfo(conversation int) (*model.Conversation, error)
|
||||
FetchConversationInfo(handle string) (*model.Conversation, error)
|
||||
AcceptConversation(conversation int) error
|
||||
BlockConversation(conversation int) error
|
||||
SetConversationAttribute(conversation int, path attr.ScopedZonedPath, value string) error
|
||||
GetConversationAttribute(conversation int, path attr.ScopedZonedPath) (string, error)
|
||||
DeleteConversation(conversation int) error
|
||||
|
||||
// New Unified Conversation Channel Interfaces
|
||||
GetChannelMessage(conversation int, channel int, id int) (string, model.Attributes, error)
|
||||
GetChannelMessageCount(conversation int, channel int) (int, error)
|
||||
GetChannelMessageByContentHash(conversation int, channel int, contenthash string) (int, error)
|
||||
GetMostRecentMessages(conversation int, channel int, offset int, limit int) ([]model.ConversationMessage, error)
|
||||
UpdateMessageAttribute(conversation int, channel int, id int, key string, value string) error
|
||||
|
||||
ShareFile(fileKey string, serializedManifest string)
|
||||
CheckPassword(password string) bool
|
||||
Delete()
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
package peer
|
||||
|
||||
import "errors"
|
||||
|
||||
// Response is a wrapper to better semantically convey the response type...
|
||||
type Response error
|
||||
|
||||
const errorSeparator = "."
|
||||
|
||||
// ConstructResponse is a helper function for creating Response structures.
|
||||
func ConstructResponse(prefix string, error string) Response {
|
||||
return errors.New(prefix + errorSeparator + error)
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
package peer
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// SQLCreateTableProfileKeyValue creates the Profile Key Value Table
|
||||
const SQLCreateTableProfileKeyValue = `create table if not exists profile_kv (KeyType text, KeyName text, KeyValue blob, UNIQUE (KeyType,KeyName));`
|
||||
|
||||
// SQLCreateTableConversations creates the Profile Key Value Table
|
||||
const SQLCreateTableConversations = `create table if not exists conversations (ID integer unique primary key autoincrement, Handle text, Attributes blob, ACL blob, Accepted bool);`
|
||||
|
||||
// initializeDatabase executes all the sql statements necessary to construct the base of the database.
|
||||
// db must be open
|
||||
func initializeDatabase(db *sql.DB) error {
|
||||
|
||||
_, err := db.Exec(SQLCreateTableProfileKeyValue)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error On Executing Query: %v %v", SQLCreateTableProfileKeyValue, err)
|
||||
}
|
||||
|
||||
_, err = db.Exec(SQLCreateTableConversations)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error On Executing Query: %v %v", SQLCreateTableConversations, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,167 @@
|
|||
package peer
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"git.openprivacy.ca/openprivacy/log"
|
||||
"golang.org/x/crypto/pbkdf2"
|
||||
"golang.org/x/crypto/sha3"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
const versionFile = "VERSION"
|
||||
const version = "2"
|
||||
const saltFile = "SALT"
|
||||
|
||||
// CreateKeySalt derives a key and salt from a password: returns key, salt, err
|
||||
func CreateKeySalt(password string) ([32]byte, [128]byte, error) {
|
||||
var salt [128]byte
|
||||
if _, err := io.ReadFull(rand.Reader, salt[:]); err != nil {
|
||||
log.Errorf("Cannot read from random: %v\n", err)
|
||||
return [32]byte{}, salt, err
|
||||
}
|
||||
dk := pbkdf2.Key([]byte(password), salt[:], 4096, 32, sha3.New512)
|
||||
|
||||
var dkr [32]byte
|
||||
copy(dkr[:], dk)
|
||||
return dkr, salt, nil
|
||||
}
|
||||
|
||||
// createKey derives a key from a password and salt
|
||||
func createKey(password string, salt []byte) [32]byte {
|
||||
dk := pbkdf2.Key([]byte(password), salt, 4096, 32, sha3.New512)
|
||||
|
||||
var dkr [32]byte
|
||||
copy(dkr[:], dk)
|
||||
return dkr
|
||||
}
|
||||
|
||||
func initV2Directory(directory, password string) ([32]byte, [128]byte, error) {
|
||||
os.Mkdir(directory, 0700)
|
||||
|
||||
key, salt, err := CreateKeySalt(password)
|
||||
if err != nil {
|
||||
log.Errorf("Could not create key for profile store from password: %v\n", err)
|
||||
return [32]byte{}, [128]byte{}, err
|
||||
}
|
||||
|
||||
if err = ioutil.WriteFile(path.Join(directory, versionFile), []byte(version), 0600); err != nil {
|
||||
log.Errorf("Could not write version file: %v", err)
|
||||
return [32]byte{}, [128]byte{}, err
|
||||
}
|
||||
|
||||
if err = ioutil.WriteFile(path.Join(directory, saltFile), salt[:], 0600); err != nil {
|
||||
log.Errorf("Could not write salt file: %v", err)
|
||||
return [32]byte{}, [128]byte{}, err
|
||||
}
|
||||
|
||||
return key, salt, nil
|
||||
}
|
||||
|
||||
func openEncryptedDatabase(profileDirectory string, password string, createIfNotExists bool) (*sql.DB, error) {
|
||||
salt, err := ioutil.ReadFile(path.Join(profileDirectory, saltFile))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
key := createKey(password, salt)
|
||||
dbPath := filepath.Join(profileDirectory, "db")
|
||||
|
||||
if !createIfNotExists {
|
||||
if _, err := os.Stat(dbPath); errors.Is(err, os.ErrNotExist) {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
dbname := fmt.Sprintf("%v?_pragma_key=x'%x'&_pragma_cipher_page_size=8192", dbPath, key)
|
||||
db, err := sql.Open("sqlite3", dbname)
|
||||
if err != nil {
|
||||
log.Errorf("could not open encrypted database", err)
|
||||
return nil, err
|
||||
}
|
||||
return db, nil
|
||||
}
|
||||
|
||||
// CreateEncryptedStorePeer creates a *new* Cwtch Profile backed by an encrypted datastore
|
||||
func CreateEncryptedStorePeer(profileDirectory string, name string, password string) (CwtchPeer, error) {
|
||||
log.Debugf("Initializing Encrypted Storage Directory")
|
||||
_, _, err := initV2Directory(profileDirectory, password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Debugf("Opening Encrypted Database")
|
||||
db, err := openEncryptedDatabase(profileDirectory, password, true)
|
||||
if db == nil || err != nil {
|
||||
return nil, fmt.Errorf("unable to open encrypted database: error: %v", err)
|
||||
}
|
||||
|
||||
log.Debugf("Initializing Database")
|
||||
err = initializeDatabase(db)
|
||||
|
||||
if err != nil {
|
||||
db.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Debugf("Creating Cwtch Profile Backed By Encrypted Database")
|
||||
|
||||
cps, err := NewCwtchProfileStorage(db, profileDirectory)
|
||||
if err != nil {
|
||||
db.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return NewProfileWithEncryptedStorage(name, cps), nil
|
||||
}
|
||||
|
||||
// CreateEncryptedStore creates a encrypted datastore
|
||||
func CreateEncryptedStore(profileDirectory string, password string) (*CwtchProfileStorage, error) {
|
||||
|
||||
log.Debugf("Creating Encrypted Database")
|
||||
db, err := openEncryptedDatabase(profileDirectory, password, true)
|
||||
if db == nil || err != nil {
|
||||
return nil, fmt.Errorf("unable to open encrypted database: error: %v", err)
|
||||
}
|
||||
|
||||
log.Debugf("Initializing Database")
|
||||
err = initializeDatabase(db)
|
||||
|
||||
if err != nil {
|
||||
db.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Debugf("Creating Cwtch Profile Backed By Encrypted Database")
|
||||
|
||||
cps, err := NewCwtchProfileStorage(db, profileDirectory)
|
||||
if err != nil {
|
||||
db.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return cps, nil
|
||||
}
|
||||
|
||||
// FromEncryptedDatabase constructs a Cwtch Profile from an existing Encrypted Database
|
||||
func FromEncryptedDatabase(profileDirectory string, password string) (CwtchPeer, error) {
|
||||
log.Infof("Loading Encrypted Profile: %v", profileDirectory)
|
||||
db, err := openEncryptedDatabase(profileDirectory, password, false)
|
||||
if db == nil || err != nil {
|
||||
return nil, fmt.Errorf("unable to open encrypted database: error: %v", err)
|
||||
}
|
||||
|
||||
log.Debugf("Initializing Profile from Encrypted Storage")
|
||||
cps, err := NewCwtchProfileStorage(db, profileDirectory)
|
||||
if err != nil {
|
||||
db.Close()
|
||||
return nil, err
|
||||
}
|
||||
return FromEncryptedStorage(cps), nil
|
||||
}
|
|
@ -140,7 +140,7 @@ func (e *engine) eventHandler() {
|
|||
case event.InvitePeerToGroup:
|
||||
err := e.sendPeerMessage(ev.Data[event.RemotePeer], pmodel.PeerMessage{ID: ev.EventID, Context: event.ContextInvite, Data: []byte(ev.Data[event.GroupInvite])})
|
||||
if err != nil {
|
||||
|
||||
e.eventManager.Publish(event.NewEvent(event.SendMessageToPeerError, map[event.Field]string{event.EventContext: string(event.InvitePeerToGroup), event.RemotePeer: ev.Data[event.RemotePeer], event.EventID: ev.EventID, event.Error: "peer is offline or the connection has yet to finalize"}))
|
||||
}
|
||||
case event.JoinServer:
|
||||
signature, err := base64.StdEncoding.DecodeString(ev.Data[event.Signature])
|
||||
|
@ -169,15 +169,15 @@ func (e *engine) eventHandler() {
|
|||
context = event.ContextRaw
|
||||
}
|
||||
if err := e.sendPeerMessage(ev.Data[event.RemotePeer], pmodel.PeerMessage{ID: ev.EventID, Context: context, Data: []byte(ev.Data[event.Data])}); err != nil {
|
||||
e.eventManager.Publish(event.NewEvent(event.SendMessageToPeerError, map[event.Field]string{event.RemotePeer: ev.Data[event.RemotePeer], event.EventID: ev.EventID, event.Error: "peer is offline or the connection has yet to finalize"}))
|
||||
e.eventManager.Publish(event.NewEvent(event.SendMessageToPeerError, map[event.Field]string{event.EventContext: string(event.SendMessageToPeer), event.RemotePeer: ev.Data[event.RemotePeer], event.EventID: ev.EventID, event.Error: "peer is offline or the connection has yet to finalize"}))
|
||||
}
|
||||
case event.SendGetValMessageToPeer:
|
||||
if err := e.sendGetValToPeer(ev.EventID, ev.Data[event.RemotePeer], ev.Data[event.Scope], ev.Data[event.Path]); err != nil {
|
||||
e.eventManager.Publish(event.NewEvent(event.SendMessageToPeerError, map[event.Field]string{event.RemotePeer: ev.Data[event.RemotePeer], event.EventID: ev.EventID, event.Error: err.Error()}))
|
||||
e.eventManager.Publish(event.NewEvent(event.SendMessageToPeerError, map[event.Field]string{event.EventContext: string(event.SendGetValMessageToPeer), event.RemotePeer: ev.Data[event.RemotePeer], event.EventID: ev.EventID, event.Error: err.Error()}))
|
||||
}
|
||||
case event.SendRetValMessageToPeer:
|
||||
if err := e.sendRetValToPeer(ev.EventID, ev.Data[event.RemotePeer], ev.Data[event.Data], ev.Data[event.Exists]); err != nil {
|
||||
e.eventManager.Publish(event.NewEvent(event.SendMessageToPeerError, map[event.Field]string{event.RemotePeer: ev.Data[event.RemotePeer], event.EventID: ev.EventID, event.Error: err.Error()}))
|
||||
e.eventManager.Publish(event.NewEvent(event.SendMessageToPeerError, map[event.Field]string{event.EventContext: string(event.SendRetValMessageToPeer), event.RemotePeer: ev.Data[event.RemotePeer], event.EventID: ev.EventID, event.Error: err.Error()}))
|
||||
}
|
||||
case event.SetPeerAuthorization:
|
||||
auth := model.Authorization(ev.Data[event.Authorization])
|
||||
|
@ -274,6 +274,15 @@ func (e *engine) listenFn() {
|
|||
func (e *engine) Shutdown() {
|
||||
e.shuttingDown = true
|
||||
e.service.Shutdown()
|
||||
|
||||
e.ephemeralServices.Range(func(_, service interface{}) bool {
|
||||
connection, ok := service.(*tor.BaseOnionService)
|
||||
if ok {
|
||||
log.Infof("shutting down ephemeral service")
|
||||
connection.Shutdown()
|
||||
}
|
||||
return true
|
||||
})
|
||||
e.queue.Shutdown()
|
||||
}
|
||||
|
||||
|
@ -403,13 +412,6 @@ func (e *engine) serverConnecting(onion string) {
|
|||
}))
|
||||
}
|
||||
|
||||
func (e *engine) serverConnected(onion string) {
|
||||
e.eventManager.Publish(event.NewEvent(event.ServerStateChange, map[event.Field]string{
|
||||
event.GroupServer: onion,
|
||||
event.ConnectionState: ConnectionStateName[CONNECTED],
|
||||
}))
|
||||
}
|
||||
|
||||
func (e *engine) serverAuthed(onion string) {
|
||||
e.eventManager.Publish(event.NewEvent(event.ServerStateChange, map[event.Field]string{
|
||||
event.GroupServer: onion,
|
||||
|
@ -585,7 +587,7 @@ func (e *engine) handlePeerMessage(hostname string, eventID string, context stri
|
|||
}
|
||||
} else {
|
||||
// Fall through handler for the default text conversation.
|
||||
e.eventManager.Publish(event.NewEvent(event.NewMessageFromPeer, 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)}))
|
||||
|
||||
// Send an explicit acknowledgement
|
||||
// Every other protocol should have a explicit acknowledgement message e.g. value lookups have responses, and file handling has an explicit flow
|
||||
|
|
|
@ -185,7 +185,7 @@ func (ta *TokenBoardClient) MakePayment() error {
|
|||
log.Debugf("Waiting for successful PoW Auth...")
|
||||
|
||||
connected, err := client.Connect(ta.tokenServiceOnion, powTokenApp)
|
||||
if connected == true && err == nil {
|
||||
if connected && err == nil {
|
||||
log.Debugf("Waiting for successful Token Acquisition...")
|
||||
conn, err := client.WaitForCapabilityOrClose(ta.tokenServiceOnion, applications.HasTokensCapability)
|
||||
if err == nil {
|
||||
|
|
|
@ -201,7 +201,7 @@ func (m *Manifest) StoreChunk(id uint64, contents []byte) (uint64, error) {
|
|||
// Write the contents of the chunk to the file
|
||||
_, err = m.openFd.Write(contents)
|
||||
|
||||
if err == nil && m.chunkComplete[id] == false {
|
||||
if err == nil && !m.chunkComplete[id] {
|
||||
m.chunkComplete[id] = true
|
||||
m.progress++
|
||||
}
|
||||
|
|
|
@ -28,17 +28,22 @@ func TestManifest(t *testing.T) {
|
|||
|
||||
t.Logf("%v", manifest)
|
||||
|
||||
// Try to tread the chunk
|
||||
contents, err := manifest.GetChunkBytes(1)
|
||||
// Try to read the chunk
|
||||
_, err = manifest.GetChunkBytes(1)
|
||||
if err == nil {
|
||||
t.Fatalf("chunk fetch should have thrown an error")
|
||||
}
|
||||
|
||||
contents, err = manifest.GetChunkBytes(0)
|
||||
_, err = manifest.GetChunkBytes(0)
|
||||
if err != nil {
|
||||
t.Fatalf("chunk fetch error: %v", err)
|
||||
}
|
||||
contents, err = manifest.GetChunkBytes(0)
|
||||
_, err = manifest.GetChunkBytes(0)
|
||||
if err != nil {
|
||||
t.Fatalf("chunk fetch error: %v", err)
|
||||
}
|
||||
|
||||
_, err = manifest.GetChunkBytes(0)
|
||||
if err != nil {
|
||||
t.Fatalf("chunk fetch error: %v", err)
|
||||
}
|
||||
|
@ -46,7 +51,6 @@ func TestManifest(t *testing.T) {
|
|||
json, _ := json.Marshal(manifest)
|
||||
t.Logf("%s", json)
|
||||
|
||||
t.Logf("%s", contents)
|
||||
}
|
||||
|
||||
func TestManifestLarge(t *testing.T) {
|
||||
|
@ -113,7 +117,20 @@ func TestManifestLarge(t *testing.T) {
|
|||
t.Fatalf("could not store chunk %v %v", i, err)
|
||||
}
|
||||
|
||||
// Attempt to store the chunk in an invalid position...
|
||||
_, err = cwtchPngOutManifest.StoreChunk(uint64(i+1), contents)
|
||||
if err == nil {
|
||||
t.Fatalf("incorrect chunk store")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Attempt to store an invalid chunk...should trigger an error
|
||||
_, err = cwtchPngOutManifest.StoreChunk(uint64(len(cwtchPngManifest.Chunks)), []byte{0xff})
|
||||
if err == nil {
|
||||
t.Fatalf("incorrect chunk store")
|
||||
}
|
||||
|
||||
err = cwtchPngOutManifest.VerifyFile()
|
||||
if err != nil {
|
||||
t.Fatalf("could not verify file %v", err)
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
Feature: legacy groups model api
|
||||
In order for multiple people to chat async
|
||||
They share a secret, and use a server to
|
||||
support offline delivery
|
||||
|
||||
Scenario: Create a Group
|
||||
Given a group on "iikv7tizbyxc42rsagnjxss65h3nfiwrkkoiikh7ui27r5xkav7gzuid"
|
||||
Then the GroupID should be cryptographically bound to "iikv7tizbyxc42rsagnjxss65h3nfiwrkkoiikh7ui27r5xkav7gzuid"
|
||||
|
||||
Scenario: Generating an Invite
|
||||
Given a group on "iikv7tizbyxc42rsagnjxss65h3nfiwrkkoiikh7ui27r5xkav7gzuid"
|
||||
When I generate an invite
|
||||
Then the invite should validate
|
||||
|
||||
Scenario Template: Validating an Invalid Invite
|
||||
When I validate the invite "<invite>"
|
||||
Then I should get a validation error "<error>"
|
||||
|
||||
Examples:
|
||||
| invite | error |
|
||||
| torv3 | invite has invalid structure |
|
||||
| not an invite | invite has invalid structure |
|
|
@ -0,0 +1,8 @@
|
|||
Feature: Cwtch Profile API
|
||||
|
||||
Scenario: Creating a Profile
|
||||
When a profile is created by "alice"
|
||||
Then the profile should have an attribute "public.profile.name" with the value "alice"
|
||||
And the profile should have a key "Ed25519PrivateKey"
|
||||
And the profile should have a key "Ed25519PublicKey"
|
||||
|
|
@ -1,94 +1,17 @@
|
|||
package storage
|
||||
|
||||
import (
|
||||
"cwtch.im/cwtch/event"
|
||||
"cwtch.im/cwtch/model"
|
||||
"cwtch.im/cwtch/storage/v0"
|
||||
"cwtch.im/cwtch/storage/v1"
|
||||
"git.openprivacy.ca/openprivacy/log"
|
||||
"io/ioutil"
|
||||
"path"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
const profileFilename = "profile"
|
||||
const versionFile = "VERSION"
|
||||
const currentVersion = 1
|
||||
|
||||
// ProfileStore is an interface to managing the storage of Cwtch Profiles
|
||||
type ProfileStore interface {
|
||||
Shutdown()
|
||||
Delete()
|
||||
GetProfileCopy(timeline bool) *model.Profile
|
||||
GetNewPeerMessage() *event.Event
|
||||
GetStatusMessages() []*event.Event
|
||||
CheckPassword(string) bool
|
||||
}
|
||||
|
||||
// CreateProfileWriterStore creates a profile store backed by a filestore listening for events and saving them
|
||||
// directory should be $appDir/profiles/$rand
|
||||
func CreateProfileWriterStore(eventManager event.Manager, directory, password string, profile *model.Profile) ProfileStore {
|
||||
return v1.CreateProfileWriterStore(eventManager, directory, password, profile)
|
||||
}
|
||||
|
||||
// LoadProfileWriterStore loads a profile store from filestore listening for events and saving them
|
||||
// directory should be $appDir/profiles/$rand
|
||||
func LoadProfileWriterStore(eventManager event.Manager, directory, password string) (ProfileStore, error) {
|
||||
versionCheckUpgrade(directory, password)
|
||||
|
||||
return v1.LoadProfileWriterStore(eventManager, directory, password)
|
||||
}
|
||||
|
||||
// ReadProfile reads a profile from storage and returns the profile
|
||||
// Should only be called for cache refresh of the profile after a ProfileWriterStore has opened
|
||||
// (and upgraded) the store, and thus supplied the key/salt
|
||||
func ReadProfile(directory string, key [32]byte, salt [128]byte) (*model.Profile, error) {
|
||||
return v1.ReadProfile(directory, key, salt)
|
||||
}
|
||||
|
||||
// NewProfile creates a new profile for use in the profile store.
|
||||
func NewProfile(name string) *model.Profile {
|
||||
profile := model.GenerateNewProfile(name)
|
||||
return profile
|
||||
}
|
||||
|
||||
// ********* Versioning and upgrade **********
|
||||
|
||||
func detectVersion(directory string) int {
|
||||
vnumberStr, err := ioutil.ReadFile(path.Join(directory, versionFile))
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
vnumber, err := strconv.Atoi(string(vnumberStr))
|
||||
if err != nil {
|
||||
log.Errorf("Could not parse VERSION file contents: '%v' - %v\n", vnumber, err)
|
||||
return -1
|
||||
}
|
||||
return vnumber
|
||||
}
|
||||
|
||||
func upgradeV0ToV1(directory, password string) error {
|
||||
log.Debugln("Attempting storage v0 to v1: Reading v0 profile...")
|
||||
profile, err := v0.ReadProfile(directory, password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Debugln("Attempting storage v0 to v1: Writing v1 profile...")
|
||||
return v1.UpgradeV0Profile(profile, directory, password)
|
||||
}
|
||||
|
||||
func versionCheckUpgrade(directory, password string) {
|
||||
version := detectVersion(directory)
|
||||
log.Debugf("versionCheck: %v\n", version)
|
||||
if version == -1 {
|
||||
return
|
||||
}
|
||||
if version == 0 {
|
||||
err := upgradeV0ToV1(directory, password)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
//version = 1
|
||||
}
|
||||
func LoadProfileWriterStore(directory, password string) (ProfileStore, error) {
|
||||
return v1.LoadProfileWriterStore(directory, password)
|
||||
}
|
||||
|
|
|
@ -1,76 +0,0 @@
|
|||
// Known race issue with event bus channel closure
|
||||
|
||||
package storage
|
||||
|
||||
import (
|
||||
"cwtch.im/cwtch/event"
|
||||
"cwtch.im/cwtch/model"
|
||||
"cwtch.im/cwtch/storage/v0"
|
||||
"fmt"
|
||||
"git.openprivacy.ca/openprivacy/log"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
const testingDir = "./testing"
|
||||
const filenameBase = "testStream"
|
||||
const password = "asdfqwer"
|
||||
const line1 = "Hello from storage!"
|
||||
const testProfileName = "Alice"
|
||||
const testKey = "key"
|
||||
const testVal = "value"
|
||||
const testInitialMessage = "howdy"
|
||||
const testMessage = "Hello from storage"
|
||||
|
||||
func TestProfileStoreUpgradeV0toV1(t *testing.T) {
|
||||
log.SetLevel(log.LevelDebug)
|
||||
os.RemoveAll(testingDir)
|
||||
eventBus := event.NewEventManager()
|
||||
|
||||
queue := event.NewQueue()
|
||||
eventBus.Subscribe(event.ChangePasswordSuccess, queue)
|
||||
|
||||
fmt.Println("Creating and initializing v0 profile and store...")
|
||||
profile := NewProfile(testProfileName)
|
||||
profile.AddContact("2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd", &model.PublicProfile{Attributes: map[string]string{string(model.KeyTypeServerOnion): "2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd"}})
|
||||
|
||||
ps1 := v0.NewProfileWriterStore(eventBus, testingDir, password, profile)
|
||||
|
||||
groupid, invite, err := profile.StartGroup("2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd")
|
||||
if err != nil {
|
||||
t.Errorf("Creating group: %v\n", err)
|
||||
}
|
||||
if err != nil {
|
||||
t.Errorf("Creating group invite: %v\n", err)
|
||||
}
|
||||
|
||||
ps1.AddGroup(invite)
|
||||
|
||||
fmt.Println("Sending 200 messages...")
|
||||
|
||||
for i := 0; i < 200; i++ {
|
||||
ps1.AddGroupMessage(groupid, time.Now().Format(time.RFC3339Nano), time.Now().Format(time.RFC3339Nano), profile.Onion, testMessage, []byte{byte(i)})
|
||||
}
|
||||
|
||||
fmt.Println("Shutdown v0 profile store...")
|
||||
ps1.Shutdown()
|
||||
|
||||
fmt.Println("New v1 Profile store...")
|
||||
ps2, err := LoadProfileWriterStore(eventBus, testingDir, password)
|
||||
if err != nil {
|
||||
t.Errorf("Error createing new profileStore with new password: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
profile2 := ps2.GetProfileCopy(true)
|
||||
|
||||
if profile2.Groups[groupid] == nil {
|
||||
t.Errorf("Failed to load group %v\n", groupid)
|
||||
return
|
||||
}
|
||||
|
||||
if len(profile2.Groups[groupid].Timeline.Messages) != 200 {
|
||||
t.Errorf("Failed to load group's 200 messages, instead got %v\n", len(profile2.Groups[groupid].Timeline.Messages))
|
||||
}
|
||||
}
|
|
@ -1,70 +0,0 @@
|
|||
package v0
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"errors"
|
||||
"git.openprivacy.ca/openprivacy/log"
|
||||
"golang.org/x/crypto/nacl/secretbox"
|
||||
"golang.org/x/crypto/pbkdf2"
|
||||
"golang.org/x/crypto/sha3"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"path"
|
||||
)
|
||||
|
||||
// createKey derives a key from a password
|
||||
func createKey(password string) ([32]byte, [128]byte, error) {
|
||||
var salt [128]byte
|
||||
if _, err := io.ReadFull(rand.Reader, salt[:]); err != nil {
|
||||
log.Errorf("Cannot read from random: %v\n", err)
|
||||
return [32]byte{}, salt, err
|
||||
}
|
||||
dk := pbkdf2.Key([]byte(password), salt[:], 4096, 32, sha3.New512)
|
||||
|
||||
var dkr [32]byte
|
||||
copy(dkr[:], dk)
|
||||
return dkr, salt, nil
|
||||
}
|
||||
|
||||
//encryptFileData encrypts the cwtchPeer via the specified key.
|
||||
func encryptFileData(data []byte, key [32]byte) ([]byte, error) {
|
||||
var nonce [24]byte
|
||||
|
||||
if _, err := io.ReadFull(rand.Reader, nonce[:]); err != nil {
|
||||
log.Errorf("Cannot read from random: %v\n", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
encrypted := secretbox.Seal(nonce[:], data, &nonce, &key)
|
||||
return encrypted, nil
|
||||
}
|
||||
|
||||
//decryptFile decrypts the passed ciphertext into a cwtchPeer via the specified key.
|
||||
func decryptFile(ciphertext []byte, key [32]byte) ([]byte, error) {
|
||||
var decryptNonce [24]byte
|
||||
copy(decryptNonce[:], ciphertext[:24])
|
||||
decrypted, ok := secretbox.Open(nil, ciphertext[24:], &decryptNonce, &key)
|
||||
if ok {
|
||||
return decrypted, nil
|
||||
}
|
||||
return nil, errors.New("Failed to decrypt")
|
||||
}
|
||||
|
||||
// Load instantiates a cwtchPeer from the file store
|
||||
func readEncryptedFile(directory, filename, password string) ([]byte, error) {
|
||||
encryptedbytes, err := ioutil.ReadFile(path.Join(directory, filename))
|
||||
if err == nil && len(encryptedbytes) > 128 {
|
||||
var dkr [32]byte
|
||||
//Separate the salt from the encrypted bytes, then generate the derived key
|
||||
salt, encryptedbytes := encryptedbytes[0:128], encryptedbytes[128:]
|
||||
dk := pbkdf2.Key([]byte(password), salt, 4096, 32, sha3.New512)
|
||||
copy(dkr[:], dk)
|
||||
|
||||
data, err := decryptFile(encryptedbytes, dkr)
|
||||
if err == nil {
|
||||
return data, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return nil, err
|
||||
}
|
|
@ -1,46 +0,0 @@
|
|||
package v0
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"path"
|
||||
)
|
||||
|
||||
// fileStore stores a cwtchPeer in an encrypted file
|
||||
type fileStore struct {
|
||||
directory string
|
||||
filename string
|
||||
password string
|
||||
}
|
||||
|
||||
// FileStore is a primitive around storing encrypted files
|
||||
type FileStore interface {
|
||||
Read() ([]byte, error)
|
||||
Write(data []byte) error
|
||||
}
|
||||
|
||||
// NewFileStore instantiates a fileStore given a filename and a password
|
||||
func NewFileStore(directory string, filename string, password string) FileStore {
|
||||
filestore := new(fileStore)
|
||||
filestore.password = password
|
||||
filestore.filename = filename
|
||||
filestore.directory = directory
|
||||
return filestore
|
||||
}
|
||||
|
||||
func (fps *fileStore) Read() ([]byte, error) {
|
||||
return readEncryptedFile(fps.directory, fps.filename, fps.password)
|
||||
}
|
||||
|
||||
// write serializes a cwtchPeer to a file
|
||||
func (fps *fileStore) Write(data []byte) error {
|
||||
key, salt, _ := createKey(fps.password)
|
||||
encryptedbytes, err := encryptFileData(data, key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// the salt for the derived key is appended to the front of the file
|
||||
encryptedbytes = append(salt[:], encryptedbytes...)
|
||||
err = ioutil.WriteFile(path.Join(fps.directory, fps.filename), encryptedbytes, 0600)
|
||||
return err
|
||||
}
|
|
@ -1,120 +0,0 @@
|
|||
package v0
|
||||
|
||||
import (
|
||||
"cwtch.im/cwtch/event"
|
||||
"cwtch.im/cwtch/model"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
const groupIDLen = 32
|
||||
const peerIDLen = 56
|
||||
const profileFilename = "profile"
|
||||
|
||||
// ProfileStoreV0 is a legacy profile store used now for upgrading legacy profile stores to newer versions
|
||||
type ProfileStoreV0 struct {
|
||||
fs FileStore
|
||||
streamStores map[string]StreamStore // map [groupId|onion] StreamStore
|
||||
directory string
|
||||
password string
|
||||
profile *model.Profile
|
||||
}
|
||||
|
||||
// NewProfileWriterStore returns a profile store backed by a filestore listening for events and saving them
|
||||
// directory should be $appDir/profiles/$rand
|
||||
func NewProfileWriterStore(eventManager event.Manager, directory, password string, profile *model.Profile) *ProfileStoreV0 {
|
||||
os.Mkdir(directory, 0700)
|
||||
ps := &ProfileStoreV0{fs: NewFileStore(directory, profileFilename, password), password: password, directory: directory, profile: profile, streamStores: map[string]StreamStore{}}
|
||||
if profile != nil {
|
||||
ps.save()
|
||||
}
|
||||
|
||||
return ps
|
||||
}
|
||||
|
||||
// ReadProfile reads a profile from storqage and returns the profile
|
||||
// directory should be $appDir/profiles/$rand
|
||||
func ReadProfile(directory, password string) (*model.Profile, error) {
|
||||
os.Mkdir(directory, 0700)
|
||||
ps := &ProfileStoreV0{fs: NewFileStore(directory, profileFilename, password), password: password, directory: directory, profile: nil, streamStores: map[string]StreamStore{}}
|
||||
|
||||
err := ps.Load()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
profile := ps.getProfileCopy(true)
|
||||
|
||||
return profile, nil
|
||||
}
|
||||
|
||||
/********************************************************************************************/
|
||||
|
||||
// AddGroup For testing, adds a group to the profile (and starts a stream store)
|
||||
func (ps *ProfileStoreV0) AddGroup(invite string) {
|
||||
gid, err := ps.profile.ProcessInvite(invite)
|
||||
if err == nil {
|
||||
ps.save()
|
||||
group := ps.profile.Groups[gid]
|
||||
ps.streamStores[group.GroupID] = NewStreamStore(ps.directory, group.LocalID, ps.password)
|
||||
}
|
||||
}
|
||||
|
||||
// AddGroupMessage for testing, adds a group message
|
||||
func (ps *ProfileStoreV0) AddGroupMessage(groupid string, timeSent, timeRecvied string, remotePeer, data string, signature []byte) {
|
||||
received, _ := time.Parse(time.RFC3339Nano, timeRecvied)
|
||||
sent, _ := time.Parse(time.RFC3339Nano, timeSent)
|
||||
message := model.Message{Received: received, Timestamp: sent, Message: data, PeerID: remotePeer, Signature: signature, PreviousMessageSig: []byte("PreviousSignature")}
|
||||
ss, exists := ps.streamStores[groupid]
|
||||
if exists {
|
||||
ss.Write(message)
|
||||
} else {
|
||||
fmt.Println("ERROR")
|
||||
}
|
||||
}
|
||||
|
||||
// GetNewPeerMessage is for AppService to call on Reload events, to reseed the AppClient with the loaded peers
|
||||
func (ps *ProfileStoreV0) GetNewPeerMessage() *event.Event {
|
||||
message := event.NewEventList(event.NewPeer, event.Identity, ps.profile.LocalID, event.Password, ps.password, event.Status, "running")
|
||||
return &message
|
||||
}
|
||||
|
||||
// Load instantiates a cwtchPeer from the file store
|
||||
func (ps *ProfileStoreV0) Load() error {
|
||||
decrypted, err := ps.fs.Read()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cp := new(model.Profile)
|
||||
err = json.Unmarshal(decrypted, &cp)
|
||||
if err == nil {
|
||||
ps.profile = cp
|
||||
|
||||
for gid, group := range cp.Groups {
|
||||
ss := NewStreamStore(ps.directory, group.LocalID, ps.password)
|
||||
|
||||
cp.Groups[gid].Timeline.SetMessages(ss.Read())
|
||||
ps.streamStores[group.GroupID] = ss
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (ps *ProfileStoreV0) getProfileCopy(timeline bool) *model.Profile {
|
||||
return ps.profile.GetCopy(timeline)
|
||||
}
|
||||
|
||||
// Shutdown saves the storage system
|
||||
func (ps *ProfileStoreV0) Shutdown() {
|
||||
ps.save()
|
||||
}
|
||||
|
||||
/************* Writing *************/
|
||||
|
||||
func (ps *ProfileStoreV0) save() error {
|
||||
bytes, _ := json.Marshal(ps.profile)
|
||||
return ps.fs.Write(bytes)
|
||||
}
|
|
@ -1,70 +0,0 @@
|
|||
// Known race issue with event bus channel closure
|
||||
|
||||
package v0
|
||||
|
||||
import (
|
||||
"cwtch.im/cwtch/event"
|
||||
"cwtch.im/cwtch/model"
|
||||
"log"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
const testProfileName = "Alice"
|
||||
const testKey = "key"
|
||||
const testVal = "value"
|
||||
const testInitialMessage = "howdy"
|
||||
const testMessage = "Hello from storage"
|
||||
|
||||
// NewProfile creates a new profile for use in the profile store.
|
||||
func NewProfile(name string) *model.Profile {
|
||||
profile := model.GenerateNewProfile(name)
|
||||
return profile
|
||||
}
|
||||
|
||||
func TestProfileStoreWriteRead(t *testing.T) {
|
||||
log.Println("profile store test!")
|
||||
os.RemoveAll(testingDir)
|
||||
eventBus := event.NewEventManager()
|
||||
profile := NewProfile(testProfileName)
|
||||
ps1 := NewProfileWriterStore(eventBus, testingDir, password, profile)
|
||||
|
||||
profile.SetAttribute(testKey, testVal)
|
||||
|
||||
groupid, invite, err := profile.StartGroup("2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd")
|
||||
if err != nil {
|
||||
t.Errorf("Creating group: %v\n", err)
|
||||
}
|
||||
if err != nil {
|
||||
t.Errorf("Creating group invite: %v\n", err)
|
||||
}
|
||||
|
||||
ps1.AddGroup(invite)
|
||||
|
||||
ps1.AddGroupMessage(groupid, time.Now().Format(time.RFC3339Nano), time.Now().Format(time.RFC3339Nano), ps1.getProfileCopy(true).Onion, testMessage, []byte{byte(0x01)})
|
||||
|
||||
ps1.Shutdown()
|
||||
|
||||
ps2 := NewProfileWriterStore(eventBus, testingDir, password, nil)
|
||||
err = ps2.Load()
|
||||
if err != nil {
|
||||
t.Errorf("Error createing ProfileStoreV0: %v\n", err)
|
||||
}
|
||||
|
||||
profile = ps2.getProfileCopy(true)
|
||||
if profile.Name != testProfileName {
|
||||
t.Errorf("Profile name from loaded profile incorrect. Expected: '%v' Actual: '%v'\n", testProfileName, profile.Name)
|
||||
}
|
||||
|
||||
v, _ := profile.GetAttribute(testKey)
|
||||
if v != testVal {
|
||||
t.Errorf("Profile attribute '%v' incorrect. Expected: '%v' Actual: '%v'\n", testKey, testVal, v)
|
||||
}
|
||||
|
||||
group2 := ps2.getProfileCopy(true).Groups[groupid]
|
||||
if group2 == nil {
|
||||
t.Errorf("Group not loaded\n")
|
||||
}
|
||||
|
||||
}
|
|
@ -1,145 +0,0 @@
|
|||
package v0
|
||||
|
||||
import (
|
||||
"cwtch.im/cwtch/model"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"git.openprivacy.ca/openprivacy/log"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"sync"
|
||||
)
|
||||
|
||||
const (
|
||||
fileStorePartitions = 16
|
||||
bytesPerFile = 15 * 1024
|
||||
)
|
||||
|
||||
// streamStore is a file-backed implementation of StreamStore using an in memory buffer of ~16KB and a rotating set of files
|
||||
type streamStore struct {
|
||||
password string
|
||||
|
||||
storeDirectory string
|
||||
filenameBase string
|
||||
|
||||
lock sync.Mutex
|
||||
|
||||
// Buffer is used just for current file to write to
|
||||
messages []model.Message
|
||||
bufferByteCount int
|
||||
}
|
||||
|
||||
// StreamStore provides a stream like interface to encrypted storage
|
||||
type StreamStore interface {
|
||||
Read() []model.Message
|
||||
Write(m model.Message)
|
||||
}
|
||||
|
||||
// NewStreamStore returns an initialized StreamStore ready for reading and writing
|
||||
func NewStreamStore(directory string, filenameBase string, password string) (store StreamStore) {
|
||||
ss := &streamStore{storeDirectory: directory, filenameBase: filenameBase, password: password}
|
||||
os.Mkdir(ss.storeDirectory, 0700)
|
||||
|
||||
ss.initBuffer()
|
||||
|
||||
return ss
|
||||
}
|
||||
|
||||
// Read returns all messages from the backing file (not the buffer, for writing to the current file)
|
||||
func (ss *streamStore) Read() (messages []model.Message) {
|
||||
ss.lock.Lock()
|
||||
defer ss.lock.Unlock()
|
||||
|
||||
resp := []model.Message{}
|
||||
|
||||
for i := fileStorePartitions - 1; i >= 0; i-- {
|
||||
filename := fmt.Sprintf("%s.%d", ss.filenameBase, i)
|
||||
|
||||
bytes, err := readEncryptedFile(ss.storeDirectory, filename, ss.password)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
msgs := []model.Message{}
|
||||
json.Unmarshal([]byte(bytes), &msgs)
|
||||
resp = append(resp, msgs...)
|
||||
}
|
||||
|
||||
// 2019.10.10 "Acknowledged" & "ReceivedByServer" are added to the struct, populate it as true for old ones without
|
||||
for i := 0; i < len(resp) && (resp[i].Acknowledged == false && resp[i].ReceivedByServer == false); i++ {
|
||||
resp[i].Acknowledged = true
|
||||
resp[i].ReceivedByServer = true
|
||||
}
|
||||
|
||||
return resp
|
||||
}
|
||||
|
||||
// ****** Writing *******/
|
||||
|
||||
func (ss *streamStore) WriteN(messages []model.Message) {
|
||||
ss.lock.Lock()
|
||||
defer ss.lock.Unlock()
|
||||
|
||||
for _, m := range messages {
|
||||
ss.updateBuffer(m)
|
||||
|
||||
if ss.bufferByteCount > bytesPerFile {
|
||||
ss.updateFile()
|
||||
log.Debugf("rotating log file")
|
||||
ss.rotateFileStore()
|
||||
ss.initBuffer()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Write adds a GroupMessage to the store
|
||||
func (ss *streamStore) Write(m model.Message) {
|
||||
ss.lock.Lock()
|
||||
defer ss.lock.Unlock()
|
||||
ss.updateBuffer(m)
|
||||
ss.updateFile()
|
||||
|
||||
if ss.bufferByteCount > bytesPerFile {
|
||||
log.Debugf("rotating log file")
|
||||
ss.rotateFileStore()
|
||||
ss.initBuffer()
|
||||
}
|
||||
}
|
||||
|
||||
func (ss *streamStore) initBuffer() {
|
||||
ss.messages = []model.Message{}
|
||||
ss.bufferByteCount = 0
|
||||
}
|
||||
|
||||
func (ss *streamStore) updateBuffer(m model.Message) {
|
||||
ss.messages = append(ss.messages, m)
|
||||
ss.bufferByteCount += (104 * 1.5) + len(m.Message)
|
||||
}
|
||||
|
||||
func (ss *streamStore) updateFile() error {
|
||||
msgs, err := json.Marshal(ss.messages)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to marshal group messages %v\n", err)
|
||||
}
|
||||
|
||||
// ENCRYPT
|
||||
key, salt, _ := createKey(ss.password)
|
||||
encryptedMsgs, err := encryptFileData(msgs, key)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to encrypt messages: %v\n", err)
|
||||
return err
|
||||
}
|
||||
encryptedMsgs = append(salt[:], encryptedMsgs...)
|
||||
|
||||
ioutil.WriteFile(path.Join(ss.storeDirectory, fmt.Sprintf("%s.%d", ss.filenameBase, 0)), encryptedMsgs, 0700)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ss *streamStore) rotateFileStore() {
|
||||
os.Remove(path.Join(ss.storeDirectory, fmt.Sprintf("%s.%d", ss.filenameBase, fileStorePartitions-1)))
|
||||
|
||||
for i := fileStorePartitions - 2; i >= 0; i-- {
|
||||
os.Rename(path.Join(ss.storeDirectory, fmt.Sprintf("%s.%d", ss.filenameBase, i)), path.Join(ss.storeDirectory, fmt.Sprintf("%s.%d", ss.filenameBase, i+1)))
|
||||
}
|
||||
}
|
|
@ -1,50 +0,0 @@
|
|||
package v0
|
||||
|
||||
import (
|
||||
"cwtch.im/cwtch/model"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const testingDir = "./testing"
|
||||
const filenameBase = "testStream"
|
||||
const password = "asdfqwer"
|
||||
const line1 = "Hello from storage!"
|
||||
|
||||
func TestStreamStoreWriteRead(t *testing.T) {
|
||||
os.Remove(".test.json")
|
||||
os.RemoveAll(testingDir)
|
||||
os.Mkdir(testingDir, 0777)
|
||||
ss1 := NewStreamStore(testingDir, filenameBase, password)
|
||||
m := model.Message{Message: line1}
|
||||
ss1.Write(m)
|
||||
|
||||
ss2 := NewStreamStore(testingDir, filenameBase, password)
|
||||
messages := ss2.Read()
|
||||
if len(messages) != 1 {
|
||||
t.Errorf("Read messages has wrong length. Expected: 1 Actual: %d\n", len(messages))
|
||||
}
|
||||
if messages[0].Message != line1 {
|
||||
t.Errorf("Read message has wrong content. Expected: '%v' Actual: '%v'\n", line1, messages[0].Message)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStreamStoreWriteReadRotate(t *testing.T) {
|
||||
os.Remove(".test.json")
|
||||
os.RemoveAll(testingDir)
|
||||
os.Mkdir(testingDir, 0777)
|
||||
ss1 := NewStreamStore(testingDir, filenameBase, password)
|
||||
m := model.Message{Message: line1}
|
||||
for i := 0; i < 400; i++ {
|
||||
ss1.Write(m)
|
||||
}
|
||||
|
||||
ss2 := NewStreamStore(testingDir, filenameBase, password)
|
||||
messages := ss2.Read()
|
||||
if len(messages) != 400 {
|
||||
t.Errorf("Read messages has wrong length. Expected: 400 Actual: %d\n", len(messages))
|
||||
}
|
||||
if messages[0].Message != line1 {
|
||||
t.Errorf("Read message has wrong content. Expected: '%v' Actual: '%v'\n", line1, messages[0].Message)
|
||||
}
|
||||
}
|
|
@ -56,18 +56,14 @@ func DecryptFile(ciphertext []byte, key [32]byte) ([]byte, error) {
|
|||
if ok {
|
||||
return decrypted, nil
|
||||
}
|
||||
return nil, errors.New("Failed to decrypt")
|
||||
return nil, errors.New("failed to decrypt")
|
||||
}
|
||||
|
||||
// ReadEncryptedFile reads data from an encrypted file in directory with key
|
||||
func ReadEncryptedFile(directory, filename string, key [32]byte) ([]byte, error) {
|
||||
encryptedbytes, err := ioutil.ReadFile(path.Join(directory, filename))
|
||||
if err == nil {
|
||||
data, err := DecryptFile(encryptedbytes, key)
|
||||
if err == nil {
|
||||
return data, nil
|
||||
}
|
||||
return nil, err
|
||||
return DecryptFile(encryptedbytes, key)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
@ -3,109 +3,27 @@ package v1
|
|||
import (
|
||||
"cwtch.im/cwtch/event"
|
||||
"cwtch.im/cwtch/model"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"git.openprivacy.ca/openprivacy/log"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
const groupIDLen = 32
|
||||
const peerIDLen = 56
|
||||
const profileFilename = "profile"
|
||||
const version = "1"
|
||||
const versionFile = "VERSION"
|
||||
const saltFile = "SALT"
|
||||
|
||||
//ProfileStoreV1 storage for profiles and message streams that uses in memory key and fs stored salt instead of in memory password
|
||||
type ProfileStoreV1 struct {
|
||||
fs FileStore
|
||||
streamStores map[string]StreamStore // map [groupId|onion] StreamStore
|
||||
directory string
|
||||
profile *model.Profile
|
||||
key [32]byte
|
||||
salt [128]byte
|
||||
eventManager event.Manager
|
||||
queue event.Queue
|
||||
writer bool
|
||||
}
|
||||
|
||||
// CheckPassword returns true if the given password produces the same key as the current stored key, otherwise false.
|
||||
func (ps *ProfileStoreV1) CheckPassword(checkpass string) bool {
|
||||
oldkey := CreateKey(checkpass, ps.salt[:])
|
||||
return oldkey == ps.key
|
||||
}
|
||||
|
||||
// InitV1Directory generates a key and salt from a password, writes a SALT and VERSION file and returns the key and salt
|
||||
func InitV1Directory(directory, password string) ([32]byte, [128]byte, error) {
|
||||
os.Mkdir(directory, 0700)
|
||||
|
||||
key, salt, err := CreateKeySalt(password)
|
||||
if err != nil {
|
||||
log.Errorf("Could not create key for profile store from password: %v\n", err)
|
||||
return [32]byte{}, [128]byte{}, err
|
||||
}
|
||||
|
||||
if err = ioutil.WriteFile(path.Join(directory, versionFile), []byte(version), 0600); err != nil {
|
||||
log.Errorf("Could not write version file: %v", err)
|
||||
return [32]byte{}, [128]byte{}, err
|
||||
}
|
||||
|
||||
if err = ioutil.WriteFile(path.Join(directory, saltFile), salt[:], 0600); err != nil {
|
||||
log.Errorf("Could not write salt file: %v", err)
|
||||
return [32]byte{}, [128]byte{}, err
|
||||
}
|
||||
|
||||
return key, salt, nil
|
||||
}
|
||||
|
||||
// CreateProfileWriterStore creates a profile store backed by a filestore listening for events and saving them
|
||||
// directory should be $appDir/profiles/$rand
|
||||
func CreateProfileWriterStore(eventManager event.Manager, directory, password string, profile *model.Profile) *ProfileStoreV1 {
|
||||
key, salt, err := InitV1Directory(directory, password)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
ps := &ProfileStoreV1{fs: NewFileStore(directory, profileFilename, key), key: key, salt: salt, directory: directory, profile: profile, eventManager: eventManager, streamStores: map[string]StreamStore{}, writer: true}
|
||||
ps.save()
|
||||
|
||||
ps.initProfileWriterStore()
|
||||
|
||||
return ps
|
||||
}
|
||||
|
||||
func (ps *ProfileStoreV1) initProfileWriterStore() {
|
||||
ps.queue = event.NewQueue()
|
||||
go ps.eventHandler()
|
||||
|
||||
ps.eventManager.Subscribe(event.SetPeerAuthorization, ps.queue)
|
||||
ps.eventManager.Subscribe(event.PeerCreated, ps.queue)
|
||||
ps.eventManager.Subscribe(event.GroupCreated, ps.queue)
|
||||
ps.eventManager.Subscribe(event.SetAttribute, ps.queue)
|
||||
ps.eventManager.Subscribe(event.SetPeerAttribute, ps.queue)
|
||||
ps.eventManager.Subscribe(event.SetGroupAttribute, ps.queue)
|
||||
ps.eventManager.Subscribe(event.AcceptGroupInvite, ps.queue)
|
||||
ps.eventManager.Subscribe(event.RejectGroupInvite, ps.queue)
|
||||
ps.eventManager.Subscribe(event.NewGroup, ps.queue)
|
||||
ps.eventManager.Subscribe(event.NewMessageFromGroup, ps.queue)
|
||||
ps.eventManager.Subscribe(event.SendMessageToPeer, ps.queue)
|
||||
ps.eventManager.Subscribe(event.PeerAcknowledgement, ps.queue)
|
||||
ps.eventManager.Subscribe(event.NewMessageFromPeer, ps.queue)
|
||||
ps.eventManager.Subscribe(event.PeerStateChange, ps.queue)
|
||||
ps.eventManager.Subscribe(event.ServerStateChange, ps.queue)
|
||||
ps.eventManager.Subscribe(event.DeleteContact, ps.queue)
|
||||
ps.eventManager.Subscribe(event.DeleteGroup, ps.queue)
|
||||
ps.eventManager.Subscribe(event.ChangePassword, ps.queue)
|
||||
ps.eventManager.Subscribe(event.UpdateMessageFlags, ps.queue)
|
||||
}
|
||||
|
||||
// LoadProfileWriterStore loads a profile store from filestore listening for events and saving them
|
||||
// directory should be $appDir/profiles/$rand
|
||||
func LoadProfileWriterStore(eventManager event.Manager, directory, password string) (*ProfileStoreV1, error) {
|
||||
func LoadProfileWriterStore(directory, password string) (*ProfileStoreV1, error) {
|
||||
salt, err := ioutil.ReadFile(path.Join(directory, saltFile))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -113,7 +31,7 @@ func LoadProfileWriterStore(eventManager event.Manager, directory, password stri
|
|||
|
||||
key := CreateKey(password, salt)
|
||||
|
||||
ps := &ProfileStoreV1{fs: NewFileStore(directory, profileFilename, key), key: key, directory: directory, profile: nil, eventManager: eventManager, streamStores: map[string]StreamStore{}, writer: true}
|
||||
ps := &ProfileStoreV1{fs: NewFileStore(directory, profileFilename, key), key: key, directory: directory, profile: nil}
|
||||
copy(ps.salt[:], salt)
|
||||
|
||||
err = ps.load()
|
||||
|
@ -121,163 +39,9 @@ func LoadProfileWriterStore(eventManager event.Manager, directory, password stri
|
|||
return nil, err
|
||||
}
|
||||
|
||||
ps.initProfileWriterStore()
|
||||
return ps, nil
|
||||
}
|
||||
|
||||
// ReadProfile reads a profile from storqage and returns the profile
|
||||
// directory should be $appDir/profiles/$rand
|
||||
func ReadProfile(directory string, key [32]byte, salt [128]byte) (*model.Profile, error) {
|
||||
os.Mkdir(directory, 0700)
|
||||
ps := &ProfileStoreV1{fs: NewFileStore(directory, profileFilename, key), key: key, salt: salt, directory: directory, profile: nil, eventManager: nil, streamStores: map[string]StreamStore{}, writer: true}
|
||||
|
||||
err := ps.load()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
profile := ps.GetProfileCopy(true)
|
||||
|
||||
return profile, nil
|
||||
}
|
||||
|
||||
// UpgradeV0Profile takes a profile (presumably from a V0 store) and creates and writes a V1 store
|
||||
func UpgradeV0Profile(profile *model.Profile, directory, password string) error {
|
||||
key, salt, err := InitV1Directory(directory, password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ps := &ProfileStoreV1{fs: NewFileStore(directory, profileFilename, key), key: key, salt: salt, directory: directory, profile: profile, eventManager: nil, streamStores: map[string]StreamStore{}, writer: true}
|
||||
ps.save()
|
||||
|
||||
for gid, group := range ps.profile.Groups {
|
||||
ss := NewStreamStore(ps.directory, group.LocalID, ps.key)
|
||||
ss.WriteN(ps.profile.Groups[gid].Timeline.Messages)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewProfile creates a new profile for use in the profile store.
|
||||
func NewProfile(name string) *model.Profile {
|
||||
profile := model.GenerateNewProfile(name)
|
||||
return profile
|
||||
}
|
||||
|
||||
// GetNewPeerMessage is for AppService to call on Reload events, to reseed the AppClient with the loaded peers
|
||||
func (ps *ProfileStoreV1) GetNewPeerMessage() *event.Event {
|
||||
message := event.NewEventList(event.NewPeer, event.Identity, ps.profile.LocalID, event.Key, string(ps.key[:]), event.Salt, string(ps.salt[:]))
|
||||
return &message
|
||||
}
|
||||
|
||||
// GetStatusMessages creates an array of status messages for all peers and group servers from current information
|
||||
func (ps *ProfileStoreV1) GetStatusMessages() []*event.Event {
|
||||
messages := []*event.Event{}
|
||||
for _, contact := range ps.profile.Contacts {
|
||||
message := event.NewEvent(event.PeerStateChange, map[event.Field]string{
|
||||
event.RemotePeer: string(contact.Onion),
|
||||
event.ConnectionState: contact.State,
|
||||
})
|
||||
messages = append(messages, &message)
|
||||
}
|
||||
|
||||
doneServers := make(map[string]bool)
|
||||
for _, group := range ps.profile.Groups {
|
||||
if _, exists := doneServers[group.GroupServer]; !exists {
|
||||
message := event.NewEvent(event.ServerStateChange, map[event.Field]string{
|
||||
event.GroupServer: string(group.GroupServer),
|
||||
event.ConnectionState: group.State,
|
||||
})
|
||||
messages = append(messages, &message)
|
||||
doneServers[group.GroupServer] = true
|
||||
}
|
||||
}
|
||||
|
||||
return messages
|
||||
}
|
||||
|
||||
// ChangePassword restores all data under a new password's encryption
|
||||
func (ps *ProfileStoreV1) ChangePassword(oldpass, newpass, eventID string) {
|
||||
oldkey := CreateKey(oldpass, ps.salt[:])
|
||||
|
||||
if oldkey != ps.key {
|
||||
ps.eventManager.Publish(event.NewEventList(event.ChangePasswordError, event.Error, "Supplied current password does not match", event.EventID, eventID))
|
||||
return
|
||||
}
|
||||
|
||||
newkey := CreateKey(newpass, ps.salt[:])
|
||||
|
||||
newStreamStores := map[string]StreamStore{}
|
||||
idToNewLocalID := map[string]string{}
|
||||
|
||||
// Generate all new StreamStores with the new password and write all the old StreamStore data into these ones
|
||||
for ssid, ss := range ps.streamStores {
|
||||
// New ss with new pass and new localID
|
||||
newlocalID := model.GenerateRandomID()
|
||||
idToNewLocalID[ssid] = newlocalID
|
||||
|
||||
newSS := NewStreamStore(ps.directory, newlocalID, newkey)
|
||||
newStreamStores[ssid] = newSS
|
||||
|
||||
// write whole store
|
||||
messages := ss.Read()
|
||||
newSS.WriteN(messages)
|
||||
}
|
||||
|
||||
// Switch over
|
||||
oldStreamStores := ps.streamStores
|
||||
ps.streamStores = newStreamStores
|
||||
for ssid, newLocalID := range idToNewLocalID {
|
||||
if len(ssid) == groupIDLen {
|
||||
ps.profile.Groups[ssid].LocalID = newLocalID
|
||||
} else {
|
||||
if ps.profile.Contacts[ssid] != nil {
|
||||
ps.profile.Contacts[ssid].LocalID = newLocalID
|
||||
} else {
|
||||
log.Errorf("Unknown Contact: %v. This is probably the result of corrupted development data from fuzzing. This contact will not appear in the new profile.", ssid)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ps.key = newkey
|
||||
ps.fs.ChangeKey(newkey)
|
||||
ps.save()
|
||||
|
||||
// Clean up
|
||||
for _, oldss := range oldStreamStores {
|
||||
oldss.Delete()
|
||||
}
|
||||
|
||||
ps.eventManager.Publish(event.NewEventList(event.ChangePasswordSuccess, event.EventID, eventID))
|
||||
return
|
||||
}
|
||||
|
||||
func (ps *ProfileStoreV1) save() error {
|
||||
if ps.writer {
|
||||
bytes, _ := json.Marshal(ps.profile)
|
||||
return ps.fs.Write(bytes)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ps *ProfileStoreV1) regenStreamStore(messages []model.Message, contact string) {
|
||||
oldss := ps.streamStores[contact]
|
||||
newLocalID := model.GenerateRandomID()
|
||||
newSS := NewStreamStore(ps.directory, newLocalID, ps.key)
|
||||
newSS.WriteN(messages)
|
||||
if len(contact) == groupIDLen {
|
||||
ps.profile.Groups[contact].LocalID = newLocalID
|
||||
} else {
|
||||
// We can assume this exists as regen stream store should only happen to *update* a message
|
||||
ps.profile.Contacts[contact].LocalID = newLocalID
|
||||
}
|
||||
ps.streamStores[contact] = newSS
|
||||
ps.save()
|
||||
oldss.Delete()
|
||||
}
|
||||
|
||||
// load instantiates a cwtchPeer from the file store
|
||||
func (ps *ProfileStoreV1) load() error {
|
||||
decrypted, err := ps.fs.Read()
|
||||
|
@ -301,16 +65,9 @@ func (ps *ProfileStoreV1) load() error {
|
|||
}
|
||||
}
|
||||
|
||||
// Check if there is any saved history...
|
||||
saveHistory, keyExists := contact.GetAttribute(event.SaveHistoryKey)
|
||||
if !keyExists {
|
||||
contact.SetAttribute(event.SaveHistoryKey, event.DeleteHistoryDefault)
|
||||
}
|
||||
|
||||
if saveHistory == event.SaveHistoryConfirmed {
|
||||
if contact.Attributes[event.SaveHistoryKey] == event.SaveHistoryConfirmed {
|
||||
ss := NewStreamStore(ps.directory, contact.LocalID, ps.key)
|
||||
cp.Contacts[contact.Onion].Timeline.SetMessages(ss.Read())
|
||||
ps.streamStores[contact.Onion] = ss
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -320,15 +77,10 @@ func (ps *ProfileStoreV1) load() error {
|
|||
delete(cp.Groups, gid)
|
||||
continue
|
||||
}
|
||||
|
||||
ss := NewStreamStore(ps.directory, group.LocalID, ps.key)
|
||||
|
||||
cp.Groups[gid].Timeline.SetMessages(ss.Read())
|
||||
cp.Groups[gid].Timeline.Sort()
|
||||
ps.streamStores[group.GroupID] = ss
|
||||
}
|
||||
|
||||
ps.save()
|
||||
}
|
||||
|
||||
return err
|
||||
|
@ -338,238 +90,3 @@ func (ps *ProfileStoreV1) load() error {
|
|||
func (ps *ProfileStoreV1) GetProfileCopy(timeline bool) *model.Profile {
|
||||
return ps.profile.GetCopy(timeline)
|
||||
}
|
||||
|
||||
func (ps *ProfileStoreV1) eventHandler() {
|
||||
for {
|
||||
ev := ps.queue.Next()
|
||||
log.Debugf("eventHandler event %v %v\n", ev.EventType, ev.EventID)
|
||||
|
||||
switch ev.EventType {
|
||||
case event.SetPeerAuthorization:
|
||||
err := ps.profile.SetContactAuthorization(ev.Data[event.RemotePeer], model.Authorization(ev.Data[event.Authorization]))
|
||||
if err == nil {
|
||||
ps.save()
|
||||
}
|
||||
case event.PeerCreated:
|
||||
var pp *model.PublicProfile
|
||||
json.Unmarshal([]byte(ev.Data[event.Data]), &pp)
|
||||
ps.profile.AddContact(ev.Data[event.RemotePeer], pp)
|
||||
case event.GroupCreated:
|
||||
var group *model.Group
|
||||
json.Unmarshal([]byte(ev.Data[event.Data]), &group)
|
||||
ps.profile.AddGroup(group)
|
||||
ps.streamStores[group.GroupID] = NewStreamStore(ps.directory, group.LocalID, ps.key)
|
||||
ps.save()
|
||||
case event.SetAttribute:
|
||||
ps.profile.SetAttribute(ev.Data[event.Key], ev.Data[event.Data])
|
||||
ps.save()
|
||||
case event.SetPeerAttribute:
|
||||
contact, exists := ps.profile.GetContact(ev.Data[event.RemotePeer])
|
||||
if exists {
|
||||
contact.SetAttribute(ev.Data[event.Key], ev.Data[event.Data])
|
||||
ps.save()
|
||||
|
||||
switch ev.Data[event.Key] {
|
||||
case event.SaveHistoryKey:
|
||||
if event.DeleteHistoryConfirmed == ev.Data[event.Data] {
|
||||
ss, exists := ps.streamStores[ev.Data[event.RemotePeer]]
|
||||
if exists {
|
||||
ss.Delete()
|
||||
delete(ps.streamStores, ev.Data[event.RemotePeer])
|
||||
}
|
||||
} else if event.SaveHistoryConfirmed == ev.Data[event.Data] {
|
||||
_, exists := ps.streamStores[ev.Data[event.RemotePeer]]
|
||||
if !exists {
|
||||
ss := NewStreamStore(ps.directory, contact.LocalID, ps.key)
|
||||
ps.streamStores[ev.Data[event.RemotePeer]] = ss
|
||||
}
|
||||
}
|
||||
default:
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
log.Errorf("error setting attribute on peer %v peer does not exist", ev)
|
||||
}
|
||||
case event.SetGroupAttribute:
|
||||
group := ps.profile.GetGroup(ev.Data[event.GroupID])
|
||||
if group != nil {
|
||||
group.SetAttribute(ev.Data[event.Key], ev.Data[event.Data])
|
||||
ps.save()
|
||||
} else {
|
||||
log.Errorf("error setting attribute on group %v group does not exist", ev)
|
||||
}
|
||||
case event.AcceptGroupInvite:
|
||||
err := ps.profile.AcceptInvite(ev.Data[event.GroupID])
|
||||
if err == nil {
|
||||
ps.save()
|
||||
} else {
|
||||
log.Errorf("error accepting group invite")
|
||||
}
|
||||
case event.RejectGroupInvite:
|
||||
ps.profile.RejectInvite(ev.Data[event.GroupID])
|
||||
ps.save()
|
||||
case event.NewGroup:
|
||||
gid, err := ps.profile.ProcessInvite(ev.Data[event.GroupInvite])
|
||||
if err == nil {
|
||||
ps.save()
|
||||
group := ps.profile.Groups[gid]
|
||||
ps.streamStores[group.GroupID] = NewStreamStore(ps.directory, group.LocalID, ps.key)
|
||||
} else {
|
||||
log.Errorf("error storing new group invite: %v (%v)", err, ev)
|
||||
}
|
||||
case event.SendMessageToPeer: // cache the message till an ack, then it's given to stream store.
|
||||
// stream store doesn't support updates, so we don't want to commit it till ack'd
|
||||
ps.profile.AddSentMessageToContactTimeline(ev.Data[event.RemotePeer], ev.Data[event.Data], time.Now(), ev.EventID)
|
||||
case event.NewMessageFromPeer:
|
||||
ps.profile.AddMessageToContactTimeline(ev.Data[event.RemotePeer], ev.Data[event.Data], time.Now())
|
||||
ps.attemptSavePeerMessage(ev.Data[event.RemotePeer], ev.Data[event.Data], ev.Data[event.TimestampReceived], true)
|
||||
case event.PeerAcknowledgement:
|
||||
onion := ev.Data[event.RemotePeer]
|
||||
eventID := ev.Data[event.EventID]
|
||||
contact, ok := ps.profile.Contacts[onion]
|
||||
if ok {
|
||||
mIdx, ok := contact.UnacknowledgedMessages[eventID]
|
||||
if ok {
|
||||
message := contact.Timeline.Messages[mIdx]
|
||||
ps.attemptSavePeerMessage(onion, message.Message, message.Timestamp.Format(time.RFC3339Nano), false)
|
||||
}
|
||||
}
|
||||
ps.profile.AckSentMessageToPeer(ev.Data[event.RemotePeer], ev.Data[event.EventID])
|
||||
case event.NewMessageFromGroup:
|
||||
groupid := ev.Data[event.GroupID]
|
||||
received, _ := time.Parse(time.RFC3339Nano, ev.Data[event.TimestampReceived])
|
||||
sent, _ := time.Parse(time.RFC3339Nano, ev.Data[event.TimestampSent])
|
||||
sig, _ := base64.StdEncoding.DecodeString(ev.Data[event.Signature])
|
||||
prevsig, _ := base64.StdEncoding.DecodeString(ev.Data[event.PreviousSignature])
|
||||
message := model.Message{Received: received, Timestamp: sent, Message: ev.Data[event.Data], PeerID: ev.Data[event.RemotePeer], Signature: sig, PreviousMessageSig: prevsig, Acknowledged: true}
|
||||
ss, exists := ps.streamStores[groupid]
|
||||
if exists {
|
||||
// We need to store a local copy of the message...
|
||||
ps.profile.GetGroup(groupid).Timeline.Insert(&message)
|
||||
ss.Write(message)
|
||||
} else {
|
||||
log.Errorf("error storing new group message: %v stream store does not exist", ev)
|
||||
}
|
||||
case event.PeerStateChange:
|
||||
if _, exists := ps.profile.Contacts[ev.Data[event.RemotePeer]]; exists {
|
||||
ps.profile.Contacts[ev.Data[event.RemotePeer]].State = ev.Data[event.ConnectionState]
|
||||
}
|
||||
case event.ServerStateChange:
|
||||
for _, group := range ps.profile.Groups {
|
||||
if group.GroupServer == ev.Data[event.GroupServer] {
|
||||
group.State = ev.Data[event.ConnectionState]
|
||||
}
|
||||
}
|
||||
case event.DeleteContact:
|
||||
onion := ev.Data[event.RemotePeer]
|
||||
ps.profile.DeleteContact(onion)
|
||||
ps.save()
|
||||
ss, exists := ps.streamStores[onion]
|
||||
if exists {
|
||||
ss.Delete()
|
||||
delete(ps.streamStores, onion)
|
||||
}
|
||||
case event.DeleteGroup:
|
||||
groupID := ev.Data[event.GroupID]
|
||||
ps.profile.DeleteGroup(groupID)
|
||||
ps.save()
|
||||
ss, exists := ps.streamStores[groupID]
|
||||
if exists {
|
||||
ss.Delete()
|
||||
delete(ps.streamStores, groupID)
|
||||
}
|
||||
case event.ChangePassword:
|
||||
oldpass := ev.Data[event.Password]
|
||||
newpass := ev.Data[event.NewPassword]
|
||||
ps.ChangePassword(oldpass, newpass, ev.EventID)
|
||||
case event.UpdateMessageFlags:
|
||||
handle := ev.Data[event.Handle]
|
||||
mIx, err := strconv.Atoi(ev.Data[event.Index])
|
||||
if err != nil {
|
||||
log.Errorf("Invalid Message Index: %v", err)
|
||||
return
|
||||
}
|
||||
flags, err := strconv.ParseUint(ev.Data[event.Flags], 2, 64)
|
||||
if err != nil {
|
||||
log.Errorf("Invalid Message Flags: %v", err)
|
||||
return
|
||||
}
|
||||
ps.profile.UpdateMessageFlags(handle, mIx, flags)
|
||||
if len(handle) == groupIDLen {
|
||||
ps.regenStreamStore(ps.profile.GetGroup(handle).Timeline.Messages, handle)
|
||||
} else if contact, exists := ps.profile.GetContact(handle); exists {
|
||||
if exists {
|
||||
val, _ := contact.GetAttribute(event.SaveHistoryKey)
|
||||
if val == event.SaveHistoryConfirmed {
|
||||
ps.regenStreamStore(contact.Timeline.Messages, handle)
|
||||
}
|
||||
}
|
||||
}
|
||||
default:
|
||||
log.Debugf("shutting down profile store: %v", ev)
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// attemptSavePeerMessage checks if the peer has been configured to save history from this peer
|
||||
// and if so the peer saves the message into history. fromPeer is used to control if the message is saved
|
||||
// as coming from the remote peer or if it was sent by out profile.
|
||||
func (ps *ProfileStoreV1) attemptSavePeerMessage(peerID, messageData, timestampeReceived string, fromPeer bool) {
|
||||
contact, exists := ps.profile.GetContact(peerID)
|
||||
if exists {
|
||||
val, _ := contact.GetAttribute(event.SaveHistoryKey)
|
||||
switch val {
|
||||
case event.SaveHistoryConfirmed:
|
||||
{
|
||||
peerID := peerID
|
||||
var received time.Time
|
||||
var message model.Message
|
||||
if fromPeer {
|
||||
received, _ = time.Parse(time.RFC3339Nano, timestampeReceived)
|
||||
message = model.Message{Received: received, Timestamp: received, Message: messageData, PeerID: peerID, Signature: []byte{}, PreviousMessageSig: []byte{}}
|
||||
} else {
|
||||
received := time.Now()
|
||||
message = model.Message{Received: received, Timestamp: received, Message: messageData, PeerID: ps.profile.Onion, Signature: []byte{}, PreviousMessageSig: []byte{}, Acknowledged: true}
|
||||
}
|
||||
ss, exists := ps.streamStores[peerID]
|
||||
if exists {
|
||||
ss.Write(message)
|
||||
} else {
|
||||
log.Errorf("error storing new peer message: %v stream store does not exist", peerID)
|
||||
}
|
||||
}
|
||||
default:
|
||||
{
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.Errorf("error saving message for peer that doesn't exist: %v", peerID)
|
||||
}
|
||||
}
|
||||
|
||||
// Shutdown shuts down the queue / thread
|
||||
func (ps *ProfileStoreV1) Shutdown() {
|
||||
if ps.queue != nil {
|
||||
ps.queue.Shutdown()
|
||||
}
|
||||
}
|
||||
|
||||
// Delete removes all stored files for this stored profile
|
||||
func (ps *ProfileStoreV1) Delete() {
|
||||
log.Debugf("Delete ProfileStore for %v\n", ps.profile.Onion)
|
||||
|
||||
for _, ss := range ps.streamStores {
|
||||
ss.Delete()
|
||||
}
|
||||
|
||||
ps.fs.Delete()
|
||||
|
||||
err := os.RemoveAll(ps.directory)
|
||||
if err != nil {
|
||||
log.Errorf("ProfileStore Delete error on RemoveAll on %v was %v\n", ps.directory, err)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,159 +0,0 @@
|
|||
// Known race issue with event bus channel closure
|
||||
|
||||
package v1
|
||||
|
||||
import (
|
||||
"cwtch.im/cwtch/event"
|
||||
"cwtch.im/cwtch/model"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
const testProfileName = "Alice"
|
||||
const testKey = "key"
|
||||
const testVal = "value"
|
||||
const testInitialMessage = "howdy"
|
||||
const testMessage = "Hello from storage"
|
||||
|
||||
func TestProfileStoreWriteRead(t *testing.T) {
|
||||
log.Println("profile store test!")
|
||||
os.RemoveAll(testingDir)
|
||||
eventBus := event.NewEventManager()
|
||||
profile := NewProfile(testProfileName)
|
||||
// The lightest weight server entry possible (usually we would import a key bundle...)
|
||||
profile.AddContact("2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd", &model.PublicProfile{Attributes: map[string]string{string(model.KeyTypeServerOnion): "2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd"}})
|
||||
|
||||
ps1 := CreateProfileWriterStore(eventBus, testingDir, password, profile)
|
||||
|
||||
eventBus.Publish(event.NewEvent(event.SetAttribute, map[event.Field]string{event.Key: testKey, event.Data: testVal}))
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
groupid, invite, err := profile.StartGroup("2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd")
|
||||
if err != nil {
|
||||
t.Errorf("Creating group: %v\n", err)
|
||||
}
|
||||
if err != nil {
|
||||
t.Errorf("Creating group invite: %v\n", err)
|
||||
}
|
||||
|
||||
eventBus.Publish(event.NewEvent(event.NewGroup, map[event.Field]string{event.TimestampReceived: time.Now().Format(time.RFC3339Nano), event.RemotePeer: ps1.GetProfileCopy(true).Onion, event.GroupInvite: string(invite)}))
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
eventBus.Publish(event.NewEvent(event.NewMessageFromGroup, map[event.Field]string{
|
||||
event.GroupID: groupid,
|
||||
event.TimestampSent: time.Now().Format(time.RFC3339Nano),
|
||||
event.TimestampReceived: time.Now().Format(time.RFC3339Nano),
|
||||
event.RemotePeer: ps1.GetProfileCopy(true).Onion,
|
||||
event.Data: testMessage,
|
||||
}))
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
ps1.Shutdown()
|
||||
|
||||
ps2, err := LoadProfileWriterStore(eventBus, testingDir, password)
|
||||
if err != nil {
|
||||
t.Errorf("Error createing ProfileStoreV1: %v\n", err)
|
||||
}
|
||||
|
||||
profile = ps2.GetProfileCopy(true)
|
||||
if profile.Name != testProfileName {
|
||||
t.Errorf("Profile name from loaded profile incorrect. Expected: '%v' Actual: '%v'\n", testProfileName, profile.Name)
|
||||
}
|
||||
|
||||
v, _ := profile.GetAttribute(testKey)
|
||||
if v != testVal {
|
||||
t.Errorf("Profile attribute '%v' inccorect. Expected: '%v' Actual: '%v'\n", testKey, testVal, v)
|
||||
}
|
||||
|
||||
group2 := ps2.GetProfileCopy(true).Groups[groupid]
|
||||
if group2 == nil {
|
||||
t.Errorf("Group not loaded\n")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestProfileStoreChangePassword(t *testing.T) {
|
||||
os.RemoveAll(testingDir)
|
||||
eventBus := event.NewEventManager()
|
||||
|
||||
queue := event.NewQueue()
|
||||
eventBus.Subscribe(event.ChangePasswordSuccess, queue)
|
||||
|
||||
profile := NewProfile(testProfileName)
|
||||
profile.AddContact("2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd", &model.PublicProfile{Attributes: map[string]string{string(model.KeyTypeServerOnion): "2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd"}})
|
||||
|
||||
ps1 := CreateProfileWriterStore(eventBus, testingDir, password, profile)
|
||||
|
||||
groupid, invite, err := profile.StartGroup("2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd")
|
||||
if err != nil {
|
||||
t.Errorf("Creating group: %v\n", err)
|
||||
}
|
||||
if err != nil {
|
||||
t.Errorf("Creating group invite: %v\n", err)
|
||||
}
|
||||
|
||||
eventBus.Publish(event.NewEvent(event.NewGroup, map[event.Field]string{event.TimestampReceived: time.Now().Format(time.RFC3339Nano), event.RemotePeer: ps1.GetProfileCopy(true).Onion, event.GroupInvite: string(invite)}))
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
fmt.Println("Sending 200 messages...")
|
||||
|
||||
for i := 0; i < 200; i++ {
|
||||
eventBus.Publish(event.NewEvent(event.NewMessageFromGroup, map[event.Field]string{
|
||||
event.GroupID: groupid,
|
||||
event.TimestampSent: time.Now().Format(time.RFC3339Nano),
|
||||
event.TimestampReceived: time.Now().Format(time.RFC3339Nano),
|
||||
event.RemotePeer: profile.Onion,
|
||||
event.Data: testMessage,
|
||||
event.Signature: base64.StdEncoding.EncodeToString([]byte{byte(i)}),
|
||||
}))
|
||||
}
|
||||
|
||||
newPass := "qwerty123"
|
||||
|
||||
fmt.Println("Sending Change Passwords event...")
|
||||
eventBus.Publish(event.NewEventList(event.ChangePassword, event.Password, password, event.NewPassword, newPass))
|
||||
|
||||
ev := queue.Next()
|
||||
if ev.EventType != event.ChangePasswordSuccess {
|
||||
t.Errorf("Unexpected event response detected %v\n", ev.EventType)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println("Sending 10 more messages...")
|
||||
for i := 0; i < 10; i++ {
|
||||
eventBus.Publish(event.NewEvent(event.NewMessageFromGroup, map[event.Field]string{
|
||||
event.GroupID: groupid,
|
||||
event.TimestampSent: time.Now().Format(time.RFC3339Nano),
|
||||
event.TimestampReceived: time.Now().Format(time.RFC3339Nano),
|
||||
event.RemotePeer: profile.Onion,
|
||||
event.Data: testMessage,
|
||||
event.Signature: base64.StdEncoding.EncodeToString([]byte{0x01, byte(i)}),
|
||||
}))
|
||||
}
|
||||
time.Sleep(3 * time.Second)
|
||||
|
||||
fmt.Println("Shutdown profile store...")
|
||||
ps1.Shutdown()
|
||||
|
||||
fmt.Println("New Profile store...")
|
||||
ps2, err := LoadProfileWriterStore(eventBus, testingDir, newPass)
|
||||
if err != nil {
|
||||
t.Errorf("Error createing new ProfileStoreV1 with new password: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
profile2 := ps2.GetProfileCopy(true)
|
||||
|
||||
if profile2.Groups[groupid] == nil {
|
||||
t.Errorf("Failed to load group %v\n", groupid)
|
||||
return
|
||||
}
|
||||
|
||||
if len(profile2.Groups[groupid].Timeline.Messages) != 210 {
|
||||
t.Errorf("Failed to load group's 210 messages, instead got %v\n", len(profile2.Groups[groupid].Timeline.Messages))
|
||||
}
|
||||
}
|
|
@ -15,7 +15,6 @@ import (
|
|||
// This number is larger that the recommend chunk size of libsodium secretbox by an order of magnitude.
|
||||
// Since this code is not performance-sensitive (and is unlikely to gain any significant performance benefit from
|
||||
// cache-efficient chunking) this size isn’t currently a concern.
|
||||
// TODO: revise and evaluate better storage options after beta”
|
||||
const (
|
||||
fileStorePartitions = 128
|
||||
bytesPerFile = 128 * 1024
|
||||
|
|
|
@ -1,27 +1,28 @@
|
|||
package testing
|
||||
|
||||
import (
|
||||
// Import SQL Cipher
|
||||
"crypto/rand"
|
||||
app2 "cwtch.im/cwtch/app"
|
||||
"cwtch.im/cwtch/app/utils"
|
||||
"cwtch.im/cwtch/event"
|
||||
"cwtch.im/cwtch/event/bridge"
|
||||
"cwtch.im/cwtch/model"
|
||||
"cwtch.im/cwtch/model/attr"
|
||||
"cwtch.im/cwtch/model/constants"
|
||||
"cwtch.im/cwtch/peer"
|
||||
"cwtch.im/cwtch/protocol/connections"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"git.openprivacy.ca/openprivacy/connectivity/tor"
|
||||
"git.openprivacy.ca/openprivacy/log"
|
||||
_ "github.com/mutecomm/go-sqlcipher/v4"
|
||||
mrand "math/rand"
|
||||
"os"
|
||||
"os/user"
|
||||
"path"
|
||||
"runtime"
|
||||
"runtime/pprof"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
@ -32,71 +33,33 @@ var (
|
|||
carolLines = []string{"Howdy, thanks!"}
|
||||
)
|
||||
|
||||
func printAndCountVerifedTimeline(t *testing.T, timeline []model.Message) int {
|
||||
numVerified := 0
|
||||
for _, message := range timeline {
|
||||
fmt.Printf("%v %v> %s\n", message.Timestamp, message.PeerID, message.Message)
|
||||
numVerified++
|
||||
}
|
||||
return numVerified
|
||||
}
|
||||
|
||||
func waitForPeerGroupConnection(t *testing.T, peer peer.CwtchPeer, groupID string) {
|
||||
func waitForConnection(t *testing.T, peer peer.CwtchPeer, addr string, target connections.ConnectionState) {
|
||||
peerName, _ := peer.GetScopedZonedAttribute(attr.LocalScope, attr.ProfileZone, constants.Name)
|
||||
for {
|
||||
fmt.Printf("%v checking group connection...\n", peerName)
|
||||
state, ok := peer.GetGroupState(groupID)
|
||||
if ok {
|
||||
fmt.Printf("Waiting for Peer %v to join group %v - state: %v\n", peerName, groupID, state)
|
||||
fmt.Printf("%v checking connection...\n", peerName)
|
||||
state := peer.GetPeerState(addr)
|
||||
fmt.Printf("Waiting for Peer %v to %v - state: %v\n", peerName, addr, state)
|
||||
if state == connections.FAILED {
|
||||
t.Fatalf("%v could not connect to %v", peer.GetOnion(), groupID)
|
||||
t.Fatalf("%v could not connect to %v", peer.GetOnion(), addr)
|
||||
}
|
||||
if state != connections.SYNCED {
|
||||
fmt.Printf("peer %v %v waiting connect to group %v, currently: %v\n", peerName, peer.GetOnion(), groupID, connections.ConnectionStateName[state])
|
||||
if state != target {
|
||||
fmt.Printf("peer %v %v waiting connect %v, currently: %v\n", peerName, peer.GetOnion(), addr, connections.ConnectionStateName[state])
|
||||
time.Sleep(time.Second * 5)
|
||||
continue
|
||||
} else {
|
||||
fmt.Printf("peer %v %v CONNECTED to group %v\n", peerName, peer.GetOnion(), groupID)
|
||||
fmt.Printf("peer %v %v CONNECTED to %v\n", peerName, peer.GetOnion(), addr)
|
||||
break
|
||||
}
|
||||
}
|
||||
time.Sleep(time.Second * 2)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func waitForPeerPeerConnection(t *testing.T, peera peer.CwtchPeer, peerb peer.CwtchPeer) {
|
||||
for {
|
||||
state, ok := peera.GetPeerState(peerb.GetOnion())
|
||||
if ok {
|
||||
//log.Infof("Waiting for Peer %v to peer with peer: %v - state: %v\n", peera.GetProfile().Name, peerb.GetProfile().Name, state)
|
||||
if state == connections.FAILED {
|
||||
t.Fatalf("%v could not connect to %v", peera.GetOnion(), peerb.GetOnion())
|
||||
}
|
||||
if state != connections.AUTHENTICATED {
|
||||
fmt.Printf("peer %v waiting connect to peer %v, currently: %v\n", peera.GetOnion(), peerb.GetOnion(), connections.ConnectionStateName[state])
|
||||
time.Sleep(time.Second * 5)
|
||||
continue
|
||||
} else {
|
||||
peerAName, _ := peera.GetScopedZonedAttribute(attr.LocalScope, attr.ProfileZone, constants.Name)
|
||||
peerBName, _ := peerb.GetScopedZonedAttribute(attr.LocalScope, attr.ProfileZone, constants.Name)
|
||||
fmt.Printf("%v CONNECTED and AUTHED to %v\n", peerAName, peerBName)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func TestCwtchPeerIntegration(t *testing.T) {
|
||||
numGoRoutinesStart := runtime.NumGoroutine()
|
||||
|
||||
log.AddEverythingFromPattern("connectivity")
|
||||
log.SetLevel(log.LevelDebug)
|
||||
log.ExcludeFromPattern("connection/connection")
|
||||
log.ExcludeFromPattern("outbound/3dhauthchannel")
|
||||
log.ExcludeFromPattern("event/eventmanager")
|
||||
log.ExcludeFromPattern("pipeBridge")
|
||||
log.ExcludeFromPattern("tapir")
|
||||
os.Mkdir("tordir", 0700)
|
||||
dataDir := path.Join("tordir", "tor")
|
||||
|
@ -119,8 +82,11 @@ func TestCwtchPeerIntegration(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatalf("Could not start Tor: %v", err)
|
||||
}
|
||||
pid, _ := acn.GetPID()
|
||||
t.Logf("Tor pid: %v", pid)
|
||||
acn.WaitTillBootstrapped()
|
||||
defer acn.Close()
|
||||
|
||||
// We don't include ACN in our routine calculations anymore
|
||||
numGoRoutinesStart := runtime.NumGoroutine()
|
||||
|
||||
// ***** Cwtch Server management *****
|
||||
|
||||
|
@ -135,41 +101,36 @@ func TestCwtchPeerIntegration(t *testing.T) {
|
|||
os.Mkdir(cwtchDir, 0700)
|
||||
os.RemoveAll(path.Join(cwtchDir, "testing"))
|
||||
os.Mkdir(path.Join(cwtchDir, "testing"), 0700)
|
||||
bridgeClient := bridge.NewPipeBridgeClient(path.Join(cwtchDir, "testing/clientPipe"), path.Join(cwtchDir, "testing/servicePipe"))
|
||||
bridgeService := bridge.NewPipeBridgeService(path.Join(cwtchDir, "testing/servicePipe"), path.Join(cwtchDir, "testing/clientPipe"))
|
||||
appClient := app2.NewAppClient("./storage", bridgeClient)
|
||||
appService := app2.NewAppService(acn, "./storage", bridgeService)
|
||||
|
||||
numGoRoutinesPostAppStart := runtime.NumGoroutine()
|
||||
|
||||
// ***** cwtchPeer setup *****
|
||||
|
||||
fmt.Println("Creating Alice...")
|
||||
app.CreatePeer("alice", "asdfasdf")
|
||||
app.CreateTaggedPeer("Alice", "asdfasdf", "test")
|
||||
|
||||
fmt.Println("Creating Bob...")
|
||||
app.CreatePeer("bob", "asdfasdf")
|
||||
app.CreateTaggedPeer("Bob", "asdfasdf", "test")
|
||||
|
||||
fmt.Println("Creating Carol...")
|
||||
appClient.CreatePeer("carol", "asdfasdf")
|
||||
app.CreateTaggedPeer("Carol", "asdfasdf", "test")
|
||||
|
||||
alice := utils.WaitGetPeer(app, "alice")
|
||||
alice := utils.WaitGetPeer(app, "Alice")
|
||||
fmt.Println("Alice created:", alice.GetOnion())
|
||||
alice.SetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants.Name, "Alice")
|
||||
alice.AutoHandleEvents([]event.Type{event.PeerStateChange, event.ServerStateChange, event.NewGroupInvite, event.NewRetValMessageFromPeer})
|
||||
|
||||
bob := utils.WaitGetPeer(app, "bob")
|
||||
bob := utils.WaitGetPeer(app, "Bob")
|
||||
fmt.Println("Bob created:", bob.GetOnion())
|
||||
bob.SetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants.Name, "Bob")
|
||||
bob.AutoHandleEvents([]event.Type{event.PeerStateChange, event.ServerStateChange, event.NewGroupInvite, event.NewRetValMessageFromPeer})
|
||||
|
||||
carol := utils.WaitGetPeer(appClient, "carol")
|
||||
carol := utils.WaitGetPeer(app, "Carol")
|
||||
fmt.Println("Carol created:", carol.GetOnion())
|
||||
carol.SetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants.Name, "Carol")
|
||||
carol.AutoHandleEvents([]event.Type{event.PeerStateChange, event.ServerStateChange, event.NewGroupInvite, event.NewRetValMessageFromPeer})
|
||||
|
||||
app.LaunchPeers()
|
||||
appClient.LaunchPeers()
|
||||
|
||||
waitTime := time.Duration(60) * time.Second
|
||||
t.Logf("** Waiting for Alice, Bob, and Carol to connect with onion network... (%v)\n", waitTime)
|
||||
|
@ -179,289 +140,240 @@ func TestCwtchPeerIntegration(t *testing.T) {
|
|||
|
||||
// ***** Peering, server joining, group creation / invite *****
|
||||
|
||||
fmt.Println("Alice joining server...")
|
||||
if _, err := alice.AddServer(string(serverKeyBundle)); err != nil {
|
||||
t.Fatalf("Failed to Add Server Bundle %v", err)
|
||||
}
|
||||
alice.JoinServer(ServerAddr)
|
||||
|
||||
fmt.Println("Alice peering with Bob...")
|
||||
alice.PeerWithOnion(bob.GetOnion())
|
||||
// Simulate Alice Adding Bob
|
||||
alice2bobConversationID, err := alice.NewContactConversation(bob.GetOnion(), model.DefaultP2PAccessControl(), true)
|
||||
if err != nil {
|
||||
t.Fatalf("error adding conversaiton %v", alice2bobConversationID)
|
||||
}
|
||||
bob2aliceConversationID, err := bob.NewContactConversation(alice.GetOnion(), model.DefaultP2PAccessControl(), true)
|
||||
if err != nil {
|
||||
t.Fatalf("error adding conversaiton %v", bob2aliceConversationID)
|
||||
}
|
||||
|
||||
fmt.Println("Alice peering with Carol...")
|
||||
t.Logf("Alice peering with Carol...")
|
||||
// Simulate Alice Adding Carol
|
||||
alice2carolConversationID, err := alice.NewContactConversation(carol.GetOnion(), model.DefaultP2PAccessControl(), true)
|
||||
if err != nil {
|
||||
t.Fatalf("error adding conversaiton %v", alice2carolConversationID)
|
||||
}
|
||||
carol2aliceConversationID, err := carol.NewContactConversation(alice.GetOnion(), model.DefaultP2PAccessControl(), true)
|
||||
if err != nil {
|
||||
t.Fatalf("error adding conversaiton %v", carol2aliceConversationID)
|
||||
}
|
||||
|
||||
alice.PeerWithOnion(bob.GetOnion())
|
||||
alice.PeerWithOnion(carol.GetOnion())
|
||||
|
||||
fmt.Println("Creating group on ", ServerAddr, "...")
|
||||
groupID, _, err := alice.StartGroup(ServerAddr)
|
||||
fmt.Printf("Created group: %v!\n", groupID)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to init group: %v", err)
|
||||
return
|
||||
}
|
||||
waitForConnection(t, alice, bob.GetOnion(), connections.AUTHENTICATED)
|
||||
waitForConnection(t, alice, carol.GetOnion(), connections.AUTHENTICATED)
|
||||
waitForConnection(t, bob, alice.GetOnion(), connections.AUTHENTICATED)
|
||||
waitForConnection(t, carol, alice.GetOnion(), connections.AUTHENTICATED)
|
||||
|
||||
fmt.Println("Waiting for alice to join server...")
|
||||
waitForPeerGroupConnection(t, alice, groupID)
|
||||
t.Logf("Alice and Bob getVal public.name...")
|
||||
|
||||
fmt.Println("Waiting for alice and Bob to peer...")
|
||||
waitForPeerPeerConnection(t, alice, bob)
|
||||
// Need to add contact else SetContactAuth fails on peer peer doesnt exist
|
||||
// Normal flow would be Bob app monitors for the new connection (a new connection state change to Auth
|
||||
// and the adds the user to peer, and then approves or blocks it
|
||||
bob.AddContact("alice?", alice.GetOnion(), model.AuthApproved)
|
||||
bob.AddServer(string(serverKeyBundle))
|
||||
bob.SetContactAuthorization(alice.GetOnion(), model.AuthApproved)
|
||||
alice.SendScopedZonedGetValToContact(alice2bobConversationID, attr.PublicScope, attr.ProfileZone, constants.Name)
|
||||
bob.SendScopedZonedGetValToContact(bob2aliceConversationID, attr.PublicScope, attr.ProfileZone, constants.Name)
|
||||
|
||||
waitForPeerPeerConnection(t, alice, carol)
|
||||
carol.AddContact("alice?", alice.GetOnion(), model.AuthApproved)
|
||||
carol.AddServer(string(serverKeyBundle))
|
||||
carol.SetContactAuthorization(alice.GetOnion(), model.AuthApproved)
|
||||
|
||||
fmt.Println("Alice and Bob getVal public.name...")
|
||||
|
||||
alice.SendScopedZonedGetValToContact(bob.GetOnion(), attr.PublicScope, attr.ProfileZone, constants.Name)
|
||||
bob.SendScopedZonedGetValToContact(alice.GetOnion(), attr.PublicScope, attr.ProfileZone, constants.Name)
|
||||
|
||||
alice.SendScopedZonedGetValToContact(carol.GetOnion(), attr.PublicScope, attr.ProfileZone, constants.Name)
|
||||
carol.SendScopedZonedGetValToContact(alice.GetOnion(), attr.PublicScope, attr.ProfileZone, constants.Name)
|
||||
alice.SendScopedZonedGetValToContact(alice2carolConversationID, attr.PublicScope, attr.ProfileZone, constants.Name)
|
||||
carol.SendScopedZonedGetValToContact(carol2aliceConversationID, attr.PublicScope, attr.ProfileZone, constants.Name)
|
||||
|
||||
// This used to be 10, but increasing it to 30 because this is now causing frequent issues
|
||||
// Probably related to latency/throughput problems in the underlying tor network.
|
||||
time.Sleep(30 * time.Second)
|
||||
|
||||
aliceName, exists := bob.GetContactAttribute(alice.GetOnion(), attr.GetPeerScope(constants.Name))
|
||||
if !exists || aliceName != "Alice" {
|
||||
t.Fatalf("Bob: alice GetKeyVal error on alice peer.name %v\n", exists)
|
||||
aliceName, err := bob.GetConversationAttribute(bob2aliceConversationID, attr.PublicScope.ConstructScopedZonedPath(attr.ProfileZone.ConstructZonedPath(constants.Name)))
|
||||
if err != nil || aliceName != "Alice" {
|
||||
t.Fatalf("Bob: alice GetKeyVal error on alice peer.name %v: %v\n", aliceName, err)
|
||||
}
|
||||
fmt.Printf("Bob has alice's name as '%v'\n", aliceName)
|
||||
|
||||
bobName, exists := alice.GetContactAttribute(bob.GetOnion(), attr.GetPeerScope(constants.Name))
|
||||
if !exists || bobName != "Bob" {
|
||||
t.Fatalf("Alice: bob GetKeyVal error on bob peer.name\n")
|
||||
bobName, err := alice.GetConversationAttribute(alice2bobConversationID, attr.PublicScope.ConstructScopedZonedPath(attr.ProfileZone.ConstructZonedPath(constants.Name)))
|
||||
if err != nil || bobName != "Bob" {
|
||||
t.Fatalf("Alice: bob GetKeyVal error on bob peer.name %v: %v \n", bobName, err)
|
||||
}
|
||||
fmt.Printf("Alice has bob's name as '%v'\n", bobName)
|
||||
|
||||
aliceName, exists = carol.GetContactAttribute(alice.GetOnion(), attr.GetPeerScope(constants.Name))
|
||||
if !exists || aliceName != "Alice" {
|
||||
t.Fatalf("carol GetKeyVal error for alice peer.name %v\n", exists)
|
||||
aliceName, err = carol.GetConversationAttribute(carol2aliceConversationID, attr.PublicScope.ConstructScopedZonedPath(attr.ProfileZone.ConstructZonedPath(constants.Name)))
|
||||
if err != nil || aliceName != "Alice" {
|
||||
t.Fatalf("carol GetKeyVal error for alice peer.name %v: %v\n", aliceName, err)
|
||||
}
|
||||
|
||||
carolName, exists := alice.GetContactAttribute(carol.GetOnion(), attr.GetPeerScope(constants.Name))
|
||||
if !exists || carolName != "Carol" {
|
||||
t.Fatalf("alice GetKeyVal error, carol peer.name\n")
|
||||
carolName, err := alice.GetConversationAttribute(alice2carolConversationID, attr.PublicScope.ConstructScopedZonedPath(attr.ProfileZone.ConstructZonedPath(constants.Name)))
|
||||
if err != nil || carolName != "Carol" {
|
||||
t.Fatalf("alice GetKeyVal error, carol peer.name: %v: %v\n", carolName, err)
|
||||
}
|
||||
fmt.Printf("Alice has carol's name as '%v'\n", carolName)
|
||||
|
||||
// Group Testing
|
||||
|
||||
// Simulate Alice Creating a Group
|
||||
fmt.Println("Alice joining server...")
|
||||
if _, err := alice.AddServer(string(serverKeyBundle)); err != nil {
|
||||
t.Fatalf("Failed to Add Server Bundle %v", err)
|
||||
}
|
||||
|
||||
bob.AddServer(string(serverKeyBundle))
|
||||
carol.AddServer(string(serverKeyBundle))
|
||||
|
||||
t.Logf("Waiting for alice to join server...")
|
||||
err = alice.JoinServer(ServerAddr)
|
||||
if err != nil {
|
||||
t.Fatalf("alice cannot join server %v %v", ServerAddr, err)
|
||||
}
|
||||
waitForConnection(t, alice, ServerAddr, connections.SYNCED)
|
||||
|
||||
// Creating a Group
|
||||
t.Logf("Creating group on %v...", ServerAddr)
|
||||
aliceGroupConversationID, err := alice.StartGroup("Our Cool Testing Group", ServerAddr)
|
||||
t.Logf("Created group: %v!\n", aliceGroupConversationID)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to init group: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Invites
|
||||
fmt.Println("Alice inviting Bob to group...")
|
||||
err = alice.InviteOnionToGroup(bob.GetOnion(), groupID)
|
||||
err = alice.SendInviteToConversation(alice2bobConversationID, aliceGroupConversationID)
|
||||
if err != nil {
|
||||
t.Fatalf("Error for Alice inviting Bob to group: %v", err)
|
||||
}
|
||||
|
||||
time.Sleep(time.Second * 5)
|
||||
|
||||
fmt.Println("Bob examining groups and accepting invites...")
|
||||
for _, message := range bob.GetContact(alice.GetOnion()).Timeline.GetMessages() {
|
||||
fmt.Printf("Found message from Alice: %v", message.Message)
|
||||
if strings.HasPrefix(message.Message, "torv3") {
|
||||
gid, err := bob.ImportGroup(message.Message)
|
||||
if err == nil {
|
||||
fmt.Printf("Bob found invite...now accepting %v...", gid)
|
||||
bob.AcceptInvite(gid)
|
||||
} else {
|
||||
t.Fatalf("Bob could not accept invite...%v", gid)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Alice invites Bob to the Group...
|
||||
message, _, err := bob.GetChannelMessage(bob2aliceConversationID, 0, 1)
|
||||
t.Logf("Alice message to Bob %v %v", message, err)
|
||||
var overlayMessage model.MessageWrapper
|
||||
json.Unmarshal([]byte(message), &overlayMessage)
|
||||
t.Logf("Parsed Overlay Message: %v", overlayMessage)
|
||||
err = bob.ImportBundle(overlayMessage.Data)
|
||||
t.Logf("Result of Bob Importing the Bundle from Alice: %v", err)
|
||||
|
||||
fmt.Println("Waiting for Bob to join connect to group server...")
|
||||
waitForPeerGroupConnection(t, bob, groupID)
|
||||
t.Logf("Waiting for Bob to join connect to group server...")
|
||||
err = bob.JoinServer(ServerAddr) // for some unrealism we skip "discovering the server from the event bus
|
||||
if err != nil {
|
||||
t.Fatalf("alice cannot join server %v %v", ServerAddr, err)
|
||||
}
|
||||
bobGroupConversationID := 3
|
||||
waitForConnection(t, bob, ServerAddr, connections.SYNCED)
|
||||
|
||||
numGoRoutinesPostServerConnect := runtime.NumGoroutine()
|
||||
|
||||
// ***** Conversation *****
|
||||
t.Logf("Starting conversation in group...")
|
||||
checkSendMessageToGroup(t, alice, aliceGroupConversationID, aliceLines[0])
|
||||
checkSendMessageToGroup(t, bob, bobGroupConversationID, bobLines[0])
|
||||
checkSendMessageToGroup(t, alice, aliceGroupConversationID, aliceLines[1])
|
||||
checkSendMessageToGroup(t, bob, bobGroupConversationID, bobLines[1])
|
||||
|
||||
fmt.Println("Starting conversation in group...")
|
||||
// Conversation
|
||||
fmt.Printf("%v> %v\n", aliceName, aliceLines[0])
|
||||
err = alice.SendMessage(groupID, aliceLines[0])
|
||||
// Alice invites Bob to the Group...
|
||||
message, _, err = carol.GetChannelMessage(carol2aliceConversationID, 0, 1)
|
||||
t.Logf("Alice message to Carol %v %v", message, err)
|
||||
json.Unmarshal([]byte(message), &overlayMessage)
|
||||
t.Logf("Parsed Overlay Message: %v", overlayMessage)
|
||||
err = carol.ImportBundle(overlayMessage.Data)
|
||||
t.Logf("Result of Carol Importing the Bundle from Alice: %v", err)
|
||||
|
||||
t.Logf("Waiting for Carol to join connect to group server...")
|
||||
err = carol.JoinServer(ServerAddr) // for some unrealism we skip "discovering the server from the event bus
|
||||
if err != nil {
|
||||
t.Fatalf("Alice failed to send a message to the group: %v", err)
|
||||
t.Fatalf("carol cannot join server %v %v", ServerAddr, err)
|
||||
}
|
||||
time.Sleep(time.Second * 10)
|
||||
carolGroupConversationID := 3
|
||||
waitForConnection(t, carol, ServerAddr, connections.SYNCED)
|
||||
|
||||
fmt.Printf("%v> %v\n", bobName, bobLines[0])
|
||||
err = bob.SendMessage(groupID, bobLines[0])
|
||||
if err != nil {
|
||||
t.Fatalf("Bob failed to send a message to the group: %v", err)
|
||||
}
|
||||
time.Sleep(time.Second * 10)
|
||||
numGoRoutinesPostCarolConnect := runtime.NumGoroutine()
|
||||
|
||||
fmt.Printf("%v> %v\n", aliceName, aliceLines[1])
|
||||
alice.SendMessage(groupID, aliceLines[1])
|
||||
time.Sleep(time.Second * 10)
|
||||
t.Logf("Shutting down Alice...")
|
||||
|
||||
fmt.Printf("%v> %v\n", bobName, bobLines[1])
|
||||
bob.SendMessage(groupID, bobLines[1])
|
||||
time.Sleep(time.Second * 10)
|
||||
// Check Alice Timeline
|
||||
checkMessage(t, alice, aliceGroupConversationID, 1, aliceLines[0])
|
||||
checkMessage(t, alice, aliceGroupConversationID, 2, bobLines[0])
|
||||
checkMessage(t, alice, aliceGroupConversationID, 3, aliceLines[1])
|
||||
checkMessage(t, alice, aliceGroupConversationID, 4, bobLines[1])
|
||||
|
||||
fmt.Println("Alice inviting Carol to group...")
|
||||
err = alice.InviteOnionToGroup(carol.GetOnion(), groupID)
|
||||
if err != nil {
|
||||
t.Fatalf("Error for Alice inviting Carol to group: %v", err)
|
||||
}
|
||||
time.Sleep(time.Second * 60) // Account for some token acquisition in Alice and Bob flows.
|
||||
fmt.Println("Carol examining groups and accepting invites...")
|
||||
for _, message := range carol.GetContact(alice.GetOnion()).Timeline.GetMessages() {
|
||||
fmt.Printf("Found message from Alice: %v", message.Message)
|
||||
if strings.HasPrefix(message.Message, "torv3") {
|
||||
gid, err := carol.ImportGroup(message.Message)
|
||||
if err == nil {
|
||||
fmt.Printf("Carol found invite...now accepting %v...", gid)
|
||||
carol.AcceptInvite(gid)
|
||||
} else {
|
||||
t.Fatalf("Carol could not accept invite...%v", gid)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println("Shutting down Alice...")
|
||||
app.ShutdownPeer(alice.GetOnion())
|
||||
time.Sleep(time.Second * 5)
|
||||
time.Sleep(time.Second * 3)
|
||||
numGoRoutinesPostAlice := runtime.NumGoroutine()
|
||||
|
||||
fmt.Println("Carol joining server...")
|
||||
carol.JoinServer(ServerAddr)
|
||||
waitForPeerGroupConnection(t, carol, groupID)
|
||||
numGoRotinesPostCarolConnect := runtime.NumGoroutine()
|
||||
|
||||
fmt.Printf("%v> %v", bobName, bobLines[2])
|
||||
bob.SendMessage(groupID, bobLines[2])
|
||||
// Bob should have enough tokens so we don't need to account for
|
||||
// token acquisition here...
|
||||
|
||||
fmt.Printf("%v> %v", carolName, carolLines[0])
|
||||
carol.SendMessage(groupID, carolLines[0])
|
||||
time.Sleep(time.Second * 30) // we need to account for spam-based token acquisition, but everything should
|
||||
// be warmed-up and delays should be pretty small.
|
||||
|
||||
// ***** Verify Test *****
|
||||
|
||||
fmt.Println("Final syncing time...")
|
||||
checkSendMessageToGroup(t, carol, carolGroupConversationID, carolLines[0])
|
||||
checkSendMessageToGroup(t, bob, bobGroupConversationID, bobLines[2])
|
||||
time.Sleep(time.Second * 30)
|
||||
|
||||
alicesGroup := alice.GetGroup(groupID)
|
||||
if alicesGroup == nil {
|
||||
t.Error("aliceGroup == nil")
|
||||
return
|
||||
}
|
||||
// Check Bob Timeline
|
||||
checkMessage(t, bob, bobGroupConversationID, 1, aliceLines[0])
|
||||
checkMessage(t, bob, bobGroupConversationID, 2, bobLines[0])
|
||||
checkMessage(t, bob, bobGroupConversationID, 3, aliceLines[1])
|
||||
checkMessage(t, bob, bobGroupConversationID, 4, bobLines[1])
|
||||
checkMessage(t, bob, bobGroupConversationID, 5, carolLines[0])
|
||||
checkMessage(t, bob, bobGroupConversationID, 6, bobLines[2])
|
||||
|
||||
fmt.Printf("Alice's TimeLine:\n")
|
||||
aliceVerified := printAndCountVerifedTimeline(t, alicesGroup.GetTimeline())
|
||||
if aliceVerified != 4 {
|
||||
t.Errorf("Alice did not have 4 verified messages")
|
||||
}
|
||||
// Check Carol Timeline
|
||||
checkMessage(t, carol, carolGroupConversationID, 1, aliceLines[0])
|
||||
checkMessage(t, carol, carolGroupConversationID, 2, bobLines[0])
|
||||
checkMessage(t, carol, carolGroupConversationID, 3, aliceLines[1])
|
||||
checkMessage(t, carol, carolGroupConversationID, 4, bobLines[1])
|
||||
checkMessage(t, carol, carolGroupConversationID, 5, carolLines[0])
|
||||
checkMessage(t, carol, carolGroupConversationID, 6, bobLines[2])
|
||||
|
||||
bobsGroup := bob.GetGroup(groupID)
|
||||
if bobsGroup == nil {
|
||||
t.Error("bobGroup == nil")
|
||||
return
|
||||
}
|
||||
fmt.Printf("Bob's TimeLine:\n")
|
||||
bobVerified := printAndCountVerifedTimeline(t, bobsGroup.GetTimeline())
|
||||
if bobVerified != 6 {
|
||||
t.Errorf("Bob did not have 6 verified messages")
|
||||
}
|
||||
|
||||
carolsGroup := carol.GetGroup(groupID)
|
||||
fmt.Printf("Carol's TimeLine:\n")
|
||||
carolVerified := printAndCountVerifedTimeline(t, carolsGroup.GetTimeline())
|
||||
if carolVerified != 6 {
|
||||
t.Errorf("Carol did not have 6 verified messages")
|
||||
}
|
||||
|
||||
if len(alicesGroup.GetTimeline()) != 4 {
|
||||
t.Errorf("Alice's timeline does not have all messages")
|
||||
} else {
|
||||
// check message 0,1,2,3
|
||||
alicesGroup.Timeline.Sort()
|
||||
aliceGroupTimeline := alicesGroup.GetTimeline()
|
||||
if aliceGroupTimeline[0].Message != aliceLines[0] || aliceGroupTimeline[1].Message != bobLines[0] ||
|
||||
aliceGroupTimeline[2].Message != aliceLines[1] || aliceGroupTimeline[3].Message != bobLines[1] {
|
||||
t.Errorf("Some of Alice's timeline messages did not have the expected content!")
|
||||
}
|
||||
}
|
||||
|
||||
if len(bobsGroup.GetTimeline()) != 6 {
|
||||
t.Errorf("Bob's timeline does not have all messages")
|
||||
} else {
|
||||
// check message 0,1,2,3,4,5
|
||||
bobsGroup.Timeline.Sort()
|
||||
bobGroupTimeline := bobsGroup.GetTimeline()
|
||||
if bobGroupTimeline[0].Message != aliceLines[0] || bobGroupTimeline[1].Message != bobLines[0] ||
|
||||
bobGroupTimeline[2].Message != aliceLines[1] || bobGroupTimeline[3].Message != bobLines[1] ||
|
||||
bobGroupTimeline[4].Message != bobLines[2] || bobGroupTimeline[5].Message != carolLines[0] {
|
||||
t.Errorf("Some of Bob's timeline messages did not have the expected content!")
|
||||
}
|
||||
}
|
||||
|
||||
if len(carolsGroup.GetTimeline()) != 6 {
|
||||
t.Errorf("Carol's timeline does not have all messages")
|
||||
} else {
|
||||
// check message 0,1,2,3,4,5
|
||||
carolsGroup.Timeline.Sort()
|
||||
carolGroupTimeline := carolsGroup.GetTimeline()
|
||||
if carolGroupTimeline[0].Message != aliceLines[0] || carolGroupTimeline[1].Message != bobLines[0] ||
|
||||
carolGroupTimeline[2].Message != aliceLines[1] || carolGroupTimeline[3].Message != bobLines[1] ||
|
||||
carolGroupTimeline[4].Message != carolLines[0] || carolGroupTimeline[5].Message != bobLines[2] {
|
||||
t.Errorf("Some of Carol's timeline messages did not have the expected content!")
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println("Shutting down Bob...")
|
||||
t.Logf("Shutting down Bob...")
|
||||
app.ShutdownPeer(bob.GetOnion())
|
||||
time.Sleep(time.Second * 3)
|
||||
numGoRoutinesPostBob := runtime.NumGoroutine()
|
||||
|
||||
fmt.Println("Shutting down Carol...")
|
||||
appClient.ShutdownPeer(carol.GetOnion())
|
||||
t.Logf("Shutting down Carol...")
|
||||
app.ShutdownPeer(carol.GetOnion())
|
||||
time.Sleep(time.Second * 3)
|
||||
numGoRoutinesPostCarol := runtime.NumGoroutine()
|
||||
|
||||
fmt.Println("Shutting down apps...")
|
||||
t.Logf("Shutting down apps...")
|
||||
fmt.Printf("app Shutdown: %v\n", runtime.NumGoroutine())
|
||||
app.Shutdown()
|
||||
fmt.Printf("appClientShutdown: %v\n", runtime.NumGoroutine())
|
||||
appClient.Shutdown()
|
||||
fmt.Printf("appServiceShutdown: %v\n", runtime.NumGoroutine())
|
||||
appService.Shutdown()
|
||||
|
||||
fmt.Printf("bridgeClientShutdown: %v\n", runtime.NumGoroutine())
|
||||
bridgeClient.Shutdown()
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
fmt.Printf("brideServiceShutdown: %v\n", runtime.NumGoroutine())
|
||||
bridgeService.Shutdown()
|
||||
time.Sleep(2 * time.Second)
|
||||
t.Logf("Done shutdown: %v\n", runtime.NumGoroutine())
|
||||
|
||||
fmt.Printf("Done shutdown: %v\n", runtime.NumGoroutine())
|
||||
numGoRoutinesPostAppShutdown := runtime.NumGoroutine()
|
||||
|
||||
fmt.Println("Shutting down ACN...")
|
||||
acn.Close()
|
||||
time.Sleep(time.Second * 2) // Server ^^ has a 5 second loop attempting reconnect before exiting
|
||||
t.Logf("Shutting down ACN...")
|
||||
acn.Restart() // kill all active tor connections...
|
||||
// acn.Close() TODO: ACN Now gets closed automatically with defer...attempting to close twice results in a dead lock...
|
||||
time.Sleep(time.Second * 30) // the network status plugin might keep goroutines alive for a minute before killing them
|
||||
numGoRoutinesPostACN := runtime.NumGoroutine()
|
||||
|
||||
numGoRoutinesPostAppShutdown := runtime.NumGoroutine()
|
||||
|
||||
// Printing out the current goroutines
|
||||
// Very useful if we are leaking any.
|
||||
pprof.Lookup("goroutine").WriteTo(os.Stdout, 1)
|
||||
|
||||
fmt.Printf("numGoRoutinesStart: %v\nnumGoRoutinesPostAppStart: %v\nnumGoRoutinesPostPeerStart: %v\nnumGoRoutinesPostPeerAndServerConnect: %v\n"+
|
||||
"numGoRoutinesPostAlice: %v\nnumGoRotinesPostCarolConnect: %v\nnumGoRoutinesPostBob: %v\nnumGoRoutinesPostCarol: %v\nnumGoRoutinesPostAppShutdown: %v\nnumGoRoutinesPostACN: %v\n",
|
||||
t.Logf("numGoRoutinesStart: %v\nnumGoRoutinesPostAppStart: %v\nnumGoRoutinesPostPeerStart: %v\nnumGoRoutinesPostPeerAndServerConnect: %v\n"+
|
||||
"numGoRoutinesPostAlice: %v\nnumGoRoutinesPostCarolConnect: %v\nnumGoRoutinesPostBob: %v\nnumGoRoutinesPostCarol: %v\nnumGoRoutinesPostAppShutdown: %v",
|
||||
numGoRoutinesStart, numGoRoutinesPostAppStart, numGoRoutinesPostPeerStart, numGoRoutinesPostServerConnect,
|
||||
numGoRoutinesPostAlice, numGoRotinesPostCarolConnect, numGoRoutinesPostBob, numGoRoutinesPostCarol, numGoRoutinesPostAppShutdown, numGoRoutinesPostACN)
|
||||
numGoRoutinesPostAlice, numGoRoutinesPostCarolConnect, numGoRoutinesPostBob, numGoRoutinesPostCarol, numGoRoutinesPostAppShutdown)
|
||||
|
||||
if numGoRoutinesStart != numGoRoutinesPostACN {
|
||||
t.Errorf("Number of GoRoutines at start (%v) does not match number of goRoutines after cleanup of peers and servers (%v), clean up failed, leak detected!", numGoRoutinesStart, numGoRoutinesPostACN)
|
||||
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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Utility function for sending a message from a peer to a group
|
||||
func checkSendMessageToGroup(t *testing.T, profile peer.CwtchPeer, id int, message string) {
|
||||
name, _ := profile.GetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants.Name)
|
||||
t.Logf("%v> %v\n", name, message)
|
||||
err := profile.SendMessage(id, message)
|
||||
if err != nil {
|
||||
t.Fatalf("Alice failed to send a message to the group: %v", err)
|
||||
}
|
||||
time.Sleep(time.Second * 10)
|
||||
}
|
||||
|
||||
// Utility function for testing that a message in a conversation is as expected
|
||||
func checkMessage(t *testing.T, profile peer.CwtchPeer, id int, messageID int, expected string) {
|
||||
message, _, err := profile.GetChannelMessage(id, 0, messageID)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected message %v expected: %v got error: %v", profile.GetOnion(), expected, err)
|
||||
}
|
||||
if message != expected {
|
||||
t.Fatalf("unexpected message %v expected: %v got: [%v]", profile.GetOnion(), expected, message)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,169 @@
|
|||
package encryptedstorage
|
||||
|
||||
import (
|
||||
// Import SQL Cipher
|
||||
"crypto/rand"
|
||||
app2 "cwtch.im/cwtch/app"
|
||||
"cwtch.im/cwtch/app/utils"
|
||||
"cwtch.im/cwtch/model"
|
||||
"cwtch.im/cwtch/model/constants"
|
||||
"cwtch.im/cwtch/peer"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"git.openprivacy.ca/openprivacy/connectivity/tor"
|
||||
"git.openprivacy.ca/openprivacy/log"
|
||||
_ "github.com/mutecomm/go-sqlcipher/v4"
|
||||
mrand "math/rand"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestEncryptedStorage(t *testing.T) {
|
||||
|
||||
log.SetLevel(log.LevelDebug)
|
||||
|
||||
os.Mkdir("tordir", 0700)
|
||||
dataDir := filepath.Join("tordir", "tor")
|
||||
os.MkdirAll(dataDir, 0700)
|
||||
|
||||
// we don't need real randomness for the port, just to avoid a possible conflict...
|
||||
mrand.Seed(int64(time.Now().Nanosecond()))
|
||||
socksPort := mrand.Intn(1000) + 9051
|
||||
controlPort := mrand.Intn(1000) + 9052
|
||||
|
||||
// generate a random password
|
||||
key := make([]byte, 64)
|
||||
_, err := rand.Read(key)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
tor.NewTorrc().WithSocksPort(socksPort).WithOnionTrafficOnly().WithHashedPassword(base64.StdEncoding.EncodeToString(key)).WithControlPort(controlPort).Build("tordir/tor/torrc")
|
||||
acn, err := tor.NewTorACNWithAuth("./tordir", path.Join("..", "..", "tor"), controlPort, tor.HashedPasswordAuthenticator{Password: base64.StdEncoding.EncodeToString(key)})
|
||||
if err != nil {
|
||||
t.Fatalf("Could not start Tor: %v", err)
|
||||
}
|
||||
|
||||
cwtchDir := path.Join(".", "encrypted_storage_profiles")
|
||||
os.RemoveAll(cwtchDir)
|
||||
os.Mkdir(cwtchDir, 0700)
|
||||
|
||||
fmt.Println("Creating Alice...")
|
||||
|
||||
defer acn.Close()
|
||||
acn.WaitTillBootstrapped()
|
||||
app := app2.NewApp(acn, cwtchDir)
|
||||
app.CreateTaggedPeer("alice", "password", constants.ProfileTypeV1Password)
|
||||
app.CreateTaggedPeer("bob", "password", constants.ProfileTypeV1Password)
|
||||
|
||||
alice := utils.WaitGetPeer(app, "alice")
|
||||
bob := utils.WaitGetPeer(app, "bob")
|
||||
|
||||
alice.Listen()
|
||||
bob.Listen()
|
||||
|
||||
// To keep this large test organized, we will break it down into sub tests...
|
||||
subTestAliceAddAndDeleteBob(t, alice, bob)
|
||||
|
||||
conversations, err := alice.FetchConversations()
|
||||
if err != nil || len(conversations) != 1 {
|
||||
t.Fatalf("unexpected issue when fetching all of alices conversations. Expected 1 got : %v %v", conversations, err)
|
||||
}
|
||||
|
||||
alice.PeerWithOnion(bob.GetOnion())
|
||||
|
||||
time.Sleep(time.Second * 40)
|
||||
|
||||
alice.SendMessage(2, "Hello Bob")
|
||||
if err != nil {
|
||||
t.Fatalf("alice should have been able to fetch her own message")
|
||||
}
|
||||
_, attr, _ := alice.GetChannelMessage(2, 0, 1)
|
||||
if attr[constants.AttrAck] != "false" {
|
||||
t.Fatalf("Alices message should have been acknowledged...yet")
|
||||
}
|
||||
|
||||
time.Sleep(time.Second * 30)
|
||||
|
||||
ci, _ := bob.FetchConversationInfo(alice.GetOnion())
|
||||
body, _, err := bob.GetChannelMessage(ci.ID, 0, 1)
|
||||
if body != "Hello Bob" || err != nil {
|
||||
t.Fatalf("unexpected message in conversation channel %v %v", body, err)
|
||||
} else {
|
||||
t.Logf("succesfully found message in conversation channel %v", body)
|
||||
}
|
||||
|
||||
// Check that we received an ACk...
|
||||
_, attr, err = alice.GetChannelMessage(2, 0, 1)
|
||||
if err != nil {
|
||||
t.Fatalf("alice should have been able to fetch her own message")
|
||||
}
|
||||
|
||||
if attr[constants.AttrAck] != "true" {
|
||||
t.Fatalf("Alices message should have been acknowledged.")
|
||||
}
|
||||
|
||||
if count, err := alice.GetChannelMessageCount(2, 0); err != nil || count != 1 {
|
||||
t.Fatalf("Channel should have a single message in it. Instead returned %v %v", count, err)
|
||||
}
|
||||
|
||||
messages, err := alice.GetMostRecentMessages(2, 0, 0, 10)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("fetching messages over offset should not result in error: %v", err)
|
||||
}
|
||||
|
||||
if len(messages) != 1 || len(messages) > 0 && messages[0].Body != "Hello Bob" {
|
||||
t.Fatalf("expeced GetMostRecentMessages to return 1, instead returned: %v %v", len(messages), messages)
|
||||
}
|
||||
|
||||
app.Shutdown()
|
||||
|
||||
}
|
||||
|
||||
// Sub Test testing that Alice can add Bob, delete the conversation associated with Bob, and then add Bob again
|
||||
// Under a different conversation identifier.
|
||||
func subTestAliceAddAndDeleteBob(t *testing.T, alice peer.CwtchPeer, bob peer.CwtchPeer) {
|
||||
|
||||
t.Logf("Starting Sub Test AliceAddAndDeleteBob")
|
||||
|
||||
alice.NewContactConversation(bob.GetOnion(), model.AccessControl{Read: true, Append: true, Blocked: false}, true)
|
||||
|
||||
// Test Basic Fetching
|
||||
bobCI, err := alice.FetchConversationInfo(bob.GetOnion())
|
||||
if bobCI == nil || err != nil {
|
||||
t.Fatalf("alice should have been able to fetch bobs conversationf info ci:%v err:%v", bobCI, err)
|
||||
} else {
|
||||
t.Logf("Bobs Conversation Info fetched successfully: %v", bobCI)
|
||||
}
|
||||
|
||||
oldID := bobCI.ID
|
||||
|
||||
alice.DeleteConversation(oldID)
|
||||
|
||||
// Test Basic Fetching
|
||||
bobCI, err = alice.FetchConversationInfo(bob.GetOnion())
|
||||
if bobCI != nil {
|
||||
t.Fatalf("alice should **not** have been able to fetch bobs conversationf info ci:%v err:%v", bobCI, err)
|
||||
} else {
|
||||
t.Logf("expected error fetching deleted conversation info: %v", err)
|
||||
}
|
||||
|
||||
alice.NewContactConversation(bob.GetOnion(), model.AccessControl{Read: true, Append: true, Blocked: false}, true)
|
||||
|
||||
// Test Basic Fetching
|
||||
bobCI, err = alice.FetchConversationInfo(bob.GetOnion())
|
||||
if bobCI == nil || err != nil {
|
||||
t.Fatalf("alice should have been able to fetch bobs conversationf info ci:%v err:%v", bobCI, err)
|
||||
} else {
|
||||
t.Logf("Bobs Conversation Info fetched successfully: %v", bobCI)
|
||||
}
|
||||
|
||||
if oldID == bobCI.ID {
|
||||
t.Fatalf("bob should have a different conversation ID. Instead it is the same as the old conversation id, meaning something has gone wrong in the storage engine.")
|
||||
}
|
||||
|
||||
}
|
|
@ -18,6 +18,8 @@ import (
|
|||
"fmt"
|
||||
"git.openprivacy.ca/openprivacy/connectivity/tor"
|
||||
"git.openprivacy.ca/openprivacy/log"
|
||||
// Import SQL Cipher
|
||||
_ "github.com/mutecomm/go-sqlcipher/v4"
|
||||
mrand "math/rand"
|
||||
"os"
|
||||
"os/user"
|
||||
|
@ -30,9 +32,7 @@ import (
|
|||
|
||||
func waitForPeerPeerConnection(t *testing.T, peera peer.CwtchPeer, peerb peer.CwtchPeer) {
|
||||
for {
|
||||
state, ok := peera.GetPeerState(peerb.GetOnion())
|
||||
if ok {
|
||||
//log.Infof("Waiting for Peer %v to peer with peer: %v - state: %v\n", peera.GetProfile().Name, peerb.GetProfile().Name, state)
|
||||
state := peera.GetPeerState(peerb.GetOnion())
|
||||
if state == connections.FAILED {
|
||||
t.Fatalf("%v could not connect to %v", peera.GetOnion(), peerb.GetOnion())
|
||||
}
|
||||
|
@ -41,20 +41,16 @@ func waitForPeerPeerConnection(t *testing.T, peera peer.CwtchPeer, peerb peer.Cw
|
|||
time.Sleep(time.Second * 5)
|
||||
continue
|
||||
} else {
|
||||
peerAName, _ := peera.GetScopedZonedAttribute(attr.LocalScope, attr.ProfileZone, constants.Name)
|
||||
peerBName, _ := peerb.GetScopedZonedAttribute(attr.LocalScope, attr.ProfileZone, constants.Name)
|
||||
peerAName, _ := peera.GetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants.Name)
|
||||
peerBName, _ := peerb.GetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants.Name)
|
||||
fmt.Printf("%v CONNECTED and AUTHED to %v\n", peerAName, peerBName)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func TestFileSharing(t *testing.T) {
|
||||
|
||||
numGoRoutinesStart := runtime.NumGoroutine()
|
||||
|
||||
os.RemoveAll("cwtch.out.png")
|
||||
os.RemoveAll("cwtch.out.png.manifest")
|
||||
|
||||
|
@ -81,7 +77,10 @@ func TestFileSharing(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatalf("Could not start Tor: %v", err)
|
||||
}
|
||||
acn.WaitTillBootstrapped()
|
||||
defer acn.Close()
|
||||
|
||||
numGoRoutinesStart := runtime.NumGoroutine()
|
||||
app := app2.NewApp(acn, "./storage")
|
||||
|
||||
usr, _ := user.Current()
|
||||
|
@ -91,11 +90,12 @@ func TestFileSharing(t *testing.T) {
|
|||
os.Mkdir(path.Join(cwtchDir, "testing"), 0700)
|
||||
|
||||
fmt.Println("Creating Alice...")
|
||||
app.CreatePeer("alice", "asdfasdf")
|
||||
app.CreateTaggedPeer("alice", "asdfasdf", "testing")
|
||||
|
||||
fmt.Println("Creating Bob...")
|
||||
app.CreatePeer("bob", "asdfasdf")
|
||||
app.CreateTaggedPeer("bob", "asdfasdf", "testing")
|
||||
|
||||
t.Logf("** Waiting for Alice, Bob...")
|
||||
alice := utils.WaitGetPeer(app, "alice")
|
||||
bob := utils.WaitGetPeer(app, "bob")
|
||||
|
||||
|
@ -105,13 +105,15 @@ func TestFileSharing(t *testing.T) {
|
|||
queueOracle := event.NewQueue()
|
||||
app.GetEventBus(bob.GetOnion()).Subscribe(event.FileDownloaded, queueOracle)
|
||||
|
||||
t.Logf("** Launching Peers...")
|
||||
app.LaunchPeers()
|
||||
|
||||
waitTime := time.Duration(30) * time.Second
|
||||
t.Logf("** Waiting for Alice, Bob to connect with onion network... (%v)\n", waitTime)
|
||||
time.Sleep(waitTime)
|
||||
|
||||
bob.AddContact("alice?", alice.GetOnion(), model.AuthApproved)
|
||||
bob.NewContactConversation(alice.GetOnion(), model.DefaultP2PAccessControl(), true)
|
||||
alice.NewContactConversation(bob.GetOnion(), model.DefaultP2PAccessControl(), true)
|
||||
alice.PeerWithOnion(bob.GetOnion())
|
||||
|
||||
fmt.Println("Waiting for alice and Bob to peer...")
|
||||
|
@ -121,7 +123,7 @@ func TestFileSharing(t *testing.T) {
|
|||
|
||||
filesharingFunctionality, _ := filesharing.FunctionalityGate(map[string]bool{"filesharing": true})
|
||||
|
||||
err = filesharingFunctionality.ShareFile("cwtch.png", alice, bob.GetOnion())
|
||||
err = filesharingFunctionality.ShareFile("cwtch.png", alice, 1)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Error!: %v", err)
|
||||
|
@ -130,38 +132,37 @@ func TestFileSharing(t *testing.T) {
|
|||
// Wait for the messages to arrive...
|
||||
time.Sleep(time.Second * 10)
|
||||
|
||||
for _, message := range bob.GetContact(alice.GetOnion()).Timeline.GetMessages() {
|
||||
message, _, err := bob.GetChannelMessage(1, 0, 1)
|
||||
if err != nil {
|
||||
t.Fatalf("could not find file sharing message: %v", err)
|
||||
}
|
||||
|
||||
var messageWrapper model.MessageWrapper
|
||||
json.Unmarshal([]byte(message.Message), &messageWrapper)
|
||||
json.Unmarshal([]byte(message), &messageWrapper)
|
||||
|
||||
if messageWrapper.Overlay == model.OverlayFileSharing {
|
||||
var fileMessageOverlay filesharing.OverlayMessage
|
||||
err := json.Unmarshal([]byte(messageWrapper.Data), &fileMessageOverlay)
|
||||
|
||||
if err == nil {
|
||||
filesharingFunctionality.DownloadFile(bob, alice.GetOnion(), "cwtch.out.png", "cwtch.out.png.manifest", fmt.Sprintf("%s.%s", fileMessageOverlay.Hash, fileMessageOverlay.Nonce))
|
||||
filesharingFunctionality.DownloadFile(bob, 1, "cwtch.out.png", "cwtch.out.png.manifest", fmt.Sprintf("%s.%s", fileMessageOverlay.Hash, fileMessageOverlay.Nonce))
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("Found message from Alice: %v", message.Message)
|
||||
}
|
||||
|
||||
// Wait for the file downloaded event
|
||||
ev := queueOracle.Next()
|
||||
if ev.EventType != event.FileDownloaded {
|
||||
t.Fatalf("Expected file download event")
|
||||
}
|
||||
|
||||
manifest, err := files.CreateManifest("cwtch.out.png")
|
||||
manifest, _ := files.CreateManifest("cwtch.out.png")
|
||||
if hex.EncodeToString(manifest.RootHash) != "8f0ed73bbb30db45b6a740b1251cae02945f48e4f991464d5f3607685c45dcd136a325dab2e5f6429ce2b715e602b20b5b16bf7438fb6235fefe912adcedb5fd" {
|
||||
t.Fatalf("file hash does not match expected %x: ", manifest.RootHash)
|
||||
}
|
||||
|
||||
queueOracle.Shutdown()
|
||||
app.Shutdown()
|
||||
acn.Close()
|
||||
|
||||
time.Sleep(3 * time.Second)
|
||||
numGoRoutinesPostACN := runtime.NumGoroutine()
|
||||
|
||||
// Printing out the current goroutines
|
||||
|
|
|
@ -9,7 +9,7 @@ go list ./... | xargs go vet
|
|||
echo ""
|
||||
echo "Linting:"
|
||||
|
||||
go list ./... | xargs golint
|
||||
staticcheck ./...
|
||||
|
||||
|
||||
echo "Time to format"
|
||||
|
@ -21,4 +21,4 @@ ineffassign .
|
|||
|
||||
# misspell (https://github.com/client9/misspell/cmd/misspell)
|
||||
echo "Checking for misspelled words..."
|
||||
misspell . | grep -v "vendor/" | grep -v "go.sum" | grep -v ".idea"
|
||||
misspell . | grep -v "testing/" | grep -v "vendor/" | grep -v "go.sum" | grep -v ".idea"
|
||||
|
|
|
@ -5,12 +5,10 @@ pwd
|
|||
GORACE="haltonerror=1"
|
||||
go test -race ${1} -coverprofile=model.cover.out -v ./model
|
||||
go test -race ${1} -coverprofile=event.cover.out -v ./event
|
||||
go test -race ${1} -coverprofile=storage.v0.cover.out -v ./storage/v0
|
||||
go test -race ${1} -coverprofile=storage.v1.cover.out -v ./storage/v1
|
||||
go test -race ${1} -coverprofile=storage.cover.out -v ./storage
|
||||
go test -race ${1} -coverprofile=peer.connections.cover.out -v ./protocol/connections
|
||||
go test -race ${1} -coverprofile=peer.filesharing.cover.out -v ./protocol/files
|
||||
go test -race ${1} -coverprofile=peer.cover.out -v ./peer
|
||||
echo "mode: set" > coverage.out && cat *.cover.out | grep -v mode: | sort -r | \
|
||||
awk '{if($1 != last) {print $0;last=$1}}' >> coverage.out
|
||||
rm -rf *.cover.out
|
||||
|
|
Loading…
Reference in New Issue