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