1 # -*- test-case-name: twisted.conch.test.test_filetransfer -*-
2 # Copyright (c) 2001-2008 Twisted Matrix Laboratories.
3 # See LICENSE file for details.
11 from twisted.trial import unittest
13 from twisted.conch import unix
14 unix # shut up pyflakes
18 del sys.modules['twisted.conch.unix'] # remove the bad import
20 # In Python 2.4, the bad import has already been cleaned up for us.
24 from twisted.conch import avatar
25 from twisted.conch.ssh import common, connection, filetransfer, session
26 from twisted.internet import defer
27 from twisted.protocols import loopback
28 from twisted.python import components
31 class TestAvatar(avatar.ConchUser):
33 avatar.ConchUser.__init__(self)
34 self.channelLookup['session'] = session.SSHSession
35 self.subsystemLookup['sftp'] = filetransfer.FileTransferServer
37 def _runAsUser(self, f, *args, **kw):
44 args = len(i)>1 and i[1] or ()
45 kw = len(i)>2 and i[2] or {}
50 class FileTransferTestAvatar(TestAvatar):
52 def __init__(self, homeDir):
53 TestAvatar.__init__(self)
54 self.homeDir = homeDir
57 return os.path.join(os.getcwd(), self.homeDir)
60 class ConchSessionForTestAvatar:
62 def __init__(self, avatar):
66 if not hasattr(unix, 'SFTPServerForUnixConchUser'):
67 # unix should either be a fully working module, or None. I'm not sure
68 # how this happens, but on win32 it does. Try to cope. --spiv.
70 warnings.warn(("twisted.conch.unix imported %r, "
71 "but doesn't define SFTPServerForUnixConchUser'")
75 class FileTransferForTestAvatar(unix.SFTPServerForUnixConchUser):
77 def gotVersion(self, version, otherExt):
78 return {'conchTest' : 'ext data'}
80 def extendedRequest(self, extName, extData):
81 if extName == 'testExtendedRequest':
83 raise NotImplementedError
85 components.registerAdapter(FileTransferForTestAvatar,
87 filetransfer.ISFTPServer)
89 class SFTPTestBase(unittest.TestCase):
92 self.testDir = self.mktemp()
93 # Give the testDir another level so we can safely "cd .." from it in
95 self.testDir = os.path.join(self.testDir, 'extra')
96 os.makedirs(os.path.join(self.testDir, 'testDirectory'))
98 f = file(os.path.join(self.testDir, 'testfile1'),'w')
99 f.write('a'*10+'b'*10)
100 f.write(file('/dev/urandom').read(1024*64)) # random data
101 os.chmod(os.path.join(self.testDir, 'testfile1'), 0644)
102 file(os.path.join(self.testDir, 'testRemoveFile'), 'w').write('a')
103 file(os.path.join(self.testDir, 'testRenameFile'), 'w').write('a')
104 file(os.path.join(self.testDir, '.testHiddenFile'), 'w').write('a')
107 class TestOurServerOurClient(SFTPTestBase):
110 skip = "can't run on non-posix computers"
113 SFTPTestBase.setUp(self)
115 self.avatar = FileTransferTestAvatar(self.testDir)
116 self.server = filetransfer.FileTransferServer(avatar=self.avatar)
117 clientTransport = loopback.LoopbackRelay(self.server)
119 self.client = filetransfer.FileTransferClient()
120 self._serverVersion = None
122 def _(serverVersion, extData):
123 self._serverVersion = serverVersion
124 self._extData = extData
125 self.client.gotServerVersion = _
126 serverTransport = loopback.LoopbackRelay(self.client)
127 self.client.makeConnection(clientTransport)
128 self.server.makeConnection(serverTransport)
130 self.clientTransport = clientTransport
131 self.serverTransport = serverTransport
136 def _emptyBuffers(self):
137 while self.serverTransport.buffer or self.clientTransport.buffer:
138 self.serverTransport.clearBuffer()
139 self.clientTransport.clearBuffer()
143 self.serverTransport.loseConnection()
144 self.clientTransport.loseConnection()
145 self.serverTransport.clearBuffer()
146 self.clientTransport.clearBuffer()
149 def testServerVersion(self):
150 self.assertEqual(self._serverVersion, 3)
151 self.assertEqual(self._extData, {'conchTest' : 'ext data'})
154 def test_openedFileClosedWithConnection(self):
156 A file opened with C{openFile} is close when the connection is lost.
158 d = self.client.openFile("testfile1", filetransfer.FXF_READ |
159 filetransfer.FXF_WRITE, {})
168 self.patch(os, "close", close)
170 def _fileOpened(openFile):
171 fd = self.server.openFiles[openFile.handle[4:]].fd
172 self.serverTransport.loseConnection()
173 self.clientTransport.loseConnection()
174 self.serverTransport.clearBuffer()
175 self.clientTransport.clearBuffer()
176 self.assertEqual(self.server.openFiles, {})
177 self.assertIn(fd, closed)
179 d.addCallback(_fileOpened)
183 def test_openedDirectoryClosedWithConnection(self):
185 A directory opened with C{openDirectory} is close when the connection
188 d = self.client.openDirectory('')
191 def _getFiles(openDir):
192 self.serverTransport.loseConnection()
193 self.clientTransport.loseConnection()
194 self.serverTransport.clearBuffer()
195 self.clientTransport.clearBuffer()
196 self.assertEqual(self.server.openDirs, {})
198 d.addCallback(_getFiles)
202 def testOpenFileIO(self):
203 d = self.client.openFile("testfile1", filetransfer.FXF_READ |
204 filetransfer.FXF_WRITE, {})
207 def _fileOpened(openFile):
208 self.assertEqual(openFile, filetransfer.ISFTPFile(openFile))
209 d = _readChunk(openFile)
210 d.addCallback(_writeChunk, openFile)
213 def _readChunk(openFile):
214 d = openFile.readChunk(0, 20)
216 d.addCallback(self.assertEqual, 'a'*10 + 'b'*10)
219 def _writeChunk(_, openFile):
220 d = openFile.writeChunk(20, 'c'*10)
222 d.addCallback(_readChunk2, openFile)
225 def _readChunk2(_, openFile):
226 d = openFile.readChunk(0, 30)
228 d.addCallback(self.assertEqual, 'a'*10 + 'b'*10 + 'c'*10)
231 d.addCallback(_fileOpened)
234 def testClosedFileGetAttrs(self):
235 d = self.client.openFile("testfile1", filetransfer.FXF_READ |
236 filetransfer.FXF_WRITE, {})
239 def _getAttrs(_, openFile):
240 d = openFile.getAttrs()
245 self.flushLoggedErrors()
248 def _close(openFile):
251 d.addCallback(_getAttrs, openFile)
253 return self.assertFailure(d, filetransfer.SFTPError)
255 d.addCallback(_close)
258 def testOpenFileAttributes(self):
259 d = self.client.openFile("testfile1", filetransfer.FXF_READ |
260 filetransfer.FXF_WRITE, {})
263 def _getAttrs(openFile):
264 d = openFile.getAttrs()
266 d.addCallback(_getAttrs2)
269 def _getAttrs2(attrs1):
270 d = self.client.getAttrs('testfile1')
272 d.addCallback(self.assertEqual, attrs1)
275 return d.addCallback(_getAttrs)
278 def testOpenFileSetAttrs(self):
280 # Ok, how about this for a start? It caught a bug :) -- spiv.
281 d = self.client.openFile("testfile1", filetransfer.FXF_READ |
282 filetransfer.FXF_WRITE, {})
285 def _getAttrs(openFile):
286 d = openFile.getAttrs()
288 d.addCallback(_setAttrs)
291 def _setAttrs(attrs):
293 d = self.client.setAttrs('testfile1', attrs)
295 d.addCallback(_getAttrs2)
296 d.addCallback(self.assertEqual, attrs)
300 d = self.client.getAttrs('testfile1')
304 d.addCallback(_getAttrs)
308 def test_openFileExtendedAttributes(self):
310 Check that L{filetransfer.FileTransferClient.openFile} can send
311 extended attributes, that should be extracted server side. By default,
312 they are ignored, so we just verify they are correctly parsed.
315 oldOpenFile = self.server.client.openFile
316 def openFile(filename, flags, attrs):
317 savedAttributes.update(attrs)
318 return oldOpenFile(filename, flags, attrs)
319 self.server.client.openFile = openFile
321 d = self.client.openFile("testfile1", filetransfer.FXF_READ |
322 filetransfer.FXF_WRITE, {"ext_foo": "bar"})
326 self.assertEqual(savedAttributes, {"ext_foo": "bar"})
328 return d.addCallback(check)
331 def testRemoveFile(self):
332 d = self.client.getAttrs("testRemoveFile")
334 def _removeFile(ignored):
335 d = self.client.removeFile("testRemoveFile")
338 d.addCallback(_removeFile)
339 d.addCallback(_removeFile)
340 return self.assertFailure(d, filetransfer.SFTPError)
342 def testRenameFile(self):
343 d = self.client.getAttrs("testRenameFile")
346 d = self.client.renameFile("testRenameFile", "testRenamedFile")
348 d.addCallback(_testRenamed, attrs)
350 def _testRenamed(_, attrs):
351 d = self.client.getAttrs("testRenamedFile")
353 d.addCallback(self.assertEqual, attrs)
354 return d.addCallback(_rename)
356 def testDirectoryBad(self):
357 d = self.client.getAttrs("testMakeDirectory")
359 return self.assertFailure(d, filetransfer.SFTPError)
361 def testDirectoryCreation(self):
362 d = self.client.makeDirectory("testMakeDirectory", {})
366 d = self.client.getAttrs("testMakeDirectory")
370 # XXX not until version 4/5
371 # self.assertEqual(filetransfer.FILEXFER_TYPE_DIRECTORY&attrs['type'],
372 # filetransfer.FILEXFER_TYPE_DIRECTORY)
374 def _removeDirectory(_):
375 d = self.client.removeDirectory("testMakeDirectory")
379 d.addCallback(_getAttrs)
380 d.addCallback(_removeDirectory)
381 d.addCallback(_getAttrs)
382 return self.assertFailure(d, filetransfer.SFTPError)
384 def testOpenDirectory(self):
385 d = self.client.openDirectory('')
389 def _getFiles(openDir):
393 d = defer.maybeDeferred(openDir.next)
395 d.addCallback(append)
396 d.addCallback(_getFiles)
397 d.addErrback(_close, openDir)
400 def _checkFiles(ignored):
401 fs = list(zip(*files)[0])
404 ['.testHiddenFile', 'testDirectory',
405 'testRemoveFile', 'testRenameFile',
408 def _close(_, openDir):
413 d.addCallback(_getFiles)
414 d.addCallback(_checkFiles)
417 def testLinkDoesntExist(self):
418 d = self.client.getAttrs('testLink')
420 return self.assertFailure(d, filetransfer.SFTPError)
422 def testLinkSharesAttrs(self):
423 d = self.client.makeLink('testLink', 'testfile1')
425 def _getFirstAttrs(_):
426 d = self.client.getAttrs('testLink', 1)
429 def _getSecondAttrs(firstAttrs):
430 d = self.client.getAttrs('testfile1')
432 d.addCallback(self.assertEqual, firstAttrs)
434 d.addCallback(_getFirstAttrs)
435 return d.addCallback(_getSecondAttrs)
437 def testLinkPath(self):
438 d = self.client.makeLink('testLink', 'testfile1')
441 d = self.client.readLink('testLink')
443 d.addCallback(self.assertEqual,
444 os.path.join(os.getcwd(), self.testDir, 'testfile1'))
447 d = self.client.realPath('testLink')
449 d.addCallback(self.assertEqual,
450 os.path.join(os.getcwd(), self.testDir, 'testfile1'))
452 d.addCallback(_readLink)
453 d.addCallback(_realPath)
456 def testExtendedRequest(self):
457 d = self.client.extendedRequest('testExtendedRequest', 'foo')
459 d.addCallback(self.assertEqual, 'bar')
460 d.addCallback(self._cbTestExtendedRequest)
463 def _cbTestExtendedRequest(self, ignored):
464 d = self.client.extendedRequest('testBadRequest', '')
466 return self.assertFailure(d, NotImplementedError)
470 def sendClose(self, channel):
474 class TestFileTransferClose(unittest.TestCase):
477 skip = "can't run on non-posix computers"
480 self.avatar = TestAvatar()
482 def buildServerConnection(self):
483 # make a server connection
484 conn = connection.SSHConnection()
485 # server connections have a 'self.transport.avatar'.
486 class DummyTransport:
488 self.transport = self
489 def sendPacket(self, kind, data):
492 return 'dummy transport'
493 conn.transport = DummyTransport()
494 conn.transport.avatar = self.avatar
497 def interceptConnectionLost(self, sftpServer):
498 self.connectionLostFired = False
499 origConnectionLost = sftpServer.connectionLost
500 def connectionLost(reason):
501 self.connectionLostFired = True
502 origConnectionLost(reason)
503 sftpServer.connectionLost = connectionLost
505 def assertSFTPConnectionLost(self):
506 self.assertTrue(self.connectionLostFired,
507 "sftpServer's connectionLost was not called")
509 def test_sessionClose(self):
511 Closing a session should notify an SFTP subsystem launched by that
515 testSession = session.SSHSession(conn=FakeConn(), avatar=self.avatar)
517 # start an SFTP subsystem on the session
518 testSession.request_subsystem(common.NS('sftp'))
519 sftpServer = testSession.client.transport.proto
521 # intercept connectionLost so we can check that it's called
522 self.interceptConnectionLost(sftpServer)
525 testSession.closeReceived()
527 self.assertSFTPConnectionLost()
529 def test_clientClosesChannelOnConnnection(self):
531 A client sending CHANNEL_CLOSE should trigger closeReceived on the
532 associated channel instance.
534 conn = self.buildServerConnection()
536 # somehow get a session
537 packet = common.NS('session') + struct.pack('>L', 0) * 3
538 conn.ssh_CHANNEL_OPEN(packet)
539 sessionChannel = conn.channels[0]
541 sessionChannel.request_subsystem(common.NS('sftp'))
542 sftpServer = sessionChannel.client.transport.proto
543 self.interceptConnectionLost(sftpServer)
545 # intercept closeReceived
546 self.interceptConnectionLost(sftpServer)
548 # close the connection
549 conn.ssh_CHANNEL_CLOSE(struct.pack('>L', 0))
551 self.assertSFTPConnectionLost()
554 def test_stopConnectionServiceClosesChannel(self):
556 Closing an SSH connection should close all sessions within it.
558 conn = self.buildServerConnection()
560 # somehow get a session
561 packet = common.NS('session') + struct.pack('>L', 0) * 3
562 conn.ssh_CHANNEL_OPEN(packet)
563 sessionChannel = conn.channels[0]
565 sessionChannel.request_subsystem(common.NS('sftp'))
566 sftpServer = sessionChannel.client.transport.proto
567 self.interceptConnectionLost(sftpServer)
569 # close the connection
570 conn.serviceStopped()
572 self.assertSFTPConnectionLost()
576 class TestConstants(unittest.TestCase):
578 Tests for the constants used by the SFTP protocol implementation.
580 @ivar filexferSpecExcerpts: Excerpts from the
581 draft-ietf-secsh-filexfer-02.txt (draft) specification of the SFTP
582 protocol. There are more recent drafts of the specification, but this
583 one describes version 3, which is what conch (and OpenSSH) implements.
587 filexferSpecExcerpts = [
589 The following values are defined for packet types.
591 #define SSH_FXP_INIT 1
592 #define SSH_FXP_VERSION 2
593 #define SSH_FXP_OPEN 3
594 #define SSH_FXP_CLOSE 4
595 #define SSH_FXP_READ 5
596 #define SSH_FXP_WRITE 6
597 #define SSH_FXP_LSTAT 7
598 #define SSH_FXP_FSTAT 8
599 #define SSH_FXP_SETSTAT 9
600 #define SSH_FXP_FSETSTAT 10
601 #define SSH_FXP_OPENDIR 11
602 #define SSH_FXP_READDIR 12
603 #define SSH_FXP_REMOVE 13
604 #define SSH_FXP_MKDIR 14
605 #define SSH_FXP_RMDIR 15
606 #define SSH_FXP_REALPATH 16
607 #define SSH_FXP_STAT 17
608 #define SSH_FXP_RENAME 18
609 #define SSH_FXP_READLINK 19
610 #define SSH_FXP_SYMLINK 20
611 #define SSH_FXP_STATUS 101
612 #define SSH_FXP_HANDLE 102
613 #define SSH_FXP_DATA 103
614 #define SSH_FXP_NAME 104
615 #define SSH_FXP_ATTRS 105
616 #define SSH_FXP_EXTENDED 200
617 #define SSH_FXP_EXTENDED_REPLY 201
619 Additional packet types should only be defined if the protocol
620 version number (see Section ``Protocol Initialization'') is
621 incremented, and their use MUST be negotiated using the version
622 number. However, the SSH_FXP_EXTENDED and SSH_FXP_EXTENDED_REPLY
623 packets can be used to implement vendor-specific extensions. See
624 Section ``Vendor-Specific-Extensions'' for more details.
627 The flags bits are defined to have the following values:
629 #define SSH_FILEXFER_ATTR_SIZE 0x00000001
630 #define SSH_FILEXFER_ATTR_UIDGID 0x00000002
631 #define SSH_FILEXFER_ATTR_PERMISSIONS 0x00000004
632 #define SSH_FILEXFER_ATTR_ACMODTIME 0x00000008
633 #define SSH_FILEXFER_ATTR_EXTENDED 0x80000000
637 The `pflags' field is a bitmask. The following bits have been
640 #define SSH_FXF_READ 0x00000001
641 #define SSH_FXF_WRITE 0x00000002
642 #define SSH_FXF_APPEND 0x00000004
643 #define SSH_FXF_CREAT 0x00000008
644 #define SSH_FXF_TRUNC 0x00000010
645 #define SSH_FXF_EXCL 0x00000020
648 Currently, the following values are defined (other values may be
649 defined by future versions of this protocol):
653 #define SSH_FX_NO_SUCH_FILE 2
654 #define SSH_FX_PERMISSION_DENIED 3
655 #define SSH_FX_FAILURE 4
656 #define SSH_FX_BAD_MESSAGE 5
657 #define SSH_FX_NO_CONNECTION 6
658 #define SSH_FX_CONNECTION_LOST 7
659 #define SSH_FX_OP_UNSUPPORTED 8
663 def test_constantsAgainstSpec(self):
665 The constants used by the SFTP protocol implementation match those
666 found by searching through the spec.
669 for excerpt in self.filexferSpecExcerpts:
670 for line in excerpt.splitlines():
671 m = re.match('^\s*#define SSH_([A-Z_]+)\s+([0-9x]*)\s*$', line)
673 constants[m.group(1)] = long(m.group(2), 0)
675 len(constants) > 0, "No constants found (the test must be buggy).")
676 for k, v in constants.items():
677 self.assertEqual(v, getattr(filetransfer, k))
681 class TestRawPacketData(unittest.TestCase):
683 Tests for L{filetransfer.FileTransferClient} which explicitly craft certain
684 less common protocol messages to exercise their handling.
687 self.ftc = filetransfer.FileTransferClient()
690 def test_packetSTATUS(self):
692 A STATUS packet containing a result code, a message, and a language is
693 parsed to produce the result of an outstanding request L{Deferred}.
695 @see: U{section 9.1<http://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-9.1>}
696 of the SFTP Internet-Draft.
699 d.addCallback(self._cbTestPacketSTATUS)
700 self.ftc.openRequests[1] = d
701 data = struct.pack('!LL', 1, filetransfer.FX_OK) + common.NS('msg') + common.NS('lang')
702 self.ftc.packet_STATUS(data)
706 def _cbTestPacketSTATUS(self, result):
708 Assert that the result is a two-tuple containing the message and
709 language from the STATUS packet.
711 self.assertEqual(result[0], 'msg')
712 self.assertEqual(result[1], 'lang')
715 def test_packetSTATUSShort(self):
717 A STATUS packet containing only a result code can also be parsed to
718 produce the result of an outstanding request L{Deferred}. Such packets
719 are sent by some SFTP implementations, though not strictly legal.
721 @see: U{section 9.1<http://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-9.1>}
722 of the SFTP Internet-Draft.
725 d.addCallback(self._cbTestPacketSTATUSShort)
726 self.ftc.openRequests[1] = d
727 data = struct.pack('!LL', 1, filetransfer.FX_OK)
728 self.ftc.packet_STATUS(data)
732 def _cbTestPacketSTATUSShort(self, result):
734 Assert that the result is a two-tuple containing empty strings, since
735 the STATUS packet had neither a message nor a language.
737 self.assertEqual(result[0], '')
738 self.assertEqual(result[1], '')
741 def test_packetSTATUSWithoutLang(self):
743 A STATUS packet containing a result code and a message but no language
744 can also be parsed to produce the result of an outstanding request
745 L{Deferred}. Such packets are sent by some SFTP implementations, though
748 @see: U{section 9.1<http://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-9.1>}
749 of the SFTP Internet-Draft.
752 d.addCallback(self._cbTestPacketSTATUSWithoutLang)
753 self.ftc.openRequests[1] = d
754 data = struct.pack('!LL', 1, filetransfer.FX_OK) + common.NS('msg')
755 self.ftc.packet_STATUS(data)
759 def _cbTestPacketSTATUSWithoutLang(self, result):
761 Assert that the result is a two-tuple containing the message from the
762 STATUS packet and an empty string, since the language was missing.
764 self.assertEqual(result[0], 'msg')
765 self.assertEqual(result[1], '')