diff --git a/ChangeLog.txt b/ChangeLog.txt index c9af47f03db2..24d89d54d79f 100644 --- a/ChangeLog.txt +++ b/ChangeLog.txt @@ -1,3 +1,8 @@ +2013-03-20 Version 0.6.2 + * Fixes for bugs: + #75 crash on python 2.7 x64 windows + #73 _convert_query_string return a wrong query string parameter + 2012-12-17 Version 0.6.1 * Fixes for bugs: #69 _get_readable_id doesn't support queues with slashes in their names diff --git a/src/azure/http/winhttp.py b/src/azure/http/winhttp.py index 6c67ef69a3a4..5ace9e635e2b 100644 --- a/src/azure/http/winhttp.py +++ b/src/azure/http/winhttp.py @@ -54,12 +54,27 @@ _SysFreeString = _oleaut32.SysFreeString _SysFreeString.argtypes = [c_void_p] -_CoTaskMemAlloc = _ole32.CoTaskMemAlloc -_CoTaskMemAlloc.restype = c_void_p -_CoTaskMemAlloc.argtypes = [c_size_t] +#SAFEARRAY* +#SafeArrayCreateVector(_In_ VARTYPE vt,_In_ LONG lLbound,_In_ ULONG cElements); +_SafeArrayCreateVector = _oleaut32.SafeArrayCreateVector +_SafeArrayCreateVector.restype = c_void_p +_SafeArrayCreateVector.argtypes = [c_ushort, c_long, c_ulong] + +#HRESULT +#SafeArrayAccessData(_In_ SAFEARRAY *psa, _Out_ void **ppvData); +_SafeArrayAccessData = _oleaut32.SafeArrayAccessData +_SafeArrayAccessData.argtypes = [c_void_p, POINTER(c_void_p)] + +#HRESULT +#SafeArrayUnaccessData(_In_ SAFEARRAY *psa); +_SafeArrayUnaccessData = _oleaut32.SafeArrayUnaccessData +_SafeArrayUnaccessData.argtypes = [c_void_p] + +#HRESULT +#SafeArrayGetUBound(_In_ SAFEARRAY *psa, _In_ UINT nDim, _Out_ LONG *plUbound); +_SafeArrayGetUBound = _oleaut32.SafeArrayGetUBound +_SafeArrayGetUBound.argtypes = [c_void_p, c_ulong, POINTER(c_long)] -_CoTaskMemFree = _ole32.CoTaskMemFree -_CoTaskMemFree.argtypes = [c_void_p] #------------------------------------------------------------------------------ @@ -72,26 +87,6 @@ def __init__(self, value): def __del__(self): _SysFreeString(self) -class _tagSAFEARRAY(Structure): - ''' - SAFEARRAY structure in python. Does not match the definition in - MSDN exactly & it is only mapping the used fields. Field names are also - slighty different. - ''' - - class _tagSAFEARRAYBOUND(Structure): - _fields_ = [('c_elements', c_ulong), ('l_lbound', c_long)] - - _fields_ = [('c_dims', c_ushort), - ('f_features', c_ushort), - ('cb_elements', c_ulong), - ('c_locks', c_ulong), - ('pvdata', c_void_p), - ('rgsabound', _tagSAFEARRAYBOUND*1)] - - def __del__(self): - _CoTaskMemFree(self.pvdata) - class VARIANT(Structure): ''' VARIANT structure in python. Does not match the definition in @@ -110,7 +105,7 @@ class _tagRecord(Structure): ('ival', c_short), ('boolval', c_ushort), ('bstrval', BSTR), - ('parray', POINTER(_tagSAFEARRAY)), + ('parray', c_void_p), ('record', _tagRecord)] _fields_ = [('vt', c_ushort), @@ -119,6 +114,63 @@ class _tagRecord(Structure): ('wReserved3', c_ushort), ('vdata', _tagData)] + @staticmethod + def create_empty(): + variant = VARIANT() + variant.vt = VT_EMPTY + variant.vdata.llval = 0 + return variant + + @staticmethod + def create_safearray_from_str(text): + variant = VARIANT() + variant.vt = VT_ARRAY | VT_UI1 + + length = len(text) + variant.vdata.parray = _SafeArrayCreateVector(VT_UI1, 0, length) + pvdata = c_void_p() + _SafeArrayAccessData(variant.vdata.parray, byref(pvdata)) + ctypes.memmove(pvdata, text, length) + _SafeArrayUnaccessData(variant.vdata.parray) + + return variant + + @staticmethod + def create_bstr_from_str(text): + variant = VARIANT() + variant.vt = VT_BSTR + variant.vdata.bstrval = BSTR(text) + return variant + + @staticmethod + def create_bool_false(): + variant = VARIANT() + variant.vt = VT_BOOL + variant.vdata.boolval = 0 + return variant + + def is_safearray_of_bytes(self): + return self.vt == VT_ARRAY | VT_UI1 + + def str_from_safearray(self): + assert self.vt == VT_ARRAY | VT_UI1 + pvdata = c_void_p() + count = c_long() + _SafeArrayGetUBound(self.vdata.parray, 1, byref(count)) + count = c_long(count.value + 1) + _SafeArrayAccessData(self.vdata.parray, byref(pvdata)) + text = ctypes.string_at(pvdata, count) + _SafeArrayUnaccessData(self.vdata.parray) + return text + + def __del__(self): + _VariantClear(self) + +#HRESULT VariantClear(_Inout_ VARIANTARG *pvarg); +_VariantClear = _oleaut32.VariantClear +_VariantClear.argtypes = [POINTER(VARIANT)] + + class GUID(Structure): ''' GUID structure in python. ''' @@ -165,10 +217,7 @@ def open(self, method, url): ''' _WinHttpRequest._SetTimeouts(self, 0, 65000, 65000, 65000) - flag = VARIANT() - flag.vt = VT_BOOL - flag.vdata.boolval = 0 - + flag = VARIANT.create_bool_false() _method = BSTR(method) _url = BSTR(url) _WinHttpRequest._Open(self, _method, _url, flag) @@ -195,24 +244,11 @@ def send(self, request = None): # Sends VT_EMPTY if it is GET, HEAD request. if request is None: - var_empty = VARIANT() - var_empty.vt = VT_EMPTY - var_empty.vdata.llval = 0 + var_empty = VARIANT.create_empty() _WinHttpRequest._Send(self, var_empty) else: # Sends request body as SAFEArray. - _request = VARIANT() - _request.vt = VT_ARRAY | VT_UI1 - safearray = _tagSAFEARRAY() - safearray.c_dims = 1 - safearray.cb_elements = 1 - safearray.c_locks = 0 - safearray.f_features = 128 - safearray.rgsabound[0].c_elements = len(request) - safearray.rgsabound[0].l_lbound = 0 - safearray.pvdata = cast(_CoTaskMemAlloc(len(request)), c_void_p) - ctypes.memmove(safearray.pvdata, request, len(request)) - _request.vdata.parray = cast(byref(safearray), POINTER(_tagSAFEARRAY)) - _WinHttpRequest._Send(self, _request) + _request = VARIANT.create_safearray_from_str(request) + _WinHttpRequest._Send(self, _request) def status(self): ''' Gets status of response. ''' @@ -238,10 +274,8 @@ def response_body(self): ''' var_respbody = VARIANT() _WinHttpRequest._ResponseBody(self, byref(var_respbody)) - if var_respbody.vt == VT_ARRAY | VT_UI1: - safearray = var_respbody.vdata.parray.contents - respbody = ctypes.string_at(safearray.pvdata, safearray.rgsabound[0].c_elements) - + if var_respbody.is_safearray_of_bytes(): + respbody = var_respbody.str_from_safearray() if respbody[3:].startswith(' 0: + respbody = resp.read(resp.length) + + return respbody + #--Test cases for blob service -------------------------------------------- def test_create_blob_service_missing_arguments(self): # Arrange @@ -1326,6 +1395,115 @@ def test_unicode_get_blob_binary_data(self): self.assertIsInstance(blob, BlobResult) self.assertEquals(blob, binary_data) + def test_no_sas_private_blob(self): + # Arrange + data = 'a private blob cannot be read without a shared access signature' + self._create_container_and_block_blob(self.container_name, 'blob1.txt', data) + res_path = self.container_name + '/blob1.txt' + + # Act + host = credentials.getStorageServicesName() + BLOB_SERVICE_HOST_BASE + url = '/' + res_path + respbody = self._get_request(host, url) + + # Assert + self.assertNotEquals(data, respbody) + self.assertNotEquals(-1, respbody.find('ResourceNotFound')) + + def test_no_sas_public_blob(self): + # Arrange + data = 'a public blob can be read without a shared access signature' + self.bc.create_container(self.container_name, None, 'blob') + self.bc.put_blob(self.container_name, 'blob1.txt', data, 'BlockBlob') + res_path = self.container_name + '/blob1.txt' + + # Act + host = credentials.getStorageServicesName() + BLOB_SERVICE_HOST_BASE + url = '/' + res_path + respbody = self._get_request(host, url) + + # Assert + self.assertEquals(data, respbody) + + def test_shared_read_access_blob(self): + # Arrange + data = 'shared access signature with read permission on blob' + self._create_container_and_block_blob(self.container_name, 'blob1.txt', data) + sas = SharedAccessSignature(credentials.getStorageServicesName(), + credentials.getStorageServicesKey()) + res_path = self.container_name + '/blob1.txt' + res_type = RESOURCE_BLOB + + # Act + sas.permission_set = [self._get_permission(sas, res_type, res_path, 'r')] + web_rsrc = self._get_signed_web_resource(sas, res_type, res_path, 'r') + host = credentials.getStorageServicesName() + BLOB_SERVICE_HOST_BASE + url = web_rsrc.request_url + respbody = self._get_request(host, url) + + # Assert + self.assertEquals(data, respbody) + + def test_shared_write_access_blob(self): + # Arrange + data = 'shared access signature with write permission on blob' + updated_data = 'updated blob data' + self._create_container_and_block_blob(self.container_name, 'blob1.txt', data) + sas = SharedAccessSignature(credentials.getStorageServicesName(), + credentials.getStorageServicesKey()) + res_path = self.container_name + '/blob1.txt' + res_type = RESOURCE_BLOB + + # Act + sas.permission_set = [self._get_permission(sas, res_type, res_path, 'w')] + web_rsrc = self._get_signed_web_resource(sas, res_type, res_path, 'w') + host = credentials.getStorageServicesName() + BLOB_SERVICE_HOST_BASE + url = web_rsrc.request_url + respbody = self._put_request(host, url, updated_data) + + # Assert + blob = self.bc.get_blob(self.container_name, 'blob1.txt') + self.assertEquals(updated_data, blob) + + def test_shared_delete_access_blob(self): + # Arrange + data = 'shared access signature with delete permission on blob' + self._create_container_and_block_blob(self.container_name, 'blob1.txt', data) + sas = SharedAccessSignature(credentials.getStorageServicesName(), + credentials.getStorageServicesKey()) + res_path = self.container_name + '/blob1.txt' + res_type = RESOURCE_BLOB + + # Act + sas.permission_set = [self._get_permission(sas, res_type, res_path, 'd')] + web_rsrc = self._get_signed_web_resource(sas, res_type, res_path, 'd') + host = credentials.getStorageServicesName() + BLOB_SERVICE_HOST_BASE + url = web_rsrc.request_url + respbody = self._del_request(host, url) + + # Assert + with self.assertRaises(WindowsAzureError): + blob = self.bc.get_blob(self.container_name, 'blob1.txt') + + def test_shared_access_container(self): + # Arrange + data = 'shared access signature with read permission on container' + self._create_container_and_block_blob(self.container_name, 'blob1.txt', data) + sas = SharedAccessSignature(credentials.getStorageServicesName(), + credentials.getStorageServicesKey()) + res_path = self.container_name + res_type = RESOURCE_CONTAINER + + # Act + sas.permission_set = [self._get_permission(sas, res_type, res_path, 'r')] + web_rsrc = self._get_signed_web_resource(sas, res_type, res_path + '/blob1.txt', 'r') + host = credentials.getStorageServicesName() + BLOB_SERVICE_HOST_BASE + url = web_rsrc.request_url + respbody = self._get_request(host, url) + + # Assert + self.assertEquals(data, respbody) + #------------------------------------------------------------------------------ if __name__ == '__main__': unittest.main() diff --git a/test/azuretest/test_sharedaccesssignature.py b/test/azuretest/test_sharedaccesssignature.py index 76c40cf7e900..c73eb0fa832b 100644 --- a/test/azuretest/test_sharedaccesssignature.py +++ b/test/azuretest/test_sharedaccesssignature.py @@ -17,24 +17,22 @@ from azure.storage.sharedaccesssignature import (SharedAccessSignature, SharedAccessPolicy, Permission, - WebResource) + WebResource, + SIGNED_START, + SIGNED_EXPIRY, + SIGNED_RESOURCE, + SIGNED_PERMISSION, + SIGNED_IDENTIFIER, + SIGNED_SIGNATURE, + RESOURCE_BLOB, + RESOURCE_CONTAINER, + SIGNED_RESOURCE_TYPE, + SHARED_ACCESS_PERMISSION) from azure.storage import AccessPolicy from azuretest.util import AzureTestCase import unittest -#------------------------------------------------------------------------------ -SIGNED_START = 'st' -SIGNED_EXPIRY = 'se' -SIGNED_RESOURCE = 'sr' -SIGNED_PERMISSION = 'sp' -SIGNED_IDENTIFIER = 'si' -SIGNED_SIGNATURE = 'sig' -RESOURCE_BLOB = 'blob' -RESOURCE_CONTAINER = 'container' -SIGNED_RESOURCE_TYPE = 'resource' -SHARED_ACCESS_PERMISSION = 'permission' - #------------------------------------------------------------------------------ class SharedAccessSignatureTest(AzureTestCase): diff --git a/test/coverage.bat b/test/coverage.bat new file mode 100644 index 000000000000..175621ae3764 --- /dev/null +++ b/test/coverage.bat @@ -0,0 +1,62 @@ +@echo OFF +SETLOCAL +REM---------------------------------------------------------------------------- +REM Copyright (c) Microsoft. All rights reserved. +REM +REM Licensed under the Apache License, Version 2.0 (the "License"); +REM you may not use this file except in compliance with the License. +REM You may obtain a copy of the License at +REM http://www.apache.org/licenses/LICENSE-2.0 +REM +REM Unless required by applicable law or agreed to in writing, software +REM distributed under the License is distributed on an "AS IS" BASIS, +REM WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +REM See the License for the specific language governing permissions and +REM limitations under the License. +REM---------------------------------------------------------------------------- +cls + +if "%1%" == "" ( + set PYTHONDIR=%SystemDrive%\Python27 +) else ( + set PYTHONDIR=%1% +) + +if "%2%" == "" ( + set COVERAGEDIR=htmlcov +) else ( + set COVERAGEDIR=%2% +) + +if "%PYTHONPATH%" == "" ( + set PYTHONPATH=..\src +) else ( + set PYTHONPATH=%PYTHONPATH%;..\src +) + +if exist "%PYTHONDIR%\Scripts\coverage.exe" ( + goto :coverage +) + + +REM --------------------------------------------------------------------------- +if not exist "%PYTHONDIR%\Scripts\pip.exe" ( + echo Cannot do a code coverage run when neither 'coverage' nor 'pip' are installed. + goto :exit_door +) + +echo Installing 'coverage' package... +%PYTHONDIR%\Scripts\pip.exe install coverage +echo Finished installing 'coverage' package + +REM --------------------------------------------------------------------------- +:coverage +echo Starting coverage run using %PYTHONDIR% +%PYTHONDIR%\Scripts\coverage.exe run -m unittest discover -p "test_*.py" +%PYTHONDIR%\Scripts\coverage.exe html -d %COVERAGEDIR% +start %CD%\%COVERAGEDIR%\index.html +echo Finished coverage run! + +REM --------------------------------------------------------------------------- +:exit_door +exit /B %UNITTEST_EC% \ No newline at end of file diff --git a/test/coverage_x64.bat b/test/coverage_x64.bat new file mode 100644 index 000000000000..3828832186fb --- /dev/null +++ b/test/coverage_x64.bat @@ -0,0 +1 @@ +call coverage.bat C:\Python27_x64 htmlcov_x64 diff --git a/test/run.bat b/test/run.bat index b610556263da..9d5a8b46acf3 100644 --- a/test/run.bat +++ b/test/run.bat @@ -16,39 +16,23 @@ REM limitations under the License. REM---------------------------------------------------------------------------- cls +if "%1%" == "" ( + set PYTHONDIR=%SystemDrive%\Python27 +) else ( + set PYTHONDIR=%1% +) + if "%PYTHONPATH%" == "" ( set PYTHONPATH=..\src ) else ( - set PYTHONPATH=%PYTHONPATH%:..\src + set PYTHONPATH=%PYTHONPATH%;..\src ) -echo Running tests... -%SystemDrive%\Python27\python.exe -m unittest discover -p "test_*.py" +echo Running tests using %PYTHONDIR% +%PYTHONDIR%\python.exe -m unittest discover -p "test_*.py" set UNITTEST_EC=%ERRORLEVEL% echo Finished running tests! -if exist "%SystemDrive%\Python27\Scripts\coverage.exe" ( - goto :coverage -) - - -REM --------------------------------------------------------------------------- -if not exist "%SystemDrive%\Python27\Scripts\pip.exe" ( - echo Cannot do a code coverage run when neither 'coverage' nor 'pip' are installed. - goto :exit_door -) - -echo Installing 'coverage' package... -%SystemDrive%\Python27\Scripts\pip.exe install coverage==3.5.2 -echo Finished installing 'coverage' package - -REM --------------------------------------------------------------------------- -:coverage -echo Starting coverage run... -%SystemDrive%\Python27\Scripts\coverage.exe run -m unittest discover -p "test_*.py" -%SystemDrive%\Python27\Scripts\coverage.exe html -start %CD%\htmlcov\index.html -echo Finished coverage run! REM --------------------------------------------------------------------------- :exit_door diff --git a/test/run_x64.bat b/test/run_x64.bat new file mode 100644 index 000000000000..e3479942a699 --- /dev/null +++ b/test/run_x64.bat @@ -0,0 +1 @@ +call run.bat C:\Python27_x64