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