Statistics
| Branch: | Tag: | Revision:

root / snf-pithos-app / pithos / api / util.py @ 29148653

History | View | Annotate | Download (41.3 kB)

1
# Copyright 2011-2012 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 datetime import datetime
36
from urllib import quote, unquote
37

    
38
from django.http import HttpResponse, Http404, HttpResponseForbidden
39
from django.template.loader import render_to_string
40
from django.utils import simplejson as json
41
from django.utils.http import http_date, parse_etags
42
from django.utils.encoding import smart_unicode, smart_str
43
from django.core.files.uploadhandler import FileUploadHandler
44
from django.core.files.uploadedfile import UploadedFile
45
from django.core.urlresolvers import reverse
46

    
47
from snf_django.lib.api.parsedate import parse_http_date_safe, parse_http_date
48
from snf_django.lib import api
49
from snf_django.lib.api import faults, utils
50

    
51
from pithos.api.settings import (BACKEND_DB_MODULE, BACKEND_DB_CONNECTION,
52
                                 BACKEND_BLOCK_MODULE, BACKEND_BLOCK_PATH,
53
                                 BACKEND_BLOCK_UMASK,
54
                                 BACKEND_QUEUE_MODULE, BACKEND_QUEUE_HOSTS,
55
                                 BACKEND_QUEUE_EXCHANGE,
56
                                 ASTAKOSCLIENT_POOLSIZE,
57
                                 SERVICE_TOKEN,
58
                                 ASTAKOS_BASE_URL,
59
                                 BACKEND_ACCOUNT_QUOTA,
60
                                 BACKEND_CONTAINER_QUOTA,
61
                                 BACKEND_VERSIONING, BACKEND_FREE_VERSIONING,
62
                                 BACKEND_POOL_ENABLED, BACKEND_POOL_SIZE,
63
                                 BACKEND_BLOCK_SIZE, BACKEND_HASH_ALGORITHM,
64
                                 RADOS_STORAGE, RADOS_POOL_BLOCKS,
65
                                 RADOS_POOL_MAPS, TRANSLATE_UUIDS,
66
                                 PUBLIC_URL_SECURITY, PUBLIC_URL_ALPHABET,
67
                                 COOKIE_NAME, BASE_HOST, UPDATE_MD5)
68
from pithos.api.resources import resources
69
from pithos.backends import connect_backend
70
from pithos.backends.base import (NotAllowedError, QuotaError, ItemNotExists,
71
                                  VersionNotExists)
72

    
73
from synnefo.lib import join_urls
74

    
75
from astakosclient import AstakosClient
76
from astakosclient.errors import NoUserName, NoUUID
77

    
78
import logging
79
import re
80
import hashlib
81
import uuid
82
import decimal
83

    
84
logger = logging.getLogger(__name__)
85

    
86

    
87
def json_encode_decimal(obj):
88
    if isinstance(obj, decimal.Decimal):
89
        return str(obj)
90
    raise TypeError(repr(obj) + " is not JSON serializable")
91

    
92

    
93
def rename_meta_key(d, old, new):
94
    if old not in d:
95
        return
96
    d[new] = d[old]
97
    del(d[old])
98

    
99

    
100
def printable_header_dict(d):
101
    """Format a meta dictionary for printing out json/xml.
102

103
    Convert all keys to lower case and replace dashes with underscores.
104
    Format 'last_modified' timestamp.
105
    """
106

    
107
    if 'last_modified' in d and d['last_modified']:
108
        d['last_modified'] = utils.isoformat(
109
            datetime.fromtimestamp(d['last_modified']))
110
    return dict([(k.lower().replace('-', '_'), v) for k, v in d.iteritems()])
111

    
112

    
113
def format_header_key(k):
114
    """Convert underscores to dashes and capitalize intra-dash strings."""
115
    return '-'.join([x.capitalize() for x in k.replace('_', '-').split('-')])
116

    
117

    
118
def get_header_prefix(request, prefix):
119
    """Get all prefix-* request headers in a dict.
120
       Reformat keys with format_header_key()."""
121

    
122
    prefix = 'HTTP_' + prefix.upper().replace('-', '_')
123
    # TODO: Document or remove '~' replacing.
124
    return dict([(format_header_key(k[5:]), v.replace('~', ''))
125
                for k, v in request.META.iteritems()
126
                if k.startswith(prefix) and len(k) > len(prefix)])
127

    
128

    
129
def check_meta_headers(meta):
130
    if len(meta) > 90:
131
        raise faults.BadRequest('Too many headers.')
132
    for k, v in meta.iteritems():
133
        if len(k) > 128:
134
            raise faults.BadRequest('Header name too large.')
135
        if len(v) > 256:
136
            raise faults.BadRequest('Header value too large.')
137

    
138

    
139
def get_account_headers(request):
140
    meta = get_header_prefix(request, 'X-Account-Meta-')
141
    check_meta_headers(meta)
142
    groups = {}
143
    for k, v in get_header_prefix(request, 'X-Account-Group-').iteritems():
144
        n = k[16:].lower()
145
        if '-' in n or '_' in n:
146
            raise faults.BadRequest('Bad characters in group name')
147
        groups[n] = v.replace(' ', '').split(',')
148
        while '' in groups[n]:
149
            groups[n].remove('')
150
    return meta, groups
151

    
152

    
153
def put_account_headers(response, meta, groups, policy):
154
    if 'count' in meta:
155
        response['X-Account-Container-Count'] = meta['count']
156
    if 'bytes' in meta:
157
        response['X-Account-Bytes-Used'] = meta['bytes']
158
    response['Last-Modified'] = http_date(int(meta['modified']))
159
    for k in [x for x in meta.keys() if x.startswith('X-Account-Meta-')]:
160
        response[smart_str(
161
            k, strings_only=True)] = smart_str(meta[k], strings_only=True)
162
    if 'until_timestamp' in meta:
163
        response['X-Account-Until-Timestamp'] = http_date(
164
            int(meta['until_timestamp']))
165
    for k, v in groups.iteritems():
166
        k = smart_str(k, strings_only=True)
167
        k = format_header_key('X-Account-Group-' + k)
168
        v = smart_str(','.join(v), strings_only=True)
169
        response[k] = v
170
    for k, v in policy.iteritems():
171
        response[smart_str(format_header_key('X-Account-Policy-' + k),
172
                 strings_only=True)] = smart_str(v, strings_only=True)
173

    
174

    
175
def get_container_headers(request):
176
    meta = get_header_prefix(request, 'X-Container-Meta-')
177
    check_meta_headers(meta)
178
    policy = dict([(k[19:].lower(), v.replace(' ', '')) for k, v in
179
                  get_header_prefix(request,
180
                                    'X-Container-Policy-').iteritems()])
181
    return meta, policy
182

    
183

    
184
def put_container_headers(request, response, meta, policy):
185
    if 'count' in meta:
186
        response['X-Container-Object-Count'] = meta['count']
187
    if 'bytes' in meta:
188
        response['X-Container-Bytes-Used'] = meta['bytes']
189
    response['Last-Modified'] = http_date(int(meta['modified']))
190
    for k in [x for x in meta.keys() if x.startswith('X-Container-Meta-')]:
191
        response[smart_str(
192
            k, strings_only=True)] = smart_str(meta[k], strings_only=True)
193
    l = [smart_str(x, strings_only=True) for x in meta['object_meta']
194
         if x.startswith('X-Object-Meta-')]
195
    response['X-Container-Object-Meta'] = ','.join([x[14:] for x in l])
196
    response['X-Container-Block-Size'] = request.backend.block_size
197
    response['X-Container-Block-Hash'] = request.backend.hash_algorithm
198
    if 'until_timestamp' in meta:
199
        response['X-Container-Until-Timestamp'] = http_date(
200
            int(meta['until_timestamp']))
201
    for k, v in policy.iteritems():
202
        response[smart_str(format_header_key('X-Container-Policy-' + k),
203
                           strings_only=True)] = smart_str(v,
204
                                                           strings_only=True)
205

    
206

    
207
def get_object_headers(request):
208
    content_type = request.META.get('CONTENT_TYPE', None)
209
    meta = get_header_prefix(request, 'X-Object-Meta-')
210
    check_meta_headers(meta)
211
    if request.META.get('HTTP_CONTENT_ENCODING'):
212
        meta['Content-Encoding'] = request.META['HTTP_CONTENT_ENCODING']
213
    if request.META.get('HTTP_CONTENT_DISPOSITION'):
214
        meta['Content-Disposition'] = request.META['HTTP_CONTENT_DISPOSITION']
215
    if request.META.get('HTTP_X_OBJECT_MANIFEST'):
216
        meta['X-Object-Manifest'] = request.META['HTTP_X_OBJECT_MANIFEST']
217
    return content_type, meta, get_sharing(request), get_public(request)
218

    
219

    
220
def put_object_headers(response, meta, restricted=False, token=None):
221
    response['ETag'] = meta['hash'] if not UPDATE_MD5 else meta['checksum']
222
    response['Content-Length'] = meta['bytes']
223
    response.override_serialization = True
224
    response['Content-Type'] = meta.get('type', 'application/octet-stream')
225
    response['Last-Modified'] = http_date(int(meta['modified']))
226
    if not restricted:
227
        response['X-Object-Hash'] = meta['hash']
228
        response['X-Object-UUID'] = meta['uuid']
229
        if TRANSLATE_UUIDS:
230
            meta['modified_by'] = \
231
                retrieve_displayname(token, meta['modified_by'])
232
        response['X-Object-Modified-By'] = smart_str(
233
            meta['modified_by'], strings_only=True)
234
        response['X-Object-Version'] = meta['version']
235
        response['X-Object-Version-Timestamp'] = http_date(
236
            int(meta['version_timestamp']))
237
        for k in [x for x in meta.keys() if x.startswith('X-Object-Meta-')]:
238
            response[smart_str(
239
                k, strings_only=True)] = smart_str(meta[k], strings_only=True)
240
        for k in (
241
            'Content-Encoding', 'Content-Disposition', 'X-Object-Manifest',
242
            'X-Object-Sharing', 'X-Object-Shared-By', 'X-Object-Allowed-To',
243
                'X-Object-Public'):
244
            if k in meta:
245
                response[k] = smart_str(meta[k], strings_only=True)
246
    else:
247
        for k in ('Content-Encoding', 'Content-Disposition'):
248
            if k in meta:
249
                response[k] = smart_str(meta[k], strings_only=True)
250

    
251

    
252
def update_manifest_meta(request, v_account, meta):
253
    """Update metadata if the object has an X-Object-Manifest."""
254

    
255
    if 'X-Object-Manifest' in meta:
256
        etag = ''
257
        bytes = 0
258
        try:
259
            src_container, src_name = split_container_object_string(
260
                '/' + meta['X-Object-Manifest'])
261
            objects = request.backend.list_objects(
262
                request.user_uniq, v_account,
263
                src_container, prefix=src_name, virtual=False)
264
            for x in objects:
265
                src_meta = request.backend.get_object_meta(
266
                    request.user_uniq, v_account, src_container, x[0],
267
                    'pithos', x[1])
268
                etag += (src_meta['hash'] if not UPDATE_MD5 else
269
                         src_meta['checksum'])
270
                bytes += src_meta['bytes']
271
        except:
272
            # Ignore errors.
273
            return
274
        meta['bytes'] = bytes
275
        md5 = hashlib.md5()
276
        md5.update(etag)
277
        meta['checksum'] = md5.hexdigest().lower()
278

    
279

    
280
def is_uuid(str):
281
    if str is None:
282
        return False
283
    try:
284
        uuid.UUID(str)
285
    except ValueError:
286
        return False
287
    else:
288
        return True
289

    
290

    
291
##########################
292
# USER CATALOG utilities #
293
##########################
294

    
295
def retrieve_displayname(token, uuid, fail_silently=True):
296
    astakos = AstakosClient(ASTAKOS_BASE_URL, retry=2, use_pool=True,
297
                            logger=logger)
298
    try:
299
        displayname = astakos.get_username(token, uuid)
300
    except NoUserName:
301
        if not fail_silently:
302
            raise ItemNotExists(uuid)
303
        else:
304
            # just return the uuid
305
            return uuid
306
    return displayname
307

    
308

    
309
def retrieve_displaynames(token, uuids, return_dict=False, fail_silently=True):
310
    astakos = AstakosClient(ASTAKOS_BASE_URL, retry=2, use_pool=True,
311
                            logger=logger)
312
    catalog = astakos.get_usernames(token, uuids) or {}
313
    missing = list(set(uuids) - set(catalog))
314
    if missing and not fail_silently:
315
        raise ItemNotExists('Unknown displaynames: %s' % ', '.join(missing))
316
    return catalog if return_dict else [catalog.get(i) for i in uuids]
317

    
318

    
319
def retrieve_uuid(token, displayname):
320
    if is_uuid(displayname):
321
        return displayname
322

    
323
    astakos = AstakosClient(ASTAKOS_BASE_URL, retry=2, use_pool=True,
324
                            logger=logger)
325
    try:
326
        uuid = astakos.get_uuid(token, displayname)
327
    except NoUUID:
328
        raise ItemNotExists(displayname)
329
    return uuid
330

    
331

    
332
def retrieve_uuids(token, displaynames, return_dict=False, fail_silently=True):
333
    astakos = AstakosClient(ASTAKOS_BASE_URL, retry=2, use_pool=True,
334
                            logger=logger)
335
    catalog = astakos.get_uuids(token, displaynames) or {}
336
    missing = list(set(displaynames) - set(catalog))
337
    if missing and not fail_silently:
338
        raise ItemNotExists('Unknown uuids: %s' % ', '.join(missing))
339
    return catalog if return_dict else [catalog.get(i) for i in displaynames]
340

    
341

    
342
def replace_permissions_displayname(token, holder):
343
    if holder == '*':
344
        return holder
345
    try:
346
        # check first for a group permission
347
        account, group = holder.split(':', 1)
348
    except ValueError:
349
        return retrieve_uuid(token, holder)
350
    else:
351
        return ':'.join([retrieve_uuid(token, account), group])
352

    
353

    
354
def replace_permissions_uuid(token, holder):
355
    if holder == '*':
356
        return holder
357
    try:
358
        # check first for a group permission
359
        account, group = holder.split(':', 1)
360
    except ValueError:
361
        return retrieve_displayname(token, holder)
362
    else:
363
        return ':'.join([retrieve_displayname(token, account), group])
364

    
365

    
366
def update_sharing_meta(request, permissions, v_account,
367
                        v_container, v_object, meta):
368
    if permissions is None:
369
        return
370
    allowed, perm_path, perms = permissions
371
    if len(perms) == 0:
372
        return
373

    
374
    # replace uuid with displayname
375
    if TRANSLATE_UUIDS:
376
        perms['read'] = [replace_permissions_uuid(
377
            getattr(request, 'token', None), x)
378
            for x in perms.get('read', [])]
379
        perms['write'] = [replace_permissions_uuid(
380
            getattr(request, 'token', None), x)
381
            for x in perms.get('write', [])]
382

    
383
    ret = []
384

    
385
    r = ','.join(perms.get('read', []))
386
    if r:
387
        ret.append('read=' + r)
388
    w = ','.join(perms.get('write', []))
389
    if w:
390
        ret.append('write=' + w)
391
    meta['X-Object-Sharing'] = '; '.join(ret)
392
    if '/'.join((v_account, v_container, v_object)) != perm_path:
393
        meta['X-Object-Shared-By'] = perm_path
394
    if request.user_uniq != v_account:
395
        meta['X-Object-Allowed-To'] = allowed
396

    
397

    
398
def update_public_meta(public, meta):
399
    if not public:
400
        return
401
    meta['X-Object-Public'] = join_urls(
402
        BASE_HOST, reverse('pithos.api.public.public_demux', args=(public,)))
403

    
404

    
405
def validate_modification_preconditions(request, meta):
406
    """Check the modified timestamp conforms with the preconditions set."""
407

    
408
    if 'modified' not in meta:
409
        return  # TODO: Always return?
410

    
411
    if_modified_since = request.META.get('HTTP_IF_MODIFIED_SINCE')
412
    if if_modified_since is not None:
413
        if_modified_since = parse_http_date_safe(if_modified_since)
414
    if (if_modified_since is not None
415
            and int(meta['modified']) <= if_modified_since):
416
        raise faults.NotModified('Resource has not been modified')
417

    
418
    if_unmodified_since = request.META.get('HTTP_IF_UNMODIFIED_SINCE')
419
    if if_unmodified_since is not None:
420
        if_unmodified_since = parse_http_date_safe(if_unmodified_since)
421
    if (if_unmodified_since is not None
422
            and int(meta['modified']) > if_unmodified_since):
423
        raise faults.PreconditionFailed('Resource has been modified')
424

    
425

    
426
def validate_matching_preconditions(request, meta):
427
    """Check that the ETag conforms with the preconditions set."""
428

    
429
    etag = meta['hash'] if not UPDATE_MD5 else meta['checksum']
430
    if not etag:
431
        etag = None
432

    
433
    if_match = request.META.get('HTTP_IF_MATCH')
434
    if if_match is not None:
435
        if etag is None:
436
            raise faults.PreconditionFailed('Resource does not exist')
437
        if (if_match != '*'
438
                and etag not in [x.lower() for x in parse_etags(if_match)]):
439
            raise faults.PreconditionFailed('Resource ETag does not match')
440

    
441
    if_none_match = request.META.get('HTTP_IF_NONE_MATCH')
442
    if if_none_match is not None:
443
        # TODO: If this passes, must ignore If-Modified-Since header.
444
        if etag is not None:
445
            if (if_none_match == '*' or etag in [x.lower() for x in
446
                                                 parse_etags(if_none_match)]):
447
                # TODO: Continue if an If-Modified-Since header is present.
448
                if request.method in ('HEAD', 'GET'):
449
                    raise faults.NotModified('Resource ETag matches')
450
                raise faults.PreconditionFailed(
451
                    'Resource exists or ETag matches')
452

    
453

    
454
def split_container_object_string(s):
455
    if not len(s) > 0 or s[0] != '/':
456
        raise ValueError
457
    s = s[1:]
458
    pos = s.find('/')
459
    if pos == -1 or pos == len(s) - 1:
460
        raise ValueError
461
    return s[:pos], s[(pos + 1):]
462

    
463

    
464
def copy_or_move_object(request, src_account, src_container, src_name,
465
                        dest_account, dest_container, dest_name,
466
                        move=False, delimiter=None):
467
    """Copy or move an object."""
468

    
469
    if 'ignore_content_type' in request.GET and 'CONTENT_TYPE' in request.META:
470
        del(request.META['CONTENT_TYPE'])
471
    content_type, meta, permissions, public = get_object_headers(request)
472
    src_version = request.META.get('HTTP_X_SOURCE_VERSION')
473
    try:
474
        if move:
475
            version_id = request.backend.move_object(
476
                request.user_uniq, src_account, src_container, src_name,
477
                dest_account, dest_container, dest_name,
478
                content_type, 'pithos', meta, False, permissions, delimiter)
479
        else:
480
            version_id = request.backend.copy_object(
481
                request.user_uniq, src_account, src_container, src_name,
482
                dest_account, dest_container, dest_name,
483
                content_type, 'pithos', meta, False, permissions,
484
                src_version, delimiter)
485
    except NotAllowedError:
486
        raise faults.Forbidden('Not allowed')
487
    except (ItemNotExists, VersionNotExists):
488
        raise faults.ItemNotFound('Container or object does not exist')
489
    except ValueError:
490
        raise faults.BadRequest('Invalid sharing header')
491
    except QuotaError, e:
492
        raise faults.RequestEntityTooLarge('Quota error: %s' % e)
493
    if public is not None:
494
        try:
495
            request.backend.update_object_public(
496
                request.user_uniq, dest_account,
497
                dest_container, dest_name, public)
498
        except NotAllowedError:
499
            raise faults.Forbidden('Not allowed')
500
        except ItemNotExists:
501
            raise faults.ItemNotFound('Object does not exist')
502
    return version_id
503

    
504

    
505
def get_int_parameter(p):
506
    if p is not None:
507
        try:
508
            p = int(p)
509
        except ValueError:
510
            return None
511
        if p < 0:
512
            return None
513
    return p
514

    
515

    
516
def get_content_length(request):
517
    content_length = get_int_parameter(request.META.get('CONTENT_LENGTH'))
518
    if content_length is None:
519
        raise faults.LengthRequired('Missing or invalid Content-Length header')
520
    return content_length
521

    
522

    
523
def get_range(request, size):
524
    """Parse a Range header from the request.
525

526
    Either returns None, when the header is not existent or should be ignored,
527
    or a list of (offset, length) tuples - should be further checked.
528
    """
529

    
530
    ranges = request.META.get('HTTP_RANGE', '').replace(' ', '')
531
    if not ranges.startswith('bytes='):
532
        return None
533

    
534
    ret = []
535
    for r in (x.strip() for x in ranges[6:].split(',')):
536
        p = re.compile('^(?P<offset>\d*)-(?P<upto>\d*)$')
537
        m = p.match(r)
538
        if not m:
539
            return None
540
        offset = m.group('offset')
541
        upto = m.group('upto')
542
        if offset == '' and upto == '':
543
            return None
544

    
545
        if offset != '':
546
            offset = int(offset)
547
            if upto != '':
548
                upto = int(upto)
549
                if offset > upto:
550
                    return None
551
                ret.append((offset, upto - offset + 1))
552
            else:
553
                ret.append((offset, size - offset))
554
        else:
555
            length = int(upto)
556
            ret.append((size - length, length))
557

    
558
    return ret
559

    
560

    
561
def get_content_range(request):
562
    """Parse a Content-Range header from the request.
563

564
    Either returns None, when the header is not existent or should be ignored,
565
    or an (offset, length, total) tuple - check as length, total may be None.
566
    Returns (None, None, None) if the provided range is '*/*'.
567
    """
568

    
569
    ranges = request.META.get('HTTP_CONTENT_RANGE', '')
570
    if not ranges:
571
        return None
572

    
573
    p = re.compile('^bytes (?P<offset>\d+)-(?P<upto>\d*)/(?P<total>(\d+|\*))$')
574
    m = p.match(ranges)
575
    if not m:
576
        if ranges == 'bytes */*':
577
            return (None, None, None)
578
        return None
579
    offset = int(m.group('offset'))
580
    upto = m.group('upto')
581
    total = m.group('total')
582
    if upto != '':
583
        upto = int(upto)
584
    else:
585
        upto = None
586
    if total != '*':
587
        total = int(total)
588
    else:
589
        total = None
590
    if (upto is not None and offset > upto) or \
591
        (total is not None and offset >= total) or \
592
            (total is not None and upto is not None and upto >= total):
593
        return None
594

    
595
    if upto is None:
596
        length = None
597
    else:
598
        length = upto - offset + 1
599
    return (offset, length, total)
600

    
601

    
602
def get_sharing(request):
603
    """Parse an X-Object-Sharing header from the request.
604

605
    Raises BadRequest on error.
606
    """
607

    
608
    permissions = request.META.get('HTTP_X_OBJECT_SHARING')
609
    if permissions is None:
610
        return None
611

    
612
    # TODO: Document or remove '~' replacing.
613
    permissions = permissions.replace('~', '')
614

    
615
    ret = {}
616
    permissions = permissions.replace(' ', '')
617
    if permissions == '':
618
        return ret
619
    for perm in (x for x in permissions.split(';')):
620
        if perm.startswith('read='):
621
            ret['read'] = list(set(
622
                [v.replace(' ', '').lower() for v in perm[5:].split(',')]))
623
            if '' in ret['read']:
624
                ret['read'].remove('')
625
            if '*' in ret['read']:
626
                ret['read'] = ['*']
627
            if len(ret['read']) == 0:
628
                raise faults.BadRequest(
629
                    'Bad X-Object-Sharing header value: invalid length')
630
        elif perm.startswith('write='):
631
            ret['write'] = list(set(
632
                [v.replace(' ', '').lower() for v in perm[6:].split(',')]))
633
            if '' in ret['write']:
634
                ret['write'].remove('')
635
            if '*' in ret['write']:
636
                ret['write'] = ['*']
637
            if len(ret['write']) == 0:
638
                raise faults.BadRequest(
639
                    'Bad X-Object-Sharing header value: invalid length')
640
        else:
641
            raise faults.BadRequest(
642
                'Bad X-Object-Sharing header value: missing prefix')
643

    
644
    # replace displayname with uuid
645
    if TRANSLATE_UUIDS:
646
        try:
647
            ret['read'] = [replace_permissions_displayname(
648
                getattr(request, 'token', None), x)
649
                for x in ret.get('read', [])]
650
            ret['write'] = [replace_permissions_displayname(
651
                getattr(request, 'token', None), x)
652
                for x in ret.get('write', [])]
653
        except ItemNotExists, e:
654
            raise faults.BadRequest(
655
                'Bad X-Object-Sharing header value: unknown account: %s' % e)
656

    
657
    # Keep duplicates only in write list.
658
    dups = [x for x in ret.get(
659
        'read', []) if x in ret.get('write', []) and x != '*']
660
    if dups:
661
        for x in dups:
662
            ret['read'].remove(x)
663
        if len(ret['read']) == 0:
664
            del(ret['read'])
665

    
666
    return ret
667

    
668

    
669
def get_public(request):
670
    """Parse an X-Object-Public header from the request.
671

672
    Raises BadRequest on error.
673
    """
674

    
675
    public = request.META.get('HTTP_X_OBJECT_PUBLIC')
676
    if public is None:
677
        return None
678

    
679
    public = public.replace(' ', '').lower()
680
    if public == 'true':
681
        return True
682
    elif public == 'false' or public == '':
683
        return False
684
    raise faults.BadRequest('Bad X-Object-Public header value')
685

    
686

    
687
def raw_input_socket(request):
688
    """Return the socket for reading the rest of the request."""
689

    
690
    server_software = request.META.get('SERVER_SOFTWARE')
691
    if server_software and server_software.startswith('mod_python'):
692
        return request._req
693
    if 'wsgi.input' in request.environ:
694
        return request.environ['wsgi.input']
695
    raise NotImplemented('Unknown server software')
696

    
697
MAX_UPLOAD_SIZE = 5 * (1024 * 1024 * 1024)  # 5GB
698

    
699

    
700
def socket_read_iterator(request, length=0, blocksize=4096):
701
    """Return maximum of blocksize data read from the socket in each iteration
702

703
    Read up to 'length'. If 'length' is negative, will attempt a chunked read.
704
    The maximum ammount of data read is controlled by MAX_UPLOAD_SIZE.
705
    """
706

    
707
    sock = raw_input_socket(request)
708
    if length < 0:  # Chunked transfers
709
        # Small version (server does the dechunking).
710
        if (request.environ.get('mod_wsgi.input_chunked', None)
711
                or request.META['SERVER_SOFTWARE'].startswith('gunicorn')):
712
            while length < MAX_UPLOAD_SIZE:
713
                data = sock.read(blocksize)
714
                if data == '':
715
                    return
716
                yield data
717
            raise faults.BadRequest('Maximum size is reached')
718

    
719
        # Long version (do the dechunking).
720
        data = ''
721
        while length < MAX_UPLOAD_SIZE:
722
            # Get chunk size.
723
            if hasattr(sock, 'readline'):
724
                chunk_length = sock.readline()
725
            else:
726
                chunk_length = ''
727
                while chunk_length[-1:] != '\n':
728
                    chunk_length += sock.read(1)
729
                chunk_length.strip()
730
            pos = chunk_length.find(';')
731
            if pos >= 0:
732
                chunk_length = chunk_length[:pos]
733
            try:
734
                chunk_length = int(chunk_length, 16)
735
            except Exception:
736
                raise faults.BadRequest('Bad chunk size')
737
                                 # TODO: Change to something more appropriate.
738
            # Check if done.
739
            if chunk_length == 0:
740
                if len(data) > 0:
741
                    yield data
742
                return
743
            # Get the actual data.
744
            while chunk_length > 0:
745
                chunk = sock.read(min(chunk_length, blocksize))
746
                chunk_length -= len(chunk)
747
                if length > 0:
748
                    length += len(chunk)
749
                data += chunk
750
                if len(data) >= blocksize:
751
                    ret = data[:blocksize]
752
                    data = data[blocksize:]
753
                    yield ret
754
            sock.read(2)  # CRLF
755
        raise faults.BadRequest('Maximum size is reached')
756
    else:
757
        if length > MAX_UPLOAD_SIZE:
758
            raise faults.BadRequest('Maximum size is reached')
759
        while length > 0:
760
            data = sock.read(min(length, blocksize))
761
            if not data:
762
                raise faults.BadRequest()
763
            length -= len(data)
764
            yield data
765

    
766

    
767
class SaveToBackendHandler(FileUploadHandler):
768
    """Handle a file from an HTML form the django way."""
769

    
770
    def __init__(self, request=None):
771
        super(SaveToBackendHandler, self).__init__(request)
772
        self.backend = request.backend
773

    
774
    def put_data(self, length):
775
        if len(self.data) >= length:
776
            block = self.data[:length]
777
            self.file.hashmap.append(self.backend.put_block(block))
778
            self.checksum_compute.update(block)
779
            self.data = self.data[length:]
780

    
781
    def new_file(self, field_name, file_name, content_type,
782
                 content_length, charset=None):
783
        self.checksum_compute = NoChecksum() if not UPDATE_MD5 else Checksum()
784
        self.data = ''
785
        self.file = UploadedFile(
786
            name=file_name, content_type=content_type, charset=charset)
787
        self.file.size = 0
788
        self.file.hashmap = []
789

    
790
    def receive_data_chunk(self, raw_data, start):
791
        self.data += raw_data
792
        self.file.size += len(raw_data)
793
        self.put_data(self.request.backend.block_size)
794
        return None
795

    
796
    def file_complete(self, file_size):
797
        l = len(self.data)
798
        if l > 0:
799
            self.put_data(l)
800
        self.file.etag = self.checksum_compute.hexdigest()
801
        return self.file
802

    
803

    
804
class ObjectWrapper(object):
805
    """Return the object's data block-per-block in each iteration.
806

807
    Read from the object using the offset and length provided
808
    in each entry of the range list.
809
    """
810

    
811
    def __init__(self, backend, ranges, sizes, hashmaps, boundary):
812
        self.backend = backend
813
        self.ranges = ranges
814
        self.sizes = sizes
815
        self.hashmaps = hashmaps
816
        self.boundary = boundary
817
        self.size = sum(self.sizes)
818

    
819
        self.file_index = 0
820
        self.block_index = 0
821
        self.block_hash = -1
822
        self.block = ''
823

    
824
        self.range_index = -1
825
        self.offset, self.length = self.ranges[0]
826

    
827
    def __iter__(self):
828
        return self
829

    
830
    def part_iterator(self):
831
        if self.length > 0:
832
            # Get the file for the current offset.
833
            file_size = self.sizes[self.file_index]
834
            while self.offset >= file_size:
835
                self.offset -= file_size
836
                self.file_index += 1
837
                file_size = self.sizes[self.file_index]
838

    
839
            # Get the block for the current position.
840
            self.block_index = int(self.offset / self.backend.block_size)
841
            if self.block_hash != \
842
                    self.hashmaps[self.file_index][self.block_index]:
843
                self.block_hash = self.hashmaps[
844
                    self.file_index][self.block_index]
845
                try:
846
                    self.block = self.backend.get_block(self.block_hash)
847
                except ItemNotExists:
848
                    raise faults.ItemNotFound('Block does not exist')
849

    
850
            # Get the data from the block.
851
            bo = self.offset % self.backend.block_size
852
            bs = self.backend.block_size
853
            if (self.block_index == len(self.hashmaps[self.file_index]) - 1 and
854
                    self.sizes[self.file_index] % self.backend.block_size):
855
                bs = self.sizes[self.file_index] % self.backend.block_size
856
            bl = min(self.length, bs - bo)
857
            data = self.block[bo:bo + bl]
858
            self.offset += bl
859
            self.length -= bl
860
            return data
861
        else:
862
            raise StopIteration
863

    
864
    def next(self):
865
        if len(self.ranges) == 1:
866
            return self.part_iterator()
867
        if self.range_index == len(self.ranges):
868
            raise StopIteration
869
        try:
870
            if self.range_index == -1:
871
                raise StopIteration
872
            return self.part_iterator()
873
        except StopIteration:
874
            self.range_index += 1
875
            out = []
876
            if self.range_index < len(self.ranges):
877
                # Part header.
878
                self.offset, self.length = self.ranges[self.range_index]
879
                self.file_index = 0
880
                if self.range_index > 0:
881
                    out.append('')
882
                out.append('--' + self.boundary)
883
                out.append('Content-Range: bytes %d-%d/%d' % (
884
                    self.offset, self.offset + self.length - 1, self.size))
885
                out.append('Content-Transfer-Encoding: binary')
886
                out.append('')
887
                out.append('')
888
                return '\r\n'.join(out)
889
            else:
890
                # Footer.
891
                out.append('')
892
                out.append('--' + self.boundary + '--')
893
                out.append('')
894
                return '\r\n'.join(out)
895

    
896

    
897
def object_data_response(request, sizes, hashmaps, meta, public=False):
898
    """Get the HttpResponse object for replying with the object's data."""
899

    
900
    # Range handling.
901
    size = sum(sizes)
902
    ranges = get_range(request, size)
903
    if ranges is None:
904
        ranges = [(0, size)]
905
        ret = 200
906
    else:
907
        check = [True for offset, length in ranges if
908
                 length <= 0 or length > size or
909
                 offset < 0 or offset >= size or
910
                 offset + length > size]
911
        if len(check) > 0:
912
            raise faults.RangeNotSatisfiable(
913
                'Requested range exceeds object limits')
914
        ret = 206
915
        if_range = request.META.get('HTTP_IF_RANGE')
916
        if if_range:
917
            try:
918
                # Modification time has passed instead.
919
                last_modified = parse_http_date(if_range)
920
                if last_modified != meta['modified']:
921
                    ranges = [(0, size)]
922
                    ret = 200
923
            except ValueError:
924
                if if_range != meta['checksum']:
925
                    ranges = [(0, size)]
926
                    ret = 200
927

    
928
    if ret == 206 and len(ranges) > 1:
929
        boundary = uuid.uuid4().hex
930
    else:
931
        boundary = ''
932
    wrapper = ObjectWrapper(request.backend, ranges, sizes, hashmaps, boundary)
933
    response = HttpResponse(wrapper, status=ret)
934
    put_object_headers(
935
        response, meta, restricted=public,
936
        token=getattr(request, 'token', None))
937
    if ret == 206:
938
        if len(ranges) == 1:
939
            offset, length = ranges[0]
940
            response[
941
                'Content-Length'] = length  # Update with the correct length.
942
            response['Content-Range'] = 'bytes %d-%d/%d' % (
943
                offset, offset + length - 1, size)
944
        else:
945
            del(response['Content-Length'])
946
            response['Content-Type'] = 'multipart/byteranges; boundary=%s' % (
947
                boundary,)
948
    return response
949

    
950

    
951
def put_object_block(request, hashmap, data, offset):
952
    """Put one block of data at the given offset."""
953

    
954
    bi = int(offset / request.backend.block_size)
955
    bo = offset % request.backend.block_size
956
    bl = min(len(data), request.backend.block_size - bo)
957
    if bi < len(hashmap):
958
        hashmap[bi] = request.backend.update_block(hashmap[bi], data[:bl], bo)
959
    else:
960
        hashmap.append(request.backend.put_block(('\x00' * bo) + data[:bl]))
961
    return bl  # Return ammount of data written.
962

    
963

    
964
def hashmap_md5(backend, hashmap, size):
965
    """Produce the MD5 sum from the data in the hashmap."""
966

    
967
    # TODO: Search backend for the MD5 of another object
968
    #       with the same hashmap and size...
969
    md5 = hashlib.md5()
970
    bs = backend.block_size
971
    for bi, hash in enumerate(hashmap):
972
        data = backend.get_block(hash)  # Blocks come in padded.
973
        if bi == len(hashmap) - 1:
974
            data = data[:size % bs]
975
        md5.update(data)
976
    return md5.hexdigest().lower()
977

    
978

    
979
def simple_list_response(request, l):
980
    if request.serialization == 'text':
981
        return '\n'.join(l) + '\n'
982
    if request.serialization == 'xml':
983
        return render_to_string('items.xml', {'items': l})
984
    if request.serialization == 'json':
985
        return json.dumps(l)
986

    
987

    
988
from pithos.backends.util import PithosBackendPool
989

    
990
if RADOS_STORAGE:
991
    BLOCK_PARAMS = {'mappool': RADOS_POOL_MAPS,
992
                    'blockpool': RADOS_POOL_BLOCKS, }
993
else:
994
    BLOCK_PARAMS = {'mappool': None,
995
                    'blockpool': None, }
996

    
997
BACKEND_KWARGS = dict(
998
    db_module=BACKEND_DB_MODULE,
999
    db_connection=BACKEND_DB_CONNECTION,
1000
    block_module=BACKEND_BLOCK_MODULE,
1001
    block_path=BACKEND_BLOCK_PATH,
1002
    block_umask=BACKEND_BLOCK_UMASK,
1003
    block_size=BACKEND_BLOCK_SIZE,
1004
    hash_algorithm=BACKEND_HASH_ALGORITHM,
1005
    queue_module=BACKEND_QUEUE_MODULE,
1006
    queue_hosts=BACKEND_QUEUE_HOSTS,
1007
    queue_exchange=BACKEND_QUEUE_EXCHANGE,
1008
    astakos_url=ASTAKOS_BASE_URL,
1009
    service_token=SERVICE_TOKEN,
1010
    astakosclient_poolsize=ASTAKOSCLIENT_POOLSIZE,
1011
    free_versioning=BACKEND_FREE_VERSIONING,
1012
    block_params=BLOCK_PARAMS,
1013
    public_url_security=PUBLIC_URL_SECURITY,
1014
    public_url_alphabet=PUBLIC_URL_ALPHABET,
1015
    account_quota_policy=BACKEND_ACCOUNT_QUOTA,
1016
    container_quota_policy=BACKEND_CONTAINER_QUOTA,
1017
    container_versioning_policy=BACKEND_VERSIONING)
1018

    
1019
_pithos_backend_pool = PithosBackendPool(size=BACKEND_POOL_SIZE,
1020
                                         **BACKEND_KWARGS)
1021

    
1022

    
1023
def get_backend():
1024
    if BACKEND_POOL_ENABLED:
1025
        backend = _pithos_backend_pool.pool_get()
1026
    else:
1027
        backend = connect_backend(**BACKEND_KWARGS)
1028
    backend.serials = []
1029
    backend.messages = []
1030
    return backend
1031

    
1032

    
1033
def update_request_headers(request):
1034
    # Handle URL-encoded keys and values.
1035
    meta = dict([(
1036
        k, v) for k, v in request.META.iteritems() if k.startswith('HTTP_')])
1037
    for k, v in meta.iteritems():
1038
        try:
1039
            k.decode('ascii')
1040
            v.decode('ascii')
1041
        except UnicodeDecodeError:
1042
            raise faults.BadRequest('Bad character in headers.')
1043
        if '%' in k or '%' in v:
1044
            del(request.META[k])
1045
            request.META[unquote(k)] = smart_unicode(unquote(
1046
                v), strings_only=True)
1047

    
1048

    
1049
def update_response_headers(request, response):
1050
    # URL-encode unicode in headers.
1051
    meta = response.items()
1052
    for k, v in meta:
1053
        if (k.startswith('X-Account-') or k.startswith('X-Container-') or
1054
                k.startswith('X-Object-') or k.startswith('Content-')):
1055
            del(response[k])
1056
            response[quote(k)] = quote(v, safe='/=,:@; ')
1057

    
1058

    
1059
def get_pithos_usage(token):
1060
    """Get Pithos Usage from astakos."""
1061
    astakos = AstakosClient(ASTAKOS_BASE_URL, retry=2, use_pool=True,
1062
                            logger=logger)
1063
    quotas = astakos.get_quotas(token)['system']
1064
    pithos_resources = [r['name'] for r in resources]
1065
    map(quotas.pop, filter(lambda k: k not in pithos_resources, quotas.keys()))
1066
    return quotas.popitem()[-1]  # assume only one resource
1067

    
1068

    
1069
def api_method(http_method=None, token_required=True, user_required=True,
1070
               logger=None, format_allowed=False, serializations=None,
1071
               strict_serlization=False, lock_container_path=False):
1072
    serializations = serializations or ['json', 'xml']
1073

    
1074
    def decorator(func):
1075
        @api.api_method(http_method=http_method, token_required=token_required,
1076
                        user_required=user_required,
1077
                        logger=logger, format_allowed=format_allowed,
1078
                        astakos_url=ASTAKOS_BASE_URL,
1079
                        serializations=serializations,
1080
                        strict_serlization=strict_serlization)
1081
        @wraps(func)
1082
        def wrapper(request, *args, **kwargs):
1083
            # The args variable may contain up to (account, container, object).
1084
            if len(args) > 1 and len(args[1]) > 256:
1085
                raise faults.BadRequest("Container name too large")
1086
            if len(args) > 2 and len(args[2]) > 1024:
1087
                raise faults.BadRequest('Object name too large.')
1088

    
1089
            success_status = False
1090
            try:
1091
                # Add a PithosBackend as attribute of the request object
1092
                request.backend = get_backend()
1093
                request.backend.pre_exec(lock_container_path)
1094

    
1095
                # Many API method expect thet X-Auth-Token in request,token
1096
                request.token = request.x_auth_token
1097
                update_request_headers(request)
1098
                response = func(request, *args, **kwargs)
1099
                update_response_headers(request, response)
1100

    
1101
                success_status = True
1102
                return response
1103
            finally:
1104
                # Always close PithosBackend connection
1105
                if getattr(request, "backend", None) is not None:
1106
                    request.backend.post_exec(success_status)
1107
                    request.backend.close()
1108
        return wrapper
1109
    return decorator
1110

    
1111

    
1112
def get_token_from_cookie(request):
1113
    assert(request.method == 'GET'),\
1114
        "Cookie based authentication is only allowed to GET requests"
1115
    token = None
1116
    if COOKIE_NAME in request.COOKIES:
1117
        cookie_value = unquote(request.COOKIES.get(COOKIE_NAME, ''))
1118
        account, sep, token = cookie_value.partition('|')
1119
    return token
1120

    
1121

    
1122
def view_method():
1123
    """Decorator function for views."""
1124

    
1125
    def decorator(func):
1126
        @wraps(func)
1127
        def wrapper(request, *args, **kwargs):
1128
            request.META['HTTP_X_AUTH_TOKEN'] = get_token_from_cookie(request)
1129
            # Get the response object
1130
            response = func(request, *args, **kwargs)
1131
            if response.status_code == 200:
1132
                return response
1133
            elif response.status_code == 404:
1134
                raise Http404()
1135
            elif response.status_code in [401, 403]:
1136
                return HttpResponseForbidden()
1137
            else:
1138
                raise Exception(response)
1139
        return wrapper
1140
    return decorator
1141

    
1142

    
1143
class Checksum:
1144
    def __init__(self):
1145
        self.md5 = hashlib.md5()
1146

    
1147
    def update(self, data):
1148
        self.md5.update(data)
1149

    
1150
    def hexdigest(self):
1151
        return self.md5.hexdigest().lower()
1152

    
1153

    
1154
class NoChecksum:
1155
    def update(self, data):
1156
        pass
1157

    
1158
    def hexdigest(self):
1159
        return ''