ricochet-go/core/inboundcontactrequest.go

345 lines
9.5 KiB
Go

package core
import (
"errors"
"github.com/ricochet-im/ricochet-go/rpc"
channels "github.com/s-rah/go-ricochet/channels"
connection "github.com/s-rah/go-ricochet/connection"
"log"
"sync"
"time"
)
type InboundContactRequest struct {
core *Ricochet
mutex sync.Mutex
data ricochet.ContactRequest
Address string
// If non-nil, sent the new contact on accept or nil on reject.
// Used to signal back to active connections when request state changes.
contactResultChan chan *Contact
// Called when the request state is changed
StatusChanged func(request *InboundContactRequest)
}
type inboundRequestHandler struct {
Name string
Message string
RequestReceivedChan chan struct{}
ResponseChan chan string
}
func (irh *inboundRequestHandler) ContactRequestRejected() {}
func (irh *inboundRequestHandler) ContactRequestAccepted() {}
func (irh *inboundRequestHandler) ContactRequestError() {}
func (irh *inboundRequestHandler) ContactRequest(name, message string) string {
if len(name) > 0 && !IsNicknameAcceptable(name) {
log.Printf("protocol: Stripping unacceptable nickname from inbound request; encoded: %x", []byte(name))
name = ""
}
irh.Name = name
if len(message) > 0 && !IsMessageAcceptable(message) {
log.Printf("protocol: Stripping unacceptable message from inbound request; len: %d, encoded: %x", len(message), []byte(message))
message = ""
}
irh.Message = message
// Signal to the goroutine that a request was received
irh.RequestReceivedChan <- struct{}{}
// Wait for a response
response := <-irh.ResponseChan
return response
}
// HandleInboundRequestConnection takes an authenticated connection that does not
// associate to any known contact and handles inbound contact request channels.
// If no request is seen after a short timeout, the connection will be closed.
// If a valid request is received, the connection will stay open to wait for the
// user's reply.
//
// This function takes full ownership of the connection. On return, the connection
// will be either closed and a non-nil error returned, or will be assigned to an
// existing Contact along with a nil return value.
func HandleInboundRequestConnection(conn *connection.Connection, contactList *ContactList) error {
log.Printf("Handling inbound contact request connection")
ach := &connection.AutoConnectionHandler{}
ach.Init()
// There may only ever be one contact request channel on the connection
req := &inboundRequestHandler{
RequestReceivedChan: make(chan struct{}),
ResponseChan: make(chan string),
}
// XXX should close conn if the channel goes away...
ach.RegisterChannelHandler("im.ricochet.contact.request", func() channels.Handler {
return &channels.ContactRequestChannel{Handler: req}
})
processChan := make(chan error)
go func() {
processChan <- conn.Process(ach)
}()
address, ok := AddressFromPlainHost(conn.RemoteHostname)
if !ok {
conn.Conn.Close()
return <-processChan
}
// Expecting to receive request data within 15 seconds
select {
case <-req.RequestReceivedChan:
break
case err := <-processChan:
if err == nil {
conn.Conn.Close()
err = errors.New("unexpected break")
}
return err
case <-time.After(15 * time.Second):
// Didn't receive a contact request fast enough
conn.Conn.Close()
return <-processChan
}
// Function to respond to the request; changed after the initial response
respond := func(status string) { req.ResponseChan <- status }
request, contact := contactList.AddOrUpdateInboundContactRequest(address, req.Name, req.Message)
if contact == nil && request != nil && !request.IsRejected() {
// Pending request; keep connection open and wait for a user response
respond("Pending")
contactChan := request.getContactResultChannel()
select {
case c, ok := <-contactChan:
if !ok {
// Replaced by a different connection or otherwise cancelled without reply
conn.Conn.Close()
return <-processChan
}
// Set contact and fall out to handle the reply
contact = c
break
case err := <-processChan:
request.clearContactResultChannel(contactChan)
for {
_, ok := <-contactChan
if !ok {
break
}
}
return err
}
// Change how future responses are sent
respond = func(status string) {
conn.Do(func() error {
channel := conn.Channel("im.ricochet.contact.request", channels.Inbound)
if channel == nil {
return errors.New("no channel")
}
channel.Handler.(*channels.ContactRequestChannel).SendResponse(status)
// Also close the channel; this was a final response
channel.CloseChannel()
return nil
})
}
}
// Have a response (either immediately or after pending)
if contact != nil {
// Accepted
respond("Accepted")
if err := conn.Break(); err != nil {
// Connection lost; but request was accepted anyway, so it'll get reconnected later
return err
}
contact.AssignConnection(conn)
return nil
} else {
// Rejected
respond("Rejected")
if request != nil {
contactList.RemoveInboundContactRequest(request)
}
conn.Conn.Close()
return <-processChan
}
}
// CreateInboundContactRequest constructs a new InboundContactRequest, usually from a newly
// received request on an open connection. Requests are managed through the ContactList, so
// generally you should use ContactList.AddOrUpdateInboundContactRequest instead of calling
// this function directly.
func CreateInboundContactRequest(core *Ricochet, address, nickname, message string) *InboundContactRequest {
cr := &InboundContactRequest{
core: core,
data: ricochet.ContactRequest{
Direction: ricochet.ContactRequest_INBOUND,
Address: address,
Text: message,
FromNickname: nickname,
WhenCreated: time.Now().Format(time.RFC3339),
},
Address: address,
}
return cr
}
// getContactResultChannel returns a channel that will be sent a Contact if the request is
// accepted, nil if the request is rejected, or closed if the channel is no longer used.
// This is used to communciate with active connections for pending requests.
func (cr *InboundContactRequest) getContactResultChannel() chan *Contact {
cr.mutex.Lock()
defer cr.mutex.Unlock()
if cr.contactResultChan != nil {
close(cr.contactResultChan)
}
cr.contactResultChan = make(chan *Contact)
return cr.contactResultChan
}
func (cr *InboundContactRequest) clearContactResultChannel(c chan *Contact) {
cr.mutex.Lock()
defer cr.mutex.Unlock()
if cr.contactResultChan == c {
close(cr.contactResultChan)
cr.contactResultChan = nil
}
}
func (cr *InboundContactRequest) CloseConnection() {
cr.mutex.Lock()
defer cr.mutex.Unlock()
if cr.contactResultChan != nil {
close(cr.contactResultChan)
cr.contactResultChan = nil
}
}
func (cr *InboundContactRequest) Update(nickname, message string) {
cr.mutex.Lock()
defer cr.mutex.Unlock()
if cr.data.Rejected {
return
}
// These should already be validated, but just in case..
if len(nickname) == 0 || IsNicknameAcceptable(nickname) {
cr.data.FromNickname = nickname
}
if len(message) == 0 || IsMessageAcceptable(message) {
cr.data.Text = message
}
if cr.StatusChanged != nil {
cr.StatusChanged(cr)
}
}
func (cr *InboundContactRequest) SetNickname(nickname string) error {
cr.mutex.Lock()
defer cr.mutex.Unlock()
if IsNicknameAcceptable(nickname) {
cr.data.FromNickname = nickname
} else {
return errors.New("Invalid nickname")
}
return nil
}
func (cr *InboundContactRequest) Accept() (*Contact, error) {
cr.mutex.Lock()
defer cr.mutex.Unlock()
if cr.data.Rejected {
log.Printf("Accept called on an inbound contact request that was already rejected; request is %v", cr)
return nil, errors.New("Contact request has already been rejected")
}
log.Printf("Accepting contact request from %s", cr.data.Address)
onion, _ := OnionFromAddress(cr.data.Address)
configContact := ConfigContact{
Hostname: onion,
Nickname: cr.data.FromNickname,
WhenCreated: cr.data.WhenCreated,
}
contact, err := cr.core.Identity.ContactList().AddNewContact(configContact)
if err != nil {
log.Printf("Error occurred in accepting contact request: %s", err)
return nil, err
}
if err := cr.acceptWithContact(contact); err != nil {
return contact, err
}
return contact, nil
}
func (cr *InboundContactRequest) AcceptWithContact(contact *Contact) error {
cr.mutex.Lock()
defer cr.mutex.Unlock()
return cr.acceptWithContact(contact)
}
// Assumes mutex
func (cr *InboundContactRequest) acceptWithContact(contact *Contact) error {
if contact.Address() != cr.data.Address {
return errors.New("Contact address does not match request in accept")
}
// Send to active connection if present
if cr.contactResultChan != nil {
cr.contactResultChan <- contact
close(cr.contactResultChan)
cr.contactResultChan = nil
}
cr.core.Identity.ContactList().RemoveInboundContactRequest(cr)
return nil
}
func (cr *InboundContactRequest) Reject() {
cr.mutex.Lock()
defer cr.mutex.Unlock()
if cr.data.Rejected {
return
}
log.Printf("Rejecting contact request from %s", cr.data.Address)
cr.data.Rejected = true
// Signal to the active connection
if cr.contactResultChan != nil {
cr.contactResultChan <- nil
close(cr.contactResultChan)
cr.contactResultChan = nil
}
// Signal update to the callback (probably from ContactList)
if cr.StatusChanged != nil {
cr.StatusChanged(cr)
}
cr.core.Identity.ContactList().RemoveInboundContactRequest(cr)
}
func (cr *InboundContactRequest) Data() ricochet.ContactRequest {
return cr.data
}
func (cr *InboundContactRequest) IsRejected() bool {
cr.mutex.Lock()
defer cr.mutex.Unlock()
return cr.data.Rejected
}