1// Package edgegrid provides the Akamai OPEN Edgegrid Authentication scheme
2//
3// Deprecated: use edgegrid/config and edgegrid/signer instead
4package edgegrid
5
6import (
7	"bytes"
8	"crypto/hmac"
9	"crypto/sha256"
10	"encoding/base64"
11	"fmt"
12	"io/ioutil"
13	"net/http"
14	"os"
15	"sort"
16	"strconv"
17	"strings"
18	"time"
19	"unicode"
20
21	"github.com/google/uuid"
22	"github.com/mitchellh/go-homedir"
23	log "github.com/sirupsen/logrus"
24	"gopkg.in/ini.v1"
25)
26
27const defaultSection = "DEFAULT"
28
29// Config struct provides all the necessary fields to
30// create authorization header, debug is optional
31//
32// Deprecated: use github.com/akamai/AkamaiOPEN-edgegrid-golang/edgegrid
33type Config struct {
34	Host         string   `ini:"host"`
35	ClientToken  string   `ini:"client_token"`
36	ClientSecret string   `ini:"client_secret"`
37	AccessToken  string   `ini:"access_token"`
38	HeaderToSign []string `ini:"headers_to_sign"`
39	MaxBody      int      `ini:"max_body"`
40	Debug        bool     `ini:"debug"`
41}
42
43// Must be assigned the UTC time when the request is signed.
44// Format of “yyyyMMddTHH:mm:ss+0000”
45func makeEdgeTimeStamp() string {
46	local := time.FixedZone("GMT", 0)
47	t := time.Now().In(local)
48	return fmt.Sprintf("%d%02d%02dT%02d:%02d:%02d+0000",
49		t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second())
50}
51
52// Must be assigned a nonce (number used once) for the request.
53// It is a random string used to detect replayed request messages.
54// A GUID is recommended.
55func createNonce() string {
56	uuid, err := uuid.NewRandom()
57	if err != nil {
58		log.Errorf("Generate Uuid failed, %s", err)
59		return ""
60	}
61	return uuid.String()
62}
63
64func stringMinifier(in string) (out string) {
65	white := false
66	for _, c := range in {
67		if unicode.IsSpace(c) {
68			if !white {
69				out = out + " "
70			}
71			white = true
72		} else {
73			out = out + string(c)
74			white = false
75		}
76	}
77	return
78}
79
80func concatPathQuery(path, query string) string {
81	if query == "" {
82		return path
83	}
84	return fmt.Sprintf("%s?%s", path, query)
85}
86
87// createSignature is the base64-encoding of the SHA–256 HMAC of the data to sign with the signing key.
88func createSignature(message string, secret string) string {
89	key := []byte(secret)
90	h := hmac.New(sha256.New, key)
91	h.Write([]byte(message))
92	return base64.StdEncoding.EncodeToString(h.Sum(nil))
93}
94
95func createHash(data string) string {
96	h := sha256.Sum256([]byte(data))
97	return base64.StdEncoding.EncodeToString(h[:])
98}
99
100func (c *Config) canonicalizeHeaders(req *http.Request) string {
101	var unsortedHeader []string
102	var sortedHeader []string
103	for k := range req.Header {
104		unsortedHeader = append(unsortedHeader, k)
105	}
106	sort.Strings(unsortedHeader)
107	for _, k := range unsortedHeader {
108		for _, sign := range c.HeaderToSign {
109			if sign == k {
110				v := strings.TrimSpace(req.Header.Get(k))
111				sortedHeader = append(sortedHeader, fmt.Sprintf("%s:%s", strings.ToLower(k), strings.ToLower(stringMinifier(v))))
112			}
113		}
114	}
115	return strings.Join(sortedHeader, "\t")
116
117}
118
119// signingKey is derived from the client secret.
120// The signing key is computed as the base64 encoding of the SHA–256 HMAC of the timestamp string
121// (the field value included in the HTTP authorization header described above) with the client secret as the key.
122func (c *Config) signingKey(timestamp string) string {
123	key := createSignature(timestamp, c.ClientSecret)
124	return key
125}
126
127// The content hash is the base64-encoded SHA–256 hash of the POST body.
128// For any other request methods, this field is empty. But the tac separator (\t) must be included.
129// The size of the POST body must be less than or equal to the value specified by the service.
130// Any request that does not meet this criteria SHOULD be rejected during the signing process,
131// as the request will be rejected by EdgeGrid.
132func (c *Config) createContentHash(req *http.Request) string {
133	var (
134		contentHash  string
135		preparedBody string
136		bodyBytes    []byte
137	)
138	if req.Body != nil {
139		bodyBytes, _ = ioutil.ReadAll(req.Body)
140		req.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes))
141		preparedBody = string(bodyBytes)
142	}
143
144	log.Debugf("Body is %s", preparedBody)
145	if req.Method == "POST" && len(preparedBody) > 0 {
146		log.Debugf("Signing content: %s", preparedBody)
147		if len(preparedBody) > c.MaxBody {
148			log.Debugf("Data length %d is larger than maximum %d",
149				len(preparedBody), c.MaxBody)
150
151			preparedBody = preparedBody[0:c.MaxBody]
152			log.Debugf("Data truncated to %d for computing the hash", len(preparedBody))
153		}
154		contentHash = createHash(preparedBody)
155	}
156	log.Debugf("Content hash is '%s'", contentHash)
157	return contentHash
158}
159
160// The data to sign includes the information from the HTTP request that is relevant to ensuring that the request is authentic.
161// This data set comprised of the request data combined with the authorization header value (excluding the signature field,
162// but including the ; right before the signature field).
163func (c *Config) signingData(req *http.Request, authHeader string) string {
164
165	dataSign := []string{
166		req.Method,
167		req.URL.Scheme,
168		req.URL.Host,
169		concatPathQuery(req.URL.Path, req.URL.RawQuery),
170		c.canonicalizeHeaders(req),
171		c.createContentHash(req),
172		authHeader,
173	}
174	log.Debugf("Data to sign %s", strings.Join(dataSign, "\t"))
175	return strings.Join(dataSign, "\t")
176}
177
178func (c *Config) signingRequest(req *http.Request, authHeader string, timestamp string) string {
179	return createSignature(c.signingData(req, authHeader),
180		c.signingKey(timestamp))
181}
182
183// The Authorization header starts with the signing algorithm moniker (name of the algorithm) used to sign the request.
184// The moniker below identifies EdgeGrid V1, hash message authentication code, SHA–256 as the hash standard.
185// This moniker is then followed by a space and an ordered list of name value pairs with each field separated by a semicolon.
186func (c *Config) createAuthHeader(req *http.Request, timestamp string, nonce string) string {
187	authHeader := fmt.Sprintf("EG1-HMAC-SHA256 client_token=%s;access_token=%s;timestamp=%s;nonce=%s;",
188		c.ClientToken,
189		c.AccessToken,
190		timestamp,
191		nonce,
192	)
193	log.Debugf("Unsigned authorization header: '%s'", authHeader)
194
195	signedAuthHeader := fmt.Sprintf("%ssignature=%s", authHeader, c.signingRequest(req, authHeader, timestamp))
196
197	log.Debugf("Signed authorization header: '%s'", signedAuthHeader)
198	return signedAuthHeader
199}
200
201// AddRequestHeader sets the authorization header to use Akamai Open API
202//
203// Deprecated: use github.com/akamai/AkamaiOPEN-edgegrid-golang/edgegrid
204func AddRequestHeader(c Config, req *http.Request) *http.Request {
205	return c.AddRequestHeader(req)
206}
207
208// AddRequestHeader set the authorization header to use Akamai OPEN API
209//
210// Deprecated: use github.com/akamai/AkamaiOPEN-edgegrid-golang/edgegrid
211func (c Config) AddRequestHeader(req *http.Request) *http.Request {
212	if c.Debug {
213		log.SetLevel(log.DebugLevel)
214	}
215	timestamp := makeEdgeTimeStamp()
216	nonce := createNonce()
217
218	req.Header.Set("Content-Type", "application/json")
219	req.Header.Set("Authorization", c.createAuthHeader(req, timestamp, nonce))
220	return req
221}
222
223// InitEdgeRc initializes Config using an .edgerc (INI) configuration file
224//
225// Deprecated: use github.com/akamai/AkamaiOPEN-edgegrid-golang/edgegrid
226func InitEdgeRc(filepath string, section string) (Config, error) {
227	var (
228		c               Config
229		requiredOptions = []string{"host", "client_token", "client_secret", "access_token"}
230		missing         []string
231	)
232
233	// Check if filepath is empty
234	if filepath == "" {
235		filepath = "~/.edgerc"
236	}
237
238	// Check if section is empty
239	if section == "" {
240		section = "default"
241	}
242
243	path, err := homedir.Expand(filepath)
244	if err != nil {
245		return c, fmt.Errorf("Fatal could not find home dir from user: %s", err)
246	}
247
248	edgerc, err := ini.Load(path)
249	if err != nil {
250		return c, fmt.Errorf("Fatal error config file: %s", err)
251	}
252	err = edgerc.Section(section).MapTo(&c)
253	if err != nil {
254		return c, fmt.Errorf("Could not map section: %s", err)
255	}
256	for _, opt := range requiredOptions {
257		if !(edgerc.Section(section).HasKey(opt)) {
258			missing = append(missing, opt)
259		}
260	}
261	if len(missing) > 0 {
262		return c, fmt.Errorf("Fatal missing required options: %s", missing)
263	}
264	if c.MaxBody == 0 {
265		c.MaxBody = 131072
266	}
267	return c, nil
268}
269
270// InitEnv initializes Config using ENV variables
271//
272// Deprecated: use github.com/akamai/AkamaiOPEN-edgegrid-golang/edgegrid
273func InitEnv(section string) (Config, error) {
274	var (
275		c               Config
276		requiredOptions = []string{"HOST", "CLIENT_TOKEN", "CLIENT_SECRET", "ACCESS_TOKEN"}
277		missing         []string
278		prefix          string
279	)
280
281	// Check if section is empty
282	if section == "" {
283		section = defaultSection
284	} else {
285		section = strings.ToUpper(section)
286	}
287
288	prefix = "AKAMAI_"
289	_, ok := os.LookupEnv("AKAMAI_" + section + "_HOST")
290	if ok {
291		prefix = "AKAMAI_" + section + "_"
292	}
293
294	for _, opt := range requiredOptions {
295		val, ok := os.LookupEnv(prefix + opt)
296		if !ok {
297			missing = append(missing, prefix+opt)
298		} else {
299			switch {
300			case opt == "HOST":
301				c.Host = val
302			case opt == "CLIENT_TOKEN":
303				c.ClientToken = val
304			case opt == "CLIENT_SECRET":
305				c.ClientSecret = val
306			case opt == "ACCESS_TOKEN":
307				c.AccessToken = val
308			}
309		}
310	}
311
312	if len(missing) > 0 {
313		return c, fmt.Errorf("Fatal missing required environment variables: %s", missing)
314	}
315
316	c.MaxBody = 0
317
318	val, ok := os.LookupEnv(prefix + "MAX_BODY")
319	if i, err := strconv.Atoi(val); err == nil {
320		c.MaxBody = i
321	}
322
323	if !ok || c.MaxBody == 0 {
324		c.MaxBody = 131072
325	}
326
327	return c, nil
328}
329
330// InitConfig initializes Config using .edgerc files
331//
332// Deprecated: Backwards compatible wrapper around InitEdgeRc which should be used instead
333func InitConfig(filepath string, section string) Config {
334	c, err := InitEdgeRc(filepath, section)
335	if err != nil {
336		log.Panic(err.Error())
337	}
338
339	return c
340}
341
342// Init initializes Config using first ENV variables, with fallback to .edgerc file
343//
344// Deprecated: use github.com/akamai/AkamaiOPEN-edgegrid-golang/edgegrid
345func Init(filepath string, section string) (Config, error) {
346	if section == "" {
347		section = defaultSection
348	} else {
349		section = strings.ToUpper(section)
350	}
351
352	_, exists := os.LookupEnv("AKAMAI_" + section + "_HOST")
353	if !exists && section == defaultSection {
354		_, exists := os.LookupEnv("AKAMAI_HOST")
355
356		if exists {
357			return InitEnv("")
358		}
359	}
360
361	if exists {
362		return InitEnv(section)
363	}
364
365	c, err := InitEdgeRc(filepath, strings.ToLower(section))
366
367	if err == nil {
368		return c, nil
369	}
370
371	if section != defaultSection {
372		_, ok := os.LookupEnv("AKAMAI_HOST")
373		if ok {
374			return InitEnv("")
375		}
376	}
377
378	return c, fmt.Errorf("Unable to create instance using environment or .edgerc file")
379}
380