--- /dev/null
+"""
+Storage backend using Microsoft Azure Blob Storage service.
+
+See http://msdn.microsoft.com/en-us/library/windowsazure/dd179428.aspx for
+details on the authentication scheme.
+"""
+import urlparse
+import base64
+import hmac
+import hashlib
+
+from zope.interface import implements
+
+from allmydata.storage.backends.cloud.cloud_common import IContainer, \
+ ContainerItem, ContainerListing, CommonContainerMixin
+
+def configure_msazure_container(*args):
+ pass
+
+
+class MSAzureStorageContainer(CommonContainerMixin):
+ implements(IContainer)
+
+ USER_AGENT = "Tahoe-LAFS Microsoft Azure client"
+
+ def __init__(self, account_name, account_key, container_name,
+ override_reactor=None):
+ CommonContainerMixin.__init__(self, container_name, override_reactor)
+ self._account_name = account_name
+ self._account_key = base64.b64decode(account_key)
+
+ def _calculate_presignature(self, method, url, headers):
+ """
+ Calculate the value to be signed for the given request information.
+
+ We only implement a subset of the standard. In particular, we assume
+ x-ms-date header has been provided, so don't include any Date header.
+
+ The HMAC, and formatting into HTTP header, is not done in this layer.
+ """
+ parsed_url = urlparse.urlparse(url)
+ result = method + "\n"
+ # Add standard headers:
+ for header in ['content-encoding', 'content-language',
+ 'content-length', 'content-md5',
+ 'content-type', 'date', 'if-modified-since',
+ 'if-match', 'if-none-match',
+ 'if-unmodified-since', 'range']:
+ value = headers.getRawHeaders(header, [""])[0]
+ if header == "date":
+ value = ""
+ result += value + "\n"
+
+ # Add x-ms headers:
+ x_ms_headers = []
+ x_ms_date = False
+ for name, values in headers.getAllRawHeaders():
+ name = name.lower()
+ if name.startswith("x-ms"):
+ x_ms_headers.append("%s:%s" % (name, values[0]))
+ if name == "x-ms-date":
+ x_ms_date = True
+ x_ms_headers.sort()
+ if x_ms_headers:
+ result += "\n".join(x_ms_headers) + "\n"
+ if not x_ms_date:
+ raise ValueError("x-ms-date must be included")
+
+ # Add path:
+ result += "/%s%s" % (self._account_name, parsed_url.path)
+
+ # Add query args:
+ query_args = urlparse.parse_qs(parsed_url.query).items()
+ query_args.sort()
+ for name, value in query_args:
+ result += "\n%s:%s" % (name, ",".join(value))
+ return result
+
+ def _calculate_signature(self, method, url, headers):
+ """
+ Calculate the signature for the given request information.
+
+ This includes base64ing and HMACing.
+
+ headers is a twisted.web.http_headers.Headers instance.
+
+ The returned value is suitable for us as an Authorization header.
+ """
+ data = self._calculate_presignature(method, url, headers)
+ signature = hmac.HMAC(self._account_key, data, hashlib.sha256).digest()
+ return "SharedKey %s:%s" % (self._account_name, base64.b64encode(signature))
+
+ def _authorized_http_request(self, what, method, url, request_headers,
+ body=None, need_response_body=False):
+ """
+ Do an HTTP request with the addition of a authorization header.
+ """
from allmydata.storage.backends.cloud.mock_cloud import MockContainer
from allmydata.storage.backends.cloud.openstack import openstack_container
from allmydata.storage.backends.cloud.googlestorage import googlestorage_container
+from allmydata.storage.backends.cloud.msazure import msazure_container
from allmydata.storage.bucket import BucketWriter, BucketReader
from allmydata.storage.common import DataTooLargeError, storage_index_to_dir
from allmydata.storage.leasedb import SHARETYPE_IMMUTABLE, SHARETYPE_MUTABLE
test_head_object.skip = "May not be necessary"
+
+class MSAzureAuthentication(unittest.TestCase):
+ """
+ Tests for Microsoft Azure Blob Storage authentication.
+ """
+ class FakeRequest:
+ """
+ Emulate request objects used by azure library.
+ """
+ def __init__(self, method, url, headers):
+ from urlparse import urlparse, parse_qs
+ self.headers = [
+ (key.lower(), value[0]) for key, value in headers.getAllRawHeaders()]
+ url = urlparse(url)
+ self.path = url.path
+ self.query = url.query
+ self.method = method
+ self.query = [(k, v[0]) for k, v in parse_qs(url.query, keep_blank_values=True).items()]
+
+ def setUp(self):
+ self.container = msazure_container.MSAzureStorageContainer(
+ "account", "key".encode("base64"), "thebucket")
+
+ def assertSignatureEqual(self, method, url, headers, result, azure_buggy=False):
+ """
+ Assert the given HTTP request parameters produce a value to be signed
+ equal to the given result.
+
+ If possible, assert the signature calculation matches the Microsoft
+ reference implementation.
+ """
+ headers = Headers(headers)
+ self.assertEqual(
+ self.container._calculate_presignature(method, url, headers),
+ result)
+ if azure_buggy:
+ # The reference client is buggy in this case, skip it
+ raise unittest.SkipTest("Azure reference client is buggy in this case")
+
+ # Now, compare our result to that of the Microsoft-provided
+ # implementation, if available:
+ try:
+ from azure.storage import _sign_storage_blob_request
+ except ImportError:
+ raise unittest.SkipTest("No azure installed")
+
+ request = self.FakeRequest(method, url, headers)
+ self.assertEqual(
+ _sign_storage_blob_request(request,
+ self.container._account_name,
+ self.container._account_key.encode("base64")),
+ self.container._calculate_signature(method, url, headers))
+
+ def test_method(self):
+ """
+ The correct HTTP method is included in the signature.
+ """
+ self.assertSignatureEqual(
+ "HEAD", "http://x/", {"x-ms-date": ["Sun, 11 Oct 2009 21:49:13 GMT"]},
+ "HEAD\n\n\n\n\n\n\n\n\n\n\n\n"
+ "x-ms-date:Sun, 11 Oct 2009 21:49:13 GMT\n"
+ "/account/")
+
+ def test_standard_headers(self):
+ """
+ A specific set of headers are included in the signature, except for
+ Date which is ignored in favor of x-ms-date.
+ """
+ headers = {"Content-Encoding": ["ce"],
+ "Content-Language": ["cl"],
+ "Content-Length": ["cl2"],
+ "Content-MD5": ["cm"],
+ "Content-Type": ["ct"],
+ "Date": ["d"],
+ "If-Modified-Since": ["ims"],
+ "If-Match": ["im"],
+ "If-None-Match": ["inm"],
+ "If-Unmodified-Since": ["ius"],
+ "Range": ["r"],
+ "Other": ["o"],
+ "x-ms-date": ["xmd"]}
+ self.assertSignatureEqual("GET", "http://x/", headers,
+ "GET\n"
+ "ce\n"
+ "cl\n"
+ "cl2\n"
+ "cm\n"
+ "ct\n"
+ "\n" # Date value is ignored!
+ "ims\n"
+ "im\n"
+ "inm\n"
+ "ius\n"
+ "r\n"
+ "x-ms-date:xmd\n"
+ "/account/", True)
+
+ def test_xms_headers(self):
+ """
+ Headers starting with x-ms are included in the signature.
+ """
+ headers = {"x-ms-foo": ["a"],
+ "x-ms-z": ["b"],
+ "x-ms-date": ["c"]}
+ self.assertSignatureEqual("GET", "http://x/", headers,
+ "GET\n"
+ "\n"
+ "\n"
+ "\n"
+ "\n"
+ "\n"
+ "\n"
+ "\n"
+ "\n"
+ "\n"
+ "\n"
+ "\n"
+ "x-ms-date:c\n"
+ "x-ms-foo:a\n"
+ "x-ms-z:b\n"
+ "/account/")
+
+ def test_xmsdate_required(self):
+ """
+ The x-ms-date header is mandatory.
+ """
+ self.assertRaises(ValueError,
+ self.assertSignatureEqual, "GET", "http://x/", {}, "")
+
+ def test_path_and_account(self):
+ """
+ The URL path and account name is included.
+ """
+ self.container._account_name = "theaccount"
+ self.assertSignatureEqual(
+ "HEAD", "http://x/foo/bar", {"x-ms-date": ["d"]},
+ "HEAD\n\n\n\n\n\n\n\n\n\n\n\n"
+ "x-ms-date:d\n"
+ "/theaccount/foo/bar")
+
+ def test_query(self):
+ """
+ The query arguments are included.
+ """
+ value = "hello%20there"
+ self.assertSignatureEqual(
+ "HEAD", "http://x/?z=%s&y=abc" % (value,), {"x-ms-date": ["d"]},
+ "HEAD\n\n\n\n\n\n\n\n\n\n\n\n"
+ "x-ms-date:d\n"
+ "/account/\n"
+ "y:abc\n"
+ "z:hello there")
+
+
class ServerMixin:
def allocate(self, account, storage_index, sharenums, size, canary=None):
# These secrets are not used, but clients still provide them.