mirror of
https://github.com/status-im/status-console-client.git
synced 2025-02-25 17:15:14 +00:00
303 lines
7.1 KiB
Go
303 lines
7.1 KiB
Go
package adapters
|
|
|
|
import (
|
|
"context"
|
|
"crypto/ecdsa"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"log"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/status-im/status-console-client/protocol/v1"
|
|
|
|
"github.com/ethereum/go-ethereum/crypto"
|
|
"github.com/ethereum/go-ethereum/rpc"
|
|
|
|
"github.com/status-im/whisper/shhclient"
|
|
|
|
whisper "github.com/status-im/whisper/whisperv6"
|
|
)
|
|
|
|
type whisperClientKeysManager struct {
|
|
client *shhclient.Client
|
|
privateKey *ecdsa.PrivateKey
|
|
|
|
passToSymMutex sync.RWMutex
|
|
passToSymKeyCache map[string]string
|
|
}
|
|
|
|
func (m *whisperClientKeysManager) PrivateKey() *ecdsa.PrivateKey {
|
|
return m.privateKey
|
|
}
|
|
|
|
func (m *whisperClientKeysManager) AddOrGetKeyPair(priv *ecdsa.PrivateKey) (string, error) {
|
|
// caching is handled in Whisper
|
|
return m.client.AddPrivateKey(context.Background(), crypto.FromECDSA(priv))
|
|
}
|
|
|
|
func (m *whisperClientKeysManager) AddOrGetSymKeyFromPassword(password string) (string, error) {
|
|
m.passToSymMutex.Lock()
|
|
defer m.passToSymMutex.Unlock()
|
|
|
|
if val, ok := m.passToSymKeyCache[password]; ok {
|
|
return val, nil
|
|
}
|
|
|
|
id, err := m.client.GenerateSymmetricKeyFromPassword(context.Background(), password)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
m.passToSymKeyCache[password] = id
|
|
|
|
return id, nil
|
|
}
|
|
|
|
func (m *whisperClientKeysManager) GetRawSymKey(id string) ([]byte, error) {
|
|
return nil, errors.New("not implemented")
|
|
}
|
|
|
|
// WhisperClientAdapter is an adapter for Whisper client
|
|
// which implements Protocol interface. It requires an RPC client
|
|
// which can use various transports like HTTP, IPC or in-proc.
|
|
type WhisperClientAdapter struct {
|
|
rpcClient *rpc.Client
|
|
shhClient *shhclient.Client
|
|
keysManager *whisperClientKeysManager
|
|
|
|
mailServerEnodes []string
|
|
selectedMailServerEnode string
|
|
}
|
|
|
|
// WhisperClientAdapter must implement Protocol interface.
|
|
var _ protocol.Protocol = (*WhisperClientAdapter)(nil)
|
|
|
|
// NewWhisperClientAdapter returns a new WhisperClientAdapter.
|
|
func NewWhisperClientAdapter(c *rpc.Client, privateKey *ecdsa.PrivateKey, mailServers []string) *WhisperClientAdapter {
|
|
client := shhclient.NewClient(c)
|
|
|
|
return &WhisperClientAdapter{
|
|
rpcClient: c,
|
|
shhClient: client,
|
|
mailServerEnodes: mailServers,
|
|
keysManager: &whisperClientKeysManager{
|
|
client: client,
|
|
privateKey: privateKey,
|
|
},
|
|
}
|
|
}
|
|
|
|
// Subscribe subscribes to a public channel.
|
|
// in channel is used to receive messages.
|
|
func (a *WhisperClientAdapter) Subscribe(
|
|
ctx context.Context,
|
|
in chan<- *protocol.Message,
|
|
options protocol.SubscribeOptions,
|
|
) (*protocol.Subscription, error) {
|
|
if err := options.Validate(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
criteria := newCriteria(a.keysManager)
|
|
if err := updateCriteriaFromSubscribeOptions(criteria, options); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return a.subscribeMessages(ctx, criteria.ToWhisper(), in)
|
|
}
|
|
|
|
func (a *WhisperClientAdapter) subscribeMessages(
|
|
ctx context.Context,
|
|
crit whisper.Criteria,
|
|
in chan<- *protocol.Message,
|
|
) (*protocol.Subscription, error) {
|
|
messages := make(chan *whisper.Message)
|
|
shhSub, err := a.shhClient.SubscribeMessages(ctx, crit, messages)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
sub := protocol.NewSubscription()
|
|
|
|
go func() {
|
|
defer shhSub.Unsubscribe()
|
|
|
|
for {
|
|
select {
|
|
case raw := <-messages:
|
|
m, err := protocol.DecodeMessage(raw.Payload)
|
|
if err != nil {
|
|
log.Printf("failed to decode message: %v", err)
|
|
break
|
|
}
|
|
|
|
sigPubKey, err := crypto.UnmarshalPubkey(raw.Sig)
|
|
if err != nil {
|
|
log.Printf("failed to get a signature: %v", err)
|
|
break
|
|
}
|
|
m.SigPubKey = sigPubKey
|
|
|
|
in <- &m
|
|
case err := <-shhSub.Err():
|
|
sub.Cancel(err)
|
|
return
|
|
case <-sub.Done():
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
|
|
return sub, nil
|
|
}
|
|
|
|
// Send sends a new message to a public chat.
|
|
// Identity is required to sign a message as only signed messages
|
|
// are accepted and displayed.
|
|
func (a *WhisperClientAdapter) Send(
|
|
ctx context.Context,
|
|
data []byte,
|
|
options protocol.SendOptions,
|
|
) ([]byte, error) {
|
|
if err := options.Validate(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
newMessage, err := newNewMessage(a.keysManager, data)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := updateNewMessageFromSendOptions(newMessage, options); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
hash, err := a.shhClient.Post(ctx, newMessage.ToWhisper())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return hex.DecodeString(hash)
|
|
}
|
|
|
|
// Request sends a request to MailServer for historic messages.
|
|
func (a *WhisperClientAdapter) Request(ctx context.Context, params protocol.RequestOptions) error {
|
|
enode, err := a.selectAndAddMailServer(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return a.requestMessages(ctx, enode, params)
|
|
}
|
|
|
|
func (a *WhisperClientAdapter) selectAndAddMailServer(ctx context.Context) (string, error) {
|
|
if a.selectedMailServerEnode != "" {
|
|
return a.selectedMailServerEnode, nil
|
|
}
|
|
|
|
enode := randomItem(a.mailServerEnodes)
|
|
|
|
if err := a.rpcClient.CallContext(ctx, nil, "admin_addPeer", enode); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
// Adding peer is asynchronous operation so we need to retry a few times.
|
|
retries := 0
|
|
for {
|
|
err := a.shhClient.MarkTrustedPeer(ctx, enode)
|
|
if ctx.Err() == context.Canceled {
|
|
log.Printf("requesting public messages canceled")
|
|
return "", err
|
|
}
|
|
if err == nil {
|
|
break
|
|
}
|
|
if retries < 3 {
|
|
retries++
|
|
<-time.After(time.Second)
|
|
} else {
|
|
return "", fmt.Errorf("failed to mark peer as trusted: %v", err)
|
|
}
|
|
}
|
|
|
|
a.selectedMailServerEnode = enode
|
|
|
|
return enode, nil
|
|
}
|
|
|
|
func (a *WhisperClientAdapter) requestMessages(ctx context.Context, enode string, options protocol.RequestOptions) error {
|
|
log.Printf("requesting messages from node %s", enode)
|
|
|
|
mailSymKeyID, err := a.keysManager.AddOrGetSymKeyFromPassword(MailServerPassword)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
arg, err := createShhextRequestMessagesParam(enode, mailSymKeyID, options)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return a.rpcClient.CallContext(ctx, nil, "shhext_requestMessages", arg)
|
|
}
|
|
|
|
type criteria struct {
|
|
whisper.Criteria
|
|
keys keysManager
|
|
}
|
|
|
|
func newCriteria(keys keysManager) *criteria {
|
|
return &criteria{
|
|
Criteria: whisper.Criteria{
|
|
MinPow: WhisperPoW,
|
|
AllowP2P: true, // messages from mail server are direct p2p messages
|
|
},
|
|
keys: keys,
|
|
}
|
|
}
|
|
|
|
func (c *criteria) ToWhisper() whisper.Criteria {
|
|
return c.Criteria
|
|
}
|
|
|
|
func (c *criteria) updateForPublicGroup(name string) error {
|
|
topic, err := ToTopic(name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
c.Topics = append(c.Topics, topic)
|
|
|
|
symKeyID, err := c.keys.AddOrGetSymKeyFromPassword(name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
c.SymKeyID = symKeyID
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *criteria) updateForPrivate(name string, recipient *ecdsa.PublicKey) error {
|
|
topic, err := ToTopic(name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
c.Topics = append(c.Topics, topic)
|
|
|
|
keyID, err := c.keys.AddOrGetKeyPair(c.keys.PrivateKey())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
c.PrivateKeyID = keyID
|
|
|
|
return nil
|
|
}
|
|
|
|
func updateCriteriaFromSubscribeOptions(c *criteria, options protocol.SubscribeOptions) error {
|
|
if options.Recipient != nil && options.ChatName != "" {
|
|
return c.updateForPrivate(options.ChatName, options.Recipient)
|
|
} else if options.ChatName != "" {
|
|
return c.updateForPublicGroup(options.ChatName)
|
|
} else {
|
|
return errors.New("unrecognized options")
|
|
}
|
|
}
|