Add a dummy authentication middleware
[pithos] / pithos / api / util.py
1 # Copyright 2011 GRNET S.A. All rights reserved.
2
3 # Redistribution and use in source and binary forms, with or
4 # without modification, are permitted provided that the following
5 # conditions are met:
6
7 #   1. Redistributions of source code must retain the above
8 #      copyright notice, this list of conditions and the following
9 #      disclaimer.
10
11 #   2. Redistributions in binary form must reproduce the above
12 #      copyright notice, this list of conditions and the following
13 #      disclaimer in the documentation and/or other materials
14 #      provided with the distribution.
15
16 # THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17 # OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18 # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19 # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20 # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23 # USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24 # AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25 # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26 # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27 # POSSIBILITY OF SUCH DAMAGE.
28
29 # The views and conclusions contained in the software and
30 # documentation are those of the authors and should not be
31 # interpreted as representing official policies, either expressed
32 # or implied, of GRNET S.A.
33
34 from functools import wraps
35 from time import time
36 from traceback import format_exc
37 from wsgiref.handlers import format_date_time
38 from binascii import hexlify
39
40 from django.conf import settings
41 from django.http import HttpResponse
42 from django.utils.http import http_date, parse_etags
43
44 from pithos.api.compat import parse_http_date_safe
45 from pithos.api.faults import (Fault, NotModified, BadRequest, ItemNotFound, LengthRequired,
46                                 PreconditionFailed, RangeNotSatisfiable, ServiceUnavailable)
47 from pithos.backends import backend
48 from pithos.backends.base import NotAllowedError
49
50 import datetime
51 import logging
52 import re
53 import hashlib
54 import uuid
55
56
57 logger = logging.getLogger(__name__)
58
59
60 def printable_meta_dict(d):
61     """Format a meta dictionary for printing out json/xml.
62     
63     Convert all keys to lower case and replace dashes to underscores.
64     Change 'modified' key from backend to 'last_modified' and format date.
65     """
66     
67     if 'modified' in d:
68         d['last_modified'] = datetime.datetime.fromtimestamp(int(d['modified'])).isoformat()
69         del(d['modified'])
70     return dict([(k.lower().replace('-', '_'), v) for k, v in d.iteritems()])
71
72 def format_meta_key(k):
73     """Convert underscores to dashes and capitalize intra-dash strings."""
74     
75     return '-'.join([x.capitalize() for x in k.replace('_', '-').split('-')])
76
77 def get_meta_prefix(request, prefix):
78     """Get all prefix-* request headers in a dict. Reformat keys with format_meta_key()."""
79     
80     prefix = 'HTTP_' + prefix.upper().replace('-', '_')
81     return dict([(format_meta_key(k[5:]), v) for k, v in request.META.iteritems() if k.startswith(prefix) and len(k) > len(prefix)])
82
83 def get_account_meta(request):
84     """Get metadata from an account request."""
85     
86     meta = get_meta_prefix(request, 'X-Account-Meta-')    
87     return meta
88
89 def put_account_meta(response, meta):
90     """Put metadata in an account response."""
91     
92     response['X-Account-Container-Count'] = meta['count']
93     response['X-Account-Bytes-Used'] = meta['bytes']
94     if 'modified' in meta:
95         response['Last-Modified'] = http_date(int(meta['modified']))
96     for k in [x for x in meta.keys() if x.startswith('X-Account-Meta-')]:
97         response[k.encode('utf-8')] = meta[k].encode('utf-8')
98     if 'until_timestamp' in meta:
99         response['X-Account-Until-Timestamp'] = http_date(int(meta['until_timestamp']))
100
101 def get_container_meta(request):
102     """Get metadata from a container request."""
103     
104     meta = get_meta_prefix(request, 'X-Container-Meta-')
105     return meta
106
107 def put_container_meta(response, meta):
108     """Put metadata in a container response."""
109     
110     response['X-Container-Object-Count'] = meta['count']
111     response['X-Container-Bytes-Used'] = meta['bytes']
112     response['Last-Modified'] = http_date(int(meta['modified']))
113     for k in [x for x in meta.keys() if x.startswith('X-Container-Meta-')]:
114         response[k.encode('utf-8')] = meta[k].encode('utf-8')
115     response['X-Container-Object-Meta'] = [x[14:] for x in meta['object_meta'] if x.startswith('X-Object-Meta-')]
116     response['X-Container-Block-Size'] = backend.block_size
117     response['X-Container-Block-Hash'] = backend.hash_algorithm
118     if 'until_timestamp' in meta:
119         response['X-Container-Until-Timestamp'] = http_date(int(meta['until_timestamp']))
120
121 def get_object_meta(request):
122     """Get metadata from an object request."""
123     
124     meta = get_meta_prefix(request, 'X-Object-Meta-')
125     if request.META.get('CONTENT_TYPE'):
126         meta['Content-Type'] = request.META['CONTENT_TYPE']
127     if request.META.get('HTTP_CONTENT_ENCODING'):
128         meta['Content-Encoding'] = request.META['HTTP_CONTENT_ENCODING']
129     if request.META.get('HTTP_CONTENT_DISPOSITION'):
130         meta['Content-Disposition'] = request.META['HTTP_CONTENT_DISPOSITION']
131     if request.META.get('HTTP_X_OBJECT_MANIFEST'):
132         meta['X-Object-Manifest'] = request.META['HTTP_X_OBJECT_MANIFEST']
133     return meta
134
135 def put_object_meta(response, meta, public=False):
136     """Put metadata in an object response."""
137     
138     response['ETag'] = meta['hash']
139     response['Content-Length'] = meta['bytes']
140     response['Content-Type'] = meta.get('Content-Type', 'application/octet-stream')
141     response['Last-Modified'] = http_date(int(meta['modified']))
142     if not public:
143         response['X-Object-Modified-By'] = meta['modified_by']
144         response['X-Object-Version'] = meta['version']
145         response['X-Object-Version-Timestamp'] = http_date(int(meta['version_timestamp']))
146         for k in [x for x in meta.keys() if x.startswith('X-Object-Meta-')]:
147             response[k.encode('utf-8')] = meta[k].encode('utf-8')
148         for k in ('Content-Encoding', 'Content-Disposition', 'X-Object-Manifest', 'X-Object-Sharing', 'X-Object-Shared-By'):
149             if k in meta:
150                 response[k] = meta[k]
151     else:
152         for k in ('Content-Encoding', 'Content-Disposition'):
153             if k in meta:
154                 response[k] = meta[k]
155
156 def update_manifest_meta(request, v_account, meta):
157     """Update metadata if the object has an X-Object-Manifest."""
158     
159     if 'X-Object-Manifest' in meta:
160         hash = ''
161         bytes = 0
162         try:
163             src_container, src_name = split_container_object_string('/' + meta['X-Object-Manifest'])
164             objects = backend.list_objects(request.user, v_account, src_container, prefix=src_name, virtual=False)
165             for x in objects:
166                 src_meta = backend.get_object_meta(request.user, v_account, src_container, x[0], x[1])
167                 hash += src_meta['hash']
168                 bytes += src_meta['bytes']
169         except:
170             # Ignore errors.
171             return
172         meta['bytes'] = bytes
173         md5 = hashlib.md5()
174         md5.update(hash)
175         meta['hash'] = md5.hexdigest().lower()
176
177 def update_sharing_meta(permissions, v_account, v_container, v_object, meta):
178     if permissions is None:
179         return
180     perm_path, perms = permissions
181     if len(perms) == 0:
182         return
183     ret = []
184     r = ','.join(perms.get('read', []))
185     if r:
186         ret.append('read=' + r)
187     w = ','.join(perms.get('write', []))
188     if w:
189         ret.append('write=' + w)
190     meta['X-Object-Sharing'] = '; '.join(ret)
191     if '/'.join((v_account, v_container, v_object)) != perm_path:
192         meta['X-Object-Shared-By'] = perm_path
193
194 def validate_modification_preconditions(request, meta):
195     """Check that the modified timestamp conforms with the preconditions set."""
196     
197     if 'modified' not in meta:
198         return # TODO: Always return?
199     
200     if_modified_since = request.META.get('HTTP_IF_MODIFIED_SINCE')
201     if if_modified_since is not None:
202         if_modified_since = parse_http_date_safe(if_modified_since)
203     if if_modified_since is not None and int(meta['modified']) <= if_modified_since:
204         raise NotModified('Resource has not been modified')
205     
206     if_unmodified_since = request.META.get('HTTP_IF_UNMODIFIED_SINCE')
207     if if_unmodified_since is not None:
208         if_unmodified_since = parse_http_date_safe(if_unmodified_since)
209     if if_unmodified_since is not None and int(meta['modified']) > if_unmodified_since:
210         raise PreconditionFailed('Resource has been modified')
211
212 def validate_matching_preconditions(request, meta):
213     """Check that the ETag conforms with the preconditions set."""
214     
215     if 'hash' not in meta:
216         return # TODO: Always return?
217     
218     if_match = request.META.get('HTTP_IF_MATCH')
219     if if_match is not None and if_match != '*':
220         if meta['hash'] not in [x.lower() for x in parse_etags(if_match)]:
221             raise PreconditionFailed('Resource Etag does not match')
222     
223     if_none_match = request.META.get('HTTP_IF_NONE_MATCH')
224     if if_none_match is not None:
225         if if_none_match == '*' or meta['hash'] in [x.lower() for x in parse_etags(if_none_match)]:
226             raise NotModified('Resource Etag matches')
227
228 def split_container_object_string(s):
229     if not len(s) > 0 or s[0] != '/':
230         raise ValueError
231     s = s[1:]
232     pos = s.find('/')
233     if pos == -1:
234         raise ValueError
235     return s[:pos], s[(pos + 1):]
236
237 def copy_or_move_object(request, v_account, src_container, src_name, dest_container, dest_name, move=False):
238     """Copy or move an object."""
239     
240     meta = get_object_meta(request)
241     permissions = get_sharing(request)
242     src_version = request.META.get('HTTP_X_SOURCE_VERSION')    
243     try:
244         if move:
245             backend.move_object(request.user, v_account, src_container, src_name, dest_container, dest_name, meta, False, permissions)
246         else:
247             backend.copy_object(request.user, v_account, src_container, src_name, dest_container, dest_name, meta, False, permissions, src_version)
248     except NotAllowedError:
249         raise Unauthorized('Access denied')
250     except NameError, IndexError:
251         raise ItemNotFound('Container or object does not exist')
252     except ValueError:
253         raise BadRequest('Invalid sharing header')
254     except AttributeError:
255         raise Conflict('Sharing already set above or below this path in the hierarchy')
256
257 def get_int_parameter(request, name):
258     p = request.GET.get(name)
259     if p is not None:
260         try:
261             p = int(p)
262         except ValueError:
263             return None
264         if p < 0:
265             return None
266     return p
267
268 def get_content_length(request):
269     content_length = request.META.get('CONTENT_LENGTH')
270     if not content_length:
271         raise LengthRequired('Missing Content-Length header')
272     try:
273         content_length = int(content_length)
274         if content_length < 0:
275             raise ValueError
276     except ValueError:
277         raise BadRequest('Invalid Content-Length header')
278     return content_length
279
280 def get_range(request, size):
281     """Parse a Range header from the request.
282     
283     Either returns None, when the header is not existent or should be ignored,
284     or a list of (offset, length) tuples - should be further checked.
285     """
286     
287     ranges = request.META.get('HTTP_RANGE', '').replace(' ', '')
288     if not ranges.startswith('bytes='):
289         return None
290     
291     ret = []
292     for r in (x.strip() for x in ranges[6:].split(',')):
293         p = re.compile('^(?P<offset>\d*)-(?P<upto>\d*)$')
294         m = p.match(r)
295         if not m:
296             return None
297         offset = m.group('offset')
298         upto = m.group('upto')
299         if offset == '' and upto == '':
300             return None
301         
302         if offset != '':
303             offset = int(offset)
304             if upto != '':
305                 upto = int(upto)
306                 if offset > upto:
307                     return None
308                 ret.append((offset, upto - offset + 1))
309             else:
310                 ret.append((offset, size - offset))
311         else:
312             length = int(upto)
313             ret.append((size - length, length))
314     
315     return ret
316
317 def get_content_range(request):
318     """Parse a Content-Range header from the request.
319     
320     Either returns None, when the header is not existent or should be ignored,
321     or an (offset, length, total) tuple - check as length, total may be None.
322     Returns (None, None, None) if the provided range is '*/*'.
323     """
324     
325     ranges = request.META.get('HTTP_CONTENT_RANGE', '')
326     if not ranges:
327         return None
328     
329     p = re.compile('^bytes (?P<offset>\d+)-(?P<upto>\d*)/(?P<total>(\d+|\*))$')
330     m = p.match(ranges)
331     if not m:
332         if ranges == 'bytes */*':
333             return (None, None, None)
334         return None
335     offset = int(m.group('offset'))
336     upto = m.group('upto')
337     total = m.group('total')
338     if upto != '':
339         upto = int(upto)
340     else:
341         upto = None
342     if total != '*':
343         total = int(total)
344     else:
345         total = None
346     if (upto is not None and offset > upto) or \
347         (total is not None and offset >= total) or \
348         (total is not None and upto is not None and upto >= total):
349         return None
350     
351     if upto is None:
352         length = None
353     else:
354         length = upto - offset + 1
355     return (offset, length, total)
356
357 def get_sharing(request):
358     """Parse an X-Object-Sharing header from the request.
359     
360     Raises BadRequest on error.
361     """
362     
363     permissions = request.META.get('HTTP_X_OBJECT_SHARING')
364     if permissions is None:
365         return None
366     
367     ret = {}
368     permissions = permissions.replace(' ', '')
369     if permissions == '':
370         return ret
371     for perm in (x for x in permissions.split(';')):
372         if perm.startswith('read='):
373             ret['read'] = [v.replace(' ','') for v in perm[5:].split(',')]
374             ret['read'].remove('')
375             if '*' in ret['read']:
376                 ret['read'] = ['*']
377             if len(ret['read']) == 0:
378                 raise BadRequest('Bad X-Object-Sharing header value')
379         elif perm.startswith('write='):
380             ret['write'] = [v.replace(' ','') for v in perm[6:].split(',')]
381             ret['write'].remove('')
382             if '*' in ret['write']:
383                 ret['write'] = ['*']
384             if len(ret['write']) == 0:
385                 raise BadRequest('Bad X-Object-Sharing header value')
386         else:
387             raise BadRequest('Bad X-Object-Sharing header value')
388     return ret
389
390 def raw_input_socket(request):
391     """Return the socket for reading the rest of the request."""
392     
393     server_software = request.META.get('SERVER_SOFTWARE')
394     if not server_software:
395         if 'wsgi.input' in request.environ:
396             return request.environ['wsgi.input']
397         raise ServiceUnavailable('Unknown server software')
398     if server_software.startswith('WSGIServer'):
399         return request.environ['wsgi.input']
400     elif server_software.startswith('mod_python'):
401         return request._req
402     raise ServiceUnavailable('Unknown server software')
403
404 MAX_UPLOAD_SIZE = 10 * (1024 * 1024) # 10MB
405
406 def socket_read_iterator(sock, length=0, blocksize=4096):
407     """Return a maximum of blocksize data read from the socket in each iteration.
408     
409     Read up to 'length'. If 'length' is negative, will attempt a chunked read.
410     The maximum ammount of data read is controlled by MAX_UPLOAD_SIZE.
411     """
412     
413     if length < 0: # Chunked transfers
414         data = ''
415         while length < MAX_UPLOAD_SIZE:
416             # Get chunk size.
417             if hasattr(sock, 'readline'):
418                 chunk_length = sock.readline()
419             else:
420                 chunk_length = ''
421                 while chunk_length[-1:] != '\n':
422                     chunk_length += sock.read(1)
423                 chunk_length.strip()
424             pos = chunk_length.find(';')
425             if pos >= 0:
426                 chunk_length = chunk_length[:pos]
427             try:
428                 chunk_length = int(chunk_length, 16)
429             except Exception, e:
430                 raise BadRequest('Bad chunk size') # TODO: Change to something more appropriate.
431             # Check if done.
432             if chunk_length == 0:
433                 if len(data) > 0:
434                     yield data
435                 return
436             # Get the actual data.
437             while chunk_length > 0:
438                 chunk = sock.read(min(chunk_length, blocksize))
439                 chunk_length -= len(chunk)
440                 if length > 0:
441                     length += len(chunk)
442                 data += chunk
443                 if len(data) >= blocksize:
444                     ret = data[:blocksize]
445                     data = data[blocksize:]
446                     yield ret
447             sock.read(2) # CRLF
448         # TODO: Raise something to note that maximum size is reached.
449     else:
450         if length > MAX_UPLOAD_SIZE:
451             # TODO: Raise something to note that maximum size is reached.
452             pass
453         while length > 0:
454             data = sock.read(min(length, blocksize))
455             length -= len(data)
456             yield data
457
458 class ObjectWrapper(object):
459     """Return the object's data block-per-block in each iteration.
460     
461     Read from the object using the offset and length provided in each entry of the range list.
462     """
463     
464     def __init__(self, ranges, sizes, hashmaps, boundary):
465         self.ranges = ranges
466         self.sizes = sizes
467         self.hashmaps = hashmaps
468         self.boundary = boundary
469         self.size = sum(self.sizes)
470         
471         self.file_index = 0
472         self.block_index = 0
473         self.block_hash = -1
474         self.block = ''
475         
476         self.range_index = -1
477         self.offset, self.length = self.ranges[0]
478     
479     def __iter__(self):
480         return self
481     
482     def part_iterator(self):
483         if self.length > 0:
484             # Get the file for the current offset.
485             file_size = self.sizes[self.file_index]
486             while self.offset >= file_size:
487                 self.offset -= file_size
488                 self.file_index += 1
489                 file_size = self.sizes[self.file_index]
490             
491             # Get the block for the current position.
492             self.block_index = int(self.offset / backend.block_size)
493             if self.block_hash != self.hashmaps[self.file_index][self.block_index]:
494                 self.block_hash = self.hashmaps[self.file_index][self.block_index]
495                 try:
496                     self.block = backend.get_block(self.block_hash)
497                 except NameError:
498                     raise ItemNotFound('Block does not exist')
499             
500             # Get the data from the block.
501             bo = self.offset % backend.block_size
502             bl = min(self.length, len(self.block) - bo)
503             data = self.block[bo:bo + bl]
504             self.offset += bl
505             self.length -= bl
506             return data
507         else:
508             raise StopIteration
509     
510     def next(self):
511         if len(self.ranges) == 1:
512             return self.part_iterator()
513         if self.range_index == len(self.ranges):
514             raise StopIteration
515         try:
516             if self.range_index == -1:
517                 raise StopIteration
518             return self.part_iterator()
519         except StopIteration:
520             self.range_index += 1
521             out = []
522             if self.range_index < len(self.ranges):
523                 # Part header.
524                 self.offset, self.length = self.ranges[self.range_index]
525                 self.file_index = 0
526                 if self.range_index > 0:
527                     out.append('')
528                 out.append('--' + self.boundary)
529                 out.append('Content-Range: bytes %d-%d/%d' % (self.offset, self.offset + self.length - 1, self.size))
530                 out.append('Content-Transfer-Encoding: binary')
531                 out.append('')
532                 out.append('')
533                 return '\r\n'.join(out)
534             else:
535                 # Footer.
536                 out.append('')
537                 out.append('--' + self.boundary + '--')
538                 out.append('')
539                 return '\r\n'.join(out)
540
541 def object_data_response(request, sizes, hashmaps, meta, public=False):
542     """Get the HttpResponse object for replying with the object's data."""
543     
544     # Range handling.
545     size = sum(sizes)
546     ranges = get_range(request, size)
547     if ranges is None:
548         ranges = [(0, size)]
549         ret = 200
550     else:
551         check = [True for offset, length in ranges if
552                     length <= 0 or length > size or
553                     offset < 0 or offset >= size or
554                     offset + length > size]
555         if len(check) > 0:
556             raise RangeNotSatisfiable('Requested range exceeds object limits')        
557         ret = 206
558     
559     if ret == 206 and len(ranges) > 1:
560         boundary = uuid.uuid4().hex
561     else:
562         boundary = ''
563     wrapper = ObjectWrapper(ranges, sizes, hashmaps, boundary)
564     response = HttpResponse(wrapper, status=ret)
565     put_object_meta(response, meta, public)
566     if ret == 206:
567         if len(ranges) == 1:
568             offset, length = ranges[0]
569             response['Content-Length'] = length # Update with the correct length.
570             response['Content-Range'] = 'bytes %d-%d/%d' % (offset, offset + length - 1, size)
571         else:
572             del(response['Content-Length'])
573             response['Content-Type'] = 'multipart/byteranges; boundary=%s' % (boundary,)
574     return response
575
576 def put_object_block(hashmap, data, offset):
577     """Put one block of data at the given offset."""
578     
579     bi = int(offset / backend.block_size)
580     bo = offset % backend.block_size
581     bl = min(len(data), backend.block_size - bo)
582     if bi < len(hashmap):
583         hashmap[bi] = backend.update_block(hashmap[bi], data[:bl], bo)
584     else:
585         hashmap.append(backend.put_block(('\x00' * bo) + data[:bl]))
586     return bl # Return ammount of data written.
587
588 def hashmap_hash(hashmap):
589     """Produce the root hash, treating the hashmap as a Merkle-like tree."""
590     
591     def subhash(d):
592         h = hashlib.new(backend.hash_algorithm)
593         h.update(d)
594         return h.digest()
595     
596     if len(hashmap) == 0:
597         return hexlify(subhash(''))
598     if len(hashmap) == 1:
599         return hexlify(subhash(hashmap[0]))
600     s = 2
601     while s < len(hashmap):
602         s = s * 2
603     h = hashmap + ([('\x00' * len(hashmap[0]))] * (s - len(hashmap)))
604     h = [subhash(h[x] + (h[x + 1] if x + 1 < len(h) else '')) for x in range(0, len(h), 2)]
605     while len(h) > 1:
606         h = [subhash(h[x] + (h[x + 1] if x + 1 < len(h) else '')) for x in range(0, len(h), 2)]
607     return hexlify(h[0])
608
609 def update_response_headers(request, response):
610     if request.serialization == 'xml':
611         response['Content-Type'] = 'application/xml; charset=UTF-8'
612     elif request.serialization == 'json':
613         response['Content-Type'] = 'application/json; charset=UTF-8'
614     elif not response['Content-Type']:
615         response['Content-Type'] = 'text/plain; charset=UTF-8'
616
617     if settings.TEST:
618         response['Date'] = format_date_time(time())
619
620 def render_fault(request, fault):
621     if settings.DEBUG or settings.TEST:
622         fault.details = format_exc(fault)
623
624     request.serialization = 'text'
625     data = '\n'.join((fault.message, fault.details)) + '\n'
626     response = HttpResponse(data, status=fault.code)
627     update_response_headers(request, response)
628     return response
629
630 def request_serialization(request, format_allowed=False):
631     """Return the serialization format requested.
632     
633     Valid formats are 'text' and 'json', 'xml' if 'format_allowed' is True.
634     """
635     
636     if not format_allowed:
637         return 'text'
638     
639     format = request.GET.get('format')
640     if format == 'json':
641         return 'json'
642     elif format == 'xml':
643         return 'xml'
644     
645     for item in request.META.get('HTTP_ACCEPT', '').split(','):
646         accept, sep, rest = item.strip().partition(';')
647         if accept == 'application/json':
648             return 'json'
649         elif accept == 'application/xml' or accept == 'text/xml':
650             return 'xml'
651     
652     return 'text'
653
654 def api_method(http_method=None, format_allowed=False):
655     """Decorator function for views that implement an API method."""
656     
657     def decorator(func):
658         @wraps(func)
659         def wrapper(request, *args, **kwargs):
660             try:
661                 if http_method and request.method != http_method:
662                     raise BadRequest('Method not allowed.')
663                 
664                 # The args variable may contain up to (account, container, object).
665                 if len(args) > 1 and len(args[1]) > 256:
666                     raise BadRequest('Container name too large.')
667                 if len(args) > 2 and len(args[2]) > 1024:
668                     raise BadRequest('Object name too large.')
669                 
670                 # Fill in custom request variables.
671                 request.serialization = request_serialization(request, format_allowed)
672                 
673                 response = func(request, *args, **kwargs)
674                 update_response_headers(request, response)
675                 return response
676             except Fault, fault:
677                 return render_fault(request, fault)
678             except BaseException, e:
679                 logger.exception('Unexpected error: %s' % e)
680                 fault = ServiceUnavailable('Unexpected error')
681                 return render_fault(request, fault)
682         return wrapper
683     return decorator