mirror of
https://github.com/d0zingcat/gocryptotrader.git
synced 2026-05-13 15:09:42 +00:00
* 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>
426 lines
14 KiB
Go
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
|
|
}
|