ftp/sftp: move to a new frontends/ directory in preparation for factoring out passwor...
authorBrian Warner <warner@allmydata.com>
Wed, 5 Nov 2008 21:07:33 +0000 (14:07 -0700)
committerBrian Warner <warner@allmydata.com>
Wed, 5 Nov 2008 21:07:33 +0000 (14:07 -0700)
src/allmydata/client.py
src/allmydata/frontends/__init__.py [new file with mode: 0644]
src/allmydata/frontends/ftpd.py [new file with mode: 0644]
src/allmydata/frontends/sftpd.py [new file with mode: 0644]
src/allmydata/ftpd.py [deleted file]
src/allmydata/sftpd.py [deleted file]

index 418ba761db4ad0b4e8e0f7f8a2196292b2bc33e2..1574615f77837f90e5bc77d07d0bb7e3b027be21 100644 (file)
@@ -267,7 +267,7 @@ class Client(node.Node, pollmixin.PollMixin):
             accounturl = self.get_config("ftpd", "ftp.accounts.url", None)
             ftp_portstr = self.get_config("ftpd", "ftp.port", "8021")
 
-            from allmydata import ftpd
+            from allmydata.frontends import ftpd
             s = ftpd.FTPServer(self, accountfile, accounturl, ftp_portstr)
             s.setServiceParent(self)
 
@@ -279,7 +279,7 @@ class Client(node.Node, pollmixin.PollMixin):
             pubkey_file = self.get_config("sftpd", "sftp.host_pubkey_file")
             privkey_file = self.get_config("sftpd", "sftp.host_privkey_file")
 
-            from allmydata import sftpd
+            from allmydata.frontends import sftpd
             s = sftpd.SFTPServer(self, accountfile, accounturl,
                                  sftp_portstr, pubkey_file, privkey_file)
             s.setServiceParent(self)
diff --git a/src/allmydata/frontends/__init__.py b/src/allmydata/frontends/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/src/allmydata/frontends/ftpd.py b/src/allmydata/frontends/ftpd.py
new file mode 100644 (file)
index 0000000..5ade0e2
--- /dev/null
@@ -0,0 +1,394 @@
+
+import os
+import tempfile
+from zope.interface import implements
+from twisted.application import service, strports
+from twisted.internet import defer
+from twisted.internet.interfaces import IConsumer
+from twisted.protocols import ftp
+from twisted.cred import error, portal, checkers, credentials
+from twisted.web.client import getPage
+
+from allmydata.interfaces import IDirectoryNode, ExistingChildError, \
+     NoSuchChildError
+from allmydata.immutable.download import ConsumerAdapter
+from allmydata.immutable.upload import FileHandle
+from allmydata.util import base32
+
+class ReadFile:
+    implements(ftp.IReadFile)
+    def __init__(self, node):
+        self.node = node
+    def send(self, consumer):
+        ad = ConsumerAdapter(consumer)
+        d = self.node.download(ad)
+        return d # when consumed
+
+class FileWriter:
+    implements(IConsumer)
+
+    def registerProducer(self, producer, streaming):
+        if not streaming:
+            raise NotImplementedError("Non-streaming producer not supported.")
+        # we write the data to a temporary file, since Tahoe can't do
+        # streaming upload yet.
+        self.f = tempfile.TemporaryFile()
+        return None
+
+    def unregisterProducer(self):
+        # the upload actually happens in WriteFile.close()
+        pass
+
+    def write(self, data):
+        self.f.write(data)
+
+class WriteFile:
+    implements(ftp.IWriteFile)
+
+    def __init__(self, parent, childname, convergence):
+        self.parent = parent
+        self.childname = childname
+        self.convergence = convergence
+
+    def receive(self):
+        self.c = FileWriter()
+        return defer.succeed(self.c)
+
+    def close(self):
+        u = FileHandle(self.c.f, self.convergence)
+        d = self.parent.add_file(self.childname, u)
+        return d
+
+
+class NoParentError(Exception):
+    pass
+
+class Handler:
+    implements(ftp.IFTPShell)
+    def __init__(self, client, rootnode, username, convergence):
+        self.client = client
+        self.root = rootnode
+        self.username = username
+        self.convergence = convergence
+
+    def makeDirectory(self, path):
+        d = self._get_root(path)
+        d.addCallback(lambda (root,path):
+                      self._get_or_create_directories(root, path))
+        return d
+
+    def _get_or_create_directories(self, node, path):
+        if not IDirectoryNode.providedBy(node):
+            # unfortunately it is too late to provide the name of the
+            # blocking directory in the error message.
+            raise ftp.FileExistsError("cannot create directory because there "
+                                      "is a file in the way")
+        if not path:
+            return defer.succeed(node)
+        d = node.get(path[0])
+        def _maybe_create(f):
+            f.trap(NoSuchChildError)
+            return node.create_empty_directory(path[0])
+        d.addErrback(_maybe_create)
+        d.addCallback(self._get_or_create_directories, path[1:])
+        return d
+
+    def _get_parent(self, path):
+        # fire with (parentnode, childname)
+        path = [unicode(p) for p in path]
+        if not path:
+            raise NoParentError
+        childname = path[-1]
+        d = self._get_root(path)
+        def _got_root((root, path)):
+            if not path:
+                raise NoParentError
+            return root.get_child_at_path(path[:-1])
+        d.addCallback(_got_root)
+        def _got_parent(parent):
+            return (parent, childname)
+        d.addCallback(_got_parent)
+        return d
+
+    def _remove_thing(self, path, must_be_directory=False, must_be_file=False):
+        d = defer.maybeDeferred(self._get_parent, path)
+        def _convert_error(f):
+            f.trap(NoParentError)
+            raise ftp.PermissionDeniedError("cannot delete root directory")
+        d.addErrback(_convert_error)
+        def _got_parent( (parent, childname) ):
+            d = parent.get(childname)
+            def _got_child(child):
+                if must_be_directory and not IDirectoryNode.providedBy(child):
+                    raise ftp.IsNotADirectoryError("rmdir called on a file")
+                if must_be_file and IDirectoryNode.providedBy(child):
+                    raise ftp.IsADirectoryError("rmfile called on a directory")
+                return parent.delete(childname)
+            d.addCallback(_got_child)
+            d.addErrback(self._convert_error)
+            return d
+        d.addCallback(_got_parent)
+        return d
+
+    def removeDirectory(self, path):
+        return self._remove_thing(path, must_be_directory=True)
+
+    def removeFile(self, path):
+        return self._remove_thing(path, must_be_file=True)
+
+    def rename(self, fromPath, toPath):
+        # the target directory must already exist
+        d = self._get_parent(fromPath)
+        def _got_from_parent( (fromparent, childname) ):
+            d = self._get_parent(toPath)
+            d.addCallback(lambda (toparent, tochildname):
+                          fromparent.move_child_to(childname,
+                                                   toparent, tochildname,
+                                                   overwrite=False))
+            return d
+        d.addCallback(_got_from_parent)
+        d.addErrback(self._convert_error)
+        return d
+
+    def access(self, path):
+        # we allow access to everything that exists. We are required to raise
+        # an error for paths that don't exist: FTP clients (at least ncftp)
+        # uses this to decide whether to mkdir or not.
+        d = self._get_node_and_metadata_for_path(path)
+        d.addErrback(self._convert_error)
+        d.addCallback(lambda res: None)
+        return d
+
+    def _convert_error(self, f):
+        if f.check(NoSuchChildError):
+            childname = f.value.args[0].encode("utf-8")
+            msg = "'%s' doesn't exist" % childname
+            raise ftp.FileNotFoundError(msg)
+        if f.check(ExistingChildError):
+            msg = f.value.args[0].encode("utf-8")
+            raise ftp.FileExistsError(msg)
+        return f
+
+    def _get_root(self, path):
+        # return (root, remaining_path)
+        path = [unicode(p) for p in path]
+        if path and path[0] == "uri":
+            d = defer.maybeDeferred(self.client.create_node_from_uri,
+                                    str(path[1]))
+            d.addCallback(lambda root: (root, path[2:]))
+        else:
+            d = defer.succeed((self.root,path))
+        return d
+
+    def _get_node_and_metadata_for_path(self, path):
+        d = self._get_root(path)
+        def _got_root((root,path)):
+            if path:
+                return root.get_child_and_metadata_at_path(path)
+            else:
+                return (root,{})
+        d.addCallback(_got_root)
+        return d
+
+    def _populate_row(self, keys, (childnode, metadata)):
+        values = []
+        isdir = bool(IDirectoryNode.providedBy(childnode))
+        for key in keys:
+            if key == "size":
+                if isdir:
+                    value = 0
+                else:
+                    value = childnode.get_size()
+            elif key == "directory":
+                value = isdir
+            elif key == "permissions":
+                value = 0600
+            elif key == "hardlinks":
+                value = 1
+            elif key == "modified":
+                value = metadata.get("mtime", 0)
+            elif key == "owner":
+                value = self.username
+            elif key == "group":
+                value = self.username
+            else:
+                value = "??"
+            values.append(value)
+        return values
+
+    def stat(self, path, keys=()):
+        # for files only, I think
+        d = self._get_node_and_metadata_for_path(path)
+        def _render((node,metadata)):
+            assert not IDirectoryNode.providedBy(node)
+            return self._populate_row(keys, (node,metadata))
+        d.addCallback(_render)
+        d.addErrback(self._convert_error)
+        return d
+
+    def list(self, path, keys=()):
+        # the interface claims that path is a list of unicodes, but in
+        # practice it is not
+        d = self._get_node_and_metadata_for_path(path)
+        def _list((node, metadata)):
+            if IDirectoryNode.providedBy(node):
+                return node.list()
+            return { path[-1]: (node, metadata) } # need last-edge metadata
+        d.addCallback(_list)
+        def _render(children):
+            results = []
+            for (name, childnode) in children.iteritems():
+                # the interface claims that the result should have a unicode
+                # object as the name, but it fails unless you give it a
+                # bytestring
+                results.append( (name.encode("utf-8"),
+                                 self._populate_row(keys, childnode) ) )
+            return results
+        d.addCallback(_render)
+        d.addErrback(self._convert_error)
+        return d
+
+    def openForReading(self, path):
+        d = self._get_node_and_metadata_for_path(path)
+        d.addCallback(lambda (node,metadata): ReadFile(node))
+        d.addErrback(self._convert_error)
+        return d
+
+    def openForWriting(self, path):
+        path = [unicode(p) for p in path]
+        if not path:
+            raise ftp.PermissionDeniedError("cannot STOR to root directory")
+        childname = path[-1]
+        d = self._get_root(path)
+        def _got_root((root, path)):
+            if not path:
+                raise ftp.PermissionDeniedError("cannot STOR to root directory")
+            return root.get_child_at_path(path[:-1])
+        d.addCallback(_got_root)
+        def _got_parent(parent):
+            return WriteFile(parent, childname, self.convergence)
+        d.addCallback(_got_parent)
+        return d
+
+
+class FTPAvatarID:
+    def __init__(self, username, rootcap):
+        self.username = username
+        self.rootcap = rootcap
+
+class AccountFileChecker:
+    implements(checkers.ICredentialsChecker)
+    credentialInterfaces = (credentials.IUsernamePassword,
+                            credentials.IUsernameHashedPassword)
+    def __init__(self, client, accountfile):
+        self.client = client
+        self.passwords = {}
+        self.rootcaps = {}
+        for line in open(os.path.expanduser(accountfile), "r"):
+            line = line.strip()
+            if line.startswith("#") or not line:
+                continue
+            name, passwd, rootcap = line.split()
+            self.passwords[name] = passwd
+            self.rootcaps[name] = rootcap
+
+    def _cbPasswordMatch(self, matched, username):
+        if matched:
+            return FTPAvatarID(username, self.rootcaps[username])
+        raise error.UnauthorizedLogin
+
+    def requestAvatarId(self, credentials):
+        if credentials.username in self.passwords:
+            d = defer.maybeDeferred(credentials.checkPassword,
+                                    self.passwords[credentials.username])
+            d.addCallback(self._cbPasswordMatch, str(credentials.username))
+            return d
+        return defer.fail(error.UnauthorizedLogin())
+
+class AccountURLChecker:
+    implements(checkers.ICredentialsChecker)
+    credentialInterfaces = (credentials.IUsernamePassword,)
+
+    def __init__(self, client, auth_url):
+        self.client = client
+        self.auth_url = auth_url
+
+    def _cbPasswordMatch(self, rootcap, username):
+        return FTPAvatarID(username, rootcap)
+
+    def post_form(self, username, password):
+        sepbase = base32.b2a(os.urandom(4))
+        sep = "--" + sepbase
+        form = []
+        form.append(sep)
+        fields = {"action": "authenticate",
+                  "email": username,
+                  "passwd": password,
+                  }
+        for name, value in fields.iteritems():
+            form.append('Content-Disposition: form-data; name="%s"' % name)
+            form.append('')
+            assert isinstance(value, str)
+            form.append(value)
+            form.append(sep)
+        form[-1] += "--"
+        body = "\r\n".join(form) + "\r\n"
+        headers = {"content-type": "multipart/form-data; boundary=%s" % sepbase,
+                   }
+        return getPage(self.auth_url, method="POST",
+                       postdata=body, headers=headers,
+                       followRedirect=True, timeout=30)
+
+    def _parse_response(self, res):
+        rootcap = res.strip()
+        if rootcap == "0":
+            raise error.UnauthorizedLogin
+        return rootcap
+
+    def requestAvatarId(self, credentials):
+        # construct a POST to the login form. While this could theoretically
+        # be done with something like the stdlib 'email' package, I can't
+        # figure out how, so we just slam together a form manually.
+        d = self.post_form(credentials.username, credentials.password)
+        d.addCallback(self._parse_response)
+        d.addCallback(self._cbPasswordMatch, str(credentials.username))
+        return d
+
+
+class Dispatcher:
+    implements(portal.IRealm)
+    def __init__(self, client):
+        self.client = client
+
+    def requestAvatar(self, avatarID, mind, interface):
+        assert interface == ftp.IFTPShell
+        rootnode = self.client.create_node_from_uri(avatarID.rootcap)
+        convergence = self.client.convergence
+        s = Handler(self.client, rootnode, avatarID.username, convergence)
+        def logout(): pass
+        return (interface, s, None)
+
+
+class FTPServer(service.MultiService):
+    def __init__(self, client, accountfile, accounturl, ftp_portstr):
+        service.MultiService.__init__(self)
+
+        if accountfile:
+            c = AccountFileChecker(self, accountfile)
+        elif accounturl:
+            c = AccountURLChecker(self, accounturl)
+        else:
+            # we could leave this anonymous, with just the /uri/CAP form
+            raise RuntimeError("must provide some translation")
+
+        # make sure we're using a patched Twisted that uses IWriteFile.close:
+        # see docs/ftp.txt for details.
+        assert "close" in ftp.IWriteFile.names(), "your twisted is lacking"
+
+        r = Dispatcher(client)
+        p = portal.Portal(r)
+        p.registerChecker(c)
+        f = ftp.FTPFactory(p)
+
+        s = strports.service(ftp_portstr, f)
+        s.setServiceParent(self)
diff --git a/src/allmydata/frontends/sftpd.py b/src/allmydata/frontends/sftpd.py
new file mode 100644 (file)
index 0000000..790e5d7
--- /dev/null
@@ -0,0 +1,565 @@
+
+import os
+import tempfile
+from zope.interface import implements
+from twisted.python import components
+from twisted.application import service, strports
+from twisted.internet import defer
+from twisted.internet.interfaces import IConsumer
+from twisted.conch.ssh import factory, keys, session
+from twisted.conch.interfaces import ISFTPServer, ISFTPFile, IConchUser
+from twisted.conch.avatar import ConchUser
+from twisted.conch.openssh_compat import primes
+from twisted.conch import ls
+from twisted.cred import error, portal, checkers, credentials
+from twisted.web.client import getPage
+
+from allmydata.interfaces import IDirectoryNode, ExistingChildError, \
+     NoSuchChildError
+from allmydata.immutable.upload import FileHandle
+from allmydata.util import base32
+
+class MemoryConsumer:
+    implements(IConsumer)
+    def __init__(self):
+        self.chunks = []
+        self.done = False
+    def registerProducer(self, p, streaming):
+        if streaming:
+            # call resumeProducing once to start things off
+            p.resumeProducing()
+        else:
+            while not self.done:
+                p.resumeProducing()
+    def write(self, data):
+        self.chunks.append(data)
+    def unregisterProducer(self):
+        self.done = True
+
+def download_to_data(n, offset=0, size=None):
+    d = n.read(MemoryConsumer(), offset, size)
+    d.addCallback(lambda mc: "".join(mc.chunks))
+    return d
+
+class ReadFile:
+    implements(ISFTPFile)
+    def __init__(self, node):
+        self.node = node
+    def readChunk(self, offset, length):
+        d = download_to_data(self.node, offset, length)
+        def _got(data):
+            return data
+        d.addCallback(_got)
+        return d
+    def close(self):
+        pass
+    def getAttrs(self):
+        print "GETATTRS(file)"
+        raise NotImplementedError
+    def setAttrs(self, attrs):
+        print "SETATTRS(file)", attrs
+        raise NotImplementedError
+
+class WriteFile:
+    implements(ISFTPFile)
+
+    def __init__(self, parent, childname, convergence):
+        self.parent = parent
+        self.childname = childname
+        self.convergence = convergence
+        self.f = tempfile.TemporaryFile()
+    def writeChunk(self, offset, data):
+        self.f.seek(offset)
+        self.f.write(data)
+
+    def close(self):
+        u = FileHandle(self.f, self.convergence)
+        d = self.parent.add_file(self.childname, u)
+        return d
+
+    def getAttrs(self):
+        print "GETATTRS(file)"
+        raise NotImplementedError
+    def setAttrs(self, attrs):
+        print "SETATTRS(file)", attrs
+        raise NotImplementedError
+
+
+class NoParentError(Exception):
+    pass
+
+class PermissionError(Exception):
+    pass
+
+from twisted.conch.ssh.filetransfer import FileTransferServer, SFTPError, \
+     FX_NO_SUCH_FILE, FX_FILE_ALREADY_EXISTS, FX_OP_UNSUPPORTED, \
+     FX_PERMISSION_DENIED
+from twisted.conch.ssh.filetransfer import FXF_READ, FXF_WRITE, FXF_APPEND, FXF_CREAT, FXF_TRUNC, FXF_EXCL
+
+class SFTPUser(ConchUser):
+    def __init__(self, client, rootnode, username, convergence):
+        ConchUser.__init__(self)
+        self.channelLookup["session"] = session.SSHSession
+        self.subsystemLookup["sftp"] = FileTransferServer
+
+        self.client = client
+        self.root = rootnode
+        self.username = username
+        self.convergence = convergence
+
+class StoppableList:
+    def __init__(self, items):
+        self.items = items
+    def __iter__(self):
+        for i in self.items:
+            yield i
+    def close(self):
+        pass
+
+class FakeStat:
+    pass
+
+class SFTPHandler:
+    implements(ISFTPServer)
+    def __init__(self, user):
+        print "Creating SFTPHandler from", user
+        self.client = user.client
+        self.root = user.root
+        self.username = user.username
+        self.convergence = user.convergence
+
+    def gotVersion(self, otherVersion, extData):
+        return {}
+
+    def openFile(self, filename, flags, attrs):
+        f = "|".join([f for f in
+                      [(flags & FXF_READ) and "FXF_READ" or None,
+                       (flags & FXF_WRITE) and "FXF_WRITE" or None,
+                       (flags & FXF_APPEND) and "FXF_APPEND" or None,
+                       (flags & FXF_CREAT) and "FXF_CREAT" or None,
+                       (flags & FXF_TRUNC) and "FXF_TRUNC" or None,
+                       (flags & FXF_EXCL) and "FXF_EXCL" or None,
+                      ]
+                      if f])
+        print "OPENFILE", filename, flags, f, attrs
+        # this is used for both reading and writing.
+
+#        createPlease = False
+#        exclusive = False
+#        openFlags = 0
+#
+#        if flags & FXF_READ == FXF_READ and flags & FXF_WRITE == 0:
+#            openFlags = os.O_RDONLY
+#        if flags & FXF_WRITE == FXF_WRITE and flags & FXF_READ == 0:
+#            createPlease = True
+#            openFlags = os.O_WRONLY
+#        if flags & FXF_WRITE == FXF_WRITE and flags & FXF_READ == FXF_READ:
+#            createPlease = True
+#            openFlags = os.O_RDWR
+#        if flags & FXF_APPEND == FXF_APPEND:
+#            createPlease = True
+#            openFlags |= os.O_APPEND
+#        if flags & FXF_CREAT == FXF_CREAT:
+#            createPlease = True
+#            openFlags |= os.O_CREAT
+#        if flags & FXF_TRUNC == FXF_TRUNC:
+#            openFlags |= os.O_TRUNC
+#        if flags & FXF_EXCL == FXF_EXCL:
+#            exclusive = True
+
+        # /usr/bin/sftp 'get' gives us FXF_READ, while 'put' on a new file
+        # gives FXF_WRITE,FXF_CREAT,FXF_TRUNC . I'm guessing that 'put' on an
+        # existing file gives the same.
+
+        path = self._convert_sftp_path(filename)
+
+        if flags & FXF_READ:
+            if flags & FXF_WRITE:
+                raise NotImplementedError
+            d = self._get_node_and_metadata_for_path(path)
+            d.addCallback(lambda (node,metadata): ReadFile(node))
+            d.addErrback(self._convert_error)
+            return d
+
+        if flags & FXF_WRITE:
+            if not (flags & FXF_CREAT) or not (flags & FXF_TRUNC):
+                raise NotImplementedError
+            if not path:
+                raise PermissionError("cannot STOR to root directory")
+            childname = path[-1]
+            d = self._get_root(path)
+            def _got_root((root, path)):
+                if not path:
+                    raise PermissionError("cannot STOR to root directory")
+                return root.get_child_at_path(path[:-1])
+            d.addCallback(_got_root)
+            def _got_parent(parent):
+                return WriteFile(parent, childname, self.convergence)
+            d.addCallback(_got_parent)
+            return d
+        raise NotImplementedError
+
+    def removeFile(self, path):
+        print "REMOVEFILE", path
+        path = self._convert_sftp_path(path)
+        return self._remove_thing(path, must_be_file=True)
+
+    def renameFile(self, oldpath, newpath):
+        print "RENAMEFILE", oldpath, newpath
+        fromPath = self._convert_sftp_path(oldpath)
+        toPath = self._convert_sftp_path(newpath)
+        # the target directory must already exist
+        d = self._get_parent(fromPath)
+        def _got_from_parent( (fromparent, childname) ):
+            d = self._get_parent(toPath)
+            d.addCallback(lambda (toparent, tochildname):
+                          fromparent.move_child_to(childname,
+                                                   toparent, tochildname,
+                                                   overwrite=False))
+            return d
+        d.addCallback(_got_from_parent)
+        d.addErrback(self._convert_error)
+        return d
+
+    def makeDirectory(self, path, attrs):
+        print "MAKEDIRECTORY", path, attrs
+        # TODO: extract attrs["mtime"], use it to set the parent metadata.
+        # Maybe also copy attrs["ext_*"] .
+        path = self._convert_sftp_path(path)
+        d = self._get_root(path)
+        d.addCallback(lambda (root,path):
+                      self._get_or_create_directories(root, path))
+        return d
+
+    def _get_or_create_directories(self, node, path):
+        if not IDirectoryNode.providedBy(node):
+            # unfortunately it is too late to provide the name of the
+            # blocking directory in the error message.
+            raise ExistingChildError("cannot create directory because there "
+                                     "is a file in the way") # close enough
+        if not path:
+            return defer.succeed(node)
+        d = node.get(path[0])
+        def _maybe_create(f):
+            f.trap(NoSuchChildError)
+            return node.create_empty_directory(path[0])
+        d.addErrback(_maybe_create)
+        d.addCallback(self._get_or_create_directories, path[1:])
+        return d
+
+    def removeDirectory(self, path):
+        print "REMOVEDIRECTORY", path
+        path = self._convert_sftp_path(path)
+        return self._remove_thing(path, must_be_directory=True)
+
+    def _remove_thing(self, path, must_be_directory=False, must_be_file=False):
+        d = defer.maybeDeferred(self._get_parent, path)
+        def _convert_error(f):
+            f.trap(NoParentError)
+            raise PermissionError("cannot delete root directory")
+        d.addErrback(_convert_error)
+        def _got_parent( (parent, childname) ):
+            d = parent.get(childname)
+            def _got_child(child):
+                if must_be_directory and not IDirectoryNode.providedBy(child):
+                    raise RuntimeError("rmdir called on a file")
+                if must_be_file and IDirectoryNode.providedBy(child):
+                    raise RuntimeError("rmfile called on a directory")
+                return parent.delete(childname)
+            d.addCallback(_got_child)
+            d.addErrback(self._convert_error)
+            return d
+        d.addCallback(_got_parent)
+        return d
+
+
+    def openDirectory(self, path):
+        print "OPENDIRECTORY", path
+        path = self._convert_sftp_path(path)
+        d = self._get_node_and_metadata_for_path(path)
+        d.addCallback(lambda (dirnode,metadata): dirnode.list())
+        def _render(children):
+            results = []
+            for filename, (node, metadata) in children.iteritems():
+                s = FakeStat()
+                if IDirectoryNode.providedBy(node):
+                    s.st_mode = 040700
+                    s.st_size = 0
+                else:
+                    s.st_mode = 0100600
+                    s.st_size = node.get_size()
+                s.st_nlink = 1
+                s.st_uid = 0
+                s.st_gid = 0
+                s.st_mtime = int(metadata.get("mtime", 0))
+                longname = ls.lsLine(filename.encode("utf-8"), s)
+                attrs = self._populate_attrs(node, metadata)
+                results.append( (filename.encode("utf-8"), longname, attrs) )
+            return StoppableList(results)
+        d.addCallback(_render)
+        return d
+
+    def getAttrs(self, path, followLinks):
+        print "GETATTRS", path, followLinks
+        # from ftp.stat
+        d = self._get_node_and_metadata_for_path(self._convert_sftp_path(path))
+        def _render((node,metadata)):
+            return self._populate_attrs(node, metadata)
+        d.addCallback(_render)
+        d.addErrback(self._convert_error)
+        def _done(res):
+            print " DONE", res
+            return res
+        d.addBoth(_done)
+        return d
+
+    def _convert_sftp_path(self, pathstring):
+        assert pathstring[0] == "/"
+        pathstring = pathstring.strip("/")
+        if pathstring == "":
+            path = []
+        else:
+            path = pathstring.split("/")
+        print "CONVERT", pathstring, path
+        path = [unicode(p) for p in path]
+        return path
+
+    def _get_node_and_metadata_for_path(self, path):
+        d = self._get_root(path)
+        def _got_root((root,path)):
+            print "ROOT", root
+            print "PATH", path
+            if path:
+                return root.get_child_and_metadata_at_path(path)
+            else:
+                return (root,{})
+        d.addCallback(_got_root)
+        return d
+
+    def _get_root(self, path):
+        # return (root, remaining_path)
+        path = [unicode(p) for p in path]
+        if path and path[0] == "uri":
+            d = defer.maybeDeferred(self.client.create_node_from_uri,
+                                    str(path[1]))
+            d.addCallback(lambda root: (root, path[2:]))
+        else:
+            d = defer.succeed((self.root,path))
+        return d
+
+    def _populate_attrs(self, childnode, metadata):
+        attrs = {}
+        attrs["uid"] = 1000
+        attrs["gid"] = 1000
+        attrs["atime"] = 0
+        attrs["mtime"] = int(metadata.get("mtime", 0))
+        isdir = bool(IDirectoryNode.providedBy(childnode))
+        if isdir:
+            attrs["size"] = 1
+            # the permissions must have the extra bits (040000 or 0100000),
+            # otherwise the client will not call openDirectory
+            attrs["permissions"] = 040700 # S_IFDIR
+        else:
+            attrs["size"] = childnode.get_size()
+            attrs["permissions"] = 0100600 # S_IFREG
+        return attrs
+
+    def _convert_error(self, f):
+        if f.check(NoSuchChildError):
+            childname = f.value.args[0].encode("utf-8")
+            raise SFTPError(FX_NO_SUCH_FILE, childname)
+        if f.check(ExistingChildError):
+            msg = f.value.args[0].encode("utf-8")
+            raise SFTPError(FX_FILE_ALREADY_EXISTS, msg)
+        if f.check(PermissionError):
+            raise SFTPError(FX_PERMISSION_DENIED, str(f.value))
+        if f.check(NotImplementedError):
+            raise SFTPError(FX_OP_UNSUPPORTED, str(f.value))
+        return f
+
+
+    def setAttrs(self, path, attrs):
+        print "SETATTRS", path, attrs
+        # ignored
+        return None
+
+    def readLink(self, path):
+        print "READLINK", path
+        raise NotImplementedError
+
+    def makeLink(self, linkPath, targetPath):
+        print "MAKELINK", linkPath, targetPath
+        raise NotImplementedError
+
+    def extendedRequest(self, extendedName, extendedData):
+        print "EXTENDEDREQUEST", extendedName, extendedData
+        # client 'df' command requires 'statvfs@openssh.com' extension
+        raise NotImplementedError
+    def realPath(self, path):
+        print "REALPATH", path
+        if path == ".":
+            return "/"
+        return path
+
+
+    def _get_parent(self, path):
+        # fire with (parentnode, childname)
+        path = [unicode(p) for p in path]
+        if not path:
+            raise NoParentError
+        childname = path[-1]
+        d = self._get_root(path)
+        def _got_root((root, path)):
+            if not path:
+                raise NoParentError
+            return root.get_child_at_path(path[:-1])
+        d.addCallback(_got_root)
+        def _got_parent(parent):
+            return (parent, childname)
+        d.addCallback(_got_parent)
+        return d
+
+
+class FTPAvatarID:
+    def __init__(self, username, rootcap):
+        self.username = username
+        self.rootcap = rootcap
+
+class AccountFileChecker:
+    implements(checkers.ICredentialsChecker)
+    credentialInterfaces = (credentials.IUsernamePassword,
+                            credentials.IUsernameHashedPassword)
+    def __init__(self, client, accountfile):
+        self.client = client
+        self.passwords = {}
+        self.pubkeys = {}
+        self.rootcaps = {}
+        for line in open(os.path.expanduser(accountfile), "r"):
+            line = line.strip()
+            if line.startswith("#") or not line:
+                continue
+            name, passwd, rest = line.split(None, 2)
+            if passwd in ("ssh-dss", "ssh-rsa"):
+                bits = rest.split()
+                keystring = " ".join(bits[-1])
+                rootcap = bits[-1]
+                self.pubkeys[name] = keystring
+            else:
+                self.passwords[name] = passwd
+                rootcap = rest
+            self.rootcaps[name] = rootcap
+
+    def _cbPasswordMatch(self, matched, username):
+        if matched:
+            return FTPAvatarID(username, self.rootcaps[username])
+        raise error.UnauthorizedLogin
+
+    def requestAvatarId(self, credentials):
+        if credentials.username in self.passwords:
+            d = defer.maybeDeferred(credentials.checkPassword,
+                                    self.passwords[credentials.username])
+            d.addCallback(self._cbPasswordMatch, str(credentials.username))
+            return d
+        return defer.fail(error.UnauthorizedLogin())
+
+class AccountURLChecker:
+    implements(checkers.ICredentialsChecker)
+    credentialInterfaces = (credentials.IUsernamePassword,)
+
+    def __init__(self, client, auth_url):
+        self.client = client
+        self.auth_url = auth_url
+
+    def _cbPasswordMatch(self, rootcap, username):
+        return FTPAvatarID(username, rootcap)
+
+    def post_form(self, username, password):
+        sepbase = base32.b2a(os.urandom(4))
+        sep = "--" + sepbase
+        form = []
+        form.append(sep)
+        fields = {"action": "authenticate",
+                  "email": username,
+                  "passwd": password,
+                  }
+        for name, value in fields.iteritems():
+            form.append('Content-Disposition: form-data; name="%s"' % name)
+            form.append('')
+            assert isinstance(value, str)
+            form.append(value)
+            form.append(sep)
+        form[-1] += "--"
+        body = "\r\n".join(form) + "\r\n"
+        headers = {"content-type": "multipart/form-data; boundary=%s" % sepbase,
+                   }
+        return getPage(self.auth_url, method="POST",
+                       postdata=body, headers=headers,
+                       followRedirect=True, timeout=30)
+
+    def _parse_response(self, res):
+        rootcap = res.strip()
+        if rootcap == "0":
+            raise error.UnauthorizedLogin
+        return rootcap
+
+    def requestAvatarId(self, credentials):
+        # construct a POST to the login form. While this could theoretically
+        # be done with something like the stdlib 'email' package, I can't
+        # figure out how, so we just slam together a form manually.
+        d = self.post_form(credentials.username, credentials.password)
+        d.addCallback(self._parse_response)
+        d.addCallback(self._cbPasswordMatch, str(credentials.username))
+        return d
+
+# if you have an SFTPUser, and you want something that provides ISFTPServer,
+# then you get SFTPHandler(user)
+components.registerAdapter(SFTPHandler, SFTPUser, ISFTPServer)
+
+class Dispatcher:
+    implements(portal.IRealm)
+    def __init__(self, client):
+        self.client = client
+
+    def requestAvatar(self, avatarID, mind, interface):
+        assert interface == IConchUser
+        rootnode = self.client.create_node_from_uri(avatarID.rootcap)
+        convergence = self.client.convergence
+        s = SFTPUser(self.client, rootnode, avatarID.username, convergence)
+        def logout(): pass
+        return (interface, s, logout)
+
+class SFTPServer(service.MultiService):
+    def __init__(self, client, accountfile, accounturl,
+                 sftp_portstr, pubkey_file, privkey_file):
+        service.MultiService.__init__(self)
+
+        if accountfile:
+            c = AccountFileChecker(self, accountfile)
+        elif accounturl:
+            c = AccountURLChecker(self, accounturl)
+        else:
+            # we could leave this anonymous, with just the /uri/CAP form
+            raise RuntimeError("must provide some translation")
+
+        r = Dispatcher(client)
+        p = portal.Portal(r)
+        p.registerChecker(c)
+
+        pubkey = keys.Key.fromFile(pubkey_file)
+        privkey = keys.Key.fromFile(privkey_file)
+        class SSHFactory(factory.SSHFactory):
+            publicKeys = {pubkey.sshType(): pubkey}
+            privateKeys = {privkey.sshType(): privkey}
+            def getPrimes(self):
+                try:
+                    # if present, this enables diffie-hellman-group-exchange
+                    return primes.parseModuliFile("/etc/ssh/moduli")
+                except IOError:
+                    return None
+
+        f = SSHFactory()
+        f.portal = p
+
+        s = strports.service(sftp_portstr, f)
+        s.setServiceParent(self)
+
diff --git a/src/allmydata/ftpd.py b/src/allmydata/ftpd.py
deleted file mode 100644 (file)
index 5ade0e2..0000000
+++ /dev/null
@@ -1,394 +0,0 @@
-
-import os
-import tempfile
-from zope.interface import implements
-from twisted.application import service, strports
-from twisted.internet import defer
-from twisted.internet.interfaces import IConsumer
-from twisted.protocols import ftp
-from twisted.cred import error, portal, checkers, credentials
-from twisted.web.client import getPage
-
-from allmydata.interfaces import IDirectoryNode, ExistingChildError, \
-     NoSuchChildError
-from allmydata.immutable.download import ConsumerAdapter
-from allmydata.immutable.upload import FileHandle
-from allmydata.util import base32
-
-class ReadFile:
-    implements(ftp.IReadFile)
-    def __init__(self, node):
-        self.node = node
-    def send(self, consumer):
-        ad = ConsumerAdapter(consumer)
-        d = self.node.download(ad)
-        return d # when consumed
-
-class FileWriter:
-    implements(IConsumer)
-
-    def registerProducer(self, producer, streaming):
-        if not streaming:
-            raise NotImplementedError("Non-streaming producer not supported.")
-        # we write the data to a temporary file, since Tahoe can't do
-        # streaming upload yet.
-        self.f = tempfile.TemporaryFile()
-        return None
-
-    def unregisterProducer(self):
-        # the upload actually happens in WriteFile.close()
-        pass
-
-    def write(self, data):
-        self.f.write(data)
-
-class WriteFile:
-    implements(ftp.IWriteFile)
-
-    def __init__(self, parent, childname, convergence):
-        self.parent = parent
-        self.childname = childname
-        self.convergence = convergence
-
-    def receive(self):
-        self.c = FileWriter()
-        return defer.succeed(self.c)
-
-    def close(self):
-        u = FileHandle(self.c.f, self.convergence)
-        d = self.parent.add_file(self.childname, u)
-        return d
-
-
-class NoParentError(Exception):
-    pass
-
-class Handler:
-    implements(ftp.IFTPShell)
-    def __init__(self, client, rootnode, username, convergence):
-        self.client = client
-        self.root = rootnode
-        self.username = username
-        self.convergence = convergence
-
-    def makeDirectory(self, path):
-        d = self._get_root(path)
-        d.addCallback(lambda (root,path):
-                      self._get_or_create_directories(root, path))
-        return d
-
-    def _get_or_create_directories(self, node, path):
-        if not IDirectoryNode.providedBy(node):
-            # unfortunately it is too late to provide the name of the
-            # blocking directory in the error message.
-            raise ftp.FileExistsError("cannot create directory because there "
-                                      "is a file in the way")
-        if not path:
-            return defer.succeed(node)
-        d = node.get(path[0])
-        def _maybe_create(f):
-            f.trap(NoSuchChildError)
-            return node.create_empty_directory(path[0])
-        d.addErrback(_maybe_create)
-        d.addCallback(self._get_or_create_directories, path[1:])
-        return d
-
-    def _get_parent(self, path):
-        # fire with (parentnode, childname)
-        path = [unicode(p) for p in path]
-        if not path:
-            raise NoParentError
-        childname = path[-1]
-        d = self._get_root(path)
-        def _got_root((root, path)):
-            if not path:
-                raise NoParentError
-            return root.get_child_at_path(path[:-1])
-        d.addCallback(_got_root)
-        def _got_parent(parent):
-            return (parent, childname)
-        d.addCallback(_got_parent)
-        return d
-
-    def _remove_thing(self, path, must_be_directory=False, must_be_file=False):
-        d = defer.maybeDeferred(self._get_parent, path)
-        def _convert_error(f):
-            f.trap(NoParentError)
-            raise ftp.PermissionDeniedError("cannot delete root directory")
-        d.addErrback(_convert_error)
-        def _got_parent( (parent, childname) ):
-            d = parent.get(childname)
-            def _got_child(child):
-                if must_be_directory and not IDirectoryNode.providedBy(child):
-                    raise ftp.IsNotADirectoryError("rmdir called on a file")
-                if must_be_file and IDirectoryNode.providedBy(child):
-                    raise ftp.IsADirectoryError("rmfile called on a directory")
-                return parent.delete(childname)
-            d.addCallback(_got_child)
-            d.addErrback(self._convert_error)
-            return d
-        d.addCallback(_got_parent)
-        return d
-
-    def removeDirectory(self, path):
-        return self._remove_thing(path, must_be_directory=True)
-
-    def removeFile(self, path):
-        return self._remove_thing(path, must_be_file=True)
-
-    def rename(self, fromPath, toPath):
-        # the target directory must already exist
-        d = self._get_parent(fromPath)
-        def _got_from_parent( (fromparent, childname) ):
-            d = self._get_parent(toPath)
-            d.addCallback(lambda (toparent, tochildname):
-                          fromparent.move_child_to(childname,
-                                                   toparent, tochildname,
-                                                   overwrite=False))
-            return d
-        d.addCallback(_got_from_parent)
-        d.addErrback(self._convert_error)
-        return d
-
-    def access(self, path):
-        # we allow access to everything that exists. We are required to raise
-        # an error for paths that don't exist: FTP clients (at least ncftp)
-        # uses this to decide whether to mkdir or not.
-        d = self._get_node_and_metadata_for_path(path)
-        d.addErrback(self._convert_error)
-        d.addCallback(lambda res: None)
-        return d
-
-    def _convert_error(self, f):
-        if f.check(NoSuchChildError):
-            childname = f.value.args[0].encode("utf-8")
-            msg = "'%s' doesn't exist" % childname
-            raise ftp.FileNotFoundError(msg)
-        if f.check(ExistingChildError):
-            msg = f.value.args[0].encode("utf-8")
-            raise ftp.FileExistsError(msg)
-        return f
-
-    def _get_root(self, path):
-        # return (root, remaining_path)
-        path = [unicode(p) for p in path]
-        if path and path[0] == "uri":
-            d = defer.maybeDeferred(self.client.create_node_from_uri,
-                                    str(path[1]))
-            d.addCallback(lambda root: (root, path[2:]))
-        else:
-            d = defer.succeed((self.root,path))
-        return d
-
-    def _get_node_and_metadata_for_path(self, path):
-        d = self._get_root(path)
-        def _got_root((root,path)):
-            if path:
-                return root.get_child_and_metadata_at_path(path)
-            else:
-                return (root,{})
-        d.addCallback(_got_root)
-        return d
-
-    def _populate_row(self, keys, (childnode, metadata)):
-        values = []
-        isdir = bool(IDirectoryNode.providedBy(childnode))
-        for key in keys:
-            if key == "size":
-                if isdir:
-                    value = 0
-                else:
-                    value = childnode.get_size()
-            elif key == "directory":
-                value = isdir
-            elif key == "permissions":
-                value = 0600
-            elif key == "hardlinks":
-                value = 1
-            elif key == "modified":
-                value = metadata.get("mtime", 0)
-            elif key == "owner":
-                value = self.username
-            elif key == "group":
-                value = self.username
-            else:
-                value = "??"
-            values.append(value)
-        return values
-
-    def stat(self, path, keys=()):
-        # for files only, I think
-        d = self._get_node_and_metadata_for_path(path)
-        def _render((node,metadata)):
-            assert not IDirectoryNode.providedBy(node)
-            return self._populate_row(keys, (node,metadata))
-        d.addCallback(_render)
-        d.addErrback(self._convert_error)
-        return d
-
-    def list(self, path, keys=()):
-        # the interface claims that path is a list of unicodes, but in
-        # practice it is not
-        d = self._get_node_and_metadata_for_path(path)
-        def _list((node, metadata)):
-            if IDirectoryNode.providedBy(node):
-                return node.list()
-            return { path[-1]: (node, metadata) } # need last-edge metadata
-        d.addCallback(_list)
-        def _render(children):
-            results = []
-            for (name, childnode) in children.iteritems():
-                # the interface claims that the result should have a unicode
-                # object as the name, but it fails unless you give it a
-                # bytestring
-                results.append( (name.encode("utf-8"),
-                                 self._populate_row(keys, childnode) ) )
-            return results
-        d.addCallback(_render)
-        d.addErrback(self._convert_error)
-        return d
-
-    def openForReading(self, path):
-        d = self._get_node_and_metadata_for_path(path)
-        d.addCallback(lambda (node,metadata): ReadFile(node))
-        d.addErrback(self._convert_error)
-        return d
-
-    def openForWriting(self, path):
-        path = [unicode(p) for p in path]
-        if not path:
-            raise ftp.PermissionDeniedError("cannot STOR to root directory")
-        childname = path[-1]
-        d = self._get_root(path)
-        def _got_root((root, path)):
-            if not path:
-                raise ftp.PermissionDeniedError("cannot STOR to root directory")
-            return root.get_child_at_path(path[:-1])
-        d.addCallback(_got_root)
-        def _got_parent(parent):
-            return WriteFile(parent, childname, self.convergence)
-        d.addCallback(_got_parent)
-        return d
-
-
-class FTPAvatarID:
-    def __init__(self, username, rootcap):
-        self.username = username
-        self.rootcap = rootcap
-
-class AccountFileChecker:
-    implements(checkers.ICredentialsChecker)
-    credentialInterfaces = (credentials.IUsernamePassword,
-                            credentials.IUsernameHashedPassword)
-    def __init__(self, client, accountfile):
-        self.client = client
-        self.passwords = {}
-        self.rootcaps = {}
-        for line in open(os.path.expanduser(accountfile), "r"):
-            line = line.strip()
-            if line.startswith("#") or not line:
-                continue
-            name, passwd, rootcap = line.split()
-            self.passwords[name] = passwd
-            self.rootcaps[name] = rootcap
-
-    def _cbPasswordMatch(self, matched, username):
-        if matched:
-            return FTPAvatarID(username, self.rootcaps[username])
-        raise error.UnauthorizedLogin
-
-    def requestAvatarId(self, credentials):
-        if credentials.username in self.passwords:
-            d = defer.maybeDeferred(credentials.checkPassword,
-                                    self.passwords[credentials.username])
-            d.addCallback(self._cbPasswordMatch, str(credentials.username))
-            return d
-        return defer.fail(error.UnauthorizedLogin())
-
-class AccountURLChecker:
-    implements(checkers.ICredentialsChecker)
-    credentialInterfaces = (credentials.IUsernamePassword,)
-
-    def __init__(self, client, auth_url):
-        self.client = client
-        self.auth_url = auth_url
-
-    def _cbPasswordMatch(self, rootcap, username):
-        return FTPAvatarID(username, rootcap)
-
-    def post_form(self, username, password):
-        sepbase = base32.b2a(os.urandom(4))
-        sep = "--" + sepbase
-        form = []
-        form.append(sep)
-        fields = {"action": "authenticate",
-                  "email": username,
-                  "passwd": password,
-                  }
-        for name, value in fields.iteritems():
-            form.append('Content-Disposition: form-data; name="%s"' % name)
-            form.append('')
-            assert isinstance(value, str)
-            form.append(value)
-            form.append(sep)
-        form[-1] += "--"
-        body = "\r\n".join(form) + "\r\n"
-        headers = {"content-type": "multipart/form-data; boundary=%s" % sepbase,
-                   }
-        return getPage(self.auth_url, method="POST",
-                       postdata=body, headers=headers,
-                       followRedirect=True, timeout=30)
-
-    def _parse_response(self, res):
-        rootcap = res.strip()
-        if rootcap == "0":
-            raise error.UnauthorizedLogin
-        return rootcap
-
-    def requestAvatarId(self, credentials):
-        # construct a POST to the login form. While this could theoretically
-        # be done with something like the stdlib 'email' package, I can't
-        # figure out how, so we just slam together a form manually.
-        d = self.post_form(credentials.username, credentials.password)
-        d.addCallback(self._parse_response)
-        d.addCallback(self._cbPasswordMatch, str(credentials.username))
-        return d
-
-
-class Dispatcher:
-    implements(portal.IRealm)
-    def __init__(self, client):
-        self.client = client
-
-    def requestAvatar(self, avatarID, mind, interface):
-        assert interface == ftp.IFTPShell
-        rootnode = self.client.create_node_from_uri(avatarID.rootcap)
-        convergence = self.client.convergence
-        s = Handler(self.client, rootnode, avatarID.username, convergence)
-        def logout(): pass
-        return (interface, s, None)
-
-
-class FTPServer(service.MultiService):
-    def __init__(self, client, accountfile, accounturl, ftp_portstr):
-        service.MultiService.__init__(self)
-
-        if accountfile:
-            c = AccountFileChecker(self, accountfile)
-        elif accounturl:
-            c = AccountURLChecker(self, accounturl)
-        else:
-            # we could leave this anonymous, with just the /uri/CAP form
-            raise RuntimeError("must provide some translation")
-
-        # make sure we're using a patched Twisted that uses IWriteFile.close:
-        # see docs/ftp.txt for details.
-        assert "close" in ftp.IWriteFile.names(), "your twisted is lacking"
-
-        r = Dispatcher(client)
-        p = portal.Portal(r)
-        p.registerChecker(c)
-        f = ftp.FTPFactory(p)
-
-        s = strports.service(ftp_portstr, f)
-        s.setServiceParent(self)
diff --git a/src/allmydata/sftpd.py b/src/allmydata/sftpd.py
deleted file mode 100644 (file)
index 790e5d7..0000000
+++ /dev/null
@@ -1,565 +0,0 @@
-
-import os
-import tempfile
-from zope.interface import implements
-from twisted.python import components
-from twisted.application import service, strports
-from twisted.internet import defer
-from twisted.internet.interfaces import IConsumer
-from twisted.conch.ssh import factory, keys, session
-from twisted.conch.interfaces import ISFTPServer, ISFTPFile, IConchUser
-from twisted.conch.avatar import ConchUser
-from twisted.conch.openssh_compat import primes
-from twisted.conch import ls
-from twisted.cred import error, portal, checkers, credentials
-from twisted.web.client import getPage
-
-from allmydata.interfaces import IDirectoryNode, ExistingChildError, \
-     NoSuchChildError
-from allmydata.immutable.upload import FileHandle
-from allmydata.util import base32
-
-class MemoryConsumer:
-    implements(IConsumer)
-    def __init__(self):
-        self.chunks = []
-        self.done = False
-    def registerProducer(self, p, streaming):
-        if streaming:
-            # call resumeProducing once to start things off
-            p.resumeProducing()
-        else:
-            while not self.done:
-                p.resumeProducing()
-    def write(self, data):
-        self.chunks.append(data)
-    def unregisterProducer(self):
-        self.done = True
-
-def download_to_data(n, offset=0, size=None):
-    d = n.read(MemoryConsumer(), offset, size)
-    d.addCallback(lambda mc: "".join(mc.chunks))
-    return d
-
-class ReadFile:
-    implements(ISFTPFile)
-    def __init__(self, node):
-        self.node = node
-    def readChunk(self, offset, length):
-        d = download_to_data(self.node, offset, length)
-        def _got(data):
-            return data
-        d.addCallback(_got)
-        return d
-    def close(self):
-        pass
-    def getAttrs(self):
-        print "GETATTRS(file)"
-        raise NotImplementedError
-    def setAttrs(self, attrs):
-        print "SETATTRS(file)", attrs
-        raise NotImplementedError
-
-class WriteFile:
-    implements(ISFTPFile)
-
-    def __init__(self, parent, childname, convergence):
-        self.parent = parent
-        self.childname = childname
-        self.convergence = convergence
-        self.f = tempfile.TemporaryFile()
-    def writeChunk(self, offset, data):
-        self.f.seek(offset)
-        self.f.write(data)
-
-    def close(self):
-        u = FileHandle(self.f, self.convergence)
-        d = self.parent.add_file(self.childname, u)
-        return d
-
-    def getAttrs(self):
-        print "GETATTRS(file)"
-        raise NotImplementedError
-    def setAttrs(self, attrs):
-        print "SETATTRS(file)", attrs
-        raise NotImplementedError
-
-
-class NoParentError(Exception):
-    pass
-
-class PermissionError(Exception):
-    pass
-
-from twisted.conch.ssh.filetransfer import FileTransferServer, SFTPError, \
-     FX_NO_SUCH_FILE, FX_FILE_ALREADY_EXISTS, FX_OP_UNSUPPORTED, \
-     FX_PERMISSION_DENIED
-from twisted.conch.ssh.filetransfer import FXF_READ, FXF_WRITE, FXF_APPEND, FXF_CREAT, FXF_TRUNC, FXF_EXCL
-
-class SFTPUser(ConchUser):
-    def __init__(self, client, rootnode, username, convergence):
-        ConchUser.__init__(self)
-        self.channelLookup["session"] = session.SSHSession
-        self.subsystemLookup["sftp"] = FileTransferServer
-
-        self.client = client
-        self.root = rootnode
-        self.username = username
-        self.convergence = convergence
-
-class StoppableList:
-    def __init__(self, items):
-        self.items = items
-    def __iter__(self):
-        for i in self.items:
-            yield i
-    def close(self):
-        pass
-
-class FakeStat:
-    pass
-
-class SFTPHandler:
-    implements(ISFTPServer)
-    def __init__(self, user):
-        print "Creating SFTPHandler from", user
-        self.client = user.client
-        self.root = user.root
-        self.username = user.username
-        self.convergence = user.convergence
-
-    def gotVersion(self, otherVersion, extData):
-        return {}
-
-    def openFile(self, filename, flags, attrs):
-        f = "|".join([f for f in
-                      [(flags & FXF_READ) and "FXF_READ" or None,
-                       (flags & FXF_WRITE) and "FXF_WRITE" or None,
-                       (flags & FXF_APPEND) and "FXF_APPEND" or None,
-                       (flags & FXF_CREAT) and "FXF_CREAT" or None,
-                       (flags & FXF_TRUNC) and "FXF_TRUNC" or None,
-                       (flags & FXF_EXCL) and "FXF_EXCL" or None,
-                      ]
-                      if f])
-        print "OPENFILE", filename, flags, f, attrs
-        # this is used for both reading and writing.
-
-#        createPlease = False
-#        exclusive = False
-#        openFlags = 0
-#
-#        if flags & FXF_READ == FXF_READ and flags & FXF_WRITE == 0:
-#            openFlags = os.O_RDONLY
-#        if flags & FXF_WRITE == FXF_WRITE and flags & FXF_READ == 0:
-#            createPlease = True
-#            openFlags = os.O_WRONLY
-#        if flags & FXF_WRITE == FXF_WRITE and flags & FXF_READ == FXF_READ:
-#            createPlease = True
-#            openFlags = os.O_RDWR
-#        if flags & FXF_APPEND == FXF_APPEND:
-#            createPlease = True
-#            openFlags |= os.O_APPEND
-#        if flags & FXF_CREAT == FXF_CREAT:
-#            createPlease = True
-#            openFlags |= os.O_CREAT
-#        if flags & FXF_TRUNC == FXF_TRUNC:
-#            openFlags |= os.O_TRUNC
-#        if flags & FXF_EXCL == FXF_EXCL:
-#            exclusive = True
-
-        # /usr/bin/sftp 'get' gives us FXF_READ, while 'put' on a new file
-        # gives FXF_WRITE,FXF_CREAT,FXF_TRUNC . I'm guessing that 'put' on an
-        # existing file gives the same.
-
-        path = self._convert_sftp_path(filename)
-
-        if flags & FXF_READ:
-            if flags & FXF_WRITE:
-                raise NotImplementedError
-            d = self._get_node_and_metadata_for_path(path)
-            d.addCallback(lambda (node,metadata): ReadFile(node))
-            d.addErrback(self._convert_error)
-            return d
-
-        if flags & FXF_WRITE:
-            if not (flags & FXF_CREAT) or not (flags & FXF_TRUNC):
-                raise NotImplementedError
-            if not path:
-                raise PermissionError("cannot STOR to root directory")
-            childname = path[-1]
-            d = self._get_root(path)
-            def _got_root((root, path)):
-                if not path:
-                    raise PermissionError("cannot STOR to root directory")
-                return root.get_child_at_path(path[:-1])
-            d.addCallback(_got_root)
-            def _got_parent(parent):
-                return WriteFile(parent, childname, self.convergence)
-            d.addCallback(_got_parent)
-            return d
-        raise NotImplementedError
-
-    def removeFile(self, path):
-        print "REMOVEFILE", path
-        path = self._convert_sftp_path(path)
-        return self._remove_thing(path, must_be_file=True)
-
-    def renameFile(self, oldpath, newpath):
-        print "RENAMEFILE", oldpath, newpath
-        fromPath = self._convert_sftp_path(oldpath)
-        toPath = self._convert_sftp_path(newpath)
-        # the target directory must already exist
-        d = self._get_parent(fromPath)
-        def _got_from_parent( (fromparent, childname) ):
-            d = self._get_parent(toPath)
-            d.addCallback(lambda (toparent, tochildname):
-                          fromparent.move_child_to(childname,
-                                                   toparent, tochildname,
-                                                   overwrite=False))
-            return d
-        d.addCallback(_got_from_parent)
-        d.addErrback(self._convert_error)
-        return d
-
-    def makeDirectory(self, path, attrs):
-        print "MAKEDIRECTORY", path, attrs
-        # TODO: extract attrs["mtime"], use it to set the parent metadata.
-        # Maybe also copy attrs["ext_*"] .
-        path = self._convert_sftp_path(path)
-        d = self._get_root(path)
-        d.addCallback(lambda (root,path):
-                      self._get_or_create_directories(root, path))
-        return d
-
-    def _get_or_create_directories(self, node, path):
-        if not IDirectoryNode.providedBy(node):
-            # unfortunately it is too late to provide the name of the
-            # blocking directory in the error message.
-            raise ExistingChildError("cannot create directory because there "
-                                     "is a file in the way") # close enough
-        if not path:
-            return defer.succeed(node)
-        d = node.get(path[0])
-        def _maybe_create(f):
-            f.trap(NoSuchChildError)
-            return node.create_empty_directory(path[0])
-        d.addErrback(_maybe_create)
-        d.addCallback(self._get_or_create_directories, path[1:])
-        return d
-
-    def removeDirectory(self, path):
-        print "REMOVEDIRECTORY", path
-        path = self._convert_sftp_path(path)
-        return self._remove_thing(path, must_be_directory=True)
-
-    def _remove_thing(self, path, must_be_directory=False, must_be_file=False):
-        d = defer.maybeDeferred(self._get_parent, path)
-        def _convert_error(f):
-            f.trap(NoParentError)
-            raise PermissionError("cannot delete root directory")
-        d.addErrback(_convert_error)
-        def _got_parent( (parent, childname) ):
-            d = parent.get(childname)
-            def _got_child(child):
-                if must_be_directory and not IDirectoryNode.providedBy(child):
-                    raise RuntimeError("rmdir called on a file")
-                if must_be_file and IDirectoryNode.providedBy(child):
-                    raise RuntimeError("rmfile called on a directory")
-                return parent.delete(childname)
-            d.addCallback(_got_child)
-            d.addErrback(self._convert_error)
-            return d
-        d.addCallback(_got_parent)
-        return d
-
-
-    def openDirectory(self, path):
-        print "OPENDIRECTORY", path
-        path = self._convert_sftp_path(path)
-        d = self._get_node_and_metadata_for_path(path)
-        d.addCallback(lambda (dirnode,metadata): dirnode.list())
-        def _render(children):
-            results = []
-            for filename, (node, metadata) in children.iteritems():
-                s = FakeStat()
-                if IDirectoryNode.providedBy(node):
-                    s.st_mode = 040700
-                    s.st_size = 0
-                else:
-                    s.st_mode = 0100600
-                    s.st_size = node.get_size()
-                s.st_nlink = 1
-                s.st_uid = 0
-                s.st_gid = 0
-                s.st_mtime = int(metadata.get("mtime", 0))
-                longname = ls.lsLine(filename.encode("utf-8"), s)
-                attrs = self._populate_attrs(node, metadata)
-                results.append( (filename.encode("utf-8"), longname, attrs) )
-            return StoppableList(results)
-        d.addCallback(_render)
-        return d
-
-    def getAttrs(self, path, followLinks):
-        print "GETATTRS", path, followLinks
-        # from ftp.stat
-        d = self._get_node_and_metadata_for_path(self._convert_sftp_path(path))
-        def _render((node,metadata)):
-            return self._populate_attrs(node, metadata)
-        d.addCallback(_render)
-        d.addErrback(self._convert_error)
-        def _done(res):
-            print " DONE", res
-            return res
-        d.addBoth(_done)
-        return d
-
-    def _convert_sftp_path(self, pathstring):
-        assert pathstring[0] == "/"
-        pathstring = pathstring.strip("/")
-        if pathstring == "":
-            path = []
-        else:
-            path = pathstring.split("/")
-        print "CONVERT", pathstring, path
-        path = [unicode(p) for p in path]
-        return path
-
-    def _get_node_and_metadata_for_path(self, path):
-        d = self._get_root(path)
-        def _got_root((root,path)):
-            print "ROOT", root
-            print "PATH", path
-            if path:
-                return root.get_child_and_metadata_at_path(path)
-            else:
-                return (root,{})
-        d.addCallback(_got_root)
-        return d
-
-    def _get_root(self, path):
-        # return (root, remaining_path)
-        path = [unicode(p) for p in path]
-        if path and path[0] == "uri":
-            d = defer.maybeDeferred(self.client.create_node_from_uri,
-                                    str(path[1]))
-            d.addCallback(lambda root: (root, path[2:]))
-        else:
-            d = defer.succeed((self.root,path))
-        return d
-
-    def _populate_attrs(self, childnode, metadata):
-        attrs = {}
-        attrs["uid"] = 1000
-        attrs["gid"] = 1000
-        attrs["atime"] = 0
-        attrs["mtime"] = int(metadata.get("mtime", 0))
-        isdir = bool(IDirectoryNode.providedBy(childnode))
-        if isdir:
-            attrs["size"] = 1
-            # the permissions must have the extra bits (040000 or 0100000),
-            # otherwise the client will not call openDirectory
-            attrs["permissions"] = 040700 # S_IFDIR
-        else:
-            attrs["size"] = childnode.get_size()
-            attrs["permissions"] = 0100600 # S_IFREG
-        return attrs
-
-    def _convert_error(self, f):
-        if f.check(NoSuchChildError):
-            childname = f.value.args[0].encode("utf-8")
-            raise SFTPError(FX_NO_SUCH_FILE, childname)
-        if f.check(ExistingChildError):
-            msg = f.value.args[0].encode("utf-8")
-            raise SFTPError(FX_FILE_ALREADY_EXISTS, msg)
-        if f.check(PermissionError):
-            raise SFTPError(FX_PERMISSION_DENIED, str(f.value))
-        if f.check(NotImplementedError):
-            raise SFTPError(FX_OP_UNSUPPORTED, str(f.value))
-        return f
-
-
-    def setAttrs(self, path, attrs):
-        print "SETATTRS", path, attrs
-        # ignored
-        return None
-
-    def readLink(self, path):
-        print "READLINK", path
-        raise NotImplementedError
-
-    def makeLink(self, linkPath, targetPath):
-        print "MAKELINK", linkPath, targetPath
-        raise NotImplementedError
-
-    def extendedRequest(self, extendedName, extendedData):
-        print "EXTENDEDREQUEST", extendedName, extendedData
-        # client 'df' command requires 'statvfs@openssh.com' extension
-        raise NotImplementedError
-    def realPath(self, path):
-        print "REALPATH", path
-        if path == ".":
-            return "/"
-        return path
-
-
-    def _get_parent(self, path):
-        # fire with (parentnode, childname)
-        path = [unicode(p) for p in path]
-        if not path:
-            raise NoParentError
-        childname = path[-1]
-        d = self._get_root(path)
-        def _got_root((root, path)):
-            if not path:
-                raise NoParentError
-            return root.get_child_at_path(path[:-1])
-        d.addCallback(_got_root)
-        def _got_parent(parent):
-            return (parent, childname)
-        d.addCallback(_got_parent)
-        return d
-
-
-class FTPAvatarID:
-    def __init__(self, username, rootcap):
-        self.username = username
-        self.rootcap = rootcap
-
-class AccountFileChecker:
-    implements(checkers.ICredentialsChecker)
-    credentialInterfaces = (credentials.IUsernamePassword,
-                            credentials.IUsernameHashedPassword)
-    def __init__(self, client, accountfile):
-        self.client = client
-        self.passwords = {}
-        self.pubkeys = {}
-        self.rootcaps = {}
-        for line in open(os.path.expanduser(accountfile), "r"):
-            line = line.strip()
-            if line.startswith("#") or not line:
-                continue
-            name, passwd, rest = line.split(None, 2)
-            if passwd in ("ssh-dss", "ssh-rsa"):
-                bits = rest.split()
-                keystring = " ".join(bits[-1])
-                rootcap = bits[-1]
-                self.pubkeys[name] = keystring
-            else:
-                self.passwords[name] = passwd
-                rootcap = rest
-            self.rootcaps[name] = rootcap
-
-    def _cbPasswordMatch(self, matched, username):
-        if matched:
-            return FTPAvatarID(username, self.rootcaps[username])
-        raise error.UnauthorizedLogin
-
-    def requestAvatarId(self, credentials):
-        if credentials.username in self.passwords:
-            d = defer.maybeDeferred(credentials.checkPassword,
-                                    self.passwords[credentials.username])
-            d.addCallback(self._cbPasswordMatch, str(credentials.username))
-            return d
-        return defer.fail(error.UnauthorizedLogin())
-
-class AccountURLChecker:
-    implements(checkers.ICredentialsChecker)
-    credentialInterfaces = (credentials.IUsernamePassword,)
-
-    def __init__(self, client, auth_url):
-        self.client = client
-        self.auth_url = auth_url
-
-    def _cbPasswordMatch(self, rootcap, username):
-        return FTPAvatarID(username, rootcap)
-
-    def post_form(self, username, password):
-        sepbase = base32.b2a(os.urandom(4))
-        sep = "--" + sepbase
-        form = []
-        form.append(sep)
-        fields = {"action": "authenticate",
-                  "email": username,
-                  "passwd": password,
-                  }
-        for name, value in fields.iteritems():
-            form.append('Content-Disposition: form-data; name="%s"' % name)
-            form.append('')
-            assert isinstance(value, str)
-            form.append(value)
-            form.append(sep)
-        form[-1] += "--"
-        body = "\r\n".join(form) + "\r\n"
-        headers = {"content-type": "multipart/form-data; boundary=%s" % sepbase,
-                   }
-        return getPage(self.auth_url, method="POST",
-                       postdata=body, headers=headers,
-                       followRedirect=True, timeout=30)
-
-    def _parse_response(self, res):
-        rootcap = res.strip()
-        if rootcap == "0":
-            raise error.UnauthorizedLogin
-        return rootcap
-
-    def requestAvatarId(self, credentials):
-        # construct a POST to the login form. While this could theoretically
-        # be done with something like the stdlib 'email' package, I can't
-        # figure out how, so we just slam together a form manually.
-        d = self.post_form(credentials.username, credentials.password)
-        d.addCallback(self._parse_response)
-        d.addCallback(self._cbPasswordMatch, str(credentials.username))
-        return d
-
-# if you have an SFTPUser, and you want something that provides ISFTPServer,
-# then you get SFTPHandler(user)
-components.registerAdapter(SFTPHandler, SFTPUser, ISFTPServer)
-
-class Dispatcher:
-    implements(portal.IRealm)
-    def __init__(self, client):
-        self.client = client
-
-    def requestAvatar(self, avatarID, mind, interface):
-        assert interface == IConchUser
-        rootnode = self.client.create_node_from_uri(avatarID.rootcap)
-        convergence = self.client.convergence
-        s = SFTPUser(self.client, rootnode, avatarID.username, convergence)
-        def logout(): pass
-        return (interface, s, logout)
-
-class SFTPServer(service.MultiService):
-    def __init__(self, client, accountfile, accounturl,
-                 sftp_portstr, pubkey_file, privkey_file):
-        service.MultiService.__init__(self)
-
-        if accountfile:
-            c = AccountFileChecker(self, accountfile)
-        elif accounturl:
-            c = AccountURLChecker(self, accounturl)
-        else:
-            # we could leave this anonymous, with just the /uri/CAP form
-            raise RuntimeError("must provide some translation")
-
-        r = Dispatcher(client)
-        p = portal.Portal(r)
-        p.registerChecker(c)
-
-        pubkey = keys.Key.fromFile(pubkey_file)
-        privkey = keys.Key.fromFile(privkey_file)
-        class SSHFactory(factory.SSHFactory):
-            publicKeys = {pubkey.sshType(): pubkey}
-            privateKeys = {privkey.sshType(): privkey}
-            def getPrimes(self):
-                try:
-                    # if present, this enables diffie-hellman-group-exchange
-                    return primes.parseModuliFile("/etc/ssh/moduli")
-                except IOError:
-                    return None
-
-        f = SSHFactory()
-        f.portal = p
-
-        s = strports.service(sftp_portstr, f)
-        s.setServiceParent(self)
-