source: trunk/src/allmydata/uri.py @ f47672d

Last change on this file since f47672d was f47672d, checked in by Brian Warner <warner@…>, at 2009-11-04T18:13:51Z

add parser for immutable directory caps: DIR2-CHK, DIR2-LIT, DIR2-CHK-Verifier

  • Property mode set to 100644
File size: 19.7 KB
Line 
1
2import re, urllib
3from zope.interface import implements
4from twisted.python.components import registerAdapter
5from allmydata.storage.server import si_a2b, si_b2a
6from allmydata.util import base32, hashutil
7from allmydata.interfaces import IURI, IDirnodeURI, IFileURI, IImmutableFileURI, \
8    IVerifierURI, IMutableFileURI, IDirectoryURI, IReadonlyDirectoryURI
9
10class BadURIError(Exception):
11    pass
12
13# the URI shall be an ascii representation of the file. It shall contain
14# enough information to retrieve and validate the contents. It shall be
15# expressed in a limited character set (namely [TODO]).
16
17BASE32STR_128bits = '(%s{25}%s)' % (base32.BASE32CHAR, base32.BASE32CHAR_3bits)
18BASE32STR_256bits = '(%s{51}%s)' % (base32.BASE32CHAR, base32.BASE32CHAR_1bits)
19
20SEP='(?::|%3A)'
21NUMBER='([0-9]+)'
22NUMBER_IGNORE='(?:[0-9]+)'
23
24# URIs (soon to be renamed "caps") are always allowed to come with a leading
25# 'http://127.0.0.1:(8123|3456)/uri/' that will be ignored.
26OPTIONALHTTPLEAD=r'(?:https?://(?:[^:/]+)(?::%s)?/uri/)?' % NUMBER_IGNORE
27
28
29class _BaseURI:
30    def __hash__(self):
31        return self.to_string().__hash__()
32    def __eq__(self, them):
33        if isinstance(them, _BaseURI):
34            return self.to_string() == them.to_string()
35        else:
36            return False
37    def __ne__(self, them):
38        if isinstance(them, _BaseURI):
39            return self.to_string() != them.to_string()
40        else:
41            return True
42    def to_human_encoding(self):
43        return 'http://127.0.0.1:3456/uri/'+self.to_string()
44
45    def get_storage_index(self):
46        return self.storage_index
47
48class CHKFileURI(_BaseURI):
49    implements(IURI, IImmutableFileURI)
50
51    BASE_STRING='URI:CHK:'
52    STRING_RE=re.compile('^URI:CHK:'+BASE32STR_128bits+':'+
53                         BASE32STR_256bits+':'+NUMBER+':'+NUMBER+':'+NUMBER+
54                         '$')
55    HUMAN_RE=re.compile('^'+OPTIONALHTTPLEAD+'URI'+SEP+'CHK'+SEP+
56                     BASE32STR_128bits+SEP+BASE32STR_256bits+SEP+NUMBER+
57                     SEP+NUMBER+SEP+NUMBER+'$')
58
59    def __init__(self, key, uri_extension_hash, needed_shares, total_shares,
60                 size):
61        self.key = key
62        self.uri_extension_hash = uri_extension_hash
63        self.needed_shares = needed_shares
64        self.total_shares = total_shares
65        self.size = size
66        self.storage_index = hashutil.storage_index_hash(self.key)
67        if not len(self.storage_index) == 16: # sha256 hash truncated to 128
68            raise BadURIError("storage index must be 16 bytes long")
69
70    @classmethod
71    def init_from_human_encoding(cls, uri):
72        mo = cls.HUMAN_RE.search(uri)
73        if not mo:
74            raise BadURIError("%s doesn't look like a cap" % (uri,))
75        return cls(base32.a2b(mo.group(1)), base32.a2b(mo.group(2)),
76                   int(mo.group(3)), int(mo.group(4)), int(mo.group(5)))
77
78    @classmethod
79    def init_from_string(cls, uri):
80        mo = cls.STRING_RE.search(uri)
81        if not mo:
82            raise BadURIError("%s doesn't look like a cap" % (uri,))
83        return cls(base32.a2b(mo.group(1)), base32.a2b(mo.group(2)),
84                   int(mo.group(3)), int(mo.group(4)), int(mo.group(5)))
85
86    def to_string(self):
87        assert isinstance(self.needed_shares, int)
88        assert isinstance(self.total_shares, int)
89        assert isinstance(self.size, (int,long))
90
91        return ('URI:CHK:%s:%s:%d:%d:%d' %
92                (base32.b2a(self.key),
93                 base32.b2a(self.uri_extension_hash),
94                 self.needed_shares,
95                 self.total_shares,
96                 self.size))
97
98    def is_readonly(self):
99        return True
100    def is_mutable(self):
101        return False
102    def get_readonly(self):
103        return self
104
105    def get_size(self):
106        return self.size
107
108    def get_verify_cap(self):
109        return CHKFileVerifierURI(storage_index=self.storage_index,
110                                  uri_extension_hash=self.uri_extension_hash,
111                                  needed_shares=self.needed_shares,
112                                  total_shares=self.total_shares,
113                                  size=self.size)
114
115class CHKFileVerifierURI(_BaseURI):
116    implements(IVerifierURI)
117
118    STRING_RE=re.compile('^URI:CHK-Verifier:'+BASE32STR_128bits+':'+
119                         BASE32STR_256bits+':'+NUMBER+':'+NUMBER+':'+NUMBER)
120    HUMAN_RE=re.compile('^'+OPTIONALHTTPLEAD+'URI'+SEP+'CHK-Verifier'+SEP+
121                        BASE32STR_128bits+SEP+BASE32STR_256bits+SEP+NUMBER+
122                        SEP+NUMBER+SEP+NUMBER)
123
124    def __init__(self, storage_index, uri_extension_hash,
125                 needed_shares, total_shares, size):
126        assert len(storage_index) == 16
127        self.storage_index = storage_index
128        self.uri_extension_hash = uri_extension_hash
129        self.needed_shares = needed_shares
130        self.total_shares = total_shares
131        self.size = size
132
133    @classmethod
134    def init_from_human_encoding(cls, uri):
135        mo = cls.HUMAN_RE.search(uri)
136        assert mo, uri
137        return cls(base32.a2b(mo.group(1)), base32.a2b(mo.group(2)),
138                   int(mo.group(3)), int(mo.group(4)), int(mo.group(5)))
139
140    @classmethod
141    def init_from_string(cls, uri):
142        mo = cls.STRING_RE.search(uri)
143        assert mo, (uri, cls, cls.STRING_RE)
144        return cls(si_a2b(mo.group(1)), base32.a2b(mo.group(2)),
145                   int(mo.group(3)), int(mo.group(4)), int(mo.group(5)))
146
147    def to_string(self):
148        assert isinstance(self.needed_shares, int)
149        assert isinstance(self.total_shares, int)
150        assert isinstance(self.size, (int,long))
151
152        return ('URI:CHK-Verifier:%s:%s:%d:%d:%d' %
153                (si_b2a(self.storage_index),
154                 base32.b2a(self.uri_extension_hash),
155                 self.needed_shares,
156                 self.total_shares,
157                 self.size))
158
159
160class LiteralFileURI(_BaseURI):
161    implements(IURI, IImmutableFileURI)
162
163    BASE_STRING='URI:LIT:'
164    STRING_RE=re.compile('^URI:LIT:'+base32.BASE32STR_anybytes+'$')
165    HUMAN_RE=re.compile('^'+OPTIONALHTTPLEAD+'URI'+SEP+'LIT'+SEP+base32.BASE32STR_anybytes+'$')
166
167    def __init__(self, data=None):
168        if data is not None:
169            self.data = data
170
171    @classmethod
172    def init_from_human_encoding(cls, uri):
173        mo = cls.HUMAN_RE.search(uri)
174        assert mo, uri
175        return cls(base32.a2b(mo.group(1)))
176
177    @classmethod
178    def init_from_string(cls, uri):
179        mo = cls.STRING_RE.search(uri)
180        assert mo, uri
181        return cls(base32.a2b(mo.group(1)))
182
183    def to_string(self):
184        return 'URI:LIT:%s' % base32.b2a(self.data)
185
186    def is_readonly(self):
187        return True
188    def is_mutable(self):
189        return False
190    def get_readonly(self):
191        return self
192    def get_storage_index(self):
193        return None
194
195    def get_verify_cap(self):
196        # LIT files need no verification, all the data is present in the URI
197        return None
198
199    def get_size(self):
200        return len(self.data)
201
202class WriteableSSKFileURI(_BaseURI):
203    implements(IURI, IMutableFileURI)
204
205    BASE_STRING='URI:SSK:'
206    STRING_RE=re.compile('^'+BASE_STRING+BASE32STR_128bits+':'+
207                         BASE32STR_256bits+'$')
208    HUMAN_RE=re.compile('^'+OPTIONALHTTPLEAD+'URI'+SEP+'SSK'+SEP+
209                        BASE32STR_128bits+SEP+BASE32STR_256bits+'$')
210
211    def __init__(self, writekey, fingerprint):
212        self.writekey = writekey
213        self.readkey = hashutil.ssk_readkey_hash(writekey)
214        self.storage_index = hashutil.ssk_storage_index_hash(self.readkey)
215        assert len(self.storage_index) == 16
216        self.fingerprint = fingerprint
217
218    @classmethod
219    def init_from_human_encoding(cls, uri):
220        mo = cls.HUMAN_RE.search(uri)
221        if not mo:
222            raise BadURIError("'%s' doesn't look like a cap" % (uri,))
223        return cls(base32.a2b(mo.group(1)), base32.a2b(mo.group(2)))
224
225    @classmethod
226    def init_from_string(cls, uri):
227        mo = cls.STRING_RE.search(uri)
228        if not mo:
229            raise BadURIError("'%s' doesn't look like a %s cap" % (uri, cls))
230        return cls(base32.a2b(mo.group(1)), base32.a2b(mo.group(2)))
231
232    def to_string(self):
233        assert isinstance(self.writekey, str)
234        assert isinstance(self.fingerprint, str)
235        return 'URI:SSK:%s:%s' % (base32.b2a(self.writekey),
236                                  base32.b2a(self.fingerprint))
237
238    def __repr__(self):
239        return "<%s %s>" % (self.__class__.__name__, self.abbrev())
240
241    def abbrev(self):
242        return base32.b2a(self.writekey[:5])
243    def abbrev_si(self):
244        return base32.b2a(self.storage_index)[:5]
245
246    def is_readonly(self):
247        return False
248    def is_mutable(self):
249        return True
250    def get_readonly(self):
251        return ReadonlySSKFileURI(self.readkey, self.fingerprint)
252    def get_verify_cap(self):
253        return SSKVerifierURI(self.storage_index, self.fingerprint)
254
255class ReadonlySSKFileURI(_BaseURI):
256    implements(IURI, IMutableFileURI)
257
258    BASE_STRING='URI:SSK-RO:'
259    STRING_RE=re.compile('^URI:SSK-RO:'+BASE32STR_128bits+':'+BASE32STR_256bits+'$')
260    HUMAN_RE=re.compile('^'+OPTIONALHTTPLEAD+'URI'+SEP+'SSK-RO'+SEP+BASE32STR_128bits+SEP+BASE32STR_256bits+'$')
261
262    def __init__(self, readkey, fingerprint):
263        self.readkey = readkey
264        self.storage_index = hashutil.ssk_storage_index_hash(self.readkey)
265        assert len(self.storage_index) == 16
266        self.fingerprint = fingerprint
267
268    @classmethod
269    def init_from_human_encoding(cls, uri):
270        mo = cls.HUMAN_RE.search(uri)
271        if not mo:
272            raise BadURIError("'%s' doesn't look like a cap" % (uri,))
273        return cls(base32.a2b(mo.group(1)), base32.a2b(mo.group(2)))
274
275    @classmethod
276    def init_from_string(cls, uri):
277        mo = cls.STRING_RE.search(uri)
278        if not mo:
279            raise BadURIError("'%s' doesn't look like a cap" % (uri,))
280        return cls(base32.a2b(mo.group(1)), base32.a2b(mo.group(2)))
281
282    def to_string(self):
283        assert isinstance(self.readkey, str)
284        assert isinstance(self.fingerprint, str)
285        return 'URI:SSK-RO:%s:%s' % (base32.b2a(self.readkey),
286                                     base32.b2a(self.fingerprint))
287
288    def __repr__(self):
289        return "<%s %s>" % (self.__class__.__name__, self.abbrev())
290
291    def abbrev(self):
292        return base32.b2a(self.readkey[:5])
293    def abbrev_si(self):
294        return base32.b2a(self.storage_index)[:5]
295
296    def is_readonly(self):
297        return True
298    def is_mutable(self):
299        return True
300    def get_readonly(self):
301        return self
302    def get_verify_cap(self):
303        return SSKVerifierURI(self.storage_index, self.fingerprint)
304
305class SSKVerifierURI(_BaseURI):
306    implements(IVerifierURI)
307
308    BASE_STRING='URI:SSK-Verifier:'
309    STRING_RE=re.compile('^'+BASE_STRING+BASE32STR_128bits+':'+BASE32STR_256bits+'$')
310    HUMAN_RE=re.compile('^'+OPTIONALHTTPLEAD+'URI'+SEP+'SSK-Verifier'+SEP+BASE32STR_128bits+SEP+BASE32STR_256bits+'$')
311
312    def __init__(self, storage_index, fingerprint):
313        assert len(storage_index) == 16
314        self.storage_index = storage_index
315        self.fingerprint = fingerprint
316
317    @classmethod
318    def init_from_human_encoding(cls, uri):
319        mo = cls.HUMAN_RE.search(uri)
320        assert mo, uri
321        return cls(si_a2b(mo.group(1)), base32.a2b(mo.group(2)))
322
323    @classmethod
324    def init_from_string(cls, uri):
325        mo = cls.STRING_RE.search(uri)
326        assert mo, (uri, cls)
327        return cls(si_a2b(mo.group(1)), base32.a2b(mo.group(2)))
328
329    def to_string(self):
330        assert isinstance(self.storage_index, str)
331        assert isinstance(self.fingerprint, str)
332        return 'URI:SSK-Verifier:%s:%s' % (si_b2a(self.storage_index),
333                                           base32.b2a(self.fingerprint))
334
335class _DirectoryBaseURI(_BaseURI):
336    implements(IURI, IDirnodeURI)
337    def __init__(self, filenode_uri=None):
338        self._filenode_uri = filenode_uri
339
340    def __repr__(self):
341        return "<%s %s>" % (self.__class__.__name__, self.abbrev())
342
343    @classmethod
344    def init_from_string(cls, uri):
345        mo = cls.BASE_STRING_RE.search(uri)
346        if not mo:
347            raise BadURIError("'%s' doesn't look like a %s cap" % (uri, cls))
348        bits = uri[mo.end():]
349        fn = cls.INNER_URI_CLASS.init_from_string(
350            cls.INNER_URI_CLASS.BASE_STRING+bits)
351        return cls(fn)
352
353    @classmethod
354    def init_from_human_encoding(cls, uri):
355        mo = cls.BASE_HUMAN_RE.search(uri)
356        if not mo:
357            raise BadURIError("'%s' doesn't look like a %s cap" % (uri, cls))
358        bits = uri[mo.end():]
359        while bits and bits[-1] == '/':
360            bits = bits[:-1]
361        fn = cls.INNER_URI_CLASS.init_from_string(
362            cls.INNER_URI_CLASS.BASE_STRING+urllib.unquote(bits))
363        return cls(fn)
364
365    def to_string(self):
366        fnuri = self._filenode_uri.to_string()
367        mo = re.match(self.INNER_URI_CLASS.BASE_STRING, fnuri)
368        assert mo, fnuri
369        bits = fnuri[mo.end():]
370        return self.BASE_STRING+bits
371
372    def abbrev(self):
373        return self._filenode_uri.to_string().split(':')[2][:5]
374    def abbrev_si(self):
375        return base32.b2a(self._filenode_uri.storage_index)[:5]
376
377    def get_filenode_uri(self):
378        return self._filenode_uri
379
380    def is_mutable(self):
381        return True
382
383    def get_verify_cap(self):
384        return DirectoryURIVerifier(self._filenode_uri.get_verify_cap())
385
386    def get_storage_index(self):
387        return self._filenode_uri.get_storage_index()
388
389class DirectoryURI(_DirectoryBaseURI):
390    implements(IDirectoryURI)
391
392    BASE_STRING='URI:DIR2:'
393    BASE_STRING_RE=re.compile('^'+BASE_STRING)
394    BASE_HUMAN_RE=re.compile('^'+OPTIONALHTTPLEAD+'URI'+SEP+'DIR2'+SEP)
395    INNER_URI_CLASS=WriteableSSKFileURI
396
397    def __init__(self, filenode_uri=None):
398        if filenode_uri:
399            assert not filenode_uri.is_readonly()
400        _DirectoryBaseURI.__init__(self, filenode_uri)
401
402    def is_readonly(self):
403        return False
404
405    def get_readonly(self):
406        return ReadonlyDirectoryURI(self._filenode_uri.get_readonly())
407
408class ReadonlyDirectoryURI(_DirectoryBaseURI):
409    implements(IReadonlyDirectoryURI)
410
411    BASE_STRING='URI:DIR2-RO:'
412    BASE_STRING_RE=re.compile('^'+BASE_STRING)
413    BASE_HUMAN_RE=re.compile('^'+OPTIONALHTTPLEAD+'URI'+SEP+'DIR2-RO'+SEP)
414    INNER_URI_CLASS=ReadonlySSKFileURI
415
416    def __init__(self, filenode_uri=None):
417        if filenode_uri:
418            assert filenode_uri.is_readonly()
419        _DirectoryBaseURI.__init__(self, filenode_uri)
420
421    def is_readonly(self):
422        return True
423
424    def get_readonly(self):
425        return self
426
427class _ImmutableDirectoryBaseURI(_DirectoryBaseURI):
428    def __init__(self, filenode_uri=None):
429        if filenode_uri:
430            assert isinstance(filenode_uri, self.INNER_URI_CLASS), filenode_uri
431        _DirectoryBaseURI.__init__(self, filenode_uri)
432
433    def is_mutable(self):
434        return False
435
436    def is_readonly(self):
437        return True
438
439    def get_readonly(self):
440        return self
441
442class ImmutableDirectoryURI(_ImmutableDirectoryBaseURI):
443    BASE_STRING='URI:DIR2-CHK:'
444    BASE_STRING_RE=re.compile('^'+BASE_STRING)
445    BASE_HUMAN_RE=re.compile('^'+OPTIONALHTTPLEAD+'URI'+SEP+'DIR2-CHK'+SEP)
446    INNER_URI_CLASS=CHKFileURI
447    def get_verify_cap(self):
448        vcap = self._filenode_uri.get_verify_cap()
449        return ImmutableDirectoryURIVerifier(vcap)
450
451
452class LiteralDirectoryURI(_ImmutableDirectoryBaseURI):
453    BASE_STRING='URI:DIR2-LIT:'
454    BASE_STRING_RE=re.compile('^'+BASE_STRING)
455    BASE_HUMAN_RE=re.compile('^'+OPTIONALHTTPLEAD+'URI'+SEP+'DIR2-LIT'+SEP)
456    INNER_URI_CLASS=LiteralFileURI
457    def __init__(self, data=None):
458        filenode_uri = LiteralFileURI(data)
459        _ImmutableDirectoryBaseURI.__init__(self, filenode_uri)
460    def get_verify_cap(self):
461        # LIT caps have no verifier, since they aren't distributed
462        return None
463
464
465class DirectoryURIVerifier(_DirectoryBaseURI):
466    implements(IVerifierURI)
467
468    BASE_STRING='URI:DIR2-Verifier:'
469    BASE_STRING_RE=re.compile('^'+BASE_STRING)
470    BASE_HUMAN_RE=re.compile('^'+OPTIONALHTTPLEAD+'URI'+SEP+'DIR2-Verifier'+SEP)
471    INNER_URI_CLASS=SSKVerifierURI
472
473    def __init__(self, filenode_uri=None):
474        if filenode_uri:
475            filenode_uri = IVerifierURI(filenode_uri)
476        self._filenode_uri = filenode_uri
477
478    def get_filenode_uri(self):
479        return self._filenode_uri
480
481class ImmutableDirectoryURIVerifier(DirectoryURIVerifier):
482    implements(IVerifierURI)
483    BASE_STRING='URI:DIR2-CHK-Verifier:'
484    BASE_STRING_RE=re.compile('^'+BASE_STRING)
485    BASE_HUMAN_RE=re.compile('^'+OPTIONALHTTPLEAD+'URI'+SEP+'DIR2-CHK-VERIFIER'+SEP)
486    INNER_URI_CLASS=CHKFileVerifierURI
487
488class UnknownURI:
489    def __init__(self, uri):
490        self._uri = uri
491    def to_string(self):
492        return self._uri
493
494def from_string(s):
495    if not isinstance(s, str):
496        raise TypeError("unknown URI type: %s.." % str(s)[:100])
497    elif s.startswith('URI:CHK:'):
498        return CHKFileURI.init_from_string(s)
499    elif s.startswith('URI:CHK-Verifier:'):
500        return CHKFileVerifierURI.init_from_string(s)
501    elif s.startswith('URI:LIT:'):
502        return LiteralFileURI.init_from_string(s)
503    elif s.startswith('URI:SSK:'):
504        return WriteableSSKFileURI.init_from_string(s)
505    elif s.startswith('URI:SSK-RO:'):
506        return ReadonlySSKFileURI.init_from_string(s)
507    elif s.startswith('URI:SSK-Verifier:'):
508        return SSKVerifierURI.init_from_string(s)
509    elif s.startswith('URI:DIR2:'):
510        return DirectoryURI.init_from_string(s)
511    elif s.startswith('URI:DIR2-RO:'):
512        return ReadonlyDirectoryURI.init_from_string(s)
513    elif s.startswith('URI:DIR2-Verifier:'):
514        return DirectoryURIVerifier.init_from_string(s)
515    elif s.startswith('URI:DIR2-CHK:'):
516        return ImmutableDirectoryURI.init_from_string(s)
517    elif s.startswith('URI:DIR2-LIT:'):
518        return LiteralDirectoryURI.init_from_string(s)
519    return UnknownURI(s)
520
521def is_uri(s):
522    try:
523        uri = from_string(s)
524        return True
525    except (TypeError, AssertionError):
526        return False
527
528def from_string_dirnode(s):
529    u = from_string(s)
530    assert IDirnodeURI.providedBy(u)
531    return u
532
533registerAdapter(from_string_dirnode, str, IDirnodeURI)
534
535def from_string_filenode(s):
536    u = from_string(s)
537    assert IFileURI.providedBy(u)
538    return u
539
540registerAdapter(from_string_filenode, str, IFileURI)
541
542def from_string_mutable_filenode(s):
543    u = from_string(s)
544    assert IMutableFileURI.providedBy(u)
545    return u
546registerAdapter(from_string_mutable_filenode, str, IMutableFileURI)
547
548def from_string_verifier(s):
549    u = from_string(s)
550    assert IVerifierURI.providedBy(u)
551    return u
552registerAdapter(from_string_verifier, str, IVerifierURI)
553
554
555def pack_extension(data):
556    pieces = []
557    for k in sorted(data.keys()):
558        value = data[k]
559        if isinstance(value, (int, long)):
560            value = "%d" % value
561        assert isinstance(value, str), k
562        assert re.match(r'^[a-zA-Z_\-]+$', k)
563        pieces.append(k + ':' + hashutil.netstring(value))
564    uri_extension = ''.join(pieces)
565    return uri_extension
566
567def unpack_extension(data):
568    d = {}
569    while data:
570        colon = data.index(':')
571        key = data[:colon]
572        data = data[colon+1:]
573
574        colon = data.index(':')
575        number = data[:colon]
576        length = int(number)
577        data = data[colon+1:]
578
579        value = data[:length]
580        assert data[length] == ','
581        data = data[length+1:]
582
583        d[key] = value
584
585    # convert certain things to numbers
586    for intkey in ('size', 'segment_size', 'num_segments',
587                   'needed_shares', 'total_shares'):
588        if intkey in d:
589            d[intkey] = int(d[intkey])
590    return d
591
592
593def unpack_extension_readable(data):
594    unpacked = unpack_extension(data)
595    unpacked["UEB_hash"] = hashutil.uri_extension_hash(data)
596    for k in sorted(unpacked.keys()):
597        if 'hash' in k:
598            unpacked[k] = base32.b2a(unpacked[k])
599    return unpacked
600
Note: See TracBrowser for help on using the repository browser.