diff --git a/backend/rpc.go b/backend/rpc.go index afffe27..61d13f9 100644 --- a/backend/rpc.go +++ b/backend/rpc.go @@ -120,7 +120,17 @@ func (s *RpcServer) MonitorContacts(req *rpc.MonitorContactsRequest, stream rpc. } func (s *RpcServer) AddContactRequest(ctx context.Context, req *rpc.ContactRequest) (*rpc.Contact, error) { - return nil, NotImplementedError + contactList := s.core.Identity.ContactList() + if req.Direction != rpc.ContactRequest_OUTBOUND { + return nil, errors.New("Request must be outbound") + } + + contact, err := contactList.AddContactRequest(req.Address, req.Nickname, req.FromNickname, req.Text) + if err != nil { + return nil, err + } + + return contact.Data(), nil } func (s *RpcServer) UpdateContact(ctx context.Context, req *rpc.Contact) (*rpc.Contact, error) { diff --git a/core/config.go b/core/config.go index a0fae10..71eb22f 100644 --- a/core/config.go +++ b/core/config.go @@ -9,6 +9,10 @@ import ( "sync" ) +// XXX This is partially but not fully compatible with Ricochet's JSON +// configs. It might be better to be explicitly not compatible, but have +// an automatic import function. + type Config struct { path string filePath string @@ -28,15 +32,24 @@ type ConfigRoot struct { type ConfigContact struct { Hostname string - LastConnected string + LastConnected string `json:",omitempty"` Nickname string WhenCreated string + Request ConfigContactRequest `json:",omitempty"` +} + +type ConfigContactRequest struct { + Pending bool + MyNickname string + Message string + WhenDelivered string `json:",omitempty"` + WhenRejected string `json:",omitempty"` + RemoteError string `json:",omitempty"` } type ConfigIdentity struct { - DataDirectory string - HostnameBlacklist []string - ServiceKey string + DataDirectory string `json:",omitempty"` + ServiceKey string } func LoadConfig(configPath string) (*Config, error) { diff --git a/core/contact.go b/core/contact.go index dbb7a70..708f744 100644 --- a/core/contact.go +++ b/core/contact.go @@ -2,9 +2,9 @@ package core import ( "fmt" - protocol "github.com/s-rah/go-ricochet" "github.com/ricochet-im/ricochet-go/core/utils" "github.com/ricochet-im/ricochet-go/rpc" + protocol "github.com/s-rah/go-ricochet" "golang.org/x/net/context" "log" "strconv" @@ -16,6 +16,9 @@ import ( // XXX There is generally a lot of duplication and boilerplate between // Contact, ConfigContact, and rpc.Contact. This should be reduced somehow. +// XXX Consider replacing the config contact with the protobuf structure, +// and extending the protobuf structure for everything it needs. + type Contact struct { core *Ricochet @@ -49,9 +52,20 @@ func ContactFromConfig(core *Ricochet, id int, data ConfigContact, events *utils return nil, fmt.Errorf("Invalid contact hostname '%s", data.Hostname) } - // XXX Should have some global trigger that starts all contact connections - // at the right time - go contact.contactConnection() + if data.Request.Pending { + if data.Request.WhenRejected != "" { + contact.status = ricochet.Contact_REJECTED + } else { + contact.status = ricochet.Contact_REQUEST + } + } + + // XXX Ugly and fragile way to inhibit connections + if contact.status != ricochet.Contact_REJECTED { + // XXX Should have some global trigger that starts all contact connections + // at the right time + go contact.contactConnection() + } return contact, nil } @@ -109,6 +123,15 @@ func (c *Contact) Data() *ricochet.Contact { LastConnected: c.data.LastConnected, Status: c.status, } + if c.data.Request.Pending { + data.Request = &ricochet.ContactRequest{ + Direction: ricochet.ContactRequest_OUTBOUND, + Address: data.Address, + Nickname: data.Nickname, + Text: c.data.Request.Message, + FromNickname: c.data.Request.MyNickname, + } + } return data } @@ -298,10 +321,24 @@ func (c *Contact) setConnection(conn *protocol.OpenConnection) error { // XXX implement this c.connection = conn - c.status = ricochet.Contact_ONLINE log.Printf("Assigned connection %v to contact %v", c.connection, c) - // XXX implicit accept contact requests + if c.data.Request.Pending { + if conn.Client { + // XXX Need to check knownContact flag in authentication and implicit accept also + // Outbound connection for contact request; send request message + // XXX hardcoded channel ID + log.Printf("Sending outbound contact request to %v", c) + conn.SendContactRequest(5, c.data.Request.MyNickname, c.data.Request.Message) + } else { + // Inbound connection for contact request; implicitly accept request + // and continue as contact + log.Printf("Contact request implicitly accepted by incoming connection for contact %v", c) + c.requestAccepted() + } + } else { + c.status = ricochet.Contact_ONLINE + } // Update LastConnected time config := c.core.Config.OpenWrite() @@ -391,6 +428,20 @@ func (c *Contact) shouldReplaceConnection(conn *protocol.OpenConnection) bool { return false } +// Assumes mutex is held, and assumes the caller will send the UPDATE event +func (c *Contact) requestAccepted() { + config := c.core.Config.OpenWrite() + c.data.Request = ConfigContactRequest{} + config.Contacts[strconv.Itoa(c.id)] = c.data + config.Save() + + if c.connection != nil { + c.status = ricochet.Contact_ONLINE + } else { + c.status = ricochet.Contact_UNKNOWN + } +} + // XXX also will go away during protocol API rework func (c *Contact) OnConnectionAuthenticated(conn *protocol.OpenConnection) { c.connChannel <- conn diff --git a/core/contactlist.go b/core/contactlist.go index 89da429..7aae165 100644 --- a/core/contactlist.go +++ b/core/contactlist.go @@ -7,9 +7,12 @@ import ( "github.com/ricochet-im/ricochet-go/rpc" "strconv" "sync" + "time" ) type ContactList struct { + core *Ricochet + mutex sync.RWMutex events *utils.Publisher @@ -18,6 +21,7 @@ type ContactList struct { func LoadContactList(core *Ricochet) (*ContactList, error) { list := &ContactList{ + core: core, events: utils.CreatePublisher(), } @@ -76,8 +80,71 @@ func (this *ContactList) ContactByAddress(address string) *Contact { return nil } -func (this *ContactList) AddContact(address string, name string) (*Contact, error) { - return nil, errors.New("Not implemented") +func (this *ContactList) AddContactRequest(address, name, fromName, text string) (*Contact, error) { + this.mutex.Lock() + defer this.mutex.Unlock() + + // XXX check that address is valid before relying on format below + // XXX validity checks on name/text also useful + + for _, contact := range this.contacts { + if contact.Address() == address { + return nil, errors.New("Contact already exists with this address") + } + if contact.Nickname() == name { + return nil, errors.New("Contact already exists with this nickname") + } + } + + // XXX check inbound requests + + // Write new contact into config + config := this.core.Config.OpenWrite() + + maxContactId := 0 + for idstr, _ := range config.Contacts { + if id, err := strconv.Atoi(idstr); err == nil { + if maxContactId < id { + maxContactId = id + } + } + } + + contactId := maxContactId + 1 + configContact := ConfigContact{ + Hostname: address[9:] + ".onion", + Nickname: name, + WhenCreated: time.Now().Format(time.RFC3339), + Request: ConfigContactRequest{ + Pending: true, + MyNickname: fromName, + Message: text, + }, + } + + config.Contacts[strconv.Itoa(contactId)] = configContact + if err := config.Save(); err != nil { + return nil, err + } + + // Create Contact + // XXX This starts connection immediately, which could cause contact update + // events before the add event in an unlikely race case + contact, err := ContactFromConfig(this.core, contactId, configContact, this.events) + if err != nil { + return nil, err + } + this.contacts[contactId] = contact + + event := ricochet.ContactEvent{ + Type: ricochet.ContactEvent_ADD, + Subject: &ricochet.ContactEvent_Contact{ + Contact: contact.Data(), + }, + } + this.events.Publish(event) + + return contact, nil } func (this *ContactList) RemoveContact(contact *Contact) error { diff --git a/core/identity.go b/core/identity.go index c7e95af..45097f6 100644 --- a/core/identity.go +++ b/core/identity.go @@ -4,8 +4,8 @@ import ( "crypto/rsa" "encoding/base64" "errors" - protocol "github.com/s-rah/go-ricochet" "github.com/ricochet-im/ricochet-go/core/utils" + protocol "github.com/s-rah/go-ricochet" "github.com/yawning/bulb/utils/pkcs1" "log" "sync"