Statistics
| Branch: | Tag: | Revision:

root / snf-pithos-app / pithos / api / util.py @ 474e609a

History | View | Annotate | Download (46.1 kB)

1
# Copyright 2011-2013 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, urlencode
37
from urlparse import urlunsplit, urlsplit, parse_qsl
38

    
39
from django.http import (HttpResponse, Http404, HttpResponseRedirect,
40
                         HttpResponseNotAllowed)
41
from django.template.loader import render_to_string
42
from django.utils import simplejson as json
43
from django.utils.http import http_date, parse_etags
44
from django.utils.encoding import smart_unicode, smart_str
45
from django.core.files.uploadhandler import FileUploadHandler
46
from django.core.files.uploadedfile import UploadedFile
47
from django.core.urlresolvers import reverse
48
from django.core.exceptions import PermissionDenied
49

    
50
from snf_django.lib.api.parsedate import parse_http_date_safe, parse_http_date
51
from snf_django.lib import api
52
from snf_django.lib.api import faults, utils
53

    
54
from pithos.api.settings import (BACKEND_DB_MODULE, BACKEND_DB_CONNECTION,
55
                                 BACKEND_BLOCK_MODULE, BACKEND_BLOCK_PATH,
56
                                 BACKEND_BLOCK_UMASK,
57
                                 BACKEND_QUEUE_MODULE, BACKEND_QUEUE_HOSTS,
58
                                 BACKEND_QUEUE_EXCHANGE,
59
                                 ASTAKOSCLIENT_POOLSIZE,
60
                                 SERVICE_TOKEN,
61
                                 ASTAKOS_AUTH_URL,
62
                                 BACKEND_ACCOUNT_QUOTA,
63
                                 BACKEND_CONTAINER_QUOTA,
64
                                 BACKEND_VERSIONING, BACKEND_FREE_VERSIONING,
65
                                 BACKEND_POOL_ENABLED, BACKEND_POOL_SIZE,
66
                                 BACKEND_BLOCK_SIZE, BACKEND_HASH_ALGORITHM,
67
                                 RADOS_STORAGE, RADOS_POOL_BLOCKS,
68
                                 RADOS_POOL_MAPS, TRANSLATE_UUIDS,
69
                                 PUBLIC_URL_SECURITY, PUBLIC_URL_ALPHABET,
70
                                 BASE_HOST, UPDATE_MD5, VIEW_PREFIX,
71
                                 OAUTH2_CLIENT_CREDENTIALS, UNSAFE_DOMAIN)
72

    
73
from pithos.api.resources import resources
74
from pithos.backends import connect_backend
75
from pithos.backends.base import (NotAllowedError, QuotaError, ItemNotExists,
76
                                  VersionNotExists)
77

    
78
from synnefo.lib import join_urls
79
from synnefo.util import text
80

    
81
from astakosclient import AstakosClient
82
from astakosclient.errors import NoUserName, NoUUID, AstakosClientException
83

    
84
import logging
85
import re
86
import hashlib
87
import uuid
88
import decimal
89

    
90
logger = logging.getLogger(__name__)
91

    
92

    
93
def json_encode_decimal(obj):
94
    if isinstance(obj, decimal.Decimal):
95
        return str(obj)
96
    raise TypeError(repr(obj) + " is not JSON serializable")
97

    
98

    
99
def rename_meta_key(d, old, new):
100
    if old not in d:
101
        return
102
    d[new] = d[old]
103
    del(d[old])
104

    
105

    
106
def printable_header_dict(d):
107
    """Format a meta dictionary for printing out json/xml.
108

109
    Convert all keys to lower case and replace dashes with underscores.
110
    Format 'last_modified' timestamp.
111
    """
112

    
113
    timestamps = ('last_modified', 'x_container_until_timestamp',
114
                  'x_acount_until_timestamp')
115
    for timestamp in timestamps:
116
        if timestamp in d and d[timestamp]:
117
            d[timestamp] = utils.isoformat(
118
                datetime.fromtimestamp(d[timestamp]))
119
    return dict([(k.lower().replace('-', '_'), v) for k, v in d.iteritems()])
120

    
121

    
122
def format_header_key(k):
123
    """Convert underscores to dashes and capitalize intra-dash strings."""
124
    return '-'.join([x.capitalize() for x in k.replace('_', '-').split('-')])
125

    
126

    
127
def get_header_prefix(request, prefix):
128
    """Get all prefix-* request headers in a dict.
129
       Reformat keys with format_header_key()."""
130

    
131
    prefix = 'HTTP_' + prefix.upper().replace('-', '_')
132
    # TODO: Document or remove '~' replacing.
133
    return dict([(format_header_key(k[5:]), v.replace('~', ''))
134
                for k, v in request.META.iteritems()
135
                if k.startswith(prefix) and len(k) > len(prefix)])
136

    
137

    
138
def check_meta_headers(meta):
139
    if len(meta) > 90:
140
        raise faults.BadRequest('Too many headers.')
141
    for k, v in meta.iteritems():
142
        if len(k) > 128:
143
            raise faults.BadRequest('Header name too large.')
144
        if len(v) > 256:
145
            raise faults.BadRequest('Header value too large.')
146

    
147

    
148
def get_account_headers(request):
149
    meta = get_header_prefix(request, 'X-Account-Meta-')
150
    check_meta_headers(meta)
151
    groups = {}
152
    for k, v in get_header_prefix(request, 'X-Account-Group-').iteritems():
153
        n = k[16:].lower()
154
        if '-' in n or '_' in n:
155
            raise faults.BadRequest('Bad characters in group name')
156
        groups[n] = v.replace(' ', '').split(',')
157
        while '' in groups[n]:
158
            groups[n].remove('')
159
    return meta, groups
160

    
161

    
162
def put_account_headers(response, meta, groups, policy):
163
    if 'count' in meta:
164
        response['X-Account-Container-Count'] = meta['count']
165
    if 'bytes' in meta:
166
        response['X-Account-Bytes-Used'] = meta['bytes']
167
    response['Last-Modified'] = http_date(int(meta['modified']))
168
    for k in [x for x in meta.keys() if x.startswith('X-Account-Meta-')]:
169
        response[smart_str(
170
            k, strings_only=True)] = smart_str(meta[k], strings_only=True)
171
    if 'until_timestamp' in meta:
172
        response['X-Account-Until-Timestamp'] = http_date(
173
            int(meta['until_timestamp']))
174
    for k, v in groups.iteritems():
175
        k = smart_str(k, strings_only=True)
176
        k = format_header_key('X-Account-Group-' + k)
177
        v = smart_str(','.join(v), strings_only=True)
178
        response[k] = v
179
    for k, v in policy.iteritems():
180
        response[smart_str(format_header_key('X-Account-Policy-' + k),
181
                 strings_only=True)] = smart_str(v, strings_only=True)
182

    
183

    
184
def get_container_headers(request):
185
    meta = get_header_prefix(request, 'X-Container-Meta-')
186
    check_meta_headers(meta)
187
    policy = dict([(k[19:].lower(), v.replace(' ', '')) for k, v in
188
                  get_header_prefix(request,
189
                                    'X-Container-Policy-').iteritems()])
190
    return meta, policy
191

    
192

    
193
def put_container_headers(request, response, meta, policy):
194
    if 'count' in meta:
195
        response['X-Container-Object-Count'] = meta['count']
196
    if 'bytes' in meta:
197
        response['X-Container-Bytes-Used'] = meta['bytes']
198
    response['Last-Modified'] = http_date(int(meta['modified']))
199
    for k in [x for x in meta.keys() if x.startswith('X-Container-Meta-')]:
200
        response[smart_str(
201
            k, strings_only=True)] = smart_str(meta[k], strings_only=True)
202
    l = [smart_str(x, strings_only=True) for x in meta['object_meta']
203
         if x.startswith('X-Object-Meta-')]
204
    response['X-Container-Object-Meta'] = ','.join([x[14:] for x in l])
205
    response['X-Container-Block-Size'] = request.backend.block_size
206
    response['X-Container-Block-Hash'] = request.backend.hash_algorithm
207
    if 'until_timestamp' in meta:
208
        response['X-Container-Until-Timestamp'] = http_date(
209
            int(meta['until_timestamp']))
210
    for k, v in policy.iteritems():
211
        response[smart_str(format_header_key('X-Container-Policy-' + k),
212
                           strings_only=True)] = smart_str(v,
213
                                                           strings_only=True)
214

    
215

    
216
def get_object_headers(request):
217
    content_type = request.META.get('CONTENT_TYPE', None)
218
    meta = get_header_prefix(request, 'X-Object-Meta-')
219
    check_meta_headers(meta)
220
    if request.META.get('HTTP_CONTENT_ENCODING'):
221
        meta['Content-Encoding'] = request.META['HTTP_CONTENT_ENCODING']
222
    if request.META.get('HTTP_CONTENT_DISPOSITION'):
223
        meta['Content-Disposition'] = request.META['HTTP_CONTENT_DISPOSITION']
224
    if request.META.get('HTTP_X_OBJECT_MANIFEST'):
225
        meta['X-Object-Manifest'] = request.META['HTTP_X_OBJECT_MANIFEST']
226
    return content_type, meta, get_sharing(request), get_public(request)
227

    
228

    
229
def put_object_headers(response, meta, restricted=False, token=None,
230
                       disposition_type=None):
231
    response['ETag'] = meta['hash'] if not UPDATE_MD5 else meta['checksum']
232
    response['Content-Length'] = meta['bytes']
233
    response.override_serialization = True
234
    response['Content-Type'] = meta.get('type', 'application/octet-stream')
235
    response['Last-Modified'] = http_date(int(meta['modified']))
236
    if not restricted:
237
        response['X-Object-Hash'] = meta['hash']
238
        response['X-Object-UUID'] = meta['uuid']
239
        if TRANSLATE_UUIDS:
240
            meta['modified_by'] = \
241
                retrieve_displayname(token, meta['modified_by'])
242
        response['X-Object-Modified-By'] = smart_str(
243
            meta['modified_by'], strings_only=True)
244
        response['X-Object-Version'] = meta['version']
245
        response['X-Object-Version-Timestamp'] = http_date(
246
            int(meta['version_timestamp']))
247
        for k in [x for x in meta.keys() if x.startswith('X-Object-Meta-')]:
248
            response[smart_str(
249
                k, strings_only=True)] = smart_str(meta[k], strings_only=True)
250
        for k in (
251
            'Content-Encoding', 'Content-Disposition', 'X-Object-Manifest',
252
            'X-Object-Sharing', 'X-Object-Shared-By', 'X-Object-Allowed-To',
253
                'X-Object-Public'):
254
            if k in meta:
255
                response[k] = smart_str(meta[k], strings_only=True)
256
    else:
257
        for k in ('Content-Encoding', 'Content-Disposition'):
258
            if k in meta:
259
                response[k] = smart_str(meta[k], strings_only=True)
260
    disposition_type = disposition_type if disposition_type in \
261
        ('inline', 'attachment') else None
262
    if disposition_type is not None:
263
        response['Content-Disposition'] = smart_str('%s; filename=%s' % (
264
            disposition_type, meta['name']), strings_only=True)
265

    
266

    
267
def update_manifest_meta(request, v_account, meta):
268
    """Update metadata if the object has an X-Object-Manifest."""
269

    
270
    if 'X-Object-Manifest' in meta:
271
        etag = ''
272
        bytes = 0
273
        try:
274
            src_container, src_name = split_container_object_string(
275
                '/' + meta['X-Object-Manifest'])
276
            objects = request.backend.list_objects(
277
                request.user_uniq, v_account,
278
                src_container, prefix=src_name, virtual=False)
279
            for x in objects:
280
                src_meta = request.backend.get_object_meta(
281
                    request.user_uniq, v_account, src_container, x[0],
282
                    'pithos', x[1])
283
                etag += (src_meta['hash'] if not UPDATE_MD5 else
284
                         src_meta['checksum'])
285
                bytes += src_meta['bytes']
286
        except:
287
            # Ignore errors.
288
            return
289
        meta['bytes'] = bytes
290
        md5 = hashlib.md5()
291
        md5.update(etag)
292
        meta['checksum'] = md5.hexdigest().lower()
293

    
294

    
295
def is_uuid(str):
296
    if str is None:
297
        return False
298
    try:
299
        uuid.UUID(str)
300
    except ValueError:
301
        return False
302
    else:
303
        return True
304

    
305

    
306
##########################
307
# USER CATALOG utilities #
308
##########################
309

    
310
def retrieve_displayname(token, uuid, fail_silently=True):
311
    astakos = AstakosClient(token, ASTAKOS_AUTH_URL,
312
                            retry=2, use_pool=True,
313
                            logger=logger)
314
    try:
315
        displayname = astakos.get_username(uuid)
316
    except NoUserName:
317
        if not fail_silently:
318
            raise ItemNotExists(uuid)
319
        else:
320
            # just return the uuid
321
            return uuid
322
    return displayname
323

    
324

    
325
def retrieve_displaynames(token, uuids, return_dict=False, fail_silently=True):
326
    astakos = AstakosClient(token, ASTAKOS_AUTH_URL,
327
                            retry=2, use_pool=True,
328
                            logger=logger)
329
    catalog = astakos.get_usernames(uuids) or {}
330
    missing = list(set(uuids) - set(catalog))
331
    if missing and not fail_silently:
332
        raise ItemNotExists('Unknown displaynames: %s' % ', '.join(missing))
333
    return catalog if return_dict else [catalog.get(i) for i in uuids]
334

    
335

    
336
def retrieve_uuid(token, displayname):
337
    if is_uuid(displayname):
338
        return displayname
339

    
340
    astakos = AstakosClient(token, ASTAKOS_AUTH_URL,
341
                            retry=2, use_pool=True,
342
                            logger=logger)
343
    try:
344
        uuid = astakos.get_uuid(displayname)
345
    except NoUUID:
346
        raise ItemNotExists(displayname)
347
    return uuid
348

    
349

    
350
def retrieve_uuids(token, displaynames, return_dict=False, fail_silently=True):
351
    astakos = AstakosClient(token, ASTAKOS_AUTH_URL,
352
                            retry=2, use_pool=True,
353
                            logger=logger)
354
    catalog = astakos.get_uuids(displaynames) or {}
355
    missing = list(set(displaynames) - set(catalog))
356
    if missing and not fail_silently:
357
        raise ItemNotExists('Unknown uuids: %s' % ', '.join(missing))
358
    return catalog if return_dict else [catalog.get(i) for i in displaynames]
359

    
360

    
361
def replace_permissions_displayname(token, holder):
362
    if holder == '*':
363
        return holder
364
    try:
365
        # check first for a group permission
366
        account, group = holder.split(':', 1)
367
    except ValueError:
368
        return retrieve_uuid(token, holder)
369
    else:
370
        return ':'.join([retrieve_uuid(token, account), group])
371

    
372

    
373
def replace_permissions_uuid(token, holder):
374
    if holder == '*':
375
        return holder
376
    try:
377
        # check first for a group permission
378
        account, group = holder.split(':', 1)
379
    except ValueError:
380
        return retrieve_displayname(token, holder)
381
    else:
382
        return ':'.join([retrieve_displayname(token, account), group])
383

    
384

    
385
def update_sharing_meta(request, permissions, v_account,
386
                        v_container, v_object, meta):
387
    if permissions is None:
388
        return
389
    allowed, perm_path, perms = permissions
390
    if len(perms) == 0:
391
        return
392

    
393
    # replace uuid with displayname
394
    if TRANSLATE_UUIDS:
395
        perms['read'] = [replace_permissions_uuid(
396
            getattr(request, 'token', None), x)
397
            for x in perms.get('read', [])]
398
        perms['write'] = [replace_permissions_uuid(
399
            getattr(request, 'token', None), x)
400
            for x in perms.get('write', [])]
401

    
402
    ret = []
403

    
404
    r = ','.join(perms.get('read', []))
405
    if r:
406
        ret.append('read=' + r)
407
    w = ','.join(perms.get('write', []))
408
    if w:
409
        ret.append('write=' + w)
410
    meta['X-Object-Sharing'] = '; '.join(ret)
411
    if '/'.join((v_account, v_container, v_object)) != perm_path:
412
        meta['X-Object-Shared-By'] = perm_path
413
    if request.user_uniq != v_account:
414
        meta['X-Object-Allowed-To'] = allowed
415

    
416

    
417
def update_public_meta(public, meta):
418
    if not public:
419
        return
420
    meta['X-Object-Public'] = join_urls(
421
        BASE_HOST, reverse('pithos.api.public.public_demux', args=(public,)))
422

    
423

    
424
def validate_modification_preconditions(request, meta):
425
    """Check the modified timestamp conforms with the preconditions set."""
426

    
427
    if 'modified' not in meta:
428
        return  # TODO: Always return?
429

    
430
    if_modified_since = request.META.get('HTTP_IF_MODIFIED_SINCE')
431
    if if_modified_since is not None:
432
        if_modified_since = parse_http_date_safe(if_modified_since)
433
    if (if_modified_since is not None
434
            and int(meta['modified']) <= if_modified_since):
435
        raise faults.NotModified('Resource has not been modified')
436

    
437
    if_unmodified_since = request.META.get('HTTP_IF_UNMODIFIED_SINCE')
438
    if if_unmodified_since is not None:
439
        if_unmodified_since = parse_http_date_safe(if_unmodified_since)
440
    if (if_unmodified_since is not None
441
            and int(meta['modified']) > if_unmodified_since):
442
        raise faults.PreconditionFailed('Resource has been modified')
443

    
444

    
445
def validate_matching_preconditions(request, meta):
446
    """Check that the ETag conforms with the preconditions set."""
447

    
448
    etag = meta['hash'] if not UPDATE_MD5 else meta['checksum']
449
    if not etag:
450
        etag = None
451

    
452
    if_match = request.META.get('HTTP_IF_MATCH')
453
    if if_match is not None:
454
        if etag is None:
455
            raise faults.PreconditionFailed('Resource does not exist')
456
        if (if_match != '*'
457
                and etag not in [x.lower() for x in parse_etags(if_match)]):
458
            raise faults.PreconditionFailed('Resource ETag does not match')
459

    
460
    if_none_match = request.META.get('HTTP_IF_NONE_MATCH')
461
    if if_none_match is not None:
462
        # TODO: If this passes, must ignore If-Modified-Since header.
463
        if etag is not None:
464
            if (if_none_match == '*' or etag in [x.lower() for x in
465
                                                 parse_etags(if_none_match)]):
466
                # TODO: Continue if an If-Modified-Since header is present.
467
                if request.method in ('HEAD', 'GET'):
468
                    raise faults.NotModified('Resource ETag matches')
469
                raise faults.PreconditionFailed(
470
                    'Resource exists or ETag matches')
471

    
472

    
473
def split_container_object_string(s):
474
    if not len(s) > 0 or s[0] != '/':
475
        raise ValueError
476
    s = s[1:]
477
    pos = s.find('/')
478
    if pos == -1 or pos == len(s) - 1:
479
        raise ValueError
480
    return s[:pos], s[(pos + 1):]
481

    
482

    
483
def copy_or_move_object(request, src_account, src_container, src_name,
484
                        dest_account, dest_container, dest_name,
485
                        move=False, delimiter=None, listing_limit=None):
486
    """Copy or move an object."""
487

    
488
    if 'ignore_content_type' in request.GET and 'CONTENT_TYPE' in request.META:
489
        del(request.META['CONTENT_TYPE'])
490
    content_type, meta, permissions, public = get_object_headers(request)
491
    src_version = request.META.get('HTTP_X_SOURCE_VERSION')
492
    try:
493
        if move:
494
            version_id = request.backend.move_object(
495
                request.user_uniq, src_account, src_container, src_name,
496
                dest_account, dest_container, dest_name,
497
                content_type, 'pithos', meta, False, permissions, delimiter,
498
                listing_limit=listing_limit)
499
        else:
500
            version_id = request.backend.copy_object(
501
                request.user_uniq, src_account, src_container, src_name,
502
                dest_account, dest_container, dest_name,
503
                content_type, 'pithos', meta, False, permissions,
504
                src_version, delimiter, listing_limit=listing_limit)
505
    except NotAllowedError:
506
        raise faults.Forbidden('Not allowed')
507
    except (ItemNotExists, VersionNotExists):
508
        raise faults.ItemNotFound('Container or object does not exist')
509
    except ValueError:
510
        raise faults.BadRequest('Invalid sharing header')
511
    except QuotaError, e:
512
        raise faults.RequestEntityTooLarge('Quota error: %s' % e)
513
    if public is not None:
514
        try:
515
            request.backend.update_object_public(
516
                request.user_uniq, dest_account,
517
                dest_container, dest_name, public)
518
        except NotAllowedError:
519
            raise faults.Forbidden('Not allowed')
520
        except ItemNotExists:
521
            raise faults.ItemNotFound('Object does not exist')
522
    return version_id
523

    
524

    
525
def get_int_parameter(p):
526
    if p is not None:
527
        try:
528
            p = int(p)
529
        except ValueError:
530
            return None
531
        if p < 0:
532
            return None
533
    return p
534

    
535

    
536
def get_content_length(request):
537
    content_length = get_int_parameter(request.META.get('CONTENT_LENGTH'))
538
    if content_length is None:
539
        raise faults.LengthRequired('Missing or invalid Content-Length header')
540
    return content_length
541

    
542

    
543
def get_range(request, size):
544
    """Parse a Range header from the request.
545

546
    Either returns None, when the header is not existent or should be ignored,
547
    or a list of (offset, length) tuples - should be further checked.
548
    """
549

    
550
    ranges = request.META.get('HTTP_RANGE', '').replace(' ', '')
551
    if not ranges.startswith('bytes='):
552
        return None
553

    
554
    ret = []
555
    for r in (x.strip() for x in ranges[6:].split(',')):
556
        p = re.compile('^(?P<offset>\d*)-(?P<upto>\d*)$')
557
        m = p.match(r)
558
        if not m:
559
            return None
560
        offset = m.group('offset')
561
        upto = m.group('upto')
562
        if offset == '' and upto == '':
563
            return None
564

    
565
        if offset != '':
566
            offset = int(offset)
567
            if upto != '':
568
                upto = int(upto)
569
                if offset > upto:
570
                    return None
571
                ret.append((offset, upto - offset + 1))
572
            else:
573
                ret.append((offset, size - offset))
574
        else:
575
            length = int(upto)
576
            ret.append((size - length, length))
577

    
578
    return ret
579

    
580

    
581
def get_content_range(request):
582
    """Parse a Content-Range header from the request.
583

584
    Either returns None, when the header is not existent or should be ignored,
585
    or an (offset, length, total) tuple - check as length, total may be None.
586
    Returns (None, None, None) if the provided range is '*/*'.
587
    """
588

    
589
    ranges = request.META.get('HTTP_CONTENT_RANGE', '')
590
    if not ranges:
591
        return None
592

    
593
    p = re.compile('^bytes (?P<offset>\d+)-(?P<upto>\d*)/(?P<total>(\d+|\*))$')
594
    m = p.match(ranges)
595
    if not m:
596
        if ranges == 'bytes */*':
597
            return (None, None, None)
598
        return None
599
    offset = int(m.group('offset'))
600
    upto = m.group('upto')
601
    total = m.group('total')
602
    if upto != '':
603
        upto = int(upto)
604
    else:
605
        upto = None
606
    if total != '*':
607
        total = int(total)
608
    else:
609
        total = None
610
    if (upto is not None and offset > upto) or \
611
        (total is not None and offset >= total) or \
612
            (total is not None and upto is not None and upto >= total):
613
        return None
614

    
615
    if upto is None:
616
        length = None
617
    else:
618
        length = upto - offset + 1
619
    return (offset, length, total)
620

    
621

    
622
def get_sharing(request):
623
    """Parse an X-Object-Sharing header from the request.
624

625
    Raises BadRequest on error.
626
    """
627

    
628
    permissions = request.META.get('HTTP_X_OBJECT_SHARING')
629
    if permissions is None:
630
        return None
631

    
632
    # TODO: Document or remove '~' replacing.
633
    permissions = permissions.replace('~', '')
634

    
635
    ret = {}
636
    permissions = permissions.replace(' ', '')
637
    if permissions == '':
638
        return ret
639
    for perm in (x for x in permissions.split(';')):
640
        if perm.startswith('read='):
641
            ret['read'] = list(set(
642
                [v.replace(' ', '').lower() for v in perm[5:].split(',')]))
643
            if '' in ret['read']:
644
                ret['read'].remove('')
645
            if '*' in ret['read']:
646
                ret['read'] = ['*']
647
            if len(ret['read']) == 0:
648
                raise faults.BadRequest(
649
                    'Bad X-Object-Sharing header value: invalid length')
650
        elif perm.startswith('write='):
651
            ret['write'] = list(set(
652
                [v.replace(' ', '').lower() for v in perm[6:].split(',')]))
653
            if '' in ret['write']:
654
                ret['write'].remove('')
655
            if '*' in ret['write']:
656
                ret['write'] = ['*']
657
            if len(ret['write']) == 0:
658
                raise faults.BadRequest(
659
                    'Bad X-Object-Sharing header value: invalid length')
660
        else:
661
            raise faults.BadRequest(
662
                'Bad X-Object-Sharing header value: missing prefix')
663

    
664
    # replace displayname with uuid
665
    if TRANSLATE_UUIDS:
666
        try:
667
            ret['read'] = [replace_permissions_displayname(
668
                getattr(request, 'token', None), x)
669
                for x in ret.get('read', [])]
670
            ret['write'] = [replace_permissions_displayname(
671
                getattr(request, 'token', None), x)
672
                for x in ret.get('write', [])]
673
        except ItemNotExists, e:
674
            raise faults.BadRequest(
675
                'Bad X-Object-Sharing header value: unknown account: %s' % e)
676

    
677
    # Keep duplicates only in write list.
678
    dups = [x for x in ret.get(
679
        'read', []) if x in ret.get('write', []) and x != '*']
680
    if dups:
681
        for x in dups:
682
            ret['read'].remove(x)
683
        if len(ret['read']) == 0:
684
            del(ret['read'])
685

    
686
    return ret
687

    
688

    
689
def get_public(request):
690
    """Parse an X-Object-Public header from the request.
691

692
    Raises BadRequest on error.
693
    """
694

    
695
    public = request.META.get('HTTP_X_OBJECT_PUBLIC')
696
    if public is None:
697
        return None
698

    
699
    public = public.replace(' ', '').lower()
700
    if public == 'true':
701
        return True
702
    elif public == 'false' or public == '':
703
        return False
704
    raise faults.BadRequest('Bad X-Object-Public header value')
705

    
706

    
707
def raw_input_socket(request):
708
    """Return the socket for reading the rest of the request."""
709

    
710
    server_software = request.META.get('SERVER_SOFTWARE')
711
    if server_software and server_software.startswith('mod_python'):
712
        return request._req
713
    if 'wsgi.input' in request.environ:
714
        return request.environ['wsgi.input']
715
    raise NotImplemented('Unknown server software')
716

    
717
MAX_UPLOAD_SIZE = 5 * (1024 * 1024 * 1024)  # 5GB
718

    
719

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

723
    Read up to 'length'. If 'length' is negative, will attempt a chunked read.
724
    The maximum ammount of data read is controlled by MAX_UPLOAD_SIZE.
725
    """
726

    
727
    sock = raw_input_socket(request)
728
    if length < 0:  # Chunked transfers
729
        # Small version (server does the dechunking).
730
        if (request.environ.get('mod_wsgi.input_chunked', None)
731
                or request.META['SERVER_SOFTWARE'].startswith('gunicorn')):
732
            while length < MAX_UPLOAD_SIZE:
733
                data = sock.read(blocksize)
734
                if data == '':
735
                    return
736
                yield data
737
            raise faults.BadRequest('Maximum size is reached')
738

    
739
        # Long version (do the dechunking).
740
        data = ''
741
        while length < MAX_UPLOAD_SIZE:
742
            # Get chunk size.
743
            if hasattr(sock, 'readline'):
744
                chunk_length = sock.readline()
745
            else:
746
                chunk_length = ''
747
                while chunk_length[-1:] != '\n':
748
                    chunk_length += sock.read(1)
749
                chunk_length.strip()
750
            pos = chunk_length.find(';')
751
            if pos >= 0:
752
                chunk_length = chunk_length[:pos]
753
            try:
754
                chunk_length = int(chunk_length, 16)
755
            except Exception:
756
                raise faults.BadRequest('Bad chunk size')
757
                                 # TODO: Change to something more appropriate.
758
            # Check if done.
759
            if chunk_length == 0:
760
                if len(data) > 0:
761
                    yield data
762
                return
763
            # Get the actual data.
764
            while chunk_length > 0:
765
                chunk = sock.read(min(chunk_length, blocksize))
766
                chunk_length -= len(chunk)
767
                if length > 0:
768
                    length += len(chunk)
769
                data += chunk
770
                if len(data) >= blocksize:
771
                    ret = data[:blocksize]
772
                    data = data[blocksize:]
773
                    yield ret
774
            sock.read(2)  # CRLF
775
        raise faults.BadRequest('Maximum size is reached')
776
    else:
777
        if length > MAX_UPLOAD_SIZE:
778
            raise faults.BadRequest('Maximum size is reached')
779
        while length > 0:
780
            data = sock.read(min(length, blocksize))
781
            if not data:
782
                raise faults.BadRequest()
783
            length -= len(data)
784
            yield data
785

    
786

    
787
class SaveToBackendHandler(FileUploadHandler):
788
    """Handle a file from an HTML form the django way."""
789

    
790
    def __init__(self, request=None):
791
        super(SaveToBackendHandler, self).__init__(request)
792
        self.backend = request.backend
793

    
794
    def put_data(self, length):
795
        if len(self.data) >= length:
796
            block = self.data[:length]
797
            self.file.hashmap.append(self.backend.put_block(block))
798
            self.checksum_compute.update(block)
799
            self.data = self.data[length:]
800

    
801
    def new_file(self, field_name, file_name, content_type,
802
                 content_length, charset=None):
803
        self.checksum_compute = NoChecksum() if not UPDATE_MD5 else Checksum()
804
        self.data = ''
805
        self.file = UploadedFile(
806
            name=file_name, content_type=content_type, charset=charset)
807
        self.file.size = 0
808
        self.file.hashmap = []
809

    
810
    def receive_data_chunk(self, raw_data, start):
811
        self.data += raw_data
812
        self.file.size += len(raw_data)
813
        self.put_data(self.request.backend.block_size)
814
        return None
815

    
816
    def file_complete(self, file_size):
817
        l = len(self.data)
818
        if l > 0:
819
            self.put_data(l)
820
        self.file.etag = self.checksum_compute.hexdigest()
821
        return self.file
822

    
823

    
824
class ObjectWrapper(object):
825
    """Return the object's data block-per-block in each iteration.
826

827
    Read from the object using the offset and length provided
828
    in each entry of the range list.
829
    """
830

    
831
    def __init__(self, backend, ranges, sizes, hashmaps, boundary):
832
        self.backend = backend
833
        self.ranges = ranges
834
        self.sizes = sizes
835
        self.hashmaps = hashmaps
836
        self.boundary = boundary
837
        self.size = sum(self.sizes)
838

    
839
        self.file_index = 0
840
        self.block_index = 0
841
        self.block_hash = -1
842
        self.block = ''
843

    
844
        self.range_index = -1
845
        self.offset, self.length = self.ranges[0]
846

    
847
    def __iter__(self):
848
        return self
849

    
850
    def part_iterator(self):
851
        if self.length > 0:
852
            # Get the file for the current offset.
853
            file_size = self.sizes[self.file_index]
854
            while self.offset >= file_size:
855
                self.offset -= file_size
856
                self.file_index += 1
857
                file_size = self.sizes[self.file_index]
858

    
859
            # Get the block for the current position.
860
            self.block_index = int(self.offset / self.backend.block_size)
861
            if self.block_hash != \
862
                    self.hashmaps[self.file_index][self.block_index]:
863
                self.block_hash = self.hashmaps[
864
                    self.file_index][self.block_index]
865
                try:
866
                    self.block = self.backend.get_block(self.block_hash)
867
                except ItemNotExists:
868
                    raise faults.ItemNotFound('Block does not exist')
869

    
870
            # Get the data from the block.
871
            bo = self.offset % self.backend.block_size
872
            bs = self.backend.block_size
873
            if (self.block_index == len(self.hashmaps[self.file_index]) - 1 and
874
                    self.sizes[self.file_index] % self.backend.block_size):
875
                bs = self.sizes[self.file_index] % self.backend.block_size
876
            bl = min(self.length, bs - bo)
877
            data = self.block[bo:bo + bl]
878
            self.offset += bl
879
            self.length -= bl
880
            return data
881
        else:
882
            raise StopIteration
883

    
884
    def next(self):
885
        if len(self.ranges) == 1:
886
            return self.part_iterator()
887
        if self.range_index == len(self.ranges):
888
            raise StopIteration
889
        try:
890
            if self.range_index == -1:
891
                raise StopIteration
892
            return self.part_iterator()
893
        except StopIteration:
894
            self.range_index += 1
895
            out = []
896
            if self.range_index < len(self.ranges):
897
                # Part header.
898
                self.offset, self.length = self.ranges[self.range_index]
899
                self.file_index = 0
900
                if self.range_index > 0:
901
                    out.append('')
902
                out.append('--' + self.boundary)
903
                out.append('Content-Range: bytes %d-%d/%d' % (
904
                    self.offset, self.offset + self.length - 1, self.size))
905
                out.append('Content-Transfer-Encoding: binary')
906
                out.append('')
907
                out.append('')
908
                return '\r\n'.join(out)
909
            else:
910
                # Footer.
911
                out.append('')
912
                out.append('--' + self.boundary + '--')
913
                out.append('')
914
                return '\r\n'.join(out)
915

    
916

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

    
920
    # Range handling.
921
    size = sum(sizes)
922
    ranges = get_range(request, size)
923
    if ranges is None:
924
        ranges = [(0, size)]
925
        ret = 200
926
    else:
927
        check = [True for offset, length in ranges if
928
                 length <= 0 or length > size or
929
                 offset < 0 or offset >= size or
930
                 offset + length > size]
931
        if len(check) > 0:
932
            raise faults.RangeNotSatisfiable(
933
                'Requested range exceeds object limits')
934
        ret = 206
935
        if_range = request.META.get('HTTP_IF_RANGE')
936
        if if_range:
937
            try:
938
                # Modification time has passed instead.
939
                last_modified = parse_http_date(if_range)
940
                if last_modified != meta['modified']:
941
                    ranges = [(0, size)]
942
                    ret = 200
943
            except ValueError:
944
                if if_range != meta['checksum']:
945
                    ranges = [(0, size)]
946
                    ret = 200
947

    
948
    if ret == 206 and len(ranges) > 1:
949
        boundary = uuid.uuid4().hex
950
    else:
951
        boundary = ''
952
    wrapper = ObjectWrapper(request.backend, ranges, sizes, hashmaps, boundary)
953
    response = HttpResponse(wrapper, status=ret)
954
    put_object_headers(
955
        response, meta, restricted=public,
956
        token=getattr(request, 'token', None),
957
        disposition_type=request.GET.get('disposition-type'))
958
    if ret == 206:
959
        if len(ranges) == 1:
960
            offset, length = ranges[0]
961
            response[
962
                'Content-Length'] = length  # Update with the correct length.
963
            response['Content-Range'] = 'bytes %d-%d/%d' % (
964
                offset, offset + length - 1, size)
965
        else:
966
            del(response['Content-Length'])
967
            response['Content-Type'] = 'multipart/byteranges; boundary=%s' % (
968
                boundary,)
969
    return response
970

    
971

    
972
def put_object_block(request, hashmap, data, offset):
973
    """Put one block of data at the given offset."""
974

    
975
    bi = int(offset / request.backend.block_size)
976
    bo = offset % request.backend.block_size
977
    bl = min(len(data), request.backend.block_size - bo)
978
    if bi < len(hashmap):
979
        hashmap[bi] = request.backend.update_block(hashmap[bi], data[:bl], bo)
980
    else:
981
        hashmap.append(request.backend.put_block(('\x00' * bo) + data[:bl]))
982
    return bl  # Return ammount of data written.
983

    
984

    
985
def hashmap_md5(backend, hashmap, size):
986
    """Produce the MD5 sum from the data in the hashmap."""
987

    
988
    # TODO: Search backend for the MD5 of another object
989
    #       with the same hashmap and size...
990
    md5 = hashlib.md5()
991
    bs = backend.block_size
992
    for bi, hash in enumerate(hashmap):
993
        data = backend.get_block(hash)  # Blocks come in padded.
994
        if bi == len(hashmap) - 1:
995
            data = data[:size % bs]
996
        md5.update(data)
997
    return md5.hexdigest().lower()
998

    
999

    
1000
def simple_list_response(request, l):
1001
    if request.serialization == 'text':
1002
        return '\n'.join(l) + '\n'
1003
    if request.serialization == 'xml':
1004
        return render_to_string('items.xml', {'items': l})
1005
    if request.serialization == 'json':
1006
        return json.dumps(l)
1007

    
1008

    
1009
from pithos.backends.util import PithosBackendPool
1010

    
1011
if RADOS_STORAGE:
1012
    BLOCK_PARAMS = {'mappool': RADOS_POOL_MAPS,
1013
                    'blockpool': RADOS_POOL_BLOCKS, }
1014
else:
1015
    BLOCK_PARAMS = {'mappool': None,
1016
                    'blockpool': None, }
1017

    
1018
BACKEND_KWARGS = dict(
1019
    db_module=BACKEND_DB_MODULE,
1020
    db_connection=BACKEND_DB_CONNECTION,
1021
    block_module=BACKEND_BLOCK_MODULE,
1022
    block_path=BACKEND_BLOCK_PATH,
1023
    block_umask=BACKEND_BLOCK_UMASK,
1024
    block_size=BACKEND_BLOCK_SIZE,
1025
    hash_algorithm=BACKEND_HASH_ALGORITHM,
1026
    queue_module=BACKEND_QUEUE_MODULE,
1027
    queue_hosts=BACKEND_QUEUE_HOSTS,
1028
    queue_exchange=BACKEND_QUEUE_EXCHANGE,
1029
    astakos_auth_url=ASTAKOS_AUTH_URL,
1030
    service_token=SERVICE_TOKEN,
1031
    astakosclient_poolsize=ASTAKOSCLIENT_POOLSIZE,
1032
    free_versioning=BACKEND_FREE_VERSIONING,
1033
    block_params=BLOCK_PARAMS,
1034
    public_url_security=PUBLIC_URL_SECURITY,
1035
    public_url_alphabet=PUBLIC_URL_ALPHABET,
1036
    account_quota_policy=BACKEND_ACCOUNT_QUOTA,
1037
    container_quota_policy=BACKEND_CONTAINER_QUOTA,
1038
    container_versioning_policy=BACKEND_VERSIONING)
1039

    
1040
_pithos_backend_pool = PithosBackendPool(size=BACKEND_POOL_SIZE,
1041
                                         **BACKEND_KWARGS)
1042

    
1043

    
1044
def get_backend():
1045
    if BACKEND_POOL_ENABLED:
1046
        backend = _pithos_backend_pool.pool_get()
1047
    else:
1048
        backend = connect_backend(**BACKEND_KWARGS)
1049
    backend.serials = []
1050
    backend.messages = []
1051
    return backend
1052

    
1053

    
1054
def update_request_headers(request):
1055
    # Handle URL-encoded keys and values.
1056
    meta = dict([(
1057
        k, v) for k, v in request.META.iteritems() if k.startswith('HTTP_')])
1058
    for k, v in meta.iteritems():
1059
        try:
1060
            k.decode('ascii')
1061
            v.decode('ascii')
1062
            if '%' in k or '%' in v:
1063
                del(request.META[k])
1064
                request.META[unquote(k)] = smart_unicode(unquote(
1065
                    v), strings_only=True)
1066
        except UnicodeDecodeError:
1067
            raise faults.BadRequest('Bad character in headers.')
1068

    
1069

    
1070
def update_response_headers(request, response):
1071
    # URL-encode unicode in headers.
1072
    meta = response.items()
1073
    for k, v in meta:
1074
        if (k.startswith('X-Account-') or k.startswith('X-Container-') or
1075
                k.startswith('X-Object-') or k.startswith('Content-')):
1076
            del(response[k])
1077
            response[quote(k)] = quote(v, safe='/=,:@; ')
1078

    
1079

    
1080
def api_method(http_method=None, token_required=True, user_required=True,
1081
               logger=None, format_allowed=False, serializations=None,
1082
               strict_serlization=False, lock_container_path=False):
1083
    serializations = serializations or ['json', 'xml']
1084

    
1085
    def decorator(func):
1086
        @api.api_method(http_method=http_method, token_required=token_required,
1087
                        user_required=user_required,
1088
                        logger=logger, format_allowed=format_allowed,
1089
                        astakos_auth_url=ASTAKOS_AUTH_URL,
1090
                        serializations=serializations,
1091
                        strict_serlization=strict_serlization)
1092
        @wraps(func)
1093
        def wrapper(request, *args, **kwargs):
1094
            # The args variable may contain up to (account, container, object).
1095
            if len(args) > 1 and len(args[1]) > 256:
1096
                raise faults.BadRequest("Container name too large")
1097
            if len(args) > 2 and len(args[2]) > 1024:
1098
                raise faults.BadRequest('Object name too large.')
1099

    
1100
            success_status = False
1101
            try:
1102
                # Add a PithosBackend as attribute of the request object
1103
                request.backend = get_backend()
1104
                request.backend.pre_exec(lock_container_path)
1105

    
1106
                # Many API method expect thet X-Auth-Token in request,token
1107
                request.token = request.x_auth_token
1108
                update_request_headers(request)
1109
                response = func(request, *args, **kwargs)
1110
                update_response_headers(request, response)
1111

    
1112
                success_status = True
1113
                return response
1114
            finally:
1115
                # Always close PithosBackend connection
1116
                if getattr(request, "backend", None) is not None:
1117
                    request.backend.post_exec(success_status)
1118
                    request.backend.close()
1119
        return wrapper
1120
    return decorator
1121

    
1122

    
1123
def restrict_to_host(host=None):
1124
    """
1125
    View decorator which restricts wrapped view to be accessed only under the
1126
    host set. If an invalid host is identified and request HTTP method is one
1127
    of ``GET``, ``HOST``, the decorator will return a redirect response using a
1128
    clone of the request with host replaced to the one the restriction applies
1129
    to.
1130

1131
    e.g.
1132
    @restrict_to_host('files.example.com')
1133
    my_restricted_view(request, path):
1134
        return HttpResponse(file(path).read())
1135

1136
    A get to ``https://api.example.com/my_restricted_view/file_path/?param=1``
1137
    will return a redirect response with Location header set to
1138
    ``https://files.example.com/my_restricted_view/file_path/?param=1``.
1139

1140
    If host is set to ``None`` no restriction will be applied.
1141
    """
1142
    def decorator(func):
1143
        # skip decoration if no host is set
1144
        if not host:
1145
            return func
1146

    
1147
        @wraps(func)
1148
        def wrapper(request, *args, **kwargs):
1149
            request_host = request.get_host()
1150
            if host != request_host:
1151
                proto = 'https' if request.is_secure() else 'http'
1152
                if request.method in ['GET', 'HEAD']:
1153
                    full_path = request.get_full_path()
1154
                    redirect_uri = "%s://%s%s" % (proto, host, full_path)
1155
                    return HttpResponseRedirect(redirect_uri)
1156
                else:
1157
                    raise PermissionDenied
1158
            return func(request, *args, **kwargs)
1159
        return wrapper
1160
    return decorator
1161

    
1162

    
1163
def view_method():
1164
    """Decorator function for views."""
1165

    
1166
    def decorator(func):
1167
        @restrict_to_host(UNSAFE_DOMAIN)
1168
        @wraps(func)
1169
        def wrapper(request, *args, **kwargs):
1170
            if request.method not in ['GET', 'HEAD']:
1171
                return HttpResponseNotAllowed(['GET', 'HEAD'])
1172

    
1173
            try:
1174
                access_token = request.GET.get('access_token')
1175
                requested_resource = text.uenc(request.path.split(VIEW_PREFIX,
1176
                                                                  2)[-1])
1177
                astakos = AstakosClient(SERVICE_TOKEN, ASTAKOS_AUTH_URL,
1178
                                        retry=2, use_pool=True,
1179
                                        logger=logger)
1180
                if access_token is not None:
1181
                    # authenticate using the short-term access token
1182
                    try:
1183
                        request.user = astakos.validate_token(
1184
                            access_token, requested_resource)
1185
                    except AstakosClientException:
1186
                        return HttpResponseRedirect(request.path)
1187
                    request.user_uniq = request.user["access"]["user"]["id"]
1188

    
1189
                    _func = api_method(token_required=False,
1190
                                       user_required=False)(func)
1191
                    response = _func(request, *args, **kwargs)
1192
                    if response.status_code == 404:
1193
                        raise Http404
1194
                    elif response.status_code == 403:
1195
                        raise PermissionDenied
1196
                    return response
1197

    
1198
                client_id, client_secret = OAUTH2_CLIENT_CREDENTIALS
1199
                # TODO: check if client credentials are not set
1200
                authorization_code = request.GET.get('code')
1201
                redirect_uri = unquote(request.build_absolute_uri(
1202
                    request.get_full_path()))
1203
                if authorization_code is None:
1204
                    # request authorization code
1205
                    params = {'response_type': 'code',
1206
                              'client_id': client_id,
1207
                              'redirect_uri': redirect_uri,
1208
                              'state': '',  # TODO include state for security
1209
                              'scope': requested_resource}
1210
                    return HttpResponseRedirect('%s?%s' %
1211
                                                (join_urls(astakos.oauth2_url,
1212
                                                           'auth'),
1213
                                                 urlencode(params)))
1214
                else:
1215
                    # request short-term access token
1216
                    parts = list(urlsplit(redirect_uri))
1217
                    params = dict(parse_qsl(parts[3], keep_blank_values=True))
1218
                    if 'code' in params:  # always True
1219
                        del params['code']
1220
                    if 'state' in params:
1221
                        del params['state']
1222
                    parts[3] = urlencode(params)
1223
                    redirect_uri = urlunsplit(parts)
1224
                    data = astakos.get_token('authorization_code',
1225
                                             *OAUTH2_CLIENT_CREDENTIALS,
1226
                                             redirect_uri=redirect_uri,
1227
                                             scope=requested_resource,
1228
                                             code=authorization_code)
1229
                    params['access_token'] = data.get('access_token', '')
1230
                    parts[3] = urlencode(params)
1231
                    redirect_uri = urlunsplit(parts)
1232
                    return HttpResponseRedirect(redirect_uri)
1233
            except AstakosClientException, err:
1234
                logger.exception(err)
1235
                raise PermissionDenied
1236
        return wrapper
1237
    return decorator
1238

    
1239

    
1240
class Checksum:
1241
    def __init__(self):
1242
        self.md5 = hashlib.md5()
1243

    
1244
    def update(self, data):
1245
        self.md5.update(data)
1246

    
1247
    def hexdigest(self):
1248
        return self.md5.hexdigest().lower()
1249

    
1250

    
1251
class NoChecksum:
1252
    def update(self, data):
1253
        pass
1254

    
1255
    def hexdigest(self):
1256
        return ''