package edgegrid import ( "bytes" "crypto/sha256" "encoding/base64" "fmt" "io" "net/http" "sort" "strings" "time" "github.com/google/uuid" "go.uber.org/ratelimit" ) type ( // Signer is the request signer interface Signer interface { SignRequest(r *http.Request) CheckRequestLimit(requestLimit int) } authHeader struct { authType string clientToken string accessToken string timestamp string nonce string signature string } ) const ( authType = "EG1-HMAC-SHA256" ) var ( // rateLimit represents the maximum number of API requests per second the provider can make requestLimit ratelimit.Limiter ) // SignRequest adds a signed authorization header to the http request func (c Config) SignRequest(r *http.Request) { if r.URL.Host == "" { r.URL.Host = c.Host } if r.URL.Scheme == "" { r.URL.Scheme = "https" } r.URL.RawQuery = c.addAccountSwitchKey(r) r.Header.Set("Authorization", c.createAuthHeader(r).String()) } // CheckRequestLimit waits if necessary to ensure that OpenAPI's request limit is not exceeded func (c Config) CheckRequestLimit(limit int) { if limit > 0 { if requestLimit == nil { requestLimit = ratelimit.New(limit) } requestLimit.Take() } } func (c Config) createAuthHeader(r *http.Request) authHeader { timestamp := Timestamp(time.Now()) auth := authHeader{ authType: authType, clientToken: c.ClientToken, accessToken: c.AccessToken, timestamp: timestamp, nonce: uuid.New().String(), } msgPath := r.URL.EscapedPath() if r.URL.RawQuery != "" { msgPath = fmt.Sprintf("%s?%s", msgPath, r.URL.RawQuery) } // create the message to be signed msgData := []string{ r.Method, r.URL.Scheme, r.URL.Host, msgPath, canonicalizeHeaders(r.Header, c.HeaderToSign), createContentHash(r, c.MaxBody), auth.String(), } msg := strings.Join(msgData, "\t") key := createSignature(timestamp, c.ClientSecret) auth.signature = createSignature(msg, key) return auth } func canonicalizeHeaders(requestHeaders http.Header, headersToSign []string) string { var unsortedHeader []string var sortedHeader []string for k := range requestHeaders { unsortedHeader = append(unsortedHeader, k) } sort.Strings(unsortedHeader) for _, k := range unsortedHeader { for _, sign := range headersToSign { if sign == k { v := strings.TrimSpace(requestHeaders.Get(k)) sortedHeader = append(sortedHeader, fmt.Sprintf("%s:%s", strings.ToLower(k), strings.ToLower(stringMinifier(v)))) } } } return strings.Join(sortedHeader, "\t") } // The content hash is the base64-encoded SHA–256 hash of the POST body. // For any other request methods, this field is empty. But the tab separator (\t) must be included. // The size of the POST body must be less than or equal to the value specified by the service. // Any request that does not meet this criteria SHOULD be rejected during the signing process, // as the request will be rejected by EdgeGrid. func createContentHash(r *http.Request, maxBody int) string { var ( contentHash string preparedBody string bodyBytes []byte ) if r.Body != nil { bodyBytes, _ = io.ReadAll(r.Body) r.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) preparedBody = string(bodyBytes) } if r.Method == http.MethodPost && len(preparedBody) > 0 { if len(preparedBody) > maxBody { preparedBody = preparedBody[0:maxBody] } sum := sha256.Sum256([]byte(preparedBody)) contentHash = base64.StdEncoding.EncodeToString(sum[:]) } return contentHash } func (a authHeader) String() string { auth := fmt.Sprintf("%s client_token=%s;access_token=%s;timestamp=%s;nonce=%s;", a.authType, a.clientToken, a.accessToken, a.timestamp, a.nonce) if a.signature != "" { auth += fmt.Sprintf("signature=%s", a.signature) } return auth } func (c Config) addAccountSwitchKey(r *http.Request) string { if c.AccountKey != "" { values := r.URL.Query() values.Add("accountSwitchKey", c.AccountKey) r.URL.RawQuery = values.Encode() } return r.URL.RawQuery }