From: david-sarah Date: Wed, 27 Jan 2010 06:44:30 +0000 (-0800) Subject: Prevent mutable objects from being retrieved from an immutable directory, and associa... X-Git-Tag: trac-4200~7 X-Git-Url: https://git.rkrishnan.org/listings/pb.xhtml?a=commitdiff_plain;h=6057bc02cc33b5b7f0e34525c8ddb67e3ebc7f05;p=tahoe-lafs%2Ftahoe-lafs.git Prevent mutable objects from being retrieved from an immutable directory, and associated forward-compatibility improvements. --- diff --git a/contrib/fuse/impl_c/blackmatch.py b/contrib/fuse/impl_c/blackmatch.py index 45296d7f..c8d2ed61 100644 --- a/contrib/fuse/impl_c/blackmatch.py +++ b/contrib/fuse/impl_c/blackmatch.py @@ -1,7 +1,7 @@ #!/usr/bin/env python #----------------------------------------------------------------------------------------------- -from allmydata.uri import CHKFileURI, DirectoryURI, LiteralFileURI +from allmydata.uri import CHKFileURI, DirectoryURI, LiteralFileURI, is_literal_file_uri from allmydata.scripts.common_http import do_http as do_http_req from allmydata.util.hashutil import tagged_hash from allmydata.util.assertutil import precondition @@ -335,7 +335,7 @@ class TahoeFuseFile(object): self.fname = self.tfs.cache.tmp_file(os.urandom(20)) if self.fnode is None: log('TFF: [%s] open() for write: no file node, creating new File %s' % (self.name, self.fname, )) - self.fnode = File(0, 'URI:LIT:') + self.fnode = File(0, LiteralFileURI.BASE_STRING) self.fnode.tmp_fname = self.fname # XXX kill this self.parent.add_child(self.name, self.fnode, {}) elif hasattr(self.fnode, 'tmp_fname'): @@ -362,7 +362,7 @@ class TahoeFuseFile(object): self.fname = self.fnode.tmp_fname log('TFF: reopening(%s) for reading' % self.fname) else: - if uri.startswith("URI:LIT") or not self.tfs.async: + if is_literal_file_uri(uri) or not self.tfs.async: log('TFF: synchronously fetching file from cache for reading') self.fname = self.tfs.cache.get_file(uri) else: @@ -1237,7 +1237,7 @@ class FileCache(object): def get_file(self, uri): self.log('get_file(%s)' % (uri,)) - if uri.startswith("URI:LIT"): + if is_literal_file_uri(uri): return self.get_literal(uri) else: return self.get_chk(uri, async=False) diff --git a/docs/frontends/webapi.txt b/docs/frontends/webapi.txt index a16ccc5c..419cf42f 100644 --- a/docs/frontends/webapi.txt +++ b/docs/frontends/webapi.txt @@ -150,8 +150,12 @@ server prefix. They will be displayed like this: === Child Lookup === -Tahoe directories contain named children, just like directories in a regular -local filesystem. These children can be either files or subdirectories. +Tahoe directories contain named child entries, just like directories in a regular +local filesystem. These child entries, called "dirnodes", consist of a name, +metadata, a write slot, and a read slot. The write and read slots normally contain +a write-cap and read-cap referring to the same object, which can be either a file +or a subdirectory. The write slot may be empty (actually, both may be empty, +but that is unusual). If you have a Tahoe URL that refers to a directory, and want to reference a named child inside it, just append the child name to the URL. For example, if @@ -390,6 +394,27 @@ POST /uri?t=mkdir-with-children } } } ] } + For forward-compatibility, a mutable directory can also contain caps in + a format that is unknown to the webapi server. When such caps are retrieved + from a mutable directory in a "ro_uri" field, they will be prefixed with + the string "ro.", indicating that they must not be decoded without + checking that they are read-only. The "ro." prefix must not be stripped + off without performing this check. (Future versions of the webapi server + will perform it where necessary.) + + If both the "rw_uri" and "ro_uri" fields are present in a given PROPDICT, + and the webapi server recognizes the rw_uri as a write cap, then it will + reset the ro_uri to the corresponding read cap and discard the original + contents of ro_uri (in order to ensure that the two caps correspond to the + same object and that the ro_uri is in fact read-only). However this may not + happen for caps in a format unknown to the webapi server. Therefore, when + writing a directory the webapi client should ensure that the contents + of "rw_uri" and "ro_uri" for a given PROPDICT are a consistent + (write cap, read cap) pair if possible. If the webapi client only has + one cap and does not know whether it is a write cap or read cap, then + it is acceptable to set "rw_uri" to that cap and omit "ro_uri". The + client must not put a write cap into a "ro_uri" field. + Note that the webapi-using client application must not provide the "Content-Type: multipart/form-data" header that usually accompanies HTML form submissions, since the body is not formatted this way. Doing so will @@ -404,59 +429,113 @@ POST /uri?t=mkdir-immutable Like t=mkdir-with-children above, but the new directory will be deep-immutable. This means that the directory itself is immutable, and that - it can only contain deep-immutable objects, like immutable files, literal - files, and deep-immutable directories. A non-empty request body is - mandatory, since after the directory is created, it will not be possible to - add more children to it. + it can only contain objects that are treated as being deep-immutable, like + immutable files, literal files, and deep-immutable directories. + + For forward-compatibility, a deep-immutable directory can also contain caps + in a format that is unknown to the webapi server. When such caps are retrieved + from a deep-immutable directory in a "ro_uri" field, they will be prefixed + with the string "imm.", indicating that they must not be decoded without + checking that they are immutable. The "imm." prefix must not be stripped + off without performing this check. (Future versions of the webapi server + will perform it where necessary.) + + The cap for each child may be given either in the "rw_uri" or "ro_uri" + field of the PROPDICT (not both). If a cap is given in the "rw_uri" field, + then the webapi server will check that it is an immutable read-cap of a + *known* format, and give an error if it is not. If a cap is given in the + "ro_uri" field, then the webapi server will still check whether known + caps are immutable, but for unknown caps it will simply assume that the + cap can be stored, as described above. Note that an attacker would be + able to store any cap in an immutable directory, so this check when + creating the directory is only to help non-malicious clients to avoid + accidentally giving away more authority than intended. + + A non-empty request body is mandatory, since after the directory is created, + it will not be possible to add more children to it. POST /uri/$DIRCAP/[SUBDIRS../]SUBDIR?t=mkdir PUT /uri/$DIRCAP/[SUBDIRS../]SUBDIR?t=mkdir Create new directories as necessary to make sure that the named target ($DIRCAP/SUBDIRS../SUBDIR) is a directory. This will create additional - intermediate directories as necessary. If the named target directory already - exists, this will make no changes to it. + intermediate mutable directories as necessary. If the named target directory + already exists, this will make no changes to it. If the final directory is created, it will be empty. - This will return an error if a blocking file is present at any of the parent - names, preventing the server from creating the necessary parent directory. + This operation will return an error if a blocking file is present at any of + the parent names, preventing the server from creating the necessary parent + directory; or if it would require changing an immutable directory. The write-cap of the new directory will be returned as the HTTP response body. POST /uri/$DIRCAP/[SUBDIRS../]SUBDIR?t=mkdir-with-children - Like above, but if the final directory is created, it will be populated with - initial children from the POST request body, as described above in the - /uri?t=mkdir-with-children operation. + Like /uri?t=mkdir-with-children, but the final directory is created as a + child of an existing mutable directory. This will create additional + intermediate mutable directories as necessary. If the final directory is + created, it will be populated with initial children from the POST request + body, as described above. + + This operation will return an error if a blocking file is present at any of + the parent names, preventing the server from creating the necessary parent + directory; or if it would require changing an immutable directory; or if + the immediate parent directory already has a a child named SUBDIR. POST /uri/$DIRCAP/[SUBDIRS../]SUBDIR?t=mkdir-immutable - Like above, but the final directory will be deep-immutable, with the - children specified as a JSON dictionary in the POST request body. + Like /uri?t=mkdir-immutable, but the final directory is created as a child + of an existing mutable directory. The final directory will be deep-immutable, + and will be populated with the children specified as a JSON dictionary in + the POST request body. + + In Tahoe 1.6 this operation creates intermediate mutable directories if + necessary, but that behaviour should not be relied on; see ticket #920. + + This operation will return an error if the parent directory is immutable, + or already has a child named SUBDIR. POST /uri/$DIRCAP/[SUBDIRS../]?t=mkdir&name=NAME - Create a new empty directory and attach it to the given existing directory. - This will create additional intermediate directories as necessary. + Create a new empty mutable directory and attach it to the given existing + directory. This will create additional intermediate directories as necessary. - The URL of this form points to the parent of the bottom-most new directory, - whereas the previous form has a URL that points directly to the bottom-most - new directory. + This operation will return an error if a blocking file is present at any of + the parent names, preventing the server from creating the necessary parent + directory, or if it would require changing any immutable directory. + + The URL of this operation points to the parent of the bottommost new directory, + whereas the /uri/$DIRCAP/[SUBDIRS../]SUBDIR?t=mkdir operation above has a URL + that points directly to the bottommost new directory. POST /uri/$DIRCAP/[SUBDIRS../]?t=mkdir-with-children&name=NAME - As above, but the new directory will be populated with initial children via - the POST request body, as described in /uri?t=mkdir-with-children above. + Like /uri/$DIRCAP/[SUBDIRS../]?t=mkdir&name=NAME, but the new directory will + be populated with initial children via the POST request body. This command + will create additional intermediate mutable directories as necessary. + + This operation will return an error if a blocking file is present at any of + the parent names, preventing the server from creating the necessary parent + directory; or if it would require changing an immutable directory; or if + the immediate parent directory already has a a child named NAME. + Note that the name= argument must be passed as a queryarg, because the POST request body is used for the initial children JSON. POST /uri/$DIRCAP/[SUBDIRS../]?t=mkdir-immutable&name=NAME - As above, but the new directory will be deep-immutable, with the children - specified as a JSON dictionary in the POST request body. Again, the name= - argument must be passed as a queryarg. + Like /uri/$DIRCAP/[SUBDIRS../]?t=mkdir-with-children&name=NAME, but the + final directory will be deep-immutable. The children are specified as a + JSON dictionary in the POST request body. Again, the name= argument must be + passed as a queryarg. + + In Tahoe 1.6 this operation creates intermediate mutable directories if + necessary, but that behaviour should not be relied on; see ticket #920. + + This operation will return an error if the parent directory is immutable, + or already has a child named NAME. === Get Information About A File Or Directory (as JSON) === @@ -679,7 +758,9 @@ POST /uri/$DIRCAP/[SUBDIRS..]?t=set-children (Tahoe >= v1.6) "childinfo" is a dictionary that contains "rw_uri", "ro_uri", and "metadata" keys. You can take the output of "GET /uri/$DIRCAP1?t=json" and use it as the input to "POST /uri/$DIRCAP2?t=set_children" to make DIR2 - look very much like DIR1. + look very much like DIR1 (except for any existing children of DIR2 that + were not overwritten, and any existing "tahoe" metadata keys as described + below). When the set_children request contains a child name that already exists in the target directory, this command defaults to overwriting that child with @@ -880,9 +961,9 @@ POST /uri?t=upload POST /uri/$DIRCAP/[SUBDIRS../]?t=upload - This uploads a file, and attaches it as a new child of the given directory. - The file must be provided as the "file" field of an HTML encoded form body, - produced in response to an HTML form like this: + This uploads a file, and attaches it as a new child of the given directory, + which must be mutable. The file must be provided as the "file" field of an + HTML-encoded form body, produced in response to an HTML form like this:
@@ -925,9 +1006,10 @@ POST /uri/$DIRCAP/[SUBDIRS../]?t=upload POST /uri/$DIRCAP/[SUBDIRS../]FILENAME?t=upload This also uploads a file and attaches it as a new child of the given - directory. It is a slight variant of the previous operation, as the URL - refers to the target file rather than the parent directory. It is otherwise - identical: this accepts mutable= and when_done= arguments too. + directory, which must be mutable. It is a slight variant of the previous + operation, as the URL refers to the target file rather than the parent + directory. It is otherwise identical: this accepts mutable= and when_done= + arguments too. POST /uri/$FILECAP?t=upload @@ -955,20 +1037,21 @@ POST /uri/$DIRCAP/[SUBDIRS../]?t=uri&name=CHILDNAME&uri=CHILDCAP POST /uri/$DIRCAP/[SUBDIRS../]?t=delete&name=CHILDNAME - This instructs the node to delete a child object (file or subdirectory) from - the given directory. Note that the entire subtree is removed. This is - somewhat like "rm -rf" (from the point of view of the parent), but other - references into the subtree will see that the child subdirectories are not - modified by this operation. Only the link from the given directory to its - child is severed. + This instructs the node to remove a child object (file or subdirectory) from + the given directory, which must be mutable. Note that the entire subtree is + unlinked from the parent. Unlike deleting a subdirectory in a UNIX local + filesystem, the subtree need not be empty; if it isn't, then other references + into the subtree will see that the child subdirectories are not modified by + this operation. Only the link from the given directory to its child is severed. === Renaming A Child === POST /uri/$DIRCAP/[SUBDIRS../]?t=rename&from_name=OLD&to_name=NEW - This instructs the node to rename a child of the given directory. This is - exactly the same as removing the child, then adding the same child-cap under - the new name. This operation cannot move the child to a different directory. + This instructs the node to rename a child of the given directory, which must + be mutable. This has a similar effect to removing the child, then adding the + same child-cap under the new name, except that it preserves metadata. This + operation cannot move the child to a different directory. This operation will replace any existing child of the new name, making it behave like the UNIX "mv -f" command. @@ -1164,9 +1247,11 @@ POST $URL?t=stream-deep-check "path": a list of strings, with the path that is traversed to reach the object - "cap": a writecap for the file or directory, if available, else a readcap - "verifycap": a verifycap for the file or directory - "repaircap": the weakest cap which can still be used to repair the object + "cap": a write-cap URI for the file or directory, if available, else a + read-cap URI + "verifycap": a verify-cap URI for the file or directory + "repaircap": an URI for the weakest cap that can still be used to repair + the object "storage-index": a base32 storage index for the object "check-results": a copy of the dictionary which would be returned by t=check&output=json, with three top-level keys: @@ -1408,9 +1493,11 @@ POST $URL?t=stream-manifest "path": a list of strings, with the path that is traversed to reach the object - "cap": a writecap for the file or directory, if available, else a readcap - "verifycap": a verifycap for the file or directory - "repaircap": the weakest cap which can still be used to repair the object + "cap": a write-cap URI for the file or directory, if available, else a + read-cap URI + "verifycap": a verify-cap URI for the file or directory + "repaircap": an URI for the weakest cap that can still be used to repair + the object "storage-index": a base32 storage index for the object Note that non-distributed files (i.e. LIT files) will have values of None @@ -1641,6 +1728,17 @@ the child, then those kinds of mistakes just can't happen. Note that both the child's name and the child's URI are included in the results of listing the parent directory, so it isn't any harder to use the URI for this purpose. +The read and write caps in a given directory node are separate URIs, and +can't be assumed to point to the same object even if they were retrieved in +the same operation (although the webapi server attempts to ensure this +in most cases). If you need to rely on that property, you should explicitly +verify it. More generally, you should not make assumptions about the +internal consistency of the contents of mutable directories. As a result +of the signatures on mutable object versions, it is guaranteed that a given +version was written in a single update, but -- as in the case of a file -- +the contents may have been chosen by a malicious writer in a way that is +designed to confuse applications that rely on their consistency. + In general, use names if you want "whatever object (whether file or directory) is found by following this name (or sequence of names) when my request reaches the server". Use URIs if you want "this particular object". diff --git a/src/allmydata/client.py b/src/allmydata/client.py index ec079762..12e74734 100644 --- a/src/allmydata/client.py +++ b/src/allmydata/client.py @@ -471,13 +471,16 @@ class Client(node.Node, pollmixin.PollMixin): # dirnodes. The first takes a URI and produces a filenode or (new-style) # dirnode. The other three create brand-new filenodes/dirnodes. - def create_node_from_uri(self, writecap, readcap=None): - # this returns synchronously. - return self.nodemaker.create_from_cap(writecap, readcap) + def create_node_from_uri(self, write_uri, read_uri=None, deep_immutable=False, name=""): + # This returns synchronously. + # Note that it does *not* validate the write_uri and read_uri; instead we + # may get an opaque node if there were any problems. + return self.nodemaker.create_from_cap(write_uri, read_uri, deep_immutable=deep_immutable, name=name) def create_dirnode(self, initial_children={}): d = self.nodemaker.create_new_mutable_directory(initial_children) return d + def create_immutable_dirnode(self, children, convergence=None): return self.nodemaker.create_immutable_directory(children, convergence) diff --git a/src/allmydata/control.py b/src/allmydata/control.py index 8c12b034..cb0c84f7 100644 --- a/src/allmydata/control.py +++ b/src/allmydata/control.py @@ -5,7 +5,7 @@ from twisted.application import service from twisted.internet import defer from twisted.internet.interfaces import IConsumer from foolscap.api import Referenceable -from allmydata.interfaces import RIControlClient +from allmydata.interfaces import RIControlClient, IFileNode from allmydata.util import fileutil, mathutil from allmydata.immutable import upload from twisted.python import log @@ -67,7 +67,9 @@ class ControlServer(Referenceable, service.Service): return d def remote_download_from_uri_to_file(self, uri, filename): - filenode = self.parent.create_node_from_uri(uri) + filenode = self.parent.create_node_from_uri(uri, name=filename) + if not IFileNode.providedBy(filenode): + raise AssertionError("The URI does not reference a file.") c = FileWritingConsumer(filename) d = filenode.read(c) d.addCallback(lambda res: filename) @@ -199,6 +201,8 @@ class SpeedTest: if i >= self.count: return n = self.parent.create_node_from_uri(self.uris[i]) + if not IFileNode.providedBy(n): + raise AssertionError("The URI does not reference a file.") if n.is_mutable(): d1 = n.download_best_version() else: diff --git a/src/allmydata/dirnode.py b/src/allmydata/dirnode.py index 2951d5a9..506771af 100644 --- a/src/allmydata/dirnode.py +++ b/src/allmydata/dirnode.py @@ -5,13 +5,13 @@ from zope.interface import implements from twisted.internet import defer from foolscap.api import fireEventually import simplejson -from allmydata.mutable.common import NotMutableError +from allmydata.mutable.common import NotWriteableError from allmydata.mutable.filenode import MutableFileNode -from allmydata.unknown import UnknownNode +from allmydata.unknown import UnknownNode, strip_prefix_for_ro from allmydata.interfaces import IFilesystemNode, IDirectoryNode, IFileNode, \ IImmutableFileNode, IMutableFileNode, \ ExistingChildError, NoSuchChildError, ICheckable, IDeepCheckable, \ - CannotPackUnknownNodeError + MustBeDeepImmutableError, CapConstraintError from allmydata.check_results import DeepCheckResults, \ DeepCheckAndRepairResults from allmydata.monitor import Monitor @@ -40,6 +40,7 @@ class Deleter: new_contents = self.node._pack_contents(children) return new_contents + class MetadataSetter: def __init__(self, node, name, metadata): self.node = node @@ -75,6 +76,11 @@ class Adder: for (name, (child, new_metadata)) in self.entries.iteritems(): precondition(isinstance(name, unicode), name) precondition(IFilesystemNode.providedBy(child), child) + + # Strictly speaking this is redundant because we would raise the + # error again in pack_children. + child.raise_error() + if name in children: if not self.overwrite: raise ExistingChildError("child '%s' already exists" % name) @@ -123,25 +129,21 @@ class Adder: new_contents = self.node._pack_contents(children) return new_contents -def _encrypt_rwcap(filenode, rwcap): - assert isinstance(rwcap, str) +def _encrypt_rw_uri(filenode, rw_uri): + assert isinstance(rw_uri, str) writekey = filenode.get_writekey() if not writekey: return "" - salt = hashutil.mutable_rwcap_salt_hash(rwcap) + salt = hashutil.mutable_rwcap_salt_hash(rw_uri) key = hashutil.mutable_rwcap_key_hash(salt, writekey) cryptor = AES(key) - crypttext = cryptor.process(rwcap) + crypttext = cryptor.process(rw_uri) mac = hashutil.hmac(key, salt + crypttext) assert len(mac) == 32 return salt + crypttext + mac # The MAC is not checked by readers in Tahoe >= 1.3.0, but we still # produce it for the sake of older readers. -class MustBeDeepImmutable(Exception): - """You tried to add a non-deep-immutable node to a deep-immutable - directory.""" - def pack_children(filenode, children, deep_immutable=False): """Take a dict that maps: children[unicode_name] = (IFileSystemNode, metadata_dict) @@ -152,7 +154,7 @@ def pack_children(filenode, children, deep_immutable=False): time. If deep_immutable is True, I will require that all my children are deeply - immutable, and will raise a MustBeDeepImmutable exception if not. + immutable, and will raise a MustBeDeepImmutableError if not. """ has_aux = isinstance(children, AuxValueDict) @@ -161,25 +163,29 @@ def pack_children(filenode, children, deep_immutable=False): assert isinstance(name, unicode) entry = None (child, metadata) = children[name] - if deep_immutable and child.is_mutable(): - # TODO: consider adding IFileSystemNode.is_deep_immutable() - raise MustBeDeepImmutable("child '%s' is mutable" % (name,)) + child.raise_error() + if deep_immutable and not child.is_allowed_in_immutable_directory(): + raise MustBeDeepImmutableError("child '%s' is not allowed in an immutable directory" % (name,), name) if has_aux: entry = children.get_aux(name) if not entry: assert IFilesystemNode.providedBy(child), (name,child) assert isinstance(metadata, dict) - rwcap = child.get_uri() # might be RO if the child is not writeable - if rwcap is None: - rwcap = "" - assert isinstance(rwcap, str), rwcap - rocap = child.get_readonly_uri() - if rocap is None: - rocap = "" - assert isinstance(rocap, str), rocap + rw_uri = child.get_write_uri() + if rw_uri is None: + rw_uri = "" + assert isinstance(rw_uri, str), rw_uri + + # should be prevented by MustBeDeepImmutableError check above + assert not (rw_uri and deep_immutable) + + ro_uri = child.get_readonly_uri() + if ro_uri is None: + ro_uri = "" + assert isinstance(ro_uri, str), ro_uri entry = "".join([netstring(name.encode("utf-8")), - netstring(rocap), - netstring(_encrypt_rwcap(filenode, rwcap)), + netstring(strip_prefix_for_ro(ro_uri, deep_immutable)), + netstring(_encrypt_rw_uri(filenode, rw_uri)), netstring(simplejson.dumps(metadata))]) entries.append(netstring(entry)) return "".join(entries) @@ -230,38 +236,64 @@ class DirectoryNode: plaintext = cryptor.process(crypttext) return plaintext - def _create_node(self, rwcap, rocap): - return self._nodemaker.create_from_cap(rwcap, rocap) + def _create_and_validate_node(self, rw_uri, ro_uri, name): + node = self._nodemaker.create_from_cap(rw_uri, ro_uri, + deep_immutable=not self.is_mutable(), + name=name) + node.raise_error() + return node 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 'name' is UTF-8 encoded. The rwcap is formatted as: - # pack("16ss32s", iv, AES(H(writekey+iv), plaintextrwcap), mac) + # Each child is serialized as a list of four netstrings: (name, ro_uri, + # rwcapdata, metadata), in which the name, ro_uri, metadata are in + # cleartext. The 'name' is UTF-8 encoded. The rwcapdata is formatted as: + # pack("16ss32s", iv, AES(H(writekey+iv), plaintext_rw_uri), mac) assert isinstance(data, str), (repr(data), type(data)) # an empty directory is serialized as an empty string if data == "": return AuxValueDict() writeable = not self.is_readonly() + mutable = self.is_mutable() children = AuxValueDict() position = 0 while position < len(data): entries, position = split_netstring(data, 1, position) entry = entries[0] - (name, rocap, rwcapdata, metadata_s), subpos = split_netstring(entry, 4) + (name, ro_uri, rwcapdata, metadata_s), subpos = split_netstring(entry, 4) + if not mutable and len(rwcapdata) > 0: + raise ValueError("the rwcapdata field of a dirnode in an immutable directory was not empty") name = name.decode("utf-8") - rwcap = None + rw_uri = "" if writeable: - rwcap = self._decrypt_rwcapdata(rwcapdata) - if not rwcap: - rwcap = None # rwcap is None or a non-empty string - if not rocap: - rocap = None # rocap is None or a non-empty string - child = self._create_node(rwcap, rocap) - metadata = simplejson.loads(metadata_s) - assert isinstance(metadata, dict) - children.set_with_aux(name, (child, metadata), auxilliary=entry) + rw_uri = self._decrypt_rwcapdata(rwcapdata) + + # Since the encryption uses CTR mode, it currently leaks the length of the + # plaintext rw_uri -- and therefore whether it is present, i.e. whether the + # dirnode is writeable (ticket #925). By stripping spaces in Tahoe >= 1.6.0, + # we may make it easier for future versions to plug this leak. + # ro_uri is treated in the same way for consistency. + # rw_uri and ro_uri will be either None or a non-empty string. + + rw_uri = rw_uri.strip(' ') or None + ro_uri = ro_uri.strip(' ') or None + + try: + child = self._create_and_validate_node(rw_uri, ro_uri, name) + if mutable or child.is_allowed_in_immutable_directory(): + metadata = simplejson.loads(metadata_s) + assert isinstance(metadata, dict) + children[name] = (child, metadata) + children.set_with_aux(name, (child, metadata), auxilliary=entry) + else: + log.msg(format="mutable cap for child '%(name)s' unpacked from an immutable directory", + name=name.encode("utf-8"), + facility="tahoe.webish", level=log.UNUSUAL) + except CapConstraintError, e: + log.msg(format="unmet constraint on cap for child '%(name)s' unpacked from a directory:\n" + "%(message)s", message=e.args[0], name=name.encode("utf-8"), + facility="tahoe.webish", level=log.UNUSUAL) + return children def _pack_contents(self, children): @@ -270,21 +302,39 @@ class DirectoryNode: def is_readonly(self): return self._node.is_readonly() + def is_mutable(self): return self._node.is_mutable() + def is_unknown(self): + return False + + def is_allowed_in_immutable_directory(self): + return not self._node.is_mutable() + + def raise_error(self): + pass + def get_uri(self): return self._uri.to_string() + def get_write_uri(self): + if self.is_readonly(): + return None + return self._uri.to_string() + def get_readonly_uri(self): return self._uri.get_readonly().to_string() def get_cap(self): return self._uri + def get_readcap(self): return self._uri.get_readonly() + def get_verify_cap(self): return self._uri.get_verify_cap() + def get_repair_cap(self): if self._node.is_readonly(): return None # readonly (mutable) dirnodes are not yet repairable @@ -350,7 +400,7 @@ class DirectoryNode: def set_metadata_for(self, name, metadata): assert isinstance(name, unicode) if self.is_readonly(): - return defer.fail(NotMutableError()) + return defer.fail(NotWriteableError()) assert isinstance(metadata, dict) s = MetadataSetter(self, name, metadata) d = self._node.modify(s.modify) @@ -398,14 +448,10 @@ class DirectoryNode: precondition(isinstance(name, unicode), name) precondition(isinstance(writecap, (str,type(None))), writecap) precondition(isinstance(readcap, (str,type(None))), readcap) - child_node = self._create_node(writecap, readcap) - if isinstance(child_node, UnknownNode): - # don't be willing to pack unknown nodes: we might accidentally - # put some write-authority into the rocap slot because we don't - # know how to diminish the URI they gave us. We don't even know - # if they gave us a readcap or a writecap. - msg = "cannot pack unknown node as child %s" % str(name) - raise CannotPackUnknownNodeError(msg) + + # We now allow packing unknown nodes, provided they are valid + # for this type of directory. + child_node = self._create_and_validate_node(writecap, readcap, name) d = self.set_node(name, child_node, metadata, overwrite) d.addCallback(lambda res: child_node) return d @@ -423,10 +469,10 @@ class DirectoryNode: writecap, readcap, metadata = e precondition(isinstance(writecap, (str,type(None))), writecap) precondition(isinstance(readcap, (str,type(None))), readcap) - child_node = self._create_node(writecap, readcap) - if isinstance(child_node, UnknownNode): - msg = "cannot pack unknown node as child %s" % str(name) - raise CannotPackUnknownNodeError(msg) + + # We now allow packing unknown nodes, provided they are valid + # for this type of directory. + child_node = self._create_and_validate_node(writecap, readcap, name) a.set_node(name, child_node, metadata) d = self._node.modify(a.modify) d.addCallback(lambda ign: self) @@ -439,12 +485,12 @@ class DirectoryNode: same name. If this directory node is read-only, the Deferred will errback with a - NotMutableError.""" + NotWriteableError.""" precondition(IFilesystemNode.providedBy(child), child) if self.is_readonly(): - return defer.fail(NotMutableError()) + return defer.fail(NotWriteableError()) assert isinstance(name, unicode) assert IFilesystemNode.providedBy(child), child a = Adder(self, overwrite=overwrite) @@ -456,7 +502,7 @@ class DirectoryNode: def set_nodes(self, entries, overwrite=True): precondition(isinstance(entries, dict), entries) if self.is_readonly(): - return defer.fail(NotMutableError()) + return defer.fail(NotWriteableError()) a = Adder(self, entries, overwrite=overwrite) d = self._node.modify(a.modify) d.addCallback(lambda res: self) @@ -470,10 +516,10 @@ class DirectoryNode: the operation completes.""" assert isinstance(name, unicode) if self.is_readonly(): - return defer.fail(NotMutableError()) + return defer.fail(NotWriteableError()) d = self._uploader.upload(uploadable) - d.addCallback(lambda results: results.uri) - d.addCallback(self._nodemaker.create_from_cap) + d.addCallback(lambda results: + self._create_and_validate_node(results.uri, None, name)) d.addCallback(lambda node: self.set_node(name, node, metadata, overwrite)) return d @@ -483,7 +529,7 @@ class DirectoryNode: fires (with the node just removed) when the operation finishes.""" assert isinstance(name, unicode) if self.is_readonly(): - return defer.fail(NotMutableError()) + return defer.fail(NotWriteableError()) deleter = Deleter(self, name) d = self._node.modify(deleter.modify) d.addCallback(lambda res: deleter.old_child) @@ -493,7 +539,7 @@ class DirectoryNode: mutable=True): assert isinstance(name, unicode) if self.is_readonly(): - return defer.fail(NotMutableError()) + return defer.fail(NotWriteableError()) if mutable: d = self._nodemaker.create_new_mutable_directory(initial_children) else: @@ -515,7 +561,7 @@ class DirectoryNode: Deferred that fires when the operation finishes.""" assert isinstance(current_child_name, unicode) if self.is_readonly() or new_parent.is_readonly(): - return defer.fail(NotMutableError()) + return defer.fail(NotWriteableError()) if new_child_name is None: new_child_name = current_child_name assert isinstance(new_child_name, unicode) diff --git a/src/allmydata/immutable/filenode.py b/src/allmydata/immutable/filenode.py index 196d4e03..0ac2cf7e 100644 --- a/src/allmydata/immutable/filenode.py +++ b/src/allmydata/immutable/filenode.py @@ -17,6 +17,9 @@ from allmydata.immutable import download class _ImmutableFileNodeBase(object): implements(IImmutableFileNode, ICheckable) + def get_write_uri(self): + return None + def get_readonly_uri(self): return self.get_uri() @@ -26,6 +29,15 @@ class _ImmutableFileNodeBase(object): def is_readonly(self): return True + def is_unknown(self): + return False + + def is_allowed_in_immutable_directory(self): + return True + + def raise_error(self): + pass + def __hash__(self): return self.u.__hash__() def __eq__(self, other): diff --git a/src/allmydata/interfaces.py b/src/allmydata/interfaces.py index 7928b433..b00a7f8f 100644 --- a/src/allmydata/interfaces.py +++ b/src/allmydata/interfaces.py @@ -456,7 +456,6 @@ class IVerifierURI(Interface, IURI): class IDirnodeURI(Interface): """I am a URI which represents a dirnode.""" - class IFileURI(Interface): """I am a URI which represents a filenode.""" def get_size(): @@ -467,21 +466,28 @@ class IImmutableFileURI(IFileURI): class IMutableFileURI(Interface): """I am a URI which represents a mutable filenode.""" + class IDirectoryURI(Interface): pass + class IReadonlyDirectoryURI(Interface): pass -class CannotPackUnknownNodeError(Exception): - """UnknownNodes (using filecaps from the future that we don't understand) - cannot yet be copied safely, so I refuse to copy them.""" +class CapConstraintError(Exception): + """A constraint on a cap was violated.""" -class UnhandledCapTypeError(Exception): - """I recognize the cap/URI, but I cannot create an IFilesystemNode for - it.""" +class MustBeDeepImmutableError(CapConstraintError): + """Mutable children cannot be added to an immutable directory. + Also, caps obtained from an immutable directory can trigger this error + if they are later found to refer to a mutable object and then used.""" -class NotDeepImmutableError(Exception): - """Deep-immutable directories can only contain deep-immutable children""" +class MustBeReadonlyError(CapConstraintError): + """Known write caps cannot be specified in a ro_uri field. Also, + caps obtained from a ro_uri field can trigger this error if they + are later found to be write caps and then used.""" + +class MustNotBeUnknownRWError(CapConstraintError): + """Cannot add an unknown child cap specified in a rw_uri field.""" # The hierarchy looks like this: # IFilesystemNode @@ -518,9 +524,8 @@ class IFilesystemNode(Interface): """ def get_uri(): - """ - Return the URI string that can be used by others to get access to - this node. If this node is read-only, the URI will only offer + """Return the URI string corresponding to the strongest cap associated + with this 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 access. @@ -528,6 +533,11 @@ class IFilesystemNode(Interface): read-only access with others, use get_readonly_uri(). """ + def get_write_uri(n): + """Return the URI string that can be used by others to get write + access to this node, if it is writeable. If this is a read-only node, + return None.""" + def get_readonly_uri(): """Return the URI string that can be used by others to get read-only access to this node. The result is a read-only URI, regardless of @@ -557,6 +567,18 @@ class IFilesystemNode(Interface): file. """ + def is_unknown(): + """Return True if this is an unknown node.""" + + def is_allowed_in_immutable_directory(): + """Return True if this node is allowed as a child of a deep-immutable + directory. This is true if either the node is of a known-immutable type, + or it is unknown and read-only. + """ + + def raise_error(): + """Raise any error associated with this node.""" + def get_size(): """Return the length (in bytes) of the data this node represents. For directory nodes, I return the size of the backing store. I return @@ -902,7 +924,7 @@ class IDirectoryNode(IFilesystemNode): ctime/mtime semantics of traditional filesystems. If this directory node is read-only, the Deferred will errback with a - NotMutableError.""" + NotWriteableError.""" def set_children(entries, overwrite=True): """Add multiple children (by writecap+readcap) to a directory node. @@ -928,7 +950,7 @@ class IDirectoryNode(IFilesystemNode): ctime/mtime semantics of traditional filesystems. If this directory node is read-only, the Deferred will errback with a - NotMutableError.""" + NotWriteableError.""" def set_nodes(entries, overwrite=True): """Add multiple children to a directory node. Takes a dict mapping @@ -2074,7 +2096,7 @@ class INodeMaker(Interface): Tahoe process will typically have a single NodeMaker, but unit tests may create simplified/mocked forms for testing purposes. """ - def create_from_cap(writecap, readcap=None): + def create_from_cap(writecap, readcap=None, **kwargs): """I create an IFilesystemNode from the given writecap/readcap. I can only provide nodes for existing file/directory objects: use my other methods to create new objects. I return synchronously.""" diff --git a/src/allmydata/mutable/common.py b/src/allmydata/mutable/common.py index fc217dfd..e154b9df 100644 --- a/src/allmydata/mutable/common.py +++ b/src/allmydata/mutable/common.py @@ -8,7 +8,7 @@ MODE_WRITE = "MODE_WRITE" # replace all shares, probably.. not for initial # creation MODE_READ = "MODE_READ" -class NotMutableError(Exception): +class NotWriteableError(Exception): pass class NeedMoreDataError(Exception): diff --git a/src/allmydata/mutable/filenode.py b/src/allmydata/mutable/filenode.py index 5e66ad36..be5ca5c2 100644 --- a/src/allmydata/mutable/filenode.py +++ b/src/allmydata/mutable/filenode.py @@ -214,6 +214,12 @@ class MutableFileNode: def get_uri(self): return self._uri.to_string() + + def get_write_uri(self): + if self.is_readonly(): + return None + return self._uri.to_string() + def get_readonly_uri(self): return self._uri.get_readonly().to_string() @@ -227,9 +233,19 @@ class MutableFileNode: def is_mutable(self): return self._uri.is_mutable() + def is_readonly(self): return self._uri.is_readonly() + def is_unknown(self): + return False + + def is_allowed_in_immutable_directory(self): + return not self._uri.is_mutable() + + def raise_error(self): + pass + def __hash__(self): return hash((self.__class__, self._uri)) def __cmp__(self, them): diff --git a/src/allmydata/nodemaker.py b/src/allmydata/nodemaker.py index 495070f9..8cc5052c 100644 --- a/src/allmydata/nodemaker.py +++ b/src/allmydata/nodemaker.py @@ -1,7 +1,7 @@ import weakref from zope.interface import implements from allmydata.util.assertutil import precondition -from allmydata.interfaces import INodeMaker, NotDeepImmutableError +from allmydata.interfaces import INodeMaker, MustBeDeepImmutableError from allmydata.immutable.filenode import ImmutableFileNode, LiteralFileNode from allmydata.immutable.upload import Data from allmydata.mutable.filenode import MutableFileNode @@ -44,28 +44,33 @@ class NodeMaker: def _create_dirnode(self, filenode): return DirectoryNode(filenode, self, self.uploader) - def create_from_cap(self, writecap, readcap=None): + def create_from_cap(self, writecap, readcap=None, deep_immutable=False, name=u""): # this returns synchronously. It starts with a "cap string". assert isinstance(writecap, (str, type(None))), type(writecap) assert isinstance(readcap, (str, type(None))), type(readcap) + bigcap = writecap or readcap if not bigcap: # maybe the writecap was hidden because we're in a readonly # directory, and the future cap format doesn't have a readcap, or # something. - return UnknownNode(writecap, readcap) - if bigcap in self._node_cache: - return self._node_cache[bigcap] - cap = uri.from_string(bigcap) - node = self._create_from_cap(cap) + return UnknownNode(None, None) # deep_immutable and name not needed + + # The name doesn't matter for caching since it's only used in the error + # attribute of an UnknownNode, and we don't cache those. + memokey = ("I" if deep_immutable else "M") + bigcap + if memokey in self._node_cache: + return self._node_cache[memokey] + cap = uri.from_string(bigcap, deep_immutable=deep_immutable, name=name) + node = self._create_from_single_cap(cap) if node: - self._node_cache[bigcap] = node # note: WeakValueDictionary + self._node_cache[memokey] = node # note: WeakValueDictionary else: - node = UnknownNode(writecap, readcap) # don't cache UnknownNode + # don't cache UnknownNode + node = UnknownNode(writecap, readcap, deep_immutable=deep_immutable, name=name) return node - def _create_from_cap(self, cap): - # This starts with a "cap instance" + def _create_from_single_cap(self, cap): if isinstance(cap, uri.LiteralFileURI): return self._create_lit(cap) if isinstance(cap, uri.CHKFileURI): @@ -76,7 +81,7 @@ class NodeMaker: uri.ReadonlyDirectoryURI, uri.ImmutableDirectoryURI, uri.LiteralDirectoryURI)): - filenode = self._create_from_cap(cap.get_filenode_cap()) + filenode = self._create_from_single_cap(cap.get_filenode_cap()) return self._create_dirnode(filenode) return None @@ -89,13 +94,11 @@ class NodeMaker: return d def create_new_mutable_directory(self, initial_children={}): - # initial_children must have metadata (i.e. {} instead of None), and - # should not contain UnknownNodes + # initial_children must have metadata (i.e. {} instead of None) for (name, (node, metadata)) in initial_children.iteritems(): - precondition(not isinstance(node, UnknownNode), - "create_new_mutable_directory does not accept UnknownNode", node) precondition(isinstance(metadata, dict), "create_new_mutable_directory requires metadata to be a dict, not None", metadata) + node.raise_error() d = self.create_mutable_file(lambda n: pack_children(n, initial_children)) d.addCallback(self._create_dirnode) @@ -105,19 +108,15 @@ class NodeMaker: if convergence is None: convergence = self.secret_holder.get_convergence_secret() for (name, (node, metadata)) in children.iteritems(): - precondition(not isinstance(node, UnknownNode), - "create_immutable_directory does not accept UnknownNode", node) precondition(isinstance(metadata, dict), "create_immutable_directory requires metadata to be a dict, not None", metadata) - if node.is_mutable(): - raise NotDeepImmutableError("%s is not immutable" % (node,)) + node.raise_error() + if not node.is_allowed_in_immutable_directory(): + raise MustBeDeepImmutableError("%s is not immutable" % (node,), name) n = DummyImmutableFileNode() # writekey=None packed = pack_children(n, children) uploadable = Data(packed, convergence) d = self.uploader.upload(uploadable, history=self.history) - def _uploaded(results): - filecap = self.create_from_cap(results.uri) - return filecap - d.addCallback(_uploaded) + d.addCallback(lambda results: self.create_from_cap(None, results.uri)) d.addCallback(self._create_dirnode) return d diff --git a/src/allmydata/scripts/common.py b/src/allmydata/scripts/common.py index de30bc60..24f9922e 100644 --- a/src/allmydata/scripts/common.py +++ b/src/allmydata/scripts/common.py @@ -128,12 +128,14 @@ class UnknownAliasError(Exception): pass def get_alias(aliases, path, default): + from allmydata import uri # transform "work:path/filename" into (aliases["work"], "path/filename"). # If default=None, then an empty alias is indicated by returning - # DefaultAliasMarker. We special-case "URI:" to make it easy to access - # specific files/directories by their read-cap. + # DefaultAliasMarker. We special-case strings with a recognized cap URI + # prefix, to make it easy to access specific files/directories by their + # caps. path = path.strip() - if path.startswith("URI:"): + if uri.has_uri_prefix(path): # The only way to get a sub-path is to use URI:blah:./foo, and we # strip out the :./ sequence. sep = path.find(":./") diff --git a/src/allmydata/scripts/tahoe_cp.py b/src/allmydata/scripts/tahoe_cp.py index 02052641..0e55d6eb 100644 --- a/src/allmydata/scripts/tahoe_cp.py +++ b/src/allmydata/scripts/tahoe_cp.py @@ -258,8 +258,7 @@ class TahoeDirectorySource: readcap = ascii_or_none(data[1].get("ro_uri")) self.children[name] = TahoeFileSource(self.nodeurl, mutable, writecap, readcap) - else: - assert data[0] == "dirnode" + elif data[0] == "dirnode": writecap = ascii_or_none(data[1].get("rw_uri")) readcap = ascii_or_none(data[1].get("ro_uri")) if writecap and writecap in self.cache: @@ -277,6 +276,11 @@ class TahoeDirectorySource: if recurse: child.populate(True) self.children[name] = child + else: + # TODO: there should be an option to skip unknown nodes. + raise TahoeError("Cannot copy unknown nodes (ticket #839). " + "You probably need to use a later version of " + "Tahoe-LAFS to copy this directory.") class TahoeMissingTarget: def __init__(self, url): @@ -353,8 +357,7 @@ class TahoeDirectoryTarget: urllib.quote(name.encode('utf-8'))]) self.children[name] = TahoeFileTarget(self.nodeurl, mutable, writecap, readcap, url) - else: - assert data[0] == "dirnode" + elif data[0] == "dirnode": writecap = ascii_or_none(data[1].get("rw_uri")) readcap = ascii_or_none(data[1].get("ro_uri")) if writecap and writecap in self.cache: @@ -372,6 +375,11 @@ class TahoeDirectoryTarget: if recurse: child.populate(True) self.children[name] = child + else: + # TODO: there should be an option to skip unknown nodes. + raise TahoeError("Cannot copy unknown nodes (ticket #839). " + "You probably need to use a later version of " + "Tahoe-LAFS to copy this directory.") def get_child_target(self, name): # return a new target for a named subdirectory of this dir @@ -407,9 +415,11 @@ class TahoeDirectoryTarget: set_data = {} for (name, filecap) in self.new_children.items(): # it just so happens that ?t=set_children will accept both file - # read-caps and write-caps as ['rw_uri'], and will handle eithe + # read-caps and write-caps as ['rw_uri'], and will handle either # correctly. So don't bother trying to figure out whether the one # we have is read-only or read-write. + # TODO: think about how this affects forward-compatibility for + # unknown caps set_data[name] = ["filenode", {"rw_uri": filecap}] body = simplejson.dumps(set_data) POST(url, body) @@ -770,6 +780,7 @@ def copy(options): # local-file-in-the-way # touch proposed # tahoe cp -r my:docs/proposed/denver.txt proposed/denver.txt +# handling of unknown nodes # things that maybe should be errors but aren't # local-dir-in-the-way diff --git a/src/allmydata/scripts/tahoe_put.py b/src/allmydata/scripts/tahoe_put.py index 226bf49f..13a8306c 100644 --- a/src/allmydata/scripts/tahoe_put.py +++ b/src/allmydata/scripts/tahoe_put.py @@ -40,6 +40,7 @@ def put(options): # DIRCAP:./subdir/foo : DIRCAP/subdir/foo # MUTABLE-FILE-WRITECAP : filecap + # FIXME: this shouldn't rely on a particular prefix. if to_file.startswith("URI:SSK:"): url = nodeurl + "uri/%s" % urllib.quote(to_file) else: diff --git a/src/allmydata/test/common.py b/src/allmydata/test/common.py index 538f6627..353ee3fc 100644 --- a/src/allmydata/test/common.py +++ b/src/allmydata/test/common.py @@ -51,6 +51,8 @@ class FakeCHKFileNode: def get_uri(self): return self.my_uri.to_string() + def get_write_uri(self): + return None def get_readonly_uri(self): return self.my_uri.to_string() def get_cap(self): @@ -103,6 +105,12 @@ class FakeCHKFileNode: return False def is_readonly(self): return True + def is_unknown(self): + return False + def is_allowed_in_immutable_directory(self): + return True + def raise_error(self): + pass def get_size(self): try: @@ -190,6 +198,10 @@ class FakeMutableFileNode: return self.my_uri.get_readonly() def get_uri(self): return self.my_uri.to_string() + def get_write_uri(self): + if self.is_readonly(): + return None + return self.my_uri.to_string() def get_readonly(self): return self.my_uri.get_readonly() def get_readonly_uri(self): @@ -200,6 +212,12 @@ class FakeMutableFileNode: return self.my_uri.is_readonly() def is_mutable(self): return self.my_uri.is_mutable() + def is_unknown(self): + return False + def is_allowed_in_immutable_directory(self): + return not self.my_uri.is_mutable() + def raise_error(self): + pass def get_writekey(self): return "\x00"*16 def get_size(self): diff --git a/src/allmydata/test/test_client.py b/src/allmydata/test/test_client.py index beb6abe8..61f1e53f 100644 --- a/src/allmydata/test/test_client.py +++ b/src/allmydata/test/test_client.py @@ -288,11 +288,15 @@ class NodeMaker(unittest.TestCase): self.failUnless(n.is_readonly()) self.failUnless(n.is_mutable()) - future = "x-tahoe-crazy://future_cap_format." - n = c.create_node_from_uri(future) + unknown_rw = "lafs://from_the_future" + unknown_ro = "lafs://readonly_from_the_future" + n = c.create_node_from_uri(unknown_rw, unknown_ro) self.failUnless(IFilesystemNode.providedBy(n)) self.failIf(IFileNode.providedBy(n)) self.failIf(IImmutableFileNode.providedBy(n)) self.failIf(IMutableFileNode.providedBy(n)) self.failIf(IDirectoryNode.providedBy(n)) - self.failUnlessEqual(n.get_uri(), future) + self.failUnless(n.is_unknown()) + self.failUnlessEqual(n.get_uri(), unknown_rw) + self.failUnlessEqual(n.get_write_uri(), unknown_rw) + self.failUnlessEqual(n.get_readonly_uri(), "ro." + unknown_ro) diff --git a/src/allmydata/test/test_dirnode.py b/src/allmydata/test/test_dirnode.py index a07d298e..1acfdb5f 100644 --- a/src/allmydata/test/test_dirnode.py +++ b/src/allmydata/test/test_dirnode.py @@ -7,8 +7,9 @@ from allmydata import uri, dirnode from allmydata.client import Client from allmydata.immutable import upload from allmydata.interfaces import IImmutableFileNode, IMutableFileNode, \ - ExistingChildError, NoSuchChildError, NotDeepImmutableError, \ - IDeepCheckResults, IDeepCheckAndRepairResults, CannotPackUnknownNodeError + ExistingChildError, NoSuchChildError, MustNotBeUnknownRWError, \ + MustBeDeepImmutableError, MustBeReadonlyError, \ + IDeepCheckResults, IDeepCheckAndRepairResults from allmydata.mutable.filenode import MutableFileNode from allmydata.mutable.common import UncoordinatedWriteError from allmydata.util import hashutil, base32 @@ -16,7 +17,7 @@ from allmydata.monitor import Monitor from allmydata.test.common import make_chk_file_uri, make_mutable_file_uri, \ ErrorMixin from allmydata.test.no_network import GridTestMixin -from allmydata.unknown import UnknownNode +from allmydata.unknown import UnknownNode, strip_prefix_for_ro from allmydata.nodemaker import NodeMaker from base64 import b32decode import common_util as testutil @@ -32,6 +33,11 @@ class Dirnode(GridTestMixin, unittest.TestCase, d = c.create_dirnode() def _done(res): self.failUnless(isinstance(res, dirnode.DirectoryNode)) + self.failUnless(res.is_mutable()) + self.failIf(res.is_readonly()) + self.failIf(res.is_unknown()) + self.failIf(res.is_allowed_in_immutable_directory()) + res.raise_error() rep = str(res) self.failUnless("RW-MUT" in rep) d.addCallback(_done) @@ -44,36 +50,74 @@ class Dirnode(GridTestMixin, unittest.TestCase, nm = c.nodemaker setup_py_uri = "URI:CHK:n7r3m6wmomelk4sep3kw5cvduq:os7ijw5c3maek7pg65e5254k2fzjflavtpejjyhshpsxuqzhcwwq:3:20:14861" one_uri = "URI:LIT:n5xgk" # LIT for "one" + mut_write_uri = "URI:SSK:vfvcbdfbszyrsaxchgevhmmlii:euw4iw7bbnkrrwpzuburbhppuxhc3gwxv26f6imekhz7zyw2ojnq" + mut_read_uri = "URI:SSK-RO:jf6wkflosyvntwxqcdo7a54jvm:euw4iw7bbnkrrwpzuburbhppuxhc3gwxv26f6imekhz7zyw2ojnq" + future_write_uri = "x-tahoe-crazy://I_am_from_the_future." + future_read_uri = "x-tahoe-crazy-readonly://I_am_from_the_future." kids = {u"one": (nm.create_from_cap(one_uri), {}), u"two": (nm.create_from_cap(setup_py_uri), {"metakey": "metavalue"}), + u"mut": (nm.create_from_cap(mut_write_uri, mut_read_uri), {}), + u"fut": (nm.create_from_cap(future_write_uri, future_read_uri), {}), + u"fro": (nm.create_from_cap(None, future_read_uri), {}), } d = c.create_dirnode(kids) + def _created(dn): self.failUnless(isinstance(dn, dirnode.DirectoryNode)) + self.failUnless(dn.is_mutable()) + self.failIf(dn.is_readonly()) + self.failIf(dn.is_unknown()) + self.failIf(dn.is_allowed_in_immutable_directory()) + dn.raise_error() rep = str(dn) self.failUnless("RW-MUT" in rep) return dn.list() d.addCallback(_created) + def _check_kids(children): - self.failUnlessEqual(sorted(children.keys()), [u"one", u"two"]) + self.failUnlessEqual(sorted(children.keys()), + [u"fro", u"fut", u"mut", u"one", u"two"]) one_node, one_metadata = children[u"one"] two_node, two_metadata = children[u"two"] + mut_node, mut_metadata = children[u"mut"] + fut_node, fut_metadata = children[u"fut"] + fro_node, fro_metadata = children[u"fro"] + self.failUnlessEqual(one_node.get_size(), 3) - self.failUnlessEqual(two_node.get_size(), 14861) + self.failUnlessEqual(one_node.get_uri(), one_uri) + self.failUnlessEqual(one_node.get_readonly_uri(), one_uri) self.failUnless(isinstance(one_metadata, dict), one_metadata) + + self.failUnlessEqual(two_node.get_size(), 14861) + self.failUnlessEqual(two_node.get_uri(), setup_py_uri) + self.failUnlessEqual(two_node.get_readonly_uri(), setup_py_uri) self.failUnlessEqual(two_metadata["metakey"], "metavalue") + + self.failUnlessEqual(mut_node.get_uri(), mut_write_uri) + self.failUnlessEqual(mut_node.get_readonly_uri(), mut_read_uri) + self.failUnless(isinstance(mut_metadata, dict), mut_metadata) + + self.failUnless(fut_node.is_unknown()) + self.failUnlessEqual(fut_node.get_uri(), future_write_uri) + self.failUnlessEqual(fut_node.get_readonly_uri(), "ro." + future_read_uri) + self.failUnless(isinstance(fut_metadata, dict), fut_metadata) + + self.failUnless(fro_node.is_unknown()) + self.failUnlessEqual(fro_node.get_uri(), "ro." + future_read_uri) + self.failUnlessEqual(fut_node.get_readonly_uri(), "ro." + future_read_uri) + self.failUnless(isinstance(fro_metadata, dict), fro_metadata) d.addCallback(_check_kids) + d.addCallback(lambda ign: nm.create_new_mutable_directory(kids)) d.addCallback(lambda dn: dn.list()) d.addCallback(_check_kids) - future_writecap = "x-tahoe-crazy://I_am_from_the_future." - future_readcap = "x-tahoe-crazy-readonly://I_am_from_the_future." - future_node = UnknownNode(future_writecap, future_readcap) - bad_kids1 = {u"one": (future_node, {})} + + bad_future_node = UnknownNode(future_write_uri, None) + bad_kids1 = {u"one": (bad_future_node, {})} d.addCallback(lambda ign: - self.shouldFail(AssertionError, "bad_kids1", - "does not accept UnknownNode", + self.shouldFail(MustNotBeUnknownRWError, "bad_kids1", + "cannot attach unknown", nm.create_new_mutable_directory, bad_kids1)) bad_kids2 = {u"one": (nm.create_from_cap(one_uri), None)} @@ -91,17 +135,24 @@ class Dirnode(GridTestMixin, unittest.TestCase, nm = c.nodemaker setup_py_uri = "URI:CHK:n7r3m6wmomelk4sep3kw5cvduq:os7ijw5c3maek7pg65e5254k2fzjflavtpejjyhshpsxuqzhcwwq:3:20:14861" one_uri = "URI:LIT:n5xgk" # LIT for "one" - mut_readcap = "URI:SSK-RO:e3mdrzfwhoq42hy5ubcz6rp3o4:ybyibhnp3vvwuq2vaw2ckjmesgkklfs6ghxleztqidihjyofgw7q" - mut_writecap = "URI:SSK:vfvcbdfbszyrsaxchgevhmmlii:euw4iw7bbnkrrwpzuburbhppuxhc3gwxv26f6imekhz7zyw2ojnq" + mut_write_uri = "URI:SSK:vfvcbdfbszyrsaxchgevhmmlii:euw4iw7bbnkrrwpzuburbhppuxhc3gwxv26f6imekhz7zyw2ojnq" + mut_read_uri = "URI:SSK-RO:e3mdrzfwhoq42hy5ubcz6rp3o4:ybyibhnp3vvwuq2vaw2ckjmesgkklfs6ghxleztqidihjyofgw7q" + future_write_uri = "x-tahoe-crazy://I_am_from_the_future." + future_read_uri = "x-tahoe-crazy-readonly://I_am_from_the_future." kids = {u"one": (nm.create_from_cap(one_uri), {}), u"two": (nm.create_from_cap(setup_py_uri), {"metakey": "metavalue"}), + u"fut": (nm.create_from_cap(None, future_read_uri), {}), } d = c.create_immutable_dirnode(kids) + def _created(dn): self.failUnless(isinstance(dn, dirnode.DirectoryNode)) self.failIf(dn.is_mutable()) self.failUnless(dn.is_readonly()) + self.failIf(dn.is_unknown()) + self.failUnless(dn.is_allowed_in_immutable_directory()) + dn.raise_error() rep = str(dn) self.failUnless("RO-IMM" in rep) cap = dn.get_cap() @@ -109,50 +160,73 @@ class Dirnode(GridTestMixin, unittest.TestCase, self.cap = cap return dn.list() d.addCallback(_created) + def _check_kids(children): - self.failUnlessEqual(sorted(children.keys()), [u"one", u"two"]) + self.failUnlessEqual(sorted(children.keys()), [u"fut", u"one", u"two"]) one_node, one_metadata = children[u"one"] two_node, two_metadata = children[u"two"] + fut_node, fut_metadata = children[u"fut"] + self.failUnlessEqual(one_node.get_size(), 3) - self.failUnlessEqual(two_node.get_size(), 14861) + self.failUnlessEqual(one_node.get_uri(), one_uri) + self.failUnlessEqual(one_node.get_readonly_uri(), one_uri) self.failUnless(isinstance(one_metadata, dict), one_metadata) + + self.failUnlessEqual(two_node.get_size(), 14861) + self.failUnlessEqual(two_node.get_uri(), setup_py_uri) + self.failUnlessEqual(two_node.get_readonly_uri(), setup_py_uri) self.failUnlessEqual(two_metadata["metakey"], "metavalue") + + self.failUnless(fut_node.is_unknown()) + self.failUnlessEqual(fut_node.get_uri(), "imm." + future_read_uri) + self.failUnlessEqual(fut_node.get_readonly_uri(), "imm." + future_read_uri) + self.failUnless(isinstance(fut_metadata, dict), fut_metadata) d.addCallback(_check_kids) + d.addCallback(lambda ign: nm.create_from_cap(self.cap.to_string())) d.addCallback(lambda dn: dn.list()) d.addCallback(_check_kids) - future_writecap = "x-tahoe-crazy://I_am_from_the_future." - future_readcap = "x-tahoe-crazy-readonly://I_am_from_the_future." - future_node = UnknownNode(future_writecap, future_readcap) - bad_kids1 = {u"one": (future_node, {})} + + bad_future_node1 = UnknownNode(future_write_uri, None) + bad_kids1 = {u"one": (bad_future_node1, {})} d.addCallback(lambda ign: - self.shouldFail(AssertionError, "bad_kids1", - "does not accept UnknownNode", + self.shouldFail(MustNotBeUnknownRWError, "bad_kids1", + "cannot attach unknown", c.create_immutable_dirnode, bad_kids1)) - bad_kids2 = {u"one": (nm.create_from_cap(one_uri), None)} + bad_future_node2 = UnknownNode(future_write_uri, future_read_uri) + bad_kids2 = {u"one": (bad_future_node2, {})} d.addCallback(lambda ign: - self.shouldFail(AssertionError, "bad_kids2", - "requires metadata to be a dict", + self.shouldFail(MustBeDeepImmutableError, "bad_kids2", + "is not immutable", c.create_immutable_dirnode, bad_kids2)) - bad_kids3 = {u"one": (nm.create_from_cap(mut_writecap), {})} + bad_kids3 = {u"one": (nm.create_from_cap(one_uri), None)} d.addCallback(lambda ign: - self.shouldFail(NotDeepImmutableError, "bad_kids3", - "is not immutable", + self.shouldFail(AssertionError, "bad_kids3", + "requires metadata to be a dict", c.create_immutable_dirnode, bad_kids3)) - bad_kids4 = {u"one": (nm.create_from_cap(mut_readcap), {})} + bad_kids4 = {u"one": (nm.create_from_cap(mut_write_uri), {})} d.addCallback(lambda ign: - self.shouldFail(NotDeepImmutableError, "bad_kids4", + self.shouldFail(MustBeDeepImmutableError, "bad_kids4", "is not immutable", c.create_immutable_dirnode, bad_kids4)) + bad_kids5 = {u"one": (nm.create_from_cap(mut_read_uri), {})} + d.addCallback(lambda ign: + self.shouldFail(MustBeDeepImmutableError, "bad_kids5", + "is not immutable", + c.create_immutable_dirnode, + bad_kids5)) d.addCallback(lambda ign: c.create_immutable_dirnode({})) def _created_empty(dn): self.failUnless(isinstance(dn, dirnode.DirectoryNode)) self.failIf(dn.is_mutable()) self.failUnless(dn.is_readonly()) + self.failIf(dn.is_unknown()) + self.failUnless(dn.is_allowed_in_immutable_directory()) + dn.raise_error() rep = str(dn) self.failUnless("RO-IMM" in rep) cap = dn.get_cap() @@ -168,6 +242,9 @@ class Dirnode(GridTestMixin, unittest.TestCase, self.failUnless(isinstance(dn, dirnode.DirectoryNode)) self.failIf(dn.is_mutable()) self.failUnless(dn.is_readonly()) + self.failIf(dn.is_unknown()) + self.failUnless(dn.is_allowed_in_immutable_directory()) + dn.raise_error() rep = str(dn) self.failUnless("RO-IMM" in rep) cap = dn.get_cap() @@ -193,9 +270,9 @@ class Dirnode(GridTestMixin, unittest.TestCase, d.addCallback(_check_kids) d.addCallback(lambda ign: n.get(u"subdir")) d.addCallback(lambda sd: self.failIf(sd.is_mutable())) - bad_kids = {u"one": (nm.create_from_cap(mut_writecap), {})} + bad_kids = {u"one": (nm.create_from_cap(mut_write_uri), {})} d.addCallback(lambda ign: - self.shouldFail(NotDeepImmutableError, "YZ", + self.shouldFail(MustBeDeepImmutableError, "YZ", "is not immutable", n.create_subdirectory, u"sub2", bad_kids, mutable=False)) @@ -203,7 +280,6 @@ class Dirnode(GridTestMixin, unittest.TestCase, d.addCallback(_made_parent) return d - def test_check(self): self.basedir = "dirnode/Dirnode/test_check" self.set_up_grid() @@ -337,24 +413,27 @@ class Dirnode(GridTestMixin, unittest.TestCase, ro_dn = c.create_node_from_uri(ro_uri) self.failUnless(ro_dn.is_readonly()) self.failUnless(ro_dn.is_mutable()) + self.failIf(ro_dn.is_unknown()) + self.failIf(ro_dn.is_allowed_in_immutable_directory()) + ro_dn.raise_error() - self.shouldFail(dirnode.NotMutableError, "set_uri ro", None, + self.shouldFail(dirnode.NotWriteableError, "set_uri ro", None, ro_dn.set_uri, u"newchild", filecap, filecap) - self.shouldFail(dirnode.NotMutableError, "set_uri ro", None, + self.shouldFail(dirnode.NotWriteableError, "set_uri ro", None, ro_dn.set_node, u"newchild", filenode) - self.shouldFail(dirnode.NotMutableError, "set_nodes ro", None, + self.shouldFail(dirnode.NotWriteableError, "set_nodes ro", None, ro_dn.set_nodes, { u"newchild": (filenode, None) }) - self.shouldFail(dirnode.NotMutableError, "set_uri ro", None, + self.shouldFail(dirnode.NotWriteableError, "set_uri ro", None, ro_dn.add_file, u"newchild", uploadable) - self.shouldFail(dirnode.NotMutableError, "set_uri ro", None, + self.shouldFail(dirnode.NotWriteableError, "set_uri ro", None, ro_dn.delete, u"child") - self.shouldFail(dirnode.NotMutableError, "set_uri ro", None, + self.shouldFail(dirnode.NotWriteableError, "set_uri ro", None, ro_dn.create_subdirectory, u"newchild") - self.shouldFail(dirnode.NotMutableError, "set_metadata_for ro", None, + self.shouldFail(dirnode.NotWriteableError, "set_metadata_for ro", None, ro_dn.set_metadata_for, u"child", {}) - self.shouldFail(dirnode.NotMutableError, "set_uri ro", None, + self.shouldFail(dirnode.NotWriteableError, "set_uri ro", None, ro_dn.move_child_to, u"child", rw_dn) - self.shouldFail(dirnode.NotMutableError, "set_uri ro", None, + self.shouldFail(dirnode.NotWriteableError, "set_uri ro", None, rw_dn.move_child_to, u"child", ro_dn) return ro_dn.list() d.addCallback(_ready) @@ -901,8 +980,8 @@ class Packing(unittest.TestCase): nodemaker = NodeMaker(None, None, None, None, None, None, {"k": 3, "n": 10}, None) - writecap = "URI:SSK-RO:e3mdrzfwhoq42hy5ubcz6rp3o4:ybyibhnp3vvwuq2vaw2ckjmesgkklfs6ghxleztqidihjyofgw7q" - filenode = nodemaker.create_from_cap(writecap) + write_uri = "URI:SSK-RO:e3mdrzfwhoq42hy5ubcz6rp3o4:ybyibhnp3vvwuq2vaw2ckjmesgkklfs6ghxleztqidihjyofgw7q" + filenode = nodemaker.create_from_cap(write_uri) node = dirnode.DirectoryNode(filenode, nodemaker, None) children = node._unpack_contents(known_tree) self._check_children(children) @@ -975,23 +1054,23 @@ class Packing(unittest.TestCase): self.failUnlessIn("lit", packed) kids = self._make_kids(nm, ["imm", "lit", "write"]) - self.failUnlessRaises(dirnode.MustBeDeepImmutable, + self.failUnlessRaises(dirnode.MustBeDeepImmutableError, dirnode.pack_children, fn, kids, deep_immutable=True) # read-only is not enough: all children must be immutable kids = self._make_kids(nm, ["imm", "lit", "read"]) - self.failUnlessRaises(dirnode.MustBeDeepImmutable, + self.failUnlessRaises(dirnode.MustBeDeepImmutableError, dirnode.pack_children, fn, kids, deep_immutable=True) kids = self._make_kids(nm, ["imm", "lit", "dirwrite"]) - self.failUnlessRaises(dirnode.MustBeDeepImmutable, + self.failUnlessRaises(dirnode.MustBeDeepImmutableError, dirnode.pack_children, fn, kids, deep_immutable=True) kids = self._make_kids(nm, ["imm", "lit", "dirread"]) - self.failUnlessRaises(dirnode.MustBeDeepImmutable, + self.failUnlessRaises(dirnode.MustBeDeepImmutableError, dirnode.pack_children, fn, kids, deep_immutable=True) @@ -1017,16 +1096,31 @@ class FakeMutableFile: def get_cap(self): return self.uri + def get_uri(self): return self.uri.to_string() + + def get_write_uri(self): + return self.uri.to_string() + def download_best_version(self): return defer.succeed(self.data) + def get_writekey(self): return "writekey" + def is_readonly(self): return False + def is_mutable(self): return True + + def is_unknown(self): + return False + + def is_allowed_in_immutable_directory(self): + return False + def modify(self, modifier): self.data = modifier(self.data, None, True) return defer.succeed(None) @@ -1049,50 +1143,186 @@ class Dirnode2(unittest.TestCase, testutil.ShouldFailMixin): self.nodemaker = client.nodemaker def test_from_future(self): - # create a dirnode that contains unknown URI types, and make sure we - # tolerate them properly. Since dirnodes aren't allowed to add - # unknown node types, we have to be tricky. + # Create a mutable directory that contains unknown URI types, and make sure + # we tolerate them properly. d = self.nodemaker.create_new_mutable_directory() - future_writecap = "x-tahoe-crazy://I_am_from_the_future." - future_readcap = "x-tahoe-crazy-readonly://I_am_from_the_future." - future_node = UnknownNode(future_writecap, future_readcap) + future_write_uri = "x-tahoe-crazy://I_am_from_the_future." + future_read_uri = "x-tahoe-crazy-readonly://I_am_from_the_future." + future_imm_uri = "x-tahoe-crazy-immutable://I_am_from_the_future." + future_node = UnknownNode(future_write_uri, future_read_uri) def _then(n): self._node = n return n.set_node(u"future", future_node) d.addCallback(_then) - # we should be prohibited from adding an unknown URI to a directory, - # since we don't know how to diminish the cap to a readcap (for the - # dirnode's rocap slot), and we don't want to accidentally grant - # write access to a holder of the dirnode's readcap. + # We should be prohibited from adding an unknown URI to a directory + # just in the rw_uri slot, since we don't know how to diminish the cap + # to a readcap (for the ro_uri slot). d.addCallback(lambda ign: - self.shouldFail(CannotPackUnknownNodeError, + self.shouldFail(MustNotBeUnknownRWError, "copy unknown", - "cannot pack unknown node as child add", + "cannot attach unknown rw cap as child", self._node.set_uri, u"add", - future_writecap, future_readcap)) + future_write_uri, None)) + + # However, we should be able to add both rw_uri and ro_uri as a pair of + # unknown URIs. + d.addCallback(lambda ign: self._node.set_uri(u"add-pair", + future_write_uri, future_read_uri)) + + # and to add an URI prefixed with "ro." or "imm." when it is given in a + # write slot (or URL parameter). + d.addCallback(lambda ign: self._node.set_uri(u"add-ro", + "ro." + future_read_uri, None)) + d.addCallback(lambda ign: self._node.set_uri(u"add-imm", + "imm." + future_imm_uri, None)) + d.addCallback(lambda ign: self._node.list()) def _check(children): - self.failUnlessEqual(len(children), 1) + self.failUnlessEqual(len(children), 4) (fn, metadata) = children[u"future"] self.failUnless(isinstance(fn, UnknownNode), fn) - self.failUnlessEqual(fn.get_uri(), future_writecap) - self.failUnlessEqual(fn.get_readonly_uri(), future_readcap) - # but we *should* be allowed to copy this node, because the - # UnknownNode contains all the information that was in the - # original directory (readcap and writecap), so we're preserving - # everything. + self.failUnlessEqual(fn.get_uri(), future_write_uri) + self.failUnlessEqual(fn.get_write_uri(), future_write_uri) + self.failUnlessEqual(fn.get_readonly_uri(), "ro." + future_read_uri) + + (fn2, metadata2) = children[u"add-pair"] + self.failUnless(isinstance(fn2, UnknownNode), fn2) + self.failUnlessEqual(fn2.get_uri(), future_write_uri) + self.failUnlessEqual(fn2.get_write_uri(), future_write_uri) + self.failUnlessEqual(fn2.get_readonly_uri(), "ro." + future_read_uri) + + (fn3, metadata3) = children[u"add-ro"] + self.failUnless(isinstance(fn3, UnknownNode), fn3) + self.failUnlessEqual(fn3.get_uri(), "ro." + future_read_uri) + self.failUnlessEqual(fn3.get_write_uri(), None) + self.failUnlessEqual(fn3.get_readonly_uri(), "ro." + future_read_uri) + + (fn4, metadata4) = children[u"add-imm"] + self.failUnless(isinstance(fn4, UnknownNode), fn4) + self.failUnlessEqual(fn4.get_uri(), "imm." + future_imm_uri) + self.failUnlessEqual(fn4.get_write_uri(), None) + self.failUnlessEqual(fn4.get_readonly_uri(), "imm." + future_imm_uri) + + # We should also be allowed to copy the "future" UnknownNode, because + # it contains all the information that was in the original directory + # (readcap and writecap), so we're preserving everything. return self._node.set_node(u"copy", fn) d.addCallback(_check) + d.addCallback(lambda ign: self._node.list()) def _check2(children): - self.failUnlessEqual(len(children), 2) + self.failUnlessEqual(len(children), 5) (fn, metadata) = children[u"copy"] self.failUnless(isinstance(fn, UnknownNode), fn) - self.failUnlessEqual(fn.get_uri(), future_writecap) - self.failUnlessEqual(fn.get_readonly_uri(), future_readcap) + self.failUnlessEqual(fn.get_uri(), future_write_uri) + self.failUnlessEqual(fn.get_write_uri(), future_write_uri) + self.failUnlessEqual(fn.get_readonly_uri(), "ro." + future_read_uri) + d.addCallback(_check2) return d + def test_unknown_strip_prefix_for_ro(self): + self.failUnlessEqual(strip_prefix_for_ro("foo", False), "foo") + self.failUnlessEqual(strip_prefix_for_ro("ro.foo", False), "foo") + self.failUnlessEqual(strip_prefix_for_ro("imm.foo", False), "imm.foo") + self.failUnlessEqual(strip_prefix_for_ro("foo", True), "foo") + self.failUnlessEqual(strip_prefix_for_ro("ro.foo", True), "foo") + self.failUnlessEqual(strip_prefix_for_ro("imm.foo", True), "foo") + + def test_unknownnode(self): + mut_write_uri = "URI:SSK:vfvcbdfbszyrsaxchgevhmmlii:euw4iw7bbnkrrwpzuburbhppuxhc3gwxv26f6imekhz7zyw2ojnq" + mut_read_uri = "URI:SSK-RO:jf6wkflosyvntwxqcdo7a54jvm:euw4iw7bbnkrrwpzuburbhppuxhc3gwxv26f6imekhz7zyw2ojnq" + lit_uri = "URI:LIT:n5xgk" + + # This does not attempt to be exhaustive. + no_no = [# Opaque node, but not an error. + ( 0, UnknownNode(None, None)), + ( 1, UnknownNode(None, None, deep_immutable=True)), + ] + unknown_rw = [# These are errors because we're only given a rw_uri, and we can't + # diminish it. + ( 2, UnknownNode("foo", None)), + ( 3, UnknownNode("foo", None, deep_immutable=True)), + ( 4, UnknownNode("ro.foo", None, deep_immutable=True)), + ( 5, UnknownNode("ro." + mut_read_uri, None, deep_immutable=True)), + ( 6, UnknownNode("URI:SSK-RO:foo", None, deep_immutable=True)), + ( 7, UnknownNode("URI:SSK:foo", None)), + ] + must_be_ro = [# These are errors because a readonly constraint is not met. + ( 8, UnknownNode("ro." + mut_write_uri, None)), + ( 9, UnknownNode(None, "ro." + mut_write_uri)), + ] + must_be_imm = [# These are errors because an immutable constraint is not met. + (10, UnknownNode(None, "ro.URI:SSK-RO:foo", deep_immutable=True)), + (11, UnknownNode(None, "imm.URI:SSK:foo")), + (12, UnknownNode(None, "imm.URI:SSK-RO:foo")), + (13, UnknownNode("bar", "ro.foo", deep_immutable=True)), + (14, UnknownNode("bar", "imm.foo", deep_immutable=True)), + (15, UnknownNode("bar", "imm." + lit_uri, deep_immutable=True)), + (16, UnknownNode("imm." + mut_write_uri, None)), + (17, UnknownNode("imm." + mut_read_uri, None)), + (18, UnknownNode("bar", "imm.foo")), + ] + bad_uri = [# These are errors because the URI is bad once we've stripped the prefix. + (19, UnknownNode("ro.URI:SSK-RO:foo", None)), + (20, UnknownNode("imm.URI:CHK:foo", None, deep_immutable=True)), + ] + ro_prefixed = [# These are valid, and the readcap should end up with a ro. prefix. + (21, UnknownNode(None, "foo")), + (22, UnknownNode(None, "ro.foo")), + (32, UnknownNode(None, "ro." + lit_uri)), + (23, UnknownNode("bar", "foo")), + (24, UnknownNode("bar", "ro.foo")), + (32, UnknownNode("bar", "ro." + lit_uri)), + (25, UnknownNode("ro.foo", None)), + (30, UnknownNode("ro." + lit_uri, None)), + ] + imm_prefixed = [# These are valid, and the readcap should end up with an imm. prefix. + (26, UnknownNode(None, "foo", deep_immutable=True)), + (27, UnknownNode(None, "ro.foo", deep_immutable=True)), + (28, UnknownNode(None, "imm.foo")), + (29, UnknownNode(None, "imm.foo", deep_immutable=True)), + (31, UnknownNode("imm." + lit_uri, None)), + (31, UnknownNode("imm." + lit_uri, None, deep_immutable=True)), + (33, UnknownNode(None, "imm." + lit_uri)), + (33, UnknownNode(None, "imm." + lit_uri, deep_immutable=True)), + ] + error = unknown_rw + must_be_ro + must_be_imm + bad_uri + ok = ro_prefixed + imm_prefixed + + for (i, n) in no_no + error + ok: + self.failUnless(n.is_unknown(), i) + + for (i, n) in no_no + error: + self.failUnless(n.get_uri() is None, i) + self.failUnless(n.get_write_uri() is None, i) + self.failUnless(n.get_readonly_uri() is None, i) + + for (i, n) in no_no + ok: + n.raise_error() + + for (i, n) in unknown_rw: + self.failUnlessRaises(MustNotBeUnknownRWError, lambda: n.raise_error()) + + for (i, n) in must_be_ro: + self.failUnlessRaises(MustBeReadonlyError, lambda: n.raise_error()) + + for (i, n) in must_be_imm: + self.failUnlessRaises(MustBeDeepImmutableError, lambda: n.raise_error()) + + for (i, n) in bad_uri: + self.failUnlessRaises(uri.BadURIError, lambda: n.raise_error()) + + for (i, n) in ok: + self.failIf(n.get_readonly_uri() is None, i) + + for (i, n) in ro_prefixed: + self.failUnless(n.get_readonly_uri().startswith("ro."), i) + + for (i, n) in imm_prefixed: + self.failUnless(n.get_readonly_uri().startswith("imm."), i) + + class DeepStats(unittest.TestCase): timeout = 240 # It takes longer than 120 seconds on Francois's arm box. def test_stats(self): diff --git a/src/allmydata/test/test_filenode.py b/src/allmydata/test/test_filenode.py index a8ea9969..6106a2f8 100644 --- a/src/allmydata/test/test_filenode.py +++ b/src/allmydata/test/test_filenode.py @@ -41,14 +41,21 @@ class Node(unittest.TestCase): self.failUnlessEqual(fn1.get_readcap(), u) self.failUnlessEqual(fn1.is_readonly(), True) self.failUnlessEqual(fn1.is_mutable(), False) + self.failUnlessEqual(fn1.is_unknown(), False) + self.failUnlessEqual(fn1.is_allowed_in_immutable_directory(), True) + self.failUnlessEqual(fn1.get_write_uri(), None) self.failUnlessEqual(fn1.get_readonly_uri(), u.to_string()) self.failUnlessEqual(fn1.get_size(), 1000) self.failUnlessEqual(fn1.get_storage_index(), u.storage_index) + fn1.raise_error() + fn2.raise_error() d = {} d[fn1] = 1 # exercise __hash__ v = fn1.get_verify_cap() self.failUnless(isinstance(v, uri.CHKFileVerifierURI)) self.failUnlessEqual(fn1.get_repair_cap(), v) + self.failUnlessEqual(v.is_readonly(), True) + self.failUnlessEqual(v.is_mutable(), False) def test_literal_filenode(self): @@ -64,9 +71,14 @@ class Node(unittest.TestCase): self.failUnlessEqual(fn1.get_readcap(), u) self.failUnlessEqual(fn1.is_readonly(), True) self.failUnlessEqual(fn1.is_mutable(), False) + self.failUnlessEqual(fn1.is_unknown(), False) + self.failUnlessEqual(fn1.is_allowed_in_immutable_directory(), True) + self.failUnlessEqual(fn1.get_write_uri(), None) self.failUnlessEqual(fn1.get_readonly_uri(), u.to_string()) self.failUnlessEqual(fn1.get_size(), len(DATA)) self.failUnlessEqual(fn1.get_storage_index(), None) + fn1.raise_error() + fn2.raise_error() d = {} d[fn1] = 1 # exercise __hash__ @@ -99,24 +111,29 @@ class Node(unittest.TestCase): self.failUnlessEqual(n.get_writekey(), wk) self.failUnlessEqual(n.get_readkey(), rk) self.failUnlessEqual(n.get_storage_index(), si) - # these itmes are populated on first read (or create), so until that + # these items are populated on first read (or create), so until that # happens they'll be None self.failUnlessEqual(n.get_privkey(), None) self.failUnlessEqual(n.get_encprivkey(), None) self.failUnlessEqual(n.get_pubkey(), None) self.failUnlessEqual(n.get_uri(), u.to_string()) + self.failUnlessEqual(n.get_write_uri(), u.to_string()) self.failUnlessEqual(n.get_readonly_uri(), u.get_readonly().to_string()) self.failUnlessEqual(n.get_cap(), u) self.failUnlessEqual(n.get_readcap(), u.get_readonly()) self.failUnlessEqual(n.is_mutable(), True) self.failUnlessEqual(n.is_readonly(), False) + self.failUnlessEqual(n.is_unknown(), False) + self.failUnlessEqual(n.is_allowed_in_immutable_directory(), False) + n.raise_error() n2 = MutableFileNode(None, None, client.get_encoding_parameters(), None).init_from_cap(u) self.failUnlessEqual(n, n2) self.failIfEqual(n, "not even the right type") self.failIfEqual(n, u) # not the right class + n.raise_error() d = {n: "can these be used as dictionary keys?"} d[n2] = "replace the old one" self.failUnlessEqual(len(d), 1) @@ -127,12 +144,16 @@ class Node(unittest.TestCase): self.failUnlessEqual(nro.get_readonly(), nro) self.failUnlessEqual(nro.get_cap(), u.get_readonly()) self.failUnlessEqual(nro.get_readcap(), u.get_readonly()) + self.failUnlessEqual(nro.is_mutable(), True) + self.failUnlessEqual(nro.is_readonly(), True) + self.failUnlessEqual(nro.is_unknown(), False) + self.failUnlessEqual(nro.is_allowed_in_immutable_directory(), False) nro_u = nro.get_uri() self.failUnlessEqual(nro_u, nro.get_readonly_uri()) self.failUnlessEqual(nro_u, u.get_readonly().to_string()) - self.failUnlessEqual(nro.is_mutable(), True) - self.failUnlessEqual(nro.is_readonly(), True) + self.failUnlessEqual(nro.get_write_uri(), None) self.failUnlessEqual(nro.get_repair_cap(), None) # RSAmut needs writecap + nro.raise_error() v = n.get_verify_cap() self.failUnless(isinstance(v, uri.SSKVerifierURI)) diff --git a/src/allmydata/test/test_system.py b/src/allmydata/test/test_system.py index 60c705cb..5b301b84 100644 --- a/src/allmydata/test/test_system.py +++ b/src/allmydata/test/test_system.py @@ -17,7 +17,7 @@ from allmydata.scripts import runner from allmydata.interfaces import IDirectoryNode, IFileNode, \ NoSuchChildError, NoSharesError from allmydata.monitor import Monitor -from allmydata.mutable.common import NotMutableError +from allmydata.mutable.common import NotWriteableError from allmydata.mutable import layout as mutable_layout from foolscap.api import DeadReferenceError from twisted.python.failure import Failure @@ -890,11 +890,11 @@ class SystemTest(SystemTestMixin, unittest.TestCase): d1.addCallback(lambda res: dirnode.list()) d1.addCallback(self.log, "dirnode.list") - d1.addCallback(lambda res: self.shouldFail2(NotMutableError, "mkdir(nope)", None, dirnode.create_subdirectory, u"nope")) + d1.addCallback(lambda res: self.shouldFail2(NotWriteableError, "mkdir(nope)", None, dirnode.create_subdirectory, u"nope")) d1.addCallback(self.log, "doing add_file(ro)") ut = upload.Data("I will disappear, unrecorded and unobserved. The tragedy of my demise is made more poignant by its silence, but this beauty is not for you to ever know.", convergence="99i-p1x4-xd4-18yc-ywt-87uu-msu-zo -- completely and totally unguessable string (unless you read this)") - d1.addCallback(lambda res: self.shouldFail2(NotMutableError, "add_file(nope)", None, dirnode.add_file, u"hope", ut)) + d1.addCallback(lambda res: self.shouldFail2(NotWriteableError, "add_file(nope)", None, dirnode.add_file, u"hope", ut)) d1.addCallback(self.log, "doing get(ro)") d1.addCallback(lambda res: dirnode.get(u"mydata992")) @@ -902,17 +902,17 @@ class SystemTest(SystemTestMixin, unittest.TestCase): self.failUnless(IFileNode.providedBy(filenode))) d1.addCallback(self.log, "doing delete(ro)") - d1.addCallback(lambda res: self.shouldFail2(NotMutableError, "delete(nope)", None, dirnode.delete, u"mydata992")) + d1.addCallback(lambda res: self.shouldFail2(NotWriteableError, "delete(nope)", None, dirnode.delete, u"mydata992")) - d1.addCallback(lambda res: self.shouldFail2(NotMutableError, "set_uri(nope)", None, dirnode.set_uri, u"hopeless", self.uri, self.uri)) + d1.addCallback(lambda res: self.shouldFail2(NotWriteableError, "set_uri(nope)", None, dirnode.set_uri, u"hopeless", self.uri, self.uri)) d1.addCallback(lambda res: self.shouldFail2(NoSuchChildError, "get(missing)", "missing", dirnode.get, u"missing")) personal = self._personal_node - d1.addCallback(lambda res: self.shouldFail2(NotMutableError, "mv from readonly", None, dirnode.move_child_to, u"mydata992", personal, u"nope")) + d1.addCallback(lambda res: self.shouldFail2(NotWriteableError, "mv from readonly", None, dirnode.move_child_to, u"mydata992", personal, u"nope")) d1.addCallback(self.log, "doing move_child_to(ro)2") - d1.addCallback(lambda res: self.shouldFail2(NotMutableError, "mv to readonly", None, personal.move_child_to, u"sekrit data", dirnode, u"nope")) + d1.addCallback(lambda res: self.shouldFail2(NotWriteableError, "mv to readonly", None, personal.move_child_to, u"sekrit data", dirnode, u"nope")) d1.addCallback(self.log, "finished with _got_s2ro") return d1 diff --git a/src/allmydata/test/test_uri.py b/src/allmydata/test/test_uri.py index fe698e98..616c664c 100644 --- a/src/allmydata/test/test_uri.py +++ b/src/allmydata/test/test_uri.py @@ -3,7 +3,7 @@ from twisted.trial import unittest from allmydata import uri from allmydata.util import hashutil, base32 from allmydata.interfaces import IURI, IFileURI, IDirnodeURI, IMutableFileURI, \ - IVerifierURI + IVerifierURI, CapConstraintError class Literal(unittest.TestCase): def _help_test(self, data): @@ -22,8 +22,16 @@ class Literal(unittest.TestCase): self.failIf(IDirnodeURI.providedBy(u2)) self.failUnlessEqual(u2.data, data) self.failUnlessEqual(u2.get_size(), len(data)) - self.failUnless(u.is_readonly()) - self.failIf(u.is_mutable()) + self.failUnless(u2.is_readonly()) + self.failIf(u2.is_mutable()) + + u2i = uri.from_string(u.to_string(), deep_immutable=True) + self.failUnless(IFileURI.providedBy(u2i)) + self.failIf(IDirnodeURI.providedBy(u2i)) + self.failUnlessEqual(u2i.data, data) + self.failUnlessEqual(u2i.get_size(), len(data)) + self.failUnless(u2i.is_readonly()) + self.failIf(u2i.is_mutable()) u3 = u.get_readonly() self.failUnlessIdentical(u, u3) @@ -51,18 +59,36 @@ class Compare(unittest.TestCase): fileURI = 'URI:CHK:f5ahxa25t4qkktywz6teyfvcx4:opuioq7tj2y6idzfp6cazehtmgs5fdcebcz3cygrxyydvcozrmeq:3:10:345834' chk1 = uri.CHKFileURI.init_from_string(fileURI) chk2 = uri.CHKFileURI.init_from_string(fileURI) + unk = uri.UnknownURI("lafs://from_the_future") self.failIfEqual(lit1, chk1) self.failUnlessEqual(chk1, chk2) self.failIfEqual(chk1, "not actually a URI") # these should be hashable too - s = set([lit1, chk1, chk2]) - self.failUnlessEqual(len(s), 2) # since chk1==chk2 + s = set([lit1, chk1, chk2, unk]) + self.failUnlessEqual(len(s), 3) # since chk1==chk2 def test_is_uri(self): lit1 = uri.LiteralFileURI("some data").to_string() self.failUnless(uri.is_uri(lit1)) self.failIf(uri.is_uri(None)) + def test_is_literal_file_uri(self): + lit1 = uri.LiteralFileURI("some data").to_string() + self.failUnless(uri.is_literal_file_uri(lit1)) + self.failIf(uri.is_literal_file_uri(None)) + self.failIf(uri.is_literal_file_uri("foo")) + self.failIf(uri.is_literal_file_uri("ro.foo")) + self.failIf(uri.is_literal_file_uri("URI:LITfoo")) + self.failUnless(uri.is_literal_file_uri("ro.URI:LIT:foo")) + self.failUnless(uri.is_literal_file_uri("imm.URI:LIT:foo")) + + def test_has_uri_prefix(self): + self.failUnless(uri.has_uri_prefix("URI:foo")) + self.failUnless(uri.has_uri_prefix("ro.URI:foo")) + self.failUnless(uri.has_uri_prefix("imm.URI:foo")) + self.failIf(uri.has_uri_prefix(None)) + self.failIf(uri.has_uri_prefix("foo")) + class CHKFile(unittest.TestCase): def test_pack(self): key = "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f" @@ -88,8 +114,7 @@ class CHKFile(unittest.TestCase): self.failUnless(IFileURI.providedBy(u)) self.failIf(IDirnodeURI.providedBy(u)) self.failUnlessEqual(u.get_size(), 1234) - self.failUnless(u.is_readonly()) - self.failIf(u.is_mutable()) + u_ro = u.get_readonly() self.failUnlessIdentical(u, u_ro) he = u.to_human_encoding() @@ -109,11 +134,19 @@ class CHKFile(unittest.TestCase): self.failUnless(IFileURI.providedBy(u2)) self.failIf(IDirnodeURI.providedBy(u2)) self.failUnlessEqual(u2.get_size(), 1234) - self.failUnless(u2.is_readonly()) - self.failIf(u2.is_mutable()) + + u2i = uri.from_string(u.to_string(), deep_immutable=True) + self.failUnlessEqual(u.to_string(), u2i.to_string()) + u2ro = uri.from_string(uri.ALLEGED_READONLY_PREFIX + u.to_string()) + self.failUnlessEqual(u.to_string(), u2ro.to_string()) + u2imm = uri.from_string(uri.ALLEGED_IMMUTABLE_PREFIX + u.to_string()) + self.failUnlessEqual(u.to_string(), u2imm.to_string()) v = u.get_verify_cap() self.failUnless(isinstance(v.to_string(), str)) + self.failUnless(v.is_readonly()) + self.failIf(v.is_mutable()) + v2 = uri.from_string(v.to_string()) self.failUnlessEqual(v, v2) he = v.to_human_encoding() @@ -126,6 +159,8 @@ class CHKFile(unittest.TestCase): total_shares=10, size=1234) self.failUnless(isinstance(v3.to_string(), str)) + self.failUnless(v3.is_readonly()) + self.failIf(v3.is_mutable()) def test_pack_badly(self): key = "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f" @@ -179,13 +214,20 @@ class Extension(unittest.TestCase): self.failUnlessEqual(readable["UEB_hash"], base32.b2a(hashutil.uri_extension_hash(ext))) -class Invalid(unittest.TestCase): +class Unknown(unittest.TestCase): def test_from_future(self): # any URI type that we don't recognize should be treated as unknown future_uri = "I am a URI from the future. Whatever you do, don't " u = uri.from_string(future_uri) self.failUnless(isinstance(u, uri.UnknownURI)) self.failUnlessEqual(u.to_string(), future_uri) + self.failUnless(u.get_readonly() is None) + self.failUnless(u.get_error() is None) + + u2 = uri.UnknownURI(future_uri, error=CapConstraintError("...")) + self.failUnlessEqual(u.to_string(), future_uri) + self.failUnless(u2.get_readonly() is None) + self.failUnless(isinstance(u2.get_error(), CapConstraintError)) class Constraint(unittest.TestCase): def test_constraint(self): @@ -226,6 +268,13 @@ class Mutable(unittest.TestCase): self.failUnless(IMutableFileURI.providedBy(u2)) self.failIf(IDirnodeURI.providedBy(u2)) + u2i = uri.from_string(u.to_string(), deep_immutable=True) + self.failUnless(isinstance(u2i, uri.UnknownURI), u2i) + u2ro = uri.from_string(uri.ALLEGED_READONLY_PREFIX + u.to_string()) + self.failUnless(isinstance(u2ro, uri.UnknownURI), u2ro) + u2imm = uri.from_string(uri.ALLEGED_IMMUTABLE_PREFIX + u.to_string()) + self.failUnless(isinstance(u2imm, uri.UnknownURI), u2imm) + u3 = u2.get_readonly() readkey = hashutil.ssk_readkey_hash(writekey) self.failUnlessEqual(u3.fingerprint, fingerprint) @@ -236,6 +285,13 @@ class Mutable(unittest.TestCase): self.failUnless(IMutableFileURI.providedBy(u3)) self.failIf(IDirnodeURI.providedBy(u3)) + u3i = uri.from_string(u3.to_string(), deep_immutable=True) + self.failUnless(isinstance(u3i, uri.UnknownURI), u3i) + u3ro = uri.from_string(uri.ALLEGED_READONLY_PREFIX + u3.to_string()) + self.failUnlessEqual(u3.to_string(), u3ro.to_string()) + u3imm = uri.from_string(uri.ALLEGED_IMMUTABLE_PREFIX + u3.to_string()) + self.failUnless(isinstance(u3imm, uri.UnknownURI), u3imm) + he = u3.to_human_encoding() u3_h = uri.ReadonlySSKFileURI.init_from_human_encoding(he) self.failUnlessEqual(u3, u3_h) @@ -249,6 +305,13 @@ class Mutable(unittest.TestCase): self.failUnless(IMutableFileURI.providedBy(u4)) self.failIf(IDirnodeURI.providedBy(u4)) + u4i = uri.from_string(u4.to_string(), deep_immutable=True) + self.failUnless(isinstance(u4i, uri.UnknownURI), u4i) + u4ro = uri.from_string(uri.ALLEGED_READONLY_PREFIX + u4.to_string()) + self.failUnlessEqual(u4.to_string(), u4ro.to_string()) + u4imm = uri.from_string(uri.ALLEGED_IMMUTABLE_PREFIX + u4.to_string()) + self.failUnless(isinstance(u4imm, uri.UnknownURI), u4imm) + u4a = uri.from_string(u4.to_string()) self.failUnlessEqual(u4a, u4) self.failUnless("ReadonlySSKFileURI" in str(u4a)) @@ -291,12 +354,19 @@ class Dirnode(unittest.TestCase): self.failIf(IFileURI.providedBy(u2)) self.failUnless(IDirnodeURI.providedBy(u2)) + u2i = uri.from_string(u1.to_string(), deep_immutable=True) + self.failUnless(isinstance(u2i, uri.UnknownURI)) + 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)) + + u3i = uri.from_string(u2.to_string(), deep_immutable=True) + self.failUnless(isinstance(u3i, uri.UnknownURI)) + u3n = u3._filenode_uri self.failUnless(u3n.is_readonly()) self.failUnless(u3n.is_mutable()) @@ -363,10 +433,16 @@ class Dirnode(unittest.TestCase): self.failIf(IFileURI.providedBy(u2)) self.failUnless(IDirnodeURI.providedBy(u2)) + u2i = uri.from_string(u1.to_string(), deep_immutable=True) + self.failUnlessEqual(u1.to_string(), u2i.to_string()) + u3 = u2.get_readonly() self.failUnlessEqual(u3.to_string(), u2.to_string()) self.failUnless(str(u3)) + u3i = uri.from_string(u2.to_string(), deep_immutable=True) + self.failUnlessEqual(u2.to_string(), u3i.to_string()) + u2_verifier = u2.get_verify_cap() self.failUnless(isinstance(u2_verifier, uri.ImmutableDirectoryURIVerifier), diff --git a/src/allmydata/test/test_web.py b/src/allmydata/test/test_web.py index 1829f093..7d46b6e0 100644 --- a/src/allmydata/test/test_web.py +++ b/src/allmydata/test/test_web.py @@ -7,7 +7,7 @@ from twisted.internet import defer, reactor from twisted.web import client, error, http from twisted.python import failure, log from nevow import rend -from allmydata import interfaces, uri, webish +from allmydata import interfaces, uri, webish, dirnode from allmydata.storage.shares import get_share_file from allmydata.storage_client import StorageFarmBroker from allmydata.immutable import upload, download @@ -18,6 +18,7 @@ from allmydata.web import status, common from allmydata.scripts.debug import CorruptShareOptions, corrupt_share from allmydata.util import fileutil, base32 from allmydata.util.consumer import download_to_data +from allmydata.util.netstring import split_netstring from allmydata.test.common import FakeCHKFileNode, FakeMutableFileNode, \ create_chk_filenode, WebErrorMixin, ShouldFailMixin, make_mutable_file_uri from allmydata.interfaces import IMutableFileNode @@ -735,7 +736,7 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase): d = self.PUT(self.public_url + "/foo/new.txt", self.NEWFILE_CONTENTS) # TODO: we lose the response code, so we can't check this #self.failUnlessEqual(responsecode, 201) - d.addCallback(self.failUnlessURIMatchesChild, self._foo_node, u"new.txt") + d.addCallback(self.failUnlessURIMatchesROChild, self._foo_node, u"new.txt") d.addCallback(lambda res: self.failUnlessChildContentsAre(self._foo_node, u"new.txt", self.NEWFILE_CONTENTS)) @@ -746,7 +747,7 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase): self.NEWFILE_CONTENTS) # TODO: we lose the response code, so we can't check this #self.failUnlessEqual(responsecode, 201) - d.addCallback(self.failUnlessURIMatchesChild, self._foo_node, u"new.txt") + d.addCallback(self.failUnlessURIMatchesROChild, self._foo_node, u"new.txt") d.addCallback(lambda res: self.failUnlessChildContentsAre(self._foo_node, u"new.txt", self.NEWFILE_CONTENTS)) @@ -776,7 +777,7 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase): self.failIf(u.is_readonly()) return res d.addCallback(_check_uri) - d.addCallback(self.failUnlessURIMatchesChild, self._foo_node, u"new.txt") + d.addCallback(self.failUnlessURIMatchesRWChild, self._foo_node, u"new.txt") d.addCallback(lambda res: self.failUnlessMutableChildContentsAre(self._foo_node, u"new.txt", @@ -796,7 +797,7 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase): d = self.PUT(self.public_url + "/foo/bar.txt", self.NEWFILE_CONTENTS) # TODO: we lose the response code, so we can't check this #self.failUnlessEqual(responsecode, 200) - d.addCallback(self.failUnlessURIMatchesChild, self._foo_node, u"bar.txt") + d.addCallback(self.failUnlessURIMatchesROChild, self._foo_node, u"bar.txt") d.addCallback(lambda res: self.failUnlessChildContentsAre(self._foo_node, u"bar.txt", self.NEWFILE_CONTENTS)) @@ -821,7 +822,7 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase): def test_PUT_NEWFILEURL_mkdirs(self): d = self.PUT(self.public_url + "/foo/newdir/new.txt", self.NEWFILE_CONTENTS) fn = self._foo_node - d.addCallback(self.failUnlessURIMatchesChild, fn, u"newdir/new.txt") + d.addCallback(self.failUnlessURIMatchesROChild, fn, u"newdir/new.txt") d.addCallback(lambda res: self.failIfNodeHasChild(fn, u"new.txt")) d.addCallback(lambda res: self.failUnlessNodeHasChild(fn, u"newdir")) d.addCallback(lambda res: @@ -954,7 +955,7 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase): self.failUnless(re.search(get_sub, res), res) d.addCallback(_check) - # look at a directory which is readonly + # look at a readonly directory d.addCallback(lambda res: self.GET(self.public_url + "/reedownlee", followRedirect=True)) def _check2(res): @@ -1167,23 +1168,33 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase): return d def test_POST_NEWDIRURL_initial_children(self): - (newkids, filecap1, filecap2, filecap3, - dircap) = self._create_initial_children() + (newkids, caps) = self._create_initial_children() d = self.POST2(self.public_url + "/foo/newdir?t=mkdir-with-children", simplejson.dumps(newkids)) def _check(uri): n = self.s.create_node_from_uri(uri.strip()) d2 = self.failUnlessNodeKeysAre(n, newkids.keys()) d2.addCallback(lambda ign: - self.failUnlessChildURIIs(n, u"child-imm", filecap1)) + self.failUnlessROChildURIIs(n, u"child-imm", + caps['filecap1'])) d2.addCallback(lambda ign: - self.failUnlessChildURIIs(n, u"child-mutable", - filecap2)) + self.failUnlessRWChildURIIs(n, u"child-mutable", + caps['filecap2'])) d2.addCallback(lambda ign: - self.failUnlessChildURIIs(n, u"child-mutable-ro", - filecap3)) + self.failUnlessROChildURIIs(n, u"child-mutable-ro", + caps['filecap3'])) d2.addCallback(lambda ign: - self.failUnlessChildURIIs(n, u"dirchild", dircap)) + self.failUnlessROChildURIIs(n, u"unknownchild-ro", + caps['unknown_rocap'])) + d2.addCallback(lambda ign: + self.failUnlessRWChildURIIs(n, u"unknownchild-rw", + caps['unknown_rwcap'])) + d2.addCallback(lambda ign: + self.failUnlessROChildURIIs(n, u"unknownchild-imm", + caps['unknown_immcap'])) + d2.addCallback(lambda ign: + self.failUnlessRWChildURIIs(n, u"dirchild", + caps['dircap'])) return d2 d.addCallback(_check) d.addCallback(lambda res: @@ -1191,21 +1202,25 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase): d.addCallback(lambda res: self._foo_node.get(u"newdir")) d.addCallback(self.failUnlessNodeKeysAre, newkids.keys()) d.addCallback(lambda res: self._foo_node.get(u"newdir")) - d.addCallback(self.failUnlessChildURIIs, u"child-imm", filecap1) + d.addCallback(self.failUnlessROChildURIIs, u"child-imm", caps['filecap1']) return d def test_POST_NEWDIRURL_immutable(self): - (newkids, filecap1, immdircap) = self._create_immutable_children() + (newkids, caps) = self._create_immutable_children() d = self.POST2(self.public_url + "/foo/newdir?t=mkdir-immutable", simplejson.dumps(newkids)) def _check(uri): n = self.s.create_node_from_uri(uri.strip()) d2 = self.failUnlessNodeKeysAre(n, newkids.keys()) d2.addCallback(lambda ign: - self.failUnlessChildURIIs(n, u"child-imm", filecap1)) + self.failUnlessROChildURIIs(n, u"child-imm", + caps['filecap1'])) + d2.addCallback(lambda ign: + self.failUnlessROChildURIIs(n, u"unknownchild-imm", + caps['unknown_immcap'])) d2.addCallback(lambda ign: - self.failUnlessChildURIIs(n, u"dirchild-imm", - immdircap)) + self.failUnlessROChildURIIs(n, u"dirchild-imm", + caps['immdircap'])) return d2 d.addCallback(_check) d.addCallback(lambda res: @@ -1213,18 +1228,19 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase): d.addCallback(lambda res: self._foo_node.get(u"newdir")) d.addCallback(self.failUnlessNodeKeysAre, newkids.keys()) d.addCallback(lambda res: self._foo_node.get(u"newdir")) - d.addCallback(self.failUnlessChildURIIs, u"child-imm", filecap1) + d.addCallback(self.failUnlessROChildURIIs, u"child-imm", caps['filecap1']) d.addCallback(lambda res: self._foo_node.get(u"newdir")) - d.addCallback(self.failUnlessChildURIIs, u"dirchild-imm", immdircap) + d.addCallback(self.failUnlessROChildURIIs, u"unknownchild-imm", caps['unknown_immcap']) + d.addCallback(lambda res: self._foo_node.get(u"newdir")) + d.addCallback(self.failUnlessROChildURIIs, u"dirchild-imm", caps['immdircap']) d.addErrback(self.explain_web_error) return d def test_POST_NEWDIRURL_immutable_bad(self): - (newkids, filecap1, filecap2, filecap3, - dircap) = self._create_initial_children() + (newkids, caps) = self._create_initial_children() d = self.shouldFail2(error.Error, "test_POST_NEWDIRURL_immutable_bad", "400 Bad Request", - "a mkdir-immutable operation was given a child that was not itself immutable", + "needed to be immutable but was not", self.POST2, self.public_url + "/foo/newdir?t=mkdir-immutable", simplejson.dumps(newkids)) @@ -1346,19 +1362,51 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase): d.addCallback(_check) return d - def failUnlessChildURIIs(self, node, name, expected_uri): + def failUnlessRWChildURIIs(self, node, name, expected_uri): + assert isinstance(name, unicode) + d = node.get_child_at_path(name) + def _check(child): + self.failUnless(child.is_unknown() or not child.is_readonly()) + self.failUnlessEqual(child.get_uri(), expected_uri.strip()) + self.failUnlessEqual(child.get_write_uri(), expected_uri.strip()) + expected_ro_uri = self._make_readonly(expected_uri) + if expected_ro_uri: + self.failUnlessEqual(child.get_readonly_uri(), expected_ro_uri.strip()) + d.addCallback(_check) + return d + + def failUnlessROChildURIIs(self, node, name, expected_uri): assert isinstance(name, unicode) d = node.get_child_at_path(name) def _check(child): + self.failUnless(child.is_unknown() or child.is_readonly()) + self.failUnlessEqual(child.get_write_uri(), None) self.failUnlessEqual(child.get_uri(), expected_uri.strip()) + self.failUnlessEqual(child.get_readonly_uri(), expected_uri.strip()) d.addCallback(_check) return d - def failUnlessURIMatchesChild(self, got_uri, node, name): + def failUnlessURIMatchesRWChild(self, got_uri, node, name): assert isinstance(name, unicode) d = node.get_child_at_path(name) def _check(child): + self.failUnless(child.is_unknown() or not child.is_readonly()) + self.failUnlessEqual(child.get_uri(), got_uri.strip()) + self.failUnlessEqual(child.get_write_uri(), got_uri.strip()) + expected_ro_uri = self._make_readonly(got_uri) + if expected_ro_uri: + self.failUnlessEqual(child.get_readonly_uri(), expected_ro_uri.strip()) + d.addCallback(_check) + return d + + def failUnlessURIMatchesROChild(self, got_uri, node, name): + assert isinstance(name, unicode) + d = node.get_child_at_path(name) + def _check(child): + self.failUnless(child.is_unknown() or child.is_readonly()) + self.failUnlessEqual(child.get_write_uri(), None) self.failUnlessEqual(got_uri.strip(), child.get_uri()) + self.failUnlessEqual(got_uri.strip(), child.get_readonly_uri()) d.addCallback(_check) return d @@ -1369,7 +1417,7 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase): d = self.POST(self.public_url + "/foo", t="upload", file=("new.txt", self.NEWFILE_CONTENTS)) fn = self._foo_node - d.addCallback(self.failUnlessURIMatchesChild, fn, u"new.txt") + d.addCallback(self.failUnlessURIMatchesROChild, fn, u"new.txt") d.addCallback(lambda res: self.failUnlessChildContentsAre(fn, u"new.txt", self.NEWFILE_CONTENTS)) @@ -1380,7 +1428,7 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase): d = self.POST(self.public_url + "/foo", t="upload", file=(filename, self.NEWFILE_CONTENTS)) fn = self._foo_node - d.addCallback(self.failUnlessURIMatchesChild, fn, filename) + d.addCallback(self.failUnlessURIMatchesROChild, fn, filename) d.addCallback(lambda res: self.failUnlessChildContentsAre(fn, filename, self.NEWFILE_CONTENTS)) @@ -1397,7 +1445,7 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase): name=filename, file=("overridden", self.NEWFILE_CONTENTS)) fn = self._foo_node - d.addCallback(self.failUnlessURIMatchesChild, fn, filename) + d.addCallback(self.failUnlessURIMatchesROChild, fn, filename) d.addCallback(lambda res: self.failUnlessChildContentsAre(fn, filename, self.NEWFILE_CONTENTS)) @@ -1499,7 +1547,7 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase): d = self.POST(self.public_url + "/foo", t="upload", mutable="true", file=("new.txt", self.NEWFILE_CONTENTS)) fn = self._foo_node - d.addCallback(self.failUnlessURIMatchesChild, fn, u"new.txt") + d.addCallback(self.failUnlessURIMatchesRWChild, fn, u"new.txt") d.addCallback(lambda res: self.failUnlessMutableChildContentsAre(fn, u"new.txt", self.NEWFILE_CONTENTS)) @@ -1518,7 +1566,7 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase): self.POST(self.public_url + "/foo", t="upload", mutable="true", file=("new.txt", NEWER_CONTENTS))) - d.addCallback(self.failUnlessURIMatchesChild, fn, u"new.txt") + d.addCallback(self.failUnlessURIMatchesRWChild, fn, u"new.txt") d.addCallback(lambda res: self.failUnlessMutableChildContentsAre(fn, u"new.txt", NEWER_CONTENTS)) @@ -1534,7 +1582,7 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase): NEW2_CONTENTS = NEWER_CONTENTS + "overwrite with PUT\n" d.addCallback(lambda res: self.PUT(self.public_url + "/foo/new.txt", NEW2_CONTENTS)) - d.addCallback(self.failUnlessURIMatchesChild, fn, u"new.txt") + d.addCallback(self.failUnlessURIMatchesRWChild, fn, u"new.txt") d.addCallback(lambda res: self.failUnlessMutableChildContentsAre(fn, u"new.txt", NEW2_CONTENTS)) @@ -1663,7 +1711,7 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase): d = self.POST(self.public_url + "/foo", t="upload", file=("bar.txt", self.NEWFILE_CONTENTS)) fn = self._foo_node - d.addCallback(self.failUnlessURIMatchesChild, fn, u"bar.txt") + d.addCallback(self.failUnlessURIMatchesROChild, fn, u"bar.txt") d.addCallback(lambda res: self.failUnlessChildContentsAre(fn, u"bar.txt", self.NEWFILE_CONTENTS)) @@ -1714,7 +1762,7 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase): fn = self._foo_node d = self.POST(self.public_url + "/foo", t="upload", name="new.txt", file=self.NEWFILE_CONTENTS) - d.addCallback(self.failUnlessURIMatchesChild, fn, u"new.txt") + d.addCallback(self.failUnlessURIMatchesROChild, fn, u"new.txt") d.addCallback(lambda res: self.failUnlessChildContentsAre(fn, u"new.txt", self.NEWFILE_CONTENTS)) @@ -1977,7 +2025,7 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase): return d def test_POST_mkdir_initial_children(self): - newkids, filecap1, ign, ign, ign = self._create_initial_children() + (newkids, caps) = self._create_initial_children() d = self.POST2(self.public_url + "/foo?t=mkdir-with-children&name=newdir", simplejson.dumps(newkids)) @@ -1986,11 +2034,11 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase): d.addCallback(lambda res: self._foo_node.get(u"newdir")) d.addCallback(self.failUnlessNodeKeysAre, newkids.keys()) d.addCallback(lambda res: self._foo_node.get(u"newdir")) - d.addCallback(self.failUnlessChildURIIs, u"child-imm", filecap1) + d.addCallback(self.failUnlessROChildURIIs, u"child-imm", caps['filecap1']) return d def test_POST_mkdir_immutable(self): - (newkids, filecap1, immdircap) = self._create_immutable_children() + (newkids, caps) = self._create_immutable_children() d = self.POST2(self.public_url + "/foo?t=mkdir-immutable&name=newdir", simplejson.dumps(newkids)) @@ -1999,17 +2047,18 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase): d.addCallback(lambda res: self._foo_node.get(u"newdir")) d.addCallback(self.failUnlessNodeKeysAre, newkids.keys()) d.addCallback(lambda res: self._foo_node.get(u"newdir")) - d.addCallback(self.failUnlessChildURIIs, u"child-imm", filecap1) + d.addCallback(self.failUnlessROChildURIIs, u"child-imm", caps['filecap1']) d.addCallback(lambda res: self._foo_node.get(u"newdir")) - d.addCallback(self.failUnlessChildURIIs, u"dirchild-imm", immdircap) + d.addCallback(self.failUnlessROChildURIIs, u"unknownchild-imm", caps['unknown_immcap']) + d.addCallback(lambda res: self._foo_node.get(u"newdir")) + d.addCallback(self.failUnlessROChildURIIs, u"dirchild-imm", caps['immdircap']) return d def test_POST_mkdir_immutable_bad(self): - (newkids, filecap1, filecap2, filecap3, - dircap) = self._create_initial_children() + (newkids, caps) = self._create_initial_children() d = self.shouldFail2(error.Error, "test_POST_mkdir_immutable_bad", "400 Bad Request", - "a mkdir-immutable operation was given a child that was not itself immutable", + "needed to be immutable but was not", self.POST2, self.public_url + "/foo?t=mkdir-immutable&name=newdir", @@ -2068,21 +2117,43 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase): d.addErrback(self.explain_web_error) return d + def _make_readonly(self, u): + ro_uri = uri.from_string(u).get_readonly() + if ro_uri is None: + return None + return ro_uri.to_string() + def _create_initial_children(self): contents, n, filecap1 = self.makefile(12) md1 = {"metakey1": "metavalue1"} filecap2 = make_mutable_file_uri() node3 = self.s.create_node_from_uri(make_mutable_file_uri()) filecap3 = node3.get_readonly_uri() + unknown_rwcap = "lafs://from_the_future" + unknown_rocap = "ro.lafs://readonly_from_the_future" + unknown_immcap = "imm.lafs://immutable_from_the_future" node4 = self.s.create_node_from_uri(make_mutable_file_uri()) dircap = DirectoryNode(node4, None, None).get_uri() - newkids = {u"child-imm": ["filenode", {"ro_uri": filecap1, - "metadata": md1, }], - u"child-mutable": ["filenode", {"rw_uri": filecap2}], + newkids = {u"child-imm": ["filenode", {"rw_uri": filecap1, + "ro_uri": self._make_readonly(filecap1), + "metadata": md1, }], + u"child-mutable": ["filenode", {"rw_uri": filecap2, + "ro_uri": self._make_readonly(filecap2)}], u"child-mutable-ro": ["filenode", {"ro_uri": filecap3}], - u"dirchild": ["dirnode", {"rw_uri": dircap}], + u"unknownchild-rw": ["unknown", {"rw_uri": unknown_rwcap, + "ro_uri": unknown_rocap}], + u"unknownchild-ro": ["unknown", {"ro_uri": unknown_rocap}], + u"unknownchild-imm": ["unknown", {"ro_uri": unknown_immcap}], + u"dirchild": ["dirnode", {"rw_uri": dircap, + "ro_uri": self._make_readonly(dircap)}], } - return newkids, filecap1, filecap2, filecap3, dircap + return newkids, {'filecap1': filecap1, + 'filecap2': filecap2, + 'filecap3': filecap3, + 'unknown_rwcap': unknown_rwcap, + 'unknown_rocap': unknown_rocap, + 'unknown_immcap': unknown_immcap, + 'dircap': dircap} def _create_immutable_children(self): contents, n, filecap1 = self.makefile(12) @@ -2090,31 +2161,45 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase): tnode = create_chk_filenode("immutable directory contents\n"*10) dnode = DirectoryNode(tnode, None, None) assert not dnode.is_mutable() + unknown_immcap = "imm.lafs://immutable_from_the_future" immdircap = dnode.get_uri() - newkids = {u"child-imm": ["filenode", {"ro_uri": filecap1, - "metadata": md1, }], - u"dirchild-imm": ["dirnode", {"ro_uri": immdircap}], + newkids = {u"child-imm": ["filenode", {"ro_uri": filecap1, + "metadata": md1, }], + u"unknownchild-imm": ["unknown", {"ro_uri": unknown_immcap}], + u"dirchild-imm": ["dirnode", {"ro_uri": immdircap}], } - return newkids, filecap1, immdircap + return newkids, {'filecap1': filecap1, + 'unknown_immcap': unknown_immcap, + 'immdircap': immdircap} def test_POST_mkdir_no_parentdir_initial_children(self): - (newkids, filecap1, filecap2, filecap3, - dircap) = self._create_initial_children() + (newkids, caps) = self._create_initial_children() d = self.POST2("/uri?t=mkdir-with-children", simplejson.dumps(newkids)) def _after_mkdir(res): self.failUnless(res.startswith("URI:DIR"), res) n = self.s.create_node_from_uri(res) d2 = self.failUnlessNodeKeysAre(n, newkids.keys()) d2.addCallback(lambda ign: - self.failUnlessChildURIIs(n, u"child-imm", filecap1)) + self.failUnlessROChildURIIs(n, u"child-imm", + caps['filecap1'])) d2.addCallback(lambda ign: - self.failUnlessChildURIIs(n, u"child-mutable", - filecap2)) + self.failUnlessRWChildURIIs(n, u"child-mutable", + caps['filecap2'])) d2.addCallback(lambda ign: - self.failUnlessChildURIIs(n, u"child-mutable-ro", - filecap3)) + self.failUnlessROChildURIIs(n, u"child-mutable-ro", + caps['filecap3'])) d2.addCallback(lambda ign: - self.failUnlessChildURIIs(n, u"dirchild", dircap)) + self.failUnlessRWChildURIIs(n, u"unknownchild-rw", + caps['unknown_rwcap'])) + d2.addCallback(lambda ign: + self.failUnlessROChildURIIs(n, u"unknownchild-ro", + caps['unknown_rocap'])) + d2.addCallback(lambda ign: + self.failUnlessROChildURIIs(n, u"unknownchild-imm", + caps['unknown_immcap'])) + d2.addCallback(lambda ign: + self.failUnlessRWChildURIIs(n, u"dirchild", + caps['dircap'])) return d2 d.addCallback(_after_mkdir) return d @@ -2122,8 +2207,7 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase): def test_POST_mkdir_no_parentdir_unexpected_children(self): # the regular /uri?t=mkdir operation is specified to ignore its body. # Only t=mkdir-with-children pays attention to it. - (newkids, filecap1, filecap2, filecap3, - dircap) = self._create_initial_children() + (newkids, caps) = self._create_initial_children() d = self.shouldHTTPError("POST t=mkdir unexpected children", 400, "Bad Request", "t=mkdir does not accept children=, " @@ -2140,28 +2224,31 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase): return d def test_POST_mkdir_no_parentdir_immutable(self): - (newkids, filecap1, immdircap) = self._create_immutable_children() + (newkids, caps) = self._create_immutable_children() d = self.POST2("/uri?t=mkdir-immutable", simplejson.dumps(newkids)) def _after_mkdir(res): self.failUnless(res.startswith("URI:DIR"), res) n = self.s.create_node_from_uri(res) d2 = self.failUnlessNodeKeysAre(n, newkids.keys()) d2.addCallback(lambda ign: - self.failUnlessChildURIIs(n, u"child-imm", filecap1)) + self.failUnlessROChildURIIs(n, u"child-imm", + caps['filecap1'])) + d2.addCallback(lambda ign: + self.failUnlessROChildURIIs(n, u"unknownchild-imm", + caps['unknown_immcap'])) d2.addCallback(lambda ign: - self.failUnlessChildURIIs(n, u"dirchild-imm", - immdircap)) + self.failUnlessROChildURIIs(n, u"dirchild-imm", + caps['immdircap'])) return d2 d.addCallback(_after_mkdir) return d def test_POST_mkdir_no_parentdir_immutable_bad(self): - (newkids, filecap1, filecap2, filecap3, - dircap) = self._create_initial_children() + (newkids, caps) = self._create_initial_children() d = self.shouldFail2(error.Error, "test_POST_mkdir_no_parentdir_immutable_bad", "400 Bad Request", - "a mkdir-immutable operation was given a child that was not itself immutable", + "needed to be immutable but was not", self.POST2, "/uri?t=mkdir-immutable", simplejson.dumps(newkids)) @@ -2269,9 +2356,9 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase): d = client.getPage(url, method="POST", postdata=reqbody) def _then(res): - self.failUnlessURIMatchesChild(newuri9, self._foo_node, u"atomic_added_1") - self.failUnlessURIMatchesChild(newuri10, self._foo_node, u"atomic_added_2") - self.failUnlessURIMatchesChild(newuri11, self._foo_node, u"atomic_added_3") + self.failUnlessURIMatchesROChild(newuri9, self._foo_node, u"atomic_added_1") + self.failUnlessURIMatchesROChild(newuri10, self._foo_node, u"atomic_added_2") + self.failUnlessURIMatchesROChild(newuri11, self._foo_node, u"atomic_added_3") d.addCallback(_then) d.addErrback(self.dump_error) @@ -2283,7 +2370,7 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase): def test_POST_put_uri(self): contents, n, newuri = self.makefile(8) d = self.POST(self.public_url + "/foo", t="uri", name="new.txt", uri=newuri) - d.addCallback(self.failUnlessURIMatchesChild, self._foo_node, u"new.txt") + d.addCallback(self.failUnlessURIMatchesROChild, self._foo_node, u"new.txt") d.addCallback(lambda res: self.failUnlessChildContentsAre(self._foo_node, u"new.txt", contents)) @@ -2292,7 +2379,7 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase): def test_POST_put_uri_replace(self): contents, n, newuri = self.makefile(8) d = self.POST(self.public_url + "/foo", t="uri", name="bar.txt", uri=newuri) - d.addCallback(self.failUnlessURIMatchesChild, self._foo_node, u"bar.txt") + d.addCallback(self.failUnlessURIMatchesROChild, self._foo_node, u"bar.txt") d.addCallback(lambda res: self.failUnlessChildContentsAre(self._foo_node, u"bar.txt", contents)) @@ -2521,9 +2608,9 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase): d.addCallback(lambda res: self.failUnlessEqual(res.strip(), new_uri)) d.addCallback(lambda res: - self.failUnlessChildURIIs(self.public_root, - u"foo", - new_uri)) + self.failUnlessRWChildURIIs(self.public_root, + u"foo", + new_uri)) return d d.addCallback(_made_dir) return d @@ -2540,9 +2627,9 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase): self.public_url + "/foo?t=uri&replace=false", new_uri) d.addCallback(lambda res: - self.failUnlessChildURIIs(self.public_root, - u"foo", - self._foo_uri)) + self.failUnlessRWChildURIIs(self.public_root, + u"foo", + self._foo_uri)) return d d.addCallback(_made_dir) return d @@ -2552,9 +2639,9 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase): "400 Bad Request", "PUT to a directory", self.PUT, self.public_url + "/foo?t=BOGUS", "") d.addCallback(lambda res: - self.failUnlessChildURIIs(self.public_root, - u"foo", - self._foo_uri)) + self.failUnlessRWChildURIIs(self.public_root, + u"foo", + self._foo_uri)) return d def test_PUT_NEWFILEURL_uri(self): @@ -3081,71 +3168,246 @@ class Grid(GridTestMixin, WebErrorMixin, unittest.TestCase, ShouldFailMixin): d.addErrback(self.explain_web_error) return d - def test_unknown(self): + def test_unknown(self, immutable=False): self.basedir = "web/Grid/unknown" + if immutable: + self.basedir = "web/Grid/unknown-immutable" + self.set_up_grid() c0 = self.g.clients[0] self.uris = {} self.fileurls = {} - future_writecap = "x-tahoe-crazy://I_am_from_the_future." - future_readcap = "x-tahoe-crazy-readonly://I_am_from_the_future." + future_write_uri = "x-tahoe-crazy://I_am_from_the_future." + future_read_uri = "x-tahoe-crazy-readonly://I_am_from_the_future." # the future cap format may contain slashes, which must be tolerated - expected_info_url = "uri/%s?t=info" % urllib.quote(future_writecap, + expected_info_url = "uri/%s?t=info" % urllib.quote(future_write_uri, safe="") - future_node = UnknownNode(future_writecap, future_readcap) - d = c0.create_dirnode() + if immutable: + name = u"future-imm" + future_node = UnknownNode(None, future_read_uri, deep_immutable=True) + d = c0.create_immutable_dirnode({name: (future_node, {})}) + else: + name = u"future" + future_node = UnknownNode(future_write_uri, future_read_uri) + d = c0.create_dirnode() + def _stash_root_and_create_file(n): self.rootnode = n self.rooturl = "uri/" + urllib.quote(n.get_uri()) + "/" self.rourl = "uri/" + urllib.quote(n.get_readonly_uri()) + "/" - return self.rootnode.set_node(u"future", future_node) + if not immutable: + return self.rootnode.set_node(name, future_node) d.addCallback(_stash_root_and_create_file) + # make sure directory listing tolerates unknown nodes d.addCallback(lambda ign: self.GET(self.rooturl)) - def _check_html(res): - self.failUnlessIn("future", res) - # find the More Info link for "future", should be relative + def _check_directory_html(res): + self.failUnlessIn("%s" % (str(name),), res) + # find the More Info link for name, should be relative mo = re.search(r'More Info', res) info_url = mo.group(1) - self.failUnlessEqual(info_url, "future?t=info") + self.failUnlessEqual(info_url, "%s?t=info" % (str(name),)) + d.addCallback(_check_directory_html) - d.addCallback(_check_html) d.addCallback(lambda ign: self.GET(self.rooturl+"?t=json")) - def _check_json(res, expect_writecap): + def _check_directory_json(res, expect_rw_uri): data = simplejson.loads(res) self.failUnlessEqual(data[0], "dirnode") - f = data[1]["children"]["future"] + f = data[1]["children"][name] self.failUnlessEqual(f[0], "unknown") - if expect_writecap: - self.failUnlessEqual(f[1]["rw_uri"], future_writecap) + if expect_rw_uri: + self.failUnlessEqual(f[1]["rw_uri"], future_write_uri) else: self.failIfIn("rw_uri", f[1]) - self.failUnlessEqual(f[1]["ro_uri"], future_readcap) + self.failUnlessEqual(f[1]["ro_uri"], + ("imm." if immutable else "ro.") + future_read_uri) self.failUnless("metadata" in f[1]) - d.addCallback(_check_json, expect_writecap=True) - d.addCallback(lambda ign: self.GET(expected_info_url)) - def _check_info(res, expect_readcap): + d.addCallback(_check_directory_json, expect_rw_uri=not immutable) + + def _check_info(res, expect_rw_uri, expect_ro_uri): self.failUnlessIn("Object Type: unknown", res) - self.failUnlessIn(future_writecap, res) - if expect_readcap: - self.failUnlessIn(future_readcap, res) + if expect_rw_uri: + self.failUnlessIn(future_write_uri, res) + if expect_ro_uri: + self.failUnlessIn(future_read_uri, res) + else: + self.failIfIn(future_read_uri, res) self.failIfIn("Raw data as", res) self.failIfIn("Directory writecap", res) self.failIfIn("Checker Operations", res) self.failIfIn("Mutable File Operations", res) self.failIfIn("Directory Operations", res) - d.addCallback(_check_info, expect_readcap=False) - d.addCallback(lambda ign: self.GET(self.rooturl+"future?t=info")) - d.addCallback(_check_info, expect_readcap=True) + + # FIXME: these should have expect_rw_uri=not immutable; I don't know + # why they fail. Possibly related to ticket #922. + + d.addCallback(lambda ign: self.GET(expected_info_url)) + d.addCallback(_check_info, expect_rw_uri=False, expect_ro_uri=False) + d.addCallback(lambda ign: self.GET("%s%s?t=info" % (self.rooturl, str(name)))) + d.addCallback(_check_info, expect_rw_uri=False, expect_ro_uri=True) + + def _check_json(res, expect_rw_uri): + data = simplejson.loads(res) + self.failUnlessEqual(data[0], "unknown") + if expect_rw_uri: + self.failUnlessEqual(data[1]["rw_uri"], future_write_uri) + else: + self.failIfIn("rw_uri", data[1]) + self.failUnlessEqual(data[1]["ro_uri"], + ("imm." if immutable else "ro.") + future_read_uri) + # TODO: check metadata contents + self.failUnless("metadata" in data[1]) + + d.addCallback(lambda ign: self.GET("%s%s?t=json" % (self.rooturl, str(name)))) + d.addCallback(_check_json, expect_rw_uri=not immutable) # and make sure that a read-only version of the directory can be - # rendered too. This version will not have future_writecap + # rendered too. This version will not have future_write_uri, whether + # or not future_node was immutable. d.addCallback(lambda ign: self.GET(self.rourl)) - d.addCallback(_check_html) + d.addCallback(_check_directory_html) d.addCallback(lambda ign: self.GET(self.rourl+"?t=json")) - d.addCallback(_check_json, expect_writecap=False) + d.addCallback(_check_directory_json, expect_rw_uri=False) + + d.addCallback(lambda ign: self.GET("%s%s?t=json" % (self.rourl, str(name)))) + d.addCallback(_check_json, expect_rw_uri=False) + + # TODO: check that getting t=info from the Info link in the ro directory + # works, and does not include the writecap URI. + return d + + def test_immutable_unknown(self): + return self.test_unknown(immutable=True) + + def test_mutant_dirnodes_are_omitted(self): + self.basedir = "web/Grid/mutant_dirnodes_are_omitted" + + self.set_up_grid() + c = self.g.clients[0] + nm = c.nodemaker + self.uris = {} + self.fileurls = {} + + lonely_uri = "URI:LIT:n5xgk" # LIT for "one" + mut_write_uri = "URI:SSK:vfvcbdfbszyrsaxchgevhmmlii:euw4iw7bbnkrrwpzuburbhppuxhc3gwxv26f6imekhz7zyw2ojnq" + mut_read_uri = "URI:SSK-RO:e3mdrzfwhoq42hy5ubcz6rp3o4:ybyibhnp3vvwuq2vaw2ckjmesgkklfs6ghxleztqidihjyofgw7q" + + # This method tests mainly dirnode, but we'd have to duplicate code in order to + # test the dirnode and web layers separately. + + # 'lonely' is a valid LIT child, 'ro' is a mutant child with an SSK-RO readcap, + # and 'write-in-ro' is a mutant child with an SSK writecap in the ro_uri field. + # When the directory is read, the mutants should be silently disposed of, leaving + # their lonely sibling. + # We don't test the case of a retrieving a cap from the encrypted rw_uri field, + # because immutable directories don't have a writecap and therefore that field + # isn't (and can't be) decrypted. + # TODO: The field still exists in the netstring. Technically we should check what + # happens if something is put there (it should be ignored), but that can wait. + + lonely_child = nm.create_from_cap(lonely_uri) + mutant_ro_child = nm.create_from_cap(mut_read_uri) + mutant_write_in_ro_child = nm.create_from_cap(mut_write_uri) + + def _by_hook_or_by_crook(): + return True + for n in [mutant_ro_child, mutant_write_in_ro_child]: + n.is_allowed_in_immutable_directory = _by_hook_or_by_crook + + mutant_write_in_ro_child.get_write_uri = lambda: None + mutant_write_in_ro_child.get_readonly_uri = lambda: mut_write_uri + + kids = {u"lonely": (lonely_child, {}), + u"ro": (mutant_ro_child, {}), + u"write-in-ro": (mutant_write_in_ro_child, {}), + } + d = c.create_immutable_dirnode(kids) + + def _created(dn): + self.failUnless(isinstance(dn, dirnode.DirectoryNode)) + self.failIf(dn.is_mutable()) + self.failUnless(dn.is_readonly()) + # This checks that if we somehow ended up calling dn._decrypt_rwcapdata, it would fail. + self.failIf(hasattr(dn._node, 'get_writekey')) + rep = str(dn) + self.failUnless("RO-IMM" in rep) + cap = dn.get_cap() + self.failUnlessIn("CHK", cap.to_string()) + self.cap = cap + self.rootnode = dn + self.rooturl = "uri/" + urllib.quote(dn.get_uri()) + "/" + return download_to_data(dn._node) + d.addCallback(_created) + + def _check_data(data): + # Decode the netstring representation of the directory to check that all children + # are present. This is a bit of an abstraction violation, but there's not really + # any other way to do it given that the real DirectoryNode._unpack_contents would + # strip the mutant children out (which is what we're trying to test, later). + position = 0 + numkids = 0 + while position < len(data): + entries, position = split_netstring(data, 1, position) + entry = entries[0] + (name, ro_uri, rwcapdata, metadata_s), subpos = split_netstring(entry, 4) + name = name.decode("utf-8") + self.failUnless(rwcapdata == "") + ro_uri = ro_uri.strip() + if name in kids: + self.failIfEqual(ro_uri, "") + (expected_child, ign) = kids[name] + self.failUnlessEqual(ro_uri, expected_child.get_readonly_uri()) + numkids += 1 + + self.failUnlessEqual(numkids, 3) + return self.rootnode.list() + d.addCallback(_check_data) + + # Now when we use the real directory listing code, the mutants should be absent. + def _check_kids(children): + self.failUnlessEqual(sorted(children.keys()), [u"lonely"]) + lonely_node, lonely_metadata = children[u"lonely"] + + self.failUnlessEqual(lonely_node.get_write_uri(), None) + self.failUnlessEqual(lonely_node.get_readonly_uri(), lonely_uri) + d.addCallback(_check_kids) + + d.addCallback(lambda ign: nm.create_from_cap(self.cap.to_string())) + d.addCallback(lambda n: n.list()) + d.addCallback(_check_kids) # again with dirnode recreated from cap + + # Make sure the lonely child can be listed in HTML... + d.addCallback(lambda ign: self.GET(self.rooturl)) + def _check_html(res): + self.failIfIn("URI:SSK", res) + get_lonely = "".join([r'FILE', + r'\s+', + r'lonely' % (urllib.quote(lonely_uri),), + r'', + r'\s+%d' % len("one"), + ]) + self.failUnless(re.search(get_lonely, res), res) + + # find the More Info link for name, should be relative + mo = re.search(r'More Info', res) + info_url = mo.group(1) + self.failUnless(info_url.endswith(urllib.quote(lonely_uri) + "?t=info"), info_url) + d.addCallback(_check_html) + + # ... and in JSON. + d.addCallback(lambda ign: self.GET(self.rooturl+"?t=json")) + def _check_json(res): + data = simplejson.loads(res) + self.failUnlessEqual(data[0], "dirnode") + listed_children = data[1]["children"] + self.failUnlessEqual(sorted(listed_children.keys()), [u"lonely"]) + ll_type, ll_data = listed_children[u"lonely"] + self.failUnlessEqual(ll_type, "filenode") + self.failIf("rw_uri" in ll_data) + self.failUnlessEqual(ll_data["ro_uri"], lonely_uri) + d.addCallback(_check_json) return d def test_deep_check(self): @@ -3178,10 +3440,10 @@ class Grid(GridTestMixin, WebErrorMixin, unittest.TestCase, ShouldFailMixin): # this tests that deep-check and stream-manifest will ignore # UnknownNode instances. Hopefully this will also cover deep-stats. - future_writecap = "x-tahoe-crazy://I_am_from_the_future." - future_readcap = "x-tahoe-crazy-readonly://I_am_from_the_future." - future_node = UnknownNode(future_writecap, future_readcap) - d.addCallback(lambda ign: self.rootnode.set_node(u"future",future_node)) + future_write_uri = "x-tahoe-crazy://I_am_from_the_future." + future_read_uri = "x-tahoe-crazy-readonly://I_am_from_the_future." + future_node = UnknownNode(future_write_uri, future_read_uri) + d.addCallback(lambda ign: self.rootnode.set_node(u"future", future_node)) def _clobber_shares(ignored): self.delete_shares_numbered(self.uris["sick"], [0,1]) diff --git a/src/allmydata/unknown.py b/src/allmydata/unknown.py index 55c84c5b..6b37b146 100644 --- a/src/allmydata/unknown.py +++ b/src/allmydata/unknown.py @@ -1,29 +1,181 @@ + from zope.interface import implements from twisted.internet import defer -from allmydata.interfaces import IFilesystemNode +from allmydata.interfaces import IFilesystemNode, MustNotBeUnknownRWError, \ + MustBeDeepImmutableError +from allmydata import uri +from allmydata.uri import ALLEGED_READONLY_PREFIX, ALLEGED_IMMUTABLE_PREFIX + + +# See ticket #833 for design rationale of UnknownNodes. + +def strip_prefix_for_ro(ro_uri, deep_immutable): + """Strip prefixes when storing an URI in a ro_uri slot.""" + + # It is possible for an alleged-immutable URI to be put into a + # mutable directory. In that case the ALLEGED_IMMUTABLE_PREFIX + # should not be stripped. In other cases, the prefix can safely + # be stripped because it is implied by the context. + + if ro_uri.startswith(ALLEGED_IMMUTABLE_PREFIX): + if not deep_immutable: + return ro_uri + return ro_uri[len(ALLEGED_IMMUTABLE_PREFIX):] + elif ro_uri.startswith(ALLEGED_READONLY_PREFIX): + return ro_uri[len(ALLEGED_READONLY_PREFIX):] + else: + return ro_uri class UnknownNode: implements(IFilesystemNode) - def __init__(self, writecap, readcap): - assert writecap is None or isinstance(writecap, str) - self.writecap = writecap - assert readcap is None or isinstance(readcap, str) - self.readcap = readcap + + def __init__(self, given_rw_uri, given_ro_uri, deep_immutable=False, + name=u""): + assert given_rw_uri is None or isinstance(given_rw_uri, str) + assert given_ro_uri is None or isinstance(given_ro_uri, str) + given_rw_uri = given_rw_uri or None + given_ro_uri = given_ro_uri or None + + # We don't raise errors when creating an UnknownNode; we instead create an + # opaque node (with rw_uri and ro_uri both None) that records the error. + # This avoids breaking operations that never store the opaque node. + # Note that this means that if a stored dirnode has only a rw_uri, it + # might be dropped. Any future "write-only" cap formats should have a dummy + # unusable readcap to stop that from happening. + + self.error = None + self.rw_uri = self.ro_uri = None + if given_rw_uri: + if deep_immutable: + if given_rw_uri.startswith(ALLEGED_IMMUTABLE_PREFIX) and not given_ro_uri: + # We needed an immutable cap, and were given one. It was given in the + # rw_uri slot, but that's fine; we'll move it to ro_uri below. + pass + elif not given_ro_uri: + self.error = MustNotBeUnknownRWError("cannot attach unknown rw cap as immutable child", + name, True) + return # node will be opaque + else: + # We could report either error, but this probably makes more sense. + self.error = MustBeDeepImmutableError("cannot attach unknown rw cap as immutable child", + name) + return # node will be opaque + + if not given_ro_uri: + # We were given a single cap argument, or a rw_uri with no ro_uri. + + if not (given_rw_uri.startswith(ALLEGED_READONLY_PREFIX) + or given_rw_uri.startswith(ALLEGED_IMMUTABLE_PREFIX)): + # If the single cap is unprefixed, then we cannot tell whether it is a + # writecap, and we don't know how to diminish it to a readcap if it is one. + # If it didn't *already* have at least an ALLEGED_READONLY_PREFIX, then + # prefixing it would be a bad idea because we have been given no reason + # to believe that it is a readcap, so we might be letting a client + # inadvertently grant excess write authority. + self.error = MustNotBeUnknownRWError("cannot attach unknown rw cap as child", + name, False) + return # node will be opaque + + # OTOH, if the single cap already had a prefix (which is of the required + # strength otherwise an error would have been thrown above), then treat it + # as though it had been given in the ro_uri slot. This has a similar effect + # to the use for known caps of 'bigcap = writecap or readcap' in + # nodemaker.py: create_from_cap. It enables copying of unknown readcaps to + # work in as many cases as we can securely allow. + given_ro_uri = given_rw_uri + given_rw_uri = None + elif given_ro_uri.startswith(ALLEGED_IMMUTABLE_PREFIX): + # Strange corner case: we were given a cap in both slots, with the ro_uri + # alleged to be immutable. A real immutable object wouldn't have a writecap. + self.error = MustBeDeepImmutableError("cannot accept a child entry that specifies " + "both rw_uri, and ro_uri with an imm. prefix", + name) + return # node will be opaque + + # If the ro_uri definitely fails the constraint, it should be treated as opaque and + # the error recorded. + if given_ro_uri: + read_cap = uri.from_string(given_ro_uri, deep_immutable=deep_immutable, name=name) + if isinstance(read_cap, uri.UnknownURI): + self.error = read_cap.get_error() + if self.error: + assert self.rw_uri is None and self.ro_uri is None + return + + if deep_immutable: + assert self.rw_uri is None + # strengthen the constraint on ro_uri to ALLEGED_IMMUTABLE_PREFIX + if given_ro_uri: + if given_ro_uri.startswith(ALLEGED_IMMUTABLE_PREFIX): + self.ro_uri = given_ro_uri + elif given_ro_uri.startswith(ALLEGED_READONLY_PREFIX): + self.ro_uri = ALLEGED_IMMUTABLE_PREFIX + given_ro_uri[len(ALLEGED_READONLY_PREFIX):] + else: + self.ro_uri = ALLEGED_IMMUTABLE_PREFIX + given_ro_uri + else: + # not immutable, so a writecap is allowed + self.rw_uri = given_rw_uri + # strengthen the constraint on ro_uri to ALLEGED_READONLY_PREFIX + if given_ro_uri: + if (given_ro_uri.startswith(ALLEGED_READONLY_PREFIX) or + given_ro_uri.startswith(ALLEGED_IMMUTABLE_PREFIX)): + self.ro_uri = given_ro_uri + else: + self.ro_uri = ALLEGED_READONLY_PREFIX + given_ro_uri + + def get_cap(self): + return uri.UnknownURI(self.rw_uri or self.ro_uri) + + def get_readcap(self): + return uri.UnknownURI(self.ro_uri) + + def is_readonly(self): + raise AssertionError("an UnknownNode might be either read-only or " + "read/write, so we shouldn't be calling is_readonly") + + def is_mutable(self): + raise AssertionError("an UnknownNode might be either mutable or immutable, " + "so we shouldn't be calling is_mutable") + + def is_unknown(self): + return True + + def is_allowed_in_immutable_directory(self): + # An UnknownNode consisting only of a ro_uri is allowed in an + # immutable directory, even though we do not know that it is + # immutable (or even read-only), provided that no error was detected. + return not self.error and not self.rw_uri + + def raise_error(self): + if self.error is not None: + raise self.error + def get_uri(self): - return self.writecap + return self.rw_uri or self.ro_uri + + def get_write_uri(self): + return self.rw_uri + def get_readonly_uri(self): - return self.readcap + return self.ro_uri + def get_storage_index(self): return None + def get_verify_cap(self): return None + def get_repair_cap(self): return None + def get_size(self): return None + def get_current_size(self): return defer.succeed(None) + def check(self, monitor, verify, add_lease): return defer.succeed(None) + def check_and_repair(self, monitor, verify, add_lease): return defer.succeed(None) diff --git a/src/allmydata/uri.py b/src/allmydata/uri.py index ec897729..b68acb08 100644 --- a/src/allmydata/uri.py +++ b/src/allmydata/uri.py @@ -5,9 +5,10 @@ from twisted.python.components import registerAdapter from allmydata.storage.server import si_a2b, si_b2a from allmydata.util import base32, hashutil from allmydata.interfaces import IURI, IDirnodeURI, IFileURI, IImmutableFileURI, \ - IVerifierURI, IMutableFileURI, IDirectoryURI, IReadonlyDirectoryURI + IVerifierURI, IMutableFileURI, IDirectoryURI, IReadonlyDirectoryURI, \ + MustBeDeepImmutableError, MustBeReadonlyError, CapConstraintError -class BadURIError(Exception): +class BadURIError(CapConstraintError): pass # the URI shall be an ascii representation of the file. It shall contain @@ -71,7 +72,7 @@ class CHKFileURI(_BaseURI): def init_from_human_encoding(cls, uri): mo = cls.HUMAN_RE.search(uri) if not mo: - raise BadURIError("%s doesn't look like a cap" % (uri,)) + raise BadURIError("'%s' doesn't look like a %s cap" % (uri, cls)) return cls(base32.a2b(mo.group(1)), base32.a2b(mo.group(2)), int(mo.group(3)), int(mo.group(4)), int(mo.group(5))) @@ -79,7 +80,7 @@ class CHKFileURI(_BaseURI): def init_from_string(cls, uri): mo = cls.STRING_RE.search(uri) if not mo: - raise BadURIError("%s doesn't look like a cap" % (uri,)) + raise BadURIError("'%s' doesn't look like a %s cap" % (uri, cls)) return cls(base32.a2b(mo.group(1)), base32.a2b(mo.group(2)), int(mo.group(3)), int(mo.group(4)), int(mo.group(5))) @@ -97,8 +98,10 @@ class CHKFileURI(_BaseURI): def is_readonly(self): return True + def is_mutable(self): return False + def get_readonly(self): return self @@ -134,14 +137,16 @@ class CHKFileVerifierURI(_BaseURI): @classmethod def init_from_human_encoding(cls, uri): mo = cls.HUMAN_RE.search(uri) - assert mo, uri + if not mo: + raise BadURIError("'%s' doesn't look like a %s cap" % (uri, cls)) return cls(base32.a2b(mo.group(1)), base32.a2b(mo.group(2)), int(mo.group(3)), int(mo.group(4)), int(mo.group(5))) @classmethod def init_from_string(cls, uri): mo = cls.STRING_RE.search(uri) - assert mo, (uri, cls, cls.STRING_RE) + if not mo: + raise BadURIError("'%s' doesn't look like a %s cap" % (uri, cls)) return cls(si_a2b(mo.group(1)), base32.a2b(mo.group(2)), int(mo.group(3)), int(mo.group(4)), int(mo.group(5))) @@ -157,6 +162,18 @@ class CHKFileVerifierURI(_BaseURI): self.total_shares, self.size)) + def is_readonly(self): + return True + + def is_mutable(self): + return False + + def get_readonly(self): + return self + + def get_verify_cap(self): + return self + class LiteralFileURI(_BaseURI): implements(IURI, IImmutableFileURI) @@ -173,13 +190,15 @@ class LiteralFileURI(_BaseURI): @classmethod def init_from_human_encoding(cls, uri): mo = cls.HUMAN_RE.search(uri) - assert mo, uri + if not mo: + raise BadURIError("'%s' doesn't look like a %s cap" % (uri, cls)) return cls(base32.a2b(mo.group(1))) @classmethod def init_from_string(cls, uri): mo = cls.STRING_RE.search(uri) - assert mo, uri + if not mo: + raise BadURIError("'%s' doesn't look like a %s cap" % (uri, cls)) return cls(base32.a2b(mo.group(1))) def to_string(self): @@ -187,10 +206,13 @@ class LiteralFileURI(_BaseURI): def is_readonly(self): return True + def is_mutable(self): return False + def get_readonly(self): return self + def get_storage_index(self): return None @@ -201,6 +223,7 @@ class LiteralFileURI(_BaseURI): def get_size(self): return len(self.data) + class WriteableSSKFileURI(_BaseURI): implements(IURI, IMutableFileURI) @@ -221,7 +244,7 @@ class WriteableSSKFileURI(_BaseURI): def init_from_human_encoding(cls, uri): mo = cls.HUMAN_RE.search(uri) if not mo: - raise BadURIError("'%s' doesn't look like a cap" % (uri,)) + raise BadURIError("'%s' doesn't look like a %s cap" % (uri, cls)) return cls(base32.a2b(mo.group(1)), base32.a2b(mo.group(2))) @classmethod @@ -242,18 +265,23 @@ class WriteableSSKFileURI(_BaseURI): def abbrev(self): return base32.b2a(self.writekey[:5]) + def abbrev_si(self): return base32.b2a(self.storage_index)[:5] def is_readonly(self): return False + def is_mutable(self): return True + def get_readonly(self): return ReadonlySSKFileURI(self.readkey, self.fingerprint) + def get_verify_cap(self): return SSKVerifierURI(self.storage_index, self.fingerprint) + class ReadonlySSKFileURI(_BaseURI): implements(IURI, IMutableFileURI) @@ -271,14 +299,14 @@ class ReadonlySSKFileURI(_BaseURI): def init_from_human_encoding(cls, uri): mo = cls.HUMAN_RE.search(uri) if not mo: - raise BadURIError("'%s' doesn't look like a cap" % (uri,)) + raise BadURIError("'%s' doesn't look like a %s cap" % (uri, cls)) return cls(base32.a2b(mo.group(1)), base32.a2b(mo.group(2))) @classmethod def init_from_string(cls, uri): mo = cls.STRING_RE.search(uri) if not mo: - raise BadURIError("'%s' doesn't look like a cap" % (uri,)) + raise BadURIError("'%s' doesn't look like a %s cap" % (uri, cls)) return cls(base32.a2b(mo.group(1)), base32.a2b(mo.group(2))) def to_string(self): @@ -292,18 +320,23 @@ class ReadonlySSKFileURI(_BaseURI): def abbrev(self): return base32.b2a(self.readkey[:5]) + def abbrev_si(self): return base32.b2a(self.storage_index)[:5] def is_readonly(self): return True + def is_mutable(self): return True + def get_readonly(self): return self + def get_verify_cap(self): return SSKVerifierURI(self.storage_index, self.fingerprint) + class SSKVerifierURI(_BaseURI): implements(IVerifierURI) @@ -319,13 +352,15 @@ class SSKVerifierURI(_BaseURI): @classmethod def init_from_human_encoding(cls, uri): mo = cls.HUMAN_RE.search(uri) - assert mo, uri + if not mo: + raise BadURIError("'%s' doesn't look like a %s cap" % (uri, cls)) return cls(si_a2b(mo.group(1)), base32.a2b(mo.group(2))) @classmethod def init_from_string(cls, uri): mo = cls.STRING_RE.search(uri) - assert mo, (uri, cls) + if not mo: + raise BadURIError("'%s' doesn't look like a %s cap" % (uri, cls)) return cls(si_a2b(mo.group(1)), base32.a2b(mo.group(2))) def to_string(self): @@ -334,6 +369,18 @@ class SSKVerifierURI(_BaseURI): return 'URI:SSK-Verifier:%s:%s' % (si_b2a(self.storage_index), base32.b2a(self.fingerprint)) + def is_readonly(self): + return True + + def is_mutable(self): + return False + + def get_readonly(self): + return self + + def get_verify_cap(self): + return self + class _DirectoryBaseURI(_BaseURI): implements(IURI, IDirnodeURI) def __init__(self, filenode_uri=None): @@ -373,15 +420,16 @@ class _DirectoryBaseURI(_BaseURI): def abbrev(self): return self._filenode_uri.to_string().split(':')[2][:5] + def abbrev_si(self): return base32.b2a(self._filenode_uri.storage_index)[:5] - def get_filenode_cap(self): - return self._filenode_uri - def is_mutable(self): return True + def get_filenode_cap(self): + return self._filenode_uri + def get_verify_cap(self): return DirectoryURIVerifier(self._filenode_uri.get_verify_cap()) @@ -407,6 +455,7 @@ class DirectoryURI(_DirectoryBaseURI): def get_readonly(self): return ReadonlyDirectoryURI(self._filenode_uri.get_readonly()) + class ReadonlyDirectoryURI(_DirectoryBaseURI): implements(IReadonlyDirectoryURI) @@ -426,26 +475,30 @@ class ReadonlyDirectoryURI(_DirectoryBaseURI): def get_readonly(self): return self + class _ImmutableDirectoryBaseURI(_DirectoryBaseURI): def __init__(self, filenode_uri=None): if filenode_uri: assert isinstance(filenode_uri, self.INNER_URI_CLASS), filenode_uri + assert not filenode_uri.is_mutable() _DirectoryBaseURI.__init__(self, filenode_uri) - def is_mutable(self): - return False - def is_readonly(self): return True + def is_mutable(self): + return False + def get_readonly(self): return self + class ImmutableDirectoryURI(_ImmutableDirectoryBaseURI): BASE_STRING='URI:DIR2-CHK:' BASE_STRING_RE=re.compile('^'+BASE_STRING) BASE_HUMAN_RE=re.compile('^'+OPTIONALHTTPLEAD+'URI'+SEP+'DIR2-CHK'+SEP) INNER_URI_CLASS=CHKFileURI + def get_verify_cap(self): vcap = self._filenode_uri.get_verify_cap() return ImmutableDirectoryURIVerifier(vcap) @@ -456,10 +509,12 @@ class LiteralDirectoryURI(_ImmutableDirectoryBaseURI): BASE_STRING_RE=re.compile('^'+BASE_STRING) BASE_HUMAN_RE=re.compile('^'+OPTIONALHTTPLEAD+'URI'+SEP+'DIR2-LIT'+SEP) INNER_URI_CLASS=LiteralFileURI + def get_verify_cap(self): # LIT caps have no verifier, since they aren't distributed return None + def wrap_dirnode_cap(filecap): if isinstance(filecap, WriteableSSKFileURI): return DirectoryURI(filecap) @@ -469,7 +524,8 @@ def wrap_dirnode_cap(filecap): return ImmutableDirectoryURI(filecap) if isinstance(filecap, LiteralFileURI): return LiteralDirectoryURI(filecap) - assert False, "cannot wrap a dirnode around %s" % filecap.__class__ + assert False, "cannot interpret as a directory cap: %s" % filecap.__class__ + class DirectoryURIVerifier(_DirectoryBaseURI): implements(IVerifierURI) @@ -487,6 +543,10 @@ class DirectoryURIVerifier(_DirectoryBaseURI): def get_filenode_cap(self): return self._filenode_uri + def is_mutable(self): + return False + + class ImmutableDirectoryURIVerifier(DirectoryURIVerifier): implements(IVerifierURI) BASE_STRING='URI:DIR2-CHK-Verifier:' @@ -494,68 +554,146 @@ class ImmutableDirectoryURIVerifier(DirectoryURIVerifier): BASE_HUMAN_RE=re.compile('^'+OPTIONALHTTPLEAD+'URI'+SEP+'DIR2-CHK-VERIFIER'+SEP) INNER_URI_CLASS=CHKFileVerifierURI + class UnknownURI: - def __init__(self, uri): + def __init__(self, uri, error=None): self._uri = uri + self._error = error + def to_string(self): return self._uri -def from_string(s): - if not isinstance(s, str): - raise TypeError("unknown URI type: %s.." % str(s)[:100]) - elif s.startswith('URI:CHK:'): - return CHKFileURI.init_from_string(s) - elif s.startswith('URI:CHK-Verifier:'): - return CHKFileVerifierURI.init_from_string(s) - elif s.startswith('URI:LIT:'): - return LiteralFileURI.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 DirectoryURI.init_from_string(s) - elif s.startswith('URI:DIR2-RO:'): - return ReadonlyDirectoryURI.init_from_string(s) - elif s.startswith('URI:DIR2-Verifier:'): - return DirectoryURIVerifier.init_from_string(s) - elif s.startswith('URI:DIR2-CHK:'): - return ImmutableDirectoryURI.init_from_string(s) - elif s.startswith('URI:DIR2-LIT:'): - return LiteralDirectoryURI.init_from_string(s) - return UnknownURI(s) + def get_readonly(self): + return None + + def get_error(self): + return self._error + + def get_verify_cap(self): + return None + + +ALLEGED_READONLY_PREFIX = 'ro.' +ALLEGED_IMMUTABLE_PREFIX = 'imm.' + +def from_string(u, deep_immutable=False, name=u""): + if not isinstance(u, str): + raise TypeError("unknown URI type: %s.." % str(u)[:100]) + + # We allow and check ALLEGED_READONLY_PREFIX or ALLEGED_IMMUTABLE_PREFIX + # on all URIs, even though we would only strictly need to do so for caps of + # new formats (post Tahoe-LAFS 1.6). URIs that are not consistent with their + # prefix are treated as unknown. This should be revisited when we add the + # new cap formats. See . + s = u + can_be_mutable = can_be_writeable = not deep_immutable + if s.startswith(ALLEGED_IMMUTABLE_PREFIX): + can_be_mutable = can_be_writeable = False + s = s[len(ALLEGED_IMMUTABLE_PREFIX):] + elif s.startswith(ALLEGED_READONLY_PREFIX): + can_be_writeable = False + s = s[len(ALLEGED_READONLY_PREFIX):] + + error = None + kind = "cap" + try: + if s.startswith('URI:CHK:'): + return CHKFileURI.init_from_string(s) + elif s.startswith('URI:CHK-Verifier:'): + return CHKFileVerifierURI.init_from_string(s) + elif s.startswith('URI:LIT:'): + return LiteralFileURI.init_from_string(s) + elif s.startswith('URI:SSK:'): + if can_be_writeable: + return WriteableSSKFileURI.init_from_string(s) + kind = "URI:SSK file writecap" + elif s.startswith('URI:SSK-RO:'): + if can_be_mutable: + return ReadonlySSKFileURI.init_from_string(s) + kind = "URI:SSK-RO readcap to a mutable file" + elif s.startswith('URI:SSK-Verifier:'): + return SSKVerifierURI.init_from_string(s) + elif s.startswith('URI:DIR2:'): + if can_be_writeable: + return DirectoryURI.init_from_string(s) + kind = "URI:DIR2 directory writecap" + elif s.startswith('URI:DIR2-RO:'): + if can_be_mutable: + return ReadonlyDirectoryURI.init_from_string(s) + kind = "URI:DIR2-RO readcap to a mutable directory" + elif s.startswith('URI:DIR2-Verifier:'): + return DirectoryURIVerifier.init_from_string(s) + elif s.startswith('URI:DIR2-CHK:'): + return ImmutableDirectoryURI.init_from_string(s) + elif s.startswith('URI:DIR2-LIT:'): + return LiteralDirectoryURI.init_from_string(s) + elif s.startswith('x-tahoe-future-test-writeable:') and not can_be_writeable: + # For testing how future writeable caps would behave in read-only contexts. + kind = "x-tahoe-future-test-writeable: testing cap" + elif s.startswith('x-tahoe-future-test-mutable:') and not can_be_mutable: + # For testing how future mutable readcaps would behave in immutable contexts. + kind = "x-tahoe-future-test-mutable: testing cap" + else: + return UnknownURI(u) + + # We fell through because a constraint was not met. + # Prefer to report the most specific constraint. + if not can_be_mutable: + error = MustBeDeepImmutableError(kind + " used in an immutable context", name) + else: + error = MustBeReadonlyError(kind + " used in a read-only context", name) + + except BadURIError, e: + error = e + + return UnknownURI(u, error=error) def is_uri(s): try: - from_string(s) + from_string(s, deep_immutable=False) return True except (TypeError, AssertionError): return False -def from_string_dirnode(s): - u = from_string(s) +def is_literal_file_uri(s): + if not isinstance(s, str): + return False + return (s.startswith('URI:LIT:') or + s.startswith(ALLEGED_READONLY_PREFIX + 'URI:LIT:') or + s.startswith(ALLEGED_IMMUTABLE_PREFIX + 'URI:LIT:')) + +def has_uri_prefix(s): + if not isinstance(s, str): + return False + return (s.startswith("URI:") or + s.startswith(ALLEGED_READONLY_PREFIX + 'URI:') or + s.startswith(ALLEGED_IMMUTABLE_PREFIX + 'URI:')) + + +# These take the same keyword arguments as from_string above. + +def from_string_dirnode(s, **kwargs): + u = from_string(s, **kwargs) assert IDirnodeURI.providedBy(u) return u registerAdapter(from_string_dirnode, str, IDirnodeURI) -def from_string_filenode(s): - u = from_string(s) +def from_string_filenode(s, **kwargs): + u = from_string(s, **kwargs) assert IFileURI.providedBy(u) return u registerAdapter(from_string_filenode, str, IFileURI) -def from_string_mutable_filenode(s): - u = from_string(s) +def from_string_mutable_filenode(s, **kwargs): + u = from_string(s, **kwargs) assert IMutableFileURI.providedBy(u) return u registerAdapter(from_string_mutable_filenode, str, IMutableFileURI) -def from_string_verifier(s): - u = from_string(s) +def from_string_verifier(s, **kwargs): + u = from_string(s, **kwargs) assert IVerifierURI.providedBy(u) return u registerAdapter(from_string_verifier, str, IVerifierURI) diff --git a/src/allmydata/web/common.py b/src/allmydata/web/common.py index 55c5c64a..902d332f 100644 --- a/src/allmydata/web/common.py +++ b/src/allmydata/web/common.py @@ -8,7 +8,8 @@ from nevow.inevow import IRequest from nevow.util import resource_filename from allmydata.interfaces import ExistingChildError, NoSuchChildError, \ FileTooLargeError, NotEnoughSharesError, NoSharesError, \ - NotDeepImmutableError, EmptyPathnameComponentError + EmptyPathnameComponentError, MustBeDeepImmutableError, \ + MustBeReadonlyError, MustNotBeUnknownRWError from allmydata.mutable.common import UnrecoverableFileError from allmydata.util import abbreviate # TODO: consolidate @@ -181,9 +182,42 @@ def humanize_failure(f): "failure, or disk corruption. You should perform a filecheck on " "this object to learn more.") return (t, http.GONE) - if f.check(NotDeepImmutableError): - t = ("NotDeepImmutableError: a mkdir-immutable operation was given " - "a child that was not itself immutable: %s" % (f.value,)) + if f.check(MustNotBeUnknownRWError): + name = f.value.args[1] + immutable = f.value.args[2] + if immutable: + t = ("MustNotBeUnknownRWError: an operation to add a child named " + "'%s' to a directory was given an unknown cap in a write slot.\n" + "If the cap is actually an immutable readcap, then using a " + "webapi server that supports a later version of Tahoe may help.\n\n" + "If you are using the webapi directly, then specifying an immutable " + "readcap in the read slot (ro_uri) of the JSON PROPDICT, and " + "omitting the write slot (rw_uri), would also work in this " + "case.") % name.encode("utf-8") + else: + t = ("MustNotBeUnknownRWError: an operation to add a child named " + "'%s' to a directory was given an unknown cap in a write slot.\n" + "Using a webapi server that supports a later version of Tahoe " + "may help.\n\n" + "If you are using the webapi directly, specifying a readcap in " + "the read slot (ro_uri) of the JSON PROPDICT, as well as a " + "writecap in the write slot if desired, would also work in this " + "case.") % name.encode("utf-8") + return (t, http.BAD_REQUEST) + if f.check(MustBeDeepImmutableError): + name = f.value.args[1] + t = ("MustBeDeepImmutableError: a cap passed to this operation for " + "the child named '%s', needed to be immutable but was not. Either " + "the cap is being added to an immutable directory, or it was " + "originally retrieved from an immutable directory as an unknown " + "cap." % name.encode("utf-8")) + return (t, http.BAD_REQUEST) + if f.check(MustBeReadonlyError): + name = f.value.args[1] + t = ("MustBeReadonlyError: a cap passed to this operation for " + "the child named '%s', needed to be read-only but was not. " + "The cap is being passed in a read slot (ro_uri), or was retrieved " + "from a read slot as an unknown cap." % name.encode("utf-8")) return (t, http.BAD_REQUEST) if f.check(WebError): return (f.value.text, f.value.code) diff --git a/src/allmydata/web/directory.py b/src/allmydata/web/directory.py index 299421f1..c6a38ffd 100644 --- a/src/allmydata/web/directory.py +++ b/src/allmydata/web/directory.py @@ -352,7 +352,12 @@ class DirectoryNodeHandler(RenderMixin, rend.Page, ReplaceMeMixin): charset = get_arg(req, "_charset", "utf-8") name = name.decode(charset) replace = boolean_of_arg(get_arg(req, "replace", "true")) - d = self.node.set_uri(name, childcap, childcap, overwrite=replace) + + # We mustn't pass childcap for the readcap argument because we don't + # know whether it is a read cap. Passing a read cap as the writecap + # argument will work (it ends up calling NodeMaker.create_from_cap, + # which derives a readcap if necessary and possible). + d = self.node.set_uri(name, childcap, None, overwrite=replace) d.addCallback(lambda res: childcap) return d @@ -363,9 +368,9 @@ class DirectoryNodeHandler(RenderMixin, rend.Page, ReplaceMeMixin): # won't show up in the resulting encoded form.. the 'name' # field is completely missing. So to allow deletion of an # empty file, we have to pretend that None means ''. The only - # downide of this is a slightly confusing error message if + # downside of this is a slightly confusing error message if # someone does a POST without a name= field. For our own HTML - # thisn't a big deal, because we create the 'delete' POST + # this isn't a big deal, because we create the 'delete' POST # buttons ourselves. name = '' charset = get_arg(req, "_charset", "utf-8") @@ -585,7 +590,11 @@ class DirectoryAsHTML(rend.Page): def render_title(self, ctx, data): si_s = abbreviated_dirnode(self.node) header = ["Tahoe-LAFS - Directory SI=%s" % si_s] - if self.node.is_readonly(): + if self.node.is_unknown(): + header.append(" (unknown)") + elif not self.node.is_mutable(): + header.append(" (immutable)") + elif self.node.is_readonly(): header.append(" (read-only)") else: header.append(" (modifiable)") @@ -594,7 +603,11 @@ class DirectoryAsHTML(rend.Page): def render_header(self, ctx, data): si_s = abbreviated_dirnode(self.node) header = ["Tahoe-LAFS Directory SI=", T.span(class_="data-chars")[si_s]] - if self.node.is_readonly(): + if self.node.is_unknown(): + header.append(" (unknown)") + elif not self.node.is_mutable(): + header.append(" (immutable)") + elif self.node.is_readonly(): header.append(" (read-only)") return ctx.tag[header] @@ -603,7 +616,7 @@ class DirectoryAsHTML(rend.Page): return T.div[T.a(href=link)["Return to Welcome page"]] def render_show_readonly(self, ctx, data): - if self.node.is_readonly(): + if self.node.is_unknown() or self.node.is_readonly(): return "" rocap = self.node.get_readonly_uri() root = get_root(ctx) @@ -630,7 +643,7 @@ class DirectoryAsHTML(rend.Page): root = get_root(ctx) here = "%s/uri/%s/" % (root, urllib.quote(self.node.get_uri())) - if self.node.is_readonly(): + if self.node.is_unknown() or self.node.is_readonly(): delete = "-" rename = "-" else: @@ -678,8 +691,8 @@ class DirectoryAsHTML(rend.Page): ctx.fillSlots("times", times) assert IFilesystemNode.providedBy(target), target - writecap = target.get_uri() or "" - quoted_uri = urllib.quote(writecap, safe="") # escape slashes too + target_uri = target.get_uri() or "" + quoted_uri = urllib.quote(target_uri, safe="") # escape slashes too if IMutableFileNode.providedBy(target): # to prevent javascript in displayed .html files from stealing a @@ -708,7 +721,7 @@ class DirectoryAsHTML(rend.Page): elif IDirectoryNode.providedBy(target): # directory - uri_link = "%s/uri/%s/" % (root, urllib.quote(writecap)) + uri_link = "%s/uri/%s/" % (root, urllib.quote(target_uri)) ctx.fillSlots("filename", T.a(href=uri_link)[html.escape(name)]) if not target.is_mutable(): @@ -795,35 +808,30 @@ def DirectoryJSONMetadata(ctx, dirnode): kids = {} for name, (childnode, metadata) in children.iteritems(): assert IFilesystemNode.providedBy(childnode), childnode - rw_uri = childnode.get_uri() + rw_uri = childnode.get_write_uri() ro_uri = childnode.get_readonly_uri() if IFileNode.providedBy(childnode): - if childnode.is_readonly(): - rw_uri = None kiddata = ("filenode", {'size': childnode.get_size(), 'mutable': childnode.is_mutable(), }) elif IDirectoryNode.providedBy(childnode): - if childnode.is_readonly(): - rw_uri = None kiddata = ("dirnode", {'mutable': childnode.is_mutable()}) else: kiddata = ("unknown", {}) + kiddata[1]["metadata"] = metadata - if ro_uri: - kiddata[1]["ro_uri"] = ro_uri if rw_uri: kiddata[1]["rw_uri"] = rw_uri + if ro_uri: + kiddata[1]["ro_uri"] = ro_uri verifycap = childnode.get_verify_cap() if verifycap: kiddata[1]['verify_uri'] = verifycap.to_string() + kids[name] = kiddata - if dirnode.is_readonly(): - drw_uri = None - dro_uri = dirnode.get_uri() - else: - drw_uri = dirnode.get_uri() - dro_uri = dirnode.get_readonly_uri() + + drw_uri = dirnode.get_write_uri() + dro_uri = dirnode.get_readonly_uri() contents = { 'children': kids } if dro_uri: contents['ro_uri'] = dro_uri @@ -834,13 +842,13 @@ def DirectoryJSONMetadata(ctx, dirnode): contents['verify_uri'] = verifycap.to_string() contents['mutable'] = dirnode.is_mutable() data = ("dirnode", contents) - return simplejson.dumps(data, indent=1) + "\n" + json = simplejson.dumps(data, indent=1) + "\n" + return json d.addCallback(_got) d.addCallback(text_plain, ctx) return d - def DirectoryURI(ctx, dirnode): return text_plain(dirnode.get_uri(), ctx) @@ -1132,18 +1140,39 @@ class DeepCheckStreamer(dirnode.DeepStats): self.req.write(j+"\n") return "" -class UnknownNodeHandler(RenderMixin, rend.Page): +class UnknownNodeHandler(RenderMixin, rend.Page): def __init__(self, client, node, parentnode=None, name=None): rend.Page.__init__(self) assert node self.node = node + self.parentnode = parentnode + self.name = name def render_GET(self, ctx): req = IRequest(ctx) t = get_arg(req, "t", "").strip() if t == "info": return MoreInfo(self.node) - raise WebError("GET unknown URI type: can only do t=info, not t=%s" % t) - - + if t == "json": + if self.parentnode and self.name: + d = self.parentnode.get_metadata_for(self.name) + else: + d = defer.succeed(None) + d.addCallback(lambda md: UnknownJSONMetadata(ctx, self.node, md)) + return d + raise WebError("GET unknown URI type: can only do t=info and t=json, not t=%s.\n" + "Using a webapi server that supports a later version of Tahoe " + "may help." % t) + +def UnknownJSONMetadata(ctx, filenode, edge_metadata): + rw_uri = filenode.get_write_uri() + ro_uri = filenode.get_readonly_uri() + data = ("unknown", {}) + if ro_uri: + data[1]['ro_uri'] = ro_uri + if rw_uri: + data[1]['rw_uri'] = rw_uri + if edge_metadata is not None: + data[1]['metadata'] = edge_metadata + return text_plain(simplejson.dumps(data, indent=1) + "\n", ctx) diff --git a/src/allmydata/web/filenode.py b/src/allmydata/web/filenode.py index a2a1a6a9..d8a334c6 100644 --- a/src/allmydata/web/filenode.py +++ b/src/allmydata/web/filenode.py @@ -6,10 +6,9 @@ from twisted.internet import defer from nevow import url, rend from nevow.inevow import IRequest -from allmydata.interfaces import ExistingChildError, CannotPackUnknownNodeError +from allmydata.interfaces import ExistingChildError from allmydata.monitor import Monitor from allmydata.immutable.upload import FileHandle -from allmydata.unknown import UnknownNode from allmydata.util import log, base32 from allmydata.web.common import text_plain, WebError, RenderMixin, \ @@ -20,7 +19,6 @@ from allmydata.web.check_results import CheckResults, \ from allmydata.web.info import MoreInfo class ReplaceMeMixin: - def replace_me_with_a_child(self, req, client, replace): # a new file is being uploaded in our place. mutable = boolean_of_arg(get_arg(req, "mutable", "false")) @@ -55,14 +53,7 @@ class ReplaceMeMixin: def replace_me_with_a_childcap(self, req, client, replace): req.content.seek(0) childcap = req.content.read() - childnode = client.create_node_from_uri(childcap, childcap+"readonly") - if isinstance(childnode, UnknownNode): - # don't be willing to pack unknown nodes: we might accidentally - # put some write-authority into the rocap slot because we don't - # know how to diminish the URI they gave us. We don't even know - # if they gave us a readcap or a writecap. - msg = "cannot attach unknown node as child %s" % str(self.name) - raise CannotPackUnknownNodeError(msg) + childnode = client.create_node_from_uri(childcap, None, name=self.name) d = self.parentnode.set_node(self.name, childnode, overwrite=replace) d.addCallback(lambda res: childnode.get_uri()) return d @@ -426,12 +417,8 @@ class FileDownloader(rend.Page): def FileJSONMetadata(ctx, filenode, edge_metadata): - if filenode.is_readonly(): - rw_uri = None - ro_uri = filenode.get_uri() - else: - rw_uri = filenode.get_uri() - ro_uri = filenode.get_readonly_uri() + rw_uri = filenode.get_write_uri() + ro_uri = filenode.get_readonly_uri() data = ("filenode", {}) data[1]['size'] = filenode.get_size() if ro_uri: diff --git a/src/allmydata/web/info.py b/src/allmydata/web/info.py index c00465f3..ae09f7b8 100644 --- a/src/allmydata/web/info.py +++ b/src/allmydata/web/info.py @@ -21,6 +21,8 @@ class MoreInfo(rend.Page): def get_type(self): node = self.original if IDirectoryNode.providedBy(node): + if not node.is_mutable(): + return "immutable directory" return "directory" if IFileNode.providedBy(node): si = node.get_storage_index() @@ -28,7 +30,7 @@ class MoreInfo(rend.Page): if node.is_mutable(): return "mutable file" return "immutable file" - return "LIT file" + return "immutable LIT file" return "unknown" def render_title(self, ctx, data): @@ -68,10 +70,10 @@ class MoreInfo(rend.Page): def render_directory_writecap(self, ctx, data): node = self.original - if node.is_readonly(): - return "" if not IDirectoryNode.providedBy(node): return "" + if node.is_readonly(): + return "" return ctx.tag[node.get_uri()] def render_directory_readcap(self, ctx, data): @@ -86,27 +88,23 @@ class MoreInfo(rend.Page): return "" return ctx.tag[node.get_verify_cap().to_string()] - def render_file_writecap(self, ctx, data): node = self.original if IDirectoryNode.providedBy(node): node = node._node - if ((IDirectoryNode.providedBy(node) or IFileNode.providedBy(node)) - and node.is_readonly()): - return "" - writecap = node.get_uri() - if not writecap: + write_uri = node.get_write_uri() + if not write_uri: return "" - return ctx.tag[writecap] + return ctx.tag[write_uri] def render_file_readcap(self, ctx, data): node = self.original if IDirectoryNode.providedBy(node): node = node._node - readcap = node.get_readonly_uri() - if not readcap: + read_uri = node.get_readonly_uri() + if not read_uri: return "" - return ctx.tag[readcap] + return ctx.tag[read_uri] def render_file_verifycap(self, ctx, data): node = self.original diff --git a/src/allmydata/web/root.py b/src/allmydata/web/root.py index 04e4b112..ee1cd921 100644 --- a/src/allmydata/web/root.py +++ b/src/allmydata/web/root.py @@ -12,7 +12,7 @@ import allmydata # to display import path from allmydata import get_package_versions_string from allmydata import provisioning from allmydata.util import idlib, log -from allmydata.interfaces import IFileNode, UnhandledCapTypeError +from allmydata.interfaces import IFileNode from allmydata.web import filenode, directory, unlinked, status, operations from allmydata.web import reliability, storage from allmydata.web.common import abbreviate_size, getxmlfile, WebError, \ @@ -85,7 +85,7 @@ class URIHandler(RenderMixin, rend.Page): try: node = self.client.create_node_from_uri(name) return directory.make_handler_for(node, self.client) - except (TypeError, UnhandledCapTypeError, AssertionError): + except (TypeError, AssertionError): raise WebError("'%s' is not a valid file- or directory- cap" % name) @@ -104,7 +104,7 @@ class FileHandler(rend.Page): # 'name' must be a file URI try: node = self.client.create_node_from_uri(name) - except (TypeError, UnhandledCapTypeError, AssertionError): + except (TypeError, AssertionError): # I think this can no longer be reached raise WebError("'%s' is not a valid file- or directory- cap" % name)