123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356 |
- package peer
-
- import (
- "cwtch.im/cwtch/event"
- "cwtch.im/cwtch/model"
- "cwtch.im/cwtch/protocol"
- "cwtch.im/cwtch/protocol/connections"
- "encoding/base32"
- "encoding/base64"
- "encoding/json"
- "errors"
- "git.openprivacy.ca/openprivacy/libricochet-go/connectivity"
- "git.openprivacy.ca/openprivacy/libricochet-go/log"
- "github.com/golang/protobuf/proto"
- "strings"
- "sync"
- "time"
- )
-
- // cwtchPeer manages incoming and outgoing connections and all processing for a Cwtch cwtchPeer
- type cwtchPeer struct {
- Profile *model.Profile
- mutex sync.Mutex
- shutdown bool
- started 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(connectivity.ACN, event.Manager)
- PeerWithOnion(string) *connections.PeerPeerConnection
- InviteOnionToGroup(string, string) error
- SendMessageToPeer(string, string) string
-
- TrustPeer(string) error
- BlockPeer(string) error
- AcceptInvite(string) error
- RejectInvite(string)
-
- JoinServer(string)
- SendMessageToGroup(string, string) error
- SendMessageToGroupTracked(string, string) (string, error)
-
- GetProfile() *model.Profile
- GetPeerState(string) connections.ConnectionState
-
- StartGroup(string) (string, []byte, error)
-
- ImportGroup(string) (string, error)
- ExportGroup(string) (string, error)
-
- GetGroup(string) *model.Group
- GetGroupState(string) connections.ConnectionState
- GetGroups() []string
- AddContact(nick, onion string, trusted bool)
- GetContacts() []string
- GetContact(string) *model.PublicProfile
-
- IsStarted() bool
- Listen()
- StartPeersConnections()
- StartGroupConnections()
- Shutdown()
- }
-
- // NewCwtchPeer creates and returns a new cwtchPeer with the given name.
- func NewCwtchPeer(name string) CwtchPeer {
- cp := new(cwtchPeer)
- cp.Profile = model.GenerateNewProfile(name)
- cp.shutdown = false
- return cp
- }
-
- // FromProfile generates a new peer from a profile.
- func FromProfile(profile *model.Profile) CwtchPeer {
- cp := new(cwtchPeer)
- cp.Profile = profile
- return cp
- }
-
- // Init instantiates a cwtchPeer
- func (cp *cwtchPeer) Init(acn connectivity.ACN, eventBus event.Manager) {
- cp.queue = event.NewEventQueue(100)
- go cp.eventHandler()
-
- cp.eventBus = eventBus
- cp.eventBus.Subscribe(event.EncryptedGroupMessage, cp.queue.EventChannel)
- cp.eventBus.Subscribe(event.NewGroupInvite, cp.queue.EventChannel)
- cp.eventBus.Subscribe(event.ServerStateChange, cp.queue.EventChannel)
- cp.eventBus.Subscribe(event.PeerStateChange, cp.queue.EventChannel)
- }
-
- // ImportGroup intializes a group from an imported source rather than a peer invite
- func (cp *cwtchPeer) ImportGroup(exportedInvite string) (groupID string, err error) {
- if strings.HasPrefix(exportedInvite, "torv3") {
- data, err := base64.StdEncoding.DecodeString(exportedInvite[5+44:])
- if err == nil {
- cpp := &protocol.CwtchPeerPacket{}
- err = proto.Unmarshal(data, cpp)
- if err == nil {
- jsobj, err := proto.Marshal(cpp.GetGroupChatInvite())
- if err == nil {
- cp.eventBus.Publish(event.NewEvent(event.NewGroupInvite, map[event.Field]string{
- event.GroupInvite: string(jsobj),
- }))
- } else {
- log.Errorf("error serializing group: %v", err)
- }
- return cpp.GroupChatInvite.GetGroupName(), nil
- }
- }
- } else {
- err = errors.New("unsupported exported group type")
- }
- return
- }
-
- // ExportGroup serializes a group invite so it can be given offline
- func (cp *cwtchPeer) ExportGroup(groupID string) (string, error) {
- group := cp.Profile.GetGroupByGroupID(groupID)
- if group != nil {
- invite, err := group.Invite(group.GetInitialMessage())
- if err == nil {
- exportedInvite := "torv3" + base64.StdEncoding.EncodeToString(cp.Profile.Ed25519PublicKey) + base64.StdEncoding.EncodeToString(invite)
- return exportedInvite, err
- }
- }
- 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) {
- groupID, invite, err = cp.Profile.StartGroupWithMessage(server, initialMessage)
- 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.Data: string(jsobj),
- }))
- }
- } else {
- log.Errorf("error creating group: %v", err)
- }
- return
- }
-
- // GetGroups returns an unordered list of all group IDs.
- func (cp *cwtchPeer) GetGroups() []string {
- return cp.Profile.GetGroups()
- }
-
- // GetGroup returns a pointer to a specific group, nil if no group exists.
- func (cp *cwtchPeer) GetGroup(groupID string) *model.Group {
- return cp.Profile.GetGroupByGroupID(groupID)
- }
-
- func (cp *cwtchPeer) AddContact(nick, onion string, trusted bool) {
- decodedPub, _ := base32.StdEncoding.DecodeString(strings.ToUpper(onion))
- pp := &model.PublicProfile{Name: nick, Ed25519PublicKey: decodedPub, Trusted: trusted, Blocked: false, Onion: onion, Attributes: map[string]string{"nick": nick}}
- cp.Profile.AddContact(onion, pp)
- pd, _ := json.Marshal(pp)
- cp.eventBus.Publish(event.NewEvent(event.PeerCreated, map[event.Field]string{
- event.Data: string(pd),
- event.RemotePeer: onion,
- }))
- }
-
- // GetContacts returns an unordered list of onions
- func (cp *cwtchPeer) GetContacts() []string {
- return cp.Profile.GetContacts()
- }
-
- // GetContact returns a given contact, nil is no such contact exists
- func (cp *cwtchPeer) GetContact(onion string) *model.PublicProfile {
- contact, _ := cp.Profile.GetContact(onion)
- return contact
- }
-
- // GetProfile returns the profile associated with this cwtchPeer.
- func (cp *cwtchPeer) GetProfile() *model.Profile {
- return cp.Profile
- }
-
- func (cp *cwtchPeer) GetPeerState(onion string) connections.ConnectionState {
- return connections.ConnectionStateType[cp.Profile.Contacts[onion].State]
- }
-
- func (cp *cwtchPeer) GetGroupState(groupid string) connections.ConnectionState {
- return connections.ConnectionStateType[cp.Profile.Groups[groupid].State]
- }
-
- // PeerWithOnion is the entry point for cwtchPeer relationships
- func (cp *cwtchPeer) PeerWithOnion(onion string) *connections.PeerPeerConnection {
- cp.eventBus.Publish(event.NewEvent(event.PeerRequest, map[event.Field]string{event.RemotePeer: onion}))
- return nil
- }
-
- // InviteOnionToGroup kicks off the invite process
- func (cp *cwtchPeer) InviteOnionToGroup(onion string, groupid string) error {
- group := cp.Profile.GetGroupByGroupID(groupid)
- if group == nil {
- return errors.New("invalid group id")
- }
-
- invite, err := group.Invite(group.InitialMessage)
- if err == nil {
- cp.eventBus.Publish(event.NewEvent(event.InvitePeerToGroup, map[event.Field]string{event.RemotePeer: onion, event.GroupInvite: string(invite)}))
- }
- return err
- }
-
- // JoinServer manages a new server connection with the given onion address
- func (cp *cwtchPeer) JoinServer(onion string) {
- cp.eventBus.Publish(event.NewEvent(event.JoinServer, map[event.Field]string{event.GroupServer: onion}))
- }
-
- // 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
- }
-
- // SendMessageToGroup 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) {
- group := cp.Profile.GetGroupByGroupID(groupid)
- if group == nil {
- return "", errors.New("invalid group id")
- }
- 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)}))
- }
-
- return string(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)
- return event.EventID
- }
-
- // TrustPeer sets an existing peer relationship to trusted
- func (cp *cwtchPeer) TrustPeer(peer string) error {
- err := cp.Profile.TrustPeer(peer)
- if err == nil {
- cp.PeerWithOnion(peer)
- }
- return err
- }
-
- // BlockPeer blocks an existing peer relationship.
- func (cp *cwtchPeer) BlockPeer(peer string) error {
- err := cp.Profile.BlockPeer(peer)
- cp.eventBus.Publish(event.NewEvent(event.BlockPeer, map[event.Field]string{event.RemotePeer: peer}))
- return err
- }
-
- // AcceptInvite accepts a given existing group invite
- func (cp *cwtchPeer) AcceptInvite(groupID string) error {
- err := cp.Profile.AcceptInvite(groupID)
- if err != nil {
- return err
- }
- cp.eventBus.Publish(event.NewEvent(event.AcceptGroupInvite, map[event.Field]string{event.GroupID: groupID}))
- cp.JoinServer(cp.Profile.Groups[groupID].GroupServer)
-
- return nil
- }
-
- // RejectInvite rejects a given group invite.
- func (cp *cwtchPeer) RejectInvite(groupID string) {
- cp.Profile.RejectInvite(groupID)
- }
-
- // Listen makes the peer open a listening port to accept incoming connections (and be detactably online)
- func (cp *cwtchPeer) Listen() {
- cp.eventBus.Publish(event.NewEvent(event.ProtocolEngineStartListen, map[event.Field]string{}))
- }
-
- // StartGroupConnections attempts to connect to all group servers (thus initiating reconnect attempts in the conectionsmanager)
- func (cp *cwtchPeer) StartPeersConnections() {
- for _, contact := range cp.GetContacts() {
- cp.PeerWithOnion(contact)
- }
- }
-
- // StartPeerConnections attempts to connect to all peers (thus initiating reconnect attempts in the conectionsmanager)
- func (cp *cwtchPeer) StartGroupConnections() {
- joinedServers := map[string]bool{}
- for _, groupID := range cp.GetGroups() {
- // Only send a join server packet if we haven't joined this server yet...
- group := cp.GetGroup(groupID)
- if joined := joinedServers[groupID]; group.Accepted && !joined {
- cp.JoinServer(group.GroupServer)
- joinedServers[group.GroupServer] = true
- }
- }
- }
-
- // Shutdown kills all connections and cleans up all goroutines for the peer
- func (cp *cwtchPeer) Shutdown() {
- cp.shutdown = true
- cp.queue.Shutdown()
- }
-
- // IsStarted returns true if Listen() has successfully been run before on this connection (ever). TODO: we will need to properly unset this flag on error if we want to support resumption in the future
- func (cp *cwtchPeer) IsStarted() bool {
- return cp.started
- }
-
- // eventHandler process events from other subsystems
- func (cp *cwtchPeer) eventHandler() {
- for {
- ev := cp.queue.Next()
- switch ev.EventType {
- case event.EncryptedGroupMessage:
- ok, groupID, message, seen := cp.Profile.AttemptDecryption([]byte(ev.Data[event.Ciphertext]), []byte(ev.Data[event.Signature]))
- 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}))
- }
- case event.NewGroupInvite:
- var groupInvite protocol.GroupChatInvite
- proto.Unmarshal([]byte(ev.Data[event.GroupInvite]), &groupInvite)
- cp.Profile.ProcessInvite(&groupInvite, ev.Data[event.RemotePeer])
- case event.PeerStateChange:
- if _, exists := cp.Profile.Contacts[ev.Data[event.RemotePeer]]; exists {
- cp.Profile.Contacts[ev.Data[event.RemotePeer]].State = ev.Data[event.ConnectionState]
- }
- case event.ServerStateChange:
- for _, group := range cp.Profile.Groups {
- if group.GroupServer == ev.Data[event.GroupServer] {
- group.State = ev.Data[event.ConnectionState]
- }
- }
- default:
- if ev.EventType != "" {
- log.Errorf("peer event handler received an event it was not subscribed for: %v", ev.EventType)
- }
- return
- }
- }
- }
|