test_filetransfer.py 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784
  1. # -*- test-case-name: twisted.conch.test.test_filetransfer -*-
  2. # Copyright (c) Twisted Matrix Laboratories.
  3. # See LICENSE file for details.
  4. """
  5. Tests for L{twisted.conch.ssh.filetransfer}.
  6. """
  7. from __future__ import division, absolute_import
  8. import os
  9. import re
  10. import struct
  11. from twisted.trial import unittest
  12. try:
  13. from twisted.conch import unix
  14. unix # shut up pyflakes
  15. except ImportError:
  16. unix = None
  17. from twisted.conch import avatar
  18. from twisted.conch.ssh import common, connection, filetransfer, session
  19. from twisted.internet import defer
  20. from twisted.protocols import loopback
  21. from twisted.python import components
  22. from twisted.python.compat import long
  23. from twisted.python.filepath import FilePath
  24. class TestAvatar(avatar.ConchUser):
  25. def __init__(self):
  26. avatar.ConchUser.__init__(self)
  27. self.channelLookup[b'session'] = session.SSHSession
  28. self.subsystemLookup[b'sftp'] = filetransfer.FileTransferServer
  29. def _runAsUser(self, f, *args, **kw):
  30. try:
  31. f = iter(f)
  32. except TypeError:
  33. f = [(f, args, kw)]
  34. for i in f:
  35. func = i[0]
  36. args = len(i)>1 and i[1] or ()
  37. kw = len(i)>2 and i[2] or {}
  38. r = func(*args, **kw)
  39. return r
  40. class FileTransferTestAvatar(TestAvatar):
  41. def __init__(self, homeDir):
  42. TestAvatar.__init__(self)
  43. self.homeDir = homeDir
  44. def getHomeDir(self):
  45. return FilePath(os.getcwd()).preauthChild(self.homeDir.path)
  46. class ConchSessionForTestAvatar:
  47. def __init__(self, avatar):
  48. self.avatar = avatar
  49. if unix:
  50. if not hasattr(unix, 'SFTPServerForUnixConchUser'):
  51. # unix should either be a fully working module, or None. I'm not sure
  52. # how this happens, but on win32 it does. Try to cope. --spiv.
  53. import warnings
  54. warnings.warn(("twisted.conch.unix imported %r, "
  55. "but doesn't define SFTPServerForUnixConchUser'")
  56. % (unix,))
  57. unix = None
  58. else:
  59. class FileTransferForTestAvatar(unix.SFTPServerForUnixConchUser):
  60. def gotVersion(self, version, otherExt):
  61. return {b'conchTest' : b'ext data'}
  62. def extendedRequest(self, extName, extData):
  63. if extName == b'testExtendedRequest':
  64. return b'bar'
  65. raise NotImplementedError
  66. components.registerAdapter(FileTransferForTestAvatar,
  67. TestAvatar,
  68. filetransfer.ISFTPServer)
  69. class SFTPTestBase(unittest.TestCase):
  70. def setUp(self):
  71. self.testDir = FilePath(self.mktemp())
  72. # Give the testDir another level so we can safely "cd .." from it in
  73. # tests.
  74. self.testDir = self.testDir.child('extra')
  75. self.testDir.child('testDirectory').makedirs(True)
  76. with self.testDir.child('testfile1').open(mode='wb') as f:
  77. f.write(b'a'*10+b'b'*10)
  78. with open('/dev/urandom', 'rb') as f2:
  79. f.write(f2.read(1024*64)) # random data
  80. self.testDir.child('testfile1').chmod(0o644)
  81. with self.testDir.child('testRemoveFile').open(mode='wb') as f:
  82. f.write(b'a')
  83. with self.testDir.child('testRenameFile').open(mode='wb') as f:
  84. f.write(b'a')
  85. with self.testDir.child('.testHiddenFile').open(mode='wb') as f:
  86. f.write(b'a')
  87. class OurServerOurClientTests(SFTPTestBase):
  88. if not unix:
  89. skip = "can't run on non-posix computers"
  90. def setUp(self):
  91. SFTPTestBase.setUp(self)
  92. self.avatar = FileTransferTestAvatar(self.testDir)
  93. self.server = filetransfer.FileTransferServer(avatar=self.avatar)
  94. clientTransport = loopback.LoopbackRelay(self.server)
  95. self.client = filetransfer.FileTransferClient()
  96. self._serverVersion = None
  97. self._extData = None
  98. def _(serverVersion, extData):
  99. self._serverVersion = serverVersion
  100. self._extData = extData
  101. self.client.gotServerVersion = _
  102. serverTransport = loopback.LoopbackRelay(self.client)
  103. self.client.makeConnection(clientTransport)
  104. self.server.makeConnection(serverTransport)
  105. self.clientTransport = clientTransport
  106. self.serverTransport = serverTransport
  107. self._emptyBuffers()
  108. def _emptyBuffers(self):
  109. while self.serverTransport.buffer or self.clientTransport.buffer:
  110. self.serverTransport.clearBuffer()
  111. self.clientTransport.clearBuffer()
  112. def tearDown(self):
  113. self.serverTransport.loseConnection()
  114. self.clientTransport.loseConnection()
  115. self.serverTransport.clearBuffer()
  116. self.clientTransport.clearBuffer()
  117. def testServerVersion(self):
  118. self.assertEqual(self._serverVersion, 3)
  119. self.assertEqual(self._extData, {b'conchTest' : b'ext data'})
  120. def test_interface_implementation(self):
  121. """
  122. It implements the ISFTPServer interface.
  123. """
  124. self.assertTrue(
  125. filetransfer.ISFTPServer.providedBy(self.server.client),
  126. "ISFTPServer not provided by %r" % (self.server.client,))
  127. def test_openedFileClosedWithConnection(self):
  128. """
  129. A file opened with C{openFile} is close when the connection is lost.
  130. """
  131. d = self.client.openFile(b"testfile1", filetransfer.FXF_READ |
  132. filetransfer.FXF_WRITE, {})
  133. self._emptyBuffers()
  134. oldClose = os.close
  135. closed = []
  136. def close(fd):
  137. closed.append(fd)
  138. oldClose(fd)
  139. self.patch(os, "close", close)
  140. def _fileOpened(openFile):
  141. fd = self.server.openFiles[openFile.handle[4:]].fd
  142. self.serverTransport.loseConnection()
  143. self.clientTransport.loseConnection()
  144. self.serverTransport.clearBuffer()
  145. self.clientTransport.clearBuffer()
  146. self.assertEqual(self.server.openFiles, {})
  147. self.assertIn(fd, closed)
  148. d.addCallback(_fileOpened)
  149. return d
  150. def test_openedDirectoryClosedWithConnection(self):
  151. """
  152. A directory opened with C{openDirectory} is close when the connection
  153. is lost.
  154. """
  155. d = self.client.openDirectory('')
  156. self._emptyBuffers()
  157. def _getFiles(openDir):
  158. self.serverTransport.loseConnection()
  159. self.clientTransport.loseConnection()
  160. self.serverTransport.clearBuffer()
  161. self.clientTransport.clearBuffer()
  162. self.assertEqual(self.server.openDirs, {})
  163. d.addCallback(_getFiles)
  164. return d
  165. def testOpenFileIO(self):
  166. d = self.client.openFile(b"testfile1", filetransfer.FXF_READ |
  167. filetransfer.FXF_WRITE, {})
  168. self._emptyBuffers()
  169. def _fileOpened(openFile):
  170. self.assertEqual(openFile, filetransfer.ISFTPFile(openFile))
  171. d = _readChunk(openFile)
  172. d.addCallback(_writeChunk, openFile)
  173. return d
  174. def _readChunk(openFile):
  175. d = openFile.readChunk(0, 20)
  176. self._emptyBuffers()
  177. d.addCallback(self.assertEqual, b'a'*10 + b'b'*10)
  178. return d
  179. def _writeChunk(_, openFile):
  180. d = openFile.writeChunk(20, b'c'*10)
  181. self._emptyBuffers()
  182. d.addCallback(_readChunk2, openFile)
  183. return d
  184. def _readChunk2(_, openFile):
  185. d = openFile.readChunk(0, 30)
  186. self._emptyBuffers()
  187. d.addCallback(self.assertEqual, b'a'*10 + b'b'*10 + b'c'*10)
  188. return d
  189. d.addCallback(_fileOpened)
  190. return d
  191. def testClosedFileGetAttrs(self):
  192. d = self.client.openFile(b"testfile1", filetransfer.FXF_READ |
  193. filetransfer.FXF_WRITE, {})
  194. self._emptyBuffers()
  195. def _getAttrs(_, openFile):
  196. d = openFile.getAttrs()
  197. self._emptyBuffers()
  198. return d
  199. def _err(f):
  200. self.flushLoggedErrors()
  201. return f
  202. def _close(openFile):
  203. d = openFile.close()
  204. self._emptyBuffers()
  205. d.addCallback(_getAttrs, openFile)
  206. d.addErrback(_err)
  207. return self.assertFailure(d, filetransfer.SFTPError)
  208. d.addCallback(_close)
  209. return d
  210. def testOpenFileAttributes(self):
  211. d = self.client.openFile(b"testfile1", filetransfer.FXF_READ |
  212. filetransfer.FXF_WRITE, {})
  213. self._emptyBuffers()
  214. def _getAttrs(openFile):
  215. d = openFile.getAttrs()
  216. self._emptyBuffers()
  217. d.addCallback(_getAttrs2)
  218. return d
  219. def _getAttrs2(attrs1):
  220. d = self.client.getAttrs(b'testfile1')
  221. self._emptyBuffers()
  222. d.addCallback(self.assertEqual, attrs1)
  223. return d
  224. return d.addCallback(_getAttrs)
  225. def testOpenFileSetAttrs(self):
  226. # XXX test setAttrs
  227. # Ok, how about this for a start? It caught a bug :) -- spiv.
  228. d = self.client.openFile(b"testfile1", filetransfer.FXF_READ |
  229. filetransfer.FXF_WRITE, {})
  230. self._emptyBuffers()
  231. def _getAttrs(openFile):
  232. d = openFile.getAttrs()
  233. self._emptyBuffers()
  234. d.addCallback(_setAttrs)
  235. return d
  236. def _setAttrs(attrs):
  237. attrs['atime'] = 0
  238. d = self.client.setAttrs(b'testfile1', attrs)
  239. self._emptyBuffers()
  240. d.addCallback(_getAttrs2)
  241. d.addCallback(self.assertEqual, attrs)
  242. return d
  243. def _getAttrs2(_):
  244. d = self.client.getAttrs(b'testfile1')
  245. self._emptyBuffers()
  246. return d
  247. d.addCallback(_getAttrs)
  248. return d
  249. def test_openFileExtendedAttributes(self):
  250. """
  251. Check that L{filetransfer.FileTransferClient.openFile} can send
  252. extended attributes, that should be extracted server side. By default,
  253. they are ignored, so we just verify they are correctly parsed.
  254. """
  255. savedAttributes = {}
  256. oldOpenFile = self.server.client.openFile
  257. def openFile(filename, flags, attrs):
  258. savedAttributes.update(attrs)
  259. return oldOpenFile(filename, flags, attrs)
  260. self.server.client.openFile = openFile
  261. d = self.client.openFile(b"testfile1", filetransfer.FXF_READ |
  262. filetransfer.FXF_WRITE, {"ext_foo": b"bar"})
  263. self._emptyBuffers()
  264. def check(ign):
  265. self.assertEqual(savedAttributes, {"ext_foo": b"bar"})
  266. return d.addCallback(check)
  267. def testRemoveFile(self):
  268. d = self.client.getAttrs(b"testRemoveFile")
  269. self._emptyBuffers()
  270. def _removeFile(ignored):
  271. d = self.client.removeFile(b"testRemoveFile")
  272. self._emptyBuffers()
  273. return d
  274. d.addCallback(_removeFile)
  275. d.addCallback(_removeFile)
  276. return self.assertFailure(d, filetransfer.SFTPError)
  277. def testRenameFile(self):
  278. d = self.client.getAttrs(b"testRenameFile")
  279. self._emptyBuffers()
  280. def _rename(attrs):
  281. d = self.client.renameFile(b"testRenameFile", b"testRenamedFile")
  282. self._emptyBuffers()
  283. d.addCallback(_testRenamed, attrs)
  284. return d
  285. def _testRenamed(_, attrs):
  286. d = self.client.getAttrs(b"testRenamedFile")
  287. self._emptyBuffers()
  288. d.addCallback(self.assertEqual, attrs)
  289. return d.addCallback(_rename)
  290. def testDirectoryBad(self):
  291. d = self.client.getAttrs(b"testMakeDirectory")
  292. self._emptyBuffers()
  293. return self.assertFailure(d, filetransfer.SFTPError)
  294. def testDirectoryCreation(self):
  295. d = self.client.makeDirectory(b"testMakeDirectory", {})
  296. self._emptyBuffers()
  297. def _getAttrs(_):
  298. d = self.client.getAttrs(b"testMakeDirectory")
  299. self._emptyBuffers()
  300. return d
  301. # XXX not until version 4/5
  302. # self.assertEqual(filetransfer.FILEXFER_TYPE_DIRECTORY&attrs['type'],
  303. # filetransfer.FILEXFER_TYPE_DIRECTORY)
  304. def _removeDirectory(_):
  305. d = self.client.removeDirectory(b"testMakeDirectory")
  306. self._emptyBuffers()
  307. return d
  308. d.addCallback(_getAttrs)
  309. d.addCallback(_removeDirectory)
  310. d.addCallback(_getAttrs)
  311. return self.assertFailure(d, filetransfer.SFTPError)
  312. def testOpenDirectory(self):
  313. d = self.client.openDirectory(b'')
  314. self._emptyBuffers()
  315. files = []
  316. def _getFiles(openDir):
  317. def append(f):
  318. files.append(f)
  319. return openDir
  320. d = defer.maybeDeferred(openDir.next)
  321. self._emptyBuffers()
  322. d.addCallback(append)
  323. d.addCallback(_getFiles)
  324. d.addErrback(_close, openDir)
  325. return d
  326. def _checkFiles(ignored):
  327. fs = list(list(zip(*files))[0])
  328. fs.sort()
  329. self.assertEqual(fs,
  330. [b'.testHiddenFile', b'testDirectory',
  331. b'testRemoveFile', b'testRenameFile',
  332. b'testfile1'])
  333. def _close(_, openDir):
  334. d = openDir.close()
  335. self._emptyBuffers()
  336. return d
  337. d.addCallback(_getFiles)
  338. d.addCallback(_checkFiles)
  339. return d
  340. def testLinkDoesntExist(self):
  341. d = self.client.getAttrs(b'testLink')
  342. self._emptyBuffers()
  343. return self.assertFailure(d, filetransfer.SFTPError)
  344. def testLinkSharesAttrs(self):
  345. d = self.client.makeLink(b'testLink', b'testfile1')
  346. self._emptyBuffers()
  347. def _getFirstAttrs(_):
  348. d = self.client.getAttrs(b'testLink', 1)
  349. self._emptyBuffers()
  350. return d
  351. def _getSecondAttrs(firstAttrs):
  352. d = self.client.getAttrs(b'testfile1')
  353. self._emptyBuffers()
  354. d.addCallback(self.assertEqual, firstAttrs)
  355. return d
  356. d.addCallback(_getFirstAttrs)
  357. return d.addCallback(_getSecondAttrs)
  358. def testLinkPath(self):
  359. d = self.client.makeLink(b'testLink', b'testfile1')
  360. self._emptyBuffers()
  361. def _readLink(_):
  362. d = self.client.readLink(b'testLink')
  363. self._emptyBuffers()
  364. testFile = FilePath(os.getcwd()).preauthChild(self.testDir.path)
  365. testFile = testFile.child('testfile1')
  366. d.addCallback(
  367. self.assertEqual,
  368. testFile.path)
  369. return d
  370. def _realPath(_):
  371. d = self.client.realPath(b'testLink')
  372. self._emptyBuffers()
  373. testLink = FilePath(os.getcwd()).preauthChild(self.testDir.path)
  374. testLink = testLink.child('testfile1')
  375. d.addCallback(
  376. self.assertEqual,
  377. testLink.path)
  378. return d
  379. d.addCallback(_readLink)
  380. d.addCallback(_realPath)
  381. return d
  382. def testExtendedRequest(self):
  383. d = self.client.extendedRequest(b'testExtendedRequest', b'foo')
  384. self._emptyBuffers()
  385. d.addCallback(self.assertEqual, b'bar')
  386. d.addCallback(self._cbTestExtendedRequest)
  387. return d
  388. def _cbTestExtendedRequest(self, ignored):
  389. d = self.client.extendedRequest(b'testBadRequest', b'')
  390. self._emptyBuffers()
  391. return self.assertFailure(d, NotImplementedError)
  392. class FakeConn:
  393. def sendClose(self, channel):
  394. pass
  395. class FileTransferCloseTests(unittest.TestCase):
  396. if not unix:
  397. skip = "can't run on non-posix computers"
  398. def setUp(self):
  399. self.avatar = TestAvatar()
  400. def buildServerConnection(self):
  401. # make a server connection
  402. conn = connection.SSHConnection()
  403. # server connections have a 'self.transport.avatar'.
  404. class DummyTransport:
  405. def __init__(self):
  406. self.transport = self
  407. def sendPacket(self, kind, data):
  408. pass
  409. def logPrefix(self):
  410. return 'dummy transport'
  411. conn.transport = DummyTransport()
  412. conn.transport.avatar = self.avatar
  413. return conn
  414. def interceptConnectionLost(self, sftpServer):
  415. self.connectionLostFired = False
  416. origConnectionLost = sftpServer.connectionLost
  417. def connectionLost(reason):
  418. self.connectionLostFired = True
  419. origConnectionLost(reason)
  420. sftpServer.connectionLost = connectionLost
  421. def assertSFTPConnectionLost(self):
  422. self.assertTrue(self.connectionLostFired,
  423. "sftpServer's connectionLost was not called")
  424. def test_sessionClose(self):
  425. """
  426. Closing a session should notify an SFTP subsystem launched by that
  427. session.
  428. """
  429. # make a session
  430. testSession = session.SSHSession(conn=FakeConn(), avatar=self.avatar)
  431. # start an SFTP subsystem on the session
  432. testSession.request_subsystem(common.NS(b'sftp'))
  433. sftpServer = testSession.client.transport.proto
  434. # intercept connectionLost so we can check that it's called
  435. self.interceptConnectionLost(sftpServer)
  436. # close session
  437. testSession.closeReceived()
  438. self.assertSFTPConnectionLost()
  439. def test_clientClosesChannelOnConnnection(self):
  440. """
  441. A client sending CHANNEL_CLOSE should trigger closeReceived on the
  442. associated channel instance.
  443. """
  444. conn = self.buildServerConnection()
  445. # somehow get a session
  446. packet = common.NS(b'session') + struct.pack('>L', 0) * 3
  447. conn.ssh_CHANNEL_OPEN(packet)
  448. sessionChannel = conn.channels[0]
  449. sessionChannel.request_subsystem(common.NS(b'sftp'))
  450. sftpServer = sessionChannel.client.transport.proto
  451. self.interceptConnectionLost(sftpServer)
  452. # intercept closeReceived
  453. self.interceptConnectionLost(sftpServer)
  454. # close the connection
  455. conn.ssh_CHANNEL_CLOSE(struct.pack('>L', 0))
  456. self.assertSFTPConnectionLost()
  457. def test_stopConnectionServiceClosesChannel(self):
  458. """
  459. Closing an SSH connection should close all sessions within it.
  460. """
  461. conn = self.buildServerConnection()
  462. # somehow get a session
  463. packet = common.NS(b'session') + struct.pack('>L', 0) * 3
  464. conn.ssh_CHANNEL_OPEN(packet)
  465. sessionChannel = conn.channels[0]
  466. sessionChannel.request_subsystem(common.NS(b'sftp'))
  467. sftpServer = sessionChannel.client.transport.proto
  468. self.interceptConnectionLost(sftpServer)
  469. # close the connection
  470. conn.serviceStopped()
  471. self.assertSFTPConnectionLost()
  472. class ConstantsTests(unittest.TestCase):
  473. """
  474. Tests for the constants used by the SFTP protocol implementation.
  475. @ivar filexferSpecExcerpts: Excerpts from the
  476. draft-ietf-secsh-filexfer-02.txt (draft) specification of the SFTP
  477. protocol. There are more recent drafts of the specification, but this
  478. one describes version 3, which is what conch (and OpenSSH) implements.
  479. """
  480. filexferSpecExcerpts = [
  481. """
  482. The following values are defined for packet types.
  483. #define SSH_FXP_INIT 1
  484. #define SSH_FXP_VERSION 2
  485. #define SSH_FXP_OPEN 3
  486. #define SSH_FXP_CLOSE 4
  487. #define SSH_FXP_READ 5
  488. #define SSH_FXP_WRITE 6
  489. #define SSH_FXP_LSTAT 7
  490. #define SSH_FXP_FSTAT 8
  491. #define SSH_FXP_SETSTAT 9
  492. #define SSH_FXP_FSETSTAT 10
  493. #define SSH_FXP_OPENDIR 11
  494. #define SSH_FXP_READDIR 12
  495. #define SSH_FXP_REMOVE 13
  496. #define SSH_FXP_MKDIR 14
  497. #define SSH_FXP_RMDIR 15
  498. #define SSH_FXP_REALPATH 16
  499. #define SSH_FXP_STAT 17
  500. #define SSH_FXP_RENAME 18
  501. #define SSH_FXP_READLINK 19
  502. #define SSH_FXP_SYMLINK 20
  503. #define SSH_FXP_STATUS 101
  504. #define SSH_FXP_HANDLE 102
  505. #define SSH_FXP_DATA 103
  506. #define SSH_FXP_NAME 104
  507. #define SSH_FXP_ATTRS 105
  508. #define SSH_FXP_EXTENDED 200
  509. #define SSH_FXP_EXTENDED_REPLY 201
  510. Additional packet types should only be defined if the protocol
  511. version number (see Section ``Protocol Initialization'') is
  512. incremented, and their use MUST be negotiated using the version
  513. number. However, the SSH_FXP_EXTENDED and SSH_FXP_EXTENDED_REPLY
  514. packets can be used to implement vendor-specific extensions. See
  515. Section ``Vendor-Specific-Extensions'' for more details.
  516. """,
  517. """
  518. The flags bits are defined to have the following values:
  519. #define SSH_FILEXFER_ATTR_SIZE 0x00000001
  520. #define SSH_FILEXFER_ATTR_UIDGID 0x00000002
  521. #define SSH_FILEXFER_ATTR_PERMISSIONS 0x00000004
  522. #define SSH_FILEXFER_ATTR_ACMODTIME 0x00000008
  523. #define SSH_FILEXFER_ATTR_EXTENDED 0x80000000
  524. """,
  525. """
  526. The `pflags' field is a bitmask. The following bits have been
  527. defined.
  528. #define SSH_FXF_READ 0x00000001
  529. #define SSH_FXF_WRITE 0x00000002
  530. #define SSH_FXF_APPEND 0x00000004
  531. #define SSH_FXF_CREAT 0x00000008
  532. #define SSH_FXF_TRUNC 0x00000010
  533. #define SSH_FXF_EXCL 0x00000020
  534. """,
  535. """
  536. Currently, the following values are defined (other values may be
  537. defined by future versions of this protocol):
  538. #define SSH_FX_OK 0
  539. #define SSH_FX_EOF 1
  540. #define SSH_FX_NO_SUCH_FILE 2
  541. #define SSH_FX_PERMISSION_DENIED 3
  542. #define SSH_FX_FAILURE 4
  543. #define SSH_FX_BAD_MESSAGE 5
  544. #define SSH_FX_NO_CONNECTION 6
  545. #define SSH_FX_CONNECTION_LOST 7
  546. #define SSH_FX_OP_UNSUPPORTED 8
  547. """]
  548. def test_constantsAgainstSpec(self):
  549. """
  550. The constants used by the SFTP protocol implementation match those
  551. found by searching through the spec.
  552. """
  553. constants = {}
  554. for excerpt in self.filexferSpecExcerpts:
  555. for line in excerpt.splitlines():
  556. m = re.match('^\s*#define SSH_([A-Z_]+)\s+([0-9x]*)\s*$', line)
  557. if m:
  558. constants[m.group(1)] = long(m.group(2), 0)
  559. self.assertTrue(
  560. len(constants) > 0, "No constants found (the test must be buggy).")
  561. for k, v in constants.items():
  562. self.assertEqual(v, getattr(filetransfer, k))
  563. class RawPacketDataTests(unittest.TestCase):
  564. """
  565. Tests for L{filetransfer.FileTransferClient} which explicitly craft certain
  566. less common protocol messages to exercise their handling.
  567. """
  568. def setUp(self):
  569. self.ftc = filetransfer.FileTransferClient()
  570. def test_packetSTATUS(self):
  571. """
  572. A STATUS packet containing a result code, a message, and a language is
  573. parsed to produce the result of an outstanding request L{Deferred}.
  574. @see: U{section 9.1<http://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-9.1>}
  575. of the SFTP Internet-Draft.
  576. """
  577. d = defer.Deferred()
  578. d.addCallback(self._cbTestPacketSTATUS)
  579. self.ftc.openRequests[1] = d
  580. data = struct.pack('!LL', 1, filetransfer.FX_OK) + common.NS(b'msg') + common.NS(b'lang')
  581. self.ftc.packet_STATUS(data)
  582. return d
  583. def _cbTestPacketSTATUS(self, result):
  584. """
  585. Assert that the result is a two-tuple containing the message and
  586. language from the STATUS packet.
  587. """
  588. self.assertEqual(result[0], b'msg')
  589. self.assertEqual(result[1], b'lang')
  590. def test_packetSTATUSShort(self):
  591. """
  592. A STATUS packet containing only a result code can also be parsed to
  593. produce the result of an outstanding request L{Deferred}. Such packets
  594. are sent by some SFTP implementations, though not strictly legal.
  595. @see: U{section 9.1<http://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-9.1>}
  596. of the SFTP Internet-Draft.
  597. """
  598. d = defer.Deferred()
  599. d.addCallback(self._cbTestPacketSTATUSShort)
  600. self.ftc.openRequests[1] = d
  601. data = struct.pack('!LL', 1, filetransfer.FX_OK)
  602. self.ftc.packet_STATUS(data)
  603. return d
  604. def _cbTestPacketSTATUSShort(self, result):
  605. """
  606. Assert that the result is a two-tuple containing empty strings, since
  607. the STATUS packet had neither a message nor a language.
  608. """
  609. self.assertEqual(result[0], b'')
  610. self.assertEqual(result[1], b'')
  611. def test_packetSTATUSWithoutLang(self):
  612. """
  613. A STATUS packet containing a result code and a message but no language
  614. can also be parsed to produce the result of an outstanding request
  615. L{Deferred}. Such packets are sent by some SFTP implementations, though
  616. not strictly legal.
  617. @see: U{section 9.1<http://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-9.1>}
  618. of the SFTP Internet-Draft.
  619. """
  620. d = defer.Deferred()
  621. d.addCallback(self._cbTestPacketSTATUSWithoutLang)
  622. self.ftc.openRequests[1] = d
  623. data = struct.pack('!LL', 1, filetransfer.FX_OK) + common.NS(b'msg')
  624. self.ftc.packet_STATUS(data)
  625. return d
  626. def _cbTestPacketSTATUSWithoutLang(self, result):
  627. """
  628. Assert that the result is a two-tuple containing the message from the
  629. STATUS packet and an empty string, since the language was missing.
  630. """
  631. self.assertEqual(result[0], b'msg')
  632. self.assertEqual(result[1], b'')