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