diff --git a/app/cli/main.go b/app/cli/main.go index 31ed802..5d08528 100644 --- a/app/cli/main.go +++ b/app/cli/main.go @@ -1,33 +1,151 @@ package main import ( - "bufio" + "fmt" app2 "git.mascherari.press/cwtch/app" - "os" + "strings" + "github.com/c-bata/go-prompt" ) + +var app app2.Application + + +func completer(d prompt.Document) []prompt.Suggest { + + s := []prompt.Suggest{} + + if d.FindStartOfPreviousWord() == 0 { + s := []prompt.Suggest{ + {Text: "new-profile", Description: "create a new profile"}, + {Text: "load-profile", Description: "load a new profile"}, + {Text: "quit", Description: "quit cwtch"}, + {Text: "servers", Description: "retrieve a list of servers and their connection status"}, + {Text: "peers", Description: "retrieve a list of peers and their connection status"}, + {Text: "contacts", Description: "retrieve a list of contacts"}, + {Text: "groups", Description: "retrieve a list of groups"}, + {Text: "send", Description: "send a message to a group"}, + {Text: "timeline", Description: "read the timeline of a given group"}, + {Text: "accept-invite", Description: "accept the invite of a group"}, + {Text: "invite", Description: "invite a new contact"}, + {Text: "invite-to-group", Description: "invite an existing contact to join an existing group"}, + {Text: "new-group", Description: "create a new group"}, + } + return prompt.FilterHasPrefix(s, d.GetWordBeforeCursor(), true) + } + + w := d.CurrentLine() + if strings.HasPrefix(w, "send") || strings.HasPrefix(w, "timeline") { + s = []prompt.Suggest{} + groups := app.Peer.Profile.Groups + for _, group := range groups { + s = append(s, prompt.Suggest{Text: group.GroupID, Description: "Group owned by " + group.Owner + " on " + group.GroupServer}) + } + return prompt.FilterHasPrefix(s, d.GetWordBeforeCursor(), true) + } + + if strings.HasPrefix(w, "invite-to-group") { + + if d.FindStartOfPreviousWordWithSpace() == 0 { + s = []prompt.Suggest{} + contacts := app.Peer.Profile.Contacts + for _, contact := range contacts { + s = append(s, prompt.Suggest{Text: contact.Onion, Description: contact.Name}) + } + return prompt.FilterHasPrefix(s, d.GetWordBeforeCursor(), true) + } else { + s = []prompt.Suggest{} + groups := app.Peer.Profile.Groups + for _, group := range groups { + if group.Owner == "self" { + s = append(s, prompt.Suggest{Text: group.GroupID, Description: "Group owned by " + group.Owner + " on " + group.GroupServer}) + } + } + return prompt.FilterHasPrefix(s, d.GetWordBeforeCursor(), true) + } + } + + if strings.HasPrefix(w, "accept-invite") { + s = []prompt.Suggest{} + groups := app.Peer.Profile.Groups + for _, group := range groups { + if group.Accepted == false { + s = append(s, prompt.Suggest{Text: group.GroupID, Description: "Group owned by " + group.Owner + " on " + group.GroupServer}) + } + } + return prompt.FilterHasPrefix(s, d.GetWordBeforeCursor(), true) + } + + return s +} + + func main() { - quit := false - app := app2.Application{} - profilefile := "" - for !quit { - reader := bufio.NewReader(os.Stdin) + cwtch := +` + #, #' + @@@@@@: + @@@@@@. + @'@@+#' @@@@+ + ''''''@ #+@ : + @''''+;+' . ' + @''@' :+' , ; ##, +' + ,@@ ;' #'#@''. #''@''# + # ''''''#:,,#'''''@ + : @''''@ :+'''@ + ' @;+'@ @'# + .:# '#..# '# @ + @@@@@@ + @@@@@@ + '@@@@ + @# . . + +++, #'@+'@ + ''', ''''''# + .#+# ''', @'''+, + @''# ''', .#@ + :; '@''# .;. ''', ' : ;. , + @+'''@ '+'+ @++ @+'@+''''+@ #+'''#: ''';#''+@ @@@@ @@@@@@@@@ :@@@@# + #''''''# +''. +'': +'''''''''+ @'''''''# '''+'''''@ @@@@ @@@@@@@@@@@@@@@@: + @'''@@'''@ @''# ,'''@ ''+ @@''+#+ :'''@@+''' ''''@@'''' @@@@ @@@@@@@@@@@@@@@@@ + '''# @''# +''@ @'''# ;''@ +''+ @''@ ,+'', '''@ #'''. @@@@ @@@@ '@@@# @@@@ +;''' @@; '''# #'@'' @''@ @''+ +''# .@@ ''', '''. @@@@ @@@ @@@ .@@@ +@''# #'' ''#''#@''. #''# '''. '''. +'', @@@@ @@@ @@@ @@@ +@''# @''@'' #'@+'+ #''# '''. ''', +'', +@@@.@@@ @@@@ @@@, @@@ ,@@@ +;''+ @, +''@'# @'+''@ @''# +''; '+ ''', +'', @@@@@@@@# @@@@ @@@. .@@@ .@@@ + '''# ++'+ ''''@ ,''''# #''' @''@ '@''+ ''', ''', @@@@@@@@: @@@@ @@@; .@@@' ;@@@ + @'''@@'''@ #'''. +'''' ;'''#@ :'''#@+''+ ''', ''', @@@@@@# @@@@ @@@+ ,@@@. @@@@ + #''''''# @''+ @''+ +'''' @'''''''# ''', ''', #@@@. @@@@ @@@+ @@@ @@@@ + @+''+@ '++@ ;++@ '#''@ ##'''@: +++, +++, :@ @@@@ @@@' @@@ '@@@ + :' ' '` + fmt.Printf("%v\n\n", cwtch) + quit := false + app = app2.Application{} + profilefile := "" + var history []string + for !quit { profile := "unset" if app.Peer != nil { profile = app.Peer.Profile.Name } - fmt.Printf("cwtch [%v]> ", profile) - text, _ := reader.ReadString('\n') - commands := strings.Split(text[0:len(text)-1], " ") + prmpt := fmt.Sprintf("cwtch [%v]> ", profile) + + + text := prompt.Input(prmpt, completer, prompt.OptionSuggestionBGColor(prompt.Purple), + prompt.OptionDescriptionBGColor(prompt.White), + prompt.OptionHistory(history)) + + commands := strings.Split(text[0:len(text)], " ") + history = append(history,text) switch commands[0] { case "quit": + app.Peer.Save(profilefile) quit = true - case "newprofile": + case "new-profile": if len(commands) == 3 { err := app.NewProfile(commands[1], commands[2]) if err == nil { @@ -38,7 +156,7 @@ func main() { } else { fmt.Printf("Error creating NewProfile, usage: newprofile [name] [filename]\n") } - case "loadprofile": + case "load-profile": if len(commands) == 2 { err := app.SetProfile(commands[1]) profilefile = commands[1] @@ -55,7 +173,7 @@ func main() { if app.Peer != nil { fmt.Printf("Address cwtch:%v\n", app.Peer.Profile.Onion) } else { - fmt.Printf("Profile needs to be set") + fmt.Printf("Profile needs to be set\n") } case "invite": if len(commands) == 2 { @@ -69,33 +187,75 @@ func main() { for p, s := range peers { fmt.Printf("Name: %v Status: %v\n", p, s) } + case "servers": + servers := app.Peer.GetServers() + for s, st := range servers { + fmt.Printf("Name: %v Status: %v\n", s, st) + } case "contacts": for _, c := range app.Peer.Profile.Contacts { fmt.Printf("Name: %v, Onion: %v, Trusted: %v\n", c.Name, c.Onion, c.Trusted) } case "groups": for gid, g := range app.Peer.Profile.Groups { - fmt.Printf("Group Id: %v, Owner: %v\n", gid, g.Owner) + fmt.Printf("Group Id: %v, Owner: %v Accepted:%v \n", gid, g.Owner, g.Accepted) } - case "invitetogroup": + case "accept-invite": + if len(commands) == 2 { + groupID:= commands[1] + err := app.Peer.AcceptInvite(groupID) + if err == nil { + fmt.Printf("Error: %v\n", err) + } + } else { + fmt.Printf("Error accepting invite, usage: accept-invite [groupid]\n") + } + case "invite-to-group": if len(commands) == 3 { fmt.Printf("Inviting %v to %v\n", commands[1], commands[2]) err := app.Peer.InviteOnionToGroup(commands[1], commands[2]) - if err == nil { - fmt.Printf("Error: %v", err) + if err != nil { + fmt.Printf("Error: %v\n", err) } } else { fmt.Printf("Error inviting peer to group, usage: invitetogroup [onion] [groupid]\n") } - case "newgroup": + case "new-group": if len(commands) == 2 { fmt.Printf("Setting up a new group on server:%v\n", commands[1]) id, _ := app.Peer.Profile.StartGroup(commands[1]) - fmt.Printf("New Group [%v] created for server %v", id, commands[1]) + fmt.Printf("New Group [%v] created for server %v\n", id, commands[1]) app.Peer.Save(profilefile) } else { fmt.Printf("Error inviting peer, usage: newgroup [server]\n") } + case "send": + if len(commands) > 2 { + message := strings.Join(commands[2:], " ") + err := app.Peer.SendMessageToGroup(commands[1], message) + if err != nil { + fmt.Printf("Error: %v\n", err) + } + } else { + fmt.Printf("Error sending message to group, usage: send [groupid] [message]\n") + } + case "timeline": + if len(commands) == 2 { + group := app.Peer.Profile.GetGroupByGroupId(commands[1]) + if group == nil { + fmt.Printf("Error: group does not exist\n") + } else { + for _, m := range group.Timeline.Messages { + verified := "not-verified" + if m.Verified { + verified = "verified" + } + fmt.Printf("%v %v: %v [%s]\n", m.Timestamp, m.PeerID, m.Message, verified) + } + } + } else { + fmt.Printf("Error reading timeline from group, usage: timeline [groupid]\n") + } case "save": app.Peer.Save(profilefile) } diff --git a/model/message.go b/model/message.go index 4cdc38f..a3601b5 100644 --- a/model/message.go +++ b/model/message.go @@ -58,6 +58,14 @@ func (t Timeline) Less(i, j int) bool { func (t *Timeline) Insert(mi *Message) { t.lock.Lock() + + for _,m := range t.Messages { + if compareSignatures(m.Signature, mi.Signature) { + t.lock.Unlock() + return + } + } + t.Messages = append(t.Messages, *mi) sort.Sort(t) t.lock.Unlock() diff --git a/model/profile.go b/model/profile.go index 049e94a..6975cb1 100644 --- a/model/profile.go +++ b/model/profile.go @@ -81,6 +81,12 @@ func (p *Profile) AddContact(onion string, profile PublicProfile) { // VerifyMessage confirms the authenticity of a message given an onion, message and signature. func (p *Profile) VerifyGroupMessage(onion string, groupID string, message string, timestamp int32, signature []byte) bool { + + if onion == p.Onion { + m := message + groupID + strconv.Itoa(int(timestamp)) + return ed25519.Verify(p.Ed25519PublicKey, []byte(m), signature) + } + contact, found := p.Contacts[onion] if found { m := message + groupID + strconv.Itoa(int(timestamp)) diff --git a/peer/connections/connectionsmanager.go b/peer/connections/connectionsmanager.go index f1515b4..45660d5 100644 --- a/peer/connections/connectionsmanager.go +++ b/peer/connections/connectionsmanager.go @@ -22,19 +22,28 @@ func NewConnectionsManager() *Manager { func (m *Manager) ManagePeerConnection(host string, profile *model.Profile) { m.lock.Lock() - ppc := NewPeerPeerConnection(host, profile) - go ppc.Run() - m.peerConnections[host] = ppc + + + _,exists := m.peerConnections[host] + if !exists { + ppc := NewPeerPeerConnection(host, profile) + go ppc.Run() + m.peerConnections[host] = ppc + } m.lock.Unlock() } func (m *Manager) ManageServerConnection(host string, handler func(string, *protocol.GroupMessage)) { m.lock.Lock() - psc := NewPeerServerConnection(host) - go psc.Run() - psc.GroupMessageHandler = handler - m.serverConnections[host] = psc + + _,exists := m.serverConnections[host] + if !exists { + psc := NewPeerServerConnection(host) + go psc.Run() + psc.GroupMessageHandler = handler + m.serverConnections[host] = psc + } m.lock.Unlock() } @@ -48,6 +57,17 @@ func (m *Manager) GetPeers() map[string]ConnectionState { return rm } + +func (m *Manager) GetServers() map[string]ConnectionState { + rm := make(map[string]ConnectionState) + m.lock.Lock() + for onion, psc := range m.serverConnections { + rm[onion] = psc.GetState() + } + m.lock.Unlock() + return rm +} + func (m *Manager) GetPeerPeerConnectionForOnion(host string) (ppc *PeerPeerConnection) { m.lock.Lock() ppc = m.peerConnections[host] diff --git a/peer/cwtch_peer.go b/peer/cwtch_peer.go index ad73a7e..1ffc8f7 100644 --- a/peer/cwtch_peer.go +++ b/peer/cwtch_peer.go @@ -46,6 +46,11 @@ func (cp *CwtchPeer) setup() { cp.PeerWithOnion(onion) } + for _,group := range cp.Profile.Groups { + if group.Accepted || group.Owner == "self" { + cp.JoinServer(group.GroupServer) + } + } } func NewCwtchPeer(name string) *CwtchPeer { @@ -87,6 +92,7 @@ func (cp *CwtchPeer) InviteOnionToGroup(onion string, groupid string) error { ppc := cp.connectionsManager.GetPeerPeerConnectionForOnion(onion) fmt.Printf("Got connection for group: %v - Sending Invite\n", ppc) ppc.SendGroupInvite(invite) + return nil } return errors.New("group id could not be found") } @@ -99,20 +105,46 @@ func (cp *CwtchPeer) JoinServer(onion string) { cp.connectionsManager.ManageServerConnection(onion, cp.ReceiveGroupMessage) } -func (cp *CwtchPeer) SendMessageToGroup(groupid string, message string) { +func (cp *CwtchPeer) SendMessageToGroup(groupid string, message string) error { group := cp.Profile.GetGroupByGroupId(groupid) + if group == nil { + return errors.New("group does not exit") + } psc := cp.connectionsManager.GetPeerServerConnectionForOnion(group.GroupServer) ct := cp.Profile.EncryptMessageToGroup(message, groupid) gm := &protocol.GroupMessage{ Ciphertext: ct, } psc.SendGroupMessage(gm) + return nil } func (cp *CwtchPeer) GetPeers() map[string]connections.ConnectionState { return cp.connectionsManager.GetPeers() } +func (cp *CwtchPeer) GetServers() map[string]connections.ConnectionState { + return cp.connectionsManager.GetServers() +} + +func (cp *CwtchPeer) AcceptInvite(groupID string) error { + g := cp.Profile.GetGroupByGroupId(groupID) + if g == nil { + return errors.New("group invite does not exit") + } + g.Accepted = true + return nil +} + +func (cp *CwtchPeer) RejectInvite(groupID string) error { + g := cp.Profile.GetGroupByGroupId(groupID) + if g == nil { + return errors.New("group invite does not exit") + } + g.Accepted = false + return nil +} + func (cp *CwtchPeer) Listen() error { cwtchpeer := new(application.RicochetApplication) l, err := application.SetupOnion("127.0.0.1:9051", "tcp4", "", cp.Profile.OnionPrivateKey, 9878)