2016-07-01 03:18:55 +00:00
|
|
|
package core
|
|
|
|
|
2016-08-30 02:46:41 +00:00
|
|
|
import (
|
|
|
|
"fmt"
|
2016-10-17 04:26:35 +00:00
|
|
|
"github.com/ricochet-im/ricochet-go/core/utils"
|
|
|
|
"github.com/ricochet-im/ricochet-go/rpc"
|
2016-10-27 19:57:04 +00:00
|
|
|
protocol "github.com/s-rah/go-ricochet"
|
2017-09-20 19:43:22 +00:00
|
|
|
channels "github.com/s-rah/go-ricochet/channels"
|
2017-08-14 02:04:35 +00:00
|
|
|
connection "github.com/s-rah/go-ricochet/connection"
|
2016-09-30 05:13:55 +00:00
|
|
|
"golang.org/x/net/context"
|
2016-09-16 00:32:58 +00:00
|
|
|
"log"
|
2016-09-20 01:53:21 +00:00
|
|
|
"strconv"
|
2016-09-16 00:32:58 +00:00
|
|
|
"sync"
|
2016-08-30 02:46:41 +00:00
|
|
|
"time"
|
2016-07-01 03:18:55 +00:00
|
|
|
)
|
|
|
|
|
2016-08-30 02:46:41 +00:00
|
|
|
// XXX There is generally a lot of duplication and boilerplate between
|
|
|
|
// Contact, ConfigContact, and rpc.Contact. This should be reduced somehow.
|
|
|
|
|
2016-10-27 19:57:04 +00:00
|
|
|
// XXX Consider replacing the config contact with the protobuf structure,
|
|
|
|
// and extending the protobuf structure for everything it needs.
|
|
|
|
|
2016-07-01 03:18:55 +00:00
|
|
|
type Contact struct {
|
2016-09-20 01:53:21 +00:00
|
|
|
core *Ricochet
|
2016-09-16 00:32:58 +00:00
|
|
|
|
2016-09-20 01:53:21 +00:00
|
|
|
id int
|
|
|
|
data ConfigContact
|
|
|
|
status ricochet.Contact_Status
|
|
|
|
|
|
|
|
mutex sync.Mutex
|
|
|
|
events *utils.Publisher
|
2016-09-16 00:32:58 +00:00
|
|
|
|
2016-10-28 22:08:16 +00:00
|
|
|
connEnabled bool
|
2017-08-14 02:04:35 +00:00
|
|
|
connection *connection.Connection
|
|
|
|
connChannel chan *connection.Connection
|
|
|
|
connEnabledSignal chan bool
|
|
|
|
connectionOnce sync.Once
|
2016-10-05 21:38:18 +00:00
|
|
|
|
2016-11-06 04:58:55 +00:00
|
|
|
timeConnected time.Time
|
|
|
|
|
2016-10-05 21:38:18 +00:00
|
|
|
conversation *Conversation
|
2016-08-30 02:46:41 +00:00
|
|
|
}
|
|
|
|
|
2016-09-20 01:53:21 +00:00
|
|
|
func ContactFromConfig(core *Ricochet, id int, data ConfigContact, events *utils.Publisher) (*Contact, error) {
|
2016-08-30 02:46:41 +00:00
|
|
|
contact := &Contact{
|
2017-08-14 02:04:35 +00:00
|
|
|
core: core,
|
|
|
|
id: id,
|
|
|
|
data: data,
|
|
|
|
events: events,
|
|
|
|
connChannel: make(chan *connection.Connection),
|
|
|
|
connEnabledSignal: make(chan bool),
|
2016-08-30 02:46:41 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if id < 0 {
|
|
|
|
return nil, fmt.Errorf("Invalid contact ID '%d'", id)
|
2016-11-06 03:27:13 +00:00
|
|
|
} else if !IsOnionValid(data.Hostname) {
|
2016-08-30 02:46:41 +00:00
|
|
|
return nil, fmt.Errorf("Invalid contact hostname '%s", data.Hostname)
|
|
|
|
}
|
|
|
|
|
2016-10-27 19:57:04 +00:00
|
|
|
if data.Request.Pending {
|
|
|
|
if data.Request.WhenRejected != "" {
|
|
|
|
contact.status = ricochet.Contact_REJECTED
|
|
|
|
} else {
|
|
|
|
contact.status = ricochet.Contact_REQUEST
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-08-30 02:46:41 +00:00
|
|
|
return contact, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *Contact) Id() int {
|
|
|
|
return c.id
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *Contact) Nickname() string {
|
2016-09-16 00:32:58 +00:00
|
|
|
c.mutex.Lock()
|
|
|
|
defer c.mutex.Unlock()
|
2016-08-30 02:46:41 +00:00
|
|
|
return c.data.Nickname
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *Contact) Address() string {
|
2016-09-16 00:32:58 +00:00
|
|
|
c.mutex.Lock()
|
|
|
|
defer c.mutex.Unlock()
|
2016-11-06 03:27:13 +00:00
|
|
|
address, _ := AddressFromOnion(c.data.Hostname)
|
|
|
|
return address
|
2016-08-30 02:46:41 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (c *Contact) Hostname() string {
|
2016-09-16 00:32:58 +00:00
|
|
|
c.mutex.Lock()
|
|
|
|
defer c.mutex.Unlock()
|
2016-08-30 02:46:41 +00:00
|
|
|
return c.data.Hostname
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *Contact) LastConnected() time.Time {
|
2016-09-16 00:32:58 +00:00
|
|
|
c.mutex.Lock()
|
|
|
|
defer c.mutex.Unlock()
|
2016-08-30 02:46:41 +00:00
|
|
|
time, _ := time.Parse(time.RFC3339, c.data.LastConnected)
|
|
|
|
return time
|
|
|
|
}
|
2016-07-01 03:18:55 +00:00
|
|
|
|
2016-08-30 02:46:41 +00:00
|
|
|
func (c *Contact) WhenCreated() time.Time {
|
2016-09-16 00:32:58 +00:00
|
|
|
c.mutex.Lock()
|
|
|
|
defer c.mutex.Unlock()
|
2016-08-30 02:46:41 +00:00
|
|
|
time, _ := time.Parse(time.RFC3339, c.data.WhenCreated)
|
|
|
|
return time
|
2016-07-01 03:18:55 +00:00
|
|
|
}
|
2016-09-16 00:32:58 +00:00
|
|
|
|
|
|
|
func (c *Contact) Status() ricochet.Contact_Status {
|
|
|
|
c.mutex.Lock()
|
|
|
|
defer c.mutex.Unlock()
|
2016-09-20 01:53:21 +00:00
|
|
|
return c.status
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *Contact) Data() *ricochet.Contact {
|
|
|
|
c.mutex.Lock()
|
|
|
|
defer c.mutex.Unlock()
|
2016-11-06 03:27:13 +00:00
|
|
|
address, _ := AddressFromOnion(c.data.Hostname)
|
2016-09-20 01:53:21 +00:00
|
|
|
data := &ricochet.Contact{
|
|
|
|
Id: int32(c.id),
|
2016-11-06 03:27:13 +00:00
|
|
|
Address: address,
|
2016-09-20 01:53:21 +00:00
|
|
|
Nickname: c.data.Nickname,
|
|
|
|
WhenCreated: c.data.WhenCreated,
|
|
|
|
LastConnected: c.data.LastConnected,
|
|
|
|
Status: c.status,
|
2016-09-16 00:32:58 +00:00
|
|
|
}
|
2016-10-27 19:57:04 +00:00
|
|
|
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,
|
|
|
|
}
|
|
|
|
}
|
2016-09-20 01:53:21 +00:00
|
|
|
return data
|
2016-09-16 00:32:58 +00:00
|
|
|
}
|
|
|
|
|
2017-08-10 17:25:50 +00:00
|
|
|
func (c *Contact) IsRequest() bool {
|
|
|
|
c.mutex.Lock()
|
|
|
|
defer c.mutex.Unlock()
|
|
|
|
return c.data.Request.Pending
|
|
|
|
}
|
|
|
|
|
2016-10-05 21:38:18 +00:00
|
|
|
func (c *Contact) Conversation() *Conversation {
|
|
|
|
c.mutex.Lock()
|
|
|
|
defer c.mutex.Unlock()
|
|
|
|
if c.conversation == nil {
|
2016-11-06 03:27:13 +00:00
|
|
|
address, _ := AddressFromOnion(c.data.Hostname)
|
2016-10-05 21:38:18 +00:00
|
|
|
entity := &ricochet.Entity{
|
|
|
|
ContactId: int32(c.id),
|
2016-11-06 03:27:13 +00:00
|
|
|
Address: address,
|
2016-10-05 21:38:18 +00:00
|
|
|
}
|
2016-10-16 00:04:19 +00:00
|
|
|
c.conversation = NewConversation(c, entity, c.core.Identity.ConversationStream)
|
2016-10-05 21:38:18 +00:00
|
|
|
}
|
|
|
|
return c.conversation
|
|
|
|
}
|
|
|
|
|
2017-08-14 02:04:35 +00:00
|
|
|
func (c *Contact) Connection() *connection.Connection {
|
2016-10-06 23:50:07 +00:00
|
|
|
c.mutex.Lock()
|
|
|
|
defer c.mutex.Unlock()
|
|
|
|
return c.connection
|
|
|
|
}
|
|
|
|
|
2017-08-14 02:04:35 +00:00
|
|
|
// StartConnection enables inbound and outbound connections for this contact, if other
|
|
|
|
// conditions permit them. This function is safe to call repeatedly.
|
2016-10-28 22:08:16 +00:00
|
|
|
func (c *Contact) StartConnection() {
|
2017-08-14 02:04:35 +00:00
|
|
|
c.connectionOnce.Do(func() {
|
|
|
|
go c.contactConnection()
|
|
|
|
})
|
2016-10-28 22:08:16 +00:00
|
|
|
|
|
|
|
c.connEnabled = true
|
2017-08-14 02:04:35 +00:00
|
|
|
c.connEnabledSignal <- true
|
2016-10-28 22:08:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (c *Contact) StopConnection() {
|
2017-08-14 02:04:35 +00:00
|
|
|
// Must be running to consume connEnabledSignal
|
|
|
|
c.connectionOnce.Do(func() {
|
|
|
|
go c.contactConnection()
|
|
|
|
})
|
|
|
|
|
|
|
|
c.connEnabled = false
|
|
|
|
c.connEnabledSignal <- false
|
2016-10-28 22:08:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (c *Contact) shouldMakeOutboundConnections() bool {
|
|
|
|
c.mutex.Lock()
|
|
|
|
defer c.mutex.Unlock()
|
|
|
|
|
|
|
|
// Don't make connections to contacts in the REJECTED state
|
|
|
|
if c.status == ricochet.Contact_REJECTED {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
return c.connEnabled
|
|
|
|
}
|
|
|
|
|
2017-08-14 02:04:35 +00:00
|
|
|
// closeUnhandledConnection takes a connection without an active Process routine
|
|
|
|
// and ensures that it is fully closed and destroyed. It is safe to call on
|
|
|
|
// a connection that has already been closed and on any connection in any
|
|
|
|
// state, as long as Process() is not currently running.
|
|
|
|
func closeUnhandledConnection(conn *connection.Connection) {
|
|
|
|
conn.Conn.Close()
|
|
|
|
nullHandler := &connection.AutoConnectionHandler{}
|
|
|
|
nullHandler.Init()
|
|
|
|
conn.Process(nullHandler)
|
|
|
|
}
|
|
|
|
|
2016-09-30 05:13:55 +00:00
|
|
|
// Goroutine to handle the protocol connection for a contact.
|
|
|
|
// Responsible for making outbound connections and taking over authenticated
|
|
|
|
// inbound connections, running protocol handlers on the active connection, and
|
|
|
|
// reacting to connection loss. Nothing else may write Contact.connection.
|
2017-08-14 02:04:35 +00:00
|
|
|
//
|
|
|
|
// This goroutine is started by the first call to StartConnection or StopConnection
|
|
|
|
// and persists for the lifetime of the contact. When connections are stopped, it
|
|
|
|
// consumes connChannel and closes all (presumably inbound) connections.
|
|
|
|
// XXX Need a hard kill for destroying contacts
|
2016-09-30 05:13:55 +00:00
|
|
|
func (c *Contact) contactConnection() {
|
2017-08-14 02:04:35 +00:00
|
|
|
// Signalled when the active connection is closed
|
|
|
|
connClosedChannel := make(chan struct{})
|
|
|
|
connectionsEnabled := false
|
|
|
|
|
2016-09-30 05:13:55 +00:00
|
|
|
for {
|
2017-08-14 02:04:35 +00:00
|
|
|
if !connectionsEnabled {
|
|
|
|
// Reject all connections on connChannel and wait for start signal
|
|
|
|
select {
|
|
|
|
case conn := <-c.connChannel:
|
|
|
|
if conn != nil {
|
|
|
|
log.Printf("Discarded connection to %s because connections are disabled", c.Address())
|
|
|
|
go closeUnhandledConnection(conn)
|
|
|
|
// XXX-protocol doing this here instead of during auth means they'll keep trying endlessly. Doing it in
|
|
|
|
// auth means they'll never try again. Both are sometimes wrong. Hmm.
|
|
|
|
}
|
|
|
|
case enable := <-c.connEnabledSignal:
|
|
|
|
if enable {
|
|
|
|
log.Printf("Contact %s connections are enabled", c.Address())
|
|
|
|
connectionsEnabled = true
|
|
|
|
}
|
|
|
|
// XXX hard kill
|
|
|
|
}
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
// If there is no active connection, spawn an outbound connector. A successful connection
|
|
|
|
// is returned via connChannel, and otherwise it will keep trying until cancelled via
|
|
|
|
// the context.
|
2016-09-30 05:13:55 +00:00
|
|
|
var outboundCtx context.Context
|
|
|
|
outboundCancel := func() {}
|
2016-10-28 22:08:16 +00:00
|
|
|
if c.connection == nil && c.shouldMakeOutboundConnections() {
|
2016-09-30 05:13:55 +00:00
|
|
|
outboundCtx, outboundCancel = context.WithCancel(context.Background())
|
2017-08-14 02:04:35 +00:00
|
|
|
go c.connectOutbound(outboundCtx, c.connChannel)
|
2016-09-30 05:13:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
select {
|
2017-08-14 02:04:35 +00:00
|
|
|
case conn := <-c.connChannel:
|
2016-09-30 05:13:55 +00:00
|
|
|
outboundCancel()
|
2017-08-14 02:04:35 +00:00
|
|
|
if conn == nil {
|
2016-09-30 05:13:55 +00:00
|
|
|
// Signal used to restart outbound connection attempts
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2017-08-14 02:04:35 +00:00
|
|
|
c.mutex.Lock()
|
|
|
|
// Decide whether to keep this connection; if this returns an error, conn is
|
|
|
|
// 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)
|
|
|
|
go closeUnhandledConnection(conn)
|
|
|
|
c.mutex.Unlock()
|
2016-09-30 05:13:55 +00:00
|
|
|
continue
|
|
|
|
}
|
2017-08-14 02:04:35 +00:00
|
|
|
replacingConn := c.connection != nil
|
|
|
|
c.connection = conn
|
|
|
|
if replacingConn {
|
|
|
|
// Wait for old handleConnection to return
|
|
|
|
c.mutex.Unlock()
|
|
|
|
<-connClosedChannel
|
|
|
|
c.mutex.Lock()
|
|
|
|
}
|
|
|
|
go c.handleConnection(conn, connClosedChannel)
|
|
|
|
c.onConnectionStateChanged()
|
|
|
|
c.mutex.Unlock()
|
2016-09-30 05:13:55 +00:00
|
|
|
|
2016-10-28 22:08:16 +00:00
|
|
|
case <-connClosedChannel:
|
2016-09-30 05:13:55 +00:00
|
|
|
outboundCancel()
|
2017-08-14 02:04:35 +00:00
|
|
|
c.mutex.Lock()
|
|
|
|
c.connection = nil
|
|
|
|
c.onConnectionStateChanged()
|
|
|
|
c.mutex.Unlock()
|
|
|
|
|
|
|
|
case enable := <-c.connEnabledSignal:
|
|
|
|
outboundCancel()
|
|
|
|
if !enable {
|
|
|
|
connectionsEnabled = false
|
|
|
|
log.Printf("Contact %s connections are disabled", c.Address())
|
|
|
|
}
|
2016-09-30 05:13:55 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
log.Printf("Exiting contact connection loop for %s", c.Address())
|
2017-08-14 02:04:35 +00:00
|
|
|
c.mutex.Lock()
|
|
|
|
if c.connection != nil {
|
|
|
|
c.connection.Conn.Close()
|
|
|
|
c.connection = nil
|
|
|
|
c.onConnectionStateChanged()
|
|
|
|
c.mutex.Unlock()
|
|
|
|
<-connClosedChannel
|
|
|
|
} else {
|
|
|
|
c.mutex.Unlock()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Goroutine to maintain an open contact connection, calls Process and reports when closed.
|
|
|
|
func (c *Contact) handleConnection(conn *connection.Connection, closedChannel chan struct{}) {
|
|
|
|
// Connection does not outlive this function
|
|
|
|
defer func() {
|
|
|
|
conn.Conn.Close()
|
|
|
|
closedChannel <- struct{}{}
|
|
|
|
}()
|
|
|
|
log.Printf("Contact connection for %s ready", conn.RemoteHostname)
|
|
|
|
handler := NewContactProtocolHandler(c, conn)
|
|
|
|
err := conn.Process(handler)
|
|
|
|
if err == nil {
|
|
|
|
// Somebody called Break?
|
|
|
|
err = fmt.Errorf("Connection handler interrupted unexpectedly")
|
|
|
|
}
|
|
|
|
log.Printf("Contact connection for %s closed: %s", conn.RemoteHostname, err)
|
2016-09-30 05:13:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Attempt an outbound connection to the contact, retrying automatically using OnionConnector.
|
|
|
|
// This function _must_ send something to connChannel before returning, unless the context has
|
|
|
|
// been cancelled.
|
2017-08-14 02:04:35 +00:00
|
|
|
func (c *Contact) connectOutbound(ctx context.Context, connChannel chan *connection.Connection) {
|
2016-09-30 05:13:55 +00:00
|
|
|
c.mutex.Lock()
|
|
|
|
connector := OnionConnector{
|
|
|
|
Network: c.core.Network,
|
|
|
|
NeverGiveUp: true,
|
|
|
|
}
|
|
|
|
hostname := c.data.Hostname
|
2017-08-14 02:04:35 +00:00
|
|
|
isRequest := c.data.Request.Pending
|
2016-09-30 05:13:55 +00:00
|
|
|
c.mutex.Unlock()
|
|
|
|
|
|
|
|
for {
|
|
|
|
conn, err := connector.Connect(hostname+":9878", ctx)
|
|
|
|
if err != nil {
|
|
|
|
// The only failure here should be context, because NeverGiveUp
|
|
|
|
// is set, but be robust anyway.
|
|
|
|
if ctx.Err() != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
log.Printf("Contact connection failure: %s", err)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2017-08-14 02:04:35 +00:00
|
|
|
// XXX-protocol Ideally this should all take place under ctx also; easy option is a goroutine
|
|
|
|
// blocked on ctx that kills the connection.
|
2016-09-30 05:13:55 +00:00
|
|
|
log.Printf("Successful outbound connection to contact %s", hostname)
|
2017-08-14 02:04:35 +00:00
|
|
|
oc, err := protocol.NegotiateVersionOutbound(conn, hostname[0:16])
|
2016-09-30 05:13:55 +00:00
|
|
|
if err != nil {
|
2017-08-14 02:04:35 +00:00
|
|
|
log.Printf("Outbound connection version negotiation failed: %v", err)
|
|
|
|
conn.Close()
|
|
|
|
if err := connector.Backoff(ctx); err != nil {
|
|
|
|
return
|
2016-10-28 15:50:04 +00:00
|
|
|
}
|
2017-08-14 02:04:35 +00:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
log.Printf("Outbound connection negotiated version; authenticating")
|
|
|
|
privateKey := c.core.Identity.PrivateKey()
|
|
|
|
known, err := connection.HandleOutboundConnection(oc).ProcessAuthAsClient(&privateKey)
|
|
|
|
if err != nil {
|
|
|
|
log.Printf("Outbound connection authentication failed: %v", err)
|
|
|
|
closeUnhandledConnection(oc)
|
2016-09-30 05:13:55 +00:00
|
|
|
if err := connector.Backoff(ctx); err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
continue
|
2017-08-14 02:04:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if !known && !isRequest {
|
|
|
|
log.Printf("Outbound connection to contact says we are not a known contact for %v", c)
|
|
|
|
// XXX Should move to rejected status, stop attempting connections.
|
|
|
|
closeUnhandledConnection(oc)
|
|
|
|
if err := connector.Backoff(ctx); err != nil {
|
|
|
|
return
|
2016-10-10 00:31:26 +00:00
|
|
|
}
|
2017-08-14 02:04:35 +00:00
|
|
|
continue
|
|
|
|
} else if known && isRequest {
|
|
|
|
log.Printf("Contact request implicitly accepted for outbound connection by contact %v", c)
|
|
|
|
c.UpdateContactRequest("Accepted")
|
2017-09-20 19:43:22 +00:00
|
|
|
isRequest = false
|
|
|
|
}
|
|
|
|
|
|
|
|
if isRequest {
|
|
|
|
// Need to send a contact request; this will block until the peer accepts or rejects,
|
|
|
|
// the connection fails, or the context is cancelled (which also closes the connection).
|
|
|
|
if err := c.sendContactRequest(oc, ctx); err != nil {
|
|
|
|
log.Printf("Outbound contact request connection closed: %s", err)
|
|
|
|
if err := connector.Backoff(ctx); err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
continue
|
|
|
|
} else {
|
|
|
|
log.Printf("Outbound contact request accepted, assigning connection")
|
|
|
|
}
|
2016-09-30 05:13:55 +00:00
|
|
|
}
|
2017-08-14 02:04:35 +00:00
|
|
|
|
|
|
|
log.Printf("Assigning outbound connection to contact")
|
|
|
|
c.AssignConnection(oc)
|
|
|
|
break
|
2016-09-30 05:13:55 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-09-20 19:43:22 +00:00
|
|
|
type requestChannelHandler struct {
|
|
|
|
Response chan string
|
|
|
|
}
|
|
|
|
|
|
|
|
func (r *requestChannelHandler) ContactRequest(name, message string) string {
|
|
|
|
log.Printf("BUG: inbound ContactRequest handler called for outbound channel")
|
|
|
|
return "Error"
|
|
|
|
}
|
|
|
|
func (r *requestChannelHandler) ContactRequestRejected() { r.Response <- "Rejected" }
|
|
|
|
func (r *requestChannelHandler) ContactRequestAccepted() { r.Response <- "Accepted" }
|
|
|
|
func (r *requestChannelHandler) ContactRequestError() { r.Response <- "Error" }
|
|
|
|
|
|
|
|
// sendContactRequest synchronously delivers a contact request to an authenticated
|
|
|
|
// outbound connection and waits for a final (yes/no) reply. This may be cancelled
|
|
|
|
// by closing the connection. Once a reply is received, it's passed to
|
|
|
|
// UpdateContactRequest to update the status and this function will return. nil is
|
|
|
|
// returned for an accepted request when the connection is still established. In all
|
|
|
|
// other cases, an error is returned and the connection will be closed.
|
|
|
|
func (c *Contact) sendContactRequest(conn *connection.Connection, ctx context.Context) error {
|
|
|
|
log.Printf("Sending request to outbound contact %v", c)
|
|
|
|
ach := &connection.AutoConnectionHandler{}
|
|
|
|
ach.Init()
|
|
|
|
|
|
|
|
processChan := make(chan error)
|
|
|
|
responseChan := make(chan string)
|
|
|
|
|
|
|
|
// No timeouts on outbound contact request; wait forever for a final reply
|
|
|
|
go func() {
|
|
|
|
processChan <- conn.Process(ach)
|
|
|
|
}()
|
|
|
|
|
|
|
|
err := conn.Do(func() error {
|
|
|
|
_, 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,
|
|
|
|
})
|
|
|
|
return err
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
// Close and end Process, resulting in an error to processChan and return when done
|
|
|
|
conn.Conn.Close()
|
|
|
|
return <-processChan
|
|
|
|
}
|
|
|
|
|
|
|
|
select {
|
|
|
|
case err := <-processChan:
|
|
|
|
// Should not get nil (via Break) return values here; prevent them
|
|
|
|
if err == nil {
|
|
|
|
closeUnhandledConnection(conn)
|
|
|
|
err = fmt.Errorf("unknown connection break")
|
|
|
|
}
|
|
|
|
return err
|
|
|
|
|
|
|
|
case response := <-responseChan:
|
|
|
|
c.UpdateContactRequest(response)
|
|
|
|
if response == "Accepted" {
|
|
|
|
conn.Break()
|
|
|
|
return <-processChan // nil if connection is still alive
|
|
|
|
} else {
|
|
|
|
conn.Conn.Close()
|
|
|
|
return <-processChan
|
|
|
|
}
|
|
|
|
|
|
|
|
case <-ctx.Done():
|
|
|
|
conn.Conn.Close()
|
|
|
|
return <-processChan
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-08-14 02:04:35 +00:00
|
|
|
// considerUsingConnection takes a newly established connection and decides whether
|
|
|
|
// the new connection is valid and acceptable, and whether to replace or keep an
|
|
|
|
// existing connection. To handle race cases when peers are connecting to eachother,
|
|
|
|
// a particular set of rules is followed for replacing an existing connection.
|
|
|
|
//
|
|
|
|
// considerUsingConnection returns nil if the new connection is valid and should be
|
|
|
|
// used. If this function returns nil, the existing connection has been closed (but
|
|
|
|
// c.connection is unmodified, and the process routine may still be executing). If
|
|
|
|
// this function returns an error, conn has been closed.
|
|
|
|
//
|
|
|
|
// Assumes that c.mutex is held.
|
|
|
|
func (c *Contact) considerUsingConnection(conn *connection.Connection) error {
|
|
|
|
killConn := conn
|
|
|
|
defer func() {
|
|
|
|
if killConn != nil {
|
|
|
|
killConn.Conn.Close()
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
|
|
|
|
if conn.IsInbound {
|
|
|
|
log.Printf("Contact %s has a new inbound connection", c.data.Hostname)
|
2016-09-30 05:13:55 +00:00
|
|
|
} else {
|
2017-08-14 02:04:35 +00:00
|
|
|
log.Printf("Contact %s has a new outbound connection", c.data.Hostname)
|
2016-09-30 05:13:55 +00:00
|
|
|
}
|
|
|
|
|
2016-09-16 00:32:58 +00:00
|
|
|
if conn == c.connection {
|
|
|
|
return fmt.Errorf("Duplicate assignment of connection %v to contact %v", conn, c)
|
|
|
|
}
|
|
|
|
|
2017-08-14 02:04:35 +00:00
|
|
|
if !conn.Authentication["im.ricochet.auth.hidden-service"] {
|
|
|
|
return fmt.Errorf("Connection %v is not authenticated", conn)
|
2016-09-16 00:32:58 +00:00
|
|
|
}
|
|
|
|
|
2017-08-14 02:04:35 +00:00
|
|
|
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])
|
2016-09-16 00:32:58 +00:00
|
|
|
}
|
|
|
|
|
2017-08-14 02:04:35 +00:00
|
|
|
if c.connection != nil && !c.shouldReplaceConnection(conn) {
|
|
|
|
return fmt.Errorf("Using existing connection")
|
2016-09-16 00:32:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// If this connection is inbound and there's an outbound attempt, keep this
|
|
|
|
// connection and cancel outbound if we haven't sent authentication yet, or
|
|
|
|
// if the outbound connection will lose the fallback comparison above.
|
2017-08-14 02:04:35 +00:00
|
|
|
// XXX implement this; currently outbound is always cancelled when an inbound
|
|
|
|
// connection succeeds.
|
2016-09-16 00:32:58 +00:00
|
|
|
|
2017-08-14 02:04:35 +00:00
|
|
|
// We will keep conn, close c.connection instead if there was one
|
|
|
|
killConn = c.connection
|
|
|
|
return nil
|
|
|
|
}
|
2016-09-16 00:32:58 +00:00
|
|
|
|
2017-08-14 02:04:35 +00:00
|
|
|
// onConnectionStateChanged is called by the connection loop when the c.connection
|
|
|
|
// is changed, which can be a transition to online or offline or a replacement.
|
|
|
|
// Assumes c.mutex is held.
|
|
|
|
func (c *Contact) onConnectionStateChanged() {
|
|
|
|
if c.connection != nil {
|
2017-09-20 19:43:22 +00:00
|
|
|
if c.data.Request.Pending && 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")
|
2016-10-27 19:57:04 +00:00
|
|
|
} else {
|
2017-08-14 02:04:35 +00:00
|
|
|
c.status = ricochet.Contact_ONLINE
|
2016-10-27 19:57:04 +00:00
|
|
|
}
|
|
|
|
} else {
|
2017-08-14 02:04:35 +00:00
|
|
|
if c.status == ricochet.Contact_ONLINE {
|
|
|
|
c.status = ricochet.Contact_OFFLINE
|
|
|
|
}
|
2016-10-27 19:57:04 +00:00
|
|
|
}
|
2016-09-20 01:53:21 +00:00
|
|
|
|
|
|
|
// Update LastConnected time
|
2016-11-06 04:58:55 +00:00
|
|
|
c.timeConnected = time.Now()
|
|
|
|
|
2016-09-20 01:53:21 +00:00
|
|
|
config := c.core.Config.OpenWrite()
|
2016-11-06 04:58:55 +00:00
|
|
|
c.data.LastConnected = c.timeConnected.Format(time.RFC3339)
|
2016-09-20 01:53:21 +00:00
|
|
|
config.Contacts[strconv.Itoa(c.id)] = c.data
|
|
|
|
config.Save()
|
|
|
|
|
2017-08-14 02:04:35 +00:00
|
|
|
// _really_ assumes c.mutex was held
|
2016-09-20 01:53:21 +00:00
|
|
|
c.mutex.Unlock()
|
|
|
|
event := ricochet.ContactEvent{
|
|
|
|
Type: ricochet.ContactEvent_UPDATE,
|
|
|
|
Subject: &ricochet.ContactEvent_Contact{
|
|
|
|
Contact: c.Data(),
|
|
|
|
},
|
|
|
|
}
|
|
|
|
c.events.Publish(event)
|
2016-09-16 00:32:58 +00:00
|
|
|
|
2017-08-14 02:04:35 +00:00
|
|
|
if c.connection != nil {
|
|
|
|
// Send any queued messages
|
|
|
|
sent := c.Conversation().SendQueuedMessages()
|
|
|
|
if sent > 0 {
|
|
|
|
log.Printf("Sent %d queued messages to contact", sent)
|
|
|
|
}
|
2016-10-16 00:04:19 +00:00
|
|
|
}
|
|
|
|
|
2016-09-30 05:13:55 +00:00
|
|
|
c.mutex.Lock()
|
|
|
|
}
|
|
|
|
|
2016-09-20 01:53:21 +00:00
|
|
|
// Decide whether to replace the existing connection with conn.
|
|
|
|
// Assumes mutex is held.
|
2017-08-14 02:04:35 +00:00
|
|
|
func (c *Contact) shouldReplaceConnection(conn *connection.Connection) bool {
|
|
|
|
myHostname, _ := PlainHostFromAddress(c.core.Identity.Address())
|
2016-09-20 01:53:21 +00:00
|
|
|
if c.connection == nil {
|
|
|
|
return true
|
2017-08-14 02:04:35 +00:00
|
|
|
} else if c.connection.IsInbound == conn.IsInbound {
|
2016-09-20 01:53:21 +00:00
|
|
|
// If the existing connection is in the same direction, always use the new one
|
|
|
|
log.Printf("Replacing existing same-direction connection %v with new connection %v for contact %v", c.connection, conn, c)
|
|
|
|
return true
|
2016-11-06 04:58:55 +00:00
|
|
|
} else if time.Since(c.timeConnected) > (30 * time.Second) {
|
2016-09-20 01:53:21 +00:00
|
|
|
// If the existing connection is more than 30 seconds old, use the new one
|
2016-11-06 04:58:55 +00:00
|
|
|
log.Printf("Replacing existing %v old connection %v with new connection %v for contact %v", time.Since(c.timeConnected), c.connection, conn, c)
|
|
|
|
return true
|
2017-08-14 02:04:35 +00:00
|
|
|
} else if preferOutbound := myHostname < conn.RemoteHostname; preferOutbound != conn.IsInbound {
|
2016-09-20 01:53:21 +00:00
|
|
|
// Fall back to string comparison of hostnames for a stable resolution
|
|
|
|
// New connection wins
|
|
|
|
log.Printf("Replacing existing connection %v with new connection %v for contact %v according to fallback order", c.connection, conn, c)
|
|
|
|
return true
|
|
|
|
} else {
|
|
|
|
// Old connection wins fallback
|
|
|
|
log.Printf("Keeping existing connection %v instead of new connection %v for contact %v according to fallback order", c.connection, conn, c)
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
2016-09-20 03:32:16 +00:00
|
|
|
|
2016-10-28 15:50:04 +00:00
|
|
|
// Update the status of a contact request from a protocol event. Returns
|
|
|
|
// true if the contact request channel should remain open.
|
|
|
|
func (c *Contact) UpdateContactRequest(status string) bool {
|
|
|
|
c.mutex.Lock()
|
|
|
|
defer c.mutex.Unlock()
|
|
|
|
|
|
|
|
if !c.data.Request.Pending {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
re := c.updateContactRequest(status)
|
|
|
|
|
|
|
|
event := ricochet.ContactEvent{
|
|
|
|
Type: ricochet.ContactEvent_UPDATE,
|
|
|
|
Subject: &ricochet.ContactEvent_Contact{
|
|
|
|
Contact: c.Data(),
|
|
|
|
},
|
|
|
|
}
|
|
|
|
c.events.Publish(event)
|
|
|
|
|
|
|
|
return re
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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 {
|
2016-10-27 19:57:04 +00:00
|
|
|
config := c.core.Config.OpenWrite()
|
2016-10-28 15:50:04 +00:00
|
|
|
now := time.Now().Format(time.RFC3339)
|
|
|
|
// Whether to keep the channel open
|
|
|
|
var re bool
|
|
|
|
|
|
|
|
switch status {
|
|
|
|
case "Pending":
|
|
|
|
c.data.Request.WhenDelivered = now
|
|
|
|
re = true
|
|
|
|
|
|
|
|
case "Accepted":
|
|
|
|
c.data.Request = ConfigContactRequest{}
|
|
|
|
if c.connection != nil {
|
|
|
|
c.status = ricochet.Contact_ONLINE
|
|
|
|
} else {
|
|
|
|
c.status = ricochet.Contact_UNKNOWN
|
|
|
|
}
|
2016-10-27 19:57:04 +00:00
|
|
|
|
2016-10-28 15:50:04 +00:00
|
|
|
case "Rejected":
|
|
|
|
c.data.Request.WhenRejected = now
|
|
|
|
|
|
|
|
case "Error":
|
|
|
|
c.data.Request.WhenRejected = now
|
|
|
|
c.data.Request.RemoteError = "error occurred"
|
|
|
|
|
|
|
|
default:
|
|
|
|
log.Printf("Unknown contact request status '%s'", status)
|
2016-10-27 19:57:04 +00:00
|
|
|
}
|
2016-10-28 15:50:04 +00:00
|
|
|
|
|
|
|
config.Contacts[strconv.Itoa(c.id)] = c.data
|
|
|
|
config.Save()
|
|
|
|
return re
|
2016-10-27 19:57:04 +00:00
|
|
|
}
|
|
|
|
|
2017-08-14 02:04:35 +00:00
|
|
|
// AssignConnection takes new connections, inbound or outbound, to this contact, and
|
|
|
|
// asynchronously decides whether to keep or close them.
|
|
|
|
func (c *Contact) AssignConnection(conn *connection.Connection) {
|
|
|
|
c.connectionOnce.Do(func() {
|
|
|
|
go c.contactConnection()
|
|
|
|
})
|
2016-09-30 05:13:55 +00:00
|
|
|
|
2017-08-14 02:04:35 +00:00
|
|
|
// If connections are disabled, this connection will be closed by contactConnection
|
|
|
|
c.connChannel <- conn
|
2016-09-20 03:32:16 +00:00
|
|
|
}
|