| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459 |
- /*
- Package signer providers functions for sign http request before request cloud.
- */
- package aksk
- import (
- "bytes"
- "crypto/hmac"
- "crypto/sha256"
- "encoding/hex"
- "encoding/json"
- "fmt"
- "io/ioutil"
- "log"
- "net/http"
- "net/textproto"
- "net/url"
- "regexp"
- "sort"
- "strings"
- "time"
- )
- //caseInsencitiveStringArray represents string case insensitive sorting operations
- type caseInsencitiveStringArray []string
- // noEscape specifies whether the character should be encoded or not
- var noEscape [256]bool
- func init() {
- // refer to https://docs.oracle.com/javase/7/docs/api/java/net/URLEncoder.html
- for i := 0; i < len(noEscape); i++ {
- noEscape[i] = (i >= 'A' && i <= 'Z') ||
- (i >= 'a' && i <= 'z') ||
- (i >= '0' && i <= '9') ||
- i == '.' ||
- i == '-' ||
- i == '_' ||
- i == '~' //java-sdk-core 3.0.1 HttpUtils.urlEncode
- }
- }
- /*
- SignOptions represents the options during signing http request, it is concurrency safely.
- Sample code:
- client := &http.Client{
- Timeout: time.Duration(3 * time.Second),
- }
- req, err := http.NewRequest("POST", "https://30030113-3657-4fb6-a7ef-90764239b038.apigw.xxx.yyy.com/app1?name=value",bytes.NewBuffer([]byte("demo")))
- signOptions := signer.SignOptions{
- AccessKey: "------------",
- SecretKey: "------------",
- }
- signer.Sign(req, signOptions)
- resp, err := client.Do(req)
- */
- type SignOptions struct {
- AccessKey string //Access Key
- SecretKey string //Secret key
- RegionName string // Region name
- ServiceName string // Service Name
- EnableCacheSignKey bool // Cache sign key for one day or not cache, cache is disabled by default
- encodeUrl bool //internal use
- SignAlgorithm string //The algorithm used for sign, the default value is "SDK-HMAC-SHA256" if you don't set its value
- TimeOffsetInseconds int64 // TimeOffsetInseconds is used for adjust x-sdk-date if set its value
- }
- // StringBuilder wraps bytes.Buffer to implement a high performance string builder.
- type StringBuilder struct {
- builder bytes.Buffer //string storage
- }
- // reqSignParams represents the option values used for signing http request.
- type reqSignParams struct {
- SignOptions
- RequestTime time.Time
- Req *http.Request
- }
- // signKeyCacheEntry represents the cache entry of sign key.
- type signKeyCacheEntry struct {
- Key []byte // sign key
- NumberOfDaysSinceEpoch int64 // number of days since epoch
- }
- // The default sign algorithm
- const SignAlgorithmHMACSHA256 = "SDK-HMAC-SHA256"
- // The header key of content hash value
- const ContentSha256HeaderKey = "x-sdk-content-sha256"
- //A regular for searching empty string
- var spaceRegexp = regexp.MustCompile(`\s+`)
- // cache sign key
- var cache = NewCache(300)
- //Sign manipulates the http.Request instance with some required authentication headers for SK/SK auth.
- func Sign(req *http.Request, signOptions SignOptions) {
- signOptions.AccessKey = strings.TrimSpace(signOptions.AccessKey)
- signOptions.SecretKey = strings.TrimSpace(signOptions.SecretKey)
- signOptions.encodeUrl = true
- signParams := reqSignParams{
- SignOptions: signOptions,
- RequestTime: time.Now(),
- Req: req,
- }
- //t, _ := time.Parse(time.RFC3339, "2018-04-15T04:28:22+00:00")
- //signParams.RequestTime = t
- if signParams.SignAlgorithm == "" {
- signParams.SignAlgorithm = SignAlgorithmHMACSHA256
- }
- addRequiredHeaders(req, signParams.getFormattedSigningDateTime())
- contentSha256 := ""
- if v, ok := req.Header[textproto.CanonicalMIMEHeaderKey(ContentSha256HeaderKey)]; !ok {
- contentSha256 = calculateContentHash(req)
- } else {
- contentSha256 = v[0]
- }
- canonicalRequest := createCanonicalRequest(signParams, contentSha256)
- /*fmt.Println("canonicalRequest: " + canonicalRequest)
- fmt.Println("*****")*/
- strToSign := createStringToSign(canonicalRequest, signParams)
- signKey := deriveSigningKey(signParams)
- signature := computeSignature(strToSign, signKey, signParams.SignAlgorithm)
- req.Header.Set("Authorization", buildAuthorizationHeader(signParams, signature))
- }
- // deriveSigningKey returns a sign key from cache, or build it and insert it into cache
- func deriveSigningKey(signParam reqSignParams) []byte {
- if signParam.EnableCacheSignKey {
- cacheKey := strings.Join([]string{signParam.SecretKey,
- signParam.RegionName,
- signParam.ServiceName,
- }, "-")
- cacheData := cache.Get(cacheKey)
- if cacheData != "" {
- var signKey signKeyCacheEntry
- json.Unmarshal([]byte(cacheData), &signKey)
- if signKey.NumberOfDaysSinceEpoch == signParam.getDaysSinceEpon() {
- return signKey.Key
- }
- }
- signKey := buildSignKey(signParam)
- signKeyStr, _ := json.Marshal(signKeyCacheEntry{
- Key: signKey,
- NumberOfDaysSinceEpoch: signParam.getDaysSinceEpon(),
- })
- cache.Add(cacheKey, string(signKeyStr))
- return signKey
- } else {
- return buildSignKey(signParam)
- }
- }
- func buildSignKey(signParam reqSignParams) []byte {
- var kSecret StringBuilder
- kSecret.Write("SDK").Write(signParam.SecretKey)
- kDate := computeSignature(signParam.getFormattedSigningDate(), kSecret.GetBytes(), signParam.SignAlgorithm)
- kRegion := computeSignature(signParam.RegionName, kDate, signParam.SignAlgorithm)
- kService := computeSignature(signParam.ServiceName, kRegion, signParam.SignAlgorithm)
- return computeSignature("sdk_request", kService, signParam.SignAlgorithm)
- }
- //HmacSha256 implements the Keyed-Hash Message Authentication Code computation.
- func HmacSha256(data string, key []byte) []byte {
- mac := hmac.New(sha256.New, key)
- mac.Write([]byte(data))
- return mac.Sum(nil)
- }
- // HashSha256 is a wrapper for sha256 implementation.
- func HashSha256(msg []byte) []byte {
- sh256 := sha256.New()
- sh256.Write(msg)
- return sh256.Sum(nil)
- }
- // buildAuthorizationHeader builds the authentication header value
- func buildAuthorizationHeader(signParam reqSignParams, signature []byte) string {
- var signingCredentials StringBuilder
- signingCredentials.Write(signParam.AccessKey).Write("/").Write(signParam.getScope())
- credential := "Credential=" + signingCredentials.ToString()
- signerHeaders := "SignedHeaders=" + getSignedHeadersString(signParam.Req)
- signatureHeader := "Signature=" + hex.EncodeToString(signature)
- return signParam.SignAlgorithm + " " + strings.Join([]string{
- credential,
- signerHeaders,
- signatureHeader,
- }, ", ")
- }
- // computeSignature computers the signature with the specified algorithm
- // and it only supports SDK-HMAC-SHA256 in currently
- func computeSignature(signData string, key []byte, algorithm string) []byte {
- if algorithm == SignAlgorithmHMACSHA256 {
- return HmacSha256(signData, key)
- } else {
- log.Fatalf("Unsupported algorithm %s, please use %s and try again", algorithm, SignAlgorithmHMACSHA256)
- return nil
- }
- }
- // createStringToSign build the need to be signed string
- func createStringToSign(canonicalRequest string, signParams reqSignParams) string {
- return strings.Join([]string{signParams.SignAlgorithm,
- signParams.getFormattedSigningDateTime(),
- signParams.getScope(),
- hex.EncodeToString(HashSha256([]byte(canonicalRequest))),
- }, "\n")
- }
- // getCanonicalizedResourcePath builds the valid url path for signing
- func getCanonicalizedResourcePath(signParas reqSignParams) string {
- urlStr := signParas.Req.URL.Path
- if !strings.HasPrefix(urlStr, "/") {
- urlStr = "/" + urlStr
- }
- if !strings.HasSuffix(urlStr, "/") {
- urlStr = urlStr + "/"
- }
- if signParas.encodeUrl {
- urlStr = urlEncode(urlStr, true)
- }
- if urlStr == "" {
- urlStr = "/"
- }
- return urlStr
- }
- // urlEncode encodes url path and url querystring according to the following rules:
- // The alphanumeric characters "a" through "z", "A" through "Z" and "0" through "9" remain the same.
- //The special characters ".", "-", "*", and "_" remain the same.
- //The space character " " is converted into a plus sign "%20".
- //All other characters are unsafe and are first converted into one or more bytes using some encoding scheme.
- func urlEncode(url string, urlPath bool) string {
- var buf bytes.Buffer
- for i := 0; i < len(url); i++ {
- c := url[i]
- if noEscape[c] || (c == '/' && urlPath) {
- buf.WriteByte(c)
- } else {
- fmt.Fprintf(&buf, "%%%02X", c)
- }
- }
- return buf.String()
- }
- // encodeQueryString build and encode querystring to a string for signing
- func encodeQueryString(queryValues url.Values) string {
- var encodedVals = make(map[string]string, len(queryValues))
- var keys = make([]string, len(queryValues))
- i := 0
- for k, _ := range queryValues {
- keys[i] = urlEncode(k, false)
- encodedVals[keys[i]] = k
- i++
- }
- caseInsensitiveSort(keys)
- var queryStr StringBuilder
- for i, k := range keys {
- if i > 0 {
- queryStr.Write("&")
- }
- queryStr.Write(k).Write("=").Write(urlEncode(queryValues.Get(encodedVals[k]), false))
- }
- return queryStr.ToString()
- }
- // getCanonicalizedQueryString return empty string if in POST method and content is nil, otherwise returns sorted,encoded querystring
- func getCanonicalizedQueryString(signParas reqSignParams) string {
- if usePayloadForQueryParameters(signParas.Req) {
- return ""
- } else {
- return encodeQueryString(signParas.Req.URL.Query())
- }
- }
- // createCanonicalRequest builds canonical string depends the official document for signing
- func createCanonicalRequest(signParas reqSignParams, contentSha256 string) string {
- return strings.Join([]string{signParas.Req.Method,
- getCanonicalizedResourcePath(signParas),
- getCanonicalizedQueryString(signParas),
- getCanonicalizedHeaderString(signParas.Req),
- getSignedHeadersString(signParas.Req),
- contentSha256,
- }, "\n")
- }
- // calculateContentHash computes the content hash value
- func calculateContentHash(req *http.Request) string {
- encodeParas := ""
- //post and content is null use queryString as content -- according to document
- if usePayloadForQueryParameters(req) {
- encodeParas = req.URL.Query().Encode()
- } else {
- if req.Body == nil {
- encodeParas = ""
- } else {
- readBody, _ := ioutil.ReadAll(req.Body)
- req.Body = ioutil.NopCloser(bytes.NewBuffer(readBody))
- encodeParas = string(readBody)
- }
- }
- return hex.EncodeToString(HashSha256([]byte(encodeParas)))
- }
- // usePayloadForQueryParameters specifies use querystring or not as content for compute content hash
- func usePayloadForQueryParameters(req *http.Request) bool {
- if strings.ToLower(req.Method) != "post" {
- return false
- }
- return req.Body == nil
- }
- // getCanonicalizedHeaderString converts header map to a string for signing
- func getCanonicalizedHeaderString(req *http.Request) string {
- var headers StringBuilder
- keys := make([]string, 0)
- for k, _ := range req.Header {
- keys = append(keys, strings.TrimSpace(k))
- }
- caseInsensitiveSort(keys)
- for _, k := range keys {
- k = strings.ToLower(k)
- newKey := spaceRegexp.ReplaceAllString(k, " ")
- headers.Write(newKey)
- headers.Write(":")
- val := req.Header.Get(k)
- val = spaceRegexp.ReplaceAllString(val, " ")
- headers.Write(val)
- headers.Write("\n")
- }
- return headers.ToString()
- }
- // getSignedHeadersString builds the string for AuthorizationHeader and signing
- func getSignedHeadersString(req *http.Request) string {
- var headers StringBuilder
- keys := make([]string, 0)
- for k, _ := range req.Header {
- keys = append(keys, strings.TrimSpace(k))
- }
- caseInsensitiveSort(keys)
- for idx, k := range keys {
- if idx > 0 {
- headers.Write(";")
- }
- headers.Write(strings.ToLower(k))
- }
- return headers.ToString()
- }
- // addRequiredHeaders adds the required heads to http.request instance
- func addRequiredHeaders(req *http.Request, timeStr string) {
- // golang handls port by default
- req.Header.Add("Host", req.URL.Host)
- req.Header.Add("X-Sdk-Date", timeStr)
- }
- func (s caseInsencitiveStringArray) Len() int {
- return len(s)
- }
- func (s caseInsencitiveStringArray) Swap(i, j int) {
- s[i], s[j] = s[j], s[i]
- }
- func (s caseInsencitiveStringArray) Less(i, j int) bool {
- return strings.ToLower(s[i]) < strings.ToLower(s[j])
- }
- func caseInsensitiveSort(strSlice []string) {
- sort.Sort(caseInsencitiveStringArray(strSlice))
- }
- func (signParas *reqSignParams) getSigningDateTimeMilli() int64 {
- return (signParas.RequestTime.UTC().Unix() - signParas.TimeOffsetInseconds) * 1000
- }
- func (signParas *reqSignParams) getSigningDateTime() time.Time {
- return time.Unix(signParas.getSigningDateTimeMilli()/1000, 0)
- }
- func (signParas *reqSignParams) getDaysSinceEpon() int64 {
- return signParas.getSigningDateTimeMilli() / 1000 / 3600 / 24
- }
- func (signParas *reqSignParams) getFormattedSigningDate() string {
- return signParas.getSigningDateTime().UTC().Format("20060102")
- }
- func (signParas *reqSignParams) getFormattedSigningDateTime() string {
- return signParas.getSigningDateTime().UTC().Format("20060102T150405Z")
- }
- func (signParas *reqSignParams) getScope() string {
- return strings.Join([]string{signParas.getFormattedSigningDate(),
- signParas.RegionName,
- signParas.ServiceName,
- "sdk_request",
- }, "/")
- }
- func (buff *StringBuilder) Write(s string) *StringBuilder {
- buff.builder.WriteString(s)
- return buff
- }
- func (buff *StringBuilder) ToString() string {
- return buff.builder.String()
- }
- func (buff *StringBuilder) GetBytes() []byte {
- return []byte(buff.ToString())
- }
|