From 6a2b0229f0753f001c989e04882441386d13446f Mon Sep 17 00:00:00 2001
From: david-sarah <david-sarah@jacaranda.org>
Date: Sun, 16 May 2010 08:43:47 -0700
Subject: [PATCH] SFTP: Support statvfs extensions, avoid logging actual data,
 and decline shell sessions politely.

---
 src/allmydata/frontends/sftpd.py | 36 ++++++++++++++++++----------
 src/allmydata/test/test_sftp.py  | 40 ++++++++++++++++++++++++--------
 2 files changed, 54 insertions(+), 22 deletions(-)

diff --git a/src/allmydata/frontends/sftpd.py b/src/allmydata/frontends/sftpd.py
index b36d29b8..e7f98633 100644
--- a/src/allmydata/frontends/sftpd.py
+++ b/src/allmydata/frontends/sftpd.py
@@ -1,5 +1,5 @@
 
-import os, tempfile, heapq, binascii, traceback, array, stat
+import os, tempfile, heapq, binascii, traceback, array, stat, struct
 from stat import S_IFREG, S_IFDIR
 from time import time, strftime, localtime
 
@@ -41,17 +41,15 @@ warnings.filterwarnings("ignore", category=DeprecationWarning,
 noisy = True
 use_foolscap_logging = True
 
+from allmydata.util.log import NOISY, OPERATIONAL, SCARY
+
 if use_foolscap_logging:
-    from allmydata.util.log import msg as logmsg, err as logerr, \
-        NOISY, OPERATIONAL, SCARY, PrefixingLogMixin
+    from allmydata.util.log import msg as logmsg, err as logerr, PrefixingLogMixin
 else:
     def logmsg(s, level=None):
         print s
     def logerr(s, level=None):
         print s
-    NOISY = None
-    OPERATIONAL = None
-    SCARY = None
     class PrefixingLogMixin:
         def __init__(self, facility=None):
             pass
@@ -290,7 +288,7 @@ class EncryptedTemporaryFile(PrefixingLogMixin):
         return plaintext
 
     def write(self, plaintext):
-        if noisy: self.log(".write(%r)" % (plaintext,), level=NOISY)
+        if noisy: self.log(".write(<data of length %r>)" % (len(plaintext),), level=NOISY)
         index = self.file.tell()
         ciphertext = self._crypt(index, plaintext)
         self.file.write(ciphertext)
@@ -357,7 +355,7 @@ class OverwriteableFileConsumer(PrefixingLogMixin):
                 p.resumeProducing()
 
     def write(self, data):
-        if noisy: self.log(".write(%r)" % (data,), level=NOISY)
+        if noisy: self.log(".write(<data of length %r>)" % (len(data),), level=NOISY)
         if self.check_abort():
             self.close()
             return
@@ -833,7 +831,8 @@ class SFTPUserHandler(ConchUser, PrefixingLogMixin):
 
     def openShell(self, protocol):
         self.log(".openShell(%r)" % (protocol,), level=OPERATIONAL)
-        raise NotImplementedError
+        protocol.write("This server supports only SFTP, not shell sessions.\n")
+        protocol.processEnded(Reason(ProcessTerminated(exitCode=1)))
 
     def execCommand(self, protocol, cmd):
         self.log(".execCommand(%r, %r)" % (protocol, cmd), level=OPERATIONAL)
@@ -1167,9 +1166,22 @@ class SFTPUserHandler(ConchUser, PrefixingLogMixin):
     def extendedRequest(self, extendedName, extendedData):
         self.log(".extendedRequest(%r, %r)" % (extendedName, extendedData), level=OPERATIONAL)
 
-        # A client 'df' command requires the 'statvfs@openssh.com' extension,
-        # but there's little point to implementing that since we would only
-        # have faked values to report.
+        if extendedName == 'statvfs@openssh.com' or extendedName == 'fstatvfs@openssh.com':
+            # <http://dev.libssh.org/ticket/11>
+            return struct.pack('>QQQQQQQQQQQ',
+                1024,         # uint64  f_bsize     /* file system block size */
+                1024,         # uint64  f_frsize    /* fundamental fs block size */
+                628318530,    # uint64  f_blocks    /* number of blocks (unit f_frsize) */
+                314159265,    # uint64  f_bfree     /* free blocks in file system */
+                314159265,    # uint64  f_bavail    /* free blocks for non-root */
+                200000000,    # uint64  f_files     /* total file inodes */
+                100000000,    # uint64  f_ffree     /* free file inodes */
+                100000000,    # uint64  f_favail    /* free file inodes for non-root */
+                0x1AF5,       # uint64  f_fsid      /* file system id */
+                2,            # uint64  f_flag      /* bit mask = ST_NOSUID; not ST_RDONLY */
+                65535,        # uint64  f_namemax   /* maximum filename length */
+                )
+
         raise SFTPError(FX_OP_UNSUPPORTED, "extendedRequest %r" % extendedName)
 
     def realPath(self, pathstring):
diff --git a/src/allmydata/test/test_sftp.py b/src/allmydata/test/test_sftp.py
index 418ba09d..919c6806 100644
--- a/src/allmydata/test/test_sftp.py
+++ b/src/allmydata/test/test_sftp.py
@@ -266,9 +266,6 @@ class Handler(GridTestMixin, ShouldFailMixin, unittest.TestCase):
         d.addCallback(lambda ign:
             self.shouldFailWithSFTPError(sftp.FX_OP_UNSUPPORTED, "makeLink link file",
                                          self.handler.makeLink, "link", "file"))
-        d.addCallback(lambda ign:
-            self.shouldFailWithSFTPError(sftp.FX_OP_UNSUPPORTED, "extendedRequest foo bar",
-                                         self.handler.extendedRequest, "foo", "bar"))
 
         return d
 
@@ -913,7 +910,7 @@ class Handler(GridTestMixin, ShouldFailMixin, unittest.TestCase):
                                          self.handler.makeDirectory, "small", {}))
         return d
 
-    def test_execCommand(self):
+    def test_execCommand_and_openShell(self):
         class FakeProtocol:
             def __init__(self):
                 self.output = ""
@@ -923,20 +920,43 @@ class Handler(GridTestMixin, ShouldFailMixin, unittest.TestCase):
             def processEnded(self, reason):
                 self.reason = reason
 
-        protocol_ok = FakeProtocol()
+        protocol_df = FakeProtocol()
         protocol_error = FakeProtocol()
+        protocol_shell = FakeProtocol()
 
-        d = self._set_up("execCommand")
+        d = self._set_up("execCommand_and_openShell")
 
-        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.execCommand(protocol_df, "df -P -k /"))
+        d.addCallback(lambda ign: self.failUnlessIn("1024-blocks", protocol_df.output))
+        d.addCallback(lambda ign: self.failUnless(isinstance(protocol_df.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.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))
+        d.addCallback(lambda ign: self.handler.closed())
+
+        d.addCallback(lambda ign: self.handler.openShell(protocol_shell))
+        d.addCallback(lambda ign: self.failUnlessIn("only SFTP", protocol_shell.output))
+        d.addCallback(lambda ign: self.failUnless(isinstance(protocol_shell.reason.value, ProcessTerminated)))
+        d.addCallback(lambda ign: self.failUnlessEqual(protocol_shell.reason.value.exitCode, 1))
+        d.addCallback(lambda ign: self.handler.closed())
+
+        return d
+
+    def test_extendedRequest(self):
+        d = self._set_up("extendedRequest")
+
+        d.addCallback(lambda ign: self.handler.extendedRequest("statvfs@openssh.com", "/"))
+        def _check(res):
+            self.failUnless(isinstance(res, str))
+            self.failUnlessEqual(len(res), 8*11)
+        d.addCallback(_check)
+
+        d.addCallback(lambda ign:
+            self.shouldFailWithSFTPError(sftp.FX_OP_UNSUPPORTED, "extendedRequest foo bar",
+                                         self.handler.extendedRequest, "foo", "bar"))
 
         return d
\ No newline at end of file
-- 
2.45.2