From: Brian Warner Date: Wed, 5 Nov 2008 21:07:33 +0000 (-0700) Subject: ftp/sftp: move to a new frontends/ directory in preparation for factoring out passwor... X-Git-Url: https://git.rkrishnan.org/pf/content//%22news.html/%22?a=commitdiff_plain;h=c489a05235cce26a4e058b5b2db837a855a2a79f;p=tahoe-lafs%2Ftahoe-lafs.git ftp/sftp: move to a new frontends/ directory in preparation for factoring out password-auth component --- diff --git a/src/allmydata/client.py b/src/allmydata/client.py index 418ba761..1574615f 100644 --- a/src/allmydata/client.py +++ b/src/allmydata/client.py @@ -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 index 00000000..e69de29b diff --git a/src/allmydata/frontends/ftpd.py b/src/allmydata/frontends/ftpd.py new file mode 100644 index 00000000..5ade0e20 --- /dev/null +++ b/src/allmydata/frontends/ftpd.py @@ -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 index 00000000..790e5d7e --- /dev/null +++ b/src/allmydata/frontends/sftpd.py @@ -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 index 5ade0e20..00000000 --- a/src/allmydata/ftpd.py +++ /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 index 790e5d7e..00000000 --- a/src/allmydata/sftpd.py +++ /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) -