github.com/huaweicloud/golangsdk@v0.0.0-20210831081626-d823fe11ceba/signer_helper.go (about)

     1  package golangsdk
     2  
     3  import (
     4  	"bytes"
     5  	"crypto/hmac"
     6  	"crypto/sha256"
     7  	"encoding/hex"
     8  	"encoding/json"
     9  	"fmt"
    10  	"io/ioutil"
    11  	"log"
    12  	"net/http"
    13  	"net/textproto"
    14  	"net/url"
    15  	"regexp"
    16  	"sort"
    17  	"strings"
    18  	"sync"
    19  	"time"
    20  )
    21  
    22  // MemoryCache presents a thread safe memory cache
    23  type MemoryCache struct {
    24  	sync.Mutex                    // handling r/w for cache
    25  	cacheHolder map[string]string // cache holder
    26  	cacheKeys   []string          // cache keys
    27  	MaxCount    int               // max cache entry count
    28  }
    29  
    30  // NewCache inits an new MemoryCache
    31  func NewCache(maxCount int) *MemoryCache {
    32  	return &MemoryCache{
    33  		cacheHolder: make(map[string]string, maxCount),
    34  		MaxCount:    maxCount,
    35  	}
    36  }
    37  
    38  // Add an new cache item
    39  func (cache *MemoryCache) Add(cacheKey string, cacheData string) {
    40  	cache.Lock()
    41  	defer cache.Unlock()
    42  
    43  	if len(cache.cacheKeys) >= cache.MaxCount && len(cache.cacheKeys) > 1 {
    44  		delete(cache.cacheHolder, cache.cacheKeys[0]) // delete first item
    45  		cache.cacheKeys = append(cache.cacheKeys[1:]) // pop first one
    46  	}
    47  
    48  	cache.cacheHolder[cacheKey] = cacheData
    49  	cache.cacheKeys = append(cache.cacheKeys, cacheKey)
    50  }
    51  
    52  // Get a cache item by its key
    53  func (cache *MemoryCache) Get(cacheKey string) string {
    54  	cache.Lock()
    55  	defer cache.Unlock()
    56  
    57  	return cache.cacheHolder[cacheKey]
    58  }
    59  
    60  //caseInsencitiveStringArray represents string case insensitive sorting operations
    61  type caseInsencitiveStringArray []string
    62  
    63  // noEscape specifies whether the character should be encoded or not
    64  var noEscape [256]bool
    65  
    66  func init() {
    67  	// refer to https://docs.oracle.com/javase/7/docs/api/java/net/URLEncoder.html
    68  	for i := 0; i < len(noEscape); i++ {
    69  		noEscape[i] = (i >= 'A' && i <= 'Z') ||
    70  			(i >= 'a' && i <= 'z') ||
    71  			(i >= '0' && i <= '9') ||
    72  			i == '.' ||
    73  			i == '-' ||
    74  			i == '_' ||
    75  			i == '~' //java-sdk-core 3.0.1 HttpUtils.urlEncode
    76  	}
    77  }
    78  
    79  // SignOptions represents the options during signing http request, it is concurency safely
    80  type SignOptions struct {
    81  	AccessKey           string //Access Key
    82  	SecretKey           string //Secret key
    83  	RegionName          string // Region name
    84  	ServiceName         string // Service Name
    85  	EnableCacheSignKey  bool   // Cache sign key for one day or not cache, cache is disabled by default
    86  	encodeUrl           bool   //internal use
    87  	SignAlgorithm       string //The algorithm used for sign, the default value is "SDK-HMAC-SHA256" if you don't set its value
    88  	TimeOffsetInseconds int64  // TimeOffsetInseconds is used for adjust x-sdk-date if set its value
    89  }
    90  
    91  // StringBuilder wraps bytes.Buffer to implement a high performance string builder
    92  type StringBuilder struct {
    93  	builder bytes.Buffer //string storage
    94  }
    95  
    96  // reqSignParams represents the option values used for signing http request
    97  type reqSignParams struct {
    98  	SignOptions
    99  	RequestTime time.Time
   100  	Req         *http.Request
   101  }
   102  
   103  // signKeyCacheEntry represents the cache entry of sign key
   104  type signKeyCacheEntry struct {
   105  	Key                    []byte // sign key
   106  	NumberOfDaysSinceEpoch int64  // number of days since epoch
   107  }
   108  
   109  // The default sign algorithm
   110  const SignAlgorithmHMACSHA256 = "SDK-HMAC-SHA256"
   111  
   112  // The header key of content hash value
   113  const ContentSha256HeaderKey = "x-sdk-content-sha256"
   114  
   115  //A regular for searching empty string
   116  var spaceRegexp = regexp.MustCompile(`\s+`)
   117  
   118  // cache sign key
   119  var cache = NewCache(300)
   120  
   121  //Sign manipulates the http.Request instance with some required authentication headers for SK/SK auth
   122  func Sign(req *http.Request, signOptions SignOptions) {
   123  	signOptions.AccessKey = strings.TrimSpace(signOptions.AccessKey)
   124  	signOptions.SecretKey = strings.TrimSpace(signOptions.SecretKey)
   125  	signOptions.encodeUrl = true
   126  
   127  	signParams := reqSignParams{
   128  		SignOptions: signOptions,
   129  		RequestTime: time.Now(),
   130  		Req:         req,
   131  	}
   132  
   133  	//t, _ := time.Parse(time.RFC3339, "2018-04-15T04:28:22+00:00")
   134  	//signParams.RequestTime = t
   135  
   136  	if signParams.SignAlgorithm == "" {
   137  		signParams.SignAlgorithm = SignAlgorithmHMACSHA256
   138  	}
   139  
   140  	addRequiredHeaders(req, signParams.getFormattedSigningDateTime())
   141  	contentSha256 := ""
   142  
   143  	if v, ok := req.Header[textproto.CanonicalMIMEHeaderKey(ContentSha256HeaderKey)]; !ok {
   144  		contentSha256 = calculateContentHash(req)
   145  	} else {
   146  		contentSha256 = v[0]
   147  	}
   148  
   149  	canonicalRequest := createCanonicalRequest(signParams, contentSha256)
   150  
   151  	/*fmt.Println("canonicalRequest: " + canonicalRequest)
   152  	fmt.Println("*****")*/
   153  
   154  	strToSign := createStringToSign(canonicalRequest, signParams)
   155  	signKey := deriveSigningKey(signParams)
   156  	signature := computeSignature(strToSign, signKey, signParams.SignAlgorithm)
   157  
   158  	req.Header.Set("Authorization", buildAuthorizationHeader(signParams, signature))
   159  }
   160  
   161  //ReSign manipulates the http.Request instance with some required authentication headers for SK/SK auth
   162  func ReSign(req *http.Request, signOptions SignOptions) {
   163  	signOptions.AccessKey = strings.TrimSpace(signOptions.AccessKey)
   164  	signOptions.SecretKey = strings.TrimSpace(signOptions.SecretKey)
   165  	signOptions.encodeUrl = true
   166  
   167  	signParams := reqSignParams{
   168  		SignOptions: signOptions,
   169  		RequestTime: time.Now(),
   170  		Req:         req,
   171  	}
   172  
   173  	if signParams.SignAlgorithm == "" {
   174  		signParams.SignAlgorithm = SignAlgorithmHMACSHA256
   175  	}
   176  
   177  	setRequiredHeaders(req, signParams.getFormattedSigningDateTime())
   178  	contentSha256 := ""
   179  
   180  	if v, ok := req.Header[textproto.CanonicalMIMEHeaderKey(ContentSha256HeaderKey)]; !ok {
   181  		contentSha256 = calculateContentHash(req)
   182  	} else {
   183  		contentSha256 = v[0]
   184  	}
   185  
   186  	canonicalRequest := createCanonicalRequest(signParams, contentSha256)
   187  
   188  	strToSign := createStringToSign(canonicalRequest, signParams)
   189  	signKey := deriveSigningKey(signParams)
   190  	signature := computeSignature(strToSign, signKey, signParams.SignAlgorithm)
   191  
   192  	req.Header.Set("Authorization", buildAuthorizationHeader(signParams, signature))
   193  }
   194  
   195  // deriveSigningKey returns a sign key from cache, or build it and insert it into cache
   196  func deriveSigningKey(signParam reqSignParams) []byte {
   197  	if signParam.EnableCacheSignKey {
   198  		cacheKey := strings.Join([]string{signParam.SecretKey,
   199  			signParam.RegionName,
   200  			signParam.ServiceName,
   201  		}, "-")
   202  
   203  		cacheData := cache.Get(cacheKey)
   204  
   205  		if cacheData != "" {
   206  			var signKey signKeyCacheEntry
   207  			json.Unmarshal([]byte(cacheData), &signKey)
   208  
   209  			if signKey.NumberOfDaysSinceEpoch == signParam.getDaysSinceEpon() {
   210  				return signKey.Key
   211  			}
   212  		}
   213  
   214  		signKey := buildSignKey(signParam)
   215  		signKeyStr, _ := json.Marshal(signKeyCacheEntry{
   216  			Key:                    signKey,
   217  			NumberOfDaysSinceEpoch: signParam.getDaysSinceEpon(),
   218  		})
   219  		cache.Add(cacheKey, string(signKeyStr))
   220  		return signKey
   221  	} else {
   222  		return buildSignKey(signParam)
   223  	}
   224  }
   225  
   226  func buildSignKey(signParam reqSignParams) []byte {
   227  	var kSecret StringBuilder
   228  	kSecret.Write("SDK").Write(signParam.SecretKey)
   229  
   230  	kDate := computeSignature(signParam.getFormattedSigningDate(), kSecret.GetBytes(), signParam.SignAlgorithm)
   231  	kRegion := computeSignature(signParam.RegionName, kDate, signParam.SignAlgorithm)
   232  	kService := computeSignature(signParam.ServiceName, kRegion, signParam.SignAlgorithm)
   233  	return computeSignature("sdk_request", kService, signParam.SignAlgorithm)
   234  }
   235  
   236  //HmacSha256 implements the  Keyed-Hash Message Authentication Code computation
   237  func HmacSha256(data string, key []byte) []byte {
   238  	mac := hmac.New(sha256.New, key)
   239  	mac.Write([]byte(data))
   240  	return mac.Sum(nil)
   241  }
   242  
   243  // HashSha256 is a wrapper for sha256 implementation
   244  func HashSha256(msg []byte) []byte {
   245  	sh256 := sha256.New()
   246  	sh256.Write(msg)
   247  
   248  	return sh256.Sum(nil)
   249  }
   250  
   251  // buildAuthorizationHeader builds the authentication header value
   252  func buildAuthorizationHeader(signParam reqSignParams, signature []byte) string {
   253  	var signingCredentials StringBuilder
   254  	signingCredentials.Write(signParam.AccessKey).Write("/").Write(signParam.getScope())
   255  
   256  	credential := "Credential=" + signingCredentials.ToString()
   257  	signerHeaders := "SignedHeaders=" + getSignedHeadersString(signParam.Req)
   258  	signatureHeader := "Signature=" + hex.EncodeToString(signature)
   259  
   260  	return signParam.SignAlgorithm + " " + strings.Join([]string{
   261  		credential,
   262  		signerHeaders,
   263  		signatureHeader,
   264  	}, ", ")
   265  }
   266  
   267  // computeSignature computers the signature with the specified algorithm
   268  // and it only supports SDK-HMAC-SHA256 in currently
   269  func computeSignature(signData string, key []byte, algorithm string) []byte {
   270  	if algorithm == SignAlgorithmHMACSHA256 {
   271  		return HmacSha256(signData, key)
   272  	} else {
   273  		log.Fatalf("Unsupported algorithm %s, please use %s and try again", algorithm, SignAlgorithmHMACSHA256)
   274  		return nil
   275  	}
   276  }
   277  
   278  // createStringToSign build the need to be signed string
   279  func createStringToSign(canonicalRequest string, signParams reqSignParams) string {
   280  	return strings.Join([]string{signParams.SignAlgorithm,
   281  		signParams.getFormattedSigningDateTime(),
   282  		signParams.getScope(),
   283  		hex.EncodeToString(HashSha256([]byte(canonicalRequest))),
   284  	}, "\n")
   285  }
   286  
   287  // getCanonicalizedResourcePath builds the valid url path for signing
   288  func getCanonicalizedResourcePath(signParas reqSignParams) string {
   289  	urlStr := signParas.Req.URL.Path
   290  	if !strings.HasPrefix(urlStr, "/") {
   291  		urlStr = "/" + urlStr
   292  	}
   293  
   294  	if !strings.HasSuffix(urlStr, "/") {
   295  		urlStr = urlStr + "/"
   296  	}
   297  
   298  	if signParas.encodeUrl {
   299  		urlStr = urlEncode(urlStr, true)
   300  	}
   301  
   302  	if urlStr == "" {
   303  		urlStr = "/"
   304  	}
   305  
   306  	return urlStr
   307  }
   308  
   309  // urlEncode encodes url path and url querystring according to the following rules:
   310  // The alphanumeric characters "a" through "z", "A" through "Z" and "0" through "9" remain the same.
   311  //The special characters ".", "-", "*", and "_" remain the same.
   312  //The space character " " is converted into a plus sign "%20".
   313  //All other characters are unsafe and are first converted into one or more bytes using some encoding scheme.
   314  func urlEncode(url string, urlPath bool) string {
   315  	var buf bytes.Buffer
   316  	for i := 0; i < len(url); i++ {
   317  		c := url[i]
   318  		if noEscape[c] || (c == '/' && urlPath) {
   319  			buf.WriteByte(c)
   320  		} else {
   321  			fmt.Fprintf(&buf, "%%%02X", c)
   322  		}
   323  	}
   324  
   325  	return buf.String()
   326  }
   327  
   328  // encodeQueryString build and encode querystring to a string for signing
   329  func encodeQueryString(queryValues url.Values) string {
   330  	var encodedVals = make(map[string]string, len(queryValues))
   331  	var keys = make([]string, len(queryValues))
   332  
   333  	i := 0
   334  
   335  	for k := range queryValues {
   336  		keys[i] = urlEncode(k, false)
   337  		encodedVals[keys[i]] = k
   338  		i++
   339  	}
   340  
   341  	caseInsensitiveSort(keys)
   342  
   343  	var queryStr StringBuilder
   344  	for i, k := range keys {
   345  		if i > 0 {
   346  			queryStr.Write("&")
   347  		}
   348  
   349  		queryStr.Write(k).Write("=").Write(urlEncode(queryValues.Get(encodedVals[k]), false))
   350  	}
   351  
   352  	return queryStr.ToString()
   353  }
   354  
   355  // getCanonicalizedQueryString return empty string if in POST method and content is nil, otherwise returns sorted,encoded querystring
   356  func getCanonicalizedQueryString(signParas reqSignParams) string {
   357  	if usePayloadForQueryParameters(signParas.Req) {
   358  		return ""
   359  	} else {
   360  		return encodeQueryString(signParas.Req.URL.Query())
   361  	}
   362  }
   363  
   364  // createCanonicalRequest builds canonical string depends the official document  for signing
   365  func createCanonicalRequest(signParas reqSignParams, contentSha256 string) string {
   366  	return strings.Join([]string{signParas.Req.Method,
   367  		getCanonicalizedResourcePath(signParas),
   368  		getCanonicalizedQueryString(signParas),
   369  		getCanonicalizedHeaderString(signParas.Req),
   370  		getSignedHeadersString(signParas.Req),
   371  		contentSha256,
   372  	}, "\n")
   373  }
   374  
   375  // calculateContentHash computes the content hash value
   376  func calculateContentHash(req *http.Request) string {
   377  	encodeParas := ""
   378  
   379  	//post and content is null use queryString as content -- according to document
   380  	if usePayloadForQueryParameters(req) {
   381  		encodeParas = req.URL.Query().Encode()
   382  	} else {
   383  		if req.Body == nil {
   384  			encodeParas = ""
   385  		} else {
   386  			readBody, _ := ioutil.ReadAll(req.Body)
   387  			req.Body = ioutil.NopCloser(bytes.NewBuffer(readBody))
   388  			encodeParas = string(readBody)
   389  		}
   390  	}
   391  
   392  	return hex.EncodeToString(HashSha256([]byte(encodeParas)))
   393  }
   394  
   395  // usePayloadForQueryParameters specifies use querystring or not as content for compute content hash
   396  func usePayloadForQueryParameters(req *http.Request) bool {
   397  	if strings.ToLower(req.Method) != "post" {
   398  		return false
   399  	}
   400  
   401  	return req.Body == nil
   402  }
   403  
   404  // getCanonicalizedHeaderString converts header map to a string for signing
   405  func getCanonicalizedHeaderString(req *http.Request) string {
   406  	var headers StringBuilder
   407  
   408  	keys := make([]string, 0)
   409  	for k := range req.Header {
   410  		keys = append(keys, strings.TrimSpace(k))
   411  	}
   412  
   413  	caseInsensitiveSort(keys)
   414  
   415  	for _, k := range keys {
   416  		k = strings.ToLower(k)
   417  		newKey := spaceRegexp.ReplaceAllString(k, " ")
   418  		headers.Write(newKey)
   419  		headers.Write(":")
   420  
   421  		val := req.Header.Get(k)
   422  		val = spaceRegexp.ReplaceAllString(val, " ")
   423  		headers.Write(val)
   424  
   425  		headers.Write("\n")
   426  	}
   427  
   428  	return headers.ToString()
   429  }
   430  
   431  // getSignedHeadersString builds the string for AuthorizationHeader and signing
   432  func getSignedHeadersString(req *http.Request) string {
   433  	var headers StringBuilder
   434  
   435  	keys := make([]string, 0)
   436  	for k := range req.Header {
   437  		keys = append(keys, strings.TrimSpace(k))
   438  	}
   439  
   440  	caseInsensitiveSort(keys)
   441  
   442  	for idx, k := range keys {
   443  
   444  		if idx > 0 {
   445  			headers.Write(";")
   446  		}
   447  
   448  		headers.Write(strings.ToLower(k))
   449  	}
   450  
   451  	return headers.ToString()
   452  }
   453  
   454  // addRequiredHeaders adds the required heads to http.request instance
   455  func addRequiredHeaders(req *http.Request, timeStr string) {
   456  	// golang handls port by default
   457  	req.Header.Add("Host", req.URL.Host)
   458  	req.Header.Add("X-Sdk-Date", timeStr)
   459  }
   460  
   461  // setRequiredHeaders sets the required heads to http.request for redirection
   462  func setRequiredHeaders(req *http.Request, timeStr string) {
   463  	req.Header.Set("X-Sdk-Date", timeStr)
   464  	req.Header.Del("Authorization")
   465  }
   466  
   467  func (s caseInsencitiveStringArray) Len() int {
   468  	return len(s)
   469  }
   470  func (s caseInsencitiveStringArray) Swap(i, j int) {
   471  	s[i], s[j] = s[j], s[i]
   472  }
   473  func (s caseInsencitiveStringArray) Less(i, j int) bool {
   474  	return strings.ToLower(s[i]) < strings.ToLower(s[j])
   475  }
   476  
   477  func caseInsensitiveSort(strSlice []string) {
   478  	sort.Sort(caseInsencitiveStringArray(strSlice))
   479  }
   480  
   481  func (signParas *reqSignParams) getSigningDateTimeMilli() int64 {
   482  	return (signParas.RequestTime.UTC().Unix() - signParas.TimeOffsetInseconds) * 1000
   483  }
   484  
   485  func (signParas *reqSignParams) getSigningDateTime() time.Time {
   486  	return time.Unix(signParas.getSigningDateTimeMilli()/1000, 0)
   487  }
   488  
   489  func (signParas *reqSignParams) getDaysSinceEpon() int64 {
   490  	return signParas.getSigningDateTimeMilli() / 1000 / 3600 / 24
   491  }
   492  
   493  func (signParas *reqSignParams) getFormattedSigningDate() string {
   494  	return signParas.getSigningDateTime().UTC().Format("20060102")
   495  }
   496  func (signParas *reqSignParams) getFormattedSigningDateTime() string {
   497  	return signParas.getSigningDateTime().UTC().Format("20060102T150405Z")
   498  }
   499  
   500  func (signParas *reqSignParams) getScope() string {
   501  	return strings.Join([]string{signParas.getFormattedSigningDate(),
   502  		signParas.RegionName,
   503  		signParas.ServiceName,
   504  		"sdk_request",
   505  	}, "/")
   506  }
   507  
   508  func (buff *StringBuilder) Write(s string) *StringBuilder {
   509  	buff.builder.WriteString((s))
   510  	return buff
   511  }
   512  
   513  func (buff *StringBuilder) ToString() string {
   514  	return buff.builder.String()
   515  }
   516  
   517  func (buff *StringBuilder) GetBytes() []byte {
   518  	return []byte(buff.ToString())
   519  }