| # 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 copy |
| import os |
| import time |
| import base64 |
| import hmac |
| |
| from hashlib import sha256 |
| from libcloud.utils.py3 import httplib |
| from libcloud.utils.py3 import b |
| from libcloud.utils.xml import fixxpath |
| |
| from libcloud.utils.py3 import ET |
| from libcloud.common.types import InvalidCredsError |
| from libcloud.common.types import LibcloudError, MalformedResponseError |
| from libcloud.common.base import ConnectionUserAndKey, RawResponse |
| from libcloud.common.base import CertificateConnection |
| from libcloud.common.base import XmlResponse |
| from libcloud.common.base import BaseDriver |
| |
| # The time format for headers in Azure requests |
| AZURE_TIME_FORMAT = '%a, %d %b %Y %H:%M:%S GMT' |
| |
| |
| class AzureRedirectException(Exception): |
| |
| def __init__(self, response): |
| self.location = response.headers['location'] |
| |
| |
| class AzureResponse(XmlResponse): |
| |
| valid_response_codes = [ |
| httplib.NOT_FOUND, |
| httplib.CONFLICT, |
| httplib.BAD_REQUEST, |
| # added TEMPORARY_REDIRECT as this can sometimes be |
| # sent by azure instead of a success or fail response |
| httplib.TEMPORARY_REDIRECT, |
| # Used by Azure Blobs range downloads |
| httplib.PARTIAL_CONTENT |
| ] |
| |
| def success(self): |
| i = int(self.status) |
| return 200 <= i <= 299 or i in self.valid_response_codes |
| |
| def parse_error(self, msg=None): |
| error_msg = 'Unknown error' |
| |
| try: |
| # Azure does give some meaningful errors, but is inconsistent |
| # Some APIs respond with an XML error. Others just dump HTML |
| body = self.parse_body() |
| |
| # pylint: disable=no-member |
| if type(body) == ET.Element: |
| code = body.findtext(fixxpath(xpath='Code')) |
| message = body.findtext(fixxpath(xpath='Message')) |
| message = message.split('\n')[0] |
| error_msg = '%s: %s' % (code, message) |
| |
| except MalformedResponseError: |
| pass |
| |
| if msg: |
| error_msg = '%s - %s' % (msg, error_msg) |
| |
| if self.status in [httplib.UNAUTHORIZED, httplib.FORBIDDEN]: |
| raise InvalidCredsError(error_msg) |
| |
| raise LibcloudError( |
| '%s Status code: %d.' % (error_msg, self.status), |
| driver=self |
| ) |
| |
| def parse_body(self): |
| is_redirect = int(self.status) == httplib.TEMPORARY_REDIRECT |
| |
| if is_redirect and self.connection.driver.follow_redirects: |
| raise AzureRedirectException(self) |
| else: |
| return super(AzureResponse, self).parse_body() |
| |
| |
| class AzureRawResponse(RawResponse): |
| pass |
| |
| |
| class AzureConnection(ConnectionUserAndKey): |
| """ |
| Represents a single connection to Azure |
| """ |
| |
| responseCls = AzureResponse |
| rawResponseCls = AzureRawResponse |
| |
| API_VERSION = '2012-02-12' |
| |
| def add_default_params(self, params): |
| return params |
| |
| def pre_connect_hook(self, params, headers): |
| headers = copy.deepcopy(headers) |
| |
| # We have to add a date header in GMT |
| headers['x-ms-date'] = time.strftime(AZURE_TIME_FORMAT, time.gmtime()) |
| headers['x-ms-version'] = self.API_VERSION |
| |
| # Add the authorization header |
| headers['Authorization'] = self._get_azure_auth_signature( |
| method=self.method, |
| headers=headers, |
| params=params, |
| account=self.user_id, |
| secret_key=self.key, |
| path=self.action |
| ) |
| |
| # Azure cribs about this in 'raw' connections |
| headers.pop('Host', None) |
| |
| return params, headers |
| |
| def _get_azure_auth_signature(self, |
| method, |
| headers, |
| params, |
| account, |
| secret_key, |
| path='/'): |
| """ |
| Signature = Base64( HMAC-SHA1( YourSecretAccessKeyID, |
| UTF-8-Encoding-Of( StringToSign ) ) ) ); |
| |
| StringToSign = HTTP-VERB + "\n" + |
| Content-Encoding + "\n" + |
| Content-Language + "\n" + |
| Content-Length + "\n" + |
| Content-MD5 + "\n" + |
| Content-Type + "\n" + |
| Date + "\n" + |
| If-Modified-Since + "\n" + |
| If-Match + "\n" + |
| If-None-Match + "\n" + |
| If-Unmodified-Since + "\n" + |
| Range + "\n" + |
| CanonicalizedHeaders + |
| CanonicalizedResource; |
| """ |
| xms_header_values = [] |
| param_list = [] |
| |
| # Split the x-ms headers and normal headers and make everything |
| # lower case |
| headers_copy = {} |
| for header, value in headers.items(): |
| header = header.lower() |
| value = str(value).strip() |
| if header.startswith('x-ms-'): |
| xms_header_values.append((header, value)) |
| else: |
| headers_copy[header] = value |
| |
| # Get the values for the headers in the specific order |
| special_header_values = self._format_special_header_values( |
| headers_copy, method) |
| |
| # Prepare the first section of the string to be signed |
| values_to_sign = [method] + special_header_values |
| # string_to_sign = '\n'.join([method] + special_header_values) |
| |
| # The x-ms-* headers have to be in lower case and sorted |
| xms_header_values.sort() |
| |
| for header, value in xms_header_values: |
| values_to_sign.append('%s:%s' % (header, value)) |
| |
| # Add the canonicalized path |
| values_to_sign.append('/%s%s' % (account, path)) |
| |
| # URL query parameters (sorted and lower case) |
| for key, value in params.items(): |
| param_list.append((key.lower(), str(value).strip())) |
| |
| param_list.sort() |
| |
| for key, value in param_list: |
| values_to_sign.append('%s:%s' % (key, value)) |
| |
| string_to_sign = b('\n'.join(values_to_sign)) |
| secret_key = b(secret_key) |
| b64_hmac = base64.b64encode( |
| hmac.new(secret_key, string_to_sign, digestmod=sha256).digest() |
| ) |
| |
| return 'SharedKey %s:%s' % (self.user_id, b64_hmac.decode('utf-8')) |
| |
| def _format_special_header_values(self, headers, method): |
| is_change = method not in ('GET', 'HEAD') |
| is_old_api = self.API_VERSION <= '2014-02-14' |
| |
| special_header_keys = [ |
| 'content-encoding', |
| 'content-language', |
| 'content-length', |
| 'content-md5', |
| 'content-type', |
| 'date', |
| 'if-modified-since', |
| 'if-match', |
| 'if-none-match', |
| 'if-unmodified-since', |
| 'range' |
| ] |
| |
| special_header_values = [] |
| |
| for header in special_header_keys: |
| header = header.lower() # Just for safety |
| if header in headers: |
| special_header_values.append(headers[header]) |
| elif header == 'content-length' and is_change and is_old_api: |
| # For old API versions, the Content-Length header must be '0' |
| # https://docs.microsoft.com/en-us/rest/api/storageservices/authorize-with-shared-key#content-length-header-in-version-2014-02-14-and-earlier |
| special_header_values.append('0') |
| else: |
| special_header_values.append('') |
| |
| return special_header_values |
| |
| |
| class AzureBaseDriver(BaseDriver): |
| name = "Microsoft Azure Service Management API" |
| |
| |
| class AzureServiceManagementConnection(CertificateConnection): |
| # This needs the following approach - |
| # 1. Make request using LibcloudHTTPSConnection which is a overloaded |
| # class which takes in a client certificate |
| # 2. Depending on the type of operation use a PollingConnection |
| # when the response id is returned |
| # 3. The Response can be used in an AzureServiceManagementResponse |
| |
| """ |
| Authentication class for "Service Account" authentication. |
| """ |
| |
| driver = AzureBaseDriver |
| responseCls = AzureResponse |
| rawResponseCls = AzureRawResponse |
| name = 'Azure Service Management API Connection' |
| host = 'management.core.windows.net' |
| keyfile = "" |
| |
| def __init__(self, subscription_id, key_file, *args, **kwargs): |
| """ |
| Check to see if PyCrypto is available, and convert key file path into a |
| key string if the key is in a file. |
| |
| :param subscription_id: Azure subscription ID. |
| :type subscription_id: ``str`` |
| |
| :param key_file: The PEM file used to authenticate with the service. |
| :type key_file: ``str`` |
| """ |
| |
| super(AzureServiceManagementConnection, self).__init__( |
| key_file, |
| *args, |
| **kwargs |
| ) |
| |
| self.subscription_id = subscription_id |
| |
| keypath = os.path.expanduser(key_file) |
| self.keyfile = keypath |
| is_file_path = os.path.exists(keypath) and os.path.isfile(keypath) |
| if not is_file_path: |
| raise InvalidCredsError( |
| 'You need an certificate PEM file to authenticate with ' |
| 'Microsoft Azure. This can be found in the portal.' |
| ) |
| self.key_file = key_file |
| |
| def add_default_headers(self, headers): |
| """ |
| @inherits: :class:`Connection.add_default_headers` |
| TODO: move to constant.. |
| """ |
| headers['x-ms-version'] = "2014-05-01" |
| headers['x-ms-date'] = time.strftime(AZURE_TIME_FORMAT, time.gmtime()) |
| # headers['host'] = self.host |
| return headers |