Merge branch 'master' of git.openprivacy.ca:cwtch.im/cwtch into countersync
This commit is contained in:
commit
e5ccb5522d
53
app/app.go
53
app/app.go
|
@ -7,8 +7,8 @@ import (
|
|||
"cwtch.im/cwtch/peer"
|
||||
"cwtch.im/cwtch/protocol/connections"
|
||||
"cwtch.im/cwtch/storage"
|
||||
"cwtch.im/tapir/primitives"
|
||||
"fmt"
|
||||
"git.openprivacy.ca/cwtch.im/tapir/primitives"
|
||||
"git.openprivacy.ca/openprivacy/connectivity"
|
||||
"git.openprivacy.ca/openprivacy/log"
|
||||
"io/ioutil"
|
||||
|
@ -44,7 +44,7 @@ type Application interface {
|
|||
LoadProfiles(password string)
|
||||
CreatePeer(name string, password string)
|
||||
CreateTaggedPeer(name string, password string, tag string)
|
||||
DeletePeer(onion string)
|
||||
DeletePeer(onion string, currentPassword string)
|
||||
AddPeerPlugin(onion string, pluginID plugins.PluginID)
|
||||
ChangePeerPassword(onion, oldpass, newpass string)
|
||||
LaunchPeers()
|
||||
|
@ -52,6 +52,7 @@ type Application interface {
|
|||
GetPrimaryBus() event.Manager
|
||||
GetEventBus(onion string) event.Manager
|
||||
QueryACNStatus()
|
||||
QueryACNVersion()
|
||||
|
||||
ShutdownPeer(string)
|
||||
Shutdown()
|
||||
|
@ -90,7 +91,7 @@ func (ac *applicationCore) CreatePeer(name string) (*model.Profile, error) {
|
|||
|
||||
_, exists := ac.eventBuses[profile.Onion]
|
||||
if exists {
|
||||
return nil, fmt.Errorf("Error: profile for onion %v already exists", profile.Onion)
|
||||
return nil, fmt.Errorf("error: profile for onion %v already exists", profile.Onion)
|
||||
}
|
||||
|
||||
eventBus := event.NewEventManager()
|
||||
|
@ -133,7 +134,7 @@ func (app *application) CreateTaggedPeer(name string, password string, tag strin
|
|||
p.SetAttribute(AttributeTag, tag)
|
||||
}
|
||||
|
||||
app.appBus.Publish(event.NewEvent(event.NewPeer, map[event.Field]string{event.Identity: profile.Onion}))
|
||||
app.appBus.Publish(event.NewEvent(event.NewPeer, map[event.Field]string{event.Identity: profile.Onion, event.Created: event.True}))
|
||||
}
|
||||
|
||||
// CreatePeer creates a new Peer with the given name and required accessories (eventbus, storage, protocol engine)
|
||||
|
@ -141,28 +142,33 @@ func (app *application) CreatePeer(name string, password string) {
|
|||
app.CreateTaggedPeer(name, password, "")
|
||||
}
|
||||
|
||||
func (app *application) DeletePeer(onion string) {
|
||||
func (app *application) DeletePeer(onion string, password string) {
|
||||
log.Infof("DeletePeer called on %v\n", onion)
|
||||
app.appmutex.Lock()
|
||||
defer app.appmutex.Unlock()
|
||||
|
||||
app.appletPlugins.ShutdownPeer(onion)
|
||||
app.plugins.Delete(onion)
|
||||
if app.storage[onion].CheckPassword(password) {
|
||||
app.appletPlugins.ShutdownPeer(onion)
|
||||
app.plugins.Delete(onion)
|
||||
|
||||
app.peers[onion].Shutdown()
|
||||
delete(app.peers, onion)
|
||||
app.peers[onion].Shutdown()
|
||||
delete(app.peers, onion)
|
||||
|
||||
app.engines[onion].Shutdown()
|
||||
delete(app.engines, onion)
|
||||
app.engines[onion].Shutdown()
|
||||
delete(app.engines, onion)
|
||||
|
||||
app.storage[onion].Shutdown()
|
||||
app.storage[onion].Delete()
|
||||
delete(app.storage, 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)
|
||||
log.Debugf("Delete peer for %v Done\n", onion)
|
||||
app.applicationCore.DeletePeer(onion)
|
||||
log.Debugf("Delete peer for %v Done\n", onion)
|
||||
app.appBus.Publish(event.NewEventList(event.PeerDeleted, event.Identity, onion))
|
||||
return
|
||||
}
|
||||
app.appBus.Publish(event.NewEventList(event.AppError, event.Error, event.PasswordMatchError, event.Identity, onion))
|
||||
}
|
||||
|
||||
func (app *application) ChangePeerPassword(onion, oldpass, newpass string) {
|
||||
|
@ -177,7 +183,7 @@ func (app *application) AddPeerPlugin(onion string, pluginID plugins.PluginID) {
|
|||
func (ac *applicationCore) LoadProfiles(password string, timeline bool, loadProfileFn LoadProfileFn) error {
|
||||
files, err := ioutil.ReadDir(path.Join(ac.directory, "profiles"))
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error: cannot read profiles directory: %v", err)
|
||||
return fmt.Errorf("error: cannot read profiles directory: %v", err)
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
|
@ -221,7 +227,7 @@ func (app *application) LoadProfiles(password string) {
|
|||
app.storage[profile.Onion] = profileStore
|
||||
app.engines[profile.Onion] = engine
|
||||
app.appmutex.Unlock()
|
||||
app.appBus.Publish(event.NewEvent(event.NewPeer, map[event.Field]string{event.Identity: profile.Onion}))
|
||||
app.appBus.Publish(event.NewEvent(event.NewPeer, map[event.Field]string{event.Identity: profile.Onion, event.Created: event.False}))
|
||||
count++
|
||||
})
|
||||
if count == 0 {
|
||||
|
@ -247,9 +253,9 @@ func (app *application) getACNStatusHandler() func(int, string) {
|
|||
return func(progress int, status string) {
|
||||
progStr := strconv.Itoa(progress)
|
||||
app.peerLock.Lock()
|
||||
app.appBus.Publish(event.NewEventList(event.ACNStatus, event.Progreess, progStr, event.Status, status))
|
||||
app.appBus.Publish(event.NewEventList(event.ACNStatus, event.Progress, progStr, event.Status, status))
|
||||
for _, bus := range app.eventBuses {
|
||||
bus.Publish(event.NewEventList(event.ACNStatus, event.Progreess, progStr, event.Status, status))
|
||||
bus.Publish(event.NewEventList(event.ACNStatus, event.Progress, progStr, event.Status, status))
|
||||
}
|
||||
app.peerLock.Unlock()
|
||||
}
|
||||
|
@ -260,6 +266,11 @@ func (app *application) QueryACNStatus() {
|
|||
app.getACNStatusHandler()(prog, status)
|
||||
}
|
||||
|
||||
func (app *application) QueryACNVersion() {
|
||||
version := app.acn.GetVersion()
|
||||
app.appBus.Publish(event.NewEventList(event.ACNVersion, event.Data, version))
|
||||
}
|
||||
|
||||
// ShutdownPeer shuts down a peer and removes it from the app's management
|
||||
func (app *application) ShutdownPeer(onion string) {
|
||||
app.appmutex.Lock()
|
||||
|
|
|
@ -45,8 +45,9 @@ func (ac *applicationClient) handleEvent(ev *event.Event) {
|
|||
key := ev.Data[event.Key]
|
||||
salt := ev.Data[event.Salt]
|
||||
reload := ev.Data[event.Status] == event.StorageRunning
|
||||
ac.newPeer(localID, key, salt, reload)
|
||||
case event.DeletePeer:
|
||||
created := ev.Data[event.Created]
|
||||
ac.newPeer(localID, key, salt, reload, created)
|
||||
case event.PeerDeleted:
|
||||
onion := ev.Data[event.Identity]
|
||||
ac.handleDeletedPeer(onion)
|
||||
case event.PeerError:
|
||||
|
@ -55,12 +56,14 @@ func (ac *applicationClient) handleEvent(ev *event.Event) {
|
|||
ac.appBus.Publish(*ev)
|
||||
case event.ACNStatus:
|
||||
ac.appBus.Publish(*ev)
|
||||
case event.ACNVersion:
|
||||
ac.appBus.Publish(*ev)
|
||||
case event.ReloadDone:
|
||||
ac.appBus.Publish(*ev)
|
||||
}
|
||||
}
|
||||
|
||||
func (ac *applicationClient) newPeer(localID, key, salt string, reload bool) {
|
||||
func (ac *applicationClient) newPeer(localID, key, salt string, reload bool, created string) {
|
||||
var keyBytes [32]byte
|
||||
var saltBytes [128]byte
|
||||
copy(keyBytes[:], key)
|
||||
|
@ -87,9 +90,9 @@ func (ac *applicationClient) newPeer(localID, key, salt string, reload bool) {
|
|||
defer ac.peerLock.Unlock()
|
||||
ac.peers[profile.Onion] = peer
|
||||
ac.eventBuses[profile.Onion] = eventBus
|
||||
npEvent := event.NewEvent(event.NewPeer, map[event.Field]string{event.Identity: profile.Onion})
|
||||
npEvent := event.NewEvent(event.NewPeer, map[event.Field]string{event.Identity: profile.Onion, event.Created: created})
|
||||
if reload {
|
||||
npEvent.Data[event.Status] = "running"
|
||||
npEvent.Data[event.Status] = event.StorageRunning
|
||||
}
|
||||
ac.appBus.Publish(npEvent)
|
||||
|
||||
|
@ -109,9 +112,9 @@ func (ac *applicationClient) CreateTaggedPeer(name, password, tag string) {
|
|||
ac.bridge.Write(&message)
|
||||
}
|
||||
|
||||
// DeletePeer messages tehe service to delete a peer
|
||||
func (ac *applicationClient) DeletePeer(onion string) {
|
||||
message := event.IPCMessage{Dest: DestApp, Message: event.NewEvent(event.DeletePeer, map[event.Field]string{event.Identity: onion})}
|
||||
// DeletePeer messages the service to delete a peer
|
||||
func (ac *applicationClient) DeletePeer(onion string, password string) {
|
||||
message := event.IPCMessage{Dest: DestApp, Message: event.NewEvent(event.DeletePeer, map[event.Field]string{event.Identity: onion, event.Password: password})}
|
||||
ac.bridge.Write(&message)
|
||||
}
|
||||
|
||||
|
@ -128,6 +131,7 @@ func (ac *applicationClient) handleDeletedPeer(onion string) {
|
|||
ac.eventBuses[onion].Publish(event.NewEventList(event.ShutdownPeer, event.Identity, onion))
|
||||
|
||||
ac.applicationCore.DeletePeer(onion)
|
||||
ac.appBus.Publish(event.NewEventList(event.PeerDeleted, event.Identity, onion))
|
||||
}
|
||||
|
||||
func (ac *applicationClient) AddPeerPlugin(onion string, pluginID plugins.PluginID) {
|
||||
|
@ -146,6 +150,11 @@ func (ac *applicationClient) QueryACNStatus() {
|
|||
ac.bridge.Write(&message)
|
||||
}
|
||||
|
||||
func (ac *applicationClient) QueryACNVersion() {
|
||||
message := event.IPCMessage{Dest: DestApp, Message: event.NewEvent(event.GetACNVersion, map[event.Field]string{})}
|
||||
ac.bridge.Write(&message)
|
||||
}
|
||||
|
||||
// ShutdownPeer shuts down a peer and removes it from the app's management
|
||||
func (ac *applicationClient) ShutdownPeer(onion string) {
|
||||
ac.acmutex.Lock()
|
||||
|
|
|
@ -6,7 +6,7 @@ import (
|
|||
"cwtch.im/cwtch/model"
|
||||
"cwtch.im/cwtch/protocol/connections"
|
||||
"cwtch.im/cwtch/storage"
|
||||
"cwtch.im/tapir/primitives"
|
||||
"git.openprivacy.ca/cwtch.im/tapir/primitives"
|
||||
"git.openprivacy.ca/openprivacy/connectivity"
|
||||
"git.openprivacy.ca/openprivacy/log"
|
||||
"path"
|
||||
|
@ -52,7 +52,8 @@ func (as *applicationService) handleEvent(ev *event.Event) {
|
|||
as.createPeer(profileName, password, tag)
|
||||
case event.DeletePeer:
|
||||
onion := ev.Data[event.Identity]
|
||||
as.deletePeer(onion)
|
||||
password := ev.Data[event.Password]
|
||||
as.deletePeer(onion, password)
|
||||
|
||||
message := event.IPCMessage{Dest: DestApp, Message: *ev}
|
||||
as.bridge.Write(&message)
|
||||
|
@ -67,6 +68,7 @@ func (as *applicationService) handleEvent(ev *event.Event) {
|
|||
for _, storage := range as.storage {
|
||||
peerMsg := *storage.GetNewPeerMessage()
|
||||
peerMsg.Data[event.Status] = event.StorageRunning
|
||||
peerMsg.Data[event.Created] = event.False
|
||||
message := event.IPCMessage{Dest: DestApp, Message: peerMsg}
|
||||
as.bridge.Write(&message)
|
||||
}
|
||||
|
@ -84,6 +86,9 @@ func (as *applicationService) handleEvent(ev *event.Event) {
|
|||
case event.GetACNStatus:
|
||||
prog, status := as.acn.GetBootstrapStatus()
|
||||
as.getACNStatusHandler()(prog, status)
|
||||
case event.GetACNVersion:
|
||||
version := as.acn.GetVersion()
|
||||
as.bridge.Write(&event.IPCMessage{Dest: DestApp, Message: event.NewEventList(event.ACNVersion, event.Data, version)})
|
||||
case event.ShutdownPeer:
|
||||
onion := ev.Data[event.Identity]
|
||||
as.ShutdownPeer(onion)
|
||||
|
@ -116,6 +121,7 @@ func (as *applicationService) createPeer(name, password, tag string) {
|
|||
as.engines[profile.Onion] = engine
|
||||
|
||||
peerMsg := *profileStore.GetNewPeerMessage()
|
||||
peerMsg.Data[event.Created] = event.True
|
||||
peerMsg.Data[event.Status] = event.StorageNew
|
||||
message := event.IPCMessage{Dest: DestApp, Message: peerMsg}
|
||||
as.bridge.Write(&message)
|
||||
|
@ -135,6 +141,7 @@ func (as *applicationService) loadProfiles(password string) {
|
|||
as.asmutex.Unlock()
|
||||
|
||||
peerMsg := *profileStore.GetNewPeerMessage()
|
||||
peerMsg.Data[event.Created] = event.False
|
||||
peerMsg.Data[event.Status] = event.StorageNew
|
||||
message := event.IPCMessage{Dest: DestApp, Message: peerMsg}
|
||||
as.bridge.Write(&message)
|
||||
|
@ -149,30 +156,41 @@ func (as *applicationService) loadProfiles(password string) {
|
|||
func (as *applicationService) getACNStatusHandler() func(int, string) {
|
||||
return func(progress int, status string) {
|
||||
progStr := strconv.Itoa(progress)
|
||||
as.bridge.Write(&event.IPCMessage{Dest: DestApp, Message: event.NewEventList(event.ACNStatus, event.Progreess, progStr, event.Status, status)})
|
||||
as.bridge.Write(&event.IPCMessage{Dest: DestApp, Message: event.NewEventList(event.ACNStatus, event.Progress, progStr, event.Status, status)})
|
||||
as.applicationCore.coremutex.Lock()
|
||||
defer as.applicationCore.coremutex.Unlock()
|
||||
for _, bus := range as.eventBuses {
|
||||
bus.Publish(event.NewEventList(event.ACNStatus, event.Progreess, progStr, event.Status, status))
|
||||
bus.Publish(event.NewEventList(event.ACNStatus, event.Progress, progStr, event.Status, status))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (as *applicationService) deletePeer(onion string) {
|
||||
func (as *applicationService) deletePeer(onion, password string) {
|
||||
as.asmutex.Lock()
|
||||
defer as.asmutex.Unlock()
|
||||
|
||||
as.appletPlugins.ShutdownPeer(onion)
|
||||
as.plugins.Delete(onion)
|
||||
if as.storage[onion].CheckPassword(password) {
|
||||
as.appletPlugins.ShutdownPeer(onion)
|
||||
as.plugins.Delete(onion)
|
||||
|
||||
as.engines[onion].Shutdown()
|
||||
delete(as.engines, onion)
|
||||
as.engines[onion].Shutdown()
|
||||
delete(as.engines, onion)
|
||||
|
||||
as.storage[onion].Shutdown()
|
||||
as.storage[onion].Delete()
|
||||
delete(as.storage, onion)
|
||||
as.storage[onion].Shutdown()
|
||||
as.storage[onion].Delete()
|
||||
delete(as.storage, onion)
|
||||
|
||||
as.applicationCore.DeletePeer(onion)
|
||||
as.eventBuses[onion].Publish(event.NewEventList(event.ShutdownPeer, event.Identity, onion))
|
||||
|
||||
as.applicationCore.DeletePeer(onion)
|
||||
log.Debugf("Delete peer for %v Done\n", onion)
|
||||
|
||||
message := event.IPCMessage{Dest: DestApp, Message: event.NewEventList(event.PeerDeleted, event.Identity, onion)}
|
||||
as.bridge.Write(&message)
|
||||
return
|
||||
}
|
||||
message := event.IPCMessage{Dest: DestApp, Message: event.NewEventList(event.AppError, event.Error, event.PasswordMatchError, event.Identity, onion)}
|
||||
as.bridge.Write(&message)
|
||||
}
|
||||
|
||||
func (as *applicationService) ShutdownPeer(onion string) {
|
||||
|
|
|
@ -69,7 +69,7 @@ func (ap *appletPeers) ListPeers() map[string]string {
|
|||
ap.peerLock.Lock()
|
||||
defer ap.peerLock.Unlock()
|
||||
for k, p := range ap.peers {
|
||||
keys[k] = p.GetName()
|
||||
keys[k] = p.GetOnion()
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
@ -113,7 +113,7 @@ func (ap *appletPlugins) AddPlugin(peerid string, id plugins.PluginID, bus event
|
|||
pluginsinf, _ := ap.plugins.Load(peerid)
|
||||
peerPlugins := pluginsinf.([]plugins.Plugin)
|
||||
|
||||
newp := plugins.Get(id, bus, acn)
|
||||
newp := plugins.Get(id, bus, acn, peerid)
|
||||
newp.Start()
|
||||
peerPlugins = append(peerPlugins, newp)
|
||||
log.Debugf("storing plugin for %v %v", peerid, peerPlugins)
|
||||
|
|
|
@ -70,7 +70,7 @@ func main() {
|
|||
timeout := 1 * time.Second
|
||||
timeElapsed := 0 * time.Second
|
||||
for {
|
||||
err := botPeer.SendMessageToGroup(groupID, timeout.String())
|
||||
_, err := botPeer.SendMessageToGroupTracked(groupID, timeout.String())
|
||||
if err != nil {
|
||||
fmt.Printf("Sent to group on server %v failed at interval %v of total %v with: %v\n", serverAddr, timeout, timeElapsed, err)
|
||||
os.Exit(1)
|
||||
|
|
|
@ -3,6 +3,7 @@ package plugins
|
|||
import (
|
||||
"cwtch.im/cwtch/event"
|
||||
"cwtch.im/cwtch/protocol/connections"
|
||||
"git.openprivacy.ca/openprivacy/log"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
@ -30,44 +31,55 @@ type contactRetry struct {
|
|||
bus event.Manager
|
||||
queue event.Queue
|
||||
networkUp bool
|
||||
|
||||
running bool
|
||||
breakChan chan bool
|
||||
onion string
|
||||
lastCheck time.Time
|
||||
|
||||
connections sync.Map //[string]*contact
|
||||
}
|
||||
|
||||
// NewConnectionRetry returns a Plugin that when started will retry connecting to contacts with a backoff timing
|
||||
func NewConnectionRetry(bus event.Manager) Plugin {
|
||||
cr := &contactRetry{bus: bus, queue: event.NewQueue(), breakChan: make(chan bool), connections: sync.Map{}, networkUp: false}
|
||||
func NewConnectionRetry(bus event.Manager, onion string) Plugin {
|
||||
cr := &contactRetry{bus: bus, queue: event.NewQueue(), breakChan: make(chan bool), connections: sync.Map{}, networkUp: false, onion: onion}
|
||||
return cr
|
||||
}
|
||||
|
||||
func (cr *contactRetry) Start() {
|
||||
go cr.run()
|
||||
if !cr.running {
|
||||
go cr.run()
|
||||
} else {
|
||||
log.Errorf("Attempted to start Contact Retry plugin twice for %v", cr.onion)
|
||||
}
|
||||
}
|
||||
|
||||
func (cr *contactRetry) run() {
|
||||
cr.running = true
|
||||
cr.bus.Subscribe(event.PeerStateChange, cr.queue)
|
||||
cr.bus.Subscribe(event.ACNStatus, cr.queue)
|
||||
cr.bus.Subscribe(event.ServerStateChange, cr.queue)
|
||||
|
||||
for {
|
||||
if time.Since(cr.lastCheck) > tickTime {
|
||||
cr.retryDisconnected()
|
||||
cr.lastCheck = time.Now()
|
||||
}
|
||||
select {
|
||||
case e := <-cr.queue.OutChan():
|
||||
switch e.EventType {
|
||||
case event.PeerStateChange:
|
||||
state := connections.ConnectionStateToType[e.Data[event.ConnectionState]]
|
||||
state := connections.ConnectionStateToType()[e.Data[event.ConnectionState]]
|
||||
peer := e.Data[event.RemotePeer]
|
||||
cr.handleEvent(peer, state, peerConn)
|
||||
|
||||
case event.ServerStateChange:
|
||||
state := connections.ConnectionStateToType[e.Data[event.ConnectionState]]
|
||||
state := connections.ConnectionStateToType()[e.Data[event.ConnectionState]]
|
||||
server := e.Data[event.GroupServer]
|
||||
cr.handleEvent(server, state, serverConn)
|
||||
|
||||
case event.ACNStatus:
|
||||
prog := e.Data[event.Progreess]
|
||||
if prog == "100" && cr.networkUp == false {
|
||||
prog := e.Data[event.Progress]
|
||||
if prog == "100" && !cr.networkUp {
|
||||
cr.networkUp = true
|
||||
cr.connections.Range(func(k, v interface{}) bool {
|
||||
p := v.(*contact)
|
||||
|
@ -76,6 +88,9 @@ func (cr *contactRetry) run() {
|
|||
if p.ctype == peerConn {
|
||||
cr.bus.Publish(event.NewEvent(event.RetryPeerRequest, map[event.Field]string{event.RemotePeer: p.id}))
|
||||
}
|
||||
if p.ctype == serverConn {
|
||||
cr.bus.Publish(event.NewEvent(event.RetryServerRequest, map[event.Field]string{event.GroupServer: p.id}))
|
||||
}
|
||||
return true
|
||||
})
|
||||
} else if prog != "100" {
|
||||
|
@ -84,32 +99,40 @@ func (cr *contactRetry) run() {
|
|||
}
|
||||
|
||||
case <-time.After(tickTime):
|
||||
cr.connections.Range(func(k, v interface{}) bool {
|
||||
p := v.(*contact)
|
||||
|
||||
if p.state == connections.DISCONNECTED {
|
||||
p.ticks++
|
||||
if p.ticks == p.backoff {
|
||||
p.ticks = 0
|
||||
if cr.networkUp {
|
||||
if p.ctype == peerConn {
|
||||
cr.bus.Publish(event.NewEvent(event.RetryPeerRequest, map[event.Field]string{event.RemotePeer: p.id}))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
continue
|
||||
|
||||
case <-cr.breakChan:
|
||||
cr.running = false
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (cr *contactRetry) retryDisconnected() {
|
||||
cr.connections.Range(func(k, v interface{}) bool {
|
||||
p := v.(*contact)
|
||||
|
||||
if p.state == connections.DISCONNECTED {
|
||||
p.ticks++
|
||||
if p.ticks >= p.backoff {
|
||||
p.ticks = 0
|
||||
if cr.networkUp {
|
||||
if p.ctype == peerConn {
|
||||
cr.bus.Publish(event.NewEvent(event.RetryPeerRequest, map[event.Field]string{event.RemotePeer: p.id}))
|
||||
}
|
||||
if p.ctype == serverConn {
|
||||
cr.bus.Publish(event.NewEvent(event.RetryServerRequest, map[event.Field]string{event.GroupServer: p.id}))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
func (cr *contactRetry) handleEvent(id string, state connections.ConnectionState, ctype connectionType) {
|
||||
if _, exists := cr.connections.Load(id); !exists {
|
||||
p := &contact{id: id, state: connections.DISCONNECTED, backoff: 1, ticks: 0, ctype: ctype}
|
||||
p := &contact{id: id, state: connections.DISCONNECTED, backoff: 0, ticks: 0, ctype: ctype}
|
||||
cr.connections.Store(id, p)
|
||||
return
|
||||
}
|
||||
|
@ -118,7 +141,9 @@ func (cr *contactRetry) handleEvent(id string, state connections.ConnectionState
|
|||
p := pinf.(*contact)
|
||||
if state == connections.DISCONNECTED || state == connections.FAILED || state == connections.KILLED {
|
||||
p.state = connections.DISCONNECTED
|
||||
if p.backoff < maxBakoff {
|
||||
if p.backoff == 0 {
|
||||
p.backoff = 1
|
||||
} else if p.backoff < maxBakoff {
|
||||
p.backoff *= 2
|
||||
}
|
||||
p.ticks = 0
|
||||
|
@ -126,10 +151,11 @@ func (cr *contactRetry) handleEvent(id string, state connections.ConnectionState
|
|||
p.state = state
|
||||
} else if state == connections.AUTHENTICATED {
|
||||
p.state = state
|
||||
p.backoff = 1
|
||||
p.backoff = 0
|
||||
}
|
||||
}
|
||||
|
||||
func (cr *contactRetry) Shutdown() {
|
||||
cr.breakChan <- true
|
||||
|
||||
}
|
||||
|
|
|
@ -10,12 +10,18 @@ import (
|
|||
"time"
|
||||
)
|
||||
|
||||
// NetworkCheckError is a status for when the NetworkCheck Plugin has had an error making an out going connection indicating it may be offline
|
||||
const NetworkCheckError = "Error"
|
||||
|
||||
// NetworkCheckSuccess is a status for when the NetworkCheck Plugin has had a successful message from a peer, indicating it is online right now
|
||||
const NetworkCheckSuccess = "Success"
|
||||
|
||||
// networkCheck is a convenience plugin for testing high level availability of onion services
|
||||
type networkCheck struct {
|
||||
bus event.Manager
|
||||
queue event.Queue
|
||||
acn connectivity.ACN
|
||||
onionsToCheck []string
|
||||
onionsToCheck sync.Map // onion:string => true:bool
|
||||
breakChan chan bool
|
||||
running bool
|
||||
offline bool
|
||||
|
@ -34,12 +40,15 @@ func (nc *networkCheck) Start() {
|
|||
|
||||
func (nc *networkCheck) run() {
|
||||
nc.running = true
|
||||
nc.offline = true
|
||||
nc.bus.Subscribe(event.ProtocolEngineStartListen, nc.queue)
|
||||
nc.bus.Subscribe(event.NewMessageFromPeer, nc.queue)
|
||||
nc.bus.Subscribe(event.PeerAcknowledgement, nc.queue)
|
||||
nc.bus.Subscribe(event.EncryptedGroupMessage, nc.queue)
|
||||
nc.bus.Subscribe(event.PeerStateChange, nc.queue)
|
||||
nc.bus.Subscribe(event.ServerStateChange, nc.queue)
|
||||
nc.bus.Subscribe(event.NewGetValMessageFromPeer, nc.queue)
|
||||
nc.bus.Subscribe(event.NewRetValMessageFromPeer, nc.queue)
|
||||
var lastMessageReceived time.Time
|
||||
for {
|
||||
select {
|
||||
|
@ -52,18 +61,26 @@ func (nc *networkCheck) run() {
|
|||
// and then we will wait a minute and check the connection for the first time (the onion should be up)
|
||||
// under normal operating circumstances
|
||||
case event.ProtocolEngineStartListen:
|
||||
log.Debugf("initiating connection check for %v", e.Data[event.Onion])
|
||||
nc.onionsToCheck = append(nc.onionsToCheck, e.Data[event.Onion])
|
||||
if _, exists := nc.onionsToCheck.Load(e.Data[event.Onion]); !exists {
|
||||
log.Debugf("initiating connection check for %v", e.Data[event.Onion])
|
||||
nc.onionsToCheck.Store(e.Data[event.Onion], true)
|
||||
if time.Since(lastMessageReceived) > time.Minute {
|
||||
nc.selfTest()
|
||||
}
|
||||
}
|
||||
case event.PeerStateChange:
|
||||
fallthrough
|
||||
case event.ServerStateChange:
|
||||
// if we successfully connect / authenticated to a remote server / peer then we obviously have internet
|
||||
connectionState := e.Data[event.ConnectionState]
|
||||
nc.offlineLock.Lock()
|
||||
if nc.offline && (connectionState == connections.ConnectionStateName[connections.AUTHENTICATED] || connectionState == connections.ConnectionStateName[connections.CONNECTED]) {
|
||||
if connectionState == connections.ConnectionStateName[connections.AUTHENTICATED] || connectionState == connections.ConnectionStateName[connections.CONNECTED] {
|
||||
lastMessageReceived = time.Now()
|
||||
nc.bus.Publish(event.NewEvent(event.NetworkStatus, map[event.Field]string{event.Error: "", event.Status: "Success"}))
|
||||
nc.offline = false
|
||||
|
||||
if nc.offline {
|
||||
nc.bus.Publish(event.NewEvent(event.NetworkStatus, map[event.Field]string{event.Error: "", event.Status: NetworkCheckSuccess}))
|
||||
nc.offline = false
|
||||
}
|
||||
}
|
||||
nc.offlineLock.Unlock()
|
||||
default:
|
||||
|
@ -74,17 +91,15 @@ func (nc *networkCheck) run() {
|
|||
lastMessageReceived = time.Now()
|
||||
nc.offlineLock.Lock()
|
||||
if nc.offline {
|
||||
nc.bus.Publish(event.NewEvent(event.NetworkStatus, map[event.Field]string{event.Error: "", event.Status: "Success"}))
|
||||
nc.bus.Publish(event.NewEvent(event.NetworkStatus, map[event.Field]string{event.Error: "", event.Status: NetworkCheckSuccess}))
|
||||
nc.offline = false
|
||||
}
|
||||
nc.offlineLock.Unlock()
|
||||
}
|
||||
case <-time.After(tickTime):
|
||||
// if we haven't received an action in the last minute...kick off a set of testing
|
||||
if time.Now().Sub(lastMessageReceived) > time.Minute {
|
||||
for _, onion := range nc.onionsToCheck {
|
||||
go nc.checkConnection(onion)
|
||||
}
|
||||
if time.Since(lastMessageReceived) > time.Minute {
|
||||
nc.selfTest()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -98,8 +113,20 @@ func (nc *networkCheck) Shutdown() {
|
|||
}
|
||||
}
|
||||
|
||||
func (nc *networkCheck) selfTest() {
|
||||
nc.onionsToCheck.Range(func(key, val interface{}) bool {
|
||||
go nc.checkConnection(key.(string))
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
//
|
||||
func (nc *networkCheck) checkConnection(onion string) {
|
||||
prog, _ := nc.acn.GetBootstrapStatus()
|
||||
if prog != 100 {
|
||||
return
|
||||
}
|
||||
|
||||
// we want to definitively time these actions out faster than tor will, because these onions should definitely be
|
||||
// online
|
||||
ClientTimeout := TimeoutPolicy(time.Second * 60)
|
||||
|
@ -114,13 +141,13 @@ func (nc *networkCheck) checkConnection(onion string) {
|
|||
defer nc.offlineLock.Unlock()
|
||||
// regardless of the outcome we want to report a status to let anyone who might care know that we did do a check
|
||||
if err != nil {
|
||||
log.Debugf("publishing network error for %v", onion)
|
||||
nc.bus.Publish(event.NewEvent(event.NetworkStatus, map[event.Field]string{event.Onion: onion, event.Error: err.Error(), event.Status: "Error"}))
|
||||
nc.offline = false
|
||||
log.Debugf("publishing network error for %v -- %v\n", onion, err)
|
||||
nc.bus.Publish(event.NewEvent(event.NetworkStatus, map[event.Field]string{event.Onion: onion, event.Error: err.Error(), event.Status: NetworkCheckError}))
|
||||
nc.offline = true
|
||||
} else {
|
||||
log.Debugf("publishing network success for %v", onion)
|
||||
nc.bus.Publish(event.NewEvent(event.NetworkStatus, map[event.Field]string{event.Onion: onion, event.Error: "", event.Status: "Success"}))
|
||||
nc.offline = true
|
||||
nc.bus.Publish(event.NewEvent(event.NetworkStatus, map[event.Field]string{event.Onion: onion, event.Error: "", event.Status: NetworkCheckSuccess}))
|
||||
nc.offline = false
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -138,9 +165,9 @@ func (tp *TimeoutPolicy) ExecuteAction(action func() error) error {
|
|||
c <- action()
|
||||
}()
|
||||
|
||||
tick := time.Tick(time.Duration(*tp))
|
||||
tick := time.NewTicker(time.Duration(*tp))
|
||||
select {
|
||||
case <-tick:
|
||||
case <-tick.C:
|
||||
return fmt.Errorf("ActionTimedOutError")
|
||||
case err := <-c:
|
||||
return err
|
||||
|
|
|
@ -21,10 +21,10 @@ type Plugin interface {
|
|||
}
|
||||
|
||||
// Get is a plugin factory for the requested plugin
|
||||
func Get(id PluginID, bus event.Manager, acn connectivity.ACN) Plugin {
|
||||
func Get(id PluginID, bus event.Manager, acn connectivity.ACN, onion string) Plugin {
|
||||
switch id {
|
||||
case CONNECTIONRETRY:
|
||||
return NewConnectionRetry(bus)
|
||||
return NewConnectionRetry(bus, onion)
|
||||
case NETWORKCHECK:
|
||||
return NewNetworkCheck(bus, acn)
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ package utils
|
|||
|
||||
import (
|
||||
app2 "cwtch.im/cwtch/app"
|
||||
"cwtch.im/cwtch/model/attr"
|
||||
"cwtch.im/cwtch/peer"
|
||||
"time"
|
||||
)
|
||||
|
@ -11,13 +12,14 @@ import (
|
|||
// however for small utility use, this function which polls the app until the peer is created
|
||||
// may fill that usecase better
|
||||
func WaitGetPeer(app app2.Application, name string) peer.CwtchPeer {
|
||||
for true {
|
||||
for id, n := range app.ListPeers() {
|
||||
if n == name {
|
||||
return app.GetPeer(id)
|
||||
for {
|
||||
for id := range app.ListPeers() {
|
||||
peer := app.GetPeer(id)
|
||||
localName, _ := peer.GetAttribute(attr.GetLocalScope("name"))
|
||||
if localName == name {
|
||||
return peer
|
||||
}
|
||||
}
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -169,10 +169,7 @@ func (pb *pipeBridge) threeShakeClient() bool {
|
|||
if string(resp) == synack {
|
||||
stop <- true
|
||||
err := pb.writeString([]byte(ack))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
return err == nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,6 +19,11 @@ const (
|
|||
// RemotePeer: [eg "chpr7qm6op5vfcg2pi4vllco3h6aa7exexc4rqwnlupqhoogx2zgd6qd"
|
||||
RetryPeerRequest = Type("RetryPeerRequest")
|
||||
|
||||
// RetryServerRequest
|
||||
// Asks CwtchPeer to retry a server connection...
|
||||
// GroupServer: [eg "chpr7qm6op5vfcg2pi4vllco3h6aa7exexc4rqwnlupqhoogx2zgd6qd"
|
||||
RetryServerRequest = Type("RetryServerRequest")
|
||||
|
||||
// RemotePeer
|
||||
// Authorization(model.peer.Auth_...)
|
||||
SetPeerAuthorization = Type("UpdatePeerAuthorization")
|
||||
|
@ -30,6 +35,9 @@ const (
|
|||
// GroupServer
|
||||
JoinServer = Type("JoinServer")
|
||||
|
||||
// attributes GroupServer - the onion of the server to leave
|
||||
LeaveServer = Type("LeaveServer")
|
||||
|
||||
ProtocolEngineStartListen = Type("ProtocolEngineStartListen")
|
||||
ProtocolEngineStopped = Type("ProtocolEngineStopped")
|
||||
|
||||
|
@ -43,8 +51,13 @@ const (
|
|||
// Imported
|
||||
NewGroupInvite = Type("NewGroupInvite")
|
||||
|
||||
// Inform the UI about a new group
|
||||
// GroupID: groupID (allows them to fetch from the peer)
|
||||
NewGroup = Type("NewGroup")
|
||||
|
||||
// GroupID
|
||||
AcceptGroupInvite = Type("AcceptGroupInvite")
|
||||
RejectGroupInvite = Type("RejectGroupInvite")
|
||||
|
||||
SendMessageToGroup = Type("SendMessagetoGroup")
|
||||
|
||||
|
@ -53,6 +66,10 @@ const (
|
|||
//TimestampReceived, TimestampSent, Data(Message), GroupID, Signature, PreviousSignature, RemotePeer
|
||||
NewMessageFromGroup = Type("NewMessageFromGroup")
|
||||
|
||||
// Sent if a Group Key is detected as being used outside of expected parameters (e.g. with tampered signatures)
|
||||
// GroupID: The ID of the Group that is presumed compromised
|
||||
GroupCompromised = Type("GroupCompromised")
|
||||
|
||||
// an error was encountered trying to send a particular Message to a group
|
||||
// attributes:
|
||||
// GroupServer: The server the Message was sent to
|
||||
|
@ -81,6 +98,24 @@ const (
|
|||
// RemotePeer: The peer associated with the acknowledgement
|
||||
PeerAcknowledgement = Type("PeerAcknowledgement")
|
||||
|
||||
// Like PeerAcknowledgement but with message index instead of event ID
|
||||
// attributes
|
||||
// Index: The original index of the message that the peer is responding too.
|
||||
// RemotePeer: The peer associated with the acknowledgement
|
||||
IndexedAcknowledgement = Type("IndexedAcknowledgement")
|
||||
|
||||
// Like PeerAcknowledgement but with message index instead of event ID
|
||||
// attributes
|
||||
// Index: The original index of the message that the peer is responding too.
|
||||
// RemotePeer: The peer associated with the acknowledgement
|
||||
IndexedFailure = Type("IndexedFailure")
|
||||
|
||||
// UpdateMessageFlags will change the flags associated with a given message.
|
||||
// Handle
|
||||
// Message Index
|
||||
// Flags
|
||||
UpdateMessageFlags = Type("UpdateMessageFlags")
|
||||
|
||||
// attributes:
|
||||
// RemotePeer: [eg "chpr7qm6op5vfcg2pi4vllco3h6aa7exexc4rqwnlupqhoogx2zgd6qd"]
|
||||
// Error: string describing the error
|
||||
|
@ -153,12 +188,14 @@ const (
|
|||
// ProfileName, Password, Data(tag)
|
||||
CreatePeer = Type("CreatePeer")
|
||||
|
||||
// service -> client: Identity(localId), Password, [Status(new/default=blank || from reload='running')]
|
||||
// app -> Key, Salt
|
||||
// app: Identity(onion), Created(bool)
|
||||
// service -> client: Identity(localId), Password, [Status(new/default=blank || from reload='running')], Created(bool)
|
||||
NewPeer = Type("NewPeer")
|
||||
|
||||
// Identity(onion)
|
||||
DeletePeer = Type("DeletePeer")
|
||||
// Identity(onion)
|
||||
PeerDeleted = Type("PeerDeleted")
|
||||
|
||||
// Identity(onion), Data(pluginID)
|
||||
AddPeerPlugin = Type("AddPeerPlugin")
|
||||
|
@ -186,17 +223,25 @@ const (
|
|||
// Error(err)
|
||||
AppError = Type("AppError")
|
||||
|
||||
GetACNStatus = Type("GetACNStatus")
|
||||
GetACNStatus = Type("GetACNStatus")
|
||||
GetACNVersion = Type("GetACNVersion")
|
||||
|
||||
// Progress, Status
|
||||
ACNStatus = Type("ACNStatus")
|
||||
|
||||
// Data
|
||||
ACNVersion = Type("ACNVersion")
|
||||
|
||||
// Network Status
|
||||
// Status: Success || Error
|
||||
// Error: Description of the Error
|
||||
// Onion: the local onion we attempt to check
|
||||
NetworkStatus = Type("NetworkError")
|
||||
|
||||
// Notify the UI that a Server has been added
|
||||
// Onion = Server Onion
|
||||
ServerCreated = Type("ServerAdded")
|
||||
|
||||
// For debugging. Allows test to emit a Syn and get a response Ack(eventID) when the subsystem is done processing a queue
|
||||
Syn = Type("Syn")
|
||||
Ack = Type("Ack")
|
||||
|
@ -233,6 +278,8 @@ const (
|
|||
Password = Field("Password")
|
||||
NewPassword = Field("NewPassword")
|
||||
|
||||
Created = Field("Created")
|
||||
|
||||
ConnectionState = Field("ConnectionState")
|
||||
|
||||
Key = Field("Key")
|
||||
|
@ -245,10 +292,17 @@ const (
|
|||
|
||||
Error = Field("Error")
|
||||
|
||||
Progreess = Field("Progress")
|
||||
Progress = Field("Progress")
|
||||
Status = Field("Status")
|
||||
EventID = Field("EventID")
|
||||
EventContext = Field("EventContext")
|
||||
Index = Field("Index")
|
||||
|
||||
// Handle denotes a contact handle of any type.
|
||||
Handle = Field("Handle")
|
||||
|
||||
// Flags denotes a set of message flags
|
||||
Flags = Field("Flags")
|
||||
|
||||
Authorization = Field("Authorization")
|
||||
|
||||
|
@ -256,11 +310,14 @@ const (
|
|||
|
||||
// Indicate whether an event was triggered by a user import
|
||||
Imported = Field("Imported")
|
||||
|
||||
Source = Field("Source")
|
||||
)
|
||||
|
||||
// Defining Common errors
|
||||
const (
|
||||
AppErrLoaded0 = "Loaded 0 profiles"
|
||||
PasswordMatchError = "Password did not match"
|
||||
)
|
||||
|
||||
// Values to be suplied in event.NewPeer for Status
|
||||
|
@ -292,3 +349,9 @@ const (
|
|||
SaveHistoryConfirmed = "SaveHistory"
|
||||
DeleteHistoryConfirmed = "DeleteHistoryConfirmed"
|
||||
)
|
||||
|
||||
// Bool strings
|
||||
const (
|
||||
True = "true"
|
||||
False = "false"
|
||||
)
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
package event
|
||||
|
||||
import "sync"
|
||||
import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
type queue struct {
|
||||
infChan infiniteChannel
|
||||
|
@ -19,7 +21,7 @@ type simpleQueue struct {
|
|||
// the event.Manager.
|
||||
type Queue interface {
|
||||
Publish(event Event)
|
||||
Next() *Event
|
||||
Next() Event
|
||||
Shutdown()
|
||||
OutChan() <-chan Event
|
||||
Len() int
|
||||
|
@ -52,9 +54,9 @@ func (sq *simpleQueue) Len() int {
|
|||
}
|
||||
|
||||
// Next returns the next available event from the front of the queue
|
||||
func (sq *simpleQueue) Next() *Event {
|
||||
func (sq *simpleQueue) Next() Event {
|
||||
event := <-sq.eventChannel
|
||||
return &event
|
||||
return event
|
||||
}
|
||||
|
||||
// Shutdown closes our eventChannel
|
||||
|
@ -83,9 +85,9 @@ func (iq *queue) OutChan() <-chan Event {
|
|||
}
|
||||
|
||||
// Out returns the next available event from the front of the queue
|
||||
func (iq *queue) Next() *Event {
|
||||
func (iq *queue) Next() Event {
|
||||
event := <-iq.infChan.Out()
|
||||
return &event
|
||||
return event
|
||||
}
|
||||
|
||||
func (iq *queue) Len() int {
|
||||
|
|
|
@ -2,12 +2,19 @@ package event
|
|||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"git.openprivacy.ca/openprivacy/log"
|
||||
"math"
|
||||
"math/big"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Event is a structure which binds a given set of data to an Type
|
||||
// Event is the core struct type passed around between various subsystems. Events consist of a type which can be
|
||||
// filtered on, an event ID for tracing and a map of Fields to string values.
|
||||
type Event struct {
|
||||
EventType Type
|
||||
EventID string
|
||||
|
@ -47,10 +54,11 @@ func NewEventList(eventType Type, args ...interface{}) Event {
|
|||
// Manager is an Event Bus which allows subsystems to subscribe to certain EventTypes and publish others.
|
||||
type manager struct {
|
||||
subscribers map[Type][]Queue
|
||||
events chan Event
|
||||
events chan []byte
|
||||
mapMutex sync.Mutex
|
||||
internal chan bool
|
||||
closed bool
|
||||
trace bool
|
||||
}
|
||||
|
||||
// Manager is an interface for an event bus
|
||||
|
@ -72,9 +80,12 @@ func NewEventManager() Manager {
|
|||
// Initialize sets up the Manager.
|
||||
func (em *manager) initialize() {
|
||||
em.subscribers = make(map[Type][]Queue)
|
||||
em.events = make(chan Event)
|
||||
em.events = make(chan []byte)
|
||||
em.internal = make(chan bool)
|
||||
em.closed = false
|
||||
|
||||
_, em.trace = os.LookupEnv("CWTCH_EVENT_SOURCE")
|
||||
|
||||
go em.eventBus()
|
||||
}
|
||||
|
||||
|
@ -88,8 +99,27 @@ func (em *manager) Subscribe(eventType Type, queue Queue) {
|
|||
|
||||
// Publish takes an Event and sends it to the internal eventBus where it is distributed to all Subscribers
|
||||
func (em *manager) Publish(event Event) {
|
||||
if event.EventType != "" && em.closed != true {
|
||||
em.events <- event
|
||||
if event.EventType != "" && !em.closed {
|
||||
|
||||
// Debug Events for Tracing, locked behind an environment variable
|
||||
// for now.
|
||||
if em.trace {
|
||||
pc, _, _, _ := runtime.Caller(1)
|
||||
funcName := runtime.FuncForPC(pc).Name()
|
||||
lastSlash := strings.LastIndexByte(funcName, '/')
|
||||
if lastSlash < 0 {
|
||||
lastSlash = 0
|
||||
}
|
||||
lastDot := strings.LastIndexByte(funcName[lastSlash:], '.') + lastSlash
|
||||
event.Data[Source] = fmt.Sprintf("%v.%v", funcName[:lastDot], funcName[lastDot+1:])
|
||||
}
|
||||
|
||||
// Deep Copy the Event...
|
||||
eventJSON, err := json.Marshal(event)
|
||||
if err != nil {
|
||||
log.Errorf("Error serializing event: %v", event)
|
||||
}
|
||||
em.events <- eventJSON
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -101,13 +131,21 @@ func (em *manager) PublishLocal(event Event) {
|
|||
// eventBus is an internal function that is used to distribute events to all subscribers
|
||||
func (em *manager) eventBus() {
|
||||
for {
|
||||
event := <-em.events
|
||||
eventJSON := <-em.events
|
||||
|
||||
// In the case on an empty event. Teardown the Queue
|
||||
if event.EventType == "" {
|
||||
if len(eventJSON) == 0 {
|
||||
log.Errorf("Received zero length event")
|
||||
break
|
||||
}
|
||||
|
||||
var event Event
|
||||
err := json.Unmarshal(eventJSON, &event)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("Error on Deep Copy: %v %v", eventJSON, err)
|
||||
}
|
||||
|
||||
// maps aren't thread safe
|
||||
em.mapMutex.Lock()
|
||||
subscribers := em.subscribers[event.EventType]
|
||||
|
@ -115,7 +153,10 @@ func (em *manager) eventBus() {
|
|||
|
||||
// Send the event to any subscribers to that event type
|
||||
for _, subscriber := range subscribers {
|
||||
subscriber.Publish(event)
|
||||
// Deep Copy for Each Subscriber
|
||||
var eventCopy Event
|
||||
json.Unmarshal(eventJSON, &eventCopy)
|
||||
subscriber.Publish(eventCopy)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -125,7 +166,7 @@ func (em *manager) eventBus() {
|
|||
|
||||
// Shutdown triggers, and waits for, the internal eventBus goroutine to finish
|
||||
func (em *manager) Shutdown() {
|
||||
em.events <- Event{}
|
||||
em.events <- []byte{}
|
||||
em.closed = true
|
||||
// wait for eventBus to finish
|
||||
<-em.internal
|
||||
|
|
10
go.mod
10
go.mod
|
@ -3,13 +3,15 @@ module cwtch.im/cwtch
|
|||
go 1.14
|
||||
|
||||
require (
|
||||
cwtch.im/tapir v0.2.0
|
||||
git.openprivacy.ca/openprivacy/connectivity v1.3.0
|
||||
git.openprivacy.ca/openprivacy/log v1.0.1
|
||||
git.openprivacy.ca/cwtch.im/tapir v0.4.3
|
||||
git.openprivacy.ca/openprivacy/connectivity v1.4.4
|
||||
git.openprivacy.ca/openprivacy/log v1.0.2
|
||||
github.com/gtank/ristretto255 v0.1.2
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
|
||||
github.com/struCoder/pidusage v0.1.3
|
||||
golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee
|
||||
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb
|
||||
golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 // indirect
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4
|
||||
golang.org/x/tools v0.1.2 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
|
||||
)
|
||||
|
|
90
go.sum
90
go.sum
|
@ -1,27 +1,46 @@
|
|||
cwtch.im/tapir v0.2.0 h1:7MkoR5+uEuPW34/O0GZRidnIjq/01Cfm8nl5IRuqpGc=
|
||||
cwtch.im/tapir v0.2.0/go.mod h1:xzzZ28adyUXNkYL1YodcHsAiTt3IJ8Loc29YVn9mIEQ=
|
||||
git.openprivacy.ca/openprivacy/bine v0.0.3 h1:PSHUmNqaW7BZUX8n2eTDeNbjsuRe+t5Ae0Og+P+jDM0=
|
||||
git.openprivacy.ca/openprivacy/bine v0.0.3/go.mod h1:13ZqhKyqakDsN/ZkQkIGNULsmLyqtXc46XBcnuXm/mU=
|
||||
git.openprivacy.ca/openprivacy/connectivity v1.2.0 h1:dbZ5CRl11vg3BNHdzRKSlDP8OUtDB+mf6FkxMVf73qw=
|
||||
git.openprivacy.ca/openprivacy/connectivity v1.2.0/go.mod h1:B7vzuVmChJtSKoh0ezph5vu6DQ0gIk0zHUNG6IgXCcA=
|
||||
git.openprivacy.ca/openprivacy/connectivity v1.3.0 h1:e2EeV6CaMNwOb+PzAjF0hGCeOqAPagRaDL4en5ITf7U=
|
||||
git.openprivacy.ca/openprivacy/connectivity v1.3.0/go.mod h1:s0/QhONuUqJQfYTAgUlu+ya7G3Ov6bKgpT5QkOhVxDI=
|
||||
git.openprivacy.ca/openprivacy/log v1.0.0 h1:Rvqm1weUdR4AOnJ79b1upHCc9vC/QF1rhSD2Um7sr1Y=
|
||||
git.openprivacy.ca/openprivacy/log v1.0.0/go.mod h1:gGYK8xHtndRLDymFtmjkG26GaMQNgyhioNS82m812Iw=
|
||||
git.openprivacy.ca/openprivacy/log v1.0.1 h1:NWV5oBTatvlSzUE6wtB+UQCulgyMOtm4BXGd34evMys=
|
||||
git.openprivacy.ca/cwtch.im/tapir v0.3.1 h1:+d1dHyPvZ8JmdfFe/oXWJPardzflRIhcdILtkeArkW8=
|
||||
git.openprivacy.ca/cwtch.im/tapir v0.3.1/go.mod h1:q6RMI/TQvRN8SCtRY3GryOawMcB0uG6NjP6M77oSMx8=
|
||||
git.openprivacy.ca/cwtch.im/tapir v0.3.2 h1:thLWqqY1LkirWFcy9Tg6NgWeYbvo9xBm+s2XVnCIvpY=
|
||||
git.openprivacy.ca/cwtch.im/tapir v0.3.2/go.mod h1:q6RMI/TQvRN8SCtRY3GryOawMcB0uG6NjP6M77oSMx8=
|
||||
git.openprivacy.ca/cwtch.im/tapir v0.3.3 h1:Q7F8JijgOMMYSy3IdZl7+r6qkWckEWV1+EY7q6MAkVs=
|
||||
git.openprivacy.ca/cwtch.im/tapir v0.3.3/go.mod h1:ZMg9Jzh0n3Os2aSF4z+bx/n8WBCJBN7KCQESXperYts=
|
||||
git.openprivacy.ca/cwtch.im/tapir v0.3.4 h1:g7yZkfz/vWr/t2tFXa/t0Ebr/w665uIKpxpCZ3lIPCo=
|
||||
git.openprivacy.ca/cwtch.im/tapir v0.3.4/go.mod h1:+Niy2AHhQC351ZTtfhC0uLjViCICyOxCJZsIlGKKNAU=
|
||||
git.openprivacy.ca/cwtch.im/tapir v0.3.5 h1:AlqAhluY4ivznGoHh37Khyxy0u9IbtYskP93wgtmYx8=
|
||||
git.openprivacy.ca/cwtch.im/tapir v0.3.5/go.mod h1:eH6dZxXrhW0C4KZX18ksUa6XJCrEvtg8cJJ/Fy6gv+E=
|
||||
git.openprivacy.ca/cwtch.im/tapir v0.4.0 h1:clG8uORt0NKEhT4P+Dpw1pzyUuYzYBMevGqn2pciKk8=
|
||||
git.openprivacy.ca/cwtch.im/tapir v0.4.0/go.mod h1:eH6dZxXrhW0C4KZX18ksUa6XJCrEvtg8cJJ/Fy6gv+E=
|
||||
git.openprivacy.ca/cwtch.im/tapir v0.4.1 h1:9LMpQX41IzecNNlRc1FZKXHg6wlFss679tFsa3vzb3Y=
|
||||
git.openprivacy.ca/cwtch.im/tapir v0.4.1/go.mod h1:eH6dZxXrhW0C4KZX18ksUa6XJCrEvtg8cJJ/Fy6gv+E=
|
||||
git.openprivacy.ca/cwtch.im/tapir v0.4.2 h1:bxMWZnVJXX4dqqOFS7ELW4iFkVL4GS8wiRkjRv5rJe8=
|
||||
git.openprivacy.ca/cwtch.im/tapir v0.4.2/go.mod h1:eH6dZxXrhW0C4KZX18ksUa6XJCrEvtg8cJJ/Fy6gv+E=
|
||||
git.openprivacy.ca/cwtch.im/tapir v0.4.3 h1:sctSfUXHDIqaHfJPDl+5lHtmoEJolQiHTcHZGAe5Qc4=
|
||||
git.openprivacy.ca/cwtch.im/tapir v0.4.3/go.mod h1:10qEaib5x021zgyZ/97JKWsEpedH5+Vfy2CvB2V+08E=
|
||||
git.openprivacy.ca/openprivacy/bine v0.0.4 h1:CO7EkGyz+jegZ4ap8g5NWRuDHA/56KKvGySR6OBPW+c=
|
||||
git.openprivacy.ca/openprivacy/bine v0.0.4/go.mod h1:13ZqhKyqakDsN/ZkQkIGNULsmLyqtXc46XBcnuXm/mU=
|
||||
git.openprivacy.ca/openprivacy/connectivity v1.4.0 h1:c7AANUCrlA4hIqXxIGDOWMtSe8CpDleD1877PShScbM=
|
||||
git.openprivacy.ca/openprivacy/connectivity v1.4.0/go.mod h1:bR0Myx9nm2YzWtsThRelkNMV4Pp7sPDa123O1qsAbVo=
|
||||
git.openprivacy.ca/openprivacy/connectivity v1.4.1 h1:zoM+j7PFj8mQeUCNiDNMe7Uq9dhcJDOhaZcSANfeDL4=
|
||||
git.openprivacy.ca/openprivacy/connectivity v1.4.1/go.mod h1:bR0Myx9nm2YzWtsThRelkNMV4Pp7sPDa123O1qsAbVo=
|
||||
git.openprivacy.ca/openprivacy/connectivity v1.4.2 h1:rQFIjWunLlRmXL5Efsv+7+1cA70T6Uza6RCy2PRm9zc=
|
||||
git.openprivacy.ca/openprivacy/connectivity v1.4.2/go.mod h1:bR0Myx9nm2YzWtsThRelkNMV4Pp7sPDa123O1qsAbVo=
|
||||
git.openprivacy.ca/openprivacy/connectivity v1.4.3 h1:i2Ad/U9FlL9dKr2bhRck7lJ8NoWyGtoEfUwoCyMT0fU=
|
||||
git.openprivacy.ca/openprivacy/connectivity v1.4.3/go.mod h1:bR0Myx9nm2YzWtsThRelkNMV4Pp7sPDa123O1qsAbVo=
|
||||
git.openprivacy.ca/openprivacy/connectivity v1.4.4 h1:11M3akVCyy/luuhMpZTM1r9Jayl7IHD944Bxsn2FDpU=
|
||||
git.openprivacy.ca/openprivacy/connectivity v1.4.4/go.mod h1:JVRCIdL+lAG6ohBFWiKeC/MN42nnC0sfFszR9XG6vPQ=
|
||||
git.openprivacy.ca/openprivacy/log v1.0.1/go.mod h1:gGYK8xHtndRLDymFtmjkG26GaMQNgyhioNS82m812Iw=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cretz/bine v0.1.1-0.20200124154328-f9f678b84cca h1:Q2r7AxHdJwWfLtBZwvW621M3sPqxPc6ITv2j1FGsYpw=
|
||||
github.com/cretz/bine v0.1.1-0.20200124154328-f9f678b84cca/go.mod h1:6PF6fWAvYtwjRGkAuDEJeWNOv3a2hUouSP/yRYXmvHw=
|
||||
git.openprivacy.ca/openprivacy/log v1.0.2 h1:HLP4wsw4ljczFAelYnbObIs821z+jgMPCe8uODPnGQM=
|
||||
git.openprivacy.ca/openprivacy/log v1.0.2/go.mod h1:gGYK8xHtndRLDymFtmjkG26GaMQNgyhioNS82m812Iw=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/gordonklaus/ineffassign v0.0.0-20200309095847-7953dde2c7bf/go.mod h1:cuNKsD1zp2v6XfE/orVX2QE1LC+i254ceGcVeDT3pTU=
|
||||
github.com/gtank/merlin v0.1.1 h1:eQ90iG7K9pOhtereWsmyRJ6RAwcP4tHTDBHXNg+u5is=
|
||||
github.com/gtank/merlin v0.1.1/go.mod h1:T86dnYJhcGOh5BjZFCJWTDeTK7XW8uE+E21Cy/bIQ+s=
|
||||
github.com/gtank/ristretto255 v0.1.2 h1:JEqUCPA1NvLq5DwYtuzigd7ss8fwbYay9fi4/5uMzcc=
|
||||
github.com/gtank/ristretto255 v0.1.2/go.mod h1:Ph5OpO6c7xKUGROZfWVLiJf9icMDwUeIvY4OmlYW69o=
|
||||
github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs=
|
||||
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pty v1.1.1 h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
|
@ -31,54 +50,61 @@ github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWb
|
|||
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/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
|
||||
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.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
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/struCoder/pidusage v0.1.3 h1:pZcSa6asBE38TJtW0Nui6GeCjLTpaT/jAnNP7dUTLSQ=
|
||||
github.com/struCoder/pidusage v0.1.3/go.mod h1:pWBlW3YuSwRl6h7R5KbvA4N8oOqe9LjaKW5CwT1SPjI=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.3.5 h1:dPmz1Snjq0kmkz159iL7S6WzdahUTHnHB5M56WFVifs=
|
||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
go.etcd.io/bbolt v1.3.4 h1:hi1bXHMVrlQh6WwxAy+qZCV/SYIlqo+Ushwdpa4tAKg=
|
||||
go.etcd.io/bbolt v1.3.4/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200204104054-c9f3fb736b72/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200206161412-a0c6ece9d31a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee h1:4yd7jl+vXjalO5ztz6Vc1VADv+S/80LGJmyl1ROJ2AI=
|
||||
golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 h1:VLliZ0d+/avPrXXH+OakdXhpJuEoBZuwh1m2j7U6Iug=
|
||||
golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb h1:mUVeFHoDKis5nxCAzoAi7E8Ghb86EXh/RK6wtvJIqRY=
|
||||
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4 h1:4nGaVu0QrbjT/AK2PRLuQfQuh6DJve+pELhqTdAj3x0=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f h1:+Nyd8tzPX9R7BWHguqsrbFdRx3WQ/1ib8I44HXV5yTA=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44 h1:Bli41pIlzTzf3KEY06n+xnzK/BESIg2ze4Pgfh/aI8c=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007 h1:gG67DSER+11cZvqIMb8S8bt0vZtiN6xWYARwirrOSfE=
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e h1:FDhOuMEY4JVRztM/gsbk+IKUQ8kj74bxZrgw87eMMVc=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200625195345-7480c7b4547d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.1.2 h1:kRBLX7v7Af8W7Gdbbc908OJcdgtK8bOz9Uaj8/F1ACA=
|
||||
golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
|
129
model/group.go
129
model/group.go
|
@ -1,39 +1,48 @@
|
|||
package model
|
||||
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"crypto/sha512"
|
||||
"cwtch.im/cwtch/model/attr"
|
||||
"cwtch.im/cwtch/protocol/groups"
|
||||
"encoding/base32"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"git.openprivacy.ca/openprivacy/connectivity/tor"
|
||||
"git.openprivacy.ca/openprivacy/log"
|
||||
"golang.org/x/crypto/nacl/secretbox"
|
||||
"golang.org/x/crypto/pbkdf2"
|
||||
"io"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// CurrentGroupVersion is used to set the version of newly created groups and make sure group structs stored are correct and up to date
|
||||
const CurrentGroupVersion = 2
|
||||
const CurrentGroupVersion = 3
|
||||
|
||||
// GroupInvitePrefix identifies a particular string as being a serialized group invite.
|
||||
const GroupInvitePrefix = "torv3"
|
||||
|
||||
// Group defines and encapsulates Cwtch's conception of group chat. Which are sessions
|
||||
// tied to a server under a given group key. Each group has a set of Messages.
|
||||
type Group struct {
|
||||
// GroupID is now derived from the GroupKey and the GroupServer
|
||||
GroupID string
|
||||
SignedGroupID []byte
|
||||
GroupKey [32]byte
|
||||
GroupServer string
|
||||
Timeline Timeline `json:"-"`
|
||||
Accepted bool
|
||||
Owner string
|
||||
IsCompromised bool
|
||||
InitialMessage []byte
|
||||
Attributes map[string]string
|
||||
lock sync.Mutex
|
||||
LocalID string
|
||||
State string `json:"-"`
|
||||
unacknowledgedMessages []Message
|
||||
UnacknowledgedMessages []Message
|
||||
Version int
|
||||
}
|
||||
|
||||
|
@ -42,9 +51,9 @@ func NewGroup(server string) (*Group, error) {
|
|||
group := new(Group)
|
||||
group.Version = CurrentGroupVersion
|
||||
group.LocalID = GenerateRandomID()
|
||||
|
||||
if tor.IsValidHostname(server) == false {
|
||||
return nil, errors.New("Server is not a valid v3 onion")
|
||||
group.Accepted = true // we are starting a group, so we assume we want to connect to it...
|
||||
if !tor.IsValidHostname(server) {
|
||||
return nil, errors.New("server is not a valid v3 onion")
|
||||
}
|
||||
|
||||
group.GroupServer = server
|
||||
|
@ -62,48 +71,49 @@ func NewGroup(server string) (*Group, error) {
|
|||
return nil, err
|
||||
}
|
||||
copy(group.GroupKey[:], groupKey[:])
|
||||
group.Owner = "self"
|
||||
|
||||
// Derive Group ID from the group key and the server public key. This binds the group to a particular server
|
||||
// and key.
|
||||
group.GroupID = deriveGroupID(groupKey[:], server)
|
||||
|
||||
group.Attributes = make(map[string]string)
|
||||
// By default we set the "name" of the group to a random string, we can override this later, but to simplify the
|
||||
// codes around invite, we assume that this is always set.
|
||||
group.Attributes[attr.GetLocalScope("name")] = group.GroupID
|
||||
return group, nil
|
||||
}
|
||||
|
||||
// SignGroup adds a signature to the group.
|
||||
func (g *Group) SignGroup(signature []byte) {
|
||||
g.SignedGroupID = signature
|
||||
copy(g.Timeline.SignedGroupID[:], g.SignedGroupID)
|
||||
// CheckGroup returns true only if the ID of the group is cryptographically valid.
|
||||
func (g *Group) CheckGroup() bool {
|
||||
return g.GroupID == deriveGroupID(g.GroupKey[:], g.GroupServer)
|
||||
}
|
||||
|
||||
// Compromised should be called if we detect a a groupkey leak.
|
||||
// deriveGroupID hashes together the key and the hostname to create a bound identifier that can later
|
||||
// be referenced and checked by profiles when they receive invites and messages.
|
||||
func deriveGroupID(groupKey []byte, serverHostname string) string {
|
||||
data, _ := base32.StdEncoding.DecodeString(strings.ToUpper(serverHostname))
|
||||
pubkey := data[0:ed25519.PublicKeySize]
|
||||
return hex.EncodeToString(pbkdf2.Key(groupKey, pubkey, 4096, 16, sha512.New))
|
||||
}
|
||||
|
||||
// Compromised should be called if we detect a groupkey leak
|
||||
func (g *Group) Compromised() {
|
||||
g.IsCompromised = true
|
||||
}
|
||||
|
||||
// GetInitialMessage returns the first message of the group, if one was sent with the invite.
|
||||
func (g *Group) GetInitialMessage() []byte {
|
||||
g.lock.Lock()
|
||||
defer g.lock.Unlock()
|
||||
return g.InitialMessage
|
||||
}
|
||||
|
||||
// Invite generates a invitation that can be sent to a cwtch peer
|
||||
func (g *Group) Invite(initialMessage []byte) ([]byte, error) {
|
||||
|
||||
if g.SignedGroupID == nil {
|
||||
return nil, errors.New("group isn't signed")
|
||||
}
|
||||
|
||||
g.InitialMessage = initialMessage[:]
|
||||
func (g *Group) Invite() (string, error) {
|
||||
|
||||
gci := &groups.GroupInvite{
|
||||
GroupName: g.GroupID,
|
||||
SharedKey: g.GroupKey[:],
|
||||
ServerHost: g.GroupServer,
|
||||
SignedGroupID: g.SignedGroupID[:],
|
||||
InitialMessage: initialMessage[:],
|
||||
GroupID: g.GroupID,
|
||||
GroupName: g.Attributes[attr.GetLocalScope("name")],
|
||||
SharedKey: g.GroupKey[:],
|
||||
ServerHost: g.GroupServer,
|
||||
}
|
||||
|
||||
invite, err := json.Marshal(gci)
|
||||
return invite, err
|
||||
serializedInvite := fmt.Sprintf("%v%v", GroupInvitePrefix, base64.StdEncoding.EncodeToString(invite))
|
||||
return serializedInvite, err
|
||||
}
|
||||
|
||||
// AddSentMessage takes a DecryptedGroupMessage and adds it to the Groups Timeline
|
||||
|
@ -119,7 +129,7 @@ func (g *Group) AddSentMessage(message *groups.DecryptedGroupMessage, sig []byte
|
|||
PreviousMessageSig: message.PreviousMessageSig,
|
||||
ReceivedByServer: false,
|
||||
}
|
||||
g.unacknowledgedMessages = append(g.unacknowledgedMessages, timelineMessage)
|
||||
g.UnacknowledgedMessages = append(g.UnacknowledgedMessages, timelineMessage)
|
||||
return timelineMessage
|
||||
}
|
||||
|
||||
|
@ -130,10 +140,10 @@ func (g *Group) ErrorSentMessage(sig []byte, error string) bool {
|
|||
var message *Message
|
||||
|
||||
// Delete the message from the unack'd buffer if it exists
|
||||
for i, unAckedMessage := range g.unacknowledgedMessages {
|
||||
for i, unAckedMessage := range g.UnacknowledgedMessages {
|
||||
if compareSignatures(unAckedMessage.Signature, sig) {
|
||||
message = &unAckedMessage
|
||||
g.unacknowledgedMessages = append(g.unacknowledgedMessages[:i], g.unacknowledgedMessages[i+1:]...)
|
||||
g.UnacknowledgedMessages = append(g.UnacknowledgedMessages[:i], g.UnacknowledgedMessages[i+1:]...)
|
||||
|
||||
message.Error = error
|
||||
g.Timeline.Insert(message)
|
||||
|
@ -150,9 +160,9 @@ func (g *Group) AddMessage(message *groups.DecryptedGroupMessage, sig []byte) (*
|
|||
defer g.lock.Unlock()
|
||||
|
||||
// Delete the message from the unack'd buffer if it exists
|
||||
for i, unAckedMessage := range g.unacknowledgedMessages {
|
||||
for i, unAckedMessage := range g.UnacknowledgedMessages {
|
||||
if compareSignatures(unAckedMessage.Signature, sig) {
|
||||
g.unacknowledgedMessages = append(g.unacknowledgedMessages[:i], g.unacknowledgedMessages[i+1:]...)
|
||||
g.UnacknowledgedMessages = append(g.UnacknowledgedMessages[:i], g.UnacknowledgedMessages[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
@ -166,6 +176,7 @@ func (g *Group) AddMessage(message *groups.DecryptedGroupMessage, sig []byte) (*
|
|||
PreviousMessageSig: message.PreviousMessageSig,
|
||||
ReceivedByServer: true,
|
||||
Error: "",
|
||||
Acknowledged: true,
|
||||
}
|
||||
seen := g.Timeline.Insert(timelineMessage)
|
||||
|
||||
|
@ -176,7 +187,7 @@ func (g *Group) AddMessage(message *groups.DecryptedGroupMessage, sig []byte) (*
|
|||
func (g *Group) GetTimeline() (timeline []Message) {
|
||||
g.lock.Lock()
|
||||
defer g.lock.Unlock()
|
||||
return append(g.Timeline.GetMessages(), g.unacknowledgedMessages...)
|
||||
return append(g.Timeline.GetMessages(), g.UnacknowledgedMessages...)
|
||||
}
|
||||
|
||||
//EncryptMessage takes a message and encrypts the message under the group key.
|
||||
|
@ -226,3 +237,41 @@ func (g *Group) GetAttribute(name string) (value string, exists bool) {
|
|||
value, exists = g.Attributes[name]
|
||||
return
|
||||
}
|
||||
|
||||
// ValidateInvite takes in a serialized invite and returns the invite structure if it is cryptographically valid
|
||||
// and an error if it is not
|
||||
func ValidateInvite(invite string) (*groups.GroupInvite, error) {
|
||||
// We prefix invites for groups with torv3
|
||||
if strings.HasPrefix(invite, GroupInvitePrefix) {
|
||||
data, err := base64.StdEncoding.DecodeString(invite[len(GroupInvitePrefix):])
|
||||
if err == nil {
|
||||
// First attempt to unmarshal the json...
|
||||
var gci groups.GroupInvite
|
||||
err := json.Unmarshal(data, &gci)
|
||||
if err == nil {
|
||||
|
||||
// Validate the Invite by first checking that the server is a valid v3 onion
|
||||
if !tor.IsValidHostname(gci.ServerHost) {
|
||||
return nil, errors.New("server is not a valid v3 onion")
|
||||
}
|
||||
|
||||
// Validate the length of the shared key...
|
||||
if len(gci.SharedKey) != 32 {
|
||||
return nil, errors.New("key length is not 32 bytes")
|
||||
}
|
||||
|
||||
// Derive the servers public key (we can ignore the error checking here because it's already been
|
||||
// done by IsValidHostname, and check that we derive the same groupID...
|
||||
derivedGroupID := deriveGroupID(gci.SharedKey, gci.ServerHost)
|
||||
if derivedGroupID != gci.GroupID {
|
||||
return nil, errors.New("group id is invalid")
|
||||
}
|
||||
|
||||
// Replace the original with the derived, this should be a no-op at this point but defense in depth...
|
||||
gci.GroupID = derivedGroupID
|
||||
return &gci, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, errors.New("invite has invalid structure")
|
||||
}
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
package model
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"cwtch.im/cwtch/protocol/groups"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
@ -16,6 +19,23 @@ func TestGroup(t *testing.T) {
|
|||
PreviousMessageSig: []byte{},
|
||||
Padding: []byte{},
|
||||
}
|
||||
|
||||
invite, err := g.Invite()
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("error creating group invite: %v", err)
|
||||
}
|
||||
|
||||
validatedInvite, err := ValidateInvite(invite)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("error validating group invite: %v", err)
|
||||
}
|
||||
|
||||
if validatedInvite.GroupID != g.GroupID {
|
||||
t.Fatalf("after validate group invite id should be identical to original: %v", err)
|
||||
}
|
||||
|
||||
encMessage, _ := g.EncryptMessage(dgm)
|
||||
ok, message := g.DecryptMessage(encMessage)
|
||||
if !ok || message.Text != "Hello World!" {
|
||||
|
@ -36,3 +56,60 @@ func TestGroupErr(t *testing.T) {
|
|||
t.Errorf("Group Setup Should Have Failed")
|
||||
}
|
||||
}
|
||||
|
||||
// Test various group invite validation failures...
|
||||
func TestGroupValidation(t *testing.T) {
|
||||
|
||||
group := &Group{
|
||||
GroupID: "",
|
||||
GroupKey: [32]byte{},
|
||||
GroupServer: "",
|
||||
Timeline: Timeline{},
|
||||
Accepted: false,
|
||||
IsCompromised: false,
|
||||
Attributes: nil,
|
||||
lock: sync.Mutex{},
|
||||
LocalID: "",
|
||||
State: "",
|
||||
UnacknowledgedMessages: nil,
|
||||
Version: 0,
|
||||
}
|
||||
|
||||
invite, _ := group.Invite()
|
||||
_, err := ValidateInvite(invite)
|
||||
|
||||
if err == nil {
|
||||
t.Fatalf("Group with empty group id should have been an error")
|
||||
}
|
||||
t.Logf("Error: %v", err)
|
||||
|
||||
// Generate a valid group but replace the group server...
|
||||
group, _ = NewGroup("2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd")
|
||||
group.GroupServer = "tcnkoch4nyr3cldkemejtkpqok342rbql6iclnjjs3ndgnjgufzyxvqd"
|
||||
invite, _ = group.Invite()
|
||||
_, err = ValidateInvite(invite)
|
||||
|
||||
if err == nil {
|
||||
t.Fatalf("Group with empty group id should have been an error")
|
||||
}
|
||||
t.Logf("Error: %v", err)
|
||||
|
||||
// Generate a valid group but replace the group key...
|
||||
group, _ = NewGroup("2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd")
|
||||
group.GroupKey = sha256.Sum256([]byte{})
|
||||
invite, _ = group.Invite()
|
||||
_, err = ValidateInvite(invite)
|
||||
|
||||
if err == nil {
|
||||
t.Fatalf("Group with different group key should have errored")
|
||||
}
|
||||
t.Logf("Error: %v", err)
|
||||
|
||||
// mangle the invite
|
||||
_, err = ValidateInvite(strings.ReplaceAll(invite, GroupInvitePrefix, ""))
|
||||
if err == nil {
|
||||
t.Fatalf("Group with different group key should have errored")
|
||||
}
|
||||
t.Logf("Error: %v", err)
|
||||
|
||||
}
|
||||
|
|
|
@ -2,10 +2,10 @@ package model
|
|||
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
"cwtch.im/tapir/primitives"
|
||||
"encoding/base32"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"git.openprivacy.ca/cwtch.im/tapir/primitives"
|
||||
"strings"
|
||||
)
|
||||
|
||||
|
@ -13,6 +13,10 @@ import (
|
|||
type KeyType string
|
||||
|
||||
const (
|
||||
|
||||
// BundleType - the attribute under which the signed server bundle is stored...
|
||||
BundleType = KeyType("server_key_bundle")
|
||||
|
||||
// KeyTypeServerOnion - a cwtch address
|
||||
KeyTypeServerOnion = KeyType("bulletin_board_onion") // bulletin board
|
||||
|
||||
|
@ -80,7 +84,7 @@ func DeserializeAndVerify(bundle []byte) (*KeyBundle, error) {
|
|||
var decodedPub []byte
|
||||
decodedPub, err = base32.StdEncoding.DecodeString(strings.ToUpper(string(serverKey)))
|
||||
if err == nil && len(decodedPub) == 35 {
|
||||
if ed25519.Verify(decodedPub[:32], keyBundle.Serialize(), signature) == true {
|
||||
if ed25519.Verify(decodedPub[:32], keyBundle.Serialize(), signature) { // == true
|
||||
return keyBundle, nil
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
package model
|
||||
|
||||
import (
|
||||
"cwtch.im/tapir/primitives"
|
||||
"git.openprivacy.ca/cwtch.im/tapir/primitives"
|
||||
"testing"
|
||||
)
|
||||
|
||||
|
|
|
@ -26,6 +26,8 @@ type Message struct {
|
|||
ReceivedByServer bool // messages sent to a server
|
||||
Acknowledged bool // peer to peer
|
||||
Error string `json:",omitempty"`
|
||||
// Application specific flags, useful for storing small amounts of metadata
|
||||
Flags uint64
|
||||
}
|
||||
|
||||
// MessageBaseSize is a rough estimate of the base number of bytes the struct uses before strings are populated
|
||||
|
@ -103,7 +105,6 @@ func (t *Timeline) Less(i, j int) bool {
|
|||
}
|
||||
|
||||
// Sort sorts the timeline in a canonical order.
|
||||
// TODO: There is almost definitely a more efficient way of doing things that involve not calling this method on every timeline load.
|
||||
func (t *Timeline) Sort() {
|
||||
t.lock.Lock()
|
||||
defer t.lock.Unlock()
|
||||
|
|
|
@ -16,7 +16,7 @@ func TestMessagePadding(t *testing.T) {
|
|||
|
||||
gid, invite, _ := alice.StartGroup("2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd")
|
||||
|
||||
sarah.ProcessInvite(string(invite), alice.Onion)
|
||||
sarah.ProcessInvite(invite)
|
||||
|
||||
group := alice.GetGroup(gid)
|
||||
|
||||
|
@ -47,9 +47,12 @@ func TestTranscriptConsistency(t *testing.T) {
|
|||
sarah.AddContact(alice.Onion, &alice.PublicProfile)
|
||||
alice.AddContact(sarah.Onion, &sarah.PublicProfile)
|
||||
|
||||
// The lightest weight server entry possible (usually we would import a key bundle...)
|
||||
sarah.AddContact("2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd", &PublicProfile{Attributes: map[string]string{string(KeyTypeServerOnion): "2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd"}})
|
||||
|
||||
gid, invite, _ := alice.StartGroup("2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd")
|
||||
|
||||
sarah.ProcessInvite(string(invite), alice.Onion)
|
||||
sarah.ProcessInvite(invite)
|
||||
|
||||
group := alice.GetGroup(gid)
|
||||
|
||||
|
|
148
model/profile.go
148
model/profile.go
|
@ -2,11 +2,13 @@ package model
|
|||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"cwtch.im/cwtch/model/attr"
|
||||
"cwtch.im/cwtch/protocol/groups"
|
||||
"encoding/base32"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"git.openprivacy.ca/openprivacy/connectivity/tor"
|
||||
"golang.org/x/crypto/ed25519"
|
||||
"io"
|
||||
|
@ -40,7 +42,7 @@ type PublicProfile struct {
|
|||
LocalID string // used by storage engine
|
||||
State string `json:"-"`
|
||||
lock sync.Mutex
|
||||
unacknowledgedMessages map[string]Message
|
||||
UnacknowledgedMessages map[string]int
|
||||
}
|
||||
|
||||
// Profile encapsulates all the attributes necessary to be a Cwtch Peer.
|
||||
|
@ -66,7 +68,7 @@ func (p *PublicProfile) init() {
|
|||
if p.Attributes == nil {
|
||||
p.Attributes = make(map[string]string)
|
||||
}
|
||||
p.unacknowledgedMessages = make(map[string]Message)
|
||||
p.UnacknowledgedMessages = make(map[string]int)
|
||||
p.LocalID = GenerateRandomID()
|
||||
}
|
||||
|
||||
|
@ -111,13 +113,33 @@ func GenerateNewProfile(name string) *Profile {
|
|||
func (p *Profile) AddContact(onion string, profile *PublicProfile) {
|
||||
p.lock.Lock()
|
||||
profile.init()
|
||||
// TODO: More Robust V3 Onion Handling
|
||||
decodedPub, _ := base32.StdEncoding.DecodeString(strings.ToUpper(onion[:56]))
|
||||
profile.Ed25519PublicKey = ed25519.PublicKey(decodedPub[:32])
|
||||
p.Contacts[onion] = profile
|
||||
// We expect callers to verify addresses before we get to this point, so if this isn't a
|
||||
// valid address this is a noop.
|
||||
if tor.IsValidHostname(onion) {
|
||||
decodedPub, err := base32.StdEncoding.DecodeString(strings.ToUpper(onion[:56]))
|
||||
if err == nil {
|
||||
profile.Ed25519PublicKey = ed25519.PublicKey(decodedPub[:32])
|
||||
p.Contacts[onion] = profile
|
||||
}
|
||||
}
|
||||
p.lock.Unlock()
|
||||
}
|
||||
|
||||
// UpdateMessageFlags updates the flags stored with a message
|
||||
func (p *Profile) UpdateMessageFlags(handle string, mIdx int, flags uint64) {
|
||||
p.lock.Lock()
|
||||
defer p.lock.Unlock()
|
||||
if contact, exists := p.Contacts[handle]; exists {
|
||||
if len(contact.Timeline.Messages) > mIdx {
|
||||
contact.Timeline.Messages[mIdx].Flags = flags
|
||||
}
|
||||
} else if group, exists := p.Groups[handle]; exists {
|
||||
if len(group.Timeline.Messages) > mIdx {
|
||||
group.Timeline.Messages[mIdx].Flags = flags
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeleteContact deletes a peer contact
|
||||
func (p *Profile) DeleteContact(onion string) {
|
||||
p.lock.Lock()
|
||||
|
@ -150,10 +172,11 @@ func (p *Profile) AddSentMessageToContactTimeline(onion string, messageTxt strin
|
|||
sig := p.SignMessage(onion + messageTxt + sent.String() + now.String())
|
||||
|
||||
message := &Message{PeerID: p.Onion, Message: messageTxt, Timestamp: sent, Received: now, Signature: sig, Acknowledged: false}
|
||||
if contact.unacknowledgedMessages == nil {
|
||||
contact.unacknowledgedMessages = make(map[string]Message)
|
||||
if contact.UnacknowledgedMessages == nil {
|
||||
contact.UnacknowledgedMessages = make(map[string]int)
|
||||
}
|
||||
contact.unacknowledgedMessages[eventID] = *message
|
||||
contact.Timeline.Insert(message)
|
||||
contact.UnacknowledgedMessages[eventID] = contact.Timeline.Len() - 1
|
||||
return message
|
||||
}
|
||||
return nil
|
||||
|
@ -176,45 +199,47 @@ func (p *Profile) AddMessageToContactTimeline(onion string, messageTxt string, s
|
|||
}
|
||||
|
||||
// ErrorSentMessageToPeer sets a sent message's error message and removes it from the unacknowledged list
|
||||
func (p *Profile) ErrorSentMessageToPeer(onion string, eventID string, error string) {
|
||||
func (p *Profile) ErrorSentMessageToPeer(onion string, eventID string, error string) int {
|
||||
p.lock.Lock()
|
||||
defer p.lock.Unlock()
|
||||
|
||||
contact, ok := p.Contacts[onion]
|
||||
if ok {
|
||||
message, ok := contact.unacknowledgedMessages[eventID]
|
||||
mIdx, ok := contact.UnacknowledgedMessages[eventID]
|
||||
if ok {
|
||||
message.Error = error
|
||||
contact.Timeline.Insert(&message) // TODO: do we want a non timeline.Insert way to handle errors
|
||||
delete(contact.unacknowledgedMessages, eventID)
|
||||
contact.Timeline.Messages[mIdx].Error = error
|
||||
delete(contact.UnacknowledgedMessages, eventID)
|
||||
return mIdx
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// AckSentMessageToPeer sets mesage to a peer as acknowledged
|
||||
func (p *Profile) AckSentMessageToPeer(onion string, eventID string) {
|
||||
func (p *Profile) AckSentMessageToPeer(onion string, eventID string) int {
|
||||
p.lock.Lock()
|
||||
defer p.lock.Unlock()
|
||||
|
||||
contact, ok := p.Contacts[onion]
|
||||
if ok {
|
||||
message, ok := contact.unacknowledgedMessages[eventID]
|
||||
mIdx, ok := contact.UnacknowledgedMessages[eventID]
|
||||
if ok {
|
||||
message.Acknowledged = true
|
||||
contact.Timeline.Insert(&message)
|
||||
delete(contact.unacknowledgedMessages, eventID)
|
||||
contact.Timeline.Messages[mIdx].Acknowledged = true
|
||||
delete(contact.UnacknowledgedMessages, eventID)
|
||||
return mIdx
|
||||
}
|
||||
}
|
||||
|
||||
return -1
|
||||
}
|
||||
|
||||
// AddGroupSentMessageError searches matching groups for the message by sig and marks it as an error
|
||||
func (p *Profile) AddGroupSentMessageError(groupServer string, signature string, error string) {
|
||||
for _, group := range p.Groups {
|
||||
if group.GroupServer == groupServer {
|
||||
if group.ErrorSentMessage([]byte(signature), error) {
|
||||
break
|
||||
}
|
||||
}
|
||||
func (p *Profile) AddGroupSentMessageError(groupID string, signature []byte, error string) {
|
||||
p.lock.Lock()
|
||||
defer p.lock.Unlock()
|
||||
group, exists := p.Groups[groupID]
|
||||
if exists {
|
||||
group.ErrorSentMessage(signature, error)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -297,20 +322,34 @@ func (p *Profile) GetContact(onion string) (*PublicProfile, bool) {
|
|||
return contact, ok
|
||||
}
|
||||
|
||||
// VerifyGroupMessage confirms the authenticity of a message given an onion, message and signature.
|
||||
func (p *Profile) VerifyGroupMessage(onion string, groupID string, message string, timestamp int32, ciphertext []byte, signature []byte) bool {
|
||||
// VerifyGroupMessage confirms the authenticity of a message given an sender onion, ciphertext and signature.
|
||||
// The goal of this function is 2-fold:
|
||||
// 1. We confirm that the sender referenced in the group text is the actual sender of the message (or at least
|
||||
// knows the senders private key)
|
||||
// 2. Secondly, we confirm that the sender sent the message to a particular group id on a specific server (it doesn't
|
||||
// matter if we actually received this message from the server or from a hybrid protocol, all that matters is
|
||||
// that the sender and receivers agree that this message was intended for the group
|
||||
// The 2nd point is important as it prevents an attack documented in the original Cwtch paper (and later at
|
||||
// https://docs.openprivacy.ca/cwtch-security-handbook/groups.html) in which a malicious profile sets up 2 groups
|
||||
// on two different servers with the same key and then forwards messages between them to convince the parties in
|
||||
// each group that they are actually in one big group (with the intent to later censor and/or selectively send messages
|
||||
// to each group).
|
||||
func (p *Profile) VerifyGroupMessage(onion string, groupID string, ciphertext []byte, signature []byte) bool {
|
||||
|
||||
group := p.GetGroup(groupID)
|
||||
if group == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// We use our group id, a known reference server and the ciphertext of the message.
|
||||
m := groupID + group.GroupServer + string(ciphertext)
|
||||
|
||||
// If the message is ostensibly from us then we check it against our public key...
|
||||
if onion == p.Onion {
|
||||
m := groupID + group.GroupServer + string(ciphertext)
|
||||
return ed25519.Verify(p.Ed25519PublicKey, []byte(m), signature)
|
||||
}
|
||||
|
||||
m := groupID + group.GroupServer + string(ciphertext)
|
||||
// Otherwise we derive the public key from the sender and check it against that.
|
||||
decodedPub, err := base32.StdEncoding.DecodeString(strings.ToUpper(onion))
|
||||
if err == nil && len(decodedPub) >= 32 {
|
||||
return ed25519.Verify(decodedPub[:32], []byte(m), signature)
|
||||
|
@ -326,22 +365,13 @@ func (p *Profile) SignMessage(message string) []byte {
|
|||
|
||||
// StartGroup when given a server, creates a new Group under this profile and returns the group id an a precomputed
|
||||
// invite which can be sent on the wire.
|
||||
func (p *Profile) StartGroup(server string) (groupID string, invite []byte, err error) {
|
||||
return p.StartGroupWithMessage(server, []byte{})
|
||||
}
|
||||
|
||||
// StartGroupWithMessage when given a server, and an initial message creates a new Group under this profile and returns the group id an a precomputed
|
||||
// invite which can be sent on the wire.
|
||||
func (p *Profile) StartGroupWithMessage(server string, initialMessage []byte) (groupID string, invite []byte, err error) {
|
||||
func (p *Profile) StartGroup(server string) (groupID string, invite string, err error) {
|
||||
group, err := NewGroup(server)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
return "", "", err
|
||||
}
|
||||
groupID = group.GroupID
|
||||
group.Owner = p.Onion
|
||||
signedGroupID := p.SignMessage(groupID + server)
|
||||
group.SignGroup(signedGroupID)
|
||||
invite, err = group.Invite(initialMessage)
|
||||
invite, err = group.Invite()
|
||||
p.lock.Lock()
|
||||
defer p.lock.Unlock()
|
||||
p.Groups[group.GroupID] = group
|
||||
|
@ -356,34 +386,35 @@ func (p *Profile) GetGroup(groupID string) (g *Group) {
|
|||
return
|
||||
}
|
||||
|
||||
// ProcessInvite adds a new group invite to the profile. returns the new group ID
|
||||
func (p *Profile) ProcessInvite(invite string, peerHostname string) (string, error) {
|
||||
var gci groups.GroupInvite
|
||||
err := json.Unmarshal([]byte(invite), &gci)
|
||||
// ProcessInvite validates a group invite and adds a new group invite to the profile if it is valid.
|
||||
// returns the new group ID on success, error on fail.
|
||||
func (p *Profile) ProcessInvite(invite string) (string, error) {
|
||||
gci, err := ValidateInvite(invite)
|
||||
if err == nil {
|
||||
if server, exists := p.GetContact(gci.ServerHost); !exists || !server.IsServer() {
|
||||
return "", fmt.Errorf("unknown server. a server key bundle needs to be imported before this group can be verified")
|
||||
}
|
||||
group := new(Group)
|
||||
group.Version = CurrentGroupVersion
|
||||
group.GroupID = gci.GroupName
|
||||
group.GroupID = gci.GroupID
|
||||
group.LocalID = GenerateRandomID()
|
||||
group.SignedGroupID = gci.SignedGroupID
|
||||
copy(group.GroupKey[:], gci.SharedKey[:])
|
||||
group.GroupServer = gci.ServerHost
|
||||
group.InitialMessage = []byte(gci.InitialMessage)
|
||||
group.Accepted = false
|
||||
group.Owner = peerHostname
|
||||
group.Attributes = make(map[string]string)
|
||||
group.Attributes[attr.GetLocalScope("name")] = gci.GroupName
|
||||
p.AddGroup(group)
|
||||
return group.GroupID, nil
|
||||
return gci.GroupID, nil
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
|
||||
// AddGroup is a convenience method for adding a group to a profile.
|
||||
func (p *Profile) AddGroup(group *Group) {
|
||||
p.lock.Lock()
|
||||
defer p.lock.Unlock()
|
||||
_, exists := p.Groups[group.GroupID]
|
||||
if !exists {
|
||||
p.lock.Lock()
|
||||
defer p.lock.Unlock()
|
||||
p.Groups[group.GroupID] = group
|
||||
}
|
||||
}
|
||||
|
@ -394,7 +425,7 @@ func (p *Profile) AttemptDecryption(ciphertext []byte, signature []byte) (bool,
|
|||
for _, group := range p.Groups {
|
||||
success, dgm := group.DecryptMessage(ciphertext)
|
||||
if success {
|
||||
verified := p.VerifyGroupMessage(dgm.Onion, group.GroupID, dgm.Text, int32(dgm.Timestamp), ciphertext, signature)
|
||||
verified := p.VerifyGroupMessage(dgm.Onion, group.GroupID, ciphertext, signature)
|
||||
|
||||
// So we have a message that has a valid group key, but the signature can't be verified.
|
||||
// The most obvious explanation for this is that the group key has been compromised (or we are in an open group and the server is being malicious)
|
||||
|
@ -434,21 +465,26 @@ func (p *Profile) EncryptMessageToGroup(message string, groupID string) ([]byte,
|
|||
if group != nil {
|
||||
timestamp := time.Now().Unix()
|
||||
|
||||
// Select the latest message from the timeline as a reference point.
|
||||
var prevSig []byte
|
||||
if len(group.Timeline.Messages) > 0 {
|
||||
prevSig = group.Timeline.Messages[len(group.Timeline.Messages)-1].Signature
|
||||
} else {
|
||||
prevSig = group.SignedGroupID
|
||||
prevSig = []byte(group.GroupID)
|
||||
}
|
||||
|
||||
lenPadding := MaxGroupMessageLength - len(message)
|
||||
padding := make([]byte, lenPadding)
|
||||
getRandomness(&padding)
|
||||
hexGroupID, err := hex.DecodeString(group.GroupID)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
dm := &groups.DecryptedGroupMessage{
|
||||
Onion: p.Onion,
|
||||
Text: message,
|
||||
SignedGroupID: group.SignedGroupID[:],
|
||||
SignedGroupID: hexGroupID,
|
||||
Timestamp: uint64(timestamp),
|
||||
PreviousMessageSig: prevSig,
|
||||
Padding: padding[:],
|
||||
|
|
|
@ -62,9 +62,11 @@ func TestRejectGroupInvite(t *testing.T) {
|
|||
alice := GenerateNewProfile("Alice")
|
||||
sarah.AddContact(alice.Onion, &alice.PublicProfile)
|
||||
alice.AddContact(sarah.Onion, &sarah.PublicProfile)
|
||||
// The lightest weight server entry possible (usually we would import a key bundle...)
|
||||
sarah.AddContact("2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd", &PublicProfile{Attributes: map[string]string{string(KeyTypeServerOnion): "2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd"}})
|
||||
|
||||
gid, invite, _ := alice.StartGroup("2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd")
|
||||
sarah.ProcessInvite(string(invite), alice.Onion)
|
||||
sarah.ProcessInvite(invite)
|
||||
group := alice.GetGroup(gid)
|
||||
if len(sarah.Groups) == 1 {
|
||||
if sarah.GetGroup(group.GroupID).Accepted {
|
||||
|
@ -85,8 +87,11 @@ func TestProfileGroup(t *testing.T) {
|
|||
sarah.AddContact(alice.Onion, &alice.PublicProfile)
|
||||
alice.AddContact(sarah.Onion, &sarah.PublicProfile)
|
||||
|
||||
gid, invite, _ := alice.StartGroupWithMessage("2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd", []byte("Hello World"))
|
||||
sarah.ProcessInvite(string(invite), alice.Onion)
|
||||
gid, invite, _ := alice.StartGroup("2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd")
|
||||
|
||||
// The lightest weight server entry possible (usually we would import a key bundle...)
|
||||
sarah.AddContact("2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd", &PublicProfile{Attributes: map[string]string{string(KeyTypeServerOnion): "2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd"}})
|
||||
sarah.ProcessInvite(invite)
|
||||
if len(sarah.GetGroups()) != 1 {
|
||||
t.Errorf("sarah should only be in 1 group instead: %v", sarah.GetGroups())
|
||||
}
|
||||
|
@ -97,17 +102,11 @@ func TestProfileGroup(t *testing.T) {
|
|||
alice.AttemptDecryption(c, s1)
|
||||
|
||||
gid2, invite2, _ := alice.StartGroup("2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd")
|
||||
sarah.ProcessInvite(string(invite2), alice.Onion)
|
||||
sarah.ProcessInvite(invite2)
|
||||
group2 := alice.GetGroup(gid2)
|
||||
c2, s2, _ := sarah.EncryptMessageToGroup("Hello World", group2.GroupID)
|
||||
alice.AttemptDecryption(c2, s2)
|
||||
|
||||
sarahGroup := sarah.GetGroup(group.GroupID)
|
||||
im := sarahGroup.GetInitialMessage()
|
||||
if string(im) != "Hello World" {
|
||||
t.Errorf("Initial Message was not stored properly: %v", im)
|
||||
}
|
||||
|
||||
_, _, err := sarah.EncryptMessageToGroup(string(make([]byte, MaxGroupMessageLength*2)), group2.GroupID)
|
||||
if err == nil {
|
||||
t.Errorf("Overly long message should have returned an error")
|
||||
|
@ -115,7 +114,10 @@ func TestProfileGroup(t *testing.T) {
|
|||
|
||||
bob := GenerateNewProfile("bob")
|
||||
bob.AddContact(alice.Onion, &alice.PublicProfile)
|
||||
bob.ProcessInvite(string(invite2), alice.Onion)
|
||||
// The lightest weight server entry possible (usually we would import a key bundle...)
|
||||
bob.AddContact("2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd", &PublicProfile{Attributes: map[string]string{string(KeyTypeServerOnion): "2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd"}})
|
||||
|
||||
bob.ProcessInvite(invite2)
|
||||
c3, s3, err := bob.EncryptMessageToGroup("Bobs Message", group2.GroupID)
|
||||
if err == nil {
|
||||
ok, _, message, _ := alice.AttemptDecryption(c3, s3)
|
||||
|
|
|
@ -16,71 +16,179 @@ import (
|
|||
"time"
|
||||
)
|
||||
|
||||
const lastKnownSignature = "LastKnowSignature"
|
||||
|
||||
var autoHandleableEvents = map[event.Type]bool{event.EncryptedGroupMessage: true, event.PeerStateChange: true,
|
||||
event.ServerStateChange: true, event.NewGroupInvite: true, event.NewMessageFromPeer: true,
|
||||
event.PeerAcknowledgement: true, event.PeerError: true, event.SendMessageToGroupError: true,
|
||||
event.NewGetValMessageFromPeer: true, event.NewRetValMessageFromPeer: true}
|
||||
event.PeerAcknowledgement: true, event.PeerError: true, event.SendMessageToPeerError: true, event.SendMessageToGroupError: true,
|
||||
event.NewGetValMessageFromPeer: true, event.NewRetValMessageFromPeer: true, event.ProtocolEngineStopped: true, event.RetryServerRequest: true}
|
||||
|
||||
// DefaultEventsToHandle specifies which events will be subscribed to
|
||||
// when a peer has its Init() function called
|
||||
var DefaultEventsToHandle = []event.Type{
|
||||
event.EncryptedGroupMessage,
|
||||
event.NewMessageFromPeer,
|
||||
event.PeerAcknowledgement,
|
||||
event.NewGroupInvite,
|
||||
event.PeerError,
|
||||
event.SendMessageToGroupError,
|
||||
event.NewGetValMessageFromPeer,
|
||||
event.ProtocolEngineStopped,
|
||||
event.RetryServerRequest,
|
||||
}
|
||||
|
||||
// cwtchPeer manages incoming and outgoing connections and all processing for a Cwtch cwtchPeer
|
||||
type cwtchPeer struct {
|
||||
Profile *model.Profile
|
||||
mutex sync.Mutex
|
||||
shutdown bool
|
||||
Profile *model.Profile
|
||||
mutex sync.Mutex
|
||||
shutdown bool
|
||||
listenStatus bool
|
||||
|
||||
queue event.Queue
|
||||
eventBus event.Manager
|
||||
}
|
||||
|
||||
// CwtchPeer provides us with a way of testing systems built on top of cwtch without having to
|
||||
// directly implement a cwtchPeer.
|
||||
type CwtchPeer interface {
|
||||
Init(event.Manager)
|
||||
AutoHandleEvents(events []event.Type)
|
||||
PeerWithOnion(string)
|
||||
InviteOnionToGroup(string, string) error
|
||||
SendMessageToPeer(string, string) string
|
||||
SendGetValToPeer(string, string, string)
|
||||
func (cp *cwtchPeer) UpdateMessageFlags(handle string, mIdx int, flags uint64) {
|
||||
cp.mutex.Lock()
|
||||
defer cp.mutex.Unlock()
|
||||
log.Debugf("Updating Flags for %v %v %v", handle, mIdx, flags)
|
||||
cp.Profile.UpdateMessageFlags(handle, mIdx, flags)
|
||||
cp.eventBus.Publish(event.NewEvent(event.UpdateMessageFlags, map[event.Field]string{event.Handle: handle, event.Index: strconv.Itoa(mIdx), event.Flags: strconv.FormatUint(flags, 2)}))
|
||||
}
|
||||
|
||||
// BlockUnknownConnections will auto disconnect from connections if authentication doesn't resolve a hostname
|
||||
// known to peer.
|
||||
func (cp *cwtchPeer) BlockUnknownConnections() {
|
||||
cp.eventBus.Publish(event.NewEvent(event.BlockUnknownPeers, map[event.Field]string{}))
|
||||
}
|
||||
|
||||
// AllowUnknownConnections will permit connections from unknown contacts.
|
||||
func (cp *cwtchPeer) AllowUnknownConnections() {
|
||||
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
|
||||
type ReadContacts interface {
|
||||
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
|
||||
ProcessInvite(string, string) (string, error)
|
||||
AcceptInvite(string) error
|
||||
RejectInvite(string)
|
||||
SetContactAttribute(string, string, string)
|
||||
DeleteContact(string)
|
||||
DeleteGroup(string)
|
||||
|
||||
AddServer(string) error
|
||||
JoinServer(string) error
|
||||
SendMessageToGroup(string, string) error
|
||||
SendMessageToGroupTracked(string, string) (string, error)
|
||||
|
||||
GetName() string
|
||||
SetName(string)
|
||||
GetOnion() string
|
||||
}
|
||||
|
||||
// AccessPeeringState provides access to functions relating to the underlying connections of a peer.
|
||||
type AccessPeeringState interface {
|
||||
GetPeerState(string) (connections.ConnectionState, bool)
|
||||
}
|
||||
|
||||
StartGroup(string) (string, []byte, error)
|
||||
// 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
|
||||
}
|
||||
|
||||
ImportGroup(string) error
|
||||
ExportGroup(string) (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
|
||||
AddContact(nick, onion string, authorization model.Authorization)
|
||||
GetContacts() []string
|
||||
GetContact(string) *model.PublicProfile
|
||||
|
||||
SetAttribute(string, string)
|
||||
GetAttribute(string) (string, bool)
|
||||
SetContactAttribute(string, string, string)
|
||||
GetContactAttribute(string, string) (string, bool)
|
||||
SetGroupAttribute(string, string, 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
|
||||
// Note:
|
||||
type SendMessages interface {
|
||||
SendGetValToPeer(string, string, string)
|
||||
SendMessageToPeer(string, string) string
|
||||
|
||||
// TODO This should probably not be exposed
|
||||
StoreMessage(onion string, messageTxt string, sent time.Time)
|
||||
|
||||
// TODO Extract once groups are stable
|
||||
InviteOnionToGroup(string, string) error
|
||||
}
|
||||
|
||||
// SendMessagesToGroup enables a caller to sender messages to a group
|
||||
type SendMessagesToGroup interface {
|
||||
SendMessageToGroupTracked(string, string) (string, error)
|
||||
}
|
||||
|
||||
// ModifyMessages enables a caller to modify the messages in a timline
|
||||
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()
|
||||
|
||||
// Relating to local attributes
|
||||
GetOnion() string
|
||||
SetAttribute(string, string)
|
||||
GetAttribute(string) (string, bool)
|
||||
|
||||
ReadContacts
|
||||
ModifyContacts
|
||||
|
||||
AccessPeeringState
|
||||
ModifyPeeringState
|
||||
|
||||
ReadGroups
|
||||
ModifyGroups
|
||||
|
||||
ReadServers
|
||||
ModifyServers
|
||||
|
||||
SendMessages
|
||||
ModifyMessages
|
||||
SendMessagesToGroup
|
||||
}
|
||||
|
||||
// NewCwtchPeer creates and returns a new cwtchPeer with the given name.
|
||||
|
@ -101,12 +209,15 @@ func FromProfile(profile *model.Profile) CwtchPeer {
|
|||
|
||||
// Init instantiates a cwtchPeer
|
||||
func (cp *cwtchPeer) Init(eventBus event.Manager) {
|
||||
cp.InitForEvents(eventBus, DefaultEventsToHandle)
|
||||
}
|
||||
|
||||
func (cp *cwtchPeer) InitForEvents(eventBus event.Manager, toBeHandled []event.Type) {
|
||||
cp.queue = event.NewQueue()
|
||||
go cp.eventHandler()
|
||||
|
||||
cp.eventBus = eventBus
|
||||
cp.AutoHandleEvents([]event.Type{event.EncryptedGroupMessage, event.NewMessageFromPeer, event.PeerAcknowledgement, event.NewGroupInvite,
|
||||
event.PeerError, event.SendMessageToGroupError, event.NewGetValMessageFromPeer})
|
||||
cp.AutoHandleEvents(toBeHandled)
|
||||
}
|
||||
|
||||
// AutoHandleEvents sets an event (if able) to be handled by this peer
|
||||
|
@ -120,21 +231,17 @@ func (cp *cwtchPeer) AutoHandleEvents(events []event.Type) {
|
|||
}
|
||||
}
|
||||
|
||||
// ImportGroup intializes a group from an imported source rather than a peer invite
|
||||
func (cp *cwtchPeer) ImportGroup(exportedInvite string) (err error) {
|
||||
if strings.HasPrefix(exportedInvite, "torv3") {
|
||||
data, err := base64.StdEncoding.DecodeString(exportedInvite[5:])
|
||||
if err == nil {
|
||||
cp.eventBus.Publish(event.NewEvent(event.NewGroupInvite, map[event.Field]string{
|
||||
event.GroupInvite: string(data),
|
||||
event.Imported: "true",
|
||||
}))
|
||||
} else {
|
||||
log.Errorf("error decoding group invite: %v", err)
|
||||
}
|
||||
return nil
|
||||
// ImportGroup initializes a group from an imported source rather than a peer invite
|
||||
func (cp *cwtchPeer) ImportGroup(exportedInvite string) (string, error) {
|
||||
cp.mutex.Lock()
|
||||
defer cp.mutex.Unlock()
|
||||
gid, err := cp.Profile.ProcessInvite(exportedInvite)
|
||||
|
||||
if err == nil {
|
||||
cp.eventBus.Publish(event.NewEvent(event.NewGroup, map[event.Field]string{event.GroupID: gid, event.GroupInvite: exportedInvite}))
|
||||
}
|
||||
return errors.New("unsupported exported group type")
|
||||
|
||||
return gid, err
|
||||
}
|
||||
|
||||
// ExportGroup serializes a group invite so it can be given offline
|
||||
|
@ -143,37 +250,32 @@ func (cp *cwtchPeer) ExportGroup(groupID string) (string, error) {
|
|||
defer cp.mutex.Unlock()
|
||||
group := cp.Profile.GetGroup(groupID)
|
||||
if group != nil {
|
||||
invite, err := group.Invite(group.GetInitialMessage())
|
||||
if err == nil {
|
||||
exportedInvite := "torv3" + base64.StdEncoding.EncodeToString(invite)
|
||||
return exportedInvite, err
|
||||
}
|
||||
return group.Invite()
|
||||
}
|
||||
return "", errors.New("group id could not be found")
|
||||
}
|
||||
|
||||
// StartGroup create a new group linked to the given server and returns the group ID, an invite or an error.
|
||||
func (cp *cwtchPeer) StartGroup(server string) (string, []byte, error) {
|
||||
return cp.StartGroupWithMessage(server, []byte{})
|
||||
}
|
||||
|
||||
// StartGroupWithMessage create a new group linked to the given server and returns the group ID, an invite or an error.
|
||||
func (cp *cwtchPeer) StartGroupWithMessage(server string, initialMessage []byte) (groupID string, invite []byte, err error) {
|
||||
func (cp *cwtchPeer) StartGroup(server string) (string, string, error) {
|
||||
cp.mutex.Lock()
|
||||
groupID, invite, err = cp.Profile.StartGroupWithMessage(server, initialMessage)
|
||||
groupID, invite, err := cp.Profile.StartGroup(server)
|
||||
cp.mutex.Unlock()
|
||||
if err == nil {
|
||||
group := cp.GetGroup(groupID)
|
||||
jsobj, err := json.Marshal(group)
|
||||
if err == nil {
|
||||
cp.eventBus.Publish(event.NewEvent(event.GroupCreated, map[event.Field]string{
|
||||
event.GroupID: groupID,
|
||||
event.GroupServer: group.GroupServer,
|
||||
event.GroupInvite: invite,
|
||||
// Needed for Storage Engine...
|
||||
event.Data: string(jsobj),
|
||||
}))
|
||||
}
|
||||
} else {
|
||||
log.Errorf("error creating group: %v", err)
|
||||
}
|
||||
return
|
||||
return groupID, invite, err
|
||||
}
|
||||
|
||||
// GetGroups returns an unordered list of all group IDs.
|
||||
|
@ -214,40 +316,45 @@ func (cp *cwtchPeer) AddServer(serverSpecification string) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Debugf("Got new key bundle %v", keyBundle)
|
||||
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
|
||||
// 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)
|
||||
if !keyBundle.HasKeyType(model.KeyTypeTokenOnion) || !keyBundle.HasKeyType(model.KeyTypeServerOnion) || !keyBundle.HasKeyType(model.KeyTypePrivacyPass) {
|
||||
return errors.New("keybundle is incomplete")
|
||||
}
|
||||
|
||||
if keyBundle.HasKeyType(model.KeyTypeServerOnion) {
|
||||
onionKey, _ := keyBundle.GetKey(model.KeyTypeServerOnion)
|
||||
onion := string(onionKey)
|
||||
|
||||
// Add the contact if we don't already have it
|
||||
if cp.GetContact(onion) == nil {
|
||||
decodedPub, _ := base32.StdEncoding.DecodeString(strings.ToUpper(onion))
|
||||
ab := keyBundle.AttributeBundle()
|
||||
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...
|
||||
cp.mutex.Lock()
|
||||
cp.Profile.AddContact(onion, pp)
|
||||
cp.mutex.Unlock()
|
||||
|
||||
pd, _ := json.Marshal(pp)
|
||||
|
||||
// Sync the Storage Engine
|
||||
cp.eventBus.Publish(event.NewEvent(event.PeerCreated, map[event.Field]string{
|
||||
event.Data: string(pd),
|
||||
event.RemotePeer: onion,
|
||||
}))
|
||||
|
||||
// Publish every key as an attribute
|
||||
for k, v := range ab {
|
||||
log.Debugf("Server (%v) has %v key %v", onion, k, v)
|
||||
cp.eventBus.Publish(event.NewEventList(event.SetPeerAttribute, event.RemotePeer, onion, k, v))
|
||||
}
|
||||
|
||||
// Default to Deleting Peer History
|
||||
cp.eventBus.Publish(event.NewEventList(event.SetPeerAttribute, event.RemotePeer, onion, event.SaveHistoryKey, event.DeleteHistoryDefault))
|
||||
return nil
|
||||
}
|
||||
|
||||
// We have already seen this server and so some additional checks are needed (and we don't need to create the
|
||||
// peer).
|
||||
// At this point we know the server exists
|
||||
server := cp.GetContact(onion)
|
||||
ab := keyBundle.AttributeBundle()
|
||||
|
||||
// Check server bundle for consistency
|
||||
// Check server bundle for consistency if we have different keys stored than in the tofu bundle then we
|
||||
// abort...
|
||||
for k, v := range ab {
|
||||
val, exists := server.GetAttribute(k)
|
||||
if exists {
|
||||
|
@ -259,11 +366,14 @@ func (cp *cwtchPeer) AddServer(serverSpecification string) error {
|
|||
// 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
|
||||
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
|
||||
// server with no conflicting keys. So we are going to publish all the keys
|
||||
for k, v := range ab {
|
||||
log.Debugf("Server (%v) has %v key %v", onion, k, v)
|
||||
cp.eventBus.Publish(event.NewEventList(event.SetPeerAttribute, event.RemotePeer, onion, k, v))
|
||||
cp.SetContactAttribute(onion, k, v)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
@ -278,6 +388,18 @@ func (cp *cwtchPeer) GetContacts() []string {
|
|||
return cp.Profile.GetContacts()
|
||||
}
|
||||
|
||||
// GetServers returns an unordered list of servers
|
||||
func (cp *cwtchPeer) GetServers() []string {
|
||||
contacts := cp.Profile.GetContacts()
|
||||
var servers []string
|
||||
for _, contact := range contacts {
|
||||
if cp.GetContact(contact).IsServer() {
|
||||
servers = append(servers, contact)
|
||||
}
|
||||
}
|
||||
return servers
|
||||
}
|
||||
|
||||
// GetContact returns a given contact, nil is no such contact exists
|
||||
func (cp *cwtchPeer) GetContact(onion string) *model.PublicProfile {
|
||||
cp.mutex.Lock()
|
||||
|
@ -286,18 +408,6 @@ func (cp *cwtchPeer) GetContact(onion string) *model.PublicProfile {
|
|||
return contact
|
||||
}
|
||||
|
||||
func (cp *cwtchPeer) GetName() string {
|
||||
cp.mutex.Lock()
|
||||
defer cp.mutex.Unlock()
|
||||
return cp.Profile.Name
|
||||
}
|
||||
|
||||
func (cp *cwtchPeer) SetName(newName string) {
|
||||
cp.mutex.Lock()
|
||||
defer cp.mutex.Unlock()
|
||||
cp.Profile.Name = newName
|
||||
}
|
||||
|
||||
func (cp *cwtchPeer) GetOnion() string {
|
||||
cp.mutex.Lock()
|
||||
defer cp.mutex.Unlock()
|
||||
|
@ -307,7 +417,7 @@ func (cp *cwtchPeer) GetPeerState(onion string) (connections.ConnectionState, bo
|
|||
cp.mutex.Lock()
|
||||
defer cp.mutex.Unlock()
|
||||
if peer, ok := cp.Profile.Contacts[onion]; ok {
|
||||
return connections.ConnectionStateToType[peer.State], true
|
||||
return connections.ConnectionStateToType()[peer.State], true
|
||||
}
|
||||
return connections.DISCONNECTED, false
|
||||
}
|
||||
|
@ -316,7 +426,7 @@ func (cp *cwtchPeer) GetGroupState(groupid string) (connections.ConnectionState,
|
|||
cp.mutex.Lock()
|
||||
defer cp.mutex.Unlock()
|
||||
if group, ok := cp.Profile.Groups[groupid]; ok {
|
||||
return connections.ConnectionStateToType[group.State], true
|
||||
return connections.ConnectionStateToType()[group.State], true
|
||||
}
|
||||
return connections.DISCONNECTED, false
|
||||
}
|
||||
|
@ -351,14 +461,14 @@ func (cp *cwtchPeer) DeleteGroup(groupID string) {
|
|||
func (cp *cwtchPeer) InviteOnionToGroup(onion string, groupid string) error {
|
||||
cp.mutex.Lock()
|
||||
group := cp.Profile.GetGroup(groupid)
|
||||
defer cp.mutex.Unlock()
|
||||
if group == nil {
|
||||
cp.mutex.Unlock()
|
||||
return errors.New("invalid group id")
|
||||
}
|
||||
|
||||
invite, err := group.Invite(group.InitialMessage)
|
||||
invite, err := group.Invite()
|
||||
cp.mutex.Unlock()
|
||||
if err == nil {
|
||||
cp.eventBus.Publish(event.NewEvent(event.InvitePeerToGroup, map[event.Field]string{event.RemotePeer: onion, event.GroupInvite: string(invite)}))
|
||||
cp.SendMessageToPeer(onion, invite)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
@ -369,26 +479,37 @@ func (cp *cwtchPeer) JoinServer(onion string) error {
|
|||
tokenY, yExists := cp.GetContact(onion).GetAttribute(string(model.KeyTypePrivacyPass))
|
||||
tokenOnion, onionExists := cp.GetContact(onion).GetAttribute(string(model.KeyTypeTokenOnion))
|
||||
if yExists && onionExists {
|
||||
cp.eventBus.Publish(event.NewEvent(event.JoinServer, map[event.Field]string{event.GroupServer: onion, event.ServerTokenY: tokenY, event.ServerTokenOnion: tokenOnion}))
|
||||
signature, exists := cp.GetContactAttribute(onion, lastKnownSignature)
|
||||
if !exists {
|
||||
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}))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return errors.New("no keys found for server connection")
|
||||
}
|
||||
|
||||
// SendMessageToGroup attempts to sent the given message to the given group id.
|
||||
// TODO: Deprecate in favour of SendMessageToGroupTracked
|
||||
func (cp *cwtchPeer) SendMessageToGroup(groupid string, message string) error {
|
||||
_, err := cp.SendMessageToGroupTracked(groupid, message)
|
||||
return err
|
||||
// ResyncServer completely tears down and resyncs a new server connection with the given onion address
|
||||
func (cp *cwtchPeer) ResyncServer(onion string) error {
|
||||
if cp.GetContact(onion) != nil {
|
||||
tokenY, yExists := cp.GetContact(onion).GetAttribute(string(model.KeyTypePrivacyPass))
|
||||
tokenOnion, onionExists := cp.GetContact(onion).GetAttribute(string(model.KeyTypeTokenOnion))
|
||||
if yExists && onionExists {
|
||||
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}))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return errors.New("no keys found for server connection")
|
||||
}
|
||||
|
||||
// SendMessageToGroup attempts to sent the given message to the given group id.
|
||||
// SendMessageToGroupTracked attempts to sent the given message to the given group id.
|
||||
// It returns the signature of the message which can be used to identify it in any UX layer.
|
||||
func (cp *cwtchPeer) SendMessageToGroupTracked(groupid string, message string) (string, error) {
|
||||
cp.mutex.Lock()
|
||||
group := cp.Profile.GetGroup(groupid)
|
||||
defer cp.mutex.Unlock()
|
||||
group := cp.Profile.GetGroup(groupid)
|
||||
|
||||
if group == nil {
|
||||
return "", errors.New("invalid group id")
|
||||
|
@ -396,20 +517,23 @@ func (cp *cwtchPeer) SendMessageToGroupTracked(groupid string, message string) (
|
|||
ct, sig, err := cp.Profile.EncryptMessageToGroup(message, groupid)
|
||||
|
||||
if err == nil {
|
||||
cp.eventBus.Publish(event.NewEvent(event.SendMessageToGroup, map[event.Field]string{event.GroupServer: group.GroupServer, event.Ciphertext: string(ct), event.Signature: string(sig)}))
|
||||
cp.eventBus.Publish(event.NewEvent(event.SendMessageToGroup, map[event.Field]string{event.GroupID: groupid, event.GroupServer: group.GroupServer, event.Ciphertext: base64.StdEncoding.EncodeToString(ct), event.Signature: base64.StdEncoding.EncodeToString(sig)}))
|
||||
}
|
||||
|
||||
return string(sig), err
|
||||
return base64.StdEncoding.EncodeToString(sig), err
|
||||
}
|
||||
|
||||
func (cp *cwtchPeer) SendMessageToPeer(onion string, message string) string {
|
||||
event := event.NewEvent(event.SendMessageToPeer, map[event.Field]string{event.RemotePeer: onion, event.Data: message})
|
||||
cp.eventBus.Publish(event)
|
||||
|
||||
cp.mutex.Lock()
|
||||
contact, _ := cp.Profile.GetContact(onion)
|
||||
event.EventID = strconv.Itoa(contact.Timeline.Len())
|
||||
cp.Profile.AddSentMessageToContactTimeline(onion, message, time.Now(), event.EventID)
|
||||
cp.mutex.Unlock()
|
||||
|
||||
cp.eventBus.Publish(event)
|
||||
|
||||
return event.EventID
|
||||
}
|
||||
|
||||
|
@ -427,13 +551,6 @@ func (cp *cwtchPeer) SetContactAuthorization(peer string, authorization model.Au
|
|||
return err
|
||||
}
|
||||
|
||||
// ProcessInvite adds a new group invite to the profile. returns the new group ID
|
||||
func (cp *cwtchPeer) ProcessInvite(invite string, remotePeer string) (string, error) {
|
||||
cp.mutex.Lock()
|
||||
defer cp.mutex.Unlock()
|
||||
return cp.Profile.ProcessInvite(invite, remotePeer)
|
||||
}
|
||||
|
||||
// AcceptInvite accepts a given existing group invite
|
||||
func (cp *cwtchPeer) AcceptInvite(groupID string) error {
|
||||
cp.mutex.Lock()
|
||||
|
@ -453,21 +570,36 @@ 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 detactably online)
|
||||
func (cp *cwtchPeer) Listen() {
|
||||
log.Debugf("cwtchPeer Listen sending ProtocolEngineStartListen\n")
|
||||
cp.eventBus.Publish(event.NewEvent(event.ProtocolEngineStartListen, map[event.Field]string{event.Onion: cp.Profile.Onion}))
|
||||
cp.mutex.Lock()
|
||||
defer cp.mutex.Unlock()
|
||||
if !cp.listenStatus {
|
||||
log.Infof("cwtchPeer Listen sending ProtocolEngineStartListen\n")
|
||||
cp.listenStatus = true
|
||||
cp.eventBus.Publish(event.NewEvent(event.ProtocolEngineStartListen, map[event.Field]string{event.Onion: cp.Profile.Onion}))
|
||||
}
|
||||
// else protocol engine is already listening
|
||||
|
||||
}
|
||||
|
||||
// StartGroupConnections attempts to connect to all group servers (thus initiating reconnect attempts in the conectionsmanager)
|
||||
// StartPeersConnections attempts to connect to peer connections
|
||||
func (cp *cwtchPeer) StartPeersConnections() {
|
||||
for _, contact := range cp.GetContacts() {
|
||||
if !cp.GetContact(contact).IsServer() {
|
||||
cp.PeerWithOnion(contact)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// StartServerConnections attempts to connect to all server connections
|
||||
func (cp *cwtchPeer) StartServerConnections() {
|
||||
for _, contact := range cp.GetContacts() {
|
||||
if cp.GetContact(contact).IsServer() {
|
||||
cp.JoinServer(contact)
|
||||
} else {
|
||||
cp.PeerWithOnion(contact)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -490,6 +622,11 @@ func (cp *cwtchPeer) GetAttribute(key string) (string, bool) {
|
|||
if val, exists := cp.Profile.GetAttribute(key); exists {
|
||||
return val, true
|
||||
}
|
||||
|
||||
if key == attr.GetLocalScope("name") {
|
||||
return cp.Profile.Name, true
|
||||
}
|
||||
|
||||
return "", false
|
||||
}
|
||||
|
||||
|
@ -553,43 +690,81 @@ func (cp *cwtchPeer) Shutdown() {
|
|||
cp.queue.Shutdown()
|
||||
}
|
||||
|
||||
func (cp *cwtchPeer) StoreMessage(onion string, messageTxt string, sent time.Time) {
|
||||
if cp.GetContact(onion) == nil {
|
||||
cp.AddContact(onion, onion, model.AuthUnknown)
|
||||
}
|
||||
cp.mutex.Lock()
|
||||
cp.Profile.AddMessageToContactTimeline(onion, messageTxt, sent)
|
||||
cp.mutex.Unlock()
|
||||
}
|
||||
|
||||
// eventHandler process events from other subsystems
|
||||
func (cp *cwtchPeer) eventHandler() {
|
||||
for {
|
||||
ev := cp.queue.Next()
|
||||
switch ev.EventType {
|
||||
/***** Default auto handled events *****/
|
||||
|
||||
case event.ProtocolEngineStopped:
|
||||
cp.mutex.Lock()
|
||||
cp.listenStatus = false
|
||||
log.Infof("Protocol engine for %v has stopped listening", cp.Profile.Onion)
|
||||
cp.mutex.Unlock()
|
||||
case event.EncryptedGroupMessage:
|
||||
// If successful, a side effect is the message is added to the group's timeline
|
||||
|
||||
ciphertext, _ := base64.StdEncoding.DecodeString(ev.Data[event.Ciphertext])
|
||||
signature, _ := base64.StdEncoding.DecodeString(ev.Data[event.Signature])
|
||||
|
||||
// SECURITY NOTE: A malicious server could insert posts such that everyone always has a different lastKnownSignature
|
||||
// However the server can always replace **all** messages in an attempt to track users
|
||||
// This is mitigated somewhat by resync events which do wipe things entire.
|
||||
// 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).
|
||||
cp.SetContactAttribute(ev.Data[event.GroupServer], lastKnownSignature, ev.Data[event.Signature])
|
||||
|
||||
cp.mutex.Lock()
|
||||
ok, groupID, message, seen := cp.Profile.AttemptDecryption([]byte(ev.Data[event.Ciphertext]), []byte(ev.Data[event.Signature]))
|
||||
ok, groupID, message, seen := cp.Profile.AttemptDecryption(ciphertext, signature)
|
||||
cp.mutex.Unlock()
|
||||
if ok && !seen {
|
||||
cp.eventBus.Publish(event.NewEvent(event.NewMessageFromGroup, map[event.Field]string{event.TimestampReceived: message.Received.Format(time.RFC3339Nano), event.TimestampSent: message.Timestamp.Format(time.RFC3339Nano), event.Data: message.Message, event.GroupID: groupID, event.Signature: string(message.Signature), event.PreviousSignature: string(message.PreviousMessageSig), event.RemotePeer: message.PeerID}))
|
||||
cp.eventBus.Publish(event.NewEvent(event.NewMessageFromGroup, map[event.Field]string{event.TimestampReceived: message.Received.Format(time.RFC3339Nano), event.TimestampSent: message.Timestamp.Format(time.RFC3339Nano), event.Data: message.Message, event.GroupID: groupID, event.Signature: base64.StdEncoding.EncodeToString(message.Signature), event.PreviousSignature: base64.StdEncoding.EncodeToString(message.PreviousMessageSig), event.RemotePeer: message.PeerID}))
|
||||
}
|
||||
|
||||
// The group has been compromised
|
||||
if !ok && groupID != "" {
|
||||
if cp.Profile.GetGroup(groupID).IsCompromised {
|
||||
cp.eventBus.Publish(event.NewEvent(event.GroupCompromised, map[event.Field]string{event.GroupID: groupID}))
|
||||
}
|
||||
}
|
||||
case event.NewMessageFromPeer: //event.TimestampReceived, event.RemotePeer, event.Data
|
||||
ts, _ := time.Parse(time.RFC3339Nano, ev.Data[event.TimestampReceived])
|
||||
cp.mutex.Lock()
|
||||
cp.Profile.AddMessageToContactTimeline(ev.Data[event.RemotePeer], ev.Data[event.Data], ts)
|
||||
cp.mutex.Unlock()
|
||||
cp.StoreMessage(ev.Data[event.RemotePeer], ev.Data[event.Data], ts)
|
||||
|
||||
case event.PeerAcknowledgement:
|
||||
cp.mutex.Lock()
|
||||
cp.Profile.AckSentMessageToPeer(ev.Data[event.RemotePeer], ev.Data[event.EventID])
|
||||
idx := cp.Profile.AckSentMessageToPeer(ev.Data[event.RemotePeer], ev.Data[event.EventID])
|
||||
edata := ev.Data
|
||||
edata[event.Index] = strconv.Itoa(idx)
|
||||
cp.eventBus.Publish(event.NewEvent(event.IndexedAcknowledgement, edata))
|
||||
cp.mutex.Unlock()
|
||||
|
||||
case event.SendMessageToGroupError:
|
||||
cp.mutex.Lock()
|
||||
cp.Profile.AddGroupSentMessageError(ev.Data[event.GroupServer], ev.Data[event.Signature], ev.Data[event.Error])
|
||||
signature, _ := base64.StdEncoding.DecodeString(ev.Data[event.Signature])
|
||||
cp.Profile.AddGroupSentMessageError(ev.Data[event.GroupID], signature, ev.Data[event.Error])
|
||||
cp.mutex.Unlock()
|
||||
|
||||
case event.SendMessageToPeerError:
|
||||
cp.mutex.Lock()
|
||||
cp.Profile.ErrorSentMessageToPeer(ev.Data[event.RemotePeer], ev.Data[event.EventID], ev.Data[event.Error])
|
||||
idx := cp.Profile.ErrorSentMessageToPeer(ev.Data[event.RemotePeer], ev.Data[event.EventID], ev.Data[event.Error])
|
||||
edata := ev.Data
|
||||
edata[event.Index] = strconv.Itoa(idx)
|
||||
cp.eventBus.Publish(event.NewEvent(event.IndexedFailure, edata))
|
||||
cp.mutex.Unlock()
|
||||
|
||||
case event.RetryServerRequest:
|
||||
// Automated Join Server Request triggered by a plugin.
|
||||
log.Debugf("profile received an automated retry event for %v", ev.Data[event.GroupServer])
|
||||
cp.JoinServer(ev.Data[event.GroupServer])
|
||||
case event.NewGetValMessageFromPeer:
|
||||
onion := ev.Data[event.RemotePeer]
|
||||
scope := ev.Data[event.Scope]
|
||||
|
@ -597,18 +772,21 @@ func (cp *cwtchPeer) eventHandler() {
|
|||
|
||||
log.Debugf("NewGetValMessageFromPeer for %v%v from %v\n", scope, path, onion)
|
||||
|
||||
if scope == attr.PublicScope {
|
||||
val, exists := cp.GetAttribute(attr.GetPublicScope(path))
|
||||
resp := event.NewEvent(event.SendRetValMessageToPeer, map[event.Field]string{event.RemotePeer: onion, event.Exists: strconv.FormatBool(exists)})
|
||||
resp.EventID = ev.EventID
|
||||
if exists {
|
||||
resp.Data[event.Data] = val
|
||||
} else {
|
||||
resp.Data[event.Data] = ""
|
||||
}
|
||||
log.Debugf("Responding with SendRetValMessageToPeer exists:%v data: %v\n", exists, val)
|
||||
remotePeer := cp.GetContact(onion)
|
||||
if remotePeer != nil && remotePeer.Authorization == model.AuthApproved {
|
||||
if scope == attr.PublicScope {
|
||||
val, exists := cp.GetAttribute(attr.GetPublicScope(path))
|
||||
resp := event.NewEvent(event.SendRetValMessageToPeer, map[event.Field]string{event.RemotePeer: onion, event.Exists: strconv.FormatBool(exists)})
|
||||
resp.EventID = ev.EventID
|
||||
if exists {
|
||||
resp.Data[event.Data] = val
|
||||
} else {
|
||||
resp.Data[event.Data] = ""
|
||||
}
|
||||
log.Debugf("Responding with SendRetValMessageToPeer exists:%v data: %v\n", exists, val)
|
||||
|
||||
cp.eventBus.Publish(resp)
|
||||
cp.eventBus.Publish(resp)
|
||||
}
|
||||
}
|
||||
|
||||
/***** Non default but requestable handlable events *****/
|
||||
|
@ -625,21 +803,6 @@ func (cp *cwtchPeer) eventHandler() {
|
|||
cp.SetContactAttribute(onion, attr.GetPeerScope(path), val)
|
||||
}
|
||||
}
|
||||
case event.NewGroupInvite:
|
||||
cp.mutex.Lock()
|
||||
group, err := cp.Profile.ProcessInvite(ev.Data[event.GroupInvite], ev.Data[event.RemotePeer])
|
||||
if err == nil {
|
||||
if ev.Data[event.Imported] == "true" {
|
||||
cp.Profile.GetGroup(group).Accepted = true
|
||||
cp.mutex.Unlock() // TODO...seriously need a better way of handling these cases
|
||||
err = cp.JoinServer(cp.Profile.GetGroup(group).GroupServer)
|
||||
cp.mutex.Lock()
|
||||
if err != nil {
|
||||
log.Errorf("Joining Server should have worked %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
cp.mutex.Unlock()
|
||||
case event.PeerStateChange:
|
||||
cp.mutex.Lock()
|
||||
if _, exists := cp.Profile.Contacts[ev.Data[event.RemotePeer]]; exists {
|
||||
|
@ -648,12 +811,17 @@ func (cp *cwtchPeer) eventHandler() {
|
|||
cp.mutex.Unlock()
|
||||
case event.ServerStateChange:
|
||||
cp.mutex.Lock()
|
||||
// We update both the server contact status, as well as the groups the server belongs to
|
||||
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()
|
||||
|
||||
default:
|
||||
if ev.EventType != "" {
|
||||
log.Errorf("peer event handler received an event it was not subscribed for: %v", ev.EventType)
|
||||
|
|
|
@ -1,79 +0,0 @@
|
|||
package peer
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TODO: Rewrite these tests (and others) using the news event bus interface.
|
||||
func TestCwtchPeerGenerate(t *testing.T) {
|
||||
/**
|
||||
alice := NewCwtchPeer("alice")
|
||||
|
||||
groupID, _, _ := alice.StartGroup("test.server")
|
||||
exportedGroup, _ := alice.ExportGroup(groupID)
|
||||
t.Logf("Exported Group: %v from %v", exportedGroup, alice.GetProfile().Onion)
|
||||
|
||||
importedGroupID, err := alice.ImportGroup(exportedGroup)
|
||||
group := alice.GetGroup(importedGroupID)
|
||||
t.Logf("Imported Group: %v, err := %v %v", group, err, importedGroupID)
|
||||
*/
|
||||
}
|
||||
|
||||
func TestTrustPeer(t *testing.T) {
|
||||
/**
|
||||
groupName := "2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd"
|
||||
alice := NewCwtchPeer("alice")
|
||||
aem := new(event.Manager)
|
||||
aem.Initialize()
|
||||
alice.Init(connectivity.LocalProvider(),aem)
|
||||
defer alice.Shutdown()
|
||||
bob := NewCwtchPeer("bob")
|
||||
bem := new(event.Manager)
|
||||
bem.Initialize()
|
||||
bob.Init(connectivity.LocalProvider(), bem)
|
||||
defer bob.Shutdown()
|
||||
|
||||
bobOnion := bob.GetProfile().Onion
|
||||
aliceOnion := alice.GetProfile().Onion
|
||||
|
||||
groupID, _, err := alice.StartGroup(groupName)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
groupAlice := alice.GetGroup(groupID)
|
||||
if groupAlice.GroupID != groupID {
|
||||
t.Errorf("Alice should be part of group %v, got %v instead", groupID, groupAlice)
|
||||
}
|
||||
|
||||
exportedGroup, err := alice.ExportGroup(groupID)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
err = alice.InviteOnionToGroup(bobOnion, groupID)
|
||||
if err == nil {
|
||||
t.Errorf("onion invitation should fail since alice does no trust bob")
|
||||
}
|
||||
|
||||
err = alice.TrustPeer(bobOnion)
|
||||
if err == nil {
|
||||
t.Errorf("trust peer should fail since alice does not know about bob")
|
||||
}
|
||||
|
||||
// bob adds alice contact by importing serialized group created by alice
|
||||
_, err = bob.ImportGroup(exportedGroup)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
err = bob.TrustPeer(aliceOnion)
|
||||
if err != nil {
|
||||
t.Errorf("bob must be able to trust alice, got %v", err)
|
||||
}
|
||||
|
||||
err = bob.InviteOnionToGroup(aliceOnion, groupID)
|
||||
if err == nil {
|
||||
t.Errorf("bob trusts alice but peer connection is not ready yet. should not be able to invite her to group, instead got: %v", err)
|
||||
}
|
||||
*/
|
||||
}
|
|
@ -4,12 +4,12 @@ import (
|
|||
"cwtch.im/cwtch/event"
|
||||
"cwtch.im/cwtch/model"
|
||||
"cwtch.im/cwtch/protocol/groups"
|
||||
"cwtch.im/tapir"
|
||||
"cwtch.im/tapir/networks/tor"
|
||||
"cwtch.im/tapir/primitives"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"git.openprivacy.ca/cwtch.im/tapir"
|
||||
"git.openprivacy.ca/cwtch.im/tapir/networks/tor"
|
||||
"git.openprivacy.ca/cwtch.im/tapir/primitives"
|
||||
"git.openprivacy.ca/openprivacy/connectivity"
|
||||
torProvider "git.openprivacy.ca/openprivacy/connectivity/tor"
|
||||
"git.openprivacy.ca/openprivacy/log"
|
||||
|
@ -27,9 +27,6 @@ type engine struct {
|
|||
identity primitives.Identity
|
||||
acn connectivity.ACN
|
||||
|
||||
// Engine State
|
||||
started bool
|
||||
|
||||
// Authorization list of contacts to authorization status
|
||||
authorizations sync.Map // string(onion) => model.Authorization
|
||||
|
||||
|
@ -53,6 +50,8 @@ type engine struct {
|
|||
|
||||
// Engine (ProtocolEngine) encapsulates the logic necessary to make and receive Cwtch connections.
|
||||
// Note: ProtocolEngine doesn't have access to any information necessary to encrypt or decrypt GroupMessages
|
||||
// Protocol Engine *can* associate Group Identifiers with Group Servers, although we don't currently make use of this fact
|
||||
// other than to route errors back to the UI.
|
||||
type Engine interface {
|
||||
ACN() connectivity.ACN
|
||||
EventManager() event.Manager
|
||||
|
@ -80,6 +79,7 @@ func NewProtocolEngine(identity primitives.Identity, privateKey ed25519.PrivateK
|
|||
engine.eventManager.Subscribe(event.RetryPeerRequest, engine.queue)
|
||||
engine.eventManager.Subscribe(event.InvitePeerToGroup, engine.queue)
|
||||
engine.eventManager.Subscribe(event.JoinServer, engine.queue)
|
||||
engine.eventManager.Subscribe(event.LeaveServer, engine.queue)
|
||||
engine.eventManager.Subscribe(event.SendMessageToGroup, engine.queue)
|
||||
engine.eventManager.Subscribe(event.SendMessageToPeer, engine.queue)
|
||||
engine.eventManager.Subscribe(event.SendGetValMessageToPeer, engine.queue)
|
||||
|
@ -126,7 +126,14 @@ func (e *engine) eventHandler() {
|
|||
case event.InvitePeerToGroup:
|
||||
e.sendMessageToPeer(ev.EventID, ev.Data[event.RemotePeer], event.ContextInvite, []byte(ev.Data[event.GroupInvite]))
|
||||
case event.JoinServer:
|
||||
e.peerWithTokenServer(ev.Data[event.GroupServer], ev.Data[event.ServerTokenOnion], ev.Data[event.ServerTokenY])
|
||||
signature, err := base64.StdEncoding.DecodeString(ev.Data[event.Signature])
|
||||
if err != nil {
|
||||
// will result in a full sync
|
||||
signature = []byte{}
|
||||
}
|
||||
go e.peerWithTokenServer(ev.Data[event.GroupServer], ev.Data[event.ServerTokenOnion], ev.Data[event.ServerTokenY], signature)
|
||||
case event.LeaveServer:
|
||||
e.leaveServer(ev.Data[event.GroupServer])
|
||||
case event.DeleteContact:
|
||||
onion := ev.Data[event.RemotePeer]
|
||||
// We remove this peer from out blocklist which will prevent them from contacting us if we have "block unknown peers" turned on.
|
||||
|
@ -135,10 +142,9 @@ func (e *engine) eventHandler() {
|
|||
case event.DeleteGroup:
|
||||
// TODO: There isn't a way here to determine if other Groups are using a server connection...
|
||||
case event.SendMessageToGroup:
|
||||
err := e.sendMessageToGroup(ev.Data[event.GroupServer], []byte(ev.Data[event.Ciphertext]), []byte(ev.Data[event.Signature]))
|
||||
if err != nil {
|
||||
e.eventManager.Publish(event.NewEvent(event.SendMessageToGroupError, map[event.Field]string{event.GroupServer: ev.Data[event.GroupServer], event.EventID: ev.EventID, event.Error: err.Error()}))
|
||||
}
|
||||
ciphertext, _ := base64.StdEncoding.DecodeString(ev.Data[event.Ciphertext])
|
||||
signature, _ := base64.StdEncoding.DecodeString(ev.Data[event.Signature])
|
||||
go e.sendMessageToGroup(ev.Data[event.GroupID], ev.Data[event.GroupServer], ciphertext, signature)
|
||||
case event.SendMessageToPeer:
|
||||
// TODO: remove this passthrough once the UI is integrated.
|
||||
context, ok := ev.Data[event.EventContext]
|
||||
|
@ -167,8 +173,10 @@ func (e *engine) eventHandler() {
|
|||
e.peerDisconnected(ev.Data[event.RemotePeer])
|
||||
}
|
||||
case event.AllowUnknownPeers:
|
||||
log.Debugf("%v now allows unknown connections", e.identity.Hostname())
|
||||
e.blockUnknownContacts = false
|
||||
case event.BlockUnknownPeers:
|
||||
log.Debugf("%v now forbids unknown connections", e.identity.Hostname())
|
||||
e.blockUnknownContacts = true
|
||||
case event.ProtocolEngineStartListen:
|
||||
go e.listenFn()
|
||||
|
@ -187,18 +195,22 @@ func (e *engine) isBlocked(onion string) bool {
|
|||
return authorization.(model.Authorization) == model.AuthBlocked
|
||||
}
|
||||
|
||||
func (e *engine) isApproved(onion string) bool {
|
||||
func (e *engine) isAllowed(onion string) bool {
|
||||
authorization, known := e.authorizations.Load(onion)
|
||||
if !known {
|
||||
log.Errorf("attempted to lookup authorization of onion not in map...that should never happen")
|
||||
return false
|
||||
}
|
||||
return authorization.(model.Authorization) == model.AuthApproved
|
||||
if e.blockUnknownContacts {
|
||||
return authorization.(model.Authorization) == model.AuthApproved
|
||||
}
|
||||
return authorization.(model.Authorization) != model.AuthBlocked
|
||||
}
|
||||
|
||||
func (e *engine) createPeerTemplate() *PeerApp {
|
||||
peerAppTemplate := new(PeerApp)
|
||||
peerAppTemplate.IsBlocked = e.isBlocked
|
||||
peerAppTemplate.IsApproved = e.isApproved
|
||||
peerAppTemplate.IsAllowed = e.isAllowed
|
||||
peerAppTemplate.MessageHandler = e.handlePeerMessage
|
||||
peerAppTemplate.OnAcknowledgement = e.ignoreOnShutdown2(e.peerAck)
|
||||
peerAppTemplate.OnAuth = e.ignoreOnShutdown(e.peerAuthed)
|
||||
|
@ -214,7 +226,6 @@ func (e *engine) listenFn() {
|
|||
if !e.shuttingDown {
|
||||
e.eventManager.Publish(event.NewEvent(event.ProtocolEngineStopped, map[event.Field]string{event.Identity: e.identity.Hostname(), event.Error: err.Error()}))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Shutdown tears down the eventHandler goroutine
|
||||
|
@ -253,10 +264,26 @@ func (e *engine) peerWithOnion(onion string) {
|
|||
|
||||
// peerWithTokenServer is the entry point for cwtchPeer - server relationships
|
||||
// needs to be run in a goroutine as will block on Open.
|
||||
func (e *engine) peerWithTokenServer(onion string, tokenServerOnion string, tokenServerY string) {
|
||||
func (e *engine) peerWithTokenServer(onion string, tokenServerOnion string, tokenServerY string, lastKnownSignature []byte) {
|
||||
|
||||
service, exists := e.ephemeralServices.Load(onion)
|
||||
if exists {
|
||||
connection := service.(*tor.BaseOnionService)
|
||||
if conn, err := connection.GetConnection(onion); err == nil {
|
||||
// We are already peered and synced so return...
|
||||
// This will only not-trigger it lastKnownSignature has been wiped, which only happens when ResyncServer is called
|
||||
// in CwtchPeer.
|
||||
if !conn.IsClosed() && len(lastKnownSignature) != 0 {
|
||||
return
|
||||
}
|
||||
// Otherwise...we are going to rebuild the connection(which will result in a bandwidth heavy resync)...
|
||||
e.leaveServer(onion)
|
||||
}
|
||||
// Otherwise...let's reconnect
|
||||
}
|
||||
|
||||
log.Debugf("Peering with Token Server %v %v", onion, tokenServerOnion)
|
||||
e.ignoreOnShutdown(e.serverConnecting)(onion)
|
||||
|
||||
// Create a new ephemeral service for this connection
|
||||
ephemeralService := new(tor.BaseOnionService)
|
||||
eid, epk := primitives.InitializeEphemeralIdentity()
|
||||
|
@ -264,7 +291,7 @@ func (e *engine) peerWithTokenServer(onion string, tokenServerOnion string, toke
|
|||
|
||||
Y := ristretto255.NewElement()
|
||||
Y.UnmarshalText([]byte(tokenServerY))
|
||||
connected, err := ephemeralService.Connect(onion, NewTokenBoardClient(e.acn, Y, tokenServerOnion, e.receiveGroupMessage, e.serverSynced))
|
||||
connected, err := ephemeralService.Connect(onion, NewTokenBoardClient(e.acn, Y, tokenServerOnion, lastKnownSignature, e.receiveGroupMessage, e.serverSynced, e.serverDisconnected))
|
||||
e.ephemeralServices.Store(onion, ephemeralService)
|
||||
// If we are already connected...check if we are authed and issue an auth event
|
||||
// (This allows the ui to be stateless)
|
||||
|
@ -333,7 +360,6 @@ func (e *engine) serverConnected(onion string) {
|
|||
}
|
||||
|
||||
func (e *engine) serverSynced(onion string) {
|
||||
log.Debugf("SERVER SYNCED: %v", onion)
|
||||
e.eventManager.Publish(event.NewEvent(event.ServerStateChange, map[event.Field]string{
|
||||
event.GroupServer: onion,
|
||||
event.ConnectionState: ConnectionStateName[SYNCED],
|
||||
|
@ -407,42 +433,48 @@ func (e *engine) deleteConnection(id string) {
|
|||
func (e *engine) receiveGroupMessage(server string, gm *groups.EncryptedGroupMessage) {
|
||||
// Publish Event so that a Profile Engine can deal with it.
|
||||
// Note: This technically means that *multiple* Profile Engines could listen to the same ProtocolEngine!
|
||||
e.eventManager.Publish(event.NewEvent(event.EncryptedGroupMessage, map[event.Field]string{event.Ciphertext: string(gm.Ciphertext), event.Signature: string(gm.Signature)}))
|
||||
e.eventManager.Publish(event.NewEvent(event.EncryptedGroupMessage, map[event.Field]string{event.GroupServer: server, event.Ciphertext: base64.StdEncoding.EncodeToString(gm.Ciphertext), event.Signature: base64.StdEncoding.EncodeToString(gm.Signature)}))
|
||||
}
|
||||
|
||||
// sendMessageToGroup attempts to sent the given message to the given group id.
|
||||
func (e *engine) sendMessageToGroup(server string, ct []byte, sig []byte) error {
|
||||
func (e *engine) sendMessageToGroup(groupID string, server string, ct []byte, sig []byte) {
|
||||
|
||||
es, ok := e.ephemeralServices.Load(server)
|
||||
if !ok {
|
||||
return fmt.Errorf("no service exists for group %v", server)
|
||||
if es == nil || !ok {
|
||||
e.eventManager.Publish(event.NewEvent(event.SendMessageToGroupError, map[event.Field]string{event.GroupID: groupID, event.GroupServer: server, event.Error: "server-not-found", event.Signature: base64.StdEncoding.EncodeToString(sig)}))
|
||||
return
|
||||
}
|
||||
ephemeralService := es.(tapir.Service)
|
||||
|
||||
conn, err := ephemeralService.WaitForCapabilityOrClose(server, groups.CwtchServerSyncedCapability)
|
||||
if err == nil {
|
||||
tokenApp, ok := (conn.App()).(*TokenBoardClient)
|
||||
if ok {
|
||||
attempts := 0
|
||||
for tokenApp.Post(ct, sig) == false {
|
||||
// TODO This should eventually be wired back into the UI to allow it to error
|
||||
tokenApp.MakePayment()
|
||||
time.Sleep(time.Second * 5)
|
||||
attempts++
|
||||
if attempts == 5 {
|
||||
return errors.New("failed to post to token board")
|
||||
if spent, numtokens := tokenApp.Post(ct, sig); !spent {
|
||||
// TODO: while this works for the spam guard, it won't work for other forms of payment...
|
||||
// Make an -inline- payment, this will hold the goroutine
|
||||
if err := tokenApp.MakePayment(); err == nil {
|
||||
// This really shouldn't fail since we now know we have the required tokens...
|
||||
if spent, _ := tokenApp.Post(ct, sig); !spent {
|
||||
e.eventManager.Publish(event.NewEvent(event.SendMessageToGroupError, map[event.Field]string{event.GroupID: groupID, event.GroupServer: server, event.Error: err.Error(), event.Signature: base64.StdEncoding.EncodeToString(sig)}))
|
||||
}
|
||||
} else {
|
||||
// Broadast the token error
|
||||
e.eventManager.Publish(event.NewEvent(event.SendMessageToGroupError, map[event.Field]string{event.GroupID: groupID, event.GroupServer: server, event.Error: err.Error(), event.Signature: base64.StdEncoding.EncodeToString(sig)}))
|
||||
}
|
||||
} else if numtokens < 5 {
|
||||
go tokenApp.MakePayment()
|
||||
}
|
||||
return nil
|
||||
// regardless we return....
|
||||
return
|
||||
}
|
||||
return errors.New("failed type assertion conn.App != TokenBoardClientApp")
|
||||
}
|
||||
return err
|
||||
e.eventManager.Publish(event.NewEvent(event.SendMessageToGroupError, map[event.Field]string{event.GroupID: groupID, event.GroupServer: server, event.Error: "server-connection-not-valid", event.Signature: base64.StdEncoding.EncodeToString(sig)}))
|
||||
}
|
||||
|
||||
func (e *engine) handlePeerMessage(hostname string, eventID string, context string, message []byte) {
|
||||
log.Debugf("New message from peer: %v %v", hostname, context)
|
||||
if context == event.ContextInvite {
|
||||
e.eventManager.Publish(event.NewEvent(event.NewGroupInvite, map[event.Field]string{event.TimestampReceived: time.Now().Format(time.RFC3339Nano), event.RemotePeer: hostname, event.GroupInvite: string(message)}))
|
||||
} else if context == event.ContextGetVal {
|
||||
if context == event.ContextGetVal {
|
||||
var getVal peerGetVal
|
||||
err := json.Unmarshal(message, &getVal)
|
||||
if err == nil {
|
||||
|
@ -472,3 +504,12 @@ func (e *engine) handlePeerRetVal(hostname string, getValData, retValData []byte
|
|||
|
||||
e.eventManager.Publish(event.NewEventList(event.NewRetValMessageFromPeer, event.RemotePeer, hostname, event.Scope, getVal.Scope, event.Path, getVal.Path, event.Exists, strconv.FormatBool(retVal.Exists), event.Data, retVal.Val))
|
||||
}
|
||||
|
||||
func (e *engine) leaveServer(server string) {
|
||||
es, ok := e.ephemeralServices.Load(server)
|
||||
if ok {
|
||||
ephemeralService := es.(tapir.Service)
|
||||
ephemeralService.Shutdown()
|
||||
e.ephemeralServices.Delete(server)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,9 +2,9 @@ package connections
|
|||
|
||||
import (
|
||||
"cwtch.im/cwtch/event"
|
||||
"cwtch.im/tapir"
|
||||
"cwtch.im/tapir/applications"
|
||||
"encoding/json"
|
||||
"git.openprivacy.ca/cwtch.im/tapir"
|
||||
"git.openprivacy.ca/cwtch.im/tapir/applications"
|
||||
"git.openprivacy.ca/openprivacy/log"
|
||||
"sync"
|
||||
)
|
||||
|
@ -18,7 +18,7 @@ type PeerApp struct {
|
|||
MessageHandler func(string, string, string, []byte)
|
||||
RetValHandler func(string, []byte, []byte)
|
||||
IsBlocked func(string) bool
|
||||
IsApproved func(string) bool
|
||||
IsAllowed func(string) bool
|
||||
OnAcknowledgement func(string, string)
|
||||
OnAuth func(string)
|
||||
OnClose func(string)
|
||||
|
@ -48,7 +48,7 @@ func (pa *PeerApp) NewInstance() tapir.Application {
|
|||
newApp := new(PeerApp)
|
||||
newApp.MessageHandler = pa.MessageHandler
|
||||
newApp.IsBlocked = pa.IsBlocked
|
||||
newApp.IsApproved = pa.IsApproved
|
||||
newApp.IsAllowed = pa.IsAllowed
|
||||
newApp.OnAcknowledgement = pa.OnAcknowledgement
|
||||
newApp.OnAuth = pa.OnAuth
|
||||
newApp.OnClose = pa.OnClose
|
||||
|
@ -75,7 +75,8 @@ func (pa *PeerApp) Init(connection tapir.Connection) {
|
|||
go pa.listen()
|
||||
}
|
||||
} else {
|
||||
pa.OnClose(connection.Hostname())
|
||||
// The auth protocol wasn't completed, we can safely shutdown the connection
|
||||
connection.Close()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -101,7 +102,7 @@ func (pa *PeerApp) listen() {
|
|||
pa.getValRequests.Delete(peerMessage.ID)
|
||||
}
|
||||
default:
|
||||
if pa.IsApproved(pa.connection.Hostname()) {
|
||||
if pa.IsAllowed(pa.connection.Hostname()) {
|
||||
pa.MessageHandler(pa.connection.Hostname(), peerMessage.ID, peerMessage.Context, peerMessage.Data)
|
||||
|
||||
// Acknowledge the message
|
||||
|
|
|
@ -22,8 +22,10 @@ const (
|
|||
var (
|
||||
// ConnectionStateName allows conversion of states to their string representations
|
||||
ConnectionStateName = []string{"Disconnected", "Connecting", "Connected", "Authenticated", "Synced", "Failed", "Killed"}
|
||||
|
||||
// ConnectionStateToType allows conversion of strings to their state type
|
||||
ConnectionStateToType = map[string]ConnectionState{"Disconnected": DISCONNECTED, "Connecting": CONNECTING,
|
||||
"Connected": CONNECTED, "Authenticated": AUTHENTICATED, "Synced": SYNCED, "Failed": FAILED, "Killed": KILLED}
|
||||
)
|
||||
|
||||
// ConnectionStateToType allows conversion of strings to their state type
|
||||
func ConnectionStateToType() map[string]ConnectionState {
|
||||
return map[string]ConnectionState{"Disconnected": DISCONNECTED, "Connecting": CONNECTING,
|
||||
"Connected": CONNECTED, "Authenticated": AUTHENTICATED, "Synced": SYNCED, "Failed": FAILED, "Killed": KILLED}
|
||||
}
|
||||
|
|
|
@ -2,20 +2,21 @@ package connections
|
|||
|
||||
import (
|
||||
"cwtch.im/cwtch/protocol/groups"
|
||||
"cwtch.im/tapir"
|
||||
"cwtch.im/tapir/applications"
|
||||
"cwtch.im/tapir/networks/tor"
|
||||
"cwtch.im/tapir/primitives"
|
||||
"cwtch.im/tapir/primitives/privacypass"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"git.openprivacy.ca/cwtch.im/tapir"
|
||||
"git.openprivacy.ca/cwtch.im/tapir/applications"
|
||||
"git.openprivacy.ca/cwtch.im/tapir/networks/tor"
|
||||
"git.openprivacy.ca/cwtch.im/tapir/primitives"
|
||||
"git.openprivacy.ca/cwtch.im/tapir/primitives/privacypass"
|
||||
"git.openprivacy.ca/openprivacy/connectivity"
|
||||
"git.openprivacy.ca/openprivacy/log"
|
||||
"github.com/gtank/ristretto255"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// NewTokenBoardClient generates a new Client for Token Board
|
||||
func NewTokenBoardClient(acn connectivity.ACN, Y *ristretto255.Element, tokenServiceOnion string, groupMessageHandler func(server string, gm *groups.EncryptedGroupMessage), serverSyncedHandler func(server string)) tapir.Application {
|
||||
func NewTokenBoardClient(acn connectivity.ACN, Y *ristretto255.Element, tokenServiceOnion string, lastKnownSignature []byte, groupMessageHandler func(server string, gm *groups.EncryptedGroupMessage), serverSyncedHandler func(server string), serverClosedHandler func(server string)) tapir.Application {
|
||||
tba := new(TokenBoardClient)
|
||||
tba.acn = acn
|
||||
tba.tokenService = privacypass.NewTokenServer()
|
||||
|
@ -23,6 +24,8 @@ func NewTokenBoardClient(acn connectivity.ACN, Y *ristretto255.Element, tokenSer
|
|||
tba.tokenServiceOnion = tokenServiceOnion
|
||||
tba.receiveGroupMessageHandler = groupMessageHandler
|
||||
tba.serverSyncedHandler = serverSyncedHandler
|
||||
tba.serverClosedHandler = serverClosedHandler
|
||||
tba.lastKnownSignature = lastKnownSignature
|
||||
return tba
|
||||
}
|
||||
|
||||
|
@ -32,22 +35,27 @@ type TokenBoardClient struct {
|
|||
connection tapir.Connection
|
||||
receiveGroupMessageHandler func(server string, gm *groups.EncryptedGroupMessage)
|
||||
serverSyncedHandler func(server string)
|
||||
serverClosedHandler func(server string)
|
||||
|
||||
// Token service handling
|
||||
acn connectivity.ACN
|
||||
tokens []*privacypass.Token
|
||||
tokenService *privacypass.TokenServer
|
||||
tokenServiceOnion string
|
||||
acn connectivity.ACN
|
||||
tokens []*privacypass.Token
|
||||
tokenLock sync.Mutex
|
||||
tokenService *privacypass.TokenServer
|
||||
tokenServiceOnion string
|
||||
lastKnownSignature []byte
|
||||
}
|
||||
|
||||
// NewInstance Client a new TokenBoardApp
|
||||
func (ta *TokenBoardClient) NewInstance() tapir.Application {
|
||||
tba := new(TokenBoardClient)
|
||||
tba.serverSyncedHandler = ta.serverSyncedHandler
|
||||
tba.serverClosedHandler = ta.serverClosedHandler
|
||||
tba.receiveGroupMessageHandler = ta.receiveGroupMessageHandler
|
||||
tba.acn = ta.acn
|
||||
tba.tokenService = ta.tokenService
|
||||
tba.tokenServiceOnion = ta.tokenServiceOnion
|
||||
tba.lastKnownSignature = ta.lastKnownSignature
|
||||
return tba
|
||||
}
|
||||
|
||||
|
@ -59,6 +67,9 @@ func (ta *TokenBoardClient) Init(connection tapir.Connection) {
|
|||
ta.connection.SetCapability(groups.CwtchServerSyncedCapability)
|
||||
log.Debugf("Successfully Initialized Connection")
|
||||
go ta.Listen()
|
||||
// Optimistically acquire many tokens for this server...
|
||||
go ta.MakePayment()
|
||||
go ta.MakePayment()
|
||||
ta.Replay()
|
||||
} else {
|
||||
connection.Close()
|
||||
|
@ -72,14 +83,15 @@ func (ta *TokenBoardClient) Listen() {
|
|||
data := ta.connection.Expect()
|
||||
if len(data) == 0 {
|
||||
log.Debugf("Server closed the connection...")
|
||||
ta.serverClosedHandler(ta.connection.Hostname())
|
||||
return // connection is closed
|
||||
}
|
||||
|
||||
// We always expect the server to follow protocol, and the second it doesn't we close the connection
|
||||
// TODO issue an error so the client is aware
|
||||
var message groups.Message
|
||||
if err := json.Unmarshal(data, &message); err != nil {
|
||||
log.Debugf("Server sent an unexpected message, closing the connection: %v", err)
|
||||
ta.serverClosedHandler(ta.connection.Hostname())
|
||||
ta.connection.Close()
|
||||
return
|
||||
}
|
||||
|
@ -89,8 +101,8 @@ func (ta *TokenBoardClient) Listen() {
|
|||
if message.NewMessage != nil {
|
||||
ta.receiveGroupMessageHandler(ta.connection.Hostname(), &message.NewMessage.EGM)
|
||||
} else {
|
||||
// TODO: Send this error to the UI
|
||||
log.Debugf("Server sent an unexpected NewMessage, closing the connection: %s", data)
|
||||
ta.serverClosedHandler(ta.connection.Hostname())
|
||||
ta.connection.Close()
|
||||
return
|
||||
}
|
||||
|
@ -101,12 +113,21 @@ func (ta *TokenBoardClient) Listen() {
|
|||
log.Debugf("Replaying %v Messages...", message.ReplayResult.NumMessages)
|
||||
for i := 0; i < message.ReplayResult.NumMessages; i++ {
|
||||
data := ta.connection.Expect()
|
||||
|
||||
if len(data) == 0 {
|
||||
log.Debugf("Server sent an unexpected EncryptedGroupMessage, closing the connection")
|
||||
ta.serverClosedHandler(ta.connection.Hostname())
|
||||
ta.connection.Close()
|
||||
return
|
||||
}
|
||||
|
||||
egm := &groups.EncryptedGroupMessage{}
|
||||
if err := json.Unmarshal(data, egm); err == nil {
|
||||
ta.receiveGroupMessageHandler(ta.connection.Hostname(), egm)
|
||||
ta.lastKnownSignature = egm.Signature
|
||||
} else {
|
||||
// TODO: Send this error to the UI
|
||||
log.Debugf("Server sent an unexpected EncryptedGroupMessage, closing the connection: %v", err)
|
||||
ta.serverClosedHandler(ta.connection.Hostname())
|
||||
ta.connection.Close()
|
||||
return
|
||||
}
|
||||
|
@ -119,8 +140,7 @@ func (ta *TokenBoardClient) Listen() {
|
|||
|
||||
// Replay posts a Replay Message to the server.
|
||||
func (ta *TokenBoardClient) Replay() {
|
||||
// TODO - Allow configurable ranges
|
||||
data, _ := json.Marshal(groups.Message{MessageType: groups.ReplayRequestMessage, ReplayRequest: &groups.ReplayRequest{LastCommit: []byte{}}})
|
||||
data, _ := json.Marshal(groups.Message{MessageType: groups.ReplayRequestMessage, ReplayRequest: &groups.ReplayRequest{LastCommit: ta.lastKnownSignature}})
|
||||
ta.connection.Send(data)
|
||||
}
|
||||
|
||||
|
@ -130,25 +150,24 @@ func (ta *TokenBoardClient) PurchaseTokens() {
|
|||
}
|
||||
|
||||
// Post sends a Post Request to the server
|
||||
func (ta *TokenBoardClient) Post(ct []byte, sig []byte) bool {
|
||||
func (ta *TokenBoardClient) Post(ct []byte, sig []byte) (bool, int) {
|
||||
egm := groups.EncryptedGroupMessage{Ciphertext: ct, Signature: sig}
|
||||
token, err := ta.NextToken(egm.ToBytes(), ta.connection.Hostname())
|
||||
token, numTokens, err := ta.NextToken(egm.ToBytes(), ta.connection.Hostname())
|
||||
if err == nil {
|
||||
data, _ := json.Marshal(groups.Message{MessageType: groups.PostRequestMessage, PostRequest: &groups.PostRequest{EGM: egm, Token: token}})
|
||||
log.Debugf("Message Length: %s %v", data, len(data))
|
||||
ta.connection.Send(data)
|
||||
return true
|
||||
return true, numTokens
|
||||
}
|
||||
log.Debugf("No Valid Tokens: %v", err)
|
||||
return false
|
||||
return false, numTokens
|
||||
}
|
||||
|
||||
// MakePayment uses the PoW based token protocol to obtain more tokens
|
||||
func (ta *TokenBoardClient) MakePayment() {
|
||||
func (ta *TokenBoardClient) MakePayment() error {
|
||||
log.Debugf("Making a Payment %v", ta)
|
||||
id, sk := primitives.InitializeEphemeralIdentity()
|
||||
var client tapir.Service
|
||||
client = new(tor.BaseOnionService)
|
||||
client := new(tor.BaseOnionService)
|
||||
client.Init(ta.acn, sk, &id)
|
||||
|
||||
tokenApplication := new(applications.TokenApplication)
|
||||
|
@ -157,23 +176,34 @@ func (ta *TokenBoardClient) MakePayment() {
|
|||
ChainApplication(new(applications.ProofOfWorkApplication), applications.SuccessfulProofOfWorkCapability).
|
||||
ChainApplication(tokenApplication, applications.HasTokensCapability)
|
||||
client.Connect(ta.tokenServiceOnion, powTokenApp)
|
||||
log.Debugf("Waiting for successful PoW Auth...")
|
||||
conn, err := client.WaitForCapabilityOrClose(ta.tokenServiceOnion, applications.HasTokensCapability)
|
||||
if err == nil {
|
||||
powtapp, _ := conn.App().(*applications.TokenApplication)
|
||||
// Update tokens...we need a lock here to prevent SpendToken from modifying the tokens
|
||||
// during this process..
|
||||
log.Debugf("Updating Tokens")
|
||||
ta.tokenLock.Lock()
|
||||
ta.tokens = append(ta.tokens, powtapp.Tokens...)
|
||||
ta.tokenLock.Unlock()
|
||||
log.Debugf("Transcript: %v", powtapp.Transcript().OutputTranscriptToAudit())
|
||||
conn.Close()
|
||||
return
|
||||
return nil
|
||||
}
|
||||
log.Debugf("Error making payment: to %v %v", ta.tokenServiceOnion, err)
|
||||
return err
|
||||
}
|
||||
|
||||
// NextToken retrieves the next token
|
||||
func (ta *TokenBoardClient) NextToken(data []byte, hostname string) (privacypass.SpentToken, error) {
|
||||
func (ta *TokenBoardClient) NextToken(data []byte, hostname string) (privacypass.SpentToken, int, error) {
|
||||
// Taken the first new token, we need a lock here because tokens can be appended by MakePayment
|
||||
// which could result in weird behaviour...
|
||||
ta.tokenLock.Lock()
|
||||
defer ta.tokenLock.Unlock()
|
||||
if len(ta.tokens) == 0 {
|
||||
return privacypass.SpentToken{}, errors.New("No more tokens")
|
||||
return privacypass.SpentToken{}, len(ta.tokens), errors.New("no more tokens")
|
||||
}
|
||||
token := ta.tokens[0]
|
||||
ta.tokens = ta.tokens[1:]
|
||||
return token.SpendToken(append(data, hostname...)), nil
|
||||
return token.SpendToken(append(data, hostname...)), len(ta.tokens), nil
|
||||
}
|
||||
|
|
|
@ -1,36 +0,0 @@
|
|||
package groups
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"cwtch.im/cwtch/model"
|
||||
"golang.org/x/crypto/nacl/secretbox"
|
||||
"io"
|
||||
)
|
||||
|
||||
// Fuzz various group related functions
|
||||
func Fuzz(data []byte) int {
|
||||
profile := model.GenerateNewProfile("fuzz")
|
||||
inviteid, err := profile.ProcessInvite(string(data), profile.Onion)
|
||||
|
||||
if err != nil {
|
||||
if inviteid != "" {
|
||||
panic("should not have added a group on err")
|
||||
}
|
||||
return 1
|
||||
}
|
||||
|
||||
id, _, _ := profile.StartGroup("2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd")
|
||||
var nonce [24]byte
|
||||
io.ReadFull(rand.Reader, nonce[:])
|
||||
encrypted := secretbox.Seal(nonce[:], data, &nonce, &profile.GetGroup(id).GroupKey)
|
||||
|
||||
ok, _, _, _ := profile.AttemptDecryption(encrypted, data)
|
||||
if ok {
|
||||
panic("this probably shouldn't happen")
|
||||
}
|
||||
ok = profile.VerifyGroupMessage(string(data), string(data), string(data), 0, encrypted, data)
|
||||
if ok {
|
||||
panic("this probably shouldn't happen")
|
||||
}
|
||||
return 0
|
||||
}
|
|
@ -1,20 +0,0 @@
|
|||
package invites
|
||||
|
||||
import (
|
||||
"cwtch.im/cwtch/event"
|
||||
"cwtch.im/cwtch/peer"
|
||||
)
|
||||
|
||||
// Fuzz import group function
|
||||
func Fuzz(data []byte) int {
|
||||
peer := peer.NewCwtchPeer("fuzz")
|
||||
peer.Init(event.NewEventManager())
|
||||
err := peer.ImportGroup(string(data))
|
||||
if err != nil {
|
||||
if len(peer.GetGroups()) > 0 {
|
||||
panic("group added despite error")
|
||||
}
|
||||
return 0
|
||||
}
|
||||
return 1
|
||||
}
|
|
@ -1,9 +1,9 @@
|
|||
package groups
|
||||
|
||||
import (
|
||||
"cwtch.im/tapir"
|
||||
"cwtch.im/tapir/primitives/privacypass"
|
||||
"encoding/json"
|
||||
"git.openprivacy.ca/cwtch.im/tapir"
|
||||
"git.openprivacy.ca/cwtch.im/tapir/primitives/privacypass"
|
||||
)
|
||||
|
||||
// CwtchServerSyncedCapability is used to indicate that a given cwtch server is synced
|
||||
|
@ -11,19 +11,23 @@ const CwtchServerSyncedCapability = tapir.Capability("CwtchServerSyncedCapabilit
|
|||
|
||||
// GroupInvite provides a structured type for communicating group information to peers
|
||||
type GroupInvite struct {
|
||||
GroupName string
|
||||
SignedGroupID []byte
|
||||
Timestamp uint64
|
||||
SharedKey []byte
|
||||
ServerHost string
|
||||
InitialMessage []byte
|
||||
GroupID string
|
||||
GroupName string
|
||||
SignedGroupID []byte
|
||||
Timestamp uint64
|
||||
SharedKey []byte
|
||||
ServerHost string
|
||||
}
|
||||
|
||||
// DecryptedGroupMessage is the main encapsulation of group message data
|
||||
type DecryptedGroupMessage struct {
|
||||
Text string
|
||||
Onion string
|
||||
Timestamp uint64
|
||||
Text string
|
||||
Onion string
|
||||
Timestamp uint64
|
||||
// NOTE: SignedGroupID is now a misnomer, the only way this is signed is indirectly via the signed encrypted group messages
|
||||
// We now treat GroupID as binding to a server/key rather than an "owner" - additional validation logic (to e.g.
|
||||
// respect particular group constitutions) can be built on top of group messages, but the underlying groups are
|
||||
// now agnostic to those models.
|
||||
SignedGroupID []byte
|
||||
PreviousMessageSig []byte
|
||||
Padding []byte
|
||||
|
|
|
@ -1,16 +1,17 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"cwtch.im/cwtch/model"
|
||||
cwtchserver "cwtch.im/cwtch/server"
|
||||
"cwtch.im/tapir/primitives"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"git.openprivacy.ca/cwtch.im/tapir/primitives"
|
||||
"git.openprivacy.ca/openprivacy/connectivity/tor"
|
||||
"git.openprivacy.ca/openprivacy/log"
|
||||
mrand "math/rand"
|
||||
"crypto/rand"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
|
@ -21,6 +22,7 @@ const (
|
|||
func main() {
|
||||
log.AddEverythingFromPattern("server/app/main")
|
||||
log.AddEverythingFromPattern("server/server")
|
||||
log.SetLevel(log.LevelDebug)
|
||||
configDir := os.Getenv("CWTCH_CONFIG_DIR")
|
||||
|
||||
if len(os.Args) == 2 && os.Args[1] == "gen1" {
|
||||
|
@ -45,7 +47,7 @@ func main() {
|
|||
|
||||
// we don't need real randomness for the port, just to avoid a possible conflict...
|
||||
mrand.Seed(int64(time.Now().Nanosecond()))
|
||||
controlPort := mrand.Intn(1000)+9052
|
||||
controlPort := mrand.Intn(1000) + 9052
|
||||
|
||||
// generate a random password
|
||||
key := make([]byte, 64)
|
||||
|
@ -54,9 +56,10 @@ func main() {
|
|||
panic(err)
|
||||
}
|
||||
|
||||
os.MkdirAll("tordir/tor",0700)
|
||||
os.MkdirAll("tordir/tor", 0700)
|
||||
tor.NewTorrc().WithHashedPassword(base64.StdEncoding.EncodeToString(key)).WithControlPort(controlPort).Build("./tordir/tor/torrc")
|
||||
acn, err := tor.NewTorACNWithAuth("tordir", "", controlPort, tor.HashedPasswordAuthenticator{Password: base64.StdEncoding.EncodeToString(key)})
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("\nError connecting to Tor: %v\n", err)
|
||||
os.Exit(1)
|
||||
|
@ -66,20 +69,32 @@ func main() {
|
|||
server := new(cwtchserver.Server)
|
||||
log.Infoln("starting cwtch server...")
|
||||
|
||||
// TODO load params from .cwtch/server.conf or command line flag
|
||||
// TODO: respond to HUP so t.Close is gracefully called
|
||||
server.Setup(serverConfig)
|
||||
|
||||
// TODO create a random group for testing
|
||||
group, _ := model.NewGroup(tor.GetTorV3Hostname(serverConfig.PublicKey))
|
||||
group.SignGroup([]byte{})
|
||||
invite, err := group.Invite([]byte{})
|
||||
invite, err := group.Invite()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
fmt.Printf("%v", "torv3"+base64.StdEncoding.EncodeToString(invite))
|
||||
|
||||
bundle := server.KeyBundle().Serialize()
|
||||
log.Infof("Server Config: server:%s", base64.StdEncoding.EncodeToString(bundle))
|
||||
|
||||
log.Infof("Server Tofu Bundle: tofubundle:server:%s||%s", base64.StdEncoding.EncodeToString(bundle), invite)
|
||||
|
||||
// Graceful Shutdown
|
||||
c := make(chan os.Signal, 1)
|
||||
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
|
||||
go func() {
|
||||
<-c
|
||||
acn.Close()
|
||||
server.Close()
|
||||
os.Exit(1)
|
||||
}()
|
||||
|
||||
server.Run(acn)
|
||||
for {
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ import (
|
|||
type counter struct {
|
||||
startTime time.Time
|
||||
count uint64
|
||||
total uint64
|
||||
}
|
||||
|
||||
// Counter providers a threadsafe counter to use for storing long running counts
|
||||
|
@ -25,7 +26,7 @@ type Counter interface {
|
|||
|
||||
// NewCounter initializes a counter starting at time.Now() and a count of 0 and returns it
|
||||
func NewCounter() Counter {
|
||||
c := &counter{startTime: time.Now(), count: 0}
|
||||
c := &counter{startTime: time.Now(), count: 0, total: 0}
|
||||
return c
|
||||
}
|
||||
|
||||
|
|
|
@ -2,8 +2,8 @@ package metrics
|
|||
|
||||
import (
|
||||
"bufio"
|
||||
"cwtch.im/tapir"
|
||||
"fmt"
|
||||
"git.openprivacy.ca/cwtch.im/tapir"
|
||||
"git.openprivacy.ca/openprivacy/log"
|
||||
"github.com/struCoder/pidusage"
|
||||
"os"
|
||||
|
@ -18,15 +18,16 @@ const (
|
|||
|
||||
// Monitors is a package of metrics for a Cwtch Server including message count, CPU, Mem, and conns
|
||||
type Monitors struct {
|
||||
MessageCounter Counter
|
||||
Messages MonitorHistory
|
||||
CPU MonitorHistory
|
||||
Memory MonitorHistory
|
||||
ClientConns MonitorHistory
|
||||
starttime time.Time
|
||||
breakChannel chan bool
|
||||
log bool
|
||||
configDir string
|
||||
MessageCounter Counter
|
||||
TotalMessageCounter Counter
|
||||
Messages MonitorHistory
|
||||
CPU MonitorHistory
|
||||
Memory MonitorHistory
|
||||
ClientConns MonitorHistory
|
||||
starttime time.Time
|
||||
breakChannel chan bool
|
||||
log bool
|
||||
configDir string
|
||||
}
|
||||
|
||||
// Start initializes a Monitors's monitors
|
||||
|
@ -36,7 +37,16 @@ func (mp *Monitors) Start(ts tapir.Service, configDir string, log bool) {
|
|||
mp.starttime = time.Now()
|
||||
mp.breakChannel = make(chan bool)
|
||||
mp.MessageCounter = NewCounter()
|
||||
mp.Messages = NewMonitorHistory(Count, Cumulative, func() (c float64) { c = float64(mp.MessageCounter.Count()); mp.MessageCounter.Reset(); return })
|
||||
|
||||
// Maintain a count of total messages
|
||||
mp.TotalMessageCounter = NewCounter()
|
||||
mp.Messages = NewMonitorHistory(Count, Cumulative, func() (c float64) {
|
||||
c = float64(mp.MessageCounter.Count())
|
||||
mp.TotalMessageCounter.Add(int(c))
|
||||
mp.MessageCounter.Reset()
|
||||
return
|
||||
})
|
||||
|
||||
var pidUsageLock sync.Mutex
|
||||
mp.CPU = NewMonitorHistory(Percent, Average, func() float64 {
|
||||
pidUsageLock.Lock()
|
||||
|
|
143
server/server.go
143
server/server.go
|
@ -5,51 +5,55 @@ import (
|
|||
"cwtch.im/cwtch/model"
|
||||
"cwtch.im/cwtch/server/metrics"
|
||||
"cwtch.im/cwtch/server/storage"
|
||||
"cwtch.im/tapir"
|
||||
"cwtch.im/tapir/applications"
|
||||
tor2 "cwtch.im/tapir/networks/tor"
|
||||
"cwtch.im/tapir/persistence"
|
||||
"cwtch.im/tapir/primitives"
|
||||
"cwtch.im/tapir/primitives/privacypass"
|
||||
"fmt"
|
||||
"git.openprivacy.ca/cwtch.im/tapir"
|
||||
"git.openprivacy.ca/cwtch.im/tapir/applications"
|
||||
tor2 "git.openprivacy.ca/cwtch.im/tapir/networks/tor"
|
||||
"git.openprivacy.ca/cwtch.im/tapir/persistence"
|
||||
"git.openprivacy.ca/cwtch.im/tapir/primitives"
|
||||
"git.openprivacy.ca/cwtch.im/tapir/primitives/privacypass"
|
||||
"git.openprivacy.ca/openprivacy/connectivity"
|
||||
"git.openprivacy.ca/openprivacy/connectivity/tor"
|
||||
"git.openprivacy.ca/openprivacy/log"
|
||||
"os"
|
||||
"time"
|
||||
"path"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Server encapsulates a complete, compliant Cwtch server.
|
||||
type Server struct {
|
||||
service tapir.Service
|
||||
config Config
|
||||
metricsPack metrics.Monitors
|
||||
closed bool
|
||||
tokenTapirService tapir.Service
|
||||
tokenServer *privacypass.TokenServer
|
||||
tokenService primitives.Identity
|
||||
tokenServicePrivKey ed25519.PrivateKey
|
||||
service tapir.Service
|
||||
config Config
|
||||
metricsPack metrics.Monitors
|
||||
tokenTapirService tapir.Service
|
||||
tokenServer *privacypass.TokenServer
|
||||
tokenService primitives.Identity
|
||||
tokenServicePrivKey ed25519.PrivateKey
|
||||
tokenServiceStopped bool
|
||||
onionServiceStopped bool
|
||||
running bool
|
||||
existingMessageCount int
|
||||
lock sync.RWMutex
|
||||
}
|
||||
|
||||
// Setup initialized a server from a given configuration
|
||||
func (s *Server) Setup(serverConfig Config) {
|
||||
s.config = serverConfig
|
||||
bs := new(persistence.BoltPersistence)
|
||||
bs.Open("./tokens.db")
|
||||
s.tokenServer = privacypass.NewTokenServerFromStore(bs)
|
||||
bs.Open(path.Join(serverConfig.ConfigDir, "tokens.db"))
|
||||
s.tokenServer = privacypass.NewTokenServerFromStore(&serverConfig.TokenServiceK, bs)
|
||||
log.Infof("Y: %v", s.tokenServer.Y)
|
||||
s.tokenService = s.config.TokenServiceIdentity()
|
||||
s.tokenServicePrivKey = s.config.TokenServerPrivateKey
|
||||
}
|
||||
|
||||
// Identity returns the main onion identity of the server
|
||||
func (s *Server) Identity() primitives.Identity {
|
||||
return s.config.Identity()
|
||||
}
|
||||
|
||||
// Run starts a server with the given privateKey
|
||||
// TODO: surface errors
|
||||
// TODO: handle HUP/KILL signals to exit and close Tor gracefully
|
||||
// TODO: handle user input to exit
|
||||
func (s *Server) Run(acn connectivity.ACN) {
|
||||
s.closed = false
|
||||
|
||||
func (s *Server) Run(acn connectivity.ACN) error {
|
||||
addressIdentity := tor.GetTorV3Hostname(s.config.PublicKey)
|
||||
|
||||
//tokenService := privacypass.NewTokenServer()
|
||||
identity := primitives.InitializeIdentity("", &s.config.PrivateKey, &s.config.PublicKey)
|
||||
var service tapir.Service
|
||||
service = new(tor2.BaseOnionService)
|
||||
|
@ -61,29 +65,33 @@ func (s *Server) Run(acn connectivity.ACN) {
|
|||
ms := new(storage.MessageStore)
|
||||
err := ms.Init(s.config.ConfigDir, s.config.MaxBufferLines, s.metricsPack.MessageCounter)
|
||||
if err != nil {
|
||||
log.Errorln(err)
|
||||
acn.Close()
|
||||
os.Exit(1)
|
||||
return err
|
||||
}
|
||||
|
||||
// Needed because we only collect metrics on a per-session basis
|
||||
// TODO fix metrics so they persist across sessions?
|
||||
s.existingMessageCount = len(ms.FetchMessages())
|
||||
|
||||
s.tokenTapirService = new(tor2.BaseOnionService)
|
||||
s.tokenTapirService.Init(acn, s.tokenServicePrivKey, &s.tokenService)
|
||||
tokenApplication := new(applications.TokenApplication)
|
||||
tokenApplication.TokenService = s.tokenServer
|
||||
powTokenApp := new(applications.ApplicationChain).
|
||||
ChainApplication(new(applications.ProofOfWorkApplication), applications.SuccessfulProofOfWorkCapability).
|
||||
ChainApplication(tokenApplication, applications.HasTokensCapability)
|
||||
go func() {
|
||||
s.tokenTapirService = new(tor2.BaseOnionService)
|
||||
s.tokenTapirService.Init(acn, s.tokenServicePrivKey, &s.tokenService)
|
||||
tokenApplication := new(applications.TokenApplication)
|
||||
tokenApplication.TokenService = s.tokenServer
|
||||
powTokenApp := new(applications.ApplicationChain).
|
||||
ChainApplication(new(applications.ProofOfWorkApplication), applications.SuccessfulProofOfWorkCapability).
|
||||
ChainApplication(tokenApplication, applications.HasTokensCapability)
|
||||
s.tokenTapirService.Listen(powTokenApp)
|
||||
s.tokenServiceStopped = true
|
||||
}()
|
||||
go func() {
|
||||
s.service.Listen(NewTokenBoardServer(ms, s.tokenServer))
|
||||
s.onionServiceStopped = true
|
||||
}()
|
||||
|
||||
for true {
|
||||
s.service.Listen(NewTokenBoardServer(ms, s.tokenServer))
|
||||
if s.closed {
|
||||
return
|
||||
}
|
||||
time.Sleep(5 * time.Second)
|
||||
}
|
||||
s.lock.Lock()
|
||||
s.running = true
|
||||
s.lock.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
// KeyBundle provides the signed keybundle of the server
|
||||
|
@ -97,10 +105,57 @@ func (s *Server) KeyBundle() *model.KeyBundle {
|
|||
return kb
|
||||
}
|
||||
|
||||
// CheckStatus returns true if the server is running and/or an error if any part of the server needs to be restarted.
|
||||
func (s *Server) CheckStatus() (bool, error) {
|
||||
s.lock.RLock()
|
||||
defer s.lock.RUnlock()
|
||||
if s.onionServiceStopped == true || s.tokenServiceStopped == true {
|
||||
return s.running, fmt.Errorf("one of more server components are down: onion:%v token service: %v", s.onionServiceStopped, s.tokenServiceStopped)
|
||||
}
|
||||
return s.running, nil
|
||||
}
|
||||
|
||||
// Shutdown kills the app closing all connections and freeing all goroutines
|
||||
func (s *Server) Shutdown() {
|
||||
s.closed = true
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
s.service.Shutdown()
|
||||
s.tokenTapirService.Shutdown()
|
||||
s.metricsPack.Stop()
|
||||
s.running = true
|
||||
|
||||
}
|
||||
|
||||
// Statistics is an encapsulation of information about the server that an operator might want to know at a glance.
|
||||
type Statistics struct {
|
||||
TotalMessages int
|
||||
}
|
||||
|
||||
// GetStatistics is a stub method for providing some high level information about
|
||||
// the server operation to bundling applications (e.g. the UI)
|
||||
func (s *Server) GetStatistics() Statistics {
|
||||
// TODO Statistics from Metrics is very awkward. Metrics needs an overhaul to make safe
|
||||
total := s.existingMessageCount
|
||||
if s.metricsPack.TotalMessageCounter != nil {
|
||||
total += s.metricsPack.TotalMessageCounter.Count()
|
||||
}
|
||||
|
||||
return Statistics{
|
||||
TotalMessages: total,
|
||||
}
|
||||
}
|
||||
|
||||
// ConfigureAutostart sets whether this server should autostart (in the Cwtch UI/bundling application)
|
||||
func (s *Server) ConfigureAutostart(autostart bool) {
|
||||
s.config.AutoStart = autostart
|
||||
s.config.Save(s.config.ConfigDir, s.config.FilePath)
|
||||
}
|
||||
|
||||
// Close shuts down the cwtch server in a safe way.
|
||||
func (s *Server) Close() {
|
||||
log.Infof("Shutting down server")
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
log.Infof("Closing Token Server Database...")
|
||||
s.tokenServer.Close()
|
||||
}
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"cwtch.im/tapir/primitives"
|
||||
"crypto/rand"
|
||||
"encoding/json"
|
||||
"git.openprivacy.ca/cwtch.im/tapir/primitives"
|
||||
"git.openprivacy.ca/openprivacy/log"
|
||||
"github.com/gtank/ristretto255"
|
||||
"golang.org/x/crypto/ed25519"
|
||||
"io/ioutil"
|
||||
"path"
|
||||
|
@ -18,13 +20,20 @@ type Reporting struct {
|
|||
|
||||
// Config is a struct for storing basic server configuration
|
||||
type Config struct {
|
||||
ConfigDir string `json:"-"`
|
||||
MaxBufferLines int `json:"maxBufferLines"`
|
||||
PublicKey ed25519.PublicKey `json:"publicKey"`
|
||||
PrivateKey ed25519.PrivateKey `json:"privateKey"`
|
||||
ConfigDir string `json:"-"`
|
||||
FilePath string `json:"-"`
|
||||
MaxBufferLines int `json:"maxBufferLines"`
|
||||
|
||||
PublicKey ed25519.PublicKey `json:"publicKey"`
|
||||
PrivateKey ed25519.PrivateKey `json:"privateKey"`
|
||||
|
||||
TokenServerPublicKey ed25519.PublicKey `json:"tokenServerPublicKey"`
|
||||
TokenServerPrivateKey ed25519.PrivateKey `json:"tokenServerPrivateKey"`
|
||||
ServerReporting Reporting `json:"serverReporting"`
|
||||
|
||||
TokenServiceK ristretto255.Scalar `json:"tokenServiceK"`
|
||||
|
||||
ServerReporting Reporting `json:"serverReporting"`
|
||||
AutoStart bool `json:"autostart"`
|
||||
}
|
||||
|
||||
// Identity returns an encapsulation of the servers keys
|
||||
|
@ -61,6 +70,19 @@ func LoadConfig(configDir, filename string) Config {
|
|||
ReportingGroupID: "",
|
||||
ReportingServerAddr: "",
|
||||
}
|
||||
config.AutoStart = false
|
||||
config.ConfigDir = configDir
|
||||
config.FilePath = filename
|
||||
|
||||
k := new(ristretto255.Scalar)
|
||||
b := make([]byte, 64)
|
||||
_, err := rand.Read(b)
|
||||
if err != nil {
|
||||
// unable to generate secure random numbers
|
||||
panic("unable to generate secure random numbers")
|
||||
}
|
||||
k.FromUniformBytes(b)
|
||||
config.TokenServiceK = *k
|
||||
|
||||
raw, err := ioutil.ReadFile(path.Join(configDir, filename))
|
||||
if err == nil {
|
||||
|
|
|
@ -3,10 +3,10 @@ package server
|
|||
import (
|
||||
"cwtch.im/cwtch/protocol/groups"
|
||||
"cwtch.im/cwtch/server/storage"
|
||||
"cwtch.im/tapir"
|
||||
"cwtch.im/tapir/applications"
|
||||
"cwtch.im/tapir/primitives/privacypass"
|
||||
"encoding/json"
|
||||
"git.openprivacy.ca/cwtch.im/tapir"
|
||||
"git.openprivacy.ca/cwtch.im/tapir/applications"
|
||||
"git.openprivacy.ca/cwtch.im/tapir/primitives/privacypass"
|
||||
"git.openprivacy.ca/openprivacy/log"
|
||||
)
|
||||
|
||||
|
|
|
@ -22,6 +22,7 @@ type ProfileStore interface {
|
|||
GetProfileCopy(timeline bool) *model.Profile
|
||||
GetNewPeerMessage() *event.Event
|
||||
GetStatusMessages() []*event.Event
|
||||
CheckPassword(string) bool
|
||||
}
|
||||
|
||||
// CreateProfileWriterStore creates a profile store backed by a filestore listening for events and saving them
|
||||
|
@ -79,7 +80,7 @@ func upgradeV0ToV1(directory, password string) error {
|
|||
|
||||
func versionCheckUpgrade(directory, password string) {
|
||||
version := detectVersion(directory)
|
||||
log.Infof("versionCheck: %v\n", version)
|
||||
log.Debugf("versionCheck: %v\n", version)
|
||||
if version == -1 {
|
||||
return
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ package storage
|
|||
|
||||
import (
|
||||
"cwtch.im/cwtch/event"
|
||||
"cwtch.im/cwtch/model"
|
||||
"cwtch.im/cwtch/storage/v0"
|
||||
"fmt"
|
||||
"git.openprivacy.ca/openprivacy/log"
|
||||
|
@ -32,6 +33,8 @@ func TestProfileStoreUpgradeV0toV1(t *testing.T) {
|
|||
|
||||
fmt.Println("Creating and initializing v0 profile and store...")
|
||||
profile := NewProfile(testProfileName)
|
||||
profile.AddContact("2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd", &model.PublicProfile{Attributes: map[string]string{string(model.KeyTypeServerOnion): "2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd"}})
|
||||
|
||||
ps1 := v0.NewProfileWriterStore(eventBus, testingDir, password, profile)
|
||||
|
||||
groupid, invite, err := profile.StartGroup("2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd")
|
||||
|
@ -42,7 +45,7 @@ func TestProfileStoreUpgradeV0toV1(t *testing.T) {
|
|||
t.Errorf("Creating group invite: %v\n", err)
|
||||
}
|
||||
|
||||
ps1.AddGroup(invite, profile.Onion)
|
||||
ps1.AddGroup(invite)
|
||||
|
||||
fmt.Println("Sending 200 messages...")
|
||||
|
||||
|
|
|
@ -52,9 +52,9 @@ func ReadProfile(directory, password string) (*model.Profile, error) {
|
|||
|
||||
/********************************************************************************************/
|
||||
|
||||
// AddGroup For testing, adds a group to the profile (and startsa stream store)
|
||||
func (ps *ProfileStoreV0) AddGroup(invite []byte, peer string) {
|
||||
gid, err := ps.profile.ProcessInvite(string(invite), peer)
|
||||
// AddGroup For testing, adds a group to the profile (and starts a stream store)
|
||||
func (ps *ProfileStoreV0) AddGroup(invite string) {
|
||||
gid, err := ps.profile.ProcessInvite(invite)
|
||||
if err == nil {
|
||||
ps.save()
|
||||
group := ps.profile.Groups[gid]
|
||||
|
|
|
@ -40,7 +40,7 @@ func TestProfileStoreWriteRead(t *testing.T) {
|
|||
t.Errorf("Creating group invite: %v\n", err)
|
||||
}
|
||||
|
||||
ps1.AddGroup(invite, profile.Onion)
|
||||
ps1.AddGroup(invite)
|
||||
|
||||
ps1.AddGroupMessage(groupid, time.Now().Format(time.RFC3339Nano), time.Now().Format(time.RFC3339Nano), ps1.getProfileCopy(true).Onion, testMessage)
|
||||
|
||||
|
|
|
@ -3,11 +3,13 @@ package v1
|
|||
import (
|
||||
"cwtch.im/cwtch/event"
|
||||
"cwtch.im/cwtch/model"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"git.openprivacy.ca/openprivacy/log"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
|
@ -31,6 +33,12 @@ type ProfileStoreV1 struct {
|
|||
writer bool
|
||||
}
|
||||
|
||||
// CheckPassword returns true if the given password produces the same key as the current stored key, otherwise false.
|
||||
func (ps *ProfileStoreV1) CheckPassword(checkpass string) bool {
|
||||
oldkey := CreateKey(checkpass, ps.salt[:])
|
||||
return oldkey == ps.key
|
||||
}
|
||||
|
||||
func initV1Directory(directory, password string) ([32]byte, [128]byte, error) {
|
||||
key, salt, err := CreateKeySalt(password)
|
||||
if err != nil {
|
||||
|
@ -74,15 +82,18 @@ func (ps *ProfileStoreV1) initProfileWriterStore() {
|
|||
ps.eventManager.Subscribe(event.SetPeerAttribute, ps.queue)
|
||||
ps.eventManager.Subscribe(event.SetGroupAttribute, ps.queue)
|
||||
ps.eventManager.Subscribe(event.AcceptGroupInvite, ps.queue)
|
||||
ps.eventManager.Subscribe(event.NewGroupInvite, ps.queue)
|
||||
ps.eventManager.Subscribe(event.RejectGroupInvite, ps.queue)
|
||||
ps.eventManager.Subscribe(event.NewGroup, ps.queue)
|
||||
ps.eventManager.Subscribe(event.NewMessageFromGroup, ps.queue)
|
||||
ps.eventManager.Subscribe(event.SendMessageToPeer, ps.queue)
|
||||
ps.eventManager.Subscribe(event.PeerAcknowledgement, ps.queue)
|
||||
ps.eventManager.Subscribe(event.NewMessageFromPeer, ps.queue)
|
||||
ps.eventManager.Subscribe(event.PeerStateChange, ps.queue)
|
||||
ps.eventManager.Subscribe(event.ServerStateChange, ps.queue)
|
||||
ps.eventManager.Subscribe(event.DeleteContact, ps.queue)
|
||||
ps.eventManager.Subscribe(event.DeleteGroup, ps.queue)
|
||||
ps.eventManager.Subscribe(event.ChangePassword, ps.queue)
|
||||
ps.eventManager.Subscribe(event.UpdateMessageFlags, ps.queue)
|
||||
}
|
||||
|
||||
// LoadProfileWriterStore loads a profile store from filestore listening for events and saving them
|
||||
|
@ -214,7 +225,11 @@ func (ps *ProfileStoreV1) ChangePassword(oldpass, newpass, eventID string) {
|
|||
if len(ssid) == groupIDLen {
|
||||
ps.profile.Groups[ssid].LocalID = newLocalID
|
||||
} else {
|
||||
ps.profile.Contacts[ssid].LocalID = newLocalID
|
||||
if ps.profile.Contacts[ssid] != nil {
|
||||
ps.profile.Contacts[ssid].LocalID = newLocalID
|
||||
} else {
|
||||
log.Errorf("Unknown Contact: %v. This is probably the result of corrupted development data from fuzzing. This contact will not appear in the new profile.", ssid)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -236,9 +251,26 @@ func (ps *ProfileStoreV1) save() error {
|
|||
bytes, _ := json.Marshal(ps.profile)
|
||||
return ps.fs.Write(bytes)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ps *ProfileStoreV1) regenStreamStore(messages []model.Message, contact string) {
|
||||
oldss := ps.streamStores[contact]
|
||||
newLocalID := model.GenerateRandomID()
|
||||
newSS := NewStreamStore(ps.directory, newLocalID, ps.key)
|
||||
newSS.WriteN(messages)
|
||||
if len(contact) == groupIDLen {
|
||||
ps.profile.Groups[contact].LocalID = newLocalID
|
||||
} else {
|
||||
// We can assume this exists as regen stream store should only happen to *update* a message
|
||||
ps.profile.Contacts[contact].LocalID = newLocalID
|
||||
}
|
||||
ps.streamStores[contact] = newSS
|
||||
ps.save()
|
||||
oldss.Delete()
|
||||
}
|
||||
|
||||
// load instantiates a cwtchPeer from the file store
|
||||
func (ps *ProfileStoreV1) load() error {
|
||||
decrypted, err := ps.fs.Read()
|
||||
|
@ -300,7 +332,6 @@ func (ps *ProfileStoreV1) GetProfileCopy(timeline bool) *model.Profile {
|
|||
}
|
||||
|
||||
func (ps *ProfileStoreV1) eventHandler() {
|
||||
log.Debugln("eventHandler()!")
|
||||
for {
|
||||
ev := ps.queue.Next()
|
||||
log.Debugf("eventHandler event %v %v\n", ev.EventType, ev.EventID)
|
||||
|
@ -372,8 +403,11 @@ func (ps *ProfileStoreV1) eventHandler() {
|
|||
} else {
|
||||
log.Errorf("error accepting group invite")
|
||||
}
|
||||
case event.NewGroupInvite:
|
||||
gid, err := ps.profile.ProcessInvite(ev.Data[event.GroupInvite], ev.Data[event.RemotePeer])
|
||||
case event.RejectGroupInvite:
|
||||
ps.profile.RejectInvite(ev.Data[event.GroupID])
|
||||
ps.save()
|
||||
case event.NewGroup:
|
||||
gid, err := ps.profile.ProcessInvite(ev.Data[event.GroupInvite])
|
||||
if err == nil {
|
||||
ps.save()
|
||||
group := ps.profile.Groups[gid]
|
||||
|
@ -381,17 +415,35 @@ func (ps *ProfileStoreV1) eventHandler() {
|
|||
} else {
|
||||
log.Errorf("error storing new group invite: %v (%v)", err, ev)
|
||||
}
|
||||
case event.SendMessageToPeer: // We need this to be able to save the outgoing messages to a peer
|
||||
ps.attemptSavePeerMessage(ev, false)
|
||||
case event.SendMessageToPeer: // cache the message till an ack, then it's given to stream store.
|
||||
// stream store doesn't support updates, so we don't want to commit it till ack'd
|
||||
ps.profile.AddSentMessageToContactTimeline(ev.Data[event.RemotePeer], ev.Data[event.Data], time.Now(), ev.EventID)
|
||||
case event.NewMessageFromPeer:
|
||||
ps.attemptSavePeerMessage(ev, true)
|
||||
ps.profile.AddMessageToContactTimeline(ev.Data[event.RemotePeer], ev.Data[event.Data], time.Now())
|
||||
ps.attemptSavePeerMessage(ev.Data[event.RemotePeer], ev.Data[event.Data], ev.Data[event.TimestampReceived], true)
|
||||
case event.PeerAcknowledgement:
|
||||
onion := ev.Data[event.RemotePeer]
|
||||
eventID := ev.Data[event.EventID]
|
||||
contact, ok := ps.profile.Contacts[onion]
|
||||
if ok {
|
||||
mIdx, ok := contact.UnacknowledgedMessages[eventID]
|
||||
if ok {
|
||||
message := contact.Timeline.Messages[mIdx]
|
||||
ps.attemptSavePeerMessage(onion, message.Message, message.Timestamp.Format(time.RFC3339Nano), false)
|
||||
}
|
||||
}
|
||||
ps.profile.AckSentMessageToPeer(ev.Data[event.RemotePeer], ev.Data[event.EventID])
|
||||
case event.NewMessageFromGroup:
|
||||
groupid := ev.Data[event.GroupID]
|
||||
received, _ := time.Parse(time.RFC3339Nano, ev.Data[event.TimestampReceived])
|
||||
sent, _ := time.Parse(time.RFC3339Nano, ev.Data[event.TimestampSent])
|
||||
message := model.Message{Received: received, Timestamp: sent, Message: ev.Data[event.Data], PeerID: ev.Data[event.RemotePeer], Signature: []byte(ev.Data[event.Signature]), PreviousMessageSig: []byte(ev.Data[event.PreviousSignature])}
|
||||
sig, _ := base64.StdEncoding.DecodeString(ev.Data[event.Signature])
|
||||
prevsig, _ := base64.StdEncoding.DecodeString(ev.Data[event.PreviousSignature])
|
||||
message := model.Message{Received: received, Timestamp: sent, Message: ev.Data[event.Data], PeerID: ev.Data[event.RemotePeer], Signature: sig, PreviousMessageSig: prevsig, Acknowledged: true}
|
||||
ss, exists := ps.streamStores[groupid]
|
||||
if exists {
|
||||
// We need to store a local copy of the message...
|
||||
ps.profile.GetGroup(groupid).Timeline.Insert(&message)
|
||||
ss.Write(message)
|
||||
} else {
|
||||
log.Errorf("error storing new group message: %v stream store does not exist", ev)
|
||||
|
@ -428,6 +480,29 @@ func (ps *ProfileStoreV1) eventHandler() {
|
|||
oldpass := ev.Data[event.Password]
|
||||
newpass := ev.Data[event.NewPassword]
|
||||
ps.ChangePassword(oldpass, newpass, ev.EventID)
|
||||
case event.UpdateMessageFlags:
|
||||
handle := ev.Data[event.Handle]
|
||||
mIx, err := strconv.Atoi(ev.Data[event.Index])
|
||||
if err != nil {
|
||||
log.Errorf("Invalid Message Index: %v", err)
|
||||
return
|
||||
}
|
||||
flags, err := strconv.ParseUint(ev.Data[event.Flags], 2, 64)
|
||||
if err != nil {
|
||||
log.Errorf("Invalid Message Falgs: %v", err)
|
||||
return
|
||||
}
|
||||
ps.profile.UpdateMessageFlags(handle, mIx, flags)
|
||||
if len(handle) == groupIDLen {
|
||||
ps.regenStreamStore(ps.profile.GetGroup(handle).Timeline.Messages, handle)
|
||||
} else if contact, exists := ps.profile.GetContact(handle); exists {
|
||||
if exists {
|
||||
val, _ := contact.GetAttribute(event.SaveHistoryKey)
|
||||
if val == event.SaveHistoryConfirmed {
|
||||
ps.regenStreamStore(contact.Timeline.Messages, handle)
|
||||
}
|
||||
}
|
||||
}
|
||||
default:
|
||||
return
|
||||
}
|
||||
|
@ -438,28 +513,28 @@ func (ps *ProfileStoreV1) eventHandler() {
|
|||
// attemptSavePeerMessage checks if the peer has been configured to save history from this peer
|
||||
// and if so the peer saves the message into history. fromPeer is used to control if the message is saved
|
||||
// as coming from the remote peer or if it was sent by out profile.
|
||||
func (ps *ProfileStoreV1) attemptSavePeerMessage(ev *event.Event, fromPeer bool) {
|
||||
contact, exists := ps.profile.GetContact(ev.Data[event.RemotePeer])
|
||||
func (ps *ProfileStoreV1) attemptSavePeerMessage(peerID, messageData, timestampeReceived string, fromPeer bool) {
|
||||
contact, exists := ps.profile.GetContact(peerID)
|
||||
if exists {
|
||||
val, _ := contact.GetAttribute(event.SaveHistoryKey)
|
||||
switch val {
|
||||
case event.SaveHistoryConfirmed:
|
||||
{
|
||||
peerID := ev.Data[event.RemotePeer]
|
||||
peerID := peerID
|
||||
var received time.Time
|
||||
var message model.Message
|
||||
if fromPeer {
|
||||
received, _ = time.Parse(time.RFC3339Nano, ev.Data[event.TimestampReceived])
|
||||
message = model.Message{Received: received, Timestamp: received, Message: ev.Data[event.Data], PeerID: peerID, Signature: []byte{}, PreviousMessageSig: []byte{}}
|
||||
received, _ = time.Parse(time.RFC3339Nano, timestampeReceived)
|
||||
message = model.Message{Received: received, Timestamp: received, Message: messageData, PeerID: peerID, Signature: []byte{}, PreviousMessageSig: []byte{}}
|
||||
} else {
|
||||
received := time.Now()
|
||||
message = model.Message{Received: received, Timestamp: received, Message: ev.Data[event.Data], PeerID: ps.profile.Onion, Signature: []byte{}, PreviousMessageSig: []byte{}}
|
||||
message = model.Message{Received: received, Timestamp: received, Message: messageData, PeerID: ps.profile.Onion, Signature: []byte{}, PreviousMessageSig: []byte{}, Acknowledged: true}
|
||||
}
|
||||
ss, exists := ps.streamStores[peerID]
|
||||
if exists {
|
||||
ss.Write(message)
|
||||
} else {
|
||||
log.Errorf("error storing new peer message: %v stream store does not exist", ev)
|
||||
log.Errorf("error storing new peer message: %v stream store does not exist", peerID)
|
||||
}
|
||||
}
|
||||
default:
|
||||
|
@ -467,7 +542,7 @@ func (ps *ProfileStoreV1) attemptSavePeerMessage(ev *event.Event, fromPeer bool)
|
|||
}
|
||||
}
|
||||
} else {
|
||||
log.Errorf("error saving message for peer that doesn't exist: %v", ev)
|
||||
log.Errorf("error saving message for peer that doesn't exist: %v", peerID)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ package v1
|
|||
|
||||
import (
|
||||
"cwtch.im/cwtch/event"
|
||||
"cwtch.im/cwtch/model"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
|
@ -22,6 +23,9 @@ func TestProfileStoreWriteRead(t *testing.T) {
|
|||
os.RemoveAll(testingDir)
|
||||
eventBus := event.NewEventManager()
|
||||
profile := NewProfile(testProfileName)
|
||||
// The lightest weight server entry possible (usually we would import a key bundle...)
|
||||
profile.AddContact("2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd", &model.PublicProfile{Attributes: map[string]string{string(model.KeyTypeServerOnion): "2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd"}})
|
||||
|
||||
ps1 := CreateProfileWriterStore(eventBus, testingDir, password, profile)
|
||||
|
||||
eventBus.Publish(event.NewEvent(event.SetAttribute, map[event.Field]string{event.Key: testKey, event.Data: testVal}))
|
||||
|
@ -35,7 +39,7 @@ func TestProfileStoreWriteRead(t *testing.T) {
|
|||
t.Errorf("Creating group invite: %v\n", err)
|
||||
}
|
||||
|
||||
eventBus.Publish(event.NewEvent(event.NewGroupInvite, map[event.Field]string{event.TimestampReceived: time.Now().Format(time.RFC3339Nano), event.RemotePeer: ps1.GetProfileCopy(true).Onion, event.GroupInvite: string(invite)}))
|
||||
eventBus.Publish(event.NewEvent(event.NewGroup, map[event.Field]string{event.TimestampReceived: time.Now().Format(time.RFC3339Nano), event.RemotePeer: ps1.GetProfileCopy(true).Onion, event.GroupInvite: string(invite)}))
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
eventBus.Publish(event.NewEvent(event.NewMessageFromGroup, map[event.Field]string{
|
||||
|
@ -79,6 +83,8 @@ func TestProfileStoreChangePassword(t *testing.T) {
|
|||
eventBus.Subscribe(event.ChangePasswordSuccess, queue)
|
||||
|
||||
profile := NewProfile(testProfileName)
|
||||
profile.AddContact("2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd", &model.PublicProfile{Attributes: map[string]string{string(model.KeyTypeServerOnion): "2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd"}})
|
||||
|
||||
ps1 := CreateProfileWriterStore(eventBus, testingDir, password, profile)
|
||||
|
||||
groupid, invite, err := profile.StartGroup("2c3kmoobnyghj2zw6pwv7d57yzld753auo3ugauezzpvfak3ahc4bdyd")
|
||||
|
@ -89,7 +95,7 @@ func TestProfileStoreChangePassword(t *testing.T) {
|
|||
t.Errorf("Creating group invite: %v\n", err)
|
||||
}
|
||||
|
||||
eventBus.Publish(event.NewEvent(event.NewGroupInvite, map[event.Field]string{event.TimestampReceived: time.Now().Format(time.RFC3339Nano), event.RemotePeer: ps1.GetProfileCopy(true).Onion, event.GroupInvite: string(invite)}))
|
||||
eventBus.Publish(event.NewEvent(event.NewGroup, map[event.Field]string{event.TimestampReceived: time.Now().Format(time.RFC3339Nano), event.RemotePeer: ps1.GetProfileCopy(true).Onion, event.GroupInvite: string(invite)}))
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
fmt.Println("Sending 200 messages...")
|
||||
|
|
|
@ -23,6 +23,7 @@ import (
|
|||
"path"
|
||||
"runtime"
|
||||
"runtime/pprof"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
@ -66,23 +67,25 @@ func serverCheck(t *testing.T, serverAddr string) bool {
|
|||
}
|
||||
|
||||
func waitForPeerGroupConnection(t *testing.T, peer peer.CwtchPeer, groupID string) {
|
||||
peerName, _ := peer.GetAttribute(attr.GetLocalScope("name"))
|
||||
for {
|
||||
fmt.Printf("%v checking group connection...\n", peer.GetName())
|
||||
fmt.Printf("%v checking group connection...\n", peerName)
|
||||
state, ok := peer.GetGroupState(groupID)
|
||||
if ok {
|
||||
fmt.Printf("Waiting for Peer %v to join group %v - state: %v\n", peer.GetName(), groupID, state)
|
||||
fmt.Printf("Waiting for Peer %v to join group %v - state: %v\n", peerName, groupID, state)
|
||||
if state == connections.FAILED {
|
||||
t.Fatalf("%v could not connect to %v", peer.GetOnion(), groupID)
|
||||
}
|
||||
if state != connections.SYNCED {
|
||||
fmt.Printf("peer %v %v waiting connect to group %v, currently: %v\n", peer.GetName(), peer.GetOnion(), groupID, connections.ConnectionStateName[state])
|
||||
fmt.Printf("peer %v %v waiting connect to group %v, currently: %v\n", peerName, peer.GetOnion(), groupID, connections.ConnectionStateName[state])
|
||||
time.Sleep(time.Second * 5)
|
||||
continue
|
||||
} else {
|
||||
fmt.Printf("peer %v %v CONNECTED to group %v\n", peer.GetName(), peer.GetOnion(), groupID)
|
||||
fmt.Printf("peer %v %v CONNECTED to group %v\n", peerName, peer.GetOnion(), groupID)
|
||||
break
|
||||
}
|
||||
}
|
||||
time.Sleep(time.Second * 2)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
@ -100,7 +103,9 @@ func waitForPeerPeerConnection(t *testing.T, peera peer.CwtchPeer, peerb peer.Cw
|
|||
time.Sleep(time.Second * 5)
|
||||
continue
|
||||
} else {
|
||||
fmt.Printf("%v CONNECTED and AUTHED to %v\n", peera.GetName(), peerb.GetName())
|
||||
peerAName, _ := peera.GetAttribute(attr.GetLocalScope("name"))
|
||||
peerBName, _ := peerb.GetAttribute(attr.GetLocalScope("name"))
|
||||
fmt.Printf("%v CONNECTED and AUTHED to %v\n", peerAName, peerBName)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
@ -118,14 +123,14 @@ func TestCwtchPeerIntegration(t *testing.T) {
|
|||
log.ExcludeFromPattern("event/eventmanager")
|
||||
log.ExcludeFromPattern("pipeBridge")
|
||||
log.ExcludeFromPattern("tapir")
|
||||
os.Mkdir("tordir",0700)
|
||||
os.Mkdir("tordir", 0700)
|
||||
dataDir := path.Join("tordir", "tor")
|
||||
os.MkdirAll(dataDir, 0700)
|
||||
|
||||
// we don't need real randomness for the port, just to avoid a possible conflict...
|
||||
mrand.Seed(int64(time.Now().Nanosecond()))
|
||||
socksPort := mrand.Intn(1000)+9051
|
||||
controlPort := mrand.Intn(1000)+9052
|
||||
socksPort := mrand.Intn(1000) + 9051
|
||||
controlPort := mrand.Intn(1000) + 9052
|
||||
|
||||
// generate a random password
|
||||
key := make([]byte, 64)
|
||||
|
@ -139,7 +144,7 @@ func TestCwtchPeerIntegration(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatalf("Could not start Tor: %v", err)
|
||||
}
|
||||
pid,err := acn.GetPID()
|
||||
pid, _ := acn.GetPID()
|
||||
t.Logf("Tor pid: %v", pid)
|
||||
|
||||
// ***** Cwtch Server management *****
|
||||
|
@ -300,12 +305,16 @@ func TestCwtchPeerIntegration(t *testing.T) {
|
|||
time.Sleep(time.Second * 5)
|
||||
|
||||
fmt.Println("Bob examining groups and accepting invites...")
|
||||
for _, groupID := range bob.GetGroups() {
|
||||
group := bob.GetGroup(groupID)
|
||||
fmt.Printf("Bob group: %v (Accepted: %v)\n", group.GroupID, group.Accepted)
|
||||
if group.Accepted == false {
|
||||
fmt.Printf("Bob received and accepting group invite: %v\n", group.GroupID)
|
||||
bob.AcceptInvite(group.GroupID)
|
||||
for _, message := range bob.GetContact(alice.GetOnion()).Timeline.GetMessages() {
|
||||
fmt.Printf("Found message from Alice: %v", message.Message)
|
||||
if strings.HasPrefix(message.Message, "torv3") {
|
||||
gid, err := bob.ImportGroup(message.Message)
|
||||
if err == nil {
|
||||
fmt.Printf("Bob found invite...now accepting %v...", gid)
|
||||
bob.AcceptInvite(gid)
|
||||
} else {
|
||||
t.Fatalf("Bob could not accept invite...%v", gid)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -319,25 +328,25 @@ func TestCwtchPeerIntegration(t *testing.T) {
|
|||
fmt.Println("Starting conversation in group...")
|
||||
// Conversation
|
||||
fmt.Printf("%v> %v\n", aliceName, aliceLines[0])
|
||||
err = alice.SendMessageToGroup(groupID, aliceLines[0])
|
||||
_, err = alice.SendMessageToGroupTracked(groupID, aliceLines[0])
|
||||
if err != nil {
|
||||
t.Fatalf("Alice failed to send a message to the group: %v", err)
|
||||
}
|
||||
time.Sleep(time.Second * 10)
|
||||
|
||||
fmt.Printf("%v> %v\n", bobName, bobLines[0])
|
||||
err = bob.SendMessageToGroup(groupID, bobLines[0])
|
||||
_, err = bob.SendMessageToGroupTracked(groupID, bobLines[0])
|
||||
if err != nil {
|
||||
t.Fatalf("Bob failed to send a message to the group: %v", err)
|
||||
}
|
||||
time.Sleep(time.Second * 10)
|
||||
|
||||
fmt.Printf("%v> %v\n", aliceName, aliceLines[1])
|
||||
alice.SendMessageToGroup(groupID, aliceLines[1])
|
||||
alice.SendMessageToGroupTracked(groupID, aliceLines[1])
|
||||
time.Sleep(time.Second * 10)
|
||||
|
||||
fmt.Printf("%v> %v\n", bobName, bobLines[1])
|
||||
bob.SendMessageToGroup(groupID, bobLines[1])
|
||||
bob.SendMessageToGroupTracked(groupID, bobLines[1])
|
||||
time.Sleep(time.Second * 10)
|
||||
|
||||
fmt.Println("Alice inviting Carol to group...")
|
||||
|
@ -347,12 +356,16 @@ func TestCwtchPeerIntegration(t *testing.T) {
|
|||
}
|
||||
time.Sleep(time.Second * 60) // Account for some token acquisition in Alice and Bob flows.
|
||||
fmt.Println("Carol examining groups and accepting invites...")
|
||||
for _, groupID := range carol.GetGroups() {
|
||||
group := carol.GetGroup(groupID)
|
||||
fmt.Printf("Carol group: %v (Accepted: %v)\n", group.GroupID, group.Accepted)
|
||||
if group.Accepted == false {
|
||||
fmt.Printf("Carol received and accepting group invite: %v\n", group.GroupID)
|
||||
carol.AcceptInvite(group.GroupID)
|
||||
for _, message := range carol.GetContact(alice.GetOnion()).Timeline.GetMessages() {
|
||||
fmt.Printf("Found message from Alice: %v", message.Message)
|
||||
if strings.HasPrefix(message.Message, "torv3") {
|
||||
gid, err := carol.ImportGroup(message.Message)
|
||||
if err == nil {
|
||||
fmt.Printf("Carol found invite...now accepting %v...", gid)
|
||||
carol.AcceptInvite(gid)
|
||||
} else {
|
||||
t.Fatalf("Carol could not accept invite...%v", gid)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -367,12 +380,12 @@ func TestCwtchPeerIntegration(t *testing.T) {
|
|||
numGoRotinesPostCarolConnect := runtime.NumGoroutine()
|
||||
|
||||
fmt.Printf("%v> %v", bobName, bobLines[2])
|
||||
bob.SendMessageToGroup(groupID, bobLines[2])
|
||||
bob.SendMessageToGroupTracked(groupID, bobLines[2])
|
||||
// Bob should have enough tokens so we don't need to account for
|
||||
// token acquisition here...
|
||||
|
||||
fmt.Printf("%v> %v", carolName, carolLines[0])
|
||||
carol.SendMessageToGroup(groupID, carolLines[0])
|
||||
carol.SendMessageToGroupTracked(groupID, carolLines[0])
|
||||
time.Sleep(time.Second * 30) // we need to account for spam-based token acquisition, but everything should
|
||||
// be warmed-up and delays should be pretty small.
|
||||
|
||||
|
|
Loading…
Reference in New Issue