Interim Work - P2P now Works on New Storage Model
continuous-integration/drone/push Build is pending
Details
continuous-integration/drone/push Build is pending
Details
This commit is contained in:
parent
e2bba41a9a
commit
8c80340a3d
118
app/app.go
118
app/app.go
|
@ -1,16 +1,16 @@
|
||||||
package app
|
package app
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/rand"
|
||||||
"cwtch.im/cwtch/app/plugins"
|
"cwtch.im/cwtch/app/plugins"
|
||||||
"cwtch.im/cwtch/event"
|
"cwtch.im/cwtch/event"
|
||||||
"cwtch.im/cwtch/model"
|
|
||||||
"cwtch.im/cwtch/model/attr"
|
"cwtch.im/cwtch/model/attr"
|
||||||
"cwtch.im/cwtch/model/constants"
|
"cwtch.im/cwtch/model/constants"
|
||||||
"cwtch.im/cwtch/peer"
|
"cwtch.im/cwtch/peer"
|
||||||
"cwtch.im/cwtch/protocol/connections"
|
"cwtch.im/cwtch/protocol/connections"
|
||||||
"cwtch.im/cwtch/storage"
|
"cwtch.im/cwtch/storage"
|
||||||
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
"git.openprivacy.ca/cwtch.im/tapir/primitives"
|
|
||||||
"git.openprivacy.ca/openprivacy/connectivity"
|
"git.openprivacy.ca/openprivacy/connectivity"
|
||||||
"git.openprivacy.ca/openprivacy/log"
|
"git.openprivacy.ca/openprivacy/log"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
@ -32,7 +32,6 @@ type application struct {
|
||||||
appletPeers
|
appletPeers
|
||||||
appletACN
|
appletACN
|
||||||
appletPlugins
|
appletPlugins
|
||||||
storage map[string]storage.ProfileStore
|
|
||||||
engines map[string]connections.Engine
|
engines map[string]connections.Engine
|
||||||
appBus event.Manager
|
appBus event.Manager
|
||||||
appmutex sync.Mutex
|
appmutex sync.Mutex
|
||||||
|
@ -41,7 +40,6 @@ type application struct {
|
||||||
// Application is a full cwtch peer application. It allows management, usage and storage of multiple peers
|
// Application is a full cwtch peer application. It allows management, usage and storage of multiple peers
|
||||||
type Application interface {
|
type Application interface {
|
||||||
LoadProfiles(password string)
|
LoadProfiles(password string)
|
||||||
CreatePeer(name string, password string)
|
|
||||||
CreateTaggedPeer(name string, password string, tag string)
|
CreateTaggedPeer(name string, password string, tag string)
|
||||||
DeletePeer(onion string, currentPassword string)
|
DeletePeer(onion string, currentPassword string)
|
||||||
AddPeerPlugin(onion string, pluginID plugins.PluginID)
|
AddPeerPlugin(onion string, pluginID plugins.PluginID)
|
||||||
|
@ -61,7 +59,7 @@ type Application interface {
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadProfileFn is the function signature for a function in an app that loads a profile
|
// 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 {
|
func newAppCore(appDirectory string) *applicationCore {
|
||||||
appCore := &applicationCore{eventBuses: make(map[string]event.Manager), directory: appDirectory}
|
appCore := &applicationCore{eventBuses: make(map[string]event.Manager), directory: appDirectory}
|
||||||
|
@ -72,33 +70,13 @@ func newAppCore(appDirectory string) *applicationCore {
|
||||||
// NewApp creates a new app with some environment awareness and initializes a Tor Manager
|
// NewApp creates a new app with some environment awareness and initializes a Tor Manager
|
||||||
func NewApp(acn connectivity.ACN, appDirectory string) Application {
|
func NewApp(acn connectivity.ACN, appDirectory string) Application {
|
||||||
log.Debugf("NewApp(%v)\n", appDirectory)
|
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.appletPeers.init()
|
||||||
|
|
||||||
app.appletACN.init(acn, app.getACNStatusHandler())
|
app.appletACN.init(acn, app.getACNStatusHandler())
|
||||||
return app
|
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) {
|
func (ac *applicationCore) DeletePeer(onion string) {
|
||||||
ac.coremutex.Lock()
|
ac.coremutex.Lock()
|
||||||
defer ac.coremutex.Unlock()
|
defer ac.coremutex.Unlock()
|
||||||
|
@ -107,38 +85,35 @@ func (ac *applicationCore) DeletePeer(onion string) {
|
||||||
delete(ac.eventBuses, onion)
|
delete(ac.eventBuses, onion)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GenerateRandomID generates a random 16 byte hex id code
|
||||||
|
func GenerateRandomID() string {
|
||||||
|
randBytes := make([]byte, 16)
|
||||||
|
rand.Read(randBytes)
|
||||||
|
return path.Join(hex.EncodeToString(randBytes))
|
||||||
|
}
|
||||||
|
|
||||||
func (app *application) CreateTaggedPeer(name string, password string, tag string) {
|
func (app *application) CreateTaggedPeer(name string, password string, tag string) {
|
||||||
profile, err := app.applicationCore.CreatePeer(name)
|
|
||||||
|
profileDirectory := path.Join(app.directory, "profiles", GenerateRandomID())
|
||||||
|
|
||||||
|
profile, err := peer.CreateEncryptedStorePeer(profileDirectory, name, password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Errorf("Error Creating Peer: %v", err)
|
||||||
app.appBus.Publish(event.NewEventList(event.PeerError, event.Error, err.Error()))
|
app.appBus.Publish(event.NewEventList(event.PeerError, event.Error, err.Error()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
profileStore := storage.CreateProfileWriterStore(app.eventBuses[profile.Onion], path.Join(app.directory, "profiles", profile.LocalID), password, profile)
|
eventBus := event.NewEventManager()
|
||||||
app.storage[profile.Onion] = profileStore
|
app.eventBuses[profile.GetOnion()] = eventBus
|
||||||
|
profile.Init(app.eventBuses[profile.GetOnion()])
|
||||||
pc := app.storage[profile.Onion].GetProfileCopy(true)
|
app.peers[profile.GetOnion()] = profile
|
||||||
p := peer.FromProfile(pc)
|
app.engines[profile.GetOnion()] = profile.GenerateProtocolEngine(app.acn, app.eventBuses[profile.GetOnion()])
|
||||||
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
|
|
||||||
|
|
||||||
if tag != "" {
|
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}))
|
app.appBus.Publish(event.NewEvent(event.NewPeer, map[event.Field]string{event.Identity: profile.GetOnion(), 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, "")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *application) DeletePeer(onion string, password string) {
|
func (app *application) DeletePeer(onion string, password string) {
|
||||||
|
@ -146,7 +121,7 @@ func (app *application) DeletePeer(onion string, password string) {
|
||||||
app.appmutex.Lock()
|
app.appmutex.Lock()
|
||||||
defer app.appmutex.Unlock()
|
defer app.appmutex.Unlock()
|
||||||
|
|
||||||
if app.storage[onion].CheckPassword(password) {
|
//if app.storage[onion].CheckPassword(password) {
|
||||||
app.appletPlugins.ShutdownPeer(onion)
|
app.appletPlugins.ShutdownPeer(onion)
|
||||||
app.plugins.Delete(onion)
|
app.plugins.Delete(onion)
|
||||||
|
|
||||||
|
@ -156,17 +131,13 @@ func (app *application) DeletePeer(onion string, password string) {
|
||||||
app.engines[onion].Shutdown()
|
app.engines[onion].Shutdown()
|
||||||
delete(app.engines, onion)
|
delete(app.engines, onion)
|
||||||
|
|
||||||
app.storage[onion].Shutdown()
|
|
||||||
app.storage[onion].Delete()
|
|
||||||
delete(app.storage, onion)
|
|
||||||
|
|
||||||
app.eventBuses[onion].Publish(event.NewEventList(event.ShutdownPeer, event.Identity, onion))
|
app.eventBuses[onion].Publish(event.NewEventList(event.ShutdownPeer, event.Identity, onion))
|
||||||
|
|
||||||
app.applicationCore.DeletePeer(onion)
|
app.applicationCore.DeletePeer(onion)
|
||||||
log.Debugf("Delete peer for %v Done\n", onion)
|
log.Debugf("Delete peer for %v Done\n", onion)
|
||||||
app.appBus.Publish(event.NewEventList(event.PeerDeleted, event.Identity, onion))
|
app.appBus.Publish(event.NewEventList(event.PeerDeleted, event.Identity, onion))
|
||||||
return
|
return
|
||||||
}
|
//}
|
||||||
app.appBus.Publish(event.NewEventList(event.AppError, event.Error, event.PasswordMatchError, event.Identity, onion))
|
app.appBus.Publish(event.NewEventList(event.AppError, event.Error, event.PasswordMatchError, event.Identity, onion))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -186,8 +157,21 @@ func (ac *applicationCore) LoadProfiles(password string, timeline bool, loadProf
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, file := range files {
|
for _, file := range files {
|
||||||
|
|
||||||
|
// 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...
|
||||||
|
loadProfileFn(profile)
|
||||||
|
}
|
||||||
|
|
||||||
|
// On failure
|
||||||
|
if err != nil {
|
||||||
eventBus := event.NewEventManager()
|
eventBus := event.NewEventManager()
|
||||||
profileStore, err := storage.LoadProfileWriterStore(eventBus, path.Join(ac.directory, "profiles", file.Name()), password)
|
|
||||||
|
profileStore, err := storage.LoadProfileWriterStore(eventBus, profileDirectory, password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
@ -206,7 +190,7 @@ func (ac *applicationCore) LoadProfiles(password string, timeline bool, loadProf
|
||||||
ac.eventBuses[profile.Onion] = eventBus
|
ac.eventBuses[profile.Onion] = eventBus
|
||||||
ac.coremutex.Unlock()
|
ac.coremutex.Unlock()
|
||||||
|
|
||||||
loadProfileFn(profile, profileStore)
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -214,19 +198,15 @@ 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
|
// 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) {
|
func (app *application) LoadProfiles(password string) {
|
||||||
count := 0
|
count := 0
|
||||||
app.applicationCore.LoadProfiles(password, true, func(profile *model.Profile, profileStore storage.ProfileStore) {
|
app.applicationCore.LoadProfiles(password, true, func(profile peer.CwtchPeer) {
|
||||||
peer := peer.FromProfile(profile)
|
eventBus := event.NewEventManager()
|
||||||
peer.Init(app.eventBuses[profile.Onion])
|
app.eventBuses[profile.GetOnion()] = eventBus
|
||||||
|
profile.Init(app.eventBuses[profile.GetOnion()])
|
||||||
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.appmutex.Lock()
|
app.appmutex.Lock()
|
||||||
app.peers[profile.Onion] = peer
|
app.peers[profile.GetOnion()] = profile
|
||||||
app.storage[profile.Onion] = profileStore
|
app.engines[profile.GetOnion()] = profile.GenerateProtocolEngine(app.acn, app.eventBuses[profile.GetOnion()])
|
||||||
app.engines[profile.Onion] = engine
|
|
||||||
app.appmutex.Unlock()
|
app.appmutex.Unlock()
|
||||||
app.appBus.Publish(event.NewEvent(event.NewPeer, map[event.Field]string{event.Identity: profile.Onion, event.Created: event.False}))
|
app.appBus.Publish(event.NewEvent(event.NewPeer, map[event.Field]string{event.Identity: profile.GetOnion(), event.Created: event.False}))
|
||||||
count++
|
count++
|
||||||
})
|
})
|
||||||
if count == 0 {
|
if count == 0 {
|
||||||
|
@ -280,8 +260,6 @@ func (app *application) ShutdownPeer(onion string) {
|
||||||
delete(app.peers, onion)
|
delete(app.peers, onion)
|
||||||
app.engines[onion].Shutdown()
|
app.engines[onion].Shutdown()
|
||||||
delete(app.engines, onion)
|
delete(app.engines, onion)
|
||||||
app.storage[onion].Shutdown()
|
|
||||||
delete(app.storage, onion)
|
|
||||||
app.appletPlugins.Shutdown()
|
app.appletPlugins.Shutdown()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -293,8 +271,6 @@ func (app *application) Shutdown() {
|
||||||
app.appletPlugins.ShutdownPeer(id)
|
app.appletPlugins.ShutdownPeer(id)
|
||||||
log.Debugf("Shutting Down Engines for %v", id)
|
log.Debugf("Shutting Down Engines for %v", id)
|
||||||
app.engines[id].Shutdown()
|
app.engines[id].Shutdown()
|
||||||
log.Debugf("Shutting Down Storage for %v", id)
|
|
||||||
app.storage[id].Shutdown()
|
|
||||||
log.Debugf("Shutting Down Bus for %v", id)
|
log.Debugf("Shutting Down Bus for %v", id)
|
||||||
app.eventBuses[id].Shutdown()
|
app.eventBuses[id].Shutdown()
|
||||||
}
|
}
|
||||||
|
|
1
go.mod
1
go.mod
|
@ -7,6 +7,7 @@ require (
|
||||||
git.openprivacy.ca/openprivacy/connectivity v1.5.0
|
git.openprivacy.ca/openprivacy/connectivity v1.5.0
|
||||||
git.openprivacy.ca/openprivacy/log v1.0.3
|
git.openprivacy.ca/openprivacy/log v1.0.3
|
||||||
github.com/gtank/ristretto255 v0.1.2
|
github.com/gtank/ristretto255 v0.1.2
|
||||||
|
github.com/mutecomm/go-sqlcipher/v4 v4.4.2
|
||||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
|
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
|
||||||
golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee
|
golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee
|
||||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007 // indirect
|
golang.org/x/sys v0.0.0-20210510120138-977fb7262007 // indirect
|
||||||
|
|
3
go.sum
3
go.sum
|
@ -22,11 +22,14 @@ 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/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
github.com/mimoo/StrobeGo v0.0.0-20181016162300-f8f6d4d2b643 h1:hLDRPB66XQT/8+wG9WsDpiCvZf1yKO7sz7scAjSlBa0=
|
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/mimoo/StrobeGo v0.0.0-20181016162300-f8f6d4d2b643/go.mod h1:43+3pMjjKimDBf5Kr4ZFNGbLql1zKkbImw+fZbw3geM=
|
||||||
|
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/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
|
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/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
|
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
|
||||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
go.etcd.io/bbolt v1.3.4 h1:hi1bXHMVrlQh6WwxAy+qZCV/SYIlqo+Ushwdpa4tAKg=
|
go.etcd.io/bbolt v1.3.4 h1:hi1bXHMVrlQh6WwxAy+qZCV/SYIlqo+Ushwdpa4tAKg=
|
||||||
|
|
|
@ -0,0 +1,54 @@
|
||||||
|
package model
|
||||||
|
|
||||||
|
import "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
|
||||||
|
|
||||||
|
func (acl *AccessControlList) Serialize() []byte {
|
||||||
|
data, _ := json.Marshal(acl)
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeserializeAccessControlList(data []byte) AccessControlList {
|
||||||
|
var acl AccessControlList
|
||||||
|
json.Unmarshal(data, &acl)
|
||||||
|
return acl
|
||||||
|
}
|
||||||
|
|
||||||
|
type Attributes map[string]string
|
||||||
|
|
||||||
|
func (a *Attributes) Serialize() []byte {
|
||||||
|
data, _ := json.Marshal(a)
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
|
@ -2,11 +2,12 @@ package peer
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"cwtch.im/cwtch/model/constants"
|
"cwtch.im/cwtch/model/constants"
|
||||||
"encoding/base32"
|
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"git.openprivacy.ca/cwtch.im/tapir/primitives"
|
||||||
|
"git.openprivacy.ca/openprivacy/connectivity"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -50,53 +51,78 @@ type cwtchPeer struct {
|
||||||
mutex sync.Mutex
|
mutex sync.Mutex
|
||||||
shutdown bool
|
shutdown bool
|
||||||
listenStatus bool
|
listenStatus bool
|
||||||
|
storage *CwtchProfileStorage
|
||||||
|
|
||||||
|
state map[string]connections.ConnectionState
|
||||||
|
|
||||||
queue event.Queue
|
queue event.Queue
|
||||||
eventBus event.Manager
|
eventBus event.Manager
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GenerateProtocolEngine
|
||||||
|
// Status: New in 1.5
|
||||||
|
func (cp *cwtchPeer) GenerateProtocolEngine(acn connectivity.ACN, bus event.Manager) connections.Engine {
|
||||||
|
return connections.NewProtocolEngine(cp.GetIdentity(), cp.Profile.Ed25519PrivateKey, acn, bus, cp.Profile.ContactsAuthorizations())
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetIdentity
|
||||||
|
// Status: New in 1.5
|
||||||
|
func (cp *cwtchPeer) GetIdentity() primitives.Identity {
|
||||||
|
return primitives.InitializeIdentity("", &cp.Profile.Ed25519PrivateKey, &cp.Profile.Ed25519PublicKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendScopedZonedGetValToContact
|
||||||
|
// Status: No change in 1.5
|
||||||
func (cp *cwtchPeer) SendScopedZonedGetValToContact(handle string, scope attr.Scope, zone attr.Zone, path string) {
|
func (cp *cwtchPeer) SendScopedZonedGetValToContact(handle string, scope attr.Scope, zone attr.Zone, path string) {
|
||||||
ev := event.NewEventList(event.SendGetValMessageToPeer, event.RemotePeer, handle, event.Scope, string(scope), event.Path, string(zone.ConstructZonedPath(path)))
|
ev := event.NewEventList(event.SendGetValMessageToPeer, event.RemotePeer, handle, event.Scope, string(scope), event.Path, string(zone.ConstructZonedPath(path)))
|
||||||
cp.eventBus.Publish(ev)
|
cp.eventBus.Publish(ev)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetScopedZonedAttribute
|
||||||
|
// Status: Ready for 1.5
|
||||||
func (cp *cwtchPeer) GetScopedZonedAttribute(scope attr.Scope, zone attr.Zone, key string) (string, bool) {
|
func (cp *cwtchPeer) GetScopedZonedAttribute(scope attr.Scope, zone attr.Zone, key string) (string, bool) {
|
||||||
cp.mutex.Lock()
|
cp.mutex.Lock()
|
||||||
defer cp.mutex.Unlock()
|
defer cp.mutex.Unlock()
|
||||||
scopedZonedKey := scope.ConstructScopedZonedPath(zone.ConstructZonedPath(key))
|
scopedZonedKey := scope.ConstructScopedZonedPath(zone.ConstructZonedPath(key))
|
||||||
|
|
||||||
log.Debugf("looking up attribute %v %v %v (%v)", scope, zone, key, scopedZonedKey)
|
log.Debugf("looking up attribute %v %v %v (%v)", scope, zone, key, scopedZonedKey)
|
||||||
|
value, err := cp.storage.LoadProfileKeyValue(TypeAttribute, scopedZonedKey.ToString())
|
||||||
|
|
||||||
if val, exists := cp.Profile.GetAttribute(scopedZonedKey.ToString()); exists {
|
log.Debugf("found [%v] %v", string(value), err)
|
||||||
return val, true
|
if err != nil {
|
||||||
}
|
|
||||||
|
|
||||||
return "", false
|
return "", false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return string(value), true
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetScopedZonedAttribute
|
||||||
|
// Status: Ready for 1.5
|
||||||
func (cp *cwtchPeer) SetScopedZonedAttribute(scope attr.Scope, zone attr.Zone, key string, value string) {
|
func (cp *cwtchPeer) SetScopedZonedAttribute(scope attr.Scope, zone attr.Zone, key string, value string) {
|
||||||
cp.mutex.Lock()
|
cp.mutex.Lock()
|
||||||
|
defer cp.mutex.Unlock()
|
||||||
|
|
||||||
scopedZonedKey := scope.ConstructScopedZonedPath(zone.ConstructZonedPath(key))
|
scopedZonedKey := scope.ConstructScopedZonedPath(zone.ConstructZonedPath(key))
|
||||||
log.Debugf("storing attribute: %v = %v", scopedZonedKey, value)
|
log.Debugf("storing attribute: %v = %v", scopedZonedKey, value)
|
||||||
cp.Profile.SetAttribute(scopedZonedKey.ToString(), value)
|
err := cp.storage.StoreProfileKeyValue(TypeAttribute, scopedZonedKey.ToString(), []byte(value))
|
||||||
defer cp.mutex.Unlock()
|
|
||||||
cp.eventBus.Publish(event.NewEvent(event.SetAttribute, map[event.Field]string{
|
if err != nil {
|
||||||
event.Key: scopedZonedKey.ToString(),
|
log.Errorf("error setting attribute %v")
|
||||||
event.Data: value,
|
}
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SendMessage is a higher level that merges sending messages to contacts and group handles
|
// SendMessage is a higher level that merges sending messages to contacts and group handles
|
||||||
// If you try to send a message to a handle that doesn't exist, malformed or an incorrect type then
|
// If you try to send a message to a handle that doesn't exist, malformed or an incorrect type then
|
||||||
// this function will error
|
// this function will error
|
||||||
|
// Status: TODO
|
||||||
func (cp *cwtchPeer) SendMessage(handle string, message string) error {
|
func (cp *cwtchPeer) SendMessage(handle string, message string) error {
|
||||||
cp.mutex.Lock()
|
|
||||||
defer cp.mutex.Unlock()
|
|
||||||
|
|
||||||
var ev event.Event
|
var ev event.Event
|
||||||
// Group Handles are always 32 bytes in length, but we forgo any further testing here
|
// Group Handles are always 32 bytes in length, but we forgo any further testing here
|
||||||
// and delegate the group existence check to EncryptMessageToGroup
|
// and delegate the group existence check to EncryptMessageToGroup
|
||||||
if len(handle) == 32 {
|
if len(handle) == 32 {
|
||||||
|
cp.mutex.Lock()
|
||||||
|
defer cp.mutex.Unlock()
|
||||||
group := cp.Profile.GetGroup(handle)
|
group := cp.Profile.GetGroup(handle)
|
||||||
if group == nil {
|
if group == nil {
|
||||||
return errors.New("invalid group id")
|
return errors.New("invalid group id")
|
||||||
|
@ -112,14 +138,15 @@ func (cp *cwtchPeer) SendMessage(handle string, message string) error {
|
||||||
ev = event.NewEvent(event.SendMessageToGroup, map[event.Field]string{event.GroupID: handle, event.GroupServer: group.GroupServer, event.Ciphertext: base64.StdEncoding.EncodeToString(ct), event.Signature: base64.StdEncoding.EncodeToString(sig)})
|
ev = event.NewEvent(event.SendMessageToGroup, map[event.Field]string{event.GroupID: handle, event.GroupServer: group.GroupServer, event.Ciphertext: base64.StdEncoding.EncodeToString(ct), event.Signature: base64.StdEncoding.EncodeToString(sig)})
|
||||||
} else if tor.IsValidHostname(handle) {
|
} else if tor.IsValidHostname(handle) {
|
||||||
// We assume we are sending to a Contact.
|
// We assume we are sending to a Contact.
|
||||||
// (Servers are technically Contacts)
|
contact, exists := cp.FetchConversationInfo(handle)
|
||||||
contact, exists := cp.Profile.GetContact(handle)
|
|
||||||
ev = event.NewEvent(event.SendMessageToPeer, map[event.Field]string{event.RemotePeer: handle, event.Data: message})
|
ev = event.NewEvent(event.SendMessageToPeer, map[event.Field]string{event.RemotePeer: handle, event.Data: message})
|
||||||
// If the contact exists replace the event id wih the index of this message in the contacts timeline...
|
// If the contact exists replace the event id wih the index of this message in the contacts timeline...
|
||||||
// Otherwise assume we don't log the message in the timeline...
|
// Otherwise assume we don't log the message in the timeline...
|
||||||
if exists {
|
if exists != nil {
|
||||||
ev.EventID = strconv.Itoa(contact.Timeline.Len())
|
//ev.EventID = strconv.Itoa(contact.Timeline.Len())
|
||||||
cp.Profile.AddSentMessageToContactTimeline(handle, message, time.Now(), ev.EventID)
|
cp.mutex.Lock()
|
||||||
|
defer cp.mutex.Unlock()
|
||||||
|
cp.storage.InsertMessage(contact.ID, 0, message, model.Attributes{"ack": event.False, "sent": time.Now().String()})
|
||||||
}
|
}
|
||||||
// Regardless we publish the send message to peer event for the protocol engine to execute on...
|
// Regardless we publish the send message to peer event for the protocol engine to execute on...
|
||||||
// We assume this is always successful as it is always valid to attempt to
|
// We assume this is always successful as it is always valid to attempt to
|
||||||
|
@ -132,6 +159,8 @@ func (cp *cwtchPeer) SendMessage(handle string, message string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpdateMessageFlags
|
||||||
|
// Status: TODO
|
||||||
func (cp *cwtchPeer) UpdateMessageFlags(handle string, mIdx int, flags uint64) {
|
func (cp *cwtchPeer) UpdateMessageFlags(handle string, mIdx int, flags uint64) {
|
||||||
cp.mutex.Lock()
|
cp.mutex.Lock()
|
||||||
defer cp.mutex.Unlock()
|
defer cp.mutex.Unlock()
|
||||||
|
@ -142,162 +171,60 @@ func (cp *cwtchPeer) UpdateMessageFlags(handle string, mIdx int, flags uint64) {
|
||||||
|
|
||||||
// BlockUnknownConnections will auto disconnect from connections if authentication doesn't resolve a hostname
|
// BlockUnknownConnections will auto disconnect from connections if authentication doesn't resolve a hostname
|
||||||
// known to peer.
|
// known to peer.
|
||||||
|
// Status: Ready for 1.5
|
||||||
func (cp *cwtchPeer) BlockUnknownConnections() {
|
func (cp *cwtchPeer) BlockUnknownConnections() {
|
||||||
cp.eventBus.Publish(event.NewEvent(event.BlockUnknownPeers, map[event.Field]string{}))
|
cp.eventBus.Publish(event.NewEvent(event.BlockUnknownPeers, map[event.Field]string{}))
|
||||||
}
|
}
|
||||||
|
|
||||||
// AllowUnknownConnections will permit connections from unknown contacts.
|
// AllowUnknownConnections will permit connections from unknown contacts.
|
||||||
|
// Status: Ready for 1.5
|
||||||
func (cp *cwtchPeer) AllowUnknownConnections() {
|
func (cp *cwtchPeer) AllowUnknownConnections() {
|
||||||
cp.eventBus.Publish(event.NewEvent(event.AllowUnknownPeers, map[event.Field]string{}))
|
cp.eventBus.Publish(event.NewEvent(event.AllowUnknownPeers, map[event.Field]string{}))
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReadContacts is a meta-interface intended to restrict callers to read-only access to contacts
|
// NewProfileWithEncryptedStorage instantiates a new Cwtch Profile from encrypted storage
|
||||||
type ReadContacts interface {
|
func NewProfileWithEncryptedStorage(name string, cps *CwtchProfileStorage) CwtchPeer {
|
||||||
GetContacts() []string
|
|
||||||
GetContact(string) *model.PublicProfile
|
|
||||||
GetContactAttribute(string, string) (string, bool)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ModifyContacts is a meta-interface intended to restrict callers to modify-only access to contacts
|
|
||||||
type ModifyContacts interface {
|
|
||||||
AddContact(nick, onion string, authorization model.Authorization)
|
|
||||||
SetContactAuthorization(string, model.Authorization) error
|
|
||||||
SetContactAttribute(string, string, string)
|
|
||||||
DeleteContact(string)
|
|
||||||
}
|
|
||||||
|
|
||||||
// AccessPeeringState provides access to functions relating to the underlying connections of a peer.
|
|
||||||
type AccessPeeringState interface {
|
|
||||||
GetPeerState(string) (connections.ConnectionState, bool)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 {
|
|
||||||
ReadContacts
|
|
||||||
ModifyContacts
|
|
||||||
ModifyPeeringState
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadServers provides access to the servers
|
|
||||||
type ReadServers interface {
|
|
||||||
GetServers() []string
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadGroups provides read-only access to group state
|
|
||||||
type ReadGroups interface {
|
|
||||||
GetGroup(string) *model.Group
|
|
||||||
GetGroupState(string) (connections.ConnectionState, bool)
|
|
||||||
GetGroups() []string
|
|
||||||
GetGroupAttribute(string, string) (string, bool)
|
|
||||||
ExportGroup(string) (string, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ModifyGroups provides write-only access add/edit/remove new groups
|
|
||||||
type ModifyGroups interface {
|
|
||||||
ImportGroup(string) (string, error)
|
|
||||||
StartGroup(string) (string, string, error)
|
|
||||||
AcceptInvite(string) error
|
|
||||||
RejectInvite(string)
|
|
||||||
DeleteGroup(string)
|
|
||||||
SetGroupAttribute(string, string, string)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ModifyServers provides write-only access to servers
|
|
||||||
type ModifyServers interface {
|
|
||||||
AddServer(string) error
|
|
||||||
ResyncServer(onion string) error
|
|
||||||
}
|
|
||||||
|
|
||||||
// SendMessages enables a caller to sender messages to a contact
|
|
||||||
type SendMessages interface {
|
|
||||||
SendMessage(handle string, message string) error
|
|
||||||
|
|
||||||
// Deprecated: is unsafe
|
|
||||||
SendGetValToPeer(string, string, string)
|
|
||||||
|
|
||||||
SendScopedZonedGetValToContact(handle string, scope attr.Scope, zone attr.Zone, key string)
|
|
||||||
|
|
||||||
// TODO
|
|
||||||
// Deprecated use overlays instead
|
|
||||||
InviteOnionToGroup(string, string) error
|
|
||||||
}
|
|
||||||
|
|
||||||
// ModifyMessages enables a caller to modify the messages in a timeline
|
|
||||||
type ModifyMessages interface {
|
|
||||||
UpdateMessageFlags(string, int, uint64)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
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)
|
|
||||||
|
|
||||||
ReadContacts
|
|
||||||
ModifyContacts
|
|
||||||
|
|
||||||
AccessPeeringState
|
|
||||||
ModifyPeeringState
|
|
||||||
|
|
||||||
ReadGroups
|
|
||||||
ModifyGroups
|
|
||||||
|
|
||||||
ReadServers
|
|
||||||
ModifyServers
|
|
||||||
|
|
||||||
SendMessages
|
|
||||||
ModifyMessages
|
|
||||||
|
|
||||||
ShareFile(fileKey string, serializedManifest string)
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewCwtchPeer creates and returns a new cwtchPeer with the given name.
|
|
||||||
func NewCwtchPeer(name string) CwtchPeer {
|
|
||||||
cp := new(cwtchPeer)
|
cp := new(cwtchPeer)
|
||||||
cp.Profile = model.GenerateNewProfile(name)
|
|
||||||
cp.shutdown = false
|
cp.shutdown = false
|
||||||
|
cp.storage = cps
|
||||||
|
cp.state = make(map[string]connections.ConnectionState)
|
||||||
|
cp.Profile = model.GenerateNewProfile(name)
|
||||||
|
|
||||||
|
// Store all the Necessary Base Attributes In The Database
|
||||||
|
cp.storage.StoreProfileKeyValue(TypeAttribute, attr.PublicScope.ConstructScopedZonedPath(attr.ProfileZone.ConstructZonedPath(constants.Name)).ToString(), []byte(name))
|
||||||
|
cp.storage.StoreProfileKeyValue(TypePrivateKey, "Ed25519PrivateKey", cp.Profile.Ed25519PrivateKey)
|
||||||
|
cp.storage.StoreProfileKeyValue(TypePublicKey, "Ed25519PublicKey", cp.Profile.Ed25519PublicKey)
|
||||||
|
|
||||||
|
return cp
|
||||||
|
}
|
||||||
|
|
||||||
|
// FromEncryptedStorage loads an existing Profile from Encrypted Storage
|
||||||
|
func FromEncryptedStorage(cps *CwtchProfileStorage) CwtchPeer {
|
||||||
|
cp := new(cwtchPeer)
|
||||||
|
cp.shutdown = false
|
||||||
|
cp.storage = cps
|
||||||
|
cp.state = make(map[string]connections.ConnectionState)
|
||||||
return cp
|
return cp
|
||||||
}
|
}
|
||||||
|
|
||||||
// FromProfile generates a new peer from a profile.
|
// FromProfile generates a new peer from a profile.
|
||||||
func FromProfile(profile *model.Profile) CwtchPeer {
|
// Deprecated - Only to be used for importing new profiles
|
||||||
|
func FromProfile(profile *model.Profile, cps *CwtchProfileStorage) CwtchPeer {
|
||||||
cp := new(cwtchPeer)
|
cp := new(cwtchPeer)
|
||||||
cp.Profile = profile
|
cp.Profile = profile
|
||||||
cp.shutdown = false
|
cp.shutdown = false
|
||||||
|
cp.storage = cps
|
||||||
|
|
||||||
|
// Store all the Necessary Base Attributes In The Database
|
||||||
|
cp.storage.StoreProfileKeyValue(TypeAttribute, "public.profile.name", []byte(profile.Name))
|
||||||
|
cp.storage.StoreProfileKeyValue(TypePrivateKey, "Ed25519PrivateKey", cp.Profile.Ed25519PrivateKey)
|
||||||
|
cp.storage.StoreProfileKeyValue(TypePublicKey, "Ed25519PublicKey", cp.Profile.Ed25519PublicKey)
|
||||||
|
|
||||||
return cp
|
return cp
|
||||||
}
|
}
|
||||||
|
|
||||||
// Init instantiates a cwtchPeer
|
// Init instantiates a cwtchPeer
|
||||||
|
// Status: Ready for 1.5
|
||||||
func (cp *cwtchPeer) Init(eventBus event.Manager) {
|
func (cp *cwtchPeer) Init(eventBus event.Manager) {
|
||||||
cp.InitForEvents(eventBus, DefaultEventsToHandle)
|
cp.InitForEvents(eventBus, DefaultEventsToHandle)
|
||||||
|
|
||||||
|
@ -354,6 +281,8 @@ func (cp *cwtchPeer) Init(eventBus event.Manager) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// InitForEvents
|
||||||
|
// Status: Ready for 1.5
|
||||||
func (cp *cwtchPeer) InitForEvents(eventBus event.Manager, toBeHandled []event.Type) {
|
func (cp *cwtchPeer) InitForEvents(eventBus event.Manager, toBeHandled []event.Type) {
|
||||||
cp.queue = event.NewQueue()
|
cp.queue = event.NewQueue()
|
||||||
go cp.eventHandler()
|
go cp.eventHandler()
|
||||||
|
@ -363,6 +292,7 @@ func (cp *cwtchPeer) InitForEvents(eventBus event.Manager, toBeHandled []event.T
|
||||||
}
|
}
|
||||||
|
|
||||||
// AutoHandleEvents sets an event (if able) to be handled by this peer
|
// AutoHandleEvents sets an event (if able) to be handled by this peer
|
||||||
|
// Status: Ready for 1.5
|
||||||
func (cp *cwtchPeer) AutoHandleEvents(events []event.Type) {
|
func (cp *cwtchPeer) AutoHandleEvents(events []event.Type) {
|
||||||
for _, ev := range events {
|
for _, ev := range events {
|
||||||
if _, exists := autoHandleableEvents[ev]; exists {
|
if _, exists := autoHandleableEvents[ev]; exists {
|
||||||
|
@ -374,6 +304,7 @@ func (cp *cwtchPeer) AutoHandleEvents(events []event.Type) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ImportGroup initializes a group from an imported source rather than a peer invite
|
// ImportGroup initializes a group from an imported source rather than a peer invite
|
||||||
|
// Status: TODO
|
||||||
func (cp *cwtchPeer) ImportGroup(exportedInvite string) (string, error) {
|
func (cp *cwtchPeer) ImportGroup(exportedInvite string) (string, error) {
|
||||||
cp.mutex.Lock()
|
cp.mutex.Lock()
|
||||||
defer cp.mutex.Unlock()
|
defer cp.mutex.Unlock()
|
||||||
|
@ -386,7 +317,65 @@ func (cp *cwtchPeer) ImportGroup(exportedInvite string) (string, error) {
|
||||||
return gid, err
|
return gid, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewContactConversation create a new p2p conversation with the given acl applied to the handle.
|
||||||
|
func (cp *cwtchPeer) NewContactConversation(handle string, acl model.AccessControl, accepted bool) error {
|
||||||
|
cp.mutex.Lock()
|
||||||
|
defer cp.mutex.Unlock()
|
||||||
|
return cp.storage.NewConversation(handle, model.Attributes{event.SaveHistoryKey: event.DeleteHistoryDefault}, model.AccessControlList{handle: acl}, accepted)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AcceptConversation looks up a conversation by `handle` and sets the Accepted status to `true`
|
||||||
|
// This will cause Cwtch to auto connect to this conversation on start up
|
||||||
|
func (cp *cwtchPeer) AcceptConversation(handle string) error {
|
||||||
|
cp.mutex.Lock()
|
||||||
|
defer cp.mutex.Unlock()
|
||||||
|
return cp.storage.AcceptConversation(handle)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FetchConversationInfo returns information about the given conversation referenced by the handle
|
||||||
|
func (cp *cwtchPeer) FetchConversationInfo(handle string) (*model.Conversation, error) {
|
||||||
|
cp.mutex.Lock()
|
||||||
|
defer cp.mutex.Unlock()
|
||||||
|
return cp.storage.GetConversation(handle)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteConversation purges all data about the conversation, including message timelines, referenced by the handle
|
||||||
|
func (cp *cwtchPeer) DeleteConversation(handle string) error {
|
||||||
|
cp.mutex.Lock()
|
||||||
|
defer cp.mutex.Unlock()
|
||||||
|
return cp.storage.DeleteConversation(handle)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetConversationAttribute sets the conversation attribute at path to value
|
||||||
|
func (cp *cwtchPeer) SetConversationAttribute(handle string, path attr.ScopedZonedPath, value string) error {
|
||||||
|
cp.mutex.Lock()
|
||||||
|
defer cp.mutex.Unlock()
|
||||||
|
return cp.storage.SetConversationAttribute(handle, path, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetConversationAttribute is a shortcut method for retrieving the value of a given path
|
||||||
|
func (cp *cwtchPeer) GetConversationAttribute(handle string, path attr.ScopedZonedPath) (string, error) {
|
||||||
|
cp.mutex.Lock()
|
||||||
|
defer cp.mutex.Unlock()
|
||||||
|
ci, err := cp.storage.GetConversation(handle)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
val, exists := ci.Attributes[path.ToString()]
|
||||||
|
if !exists {
|
||||||
|
return "", fmt.Errorf("%v does not exist for conversation %v", path.ToString(), handle)
|
||||||
|
}
|
||||||
|
return val, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cp *cwtchPeer) GetChannelMessage(conversation int, channel int, id int) (string, model.Attributes, error) {
|
||||||
|
cp.mutex.Lock()
|
||||||
|
defer cp.mutex.Unlock()
|
||||||
|
return cp.storage.GetChannelMessage(conversation, channel, id)
|
||||||
|
}
|
||||||
|
|
||||||
// ExportGroup serializes a group invite so it can be given offline
|
// ExportGroup serializes a group invite so it can be given offline
|
||||||
|
// Status: TODO
|
||||||
func (cp *cwtchPeer) ExportGroup(groupID string) (string, error) {
|
func (cp *cwtchPeer) ExportGroup(groupID string) (string, error) {
|
||||||
cp.mutex.Lock()
|
cp.mutex.Lock()
|
||||||
defer cp.mutex.Unlock()
|
defer cp.mutex.Unlock()
|
||||||
|
@ -398,6 +387,7 @@ func (cp *cwtchPeer) ExportGroup(groupID string) (string, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// StartGroup create a new group linked to the given server and returns the group ID, an invite or an error.
|
// StartGroup create a new group linked to the given server and returns the group ID, an invite or an error.
|
||||||
|
// Status: TODO
|
||||||
func (cp *cwtchPeer) StartGroup(server string) (string, string, error) {
|
func (cp *cwtchPeer) StartGroup(server string) (string, string, error) {
|
||||||
cp.mutex.Lock()
|
cp.mutex.Lock()
|
||||||
groupID, invite, err := cp.Profile.StartGroup(server)
|
groupID, invite, err := cp.Profile.StartGroup(server)
|
||||||
|
@ -421,6 +411,7 @@ func (cp *cwtchPeer) StartGroup(server string) (string, string, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetGroups returns an unordered list of all group IDs.
|
// GetGroups returns an unordered list of all group IDs.
|
||||||
|
// Status: TODO
|
||||||
func (cp *cwtchPeer) GetGroups() []string {
|
func (cp *cwtchPeer) GetGroups() []string {
|
||||||
cp.mutex.Lock()
|
cp.mutex.Lock()
|
||||||
defer cp.mutex.Unlock()
|
defer cp.mutex.Unlock()
|
||||||
|
@ -428,133 +419,105 @@ func (cp *cwtchPeer) GetGroups() []string {
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetGroup returns a pointer to a specific group, nil if no group exists.
|
// GetGroup returns a pointer to a specific group, nil if no group exists.
|
||||||
|
// Status: TODO
|
||||||
func (cp *cwtchPeer) GetGroup(groupID string) *model.Group {
|
func (cp *cwtchPeer) GetGroup(groupID string) *model.Group {
|
||||||
cp.mutex.Lock()
|
cp.mutex.Lock()
|
||||||
defer cp.mutex.Unlock()
|
defer cp.mutex.Unlock()
|
||||||
return cp.Profile.GetGroup(groupID)
|
return cp.Profile.GetGroup(groupID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cp *cwtchPeer) AddContact(nick, onion string, authorization model.Authorization) {
|
|
||||||
decodedPub, _ := base32.StdEncoding.DecodeString(strings.ToUpper(onion))
|
|
||||||
pp := &model.PublicProfile{Name: nick, Ed25519PublicKey: decodedPub, Authorization: authorization, Onion: onion, Attributes: map[string]string{"nick": nick}}
|
|
||||||
cp.Profile.AddContact(onion, pp)
|
|
||||||
pd, _ := json.Marshal(pp)
|
|
||||||
cp.eventBus.Publish(event.NewEvent(event.PeerCreated, map[event.Field]string{
|
|
||||||
event.Data: string(pd),
|
|
||||||
event.RemotePeer: onion,
|
|
||||||
}))
|
|
||||||
cp.eventBus.Publish(event.NewEventList(event.SetPeerAuthorization, event.RemotePeer, onion, event.Authorization, string(authorization)))
|
|
||||||
|
|
||||||
// Default to Deleting Peer History
|
|
||||||
cp.eventBus.Publish(event.NewEventList(event.SetPeerAttribute, event.RemotePeer, onion, event.SaveHistoryKey, event.DeleteHistoryDefault))
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddServer takes in a serialized server specification (a bundle of related keys) and adds a contact for the
|
// AddServer takes in a serialized server specification (a bundle of related keys) and adds a contact for the
|
||||||
// server assuming there are no errors and the contact doesn't already exist.
|
// server assuming there are no errors and the contact doesn't already exist.
|
||||||
// TODO in the future this function should also integrate with a trust provider to validate the key bundle.
|
// TODO in the future this function should also integrate with a trust provider to validate the key bundle.
|
||||||
|
// Status: TODO
|
||||||
func (cp *cwtchPeer) AddServer(serverSpecification string) error {
|
func (cp *cwtchPeer) AddServer(serverSpecification string) error {
|
||||||
// This confirms that the server did at least sign the bundle
|
//// This confirms that the server did at least sign the bundle
|
||||||
keyBundle, err := model.DeserializeAndVerify([]byte(serverSpecification))
|
//keyBundle, err := model.DeserializeAndVerify([]byte(serverSpecification))
|
||||||
if err != nil {
|
//if err != nil {
|
||||||
return err
|
// return err
|
||||||
}
|
//}
|
||||||
log.Debugf("Got new key bundle %v", keyBundle.Serialize())
|
//log.Debugf("Got new key bundle %v", keyBundle.Serialize())
|
||||||
|
//
|
||||||
// TODO if the key bundle is incomplete then error out. In the future we may allow servers to attest to new
|
//// TODO if the key bundle is incomplete then error out. In the future we may allow servers to attest to new
|
||||||
// keys or subsets of keys, but for now they must commit only to a complete set of keys required for Cwtch Groups
|
//// keys or subsets of keys, but for now they must commit only to a complete set of keys required for Cwtch Groups
|
||||||
// (that way we can be assured that the keybundle we store is a valid one)
|
//// (that way we can be assured that the keybundle we store is a valid one)
|
||||||
if !keyBundle.HasKeyType(model.KeyTypeTokenOnion) || !keyBundle.HasKeyType(model.KeyTypeServerOnion) || !keyBundle.HasKeyType(model.KeyTypePrivacyPass) {
|
//if !keyBundle.HasKeyType(model.KeyTypeTokenOnion) || !keyBundle.HasKeyType(model.KeyTypeServerOnion) || !keyBundle.HasKeyType(model.KeyTypePrivacyPass) {
|
||||||
return errors.New("keybundle is incomplete")
|
// return errors.New("keybundle is incomplete")
|
||||||
}
|
//}
|
||||||
|
//
|
||||||
if keyBundle.HasKeyType(model.KeyTypeServerOnion) {
|
//if keyBundle.HasKeyType(model.KeyTypeServerOnion) {
|
||||||
onionKey, _ := keyBundle.GetKey(model.KeyTypeServerOnion)
|
// onionKey, _ := keyBundle.GetKey(model.KeyTypeServerOnion)
|
||||||
onion := string(onionKey)
|
// onion := string(onionKey)
|
||||||
|
//
|
||||||
// Add the contact if we don't already have it
|
// // Add the contact if we don't already have it
|
||||||
if cp.GetContact(onion) == nil {
|
// if cp.GetContact(onion) == nil {
|
||||||
decodedPub, _ := base32.StdEncoding.DecodeString(strings.ToUpper(onion))
|
// decodedPub, _ := base32.StdEncoding.DecodeString(strings.ToUpper(onion))
|
||||||
ab := keyBundle.AttributeBundle()
|
// ab := keyBundle.AttributeBundle()
|
||||||
pp := &model.PublicProfile{Name: onion, Ed25519PublicKey: decodedPub, Authorization: model.AuthUnknown, Onion: onion, Attributes: ab}
|
// pp := &model.PublicProfile{Name: onion, Ed25519PublicKey: decodedPub, Authorization: model.AuthUnknown, Onion: onion, Attributes: ab}
|
||||||
|
//
|
||||||
// The only part of this function that actually modifies the profile...
|
// // The only part of this function that actually modifies the profile...
|
||||||
cp.mutex.Lock()
|
// cp.mutex.Lock()
|
||||||
cp.Profile.AddContact(onion, pp)
|
// cp.Profile.AddContact(onion, pp)
|
||||||
cp.mutex.Unlock()
|
// cp.mutex.Unlock()
|
||||||
|
//
|
||||||
pd, _ := json.Marshal(pp)
|
// pd, _ := json.Marshal(pp)
|
||||||
|
//
|
||||||
// Sync the Storage Engine
|
// // Sync the Storage Engine
|
||||||
cp.eventBus.Publish(event.NewEvent(event.PeerCreated, map[event.Field]string{
|
// cp.eventBus.Publish(event.NewEvent(event.PeerCreated, map[event.Field]string{
|
||||||
event.Data: string(pd),
|
// event.Data: string(pd),
|
||||||
event.RemotePeer: onion,
|
// event.RemotePeer: onion,
|
||||||
}))
|
// }))
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
// At this point we know the server exists
|
// // At this point we know the server exists
|
||||||
server := cp.GetContact(onion)
|
// server := cp.GetContact(onion)
|
||||||
ab := keyBundle.AttributeBundle()
|
// ab := keyBundle.AttributeBundle()
|
||||||
|
//
|
||||||
// Check server bundle for consistency if we have different keys stored than in the tofu bundle then we
|
// // Check server bundle for consistency if we have different keys stored than in the tofu bundle then we
|
||||||
// abort...
|
// // abort...
|
||||||
for k, v := range ab {
|
// for k, v := range ab {
|
||||||
val, exists := server.GetAttribute(k)
|
// val, exists := server.GetAttribute(k)
|
||||||
if exists {
|
// if exists {
|
||||||
if val != v {
|
// if val != v {
|
||||||
// this is inconsistent!
|
// // this is inconsistent!
|
||||||
return model.InconsistentKeyBundleError
|
// return model.InconsistentKeyBundleError
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
// we haven't seen this key associated with the server before
|
// // we haven't seen this key associated with the server before
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
// Store the key bundle for the server so we can reconstruct a tofubundle invite
|
// // Store the key bundle for the server so we can reconstruct a tofubundle invite
|
||||||
cp.SetContactAttribute(onion, string(model.BundleType), serverSpecification)
|
// cp.SetContactAttribute(onion, string(model.BundleType), serverSpecification)
|
||||||
|
//
|
||||||
// If we have gotten to this point we can assume this is a safe key bundle signed by the
|
// // If we have gotten to this point we can assume this is a safe key bundle signed by the
|
||||||
// server with no conflicting keys. So we are going to publish all the keys
|
// // server with no conflicting keys. So we are going to publish all the keys
|
||||||
for k, v := range ab {
|
// for k, v := range ab {
|
||||||
log.Debugf("Server (%v) has %v key %v", onion, k, v)
|
// log.Debugf("Server (%v) has %v key %v", onion, k, v)
|
||||||
cp.SetContactAttribute(onion, k, v)
|
// cp.SetContactAttribute(onion, k, v)
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
|
// return nil
|
||||||
|
//}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetContacts returns an unordered list of onions
|
|
||||||
func (cp *cwtchPeer) GetContacts() []string {
|
|
||||||
cp.mutex.Lock()
|
|
||||||
defer cp.mutex.Unlock()
|
|
||||||
return cp.Profile.GetContacts()
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetServers returns an unordered list of servers
|
// GetServers returns an unordered list of servers
|
||||||
|
// Status: TODO
|
||||||
func (cp *cwtchPeer) GetServers() []string {
|
func (cp *cwtchPeer) GetServers() []string {
|
||||||
contacts := cp.Profile.GetContacts()
|
|
||||||
var servers []string
|
var servers []string
|
||||||
for _, contact := range contacts {
|
|
||||||
if cp.GetContact(contact).IsServer() {
|
|
||||||
servers = append(servers, contact)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return servers
|
return servers
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetContact returns a given contact, nil is no such contact exists
|
// GetOnion
|
||||||
func (cp *cwtchPeer) GetContact(onion string) *model.PublicProfile {
|
// Status: Deprecated in 1.5
|
||||||
cp.mutex.Lock()
|
|
||||||
defer cp.mutex.Unlock()
|
|
||||||
contact, _ := cp.Profile.GetContact(onion)
|
|
||||||
return contact
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cp *cwtchPeer) GetOnion() string {
|
func (cp *cwtchPeer) GetOnion() string {
|
||||||
cp.mutex.Lock()
|
cp.mutex.Lock()
|
||||||
defer cp.mutex.Unlock()
|
defer cp.mutex.Unlock()
|
||||||
return cp.Profile.Onion
|
return cp.Profile.Onion
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetPeerState
|
||||||
|
// Status: TODO
|
||||||
func (cp *cwtchPeer) GetPeerState(onion string) (connections.ConnectionState, bool) {
|
func (cp *cwtchPeer) GetPeerState(onion string) (connections.ConnectionState, bool) {
|
||||||
cp.mutex.Lock()
|
cp.mutex.Lock()
|
||||||
defer cp.mutex.Unlock()
|
defer cp.mutex.Unlock()
|
||||||
|
@ -564,6 +527,8 @@ func (cp *cwtchPeer) GetPeerState(onion string) (connections.ConnectionState, bo
|
||||||
return connections.DISCONNECTED, false
|
return connections.DISCONNECTED, false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetGroupState
|
||||||
|
// Status: TODO
|
||||||
func (cp *cwtchPeer) GetGroupState(groupid string) (connections.ConnectionState, bool) {
|
func (cp *cwtchPeer) GetGroupState(groupid string) (connections.ConnectionState, bool) {
|
||||||
cp.mutex.Lock()
|
cp.mutex.Lock()
|
||||||
defer cp.mutex.Unlock()
|
defer cp.mutex.Unlock()
|
||||||
|
@ -573,33 +538,14 @@ func (cp *cwtchPeer) GetGroupState(groupid string) (connections.ConnectionState,
|
||||||
return connections.DISCONNECTED, false
|
return connections.DISCONNECTED, false
|
||||||
}
|
}
|
||||||
|
|
||||||
// PeerWithOnion is the entry point for cwtchPeer relationships
|
// PeerWithOnion initiates a request to the Protocol Engine to set up Cwtch Session with a given tor v3 onion
|
||||||
|
// address.
|
||||||
func (cp *cwtchPeer) PeerWithOnion(onion string) {
|
func (cp *cwtchPeer) PeerWithOnion(onion string) {
|
||||||
cp.mutex.Lock()
|
|
||||||
defer cp.mutex.Unlock()
|
|
||||||
if _, exists := cp.Profile.GetContact(onion); !exists {
|
|
||||||
cp.AddContact(onion, onion, model.AuthApproved)
|
|
||||||
}
|
|
||||||
cp.eventBus.Publish(event.NewEvent(event.PeerRequest, map[event.Field]string{event.RemotePeer: onion}))
|
cp.eventBus.Publish(event.NewEvent(event.PeerRequest, map[event.Field]string{event.RemotePeer: onion}))
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteContact deletes a peer from the profile, storage, and handling
|
|
||||||
func (cp *cwtchPeer) DeleteContact(onion string) {
|
|
||||||
cp.mutex.Lock()
|
|
||||||
cp.Profile.DeleteContact(onion)
|
|
||||||
defer cp.mutex.Unlock()
|
|
||||||
cp.eventBus.Publish(event.NewEventList(event.DeleteContact, event.RemotePeer, onion))
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteGroup deletes a Group from the profile, storage, and handling
|
|
||||||
func (cp *cwtchPeer) DeleteGroup(groupID string) {
|
|
||||||
cp.mutex.Lock()
|
|
||||||
cp.Profile.DeleteGroup(groupID)
|
|
||||||
defer cp.mutex.Unlock()
|
|
||||||
cp.eventBus.Publish(event.NewEventList(event.DeleteGroup, event.GroupID, groupID))
|
|
||||||
}
|
|
||||||
|
|
||||||
// InviteOnionToGroup kicks off the invite process
|
// InviteOnionToGroup kicks off the invite process
|
||||||
|
// Status: TODO
|
||||||
func (cp *cwtchPeer) InviteOnionToGroup(onion string, groupid string) error {
|
func (cp *cwtchPeer) InviteOnionToGroup(onion string, groupid string) error {
|
||||||
cp.mutex.Lock()
|
cp.mutex.Lock()
|
||||||
group := cp.Profile.GetGroup(groupid)
|
group := cp.Profile.GetGroup(groupid)
|
||||||
|
@ -616,73 +562,47 @@ func (cp *cwtchPeer) InviteOnionToGroup(onion string, groupid string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// JoinServer manages a new server connection with the given onion address
|
// JoinServer manages a new server connection with the given onion address
|
||||||
|
// Status: TODO
|
||||||
func (cp *cwtchPeer) JoinServer(onion string) error {
|
func (cp *cwtchPeer) JoinServer(onion string) error {
|
||||||
if cp.GetContact(onion) != nil {
|
//if cp.GetContact(onion) != nil {
|
||||||
tokenY, yExists := cp.GetContact(onion).GetAttribute(string(model.KeyTypePrivacyPass))
|
// tokenY, yExists := cp.GetContact(onion).GetAttribute(string(model.KeyTypePrivacyPass))
|
||||||
tokenOnion, onionExists := cp.GetContact(onion).GetAttribute(string(model.KeyTypeTokenOnion))
|
// tokenOnion, onionExists := cp.GetContact(onion).GetAttribute(string(model.KeyTypeTokenOnion))
|
||||||
if yExists && onionExists {
|
// if yExists && onionExists {
|
||||||
signature, exists := cp.GetContactAttribute(onion, lastKnownSignature)
|
// signature, exists := cp.GetContactAttribute(onion, lastKnownSignature)
|
||||||
if !exists {
|
// if !exists {
|
||||||
signature = base64.StdEncoding.EncodeToString([]byte{})
|
// signature = base64.StdEncoding.EncodeToString([]byte{})
|
||||||
}
|
// }
|
||||||
cp.eventBus.Publish(event.NewEvent(event.JoinServer, map[event.Field]string{event.GroupServer: onion, event.ServerTokenY: tokenY, event.ServerTokenOnion: tokenOnion, event.Signature: signature}))
|
// cp.eventBus.Publish(event.NewEvent(event.JoinServer, map[event.Field]string{event.GroupServer: onion, event.ServerTokenY: tokenY, event.ServerTokenOnion: tokenOnion, event.Signature: signature}))
|
||||||
return nil
|
// return nil
|
||||||
}
|
// }
|
||||||
}
|
//}
|
||||||
return errors.New("no keys found for server connection")
|
return errors.New("no keys found for server connection")
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResyncServer completely tears down and resyncs a new server connection with the given onion address
|
// ResyncServer completely tears down and resyncs a new server connection with the given onion address
|
||||||
|
// Status: TODO
|
||||||
func (cp *cwtchPeer) ResyncServer(onion string) error {
|
func (cp *cwtchPeer) ResyncServer(onion string) error {
|
||||||
if cp.GetContact(onion) != nil {
|
//if cp.GetContact(onion) != nil {
|
||||||
tokenY, yExists := cp.GetContact(onion).GetAttribute(string(model.KeyTypePrivacyPass))
|
// tokenY, yExists := cp.GetContact(onion).GetAttribute(string(model.KeyTypePrivacyPass))
|
||||||
tokenOnion, onionExists := cp.GetContact(onion).GetAttribute(string(model.KeyTypeTokenOnion))
|
// tokenOnion, onionExists := cp.GetContact(onion).GetAttribute(string(model.KeyTypeTokenOnion))
|
||||||
if yExists && onionExists {
|
// if yExists && onionExists {
|
||||||
signature := base64.StdEncoding.EncodeToString([]byte{})
|
// signature := base64.StdEncoding.EncodeToString([]byte{})
|
||||||
cp.eventBus.Publish(event.NewEvent(event.JoinServer, map[event.Field]string{event.GroupServer: onion, event.ServerTokenY: tokenY, event.ServerTokenOnion: tokenOnion, event.Signature: signature}))
|
// cp.eventBus.Publish(event.NewEvent(event.JoinServer, map[event.Field]string{event.GroupServer: onion, event.ServerTokenY: tokenY, event.ServerTokenOnion: tokenOnion, event.Signature: signature}))
|
||||||
return nil
|
// return nil
|
||||||
}
|
// }
|
||||||
}
|
//}
|
||||||
return errors.New("no keys found for server connection")
|
return errors.New("no keys found for server connection")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SendGetValToPeer
|
||||||
|
// Status: Ready for 1.5
|
||||||
func (cp *cwtchPeer) SendGetValToPeer(onion string, scope string, path string) {
|
func (cp *cwtchPeer) SendGetValToPeer(onion string, scope string, path string) {
|
||||||
ev := event.NewEventList(event.SendGetValMessageToPeer, event.RemotePeer, onion, event.Scope, scope, event.Path, path)
|
ev := event.NewEventList(event.SendGetValMessageToPeer, event.RemotePeer, onion, event.Scope, scope, event.Path, path)
|
||||||
cp.eventBus.Publish(ev)
|
cp.eventBus.Publish(ev)
|
||||||
}
|
}
|
||||||
|
|
||||||
// BlockPeer blocks an existing peer relationship.
|
|
||||||
func (cp *cwtchPeer) SetContactAuthorization(peer string, authorization model.Authorization) error {
|
|
||||||
cp.mutex.Lock()
|
|
||||||
err := cp.Profile.SetContactAuthorization(peer, authorization)
|
|
||||||
cp.mutex.Unlock()
|
|
||||||
cp.eventBus.Publish(event.NewEvent(event.SetPeerAuthorization, map[event.Field]string{event.RemotePeer: peer, event.Authorization: string(authorization)}))
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// AcceptInvite accepts a given existing group invite
|
|
||||||
func (cp *cwtchPeer) AcceptInvite(groupID string) error {
|
|
||||||
cp.mutex.Lock()
|
|
||||||
err := cp.Profile.AcceptInvite(groupID)
|
|
||||||
cp.mutex.Unlock()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
cp.eventBus.Publish(event.NewEvent(event.AcceptGroupInvite, map[event.Field]string{event.GroupID: groupID}))
|
|
||||||
err = cp.JoinServer(cp.Profile.Groups[groupID].GroupServer)
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// RejectInvite rejects a given group invite.
|
|
||||||
func (cp *cwtchPeer) RejectInvite(groupID string) {
|
|
||||||
cp.mutex.Lock()
|
|
||||||
defer cp.mutex.Unlock()
|
|
||||||
cp.Profile.RejectInvite(groupID)
|
|
||||||
cp.eventBus.Publish(event.NewEvent(event.RejectGroupInvite, map[event.Field]string{event.GroupID: groupID}))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Listen makes the peer open a listening port to accept incoming connections (and be detectably online)
|
// Listen makes the peer open a listening port to accept incoming connections (and be detectably online)
|
||||||
|
// Status: Ready for 1.5
|
||||||
func (cp *cwtchPeer) Listen() {
|
func (cp *cwtchPeer) Listen() {
|
||||||
cp.mutex.Lock()
|
cp.mutex.Lock()
|
||||||
defer cp.mutex.Unlock()
|
defer cp.mutex.Unlock()
|
||||||
|
@ -696,96 +616,62 @@ func (cp *cwtchPeer) Listen() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// StartPeersConnections attempts to connect to peer connections
|
// StartPeersConnections attempts to connect to peer connections
|
||||||
|
// Status: Ready for 1.5
|
||||||
func (cp *cwtchPeer) StartPeersConnections() {
|
func (cp *cwtchPeer) StartPeersConnections() {
|
||||||
for _, contact := range cp.GetContacts() {
|
//for _, contact := range cp.GetContacts() {
|
||||||
if !cp.GetContact(contact).IsServer() {
|
// if !cp.GetContact(contact).IsServer() {
|
||||||
cp.PeerWithOnion(contact)
|
// cp.PeerWithOnion(contact)
|
||||||
}
|
// }
|
||||||
}
|
//}
|
||||||
}
|
}
|
||||||
|
|
||||||
// StartServerConnections attempts to connect to all server connections
|
// StartServerConnections attempts to connect to all server connections
|
||||||
|
// Status: Ready for 1.5
|
||||||
func (cp *cwtchPeer) StartServerConnections() {
|
func (cp *cwtchPeer) StartServerConnections() {
|
||||||
for _, contact := range cp.GetContacts() {
|
//for _, contact := range cp.GetContacts() {
|
||||||
if cp.GetContact(contact).IsServer() {
|
// if cp.GetContact(contact).IsServer() {
|
||||||
err := cp.JoinServer(contact)
|
// err := cp.JoinServer(contact)
|
||||||
if err != nil {
|
// if err != nil {
|
||||||
// Almost certainly a programming error so print it..
|
// // Almost certainly a programming error so print it..
|
||||||
log.Errorf("error joining server %v", err)
|
// log.Errorf("error joining server %v", err)
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
//}
|
||||||
}
|
|
||||||
|
|
||||||
// SetContactAttribute sets an attribute for the indicated contact and emits an event
|
|
||||||
func (cp *cwtchPeer) SetContactAttribute(onion string, key string, val string) {
|
|
||||||
cp.mutex.Lock()
|
|
||||||
defer cp.mutex.Unlock()
|
|
||||||
if contact, ok := cp.Profile.GetContact(onion); ok {
|
|
||||||
contact.SetAttribute(key, val)
|
|
||||||
cp.eventBus.Publish(event.NewEvent(event.SetPeerAttribute, map[event.Field]string{
|
|
||||||
event.RemotePeer: onion,
|
|
||||||
event.Key: key,
|
|
||||||
event.Data: val,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetContactAttribute gets an attribute for the indicated contact
|
|
||||||
func (cp *cwtchPeer) GetContactAttribute(onion string, key string) (string, bool) {
|
|
||||||
cp.mutex.Lock()
|
|
||||||
defer cp.mutex.Unlock()
|
|
||||||
if contact, ok := cp.Profile.GetContact(onion); ok {
|
|
||||||
if val, exists := contact.GetAttribute(key); exists {
|
|
||||||
return val, true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetGroupAttribute sets an attribute for the indicated group and emits an event
|
|
||||||
func (cp *cwtchPeer) SetGroupAttribute(gid string, key string, val string) {
|
|
||||||
cp.mutex.Lock()
|
|
||||||
defer cp.mutex.Unlock()
|
|
||||||
if group := cp.Profile.GetGroup(gid); group != nil {
|
|
||||||
group.SetAttribute(key, val)
|
|
||||||
cp.eventBus.Publish(event.NewEvent(event.SetGroupAttribute, map[event.Field]string{
|
|
||||||
event.GroupID: gid,
|
|
||||||
event.Key: key,
|
|
||||||
event.Data: val,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetGroupAttribute gets an attribute for the indicated group
|
|
||||||
func (cp *cwtchPeer) GetGroupAttribute(gid string, key string) (string, bool) {
|
|
||||||
cp.mutex.Lock()
|
|
||||||
defer cp.mutex.Unlock()
|
|
||||||
if group := cp.Profile.GetGroup(gid); group != nil {
|
|
||||||
if val, exists := group.GetAttribute(key); exists {
|
|
||||||
return val, true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return "", false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Shutdown kills all connections and cleans up all goroutines for the peer
|
// Shutdown kills all connections and cleans up all goroutines for the peer
|
||||||
|
// Status: Ready for 1.5
|
||||||
func (cp *cwtchPeer) Shutdown() {
|
func (cp *cwtchPeer) Shutdown() {
|
||||||
cp.mutex.Lock()
|
cp.mutex.Lock()
|
||||||
defer cp.mutex.Unlock()
|
defer cp.mutex.Unlock()
|
||||||
cp.shutdown = true
|
cp.shutdown = true
|
||||||
cp.queue.Shutdown()
|
cp.queue.Shutdown()
|
||||||
|
if cp.storage != nil {
|
||||||
|
cp.storage.Close()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cp *cwtchPeer) storeMessage(onion string, messageTxt string, sent time.Time) {
|
// Status: TODO
|
||||||
if cp.GetContact(onion) == nil {
|
func (cp *cwtchPeer) storeMessage(handle string, message string, sent time.Time) error {
|
||||||
cp.AddContact(onion, onion, model.AuthUnknown)
|
// TOOD maybe atomize this?
|
||||||
|
ci, err := cp.FetchConversationInfo(handle)
|
||||||
|
if err != nil {
|
||||||
|
err := cp.NewContactConversation(handle, model.DefaultP2PAccessControl(), false)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ci, err = cp.FetchConversationInfo(handle)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
cp.mutex.Lock()
|
cp.mutex.Lock()
|
||||||
cp.Profile.AddMessageToContactTimeline(onion, messageTxt, sent)
|
defer cp.mutex.Unlock()
|
||||||
cp.mutex.Unlock()
|
return cp.storage.InsertMessage(ci.ID, 0, message, model.Attributes{"ack": event.True, "sent": sent.String()})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ShareFile begins hosting the given serialized manifest
|
||||||
|
// Status: Ready for 1.5
|
||||||
func (cp *cwtchPeer) ShareFile(fileKey string, serializedManifest string) {
|
func (cp *cwtchPeer) ShareFile(fileKey string, serializedManifest string) {
|
||||||
cp.eventBus.Publish(event.NewEvent(event.ShareManifest, map[event.Field]string{event.FileKey: fileKey, event.SerializedManifest: serializedManifest}))
|
cp.eventBus.Publish(event.NewEvent(event.ShareManifest, map[event.Field]string{event.FileKey: fileKey, event.SerializedManifest: serializedManifest}))
|
||||||
}
|
}
|
||||||
|
@ -813,7 +699,7 @@ func (cp *cwtchPeer) eventHandler() {
|
||||||
// The security of cwtch groups are also not dependent on the servers inability to uniquely tag connections (as long as
|
// The security of cwtch groups are also not dependent on the servers inability to uniquely tag connections (as long as
|
||||||
// it learns nothing else about each connection).
|
// it learns nothing else about each connection).
|
||||||
// store the base64 encoded signature for later use
|
// store the base64 encoded signature for later use
|
||||||
cp.SetContactAttribute(ev.Data[event.GroupServer], lastKnownSignature, ev.Data[event.Signature])
|
cp.SetConversationAttribute(ev.Data[event.GroupServer], lastKnownSignature, ev.Data[event.Signature])
|
||||||
|
|
||||||
cp.mutex.Lock()
|
cp.mutex.Lock()
|
||||||
ok, groupID, message, index := cp.Profile.AttemptDecryption(ciphertext, signature)
|
ok, groupID, message, index := cp.Profile.AttemptDecryption(ciphertext, signature)
|
||||||
|
@ -867,8 +753,8 @@ func (cp *cwtchPeer) eventHandler() {
|
||||||
|
|
||||||
log.Debugf("NewGetValMessageFromPeer for %v.%v from %v\n", scope, path, onion)
|
log.Debugf("NewGetValMessageFromPeer for %v.%v from %v\n", scope, path, onion)
|
||||||
|
|
||||||
remotePeer := cp.GetContact(onion)
|
conversationInfo, _ := cp.FetchConversationInfo(onion)
|
||||||
if remotePeer != nil && remotePeer.Authorization == model.AuthApproved {
|
if conversationInfo != nil && conversationInfo.Accepted {
|
||||||
scope := attr.IntoScope(scope)
|
scope := attr.IntoScope(scope)
|
||||||
if scope.IsPublic() || scope.IsConversation() {
|
if scope.IsPublic() || scope.IsConversation() {
|
||||||
zone, zpath := attr.ParseZone(path)
|
zone, zpath := attr.ParseZone(path)
|
||||||
|
@ -965,27 +851,16 @@ func (cp *cwtchPeer) eventHandler() {
|
||||||
|
|
||||||
// Allow public profile parameters to be added as peer specific attributes...
|
// Allow public profile parameters to be added as peer specific attributes...
|
||||||
if attr.Scope(scope).IsPublic() && zone == attr.ProfileZone {
|
if attr.Scope(scope).IsPublic() && zone == attr.ProfileZone {
|
||||||
cp.SetContactAttribute(onion, attr.GetPeerScope(path), val)
|
cp.SetConversationAttribute(onion, attr.Scope(scope).ConstructScopedZonedPath(zone.ConstructZonedPath(path)), val)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case event.PeerStateChange:
|
case event.PeerStateChange:
|
||||||
cp.mutex.Lock()
|
cp.mutex.Lock()
|
||||||
if _, exists := cp.Profile.Contacts[ev.Data[event.RemotePeer]]; exists {
|
cp.state[ev.Data[event.RemotePeer]] = connections.ConnectionStateToType()[ev.Data[event.ConnectionState]]
|
||||||
cp.Profile.Contacts[ev.Data[event.RemotePeer]].State = ev.Data[event.ConnectionState]
|
|
||||||
}
|
|
||||||
cp.mutex.Unlock()
|
cp.mutex.Unlock()
|
||||||
case event.ServerStateChange:
|
case event.ServerStateChange:
|
||||||
cp.mutex.Lock()
|
cp.mutex.Lock()
|
||||||
// We update both the server contact status, as well as the groups the server belongs to
|
cp.state[ev.Data[event.GroupServer]] = connections.ConnectionStateToType()[ev.Data[event.ConnectionState]]
|
||||||
log.Debugf("Got Server State Change %v", ev)
|
|
||||||
cp.Profile.Contacts[ev.Data[event.GroupServer]].State = ev.Data[event.ConnectionState]
|
|
||||||
|
|
||||||
// TODO deprecate this, the UI should consult the server contact entry instead (it's far more efficient)
|
|
||||||
for _, group := range cp.Profile.Groups {
|
|
||||||
if group.GroupServer == ev.Data[event.GroupServer] {
|
|
||||||
group.State = ev.Data[event.ConnectionState]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
cp.mutex.Unlock()
|
cp.mutex.Unlock()
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
|
|
@ -0,0 +1,323 @@
|
||||||
|
package peer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"cwtch.im/cwtch/model"
|
||||||
|
"cwtch.im/cwtch/model/attr"
|
||||||
|
"database/sql"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"git.openprivacy.ca/openprivacy/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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
|
||||||
|
selectConversationStmt *sql.Stmt
|
||||||
|
acceptConversationStmt *sql.Stmt
|
||||||
|
deleteConversationStmt *sql.Stmt
|
||||||
|
setConversationAttributesStmt *sql.Stmt
|
||||||
|
|
||||||
|
channelInsertStmts map[ChannelID]*sql.Stmt
|
||||||
|
channelGetMessageStmts map[ChannelID]*sql.Stmt
|
||||||
|
|
||||||
|
db *sql.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
type ChannelID struct {
|
||||||
|
Conversation int
|
||||||
|
Channel int
|
||||||
|
}
|
||||||
|
|
||||||
|
const insertProfileKeySQLStmt = `insert 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 selectConversationSQLStmt = `select ID, Handle, Attributes, ACL, Accepted from conversations where Handle=(?);`
|
||||||
|
const acceptedConversationSQLStmt = `update conversations set Accepted=true where Handle=(?);`
|
||||||
|
const setConversationAttributesSQLStmt = `update conversations set Attributes=(?) where Handle=(?) ;`
|
||||||
|
const deleteConversationSQLStmt = `delete from conversations where Handle=(?);`
|
||||||
|
|
||||||
|
// 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);`
|
||||||
|
|
||||||
|
// insertMessageIntoConversationSQLStmt is a template for creating conversation based tables...
|
||||||
|
const insertMessageIntoConversationSQLStmt = `insert into channel_%d_%d_chat (Body, Attributes) values(?,?);`
|
||||||
|
|
||||||
|
// getMessageFromConversationSQLStmt is a template for creating conversation based tables...
|
||||||
|
const getMessageFromConversationSQLStmt = `select Body, Attributes from channel_%d_%d_chat where ID=(?);`
|
||||||
|
|
||||||
|
// NewCwtchProfileStorage constructs a new CwtchProfileStorage from a database. It is also responsible for
|
||||||
|
// Preparing commonly used SQL Statements
|
||||||
|
func NewCwtchProfileStorage(db *sql.DB) (*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
|
||||||
|
}
|
||||||
|
|
||||||
|
selectConversationStmt, err := db.Prepare(selectConversationSQLStmt)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("error preparing query: %v %v", selectConversationSQLStmt, err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
acceptConversationStmt, err := db.Prepare(acceptedConversationSQLStmt)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("error preparing query: %v %v", acceptedConversationSQLStmt, 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
|
||||||
|
}
|
||||||
|
|
||||||
|
return &CwtchProfileStorage{db: db, insertProfileKeyValueStmt: insertProfileKeyValueStmt, selectProfileKeyValueStmt: selectProfileKeyStmt, insertConversationStmt: insertConversationStmt, selectConversationStmt: selectConversationStmt, acceptConversationStmt: acceptConversationStmt, deleteConversationStmt: deleteConversationStmt, setConversationAttributesStmt: setConversationAttributesStmt, channelInsertStmts: map[ChannelID]*sql.Stmt{}, channelGetMessageStmts: 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) error {
|
||||||
|
tx, err := cps.db.Begin()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("error executing transaction: %v", err)
|
||||||
|
return 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 tx.Rollback()
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err := result.LastInsertId()
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("error executing transaction: %v", err)
|
||||||
|
return tx.Rollback()
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = tx.Exec(fmt.Sprintf(createTableConversationMessagesSQLStmt, id))
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("error executing transaction: %v", err)
|
||||||
|
return tx.Rollback()
|
||||||
|
}
|
||||||
|
|
||||||
|
return tx.Commit()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetConversation looks up a particular conversation by handle
|
||||||
|
func (cps *CwtchProfileStorage) GetConversation(handle string) (*model.Conversation, error) {
|
||||||
|
rows, err := cps.selectConversationStmt.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 rhandle string
|
||||||
|
var acl []byte
|
||||||
|
var attributes []byte
|
||||||
|
var accepted bool
|
||||||
|
err = rows.Scan(&id, &rhandle, &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: rhandle, 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(handle string) error {
|
||||||
|
_, err := cps.acceptConversationStmt.Exec(handle)
|
||||||
|
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(handle string) error {
|
||||||
|
_, err := cps.deleteConversationStmt.Exec(handle)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("error executing query: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cps *CwtchProfileStorage) SetConversationAttribute(handle string, path attr.ScopedZonedPath, value string) error {
|
||||||
|
ci, err := cps.GetConversation(handle)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ci.Attributes[path.ToString()] = value
|
||||||
|
_, err = cps.setConversationAttributesStmt.Exec(ci.Attributes.Serialize(), handle)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("error executing query: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cps *CwtchProfileStorage) InsertMessage(conversation int, channel int, body string, attributes model.Attributes) 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 err
|
||||||
|
}
|
||||||
|
cps.channelInsertStmts[channelID] = conversationStmt
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := cps.channelInsertStmts[channelID].Exec(body, attributes.Serialize())
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("error inserting message: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close closes the underlying database and prepared statements
|
||||||
|
func (cps *CwtchProfileStorage) Close() {
|
||||||
|
if cps.db != nil {
|
||||||
|
cps.insertProfileKeyValueStmt.Close()
|
||||||
|
cps.selectProfileKeyValueStmt.Close()
|
||||||
|
cps.db.Close()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,128 @@
|
||||||
|
package peer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"cwtch.im/cwtch/event"
|
||||||
|
"cwtch.im/cwtch/model"
|
||||||
|
"cwtch.im/cwtch/model/attr"
|
||||||
|
"cwtch.im/cwtch/protocol/connections"
|
||||||
|
"git.openprivacy.ca/cwtch.im/tapir/primitives"
|
||||||
|
"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, bool)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadGroups provides read-only access to group state
|
||||||
|
type ReadGroups interface {
|
||||||
|
GetGroup(string) *model.Group
|
||||||
|
GetGroupState(string) (connections.ConnectionState, bool)
|
||||||
|
GetGroups() []string
|
||||||
|
ExportGroup(string) (string, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ModifyGroups provides write-only access add/edit/remove new groups
|
||||||
|
type ModifyGroups interface {
|
||||||
|
ImportGroup(string) (string, error)
|
||||||
|
StartGroup(string) (string, string, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ModifyServers provides write-only access to servers
|
||||||
|
type ModifyServers interface {
|
||||||
|
AddServer(string) error
|
||||||
|
ResyncServer(onion string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendMessages enables a caller to sender messages to a contact
|
||||||
|
type SendMessages interface {
|
||||||
|
SendMessage(handle string, message string) error
|
||||||
|
|
||||||
|
// Deprecated: is unsafe
|
||||||
|
SendGetValToPeer(string, string, string)
|
||||||
|
|
||||||
|
SendScopedZonedGetValToContact(handle string, scope attr.Scope, zone attr.Zone, key string)
|
||||||
|
|
||||||
|
// TODO
|
||||||
|
// Deprecated use overlays instead
|
||||||
|
InviteOnionToGroup(string, string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// ModifyMessages enables a caller to modify the messages in a timeline
|
||||||
|
type ModifyMessages interface {
|
||||||
|
UpdateMessageFlags(string, int, uint64)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
GetIdentity() primitives.Identity
|
||||||
|
GenerateProtocolEngine(acn connectivity.ACN, bus event.Manager) connections.Engine
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
ReadGroups
|
||||||
|
ModifyGroups
|
||||||
|
|
||||||
|
ReadServers
|
||||||
|
ModifyServers
|
||||||
|
|
||||||
|
SendMessages
|
||||||
|
ModifyMessages
|
||||||
|
|
||||||
|
// New Unified Conversation Interfaces
|
||||||
|
NewContactConversation(handle string, acl model.AccessControl, accepted bool) error
|
||||||
|
FetchConversationInfo(handle string) (*model.Conversation, error)
|
||||||
|
AcceptConversation(handle string) error
|
||||||
|
SetConversationAttribute(handle string, path attr.ScopedZonedPath, value string) error
|
||||||
|
GetConversationAttribute(handle string, path attr.ScopedZonedPath) (string, error)
|
||||||
|
DeleteConversation(handle string) error
|
||||||
|
|
||||||
|
GetChannelMessage(converstion int, channel int, id int) (string, model.Attributes, error)
|
||||||
|
|
||||||
|
ShareFile(fileKey string, serializedManifest string)
|
||||||
|
}
|
|
@ -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);`
|
||||||
|
|
||||||
|
// 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,133 @@
|
||||||
|
package peer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"git.openprivacy.ca/openprivacy/log"
|
||||||
|
// Import SQL Cipher
|
||||||
|
_ "github.com/mutecomm/go-sqlcipher/v4"
|
||||||
|
"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) (*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")
|
||||||
|
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)
|
||||||
|
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)
|
||||||
|
if err != nil {
|
||||||
|
db.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return NewProfileWithEncryptedStorage(name, cps), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FromEncryptedDatabase constructs a Cwtch Profile from an existing Encrypted Database
|
||||||
|
func FromEncryptedDatabase(profileDirectory string, password string) (CwtchPeer, error) {
|
||||||
|
log.Debugf("Loading Encrypted Profile")
|
||||||
|
db, err := openEncryptedDatabase(profileDirectory, password)
|
||||||
|
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)
|
||||||
|
if err != nil {
|
||||||
|
db.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return FromEncryptedStorage(cps), nil
|
||||||
|
}
|
|
@ -139,13 +139,13 @@ func TestCwtchPeerIntegration(t *testing.T) {
|
||||||
// ***** cwtchPeer setup *****
|
// ***** cwtchPeer setup *****
|
||||||
|
|
||||||
fmt.Println("Creating Alice...")
|
fmt.Println("Creating Alice...")
|
||||||
app.CreatePeer("alice", "asdfasdf")
|
app.CreateTaggedPeer("alice", "asdfasdf", "test")
|
||||||
|
|
||||||
fmt.Println("Creating Bob...")
|
fmt.Println("Creating Bob...")
|
||||||
app.CreatePeer("bob", "asdfasdf")
|
app.CreateTaggedPeer("bob", "asdfasdf", "test")
|
||||||
|
|
||||||
fmt.Println("Creating Carol...")
|
fmt.Println("Creating Carol...")
|
||||||
app.CreatePeer("carol", "asdfasdf")
|
app.CreateTaggedPeer("carol", "asdfasdf", "test")
|
||||||
|
|
||||||
alice := utils.WaitGetPeer(app, "alice")
|
alice := utils.WaitGetPeer(app, "alice")
|
||||||
fmt.Println("Alice created:", alice.GetOnion())
|
fmt.Println("Alice created:", alice.GetOnion())
|
||||||
|
@ -200,14 +200,12 @@ func TestCwtchPeerIntegration(t *testing.T) {
|
||||||
// Need to add contact else SetContactAuth fails on peer peer doesnt exist
|
// 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
|
// 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
|
// and the adds the user to peer, and then approves or blocks it
|
||||||
bob.AddContact("alice?", alice.GetOnion(), model.AuthApproved)
|
bob.NewContactConversation("alice?", model.DefaultP2PAccessControl(), true)
|
||||||
bob.AddServer(string(serverKeyBundle))
|
bob.AddServer(string(serverKeyBundle))
|
||||||
bob.SetContactAuthorization(alice.GetOnion(), model.AuthApproved)
|
|
||||||
|
|
||||||
waitForPeerPeerConnection(t, alice, carol)
|
waitForPeerPeerConnection(t, alice, carol)
|
||||||
carol.AddContact("alice?", alice.GetOnion(), model.AuthApproved)
|
carol.NewContactConversation("alice?", model.DefaultP2PAccessControl(), true)
|
||||||
carol.AddServer(string(serverKeyBundle))
|
carol.AddServer(string(serverKeyBundle))
|
||||||
carol.SetContactAuthorization(alice.GetOnion(), model.AuthApproved)
|
|
||||||
|
|
||||||
fmt.Println("Alice and Bob getVal public.name...")
|
fmt.Println("Alice and Bob getVal public.name...")
|
||||||
|
|
||||||
|
@ -221,26 +219,26 @@ func TestCwtchPeerIntegration(t *testing.T) {
|
||||||
// Probably related to latency/throughput problems in the underlying tor network.
|
// Probably related to latency/throughput problems in the underlying tor network.
|
||||||
time.Sleep(30 * time.Second)
|
time.Sleep(30 * time.Second)
|
||||||
|
|
||||||
aliceName, exists := bob.GetContactAttribute(alice.GetOnion(), attr.GetPeerScope(constants.Name))
|
aliceName, err := bob.GetConversationAttribute(alice.GetOnion(), attr.PeerScope.ConstructScopedZonedPath(attr.ProfileZone.ConstructZonedPath(constants.Name)))
|
||||||
if !exists || aliceName != "Alice" {
|
if err != nil || aliceName != "Alice" {
|
||||||
t.Fatalf("Bob: alice GetKeyVal error on alice peer.name %v\n", exists)
|
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)
|
fmt.Printf("Bob has alice's name as '%v'\n", aliceName)
|
||||||
|
|
||||||
bobName, exists := alice.GetContactAttribute(bob.GetOnion(), attr.GetPeerScope(constants.Name))
|
bobName, err := alice.GetConversationAttribute(bob.GetOnion(), attr.PeerScope.ConstructScopedZonedPath(attr.ProfileZone.ConstructZonedPath(constants.Name)))
|
||||||
if !exists || bobName != "Bob" {
|
if err != nil || bobName != "Bob" {
|
||||||
t.Fatalf("Alice: bob GetKeyVal error on bob peer.name\n")
|
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)
|
fmt.Printf("Alice has bob's name as '%v'\n", bobName)
|
||||||
|
|
||||||
aliceName, exists = carol.GetContactAttribute(alice.GetOnion(), attr.GetPeerScope(constants.Name))
|
aliceName, err = carol.GetConversationAttribute(alice.GetOnion(), attr.PeerScope.ConstructScopedZonedPath(attr.ProfileZone.ConstructZonedPath(constants.Name)))
|
||||||
if !exists || aliceName != "Alice" {
|
if err != nil || aliceName != "Alice" {
|
||||||
t.Fatalf("carol GetKeyVal error for alice peer.name %v\n", exists)
|
t.Fatalf("carol GetKeyVal error for alice peer.name %v: %v\n", aliceName, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
carolName, exists := alice.GetContactAttribute(carol.GetOnion(), attr.GetPeerScope(constants.Name))
|
carolName, err := alice.GetConversationAttribute(carol.GetOnion(), attr.PeerScope.ConstructScopedZonedPath(attr.ProfileZone.ConstructZonedPath(constants.Name)))
|
||||||
if !exists || carolName != "Carol" {
|
if err != nil || carolName != "Carol" {
|
||||||
t.Fatalf("alice GetKeyVal error, carol peer.name\n")
|
t.Fatalf("alice GetKeyVal error, carol peer.name: %v: %v\n", carolName, err)
|
||||||
}
|
}
|
||||||
fmt.Printf("Alice has carol's name as '%v'\n", carolName)
|
fmt.Printf("Alice has carol's name as '%v'\n", carolName)
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,127 @@
|
||||||
|
package encryptedstorage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"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"
|
||||||
|
mrand "math/rand"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestEncryptedStorage(t *testing.T) {
|
||||||
|
|
||||||
|
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...")
|
||||||
|
|
||||||
|
log.SetLevel(log.LevelDebug)
|
||||||
|
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)
|
||||||
|
|
||||||
|
alice.PeerWithOnion(bob.GetOnion())
|
||||||
|
|
||||||
|
time.Sleep(time.Second * 30)
|
||||||
|
|
||||||
|
alice.SendMessage(bob.GetOnion(), "Hello Bob")
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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(bob.GetOnion())
|
||||||
|
|
||||||
|
// 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.")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue