]> git.rkrishnan.org Git - tahoe-lafs/tahoe-lafs.git/blobdiff - src/allmydata/test/test_sftp.py
Improve SFTP error handling and remove use of IFinishableConsumer. fixes #1525
[tahoe-lafs/tahoe-lafs.git] / src / allmydata / test / test_sftp.py
index 0bc20e8789e9f0315f6e42b980757969314676a7..69fedce4e6854fda00d60304131db069549f8ca1 100644 (file)
@@ -6,6 +6,7 @@ from twisted.trial import unittest
 from twisted.internet import defer, reactor
 from twisted.python.failure import Failure
 from twisted.internet.error import ProcessDone, ProcessTerminated
+from allmydata.util import deferredutil
 
 conch_interfaces = None
 sftp = None
@@ -28,12 +29,17 @@ from allmydata.mutable.common import NotWriteableError
 
 from allmydata.util.consumer import download_to_data
 from allmydata.immutable import upload
+from allmydata.mutable import publish
 from allmydata.test.no_network import GridTestMixin
 from allmydata.test.common import ShouldFailMixin
 from allmydata.test.common_util import ReallyEqualMixin
 
 timeout = 240
 
+#defer.setDebugging(True)
+#from twisted.internet import base
+#base.DelayedCall.debug = True
+
 class Handler(GridTestMixin, ShouldFailMixin, ReallyEqualMixin, unittest.TestCase):
     """This is a no-network unit test of the SFTPUserHandler and the abstractions it uses."""
 
@@ -49,19 +55,15 @@ class Handler(GridTestMixin, ShouldFailMixin, ReallyEqualMixin, unittest.TestCas
             if isinstance(res, Failure):
                 res.trap(sftp.SFTPError)
                 self.failUnlessReallyEqual(res.value.code, expected_code,
-                                           "%s was supposed to raise SFTPError(%d), not SFTPError(%d): %s" %
+                                           "%s was supposed to raise SFTPError(%r), not SFTPError(%r): %s" %
                                            (which, expected_code, res.value.code, res))
             else:
                 print '@' + '@'.join(s)
-                self.fail("%s was supposed to raise SFTPError(%d), not get %r" %
+                self.fail("%s was supposed to raise SFTPError(%r), not get %r" %
                           (which, expected_code, res))
         d.addBoth(_done)
         return d
 
-    def failUnlessReallyEqual(self, a, b, msg=None):
-        self.failUnlessEqual(a, b, msg=msg)
-        self.failUnlessEqual(type(a), type(b), msg=msg)
-
     def _set_up(self, basedir, num_clients=1, num_servers=10):
         self.basedir = "sftp/" + basedir
         self.set_up_grid(num_clients=num_clients, num_servers=num_servers)
@@ -73,12 +75,14 @@ class Handler(GridTestMixin, ShouldFailMixin, ReallyEqualMixin, unittest.TestCas
         def _created_root(node):
             self.root = node
             self.root_uri = node.get_uri()
+            sftpd._reload()
             self.handler = sftpd.SFTPUserHandler(self.client, self.root, self.username)
         d.addCallback(_created_root)
         return d
 
     def _set_up_tree(self):
-        d = self.client.create_mutable_file("mutable file contents")
+        u = publish.MutableData("mutable file contents")
+        d = self.client.create_mutable_file(u)
         d.addCallback(lambda node: self.root.set_node(u"mutable", node))
         def _created_mutable(n):
             self.mutable = n
@@ -169,6 +173,8 @@ class Handler(GridTestMixin, ShouldFailMixin, ReallyEqualMixin, unittest.TestCas
             self.failUnlessReallyEqual(self.handler._path_from_string("/foo/bar"), [u"foo", u"bar"])
             self.failUnlessReallyEqual(self.handler._path_from_string("foo/bar//"), [u"foo", u"bar"])
             self.failUnlessReallyEqual(self.handler._path_from_string("/foo/bar//"), [u"foo", u"bar"])
+            self.failUnlessReallyEqual(self.handler._path_from_string("foo/./bar"), [u"foo", u"bar"])
+            self.failUnlessReallyEqual(self.handler._path_from_string("./foo/./bar"), [u"foo", u"bar"])
             self.failUnlessReallyEqual(self.handler._path_from_string("foo/../bar"), [u"bar"])
             self.failUnlessReallyEqual(self.handler._path_from_string("/foo/../bar"), [u"bar"])
             self.failUnlessReallyEqual(self.handler._path_from_string("../bar"), [u"bar"])
@@ -188,6 +194,8 @@ class Handler(GridTestMixin, ShouldFailMixin, ReallyEqualMixin, unittest.TestCas
             self.failUnlessReallyEqual(self.handler.realPath("/foo/bar"), "/foo/bar")
             self.failUnlessReallyEqual(self.handler.realPath("foo/bar//"), "/foo/bar")
             self.failUnlessReallyEqual(self.handler.realPath("/foo/bar//"), "/foo/bar")
+            self.failUnlessReallyEqual(self.handler.realPath("foo/./bar"), "/foo/bar")
+            self.failUnlessReallyEqual(self.handler.realPath("./foo/./bar"), "/foo/bar")
             self.failUnlessReallyEqual(self.handler.realPath("foo/../bar"), "/bar")
             self.failUnlessReallyEqual(self.handler.realPath("/foo/../bar"), "/bar")
             self.failUnlessReallyEqual(self.handler.realPath("../bar"), "/bar")
@@ -205,7 +213,7 @@ class Handler(GridTestMixin, ShouldFailMixin, ReallyEqualMixin, unittest.TestCas
 
     def test_convert_error(self):
         self.failUnlessReallyEqual(sftpd._convert_error(None, "request"), None)
-        
+
         d = defer.succeed(None)
         d.addCallback(lambda ign:
             self.shouldFailWithSFTPError(sftp.FX_FAILURE, "_convert_error SFTPError",
@@ -287,17 +295,17 @@ class Handler(GridTestMixin, ShouldFailMixin, ReallyEqualMixin, unittest.TestCas
 
         gross = u"gro\u00DF".encode("utf-8")
         expected_root = [
-            ('empty_lit_dir', r'dr-xr-xr-x .* \? .* empty_lit_dir$',      {'permissions': S_IFDIR | 0555}),
+            ('empty_lit_dir', r'dr-xr-xr-x .* 0 .* empty_lit_dir$',       {'permissions': S_IFDIR | 0555}),
             (gross,           r'-rw-rw-rw- .* 1010 .* '+gross+'$',        {'permissions': S_IFREG | 0666, 'size': 1010}),
             # The fall of the Berlin wall may have been on 9th or 10th November 1989 depending on the gateway's timezone.
-            #('loop',          r'drwxrwxrwx .* \? Nov (09|10)  1989 loop$', {'permissions': S_IFDIR | 0777}),
-            ('loop',          r'drwxrwxrwx .* \? .* loop$',               {'permissions': S_IFDIR | 0777}),
-            ('mutable',       r'-rw-rw-rw- .* \? .* mutable$',            {'permissions': S_IFREG | 0666}),
-            ('readonly',      r'-r--r--r-- .* \? .* readonly$',           {'permissions': S_IFREG | 0444}),
+            #('loop',          r'drwxrwxrwx .* 0 Nov (09|10)  1989 loop$', {'permissions': S_IFDIR | 0777}),
+            ('loop',          r'drwxrwxrwx .* 0 .* loop$',                {'permissions': S_IFDIR | 0777}),
+            ('mutable',       r'-rw-rw-rw- .* 0 .* mutable$',             {'permissions': S_IFREG | 0666}),
+            ('readonly',      r'-r--r--r-- .* 0 .* readonly$',            {'permissions': S_IFREG | 0444}),
             ('small',         r'-rw-rw-rw- .* 10 .* small$',              {'permissions': S_IFREG | 0666, 'size': 10}),
             ('small2',        r'-rw-rw-rw- .* 26 .* small2$',             {'permissions': S_IFREG | 0666, 'size': 26}),
-            ('tiny_lit_dir',  r'dr-xr-xr-x .* \? .* tiny_lit_dir$',       {'permissions': S_IFDIR | 0555}),
-            ('unknown',       r'\?--------- .* \? .* unknown$',           {'permissions': 0}),
+            ('tiny_lit_dir',  r'dr-xr-xr-x .* 0 .* tiny_lit_dir$',        {'permissions': S_IFDIR | 0555}),
+            ('unknown',       r'\?--------- .* 0 .* unknown$',            {'permissions': 0}),
         ]
 
         d.addCallback(lambda ign: self.handler.openDirectory(""))
@@ -333,6 +341,8 @@ class Handler(GridTestMixin, ShouldFailMixin, ReallyEqualMixin, unittest.TestCas
             self.shouldFailWithSFTPError(sftp.FX_OP_UNSUPPORTED, "setAttrs size",
                                          self.handler.setAttrs, "small", {'size': 0}))
 
+        d.addCallback(lambda ign: self.failUnlessEqual(sftpd.all_heisenfiles, {}))
+        d.addCallback(lambda ign: self.failUnlessEqual(self.handler._heisenfiles, {}))
         return d
 
     def test_openFile_read(self):
@@ -513,6 +523,45 @@ class Handler(GridTestMixin, ShouldFailMixin, ReallyEqualMixin, unittest.TestCas
             return d2
         d.addCallback(_read_short)
 
+        # check that failed downloads cause failed reads
+        d.addCallback(lambda ign: self.handler.openFile("uri/"+self.gross_uri, sftp.FXF_READ, {}))
+        def _read_broken(rf):
+            d2 = defer.succeed(None)
+            d2.addCallback(lambda ign: self.g.nuke_from_orbit())
+            d2.addCallback(lambda ign:
+                self.shouldFailWithSFTPError(sftp.FX_FAILURE, "read broken",
+                                             rf.readChunk, 0, 100))
+            # close shouldn't fail
+            d2.addCallback(lambda ign: rf.close())
+            d2.addCallback(lambda res: self.failUnlessReallyEqual(res, None))
+            return d2
+        d.addCallback(_read_broken)
+
+        d.addCallback(lambda ign: self.failUnlessEqual(sftpd.all_heisenfiles, {}))
+        d.addCallback(lambda ign: self.failUnlessEqual(self.handler._heisenfiles, {}))
+        return d
+
+    def test_openFile_read_error(self):
+        # The check at the end of openFile_read tested this for large files, but it trashed
+        # the grid in the process, so this needs to be a separate test.
+        small = upload.Data("0123456789"*10, None)
+        d = self._set_up("openFile_read_error")
+        d.addCallback(lambda ign: self.root.add_file(u"small", small))
+        d.addCallback(lambda n: self.handler.openFile("/uri/"+n.get_uri(), sftp.FXF_READ, {}))
+        def _read_broken(rf):
+            d2 = defer.succeed(None)
+            d2.addCallback(lambda ign: self.g.nuke_from_orbit())
+            d2.addCallback(lambda ign:
+                self.shouldFailWithSFTPError(sftp.FX_FAILURE, "read broken",
+                                             rf.readChunk, 0, 100))
+            # close shouldn't fail
+            d2.addCallback(lambda ign: rf.close())
+            d2.addCallback(lambda res: self.failUnlessReallyEqual(res, None))
+            return d2
+        d.addCallback(_read_broken)
+
+        d.addCallback(lambda ign: self.failUnlessEqual(sftpd.all_heisenfiles, {}))
+        d.addCallback(lambda ign: self.failUnlessEqual(self.handler._heisenfiles, {}))
         return d
 
     def test_openFile_write(self):
@@ -641,6 +690,8 @@ class Handler(GridTestMixin, ShouldFailMixin, ReallyEqualMixin, unittest.TestCas
             d2.addCallback(lambda ign: wf.setAttrs({'size': 17}))
             d2.addCallback(lambda ign: wf.getAttrs())
             d2.addCallback(lambda attrs: self.failUnlessReallyEqual(attrs['size'], 17))
+            d2.addCallback(lambda ign: self.handler.getAttrs("newfile", followLinks=0))
+            d2.addCallback(lambda attrs: self.failUnlessReallyEqual(attrs['size'], 17))
 
             d2.addCallback(lambda ign:
                 self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "readChunk on write-only handle denied",
@@ -669,12 +720,18 @@ class Handler(GridTestMixin, ShouldFailMixin, ReallyEqualMixin, unittest.TestCas
         def _write_append(wf):
             d2 = wf.writeChunk(0, "0123456789")
             d2.addCallback(lambda ign: wf.writeChunk(8, "0123"))
+
+            d2.addCallback(lambda ign: wf.setAttrs({'size': 17}))
+            d2.addCallback(lambda ign: wf.getAttrs())
+            d2.addCallback(lambda attrs: self.failUnlessReallyEqual(attrs['size'], 17))
+
+            d2.addCallback(lambda ign: wf.writeChunk(0, "z"))
             d2.addCallback(lambda ign: wf.close())
             return d2
         d.addCallback(_write_append)
         d.addCallback(lambda ign: self.root.get(u"newfile"))
         d.addCallback(lambda node: download_to_data(node))
-        d.addCallback(lambda data: self.failUnlessReallyEqual(data, "01234567890123"))
+        d.addCallback(lambda data: self.failUnlessReallyEqual(data, "01234567890123\x00\x00\x00z"))
 
         # test WRITE | TRUNC without CREAT, when the file already exists
         # This is invalid according to section 6.3 of the SFTP spec, but required for interoperability,
@@ -812,6 +869,8 @@ class Handler(GridTestMixin, ShouldFailMixin, ReallyEqualMixin, unittest.TestCas
         d.addCallback(lambda node: download_to_data(node))
         d.addCallback(lambda data: self.failUnlessReallyEqual(data, "abcde56789"))
 
+        d.addCallback(lambda ign: self.root.set_node(u"mutable2", self.mutable))
+
         # test writing to a mutable file
         d.addCallback(lambda ign:
                       self.handler.openFile("mutable", sftp.FXF_WRITE, {}))
@@ -823,6 +882,7 @@ class Handler(GridTestMixin, ShouldFailMixin, ReallyEqualMixin, unittest.TestCas
         d.addCallback(lambda ign: self.root.get(u"mutable"))
         def _check_same_file(node):
             self.failUnless(node.is_mutable())
+            self.failIf(node.is_readonly())
             self.failUnlessReallyEqual(node.get_uri(), self.mutable_uri)
             return node.download_best_version()
         d.addCallback(_check_same_file)
@@ -836,11 +896,67 @@ class Handler(GridTestMixin, ShouldFailMixin, ReallyEqualMixin, unittest.TestCas
         d.addCallback(_check_same_file)
         d.addCallback(lambda data: self.failUnlessReallyEqual(data, "mutable new! contents"))
 
+        # ... and with a setAttrs call that diminishes the parent link to read-only, first by path
+        d.addCallback(lambda ign:
+                      self.handler.openFile("mutable", sftp.FXF_WRITE, {}))
+        def _write_mutable_setattr(wf):
+            d2 = wf.writeChunk(8, "read-only link from parent")
+
+            d2.addCallback(lambda ign: self.handler.setAttrs("mutable", {'permissions': 0444}))
+
+            d2.addCallback(lambda ign: self.root.get(u"mutable"))
+            d2.addCallback(lambda node: self.failUnless(node.is_readonly()))
+
+            d2.addCallback(lambda ign: wf.getAttrs())
+            d2.addCallback(lambda attrs: self.failUnlessReallyEqual(attrs['permissions'], S_IFREG | 0666))
+            d2.addCallback(lambda ign: self.handler.getAttrs("mutable", followLinks=0))
+            d2.addCallback(lambda attrs: self.failUnlessReallyEqual(attrs['permissions'], S_IFREG | 0444))
+
+            d2.addCallback(lambda ign: wf.close())
+            return d2
+        d.addCallback(_write_mutable_setattr)
+        d.addCallback(lambda ign: self.root.get(u"mutable"))
+        def _check_readonly_file(node):
+            self.failUnless(node.is_mutable())
+            self.failUnless(node.is_readonly())
+            self.failUnlessReallyEqual(node.get_write_uri(), None)
+            self.failUnlessReallyEqual(node.get_storage_index(), self.mutable.get_storage_index())
+            return node.download_best_version()
+        d.addCallback(_check_readonly_file)
+        d.addCallback(lambda data: self.failUnlessReallyEqual(data, "mutable read-only link from parent"))
+
+        # ... and then by handle
+        d.addCallback(lambda ign:
+                      self.handler.openFile("mutable2", sftp.FXF_WRITE, {}))
+        def _write_mutable2_setattr(wf):
+            d2 = wf.writeChunk(7, "2")
+
+            d2.addCallback(lambda ign: wf.setAttrs({'permissions': 0444, 'size': 8}))
+
+            # The link isn't made read-only until the file is closed.
+            d2.addCallback(lambda ign: self.root.get(u"mutable2"))
+            d2.addCallback(lambda node: self.failIf(node.is_readonly()))
+
+            d2.addCallback(lambda ign: wf.getAttrs())
+            d2.addCallback(lambda attrs: self.failUnlessReallyEqual(attrs['permissions'], S_IFREG | 0444))
+            d2.addCallback(lambda ign: self.handler.getAttrs("mutable2", followLinks=0))
+            d2.addCallback(lambda attrs: self.failUnlessReallyEqual(attrs['permissions'], S_IFREG | 0666))
+
+            d2.addCallback(lambda ign: wf.close())
+            return d2
+        d.addCallback(_write_mutable2_setattr)
+        d.addCallback(lambda ign: self.root.get(u"mutable2"))
+        d.addCallback(_check_readonly_file)  # from above
+        d.addCallback(lambda data: self.failUnlessReallyEqual(data, "mutable2"))
+
         # test READ | WRITE without CREAT or TRUNC
         d.addCallback(lambda ign:
                       self.handler.openFile("small", sftp.FXF_READ | sftp.FXF_WRITE, {}))
         def _read_write(rwf):
             d2 = rwf.writeChunk(8, "0123")
+            # test immediate read starting after the old end-of-file
+            d2.addCallback(lambda ign: rwf.readChunk(11, 1))
+            d2.addCallback(lambda data: self.failUnlessReallyEqual(data, "3"))
             d2.addCallback(lambda ign: rwf.readChunk(0, 100))
             d2.addCallback(lambda data: self.failUnlessReallyEqual(data, "012345670123"))
             d2.addCallback(lambda ign: rwf.close())
@@ -892,7 +1008,7 @@ class Handler(GridTestMixin, ShouldFailMixin, ReallyEqualMixin, unittest.TestCas
 
             # deliberate race between openFile and renameFile
             d3 = self.handler.renameFile("new", "new2")
-            del d3
+            d3.addErrback(lambda err: self.fail("renameFile failed: %r" % (err,)))
             return d2
         d.addCallback(_open_and_rename_race)
         def _write_rename_race(wf):
@@ -907,6 +1023,26 @@ class Handler(GridTestMixin, ShouldFailMixin, ReallyEqualMixin, unittest.TestCas
                       self.shouldFail(NoSuchChildError, "rename new while open", "new",
                                       self.root.get, u"new"))
 
+        # check that failed downloads cause failed reads and failed close, when open for writing
+        gross = u"gro\u00DF".encode("utf-8")
+        d.addCallback(lambda ign: self.handler.openFile(gross, sftp.FXF_READ | sftp.FXF_WRITE, {}))
+        def _read_write_broken(rwf):
+            d2 = rwf.writeChunk(0, "abcdefghij")
+            d2.addCallback(lambda ign: self.g.nuke_from_orbit())
+
+            # reading should fail (reliably if we read past the written chunk)
+            d2.addCallback(lambda ign:
+                self.shouldFailWithSFTPError(sftp.FX_FAILURE, "read/write broken",
+                                             rwf.readChunk, 0, 100))
+            # close should fail in this case
+            d2.addCallback(lambda ign:
+                self.shouldFailWithSFTPError(sftp.FX_FAILURE, "read/write broken close",
+                                             rwf.close))
+            return d2
+        d.addCallback(_read_write_broken)
+
+        d.addCallback(lambda ign: self.failUnlessEqual(sftpd.all_heisenfiles, {}))
+        d.addCallback(lambda ign: self.failUnlessEqual(self.handler._heisenfiles, {}))
         return d
 
     def test_removeFile(self):
@@ -925,7 +1061,7 @@ class Handler(GridTestMixin, ShouldFailMixin, ReallyEqualMixin, unittest.TestCas
         d.addCallback(lambda ign:
             self.shouldFailWithSFTPError(sftp.FX_NO_SUCH_FILE, "removefile ''",
                                          self.handler.removeFile, ""))
-            
+
         # removing a directory should fail
         d.addCallback(lambda ign:
             self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "removeFile tiny_lit_dir",
@@ -992,6 +1128,8 @@ class Handler(GridTestMixin, ShouldFailMixin, ReallyEqualMixin, unittest.TestCas
                       self.shouldFail(NoSuchChildError, "removeFile tempfile3", "tempfile3",
                                       self.root.get, u"tempfile3"))
 
+        d.addCallback(lambda ign: self.failUnlessEqual(sftpd.all_heisenfiles, {}))
+        d.addCallback(lambda ign: self.failUnlessEqual(self.handler._heisenfiles, {}))
         return d
 
     def test_removeDirectory(self):
@@ -1027,6 +1165,8 @@ class Handler(GridTestMixin, ShouldFailMixin, ReallyEqualMixin, unittest.TestCas
                       self.shouldFail(NoSuchChildError, "removeDirectory unknown", "unknown",
                                       self.root.get, u"unknown"))
 
+        d.addCallback(lambda ign: self.failUnlessEqual(sftpd.all_heisenfiles, {}))
+        d.addCallback(lambda ign: self.failUnlessEqual(self.handler._heisenfiles, {}))
         return d
 
     def test_renameFile(self):
@@ -1072,6 +1212,20 @@ class Handler(GridTestMixin, ShouldFailMixin, ReallyEqualMixin, unittest.TestCas
             self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "renameFile small unknown",
                                          self.handler.renameFile, "small", "unknown"))
 
+        # renaming a file onto a heisenfile should fail, even if the open hasn't completed
+        def _rename_onto_heisenfile_race(wf):
+            slow_open = defer.Deferred()
+            reactor.callLater(1, slow_open.callback, None)
+
+            d2 = self.handler.openFile("heisenfile", sftp.FXF_WRITE | sftp.FXF_CREAT, {}, delay=slow_open)
+
+            # deliberate race between openFile and renameFile
+            d3 = self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "renameFile small heisenfile",
+                                              self.handler.renameFile, "small", "heisenfile")
+            d2.addCallback(lambda wf: wf.close())
+            return deferredutil.gatherResults([d2, d3])
+        d.addCallback(_rename_onto_heisenfile_race)
+
         # renaming a file to a correct path should succeed
         d.addCallback(lambda ign: self.handler.renameFile("small", "new_small"))
         d.addCallback(lambda ign: self.root.get(u"new_small"))
@@ -1093,6 +1247,8 @@ class Handler(GridTestMixin, ShouldFailMixin, ReallyEqualMixin, unittest.TestCas
         d.addCallback(lambda ign: self.root.get(u"new_unknown"))
         d.addCallback(lambda node: self.failUnlessReallyEqual(node.get_uri(), self.unknown_uri))
 
+        d.addCallback(lambda ign: self.failUnlessEqual(sftpd.all_heisenfiles, {}))
+        d.addCallback(lambda ign: self.failUnlessEqual(self.handler._heisenfiles, {}))
         return d
 
     def test_renameFile_posix(self):
@@ -1177,6 +1333,8 @@ class Handler(GridTestMixin, ShouldFailMixin, ReallyEqualMixin, unittest.TestCas
         d.addCallback(lambda ign: self.root.get(u"new_unknown"))
         d.addCallback(lambda node: self.failUnlessReallyEqual(node.get_uri(), self.unknown_uri))
 
+        d.addCallback(lambda ign: self.failUnlessEqual(sftpd.all_heisenfiles, {}))
+        d.addCallback(lambda ign: self.failUnlessEqual(self.handler._heisenfiles, {}))
         return d
 
     def test_makeDirectory(self):
@@ -1228,55 +1386,77 @@ class Handler(GridTestMixin, ShouldFailMixin, ReallyEqualMixin, unittest.TestCas
                                          self.handler.makeDirectory, "newdir2",
                                          {'permissions': 0444}))
 
+        d.addCallback(lambda ign: self.failUnlessEqual(sftpd.all_heisenfiles, {}))
+        d.addCallback(lambda ign: self.failUnlessEqual(self.handler._heisenfiles, {}))
         return d
 
     def test_execCommand_and_openShell(self):
-        class FakeProtocol:
+        class MockProtocol:
             def __init__(self):
                 self.output = ""
+                self.error = ""
                 self.reason = None
+
             def write(self, data):
+                return self.outReceived(data)
+
+            def outReceived(self, data):
                 self.output += data
                 return defer.succeed(None)
+
+            def errReceived(self, data):
+                self.error += data
+                return defer.succeed(None)
+
             def processEnded(self, reason):
                 self.reason = reason
                 return defer.succeed(None)
 
+        def _lines_end_in_crlf(s):
+            return s.replace('\r\n', '').find('\n') == -1 and s.endswith('\r\n')
+
         d = self._set_up("execCommand_and_openShell")
 
         d.addCallback(lambda ign: conch_interfaces.ISession(self.handler))
         def _exec_df(session):
-            protocol = FakeProtocol()
+            protocol = MockProtocol()
             d2 = session.execCommand(protocol, "df -P -k /")
             d2.addCallback(lambda ign: self.failUnlessIn("1024-blocks", protocol.output))
+            d2.addCallback(lambda ign: self.failUnless(_lines_end_in_crlf(protocol.output), protocol.output))
+            d2.addCallback(lambda ign: self.failUnlessEqual(protocol.error, ""))
             d2.addCallback(lambda ign: self.failUnless(isinstance(protocol.reason.value, ProcessDone)))
             d2.addCallback(lambda ign: session.eofReceived())
             d2.addCallback(lambda ign: session.closed())
             return d2
         d.addCallback(_exec_df)
 
+        def _check_unsupported(protocol):
+            d2 = defer.succeed(None)
+            d2.addCallback(lambda ign: self.failUnlessEqual(protocol.output, ""))
+            d2.addCallback(lambda ign: self.failUnlessIn("only the SFTP protocol", protocol.error))
+            d2.addCallback(lambda ign: self.failUnless(_lines_end_in_crlf(protocol.error), protocol.error))
+            d2.addCallback(lambda ign: self.failUnless(isinstance(protocol.reason.value, ProcessTerminated)))
+            d2.addCallback(lambda ign: self.failUnlessEqual(protocol.reason.value.exitCode, 1))
+            return d2
+
         d.addCallback(lambda ign: conch_interfaces.ISession(self.handler))
         def _exec_error(session):
-            protocol = FakeProtocol()
+            protocol = MockProtocol()
             d2 = session.execCommand(protocol, "error")
             d2.addCallback(lambda ign: session.windowChanged(None))
-            d2.addCallback(lambda ign: self.failUnlessEqual("", protocol.output))
-            d2.addCallback(lambda ign: self.failUnless(isinstance(protocol.reason.value, ProcessTerminated)))
-            d2.addCallback(lambda ign: self.failUnlessEqual(protocol.reason.value.exitCode, 1))
+            d2.addCallback(lambda ign: _check_unsupported(protocol))
             d2.addCallback(lambda ign: session.closed())
             return d2
         d.addCallback(_exec_error)
 
         d.addCallback(lambda ign: conch_interfaces.ISession(self.handler))
         def _openShell(session):
-            protocol = FakeProtocol()
+            protocol = MockProtocol()
             d2 = session.openShell(protocol)
-            d2.addCallback(lambda ign: self.failUnlessIn("only SFTP", protocol.output))
-            d2.addCallback(lambda ign: self.failUnless(isinstance(protocol.reason.value, ProcessTerminated)))
-            d2.addCallback(lambda ign: self.failUnlessEqual(protocol.reason.value.exitCode, 1))
+            d2.addCallback(lambda ign: _check_unsupported(protocol))
             d2.addCallback(lambda ign: session.closed())
             return d2
-        d.addCallback(_exec_error)
+        d.addCallback(_openShell)
 
         return d
 
@@ -1293,4 +1473,14 @@ class Handler(GridTestMixin, ShouldFailMixin, ReallyEqualMixin, unittest.TestCas
             self.shouldFailWithSFTPError(sftp.FX_OP_UNSUPPORTED, "extendedRequest foo bar",
                                          self.handler.extendedRequest, "foo", "bar"))
 
+        d.addCallback(lambda ign:
+            self.shouldFailWithSFTPError(sftp.FX_BAD_MESSAGE, "extendedRequest posix-rename@openssh.com invalid 1",
+                                         self.handler.extendedRequest, 'posix-rename@openssh.com', ''))
+        d.addCallback(lambda ign:
+            self.shouldFailWithSFTPError(sftp.FX_BAD_MESSAGE, "extendedRequest posix-rename@openssh.com invalid 2",
+                                         self.handler.extendedRequest, 'posix-rename@openssh.com', '\x00\x00\x00\x01'))
+        d.addCallback(lambda ign:
+            self.shouldFailWithSFTPError(sftp.FX_BAD_MESSAGE, "extendedRequest posix-rename@openssh.com invalid 3",
+                                         self.handler.extendedRequest, 'posix-rename@openssh.com', '\x00\x00\x00\x01_\x00\x00\x00\x01'))
+
         return d