Skip to content

Commit

Permalink
Merge pull request #244 from newrelic/hren/huaweiURLSigner
Browse files Browse the repository at this point in the history
Feature: added request signing support for Huawei cloud service API
  • Loading branch information
brushknight authored Nov 27, 2020
2 parents 0788409 + 41608a4 commit b84f6d3
Show file tree
Hide file tree
Showing 5 changed files with 297 additions and 0 deletions.
42 changes: 42 additions & 0 deletions internal/huaweihws/escape.go
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)
}
201 changes: 201 additions & 0 deletions internal/huaweihws/signer.go
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
}
27 changes: 27 additions & 0 deletions internal/huaweihws/signer_test.go
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))

}
20 changes: 20 additions & 0 deletions internal/inputs/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"time"

xj "github.com/basgys/goxml2json"
"github.com/newrelic/nri-flex/internal/huaweihws"
"github.com/newrelic/nri-flex/internal/load"
"github.com/parnurzeal/gorequest"
"github.com/sirupsen/logrus"
Expand Down Expand Up @@ -254,6 +255,25 @@ func setRequestOptions(request *gorequest.SuperAgent, yml load.Config, api load.
request = request.TLSClientConfig(&tmpAPITLSConfig)
}

if api.HWSigner.Key != "" && api.HWSigner.Secret != "" {
signer := huaweihws.Signer{
Key: api.HWSigner.Key,
Secret: api.HWSigner.Secret,
}
r, err := request.MakeRequest()
if err != nil {
load.Logrus.WithError(err).Error("http: signer failed to convert request for HWSigner.")
} else {
err := signer.Sign(r)
if err != nil {
load.Logrus.WithError(err).Error("http: signer failed to sign the request.")
} else {
request = request.Set(huaweihws.HeaderAuthorization, r.Header.Get(huaweihws.HeaderAuthorization))
request = request.Set(huaweihws.HeaderXDate, r.Header.Get(huaweihws.HeaderXDate))
}

}
}
return request
}

Expand Down
7 changes: 7 additions & 0 deletions internal/load/load.go
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,7 @@ type API struct {
SplitArray bool `yaml:"split_array"` // convert array to samples, use SetHeader to set attribute name
LeafArray bool `yaml:"leaf_array"` // convert array element to samples when SplitArray, use SetHeader to set attribute name
Scp SCP `yaml:"scp"`
HWSigner HWSigner `yaml:"hw_signer"` // Huawei Cloud Service API signer
// Key manipulation
ToLower bool `yaml:"to_lower"` // convert all unicode letters mapped to their lower case.
ConvertSpace string `yaml:"convert_space"` // convert spaces to another char
Expand Down Expand Up @@ -478,6 +479,12 @@ type SCP struct {
SSHPEMFile string `yaml:"ssh_pem_file"`
}

// HWSigner struct
type HWSigner struct {
Key string `yaml:"key"`
Secret string `yaml:"secret"`
}

// Parse struct
type Parse struct {
Type string `yaml:"type"` // perform a contains, match, hasPrefix or regex for specified key
Expand Down

0 comments on commit b84f6d3

Please sign in to comment.