cwtch/protocol/connections/tokenboardclientapp.go

226 lines
8.2 KiB
Go

package connections
import (
"cwtch.im/cwtch/protocol/groups"
"encoding/json"
"errors"
"git.openprivacy.ca/cwtch.im/tapir"
"git.openprivacy.ca/cwtch.im/tapir/applications"
"git.openprivacy.ca/cwtch.im/tapir/networks/tor"
"git.openprivacy.ca/cwtch.im/tapir/primitives"
"git.openprivacy.ca/cwtch.im/tapir/primitives/privacypass"
"git.openprivacy.ca/openprivacy/connectivity"
"git.openprivacy.ca/openprivacy/log"
"github.com/gtank/ristretto255"
"reflect"
"time"
)
// TokenBoardHandler encapsulates all the various handlers a client needs to interact with a token board
// this includes handlers to receive new messages, as well as handlers to manage tokens.
type TokenBoardHandler interface {
GroupMessageHandler(server string, gm *groups.EncryptedGroupMessage)
ServerAuthedHandler(server string)
ServerSyncedHandler(server string)
ServerClosedHandler(server string)
NewTokenHandler(tokenService string, tokens []*privacypass.Token)
FetchToken(tokenService string) (*privacypass.Token, int, error)
}
// NewTokenBoardClient generates a new Client for Token Board
func NewTokenBoardClient(acn connectivity.ACN, Y *ristretto255.Element, tokenServiceOnion string, lastKnownSignature []byte, tokenBoardHandler TokenBoardHandler) tapir.Application {
tba := new(TokenBoardClient)
tba.acn = acn
tba.tokenService = privacypass.NewTokenServer()
tba.tokenService.Y = Y
tba.tokenServiceOnion = tokenServiceOnion
tba.tokenBoardHandler = tokenBoardHandler
tba.lastKnownSignature = lastKnownSignature
return tba
}
// TokenBoardClient defines a client for the TokenBoard server
type TokenBoardClient struct {
applications.AuthApp
connection tapir.Connection
tokenBoardHandler TokenBoardHandler
// Token service handling
acn connectivity.ACN
tokenService *privacypass.TokenServer
tokenServiceOnion string
lastKnownSignature []byte
}
// NewInstance Client a new TokenBoardApp
func (ta *TokenBoardClient) NewInstance() tapir.Application {
tba := new(TokenBoardClient)
tba.tokenBoardHandler = ta.tokenBoardHandler
tba.acn = ta.acn
tba.tokenService = ta.tokenService
tba.tokenServiceOnion = ta.tokenServiceOnion
tba.lastKnownSignature = ta.lastKnownSignature
return tba
}
// Init initializes the cryptographic TokenBoardApp
func (ta *TokenBoardClient) Init(connection tapir.Connection) {
ta.AuthApp.Init(connection)
if connection.HasCapability(applications.AuthCapability) {
ta.connection = connection
ta.tokenBoardHandler.ServerAuthedHandler(ta.connection.Hostname())
log.Debugf("Successfully Initialized Connection to %v", connection.Hostname())
go ta.Listen()
// Optimistically acquire many tokens for this server...
go ta.MakePayment()
go ta.MakePayment()
ta.Replay()
} else {
connection.Close()
}
}
// Listen processes the messages for this application
func (ta *TokenBoardClient) Listen() {
for {
log.Debugf("Client waiting...")
data := ta.connection.Expect()
if len(data) == 0 {
log.Debugf("Server closed the connection...")
ta.tokenBoardHandler.ServerClosedHandler(ta.connection.Hostname())
return // connection is closed
}
// We always expect the server to follow protocol, and the second it doesn't we close the connection
var message groups.Message
if err := json.Unmarshal(data, &message); err != nil {
log.Debugf("Server sent an unexpected message, closing the connection: %v", err)
ta.tokenBoardHandler.ServerClosedHandler(ta.connection.Hostname())
ta.connection.Close()
return
}
switch message.MessageType {
case groups.NewMessageMessage:
if message.NewMessage != nil {
ta.tokenBoardHandler.GroupMessageHandler(ta.connection.Hostname(), &message.NewMessage.EGM)
} else {
log.Debugf("Server sent an unexpected NewMessage, closing the connection: %s", data)
ta.tokenBoardHandler.ServerClosedHandler(ta.connection.Hostname())
ta.connection.Close()
return
}
case groups.PostResultMessage:
// TODO handle failure
case groups.ReplayResultMessage:
if message.ReplayResult != nil {
log.Debugf("Replaying %v Messages...", message.ReplayResult.NumMessages)
for i := 0; i < message.ReplayResult.NumMessages; i++ {
data := ta.connection.Expect()
if len(data) == 0 {
log.Debugf("Server sent an unexpected EncryptedGroupMessage, closing the connection")
ta.tokenBoardHandler.ServerClosedHandler(ta.connection.Hostname())
ta.connection.Close()
return
}
egm := &groups.EncryptedGroupMessage{}
if err := json.Unmarshal(data, egm); err == nil {
ta.tokenBoardHandler.GroupMessageHandler(ta.connection.Hostname(), egm)
ta.lastKnownSignature = egm.Signature
} else {
log.Debugf("Server sent an unexpected EncryptedGroupMessage, closing the connection: %v", err)
ta.tokenBoardHandler.ServerClosedHandler(ta.connection.Hostname())
ta.connection.Close()
return
}
}
ta.tokenBoardHandler.ServerSyncedHandler(ta.connection.Hostname())
ta.connection.SetCapability(groups.CwtchServerSyncedCapability)
}
}
}
}
// Replay posts a Replay Message to the server.
func (ta *TokenBoardClient) Replay() {
data, _ := json.Marshal(groups.Message{MessageType: groups.ReplayRequestMessage, ReplayRequest: &groups.ReplayRequest{LastCommit: ta.lastKnownSignature}})
ta.connection.Send(data)
}
// PurchaseTokens purchases the given number of tokens from the server (using the provided payment handler)
func (ta *TokenBoardClient) PurchaseTokens() {
ta.MakePayment()
}
// Post sends a Post Request to the server
func (ta *TokenBoardClient) Post(ct []byte, sig []byte) (bool, int) {
egm := groups.EncryptedGroupMessage{Ciphertext: ct, Signature: sig}
token, numTokens, err := ta.NextToken(egm.ToBytes(), ta.connection.Hostname())
if err == nil {
data, _ := json.Marshal(groups.Message{MessageType: groups.PostRequestMessage, PostRequest: &groups.PostRequest{EGM: egm, Token: token}})
log.Debugf("Message Length: %s %v", data, len(data))
err := ta.connection.Send(data)
if err != nil {
return false, numTokens
}
return true, numTokens
}
log.Debugf("No Valid Tokens: %v", err)
return false, numTokens
}
// MakePayment uses the PoW based token protocol to obtain more tokens
func (ta *TokenBoardClient) MakePayment() error {
log.Debugf("Making a Payment")
id, sk := primitives.InitializeEphemeralIdentity()
client := new(tor.BaseOnionService)
client.Init(ta.acn, sk, &id)
defer client.Shutdown()
tokenApplication := new(applications.TokenApplication)
tokenApplication.TokenService = ta.tokenService
powTokenApp := new(applications.ApplicationChain).
ChainApplication(new(applications.ProofOfWorkApplication), applications.SuccessfulProofOfWorkCapability).
ChainApplication(tokenApplication, applications.HasTokensCapability)
log.Debugf("Waiting for successful PoW Auth...")
connected, err := client.Connect(ta.tokenServiceOnion, powTokenApp)
if connected && err == nil {
log.Debugf("Waiting for successful Token Acquisition...")
conn, err := client.WaitForCapabilityOrClose(ta.tokenServiceOnion, applications.HasTokensCapability)
if err == nil {
powtapp, ok := conn.App().(*applications.TokenApplication)
if ok {
log.Debugf("Updating Tokens")
ta.tokenBoardHandler.NewTokenHandler(ta.tokenServiceOnion, powtapp.Tokens)
log.Debugf("Transcript: %v", powtapp.Transcript().OutputTranscriptToAudit())
conn.Close()
return nil
}
log.Errorf("invalid cast of powapp. this should never happen %v %v", powtapp, reflect.TypeOf(conn.App()))
return errors.New("invalid cast of powapp. this should never happen")
}
log.Debugf("could not connect to payment server..trying again: %v", err)
return ta.MakePayment()
} else if connected && err != nil {
log.Debugf("inexplicable error: %v", err)
}
log.Debugf("failed to make a connection. trying again...")
// it doesn't actually take that long to make a payment, so waiting a small amount of time should suffice
time.Sleep(time.Second)
return ta.MakePayment()
}
// NextToken retrieves the next token
func (ta *TokenBoardClient) NextToken(data []byte, hostname string) (privacypass.SpentToken, int, error) {
token, numtokens, err := ta.tokenBoardHandler.FetchToken(ta.tokenServiceOnion)
if err != nil {
return privacypass.SpentToken{}, numtokens, err
}
return token.SpendToken(append(data, hostname...)), numtokens, nil
}