| # Licensed to the Apache Software Foundation (ASF) under one or more |
| # contributor license agreements. See the NOTICE file distributed with |
| # this work for additional information regarding copyright ownership. |
| # The ASF licenses this file to You 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. |
| |
| import sys |
| import hmac |
| import time |
| import uuid |
| import base64 |
| import hashlib |
| |
| from libcloud.utils.py3 import ET, b, u, urlquote |
| from libcloud.utils.xml import findtext |
| from libcloud.common.base import XmlResponse, ConnectionUserAndKey |
| from libcloud.common.types import MalformedResponseError |
| |
| __all__ = [ |
| "AliyunXmlResponse", |
| "AliyunRequestSigner", |
| "AliyunRequestSignerAlgorithmV1_0", |
| "SignedAliyunConnection", |
| "AliyunConnection", |
| "SIGNATURE_VERSION_1_0", |
| "DEFAULT_SIGNATURE_VERSION", |
| ] |
| |
| SIGNATURE_VERSION_1_0 = "1.0" |
| DEFAULT_SIGNATURE_VERSION = SIGNATURE_VERSION_1_0 |
| |
| |
| class AliyunXmlResponse(XmlResponse): |
| namespace = None |
| |
| def success(self): |
| return 200 <= self.status < 300 |
| |
| def parse_body(self): |
| """ |
| Each response from Aliyun contains a request id and a host id. |
| The response body is in utf-8 encoding. |
| """ |
| if len(self.body) == 0 and not self.parse_zero_length_body: |
| return self.body |
| |
| try: |
| parser = ET.XMLParser(encoding="utf-8") |
| body = ET.XML(self.body.encode("utf-8"), parser=parser) |
| except Exception: |
| raise MalformedResponseError( |
| "Failed to parse XML", body=self.body, driver=self.connection.driver |
| ) |
| self.request_id = findtext(element=body, xpath="RequestId", namespace=self.namespace) |
| self.host_id = findtext(element=body, xpath="HostId", namespace=self.namespace) |
| return body |
| |
| def parse_error(self): |
| """ |
| Parse error responses from Aliyun. |
| """ |
| body = super().parse_error() |
| code, message = self._parse_error_details(element=body) |
| request_id = findtext(element=body, xpath="RequestId", namespace=self.namespace) |
| host_id = findtext(element=body, xpath="HostId", namespace=self.namespace) |
| error = { |
| "code": code, |
| "message": message, |
| "request_id": request_id, |
| "host_id": host_id, |
| } |
| return u(error) |
| |
| def _parse_error_details(self, element): |
| """ |
| Parse error code and message from the provided error element. |
| |
| :return: ``tuple`` with two elements: (code, message) |
| :rtype: ``tuple`` |
| """ |
| code = findtext(element=element, xpath="Code", namespace=self.namespace) |
| message = findtext(element=element, xpath="Message", namespace=self.namespace) |
| |
| return (code, message) |
| |
| |
| class AliyunRequestSigner: |
| """ |
| Class handles signing the outgoing Aliyun requests. |
| """ |
| |
| def __init__(self, access_key, access_secret, version): |
| """ |
| :param access_key: Access key. |
| :type access_key: ``str`` |
| |
| :param access_secret: Access secret. |
| :type access_secret: ``str`` |
| |
| :param version: API version. |
| :type version: ``str`` |
| """ |
| self.access_key = access_key |
| self.access_secret = access_secret |
| self.version = version |
| |
| def get_request_params(self, params, method="GET", path="/"): |
| return params |
| |
| def get_request_headers(self, params, headers, method="GET", path="/"): |
| return params, headers |
| |
| |
| class AliyunRequestSignerAlgorithmV1_0(AliyunRequestSigner): |
| """Aliyun request signer using signature version 1.0.""" |
| |
| def get_request_params(self, params, method="GET", path="/"): |
| params["Format"] = "XML" |
| params["Version"] = self.version |
| params["AccessKeyId"] = self.access_key |
| params["SignatureMethod"] = "HMAC-SHA1" |
| params["SignatureVersion"] = SIGNATURE_VERSION_1_0 |
| params["SignatureNonce"] = _get_signature_nonce() |
| # TODO: Support 'ResourceOwnerAccount' |
| params["Timestamp"] = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()) |
| params["Signature"] = self._sign_request(params, method, path) |
| return params |
| |
| def _sign_request(self, params, method, path): |
| """ |
| Sign Aliyun requests parameters and get the signature. |
| |
| StringToSign = HTTPMethod + '&' + |
| percentEncode('/') + '&' + |
| percentEncode(CanonicalizedQueryString) |
| """ |
| keys = list(params.keys()) |
| keys.sort() |
| pairs = [] |
| for key in keys: |
| pairs.append("{}={}".format(_percent_encode(key), _percent_encode(params[key]))) |
| qs = urlquote("&".join(pairs), safe="-_.~") |
| string_to_sign = "&".join((method, urlquote(path, safe=""), qs)) |
| b64_hmac = base64.b64encode( |
| hmac.new( |
| b(self._get_access_secret()), b(string_to_sign), digestmod=hashlib.sha1 |
| ).digest() |
| ) |
| |
| return b64_hmac.decode("utf8") |
| |
| def _get_access_secret(self): |
| return "%s&" % self.access_secret |
| |
| |
| class AliyunConnection(ConnectionUserAndKey): |
| pass |
| |
| |
| class SignedAliyunConnection(AliyunConnection): |
| |
| api_version = None |
| |
| def __init__( |
| self, |
| user_id, |
| key, |
| secure=True, |
| host=None, |
| port=None, |
| url=None, |
| timeout=None, |
| proxy_url=None, |
| retry_delay=None, |
| backoff=None, |
| api_version=None, |
| signature_version=DEFAULT_SIGNATURE_VERSION, |
| ): |
| super().__init__( |
| user_id=user_id, |
| key=key, |
| secure=secure, |
| host=host, |
| port=port, |
| url=url, |
| timeout=timeout, |
| proxy_url=proxy_url, |
| retry_delay=retry_delay, |
| backoff=backoff, |
| ) |
| |
| self.signature_version = str(signature_version) |
| |
| if self.signature_version == "1.0": |
| signer_cls = AliyunRequestSignerAlgorithmV1_0 |
| else: |
| raise ValueError("Unsupported signature_version: %s" % signature_version) |
| |
| if api_version is not None: |
| self.api_version = str(api_version) |
| else: |
| if self.api_version is None: |
| raise ValueError("Unsupported null api_version") |
| |
| self.signer = signer_cls( |
| access_key=self.user_id, access_secret=self.key, version=self.api_version |
| ) |
| |
| def add_default_params(self, params): |
| params = self.signer.get_request_params(params=params, method=self.method, path=self.action) |
| return params |
| |
| |
| def _percent_encode(encode_str): |
| """ |
| Encode string to utf8, quote for url and replace '+' with %20, |
| '*' with %2A and keep '~' not converted. |
| |
| :param src_str: ``str`` in the same encoding with sys.stdin, |
| default to encoding cp936. |
| :return: ``str`` represents the encoded result |
| :rtype: ``str`` |
| """ |
| encoding = sys.stdin.encoding or "cp936" |
| decoded = str(encode_str) |
| |
| if isinstance(encode_str, bytes): |
| decoded = encode_str.decode(encoding) |
| |
| res = urlquote(decoded.encode("utf8"), "") |
| res = res.replace("+", "%20") |
| res = res.replace("*", "%2A") |
| res = res.replace("%7E", "~") |
| return res |
| |
| |
| def _get_signature_nonce(): |
| return str(uuid.uuid4()) |