status-im/status-go

View on GitHub
server/pairing/client.go

Summary

Maintainability
A
0 mins
Test Coverage
D
63%
package pairing

import (
    "bytes"
    "crypto/tls"
    "crypto/x509"
    "encoding/json"
    "encoding/pem"
    "fmt"
    "io"
    "net"
    "net/http"
    "net/http/cookiejar"
    "net/url"
    "runtime"

    "go.uber.org/zap"

    "github.com/status-im/status-go/api"
    gocommon "github.com/status-im/status-go/common"
    "github.com/status-im/status-go/logutils"
    "github.com/status-im/status-go/server"
    "github.com/status-im/status-go/signal"
    "github.com/status-im/status-go/timesource"
)

/*
|--------------------------------------------------------------------------
| BaseClient
|--------------------------------------------------------------------------
|
|
|
*/

// BaseClient is responsible for lower level pairing.Client functionality common to dependent Client types
type BaseClient struct {
    *http.Client
    serverCert     *x509.Certificate
    baseAddress    *url.URL
    challengeTaker *ChallengeTaker
}

func findServerCert(c *ConnectionParams, reachableIPs []net.IP) (*url.URL, *x509.Certificate, error) {
    var baseAddress *url.URL
    var serverCert *x509.Certificate

    type connectionError struct {
        ip  net.IP
        err error
    }
    errCh := make(chan connectionError, len(reachableIPs))

    type result struct {
        u    *url.URL
        cert *x509.Certificate
    }
    successCh := make(chan result, 1) // as we close on the first success

    for _, ip := range reachableIPs {
        go func(ip net.IP) {
            defer gocommon.LogOnPanic()
            u := c.BuildURL(ip)
            cert, err := getServerCert(u)
            if err != nil {
                errCh <- connectionError{ip: ip, err: fmt.Errorf("connecting to '%s' failed: %s", u, err.Error())}
                return
            }
            // If no error, send the results to the success channel
            successCh <- result{u: u, cert: cert}
        }(ip)
    }

    // Keep track of error counts
    errorCount := 0
    var combinedErrors string
    for {
        select {
        case success := <-successCh:
            baseAddress = success.u
            serverCert = success.cert
            return baseAddress, serverCert, nil
        case ipErr := <-errCh:
            errorCount++
            combinedErrors += fmt.Sprintf("IP %s: %s; ", ipErr.ip, ipErr.err)
            if errorCount == len(reachableIPs) {
                return nil, nil, fmt.Errorf(combinedErrors)
            }
        }
    }
}

// NewBaseClient returns a fully qualified BaseClient from the given ConnectionParams
func NewBaseClient(c *ConnectionParams, logger *zap.Logger) (*BaseClient, error) {
    var baseAddress *url.URL
    var serverCert *x509.Certificate
    var certErrs error

    netIPs, err := server.FindReachableAddressesForPairingClient(c.netIPs)
    if err != nil {
        logger.Error("[local pair client] failed to find reachable addresses", zap.Error(err), zap.Any("netIPs", netIPs))
        signal.SendLocalPairingEvent(Event{Type: EventConnectionError, Error: err.Error(), Action: ActionConnect})
        return nil, err
    }
    // if client and server aren't on the same network, netIPs maybe empty, we should check it before invoking findServerCert
    if len(netIPs) == 0 {
        logger.Error("[local pair client] no reachable addresses found")
        signal.SendLocalPairingEvent(Event{Type: EventConnectionError, Error: "no reachable addresses found", Action: ActionConnect})
        return nil, fmt.Errorf("no reachable addresses found")
    }

    maxRetries := 3
    for i := 0; i < maxRetries; i++ {
        baseAddress, serverCert, certErrs = findServerCert(c, netIPs)
        if serverCert == nil {
            certErrs = fmt.Errorf("failed to connect to any of given addresses. %w", certErrs)
            logger.Warn("failed to connect to any of given addresses. Retrying...", zap.Error(certErrs), zap.Any("netIPs", netIPs), zap.Int("retry", i+1))
        } else {
            break
        }
    }

    if serverCert == nil {
        certErrs = fmt.Errorf("failed to connect to any of given addresses. %w", certErrs)
        signal.SendLocalPairingEvent(Event{Type: EventConnectionError, Error: certErrs.Error(), Action: ActionConnect})
        return nil, certErrs
    }

    // No error on the dial out then the URL.Host is accessible
    signal.SendLocalPairingEvent(Event{Type: EventConnectionSuccess, Action: ActionConnect})

    err = verifyCert(serverCert, c.publicKey)
    if err != nil {
        return nil, err
    }

    certPem := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: serverCert.Raw})

    rootCAs, err := x509.SystemCertPool()
    if err != nil {
        return nil, err
    }
    if ok := rootCAs.AppendCertsFromPEM(certPem); !ok {
        return nil, fmt.Errorf("failed to append certPem to rootCAs")
    }

    tr := &http.Transport{
        TLSClientConfig: &tls.Config{
            MinVersion:         tls.VersionTLS12,
            InsecureSkipVerify: false, // MUST BE FALSE
            RootCAs:            rootCAs,
            Time:               timesource.GetCurrentTime,
        },
    }

    cj, err := cookiejar.New(nil)
    if err != nil {
        return nil, err
    }

    return &BaseClient{
        Client:         &http.Client{Transport: tr, Jar: cj},
        serverCert:     serverCert,
        challengeTaker: NewChallengeTaker(NewPayloadEncryptor(c.aesKey)),
        baseAddress:    baseAddress,
    }, nil
}

// getChallenge makes a call to the identified Server and receives a [32]byte challenge
func (c *BaseClient) getChallenge() error {
    c.baseAddress.Path = pairingChallenge
    resp, err := c.Get(c.baseAddress.String())
    if err != nil {
        return err
    }

    if resp.StatusCode != http.StatusOK {
        return fmt.Errorf("[client] status not ok when getting challenge, received '%s'", resp.Status)
    }

    return c.challengeTaker.SetChallenge(resp)
}

/*
|--------------------------------------------------------------------------
| SenderClient
|--------------------------------------------------------------------------
|
| With AccountPayloadMounter, RawMessagePayloadMounter and InstallationPayloadMounterReceiver
|
*/

// SenderClient is responsible for sending pairing data to a ReceiverServer
type SenderClient struct {
    *BaseClient
    accountMounter      PayloadMounter
    rawMessageMounter   PayloadMounter
    installationMounter PayloadMounterReceiver
}

// NewSenderClient returns a fully qualified SenderClient created with the incoming parameters
func NewSenderClient(backend *api.GethStatusBackend, c *ConnectionParams, config *SenderClientConfig) (*SenderClient, error) {
    logger := logutils.ZapLogger().Named("SenderClient")
    pe := NewPayloadEncryptor(c.aesKey)

    bc, err := NewBaseClient(c, logger)
    if err != nil {
        return nil, err
    }

    am, rmm, imr, err := NewPayloadMounters(logger, pe, backend, config.SenderConfig)
    if err != nil {
        return nil, err
    }

    return &SenderClient{
        BaseClient:          bc,
        accountMounter:      am,
        rawMessageMounter:   rmm,
        installationMounter: imr,
    }, nil
}

func (c *SenderClient) sendAccountData() error {
    err := c.accountMounter.Mount()
    if err != nil {
        return err
    }

    c.baseAddress.Path = pairingReceiveAccount
    resp, err := c.Post(c.baseAddress.String(), "application/octet-stream", bytes.NewBuffer(c.accountMounter.ToSend()))
    if err != nil {
        signal.SendLocalPairingEvent(Event{Type: EventTransferError, Error: err.Error(), Action: ActionPairingAccount})
        return err
    }

    if resp.StatusCode != http.StatusOK {
        err = fmt.Errorf("[client] status not ok when sending account data, received '%s'", resp.Status)
        signal.SendLocalPairingEvent(Event{Type: EventTransferError, Error: err.Error(), Action: ActionPairingAccount})
        return err
    }

    signal.SendLocalPairingEvent(Event{Type: EventTransferSuccess, Action: ActionPairingAccount})

    c.accountMounter.LockPayload()
    return nil
}

func (c *SenderClient) sendSyncDeviceData() error {
    err := c.rawMessageMounter.Mount()
    if err != nil {
        return err
    }

    c.baseAddress.Path = pairingReceiveSyncDevice
    resp, err := c.Post(c.baseAddress.String(), "application/octet-stream", bytes.NewBuffer(c.rawMessageMounter.ToSend()))
    if err != nil {
        signal.SendLocalPairingEvent(Event{Type: EventTransferError, Error: err.Error(), Action: ActionSyncDevice})
        return err
    }

    if resp.StatusCode != http.StatusOK {
        err = fmt.Errorf("[client] status not okay when sending sync device data, status: %s", resp.Status)
        signal.SendLocalPairingEvent(Event{Type: EventTransferError, Error: err.Error(), Action: ActionSyncDevice})
        return err
    }

    signal.SendLocalPairingEvent(Event{Type: EventTransferSuccess, Action: ActionSyncDevice})
    return nil
}

func (c *SenderClient) receiveInstallationData() error {
    c.baseAddress.Path = pairingSendInstallation
    req, err := http.NewRequest(http.MethodGet, c.baseAddress.String(), nil)
    if err != nil {
        return err
    }

    err = c.challengeTaker.DoChallenge(req)
    if err != nil {
        signal.SendLocalPairingEvent(Event{Type: EventTransferError, Error: err.Error(), Action: ActionPairingInstallation})
        return err
    }

    resp, err := c.Do(req)
    if err != nil {
        signal.SendLocalPairingEvent(Event{Type: EventTransferError, Error: err.Error(), Action: ActionPairingInstallation})
        return err
    }

    if resp.StatusCode != http.StatusOK {
        err = fmt.Errorf("[client] status not ok when receiving installation data, received '%s'", resp.Status)
        signal.SendLocalPairingEvent(Event{Type: EventTransferError, Error: err.Error(), Action: ActionPairingInstallation})
        return err
    }

    payload, err := io.ReadAll(resp.Body)
    if err != nil {
        signal.SendLocalPairingEvent(Event{Type: EventTransferError, Error: err.Error(), Action: ActionPairingInstallation})
        return err
    }
    signal.SendLocalPairingEvent(Event{Type: EventTransferSuccess, Action: ActionPairingInstallation})

    err = c.installationMounter.Receive(payload)
    if err != nil {
        signal.SendLocalPairingEvent(Event{Type: EventProcessError, Error: err.Error(), Action: ActionPairingInstallation})
        return err
    }
    signal.SendLocalPairingEvent(Event{Type: EventProcessSuccess, Action: ActionPairingInstallation})
    return nil
}

// setupSendingClient creates a new SenderClient after parsing string inputs
func setupSendingClient(backend *api.GethStatusBackend, cs, configJSON string) (*SenderClient, error) {
    ccp := new(ConnectionParams)
    err := ccp.FromString(cs)
    if err != nil {
        return nil, err
    }

    conf := NewSenderClientConfig()
    err = json.Unmarshal([]byte(configJSON), conf)
    if err != nil {
        return nil, err
    }
    err = validateAndVerifyPassword(conf, conf.SenderConfig)
    if err != nil {
        return nil, err
    }

    conf.SenderConfig.DB = backend.GetMultiaccountDB()

    return NewSenderClient(backend, ccp, conf)
}

// StartUpSendingClient creates a SenderClient and triggers all `send` calls in sequence to the ReceiverServer
func StartUpSendingClient(backend *api.GethStatusBackend, cs, configJSON string) error {
    c, err := setupSendingClient(backend, cs, configJSON)
    if err != nil {
        return err
    }
    err = c.sendAccountData()
    if err != nil {
        return err
    }
    err = c.sendSyncDeviceData()
    if err != nil {
        return err
    }
    err = c.getChallenge()
    if err != nil {
        return err
    }
    return c.receiveInstallationData()
}

/*
|--------------------------------------------------------------------------
| ReceiverClient
|--------------------------------------------------------------------------
|
| With AccountPayloadReceiver, RawMessagePayloadReceiver, InstallationPayloadMounterReceiver
|
*/

// ReceiverClient is responsible for accepting pairing data to a SenderServer
type ReceiverClient struct {
    *BaseClient

    accountReceiver      PayloadReceiver
    rawMessageReceiver   PayloadReceiver
    installationReceiver PayloadMounterReceiver
}

// NewReceiverClient returns a fully qualified ReceiverClient created with the incoming parameters
func NewReceiverClient(backend *api.GethStatusBackend, c *ConnectionParams, config *ReceiverClientConfig) (*ReceiverClient, error) {
    logger := logutils.ZapLogger().Named("ReceiverClient")

    bc, err := NewBaseClient(c, logger)
    if err != nil {
        return nil, err
    }

    pe := NewPayloadEncryptor(c.aesKey)

    ar, rmr, imr, err := NewPayloadReceivers(logger, pe, backend, config.ReceiverConfig)
    if err != nil {
        return nil, err
    }

    return &ReceiverClient{
        BaseClient:           bc,
        accountReceiver:      ar,
        rawMessageReceiver:   rmr,
        installationReceiver: imr,
    }, nil
}

func (c *ReceiverClient) receiveAccountData() error {
    c.baseAddress.Path = pairingSendAccount
    req, err := http.NewRequest(http.MethodGet, c.baseAddress.String(), nil)
    if err != nil {
        return err
    }

    err = c.challengeTaker.DoChallenge(req)
    if err != nil {
        signal.SendLocalPairingEvent(Event{Type: EventTransferError, Error: err.Error(), Action: ActionPairingAccount})
        return err
    }

    resp, err := c.Do(req)
    if err != nil {
        signal.SendLocalPairingEvent(Event{Type: EventTransferError, Error: err.Error(), Action: ActionPairingAccount})
        return err
    }

    if resp.StatusCode != http.StatusOK {
        err = fmt.Errorf("[client] status not ok when receiving account data, received '%s'", resp.Status)
        signal.SendLocalPairingEvent(Event{Type: EventTransferError, Error: err.Error(), Action: ActionPairingAccount})
        return err
    }

    payload, err := io.ReadAll(resp.Body)
    if err != nil {
        signal.SendLocalPairingEvent(Event{Type: EventTransferError, Error: err.Error(), Action: ActionPairingAccount})
        return err
    }
    signal.SendLocalPairingEvent(Event{Type: EventTransferSuccess, Action: ActionPairingAccount})

    err = c.accountReceiver.Receive(payload)
    if err != nil {
        signal.SendLocalPairingEvent(Event{Type: EventProcessError, Error: err.Error(), Action: ActionPairingAccount})
        return err
    }
    signal.SendLocalPairingEvent(Event{Type: EventProcessSuccess, Action: ActionPairingAccount})
    return nil
}

func (c *ReceiverClient) receiveSyncDeviceData() error {
    c.baseAddress.Path = pairingSendSyncDevice
    req, err := http.NewRequest(http.MethodGet, c.baseAddress.String(), nil)
    if err != nil {
        return err
    }

    err = c.challengeTaker.DoChallenge(req)
    if err != nil {
        signal.SendLocalPairingEvent(Event{Type: EventTransferError, Error: err.Error(), Action: ActionSyncDevice})
        return err
    }

    resp, err := c.Do(req)
    if err != nil {
        signal.SendLocalPairingEvent(Event{Type: EventTransferError, Error: err.Error(), Action: ActionSyncDevice})
        return err
    }

    if resp.StatusCode != http.StatusOK {
        err = fmt.Errorf("[client] status not ok when receiving sync device data, received '%s'", resp.Status)
        signal.SendLocalPairingEvent(Event{Type: EventTransferError, Error: err.Error(), Action: ActionSyncDevice})
        return err
    }

    payload, err := io.ReadAll(resp.Body)
    if err != nil {
        signal.SendLocalPairingEvent(Event{Type: EventTransferError, Error: err.Error(), Action: ActionSyncDevice})
        return err
    }
    signal.SendLocalPairingEvent(Event{Type: EventTransferSuccess, Action: ActionSyncDevice})

    err = c.rawMessageReceiver.Receive(payload)
    if err != nil {
        signal.SendLocalPairingEvent(Event{Type: EventProcessError, Error: err.Error(), Action: ActionSyncDevice})
        return err
    }
    signal.SendLocalPairingEvent(Event{Type: EventProcessSuccess, Action: ActionSyncDevice})
    return nil
}

func (c *ReceiverClient) sendInstallationData() error {
    err := c.installationReceiver.Mount()
    if err != nil {
        return err
    }

    c.baseAddress.Path = pairingReceiveInstallation
    req, err := http.NewRequest(http.MethodPost, c.baseAddress.String(), bytes.NewBuffer(c.installationReceiver.ToSend()))
    if err != nil {
        return err
    }
    req.Header.Set("Content-Type", "application/octet-stream")

    err = c.challengeTaker.DoChallenge(req)
    if err != nil {
        signal.SendLocalPairingEvent(Event{Type: EventTransferError, Error: err.Error(), Action: ActionPairingInstallation})
        return err
    }

    resp, err := c.Do(req)
    if err != nil {
        signal.SendLocalPairingEvent(Event{Type: EventTransferError, Error: err.Error(), Action: ActionPairingInstallation})
        return err
    }

    if resp.StatusCode != http.StatusOK {
        err = fmt.Errorf("[client] status not okay when sending installation data, status: %s", resp.Status)
        signal.SendLocalPairingEvent(Event{Type: EventTransferError, Error: err.Error(), Action: ActionPairingInstallation})
        return err
    }

    signal.SendLocalPairingEvent(Event{Type: EventTransferSuccess, Action: ActionPairingInstallation})
    return nil
}

// setupReceivingClient creates a new ReceiverClient after parsing string inputs
func setupReceivingClient(backend *api.GethStatusBackend, cs, configJSON string) (*ReceiverClient, error) {
    ccp := new(ConnectionParams)
    err := ccp.FromString(cs)
    if err != nil {
        return nil, err
    }

    conf := NewReceiverClientConfig()
    err = json.Unmarshal([]byte(configJSON), conf)
    if err != nil {
        return nil, err
    }

    // This is a temporal solution to allow clients not to pass DeviceType.
    // Check DeviceType deprecation reason for more info.
    conf.ReceiverConfig.DeviceType = runtime.GOOS

    err = validateReceiverConfig(conf, conf.ReceiverConfig)
    if err != nil {
        return nil, err
    }

    // ignore err because we allow no active account here
    activeAccount, _ := backend.GetActiveAccount()
    if activeAccount != nil {
        conf.ReceiverConfig.LoggedInKeyUID = activeAccount.KeyUID
    }

    conf.ReceiverConfig.DB = backend.GetMultiaccountDB()

    return NewReceiverClient(backend, ccp, conf)
}

// StartUpReceivingClient creates a ReceiverClient and triggers all `receive` calls in sequence to the SenderServer
func StartUpReceivingClient(backend *api.GethStatusBackend, cs, configJSON string) error {
    c, err := setupReceivingClient(backend, cs, configJSON)
    if err != nil {
        return err
    }

    err = c.getChallenge()
    if err != nil {
        return err
    }
    err = c.receiveAccountData()
    if err != nil {
        return err
    }

    err = c.getChallenge()
    if err != nil {
        return err
    }
    err = c.receiveSyncDeviceData()
    if err != nil {
        return err
    }

    err = c.getChallenge()
    if err != nil {
        return err
    }
    return c.sendInstallationData()
}

/*
|--------------------------------------------------------------------------
| ReceiverClient
|--------------------------------------------------------------------------
*/

type KeystoreFilesReceiverClient struct {
    *BaseClient

    keystoreFilesReceiver PayloadReceiver
}

func NewKeystoreFilesReceiverClient(backend *api.GethStatusBackend, c *ConnectionParams, config *KeystoreFilesReceiverClientConfig) (*KeystoreFilesReceiverClient, error) {
    logger := logutils.ZapLogger().Named("ReceiverClient")
    bc, err := NewBaseClient(c, logger)
    if err != nil {
        return nil, err
    }
    pe := NewPayloadEncryptor(c.aesKey)

    kfrc, err := NewKeystoreFilesPayloadReceiver(backend, pe, config.ReceiverConfig, logger)
    if err != nil {
        return nil, err
    }

    return &KeystoreFilesReceiverClient{
        BaseClient:            bc,
        keystoreFilesReceiver: kfrc,
    }, nil
}

func (c *KeystoreFilesReceiverClient) receiveKeystoreFilesData() error {
    c.baseAddress.Path = pairingSendAccount
    req, err := http.NewRequest(http.MethodGet, c.baseAddress.String(), nil)
    if err != nil {
        return err
    }

    err = c.challengeTaker.DoChallenge(req)
    if err != nil {
        signal.SendLocalPairingEvent(Event{Type: EventTransferError, Error: err.Error(), Action: ActionKeystoreFilesTransfer})
        return err
    }

    resp, err := c.Do(req)
    if err != nil {
        signal.SendLocalPairingEvent(Event{Type: EventTransferError, Error: err.Error(), Action: ActionKeystoreFilesTransfer})
        return err
    }

    if resp.StatusCode != http.StatusOK {
        err = fmt.Errorf("[client] status not ok when receiving account data, received '%s'", resp.Status)
        signal.SendLocalPairingEvent(Event{Type: EventTransferError, Error: err.Error(), Action: ActionKeystoreFilesTransfer})
        return err
    }

    payload, err := io.ReadAll(resp.Body)
    if err != nil {
        signal.SendLocalPairingEvent(Event{Type: EventTransferError, Error: err.Error(), Action: ActionKeystoreFilesTransfer})
        return err
    }
    signal.SendLocalPairingEvent(Event{Type: EventTransferSuccess, Action: ActionKeystoreFilesTransfer})

    err = c.keystoreFilesReceiver.Receive(payload)
    if err != nil {
        signal.SendLocalPairingEvent(Event{Type: EventProcessError, Error: err.Error(), Action: ActionKeystoreFilesTransfer})
        return err
    }
    signal.SendLocalPairingEvent(Event{Type: EventProcessSuccess, Action: ActionKeystoreFilesTransfer})
    return nil
}

// setupKeystoreFilesReceivingClient creates a new ReceiverClient after parsing string inputs
func setupKeystoreFilesReceivingClient(backend *api.GethStatusBackend, cs, configJSON string) (*KeystoreFilesReceiverClient, error) {
    ccp := new(ConnectionParams)
    err := ccp.FromString(cs)
    if err != nil {
        return nil, err
    }

    conf := NewKeystoreFilesReceiverClientConfig()
    err = json.Unmarshal([]byte(configJSON), conf)
    if err != nil {
        return nil, err
    }
    err = validateKeystoreFilesConfig(backend, conf)
    if err != nil {
        return nil, err
    }

    return NewKeystoreFilesReceiverClient(backend, ccp, conf)
}

// StartUpKeystoreFilesReceivingClient creates a KeystoreFilesReceiverClient and triggers all `receive` calls in sequence to the KeystoreFilesSenderServer
func StartUpKeystoreFilesReceivingClient(backend *api.GethStatusBackend, cs, configJSON string) error {
    c, err := setupKeystoreFilesReceivingClient(backend, cs, configJSON)
    if err != nil {
        return err
    }

    err = c.getChallenge()
    if err != nil {
        return err
    }

    return c.receiveKeystoreFilesData()
}