From 8028bf01bc9ad5c476906a435bb044d7c04a2749 Mon Sep 17 00:00:00 2001 From: david-sarah Date: Sat, 15 May 2010 18:27:54 -0700 Subject: [PATCH] SFTP: implement execCommand to interoperate with clients that issue a 'df -P -k /' command. Also eliminate use of Zope adaptation. --- src/allmydata/frontends/sftpd.py | 79 ++++++++++++++++++++++++-------- src/allmydata/test/test_sftp.py | 36 +++++++++++++-- 2 files changed, 92 insertions(+), 23 deletions(-) diff --git a/src/allmydata/frontends/sftpd.py b/src/allmydata/frontends/sftpd.py index 826f8192..5407ebad 100644 --- a/src/allmydata/frontends/sftpd.py +++ b/src/allmydata/frontends/sftpd.py @@ -16,6 +16,7 @@ from twisted.conch.interfaces import ISFTPServer, ISFTPFile, IConchUser, ISessio from twisted.conch.avatar import ConchUser from twisted.conch.openssh_compat import primes from twisted.cred import portal +from twisted.internet.error import ProcessDone, ProcessTerminated from twisted.internet import defer from twisted.internet.interfaces import IFinishableConsumer @@ -786,6 +787,7 @@ class GeneralSFTPFile(PrefixingLogMixin): self.async.addCallbacks(_resize, eventually_errback(d)) return d + class StoppableList: def __init__(self, items): self.items = items @@ -796,17 +798,63 @@ class StoppableList: pass -class SFTPHandler(PrefixingLogMixin): - implements(ISFTPServer) - def __init__(self, user): +class Reason: + def __init__(self, value): + self.value = value + + +class SFTPUserHandler(ConchUser, PrefixingLogMixin): + implements(ISFTPServer, ISession) + def __init__(self, check_abort, client, rootnode, username): + ConchUser.__init__(self) PrefixingLogMixin.__init__(self, facility="tahoe.sftp") - if noisy: self.log(".__init__(%r)" % (user,), level=NOISY) + if noisy: self.log(".__init__(%r, %r, %r, %r)" % + (check_abort, client, rootnode, username), level=NOISY) + + self.channelLookup["session"] = session.SSHSession + self.subsystemLookup["sftp"] = FileTransferServer + + self.client = client + self.root = rootnode + self.username = username + self.convergence = client.convergence + self.logged_out = False + + def logout(self): + self.logged_out = True + + def check_abort(self): + return self.logged_out + + # ISession + # This is needed because some clients may try to issue a 'df' command. + + def getPty(self, terminal, windowSize, attrs): + self.log(".getPty(%r, %r, %r)" % (terminal, windowSize, attrs), level=OPERATIONAL) - self.check_abort = user.check_abort - self.client = user.client - self.root = user.root - self.username = user.username - self.convergence = user.convergence + def openShell(self, protocol): + self.log(".openShell(%r)" % (protocol,), level=OPERATIONAL) + raise NotImplementedError + + def execCommand(self, protocol, cmd): + self.log(".execCommand(%r, %r)" % (protocol, cmd), level=OPERATIONAL) + if cmd == "df -P -k /": + protocol.write("Filesystem 1024-blocks Used Available Capacity Mounted on\n" + "tahoe 628318530 314159265 314159265 50% /\n") + protocol.processEnded(Reason(ProcessDone(None))) + else: + protocol.processEnded(Reason(ProcessTerminated(exitCode=1))) + + def windowChanged(self, newWindowSize): + self.log(".windowChanged(%r)" % (newWindowSize,), level=OPERATIONAL) + + def eofReceived(self): + self.log(".eofReceived()", level=OPERATIONAL) + + def closed(self): + self.log(".closed()", level=OPERATIONAL) + + # ISFTPServer def gotVersion(self, otherVersion, extData): self.log(".gotVersion(%r, %r)" % (otherVersion, extData), level=OPERATIONAL) @@ -1262,14 +1310,9 @@ class Dispatcher: def requestAvatar(self, avatarID, mind, interface): assert interface == IConchUser rootnode = self.client.create_node_from_uri(avatarID.rootcap) - convergence = self.client.convergence - logged_out = {'flag': False} - def check_abort(): - return logged_out['flag'] - def logout(): - logged_out['flag'] = True - s = SFTPUser(check_abort, self.client, rootnode, avatarID.username, convergence) - return (interface, s, logout) + handler = SFTPUserHandler(self.client, rootnode, avatarID.username) + return (interface, handler, handler.logout) + class SFTPServer(service.MultiService): def __init__(self, client, accountfile, accounturl, @@ -1287,7 +1330,7 @@ class SFTPServer(service.MultiService): p.registerChecker(c) if not accountfile and not accounturl: # we could leave this anonymous, with just the /uri/CAP form - raise NeedRootcapLookupScheme("must provide some translation") + raise NeedRootcapLookupScheme("must provide an account file or URL") pubkey = keys.Key.fromFile(pubkey_file) privkey = keys.Key.fromFile(privkey_file) diff --git a/src/allmydata/test/test_sftp.py b/src/allmydata/test/test_sftp.py index 7e31fcce..de0561a9 100644 --- a/src/allmydata/test/test_sftp.py +++ b/src/allmydata/test/test_sftp.py @@ -5,6 +5,7 @@ from stat import S_IFREG, S_IFDIR from twisted.trial import unittest from twisted.internet import defer from twisted.python.failure import Failure +from twisted.internet.error import ProcessDone, ProcessTerminated sftp = None sftpd = None @@ -89,18 +90,15 @@ class Handler(GridTestMixin, ShouldFailMixin, unittest.TestCase): self.basedir = "sftp/" + basedir self.set_up_grid(num_clients=num_clients, num_servers=num_servers) - def check_abort(): - pass + self.check_abort = lambda: False self.client = self.g.clients[0] self.username = "alice" - self.convergence = "convergence" d = self.client.create_dirnode() def _created_root(node): self.root = node self.root_uri = node.get_uri() - self.user = sftpd.SFTPUser(check_abort, self.client, self.root, self.username, self.convergence) - self.handler = sftpd.SFTPHandler(self.user) + self.handler = sftpd.SFTPUserHandler(self.check_abort, self.client, self.root, self.username) d.addCallback(_created_root) return d @@ -915,3 +913,31 @@ class Handler(GridTestMixin, ShouldFailMixin, unittest.TestCase): self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "makeDirectory small", self.handler.makeDirectory, "small", {})) return d + + def test_execCommand(self): + class FakeProtocol: + def __init__(self): + self.output = "" + self.reason = None + def write(self, data): + self.output += data + def processEnded(self, reason): + self.reason = reason + + protocol_ok = FakeProtocol() + protocol_error = FakeProtocol() + + d = self._set_up("execCommand") + + d.addCallback(lambda ign: self.handler.execCommand(protocol_ok, "df -P -k /")) + d.addCallback(lambda ign: self.failUnlessIn("1024-blocks", protocol_ok.output)) + d.addCallback(lambda ign: self.failUnless(isinstance(protocol_ok.reason.value, ProcessDone))) + d.addCallback(lambda ign: self.handler.eofReceived()) + d.addCallback(lambda ign: self.handler.closed()) + + d.addCallback(lambda ign: self.handler.execCommand(protocol_error, "error")) + d.addCallback(lambda ign: self.failUnlessEqual(protocol_error.output, "")) + d.addCallback(lambda ign: self.failUnless(isinstance(protocol_error.reason.value, ProcessTerminated))) + d.addCallback(lambda ign: self.failUnlessEqual(protocol_error.reason.value.exitCode, 1)) + + return d \ No newline at end of file -- 2.45.2