]> git.rkrishnan.org Git - tahoe-lafs/tahoe-lafs.git/blob - src/allmydata/uri.py
add parser for immutable directory caps: DIR2-CHK, DIR2-LIT, DIR2-CHK-Verifier
[tahoe-lafs/tahoe-lafs.git] / src / allmydata / uri.py
1
2 import re, urllib
3 from zope.interface import implements
4 from twisted.python.components import registerAdapter
5 from allmydata.storage.server import si_a2b, si_b2a
6 from allmydata.util import base32, hashutil
7 from allmydata.interfaces import IURI, IDirnodeURI, IFileURI, IImmutableFileURI, \
8     IVerifierURI, IMutableFileURI, IDirectoryURI, IReadonlyDirectoryURI
9
10 class 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
17 BASE32STR_128bits = '(%s{25}%s)' % (base32.BASE32CHAR, base32.BASE32CHAR_3bits)
18 BASE32STR_256bits = '(%s{51}%s)' % (base32.BASE32CHAR, base32.BASE32CHAR_1bits)
19
20 SEP='(?::|%3A)'
21 NUMBER='([0-9]+)'
22 NUMBER_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.
26 OPTIONALHTTPLEAD=r'(?:https?://(?:[^:/]+)(?::%s)?/uri/)?' % NUMBER_IGNORE
27
28
29 class _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
48 class 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
115 class 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
160 class 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
202 class 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
255 class 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
305 class 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
335 class _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
389 class 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
408 class 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
427 class _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
442 class 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
452 class 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
465 class 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
481 class 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
488 class UnknownURI:
489     def __init__(self, uri):
490         self._uri = uri
491     def to_string(self):
492         return self._uri
493
494 def 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
521 def is_uri(s):
522     try:
523         uri = from_string(s)
524         return True
525     except (TypeError, AssertionError):
526         return False
527
528 def from_string_dirnode(s):
529     u = from_string(s)
530     assert IDirnodeURI.providedBy(u)
531     return u
532
533 registerAdapter(from_string_dirnode, str, IDirnodeURI)
534
535 def from_string_filenode(s):
536     u = from_string(s)
537     assert IFileURI.providedBy(u)
538     return u
539
540 registerAdapter(from_string_filenode, str, IFileURI)
541
542 def from_string_mutable_filenode(s):
543     u = from_string(s)
544     assert IMutableFileURI.providedBy(u)
545     return u
546 registerAdapter(from_string_mutable_filenode, str, IMutableFileURI)
547
548 def from_string_verifier(s):
549     u = from_string(s)
550     assert IVerifierURI.providedBy(u)
551     return u
552 registerAdapter(from_string_verifier, str, IVerifierURI)
553
554
555 def 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
567 def 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
593 def 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