Files
gocryptotrader/exchanges/credentials.go
Ryan O'Hara-Reid 14cde7b786 exchange: expose GetCredentials() and split GetAuthenticatedAPISupport() (#954)
* exchange/wrapper: expose GetCredentials func to IBotInterface

* exchanges: split up GetAuthenticatedAPISupport into specific function calls, organize IBotExchange functionality getter functions

* interface: change name - RPCSercer: rm GetBase func call.

* glorious: nits (fix panic)

Co-authored-by: Ryan O'Hara-Reid <ryan.oharareid@thrasher.io>
2022-05-18 10:52:16 +10:00

426 lines
14 KiB
Go

package exchange
import (
"context"
"errors"
"fmt"
"strings"
"sync"
"github.com/thrasher-corp/gocryptotrader/common/crypto"
"github.com/thrasher-corp/gocryptotrader/config"
"github.com/thrasher-corp/gocryptotrader/log"
"google.golang.org/grpc/metadata"
)
// contextCredential is a string flag for use with context values when setting
// credentials internally or via gRPC.
type contextCredential string
const (
contextCredentialsFlag contextCredential = "apicredentials"
contextSubAccountFlag contextCredential = "subaccountoverride"
key = "key"
secret = "secret"
subAccount = "subaccount"
clientID = "clientid"
oneTimePassword = "otp"
_PEMKey = "pemkey"
)
var (
// ErrAuthenticationSupportNotEnabled defines an error when
// authenticatedSupport and authenticatedWebsocketApiSupport are set to
// false in config.json
ErrAuthenticationSupportNotEnabled = errors.New("REST or Websocket authentication support is not enabled")
// ErrCredentialsAreEmpty defines an error for when the credentials are
// completely empty but an attempt at retrieving credentials was made to
// undertake an authenticated HTTP request.
ErrCredentialsAreEmpty = errors.New("credentials are empty")
errRequiresAPIKey = errors.New("requires API key but default/empty one set")
errRequiresAPISecret = errors.New("requires API secret but default/empty one set")
errRequiresAPIPEMKey = errors.New("requires API PEM key but default/empty one set")
errRequiresAPIClientID = errors.New("requires API Client ID but default/empty one set")
errBase64DecodeFailure = errors.New("base64 decode has failed")
errMissingInfo = errors.New("cannot parse meta data missing information in key value pair")
errInvalidCredentialMetaDataLength = errors.New("invalid meta data to process credentials")
errContextCredentialsFailure = errors.New("context credentials type assertion failure")
errMetaDataIsNil = errors.New("meta data is nil")
)
// ParseCredentialsMetadata intercepts and converts credentials metadata to a
// static type for authentication processing and protection.
func ParseCredentialsMetadata(ctx context.Context, md metadata.MD) (context.Context, error) {
if md == nil {
return ctx, errMetaDataIsNil
}
credMD, ok := md[string(contextCredentialsFlag)]
if !ok || len(credMD) == 0 {
return ctx, nil
}
if len(credMD) != 1 {
return ctx, errInvalidCredentialMetaDataLength
}
segregatedCreds := strings.Split(credMD[0], ",")
var ctxCreds Credentials
var subAccountHere string
for x := range segregatedCreds {
keyVals := strings.Split(segregatedCreds[x], ":")
if len(keyVals) != 2 {
return ctx, fmt.Errorf("%w received %v fields, expected 2 contains: %s",
errMissingInfo,
len(keyVals),
keyVals)
}
switch keyVals[0] {
case key:
ctxCreds.Key = keyVals[1]
case secret:
ctxCreds.Secret = keyVals[1]
case subAccount:
// Capture sub account as this can override if other values are
// not included in metadata.
subAccountHere = keyVals[1]
case clientID:
ctxCreds.ClientID = keyVals[1]
case _PEMKey:
ctxCreds.PEMKey = keyVals[1]
case oneTimePassword:
ctxCreds.OneTimePassword = keyVals[1]
}
}
if ctxCreds.IsEmpty() && subAccountHere != "" {
// This will override default sub account details if needed.
return deploySubAccountOverrideToContext(ctx, subAccountHere), nil
}
// merge sub account to main context credentials
ctxCreds.SubAccount = subAccountHere
return DeployCredentialsToContext(ctx, &ctxCreds), nil
}
// Credentials define parameters that allow for an authenticated request.
type Credentials struct {
Key string
Secret string
ClientID string
PEMKey string
SubAccount string
OneTimePassword string
}
// DeployCredentialsToContext sets credentials for internal use to context which
// can override default credential values.
func DeployCredentialsToContext(ctx context.Context, creds *Credentials) context.Context {
flag, store := creds.getInternal()
return context.WithValue(ctx, flag, store)
}
// deploySubAccountOverrideToContext sets subaccount as override to credentials
// as a separate flag.
func deploySubAccountOverrideToContext(ctx context.Context, subAccount string) context.Context {
return context.WithValue(ctx, contextSubAccountFlag, subAccount)
}
// GetMetaData returns the credentials for metadata context deployment
func (c *Credentials) GetMetaData() (flag, values string) {
vals := make([]string, 0, 6)
if c.Key != "" {
vals = append(vals, key+":"+c.Key)
}
if c.Secret != "" {
vals = append(vals, secret+":"+c.Secret)
}
if c.SubAccount != "" {
vals = append(vals, subAccount+":"+c.SubAccount)
}
if c.ClientID != "" {
vals = append(vals, clientID+":"+c.ClientID)
}
if c.PEMKey != "" {
vals = append(vals, _PEMKey+":"+c.PEMKey)
}
if c.OneTimePassword != "" {
vals = append(vals, oneTimePassword+":"+c.OneTimePassword)
}
return string(contextCredentialsFlag), strings.Join(vals, ",")
}
// IsEmpty return true if the underlying credentials type has not been filled
// with at least one item.
func (c *Credentials) IsEmpty() bool {
return c == nil || c.ClientID == "" &&
c.Key == "" &&
c.OneTimePassword == "" &&
c.PEMKey == "" &&
c.Secret == "" &&
c.SubAccount == ""
}
// contextCredentialsStore protects the stored credentials for use in a context
type contextCredentialsStore struct {
creds *Credentials
mu sync.RWMutex
}
// getInternal returns the values for assignment to an internal context
func (c *Credentials) getInternal() (contextCredential, *contextCredentialsStore) {
if c.IsEmpty() {
return "", nil
}
store := &contextCredentialsStore{}
store.Load(c)
return contextCredentialsFlag, store
}
// Load stores provided credentials
func (c *contextCredentialsStore) Load(creds *Credentials) {
// Segregate from external call
cpy := *creds
c.mu.Lock()
c.creds = &cpy
c.mu.Unlock()
}
// Get returns the full credentials from the store
func (c *contextCredentialsStore) Get() *Credentials {
c.mu.RLock()
creds := *c.creds
c.mu.RUnlock()
return &creds
}
// SetKey sets new key for the default credentials
func (a *API) SetKey(key string) {
a.credMu.Lock()
defer a.credMu.Unlock()
if a.credentials == nil {
a.credentials = &Credentials{}
}
a.credentials.Key = key
}
// SetSecret sets new secret for the default credentials
func (a *API) SetSecret(secret string) {
a.credMu.Lock()
defer a.credMu.Unlock()
if a.credentials == nil {
a.credentials = &Credentials{}
}
a.credentials.Secret = secret
}
// SetClientID sets new clientID for the default credentials
func (a *API) SetClientID(clientID string) {
a.credMu.Lock()
defer a.credMu.Unlock()
if a.credentials == nil {
a.credentials = &Credentials{}
}
a.credentials.ClientID = clientID
}
// SetPEMKey sets pem key for the default credentials
func (a *API) SetPEMKey(pem string) {
a.credMu.Lock()
defer a.credMu.Unlock()
if a.credentials == nil {
a.credentials = &Credentials{}
}
a.credentials.PEMKey = pem
}
// SetSubAccount sets sub account for the default credentials
func (a *API) SetSubAccount(sub string) {
a.credMu.Lock()
defer a.credMu.Unlock()
if a.credentials == nil {
a.credentials = &Credentials{}
}
a.credentials.SubAccount = sub
}
// CheckCredentials checks to see if the required fields have been set before
// sending an authenticated API request
func (b *Base) CheckCredentials(creds *Credentials, isContext bool) error {
if b.SkipAuthCheck {
return nil
}
// Individual package usage, allow request if API credentials are valid a
// and without needing to set AuthenticatedSupport to true
if !b.LoadedByConfig {
return b.ValidateAPICredentials(creds)
}
// Bot usage, AuthenticatedSupport can be disabled by user if desired, so
// don't allow authenticated requests. Context credentials set will override
// default credentials and supported checks.
if !b.API.AuthenticatedSupport && !b.API.AuthenticatedWebsocketSupport && !isContext {
return fmt.Errorf("%s %w", b.Name, ErrAuthenticationSupportNotEnabled)
}
// Check to see if the user has enabled AuthenticatedSupport, but has
// invalid API credentials set and loaded by config
return b.ValidateAPICredentials(creds)
}
// AreCredentialsValid returns if the supplied credentials are valid.
func (b *Base) AreCredentialsValid(ctx context.Context) bool {
creds, err := b.GetCredentials(ctx)
return err == nil && b.ValidateAPICredentials(creds) == nil
}
// GetDefaultCredentials returns the exchange.Base api credentials loaded by
// config.json
func (b *Base) GetDefaultCredentials() *Credentials {
b.API.credMu.RLock()
defer b.API.credMu.RUnlock()
if b.API.credentials == nil {
return nil
}
creds := *b.API.credentials
return &creds
}
// GetCredentials checks and validates current credentials, context credentials
// override default credentials, if no credentials found, will return an error.
func (b *Base) GetCredentials(ctx context.Context) (*Credentials, error) {
value := ctx.Value(contextCredentialsFlag)
if value != nil {
ctxCredStore, ok := value.(*contextCredentialsStore)
if !ok {
return &Credentials{}, errContextCredentialsFailure
}
creds := ctxCredStore.Get()
if err := b.CheckCredentials(creds, true); err != nil {
return creds, fmt.Errorf("context credentials issue: %w", err)
}
return creds, nil
}
err := b.CheckCredentials(b.API.credentials, false)
if err != nil {
return &Credentials{}, err
}
subAccountOverride, ok := ctx.Value(contextSubAccountFlag).(string)
b.API.credMu.RLock()
defer b.API.credMu.RUnlock()
creds := *b.API.credentials
if ok {
creds.SubAccount = subAccountOverride
}
return &creds, nil
}
// ValidateAPICredentials validates the exchanges API credentials
func (b *Base) ValidateAPICredentials(creds *Credentials) error {
b.API.credMu.RLock()
defer b.API.credMu.RUnlock()
if creds.IsEmpty() {
return fmt.Errorf("%s %w", b.Name, ErrCredentialsAreEmpty)
}
if b.API.CredentialsValidator.RequiresKey &&
(creds.Key == "" || creds.Key == config.DefaultAPIKey) {
return fmt.Errorf("%s %w", b.Name, errRequiresAPIKey)
}
if b.API.CredentialsValidator.RequiresSecret &&
(creds.Secret == "" || creds.Secret == config.DefaultAPISecret) {
return fmt.Errorf("%s %w", b.Name, errRequiresAPISecret)
}
if b.API.CredentialsValidator.RequiresPEM &&
(creds.PEMKey == "" || strings.Contains(creds.PEMKey, "JUSTADUMMY")) {
return fmt.Errorf("%s %w", b.Name, errRequiresAPIPEMKey)
}
if b.API.CredentialsValidator.RequiresClientID &&
(creds.ClientID == "" || creds.ClientID == config.DefaultAPIClientID) {
return fmt.Errorf("%s %w", b.Name, errRequiresAPIClientID)
}
if b.API.CredentialsValidator.RequiresBase64DecodeSecret && !b.LoadedByConfig {
_, err := crypto.Base64Decode(creds.Secret)
if err != nil {
return fmt.Errorf("%s API secret %w: %s", b.Name, errBase64DecodeFailure, err)
}
}
return nil
}
// SetCredentials is a method that sets the current API keys for the exchange
func (b *Base) SetCredentials(apiKey, apiSecret, clientID, subaccount, pemKey, oneTimePassword string) {
b.API.credMu.Lock()
defer b.API.credMu.Unlock()
if b.API.credentials == nil {
b.API.credentials = &Credentials{}
}
b.API.credentials.Key = apiKey
b.API.credentials.ClientID = clientID
b.API.credentials.SubAccount = subaccount
b.API.credentials.PEMKey = pemKey
b.API.credentials.OneTimePassword = oneTimePassword
if b.API.CredentialsValidator.RequiresBase64DecodeSecret {
result, err := crypto.Base64Decode(apiSecret)
if err != nil {
b.API.AuthenticatedSupport = false
b.API.AuthenticatedWebsocketSupport = false
log.Warnf(log.ExchangeSys,
warningBase64DecryptSecretKeyFailed,
b.Name)
return
}
b.API.credentials.Secret = string(result)
} else {
b.API.credentials.Secret = apiSecret
}
}
// SetAPICredentialDefaults sets the API Credential validator defaults
func (b *Base) SetAPICredentialDefaults() {
b.API.credMu.Lock()
defer b.API.credMu.Unlock()
// Exchange hardcoded settings take precedence and overwrite the config settings
if b.Config.API.CredentialsValidator == nil {
b.Config.API.CredentialsValidator = new(config.APICredentialsValidatorConfig)
}
if b.Config.API.CredentialsValidator.RequiresKey != b.API.CredentialsValidator.RequiresKey {
b.Config.API.CredentialsValidator.RequiresKey = b.API.CredentialsValidator.RequiresKey
}
if b.Config.API.CredentialsValidator.RequiresSecret != b.API.CredentialsValidator.RequiresSecret {
b.Config.API.CredentialsValidator.RequiresSecret = b.API.CredentialsValidator.RequiresSecret
}
if b.Config.API.CredentialsValidator.RequiresBase64DecodeSecret != b.API.CredentialsValidator.RequiresBase64DecodeSecret {
b.Config.API.CredentialsValidator.RequiresBase64DecodeSecret = b.API.CredentialsValidator.RequiresBase64DecodeSecret
}
if b.Config.API.CredentialsValidator.RequiresClientID != b.API.CredentialsValidator.RequiresClientID {
b.Config.API.CredentialsValidator.RequiresClientID = b.API.CredentialsValidator.RequiresClientID
}
if b.Config.API.CredentialsValidator.RequiresPEM != b.API.CredentialsValidator.RequiresPEM {
b.Config.API.CredentialsValidator.RequiresPEM = b.API.CredentialsValidator.RequiresPEM
}
}
// IsWebsocketAuthenticationSupported returns whether the exchange supports
// websocket authenticated API requests
func (b *Base) IsWebsocketAuthenticationSupported() bool {
return b.API.AuthenticatedWebsocketSupport
}
// IsRESTAuthenticationSupported returns whether the exchange supports REST authenticated
// API requests
func (b *Base) IsRESTAuthenticationSupported() bool {
return b.API.AuthenticatedSupport
}