github.com/akamai/AkamaiOPEN-edgegrid-golang/v8@v8.1.0/pkg/edgegrid/signer.go (about)

     1  package edgegrid
     2  
     3  import (
     4  	"bytes"
     5  	"crypto/sha256"
     6  	"encoding/base64"
     7  	"fmt"
     8  	"io/ioutil"
     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, _ = ioutil.ReadAll(r.Body)
   131  		r.Body = ioutil.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  }