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