Use hashmap lib in api.
[pithos] / pithos / api / util.py
index ccc83ec..1ac568f 100644 (file)
@@ -37,19 +37,23 @@ from traceback import format_exc
 from wsgiref.handlers import format_date_time
 from binascii import hexlify, unhexlify
 from datetime import datetime, tzinfo, timedelta
+from urllib import quote, unquote
 
 from django.conf import settings
 from django.http import HttpResponse
 from django.utils import simplejson as json
 from django.utils.http import http_date, parse_etags
-from django.utils.encoding import smart_str
+from django.utils.encoding import smart_unicode, smart_str
 from django.core.files.uploadhandler import FileUploadHandler
 from django.core.files.uploadedfile import UploadedFile
 
-from pithos.api.compat import parse_http_date_safe, parse_http_date
+from pithos.lib.compat import parse_http_date_safe, parse_http_date
+from pithos.lib.hashmap import HashMap
+
 from pithos.api.faults import (Fault, NotModified, BadRequest, Unauthorized, Forbidden, ItemNotFound,
                                 Conflict, LengthRequired, PreconditionFailed, RequestEntityTooLarge,
                                 RangeNotSatisfiable, ServiceUnavailable)
+from pithos.api.short_url import encode_url
 from pithos.backends import connect_backend
 from pithos.backends.base import NotAllowedError, QuotaError
 
@@ -96,7 +100,8 @@ def printable_header_dict(d):
     Format 'last_modified' timestamp.
     """
     
-    d['last_modified'] = isoformat(datetime.fromtimestamp(d['last_modified']))
+    if 'last_modified' in d:
+        d['last_modified'] = isoformat(datetime.fromtimestamp(d['last_modified']))
     return dict([(k.lower().replace('-', '_'), v) for k, v in d.iteritems()])
 
 def format_header_key(k):
@@ -194,7 +199,7 @@ def put_object_headers(response, meta, restricted=False):
     else:
         for k in ('Content-Encoding', 'Content-Disposition'):
             if k in meta:
-                response[k] = meta[k]
+                response[k] = smart_str(meta[k], strings_only=True)
 
 def update_manifest_meta(request, v_account, meta):
     """Update metadata if the object has an X-Object-Manifest."""
@@ -241,7 +246,7 @@ def update_sharing_meta(request, permissions, v_account, v_container, v_object,
 def update_public_meta(public, meta):
     if not public:
         return
-    meta['X-Object-Public'] = public
+    meta['X-Object-Public'] = '/public/' + encode_url(public)
 
 def validate_modification_preconditions(request, meta):
     """Check that the modified timestamp conforms with the preconditions set."""
@@ -288,7 +293,7 @@ def split_container_object_string(s):
         raise ValueError
     s = s[1:]
     pos = s.find('/')
-    if pos == -1:
+    if pos == -1 or pos == len(s) - 1:
         raise ValueError
     return s[:pos], s[(pos + 1):]
 
@@ -738,24 +743,29 @@ def put_object_block(request, hashmap, data, offset):
 def hashmap_hash(request, hashmap):
     """Produce the root hash, treating the hashmap as a Merkle-like tree."""
     
-    def subhash(d):
-        h = hashlib.new(request.backend.hash_algorithm)
-        h.update(d)
-        return h.digest()
-    
-    if len(hashmap) == 0:
-        return hexlify(subhash(''))
-    if len(hashmap) == 1:
-        return hashmap[0]
-    
-    s = 2
-    while s < len(hashmap):
-        s = s * 2
-    h = [unhexlify(x) for x in hashmap]
-    h += [('\x00' * len(h[0]))] * (s - len(hashmap))
-    while len(h) > 1:
-        h = [subhash(h[x] + h[x + 1]) for x in range(0, len(h), 2)]
-    return hexlify(h[0])
+    map = HashMap(request.backend.block_size, request.backend.hash_algorithm)
+    map.extend([unhexlify(x) for x in hashmap])
+    return hexlify(map.hash())
+
+def update_request_headers(request):
+    # Handle URL-encoded keys and values.
+    # Handle URL-encoded keys and values.
+    meta = dict([(k, v) for k, v in request.META.iteritems() if k.startswith('HTTP_')])
+    if len(meta) > 90:
+        raise BadRequest('Too many headers.')
+    for k, v in meta.iteritems():
+        if len(k) > 128:
+            raise BadRequest('Header name too large.')
+        if len(v) > 256:
+            raise BadRequest('Header value too large.')
+        try:
+            k.decode('ascii')
+            v.decode('ascii')
+        except UnicodeDecodeError:
+            raise BadRequest('Bad character in headers.')
+        if '%' in k or '%' in v:
+            del(request.META[k])
+            request.META[unquote(k)] = smart_unicode(unquote(v), strings_only=True)
 
 def update_response_headers(request, response):
     if request.serialization == 'xml':
@@ -765,9 +775,19 @@ def update_response_headers(request, response):
     elif not response['Content-Type']:
         response['Content-Type'] = 'text/plain; charset=UTF-8'
     
-    if not response.has_header('Content-Length') and not (response.has_header('Content-Type') and response['Content-Type'].startswith('multipart/byteranges')):
+    if (not response.has_header('Content-Length') and
+        not (response.has_header('Content-Type') and
+             response['Content-Type'].startswith('multipart/byteranges'))):
         response['Content-Length'] = len(response.content)
     
+    # URL-encode unicode in headers.
+    meta = response.items()
+    for k, v in meta:
+        if (k.startswith('X-Account-') or k.startswith('X-Container-') or
+            k.startswith('X-Object-') or k.startswith('Content-')):
+            del(response[k])
+            response[quote(k)] = quote(v, safe='/=,:@; ')
+    
     if settings.TEST:
         response['Date'] = format_date_time(time())
 
@@ -823,6 +843,9 @@ def api_method(http_method=None, format_allowed=False, user_required=True):
                 if len(args) > 2 and len(args[2]) > 1024:
                     raise BadRequest('Object name too large.')
                 
+                # Format and check headers.
+                update_request_headers(request)
+                
                 # Fill in custom request variables.
                 request.serialization = request_serialization(request, format_allowed)
                 request.backend = connect_backend()