import (
"bytes"
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"reflect"
"sort"
"strings"
"testing"
"time"
"github.com/google/uuid"
)
const (
timeFormat = "20060102T150405Z"
shortTimeFormat = "20060102"
)
type Signer interface {
Sign(credential *Credential, request *CTYunRequestInfo) (http.Header, error)
}
// Credential the gateway credential
type Credential struct {
AccessKey string
SecretAccessKey string
}
// NewCredential create openapi gateway auth credential
func NewCredential(accessKey, secretAccessKey string) *Credential {
return &Credential{
AccessKey: accessKey,
SecretAccessKey: secretAccessKey,
}
}
// Logger sdk core logger interface
type Logger interface {
// Debugf write debug log
Debugf(message string, fmtParam ...interface{})
// Infof write info log
Infof(message string, fmtParam ...interface{})
//Errorf write error log
Errorf(err error, message string, fmtParam ...interface{})
}
type DefaultLoggerLevel int
const (
DEBUG DefaultLoggerLevel = iota
INFO
ERROR
)
// DefaultLogger sdk core default log object
type DefaultLogger struct {
LoggerLevel DefaultLoggerLevel
}
// NewDefaultLogger create default logger with default log level ERROR
func NewDefaultLogger() *DefaultLogger {
return &DefaultLogger{LoggerLevel: ERROR}
}
// NewDefaultLoggerWithLevel create new default sdk logger with log level config
func NewDefaultLoggerWithLevel(loggerLevel DefaultLoggerLevel) *DefaultLogger {
return &DefaultLogger{LoggerLevel: loggerLevel}
}
// Infof write info log
func (d *DefaultLogger) Infof(message string, fmtParam ...interface{}) {
if d.LoggerLevel >= INFO {
fmt.Printf(message+"\n", fmtParam)
}
}
// Debugf write debug log
func (d *DefaultLogger) Debugf(message string, fmtParam ...interface{}) {
if d.LoggerLevel >= DEBUG {
fmt.Printf(message+"\n", fmtParam)
}
}
// Errorf write error log
func (d *DefaultLogger) Errorf(err error, message string, fmtParam ...interface{}) {
if d.LoggerLevel == ERROR {
fmt.Printf("err:%s msg:"+message+"\n,", err, fmtParam)
}
}
type CTYunRequestInfo struct {
Header http.Header
Url *url.URL
Query url.Values
Method string
Service string
body []byte
Date time.Time
Singer Signer
Credential *Credential
logger Logger
}
// HttpSchema http request schema ,eg:HTTPS HTTP
type HttpScheme string
const (
//HTTPS https 请求
HTTPS HttpScheme = "https"
//HTTP http 请求
HTTP HttpScheme = "http"
)
// NewCTYunRequestInfo create ctyun sign request info
func NewCTYunRequestInfo(service, host, path, method string, scheme HttpScheme,
header map[string]string, query url.Values, body interface{}, logger Logger) (*CTYunRequestInfo, error) {
if logger == nil {
logger = &DefaultLogger{}
}
var ctYunRequest = &CTYunRequestInfo{
Header: http.Header{},
Query: query,
logger: logger,
Service: service,
Method: method,
Date: time.Now(),
}
if !strings.HasPrefix(path, "/") {
path = "/" + path
}
requestURL := fmt.Sprintf("%s://%s%s", scheme, host, path)
if query != nil {
requestURL = fmt.Sprintf("%s?%s", requestURL, query.Encode())
}
requestUri, err := url.Parse(requestURL)
if err != nil {
return nil, err
}
ctYunRequest.Url = requestUri
if body != nil {
if header != nil && header["content-type"] == "application/x-www-form-urlencoded" {
if reflect.TypeOf(body).Kind() == reflect.String {
ctYunRequest.body = []byte(body.(string))
}
} else {
data, err := json.Marshal(body)
if err == nil {
ctYunRequest.body = data
}
}
} else {
ctYunRequest.body = nil
}
if len(header) > 0 {
for k, v := range header {
if k != "" {
ctYunRequest.Header.Set(k, v)
}
}
}
return ctYunRequest, nil
}
// GetBody get the reqeuest body info
func (c *CTYunRequestInfo) GetBody() []byte {
return c.body
}
// WithCredential set the auth credential for the sign reqeust info
func (c *CTYunRequestInfo) WithCredential(credential *Credential) {
c.Credential = credential
}
// WithSigner set the signer method for signer
func (c *CTYunRequestInfo) WithSigner(Signer Signer) {
c.Singer = Signer
}
// DoSign exec the sign method for sign request info
func (c *CTYunRequestInfo) DoSign() (http.Header, error) {
if c.Singer == nil {
c.Singer = &CTYunHybridSigner{}
}
httpHeader, err := c.Singer.Sign(c.Credential, c)
if err != nil {
return nil, err
}
for k, v := range httpHeader {
c.Header.Set(k, strings.Join(v, ", "))
}
return httpHeader, nil
}
func (c *CTYunRequestInfo) BuildRequest() (*http.Request, error) {
var bodyContent io.Reader
if c.body != nil {
bodyContent = io.NopCloser(bytes.NewBuffer(c.body))
}
// requestURL := fmt.Sprintf("%s://%s%s", c.scheme, c.host, c.path)
// if query != nil {
// requestURL = fmt.Sprintf("%s?%s", requestURL, c.query.Encode())
// }
req, err := http.NewRequest(c.Method, c.Url.String(), bodyContent)
if err != nil {
return nil, err
}
if c.Header != nil {
req.Header = c.Header
}
return req, nil
}
type CTYunHybridSigner struct {
}
type ctYunHybridSignerContext struct {
header http.Header
query url.Values
body []byte
method string
requestUrl *url.URL
headerToSign string
queryToSign string
bodyToSign string
stringToSign string
headerSignOrderNames string
date time.Time
formattedTime string
formattedShortTime string
signature string
authorization string
logger Logger
credential *Credential
}
// header 以 header_name:header_value 来一个一
// 个 通 过 \n 拼 接 起 来 , EOP 是 强 制 要 求
// ctyun-hybrid-request-id和hybrid-date这个头
// header) 作为 Header 中的一部分,并且必须是待签名
// Header 里的一个,EOP 末尾 有 \n 混合云没有
func (c *ctYunHybridSignerContext) buildHeader() {
// var host = c.requestUrl.Host
// if host != "" {
// // c.header.Set("Host", host)
// }
var headerKeys []string
var headersWillSign = map[string]string{}
for k, v := range c.header {
var valueString = strings.Join(v, ", ")
// header_name:header_value
headerDataWillSigner := fmt.Sprintf("%s:%s", k, valueString)
headersWillSign[k] = headerDataWillSigner
headerKeys = append(headerKeys, k)
}
sort.Strings(headerKeys)
var headersWillDatas []string
for _, key := range headerKeys {
headersWillDatas = append(headersWillDatas, headersWillSign[key])
}
c.headerToSign = strings.Join(headersWillDatas, "\n")
c.headerSignOrderNames = strings.Join(headerKeys, ";")
}
// query 以&作为拼接,key 和值以=连接,排序规则
// 使用 26 个英文字母的顺序来排序,Query 参数全
// 部都需要进行签 值 需要进行编码 (混合云 值不需要进行编码)注意如果有空格需要进行替换,go 的query编码标准与网关使用的java 不同,java 的 非rfc3986
func (c *ctYunHybridSignerContext) buildQuery() {
if c.query == nil || len(c.query) == 0 {
c.queryToSign = ""
return
}
var querys []string
for k, v := range c.query {
for _, item := range v {
queryItem := fmt.Sprintf("%s=%s", k, item)
querys = append(querys, queryItem)
}
}
sort.Strings(querys)
c.queryToSign = strings.Join(querys, "&")
}
// EOP body 如果为空需要添加empty sha256 混合云 不需要 也不用拼接
func (c *ctYunHybridSignerContext) buildBody() {
if c.body == nil || len(c.body) == 0 {
c.bodyToSign = ""
return
}
c.bodyToSign = hex.EncodeToString(makeSha256(c.body))
}
func (c *ctYunHybridSignerContext) buildStringToSign() {
if c.queryToSign != "" && c.bodyToSign != "" {
c.stringToSign = c.headerToSign + "\n" + c.queryToSign + "\n" + c.bodyToSign
} else if c.queryToSign != "" && c.bodyToSign == "" {
c.stringToSign = c.headerToSign + "\n" + c.queryToSign
} else if c.queryToSign == "" && c.bodyToSign != "" {
c.stringToSign = c.headerToSign + "\n" + c.bodyToSign
} else {
c.stringToSign = c.headerToSign
}
}
// 4.2.1 使用eop-date作为数据,sk作为密钥,算出ktime。
// 4.2.2 使用ak作为数据,ktime作为密钥,算出kAk。
// 4.2.3 使用eop-date的年月日值作为数据,kAk作为密钥,算出kdate。
// eop-date
// yyyymmddTHHMMSSZ(20211221T163614Z)(年月日T时分秒Z)
// Ktime
// 使用eop-date作为数据,sk作为密钥,算出ktime。
// Ktime = hmacSha256(eop-date,sk)
// kAk
// 使用ak作为数据,ktime作为密钥,算出kAk。
// kAk = hmacsha256(ak,ktime)
// kdate
// 使用eop-date的年月日值作为数据,kAk作为密钥,算出kdate。
// kdate = hmacsha256(eop-date,kAk)
// 使用kdate作为密钥、sigture作为数据,将其得到的结果进行base64编码得出Signature
// Signature
// 1)hmacsha256(sigture,kdate)
// 2)将上一步的结果进行base64加密得出Signature
func (c *ctYunHybridSignerContext) buildSignature() {
keyTime := makeHmac([]byte(c.credential.SecretAccessKey), []byte(c.formattedTime))
c.logger.Debugf("===============signature===================\nkeyTime:%s\n", hex.EncodeToString(keyTime))
keyAk := makeHmac(keyTime, []byte(c.credential.AccessKey))
c.logger.Debugf("keyAk:%s\n", hex.EncodeToString(keyAk))
kDate := makeHmac(keyAk, []byte(c.formattedShortTime))
c.logger.Debugf("kdate:%s\n", hex.EncodeToString(kDate))
sigtureData := makeHmac(kDate, []byte(c.stringToSign))
c.signature = base64.StdEncoding.EncodeToString(sigtureData)
c.logger.Debugf("signResult=%s\n=============== end signature===================\n", c.signature)
}
// 4.4.1 构造Headers。
// 4.4.2得到Eop-Authorization。
// Eop-Authorization:ak Headers=xxx Signature==xxx。
// Headers
// 将需要进行签名的请求头字段以 “header_name”的形式、以“;”作为间隔符、以英文字母表作为header_name的排序依据将它们拼接起来。
// 例子(假设你需要将ctyun-eop-request-id、eop-date都要签名):Headers=
// ctyun-eop-request-id;eop-date
// Eop-Authorization
// Eop-Authorization:ak Headers=xxx Signature=xxx。注意,ak、Headers、Signature之间以空格隔开。
// 例如:Eop-Authorization:ak Headers=ctyun-eop-request-id;eop-date Signature=NlMHOhk5bVfZ9MwDSSJydcZjjENmDtpNYigJGVb
// 注意:如果你需要进行签名的Header不止默认的ctyun-eop-request-id和eop-date,那么你需要在http_client的请求头部中加上,并且Eop-Authorization中也需要增加
func (c *ctYunHybridSignerContext) buildAuthorization() {
c.authorization = fmt.Sprintf("%s Header=%s Signature=%s", c.credential.AccessKey, c.headerSignOrderNames, c.signature)
c.header.Set("Hybrid-Authorization", c.authorization)
}
func (s *CTYunHybridSigner) Sign(credential *Credential, request *CTYunRequestInfo) (http.Header, error) {
signerContext := &ctYunHybridSignerContext{
header: request.Header,
query: request.Query,
body: request.body,
method: request.Method,
requestUrl: request.Url,
logger: request.logger,
credential: credential,
date: request.Date,
}
if signerContext.header == nil {
signerContext.header = http.Header{}
}
var requestId = signerContext.header.Get("ctyun-hybrid-request-id")
if requestId == "" {
requestId = uuid.NewString()
signerContext.header.Set("ctyun-hybrid-request-id", requestId)
}
// hybrid-date:20210531T100101Z
signerContext.formattedTime = signerContext.date.Format(timeFormat)
signerContext.formattedShortTime = signerContext.date.Format(shortTimeFormat)
signerContext.header.Set("hybrid-date", signerContext.formattedTime)
signerContext.buildHeader()
signerContext.buildQuery()
signerContext.buildBody()
signerContext.buildStringToSign()
signerContext.buildSignature()
signerContext.buildAuthorization()
signerContext.logger.Debugf("------------------------------------------------------------\n")
signerContext.logger.Debugf("hybrid-date: %s\n", signerContext.formattedTime)
signerContext.logger.Debugf("signedHeaders:%s\n", signerContext.headerSignOrderNames)
signerContext.logger.Debugf("headerToSign:%s\n", signerContext.headerToSign)
signerContext.logger.Debugf("queryToSign:%s\n", signerContext.queryToSign)
signerContext.logger.Debugf("bodyToSign:\n%s\n", signerContext.bodyToSign)
signerContext.logger.Debugf("stringToSign:\n%s\n", signerContext.stringToSign)
signerContext.logger.Debugf("Hybrid-Authorization:\n%s\n-----------------------------------------------------\n", signerContext.authorization)
return signerContext.header, nil
}
func makeHmac(key []byte, data []byte) []byte {
hash := hmac.New(sha256.New, key)
hash.Write(data)
return hash.Sum(nil)
}
func makeSha256(data []byte) []byte {
hash := sha256.New()
hash.Write(data)
return hash.Sum(nil)
}
// 执行请求用例
func Test_GetImageList(t *testing.T) {
start := time.Now().Unix()
fmt.Printf("start : %d", start)
var header = map[string]string{}
var query = url.Values{}
query.Add("regionID", "nm8")
query.Add("pageSize", "50")
query.Add("pageNo", "1")
request, err := NewCTYunRequestInfo("1", "127.0.0.1:9080", "/v4/image/list", "GET", HTTP, header, query,
nil, NewDefaultLoggerWithLevel(DEBUG))
if err != nil {
t.Fatal(err)
}
var authCredential = NewCredential("", "")
request.WithCredential(authCredential)
request.WithSigner(&CTYunHybridSigner{})
signHeader, err := request.DoSign()
if err != nil {
t.Fatal(err)
}
for k, v := range signHeader {
fmt.Printf("%s:%s\n", k, v)
}
req, err := request.BuildRequest()
if err != nil {
t.Fatal(err)
}
req.Header.Add("ctuserid", "1000000566")
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatal(err)
}
fmt.Println(resp.Status)
if resp.Body != nil {
data, err := ioutil.ReadAll(resp.Body)
if err != nil {
t.Fatal(err)
}
fmt.Println(string(data))
}
end := time.Now().Unix()
fmt.Printf("end : %d, cost: %d", end, end-start)
}