Fix aws request signing so it also works with the "region" argument.

Move away from class per version approach to "signature_version" argument.


Project: http://git-wip-us.apache.org/repos/asf/libcloud/repo
Commit: http://git-wip-us.apache.org/repos/asf/libcloud/commit/a84f5782
Tree: http://git-wip-us.apache.org/repos/asf/libcloud/tree/a84f5782
Diff: http://git-wip-us.apache.org/repos/asf/libcloud/diff/a84f5782

Branch: refs/heads/trunk
Commit: a84f57826fdf73c34b8ef6e98738f971dcc9a912
Parents: 882fdf8
Author: Tomaz Muraus <[email protected]>
Authored: Sat Mar 7 16:36:32 2015 +0100
Committer: Tomaz Muraus <[email protected]>
Committed: Sat Mar 7 16:54:53 2015 +0100

----------------------------------------------------------------------
 libcloud/common/aws.py          | 171 +++++++++++++++++++++++++++--------
 libcloud/compute/drivers/ec2.py |  19 ++++
 2 files changed, 154 insertions(+), 36 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/libcloud/blob/a84f5782/libcloud/common/aws.py
----------------------------------------------------------------------
diff --git a/libcloud/common/aws.py b/libcloud/common/aws.py
index f1b9a11..513e821 100644
--- a/libcloud/common/aws.py
+++ b/libcloud/common/aws.py
@@ -30,6 +30,21 @@ 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'
+
 
 class AWSBaseResponse(XmlResponse):
     namespace = None
@@ -132,17 +147,49 @@ class AWSTokenConnection(ConnectionUserAndKey):
         return super(AWSTokenConnection, self).add_default_headers(headers)
 
 
-class SignedAWSConnection(AWSTokenConnection):
+class AWSRequestSigner(object):
+    """
+    Class which handles signing the outgoing AWS requests.
+    """
 
-    def add_default_params(self, params):
+    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='/'):
+        return params, headers
+
+
+class AWSRequestSignerAlgorithmV2(AWSRequestSigner):
+    def get_request_params(self, params, method='GET', path='/'):
         params['SignatureVersion'] = '2'
         params['SignatureMethod'] = 'HmacSHA256'
-        params['AWSAccessKeyId'] = self.user_id
+        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, self.key,
-                                                       self.action)
+        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='/'):
@@ -155,6 +202,8 @@ class SignedAWSConnection(AWSTokenConnection):
                        HTTPRequestURI + "\n" +
                        CanonicalizedQueryString <from the preceding step>
         """
+        connection = self.connection
+
         keys = list(params.keys())
         keys.sort()
         pairs = []
@@ -165,10 +214,10 @@ class SignedAWSConnection(AWSTokenConnection):
 
         qs = '&'.join(pairs)
 
-        hostname = self.host
-        if (self.secure and self.port != 443) or \
-           (not self.secure and self.port != 80):
-            hostname += ":" + str(self.port)
+        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'.join(('GET', hostname, path, qs))
 
@@ -180,57 +229,70 @@ class SignedAWSConnection(AWSTokenConnection):
         return b64_hmac.decode('utf-8')
 
 
-class V4SignedAWSConnection(AWSTokenConnection):
-
-    def add_default_params(self, params):
+class AWSRequestSignerAlgorithmV4(AWSRequestSigner):
+    def get_request_params(self, params, method='GET', path='/'):
         params['Version'] = self.version
         return params
 
-    def pre_connect_hook(self, params, headers):
+    def get_request_headers(self, params, headers, method='GET', path='/'):
         now = datetime.utcnow()
         headers['X-AMZ-Date'] = now.strftime('%Y%m%dT%H%M%SZ')
         headers['Authorization'] = \
-            self._get_authorization_v4_header(params, headers, now)
+            self._get_authorization_v4_header(params=params, headers=headers,
+                                              dt=now, method=method, path=path)
 
         return params, headers
 
-    def _get_authorization_v4_header(self, params, headers, dt):
-        assert self.method == 'GET', 'AWS Signature V4 not implemented for ' \
-                                     'other methods than GET'
+    def _get_authorization_v4_header(self, params, headers, dt, method='GET',
+                                     path='/'):
+        assert method == 'GET', 'AWS Signature V4 not implemented for ' \
+                                'other methods than GET'
+
+        credentials_scope = self._get_credential_scope(dt=dt)
+        signed_headers = self._get_signed_headers(headers=headers)
+        signature = self._get_signature(params=params, headers=headers,
+                                        dt=dt, method=method, path=path)
 
         return 'AWS4-HMAC-SHA256 Credential=%(u)s/%(c)s, ' \
                'SignedHeaders=%(sh)s, Signature=%(s)s' % {
-                   'u': self.user_id,
-                   'c': self._get_credential_scope(dt),
-                   'sh': self._get_signed_headers(headers),
-                   's': self._get_signature(params, headers, dt)
+                   'u': self.access_key,
+                   'c': credentials_scope,
+                   'sh': signed_headers,
+                   's': signature
                }
 
-    def _get_signature(self, params, headers, dt):
-        return _sign(
-            self._get_key_to_sign_with(dt),
-            self._get_string_to_sign(params, headers, dt),
-            hex=True)
+    def _get_signature(self, params, headers, dt, method, path):
+        key = self._get_key_to_sign_with(dt)
+        string_to_sign = self._get_string_to_sign(params=params,
+                                                  headers=headers, dt=dt,
+                                                  method=method, path=path)
+        return _sign(key=key, msg=string_to_sign, hex=True)
 
     def _get_key_to_sign_with(self, dt):
         return _sign(
             _sign(
                 _sign(
-                    _sign(('AWS4' + self.key), dt.strftime('%Y%m%d')),
-                    self.driver.region_name),
-                self.service_name),
+                    _sign(('AWS4' + self.access_secret),
+                          dt.strftime('%Y%m%d')),
+                    self.connection.driver.region_name),
+                self.connection.service_name),
             'aws4_request')
 
-    def _get_string_to_sign(self, params, headers, dt):
+    def _get_string_to_sign(self, params, headers, dt, method, path):
+        canonical_request = self._get_canonical_request(params=params,
+                                                        headers=headers,
+                                                        method=method,
+                                                        path=path)
+
         return '\n'.join(['AWS4-HMAC-SHA256',
                           dt.strftime('%Y%m%dT%H%M%SZ'),
                           self._get_credential_scope(dt),
-                          _hash(self._get_canonical_request(params, headers))])
+                          _hash(canonical_request)])
 
     def _get_credential_scope(self, dt):
         return '/'.join([dt.strftime('%Y%m%d'),
-                         self.driver.region_name,
-                         self.service_name,
+                         self.connection.driver.region_name,
+                         self.connection.service_name,
                          'aws4_request'])
 
     def _get_signed_headers(self, headers):
@@ -249,10 +311,10 @@ class V4SignedAWSConnection(AWSTokenConnection):
                          (urlquote(k, safe=''), urlquote(str(v), safe='~'))
                          for k, v in sorted(params.items())])
 
-    def _get_canonical_request(self, params, headers):
+    def _get_canonical_request(self, params, headers, method, path):
         return '\n'.join([
-            self.method,
-            self.action,
+            method,
+            path,
             self._get_request_params(params),
             self._get_canonical_headers(headers),
             self._get_signed_headers(headers),
@@ -260,6 +322,43 @@ class V4SignedAWSConnection(AWSTokenConnection):
         ])
 
 
+class SignedAWSConnection(AWSTokenConnection):
+    def __init__(self, user_id, key, secure=True, host=None, port=None,
+                 url=None, timeout=None, token=None,
+                 signature_version=DEFAULT_SIGNATURE_VERSION):
+        super(SignedAWSConnection, self).__init__(user_id=user_id, key=key,
+                                                  secure=secure, host=host,
+                                                  port=port, url=url,
+                                                  timeout=timeout, token=token)
+        self.signature_version = str(signature_version)
+
+        if self.signature_version == '2':
+            signer_cls = AWSRequestSignerAlgorithmV2
+        elif signature_version == '4':
+            signer_cls = AWSRequestSignerAlgorithmV4
+        else:
+            raise ValueError('Unsupported signature_version: %s' %
+                             (signature_version))
+
+        self.signer = signer_cls(access_key=self.user_id,
+                                 access_secret=self.key,
+                                 version=self.version,
+                                 connection=self)
+
+    def add_default_params(self, params):
+        params = self.signer.get_request_params(params=params,
+                                                method=self.method,
+                                                path=self.action)
+        return params
+
+    def pre_connect_hook(self, params, headers):
+        params, headers = self.signer.get_request_headers(params=params,
+                                                          headers=headers,
+                                                          method=self.method,
+                                                          path=self.action)
+        return params, headers
+
+
 def _sign(key, msg, hex=False):
     if hex:
         return hmac.new(b(key), b(msg), hashlib.sha256).hexdigest()

http://git-wip-us.apache.org/repos/asf/libcloud/blob/a84f5782/libcloud/compute/drivers/ec2.py
----------------------------------------------------------------------
diff --git a/libcloud/compute/drivers/ec2.py b/libcloud/compute/drivers/ec2.py
index 03c817f..8f64705 100644
--- a/libcloud/compute/drivers/ec2.py
+++ b/libcloud/compute/drivers/ec2.py
@@ -36,6 +36,7 @@ from libcloud.utils.publickey import get_pubkey_comment
 from libcloud.utils.iso8601 import parse_date
 from libcloud.common.aws import (AWSBaseResponse, SignedAWSConnection,
                                  V4SignedAWSConnection)
+from libcloud.common.aws import DEFAULT_SIGNATURE_VERSION
 from libcloud.common.types import (InvalidCredsError, MalformedResponseError,
                                    LibcloudError)
 from libcloud.compute.providers import Provider
@@ -377,6 +378,7 @@ REGION_DETAILS = {
         'endpoint': 'ec2.us-east-1.amazonaws.com',
         'api_name': 'ec2_us_east',
         'country': 'USA',
+        'signature_version': '2',
         'instance_types': [
             't1.micro',
             'm1.small',
@@ -421,6 +423,7 @@ REGION_DETAILS = {
         'endpoint': 'ec2.us-west-1.amazonaws.com',
         'api_name': 'ec2_us_west',
         'country': 'USA',
+        'signature_version': '2',
         'instance_types': [
             't1.micro',
             'm1.small',
@@ -461,6 +464,7 @@ REGION_DETAILS = {
         'endpoint': 'ec2.us-west-2.amazonaws.com',
         'api_name': 'ec2_us_west_oregon',
         'country': 'US',
+        'signature_version': '2',
         'instance_types': [
             't1.micro',
             'm1.small',
@@ -503,6 +507,7 @@ REGION_DETAILS = {
         'endpoint': 'ec2.eu-west-1.amazonaws.com',
         'api_name': 'ec2_eu_west',
         'country': 'Ireland',
+        'signature_version': '2',
         'instance_types': [
             't1.micro',
             'm1.small',
@@ -545,6 +550,7 @@ REGION_DETAILS = {
         'endpoint': 'ec2.eu-central-1.amazonaws.com',
         'api_name': 'ec2_eu_central',
         'country': 'Frankfurt',
+        'signature_version': '4',
         'instance_types': [
             'm3.medium',
             'm3.large',
@@ -574,6 +580,7 @@ REGION_DETAILS = {
         'endpoint': 'ec2.ap-southeast-1.amazonaws.com',
         'api_name': 'ec2_ap_southeast',
         'country': 'Singapore',
+        'signature_version': '2',
         'instance_types': [
             't1.micro',
             'm1.small',
@@ -609,6 +616,7 @@ REGION_DETAILS = {
         'endpoint': 'ec2.ap-northeast-1.amazonaws.com',
         'api_name': 'ec2_ap_northeast',
         'country': 'Japan',
+        'signature_version': '2',
         'instance_types': [
             't1.micro',
             'm1.small',
@@ -650,6 +658,7 @@ REGION_DETAILS = {
         'endpoint': 'ec2.sa-east-1.amazonaws.com',
         'api_name': 'ec2_sa_east',
         'country': 'Brazil',
+        'signature_version': '2',
         'instance_types': [
             't1.micro',
             'm1.small',
@@ -675,6 +684,7 @@ REGION_DETAILS = {
         'endpoint': 'ec2.ap-southeast-2.amazonaws.com',
         'api_name': 'ec2_ap_southeast_2',
         'country': 'Australia',
+        'signature_version': '2',
         'instance_types': [
             't1.micro',
             'm1.small',
@@ -714,6 +724,7 @@ REGION_DETAILS = {
         'endpoint': 'ec2.us-gov-west-1.amazonaws.com',
         'api_name': 'ec2_us_govwest',
         'country': 'US',
+        'signature_version': '2',
         'instance_types': [
             't1.micro',
             'm1.small',
@@ -755,6 +766,7 @@ REGION_DETAILS = {
         # Nimbus clouds have 3 EC2-style instance types but their particular
         # RAM allocations are configured by the admin
         'country': 'custom',
+        'signature_version': '2',
         'instance_types': [
             'm1.small',
             'm1.large',
@@ -4636,6 +4648,11 @@ class BaseEC2NodeDriver(NodeDriver):
 
         return self._get_boolean(res)
 
+    def _ex_connection_class_kwargs(self):
+        kwargs = super(BaseEC2NodeDriver, self)._ex_connection_class_kwargs()
+        kwargs['signature_version'] = self.signature_version
+        return kwargs
+
     def _to_nodes(self, object, xpath):
         return [self._to_node(el)
                 for el in object.findall(fixxpath(xpath=xpath,
@@ -5560,6 +5577,8 @@ class EC2NodeDriver(BaseEC2NodeDriver):
         self.region_name = region
         self.api_name = details['api_name']
         self.country = details['country']
+        self.signature_version = details.pop('signature_version',
+                                             DEFAULT_SIGNATURE_VERSION)
 
         host = host or details['endpoint']
 

Reply via email to