Repository: kumar303/mohawk

Issue:
Support file objects as content
We have some large files we need to send via an HTTP POST, and the API requires Hawk authentication. Currently this means the entire file has to be loaded into memory so that it can be passed as `content` to `mohawk.Sender`

Can we instead pass an open file object, and then mohawk can read in smaller blocks of data from the file to create the payload hash?

Discussion:

Patch:
diff --git a/.travis.yml b/.travis.yml
index c5b41fe..7617ace 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,4 +1,4 @@
-sudo: false
+dist: trusty  # For python 2.6
 language: python
 matrix:
     include:
diff --git a/docs/index.rst b/docs/index.rst
index 61cdead..7d7ed8c 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -106,6 +106,8 @@ Changelog
 
 - **UNRELEASED**
 
+  - Support passing file-like objects (supporting `.read(n)`) as the `content`
+    parameter for Resources.
   - (Unreleased features should be listed here.)
 
 - **1.0.0** (2019-01-09)
diff --git a/mohawk/sender.py b/mohawk/sender.py
index 3fbd1bb..5c599e3 100644
--- a/mohawk/sender.py
+++ b/mohawk/sender.py
@@ -26,8 +26,8 @@ class Sender(HawkAuthority):
     :param method: Method of the request. E.G. POST, GET
     :type method: str
 
-    :param content=EmptyValue: Byte string of request body.
-    :type content=EmptyValue: str
+    :param content=EmptyValue: Byte string of request body or a file-like object.
+    :type content=EmptyValue: str or file-like object
 
     :param content_type=EmptyValue: content-type header value for request.
     :type content_type=EmptyValue: str
diff --git a/mohawk/util.py b/mohawk/util.py
index bb8a14e..0c51428 100644
--- a/mohawk/util.py
+++ b/mohawk/util.py
@@ -46,7 +46,7 @@ def random_string(length):
     return urlsafe_b64encode(os.urandom(length))[:length]
 
 
-def calculate_payload_hash(payload, algorithm, content_type):
+def calculate_payload_hash(payload, algorithm, content_type, block_size=1024):
     """Calculates a hash for a given payload."""
     p_hash = hashlib.new(algorithm)
 
@@ -58,9 +58,18 @@ def calculate_payload_hash(payload, algorithm, content_type):
 
     for i, p in enumerate(parts):
         # Make sure we are about to hash binary strings.
-        if not isinstance(p, six.binary_type):
+        if hasattr(p, "read"):
+            log.debug("part %i being handled as a file object", i)
+            while True:
+                block = p.read(block_size)
+                if not block:
+                    break
+                p_hash.update(block)
+        elif not isinstance(p, six.binary_type):
             p = p.encode('utf8')
-        p_hash.update(p)
+            p_hash.update(p)
+        else:
+            p_hash.update(p)
         parts[i] = p
 
     log.debug('calculating payload hash from:\n{parts}'


Test patch:
diff --git a/mohawk/tests.py b/mohawk/tests.py
index a2710c6..e680cf8 100644
--- a/mohawk/tests.py
+++ b/mohawk/tests.py
@@ -22,6 +22,7 @@ from .exc import (AlreadyProcessed,
                   MissingContent)
 from .util import (parse_authorization_header,
                    utc_now,
+                   calculate_payload_hash,
                    calculate_ts_mac,
                    validate_credentials)
 from .bewit import (get_bewit,
@@ -443,6 +444,25 @@ class TestSender(Base):
         header = sn.request_header.replace(dlg, 'TAMPERED-WITH')
         self.receive(header)
 
+    def test_file_content(self):
+        method = "POST"
+        content = six.BytesIO(b"FILE CONTENT")
+        sn = self.Sender(method, content=content)
+        self.receive(sn.request_header, method=method, content=content.getvalue())
+
+    def test_binary_file_content(self):
+        method = "POST"
+        content = six.BytesIO(b"\x00\xffCONTENT\xff\x00")
+        sn = self.Sender(method, content=content)
+        self.receive(sn.request_header, method=method, content=content.getvalue())
+
+    @raises(MisComputedContentHash)
+    def test_bad_file_content(self):
+        method = "POST"
+        content = six.BytesIO(b"FILE CONTENT")
+        sn = self.Sender(method, content=content)
+        self.receive(sn.request_header, method=method, content="BAD FILE CONTENT")
+
 
 class TestReceiver(Base):
 
@@ -999,3 +1019,12 @@ class TestBewit(Base):
             'other_id': self.credentials,
         })
         check_bewit(url, credential_lookup=credential_lookup, now=1356420407 + 10)
+
+
+class TestPayloadHash(Base):
+    def test_hash_file_read_blocks(self):
+        payload = six.BytesIO(b"\x00\xffhello world\xff\x00")
+        h1 = calculate_payload_hash(payload, 'sha256', 'application/json', block_size=1)
+        payload.seek(0)
+        h2 = calculate_payload_hash(payload, 'sha256', 'application/json', block_size=1024)
+        self.assertEqual(h1, h2)