This commit is contained in:
bel
2020-01-13 03:37:51 +00:00
commit c8eb52f9ba
2023 changed files with 702080 additions and 0 deletions

View File

@@ -0,0 +1,291 @@
// +-------------------------------------------------------------------------
// | Copyright (C) 2016 Yunify, Inc.
// +-------------------------------------------------------------------------
// | Licensed under the Apache License, Version 2.0 (the "License");
// | you may not use this work except in compliance with the License.
// | You may obtain a copy of the License in the LICENSE file, or at:
// |
// | http://www.apache.org/licenses/LICENSE-2.0
// |
// | Unless required by applicable law or agreed to in writing, software
// | distributed under the License is distributed on an "AS IS" BASIS,
// | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// | See the License for the specific language governing permissions and
// | limitations under the License.
// +-------------------------------------------------------------------------
package builder
import (
"encoding/json"
"errors"
"io"
"net/http"
"reflect"
"strconv"
"strings"
"time"
"unicode"
"github.com/pengsrc/go-shared/convert"
"github.com/yunify/qingstor-sdk-go/request/data"
"github.com/yunify/qingstor-sdk-go/utils"
)
// BaseBuilder is the base builder for all services.
type BaseBuilder struct {
parsedURL string
parsedProperties *map[string]string
parsedQuery *map[string]string
parsedHeaders *map[string]string
parsedBodyString string
parsedBody io.Reader
operation *data.Operation
input *reflect.Value
}
// BuildHTTPRequest builds http request with an operation and an input.
func (b *BaseBuilder) BuildHTTPRequest(o *data.Operation, i *reflect.Value) (*http.Request, error) {
b.operation = o
b.input = i
_, err := b.parse()
if err != nil {
return nil, err
}
return b.build()
}
func (b *BaseBuilder) build() (*http.Request, error) {
httpRequest, err := http.NewRequest(b.operation.RequestMethod, b.parsedURL, b.parsedBody)
if err != nil {
return nil, err
}
err = b.setupHeaders(httpRequest)
if err != nil {
return nil, err
}
return httpRequest, nil
}
func (b *BaseBuilder) parse() (*BaseBuilder, error) {
err := b.parseRequestQueryAndHeaders()
if err != nil {
return b, err
}
err = b.parseRequestBody()
if err != nil {
return b, err
}
err = b.parseRequestProperties()
if err != nil {
return b, err
}
err = b.parseRequestURL()
if err != nil {
return b, err
}
return b, nil
}
func (b *BaseBuilder) parseRequestQueryAndHeaders() error {
requestQuery := map[string]string{}
requestHeaders := map[string]string{}
maps := map[string](map[string]string){
"query": requestQuery,
"headers": requestHeaders,
}
b.parsedQuery = &requestQuery
b.parsedHeaders = &requestHeaders
if !b.input.IsValid() {
return nil
}
fields := b.input.Elem()
if !fields.IsValid() {
return nil
}
for i := 0; i < fields.NumField(); i++ {
tagName := fields.Type().Field(i).Tag.Get("name")
tagLocation := fields.Type().Field(i).Tag.Get("location")
if tagDefault := fields.Type().Field(i).Tag.Get("default"); tagDefault != "" {
maps[tagLocation][tagName] = tagDefault
}
if tagName != "" && tagLocation != "" && maps[tagLocation] != nil {
switch value := fields.Field(i).Interface().(type) {
case *string:
if value != nil {
maps[tagLocation][tagName] = *value
}
case *int:
if value != nil {
maps[tagLocation][tagName] = strconv.Itoa(int(*value))
}
case *int64:
if value != nil {
maps[tagLocation][tagName] = strconv.FormatInt(int64(*value), 10)
}
case *bool:
case *time.Time:
if value != nil {
formatString := fields.Type().Field(i).Tag.Get("format")
format := ""
switch formatString {
case "RFC 822":
format = convert.RFC822
case "ISO 8601":
format = convert.ISO8601
}
maps[tagLocation][tagName] = convert.TimeToString(*value, format)
}
}
}
}
return nil
}
func (b *BaseBuilder) parseRequestBody() error {
requestData := map[string]interface{}{}
if !b.input.IsValid() {
return nil
}
fields := b.input.Elem()
if !fields.IsValid() {
return nil
}
for i := 0; i < fields.NumField(); i++ {
location := fields.Type().Field(i).Tag.Get("location")
if location == "elements" {
name := fields.Type().Field(i).Tag.Get("name")
requestData[name] = fields.Field(i).Interface()
}
}
if len(requestData) != 0 {
dataValue, err := json.Marshal(requestData)
if err != nil {
return err
}
b.parsedBodyString = string(dataValue)
b.parsedBody = strings.NewReader(b.parsedBodyString)
(*b.parsedHeaders)["Content-Type"] = "application/json"
} else {
value := fields.FieldByName("Body")
if value.IsValid() {
switch value.Interface().(type) {
case string:
if value.String() != "" {
b.parsedBodyString = value.String()
b.parsedBody = strings.NewReader(value.String())
}
case io.Reader:
if value.Interface().(io.Reader) != nil {
b.parsedBody = value.Interface().(io.Reader)
}
}
}
}
return nil
}
func (b *BaseBuilder) parseRequestProperties() error {
propertiesMap := map[string]string{}
b.parsedProperties = &propertiesMap
if b.operation.Properties != nil {
fields := reflect.ValueOf(b.operation.Properties).Elem()
if fields.IsValid() {
for i := 0; i < fields.NumField(); i++ {
switch value := fields.Field(i).Interface().(type) {
case *string:
if value != nil {
propertiesMap[fields.Type().Field(i).Tag.Get("name")] = *value
}
case *int:
if value != nil {
numberString := strconv.Itoa(int(*value))
propertiesMap[fields.Type().Field(i).Tag.Get("name")] = numberString
}
}
}
}
}
return nil
}
func (b *BaseBuilder) parseRequestURL() error {
return nil
}
func (b *BaseBuilder) setupHeaders(httpRequest *http.Request) error {
if b.parsedHeaders != nil {
for headerKey, headerValue := range *b.parsedHeaders {
for _, r := range headerValue {
if r > unicode.MaxASCII {
headerValue = utils.URLQueryEscape(headerValue)
break
}
}
httpRequest.Header.Set(headerKey, headerValue)
}
}
if httpRequest.Header.Get("Content-Length") == "" {
var length int64
switch body := b.parsedBody.(type) {
case nil:
length = 0
case io.Seeker:
//start, err := body.Seek(0, io.SeekStart)
start, err := body.Seek(0, 0)
if err != nil {
return err
}
//end, err := body.Seek(0, io.SeekEnd)
end, err := body.Seek(0, 2)
if err != nil {
return err
}
//body.Seek(0, io.SeekStart)
body.Seek(0, 0)
length = end - start
default:
return errors.New("can not get Content-Length")
}
if length > 0 {
httpRequest.ContentLength = length
httpRequest.Header.Set("Content-Length", strconv.Itoa(int(length)))
} else {
httpRequest.Header.Set("Content-Length", "0")
}
}
length, err := strconv.Atoi(httpRequest.Header.Get("Content-Length"))
if err != nil {
return err
}
httpRequest.ContentLength = int64(length)
if httpRequest.Header.Get("Date") == "" {
httpRequest.Header.Set("Date", convert.TimeToString(time.Now(), convert.RFC822))
}
return nil
}

View File

@@ -0,0 +1,175 @@
// +-------------------------------------------------------------------------
// | Copyright (C) 2016 Yunify, Inc.
// +-------------------------------------------------------------------------
// | Licensed under the Apache License, Version 2.0 (the "License");
// | you may not use this work except in compliance with the License.
// | You may obtain a copy of the License in the LICENSE file, or at:
// |
// | http://www.apache.org/licenses/LICENSE-2.0
// |
// | Unless required by applicable law or agreed to in writing, software
// | distributed under the License is distributed on an "AS IS" BASIS,
// | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// | See the License for the specific language governing permissions and
// | limitations under the License.
// +-------------------------------------------------------------------------
package builder
import (
"bytes"
"crypto/md5"
"encoding/base64"
"fmt"
"io/ioutil"
"mime"
"net/http"
"net/url"
"path"
"reflect"
"regexp"
"runtime"
"strconv"
"strings"
"github.com/pengsrc/go-shared/convert"
"github.com/yunify/qingstor-sdk-go"
"github.com/yunify/qingstor-sdk-go/logger"
"github.com/yunify/qingstor-sdk-go/request/data"
"github.com/yunify/qingstor-sdk-go/utils"
)
// QingStorBuilder is the request builder for QingStor service.
type QingStorBuilder struct {
baseBuilder *BaseBuilder
}
// BuildHTTPRequest builds http request with an operation and an input.
func (qb *QingStorBuilder) BuildHTTPRequest(o *data.Operation, i *reflect.Value) (*http.Request, error) {
qb.baseBuilder = &BaseBuilder{}
qb.baseBuilder.operation = o
qb.baseBuilder.input = i
_, err := qb.baseBuilder.parse()
if err != nil {
return nil, err
}
err = qb.parseURL()
if err != nil {
return nil, err
}
httpRequest, err := http.NewRequest(qb.baseBuilder.operation.RequestMethod,
qb.baseBuilder.parsedURL, qb.baseBuilder.parsedBody)
if err != nil {
return nil, err
}
err = qb.baseBuilder.setupHeaders(httpRequest)
if err != nil {
return nil, err
}
err = qb.setupHeaders(httpRequest)
if err != nil {
return nil, err
}
logger.Infof(nil, fmt.Sprintf(
"Built QingStor request: [%d] %s",
convert.StringToTimestamp(httpRequest.Header.Get("Date"), convert.RFC822),
httpRequest.URL.String(),
))
logger.Infof(nil, fmt.Sprintf(
"QingStor request headers: [%d] %s",
convert.StringToTimestamp(httpRequest.Header.Get("Date"), convert.RFC822),
fmt.Sprint(httpRequest.Header),
))
if qb.baseBuilder.parsedBodyString != "" {
logger.Infof(nil, fmt.Sprintf(
"QingStor request body string: [%d] %s",
convert.StringToTimestamp(httpRequest.Header.Get("Date"), convert.RFC822),
qb.baseBuilder.parsedBodyString,
))
}
return httpRequest, nil
}
func (qb *QingStorBuilder) parseURL() error {
config := qb.baseBuilder.operation.Config
zone := (*qb.baseBuilder.parsedProperties)["zone"]
port := strconv.Itoa(config.Port)
endpoint := config.Protocol + "://" + config.Host + ":" + port
if zone != "" {
endpoint = config.Protocol + "://" + zone + "." + config.Host + ":" + port
}
requestURI := qb.baseBuilder.operation.RequestURI
for key, value := range *qb.baseBuilder.parsedProperties {
endpoint = strings.Replace(endpoint, "<"+key+">", utils.URLQueryEscape(value), -1)
requestURI = strings.Replace(requestURI, "<"+key+">", utils.URLQueryEscape(value), -1)
}
requestURI = regexp.MustCompile(`/+`).ReplaceAllString(requestURI, "/")
requestURL, err := url.Parse(endpoint + requestURI)
if err != nil {
return err
}
if qb.baseBuilder.parsedQuery != nil {
queryValue := requestURL.Query()
for key, value := range *qb.baseBuilder.parsedQuery {
queryValue.Set(key, value)
}
requestURL.RawQuery = queryValue.Encode()
}
qb.baseBuilder.parsedURL = requestURL.String()
return nil
}
func (qb *QingStorBuilder) setupHeaders(httpRequest *http.Request) error {
method := httpRequest.Method
if method == "POST" || method == "PUT" || method == "DELETE" {
if httpRequest.Header.Get("Content-Type") == "" {
mimeType := mime.TypeByExtension(path.Ext(httpRequest.URL.Path))
if mimeType != "" {
httpRequest.Header.Set("Content-Type", mimeType)
}
}
}
if httpRequest.Header.Get("User-Agent") == "" {
version := fmt.Sprintf(`Go v%s`, strings.Replace(runtime.Version(), "go", "", -1))
system := fmt.Sprintf(`%s_%s_%s`, runtime.GOOS, runtime.GOARCH, runtime.Compiler)
ua := fmt.Sprintf(`qingstor-sdk-go/%s (%s; %s)`, sdk.Version, version, system)
if qb.baseBuilder.operation.Config.AdditionalUserAgent != "" {
ua = fmt.Sprintf(`%s %s`, ua, qb.baseBuilder.operation.Config.AdditionalUserAgent)
}
httpRequest.Header.Set("User-Agent", ua)
}
if s := httpRequest.Header.Get("X-QS-Fetch-Source"); s != "" {
u, err := url.Parse(s)
if err != nil {
return fmt.Errorf("invalid HTTP header value: %s", s)
}
httpRequest.Header.Set("X-QS-Fetch-Source", u.String())
}
if qb.baseBuilder.operation.APIName == "Delete Multiple Objects" {
buffer := &bytes.Buffer{}
buffer.ReadFrom(httpRequest.Body)
httpRequest.Body = ioutil.NopCloser(bytes.NewReader(buffer.Bytes()))
md5Value := md5.Sum(buffer.Bytes())
httpRequest.Header.Set("Content-MD5", base64.StdEncoding.EncodeToString(md5Value[:]))
}
return nil
}

View File

@@ -0,0 +1,22 @@
// +-------------------------------------------------------------------------
// | Copyright (C) 2016 Yunify, Inc.
// +-------------------------------------------------------------------------
// | Licensed under the Apache License, Version 2.0 (the "License");
// | you may not use this work except in compliance with the License.
// | You may obtain a copy of the License in the LICENSE file, or at:
// |
// | http://www.apache.org/licenses/LICENSE-2.0
// |
// | Unless required by applicable law or agreed to in writing, software
// | distributed under the License is distributed on an "AS IS" BASIS,
// | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// | See the License for the specific language governing permissions and
// | limitations under the License.
// +-------------------------------------------------------------------------
package data
// Input defines the interfaces that input should implement.
type Input interface {
Validation
}

View File

@@ -0,0 +1,35 @@
// +-------------------------------------------------------------------------
// | Copyright (C) 2016 Yunify, Inc.
// +-------------------------------------------------------------------------
// | Licensed under the Apache License, Version 2.0 (the "License");
// | you may not use this work except in compliance with the License.
// | You may obtain a copy of the License in the LICENSE file, or at:
// |
// | http://www.apache.org/licenses/LICENSE-2.0
// |
// | Unless required by applicable law or agreed to in writing, software
// | distributed under the License is distributed on an "AS IS" BASIS,
// | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// | See the License for the specific language governing permissions and
// | limitations under the License.
// +-------------------------------------------------------------------------
package data
import (
"github.com/yunify/qingstor-sdk-go/config"
)
// Operation stores information of an operation.
type Operation struct {
Config *config.Config
Properties interface{}
APIName string
ServiceName string
RequestMethod string
RequestURI string
StatusCodes []int
}

View File

@@ -0,0 +1,22 @@
// +-------------------------------------------------------------------------
// | Copyright (C) 2016 Yunify, Inc.
// +-------------------------------------------------------------------------
// | Licensed under the Apache License, Version 2.0 (the "License");
// | you may not use this work except in compliance with the License.
// | You may obtain a copy of the License in the LICENSE file, or at:
// |
// | http://www.apache.org/licenses/LICENSE-2.0
// |
// | Unless required by applicable law or agreed to in writing, software
// | distributed under the License is distributed on an "AS IS" BASIS,
// | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// | See the License for the specific language governing permissions and
// | limitations under the License.
// +-------------------------------------------------------------------------
package data
// Validation defines the validate interface.
type Validation interface {
Validate() error
}

View File

@@ -0,0 +1,53 @@
// +-------------------------------------------------------------------------
// | Copyright (C) 2016 Yunify, Inc.
// +-------------------------------------------------------------------------
// | Licensed under the Apache License, Version 2.0 (the "License");
// | you may not use this work except in compliance with the License.
// | You may obtain a copy of the License in the LICENSE file, or at:
// |
// | http://www.apache.org/licenses/LICENSE-2.0
// |
// | Unless required by applicable law or agreed to in writing, software
// | distributed under the License is distributed on an "AS IS" BASIS,
// | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// | See the License for the specific language governing permissions and
// | limitations under the License.
// +-------------------------------------------------------------------------
package errors
import (
"fmt"
"strings"
)
// ParameterRequiredError indicates that the required parameter is missing.
type ParameterRequiredError struct {
ParameterName string
ParentName string
}
// Error returns the description of ParameterRequiredError.
func (e ParameterRequiredError) Error() string {
return fmt.Sprintf(`"%s" is required in "%s"`, e.ParameterName, e.ParentName)
}
// ParameterValueNotAllowedError indicates that the parameter value is not allowed.
type ParameterValueNotAllowedError struct {
ParameterName string
ParameterValue string
AllowedValues []string
}
// Error returns the description of ParameterValueNotAllowedError.
func (e ParameterValueNotAllowedError) Error() string {
allowedValues := []string{}
for _, value := range e.AllowedValues {
allowedValues = append(allowedValues, "\""+value+"\"")
}
return fmt.Sprintf(
`"%s" value "%s" is not allowed, should be one of %s`,
e.ParameterName,
e.ParameterValue,
strings.Join(allowedValues, ", "))
}

View File

@@ -0,0 +1,36 @@
// +-------------------------------------------------------------------------
// | Copyright (C) 2016 Yunify, Inc.
// +-------------------------------------------------------------------------
// | Licensed under the Apache License, Version 2.0 (the "License");
// | you may not use this work except in compliance with the License.
// | You may obtain a copy of the License in the LICENSE file, or at:
// |
// | http://www.apache.org/licenses/LICENSE-2.0
// |
// | Unless required by applicable law or agreed to in writing, software
// | distributed under the License is distributed on an "AS IS" BASIS,
// | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// | See the License for the specific language governing permissions and
// | limitations under the License.
// +-------------------------------------------------------------------------
package errors
import "fmt"
// QingStorError stores information of an QingStor error response.
type QingStorError struct {
StatusCode int
Code string `json:"code"`
Message string `json:"message"`
RequestID string `json:"request_id"`
ReferenceURL string `json:"url"`
}
// Error returns the description of QingStor error response.
func (qse QingStorError) Error() string {
return fmt.Sprintf(
"QingStor Error: StatusCode \"%d\", Code \"%s\", Message \"%s\", Request ID \"%s\", Reference URL \"%s\"",
qse.StatusCode, qse.Code, qse.Message, qse.RequestID, qse.ReferenceURL)
}

View File

@@ -0,0 +1,253 @@
// +-------------------------------------------------------------------------
// | Copyright (C) 2016 Yunify, Inc.
// +-------------------------------------------------------------------------
// | Licensed under the Apache License, Version 2.0 (the "License");
// | you may not use this work except in compliance with the License.
// | You may obtain a copy of the License in the LICENSE file, or at:
// |
// | http://www.apache.org/licenses/LICENSE-2.0
// |
// | Unless required by applicable law or agreed to in writing, software
// | distributed under the License is distributed on an "AS IS" BASIS,
// | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// | See the License for the specific language governing permissions and
// | limitations under the License.
// +-------------------------------------------------------------------------
package request
import (
"errors"
"fmt"
"net/http"
"reflect"
"strconv"
"time"
"github.com/pengsrc/go-shared/convert"
"github.com/yunify/qingstor-sdk-go/logger"
"github.com/yunify/qingstor-sdk-go/request/builder"
"github.com/yunify/qingstor-sdk-go/request/data"
"github.com/yunify/qingstor-sdk-go/request/signer"
"github.com/yunify/qingstor-sdk-go/request/unpacker"
)
// A Request can build, sign, send and unpack API request.
type Request struct {
Operation *data.Operation
Input *reflect.Value
Output *reflect.Value
HTTPRequest *http.Request
HTTPResponse *http.Response
}
// New create a Request from given Operation, Input and Output.
// It returns a Request.
func New(o *data.Operation, i data.Input, x interface{}) (*Request, error) {
input := reflect.ValueOf(i)
if input.IsValid() && input.Elem().IsValid() {
err := i.Validate()
if err != nil {
return nil, err
}
}
output := reflect.ValueOf(x)
return &Request{
Operation: o,
Input: &input,
Output: &output,
}, nil
}
// Send sends API request.
// It returns error if error occurred.
func (r *Request) Send() error {
err := r.Build()
if err != nil {
return err
}
err = r.Sign()
if err != nil {
return err
}
err = r.Do()
if err != nil {
return err
}
return nil
}
// Build checks and builds the API request.
// It returns error if error occurred.
func (r *Request) Build() error {
err := r.check()
if err != nil {
return err
}
err = r.build()
if err != nil {
return err
}
return nil
}
// Do sends and unpacks the API request.
// It returns error if error occurred.
func (r *Request) Do() error {
err := r.send()
if err != nil {
return err
}
err = r.unpack()
if err != nil {
return err
}
return nil
}
// Sign sign the API request by setting the authorization header.
// It returns error if error occurred.
func (r *Request) Sign() error {
err := r.sign()
if err != nil {
return err
}
return nil
}
// SignQuery sign the API request by appending query string.
// It returns error if error occurred.
func (r *Request) SignQuery(timeoutSeconds int) error {
err := r.signQuery(int(time.Now().Unix()) + timeoutSeconds)
if err != nil {
return err
}
return nil
}
// ApplySignature applies the Authorization header.
// It returns error if error occurred.
func (r *Request) ApplySignature(authorization string) error {
r.HTTPRequest.Header.Set("Authorization", authorization)
return nil
}
// ApplyQuerySignature applies the query signature.
// It returns error if error occurred.
func (r *Request) ApplyQuerySignature(accessKeyID string, expires int, signature string) error {
queryValue := r.HTTPRequest.URL.Query()
queryValue.Set("access_key_id", accessKeyID)
queryValue.Set("expires", strconv.Itoa(expires))
queryValue.Set("signature", signature)
r.HTTPRequest.URL.RawQuery = queryValue.Encode()
return nil
}
func (r *Request) check() error {
if r.Operation.Config.AccessKeyID == "" {
return errors.New("access key not provided")
}
if r.Operation.Config.SecretAccessKey == "" {
return errors.New("secret access key not provided")
}
return nil
}
func (r *Request) build() error {
b := &builder.QingStorBuilder{}
httpRequest, err := b.BuildHTTPRequest(r.Operation, r.Input)
if err != nil {
return err
}
r.HTTPRequest = httpRequest
return nil
}
func (r *Request) sign() error {
s := &signer.QingStorSigner{
AccessKeyID: r.Operation.Config.AccessKeyID,
SecretAccessKey: r.Operation.Config.SecretAccessKey,
}
err := s.WriteSignature(r.HTTPRequest)
if err != nil {
return err
}
return nil
}
func (r *Request) signQuery(expires int) error {
s := &signer.QingStorSigner{
AccessKeyID: r.Operation.Config.AccessKeyID,
SecretAccessKey: r.Operation.Config.SecretAccessKey,
}
err := s.WriteQuerySignature(r.HTTPRequest, expires)
if err != nil {
return err
}
return nil
}
func (r *Request) send() error {
var response *http.Response
var err error
if r.Operation.Config.Connection == nil {
r.Operation.Config.InitHTTPClient()
}
retries := r.Operation.Config.ConnectionRetries + 1
for {
if retries > 0 {
logger.Infof(nil, fmt.Sprintf(
"Sending request: [%d] %s %s",
convert.StringToTimestamp(r.HTTPRequest.Header.Get("Date"), convert.RFC822),
r.Operation.RequestMethod,
r.HTTPRequest.Host,
))
response, err = r.Operation.Config.Connection.Do(r.HTTPRequest)
if err == nil {
retries = 0
} else {
retries--
time.Sleep(time.Second)
}
} else {
break
}
}
if err != nil {
return err
}
r.HTTPResponse = response
return nil
}
func (r *Request) unpack() error {
u := &unpacker.QingStorUnpacker{}
err := u.UnpackHTTPRequest(r.Operation, r.HTTPResponse, r.Output)
if err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,266 @@
// +-------------------------------------------------------------------------
// | Copyright (C) 2016 Yunify, Inc.
// +-------------------------------------------------------------------------
// | Licensed under the Apache License, Version 2.0 (the "License");
// | you may not use this work except in compliance with the License.
// | You may obtain a copy of the License in the LICENSE file, or at:
// |
// | http://www.apache.org/licenses/LICENSE-2.0
// |
// | Unless required by applicable law or agreed to in writing, software
// | distributed under the License is distributed on an "AS IS" BASIS,
// | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// | See the License for the specific language governing permissions and
// | limitations under the License.
// +-------------------------------------------------------------------------
package signer
import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"fmt"
"net/http"
"sort"
"strings"
"github.com/pengsrc/go-shared/convert"
"github.com/yunify/qingstor-sdk-go/logger"
"github.com/yunify/qingstor-sdk-go/utils"
)
// QingStorSigner is the http request signer for QingStor service.
type QingStorSigner struct {
AccessKeyID string
SecretAccessKey string
}
// WriteSignature calculates signature and write it to http request header.
func (qss *QingStorSigner) WriteSignature(request *http.Request) error {
authorization, err := qss.BuildSignature(request)
if err != nil {
return err
}
request.Header.Set("Authorization", authorization)
return nil
}
// WriteQuerySignature calculates signature and write it to http request url.
func (qss *QingStorSigner) WriteQuerySignature(request *http.Request, expires int) error {
query, err := qss.BuildQuerySignature(request, expires)
if err != nil {
return err
}
if request.URL.RawQuery != "" {
query = "?" + request.URL.RawQuery + "&" + query
} else {
query = "?" + query
}
newRequest, err := http.NewRequest(request.Method,
request.URL.Scheme+"://"+request.URL.Host+utils.URLQueryEscape(request.URL.Path)+query, nil)
if err != nil {
return err
}
request.URL = newRequest.URL
return nil
}
// BuildSignature calculates the signature string.
func (qss *QingStorSigner) BuildSignature(request *http.Request) (string, error) {
stringToSign, err := qss.BuildStringToSign(request)
if err != nil {
return "", err
}
h := hmac.New(sha256.New, []byte(qss.SecretAccessKey))
h.Write([]byte(stringToSign))
signature := strings.TrimSpace(base64.StdEncoding.EncodeToString(h.Sum(nil)))
authorization := "QS " + qss.AccessKeyID + ":" + signature
logger.Debugf(nil, fmt.Sprintf(
"QingStor authorization: [%d] %s",
convert.StringToTimestamp(request.Header.Get("Date"), convert.RFC822),
authorization,
))
return authorization, nil
}
// BuildQuerySignature calculates the signature string for query.
func (qss *QingStorSigner) BuildQuerySignature(request *http.Request, expires int) (string, error) {
stringToSign, err := qss.BuildQueryStringToSign(request, expires)
if err != nil {
return "", err
}
h := hmac.New(sha256.New, []byte(qss.SecretAccessKey))
h.Write([]byte(stringToSign))
signature := strings.TrimSpace(base64.StdEncoding.EncodeToString(h.Sum(nil)))
signature = utils.URLQueryEscape(signature)
query := fmt.Sprintf(
"access_key_id=%s&expires=%d&signature=%s",
qss.AccessKeyID, expires, signature,
)
logger.Debugf(nil, fmt.Sprintf(
"QingStor query signature: [%d] %s",
convert.StringToTimestamp(request.Header.Get("Date"), convert.RFC822),
query,
))
return query, nil
}
// BuildStringToSign build the string to sign.
func (qss *QingStorSigner) BuildStringToSign(request *http.Request) (string, error) {
date := request.Header.Get("Date")
if request.Header.Get("X-QS-Date") != "" {
date = ""
}
stringToSign := fmt.Sprintf(
"%s\n%s\n%s\n%s\n",
request.Method,
request.Header.Get("Content-MD5"),
request.Header.Get("Content-Type"),
date,
)
stringToSign += qss.buildCanonicalizedHeaders(request)
canonicalizedResource, err := qss.buildCanonicalizedResource(request)
if err != nil {
return "", err
}
stringToSign += canonicalizedResource
logger.Debugf(nil, fmt.Sprintf(
"QingStor string to sign: [%d] %s",
convert.StringToTimestamp(request.Header.Get("Date"), convert.RFC822),
stringToSign,
))
return stringToSign, nil
}
// BuildQueryStringToSign build the string to sign for query.
func (qss *QingStorSigner) BuildQueryStringToSign(request *http.Request, expires int) (string, error) {
stringToSign := fmt.Sprintf(
"%s\n%s\n%s\n%d\n",
request.Method,
request.Header.Get("Content-MD5"),
request.Header.Get("Content-Type"),
expires,
)
stringToSign += qss.buildCanonicalizedHeaders(request)
canonicalizedResource, err := qss.buildCanonicalizedResource(request)
if err != nil {
return "", err
}
stringToSign += canonicalizedResource
logger.Debugf(nil, fmt.Sprintf(
"QingStor query string to sign: [%d] %s",
convert.StringToTimestamp(request.Header.Get("Date"), convert.RFC822),
stringToSign,
))
return stringToSign, nil
}
func (qss *QingStorSigner) buildCanonicalizedHeaders(request *http.Request) string {
keys := []string{}
for key := range request.Header {
if strings.HasPrefix(strings.ToLower(key), "x-qs-") {
keys = append(keys, strings.TrimSpace(strings.ToLower(key)))
}
}
sort.Strings(keys)
canonicalizedHeaders := ""
for _, key := range keys {
canonicalizedHeaders += key + ":" + strings.TrimSpace(request.Header.Get(key)) + "\n"
}
return canonicalizedHeaders
}
func (qss *QingStorSigner) buildCanonicalizedResource(request *http.Request) (string, error) {
path := utils.URLQueryEscape(request.URL.Path)
query := request.URL.Query()
keys := []string{}
for key := range query {
keys = append(keys, key)
}
sort.Strings(keys)
parts := []string{}
for _, key := range keys {
values := query[key]
if qss.queryToSign(key) {
if len(values) > 0 {
if values[0] != "" {
value := strings.TrimSpace(strings.Join(values, ""))
value, err := utils.URLQueryUnescape(value)
if err != nil {
return "", err
}
parts = append(parts, key+"="+value)
} else {
parts = append(parts, key)
}
} else {
parts = append(parts, key)
}
}
}
joinedParts := strings.Join(parts, "&")
if joinedParts != "" {
path = path + "?" + joinedParts
}
logger.Debugf(nil, fmt.Sprintf(
"QingStor canonicalized resource: [%d] %s",
convert.StringToTimestamp(request.Header.Get("Date"), convert.RFC822),
path,
))
return path, nil
}
func (qss *QingStorSigner) queryToSign(key string) bool {
keysMap := map[string]bool{
"acl": true,
"cors": true,
"delete": true,
"mirror": true,
"part_number": true,
"policy": true,
"stats": true,
"upload_id": true,
"uploads": true,
"image": true,
"lifecycle": true,
"logging": true,
"response-expires": true,
"response-cache-control": true,
"response-content-type": true,
"response-content-language": true,
"response-content-encoding": true,
"response-content-disposition": true,
}
return keysMap[key]
}

View File

@@ -0,0 +1,220 @@
// +-------------------------------------------------------------------------
// | Copyright (C) 2016 Yunify, Inc.
// +-------------------------------------------------------------------------
// | Licensed under the Apache License, Version 2.0 (the "License");
// | you may not use this work except in compliance with the License.
// | You may obtain a copy of the License in the LICENSE file, or at:
// |
// | http://www.apache.org/licenses/LICENSE-2.0
// |
// | Unless required by applicable law or agreed to in writing, software
// | distributed under the License is distributed on an "AS IS" BASIS,
// | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// | See the License for the specific language governing permissions and
// | limitations under the License.
// +-------------------------------------------------------------------------
package unpacker
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"reflect"
"strconv"
"time"
"github.com/pengsrc/go-shared/convert"
"github.com/yunify/qingstor-sdk-go/logger"
"github.com/yunify/qingstor-sdk-go/request/data"
)
// BaseUnpacker is the base unpacker for all services.
type BaseUnpacker struct {
operation *data.Operation
httpResponse *http.Response
output *reflect.Value
}
// UnpackHTTPRequest unpacks http response with an operation and an output.
func (b *BaseUnpacker) UnpackHTTPRequest(o *data.Operation, r *http.Response, x *reflect.Value) error {
b.operation = o
b.httpResponse = r
b.output = x
err := b.exposeStatusCode()
if err != nil {
return err
}
err = b.parseResponseHeaders()
if err != nil {
return err
}
err = b.parseResponseBody()
if err != nil {
return err
}
err = b.parseResponseElements()
if err != nil {
return err
}
return nil
}
func (b *BaseUnpacker) exposeStatusCode() error {
value := b.output.Elem().FieldByName("StatusCode")
if value.IsValid() {
switch value.Interface().(type) {
case *int:
logger.Infof(nil, fmt.Sprintf(
"QingStor response status code: [%d] %d",
convert.StringToTimestamp(b.httpResponse.Header.Get("Date"), convert.RFC822),
b.httpResponse.StatusCode,
))
value.Set(reflect.ValueOf(&b.httpResponse.StatusCode))
}
}
return nil
}
func (b *BaseUnpacker) parseResponseHeaders() error {
logger.Infof(nil, fmt.Sprintf(
"QingStor response headers: [%d] %s",
convert.StringToTimestamp(b.httpResponse.Header.Get("Date"), convert.RFC822),
fmt.Sprint(b.httpResponse.Header),
))
if b.isResponseRight() {
fields := b.output.Elem()
for i := 0; i < fields.NumField(); i++ {
field := fields.Field(i)
fieldTagName := fields.Type().Field(i).Tag.Get("name")
fieldTagLocation := fields.Type().Field(i).Tag.Get("location")
fieldStringValue := b.httpResponse.Header.Get(fieldTagName)
// Empty value should be ignored.
if fieldStringValue == "" {
continue
}
if fieldTagName != "" && fieldTagLocation == "headers" {
switch field.Interface().(type) {
case *string:
field.Set(reflect.ValueOf(&fieldStringValue))
case *int:
intValue, err := strconv.Atoi(fieldStringValue)
if err != nil {
return err
}
field.Set(reflect.ValueOf(&intValue))
case *int64:
int64Value, err := strconv.ParseInt(fieldStringValue, 10, 64)
if err != nil {
return err
}
field.Set(reflect.ValueOf(&int64Value))
case *bool:
case *time.Time:
formatString := fields.Type().Field(i).Tag.Get("format")
format := ""
switch formatString {
case "RFC 822":
format = convert.RFC822
case "ISO 8601":
format = convert.ISO8601
}
timeValue, err := convert.StringToTime(fieldStringValue, format)
if err != nil {
return err
}
field.Set(reflect.ValueOf(&timeValue))
}
}
}
}
return nil
}
func (b *BaseUnpacker) parseResponseBody() error {
if b.isResponseRight() {
value := b.output.Elem().FieldByName("Body")
if value.IsValid() {
switch value.Type().String() {
case "string":
buffer := &bytes.Buffer{}
buffer.ReadFrom(b.httpResponse.Body)
b.httpResponse.Body.Close()
logger.Infof(nil, fmt.Sprintf(
"QingStor response body string: [%d] %s",
convert.StringToTimestamp(b.httpResponse.Header.Get("Date"), convert.RFC822),
string(buffer.Bytes()),
))
value.SetString(string(buffer.Bytes()))
case "io.ReadCloser":
value.Set(reflect.ValueOf(b.httpResponse.Body))
}
}
}
return nil
}
func (b *BaseUnpacker) parseResponseElements() error {
if !b.isResponseRight() {
return nil
}
// Do not parse GetObject and ImageProcess's body.
if b.operation.APIName == "GET Object" ||
b.operation.APIName == "Image Process" {
return nil
}
if b.httpResponse.Header.Get("Content-Type") != "application/json" {
return nil
}
buffer := &bytes.Buffer{}
buffer.ReadFrom(b.httpResponse.Body)
b.httpResponse.Body.Close()
if buffer.Len() == 0 {
return nil
}
logger.Infof(nil, fmt.Sprintf(
"QingStor response body string: [%d] %s",
convert.StringToTimestamp(b.httpResponse.Header.Get("Date"), convert.RFC822),
string(buffer.Bytes()),
))
err := json.Unmarshal(buffer.Bytes(), b.output.Interface())
if err != nil {
return err
}
return nil
}
func (b *BaseUnpacker) isResponseRight() bool {
rightStatusCodes := b.operation.StatusCodes
if len(rightStatusCodes) == 0 {
rightStatusCodes = append(rightStatusCodes, 200)
}
flag := false
for _, statusCode := range rightStatusCodes {
if statusCode == b.httpResponse.StatusCode {
flag = true
}
}
return flag
}

View File

@@ -0,0 +1,84 @@
// +-------------------------------------------------------------------------
// | Copyright (C) 2016 Yunify, Inc.
// +-------------------------------------------------------------------------
// | Licensed under the Apache License, Version 2.0 (the "License");
// | you may not use this work except in compliance with the License.
// | You may obtain a copy of the License in the LICENSE file, or at:
// |
// | http://www.apache.org/licenses/LICENSE-2.0
// |
// | Unless required by applicable law or agreed to in writing, software
// | distributed under the License is distributed on an "AS IS" BASIS,
// | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// | See the License for the specific language governing permissions and
// | limitations under the License.
// +-------------------------------------------------------------------------
package unpacker
import (
"bytes"
"encoding/json"
"net/http"
"reflect"
"github.com/yunify/qingstor-sdk-go/request/data"
"github.com/yunify/qingstor-sdk-go/request/errors"
)
// QingStorUnpacker is the response unpacker for QingStor service.
type QingStorUnpacker struct {
baseUnpacker *BaseUnpacker
}
// UnpackHTTPRequest unpack the http response with an operation, http response and an output.
func (qu *QingStorUnpacker) UnpackHTTPRequest(o *data.Operation, r *http.Response, x *reflect.Value) error {
qu.baseUnpacker = &BaseUnpacker{}
err := qu.baseUnpacker.UnpackHTTPRequest(o, r, x)
if err != nil {
return err
}
err = qu.parseError()
if err != nil {
return err
}
// Close body for every API except GetObject and ImageProcess.
if o.APIName != "GET Object" && o.APIName != "Image Process" && r.Body != nil {
err = r.Body.Close()
if err != nil {
return err
}
r.Body = nil
}
return nil
}
func (qu *QingStorUnpacker) parseError() error {
if !qu.baseUnpacker.isResponseRight() {
if qu.baseUnpacker.httpResponse.Header.Get("Content-Type") == "application/json" {
buffer := &bytes.Buffer{}
buffer.ReadFrom(qu.baseUnpacker.httpResponse.Body)
qu.baseUnpacker.httpResponse.Body.Close()
qsError := &errors.QingStorError{}
if buffer.Len() > 0 {
err := json.Unmarshal(buffer.Bytes(), qsError)
if err != nil {
return err
}
}
qsError.StatusCode = qu.baseUnpacker.httpResponse.StatusCode
if qsError.RequestID == "" {
qsError.RequestID = qu.baseUnpacker.httpResponse.Header.Get(http.CanonicalHeaderKey("X-QS-Request-ID"))
}
return qsError
}
}
return nil
}