core: Reuse protobuf structures for configuration

The existing configuration was partially compatible with Ricochet's,
but not enough to actually be useful. It also required a bunch of
boilerplate code to copy data between configuration data structures,
internal data structures, and RPC data structures.

Protobuf conveniently supports encoding messages to JSON, and we already
need to store most data (e.g. contacts) in protobuf structures. This
commit changes the configuration to be a protobuf JSON serialization of
the Config message, which can directly reuse RPC messages like Contact.

Additionally, the RWMutex-based configuration type was a deadlock
waiting to happen. There is now a read-only clone of the configuration
available atomically at any time. Writers need an exclusive lock on the
ConfigFile object, which commits its changes to disk and readers on
unlock.
This commit is contained in:
John Brooks 2017-09-24 14:15:53 -06:00
parent 914163c7de
commit 32230b77c1
13 changed files with 388 additions and 335 deletions

View File

@ -1,177 +0,0 @@
package core
import (
"encoding/json"
"io/ioutil"
"log"
"os"
"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 {
filePath string
root ConfigRoot
mutex sync.RWMutex
}
type ConfigRoot struct {
Contacts map[string]ConfigContact
Identity ConfigIdentity
// Not used by the permanent instance in Config
writable bool
config *Config
}
type ConfigContact struct {
Hostname 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 `json:",omitempty"`
ServiceKey string
}
func LoadConfig(filePath string) (*Config, error) {
config := &Config{
filePath: filePath,
}
data, err := ioutil.ReadFile(config.filePath)
if err != nil {
log.Printf("Config read error from %s: %v", config.filePath, err)
return nil, err
}
if err := json.Unmarshal(data, &config.root); err != nil {
log.Printf("Config parse error: %v", err)
return nil, err
}
return config, nil
}
// Return a read-only snapshot of the current configuration. This object
// _must not_ be stored, and you must call Close() when finished with it.
// This function may block.
func (c *Config) OpenRead() *ConfigRoot {
c.mutex.RLock()
root := c.root.Clone()
root.writable = false
root.config = c
return root
}
// Return a writable snapshot of the current configuration. This object
// _must not_ be stored, and you must call Save() or Discard() when
// finished with it. This function may block.
func (c *Config) OpenWrite() *ConfigRoot {
c.mutex.Lock()
root := c.root.Clone()
root.writable = true
root.config = c
return root
}
func (root *ConfigRoot) Clone() *ConfigRoot {
re := *root
re.Contacts = make(map[string]ConfigContact)
for k, v := range root.Contacts {
re.Contacts[k] = v
}
return &re
}
func (root *ConfigRoot) Close() {
if root.writable {
log.Panic("Close called on writable config object; use Save or Discard")
}
if root.config == nil {
log.Panic("Close called on closed config object")
}
root.config.mutex.RUnlock()
root.config = nil
}
// Save writes the state to disk, and updates the Config object if
// successful. Changes to the object are discarded on error.
func (root *ConfigRoot) Save() error {
if !root.writable {
log.Panic("Save called on read-only config object")
}
if root.config == nil {
log.Panic("Save called on closed config object")
}
c := root.config
root.writable = false
root.config = nil
err := c.save(root)
c.mutex.Unlock()
return err
}
// Discard closes a config write without saving any changes to disk
// or to the Config object.
func (root *ConfigRoot) Discard() {
if !root.writable {
log.Panic("Discard called on read-only config object")
}
if root.config == nil {
log.Panic("Discard called on closed config object")
}
c := root.config
root.config = nil
c.mutex.Unlock()
}
func (c *Config) save(newRoot *ConfigRoot) error {
data, err := json.MarshalIndent(newRoot, "", " ")
if err != nil {
log.Printf("Config encoding error: %v", err)
return err
}
// Make a pathetic attempt at atomic file write by writing into a
// temporary file and renaming over the original; this is probably
// imperfect as-implemented, but better than truncating and writing
// directly.
tempPath := c.filePath + ".new"
file, err := os.OpenFile(tempPath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0600)
if err != nil {
log.Printf("Config save error: %v", err)
return err
}
if _, err := file.Write(data); err != nil {
log.Printf("Config write error: %v", err)
file.Close()
return err
}
file.Close()
if err := os.Rename(tempPath, c.filePath); err != nil {
log.Printf("Config replace error: %v", err)
return err
}
c.root = *newRoot
return nil
}

115
core/config/config.go Normal file
View File

@ -0,0 +1,115 @@
package config
import (
"github.com/golang/protobuf/jsonpb"
"github.com/golang/protobuf/proto"
"github.com/ricochet-im/ricochet-go/rpc"
"log"
"os"
"sync"
"sync/atomic"
)
type ConfigFile struct {
filePath string
root *ricochet.Config
readSnapshot atomic.Value
mutex sync.Mutex
}
func NewConfigFile(path string) (*ConfigFile, error) {
cfg := &ConfigFile{
filePath: path,
root: &ricochet.Config{},
}
cfg.readSnapshot.Store(cfg.root)
if err := cfg.save(); err != nil {
return nil, err
}
return cfg, nil
}
func LoadConfigFile(path string) (*ConfigFile, error) {
cfg := &ConfigFile{
filePath: path,
root: &ricochet.Config{},
}
file, err := os.Open(cfg.filePath)
if err != nil {
return nil, err
}
defer file.Close()
json := jsonpb.Unmarshaler{
AllowUnknownFields: true,
}
if err := json.Unmarshal(file, cfg.root); err != nil {
return nil, err
}
cfg.readSnapshot.Store(cfg.root)
return cfg, nil
}
// Read returns a **read-only** snapshot of the current configuration. This
// function is threadsafe, and the values in this instance of the configuration
// will not change when the configuration changes.
//
// Do not under any circumstances modify any part of this object.
func (cfg *ConfigFile) Read() *ricochet.Config {
return cfg.readSnapshot.Load().(*ricochet.Config)
}
// Lock gains exclusive control of the configuration state, allowing the caller
// to safely make changes to the configuration. Concurrent readers will not see
// any changes until Unlock() is called, at which point they will atomically be
// visible in future calls to Read() as well as being saved persistently.
func (cfg *ConfigFile) Lock() *ricochet.Config {
cfg.mutex.Lock()
// Clone the current configuration for a mutable copy
cfg.root = proto.Clone(cfg.root).(*ricochet.Config)
return cfg.root
}
func (cfg *ConfigFile) Unlock() {
// Clone cfg.root again to guarantee that any messages are detached from
// instances that exist elsewhere in the code. Inefficient but safe.
cfg.root = proto.Clone(cfg.root).(*ricochet.Config)
cfg.readSnapshot.Store(cfg.root)
err := cfg.save()
if err != nil {
log.Printf("WARNING: Unable to save configuration: %s", err)
}
cfg.mutex.Unlock()
}
func (cfg *ConfigFile) save() error {
json := jsonpb.Marshaler{Indent: " "}
// Make a pathetic attempt at atomic file write by writing into a
// temporary file and renaming over the original; this is probably
// imperfect as-implemented, but better than truncating and writing
// directly.
tempPath := cfg.filePath + ".new"
file, err := os.OpenFile(tempPath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0600)
if err != nil {
log.Printf("Config save error: %v", err)
return err
}
err = json.Marshal(file, cfg.root)
if err != nil {
log.Printf("Config encoding error: %v", err)
file.Close()
return err
}
file.Close()
if err := os.Rename(tempPath, cfg.filePath); err != nil {
log.Printf("Config replace error: %v", err)
return err
}
return nil
}

View File

@ -2,6 +2,7 @@ package core
import ( import (
"fmt" "fmt"
"github.com/golang/protobuf/proto"
"github.com/ricochet-im/ricochet-go/core/utils" "github.com/ricochet-im/ricochet-go/core/utils"
"github.com/ricochet-im/ricochet-go/rpc" "github.com/ricochet-im/ricochet-go/rpc"
protocol "github.com/s-rah/go-ricochet" protocol "github.com/s-rah/go-ricochet"
@ -14,18 +15,11 @@ import (
"time" "time"
) )
// 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 { type Contact struct {
core *Ricochet core *Ricochet
id int id int
data ConfigContact data *ricochet.Contact
status ricochet.Contact_Status
mutex sync.Mutex mutex sync.Mutex
events *utils.Publisher events *utils.Publisher
@ -41,7 +35,7 @@ type Contact struct {
conversation *Conversation conversation *Conversation
} }
func ContactFromConfig(core *Ricochet, id int, data ConfigContact, events *utils.Publisher) (*Contact, error) { func ContactFromConfig(core *Ricochet, id int, data *ricochet.Contact, events *utils.Publisher) (*Contact, error) {
contact := &Contact{ contact := &Contact{
core: core, core: core,
id: id, id: id,
@ -53,16 +47,18 @@ func ContactFromConfig(core *Ricochet, id int, data ConfigContact, events *utils
if id < 0 { if id < 0 {
return nil, fmt.Errorf("Invalid contact ID '%d'", id) return nil, fmt.Errorf("Invalid contact ID '%d'", id)
} else if !IsOnionValid(data.Hostname) { } else if !IsAddressValid(data.Address) {
return nil, fmt.Errorf("Invalid contact hostname '%s", data.Hostname) return nil, fmt.Errorf("Invalid contact address '%s", data.Address)
} }
if data.Request.Pending { if data.Request != nil {
if data.Request.WhenRejected != "" { if data.Request.Rejected {
contact.status = ricochet.Contact_REJECTED contact.data.Status = ricochet.Contact_REJECTED
} else { } else {
contact.status = ricochet.Contact_REQUEST contact.data.Status = ricochet.Contact_REQUEST
} }
} else if contact.data.Status != ricochet.Contact_REJECTED {
contact.data.Status = ricochet.Contact_UNKNOWN
} }
return contact, nil return contact, nil
@ -81,14 +77,14 @@ func (c *Contact) Nickname() string {
func (c *Contact) Address() string { func (c *Contact) Address() string {
c.mutex.Lock() c.mutex.Lock()
defer c.mutex.Unlock() defer c.mutex.Unlock()
address, _ := AddressFromOnion(c.data.Hostname) return c.data.Address
return address
} }
func (c *Contact) Hostname() string { func (c *Contact) Hostname() string {
c.mutex.Lock() c.mutex.Lock()
defer c.mutex.Unlock() defer c.mutex.Unlock()
return c.data.Hostname hostname, _ := OnionFromAddress(c.data.Address)
return hostname
} }
func (c *Contact) LastConnected() time.Time { func (c *Contact) LastConnected() time.Time {
@ -108,47 +104,28 @@ func (c *Contact) WhenCreated() time.Time {
func (c *Contact) Status() ricochet.Contact_Status { func (c *Contact) Status() ricochet.Contact_Status {
c.mutex.Lock() c.mutex.Lock()
defer c.mutex.Unlock() defer c.mutex.Unlock()
return c.status return c.data.Status
} }
func (c *Contact) Data() *ricochet.Contact { func (c *Contact) Data() *ricochet.Contact {
c.mutex.Lock() c.mutex.Lock()
defer c.mutex.Unlock() defer c.mutex.Unlock()
address, _ := AddressFromOnion(c.data.Hostname) return proto.Clone(c.data).(*ricochet.Contact)
data := &ricochet.Contact{
Id: int32(c.id),
Address: address,
Nickname: c.data.Nickname,
WhenCreated: c.data.WhenCreated,
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
} }
func (c *Contact) IsRequest() bool { func (c *Contact) IsRequest() bool {
c.mutex.Lock() c.mutex.Lock()
defer c.mutex.Unlock() defer c.mutex.Unlock()
return c.data.Request.Pending return c.data.Request != nil
} }
func (c *Contact) Conversation() *Conversation { func (c *Contact) Conversation() *Conversation {
c.mutex.Lock() c.mutex.Lock()
defer c.mutex.Unlock() defer c.mutex.Unlock()
if c.conversation == nil { if c.conversation == nil {
address, _ := AddressFromOnion(c.data.Hostname)
entity := &ricochet.Entity{ entity := &ricochet.Entity{
ContactId: int32(c.id), ContactId: int32(c.id),
Address: address, Address: c.data.Address,
} }
c.conversation = NewConversation(c, entity, c.core.Identity.ConversationStream) c.conversation = NewConversation(c, entity, c.core.Identity.ConversationStream)
} }
@ -187,7 +164,7 @@ func (c *Contact) shouldMakeOutboundConnections() bool {
defer c.mutex.Unlock() defer c.mutex.Unlock()
// Don't make connections to contacts in the REJECTED state // Don't make connections to contacts in the REJECTED state
if c.status == ricochet.Contact_REJECTED { if c.data.Status == ricochet.Contact_REJECTED {
return false return false
} }
@ -263,7 +240,7 @@ func (c *Contact) contactConnection() {
// already closed. If there was an existing connection and this returns nil, // already closed. If there was an existing connection and this returns nil,
// the old connection is closed but c.connection has not been reset. // the old connection is closed but c.connection has not been reset.
if err := c.considerUsingConnection(conn); err != nil { if err := c.considerUsingConnection(conn); err != nil {
log.Printf("Discarded new contact %s connection: %s", c.data.Hostname, err) log.Printf("Discarded new contact %s connection: %s", c.data.Address, err)
go closeUnhandledConnection(conn) go closeUnhandledConnection(conn)
c.mutex.Unlock() c.mutex.Unlock()
continue continue
@ -335,8 +312,8 @@ func (c *Contact) connectOutbound(ctx context.Context, connChannel chan *connect
Network: c.core.Network, Network: c.core.Network,
NeverGiveUp: true, NeverGiveUp: true,
} }
hostname := c.data.Hostname hostname, _ := OnionFromAddress(c.data.Address)
isRequest := c.data.Request.Pending isRequest := c.data.Request != nil
c.mutex.Unlock() c.mutex.Unlock()
for { for {
@ -446,8 +423,8 @@ func (c *Contact) sendContactRequest(conn *connection.Connection, ctx context.Co
_, err := conn.RequestOpenChannel("im.ricochet.contact.request", _, err := conn.RequestOpenChannel("im.ricochet.contact.request",
&channels.ContactRequestChannel{ &channels.ContactRequestChannel{
Handler: &requestChannelHandler{Response: responseChan}, Handler: &requestChannelHandler{Response: responseChan},
Name: c.data.Request.MyNickname, // XXX mutex Name: c.data.Request.FromNickname, // XXX mutex
Message: c.data.Request.Message, Message: c.data.Request.Text,
}) })
return err return err
}) })
@ -502,9 +479,9 @@ func (c *Contact) considerUsingConnection(conn *connection.Connection) error {
}() }()
if conn.IsInbound { if conn.IsInbound {
log.Printf("Contact %s has a new inbound connection", c.data.Hostname) log.Printf("Contact %s has a new inbound connection", c.data.Address)
} else { } else {
log.Printf("Contact %s has a new outbound connection", c.data.Hostname) log.Printf("Contact %s has a new outbound connection", c.data.Address)
} }
if conn == c.connection { if conn == c.connection {
@ -515,8 +492,9 @@ func (c *Contact) considerUsingConnection(conn *connection.Connection) error {
return fmt.Errorf("Connection %v is not authenticated", conn) return fmt.Errorf("Connection %v is not authenticated", conn)
} }
if c.data.Hostname[0:16] != conn.RemoteHostname { plainHost, _ := PlainHostFromAddress(c.data.Address)
return fmt.Errorf("Connection hostname %s doesn't match contact hostname %s when assigning connection", conn.RemoteHostname, c.data.Hostname[0:16]) if plainHost != conn.RemoteHostname {
return fmt.Errorf("Connection hostname %s doesn't match contact hostname %s when assigning connection", conn.RemoteHostname, plainHost)
} }
if c.connection != nil && !c.shouldReplaceConnection(conn) { if c.connection != nil && !c.shouldReplaceConnection(conn) {
@ -539,28 +517,29 @@ func (c *Contact) considerUsingConnection(conn *connection.Connection) error {
// Assumes c.mutex is held. // Assumes c.mutex is held.
func (c *Contact) onConnectionStateChanged() { func (c *Contact) onConnectionStateChanged() {
if c.connection != nil { if c.connection != nil {
if c.data.Request.Pending && c.connection.IsInbound { if c.data.Request != nil && c.connection.IsInbound {
// Inbound connection implicitly accepts the contact request and can continue as a contact // Inbound connection implicitly accepts the contact request and can continue as a contact
// Outbound request logic is all handled by connectOutbound. // Outbound request logic is all handled by connectOutbound.
log.Printf("Contact request implicitly accepted by contact %v", c) log.Printf("Contact request implicitly accepted by contact %v", c)
c.updateContactRequest("Accepted") c.updateContactRequest("Accepted")
} else { } else {
c.status = ricochet.Contact_ONLINE c.data.Status = ricochet.Contact_ONLINE
} }
} else { } else {
if c.status == ricochet.Contact_ONLINE { if c.data.Status == ricochet.Contact_ONLINE {
c.status = ricochet.Contact_OFFLINE c.data.Status = ricochet.Contact_OFFLINE
} }
} }
// Update LastConnected time // Update LastConnected time
c.timeConnected = time.Now() c.timeConnected = time.Now()
config := c.core.Config.OpenWrite()
c.data.LastConnected = c.timeConnected.Format(time.RFC3339) c.data.LastConnected = c.timeConnected.Format(time.RFC3339)
config.Contacts[strconv.Itoa(c.id)] = c.data
config.Save()
config := c.core.Config.Lock()
config.Contacts[strconv.Itoa(c.id)] = c.data
c.core.Config.Unlock()
// XXX I wonder if events and config updates can be combined now, and made safer...
// _really_ assumes c.mutex was held // _really_ assumes c.mutex was held
c.mutex.Unlock() c.mutex.Unlock()
event := ricochet.ContactEvent{ event := ricochet.ContactEvent{
@ -615,7 +594,7 @@ func (c *Contact) UpdateContactRequest(status string) bool {
c.mutex.Lock() c.mutex.Lock()
defer c.mutex.Unlock() defer c.mutex.Unlock()
if !c.data.Request.Pending { if c.data.Request == nil {
return false return false
} }
@ -635,7 +614,6 @@ func (c *Contact) UpdateContactRequest(status string) bool {
// Same as above, but assumes the mutex is already held and that the caller // Same as above, but assumes the mutex is already held and that the caller
// will send an UPDATE event // will send an UPDATE event
func (c *Contact) updateContactRequest(status string) bool { func (c *Contact) updateContactRequest(status string) bool {
config := c.core.Config.OpenWrite()
now := time.Now().Format(time.RFC3339) now := time.Now().Format(time.RFC3339)
// Whether to keep the channel open // Whether to keep the channel open
var re bool var re bool
@ -646,11 +624,11 @@ func (c *Contact) updateContactRequest(status string) bool {
re = true re = true
case "Accepted": case "Accepted":
c.data.Request = ConfigContactRequest{} c.data.Request = nil
if c.connection != nil { if c.connection != nil {
c.status = ricochet.Contact_ONLINE c.data.Status = ricochet.Contact_ONLINE
} else { } else {
c.status = ricochet.Contact_UNKNOWN c.data.Status = ricochet.Contact_UNKNOWN
} }
case "Rejected": case "Rejected":
@ -664,8 +642,9 @@ func (c *Contact) updateContactRequest(status string) bool {
log.Printf("Unknown contact request status '%s'", status) log.Printf("Unknown contact request status '%s'", status)
} }
config := c.core.Config.Lock()
defer c.core.Config.Unlock()
config.Contacts[strconv.Itoa(c.id)] = c.data config.Contacts[strconv.Itoa(c.id)] = c.data
config.Save()
return re return re
} }

View File

@ -27,9 +27,7 @@ func LoadContactList(core *Ricochet) (*ContactList, error) {
inboundRequests: make(map[string]*InboundContactRequest), inboundRequests: make(map[string]*InboundContactRequest),
} }
config := core.Config.OpenRead() config := core.Config.Read()
defer config.Close()
list.contacts = make(map[int]*Contact, len(config.Contacts)) list.contacts = make(map[int]*Contact, len(config.Contacts))
for idStr, data := range config.Contacts { for idStr, data := range config.Contacts {
id, err := strconv.Atoi(idStr) id, err := strconv.Atoi(idStr)
@ -99,20 +97,15 @@ func (cl *ContactList) InboundRequestByAddress(address string) *InboundContactRe
// Generally, you will use AddContactRequest (for outbound requests) and // Generally, you will use AddContactRequest (for outbound requests) and
// AddOrUpdateInboundContactRequest plus InboundContactRequest.Accept() instead of // AddOrUpdateInboundContactRequest plus InboundContactRequest.Accept() instead of
// using this function directly. // using this function directly.
func (this *ContactList) AddNewContact(configContact ConfigContact) (*Contact, error) { func (this *ContactList) AddNewContact(data *ricochet.Contact) (*Contact, error) {
this.mutex.Lock() this.mutex.Lock()
defer this.mutex.Unlock() defer this.mutex.Unlock()
address, ok := AddressFromOnion(configContact.Hostname)
if !ok {
return nil, errors.New("Invalid ricochet address")
}
for _, contact := range this.contacts { for _, contact := range this.contacts {
if contact.Address() == address { if contact.Address() == data.Address {
return nil, errors.New("Contact already exists with this address") return nil, errors.New("Contact already exists with this address")
} }
if contact.Nickname() == configContact.Nickname { if contact.Nickname() == data.Nickname {
return nil, errors.New("Contact already exists with this nickname") return nil, errors.New("Contact already exists with this nickname")
} }
} }
@ -120,7 +113,7 @@ func (this *ContactList) AddNewContact(configContact ConfigContact) (*Contact, e
// XXX check inbound requests (but this can be called for an inbound req too) // XXX check inbound requests (but this can be called for an inbound req too)
// Write new contact into config // Write new contact into config
config := this.core.Config.OpenWrite() config := this.core.Config.Lock()
maxContactId := 0 maxContactId := 0
for idstr, _ := range config.Contacts { for idstr, _ := range config.Contacts {
@ -132,13 +125,15 @@ func (this *ContactList) AddNewContact(configContact ConfigContact) (*Contact, e
} }
contactId := maxContactId + 1 contactId := maxContactId + 1
config.Contacts[strconv.Itoa(contactId)] = configContact
if err := config.Save(); err != nil { if config.Contacts == nil {
return nil, err config.Contacts = make(map[string]*ricochet.Contact)
} }
config.Contacts[strconv.Itoa(contactId)] = data
this.core.Config.Unlock()
// Create Contact // Create Contact
contact, err := ContactFromConfig(this.core, contactId, configContact, this.events) contact, err := ContactFromConfig(this.core, contactId, data, this.events)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -163,8 +158,7 @@ func (this *ContactList) AddNewContact(configContact ConfigContact) (*Contact, e
// If an inbound request already exists for this address, that request will be automatically // If an inbound request already exists for this address, that request will be automatically
// accepted, and the returned contact will already be fully established. // accepted, and the returned contact will already be fully established.
func (cl *ContactList) AddContactRequest(address, name, fromName, text string) (*Contact, error) { func (cl *ContactList) AddContactRequest(address, name, fromName, text string) (*Contact, error) {
onion, valid := OnionFromAddress(address) if !IsAddressValid(address) {
if !valid {
return nil, errors.New("Invalid ricochet address") return nil, errors.New("Invalid ricochet address")
} }
if !IsNicknameAcceptable(name) { if !IsNicknameAcceptable(name) {
@ -177,17 +171,20 @@ func (cl *ContactList) AddContactRequest(address, name, fromName, text string) (
return nil, errors.New("Invalid message") return nil, errors.New("Invalid message")
} }
configContact := ConfigContact{ data := &ricochet.Contact{
Hostname: onion, Address: address,
Nickname: name, Nickname: name,
WhenCreated: time.Now().Format(time.RFC3339), WhenCreated: time.Now().Format(time.RFC3339),
Request: ConfigContactRequest{ Request: &ricochet.ContactRequest{
Pending: true, Direction: ricochet.ContactRequest_OUTBOUND,
MyNickname: fromName, Address: address,
Message: text, Nickname: name,
FromNickname: fromName,
Text: text,
WhenCreated: time.Now().Format(time.RFC3339),
}, },
} }
contact, err := cl.AddNewContact(configContact) contact, err := cl.AddNewContact(data)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -213,11 +210,9 @@ func (this *ContactList) RemoveContact(contact *Contact) error {
// leaves a goroutine up among other things. // leaves a goroutine up among other things.
contact.StopConnection() contact.StopConnection()
config := this.core.Config.OpenWrite() config := this.core.Config.Lock()
delete(config.Contacts, strconv.Itoa(contact.Id())) delete(config.Contacts, strconv.Itoa(contact.Id()))
if err := config.Save(); err != nil { this.core.Config.Unlock()
return err
}
delete(this.contacts, contact.Id()) delete(this.contacts, contact.Id())

View File

@ -2,9 +2,9 @@ package core
import ( import (
"crypto/rsa" "crypto/rsa"
"encoding/base64"
"errors" "errors"
"github.com/ricochet-im/ricochet-go/core/utils" "github.com/ricochet-im/ricochet-go/core/utils"
"github.com/ricochet-im/ricochet-go/rpc"
protocol "github.com/s-rah/go-ricochet" protocol "github.com/s-rah/go-ricochet"
connection "github.com/s-rah/go-ricochet/connection" connection "github.com/s-rah/go-ricochet/connection"
"github.com/yawning/bulb/utils/pkcs1" "github.com/yawning/bulb/utils/pkcs1"
@ -51,15 +51,10 @@ func CreateIdentity(core *Ricochet) (*Identity, error) {
} }
func (me *Identity) loadIdentity() error { func (me *Identity) loadIdentity() error {
config := me.core.Config.OpenRead() config := me.core.Config.Read()
defer config.Close()
if config.Identity.ServiceKey != "" {
keyData, err := base64.StdEncoding.DecodeString(config.Identity.ServiceKey)
if err != nil {
return err
}
if keyData := config.Secrets.GetServicePrivateKey(); keyData != nil {
var err error
me.privateKey, _, err = pkcs1.DecodePrivateKeyDER(keyData) me.privateKey, _, err = pkcs1.DecodePrivateKeyDER(keyData)
if err != nil { if err != nil {
return err return err
@ -90,9 +85,12 @@ func (me *Identity) setPrivateKey(key *rsa.PrivateKey) error {
if err != nil { if err != nil {
return err return err
} }
config := me.core.Config.OpenWrite() config := me.core.Config.Lock()
config.Identity.ServiceKey = base64.StdEncoding.EncodeToString(keyData) if config.Secrets == nil {
config.Save() config.Secrets = &ricochet.Secrets{}
}
config.Secrets.ServicePrivateKey = keyData
me.core.Config.Unlock()
// Update Identity // Update Identity
me.address, err = AddressFromKey(&key.PublicKey) me.address, err = AddressFromKey(&key.PublicKey)

View File

@ -266,13 +266,12 @@ func (cr *InboundContactRequest) Accept() (*Contact, error) {
log.Printf("Accepting contact request from %s", cr.data.Address) log.Printf("Accepting contact request from %s", cr.data.Address)
onion, _ := OnionFromAddress(cr.data.Address) data := &ricochet.Contact{
configContact := ConfigContact{ Address: cr.data.Address,
Hostname: onion,
Nickname: cr.data.FromNickname, Nickname: cr.data.FromNickname,
WhenCreated: cr.data.WhenCreated, WhenCreated: cr.data.WhenCreated,
} }
contact, err := cr.core.Identity.ContactList().AddNewContact(configContact) contact, err := cr.core.Identity.ContactList().AddNewContact(data)
if err != nil { if err != nil {
log.Printf("Error occurred in accepting contact request: %s", err) log.Printf("Error occurred in accepting contact request: %s", err)
return nil, err return nil, err

View File

@ -2,6 +2,7 @@ package core
import ( import (
cryptorand "crypto/rand" cryptorand "crypto/rand"
"github.com/ricochet-im/ricochet-go/core/config"
"log" "log"
"math" "math"
"math/big" "math/big"
@ -11,12 +12,12 @@ import (
) )
type Ricochet struct { type Ricochet struct {
Config *Config Config *config.ConfigFile
Network *Network Network *Network
Identity *Identity Identity *Identity
} }
func (core *Ricochet) Init(conf *Config) (err error) { func (core *Ricochet) Init(conf *config.ConfigFile) (err error) {
initRand() initRand()
core.Config = conf core.Config = conf

View File

@ -6,6 +6,7 @@ import (
"fmt" "fmt"
"github.com/chzyer/readline" "github.com/chzyer/readline"
ricochet "github.com/ricochet-im/ricochet-go/core" ricochet "github.com/ricochet-im/ricochet-go/core"
"github.com/ricochet-im/ricochet-go/core/config"
rpc "github.com/ricochet-im/ricochet-go/rpc" rpc "github.com/ricochet-im/ricochet-go/rpc"
"google.golang.org/grpc" "google.golang.org/grpc"
"log" "log"
@ -175,13 +176,16 @@ func checkBackendAddressSafety(address string) error {
} }
func startBackend() error { func startBackend() error {
config, err := ricochet.LoadConfig(configPath) cfg, err := config.LoadConfigFile(configPath)
if err != nil && os.IsNotExist(err) {
cfg, err = config.NewConfigFile(configPath)
}
if err != nil { if err != nil {
return err return err
} }
core := new(ricochet.Ricochet) core := new(ricochet.Ricochet)
if err := core.Init(config); err != nil { if err := core.Init(cfg); err != nil {
return err return err
} }

89
rpc/config.pb.go Normal file
View File

@ -0,0 +1,89 @@
// Code generated by protoc-gen-go.
// source: config.proto
// DO NOT EDIT!
package ricochet
import proto "github.com/golang/protobuf/proto"
import fmt "fmt"
import math "math"
// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf
type Config struct {
Identity *Identity `protobuf:"bytes,1,opt,name=identity" json:"identity,omitempty"`
Contacts map[string]*Contact `protobuf:"bytes,2,rep,name=contacts" json:"contacts,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
Secrets *Secrets `protobuf:"bytes,3,opt,name=secrets" json:"secrets,omitempty"`
}
func (m *Config) Reset() { *m = Config{} }
func (m *Config) String() string { return proto.CompactTextString(m) }
func (*Config) ProtoMessage() {}
func (*Config) Descriptor() ([]byte, []int) { return fileDescriptor5, []int{0} }
func (m *Config) GetIdentity() *Identity {
if m != nil {
return m.Identity
}
return nil
}
func (m *Config) GetContacts() map[string]*Contact {
if m != nil {
return m.Contacts
}
return nil
}
func (m *Config) GetSecrets() *Secrets {
if m != nil {
return m.Secrets
}
return nil
}
// Secrets are not transmitted to frontend RPC clients
type Secrets struct {
ServicePrivateKey []byte `protobuf:"bytes,1,opt,name=servicePrivateKey,proto3" json:"servicePrivateKey,omitempty"`
}
func (m *Secrets) Reset() { *m = Secrets{} }
func (m *Secrets) String() string { return proto.CompactTextString(m) }
func (*Secrets) ProtoMessage() {}
func (*Secrets) Descriptor() ([]byte, []int) { return fileDescriptor5, []int{1} }
func (m *Secrets) GetServicePrivateKey() []byte {
if m != nil {
return m.ServicePrivateKey
}
return nil
}
func init() {
proto.RegisterType((*Config)(nil), "ricochet.Config")
proto.RegisterType((*Secrets)(nil), "ricochet.Secrets")
}
func init() { proto.RegisterFile("config.proto", fileDescriptor5) }
var fileDescriptor5 = []byte{
// 235 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xe2, 0xe2, 0x49, 0xce, 0xcf, 0x4b,
0xcb, 0x4c, 0xd7, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0xe2, 0x28, 0xca, 0x4c, 0xce, 0x4f, 0xce,
0x48, 0x2d, 0x91, 0xe2, 0x4d, 0xce, 0xcf, 0x2b, 0x49, 0x4c, 0x2e, 0x81, 0x48, 0x48, 0xf1, 0x65,
0xa6, 0xa4, 0xe6, 0x95, 0x64, 0x96, 0x54, 0x42, 0xf8, 0x4a, 0x1f, 0x19, 0xb9, 0xd8, 0x9c, 0xc1,
0x3a, 0x85, 0xf4, 0xb8, 0x38, 0x60, 0x92, 0x12, 0x8c, 0x0a, 0x8c, 0x1a, 0xdc, 0x46, 0x42, 0x7a,
0x30, 0x63, 0xf4, 0x3c, 0xa1, 0x32, 0x41, 0x70, 0x35, 0x42, 0x56, 0x5c, 0x1c, 0x50, 0xb3, 0x8b,
0x25, 0x98, 0x14, 0x98, 0x35, 0xb8, 0x8d, 0xe4, 0x10, 0xea, 0x21, 0x66, 0x82, 0x28, 0xb0, 0x02,
0xd7, 0xbc, 0x92, 0xa2, 0xca, 0x20, 0xb8, 0x7a, 0x21, 0x6d, 0x2e, 0xf6, 0xe2, 0xd4, 0xe4, 0xa2,
0xd4, 0x92, 0x62, 0x09, 0x66, 0xb0, 0x55, 0x82, 0x08, 0xad, 0xc1, 0x10, 0x89, 0x20, 0x98, 0x0a,
0x29, 0x3f, 0x2e, 0x5e, 0x14, 0x73, 0x84, 0x04, 0xb8, 0x98, 0xb3, 0x53, 0x21, 0x8e, 0xe4, 0x0c,
0x02, 0x31, 0x85, 0xd4, 0xb9, 0x58, 0xcb, 0x12, 0x73, 0x4a, 0x53, 0x25, 0x98, 0xd0, 0x4d, 0x83,
0xea, 0x0c, 0x82, 0xc8, 0x5b, 0x31, 0x59, 0x30, 0x2a, 0x99, 0x73, 0xb1, 0x43, 0xed, 0x10, 0xd2,
0xe1, 0x12, 0x2c, 0x4e, 0x2d, 0x2a, 0xcb, 0x4c, 0x4e, 0x0d, 0x28, 0xca, 0x2c, 0x4b, 0x2c, 0x49,
0xf5, 0x86, 0x9a, 0xcb, 0x13, 0x84, 0x29, 0x91, 0xc4, 0x06, 0x0e, 0x33, 0x63, 0x40, 0x00, 0x00,
0x00, 0xff, 0xff, 0x42, 0xba, 0x23, 0x37, 0x6c, 0x01, 0x00, 0x00,
}

17
rpc/config.proto Normal file
View File

@ -0,0 +1,17 @@
syntax = "proto3";
package ricochet;
import "contact.proto";
import "identity.proto";
message Config {
Identity identity = 1;
map<string, Contact> contacts = 2;
Secrets secrets = 3;
}
// Secrets are not transmitted to frontend RPC clients
message Secrets {
bytes servicePrivateKey = 1;
}

View File

@ -11,6 +11,7 @@ It is generated from these files:
core.proto core.proto
identity.proto identity.proto
network.proto network.proto
config.proto
It has these top-level messages: It has these top-level messages:
Contact Contact
@ -38,6 +39,8 @@ It has these top-level messages:
NetworkStatus NetworkStatus
StartNetworkRequest StartNetworkRequest
StopNetworkRequest StopNetworkRequest
Config
Secrets
*/ */
package ricochet package ricochet
@ -202,13 +205,16 @@ func (m *Contact) GetStatus() Contact_Status {
} }
type ContactRequest struct { type ContactRequest struct {
Direction ContactRequest_Direction `protobuf:"varint,1,opt,name=direction,enum=ricochet.ContactRequest_Direction" json:"direction,omitempty"` Direction ContactRequest_Direction `protobuf:"varint,1,opt,name=direction,enum=ricochet.ContactRequest_Direction" json:"direction,omitempty"`
Address string `protobuf:"bytes,2,opt,name=address" json:"address,omitempty"` Address string `protobuf:"bytes,2,opt,name=address" json:"address,omitempty"`
Nickname string `protobuf:"bytes,3,opt,name=nickname" json:"nickname,omitempty"` Nickname string `protobuf:"bytes,3,opt,name=nickname" json:"nickname,omitempty"`
Text string `protobuf:"bytes,4,opt,name=text" json:"text,omitempty"` Text string `protobuf:"bytes,4,opt,name=text" json:"text,omitempty"`
FromNickname string `protobuf:"bytes,5,opt,name=fromNickname" json:"fromNickname,omitempty"` FromNickname string `protobuf:"bytes,5,opt,name=fromNickname" json:"fromNickname,omitempty"`
WhenCreated string `protobuf:"bytes,6,opt,name=whenCreated" json:"whenCreated,omitempty"` WhenCreated string `protobuf:"bytes,6,opt,name=whenCreated" json:"whenCreated,omitempty"`
Rejected bool `protobuf:"varint,7,opt,name=rejected" json:"rejected,omitempty"` Rejected bool `protobuf:"varint,7,opt,name=rejected" json:"rejected,omitempty"`
WhenDelivered string `protobuf:"bytes,8,opt,name=whenDelivered" json:"whenDelivered,omitempty"`
WhenRejected string `protobuf:"bytes,9,opt,name=whenRejected" json:"whenRejected,omitempty"`
RemoteError string `protobuf:"bytes,10,opt,name=remoteError" json:"remoteError,omitempty"`
} }
func (m *ContactRequest) Reset() { *m = ContactRequest{} } func (m *ContactRequest) Reset() { *m = ContactRequest{} }
@ -265,6 +271,27 @@ func (m *ContactRequest) GetRejected() bool {
return false return false
} }
func (m *ContactRequest) GetWhenDelivered() string {
if m != nil {
return m.WhenDelivered
}
return ""
}
func (m *ContactRequest) GetWhenRejected() string {
if m != nil {
return m.WhenRejected
}
return ""
}
func (m *ContactRequest) GetRemoteError() string {
if m != nil {
return m.RemoteError
}
return ""
}
type MonitorContactsRequest struct { type MonitorContactsRequest struct {
} }
@ -467,39 +494,42 @@ func init() {
func init() { proto.RegisterFile("contact.proto", fileDescriptor0) } func init() { proto.RegisterFile("contact.proto", fileDescriptor0) }
var fileDescriptor0 = []byte{ var fileDescriptor0 = []byte{
// 539 bytes of a gzipped FileDescriptorProto // 581 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x9c, 0x54, 0xd1, 0x72, 0x93, 0x40, 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x9c, 0x54, 0xdd, 0x6e, 0xd3, 0x30,
0x14, 0x2d, 0x84, 0x02, 0xb9, 0x69, 0x23, 0xdd, 0xe9, 0x38, 0xd8, 0xbe, 0x30, 0x3b, 0x8e, 0x93, 0x14, 0x5e, 0xda, 0x2c, 0x3f, 0xa7, 0x5b, 0xc9, 0xac, 0x09, 0x85, 0xed, 0x26, 0xb2, 0x10, 0xea,
0x17, 0xd1, 0x89, 0xbe, 0xdb, 0x34, 0xd0, 0x31, 0x1a, 0x21, 0x6e, 0x61, 0x7c, 0x26, 0xb0, 0x4e, 0x0d, 0x01, 0x15, 0xee, 0xd9, 0xd6, 0x64, 0xa2, 0x50, 0x92, 0xe1, 0x25, 0xe2, 0x3a, 0x4b, 0x8c,
0xd1, 0x74, 0x89, 0xb0, 0x51, 0xf3, 0x43, 0x7e, 0x8b, 0x9f, 0xe3, 0x27, 0x38, 0xbb, 0x40, 0x6a, 0x16, 0xe8, 0xe2, 0xe2, 0xb8, 0x83, 0xbd, 0x06, 0x4f, 0xc5, 0xe3, 0xf0, 0x08, 0xc8, 0x4e, 0xd2,
0x12, 0x75, 0x1c, 0xdf, 0xf6, 0x9e, 0x73, 0x2e, 0xbb, 0x7b, 0xee, 0x59, 0xe0, 0x38, 0x2d, 0x18, 0xad, 0x2b, 0x20, 0xc4, 0x9d, 0xcf, 0x77, 0xbe, 0x63, 0x1f, 0x7f, 0xdf, 0xb1, 0x61, 0x37, 0x67,
0x4f, 0x52, 0xee, 0x2e, 0xcb, 0x82, 0x17, 0xc8, 0x2c, 0xf3, 0xb4, 0x48, 0x6f, 0x28, 0xc7, 0xdf, 0x95, 0xc8, 0x72, 0xe1, 0x2f, 0x38, 0x13, 0x0c, 0x59, 0xbc, 0xcc, 0x59, 0x7e, 0x49, 0x05, 0xfe,
0x55, 0x30, 0xc6, 0x35, 0x87, 0xfa, 0xa0, 0xe6, 0x99, 0xad, 0x38, 0xca, 0xe0, 0x90, 0xa8, 0x79, 0xd1, 0x03, 0x73, 0xd2, 0xe4, 0xd0, 0x10, 0x7a, 0x65, 0xe1, 0x6a, 0x9e, 0x36, 0xda, 0x26, 0xbd,
0x86, 0x6c, 0x30, 0x92, 0x2c, 0x2b, 0x69, 0x55, 0xd9, 0xaa, 0xa3, 0x0c, 0xba, 0xa4, 0x2d, 0xd1, 0xb2, 0x40, 0x2e, 0x98, 0x59, 0x51, 0x70, 0x5a, 0xd7, 0x6e, 0xcf, 0xd3, 0x46, 0x36, 0xe9, 0x42,
0x19, 0x98, 0x2c, 0x4f, 0x3f, 0xb2, 0xe4, 0x96, 0xda, 0x1d, 0x49, 0x6d, 0x6a, 0xe4, 0x40, 0xef, 0x74, 0x00, 0x56, 0x55, 0xe6, 0x9f, 0xab, 0xec, 0x8a, 0xba, 0x7d, 0x95, 0x5a, 0xc5, 0xc8, 0x83,
0xcb, 0x0d, 0x65, 0xe3, 0x92, 0x26, 0x9c, 0x66, 0xb6, 0x26, 0xe9, 0x5f, 0x21, 0xf4, 0x10, 0x8e, 0xc1, 0xd7, 0x4b, 0x5a, 0x4d, 0x38, 0xcd, 0x04, 0x2d, 0x5c, 0x5d, 0xa5, 0xef, 0x42, 0xe8, 0x31,
0x17, 0x49, 0xc5, 0xc7, 0x05, 0x63, 0x34, 0x15, 0x9a, 0x43, 0xa9, 0xd9, 0x06, 0xd1, 0x10, 0x8c, 0xec, 0xce, 0xb3, 0x5a, 0x4c, 0x58, 0x55, 0xd1, 0x5c, 0x72, 0xb6, 0x15, 0x67, 0x1d, 0x44, 0x63,
0x92, 0x7e, 0x5a, 0xd1, 0x8a, 0xdb, 0xba, 0xa3, 0x0c, 0x7a, 0x43, 0xdb, 0x6d, 0x4f, 0xed, 0x36, 0x30, 0x39, 0xfd, 0xb2, 0xa4, 0xb5, 0x70, 0x0d, 0x4f, 0x1b, 0x0d, 0xc6, 0xae, 0xdf, 0x75, 0xed,
0x27, 0x26, 0x35, 0x4f, 0x5a, 0x21, 0x7a, 0x0a, 0x7a, 0xc5, 0x13, 0xbe, 0xaa, 0x6c, 0x70, 0x94, 0xb7, 0x1d, 0x93, 0x26, 0x4f, 0x3a, 0x22, 0x7a, 0x0e, 0x46, 0x2d, 0x32, 0xb1, 0xac, 0x5d, 0xf0,
0x41, 0xff, 0x37, 0x2d, 0xee, 0xb5, 0xe4, 0x49, 0xa3, 0xc3, 0x13, 0xd0, 0x6b, 0x04, 0xf5, 0xc0, 0xb4, 0xd1, 0xf0, 0x37, 0x25, 0xfe, 0xb9, 0xca, 0x93, 0x96, 0x87, 0xa7, 0x60, 0x34, 0x08, 0x1a,
0x88, 0x83, 0xd7, 0x41, 0xf8, 0x2e, 0xb0, 0x0e, 0x44, 0x11, 0x5e, 0x5d, 0x4d, 0x27, 0x81, 0x6f, 0x80, 0x99, 0x46, 0x6f, 0xa3, 0xf8, 0x43, 0xe4, 0x6c, 0xc9, 0x20, 0x3e, 0x3d, 0x9d, 0x4d, 0xa3,
0x29, 0x08, 0x40, 0x0f, 0x03, 0xb9, 0x56, 0x05, 0x41, 0xfc, 0xb7, 0xb1, 0x7f, 0x1d, 0x59, 0x1d, 0xd0, 0xd1, 0x10, 0x80, 0x11, 0x47, 0x6a, 0xdd, 0x93, 0x09, 0x12, 0xbe, 0x4f, 0xc3, 0xf3, 0xc4,
0x74, 0x04, 0x26, 0xf1, 0x5f, 0xf9, 0xe3, 0xc8, 0xf7, 0x2c, 0x0d, 0x7f, 0x53, 0xa1, 0xbf, 0x7d, 0xe9, 0xa3, 0x1d, 0xb0, 0x48, 0xf8, 0x26, 0x9c, 0x24, 0x61, 0xe0, 0xe8, 0xf8, 0x7b, 0x1f, 0x86,
0x30, 0x74, 0x01, 0xdd, 0x2c, 0x2f, 0x69, 0xca, 0xf3, 0x82, 0x49, 0x63, 0xfb, 0x43, 0xfc, 0xa7, 0xeb, 0x8d, 0xa1, 0x23, 0xb0, 0x8b, 0x92, 0xd3, 0x5c, 0x94, 0xac, 0x52, 0xc2, 0x0e, 0xc7, 0xf8,
0x5b, 0xb8, 0x5e, 0xab, 0x24, 0x77, 0x4d, 0xff, 0x39, 0x03, 0x04, 0x1a, 0xa7, 0x5f, 0x79, 0x63, 0x4f, 0xb7, 0xf0, 0x83, 0x8e, 0x49, 0x6e, 0x8b, 0xfe, 0xd3, 0x03, 0x04, 0xba, 0xa0, 0xdf, 0x44,
0xbe, 0x5c, 0x23, 0x0c, 0x47, 0xef, 0xcb, 0xe2, 0x36, 0x68, 0x7b, 0x6a, 0xd3, 0xb7, 0xb0, 0xdd, 0x2b, 0xbe, 0x5a, 0x23, 0x0c, 0x3b, 0x1f, 0x39, 0xbb, 0x8a, 0xba, 0x9a, 0x46, 0xf4, 0x35, 0xec,
0xd9, 0xe9, 0xfb, 0xb3, 0x3b, 0x03, 0xb3, 0xa4, 0x1f, 0xea, 0xb1, 0x19, 0x8e, 0x32, 0x30, 0xc9, 0xbe, 0x77, 0xc6, 0xa6, 0x77, 0x07, 0x60, 0x71, 0xfa, 0xa9, 0xb1, 0xcd, 0xf4, 0xb4, 0x91, 0x45,
0xa6, 0xc6, 0x8f, 0xa0, 0xbb, 0xb9, 0x83, 0x30, 0x6a, 0x12, 0x5c, 0x86, 0x71, 0xe0, 0x59, 0x07, 0x56, 0xb1, 0xf4, 0x55, 0x52, 0x03, 0x3a, 0x2f, 0xaf, 0x29, 0xa7, 0x85, 0x6b, 0x35, 0xbe, 0xae,
0xc2, 0xa8, 0x30, 0x8e, 0xea, 0x4a, 0xc1, 0x36, 0xdc, 0x7f, 0x53, 0xb0, 0x9c, 0x17, 0x65, 0xe3, 0x81, 0xb2, 0x0f, 0x09, 0x90, 0x6e, 0x17, 0xbb, 0xe9, 0xe3, 0x2e, 0x26, 0xfb, 0xe0, 0xf4, 0x8a,
0x40, 0xd5, 0x58, 0x80, 0x7f, 0x28, 0x70, 0xd4, 0x60, 0xfe, 0x67, 0xca, 0x38, 0x7a, 0x02, 0x1a, 0x09, 0x1a, 0x72, 0xce, 0xb8, 0x32, 0xd3, 0x26, 0x77, 0x21, 0xfc, 0x04, 0xec, 0x95, 0x5e, 0xd2,
0x5f, 0x2f, 0x69, 0xe3, 0xdd, 0xf9, 0x9e, 0x77, 0x52, 0xe5, 0x46, 0xeb, 0x25, 0x25, 0x52, 0x88, 0x94, 0x69, 0x74, 0x12, 0xa7, 0x51, 0xe0, 0x6c, 0x49, 0x53, 0xe2, 0x34, 0x69, 0x22, 0x0d, 0xbb,
0x1e, 0x83, 0xd1, 0x44, 0x5d, 0xfa, 0xd5, 0x1b, 0x9e, 0xec, 0xf5, 0xbc, 0x3c, 0x20, 0xad, 0x06, 0xf0, 0xf0, 0x1d, 0xab, 0x4a, 0xc1, 0x78, 0xab, 0x76, 0xdd, 0xca, 0x8d, 0x7f, 0x6a, 0xb0, 0xd3,
0x3d, 0xbf, 0x0b, 0x59, 0xe7, 0xef, 0x21, 0x13, 0x5d, 0x8d, 0x14, 0xbf, 0x00, 0x4d, 0x6c, 0x89, 0x62, 0xe1, 0x35, 0xad, 0x04, 0x7a, 0x06, 0xba, 0xb8, 0x59, 0xd0, 0xd6, 0xa7, 0xc3, 0x0d, 0x9f,
0x4c, 0xd0, 0x82, 0x78, 0x3a, 0xad, 0x2f, 0x38, 0x0b, 0x67, 0xf1, 0x74, 0x14, 0x89, 0xc0, 0x18, 0x14, 0xcb, 0x4f, 0x6e, 0x16, 0x94, 0x28, 0x22, 0x7a, 0x0a, 0x66, 0xfb, 0xac, 0x94, 0x37, 0x83,
0xd0, 0x19, 0x79, 0x9e, 0xa5, 0x8a, 0xe4, 0xc4, 0x33, 0x4f, 0x80, 0x1d, 0xb1, 0xf6, 0xfc, 0xa9, 0xf1, 0xde, 0x46, 0xcd, 0xeb, 0x2d, 0xd2, 0x71, 0xd0, 0xcb, 0xdb, 0x81, 0xee, 0xff, 0x7d, 0xa0,
0x1f, 0xf9, 0x96, 0x76, 0xd9, 0x05, 0xa3, 0x5a, 0xcd, 0x85, 0x6d, 0xf8, 0x04, 0xee, 0x8d, 0xb2, 0x65, 0x55, 0x4b, 0xc5, 0xaf, 0x40, 0x97, 0x47, 0x22, 0x0b, 0xf4, 0x28, 0x9d, 0xcd, 0x9a, 0x0b,
0x6c, 0xb3, 0xd7, 0x72, 0xb1, 0xc6, 0x17, 0x70, 0xea, 0xd1, 0x05, 0xe5, 0x74, 0x27, 0x4d, 0xff, 0x9e, 0xc5, 0x67, 0xe9, 0xec, 0x38, 0x91, 0xc3, 0x69, 0x42, 0xff, 0x38, 0x08, 0x9c, 0x9e, 0x9c,
0xfc, 0x3e, 0xf1, 0x29, 0xa0, 0x9d, 0x2f, 0x88, 0xef, 0x9e, 0xc3, 0x03, 0x22, 0x67, 0x35, 0x61, 0xd2, 0xf4, 0x2c, 0x90, 0x60, 0x5f, 0xae, 0x83, 0x70, 0x16, 0x26, 0xa1, 0xa3, 0x9f, 0xd8, 0x60,
0xf3, 0x62, 0xc5, 0xb2, 0xf6, 0xf9, 0x08, 0x72, 0xae, 0xcb, 0x3f, 0xc3, 0xb3, 0x9f, 0x01, 0x00, 0xd6, 0xcb, 0x0b, 0x29, 0x2c, 0xde, 0x83, 0x07, 0xc7, 0x45, 0xb1, 0x3a, 0x6b, 0x31, 0xbf, 0xc1,
0x00, 0xff, 0xff, 0xe5, 0x62, 0x19, 0x3f, 0x2a, 0x04, 0x00, 0x00, 0x47, 0xb0, 0x1f, 0xd0, 0x39, 0x15, 0xf4, 0xde, 0xe4, 0xfe, 0xf3, 0x5f, 0x80, 0xf7, 0x01, 0xdd,
0xdb, 0x41, 0xee, 0x7b, 0x08, 0x8f, 0x1a, 0x37, 0xa7, 0xd5, 0x05, 0x5b, 0x56, 0x45, 0xf7, 0x54,
0x65, 0xf2, 0xc2, 0x50, 0xbf, 0xd0, 0x8b, 0x5f, 0x01, 0x00, 0x00, 0xff, 0xff, 0x13, 0x18, 0x1c,
0x90, 0x96, 0x04, 0x00, 0x00,
} }

View File

@ -34,6 +34,9 @@ message ContactRequest {
string fromNickname = 5; string fromNickname = 5;
string whenCreated = 6; string whenCreated = 6;
bool rejected = 7; bool rejected = 7;
string whenDelivered = 8;
string whenRejected = 9;
string remoteError = 10;
} }
message MonitorContactsRequest { message MonitorContactsRequest {

View File

@ -1,3 +1,3 @@
package ricochet package ricochet
//go:generate protoc --go_out=plugins=grpc:. contact.proto conversation.proto core.proto identity.proto network.proto //go:generate protoc --go_out=plugins=grpc:. contact.proto conversation.proto core.proto identity.proto network.proto config.proto