files: # 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.
  
  from typing import Dict
  from typing import Optional
  from typing import Type
  
  import base64
  from datetime import datetime
  import hashlib
  import hmac
  import time
  from hashlib import sha256
  
  try:
      import simplejson as json
  except ImportError:
      import json  # type: ignore
  
  from libcloud.utils.py3 import ET
  from libcloud.utils.py3 import _real_unicode
  from libcloud.utils.py3 import basestring
  from libcloud.common.base import ConnectionUserAndKey, XmlResponse, BaseDriver
  from libcloud.common.base import JsonResponse
  from libcloud.common.types import InvalidCredsError, MalformedResponseError
  from libcloud.utils.py3 import b, httplib, urlquote
  from libcloud.utils.xml import findtext, findall
  from libcloud.utils.xml import findall_ignore_namespace
  from libcloud.utils.xml import findtext_ignore_namespace
  
  __all__ = [
      "AWSBaseResponse",
      "AWSGenericResponse",
      "AWSTokenConnection",
      "SignedAWSConnection",
      "AWSRequestSignerAlgorithmV2",
      "AWSRequestSignerAlgorithmV4",
      "AWSDriver",
  ]
  
  DEFAULT_SIGNATURE_VERSION = "2"
  UNSIGNED_PAYLOAD = "UNSIGNED-PAYLOAD"
  
  PARAMS_NOT_STRING_ERROR_MSG = """
  "params" dictionary contains an attribute "%s" which value (%s, %s) is not a
  string.
  
  Parameters are sent via query parameters and not via request body and as such,
  all the values need to be of a simple type (string, int, bool).
  
  For arrays and other complex types, you should use notation similar to this
  one:
  
  params['TagSpecification.1.Tag.Value'] = 'foo'
  params['TagSpecification.2.Tag.Value'] = 'bar'
  
  See https://docs.aws.amazon.com/AWSEC2/latest/APIReference/Query-Requests.html
  for details.
  """.strip()
  
  
  class AWSBaseResponse(XmlResponse):
      namespace = None
  
      def _parse_error_details(self, element):
          """
          Parse code and message from the provided error element.
  
          :return: ``tuple`` with two elements: (code, message)
          :rtype: ``tuple``
          """
          code = findtext_ignore_namespace(
              element=element, xpath="Code", namespace=self.namespace
          )
          message = findtext_ignore_namespace(
              element=element, xpath="Message", namespace=self.namespace
          )
  
          return code, message
  
  
  class AWSGenericResponse(AWSBaseResponse):
      # There are multiple error messages in AWS, but they all have an Error node
      # with Code and Message child nodes. Xpath to select them
      # None if the root node *is* the Error node
      xpath = None
  
      # This dict maps <Error><Code>CodeName</Code></Error> to a specific
      # exception class that is raised immediately.
      # If a custom exception class is not defined, errors are accumulated and
      # returned from the parse_error method.
      exceptions = {}  # type: Dict[str, Type[Exception]]
  
      def success(self):
          return self.status in [httplib.OK, httplib.CREATED, httplib.ACCEPTED]
  
      def parse_error(self):
          context = self.connection.context
          status = int(self.status)
  
          # FIXME: Probably ditch this as the forbidden message will have
          # corresponding XML.
          if status == httplib.FORBIDDEN:
              if not self.body:
                  raise InvalidCredsError(str(self.status) + ": " + self.error)
              else:
                  raise InvalidCredsError(self.body)
  
          try:
              body = ET.XML(self.body)
          except Exception:
              raise MalformedResponseError(
                  "Failed to parse XML", body=self.body, driver=self.connection.driver
              )
  
          if self.xpath:
              errs = findall_ignore_namespace(
                  element=body, xpath=self.xpath, namespace=self.namespace
              )
          else:
              errs = [body]
  
          msgs = []
          for err in errs:
              code, message = self._parse_error_details(element=err)
              exceptionCls = self.exceptions.get(code, None)
  
              if exceptionCls is None:
                  msgs.append("%s: %s" % (code, message))
                  continue
  
              # Custom exception class is defined, immediately throw an exception
              params = {}
              if hasattr(exceptionCls, "kwargs"):
                  for key in exceptionCls.kwargs:
                      if key in context:
                          params[key] = context[key]
  
              raise exceptionCls(value=message, driver=self.connection.driver, **params)
  
          return "\n".join(msgs)
  
  
  class AWSTokenConnection(ConnectionUserAndKey):
      def __init__(
          self,
          user_id,
          key,
          secure=True,
          host=None,
          port=None,
          url=None,
          timeout=None,
          proxy_url=None,
          token=None,
          retry_delay=None,
          backoff=None,
      ):
          self.token = token
          super(AWSTokenConnection, self).__init__(
              user_id,
              key,
              secure=secure,
              host=host,
              port=port,
              url=url,
              timeout=timeout,
              retry_delay=retry_delay,
              backoff=backoff,
              proxy_url=proxy_url,
          )
  
      def add_default_params(self, params):
          # Even though we are adding it to the headers, we need it here too
          # so that the token is added to the signature.
          if self.token:
              params["x-amz-security-token"] = self.token
          return super(AWSTokenConnection, self).add_default_params(params)
  
      def add_default_headers(self, headers):
          if self.token:
              headers["x-amz-security-token"] = self.token
          return super(AWSTokenConnection, self).add_default_headers(headers)
  
  
  class AWSRequestSigner(object):
      """
      Class which handles signing the outgoing AWS requests.
      """
  
      def __init__(self, access_key, access_secret, version, connection):
          """
          :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``
  
          :param connection: Connection instance.
          :type connection: :class:`Connection`
          """
          self.access_key = access_key
          self.access_secret = access_secret
          self.version = version
          # TODO: Remove cycling dependency between connection and signer
          self.connection = connection
  
      def get_request_params(self, params, method="GET", path="/"):
          return params
  
      def get_request_headers(self, params, headers, method="GET", path="/", data=None):
          return params, headers
  
  
  class AWSRequestSignerAlgorithmV2(AWSRequestSigner):
      def get_request_params(self, params, method="GET", path="/"):
          params["SignatureVersion"] = "2"
          params["SignatureMethod"] = "HmacSHA256"
          params["AWSAccessKeyId"] = self.access_key
          params["Version"] = self.version
          params["Timestamp"] = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
          params["Signature"] = self._get_aws_auth_param(
              params=params, secret_key=self.access_secret, path=path
          )
          return params
  
      def _get_aws_auth_param(self, params, secret_key, path="/"):
          """
          Creates the signature required for AWS, per
          http://bit.ly/aR7GaQ [docs.amazonwebservices.com]:
  
          StringToSign = HTTPVerb + "\n" +
                         ValueOfHostHeaderInLowercase + "\n" +
                         HTTPRequestURI + "\n" +
                         CanonicalizedQueryString <from the preceding step>
          """
          connection = self.connection
  
          keys = list(params.keys())
          keys.sort()
          pairs = []
          for key in keys:
              value = str(params[key])
              pairs.append(urlquote(key, safe="") + "=" + urlquote(value, safe="-_~"))
  
          qs = "&".join(pairs)
  
          hostname = connection.host
         
  
  [... the rest is omitted ...]
files: libcloud/common/aws.py
files: # 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.
  
  from typing import Dict
  from typing import Optional
  from typing import Type
  
  import base64
  from datetime import datetime
  import hashlib
  import hmac
  import time
  from hashlib import sha256
  
  try:
      import simplejson as json
  except ImportError:
      import json  # type: ignore
  
  from libcloud.utils.py3 import ET
  from libcloud.utils.py3 import _real_unicode
  from libcloud.utils.py3 import basestring
  from libcloud.common.base import ConnectionUserAndKey, XmlResponse, BaseDriver
  from libcloud.common.base import JsonResponse
  from libcloud.common.types import InvalidCredsError, MalformedResponseError
  from libcloud.utils.py3 import b, httplib, urlquote
  from libcloud.utils.xml import findtext, findall
  
  __all__ = [
      "AWSBaseResponse",
      "AWSGenericResponse",
      "AWSTokenConnection",
      "SignedAWSConnection",
      "AWSRequestSignerAlgorithmV2",
      "AWSRequestSignerAlgorithmV4",
      "AWSDriver",
  ]
  
  DEFAULT_SIGNATURE_VERSION = "2"
  UNSIGNED_PAYLOAD = "UNSIGNED-PAYLOAD"
  
  PARAMS_NOT_STRING_ERROR_MSG = """
  "params" dictionary contains an attribute "%s" which value (%s, %s) is not a
  string.
  
  Parameters are sent via query parameters and not via request body and as such,
  all the values need to be of a simple type (string, int, bool).
  
  For arrays and other complex types, you should use notation similar to this
  one:
  
  params['TagSpecification.1.Tag.Value'] = 'foo'
  params['TagSpecification.2.Tag.Value'] = 'bar'
  
  See https://docs.aws.amazon.com/AWSEC2/latest/APIReference/Query-Requests.html
  for details.
  """.strip()
  
  
  class AWSBaseResponse(XmlResponse):
      namespace = None
  
      def _parse_error_details(self, element):
          """
          Parse 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 AWSGenericResponse(AWSBaseResponse):
      # There are multiple error messages in AWS, but they all have an Error node
      # with Code and Message child nodes. Xpath to select them
      # None if the root node *is* the Error node
      xpath = None
  
      # This dict maps <Error><Code>CodeName</Code></Error> to a specific
      # exception class that is raised immediately.
      # If a custom exception class is not defined, errors are accumulated and
      # returned from the parse_error method.
      exceptions = {}  # type: Dict[str, Type[Exception]]
  
      def success(self):
          return self.status in [httplib.OK, httplib.CREATED, httplib.ACCEPTED]
  
      def parse_error(self):
          context = self.connection.context
          status = int(self.status)
  
          # FIXME: Probably ditch this as the forbidden message will have
          # corresponding XML.
          if status == httplib.FORBIDDEN:
              if not self.body:
                  raise InvalidCredsError(str(self.status) + ": " + self.error)
              else:
                  raise InvalidCredsError(self.body)
  
          try:
              body = ET.XML(self.body)
          except Exception:
              raise MalformedResponseError(
                  "Failed to parse XML", body=self.body, driver=self.connection.driver
              )
  
          if self.xpath:
              errs = findall(element=body, xpath=self.xpath, namespace=self.namespace)
          else:
              errs = [body]
  
          msgs = []
          for err in errs:
              code, message = self._parse_error_details(element=err)
              exceptionCls = self.exceptions.get(code, None)
  
              if exceptionCls is None:
                  msgs.append("%s: %s" % (code, message))
                  continue
  
              # Custom exception class is defined, immediately throw an exception
              params = {}
              if hasattr(exceptionCls, "kwargs"):
                  for key in exceptionCls.kwargs:
                      if key in context:
                          params[key] = context[key]
  
              raise exceptionCls(value=message, driver=self.connection.driver, **params)
  
          return "\n".join(msgs)
  
  
  class AWSTokenConnection(ConnectionUserAndKey):
      def __init__(
          self,
          user_id,
          key,
          secure=True,
          host=None,
          port=None,
          url=None,
          timeout=None,
          proxy_url=None,
          token=None,
          retry_delay=None,
          backoff=None,
      ):
          self.token = token
          super(AWSTokenConnection, self).__init__(
              user_id,
              key,
              secure=secure,
              host=host,
              port=port,
              url=url,
              timeout=timeout,
              retry_delay=retry_delay,
              backoff=backoff,
              proxy_url=proxy_url,
          )
  
      def add_default_params(self, params):
          # Even though we are adding it to the headers, we need it here too
          # so that the token is added to the signature.
          if self.token:
              params["x-amz-security-token"] = self.token
          return super(AWSTokenConnection, self).add_default_params(params)
  
      def add_default_headers(self, headers):
          if self.token:
              headers["x-amz-security-token"] = self.token
          return super(AWSTokenConnection, self).add_default_headers(headers)
  
  
  class AWSRequestSigner(object):
      """
      Class which handles signing the outgoing AWS requests.
      """
  
      def __init__(self, access_key, access_secret, version, connection):
          """
          :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``
  
          :param connection: Connection instance.
          :type connection: :class:`Connection`
          """
          self.access_key = access_key
          self.access_secret = access_secret
          self.version = version
          # TODO: Remove cycling dependency between connection and signer
          self.connection = connection
  
      def get_request_params(self, params, method="GET", path="/"):
          return params
  
      def get_request_headers(self, params, headers, method="GET", path="/", data=None):
          return params, headers
  
  
  class AWSRequestSignerAlgorithmV2(AWSRequestSigner):
      def get_request_params(self, params, method="GET", path="/"):
          params["SignatureVersion"] = "2"
          params["SignatureMethod"] = "HmacSHA256"
          params["AWSAccessKeyId"] = self.access_key
          params["Version"] = self.version
          params["Timestamp"] = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
          params["Signature"] = self._get_aws_auth_param(
              params=params, secret_key=self.access_secret, path=path
          )
          return params
  
      def _get_aws_auth_param(self, params, secret_key, path="/"):
          """
          Creates the signature required for AWS, per
          http://bit.ly/aR7GaQ [docs.amazonwebservices.com]:
  
          StringToSign = HTTPVerb + "\n" +
                         ValueOfHostHeaderInLowercase + "\n" +
                         HTTPRequestURI + "\n" +
                         CanonicalizedQueryString <from the preceding step>
          """
          connection = self.connection
  
          keys = list(params.keys())
          keys.sort()
          pairs = []
          for key in keys:
              value = str(params[key])
              pairs.append(urlquote(key, safe="") + "=" + urlquote(value, safe="-_~"))
  
          qs = "&".join(pairs)
  
          hostname = connection.host
          if (connection.secure and connection.port != 443) or (
              not connection.secure and connection.port != 80
          ):
              hostname += ":" + str(connection.port)
  
          string_to_sign = "\n
  
  [... the rest is omitted ...]
files: libcloud/common/aws.py
files: # 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.
  
  __all__ = [
      "fixxpath",
      "findtext",
      "findattr",
      "findall",
      "findall_ignore_namespace",
      "findtext_ignore_namespace",
  ]
  
  
  def fixxpath(xpath, namespace=None):
      # ElementTree wants namespaces in its xpaths, so here we add them.
      if not namespace:
          return xpath
      return "/".join(["{%s}%s" % (namespace, e) for e in xpath.split("/")])
  
  
  def findtext(element, xpath, namespace=None, no_text_value=""):
      """
      :param no_text_value: Value to return if the provided element has no text
                            value.
      :type no_text_value: ``object``
      """
      value = element.findtext(fixxpath(xpath=xpath, namespace=namespace))
  
      if value == "":
          return no_text_value
      return value
  
  
  def findtext_ignore_namespace(element, xpath, namespace=None, no_text_value=""):
      """
      Special version of findtext() which first tries to find the provided value using the provided
      namespace and in case no results are found we fallback to the xpath lookup without namespace.
  
      This is needed because some providers return some responses with namespace and some without.
      """
  
      result = findtext(
          element=element, xpath=xpath, namespace=namespace, no_text_value=no_text_value
      )
  
      if not result and namespace:
          result = findtext(
              element=element, xpath=xpath, namespace=None, no_text_value=no_text_value
          )
  
      return result
  
  
  def findattr(element, xpath, namespace=None):
      return element.findtext(fixxpath(xpath=xpath, namespace=namespace))
  
  
  def findall(element, xpath, namespace=None):
      return element.findall(fixxpath(xpath=xpath, namespace=namespace))
  
  
  def findall_ignore_namespace(element, xpath, namespace=None):
      """
      Special version of findall() which first tries to find the provided value using the provided
      namespace and in case no results are found we fallback to the xpath lookup without namespace.
  
      This is needed because some providers return some responses with namespace and some without.
      """
      result = findall(element=element, xpath=xpath, namespace=namespace)
  
      if not result and namespace:
          result = findall(element=element, xpath=xpath, namespace=None)
  
      return result
  
  
  [... the rest is omitted ...]
files: libcloud/utils/xml.py
files: # 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.
  
  __all__ = ["fixxpath", "findtext", "findattr", "findall"]
  
  
  def fixxpath(xpath, namespace=None):
      # ElementTree wants namespaces in its xpaths, so here we add them.
      if not namespace:
          return xpath
      return "/".join(["{%s}%s" % (namespace, e) for e in xpath.split("/")])
  
  
  def findtext(element, xpath, namespace=None, no_text_value=""):
      """
      :param no_text_value: Value to return if the provided element has no text
                            value.
      :type no_text_value: ``object``
      """
      value = element.findtext(fixxpath(xpath=xpath, namespace=namespace))
  
      if value == "":
          return no_text_value
      return value
  
  
  def findattr(element, xpath, namespace=None):
      return element.findtext(fixxpath(xpath=xpath, namespace=namespace))
  
  
  def findall(element, xpath, namespace=None):
      return element.findall(fixxpath(xpath=xpath, namespace=namespace))
  
  
  [... the rest is omitted ...]
files: libcloud/utils/xml.py