mirror of
https://github.com/tahoe-lafs/tahoe-lafs.git
synced 2025-02-20 17:52:50 +00:00
mutable: first pass at dirnodes, filenodes, new URIs. Some test coverage.
The URI typenames need revision, and only a few dirnode methods are implemented. Filenodes are non-functional, but URI/key-management is in place. There are a lot of classes with names like "NewDirectoryNode" that will need to be rename once we decide what (if any) backwards compatibility want to retain.
This commit is contained in:
parent
fb3eddafdb
commit
1d8a4cdfe7
@ -207,3 +207,29 @@ class Client(node.Node, Referenceable, testutil.PollMixin):
|
||||
d.addCallback(lambda res: None)
|
||||
return d
|
||||
|
||||
|
||||
def create_empty_dirnode(self):
|
||||
from allmydata.mutable import NewDirectoryNode
|
||||
n = NewDirectoryNode(self)
|
||||
d = n.create()
|
||||
d.addCallback(lambda res: n)
|
||||
return d
|
||||
|
||||
def create_dirnode_from_uri(self, u):
|
||||
from allmydata.mutable import NewDirectoryNode
|
||||
return NewDirectoryNode(self).init_from_uri(u)
|
||||
|
||||
def create_mutable_file(self, contents=""):
|
||||
from allmydata.mutable import MutableFileNode
|
||||
n = MutableFileNode(self)
|
||||
d = n.create(contents)
|
||||
d.addCallback(lambda res: n)
|
||||
return d
|
||||
|
||||
def create_mutable_file_from_uri(self, u):
|
||||
from allmydata.mutable import MutableFileNode
|
||||
return MutableFileNode(self).init_from_uri(u)
|
||||
|
||||
def create_file_from_uri(self, u):
|
||||
from allmydata.mutable import FileNode
|
||||
return FileNode(u, self)
|
||||
|
@ -427,6 +427,12 @@ class IFileURI(Interface):
|
||||
def get_size():
|
||||
"""Return the length (in bytes) of the file that I represent."""
|
||||
|
||||
class IMutableFileURI(Interface):
|
||||
"""I am a URI which represents a mutable filenode."""
|
||||
pass
|
||||
class INewDirectoryURI(Interface):
|
||||
pass
|
||||
|
||||
|
||||
class IFileNode(Interface):
|
||||
def download(target):
|
||||
@ -453,6 +459,45 @@ class IFileNode(Interface):
|
||||
def check():
|
||||
"""Perform a file check. See IChecker.check for details."""
|
||||
|
||||
class IMutableFileNode(Interface):
|
||||
def download_to_data():
|
||||
"""Download the file's contents. Return a Deferred that fires with
|
||||
those contents. If there are multiple retrievable versions in the
|
||||
grid (because you failed to avoid simultaneous writes, see
|
||||
docs/mutable.txt), this will return the first version that it can
|
||||
reconstruct, and will silently ignore the others. In the future, a
|
||||
more advanced API will signal and provide access to the multiple
|
||||
heads."""
|
||||
def replace(newdata):
|
||||
"""Replace the old contents with the new data. Returns a Deferred
|
||||
that fires (with None) when the operation is complete.
|
||||
|
||||
If the node detects that there are multiple outstanding versions of
|
||||
the file, this will raise ConsistencyError, and may leave the
|
||||
distributed file in an unusual state (the node will try to ensure
|
||||
that at least one version of the file remains retrievable, but it may
|
||||
or may not be the one you just tried to upload). You should respond
|
||||
to this by downloading the current contents of the file and retrying
|
||||
the replace() operation.
|
||||
"""
|
||||
|
||||
def get_uri():
|
||||
pass
|
||||
def get_verifier():
|
||||
pass
|
||||
def check():
|
||||
pass
|
||||
|
||||
def get_writekey():
|
||||
"""Return this filenode's writekey, or None if the node does not have
|
||||
write-capability. This may be used to assist with data structures
|
||||
that need to make certain data available only to writers, such as the
|
||||
read-write child caps in dirnodes. The recommended process is to have
|
||||
reader-visible data be submitted to the filenode in the clear (where
|
||||
it will be encrypted by the filenode using the readkey), but encrypt
|
||||
writer-visible data using this writekey.
|
||||
"""
|
||||
|
||||
class IDirectoryNode(Interface):
|
||||
def is_mutable():
|
||||
"""Return True if this directory is mutable, False if it is read-only.
|
||||
|
313
src/allmydata/mutable.py
Normal file
313
src/allmydata/mutable.py
Normal file
@ -0,0 +1,313 @@
|
||||
|
||||
import os
|
||||
from zope.interface import implements
|
||||
from twisted.internet import defer
|
||||
import simplejson
|
||||
from allmydata.interfaces import IMutableFileNode, IDirectoryNode,\
|
||||
IMutableFileURI, INewDirectoryURI, IURI, IFileNode, NotMutableError
|
||||
from allmydata.util import hashutil
|
||||
from allmydata.util.hashutil import netstring
|
||||
from allmydata.dirnode import IntegrityCheckError
|
||||
from allmydata.uri import WriteableSSKFileURI, NewDirectoryURI
|
||||
from allmydata.Crypto.Cipher import AES
|
||||
|
||||
class MutableFileNode:
|
||||
implements(IMutableFileNode)
|
||||
|
||||
def __init__(self, client):
|
||||
self._client = client
|
||||
self._pubkey = None # filled in upon first read
|
||||
self._privkey = None # filled in if we're mutable
|
||||
self._sharemap = {} # known shares, shnum-to-nodeid
|
||||
|
||||
def init_from_uri(self, myuri):
|
||||
self._uri = IMutableFileURI(myuri)
|
||||
return self
|
||||
|
||||
def create(self, initial_contents):
|
||||
"""Call this when the filenode is first created. This will generate
|
||||
the keys, generate the initial shares, allocate shares, and upload
|
||||
the initial contents. Returns a Deferred that fires (with the
|
||||
MutableFileNode instance you should use) when it completes.
|
||||
"""
|
||||
self._privkey = "very private"
|
||||
self._pubkey = "public"
|
||||
self._writekey = hashutil.ssk_writekey_hash(self._privkey)
|
||||
self._fingerprint = hashutil.ssk_pubkey_fingerprint_hash(self._pubkey)
|
||||
self._uri = WriteableSSKFileURI(self._writekey, self._fingerprint)
|
||||
d = defer.succeed(None)
|
||||
return d
|
||||
|
||||
|
||||
def get_uri(self):
|
||||
return self._uri.to_string()
|
||||
|
||||
def is_mutable(self):
|
||||
return self._uri.is_mutable()
|
||||
|
||||
def __hash__(self):
|
||||
return hash((self.__class__, self.uri))
|
||||
def __cmp__(self, them):
|
||||
if cmp(type(self), type(them)):
|
||||
return cmp(type(self), type(them))
|
||||
if cmp(self.__class__, them.__class__):
|
||||
return cmp(self.__class__, them.__class__)
|
||||
return cmp(self.uri, them.uri)
|
||||
|
||||
def get_verifier(self):
|
||||
return IMutableFileURI(self.uri).get_verifier()
|
||||
|
||||
def check(self):
|
||||
verifier = self.get_verifier()
|
||||
return self._client.getServiceNamed("checker").check(verifier)
|
||||
|
||||
def download(self, target):
|
||||
#downloader = self._client.getServiceNamed("downloader")
|
||||
#return downloader.download(self.uri, target)
|
||||
raise NotImplementedError
|
||||
|
||||
def download_to_data(self):
|
||||
#downloader = self._client.getServiceNamed("downloader")
|
||||
#return downloader.download_to_data(self.uri)
|
||||
return defer.succeed("this isn't going to fool you, is it")
|
||||
|
||||
def replace(self, newdata):
|
||||
return defer.succeed(None)
|
||||
|
||||
# use client.create_mutable_file() to make one of these
|
||||
|
||||
def split_netstring(data, numstrings, allow_leftover=False):
|
||||
"""like string.split(), but extracts netstrings. If allow_leftover=False,
|
||||
returns numstrings elements, and throws ValueError if there was leftover
|
||||
data. If allow_leftover=True, returns numstrings+1 elements, in which the
|
||||
last element is the leftover data (possibly an empty string)"""
|
||||
elements = []
|
||||
assert numstrings >= 0
|
||||
while data:
|
||||
colon = data.index(":")
|
||||
length = int(data[:colon])
|
||||
string = data[colon+1:colon+1+length]
|
||||
assert len(string) == length
|
||||
elements.append(string)
|
||||
assert data[colon+1+length] == ","
|
||||
data = data[colon+1+length+1:]
|
||||
if len(elements) == numstrings:
|
||||
break
|
||||
if len(elements) < numstrings:
|
||||
raise ValueError("ran out of netstrings")
|
||||
if allow_leftover:
|
||||
return tuple(elements + [data])
|
||||
if data:
|
||||
raise ValueError("leftover data in netstrings")
|
||||
return tuple(elements)
|
||||
|
||||
class NewDirectoryNode:
|
||||
implements(IDirectoryNode)
|
||||
filenode_class = MutableFileNode
|
||||
|
||||
def __init__(self, client):
|
||||
self._client = client
|
||||
def init_from_uri(self, myuri):
|
||||
u = INewDirectoryURI(myuri)
|
||||
self._uri = u
|
||||
self._node = self.filenode_class(self._client)
|
||||
self._node.init_from_uri(u.get_filenode_uri())
|
||||
return self
|
||||
|
||||
def create(self):
|
||||
# first we create a MutableFileNode with empty_contents, then use its
|
||||
# URI to create our own.
|
||||
self._node = self.filenode_class(self._client)
|
||||
empty_contents = self._pack_contents({})
|
||||
d = self._node.create(empty_contents)
|
||||
d.addCallback(self._filenode_created)
|
||||
return d
|
||||
def _filenode_created(self, res):
|
||||
self._uri = NewDirectoryURI(self._node._uri)
|
||||
return None
|
||||
|
||||
def _read(self):
|
||||
d = self._node.download_to_data()
|
||||
d.addCallback(self._unpack_contents)
|
||||
return d
|
||||
|
||||
def _encrypt_rwcap(self, rwcap):
|
||||
assert isinstance(rwcap, str)
|
||||
IV = os.urandom(16)
|
||||
key = hashutil.mutable_rwcap_key_hash(IV, self._node.writekey)
|
||||
counterstart = "\x00"*16
|
||||
cryptor = AES.new(key=key, mode=AES.MODE_CTR, counterstart=counterstart)
|
||||
crypttext = cryptor.encrypt(rwcap)
|
||||
mac = hashutil.hmac(key, IV + crypttext)
|
||||
assert len(mac) == 32
|
||||
return IV + crypttext + mac
|
||||
|
||||
def _decrypt_rwcapdata(self, encwrcap):
|
||||
IV = encwrcap[:16]
|
||||
crypttext = encwrcap[16:-32]
|
||||
mac = encwrcap[-32:]
|
||||
key = hashutil.mutable_rwcap_key_hash(IV, self._node.writekey)
|
||||
if mac != hashutil.hmac(key, IV+crypttext):
|
||||
raise IntegrityCheckError("HMAC does not match, crypttext is corrupted")
|
||||
counterstart = "\x00"*16
|
||||
cryptor = AES.new(key=key, mode=AES.MODE_CTR, counterstart=counterstart)
|
||||
plaintext = cryptor.decrypt(crypttext)
|
||||
return plaintext
|
||||
|
||||
def _create_node(self, child_uri):
|
||||
u = IURI(child_uri)
|
||||
if INewDirectoryURI.providedBy(u):
|
||||
return self._client.create_dirnode_from_uri(u)
|
||||
if IFileNode.providedBy(u):
|
||||
return self._client.create_file_from_uri(u)
|
||||
if IMutableFileURI.providedBy(u):
|
||||
return self._client.create_mutable_file_from_uri(u)
|
||||
raise TypeError("cannot handle URI")
|
||||
|
||||
def _unpack_contents(self, data):
|
||||
# the directory is serialized as a list of netstrings, one per child.
|
||||
# Each child is serialized as a list of four netstrings: (name,
|
||||
# rocap, rwcap, metadata), in which the name,rocap,metadata are in
|
||||
# cleartext. The rwcap is formatted as:
|
||||
# pack("16ss32s", iv, AES(H(writekey+iv), plaintextrwcap), mac)
|
||||
assert isinstance(data, str)
|
||||
# an empty directory is serialized as an empty string
|
||||
if data == "":
|
||||
return {}
|
||||
mutable = self.is_mutable()
|
||||
children = {}
|
||||
while len(data) > 0:
|
||||
entry, data = split_netstring(data, 1, True)
|
||||
name, rocap, rwcapdata, metadata_s = split_netstring(entry, 4)
|
||||
if mutable:
|
||||
rwcap = self._decrypt_rwcapdata(rwcapdata)
|
||||
child = self._create_node(rwcap)
|
||||
else:
|
||||
child = self._create_node(rocap)
|
||||
metadata = simplejson.loads(metadata_s)
|
||||
assert isinstance(metadata, dict)
|
||||
children[name] = (child, metadata)
|
||||
return children
|
||||
|
||||
def _pack_contents(self, children):
|
||||
# expects children in the same format as _unpack_contents
|
||||
assert isinstance(children, dict)
|
||||
entries = []
|
||||
for name in sorted(children.keys()):
|
||||
child, metadata = children[name]
|
||||
assert (IFileNode.providedBy(child)
|
||||
or IMutableFileNode.providedBy(child)
|
||||
or IDirectoryNode.providedBy(child))
|
||||
assert isinstance(metadata, dict)
|
||||
rwcap = child.get_uri() # might be RO if the child is not mutable
|
||||
rocap = child.get_readonly()
|
||||
entry = "".join([netstring(name),
|
||||
netstring(rocap),
|
||||
netstring(self._encrypt_rwcap(rwcap)),
|
||||
netstring(simplejson.dumps(metadata))])
|
||||
entries.append(netstring(entry))
|
||||
return "".join(entries)
|
||||
|
||||
def is_mutable(self):
|
||||
return self._node.is_mutable()
|
||||
|
||||
def get_uri(self):
|
||||
return self._uri.to_string()
|
||||
|
||||
def get_immutable_uri(self):
|
||||
return self._uri.get_readonly().to_string()
|
||||
|
||||
def get_verifier(self):
|
||||
return self._uri.get_verifier().to_string()
|
||||
|
||||
def check(self):
|
||||
"""Perform a file check. See IChecker.check for details."""
|
||||
pass
|
||||
|
||||
def list(self):
|
||||
"""I return a Deferred that fires with a dictionary mapping child
|
||||
name to an IFileNode or IDirectoryNode."""
|
||||
return self._read()
|
||||
|
||||
def has_child(self, name):
|
||||
"""I return a Deferred that fires with a boolean, True if there
|
||||
exists a child of the given name, False if not."""
|
||||
d = self._read()
|
||||
d.addCallback(lambda children: children.has_key(name))
|
||||
return d
|
||||
|
||||
def get(self, name):
|
||||
"""I return a Deferred that fires with a specific named child node,
|
||||
either an IFileNode or an IDirectoryNode."""
|
||||
d = self._read()
|
||||
d.addCallback(lambda children: children[name])
|
||||
return d
|
||||
|
||||
def get_child_at_path(self, path):
|
||||
"""Transform a child path into an IDirectoryNode or IFileNode.
|
||||
|
||||
I perform a recursive series of 'get' operations to find the named
|
||||
descendant node. I return a Deferred that fires with the node, or
|
||||
errbacks with IndexError if the node could not be found.
|
||||
|
||||
The path can be either a single string (slash-separated) or a list of
|
||||
path-name elements.
|
||||
"""
|
||||
|
||||
def set_uri(self, name, child_uri, metadata={}):
|
||||
"""I add a child (by URI) at the specific name. I return a Deferred
|
||||
that fires when the operation finishes. I will replace any existing
|
||||
child of the same name.
|
||||
|
||||
The child_uri could be for a file, or for a directory (either
|
||||
read-write or read-only, using a URI that came from get_uri() ).
|
||||
|
||||
If this directory node is read-only, the Deferred will errback with a
|
||||
NotMutableError."""
|
||||
return self.set_node(name, self._create_node(child_uri), metadata)
|
||||
|
||||
def set_node(self, name, child, metadata={}):
|
||||
"""I add a child at the specific name. I return a Deferred that fires
|
||||
when the operation finishes. This Deferred will fire with the child
|
||||
node that was just added. I will replace any existing child of the
|
||||
same name.
|
||||
|
||||
If this directory node is read-only, the Deferred will errback with a
|
||||
NotMutableError."""
|
||||
if self._node.is_readonly():
|
||||
return defer.fail(NotMutableError())
|
||||
d = self._read()
|
||||
def _add(children):
|
||||
children[name] = (child, metadata)
|
||||
new_contents = self._pack_contents(children)
|
||||
return self._node.replace(new_contents)
|
||||
d.addCallback(_add)
|
||||
d.addCallback(lambda res: None)
|
||||
return d
|
||||
|
||||
def add_file(self, name, uploadable):
|
||||
"""I upload a file (using the given IUploadable), then attach the
|
||||
resulting FileNode to the directory at the given name. I return a
|
||||
Deferred that fires (with the IFileNode of the uploaded file) when
|
||||
the operation completes."""
|
||||
|
||||
def delete(self, name):
|
||||
"""I remove the child at the specific name. I return a Deferred that
|
||||
fires when the operation finishes."""
|
||||
|
||||
def create_empty_directory(self, name):
|
||||
"""I create and attach an empty directory at the given name. I return
|
||||
a Deferred that fires when the operation finishes."""
|
||||
|
||||
def move_child_to(self, current_child_name, new_parent, new_child_name=None):
|
||||
"""I take one of my children and move them to a new parent. The child
|
||||
is referenced by name. On the new parent, the child will live under
|
||||
'new_child_name', which defaults to 'current_child_name'. I return a
|
||||
Deferred that fires when the operation finishes."""
|
||||
|
||||
def build_manifest(self):
|
||||
"""Return a frozenset of verifier-capability strings for all nodes
|
||||
(directories and files) reachable from this one."""
|
||||
|
||||
# use client.create_dirnode() to make one of these
|
||||
|
147
src/allmydata/test/test_mutable.py
Normal file
147
src/allmydata/test/test_mutable.py
Normal file
@ -0,0 +1,147 @@
|
||||
|
||||
from twisted.trial import unittest
|
||||
from twisted.internet import defer
|
||||
|
||||
from allmydata import mutable, uri
|
||||
from allmydata.mutable import split_netstring
|
||||
from allmydata.util.hashutil import netstring
|
||||
|
||||
class Netstring(unittest.TestCase):
|
||||
def test_split(self):
|
||||
a = netstring("hello") + netstring("world")
|
||||
self.failUnlessEqual(split_netstring(a, 2), ("hello", "world"))
|
||||
self.failUnlessEqual(split_netstring(a, 2, False), ("hello", "world"))
|
||||
self.failUnlessEqual(split_netstring(a, 2, True),
|
||||
("hello", "world", ""))
|
||||
self.failUnlessRaises(ValueError, split_netstring, a+" extra", 2)
|
||||
self.failUnlessRaises(ValueError, split_netstring, a+" extra", 2, False)
|
||||
|
||||
def test_extra(self):
|
||||
a = netstring("hello")
|
||||
self.failUnlessEqual(split_netstring(a, 1, True), ("hello", ""))
|
||||
b = netstring("hello") + "extra stuff"
|
||||
self.failUnlessEqual(split_netstring(b, 1, True),
|
||||
("hello", "extra stuff"))
|
||||
|
||||
def test_nested(self):
|
||||
a = netstring("hello") + netstring("world") + "extra stuff"
|
||||
b = netstring("a") + netstring("is") + netstring(a) + netstring(".")
|
||||
top = split_netstring(b, 4)
|
||||
self.failUnlessEqual(len(top), 4)
|
||||
self.failUnlessEqual(top[0], "a")
|
||||
self.failUnlessEqual(top[1], "is")
|
||||
self.failUnlessEqual(top[2], a)
|
||||
self.failUnlessEqual(top[3], ".")
|
||||
self.failUnlessRaises(ValueError, split_netstring, a, 2)
|
||||
self.failUnlessRaises(ValueError, split_netstring, a, 2, False)
|
||||
bottom = split_netstring(a, 2, True)
|
||||
self.failUnlessEqual(bottom, ("hello", "world", "extra stuff"))
|
||||
|
||||
class FakeFilenode(mutable.MutableFileNode):
|
||||
def init_from_uri(self, myuri):
|
||||
self._uri = myuri
|
||||
self.writekey = myuri.writekey
|
||||
return self
|
||||
def create(self, initial_contents):
|
||||
self.contents = initial_contents
|
||||
self.init_from_uri(uri.WriteableSSKFileURI("key", "fingerprint"))
|
||||
return defer.succeed(None)
|
||||
def download_to_data(self):
|
||||
return defer.succeed(self.contents)
|
||||
def replace(self, newdata):
|
||||
self.contents = newdata
|
||||
return defer.succeed(None)
|
||||
def is_readonly(self):
|
||||
return False
|
||||
def get_readonly(self):
|
||||
return "fake readonly"
|
||||
|
||||
class FakeNewDirectoryNode(mutable.NewDirectoryNode):
|
||||
filenode_class = FakeFilenode
|
||||
|
||||
class MyClient:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def create_empty_dirnode(self):
|
||||
n = FakeNewDirectoryNode(self)
|
||||
d = n.create()
|
||||
d.addCallback(lambda res: n)
|
||||
return d
|
||||
|
||||
def create_dirnode_from_uri(self, u):
|
||||
return FakeNewDirectoryNode(self).init_from_uri(u)
|
||||
|
||||
def create_mutable_file(self, contents=""):
|
||||
n = FakeFilenode(self)
|
||||
d = n.create(contents)
|
||||
d.addCallback(lambda res: n)
|
||||
return d
|
||||
def create_mutable_file_from_uri(self, u):
|
||||
return FakeFilenode(self).init_from_uri(u)
|
||||
|
||||
|
||||
class Filenode(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.client = MyClient()
|
||||
|
||||
def test_create(self):
|
||||
d = self.client.create_mutable_file()
|
||||
def _created(n):
|
||||
d = n.replace("contents 1")
|
||||
d.addCallback(lambda res: self.failUnlessIdentical(res, None))
|
||||
d.addCallback(lambda res: n.download_to_data())
|
||||
d.addCallback(lambda res: self.failUnlessEqual(res, "contents 1"))
|
||||
d.addCallback(lambda res: n.replace("contents 2"))
|
||||
d.addCallback(lambda res: n.download_to_data())
|
||||
d.addCallback(lambda res: self.failUnlessEqual(res, "contents 2"))
|
||||
return d
|
||||
d.addCallback(_created)
|
||||
return d
|
||||
|
||||
def test_create_with_initial_contents(self):
|
||||
d = self.client.create_mutable_file("contents 1")
|
||||
def _created(n):
|
||||
d = n.download_to_data()
|
||||
d.addCallback(lambda res: self.failUnlessEqual(res, "contents 1"))
|
||||
d.addCallback(lambda res: n.replace("contents 2"))
|
||||
d.addCallback(lambda res: n.download_to_data())
|
||||
d.addCallback(lambda res: self.failUnlessEqual(res, "contents 2"))
|
||||
return d
|
||||
d.addCallback(_created)
|
||||
return d
|
||||
|
||||
class Dirnode(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.client = MyClient()
|
||||
|
||||
def test_create(self):
|
||||
d = self.client.create_empty_dirnode()
|
||||
def _check(n):
|
||||
self.failUnless(n.is_mutable())
|
||||
u = n.get_uri()
|
||||
self.failUnless(u)
|
||||
self.failUnless(u.startswith("URI:DIR2:"), u)
|
||||
u_ro = n.get_immutable_uri()
|
||||
self.failUnless(u_ro.startswith("URI:DIR2-RO:"), u_ro)
|
||||
u_v = n.get_verifier()
|
||||
self.failUnless(u_v.startswith("URI:DIR2-Verifier:"), u_v)
|
||||
|
||||
d = n.list()
|
||||
d.addCallback(lambda res: self.failUnlessEqual(res, {}))
|
||||
d.addCallback(lambda res: n.has_child("missing"))
|
||||
d.addCallback(lambda res: self.failIf(res))
|
||||
fake_file_uri = uri.WriteableSSKFileURI("a"*16,"b"*32)
|
||||
d.addCallback(lambda res: n.set_uri("child", fake_file_uri))
|
||||
d.addCallback(lambda res: self.failUnlessEqual(res, None))
|
||||
d.addCallback(lambda res: n.list())
|
||||
def _check_list(children):
|
||||
self.failUnless("child" in children)
|
||||
d.addCallback(_check_list)
|
||||
|
||||
return d
|
||||
|
||||
d.addCallback(_check)
|
||||
|
||||
return d
|
||||
|
@ -2,7 +2,8 @@
|
||||
from twisted.trial import unittest
|
||||
from allmydata import uri
|
||||
from allmydata.util import hashutil
|
||||
from allmydata.interfaces import IURI, IFileURI, IDirnodeURI, DirnodeURI
|
||||
from allmydata.interfaces import IURI, IFileURI, IDirnodeURI, DirnodeURI, \
|
||||
IMutableFileURI, IVerifierURI
|
||||
from foolscap.schema import Violation
|
||||
|
||||
class Literal(unittest.TestCase):
|
||||
@ -184,3 +185,108 @@ class Constraint(unittest.TestCase):
|
||||
fileURI = 'URI:CHK:f3mf6az85wpcai8ma4qayfmxuc:nnw518w5hu3t5oohwtp7ah9n81z9rfg6c1ywk33ia3m64o67nsgo:3:10:345834'
|
||||
self.failUnlessRaises(Violation, DirnodeURI.checkObject, fileURI, False)
|
||||
|
||||
|
||||
class Mutable(unittest.TestCase):
|
||||
def test_pack(self):
|
||||
writekey = "\x01" * 16
|
||||
fingerprint = "\x02" * 32
|
||||
|
||||
u = uri.WriteableSSKFileURI(writekey, fingerprint)
|
||||
self.failUnlessEqual(u.writekey, writekey)
|
||||
self.failUnlessEqual(u.fingerprint, fingerprint)
|
||||
self.failIf(u.is_readonly())
|
||||
self.failUnless(u.is_mutable())
|
||||
self.failUnless(IURI.providedBy(u))
|
||||
self.failUnless(IMutableFileURI.providedBy(u))
|
||||
self.failIf(IDirnodeURI.providedBy(u))
|
||||
|
||||
u2 = uri.from_string(u.to_string())
|
||||
self.failUnlessEqual(u2.writekey, writekey)
|
||||
self.failUnlessEqual(u2.fingerprint, fingerprint)
|
||||
self.failIf(u2.is_readonly())
|
||||
self.failUnless(u2.is_mutable())
|
||||
self.failUnless(IURI.providedBy(u2))
|
||||
self.failUnless(IMutableFileURI.providedBy(u2))
|
||||
self.failIf(IDirnodeURI.providedBy(u2))
|
||||
|
||||
u3 = u2.get_readonly()
|
||||
readkey = hashutil.ssk_readkey_hash(writekey)
|
||||
self.failUnlessEqual(u3.fingerprint, fingerprint)
|
||||
self.failUnlessEqual(u3.readkey, readkey)
|
||||
self.failUnless(u3.is_readonly())
|
||||
self.failUnless(u3.is_mutable())
|
||||
self.failUnless(IURI.providedBy(u3))
|
||||
self.failUnless(IMutableFileURI.providedBy(u3))
|
||||
self.failIf(IDirnodeURI.providedBy(u3))
|
||||
|
||||
u4 = uri.ReadonlySSKFileURI(readkey, fingerprint)
|
||||
self.failUnlessEqual(u4.fingerprint, fingerprint)
|
||||
self.failUnlessEqual(u4.readkey, readkey)
|
||||
self.failUnless(u4.is_readonly())
|
||||
self.failUnless(u4.is_mutable())
|
||||
self.failUnless(IURI.providedBy(u4))
|
||||
self.failUnless(IMutableFileURI.providedBy(u4))
|
||||
self.failIf(IDirnodeURI.providedBy(u4))
|
||||
|
||||
u5 = u4.get_verifier()
|
||||
self.failUnless(IVerifierURI.providedBy(u5))
|
||||
self.failUnlessEqual(u5.storage_index, u.storage_index)
|
||||
u6 = IVerifierURI(u5.to_string())
|
||||
self.failUnless(IVerifierURI.providedBy(u6))
|
||||
self.failUnlessEqual(u6.storage_index, u.storage_index)
|
||||
u7 = u.get_verifier()
|
||||
self.failUnless(IVerifierURI.providedBy(u7))
|
||||
self.failUnlessEqual(u7.storage_index, u.storage_index)
|
||||
|
||||
|
||||
class NewDirnode(unittest.TestCase):
|
||||
def test_pack(self):
|
||||
writekey = "\x01" * 16
|
||||
fingerprint = "\x02" * 16
|
||||
|
||||
n = uri.WriteableSSKFileURI(writekey, fingerprint)
|
||||
u1 = uri.NewDirectoryURI(n)
|
||||
self.failIf(u1.is_readonly())
|
||||
self.failUnless(u1.is_mutable())
|
||||
self.failUnless(IURI.providedBy(u1))
|
||||
self.failIf(IFileURI.providedBy(u1))
|
||||
self.failUnless(IDirnodeURI.providedBy(u1))
|
||||
|
||||
u2 = uri.from_string(u1.to_string())
|
||||
self.failUnlessEqual(u1.to_string(), u2.to_string())
|
||||
self.failIf(u2.is_readonly())
|
||||
self.failUnless(u2.is_mutable())
|
||||
self.failUnless(IURI.providedBy(u2))
|
||||
self.failIf(IFileURI.providedBy(u2))
|
||||
self.failUnless(IDirnodeURI.providedBy(u2))
|
||||
|
||||
u3 = u2.get_readonly()
|
||||
self.failUnless(u3.is_readonly())
|
||||
self.failUnless(u3.is_mutable())
|
||||
self.failUnless(IURI.providedBy(u3))
|
||||
self.failIf(IFileURI.providedBy(u3))
|
||||
self.failUnless(IDirnodeURI.providedBy(u3))
|
||||
u3n = u3._filenode_uri
|
||||
self.failUnless(u3n.is_readonly())
|
||||
self.failUnless(u3n.is_mutable())
|
||||
|
||||
u4 = uri.ReadonlyNewDirectoryURI(u2._filenode_uri.get_readonly())
|
||||
self.failUnlessEqual(u4.to_string(), u3.to_string())
|
||||
self.failUnless(u4.is_readonly())
|
||||
self.failUnless(u4.is_mutable())
|
||||
self.failUnless(IURI.providedBy(u4))
|
||||
self.failIf(IFileURI.providedBy(u4))
|
||||
self.failUnless(IDirnodeURI.providedBy(u4))
|
||||
|
||||
verifiers = [u1.get_verifier(), u2.get_verifier(),
|
||||
u3.get_verifier(), u4.get_verifier(),
|
||||
IVerifierURI(u1.get_verifier().to_string()),
|
||||
uri.NewDirectoryURIVerifier(n.get_verifier()),
|
||||
uri.NewDirectoryURIVerifier(n.get_verifier().to_string()),
|
||||
]
|
||||
for v in verifiers:
|
||||
self.failUnless(IVerifierURI.providedBy(v))
|
||||
self.failUnlessEqual(v._filenode_uri,
|
||||
u1.get_verifier()._filenode_uri)
|
||||
|
||||
|
||||
|
@ -3,7 +3,8 @@ import re
|
||||
from zope.interface import implements
|
||||
from twisted.python.components import registerAdapter
|
||||
from allmydata.util import idlib, hashutil
|
||||
from allmydata.interfaces import IURI, IDirnodeURI, IFileURI, IVerifierURI
|
||||
from allmydata.interfaces import IURI, IDirnodeURI, IFileURI, IVerifierURI, \
|
||||
IMutableFileURI
|
||||
|
||||
# the URI shall be an ascii representation of the file. It shall contain
|
||||
# enough information to retrieve and validate the contents. It shall be
|
||||
@ -176,6 +177,186 @@ class LiteralFileURI(_BaseURI):
|
||||
def get_size(self):
|
||||
return len(self.data)
|
||||
|
||||
class WriteableSSKFileURI(_BaseURI):
|
||||
implements(IURI, IMutableFileURI)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
if not args and not kwargs:
|
||||
return
|
||||
self.populate(*args, **kwargs)
|
||||
|
||||
def populate(self, writekey, fingerprint):
|
||||
self.writekey = writekey
|
||||
self.readkey = hashutil.ssk_readkey_hash(writekey)
|
||||
self.storage_index = hashutil.ssk_storage_index_hash(self.readkey)
|
||||
self.fingerprint = fingerprint
|
||||
|
||||
def init_from_string(self, uri):
|
||||
assert uri.startswith("URI:SSK:"), uri
|
||||
(header_uri, header_ssk, writekey_s, fingerprint_s) = uri.split(":")
|
||||
self.populate(idlib.a2b(writekey_s), idlib.a2b(fingerprint_s))
|
||||
return self
|
||||
|
||||
def to_string(self):
|
||||
assert isinstance(self.writekey, str)
|
||||
assert isinstance(self.fingerprint, str)
|
||||
return "URI:SSK:%s:%s" % (idlib.b2a(self.writekey),
|
||||
idlib.b2a(self.fingerprint))
|
||||
|
||||
def is_readonly(self):
|
||||
return False
|
||||
def is_mutable(self):
|
||||
return True
|
||||
def get_readonly(self):
|
||||
return ReadonlySSKFileURI(self.readkey, self.fingerprint)
|
||||
def get_verifier(self):
|
||||
return SSKVerifierURI(self.storage_index, self.fingerprint)
|
||||
|
||||
class ReadonlySSKFileURI(_BaseURI):
|
||||
implements(IURI, IMutableFileURI)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
if not args and not kwargs:
|
||||
return
|
||||
self.populate(*args, **kwargs)
|
||||
|
||||
def populate(self, readkey, fingerprint):
|
||||
self.readkey = readkey
|
||||
self.storage_index = hashutil.ssk_storage_index_hash(self.readkey)
|
||||
self.fingerprint = fingerprint
|
||||
|
||||
def init_from_string(self, uri):
|
||||
assert uri.startswith("URI:SSK-RO:"), uri
|
||||
(header_uri, header_ssk, readkey_s, fingerprint_s) = uri.split(":")
|
||||
self.populate(idlib.a2b(readkey_s), idlib.a2b(fingerprint_s))
|
||||
return self
|
||||
|
||||
def to_string(self):
|
||||
assert isinstance(self.readkey, str)
|
||||
assert isinstance(self.fingerprint, str)
|
||||
return "URI:SSK-RO:%s:%s" % (idlib.b2a(self.readkey),
|
||||
idlib.b2a(self.fingerprint))
|
||||
|
||||
def is_readonly(self):
|
||||
return True
|
||||
def is_mutable(self):
|
||||
return True
|
||||
def get_readonly(self):
|
||||
return self
|
||||
def get_verifier(self):
|
||||
return SSKVerifierURI(self.storage_index, self.fingerprint)
|
||||
|
||||
class SSKVerifierURI(_BaseURI):
|
||||
implements(IVerifierURI)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
if not args and not kwargs:
|
||||
return
|
||||
self.populate(*args, **kwargs)
|
||||
|
||||
def populate(self, storage_index, fingerprint):
|
||||
self.storage_index = storage_index
|
||||
self.fingerprint = fingerprint
|
||||
|
||||
def init_from_string(self, uri):
|
||||
assert uri.startswith("URI:SSK-Verifier:"), uri
|
||||
(header_uri, header_ssk,
|
||||
storage_index_s, fingerprint_s) = uri.split(":")
|
||||
self.populate(idlib.a2b(storage_index_s), idlib.a2b(fingerprint_s))
|
||||
return self
|
||||
|
||||
def to_string(self):
|
||||
assert isinstance(self.storage_index, str)
|
||||
assert isinstance(self.fingerprint, str)
|
||||
return "URI:SSK-Verifier:%s:%s" % (idlib.b2a(self.storage_index),
|
||||
idlib.b2a(self.fingerprint))
|
||||
|
||||
class NewDirectoryURI(_BaseURI):
|
||||
implements(IURI, IDirnodeURI)
|
||||
|
||||
def __init__(self, filenode_uri=None):
|
||||
if filenode_uri:
|
||||
assert not filenode_uri.is_readonly()
|
||||
self._filenode_uri = filenode_uri
|
||||
|
||||
def init_from_string(self, uri):
|
||||
assert uri.startswith("URI:DIR2:")
|
||||
(header_uri, header_dir2, bits) = uri.split(":", 2)
|
||||
fn = WriteableSSKFileURI()
|
||||
fn.init_from_string("URI:SSK:" + bits)
|
||||
self._filenode_uri = fn
|
||||
return self
|
||||
|
||||
def to_string(self):
|
||||
assert isinstance(self._filenode_uri, WriteableSSKFileURI)
|
||||
fn_u = self._filenode_uri.to_string()
|
||||
(header_uri, header_ssk, bits) = fn_u.split(":", 2)
|
||||
return "URI:DIR2:" + bits
|
||||
|
||||
def is_readonly(self):
|
||||
return False
|
||||
def is_mutable(self):
|
||||
return True
|
||||
def get_readonly(self):
|
||||
return ReadonlyNewDirectoryURI(self._filenode_uri.get_readonly())
|
||||
def get_verifier(self):
|
||||
return NewDirectoryURIVerifier(self._filenode_uri.get_verifier())
|
||||
|
||||
class ReadonlyNewDirectoryURI(_BaseURI):
|
||||
implements(IURI, IDirnodeURI)
|
||||
|
||||
def __init__(self, filenode_uri=None):
|
||||
if filenode_uri:
|
||||
assert filenode_uri.is_readonly()
|
||||
self._filenode_uri = filenode_uri
|
||||
|
||||
def init_from_string(self, uri):
|
||||
assert uri.startswith("URI:DIR2-RO:")
|
||||
(header_uri, header_dir2, bits) = uri.split(":", 2)
|
||||
fn = ReadonlySSKFileURI()
|
||||
fn.init_from_string("URI:SSK-RO:" + bits)
|
||||
self._filenode_uri = fn
|
||||
return self
|
||||
|
||||
def to_string(self):
|
||||
assert isinstance(self._filenode_uri, ReadonlySSKFileURI)
|
||||
fn_u = self._filenode_uri.to_string()
|
||||
(header_uri, header_ssk, bits) = fn_u.split(":", 2)
|
||||
return "URI:DIR2-RO:" + bits
|
||||
|
||||
def is_readonly(self):
|
||||
return True
|
||||
def is_mutable(self):
|
||||
return True
|
||||
def get_readonly(self):
|
||||
return self
|
||||
def get_verifier(self):
|
||||
return NewDirectoryURIVerifier(self._filenode_uri.get_verifier())
|
||||
|
||||
class NewDirectoryURIVerifier(_BaseURI):
|
||||
implements(IVerifierURI)
|
||||
|
||||
def __init__(self, filenode_uri=None):
|
||||
if filenode_uri:
|
||||
filenode_uri = IVerifierURI(filenode_uri)
|
||||
self._filenode_uri = filenode_uri
|
||||
|
||||
def init_from_string(self, uri):
|
||||
assert uri.startswith("URI:DIR2-Verifier:")
|
||||
(header_uri, header_dir2, bits) = uri.split(":", 2)
|
||||
fn = SSKVerifierURI()
|
||||
fn.init_from_string("URI:SSK-Verifier:" + bits)
|
||||
self._filenode_uri = fn
|
||||
return self
|
||||
|
||||
def to_string(self):
|
||||
assert isinstance(self._filenode_uri, SSKVerifierURI)
|
||||
fn_u = self._filenode_uri.to_string()
|
||||
(header_uri, header_ssk, bits) = fn_u.split(":", 2)
|
||||
return "URI:DIR2-Verifier:" + bits
|
||||
|
||||
|
||||
|
||||
class DirnodeURI(_BaseURI):
|
||||
implements(IURI, IDirnodeURI)
|
||||
|
||||
@ -300,8 +481,20 @@ def from_string(s):
|
||||
return ReadOnlyDirnodeURI().init_from_string(s)
|
||||
elif s.startswith("URI:DIR-Verifier:"):
|
||||
return DirnodeVerifierURI().init_from_string(s)
|
||||
elif s.startswith("URI:SSK:"):
|
||||
return WriteableSSKFileURI().init_from_string(s)
|
||||
elif s.startswith("URI:SSK-RO:"):
|
||||
return ReadonlySSKFileURI().init_from_string(s)
|
||||
elif s.startswith("URI:SSK-Verifier:"):
|
||||
return SSKVerifierURI().init_from_string(s)
|
||||
elif s.startswith("URI:DIR2:"):
|
||||
return NewDirectoryURI().init_from_string(s)
|
||||
elif s.startswith("URI:DIR2-RO:"):
|
||||
return ReadonlyNewDirectoryURI().init_from_string(s)
|
||||
elif s.startswith("URI:DIR2-Verifier:"):
|
||||
return NewDirectoryURIVerifier().init_from_string(s)
|
||||
else:
|
||||
raise TypeError("unknown URI type: %s.." % s[:10])
|
||||
raise TypeError("unknown URI type: %s.." % s[:12])
|
||||
|
||||
registerAdapter(from_string, str, IURI)
|
||||
|
||||
@ -319,6 +512,12 @@ def from_string_filenode(s):
|
||||
|
||||
registerAdapter(from_string_filenode, str, IFileURI)
|
||||
|
||||
def from_string_mutable_filenode(s):
|
||||
u = from_string(s)
|
||||
assert IMutableFileURI.providedBy(u)
|
||||
return u
|
||||
registerAdapter(from_string_mutable_filenode, str, IMutableFileURI)
|
||||
|
||||
def from_string_verifier(s):
|
||||
u = from_string(s)
|
||||
assert IVerifierURI.providedBy(u)
|
||||
|
@ -115,3 +115,15 @@ def hmac(tag, data):
|
||||
h1 = SHA256.new(ikey + data).digest()
|
||||
h2 = SHA256.new(okey + h1).digest()
|
||||
return h2
|
||||
|
||||
def mutable_rwcap_key_hash(iv, writekey):
|
||||
return tagged_pair_hash("allmydata_mutable_rwcap_key_v1", iv, writekey)
|
||||
def ssk_writekey_hash(privkey):
|
||||
return tagged_hash("allmydata_mutable_writekey_v1", privkey)
|
||||
def ssk_pubkey_fingerprint_hash(pubkey):
|
||||
return tagged_hash("allmydata_mutable_pubkey_v1", pubkey)
|
||||
|
||||
def ssk_readkey_hash(writekey):
|
||||
return tagged_hash("allmydata_mutable_readkey_v1", writekey)
|
||||
def ssk_storage_index_hash(readkey):
|
||||
return tagged_hash("allmydata_mutable_storage_index_v1", readkey)
|
||||
|
Loading…
x
Reference in New Issue
Block a user