// Copyright 2019 Huawei Technologies Co.,Ltd. // Licensed under the Apache License, Version 2.0 (the "License"); you may not use // this file except in compliance with the License. You may obtain a copy of the // License 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 obs import ( "crypto/hmac" "crypto/md5" "crypto/sha1" "crypto/sha256" "encoding/base64" "encoding/hex" "encoding/json" "encoding/xml" "fmt" "net/url" "regexp" "strconv" "strings" "time" ) var regex = regexp.MustCompile("^[\u4e00-\u9fa5]$") var ipRegex = regexp.MustCompile("^((2[0-4]\\d|25[0-5]|[01]?\\d\\d?)\\.){3}(2[0-4]\\d|25[0-5]|[01]?\\d\\d?)$") var v4AuthRegex = regexp.MustCompile("Credential=(.+?),SignedHeaders=(.+?),Signature=.+") var regionRegex = regexp.MustCompile(".+/\\d+/(.+?)/.+") // StringContains replaces subStr in src with subTranscoding and returns the new string func StringContains(src string, subStr string, subTranscoding string) string { return strings.Replace(src, subStr, subTranscoding, -1) } // XmlTranscoding replaces special characters with their escaped form func XmlTranscoding(src string) string { srcTmp := StringContains(src, "&", "&") srcTmp = StringContains(srcTmp, "<", "<") srcTmp = StringContains(srcTmp, ">", ">") srcTmp = StringContains(srcTmp, "'", "'") srcTmp = StringContains(srcTmp, "\"", """) return srcTmp } // StringToInt converts string value to int value with default value func StringToInt(value string, def int) int { ret, err := strconv.Atoi(value) if err != nil { ret = def } return ret } // StringToInt64 converts string value to int64 value with default value func StringToInt64(value string, def int64) int64 { ret, err := strconv.ParseInt(value, 10, 64) if err != nil { ret = def } return ret } // IntToString converts int value to string value func IntToString(value int) string { return strconv.Itoa(value) } // Int64ToString converts int64 value to string value func Int64ToString(value int64) string { return strconv.FormatInt(value, 10) } // GetCurrentTimestamp gets unix time in milliseconds func GetCurrentTimestamp() int64 { return time.Now().UnixNano() / 1000000 } // FormatUtcNow gets a textual representation of the UTC format time value func FormatUtcNow(format string) string { return time.Now().UTC().Format(format) } // FormatUtcToRfc1123 gets a textual representation of the RFC1123 format time value func FormatUtcToRfc1123(t time.Time) string { ret := t.UTC().Format(time.RFC1123) return ret[:strings.LastIndex(ret, "UTC")] + "GMT" } // Md5 gets the md5 value of input func Md5(value []byte) []byte { m := md5.New() _, err := m.Write(value) if err != nil { doLog(LEVEL_WARN, "MD5 failed to write") } return m.Sum(nil) } // HmacSha1 gets hmac sha1 value of input func HmacSha1(key, value []byte) []byte { mac := hmac.New(sha1.New, key) _, err := mac.Write(value) if err != nil { doLog(LEVEL_WARN, "HmacSha1 failed to write") } return mac.Sum(nil) } // HmacSha256 get hmac sha256 value if input func HmacSha256(key, value []byte) []byte { mac := hmac.New(sha256.New, key) _, err := mac.Write(value) if err != nil { doLog(LEVEL_WARN, "HmacSha256 failed to write") } return mac.Sum(nil) } // Base64Encode wrapper of base64.StdEncoding.EncodeToString func Base64Encode(value []byte) string { return base64.StdEncoding.EncodeToString(value) } // Base64Decode wrapper of base64.StdEncoding.DecodeString func Base64Decode(value string) ([]byte, error) { return base64.StdEncoding.DecodeString(value) } // HexMd5 returns the md5 value of input in hexadecimal format func HexMd5(value []byte) string { return Hex(Md5(value)) } // Base64Md5 returns the md5 value of input with Base64Encode func Base64Md5(value []byte) string { return Base64Encode(Md5(value)) } // Sha256Hash returns sha256 checksum func Sha256Hash(value []byte) []byte { hash := sha256.New() _, err := hash.Write(value) if err != nil { doLog(LEVEL_WARN, "Sha256Hash failed to write") } return hash.Sum(nil) } // ParseXml wrapper of xml.Unmarshal func ParseXml(value []byte, result interface{}) error { if len(value) == 0 { return nil } return xml.Unmarshal(value, result) } // parseJSON wrapper of json.Unmarshal func parseJSON(value []byte, result interface{}) error { if len(value) == 0 { return nil } return json.Unmarshal(value, result) } // TransToXml wrapper of xml.Marshal func TransToXml(value interface{}) ([]byte, error) { if value == nil { return []byte{}, nil } return xml.Marshal(value) } // Hex wrapper of hex.EncodeToString func Hex(value []byte) string { return hex.EncodeToString(value) } // HexSha256 returns the Sha256Hash value of input in hexadecimal format func HexSha256(value []byte) string { return Hex(Sha256Hash(value)) } // UrlDecode wrapper of url.QueryUnescape func UrlDecode(value string) (string, error) { ret, err := url.QueryUnescape(value) if err == nil { return ret, nil } return "", err } // UrlDecodeWithoutError wrapper of UrlDecode func UrlDecodeWithoutError(value string) string { ret, err := UrlDecode(value) if err == nil { return ret } if isErrorLogEnabled() { doLog(LEVEL_ERROR, "Url decode error") } return "" } // IsIP checks whether the value matches ip address func IsIP(value string) bool { return ipRegex.MatchString(value) } // UrlEncode encodes the input value func UrlEncode(value string, chineseOnly bool) string { if chineseOnly { values := make([]string, 0, len(value)) for _, val := range value { _value := string(val) if regex.MatchString(_value) { _value = url.QueryEscape(_value) } values = append(values, _value) } return strings.Join(values, "") } return url.QueryEscape(value) } func copyHeaders(m map[string][]string) (ret map[string][]string) { if m != nil { ret = make(map[string][]string, len(m)) for key, values := range m { _values := make([]string, 0, len(values)) for _, value := range values { _values = append(_values, value) } ret[strings.ToLower(key)] = _values } } else { ret = make(map[string][]string) } return } func parseHeaders(headers map[string][]string) (signature string, region string, signedHeaders string) { signature = "v2" if receviedAuthorization, ok := headers[strings.ToLower(HEADER_AUTH_CAMEL)]; ok && len(receviedAuthorization) > 0 { if strings.HasPrefix(receviedAuthorization[0], V4_HASH_PREFIX) { signature = "v4" matches := v4AuthRegex.FindStringSubmatch(receviedAuthorization[0]) if len(matches) >= 3 { region = matches[1] regions := regionRegex.FindStringSubmatch(region) if len(regions) >= 2 { region = regions[1] } signedHeaders = matches[2] } } else if strings.HasPrefix(receviedAuthorization[0], V2_HASH_PREFIX) { signature = "v2" } } return } func getTemporaryKeys() []string { return []string{ "Signature", "signature", "X-Amz-Signature", "x-amz-signature", } } func getIsObs(isTemporary bool, querys []string, headers map[string][]string) bool { isObs := true if isTemporary { for _, value := range querys { keyPrefix := strings.ToLower(value) if strings.HasPrefix(keyPrefix, HEADER_PREFIX) { isObs = false } else if strings.HasPrefix(value, HEADER_ACCESSS_KEY_AMZ) { isObs = false } } } else { for key := range headers { keyPrefix := strings.ToLower(key) if strings.HasPrefix(keyPrefix, HEADER_PREFIX) { isObs = false break } } } return isObs } func isPathStyle(headers map[string][]string, bucketName string) bool { if receviedHost, ok := headers[HEADER_HOST]; ok && len(receviedHost) > 0 && !strings.HasPrefix(receviedHost[0], bucketName+".") { return true } return false } // GetV2Authorization v2 Authorization func GetV2Authorization(ak, sk, method, bucketName, objectKey, queryURL string, headers map[string][]string) (ret map[string]string) { if strings.HasPrefix(queryURL, "?") { queryURL = queryURL[1:] } method = strings.ToUpper(method) querys := strings.Split(queryURL, "&") querysResult := make([]string, 0) for _, value := range querys { if value != "=" && len(value) != 0 { querysResult = append(querysResult, value) } } params := make(map[string]string) for _, value := range querysResult { kv := strings.Split(value, "=") length := len(kv) if length == 1 { key := UrlDecodeWithoutError(kv[0]) params[key] = "" } else if length >= 2 { key := UrlDecodeWithoutError(kv[0]) vals := make([]string, 0, length-1) for i := 1; i < length; i++ { val := UrlDecodeWithoutError(kv[i]) vals = append(vals, val) } params[key] = strings.Join(vals, "=") } } headers = copyHeaders(headers) pathStyle := isPathStyle(headers, bucketName) conf := &config{securityProviders: []securityProvider{NewBasicSecurityProvider(ak, sk, "")}, urlHolder: &urlHolder{scheme: "https", host: "dummy", port: 443}, pathStyle: pathStyle} conf.signature = SignatureObs _, canonicalizedURL := conf.formatUrls(bucketName, objectKey, params, false) ret = v2Auth(ak, sk, method, canonicalizedURL, headers, true) v2HashPrefix := OBS_HASH_PREFIX ret[HEADER_AUTH_CAMEL] = fmt.Sprintf("%s %s:%s", v2HashPrefix, ak, ret["Signature"]) return } func getQuerysResult(querys []string) []string { querysResult := make([]string, 0) for _, value := range querys { if value != "=" && len(value) != 0 { querysResult = append(querysResult, value) } } return querysResult } func getParams(querysResult []string) map[string]string { params := make(map[string]string) for _, value := range querysResult { kv := strings.Split(value, "=") length := len(kv) if length == 1 { key := UrlDecodeWithoutError(kv[0]) params[key] = "" } else if length >= 2 { key := UrlDecodeWithoutError(kv[0]) vals := make([]string, 0, length-1) for i := 1; i < length; i++ { val := UrlDecodeWithoutError(kv[i]) vals = append(vals, val) } params[key] = strings.Join(vals, "=") } } return params } func getTemporaryAndSignature(params map[string]string) (bool, string) { isTemporary := false signature := "v2" temporaryKeys := getTemporaryKeys() for _, key := range temporaryKeys { if _, ok := params[key]; ok { isTemporary = true if strings.ToLower(key) == "signature" { signature = "v2" } else if strings.ToLower(key) == "x-amz-signature" { signature = "v4" } break } } return isTemporary, signature } // GetAuthorization Authorization func GetAuthorization(ak, sk, method, bucketName, objectKey, queryURL string, headers map[string][]string) (ret map[string]string) { if strings.HasPrefix(queryURL, "?") { queryURL = queryURL[1:] } method = strings.ToUpper(method) querys := strings.Split(queryURL, "&") querysResult := getQuerysResult(querys) params := getParams(querysResult) isTemporary, signature := getTemporaryAndSignature(params) isObs := getIsObs(isTemporary, querysResult, headers) headers = copyHeaders(headers) pathStyle := false if receviedHost, ok := headers[HEADER_HOST]; ok && len(receviedHost) > 0 && !strings.HasPrefix(receviedHost[0], bucketName+".") { pathStyle = true } conf := &config{securityProviders: []securityProvider{NewBasicSecurityProvider(ak, sk, "")}, urlHolder: &urlHolder{scheme: "https", host: "dummy", port: 443}, pathStyle: pathStyle} if isTemporary { return getTemporaryAuthorization(ak, sk, method, bucketName, objectKey, signature, conf, params, headers, isObs) } signature, region, signedHeaders := parseHeaders(headers) if signature == "v4" { conf.signature = SignatureV4 requestURL, canonicalizedURL := conf.formatUrls(bucketName, objectKey, params, false) parsedRequestURL, _err := url.Parse(requestURL) if _err != nil { doLog(LEVEL_WARN, "Failed to parse requestURL") return nil } headerKeys := strings.Split(signedHeaders, ";") _headers := make(map[string][]string, len(headerKeys)) for _, headerKey := range headerKeys { _headers[headerKey] = headers[headerKey] } ret = v4Auth(ak, sk, region, method, canonicalizedURL, parsedRequestURL.RawQuery, _headers) ret[HEADER_AUTH_CAMEL] = fmt.Sprintf("%s Credential=%s,SignedHeaders=%s,Signature=%s", V4_HASH_PREFIX, ret["Credential"], ret["SignedHeaders"], ret["Signature"]) } else if signature == "v2" { if isObs { conf.signature = SignatureObs } else { conf.signature = SignatureV2 } _, canonicalizedURL := conf.formatUrls(bucketName, objectKey, params, false) ret = v2Auth(ak, sk, method, canonicalizedURL, headers, isObs) v2HashPrefix := V2_HASH_PREFIX if isObs { v2HashPrefix = OBS_HASH_PREFIX } ret[HEADER_AUTH_CAMEL] = fmt.Sprintf("%s %s:%s", v2HashPrefix, ak, ret["Signature"]) } return } func getTemporaryAuthorization(ak, sk, method, bucketName, objectKey, signature string, conf *config, params map[string]string, headers map[string][]string, isObs bool) (ret map[string]string) { if signature == "v4" { conf.signature = SignatureV4 longDate, ok := params[PARAM_DATE_AMZ_CAMEL] if !ok { longDate = params[HEADER_DATE_AMZ] } shortDate := longDate[:8] credential, ok := params[PARAM_CREDENTIAL_AMZ_CAMEL] if !ok { credential = params[strings.ToLower(PARAM_CREDENTIAL_AMZ_CAMEL)] } _credential := UrlDecodeWithoutError(credential) regions := regionRegex.FindStringSubmatch(_credential) var region string if len(regions) >= 2 { region = regions[1] } _, scope := getCredential(ak, region, shortDate) expires, ok := params[PARAM_EXPIRES_AMZ_CAMEL] if !ok { expires = params[strings.ToLower(PARAM_EXPIRES_AMZ_CAMEL)] } signedHeaders, ok := params[PARAM_SIGNEDHEADERS_AMZ_CAMEL] if !ok { signedHeaders = params[strings.ToLower(PARAM_SIGNEDHEADERS_AMZ_CAMEL)] } algorithm, ok := params[PARAM_ALGORITHM_AMZ_CAMEL] if !ok { algorithm = params[strings.ToLower(PARAM_ALGORITHM_AMZ_CAMEL)] } if _, ok := params[PARAM_SIGNATURE_AMZ_CAMEL]; ok { delete(params, PARAM_SIGNATURE_AMZ_CAMEL) } else if _, ok := params[strings.ToLower(PARAM_SIGNATURE_AMZ_CAMEL)]; ok { delete(params, strings.ToLower(PARAM_SIGNATURE_AMZ_CAMEL)) } ret = make(map[string]string, 6) ret[PARAM_ALGORITHM_AMZ_CAMEL] = algorithm ret[PARAM_CREDENTIAL_AMZ_CAMEL] = credential ret[PARAM_DATE_AMZ_CAMEL] = longDate ret[PARAM_EXPIRES_AMZ_CAMEL] = expires ret[PARAM_SIGNEDHEADERS_AMZ_CAMEL] = signedHeaders requestURL, canonicalizedURL := conf.formatUrls(bucketName, objectKey, params, false) parsedRequestURL, _err := url.Parse(requestURL) if _err != nil { doLog(LEVEL_WARN, "Failed to parse requestUrl") return nil } stringToSign := getV4StringToSign(method, canonicalizedURL, parsedRequestURL.RawQuery, scope, longDate, UNSIGNED_PAYLOAD, strings.Split(signedHeaders, ";"), headers) ret[PARAM_SIGNATURE_AMZ_CAMEL] = UrlEncode(getSignature(stringToSign, sk, region, shortDate), false) } else if signature == "v2" { if isObs { conf.signature = SignatureObs } else { conf.signature = SignatureV2 } _, canonicalizedURL := conf.formatUrls(bucketName, objectKey, params, false) expires, ok := params["Expires"] if !ok { expires = params["expires"] } headers[HEADER_DATE_CAMEL] = []string{expires} stringToSign := getV2StringToSign(method, canonicalizedURL, headers, isObs) ret = make(map[string]string, 3) ret["Signature"] = UrlEncode(Base64Encode(HmacSha1([]byte(sk), []byte(stringToSign))), false) ret["AWSAccessKeyId"] = UrlEncode(ak, false) ret["Expires"] = UrlEncode(expires, false) } return }