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

View File

@ -27,9 +27,7 @@ func LoadContactList(core *Ricochet) (*ContactList, error) {
inboundRequests: make(map[string]*InboundContactRequest),
}
config := core.Config.OpenRead()
defer config.Close()
config := core.Config.Read()
list.contacts = make(map[int]*Contact, len(config.Contacts))
for idStr, data := range config.Contacts {
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
// AddOrUpdateInboundContactRequest plus InboundContactRequest.Accept() instead of
// using this function directly.
func (this *ContactList) AddNewContact(configContact ConfigContact) (*Contact, error) {
func (this *ContactList) AddNewContact(data *ricochet.Contact) (*Contact, error) {
this.mutex.Lock()
defer this.mutex.Unlock()
address, ok := AddressFromOnion(configContact.Hostname)
if !ok {
return nil, errors.New("Invalid ricochet address")
}
for _, contact := range this.contacts {
if contact.Address() == address {
if contact.Address() == data.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")
}
}
@ -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)
// Write new contact into config
config := this.core.Config.OpenWrite()
config := this.core.Config.Lock()
maxContactId := 0
for idstr, _ := range config.Contacts {
@ -132,13 +125,15 @@ func (this *ContactList) AddNewContact(configContact ConfigContact) (*Contact, e
}
contactId := maxContactId + 1
config.Contacts[strconv.Itoa(contactId)] = configContact
if err := config.Save(); err != nil {
return nil, err
if config.Contacts == nil {
config.Contacts = make(map[string]*ricochet.Contact)
}
config.Contacts[strconv.Itoa(contactId)] = data
this.core.Config.Unlock()
// Create Contact
contact, err := ContactFromConfig(this.core, contactId, configContact, this.events)
contact, err := ContactFromConfig(this.core, contactId, data, this.events)
if err != nil {
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
// accepted, and the returned contact will already be fully established.
func (cl *ContactList) AddContactRequest(address, name, fromName, text string) (*Contact, error) {
onion, valid := OnionFromAddress(address)
if !valid {
if !IsAddressValid(address) {
return nil, errors.New("Invalid ricochet address")
}
if !IsNicknameAcceptable(name) {
@ -177,17 +171,20 @@ func (cl *ContactList) AddContactRequest(address, name, fromName, text string) (
return nil, errors.New("Invalid message")
}
configContact := ConfigContact{
Hostname: onion,
data := &ricochet.Contact{
Address: address,
Nickname: name,
WhenCreated: time.Now().Format(time.RFC3339),
Request: ConfigContactRequest{
Pending: true,
MyNickname: fromName,
Message: text,
Request: &ricochet.ContactRequest{
Direction: ricochet.ContactRequest_OUTBOUND,
Address: address,
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 {
return nil, err
}
@ -213,11 +210,9 @@ func (this *ContactList) RemoveContact(contact *Contact) error {
// leaves a goroutine up among other things.
contact.StopConnection()
config := this.core.Config.OpenWrite()
config := this.core.Config.Lock()
delete(config.Contacts, strconv.Itoa(contact.Id()))
if err := config.Save(); err != nil {
return err
}
this.core.Config.Unlock()
delete(this.contacts, contact.Id())

View File

@ -2,9 +2,9 @@ package core
import (
"crypto/rsa"
"encoding/base64"
"errors"
"github.com/ricochet-im/ricochet-go/core/utils"
"github.com/ricochet-im/ricochet-go/rpc"
protocol "github.com/s-rah/go-ricochet"
connection "github.com/s-rah/go-ricochet/connection"
"github.com/yawning/bulb/utils/pkcs1"
@ -51,15 +51,10 @@ func CreateIdentity(core *Ricochet) (*Identity, error) {
}
func (me *Identity) loadIdentity() error {
config := me.core.Config.OpenRead()
defer config.Close()
if config.Identity.ServiceKey != "" {
keyData, err := base64.StdEncoding.DecodeString(config.Identity.ServiceKey)
if err != nil {
return err
}
config := me.core.Config.Read()
if keyData := config.Secrets.GetServicePrivateKey(); keyData != nil {
var err error
me.privateKey, _, err = pkcs1.DecodePrivateKeyDER(keyData)
if err != nil {
return err
@ -90,9 +85,12 @@ func (me *Identity) setPrivateKey(key *rsa.PrivateKey) error {
if err != nil {
return err
}
config := me.core.Config.OpenWrite()
config.Identity.ServiceKey = base64.StdEncoding.EncodeToString(keyData)
config.Save()
config := me.core.Config.Lock()
if config.Secrets == nil {
config.Secrets = &ricochet.Secrets{}
}
config.Secrets.ServicePrivateKey = keyData
me.core.Config.Unlock()
// Update Identity
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)
onion, _ := OnionFromAddress(cr.data.Address)
configContact := ConfigContact{
Hostname: onion,
data := &ricochet.Contact{
Address: cr.data.Address,
Nickname: cr.data.FromNickname,
WhenCreated: cr.data.WhenCreated,
}
contact, err := cr.core.Identity.ContactList().AddNewContact(configContact)
contact, err := cr.core.Identity.ContactList().AddNewContact(data)
if err != nil {
log.Printf("Error occurred in accepting contact request: %s", err)
return nil, err

View File

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

View File

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

View File

@ -1,3 +1,3 @@
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