From: Brian Warner Date: Mon, 25 Jun 2007 20:23:51 +0000 (-0700) Subject: vdrive: switch to URI:DIR and URI:DIR-RO, providing transitive readonlyness X-Git-Tag: allmydata-tahoe-0.4.0~32 X-Git-Url: https://git.rkrishnan.org/pf/content/en/(%5B%5E?a=commitdiff_plain;h=fb02488a8e8a613cecbf579d2c46f817de3859d5;p=tahoe-lafs%2Ftahoe-lafs.git vdrive: switch to URI:DIR and URI:DIR-RO, providing transitive readonlyness --- diff --git a/src/allmydata/client.py b/src/allmydata/client.py index bdd9189e..70a47add 100644 --- a/src/allmydata/client.py +++ b/src/allmydata/client.py @@ -2,8 +2,8 @@ import os, sha, stat, time from foolscap import Referenceable, SturdyRef from zope.interface import implements -from allmydata.interfaces import RIClient -from allmydata import node +from allmydata.interfaces import RIClient, IDirectoryNode +from allmydata import node, vdrive, uri from twisted.internet import defer, reactor from twisted.application.internet import TimerService @@ -13,7 +13,6 @@ from allmydata.Crypto.Util.number import bytes_to_long from allmydata.storageserver import StorageServer from allmydata.upload import Uploader from allmydata.download import Downloader -from allmydata.vdrive import DirectoryNode from allmydata.webish import WebishServer from allmydata.control import ControlServer from allmydata.introducer import IntroducerClient @@ -28,7 +27,7 @@ class Client(node.Node, Referenceable): GLOBAL_VDRIVE_FURL_FILE = "vdrive.furl" MY_FURL_FILE = "myself.furl" SUICIDE_PREVENTION_HOTLINE_FILE = "suicide_prevention_hotline" - MY_VDRIVE_FURL_FILE = "my_vdrive.furl" + MY_VDRIVE_URI_FILE = "my_vdrive.uri" # we're pretty narrow-minded right now OLDEST_SUPPORTED_VERSION = allmydata.__version__ @@ -115,16 +114,20 @@ class Client(node.Node, Referenceable): def _got_vdrive(self, vdrive_server): # vdrive_server implements RIVirtualDriveServer self.log("connected to vdrive server") - d = vdrive_server.callRemote("get_public_root_furl") - d.addCallback(self._got_vdrive_root_furl, vdrive_server) - d.addCallback(self._create_my_vdrive) + d = vdrive_server.callRemote("get_public_root_uri") + d.addCallback(self._got_vdrive_uri) + d.addCallback(self._got_vdrive_rootnode) + d.addCallback(self._create_my_vdrive, vdrive_server) d.addCallback(self._got_my_vdrive) - def _got_vdrive_root_furl(self, vdrive_root_furl, vdrive_server): - root = DirectoryNode(vdrive_root_furl, self) + def _got_vdrive_uri(self, root_uri): + furl, wk = uri.unpack_dirnode_uri(root_uri) + self._vdrive_furl = furl + return vdrive.create_directory_node(self, root_uri) + + def _got_vdrive_rootnode(self, rootnode): self.log("got vdrive root") - self._vdrive_server = vdrive_server - self._vdrive_root = root + self._vdrive_root = rootnode self._connected_to_vdrive = True #vdrive = self.getServiceNamed("vdrive") @@ -133,34 +136,34 @@ class Client(node.Node, Referenceable): if "webish" in self.namedServices: webish = self.getServiceNamed("webish") - webish.set_vdrive_root(root) + webish.set_vdrive_rootnode(rootnode) - def _create_my_vdrive(self, ignored=None): - MY_VDRIVE_FURL_FILE = os.path.join(self.basedir, - self.MY_VDRIVE_FURL_FILE) + def _create_my_vdrive(self, ignored, vdrive_server): + MY_VDRIVE_URI_FILE = os.path.join(self.basedir, + self.MY_VDRIVE_URI_FILE) try: - f = open(MY_VDRIVE_FURL_FILE, "r") - my_vdrive_furl = f.read().strip() + f = open(MY_VDRIVE_URI_FILE, "r") + my_vdrive_uri = f.read().strip() f.close() - return defer.succeed(DirectoryNode(my_vdrive_furl, self)) + return vdrive.create_directory_node(self, my_vdrive_uri) except EnvironmentError: - d = self._vdrive_server.callRemote("create_directory") + assert self._vdrive_furl + d = vdrive.create_directory(self, self._vdrive_furl) def _got_directory(dirnode): - f = open(MY_VDRIVE_FURL_FILE, "w") - f.write(dirnode.furl + "\n") + f = open(MY_VDRIVE_URI_FILE, "w") + f.write(dirnode.get_uri() + "\n") f.close() - dirnode._set_client(self) return dirnode d.addCallback(_got_directory) return d def _got_my_vdrive(self, my_vdrive): - assert isinstance(my_vdrive, DirectoryNode), my_vdrive + IDirectoryNode(my_vdrive) self._my_vdrive = my_vdrive if "webish" in self.namedServices: webish = self.getServiceNamed("webish") - webish.set_my_vdrive_root(my_vdrive) + webish.set_my_vdrive_rootnode(my_vdrive) def remote_get_versions(self): diff --git a/src/allmydata/filetable.py b/src/allmydata/filetable.py index c948ded9..99b8777f 100644 --- a/src/allmydata/filetable.py +++ b/src/allmydata/filetable.py @@ -1,165 +1,110 @@ import os from zope.interface import implements -from foolscap import Referenceable -from allmydata.interfaces import RIVirtualDriveServer, RIMutableDirectoryNode -from allmydata.vdrive import FileNode, DirectoryNode -from allmydata.util import bencode, idlib from twisted.application import service -from twisted.python import log - -class BadNameError(Exception): - """Bad filename component""" +from foolscap import Referenceable +from allmydata.interfaces import RIVirtualDriveServer +from allmydata.util import bencode, idlib, hashutil, fileutil +from allmydata import uri -class BadFileError(Exception): +class BadWriteEnablerError(Exception): pass - -class BadDirectoryError(Exception): +class ChildAlreadyPresentError(Exception): pass -class MutableDirectoryNode(Referenceable): - """I represent a single directory. - - I am associated with a file on disk, using a randomly-generated (and - hopefully unique) name. This file contains a serialized dictionary which - maps child names to 'child specifications'. These specifications are - tuples, either of ('file', URI), or ('subdir', FURL). - """ - - implements(RIMutableDirectoryNode) - - def __init__(self, basedir, name=None): - self._basedir = basedir - if name: - self._name = name - # for well-known nodes, make sure they exist - try: - ignored = self._read_from_file() - except EnvironmentError: - self._write_to_file({}) - else: - self._name = self.create_filename() - self._write_to_file({}) # start out empty - - def _read_from_file(self): - data = open(os.path.join(self._basedir, self._name), "rb").read() - children = bencode.bdecode(data) - child_nodes = {} - for k,v in children.iteritems(): - if v[0] == "file": - child_nodes[k] = FileNode(v[1]) - elif v[0] == "subdir": - child_nodes[k] = DirectoryNode(v[1]) - else: - raise RuntimeError("unknown child spec '%s'" % (v[0],)) - return child_nodes - - def _write_to_file(self, children): - child_nodes = {} - for k,v in children.iteritems(): - if isinstance(v, FileNode): - child_nodes[k] = ("file", v.uri) - elif isinstance(v, DirectoryNode): - child_nodes[k] = ("subdir", v.furl) - else: - raise RuntimeError("unknown child[%s] node '%s'" % (k,v)) - data = bencode.bencode(child_nodes) - f = open(os.path.join(self._basedir, self._name), "wb") - f.write(data) - f.close() - - - def create_filename(self): - return idlib.b2a(os.urandom(8)) - - def validate_name(self, name): - if name == "." or name == ".." or "/" in name: - raise BadNameError("'%s' is not cool" % name) - - # these are the public methods, available to anyone who holds a reference - - def list(self): - log.msg("Dir(%s).list()" % self._name) - children = self._read_from_file() - results = list(children.items()) - return sorted(results) - remote_list = list - - def get(self, name): - log.msg("Dir(%s).get(%s)" % (self._name, name)) - self.validate_name(name) - children = self._read_from_file() - if name not in children: - raise BadFileError("no such child") - return children[name] - remote_get = get - - def add(self, name, child): - self.validate_name(name) - children = self._read_from_file() - if name in children: - raise BadNameError("the child already existed") - children[name] = child - self._write_to_file(children) - return child - remote_add = add - - def remove(self, name): - self.validate_name(name) - children = self._read_from_file() - if name not in children: - raise BadFileError("cannot remove non-existent child") - child = children[name] - del children[name] - self._write_to_file(children) - return child - remote_remove = remove - - class NoPublicRootError(Exception): pass class VirtualDriveServer(service.MultiService, Referenceable): implements(RIVirtualDriveServer) name = "filetable" - VDRIVEDIR = "vdrive" - def __init__(self, basedir=".", offer_public_root=True): + def __init__(self, basedir, offer_public_root=True): service.MultiService.__init__(self) - vdrive_dir = os.path.join(basedir, self.VDRIVEDIR) - if not os.path.exists(vdrive_dir): - os.mkdir(vdrive_dir) - self._vdrive_dir = vdrive_dir + self._basedir = os.path.abspath(basedir) + fileutil.make_dirs(self._basedir) self._root = None if offer_public_root: - self._root = MutableDirectoryNode(vdrive_dir, "root") - - def startService(self): - service.MultiService.startService(self) - # _register_all_dirnodes exists to avoid hacking our Tub to - # automatically translate inbound your-reference names - # (Tub.getReferenceForName) into MutableDirectoryNode instances by - # looking in our basedir for them. Without that hack, we have to - # register all nodes at startup to make sure they'll be available to - # all callers. In addition, we must register any new ones that we - # create later on. - tub = self.parent.tub - self._root_furl = tub.registerReference(self._root, "root") - self._register_all_dirnodes(tub) - - def _register_all_dirnodes(self, tub): - for name in os.listdir(self._vdrive_dir): - node = MutableDirectoryNode(self._vdrive_dir, name) - ignored_furl = tub.registerReference(node, name) - - def get_public_root_furl(self): + rootfile = os.path.join(self._basedir, "root") + if not os.path.exists(rootfile): + write_key = hashutil.random_key() + (wk, we, rk, index) = \ + hashutil.generate_dirnode_keys_from_writekey(write_key) + self.create_directory(index, we) + f = open(rootfile, "wb") + f.write(wk) + f.close() + self._root = wk + else: + f = open(rootfile, "rb") + self._root = f.read() + + def set_furl(self, myfurl): + self._myfurl = myfurl + + def get_public_root_uri(self): if self._root: - return self._root_furl + return uri.pack_dirnode_uri(self._myfurl, self._root) raise NoPublicRootError - remote_get_public_root_furl = get_public_root_furl + remote_get_public_root_uri = get_public_root_uri - def create_directory(self): - node = MutableDirectoryNode(self._vdrive_dir) - furl = self.parent.tub.registerReference(node, node._name) - return DirectoryNode(furl) + def create_directory(self, index, write_enabler): + data = [write_enabler, []] + self._write_to_file(index, data) + return index remote_create_directory = create_directory + + # the file on disk consists of the write_enabler token and a list of + # (H(name), E(name), E(write), E(read)) tuples. + + def _read_from_file(self, index): + name = idlib.b2a(index) + data = open(os.path.join(self._basedir, name), "rb").read() + return bencode.bdecode(data) + + def _write_to_file(self, index, data): + name = idlib.b2a(index) + f = open(os.path.join(self._basedir, name), "wb") + f.write(bencode.bencode(data)) + f.close() + + + def get(self, index, key): + data = self._read_from_file(index) + for (H_key, E_key, E_write, E_read) in data[1]: + if H_key == key: + return (E_write, E_read) + raise IndexError("unable to find key %s" % idlib.b2a(key)) + remote_get = get + + def list(self, index): + data = self._read_from_file(index) + response = [ (E_key, E_write, E_read) + for (H_key, E_key, E_write, E_read) in data[1] ] + return response + remote_list = list + + def delete(self, index, write_enabler, key): + data = self._read_from_file(index) + if data[0] != write_enabler: + raise BadWriteEnablerError + for i,(H_key, E_key, E_write, E_read) in enumerate(data[1]): + if H_key == key: + del data[1][i] + self._write_to_file(index, data) + return + raise IndexError("unable to find key %s" % idlib.b2a(key)) + remote_delete = delete + + def set(self, index, write_enabler, key, name, write, read): + data = self._read_from_file(index) + if data[0] != write_enabler: + raise BadWriteEnablerError + # first, see if the key is already present + for i,(H_key, E_key, E_write, E_read) in enumerate(data[1]): + if H_key == key: + raise ChildAlreadyPresentError + # now just append the data + data[1].append( (key, name, write, read) ) + self._write_to_file(index, data) + remote_set = set diff --git a/src/allmydata/interfaces.py b/src/allmydata/interfaces.py index b7a88e82..028ef26d 100644 --- a/src/allmydata/interfaces.py +++ b/src/allmydata/interfaces.py @@ -126,33 +126,79 @@ RIMutableDirectoryNode_ = Any() # TODO: how can we avoid this? FileNode_ = Any() # TODO: foolscap needs constraints on copyables DirectoryNode_ = Any() # TODO: same AnyNode_ = ChoiceOf(FileNode_, DirectoryNode_) +EncryptedThing = str -class RIMutableDirectoryNode(RemoteInterface): - def list(): - return ListOf( TupleOf(str, # name, relative to directory - AnyNode_, - ), - maxLength=100, - ) +class RIVirtualDriveServer(RemoteInterface): + def get_public_root_uri(): + """Obtain the URI for this server's global publically-writable root + directory. This returns a read-write directory URI. + + If this vdrive server does not offer a public root, this will + raise an exception.""" + return URI - def get(name=str): - return AnyNode_ + def create_directory(index=Hash, write_enabler=Hash): + """Create a new (empty) directory, unattached to anything else. - def add(name=str, what=AnyNode_): - return AnyNode_ + This returns the same index that was passed in. + """ + return Hash - def remove(name=str): - return AnyNode_ + def get(index=Hash, key=Hash): + """Retrieve a named child of the given directory. 'index' specifies + which directory is being accessed, and is generally the hash of the + read key. 'key' is the hash of the read key and the child name. + This operation returns a pair of encrypted strings. The first string + is meant to be decrypted by the Write Key and provides read-write + access to the child. If this directory holds read-only access to the + child, this first string will be an empty string. The second string + is meant to be decrypted by the Read Key and provides read-only + access to the child. -class RIVirtualDriveServer(RemoteInterface): - def get_public_root_furl(): - """If this vdrive server does not offer a public root, this will - raise an exception.""" - return FURL + When the child is a read-write directory, the encrypted URI:DIR-RO + will be in the read slot, and the encrypted URI:DIR will be in the + write slot. When the child is a read-only directory, the encrypted + URI:DIR-RO will be in the read slot and the write slot will be empty. + When the child is a CHK file, the encrypted URI:CHK will be in the + read slot, and the write slot will be empty. + + This might raise IndexError if there is no child by the desired name. + """ + return (EncryptedThing, EncryptedThing) + + def list(index=Hash): + """List the contents of a directory. + + This returns a list of (NAME, WRITE, READ) tuples. Each value is an + encrypted string (although the WRITE value may sometimes be an empty + string). + + NAME: the child name, encrypted with the Read Key + WRITE: the child write URI, encrypted with the Write Key, or an + empty string if this child is read-only + READ: the child read URI, encrypted with the Read Key + """ + return ListOf((EncryptedThing, EncryptedThing, EncryptedThing), + maxLength=100, + ) + + def set(index=Hash, write_enabler=Hash, key=Hash, + name=EncryptedThing, write=EncryptedThing, read=EncryptedThing): + """Set a child object. + + This will raise IndexError if a child with the given name already + exists. + """ + pass + + def delete(index=Hash, write_enabler=Hash, key=Hash): + """Delete a specific child. + + This uses the hashed key to locate a specific child, and deletes it. + """ + pass - def create_directory(): - return DirectoryNode_ class IFileNode(Interface): def download(target): @@ -161,23 +207,66 @@ class IFileNode(Interface): pass class IDirectoryNode(Interface): + def is_mutable(): + """Return True if this directory is mutable, False if it is read-only. + """ + def get_uri(): + """Return the directory URI that can be used by others to get access + to this directory node. If this node is read-only, the URI will only + offer read-only access. If this node is read-write, the URI will + offer read-write acess. + + If you have read-write access to a directory and wish to share merely + read-only access with others, use get_immutable_uri(). + + The dirnode ('1') URI returned by this method can be used in set() on + a different directory ('2') to 'mount' a reference to this directory + ('1') under the other ('2'). This URI is just a string, so it can be + passed around through email or other out-of-band protocol. + """ + + def get_immutable_uri(): + """Return the directory URI that can be used by others to get + read-only access to this directory node. The result is a read-only + URI, regardless of whether this dirnode is read-only or read-write. + + If you have merely read-only access to this dirnode, + get_immutable_uri() will return the same thing as get_uri(). + """ + def list(): - """I return a Deferred that fires with a""" - pass + """I return a Deferred that fires with a dictionary mapping child + name to an IFileNode or IDirectoryNode.""" def get(name): - """I return a Deferred that fires with a specific named child.""" - pass + """I return a Deferred that fires with a specific named child node, + either an IFileNode or an IDirectoryNode.""" + + def set_uri(name, child_uri): + """I add a child (by URI) at the specific name. I return a Deferred + that fires when the operation finishes. - def add(name, child): + 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.""" + + def set_node(name, child): """I add a child at the specific name. I return a Deferred that fires - when the operation finishes.""" + when the operation finishes. This Deferred will fire with the child + node that was just added. + + If this directory node is read-only, the Deferred will errback with a + NotMutableError.""" def add_file(name, uploadable): """I upload a file (using the given IUploadable), then attach the - resulting FileNode to the directory at the given name.""" + 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 remove(name): + def delete(name): """I remove the child at the specific name. I return a Deferred that fires when the operation finishes.""" @@ -185,18 +274,6 @@ class IDirectoryNode(Interface): """I create and attach an empty directory at the given name. I return a Deferred that fires when the operation finishes.""" - def attach_shared_directory(name, furl): - """I attach a directory that was shared (possibly by someone else) - with IDirectoryNode.get_furl to this parent at the given name. I - return a Deferred that fires when the operation finishes.""" - - def get_shared_directory_furl(): - """I return a FURL that can be used to attach this directory to - somewhere else. The FURL is just a string, so it can be passed - through email or other out-of-band protocol. Use it by passing it in - to attach_shared_directory(). I return a Deferred that fires when the - operation finishes.""" - def move_child_to(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 diff --git a/src/allmydata/introducer_and_vdrive.py b/src/allmydata/introducer_and_vdrive.py index 5209facd..2eeb1db2 100644 --- a/src/allmydata/introducer_and_vdrive.py +++ b/src/allmydata/introducer_and_vdrive.py @@ -8,6 +8,7 @@ from allmydata.introducer import Introducer class IntroducerAndVdrive(node.Node): PORTNUMFILE = "introducer.port" NODETYPE = "introducer" + VDRIVEDIR = "vdrive" def __init__(self, basedir="."): node.Node.__init__(self, basedir) @@ -21,8 +22,11 @@ class IntroducerAndVdrive(node.Node): f.write(self.urls["introducer"] + "\n") f.close() - vds = self.add_service(VirtualDriveServer(self.basedir)) - self.urls["vdrive"] = self.tub.registerReference(vds, "vdrive") + vdrive_dir = os.path.join(self.basedir, self.VDRIVEDIR) + vds = self.add_service(VirtualDriveServer(vdrive_dir)) + vds_furl = self.tub.registerReference(vds, "vdrive") + vds.set_furl(vds_furl) + self.urls["vdrive"] = vds_furl self.log(" vdrive is at %s" % self.urls["vdrive"]) f = open(os.path.join(self.basedir, "vdrive.furl"), "w") f.write(self.urls["vdrive"] + "\n") diff --git a/src/allmydata/scripts/runner.py b/src/allmydata/scripts/runner.py index a52e0f60..9c25d8c3 100644 --- a/src/allmydata/scripts/runner.py +++ b/src/allmydata/scripts/runner.py @@ -129,6 +129,30 @@ class DumpOptions(usage.Options): if not self['filename']: raise usage.UsageError(" parameter is required") +class DumpRootDirnodeOptions(BasedirMixin, usage.Options): + optParameters = [ + ["basedir", "C", None, "the vdrive-server's base directory"], + ] + +class DumpDirnodeOptions(BasedirMixin, usage.Options): + optParameters = [ + ["uri", "u", None, "the URI of the dirnode to dump."], + ["basedir", "C", None, "which directory to create the introducer in"], + ] + optFlags = [ + ["verbose", "v", "be extra noisy (show encrypted data)"], + ] + def parseArgs(self, *args): + if len(args) == 1: + self['uri'] = args[-1] + args = args[:-1] + BasedirMixin.parseArgs(self, *args) + + def postOptions(self): + BasedirMixin.postOptions(self) + if not self['uri']: + raise usage.UsageError(" parameter is required") + client_tac = """ # -*- python -*- @@ -164,7 +188,9 @@ class Options(usage.Options): ["restart", None, RestartOptions, "Restart a node."], ["dump-uri-extension", None, DumpOptions, "Unpack and display the contents of a uri_extension file."], - ["dump-directory-node", None, DumpOptions, + ["dump-root-dirnode", None, DumpRootDirnodeOptions, + "Compute most of the URI for the vdrive server's root dirnode."], + ["dump-dirnode", None, DumpDirnodeOptions, "Unpack and display the contents of a vdrive DirectoryNode."], ] @@ -211,8 +237,10 @@ def runner(argv, run_by_human=True): rc = start(basedir, so) or rc elif command == "dump-uri-extension": rc = dump_uri_extension(so) - elif command == "dump-directory-node": - rc = dump_directory_node(so) + elif command == "dump-root-dirnode": + rc = dump_root_dirnode(so.basedirs[0], so) + elif command == "dump-dirnode": + rc = dump_directory_node(so.basedirs[0], so) return rc def run(): @@ -329,30 +357,71 @@ def dump_uri_extension(config): print return 0 -def dump_directory_node(config): - from allmydata import filetable, vdrive - filename = config['filename'] +def dump_root_dirnode(basedir, config): + from allmydata import uri - basedir, name = os.path.split(filename) - dirnode = filetable.MutableDirectoryNode(basedir, name) + root_dirnode_file = os.path.join(basedir, "vdrive", "root") + try: + f = open(root_dirnode_file, "rb") + key = f.read() + rooturi = uri.pack_dirnode_uri("fakeFURL", key) + print rooturi + return 0 + except EnvironmentError: + print "unable to read root dirnode file from %s" % root_dirnode_file + return 1 + +def dump_directory_node(basedir, config): + from allmydata import filetable, vdrive, uri + from allmydata.util import hashutil, idlib + dir_uri = config['uri'] + verbose = config['verbose'] + + furl, key = uri.unpack_dirnode_uri(dir_uri) + if uri.is_mutable_dirnode_uri(dir_uri): + wk, we, rk, index = hashutil.generate_dirnode_keys_from_writekey(key) + else: + wk, we, rk, index = hashutil.generate_dirnode_keys_from_readkey(key) + + filename = os.path.join(basedir, "vdrive", idlib.b2a(index)) print - print "DirectoryNode at %s" % name - print + print "dirnode uri: %s" % dir_uri + print "filename : %s" % filename + print "index : %s" % idlib.b2a(index) + if wk: + print "writekey : %s" % idlib.b2a(wk) + print "write_enabler: %s" % idlib.b2a(we) + else: + print "writekey : None" + print "write_enabler: None" + print "readkey : %s" % idlib.b2a(rk) - children = dirnode._read_from_file() - names = sorted(children.keys()) - for name in names: - v = children[name] - if isinstance(v, vdrive.FileNode): - value = "File (uri=%s...)" % v.uri[:40] - elif isinstance(v, vdrive.DirectoryNode): - lastslash = v.furl.rindex("/") - furlname = v.furl[lastslash+1:lastslash+1+15] - value = "Directory (furl=%s.../%s...)" % (v.furl[:15], furlname) - else: - value = "weird: %s" % (v,) - print "%20s: %s" % (name, value) print - return 0 + vds = filetable.VirtualDriveServer(os.path.join(basedir, "vdrive"), False) + data = vds._read_from_file(index) + if we: + if we != data[0]: + print "ERROR: write_enabler does not match" + + for (H_key, E_key, E_write, E_read) in data[1]: + if verbose: + print " H_key %s" % idlib.b2a(H_key) + print " E_key %s" % idlib.b2a(E_key) + print " E_write %s" % idlib.b2a(E_write) + print " E_read %s" % idlib.b2a(E_read) + key = vdrive.decrypt(rk, E_key) + print " key %s" % key + if hashutil.dir_name_hash(rk, key) != H_key: + print " ERROR: H_key does not match" + if wk and E_write: + if len(E_write) < 14: + print " ERROR: write data is short:", idlib.b2a(E_write) + write = vdrive.decrypt(wk, E_write) + print " write: %s" % write + read = vdrive.decrypt(rk, E_read) + print " read: %s" % read + print + + return 0 diff --git a/src/allmydata/test/test_filetable.py b/src/allmydata/test/test_filetable.py index 12a26ac5..4886941b 100644 --- a/src/allmydata/test/test_filetable.py +++ b/src/allmydata/test/test_filetable.py @@ -1,54 +1,86 @@ -import os from twisted.trial import unittest -from allmydata.filetable import (MutableDirectoryNode, - BadFileError, BadNameError) -from allmydata.vdrive import FileNode, DirectoryNode +from allmydata import filetable, uri +from allmydata.util import hashutil class FileTable(unittest.TestCase): - def test_files(self): - os.mkdir("filetable") - basedir = os.path.abspath("filetable") - root = MutableDirectoryNode(basedir, "root") - self.failUnlessEqual(root.list(), []) - root.add("one", FileNode("vid-one")) - root.add("two", FileNode("vid-two")) - self.failUnlessEqual(root.list(), [("one", FileNode("vid-one")), - ("two", FileNode("vid-two"))]) - root.remove("two") - self.failUnlessEqual(root.list(), [("one", FileNode("vid-one"))]) - self.failUnlessRaises(BadFileError, root.remove, "two") - self.failUnlessRaises(BadFileError, root.remove, "three") - - self.failUnlessEqual(root.get("one"), FileNode("vid-one")) - self.failUnlessRaises(BadFileError, root.get, "missing") - self.failUnlessRaises(BadNameError, root.get, "/etc/passwd") # evil - self.failUnlessRaises(BadNameError, root.get, "..") # sneaky - self.failUnlessRaises(BadNameError, root.get, ".") # dumb - - # now play with directories - subdir1 = root.add("subdir1", DirectoryNode("subdir1.furl")) - self.failUnless(isinstance(subdir1, DirectoryNode)) - subdir1a = root.get("subdir1") - self.failUnless(isinstance(subdir1a, DirectoryNode)) - self.failUnlessEqual(subdir1a, subdir1) - entries = root.list() - self.failUnlessEqual(len(entries), 2) - one_index = entries.index( ("one", FileNode("vid-one")) ) - subdir_index = 1 - one_index - self.failUnlessEqual(entries[subdir_index][0], "subdir1") - subdir2 = entries[subdir_index][1] - self.failUnless(isinstance(subdir2, DirectoryNode)) - - self.failUnlessEqual(len(root.list()), 2) - - self.failUnlessRaises(BadNameError, # replacing an existing child - root.add, - "subdir1", DirectoryNode("subdir1.furl")) - - root.remove("subdir1") - self.failUnlessEqual(root.list(), [("one", FileNode("vid-one"))]) + def test_vdrive_server(self): + basedir = "filetable/FileTable/test_vdrive_server" + vds = filetable.VirtualDriveServer(basedir) + vds.set_furl("myFURL") + root_uri = vds.get_public_root_uri() + self.failUnless(uri.is_dirnode_uri(root_uri)) + self.failUnless(uri.is_mutable_dirnode_uri(root_uri)) + furl, key = uri.unpack_dirnode_uri(root_uri) + self.failUnlessEqual(furl, "myFURL") + self.failUnlessEqual(len(key), hashutil.KEYLEN) + wk, we, rk, index = hashutil.generate_dirnode_keys_from_writekey(key) + empty_list = vds.list(index) + self.failUnlessEqual(empty_list, []) + vds.set(index, we, "key1", "name1", "write1", "read1") + vds.set(index, we, "key2", "name2", "", "read2") + + self.failUnlessRaises(filetable.ChildAlreadyPresentError, + vds.set, + index, we, "key2", "name2", "write2", "read2") + + self.failUnlessRaises(filetable.BadWriteEnablerError, + vds.set, + index, "not the write enabler", + "key2", "name2", "write2", "read2") + + self.failUnlessEqual(vds.get(index, "key1"), + ("write1", "read1")) + self.failUnlessEqual(vds.get(index, "key2"), + ("", "read2")) + self.failUnlessRaises(IndexError, + vds.get, index, "key3") + + self.failUnlessEqual(sorted(vds.list(index)), + [ ("name1", "write1", "read1"), + ("name2", "", "read2"), + ]) + + self.failUnlessRaises(filetable.BadWriteEnablerError, + vds.delete, + index, "not the write enabler", "name1") + self.failUnlessEqual(sorted(vds.list(index)), + [ ("name1", "write1", "read1"), + ("name2", "", "read2"), + ]) + self.failUnlessRaises(IndexError, + vds.delete, + index, we, "key3") + + vds.delete(index, we, "key1") + self.failUnlessEqual(sorted(vds.list(index)), + [ ("name2", "", "read2"), + ]) + self.failUnlessRaises(IndexError, + vds.get, index, "key1") + self.failUnlessEqual(vds.get(index, "key2"), + ("", "read2")) + + + vds2 = filetable.VirtualDriveServer(basedir) + vds2.set_furl("myFURL") + root_uri2 = vds.get_public_root_uri() + self.failUnless(uri.is_mutable_dirnode_uri(root_uri2)) + furl2, key2 = uri.unpack_dirnode_uri(root_uri2) + (wk2, we2, rk2, index2) = \ + hashutil.generate_dirnode_keys_from_writekey(key2) + self.failUnlessEqual(sorted(vds2.list(index2)), + [ ("name2", "", "read2"), + ]) + + def test_no_root(self): + basedir = "FileTable/test_no_root" + vds = filetable.VirtualDriveServer(basedir, offer_public_root=False) + vds.set_furl("myFURL") + + self.failUnlessRaises(filetable.NoPublicRootError, + vds.get_public_root_uri) diff --git a/src/allmydata/test/test_system.py b/src/allmydata/test/test_system.py index 84839abb..af1bda46 100644 --- a/src/allmydata/test/test_system.py +++ b/src/allmydata/test/test_system.py @@ -240,7 +240,7 @@ class SystemTest(testutil.SignalMixin, unittest.TestCase): d1.addCallback(lambda subdir1_node: subdir1_node.add_file("mydata567", ut)) def _stash_uri(filenode): - self.uri = filenode.uri + self.uri = filenode.get_uri() return filenode d1.addCallback(_stash_uri) return d1 diff --git a/src/allmydata/test/test_vdrive.py b/src/allmydata/test/test_vdrive.py index 536f1c0c..472bffcd 100644 --- a/src/allmydata/test/test_vdrive.py +++ b/src/allmydata/test/test_vdrive.py @@ -1,17 +1,262 @@ -import os from twisted.trial import unittest from twisted.internet import defer -from allmydata import vdrive, filetable +from twisted.python import failure +from allmydata import vdrive, filetable, uri +from allmydata.interfaces import IDirectoryNode +from foolscap import eventual -class LocalDirNode(filetable.MutableDirectoryNode): +class LocalReference: + def __init__(self, target): + self.target = target def callRemote(self, methname, *args, **kwargs): - def _call(): - meth = getattr(self, methname) + def _call(ignored): + meth = getattr(self.target, methname) return meth(*args, **kwargs) - return defer.maybeDeferred(_call) + d = eventual.fireEventually(None) + d.addCallback(_call) + return d + +class MyTub: + def __init__(self, vds, myfurl): + self.vds = vds + self.myfurl = myfurl + def getReference(self, furl): + assert furl == self.myfurl + return eventual.fireEventually(LocalReference(self.vds)) + +class MyClient: + def __init__(self, vds, myfurl): + self.tub = MyTub(vds, myfurl) + +class Test(unittest.TestCase): + def test_create_directory(self): + basedir = "vdrive/test_create_directory" + vds = filetable.VirtualDriveServer(basedir) + vds.set_furl("myFURL") + self.client = client = MyClient(vds, "myFURL") + d = vdrive.create_directory(client, "myFURL") + def _created(node): + self.failUnless(IDirectoryNode.providedBy(node)) + self.failUnless(node.is_mutable()) + d.addCallback(_created) + return d + + def test_one(self): + basedir = "vdrive/test_one" + vds = filetable.VirtualDriveServer(basedir) + vds.set_furl("myFURL") + root_uri = vds.get_public_root_uri() + + self.client = client = MyClient(vds, "myFURL") + d1 = vdrive.create_directory_node(client, root_uri) + d2 = vdrive.create_directory_node(client, root_uri) + d = defer.gatherResults( [d1,d2] ) + d.addCallback(self._test_one_1) + return d + + def _test_one_1(self, (rootnode1, rootnode2) ): + self.failUnlessEqual(rootnode1, rootnode2) + self.failIfEqual(rootnode1, "not") + + self.rootnode = rootnode = rootnode1 + self.failUnless(rootnode.is_mutable()) + self.readonly_uri = rootnode.get_immutable_uri() + d = vdrive.create_directory_node(self.client, self.readonly_uri) + d.addCallback(self._test_one_2) + return d + + def _test_one_2(self, ro_rootnode): + self.ro_rootnode = ro_rootnode + self.failIf(ro_rootnode.is_mutable()) + self.failUnlessEqual(ro_rootnode.get_immutable_uri(), + self.readonly_uri) + + rootnode = self.rootnode + + ignored = rootnode.dump() + + # root/ + d = rootnode.list() + def _listed(res): + self.failUnlessEqual(res, {}) + d.addCallback(_listed) + + file1 = uri.pack_uri("i"*32, "k"*16, "e"*32, 25, 100, 12345) + file2 = uri.pack_uri("i"*31 + "2", "k"*16, "e"*32, 25, 100, 12345) + file2_node = vdrive.FileNode(file2, None) + d.addCallback(lambda res: rootnode.set_uri("foo", file1)) + # root/ + # root/foo =file1 + + d.addCallback(lambda res: rootnode.list()) + def _listed2(res): + self.failUnlessEqual(res.keys(), ["foo"]) + file1_node = res["foo"] + self.failUnless(isinstance(file1_node, vdrive.FileNode)) + self.failUnlessEqual(file1_node.uri, file1) + d.addCallback(_listed2) + + d.addCallback(lambda res: rootnode.get("foo")) + def _got_foo(res): + self.failUnless(isinstance(res, vdrive.FileNode)) + self.failUnlessEqual(res.uri, file1) + d.addCallback(_got_foo) + + d.addCallback(lambda res: rootnode.get("missing")) + # this should raise an exception + d.addBoth(self.shouldFail, IndexError, "get('missing')", + "unable to find child named 'missing'") + + d.addCallback(lambda res: rootnode.create_empty_directory("bar")) + # root/ + # root/foo =file1 + # root/bar/ + + d.addCallback(lambda res: rootnode.list()) + d.addCallback(self.failUnlessKeysMatch, ["foo", "bar"]) + def _listed3(res): + self.failIfEqual(res["foo"], res["bar"]) + self.failIfEqual(res["bar"], res["foo"]) + self.failIfEqual(res["foo"], "not") + self.failIfEqual(res["bar"], self.rootnode) + self.failUnlessEqual(res["foo"], res["foo"]) + # make sure the objects can be used as dict keys + testdict = {res["foo"]: 1, res["bar"]: 2} + bar_node = res["bar"] + self.failUnless(isinstance(bar_node, vdrive.MutableDirectoryNode)) + self.bar_node = bar_node + bar_ro_uri = bar_node.get_immutable_uri() + return rootnode.set_uri("bar-ro", bar_ro_uri) + d.addCallback(_listed3) + # root/ + # root/foo =file1 + # root/bar/ + # root/bar-ro/ (read-only) + + d.addCallback(lambda res: rootnode.list()) + d.addCallback(self.failUnlessKeysMatch, ["foo", "bar", "bar-ro"]) + def _listed4(res): + self.failIf(res["bar-ro"].is_mutable()) + self.bar_node_readonly = res["bar-ro"] + + # add another file to bar/ + bar = res["bar"] + return bar.set_node("file2", file2_node) + d.addCallback(_listed4) + d.addCallback(self.failUnlessIdentical, file2_node) + # and a directory + d.addCallback(lambda res: self.bar_node.create_empty_directory("baz")) + # root/ + # root/foo =file1 + # root/bar/ + # root/bar/file2 =file2 + # root/bar/baz/ + # root/bar-ro/ (read-only) + # root/bar-ro/file2 =file2 + # root/bar-ro/baz/ + + d.addCallback(lambda res: self.bar_node.list()) + d.addCallback(self.failUnlessKeysMatch, ["file2", "baz"]) + d.addCallback(lambda res: + self.failUnless(res["baz"].is_mutable())) + + d.addCallback(lambda res: self.bar_node_readonly.list()) + d.addCallback(self.failUnlessKeysMatch, ["file2", "baz"]) + d.addCallback(lambda res: + self.failIf(res["baz"].is_mutable())) + + # try to add a file to bar-ro, should get exception + d.addCallback(lambda res: + self.bar_node_readonly.set_uri("file3", file2)) + d.addBoth(self.shouldFail, vdrive.NotMutableError, + "bar-ro.set('file3')") + + # try to delete a file from bar-ro, should get exception + d.addCallback(lambda res: self.bar_node_readonly.delete("file2")) + d.addBoth(self.shouldFail, vdrive.NotMutableError, + "bar-ro.delete('file2')") + + # try to mkdir in bar-ro, should get exception + d.addCallback(lambda res: + self.bar_node_readonly.create_empty_directory("boffo")) + d.addBoth(self.shouldFail, vdrive.NotMutableError, + "bar-ro.mkdir('boffo')") + + d.addCallback(lambda res: rootnode.delete("foo")) + # root/ + # root/bar/ + # root/bar/file2 =file2 + # root/bar/baz/ + # root/bar-ro/ (read-only) + # root/bar-ro/file2 =file2 + # root/bar-ro/baz/ + + d.addCallback(lambda res: rootnode.list()) + d.addCallback(self.failUnlessKeysMatch, ["bar", "bar-ro"]) + + d.addCallback(lambda res: + self.bar_node.move_child_to("file2", + self.rootnode, "file4")) + # root/ + # root/file4 = file4 + # root/bar/ + # root/bar/baz/ + # root/bar-ro/ (read-only) + # root/bar-ro/baz/ + + d.addCallback(lambda res: rootnode.list()) + d.addCallback(self.failUnlessKeysMatch, ["bar", "bar-ro", "file4"]) + d.addCallback(lambda res:self.bar_node.list()) + d.addCallback(self.failUnlessKeysMatch, ["baz"]) + d.addCallback(lambda res:self.bar_node_readonly.list()) + d.addCallback(self.failUnlessKeysMatch, ["baz"]) + + + d.addCallback(lambda res: + rootnode.move_child_to("file4", + self.bar_node_readonly, "boffo")) + d.addBoth(self.shouldFail, vdrive.NotMutableError, + "mv root/file4 root/bar-ro/boffo") + + d.addCallback(lambda res: rootnode.list()) + d.addCallback(self.failUnlessKeysMatch, ["bar", "bar-ro", "file4"]) + d.addCallback(lambda res:self.bar_node.list()) + d.addCallback(self.failUnlessKeysMatch, ["baz"]) + d.addCallback(lambda res:self.bar_node_readonly.list()) + d.addCallback(self.failUnlessKeysMatch, ["baz"]) + + + d.addCallback(lambda res: + rootnode.move_child_to("file4", self.bar_node)) + + d.addCallback(lambda res: rootnode.list()) + d.addCallback(self.failUnlessKeysMatch, ["bar", "bar-ro"]) + d.addCallback(lambda res:self.bar_node.list()) + d.addCallback(self.failUnlessKeysMatch, ["baz", "file4"]) + d.addCallback(lambda res:self.bar_node_readonly.list()) + d.addCallback(self.failUnlessKeysMatch, ["baz", "file4"]) + + return d + + def shouldFail(self, res, expected_failure, which, substring=None): + if isinstance(res, failure.Failure): + res.trap(expected_failure) + if substring: + self.failUnless(substring in str(res), + "substring '%s' not in '%s'" + % (substring, str(res))) + else: + self.fail("%s was supposed to raise %s, not get '%s'" % + (which, expected_failure, res)) + + def failUnlessKeysMatch(self, res, expected_keys): + self.failUnlessEqual(sorted(res.keys()), + sorted(expected_keys)) + return res +""" class Traverse(unittest.TestCase): def make_tree(self, basedir): os.makedirs(basedir) @@ -67,3 +312,4 @@ class Traverse(unittest.TestCase): ["2.a", "2.b", "d2.1"])) return d del Traverse +""" diff --git a/src/allmydata/uri.py b/src/allmydata/uri.py index 308f64de..80ff5b3c 100644 --- a/src/allmydata/uri.py +++ b/src/allmydata/uri.py @@ -86,3 +86,31 @@ def unpack_extension_readable(data): if "hash" in k: unpacked[k] = idlib.b2a(unpacked[k]) return unpacked + +def is_dirnode_uri(uri): + return uri.startswith("URI:DIR:") or uri.startswith("URI:DIR-RO:") +def is_mutable_dirnode_uri(uri): + return uri.startswith("URI:DIR:") +def unpack_dirnode_uri(uri): + assert is_dirnode_uri(uri) + # URI:DIR:furl:key + # but note that the furl contains colons + for prefix in ("URI:DIR:", "URI:DIR-RO:"): + if uri.startswith(prefix): + uri = uri[len(prefix):] + break + else: + assert 0 + colon = uri.rindex(":") + furl = uri[:colon] + key = uri[colon+1:] + return furl, idlib.a2b(key) + +def make_immutable_dirnode_uri(mutable_uri): + assert is_mutable_dirnode_uri(mutable_uri) + furl, writekey = unpack_dirnode_uri(mutable_uri) + readkey = hashutil.dir_read_key_hash(writekey) + return "URI:DIR-RO:%s:%s" % (furl, idlib.b2a(readkey)) + +def pack_dirnode_uri(furl, writekey): + return "URI:DIR:%s:%s" % (furl, idlib.b2a(writekey)) diff --git a/src/allmydata/util/hashutil.py b/src/allmydata/util/hashutil.py index c3d967b9..d44003c5 100644 --- a/src/allmydata/util/hashutil.py +++ b/src/allmydata/util/hashutil.py @@ -1,4 +1,5 @@ from allmydata.Crypto.Hash import SHA256 +import os def netstring(s): return "%d:%s," % (len(s), s,) @@ -56,3 +57,25 @@ def key_hash(data): def key_hasher(): return tagged_hasher("allmydata_encryption_key_v1") +KEYLEN = 16 +def random_key(): + return os.urandom(KEYLEN) + +def dir_write_enabler_hash(write_key): + return tagged_hash("allmydata_dir_write_enabler_v1", write_key) +def dir_read_key_hash(write_key): + return tagged_hash("allmydata_dir_read_key_v1", write_key)[:KEYLEN] +def dir_index_hash(read_key): + return tagged_hash("allmydata_dir_index_v1", read_key) +def dir_name_hash(readkey, name): + return tagged_pair_hash("allmydata_dir_name_v1", readkey, name) + +def generate_dirnode_keys_from_writekey(write_key): + readkey = dir_read_key_hash(write_key) + write_enabler = dir_write_enabler_hash(write_key) + index = dir_index_hash(readkey) + return write_key, write_enabler, readkey, index + +def generate_dirnode_keys_from_readkey(read_key): + index = dir_index_hash(read_key) + return None, None, read_key, index diff --git a/src/allmydata/vdrive.py b/src/allmydata/vdrive.py index 7e8672d0..bd11576c 100644 --- a/src/allmydata/vdrive.py +++ b/src/allmydata/vdrive.py @@ -1,11 +1,18 @@ """This is the client-side facility to manipulate virtual drives.""" +import os.path +from zope.interface import implements from twisted.application import service from twisted.internet import defer from twisted.python import log -from allmydata import upload, download -from foolscap import Copyable, RemoteCopy +from allmydata import upload, download, uri +from allmydata.Crypto.Cipher import AES +from allmydata.util import hashutil, idlib +from allmydata.interfaces import IDirectoryNode, IFileNode + +class NotMutableError(Exception): + pass class VDrive(service.MultiService): name = "vdrive" @@ -181,99 +188,243 @@ class VDrive(service.MultiService): return self.get_file(from_where, download.FileHandle(filehandle)) -class DirectoryNode(Copyable, RemoteCopy): - """I have either a .furl attribute or a .get(tub) method.""" - typeToCopy = "allmydata.com/tahoe/interfaces/DirectoryNode/v1" - copytype = typeToCopy - def __init__(self, furl=None, client=None): - # RemoteCopy subclasses are always called without arguments - self.furl = furl - self._set_client(client) - def _set_client(self, client): +def create_directory_node(client, diruri): + assert uri.is_dirnode_uri(diruri) + if uri.is_mutable_dirnode_uri(diruri): + dirnode_class = MutableDirectoryNode + else: + dirnode_class = ImmutableDirectoryNode + (furl, key) = uri.unpack_dirnode_uri(diruri) + d = client.tub.getReference(furl) + def _got(rref): + dirnode = dirnode_class(diruri, client, rref, key) + return dirnode + d.addCallback(_got) + return d + +def encrypt(key, data): + # TODO: add the hmac + IV = os.urandom(14) + counterstart = IV + "\x00"*2 + assert len(counterstart) == 16, len(counterstart) + cryptor = AES.new(key=key, mode=AES.MODE_CTR, counterstart=counterstart) + crypttext = cryptor.encrypt(data) + return IV + crypttext + +def decrypt(key, data): + # TODO: validate the hmac + assert len(data) >= 14, len(data) + IV = data[:14] + counterstart = IV + "\x00"*2 + assert len(counterstart) == 16, len(counterstart) + cryptor = AES.new(key=key, mode=AES.MODE_CTR, counterstart=counterstart) + plaintext = cryptor.decrypt(data[14:]) + return plaintext + + +class ImmutableDirectoryNode: + implements(IDirectoryNode) + + def __init__(self, myuri, client, rref, readkey): + self._uri = myuri self._client = client - return self - def getStateToCopy(self): - return {"furl": self.furl } - def setCopyableState(self, state): - self.furl = state['furl'] + self._tub = client.tub + self._rref = rref + self._readkey = readkey + self._writekey = None + self._write_enabler = None + self._index = hashutil.dir_index_hash(self._readkey) + self._mutable = False + + def dump(self): + return ["URI: %s" % self._uri, + "rk: %s" % idlib.b2a(self._readkey), + "index: %s" % idlib.b2a(self._index), + ] + + def is_mutable(self): + return self._mutable + + def get_uri(self): + return self._uri + + def get_immutable_uri(self): + # return the dirnode URI for a read-only form of this directory + if self._mutable: + return uri.make_immutable_dirnode_uri(self._uri) + else: + return self._uri + def __hash__(self): - return hash((self.__class__, self.furl)) + 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.furl, them.furl) + return cmp(self._uri, them._uri) + + def _encrypt(self, key, data): + return encrypt(key, data) + + def _decrypt(self, key, data): + return decrypt(key, data) + + def _decrypt_child(self, E_write, E_read): + if E_write and self._writekey: + # we prefer read-write children when we can get them + return self._decrypt(self._writekey, E_write) + else: + return self._decrypt(self._readkey, E_read) def list(self): - d = self._client.tub.getReference(self.furl) - d.addCallback(lambda node: node.callRemote("list")) - d.addCallback(lambda children: - [(name,child._set_client(self._client)) - for name,child in children]) + d = self._rref.callRemote("list", self._index) + entries = {} + def _got(res): + dl = [] + for (E_name, E_write, E_read) in res: + name = self._decrypt(self._readkey, E_name) + child_uri = self._decrypt_child(E_write, E_read) + d2 = self._create_node(child_uri) + def _created(node, name): + entries[name] = node + d2.addCallback(_created, name) + dl.append(d2) + return defer.DeferredList(dl) + d.addCallback(_got) + d.addCallback(lambda res: entries) return d + def _hash_name(self, name): + return hashutil.dir_name_hash(self._readkey, name) + def get(self, name): - d = self._client.tub.getReference(self.furl) - d.addCallback(lambda node: node.callRemote("get", name)) - d.addCallback(lambda child: child._set_client(self._client)) + H_name = self._hash_name(name) + d = self._rref.callRemote("get", self._index, H_name) + def _check_index_error(f): + f.trap(IndexError) + raise IndexError("get(index=%s): unable to find child named '%s'" + % (idlib.b2a(self._index), name)) + d.addErrback(_check_index_error) + d.addCallback(lambda (E_write, E_read): + self._decrypt_child(E_write, E_read)) + d.addCallback(self._create_node) return d - def add(self, name, child): - d = self._client.tub.getReference(self.furl) - d.addCallback(lambda node: node.callRemote("add", name, child)) - d.addCallback(lambda newnode: newnode._set_client(self._client)) + def _set(self, name, write_child, read_child): + if not self._mutable: + return defer.fail(NotMutableError()) + H_name = self._hash_name(name) + E_name = self._encrypt(self._readkey, name) + E_write = "" + if self._writekey and write_child: + E_write = self._encrypt(self._writekey, write_child) + E_read = self._encrypt(self._readkey, read_child) + d = self._rref.callRemote("set", self._index, self._write_enabler, + H_name, E_name, E_write, E_read) return d - def add_file(self, name, uploadable): - uploader = self._client.getServiceNamed("uploader") - d = uploader.upload(uploadable) - d.addCallback(lambda uri: self.add(name, FileNode(uri, self._client))) + def set_uri(self, name, child_uri): + write, read = self._split_uri(child_uri) + return self._set(name, write, read) + + def set_node(self, name, child): + d = self.set_uri(name, child.get_uri()) + d.addCallback(lambda res: child) return d - def remove(self, name): - d = self._client.tub.getReference(self.furl) - d.addCallback(lambda node: node.callRemote("remove", name)) - d.addCallback(lambda newnode: newnode._set_client(self._client)) + def delete(self, name): + if not self._mutable: + return defer.fail(NotMutableError()) + H_name = self._hash_name(name) + d = self._rref.callRemote("delete", self._index, self._write_enabler, + H_name) return d + def _create_node(self, child_uri): + if uri.is_dirnode_uri(child_uri): + return create_directory_node(self._client, child_uri) + else: + return defer.succeed(FileNode(child_uri, self._client)) + + def _split_uri(self, child_uri): + if uri.is_dirnode_uri(child_uri): + if uri.is_mutable_dirnode_uri(child_uri): + write = child_uri + read = uri.make_immutable_dirnode_uri(child_uri) + else: + write = None + read = child_uri + return (write, read) + return (None, child_uri) # file + def create_empty_directory(self, name): - vdrive_server = self._client._vdrive_server - d = vdrive_server.callRemote("create_directory") - d.addCallback(lambda node: self.add(name, node)) + if not self._mutable: + return defer.fail(NotMutableError()) + child_writekey = hashutil.random_key() + my_furl, parent_writekey = uri.unpack_dirnode_uri(self._uri) + child_uri = uri.pack_dirnode_uri(my_furl, child_writekey) + child = MutableDirectoryNode(child_uri, self._client, self._rref, + child_writekey) + d = self._rref.callRemote("create_directory", + child._index, child._write_enabler) + d.addCallback(lambda index: self.set_node(name, child)) return d - def attach_shared_directory(self, name, furl): - d = self.add(name, DirectoryNode(furl)) + def add_file(self, name, uploadable): + if not self._mutable: + return defer.fail(NotMutableError()) + uploader = self._client.getServiceNamed("uploader") + d = uploader.upload(uploadable) + d.addCallback(lambda uri: self.set_node(name, + FileNode(uri, self._client))) return d - def get_shared_directory_furl(self): - return defer.succeed(self.furl) - def move_child_to(self, current_child_name, new_parent, new_child_name=None): + if not (self._mutable and new_parent.is_mutable()): + return defer.fail(NotMutableError()) if new_child_name is None: new_child_name = current_child_name d = self.get(current_child_name) - d.addCallback(lambda child: new_parent.add(new_child_name, child)) - d.addCallback(lambda child: self.remove(current_child_name)) + d.addCallback(lambda child: new_parent.set_node(new_child_name, child)) + d.addCallback(lambda child: self.delete(current_child_name)) return d -class FileNode(Copyable, RemoteCopy): - """I have a .uri attribute.""" - typeToCopy = "allmydata.com/tahoe/interfaces/FileNode/v1" - copytype = typeToCopy - def __init__(self, uri=None, client=None): - # RemoteCopy subclasses are always called without arguments +class MutableDirectoryNode(ImmutableDirectoryNode): + implements(IDirectoryNode) + + def __init__(self, myuri, client, rref, writekey): + readkey = hashutil.dir_read_key_hash(writekey) + ImmutableDirectoryNode.__init__(self, myuri, client, rref, readkey) + self._writekey = writekey + self._write_enabler = hashutil.dir_write_enabler_hash(writekey) + self._mutable = True + +def create_directory(client, furl): + write_key = hashutil.random_key() + (wk, we, rk, index) = \ + hashutil.generate_dirnode_keys_from_writekey(write_key) + myuri = uri.pack_dirnode_uri(furl, wk) + d = client.tub.getReference(furl) + def _got_vdrive_server(vdrive_server): + node = MutableDirectoryNode(myuri, client, vdrive_server, wk) + d2 = vdrive_server.callRemote("create_directory", index, we) + d2.addCallback(lambda res: node) + return d2 + d.addCallback(_got_vdrive_server) + return d + +class FileNode: + implements(IFileNode) + + def __init__(self, uri, client): self.uri = uri - self._set_client(client) - def _set_client(self, client): self._client = client - return self - def getStateToCopy(self): - return {"uri": self.uri } - def setCopyableState(self, state): - self.uri = state['uri'] + + def get_uri(self): + return self.uri + def __hash__(self): return hash((self.__class__, self.uri)) def __cmp__(self, them): diff --git a/src/allmydata/web/directory.xhtml b/src/allmydata/web/directory.xhtml index 03f26d8d..a4b73b7c 100644 --- a/src/allmydata/web/directory.xhtml +++ b/src/allmydata/web/directory.xhtml @@ -13,9 +13,11 @@
Refresh this view
Parent Directory
-
To share this directory, paste the following FURL string into an +
To share this directory, paste the following URI string into an "Add Shared Directory" box: -
+
+
To share a transitively read-only copy, use the following URI instead: +
diff --git a/src/allmydata/webish.py b/src/allmydata/webish.py index d16653ad..e8b518c6 100644 --- a/src/allmydata/webish.py +++ b/src/allmydata/webish.py @@ -6,8 +6,8 @@ from nevow import inevow, rend, loaders, appserver, url, tags as T from nevow.static import File as nevow_File # TODO: merge with static.File? from allmydata.util import idlib from allmydata.uri import unpack_uri -from allmydata.interfaces import IDownloadTarget -from allmydata.vdrive import FileNode, DirectoryNode +from allmydata.interfaces import IDownloadTarget, IDirectoryNode, IFileNode +from allmydata.vdrive import FileNode from allmydata import upload from zope.interface import implements, Interface import urllib @@ -120,10 +120,10 @@ class Directory(rend.Page): dirname = self._dirname + "/" + name d = self._dirnode.get(name) def _got_child(res): - if isinstance(res, FileNode): + if IFileNode.providedBy(res): dl = get_downloader_service(ctx) return Downloader(dl, name, res) - elif isinstance(res, DirectoryNode): + elif IDirectoryNode.providedBy(res): return Directory(res, dirname) else: raise RuntimeError("what is this %s" % res) @@ -134,18 +134,39 @@ class Directory(rend.Page): return ctx.tag["Directory of '%s':" % self._dirname] def render_header(self, ctx, data): - return "Directory of '%s':" % self._dirname + header = "Directory of '%s':" % self._dirname + if not self._dirnode.is_mutable(): + header += " (readonly)" + return header - def data_share_url(self, ctx, data): - return self._dirnode.furl + def data_share_uri(self, ctx, data): + return self._dirnode.get_uri() + def data_share_readonly_uri(self, ctx, data): + return self._dirnode.get_immutable_uri() def data_children(self, ctx, data): d = self._dirnode.list() + d.addCallback(lambda dict: sorted(dict.items())) return d def render_row(self, ctx, data): name, target = data - if isinstance(target, FileNode): + + if self._dirnode.is_mutable(): + # this creates a button which will cause our child__delete method + # to be invoked, which deletes the file and then redirects the + # browser back to this directory + del_url = url.here.child("_delete") + #del_url = del_url.add("uri", target.uri) + del_url = del_url.add("name", name) + delete = T.form(action=del_url, method="post")[ + T.input(type='submit', value='del', name="del"), + ] + else: + delete = "-" + ctx.fillSlots("delete", delete) + + if IFileNode.providedBy(target): # file dlurl = urllib.quote(name) ctx.fillSlots("filename", @@ -160,17 +181,7 @@ class Directory(rend.Page): #extract and display file size ctx.fillSlots("size", unpack_uri(uri)['size']) - # this creates a button which will cause our child__delete method - # to be invoked, which deletes the file and then redirects the - # browser back to this directory - del_url = url.here.child("_delete") - #del_url = del_url.add("uri", target.uri) - del_url = del_url.add("name", name) - delete = T.form(action=del_url, method="post")[ - T.input(type='submit', value='del', name="del"), - ] - ctx.fillSlots("delete", delete) - elif isinstance(target, DirectoryNode): + elif IDirectoryNode.providedBy(target): # directory subdir_url = urllib.quote(name) ctx.fillSlots("filename", @@ -178,19 +189,14 @@ class Directory(rend.Page): ctx.fillSlots("type", "DIR") ctx.fillSlots("size", "-") ctx.fillSlots("uri", "-") - - del_url = url.here.child("_delete") - del_url = del_url.add("name", name) - delete = T.form(action=del_url, method="post")[ - T.input(type='submit', value='del', name="del"), - ] - ctx.fillSlots("delete", delete) else: raise RuntimeError("unknown thing %s" % (target,)) return ctx.tag def render_forms(self, ctx, data): - return webform.renderForms() + if self._dirnode.is_mutable(): + return webform.renderForms() + return T.div["No upload forms: directory is immutable"] def render_results(self, ctx, data): req = inevow.IRequest(ctx) @@ -235,14 +241,13 @@ class Directory(rend.Page): log.msg("starting webish upload") uploader = get_uploader_service(ctx) - d = uploader.upload(upload.FileHandle(contents.file)) + uploadable = upload.FileHandle(contents.file) name = contents.filename - def _uploaded(uri): - if privateupload: - return self.uploadprivate(name, uri) - else: - return self._dirnode.add(name, FileNode(uri)) - d.addCallback(_uploaded) + if privateupload: + d = uploader.upload(uploadable) + d.addCallback(lambda uri: self.uploadprivate(name, uri)) + else: + d = self._dirnode.add_file(name, uploadable) def _done(res): log.msg("webish upload complete") return res @@ -271,15 +276,15 @@ class Directory(rend.Page): def bind_mount(self, ctx): namearg = annotate.Argument("name", annotate.String("Name to place incoming directory: ")) - furlarg = annotate.Argument("furl", - annotate.String("FURL of Shared Directory")) - meth = annotate.Method(arguments=[namearg, furlarg], + uriarg = annotate.Argument("uri", + annotate.String("URI of Shared Directory")) + meth = annotate.Method(arguments=[namearg, uriarg], label="Add Shared Directory") return annotate.MethodBinding("mount", meth, action="Mount Shared Directory") - def mount(self, name, furl): - d = self._dirnode.attach_shared_directory(name, furl) + def mount(self, name, uri): + d = self._dirnode.set_uri(name, uri) #d.addCallback(lambda done: url.here.child(name)) return d @@ -287,7 +292,7 @@ class Directory(rend.Page): # perform the delete, then redirect back to the directory page args = inevow.IRequest(ctx).args name = args["name"][0] - d = self._dirnode.remove(name) + d = self._dirnode.delete(name) d.addCallback(lambda done: url.here.up()) return d @@ -324,7 +329,7 @@ class Downloader(resource.Resource): def __init__(self, downloader, name, filenode): self._downloader = downloader self._name = name - assert isinstance(filenode, FileNode) + IFileNode(filenode) self._filenode = filenode def render(self, ctx): @@ -397,14 +402,14 @@ class WebishServer(service.MultiService): # apparently 'ISite' does not exist #self.site._client = self.parent - def set_vdrive_root(self, root): + def set_vdrive_rootnode(self, root): self.root.putChild("global_vdrive", Directory(root, "/")) self.root.child_welcome.has_global_vdrive = True # I tried doing it this way and for some reason it didn't seem to work #print "REMEMBERING", self.site, dl, IDownloader #self.site.remember(dl, IDownloader) - def set_my_vdrive_root(self, my_vdrive): + def set_my_vdrive_rootnode(self, my_vdrive): self.root.putChild("my_vdrive", Directory(my_vdrive, "~")) self.root.child_welcome.has_my_vdrive = True