-
Notifications
You must be signed in to change notification settings - Fork 121
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #244 from newrelic/hren/huaweiURLSigner
Feature: added request signing support for Huawei cloud service API
- Loading branch information
Showing
5 changed files
with
297 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
// based on https://github.com/golang/go/blob/master/src/net/url/url.go | ||
// Copyright 2009 The Go Authors. All rights reserved. | ||
// Use of this source code is governed by a BSD-style | ||
// license that can be found in the LICENSE file. | ||
|
||
package huaweihws | ||
|
||
func shouldEscape(c byte) bool { | ||
if 'A' <= c && c <= 'Z' || 'a' <= c && c <= 'z' || '0' <= c && c <= '9' || c == '_' || c == '-' || c == '~' || c == '.' { | ||
return false | ||
} | ||
return true | ||
} | ||
func escape(s string) string { | ||
hexCount := 0 | ||
for i := 0; i < len(s); i++ { | ||
c := s[i] | ||
if shouldEscape(c) { | ||
hexCount++ | ||
} | ||
} | ||
|
||
if hexCount == 0 { | ||
return s | ||
} | ||
|
||
t := make([]byte, len(s)+2*hexCount) | ||
j := 0 | ||
for i := 0; i < len(s); i++ { | ||
switch c := s[i]; { | ||
case shouldEscape(c): | ||
t[j] = '%' | ||
t[j+1] = "0123456789ABCDEF"[c>>4] | ||
t[j+2] = "0123456789ABCDEF"[c&15] | ||
j += 3 | ||
default: | ||
t[j] = s[i] | ||
j++ | ||
} | ||
} | ||
return string(t) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,201 @@ | ||
// HWS API Gateway Signature | ||
// based on https://github.com/datastream/aws/blob/master/signv4.go | ||
// Copyright (c) 2014, Xianjie | ||
|
||
package huaweihws | ||
|
||
import ( | ||
"bytes" | ||
"crypto/hmac" | ||
"crypto/sha256" | ||
"fmt" | ||
"io/ioutil" | ||
"net/http" | ||
"sort" | ||
"strings" | ||
"time" | ||
) | ||
|
||
// BasicDateFormat | ||
const ( | ||
BasicDateFormat = "20060102T150405Z" | ||
Algorithm = "SDK-HMAC-SHA256" | ||
HeaderXDate = "X-Sdk-Date" | ||
HeaderHost = "host" | ||
HeaderAuthorization = "Authorization" | ||
HeaderContentSha256 = "X-Sdk-Content-Sha256" | ||
) | ||
|
||
func hmacsha256(key []byte, data string) ([]byte, error) { | ||
h := hmac.New(sha256.New, key) | ||
if _, err := h.Write([]byte(data)); err != nil { | ||
return nil, err | ||
} | ||
return h.Sum(nil), nil | ||
} | ||
|
||
// CanonicalRequest func | ||
func CanonicalRequest(r *http.Request, signedHeaders []string) (string, error) { | ||
var hexencode string | ||
// var err error | ||
if hex := r.Header.Get(HeaderContentSha256); hex != "" { | ||
hexencode = hex | ||
} else { | ||
data, err := RequestPayload(r) | ||
if err != nil { | ||
return "", err | ||
} | ||
hexencode, err = HexEncodeSHA256Hash(data) | ||
if err != nil { | ||
return "", err | ||
} | ||
} | ||
return fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n%s", r.Method, CanonicalURI(r), CanonicalQueryString(r), CanonicalHeaders(r, signedHeaders), strings.Join(signedHeaders, ";"), hexencode), nil | ||
} | ||
|
||
// CanonicalURI returns request uri | ||
func CanonicalURI(r *http.Request) string { | ||
pattens := strings.Split(r.URL.Path, "/") | ||
uri := make([]string, 0) | ||
for _, v := range pattens { | ||
uri = append(uri, escape(v)) | ||
} | ||
urlpath := strings.Join(uri, "/") | ||
if len(urlpath) == 0 || urlpath[len(urlpath)-1] != '/' { | ||
urlpath = urlpath + "/" | ||
} | ||
return urlpath | ||
} | ||
|
||
// CanonicalQueryString func | ||
func CanonicalQueryString(r *http.Request) string { | ||
keys := make([]string, 0) | ||
query := r.URL.Query() | ||
for key := range query { | ||
keys = append(keys, key) | ||
} | ||
sort.Strings(keys) | ||
var a []string | ||
for _, key := range keys { | ||
k := escape(key) | ||
sort.Strings(query[key]) | ||
for _, v := range query[key] { | ||
kv := fmt.Sprintf("%s=%s", k, escape(v)) | ||
a = append(a, kv) | ||
} | ||
} | ||
queryStr := strings.Join(a, "&") | ||
r.URL.RawQuery = queryStr | ||
return queryStr | ||
} | ||
|
||
// CanonicalHeaders func | ||
func CanonicalHeaders(r *http.Request, signerHeaders []string) string { | ||
var a []string | ||
header := make(map[string][]string) | ||
for k, v := range r.Header { | ||
header[strings.ToLower(k)] = v | ||
} | ||
for _, key := range signerHeaders { | ||
value := header[key] | ||
if strings.EqualFold(key, HeaderHost) { | ||
value = []string{r.Host} | ||
} | ||
sort.Strings(value) | ||
for _, v := range value { | ||
a = append(a, key+":"+strings.TrimSpace(v)) | ||
} | ||
} | ||
return fmt.Sprintf("%s\n", strings.Join(a, "\n")) | ||
} | ||
|
||
// SignedHeaders func | ||
func SignedHeaders(r *http.Request) []string { | ||
a := make([]string, 0) | ||
for key := range r.Header { | ||
a = append(a, strings.ToLower(key)) | ||
} | ||
sort.Strings(a) | ||
return a | ||
} | ||
|
||
// RequestPayload func | ||
func RequestPayload(r *http.Request) ([]byte, error) { | ||
if r.Body == nil { | ||
return []byte(""), nil | ||
} | ||
b, err := ioutil.ReadAll(r.Body) | ||
if err != nil { | ||
return []byte(""), err | ||
} | ||
r.Body = ioutil.NopCloser(bytes.NewBuffer(b)) | ||
return b, err | ||
} | ||
|
||
// StringToSign Create a "String to Sign". | ||
func StringToSign(canonicalRequest string, t time.Time) (string, error) { | ||
hash := sha256.New() | ||
_, err := hash.Write([]byte(canonicalRequest)) | ||
if err != nil { | ||
return "", err | ||
} | ||
return fmt.Sprintf("%s\n%s\n%x", | ||
Algorithm, t.UTC().Format(BasicDateFormat), hash.Sum(nil)), nil | ||
} | ||
|
||
// SignStringToSign Create the HWS Signature. | ||
func SignStringToSign(stringToSign string, signingKey []byte) (string, error) { | ||
hm, err := hmacsha256(signingKey, stringToSign) | ||
return fmt.Sprintf("%x", hm), err | ||
} | ||
|
||
// HexEncodeSHA256Hash returns hexcode of sha256 | ||
func HexEncodeSHA256Hash(body []byte) (string, error) { | ||
hash := sha256.New() | ||
if body == nil { | ||
body = []byte("") | ||
} | ||
_, err := hash.Write(body) | ||
return fmt.Sprintf("%x", hash.Sum(nil)), err | ||
} | ||
|
||
// AuthHeaderValue Get the finalized value for the "Authorization" header. The signature parameter is the output from SignStringToSign | ||
func AuthHeaderValue(signature, accessKey string, signedHeaders []string) string { | ||
return fmt.Sprintf("%s Access=%s, SignedHeaders=%s, Signature=%s", Algorithm, accessKey, strings.Join(signedHeaders, ";"), signature) | ||
} | ||
|
||
// Signer Signature HWS meta | ||
type Signer struct { | ||
Key string | ||
Secret string | ||
} | ||
|
||
// Sign SignRequest set Authorization header | ||
func (s *Signer) Sign(r *http.Request) error { | ||
var t time.Time | ||
var err error | ||
var dt string | ||
if dt = r.Header.Get(HeaderXDate); dt != "" { | ||
t, err = time.Parse(BasicDateFormat, dt) | ||
} | ||
if err != nil || dt == "" { | ||
t = time.Now() | ||
r.Header.Set(HeaderXDate, t.UTC().Format(BasicDateFormat)) | ||
} | ||
signedHeaders := SignedHeaders(r) | ||
canonicalRequest, err := CanonicalRequest(r, signedHeaders) | ||
if err != nil { | ||
return err | ||
} | ||
stringToSign, err := StringToSign(canonicalRequest, t) | ||
if err != nil { | ||
return err | ||
} | ||
signature, err := SignStringToSign(stringToSign, []byte(s.Secret)) | ||
if err != nil { | ||
return err | ||
} | ||
authValue := AuthHeaderValue(signature, s.Key, signedHeaders) | ||
r.Header.Set(HeaderAuthorization, authValue) | ||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
package huaweihws | ||
|
||
import ( | ||
"testing" | ||
|
||
"github.com/parnurzeal/gorequest" | ||
"gotest.tools/assert" | ||
) | ||
|
||
func TestSign(t *testing.T) { | ||
expectedSignature := `SDK-HMAC-SHA256 Access=keyabc111, SignedHeaders=content-type;header1;x-sdk-date, Signature=c5f664f53d5f02d79428e5d7d188ecb9f4018ce0fbdf0ac46673362d952cce81` | ||
|
||
reqURL := "dummyURL.com" | ||
signer := Signer{ | ||
Key: "keyabc111", | ||
Secret: "secretxyz999", | ||
} | ||
request := gorequest.New() | ||
request = request.Get(reqURL) | ||
request = request.Set("header1", "headerValue1") | ||
request = request.Set("X-Sdk-Date", "20201117T025305Z") | ||
r, _ := request.MakeRequest() | ||
_ = signer.Sign(r) | ||
|
||
assert.Equal(t, expectedSignature, r.Header.Get(HeaderAuthorization)) | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters