package peer import ( "cwtch.im/cwtch/event" "cwtch.im/cwtch/model" "cwtch.im/cwtch/protocol/connections" "encoding/base32" "encoding/base64" "encoding/json" "errors" "git.openprivacy.ca/openprivacy/libricochet-go/log" "strings" "sync" "time" ) 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} // cwtchPeer manages incoming and outgoing connections and all processing for a Cwtch cwtchPeer type cwtchPeer struct { Profile *model.Profile mutex sync.Mutex shutdown 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 TrustPeer(string) error BlockPeer(string) error UnblockPeer(string) error AcceptInvite(string) error RejectInvite(string) DeleteContact(string) DeleteGroup(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) 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 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) 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 cp.shutdown = false return cp } // Init instantiates a cwtchPeer func (cp *cwtchPeer) Init(eventBus event.Manager) { cp.queue = event.NewQueue() go cp.eventHandler() cp.eventBus = eventBus cp.AutoHandleEvents([]event.Type{event.EncryptedGroupMessage, event.NewMessageFromPeer, event.PeerAcknowledgement, event.PeerError, event.SendMessageToGroupError}) } // AutoHandleEvents sets an event (if able) to be handled by this peer func (cp *cwtchPeer) AutoHandleEvents(events []event.Type) { for _, ev := range events { if _, exists := autoHandleableEvents[ev]; exists { cp.eventBus.Subscribe(ev, cp.queue) } else { log.Errorf("Peer asked to autohandle event it cannot: %v\n", ev) } } } // 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), })) } else { log.Errorf("error decoding group invite: %v", err) } return nil } return errors.New("unsupported exported group type") } // ExportGroup serializes a group invite so it can be given offline func (cp *cwtchPeer) ExportGroup(groupID string) (string, error) { 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 "", 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.GetGroup(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.ConnectionStateToType[cp.Profile.Contacts[onion].State] } func (cp *cwtchPeer) GetGroupState(groupid string) connections.ConnectionState { return connections.ConnectionStateToType[cp.Profile.Groups[groupid].State] } // PeerWithOnion is the entry point for cwtchPeer relationships func (cp *cwtchPeer) PeerWithOnion(onion string) { cp.eventBus.Publish(event.NewEvent(event.PeerRequest, map[event.Field]string{event.RemotePeer: onion})) } // DeleteContact deletes a peer from the profile, storage, and handling func (cp *cwtchPeer) DeleteContact(onion string) { cp.Profile.DeleteContact(onion) cp.eventBus.Publish(event.NewEventList(event.DeleteContact, event.RemotePeer, onion)) } // DeleteGroup deletes a Group from the profile, storage, and handling func (cp *cwtchPeer) DeleteGroup(groupID string) { cp.Profile.DeleteGroup(groupID) cp.eventBus.Publish(event.NewEventList(event.DeleteGroup, event.GroupID, groupID)) } // InviteOnionToGroup kicks off the invite process func (cp *cwtchPeer) InviteOnionToGroup(onion string, groupid string) error { group := cp.Profile.GetGroup(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.GetGroup(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) cp.Profile.AddSentMessageToContactTimeline(onion, message, time.Now(), event.EventID) 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 } // UnblockPeer blocks an existing peer relationship. func (cp *cwtchPeer) UnblockPeer(peer string) error { err := cp.Profile.UnblockPeer(peer) cp.eventBus.Publish(event.NewEvent(event.UnblockPeer, 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() { log.Debugf("cwtchPeer Listen sending ProtocolEngineStartListen\n") cp.eventBus.Publish(event.NewEvent(event.ProtocolEngineStartListen, map[event.Field]string{event.Onion: cp.Profile.Onion})) } // 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 { log.Infof("Join Server %v (%v)\n", group.GroupServer, joined) cp.JoinServer(group.GroupServer) joinedServers[group.GroupServer] = true } } } // SetAttribute sets an attribute for this profile and emits an event func (cp *cwtchPeer) SetAttribute(key string, val string) { cp.Profile.SetAttribute(key, val) cp.eventBus.Publish(event.NewEvent(event.SetAttribute, map[event.Field]string{ event.Key: key, event.Data: val, })) } // GetAttribute gets an attribute for the profile func (cp *cwtchPeer) GetAttribute(key string) (string, bool) { return cp.Profile.GetAttribute(key) } // SetContactAttribute sets an attribute for the indicated contact and emits an event func (cp *cwtchPeer) SetContactAttribute(onion string, key string, val string) { if contact, ok := cp.Profile.GetContact(onion); ok { contact.SetAttribute(key, val) cp.eventBus.Publish(event.NewEvent(event.SetPeerAttribute, map[event.Field]string{ event.RemotePeer: onion, event.Key: key, event.Data: val, })) } } // GetContactAttribute gets an attribute for the indicated contact func (cp *cwtchPeer) GetContactAttribute(onion string, key string) (string, bool) { if contact, ok := cp.Profile.GetContact(onion); ok { return contact.GetAttribute(key) } return "", false } // SetGroupAttribute sets an attribute for the indicated group and emits an event func (cp *cwtchPeer) SetGroupAttribute(gid string, key string, val string) { if group := cp.Profile.GetGroup(gid); group != nil { group.SetAttribute(key, val) cp.eventBus.Publish(event.NewEvent(event.SetGroupAttribute, map[event.Field]string{ event.GroupID: gid, event.Key: key, event.Data: val, })) } } // GetGroupAttribute gets an attribute for the indicated group func (cp *cwtchPeer) GetGroupAttribute(gid string, key string) (string, bool) { if group := cp.Profile.GetGroup(gid); group != nil { return group.GetAttribute(key) } return "", false } // Shutdown kills all connections and cleans up all goroutines for the peer func (cp *cwtchPeer) Shutdown() { cp.shutdown = true cp.queue.Shutdown() } // eventHandler process events from other subsystems func (cp *cwtchPeer) eventHandler() { for { ev := cp.queue.Next() switch ev.EventType { /***** Default auto handled events *****/ case event.EncryptedGroupMessage: // If successful, a side effect is the message is added to the group's timeline 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.NewMessageFromPeer: //event.TimestampReceived, event.RemotePeer, event.Data ts, _ := time.Parse(time.RFC3339Nano, ev.Data[event.TimestampReceived]) cp.Profile.AddMessageToContactTimeline(ev.Data[event.RemotePeer], ev.Data[event.Data], ts) case event.PeerAcknowledgement: cp.Profile.AckSentMessageToPeer(ev.Data[event.RemotePeer], ev.Data[event.EventID]) case event.SendMessageToGroupError: cp.Profile.AddGroupSentMessageError(ev.Data[event.GroupServer], ev.Data[event.Signature], ev.Data[event.Error]) case event.SendMessageToPeerError: cp.Profile.ErrorSentMessageToPeer(ev.Data[event.RemotePeer], ev.Data[event.EventID], ev.Data[event.Error]) /***** Non default but requestable handlable events *****/ case event.NewGroupInvite: cp.Profile.ProcessInvite(ev.Data[event.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 } } }