Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Verify MD5 is available #807

Merged
merged 4 commits into from
Feb 25, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions botocore/compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,15 @@
import sys
import inspect
import warnings
import hashlib
import logging

from botocore.vendored import six
from botocore.exceptions import MD5UnavailableError
from botocore.vendored.requests.packages.urllib3 import exceptions

logger = logging.getLogger(__name__)


if six.PY3:
from six.moves import http_client
Expand Down Expand Up @@ -237,3 +242,30 @@ def total_seconds(delta):
day_in_seconds = delta.days * 24 * 3600.0
micro_in_seconds = delta.microseconds / 10.0**6
return day_in_seconds + delta.seconds + micro_in_seconds


# Checks to see if md5 is available on this system. A given system might not
# have access to it for various reasons, such as FIPS mode being enabled.
try:
hashlib.md5()
MD5_AVAILABLE = True
except ValueError:
MD5_AVAILABLE = False


def get_md5(*args, **kwargs):
"""
Attempts to get an md5 hashing object.

:param raise_error_if_unavailable: raise an error if md5 is unavailable on
this system. If False, None will be returned if it is unavailable.
:type raise_error_if_unavailable: bool
:param args: Args to pass to the MD5 constructor
:param kwargs: Key word arguments to pass to the MD5 constructor
:return: An MD5 hashing object if available. If it is unavailable, None
is returned if raise_error_if_unavailable is set to False.
"""
if MD5_AVAILABLE:
return hashlib.md5(*args, **kwargs)
else:
raise MD5UnavailableError()
4 changes: 4 additions & 0 deletions botocore/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -367,3 +367,7 @@ class InvalidConfigError(BotoCoreError):

class RefreshWithMFAUnsupportedError(BotoCoreError):
fmt = 'Cannot refresh credentials: MFA token required.'


class MD5UnavailableError(BotoCoreError):
fmt = "This system does not support MD5 generation."
12 changes: 6 additions & 6 deletions botocore/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,14 @@
"""

import base64
import hashlib
import logging
import xml.etree.cElementTree
import copy
import re
import warnings

from botocore.compat import urlsplit, urlunsplit, unquote, \
json, quote, six, unquote_str, ensure_bytes
json, quote, six, unquote_str, ensure_bytes, get_md5, MD5_AVAILABLE
from botocore.docs.utils import AutoPopulatedParam
from botocore.docs.utils import HideParamFromOperations
from botocore.docs.utils import AppendParamDocumentation
Expand Down Expand Up @@ -134,13 +133,13 @@ def calculate_md5(params, **kwargs):


def _calculate_md5_from_bytes(body_bytes):
md5 = hashlib.md5(body_bytes)
md5 = get_md5(body_bytes)
return md5.digest()


def _calculate_md5_from_file(fileobj):
start_position = fileobj.tell()
md5 = hashlib.md5()
md5 = get_md5()
for chunk in iter(lambda: fileobj.read(1024 * 1024), b''):
md5.update(chunk)
fileobj.seek(start_position)
Expand All @@ -150,7 +149,7 @@ def _calculate_md5_from_file(fileobj):
def conditionally_calculate_md5(params, **kwargs):
"""Only add a Content-MD5 when not using sigv4"""
signer = kwargs['request_signer']
if signer.signature_version not in ['v4', 's3v4']:
if signer.signature_version not in ['v4', 's3v4'] and MD5_AVAILABLE:
calculate_md5(params, **kwargs)


Expand Down Expand Up @@ -188,13 +187,14 @@ def copy_source_sse_md5(params, **kwargs):
def _sse_md5(params, sse_member_prefix='SSECustomer'):
if not _needs_s3_sse_customization(params, sse_member_prefix):
return

sse_key_member = sse_member_prefix + 'Key'
sse_md5_member = sse_member_prefix + 'KeyMD5'
key_as_bytes = params[sse_key_member]
if isinstance(key_as_bytes, six.text_type):
key_as_bytes = key_as_bytes.encode('utf-8')
key_md5_str = base64.b64encode(
hashlib.md5(key_as_bytes).digest()).decode('utf-8')
get_md5(key_as_bytes).digest()).decode('utf-8')
key_b64_encoded = base64.b64encode(key_as_bytes).decode('utf-8')
params[sse_key_member] = key_b64_encoded
params[sse_md5_member] = key_md5_str
Expand Down
19 changes: 18 additions & 1 deletion tests/unit/test_compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,12 @@
# language governing permissions and limitations under the License.

import datetime
import mock

from botocore.compat import total_seconds, unquote_str, six, ensure_bytes
from botocore.exceptions import MD5UnavailableError
from botocore.compat import (
total_seconds, unquote_str, six, ensure_bytes, get_md5
)

from tests import BaseEnvVar, unittest

Expand Down Expand Up @@ -78,3 +82,16 @@ def test_non_string_or_bytes_raises_error(self):
value = 500
with self.assertRaises(ValueError):
ensure_bytes(value)


class TestGetMD5(unittest.TestCase):
def test_available(self):
md5 = mock.Mock()
with mock.patch('botocore.compat.MD5_AVAILABLE', True):
with mock.patch('hashlib.md5', mock.Mock(return_value=md5)):
self.assertEqual(get_md5(), md5)

def test_unavailable_raises_error(self):
with mock.patch('botocore.compat.MD5_AVAILABLE', False):
with self.assertRaises(MD5UnavailableError):
get_md5()
Loading