signer.go raw

   1  package edgegrid
   2  
   3  import (
   4  	"bytes"
   5  	"crypto/sha256"
   6  	"encoding/base64"
   7  	"fmt"
   8  	"io"
   9  	"net/http"
  10  	"sort"
  11  	"strings"
  12  	"time"
  13  
  14  	"github.com/google/uuid"
  15  	"go.uber.org/ratelimit"
  16  )
  17  
  18  type (
  19  	// Signer is the request signer interface
  20  	Signer interface {
  21  		SignRequest(r *http.Request)
  22  		CheckRequestLimit(requestLimit int)
  23  	}
  24  
  25  	authHeader struct {
  26  		authType    string
  27  		clientToken string
  28  		accessToken string
  29  		timestamp   string
  30  		nonce       string
  31  		signature   string
  32  	}
  33  )
  34  
  35  const (
  36  	authType = "EG1-HMAC-SHA256"
  37  )
  38  
  39  var (
  40  	// rateLimit represents the maximum number of API requests per second the provider can make
  41  	requestLimit ratelimit.Limiter
  42  )
  43  
  44  // SignRequest adds a signed authorization header to the http request
  45  func (c Config) SignRequest(r *http.Request) {
  46  	if r.URL.Host == "" {
  47  		r.URL.Host = c.Host
  48  	}
  49  	if r.URL.Scheme == "" {
  50  		r.URL.Scheme = "https"
  51  	}
  52  	r.URL.RawQuery = c.addAccountSwitchKey(r)
  53  	r.Header.Set("Authorization", c.createAuthHeader(r).String())
  54  }
  55  
  56  // CheckRequestLimit waits if necessary to ensure that OpenAPI's request limit is not exceeded
  57  func (c Config) CheckRequestLimit(limit int) {
  58  	if limit > 0 {
  59  		if requestLimit == nil {
  60  			requestLimit = ratelimit.New(limit)
  61  		}
  62  		requestLimit.Take()
  63  	}
  64  }
  65  
  66  func (c Config) createAuthHeader(r *http.Request) authHeader {
  67  	timestamp := Timestamp(time.Now())
  68  
  69  	auth := authHeader{
  70  		authType:    authType,
  71  		clientToken: c.ClientToken,
  72  		accessToken: c.AccessToken,
  73  		timestamp:   timestamp,
  74  		nonce:       uuid.New().String(),
  75  	}
  76  
  77  	msgPath := r.URL.EscapedPath()
  78  	if r.URL.RawQuery != "" {
  79  		msgPath = fmt.Sprintf("%s?%s", msgPath, r.URL.RawQuery)
  80  	}
  81  
  82  	// create the message to be signed
  83  	msgData := []string{
  84  		r.Method,
  85  		r.URL.Scheme,
  86  		r.URL.Host,
  87  		msgPath,
  88  		canonicalizeHeaders(r.Header, c.HeaderToSign),
  89  		createContentHash(r, c.MaxBody),
  90  		auth.String(),
  91  	}
  92  	msg := strings.Join(msgData, "\t")
  93  
  94  	key := createSignature(timestamp, c.ClientSecret)
  95  	auth.signature = createSignature(msg, key)
  96  	return auth
  97  }
  98  
  99  func canonicalizeHeaders(requestHeaders http.Header, headersToSign []string) string {
 100  	var unsortedHeader []string
 101  	var sortedHeader []string
 102  	for k := range requestHeaders {
 103  		unsortedHeader = append(unsortedHeader, k)
 104  	}
 105  	sort.Strings(unsortedHeader)
 106  	for _, k := range unsortedHeader {
 107  		for _, sign := range headersToSign {
 108  			if sign == k {
 109  				v := strings.TrimSpace(requestHeaders.Get(k))
 110  				sortedHeader = append(sortedHeader, fmt.Sprintf("%s:%s", strings.ToLower(k), strings.ToLower(stringMinifier(v))))
 111  			}
 112  		}
 113  	}
 114  	return strings.Join(sortedHeader, "\t")
 115  }
 116  
 117  // The content hash is the base64-encoded SHA–256 hash of the POST body.
 118  // For any other request methods, this field is empty. But the tab separator (\t) must be included.
 119  // The size of the POST body must be less than or equal to the value specified by the service.
 120  // Any request that does not meet this criteria SHOULD be rejected during the signing process,
 121  // as the request will be rejected by EdgeGrid.
 122  func createContentHash(r *http.Request, maxBody int) string {
 123  	var (
 124  		contentHash  string
 125  		preparedBody string
 126  		bodyBytes    []byte
 127  	)
 128  
 129  	if r.Body != nil {
 130  		bodyBytes, _ = io.ReadAll(r.Body)
 131  		r.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
 132  		preparedBody = string(bodyBytes)
 133  	}
 134  
 135  	if r.Method == http.MethodPost && len(preparedBody) > 0 {
 136  		if len(preparedBody) > maxBody {
 137  			preparedBody = preparedBody[0:maxBody]
 138  		}
 139  
 140  		sum := sha256.Sum256([]byte(preparedBody))
 141  
 142  		contentHash = base64.StdEncoding.EncodeToString(sum[:])
 143  	}
 144  
 145  	return contentHash
 146  }
 147  
 148  func (a authHeader) String() string {
 149  	auth := fmt.Sprintf("%s client_token=%s;access_token=%s;timestamp=%s;nonce=%s;",
 150  		a.authType,
 151  		a.clientToken,
 152  		a.accessToken,
 153  		a.timestamp,
 154  		a.nonce)
 155  	if a.signature != "" {
 156  		auth += fmt.Sprintf("signature=%s", a.signature)
 157  	}
 158  	return auth
 159  }
 160  
 161  func (c Config) addAccountSwitchKey(r *http.Request) string {
 162  	if c.AccountKey != "" {
 163  		values := r.URL.Query()
 164  		values.Add("accountSwitchKey", c.AccountKey)
 165  		r.URL.RawQuery = values.Encode()
 166  	}
 167  	return r.URL.RawQuery
 168  }
 169