package peer import ( "cwtch.im/cwtch/model/constants" "encoding/base64" "encoding/json" "errors" "fmt" "git.openprivacy.ca/cwtch.im/tapir/primitives" "git.openprivacy.ca/openprivacy/connectivity" "runtime" "strconv" "strings" "sync" "time" "cwtch.im/cwtch/event" "cwtch.im/cwtch/model" "cwtch.im/cwtch/model/attr" "cwtch.im/cwtch/protocol/connections" "cwtch.im/cwtch/protocol/files" "git.openprivacy.ca/openprivacy/connectivity/tor" "git.openprivacy.ca/openprivacy/log" ) 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.SendMessageToPeerError: true, event.SendMessageToGroupError: true, event.NewGetValMessageFromPeer: true, event.NewRetValMessageFromPeer: true, event.ProtocolEngineStopped: true, event.RetryServerRequest: true, event.ManifestSizeReceived: true, event.ManifestReceived: 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 listenStatus bool storage *CwtchProfileStorage state map[string]connections.ConnectionState queue event.Queue eventBus event.Manager } // GenerateProtocolEngine // Status: New in 1.5 func (cp *cwtchPeer) GenerateProtocolEngine(acn connectivity.ACN, bus event.Manager) connections.Engine { return connections.NewProtocolEngine(cp.GetIdentity(), cp.Profile.Ed25519PrivateKey, acn, bus, cp.Profile.ContactsAuthorizations()) } // GetIdentity // Status: New in 1.5 func (cp *cwtchPeer) GetIdentity() primitives.Identity { return primitives.InitializeIdentity("", &cp.Profile.Ed25519PrivateKey, &cp.Profile.Ed25519PublicKey) } // SendScopedZonedGetValToContact // Status: No change in 1.5 func (cp *cwtchPeer) SendScopedZonedGetValToContact(handle string, scope attr.Scope, zone attr.Zone, path string) { ev := event.NewEventList(event.SendGetValMessageToPeer, event.RemotePeer, handle, event.Scope, string(scope), event.Path, string(zone.ConstructZonedPath(path))) cp.eventBus.Publish(ev) } // GetScopedZonedAttribute // Status: Ready for 1.5 func (cp *cwtchPeer) GetScopedZonedAttribute(scope attr.Scope, zone attr.Zone, key string) (string, bool) { cp.mutex.Lock() defer cp.mutex.Unlock() scopedZonedKey := scope.ConstructScopedZonedPath(zone.ConstructZonedPath(key)) log.Debugf("looking up attribute %v %v %v (%v)", scope, zone, key, scopedZonedKey) value, err := cp.storage.LoadProfileKeyValue(TypeAttribute, scopedZonedKey.ToString()) log.Debugf("found [%v] %v", string(value), err) if err != nil { return "", false } return string(value), true } // SetScopedZonedAttribute // Status: Ready for 1.5 func (cp *cwtchPeer) SetScopedZonedAttribute(scope attr.Scope, zone attr.Zone, key string, value string) { cp.mutex.Lock() defer cp.mutex.Unlock() scopedZonedKey := scope.ConstructScopedZonedPath(zone.ConstructZonedPath(key)) log.Debugf("storing attribute: %v = %v", scopedZonedKey, value) err := cp.storage.StoreProfileKeyValue(TypeAttribute, scopedZonedKey.ToString(), []byte(value)) if err != nil { log.Errorf("error setting attribute %v") } } // SendMessage is a higher level that merges sending messages to contacts and group handles // If you try to send a message to a handle that doesn't exist, malformed or an incorrect type then // this function will error // Status: TODO func (cp *cwtchPeer) SendMessage(handle string, message string) error { var ev event.Event // Group Handles are always 32 bytes in length, but we forgo any further testing here // and delegate the group existence check to EncryptMessageToGroup if len(handle) == 32 { cp.mutex.Lock() defer cp.mutex.Unlock() group := cp.Profile.GetGroup(handle) if group == nil { return errors.New("invalid group id") } // Group adds it's own sent message to timeline ct, sig, err := cp.Profile.EncryptMessageToGroup(message, handle) // Group does not exist or some other unrecoverable error... if err != nil { return err } ev = event.NewEvent(event.SendMessageToGroup, map[event.Field]string{event.GroupID: handle, event.GroupServer: group.GroupServer, event.Ciphertext: base64.StdEncoding.EncodeToString(ct), event.Signature: base64.StdEncoding.EncodeToString(sig)}) } else if tor.IsValidHostname(handle) { // We assume we are sending to a Contact. contact, err := cp.FetchConversationInfo(handle) ev = event.NewEvent(event.SendMessageToPeer, map[event.Field]string{event.RemotePeer: handle, event.Data: message}) // If the contact exists replace the event id wih the index of this message in the contacts timeline... // Otherwise assume we don't log the message in the timeline... if contact != nil && err == nil { //ev.EventID = strconv.Itoa(contact.Timeline.Len()) cp.mutex.Lock() defer cp.mutex.Unlock() cp.storage.InsertMessage(contact.ID, 0, message, model.Attributes{"ack": event.False, "sent": time.Now().String()}) } // Regardless we publish the send message to peer event for the protocol engine to execute on... // We assume this is always successful as it is always valid to attempt to // Contact a valid hostname } else { return errors.New("malformed handle type") } cp.eventBus.Publish(ev) return nil } // UpdateMessageFlags // Status: TODO 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. // Status: Ready for 1.5 func (cp *cwtchPeer) BlockUnknownConnections() { cp.eventBus.Publish(event.NewEvent(event.BlockUnknownPeers, map[event.Field]string{})) } // AllowUnknownConnections will permit connections from unknown contacts. // Status: Ready for 1.5 func (cp *cwtchPeer) AllowUnknownConnections() { cp.eventBus.Publish(event.NewEvent(event.AllowUnknownPeers, map[event.Field]string{})) } // NewProfileWithEncryptedStorage instantiates a new Cwtch Profile from encrypted storage func NewProfileWithEncryptedStorage(name string, cps *CwtchProfileStorage) CwtchPeer { cp := new(cwtchPeer) cp.shutdown = false cp.storage = cps cp.state = make(map[string]connections.ConnectionState) cp.Profile = model.GenerateNewProfile(name) // Store all the Necessary Base Attributes In The Database cp.storage.StoreProfileKeyValue(TypeAttribute, attr.PublicScope.ConstructScopedZonedPath(attr.ProfileZone.ConstructZonedPath(constants.Name)).ToString(), []byte(name)) cp.storage.StoreProfileKeyValue(TypePrivateKey, "Ed25519PrivateKey", cp.Profile.Ed25519PrivateKey) cp.storage.StoreProfileKeyValue(TypePublicKey, "Ed25519PublicKey", cp.Profile.Ed25519PublicKey) return cp } // FromEncryptedStorage loads an existing Profile from Encrypted Storage func FromEncryptedStorage(cps *CwtchProfileStorage) CwtchPeer { cp := new(cwtchPeer) cp.shutdown = false cp.storage = cps cp.state = make(map[string]connections.ConnectionState) return cp } // FromProfile generates a new peer from a profile. // Deprecated - Only to be used for importing new profiles func FromProfile(profile *model.Profile, cps *CwtchProfileStorage) CwtchPeer { cp := new(cwtchPeer) cp.Profile = profile cp.shutdown = false cp.storage = cps // Store all the Necessary Base Attributes In The Database cp.storage.StoreProfileKeyValue(TypeAttribute, "public.profile.name", []byte(profile.Name)) cp.storage.StoreProfileKeyValue(TypePrivateKey, "Ed25519PrivateKey", cp.Profile.Ed25519PrivateKey) cp.storage.StoreProfileKeyValue(TypePublicKey, "Ed25519PublicKey", cp.Profile.Ed25519PublicKey) return cp } // Init instantiates a cwtchPeer // Status: Ready for 1.5 func (cp *cwtchPeer) Init(eventBus event.Manager) { cp.InitForEvents(eventBus, DefaultEventsToHandle) // Upgrade the Cwtch Peer if necessary // It would be nice to do these checks in the storage engine itself, but it is easier to do them here // rather than duplicating the logic to construct/reconstruct attributes in storage engine... // TODO: Remove these checks after Cwtch ~1.5 storage engine is implemented if _, exists := cp.GetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants.Name); !exists { // If public.profile.name does not exist, and we have an existing public.name then: // set public.profile.name from public.name // set local.profile.name from public.name if name, exists := cp.Profile.GetAttribute(attr.GetPublicScope(constants.Name)); exists { cp.SetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants.Name, name) cp.SetScopedZonedAttribute(attr.LocalScope, attr.ProfileZone, constants.Name, name) } else { // Otherwise check if local.name exists and set it from that // If not, then check the very old unzoned, unscoped name. // If not, then set directly from Profile.Name... if name, exists := cp.Profile.GetAttribute(attr.GetLocalScope(constants.Name)); exists { cp.SetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants.Name, name) cp.SetScopedZonedAttribute(attr.LocalScope, attr.ProfileZone, constants.Name, name) } else if name, exists := cp.Profile.GetAttribute(constants.Name); exists { cp.SetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants.Name, name) cp.SetScopedZonedAttribute(attr.LocalScope, attr.ProfileZone, constants.Name, name) } else { // Profile.Name is very deprecated at this point... cp.SetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants.Name, cp.Profile.Name) cp.SetScopedZonedAttribute(attr.LocalScope, attr.ProfileZone, constants.Name, cp.Profile.Name) } } } // At this point we can safely assume that public.profile.name exists localName, _ := cp.GetScopedZonedAttribute(attr.LocalScope, attr.ProfileZone, constants.Name) publicName, _ := cp.GetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants.Name) if localName != publicName { cp.SetScopedZonedAttribute(attr.LocalScope, attr.ProfileZone, constants.Name, publicName) } // At this point we can safely assume that public.profile.name exists AND is consistent with // local.profile.name - regardless of whatever Cwtch version we have upgraded from. This will // be important after Cwtch 1.5 when we purge all previous references to local.profile.name and // profile-> name - and remove all name processing code from libcwtch-go. // If local.profile.tag does not exist then set it from deprecated GetAttribute if _, exists := cp.GetScopedZonedAttribute(attr.LocalScope, attr.ProfileZone, constants.Tag); !exists { if tag, exists := cp.Profile.GetAttribute(constants.Tag); exists { cp.SetScopedZonedAttribute(attr.LocalScope, attr.ProfileZone, constants.Tag, tag) } else { // Assume a default password, which will allow the older profile to have it's password reset by the UI cp.SetScopedZonedAttribute(attr.LocalScope, attr.ProfileZone, constants.Tag, constants.ProfileTypeV1DefaultPassword) } } } // InitForEvents // Status: Ready for 1.5 func (cp *cwtchPeer) InitForEvents(eventBus event.Manager, toBeHandled []event.Type) { cp.queue = event.NewQueue() go cp.eventHandler() cp.eventBus = eventBus cp.AutoHandleEvents(toBeHandled) } // AutoHandleEvents sets an event (if able) to be handled by this peer // Status: Ready for 1.5 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 initializes a group from an imported source rather than a peer invite // Status: TODO 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 gid, err } // NewContactConversation create a new p2p conversation with the given acl applied to the handle. func (cp *cwtchPeer) NewContactConversation(handle string, acl model.AccessControl, accepted bool) error { cp.mutex.Lock() defer cp.mutex.Unlock() return cp.storage.NewConversation(handle, model.Attributes{event.SaveHistoryKey: event.DeleteHistoryDefault}, model.AccessControlList{handle: acl}, accepted) } // AcceptConversation looks up a conversation by `handle` and sets the Accepted status to `true` // This will cause Cwtch to auto connect to this conversation on start up func (cp *cwtchPeer) AcceptConversation(id int) error { cp.mutex.Lock() defer cp.mutex.Unlock() return cp.storage.AcceptConversation(id) } func (cp *cwtchPeer) FetchConversations() ([]*model.Conversation, error) { cp.mutex.Lock() defer cp.mutex.Unlock() return cp.storage.FetchConversations() } // FetchConversationInfo returns information about the given conversation referenced by the handle func (cp *cwtchPeer) FetchConversationInfo(handle string) (*model.Conversation, error) { cp.mutex.Lock() defer cp.mutex.Unlock() return cp.storage.GetConversationByHandle(handle) } // DeleteConversation purges all data about the conversation, including message timelines, referenced by the handle func (cp *cwtchPeer) DeleteConversation(id int) error { cp.mutex.Lock() defer cp.mutex.Unlock() return cp.storage.DeleteConversation(id) } // SetConversationAttribute sets the conversation attribute at path to value func (cp *cwtchPeer) SetConversationAttribute(id int, path attr.ScopedZonedPath, value string) error { cp.mutex.Lock() defer cp.mutex.Unlock() return cp.storage.SetConversationAttribute(id, path, value) } // GetConversationAttribute is a shortcut method for retrieving the value of a given path func (cp *cwtchPeer) GetConversationAttribute(id int, path attr.ScopedZonedPath) (string, error) { cp.mutex.Lock() defer cp.mutex.Unlock() ci, err := cp.storage.GetConversation(id) if err != nil { return "", err } val, exists := ci.Attributes[path.ToString()] if !exists { return "", fmt.Errorf("%v does not exist for conversation %v", path.ToString(), id) } return val, nil } func (cp *cwtchPeer) GetChannelMessage(conversation int, channel int, id int) (string, model.Attributes, error) { cp.mutex.Lock() defer cp.mutex.Unlock() return cp.storage.GetChannelMessage(conversation, channel, id) } // ExportGroup serializes a group invite so it can be given offline // Status: TODO func (cp *cwtchPeer) ExportGroup(groupID string) (string, error) { cp.mutex.Lock() defer cp.mutex.Unlock() group := cp.Profile.GetGroup(groupID) if group != nil { 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. // Status: TODO func (cp *cwtchPeer) StartGroup(server string) (string, string, error) { cp.mutex.Lock() 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 groupID, invite, err } // GetGroups returns an unordered list of all group IDs. // Status: TODO func (cp *cwtchPeer) GetGroups() []string { cp.mutex.Lock() defer cp.mutex.Unlock() return cp.Profile.GetGroups() } // GetGroup returns a pointer to a specific group, nil if no group exists. // Status: TODO func (cp *cwtchPeer) GetGroup(groupID string) *model.Group { cp.mutex.Lock() defer cp.mutex.Unlock() return cp.Profile.GetGroup(groupID) } // AddServer takes in a serialized server specification (a bundle of related keys) and adds a contact for the // server assuming there are no errors and the contact doesn't already exist. // TODO in the future this function should also integrate with a trust provider to validate the key bundle. // Status: TODO func (cp *cwtchPeer) AddServer(serverSpecification string) error { //// This confirms that the server did at least sign the bundle //keyBundle, err := model.DeserializeAndVerify([]byte(serverSpecification)) //if err != nil { // return err //} //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, // })) // } // // // At this point we know the server exists // server := cp.GetContact(onion) // ab := keyBundle.AttributeBundle() // // // 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 { // if val != v { // // this is inconsistent! // return model.InconsistentKeyBundleError // } // } // // 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.SetContactAttribute(onion, k, v) // } // // return nil //} return nil } // GetServers returns an unordered list of servers // Status: TODO func (cp *cwtchPeer) GetServers() []string { var servers []string return servers } // GetOnion // Status: Deprecated in 1.5 func (cp *cwtchPeer) GetOnion() string { cp.mutex.Lock() defer cp.mutex.Unlock() return cp.Profile.Onion } // GetPeerState // Status: TODO func (cp *cwtchPeer) GetPeerState(onion string) (connections.ConnectionState, bool) { cp.mutex.Lock() defer cp.mutex.Unlock() if state, ok := cp.state[onion]; ok { return state, ok } return connections.DISCONNECTED, false } // GetGroupState // Status: TODO func (cp *cwtchPeer) GetGroupState(groupid string) (connections.ConnectionState, bool) { cp.mutex.Lock() defer cp.mutex.Unlock() if group, ok := cp.Profile.Groups[groupid]; ok { return connections.ConnectionStateToType()[group.State], true } return connections.DISCONNECTED, false } // PeerWithOnion initiates a request to the Protocol Engine to set up Cwtch Session with a given tor v3 onion // address. func (cp *cwtchPeer) PeerWithOnion(onion string) { cp.eventBus.Publish(event.NewEvent(event.PeerRequest, map[event.Field]string{event.RemotePeer: onion})) } // InviteOnionToGroup kicks off the invite process // Status: TODO func (cp *cwtchPeer) InviteOnionToGroup(onion string, groupid string) error { cp.mutex.Lock() group := cp.Profile.GetGroup(groupid) if group == nil { cp.mutex.Unlock() return errors.New("invalid group id") } invite, err := group.Invite() cp.mutex.Unlock() if err == nil { err = cp.SendMessage(onion, invite) } return err } // JoinServer manages a new server connection with the given onion address // Status: TODO func (cp *cwtchPeer) JoinServer(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, 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") } // ResyncServer completely tears down and resyncs a new server connection with the given onion address // Status: TODO 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") } // SendGetValToPeer // Status: Ready for 1.5 func (cp *cwtchPeer) SendGetValToPeer(onion string, scope string, path string) { ev := event.NewEventList(event.SendGetValMessageToPeer, event.RemotePeer, onion, event.Scope, scope, event.Path, path) cp.eventBus.Publish(ev) } // Listen makes the peer open a listening port to accept incoming connections (and be detectably online) // Status: Ready for 1.5 func (cp *cwtchPeer) Listen() { 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 } // StartPeersConnections attempts to connect to peer connections // Status: Ready for 1.5 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 // Status: Ready for 1.5 func (cp *cwtchPeer) StartServerConnections() { //for _, contact := range cp.GetContacts() { // if cp.GetContact(contact).IsServer() { // err := cp.JoinServer(contact) // if err != nil { // // Almost certainly a programming error so print it.. // log.Errorf("error joining server %v", err) // } // } //} } // Shutdown kills all connections and cleans up all goroutines for the peer // Status: Ready for 1.5 func (cp *cwtchPeer) Shutdown() { cp.mutex.Lock() defer cp.mutex.Unlock() cp.shutdown = true cp.queue.Shutdown() if cp.storage != nil { cp.storage.Close() } } // Status: TODO func (cp *cwtchPeer) storeMessage(handle string, message string, sent time.Time) error { // TOOD maybe atomize this? ci, err := cp.FetchConversationInfo(handle) if err != nil { err := cp.NewContactConversation(handle, model.DefaultP2PAccessControl(), false) if err != nil { return err } ci, err = cp.FetchConversationInfo(handle) if err != nil { return err } } cp.mutex.Lock() defer cp.mutex.Unlock() return cp.storage.InsertMessage(ci.ID, 0, message, model.Attributes{"ack": event.True, "sent": sent.String()}) } // ShareFile begins hosting the given serialized manifest // Status: Ready for 1.5 func (cp *cwtchPeer) ShareFile(fileKey string, serializedManifest string) { cp.eventBus.Publish(event.NewEvent(event.ShareManifest, map[event.Field]string{event.FileKey: fileKey, event.SerializedManifest: serializedManifest})) } // 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). // store the base64 encoded signature for later use //cp.SetConversationAttribute(ev.Data[event.GroupServer], lastKnownSignature, ev.Data[event.Signature]) cp.mutex.Lock() ok, groupID, message, index := cp.Profile.AttemptDecryption(ciphertext, signature) cp.mutex.Unlock() if ok && index > -1 { 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, event.Index: strconv.Itoa(index)})) } // 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.storeMessage(ev.Data[event.RemotePeer], ev.Data[event.Data], ts) case event.PeerAcknowledgement: cp.mutex.Lock() 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() 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() 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]) err := cp.JoinServer(ev.Data[event.GroupServer]) if err != nil { log.Errorf("error joining server... %v", err) } case event.NewGetValMessageFromPeer: onion := ev.Data[event.RemotePeer] scope := ev.Data[event.Scope] path := ev.Data[event.Path] log.Debugf("NewGetValMessageFromPeer for %v.%v from %v\n", scope, path, onion) conversationInfo, _ := cp.FetchConversationInfo(onion) if conversationInfo != nil && conversationInfo.Accepted { scope := attr.IntoScope(scope) if scope.IsPublic() || scope.IsConversation() { zone, zpath := attr.ParseZone(path) val, exists := cp.GetScopedZonedAttribute(scope, zone, zpath) // NOTE: Temporary Override because UI currently wipes names if it can't find them... if !exists && zone == attr.UnknownZone && path == constants.Name { val, exists = cp.GetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants.Name) } 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) } } /***** Non default but requestable handleable events *****/ case event.ManifestReceived: log.Debugf("Manifest Received Event!: %v", ev) handle := ev.Data[event.Handle] fileKey := ev.Data[event.FileKey] serializedManifest := ev.Data[event.SerializedManifest] manifestFilePath, exists := cp.GetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fmt.Sprintf("%v.manifest", fileKey)) if exists { downloadFilePath, exists := cp.GetScopedZonedAttribute(attr.LocalScope, attr.FilesharingZone, fileKey) if exists { log.Debugf("downloading manifest to %v, file to %v", manifestFilePath, downloadFilePath) var manifest files.Manifest err := json.Unmarshal([]byte(serializedManifest), &manifest) if err == nil { manifest.Title = manifest.FileName manifest.FileName = downloadFilePath log.Debugf("saving manifest") err = manifest.Save(manifestFilePath) if err != nil { log.Errorf("could not save manifest: %v", err) } else { tempFile := "" if runtime.GOOS == "android" { tempFile = manifestFilePath[0 : len(manifestFilePath)-len(".manifest")] log.Debugf("derived android temp path: %v", tempFile) } cp.eventBus.Publish(event.NewEvent(event.ManifestSaved, map[event.Field]string{ event.FileKey: fileKey, event.Handle: handle, event.SerializedManifest: string(manifest.Serialize()), event.TempFile: tempFile, event.NameSuggestion: manifest.Title, })) } } else { log.Errorf("error saving manifest: %v", err) } } else { log.Errorf("found manifest path but not download path for %v", fileKey) } } else { log.Errorf("no download path found for manifest: %v", fileKey) } case event.NewRetValMessageFromPeer: onion := ev.Data[event.RemotePeer] scope := ev.Data[event.Scope] path := ev.Data[event.Path] val := ev.Data[event.Data] exists, _ := strconv.ParseBool(ev.Data[event.Exists]) log.Debugf("NewRetValMessageFromPeer %v %v%v %v %v\n", onion, scope, path, exists, val) if exists { // Handle File Sharing Metadata // TODO This probably should be broken out to it's own code.. zone, path := attr.ParseZone(path) if attr.Scope(scope).IsConversation() && zone == attr.FilesharingZone && strings.HasSuffix(path, ".manifest.size") { fileKey := strings.Replace(path, ".manifest.size", "", 1) size, err := strconv.Atoi(val) // if size is valid and below the maximum size for a manifest // this is to prevent malicious sharers from using large amounts of memory when distributing // a manifest as we reconstruct this in-memory if err == nil && size < files.MaxManifestSize { cp.eventBus.Publish(event.NewEvent(event.ManifestSizeReceived, map[event.Field]string{event.FileKey: fileKey, event.ManifestSize: val, event.Handle: onion})) } else { cp.eventBus.Publish(event.NewEvent(event.ManifestError, map[event.Field]string{event.FileKey: fileKey, event.Handle: onion})) } } // Allow public profile parameters to be added as peer specific attributes... if attr.Scope(scope).IsPublic() && zone == attr.ProfileZone { ci, err := cp.FetchConversationInfo(onion) if ci != nil && err != nil { cp.SetConversationAttribute(ci.ID, attr.Scope(scope).ConstructScopedZonedPath(zone.ConstructZonedPath(path)), val) } } } case event.PeerStateChange: cp.mutex.Lock() cp.state[ev.Data[event.RemotePeer]] = connections.ConnectionStateToType()[ev.Data[event.ConnectionState]] cp.mutex.Unlock() case event.ServerStateChange: cp.mutex.Lock() cp.state[ev.Data[event.GroupServer]] = connections.ConnectionStateToType()[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) } return } } }