test_cftp.py 50 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515
  1. # -*- test-case-name: twisted.conch.test.test_cftp -*-
  2. # Copyright (c) Twisted Matrix Laboratories.
  3. # See LICENSE file for details.
  4. """
  5. Tests for L{twisted.conch.scripts.cftp}.
  6. """
  7. import locale
  8. import time, sys, os, operator, getpass, struct
  9. from io import BytesIO
  10. from twisted.python.filepath import FilePath
  11. from zope.interface import implementer
  12. try:
  13. import pyasn1
  14. except ImportError:
  15. pyasn1 = None
  16. try:
  17. import cryptography
  18. except ImportError:
  19. cryptography = None
  20. _reason = None
  21. if cryptography and pyasn1:
  22. try:
  23. from twisted.conch import unix
  24. from twisted.conch.scripts import cftp
  25. from twisted.conch.scripts.cftp import SSHSession
  26. from twisted.conch.test.test_filetransfer import FileTransferForTestAvatar
  27. except ImportError as e:
  28. unix = None
  29. _reason = str(e)
  30. del e
  31. else:
  32. unix = None
  33. from twisted.conch import ls
  34. from twisted.conch.interfaces import ISFTPFile
  35. from twisted.conch.ssh import filetransfer
  36. from twisted.conch.test import test_ssh, test_conch
  37. from twisted.conch.test.test_filetransfer import SFTPTestBase
  38. from twisted.conch.test.test_filetransfer import FileTransferTestAvatar
  39. from twisted.conch.test.test_conch import FakeStdio
  40. from twisted.cred import portal
  41. from twisted.internet import reactor, protocol, interfaces, defer, error
  42. from twisted.internet.utils import getProcessOutputAndValue, getProcessValue
  43. from twisted.python import log
  44. from twisted.python.compat import _PY3, unicode
  45. from twisted.python.fakepwd import UserDatabase
  46. from twisted.test.proto_helpers import StringTransport
  47. from twisted.internet.task import Clock
  48. from twisted.trial.unittest import TestCase
  49. class SSHSessionTests(TestCase):
  50. """
  51. Tests for L{twisted.conch.scripts.cftp.SSHSession}.
  52. """
  53. def test_eofReceived(self):
  54. """
  55. L{twisted.conch.scripts.cftp.SSHSession.eofReceived} loses the write
  56. half of its stdio connection.
  57. """
  58. stdio = FakeStdio()
  59. channel = SSHSession()
  60. channel.stdio = stdio
  61. channel.eofReceived()
  62. self.assertTrue(stdio.writeConnLost)
  63. class ListingTests(TestCase):
  64. """
  65. Tests for L{lsLine}, the function which generates an entry for a file or
  66. directory in an SFTP I{ls} command's output.
  67. """
  68. if getattr(time, 'tzset', None) is None:
  69. skip = "Cannot test timestamp formatting code without time.tzset"
  70. def setUp(self):
  71. """
  72. Patch the L{ls} module's time function so the results of L{lsLine} are
  73. deterministic.
  74. """
  75. self.now = 123456789
  76. def fakeTime():
  77. return self.now
  78. self.patch(ls, 'time', fakeTime)
  79. # Make sure that the timezone ends up the same after these tests as
  80. # it was before.
  81. if 'TZ' in os.environ:
  82. self.addCleanup(operator.setitem, os.environ, 'TZ', os.environ['TZ'])
  83. self.addCleanup(time.tzset)
  84. else:
  85. def cleanup():
  86. # os.environ.pop is broken! Don't use it! Ever! Or die!
  87. try:
  88. del os.environ['TZ']
  89. except KeyError:
  90. pass
  91. time.tzset()
  92. self.addCleanup(cleanup)
  93. def _lsInTimezone(self, timezone, stat):
  94. """
  95. Call L{ls.lsLine} after setting the timezone to C{timezone} and return
  96. the result.
  97. """
  98. # Set the timezone to a well-known value so the timestamps are
  99. # predictable.
  100. os.environ['TZ'] = timezone
  101. time.tzset()
  102. return ls.lsLine('foo', stat)
  103. def test_oldFile(self):
  104. """
  105. A file with an mtime six months (approximately) or more in the past has
  106. a listing including a low-resolution timestamp.
  107. """
  108. # Go with 7 months. That's more than 6 months.
  109. then = self.now - (60 * 60 * 24 * 31 * 7)
  110. stat = os.stat_result((0, 0, 0, 0, 0, 0, 0, 0, then, 0))
  111. self.assertEqual(
  112. self._lsInTimezone('America/New_York', stat),
  113. '!--------- 0 0 0 0 Apr 26 1973 foo')
  114. self.assertEqual(
  115. self._lsInTimezone('Pacific/Auckland', stat),
  116. '!--------- 0 0 0 0 Apr 27 1973 foo')
  117. def test_oldSingleDigitDayOfMonth(self):
  118. """
  119. A file with a high-resolution timestamp which falls on a day of the
  120. month which can be represented by one decimal digit is formatted with
  121. one padding 0 to preserve the columns which come after it.
  122. """
  123. # A point about 7 months in the past, tweaked to fall on the first of a
  124. # month so we test the case we want to test.
  125. then = self.now - (60 * 60 * 24 * 31 * 7) + (60 * 60 * 24 * 5)
  126. stat = os.stat_result((0, 0, 0, 0, 0, 0, 0, 0, then, 0))
  127. self.assertEqual(
  128. self._lsInTimezone('America/New_York', stat),
  129. '!--------- 0 0 0 0 May 01 1973 foo')
  130. self.assertEqual(
  131. self._lsInTimezone('Pacific/Auckland', stat),
  132. '!--------- 0 0 0 0 May 02 1973 foo')
  133. def test_newFile(self):
  134. """
  135. A file with an mtime fewer than six months (approximately) in the past
  136. has a listing including a high-resolution timestamp excluding the year.
  137. """
  138. # A point about three months in the past.
  139. then = self.now - (60 * 60 * 24 * 31 * 3)
  140. stat = os.stat_result((0, 0, 0, 0, 0, 0, 0, 0, then, 0))
  141. self.assertEqual(
  142. self._lsInTimezone('America/New_York', stat),
  143. '!--------- 0 0 0 0 Aug 28 17:33 foo')
  144. self.assertEqual(
  145. self._lsInTimezone('Pacific/Auckland', stat),
  146. '!--------- 0 0 0 0 Aug 29 09:33 foo')
  147. def test_localeIndependent(self):
  148. """
  149. The month name in the date is locale independent.
  150. """
  151. # A point about three months in the past.
  152. then = self.now - (60 * 60 * 24 * 31 * 3)
  153. stat = os.stat_result((0, 0, 0, 0, 0, 0, 0, 0, then, 0))
  154. # Fake that we're in a language where August is not Aug (e.g.: Spanish)
  155. currentLocale = locale.getlocale()
  156. locale.setlocale(locale.LC_ALL, "es_AR.UTF8")
  157. self.addCleanup(locale.setlocale, locale.LC_ALL, currentLocale)
  158. self.assertEqual(
  159. self._lsInTimezone('America/New_York', stat),
  160. '!--------- 0 0 0 0 Aug 28 17:33 foo')
  161. self.assertEqual(
  162. self._lsInTimezone('Pacific/Auckland', stat),
  163. '!--------- 0 0 0 0 Aug 29 09:33 foo')
  164. # If alternate locale is not available, the previous test will be
  165. # skipped, please install this locale for it to run
  166. currentLocale = locale.getlocale()
  167. try:
  168. try:
  169. locale.setlocale(locale.LC_ALL, "es_AR.UTF8")
  170. except locale.Error:
  171. test_localeIndependent.skip = "The es_AR.UTF8 locale is not installed."
  172. finally:
  173. locale.setlocale(locale.LC_ALL, currentLocale)
  174. def test_newSingleDigitDayOfMonth(self):
  175. """
  176. A file with a high-resolution timestamp which falls on a day of the
  177. month which can be represented by one decimal digit is formatted with
  178. one padding 0 to preserve the columns which come after it.
  179. """
  180. # A point about three months in the past, tweaked to fall on the first
  181. # of a month so we test the case we want to test.
  182. then = self.now - (60 * 60 * 24 * 31 * 3) + (60 * 60 * 24 * 4)
  183. stat = os.stat_result((0, 0, 0, 0, 0, 0, 0, 0, then, 0))
  184. self.assertEqual(
  185. self._lsInTimezone('America/New_York', stat),
  186. '!--------- 0 0 0 0 Sep 01 17:33 foo')
  187. self.assertEqual(
  188. self._lsInTimezone('Pacific/Auckland', stat),
  189. '!--------- 0 0 0 0 Sep 02 09:33 foo')
  190. class InMemorySSHChannel(StringTransport, object):
  191. """
  192. Minimal implementation of a L{SSHChannel} like class which only reads and
  193. writes data from memory.
  194. """
  195. def __init__(self, conn):
  196. """
  197. @param conn: The SSH connection associated with this channel.
  198. @type conn: L{SSHConnection}
  199. """
  200. self.conn = conn
  201. self.localClosed = 0
  202. super(InMemorySSHChannel, self).__init__()
  203. class FilesystemAccessExpectations(object):
  204. """
  205. A test helper used to support expected filesystem access.
  206. """
  207. def __init__(self):
  208. self._cache = {}
  209. def put(self, path, flags, stream):
  210. """
  211. @param path: Path at which the stream is requested.
  212. @type path: L{str}
  213. @param path: Flags with which the stream is requested.
  214. @type path: L{str}
  215. @param stream: A stream.
  216. @type stream: C{File}
  217. """
  218. self._cache[(path, flags)] = stream
  219. def pop(self, path, flags):
  220. """
  221. Remove a stream from the memory.
  222. @param path: Path at which the stream is requested.
  223. @type path: L{str}
  224. @param path: Flags with which the stream is requested.
  225. @type path: L{str}
  226. @return: A stream.
  227. @rtype: C{File}
  228. """
  229. return self._cache.pop((path, flags))
  230. class InMemorySFTPClient(object):
  231. """
  232. A L{filetransfer.FileTransferClient} which does filesystem operations in
  233. memory, without touching the local disc or the network interface.
  234. @ivar _availableFiles: File like objects which are available to the SFTP
  235. client.
  236. @type _availableFiles: L{FilesystemRegister}
  237. """
  238. def __init__(self, availableFiles):
  239. self.transport = InMemorySSHChannel(self)
  240. self.options = {
  241. 'requests': 1,
  242. 'buffersize': 10,
  243. }
  244. self._availableFiles = availableFiles
  245. def openFile(self, filename, flags, attrs):
  246. """
  247. @see: L{filetransfer.FileTransferClient.openFile}.
  248. Retrieve and remove cached file based on flags.
  249. """
  250. return self._availableFiles.pop(filename, flags)
  251. @implementer(ISFTPFile)
  252. class InMemoryRemoteFile(BytesIO):
  253. """
  254. An L{ISFTPFile} which handles all data in memory.
  255. """
  256. def __init__(self, name):
  257. """
  258. @param name: Name of this file.
  259. @type name: L{str}
  260. """
  261. self.name = name
  262. BytesIO.__init__(self)
  263. def writeChunk(self, start, data):
  264. """
  265. @see: L{ISFTPFile.writeChunk}
  266. """
  267. self.seek(start)
  268. self.write(data)
  269. return defer.succeed(self)
  270. def close(self):
  271. """
  272. @see: L{ISFTPFile.writeChunk}
  273. Keeps data after file was closed to help with testing.
  274. """
  275. self._closed = True
  276. def getvalue(self):
  277. """
  278. Get current data of file.
  279. Allow reading data event when file is closed.
  280. """
  281. return BytesIO.getvalue(self)
  282. class StdioClientTests(TestCase):
  283. """
  284. Tests for L{cftp.StdioClient}.
  285. """
  286. def setUp(self):
  287. """
  288. Create a L{cftp.StdioClient} hooked up to dummy transport and a fake
  289. user database.
  290. """
  291. self.fakeFilesystem = FilesystemAccessExpectations()
  292. sftpClient = InMemorySFTPClient(self.fakeFilesystem )
  293. self.client = cftp.StdioClient(sftpClient)
  294. self.client.currentDirectory = '/'
  295. self.database = self.client._pwd = UserDatabase()
  296. # Use a fixed width for all tests so that we get the same results when
  297. # running these tests from different terminals.
  298. # Run tests in a wide console so that all items are delimited by at
  299. # least one space character.
  300. self.setKnownConsoleSize(500, 24)
  301. # Intentionally bypassing makeConnection - that triggers some code
  302. # which uses features not provided by our dumb Connection fake.
  303. self.client.transport = self.client.client.transport
  304. def test_exec(self):
  305. """
  306. The I{exec} command runs its arguments locally in a child process
  307. using the user's shell.
  308. """
  309. self.database.addUser(
  310. getpass.getuser(), 'secret', os.getuid(), 1234, 'foo', 'bar',
  311. sys.executable)
  312. d = self.client._dispatchCommand("exec print(1 + 2)")
  313. d.addCallback(self.assertEqual, b"3\n")
  314. return d
  315. def test_execWithoutShell(self):
  316. """
  317. If the local user has no shell, the I{exec} command runs its arguments
  318. using I{/bin/sh}.
  319. """
  320. self.database.addUser(
  321. getpass.getuser(), 'secret', os.getuid(), 1234, 'foo', 'bar', '')
  322. d = self.client._dispatchCommand("exec echo hello")
  323. d.addCallback(self.assertEqual, b"hello\n")
  324. return d
  325. def test_bang(self):
  326. """
  327. The I{exec} command is run for lines which start with C{"!"}.
  328. """
  329. self.database.addUser(
  330. getpass.getuser(), 'secret', os.getuid(), 1234, 'foo', 'bar',
  331. '/bin/sh')
  332. d = self.client._dispatchCommand("!echo hello")
  333. d.addCallback(self.assertEqual, b"hello\n")
  334. return d
  335. def setKnownConsoleSize(self, width, height):
  336. """
  337. For the duration of this test, patch C{cftp}'s C{fcntl} module to return
  338. a fixed width and height.
  339. @param width: the width in characters
  340. @type width: L{int}
  341. @param height: the height in characters
  342. @type height: L{int}
  343. """
  344. # Local import to avoid win32 issues.
  345. import tty
  346. class FakeFcntl(object):
  347. def ioctl(self, fd, opt, mutate):
  348. if opt != tty.TIOCGWINSZ:
  349. self.fail("Only window-size queries supported.")
  350. return struct.pack("4H", height, width, 0, 0)
  351. self.patch(cftp, "fcntl", FakeFcntl())
  352. def test_printProgressBarReporting(self):
  353. """
  354. L{StdioClient._printProgressBar} prints a progress description,
  355. including percent done, amount transferred, transfer rate, and time
  356. remaining, all based the given start time, the given L{FileWrapper}'s
  357. progress information and the reactor's current time.
  358. """
  359. # Use a short, known console width because this simple test doesn't
  360. # need to test the console padding.
  361. self.setKnownConsoleSize(10, 34)
  362. clock = self.client.reactor = Clock()
  363. wrapped = BytesIO(b"x")
  364. wrapped.name = b"sample"
  365. wrapper = cftp.FileWrapper(wrapped)
  366. wrapper.size = 1024 * 10
  367. startTime = clock.seconds()
  368. clock.advance(2.0)
  369. wrapper.total += 4096
  370. self.client._printProgressBar(wrapper, startTime)
  371. if _PY3:
  372. result = b"\rb'sample' 40% 4.0kB 2.0kBps 00:03 "
  373. else:
  374. result = "\rsample 40% 4.0kB 2.0kBps 00:03 "
  375. self.assertEqual(self.client.transport.value(), result)
  376. def test_printProgressBarNoProgress(self):
  377. """
  378. L{StdioClient._printProgressBar} prints a progress description that
  379. indicates 0 bytes transferred if no bytes have been transferred and no
  380. time has passed.
  381. """
  382. self.setKnownConsoleSize(10, 34)
  383. clock = self.client.reactor = Clock()
  384. wrapped = BytesIO(b"x")
  385. wrapped.name = b"sample"
  386. wrapper = cftp.FileWrapper(wrapped)
  387. startTime = clock.seconds()
  388. self.client._printProgressBar(wrapper, startTime)
  389. if _PY3:
  390. result = b"\rb'sample' 0% 0.0B 0.0Bps 00:00 "
  391. else:
  392. result = "\rsample 0% 0.0B 0.0Bps 00:00 "
  393. self.assertEqual(self.client.transport.value(), result)
  394. def test_printProgressBarEmptyFile(self):
  395. """
  396. Print the progress for empty files.
  397. """
  398. self.setKnownConsoleSize(10, 34)
  399. wrapped = BytesIO()
  400. wrapped.name = b'empty-file'
  401. wrapper = cftp.FileWrapper(wrapped)
  402. self.client._printProgressBar(wrapper, 0)
  403. if _PY3:
  404. result = b"\rb'empty-file'100% 0.0B 0.0Bps 00:00 "
  405. else:
  406. result = "\rempty-file100% 0.0B 0.0Bps 00:00 "
  407. self.assertEqual(result, self.client.transport.value())
  408. def test_getFilenameEmpty(self):
  409. """
  410. Returns empty value for both filename and remaining data.
  411. """
  412. result = self.client._getFilename(' ')
  413. self.assertEqual(('', ''), result)
  414. def test_getFilenameOnlyLocal(self):
  415. """
  416. Returns empty value for remaining data when line contains
  417. only a filename.
  418. """
  419. result = self.client._getFilename('only-local')
  420. self.assertEqual(('only-local', ''), result)
  421. def test_getFilenameNotQuoted(self):
  422. """
  423. Returns filename and remaining data striped of leading and trailing
  424. spaces.
  425. """
  426. result = self.client._getFilename(' local remote file ')
  427. self.assertEqual(('local', 'remote file'), result)
  428. def test_getFilenameQuoted(self):
  429. """
  430. Returns filename and remaining data not striped of leading and trailing
  431. spaces when quoted paths are requested.
  432. """
  433. result = self.client._getFilename(' " local file " " remote file " ')
  434. self.assertEqual((' local file ', '" remote file "'), result)
  435. def makeFile(self, path=None, content=b''):
  436. """
  437. Create a local file and return its path.
  438. When `path` is L{None}, it will create a new temporary file.
  439. @param path: Optional path for the new file.
  440. @type path: L{str}
  441. @param content: Content to be written in the new file.
  442. @type content: L{bytes}
  443. @return: Path to the newly create file.
  444. """
  445. if path is None:
  446. path = self.mktemp()
  447. with open(path, 'wb') as file:
  448. file.write(content)
  449. return path
  450. def checkPutMessage(self, transfers, randomOrder=False):
  451. """
  452. Check output of cftp client for a put request.
  453. @param transfers: List with tuple of (local, remote, progress).
  454. @param randomOrder: When set to C{True}, it will ignore the order
  455. in which put reposes are received
  456. """
  457. output = self.client.transport.value()
  458. if _PY3:
  459. output = output.decode("utf-8")
  460. output = output.split('\n\r')
  461. expectedOutput = []
  462. actualOutput = []
  463. for local, remote, expected in transfers:
  464. # For each transfer we have a list of reported progress which
  465. # ends with the final message informing that file was transferred.
  466. expectedTransfer = []
  467. for line in expected:
  468. expectedTransfer.append('%s %s' % (local, line))
  469. expectedTransfer.append('Transferred %s to %s' % (local, remote))
  470. expectedOutput.append(expectedTransfer)
  471. progressParts = output.pop(0).strip('\r').split('\r')
  472. actual = progressParts[:-1]
  473. last = progressParts[-1].strip('\n').split('\n')
  474. actual.extend(last)
  475. actualTransfer = []
  476. # Each transferred file is on a line with summary on the last
  477. # line. Summary is copying at the end.
  478. for line in actual[:-1]:
  479. # Output line is in the format
  480. # NAME PROGRESS_PERCENTAGE PROGRESS_BYTES SPEED ETA.
  481. # For testing we only care about the
  482. # PROGRESS_PERCENTAGE and PROGRESS values.
  483. # Ignore SPPED and ETA.
  484. line = line.strip().rsplit(' ', 2)[0]
  485. # NAME can be followed by a lot of spaces so we need to
  486. # reduce them to single space.
  487. line = line.strip().split(' ', 1)
  488. actualTransfer.append('%s %s' % (line[0], line[1].strip()))
  489. actualTransfer.append(actual[-1])
  490. actualOutput.append(actualTransfer)
  491. if randomOrder:
  492. self.assertEqual(sorted(expectedOutput), sorted(actualOutput))
  493. else:
  494. self.assertEqual(expectedOutput, actualOutput)
  495. self.assertEqual(
  496. 0, len(output),
  497. 'There are still put responses which were not checked.',
  498. )
  499. def test_cmd_PUTSingleNoRemotePath(self):
  500. """
  501. A name based on local path is used when remote path is not
  502. provided.
  503. The progress is updated while chunks are transferred.
  504. """
  505. content = b'Test\r\nContent'
  506. localPath = self.makeFile(content=content)
  507. flags = (
  508. filetransfer.FXF_WRITE |
  509. filetransfer.FXF_CREAT |
  510. filetransfer.FXF_TRUNC
  511. )
  512. remoteName = os.path.join('/', os.path.basename(localPath))
  513. remoteFile = InMemoryRemoteFile(remoteName)
  514. self.fakeFilesystem.put(remoteName, flags, defer.succeed(remoteFile))
  515. self.client.client.options['buffersize'] = 10
  516. deferred = self.client.cmd_PUT(localPath)
  517. self.successResultOf(deferred)
  518. self.assertEqual(content, remoteFile.getvalue())
  519. self.assertTrue(remoteFile._closed)
  520. self.checkPutMessage(
  521. [(localPath, remoteName,
  522. ['76% 10.0B', '100% 13.0B', '100% 13.0B'])])
  523. def test_cmd_PUTSingleRemotePath(self):
  524. """
  525. Remote path is extracted from first filename after local file.
  526. Any other data in the line is ignored.
  527. """
  528. localPath = self.makeFile()
  529. flags = (
  530. filetransfer.FXF_WRITE |
  531. filetransfer.FXF_CREAT |
  532. filetransfer.FXF_TRUNC
  533. )
  534. remoteName = '/remote-path'
  535. remoteFile = InMemoryRemoteFile(remoteName)
  536. self.fakeFilesystem.put(remoteName, flags, defer.succeed(remoteFile))
  537. deferred = self.client.cmd_PUT(
  538. '%s %s ignored' % (localPath, remoteName))
  539. self.successResultOf(deferred)
  540. self.checkPutMessage([(localPath, remoteName, ['100% 0.0B'])])
  541. self.assertTrue(remoteFile._closed)
  542. self.assertEqual(b'', remoteFile.getvalue())
  543. def test_cmd_PUTMultipleNoRemotePath(self):
  544. """
  545. When a gobbing expression is used local files are transferred with
  546. remote file names based on local names.
  547. """
  548. first = self.makeFile()
  549. firstName = os.path.basename(first)
  550. secondName = 'second-name'
  551. parent = os.path.dirname(first)
  552. second = self.makeFile(path=os.path.join(parent, secondName))
  553. flags = (
  554. filetransfer.FXF_WRITE |
  555. filetransfer.FXF_CREAT |
  556. filetransfer.FXF_TRUNC
  557. )
  558. firstRemotePath = '/%s' % (firstName,)
  559. secondRemotePath = '/%s' % (secondName,)
  560. firstRemoteFile = InMemoryRemoteFile(firstRemotePath)
  561. secondRemoteFile = InMemoryRemoteFile(secondRemotePath)
  562. self.fakeFilesystem.put(
  563. firstRemotePath, flags, defer.succeed(firstRemoteFile))
  564. self.fakeFilesystem.put(
  565. secondRemotePath, flags, defer.succeed(secondRemoteFile))
  566. deferred = self.client.cmd_PUT(os.path.join(parent, '*'))
  567. self.successResultOf(deferred)
  568. self.assertTrue(firstRemoteFile._closed)
  569. self.assertEqual(b'', firstRemoteFile.getvalue())
  570. self.assertTrue(secondRemoteFile._closed)
  571. self.assertEqual(b'', secondRemoteFile.getvalue())
  572. self.checkPutMessage([
  573. (first, firstRemotePath, ['100% 0.0B']),
  574. (second, secondRemotePath, ['100% 0.0B']),
  575. ],
  576. randomOrder=True,
  577. )
  578. def test_cmd_PUTMultipleWithRemotePath(self):
  579. """
  580. When a gobbing expression is used local files are transferred with
  581. remote file names based on local names.
  582. when a remote folder is requested remote paths are composed from
  583. remote path and local filename.
  584. """
  585. first = self.makeFile()
  586. firstName = os.path.basename(first)
  587. secondName = 'second-name'
  588. parent = os.path.dirname(first)
  589. second = self.makeFile(path=os.path.join(parent, secondName))
  590. flags = (
  591. filetransfer.FXF_WRITE |
  592. filetransfer.FXF_CREAT |
  593. filetransfer.FXF_TRUNC
  594. )
  595. firstRemoteFile = InMemoryRemoteFile(firstName)
  596. secondRemoteFile = InMemoryRemoteFile(secondName)
  597. firstRemotePath = '/remote/%s' % (firstName,)
  598. secondRemotePath = '/remote/%s' % (secondName,)
  599. self.fakeFilesystem.put(
  600. firstRemotePath, flags, defer.succeed(firstRemoteFile))
  601. self.fakeFilesystem.put(
  602. secondRemotePath, flags, defer.succeed(secondRemoteFile))
  603. deferred = self.client.cmd_PUT(
  604. '%s remote' % (os.path.join(parent, '*'),))
  605. self.successResultOf(deferred)
  606. self.assertTrue(firstRemoteFile._closed)
  607. self.assertEqual(b'', firstRemoteFile.getvalue())
  608. self.assertTrue(secondRemoteFile._closed)
  609. self.assertEqual(b'', secondRemoteFile.getvalue())
  610. self.checkPutMessage([
  611. (first, firstName, ['100% 0.0B']),
  612. (second, secondName, ['100% 0.0B']),
  613. ],
  614. randomOrder=True,
  615. )
  616. class FileTransferTestRealm:
  617. def __init__(self, testDir):
  618. self.testDir = testDir
  619. def requestAvatar(self, avatarID, mind, *interfaces):
  620. a = FileTransferTestAvatar(self.testDir)
  621. return interfaces[0], a, lambda: None
  622. class SFTPTestProcess(protocol.ProcessProtocol):
  623. """
  624. Protocol for testing cftp. Provides an interface between Python (where all
  625. the tests are) and the cftp client process (which does the work that is
  626. being tested).
  627. """
  628. def __init__(self, onOutReceived):
  629. """
  630. @param onOutReceived: A L{Deferred} to be fired as soon as data is
  631. received from stdout.
  632. """
  633. self.clearBuffer()
  634. self.onOutReceived = onOutReceived
  635. self.onProcessEnd = None
  636. self._expectingCommand = None
  637. self._processEnded = False
  638. def clearBuffer(self):
  639. """
  640. Clear any buffered data received from stdout. Should be private.
  641. """
  642. self.buffer = b''
  643. self._linesReceived = []
  644. self._lineBuffer = b''
  645. def outReceived(self, data):
  646. """
  647. Called by Twisted when the cftp client prints data to stdout.
  648. """
  649. log.msg('got %r' % data)
  650. lines = (self._lineBuffer + data).split(b'\n')
  651. self._lineBuffer = lines.pop(-1)
  652. self._linesReceived.extend(lines)
  653. # XXX - not strictly correct.
  654. # We really want onOutReceived to fire after the first 'cftp>' prompt
  655. # has been received. (See use in OurServerCmdLineClientTests.setUp)
  656. if self.onOutReceived is not None:
  657. d, self.onOutReceived = self.onOutReceived, None
  658. d.callback(data)
  659. self.buffer += data
  660. self._checkForCommand()
  661. def _checkForCommand(self):
  662. prompt = b'cftp> '
  663. if self._expectingCommand and self._lineBuffer == prompt:
  664. buf = b'\n'.join(self._linesReceived)
  665. if buf.startswith(prompt):
  666. buf = buf[len(prompt):]
  667. self.clearBuffer()
  668. d, self._expectingCommand = self._expectingCommand, None
  669. d.callback(buf)
  670. def errReceived(self, data):
  671. """
  672. Called by Twisted when the cftp client prints data to stderr.
  673. """
  674. log.msg('err: %s' % data)
  675. def getBuffer(self):
  676. """
  677. Return the contents of the buffer of data received from stdout.
  678. """
  679. return self.buffer
  680. def runCommand(self, command):
  681. """
  682. Issue the given command via the cftp client. Return a C{Deferred} that
  683. fires when the server returns a result. Note that the C{Deferred} will
  684. callback even if the server returns some kind of error.
  685. @param command: A string containing an sftp command.
  686. @return: A C{Deferred} that fires when the sftp server returns a
  687. result. The payload is the server's response string.
  688. """
  689. self._expectingCommand = defer.Deferred()
  690. self.clearBuffer()
  691. if isinstance(command, unicode):
  692. command = command.encode("utf-8")
  693. self.transport.write(command + b'\n')
  694. return self._expectingCommand
  695. def runScript(self, commands):
  696. """
  697. Run each command in sequence and return a Deferred that fires when all
  698. commands are completed.
  699. @param commands: A list of strings containing sftp commands.
  700. @return: A C{Deferred} that fires when all commands are completed. The
  701. payload is a list of response strings from the server, in the same
  702. order as the commands.
  703. """
  704. sem = defer.DeferredSemaphore(1)
  705. dl = [sem.run(self.runCommand, command) for command in commands]
  706. return defer.gatherResults(dl)
  707. def killProcess(self):
  708. """
  709. Kill the process if it is still running.
  710. If the process is still running, sends a KILL signal to the transport
  711. and returns a C{Deferred} which fires when L{processEnded} is called.
  712. @return: a C{Deferred}.
  713. """
  714. if self._processEnded:
  715. return defer.succeed(None)
  716. self.onProcessEnd = defer.Deferred()
  717. self.transport.signalProcess('KILL')
  718. return self.onProcessEnd
  719. def processEnded(self, reason):
  720. """
  721. Called by Twisted when the cftp client process ends.
  722. """
  723. self._processEnded = True
  724. if self.onProcessEnd:
  725. d, self.onProcessEnd = self.onProcessEnd, None
  726. d.callback(None)
  727. class CFTPClientTestBase(SFTPTestBase):
  728. def setUp(self):
  729. with open('dsa_test.pub', 'wb') as f:
  730. f.write(test_ssh.publicDSA_openssh)
  731. with open('dsa_test', 'wb') as f:
  732. f.write(test_ssh.privateDSA_openssh)
  733. os.chmod('dsa_test', 33152)
  734. with open('kh_test', 'wb') as f:
  735. f.write(b'127.0.0.1 ' + test_ssh.publicRSA_openssh)
  736. return SFTPTestBase.setUp(self)
  737. def startServer(self):
  738. realm = FileTransferTestRealm(self.testDir)
  739. p = portal.Portal(realm)
  740. p.registerChecker(test_ssh.conchTestPublicKeyChecker())
  741. fac = test_ssh.ConchTestServerFactory()
  742. fac.portal = p
  743. self.server = reactor.listenTCP(0, fac, interface="127.0.0.1")
  744. def stopServer(self):
  745. if not hasattr(self.server.factory, 'proto'):
  746. return self._cbStopServer(None)
  747. self.server.factory.proto.expectedLoseConnection = 1
  748. d = defer.maybeDeferred(
  749. self.server.factory.proto.transport.loseConnection)
  750. d.addCallback(self._cbStopServer)
  751. return d
  752. def _cbStopServer(self, ignored):
  753. return defer.maybeDeferred(self.server.stopListening)
  754. def tearDown(self):
  755. for f in ['dsa_test.pub', 'dsa_test', 'kh_test']:
  756. try:
  757. os.remove(f)
  758. except:
  759. pass
  760. return SFTPTestBase.tearDown(self)
  761. class OurServerCmdLineClientTests(CFTPClientTestBase):
  762. """
  763. Functional tests which launch a SFTP server over TCP on localhost and check
  764. cftp command line interface using a spawned process.
  765. Due to the spawned process you can not add a debugger breakpoint for the
  766. client code.
  767. """
  768. def setUp(self):
  769. CFTPClientTestBase.setUp(self)
  770. self.startServer()
  771. cmds = ('-p %i -l testuser '
  772. '--known-hosts kh_test '
  773. '--user-authentications publickey '
  774. '--host-key-algorithms ssh-rsa '
  775. '-i dsa_test '
  776. '-a '
  777. '-v '
  778. '127.0.0.1')
  779. port = self.server.getHost().port
  780. cmds = test_conch._makeArgs((cmds % port).split(), mod='cftp')
  781. log.msg('running %s %s' % (sys.executable, cmds))
  782. d = defer.Deferred()
  783. self.processProtocol = SFTPTestProcess(d)
  784. d.addCallback(lambda _: self.processProtocol.clearBuffer())
  785. env = os.environ.copy()
  786. env['PYTHONPATH'] = os.pathsep.join(sys.path)
  787. encodedCmds = []
  788. encodedEnv = {}
  789. for cmd in cmds:
  790. if isinstance(cmd, unicode):
  791. cmd = cmd.encode("utf-8")
  792. encodedCmds.append(cmd)
  793. for var in env:
  794. val = env[var]
  795. if isinstance(var, unicode):
  796. var = var.encode("utf-8")
  797. if isinstance(val, unicode):
  798. val = val.encode("utf-8")
  799. encodedEnv[var] = val
  800. log.msg(encodedCmds)
  801. log.msg(encodedEnv)
  802. reactor.spawnProcess(self.processProtocol, sys.executable, encodedCmds,
  803. env=encodedEnv)
  804. return d
  805. def tearDown(self):
  806. d = self.stopServer()
  807. d.addCallback(lambda _: self.processProtocol.killProcess())
  808. return d
  809. def _killProcess(self, ignored):
  810. try:
  811. self.processProtocol.transport.signalProcess('KILL')
  812. except error.ProcessExitedAlready:
  813. pass
  814. def runCommand(self, command):
  815. """
  816. Run the given command with the cftp client. Return a C{Deferred} that
  817. fires when the command is complete. Payload is the server's output for
  818. that command.
  819. """
  820. return self.processProtocol.runCommand(command)
  821. def runScript(self, *commands):
  822. """
  823. Run the given commands with the cftp client. Returns a C{Deferred}
  824. that fires when the commands are all complete. The C{Deferred}'s
  825. payload is a list of output for each command.
  826. """
  827. return self.processProtocol.runScript(commands)
  828. def testCdPwd(self):
  829. """
  830. Test that 'pwd' reports the current remote directory, that 'lpwd'
  831. reports the current local directory, and that changing to a
  832. subdirectory then changing to its parent leaves you in the original
  833. remote directory.
  834. """
  835. # XXX - not actually a unit test, see docstring.
  836. homeDir = self.testDir
  837. d = self.runScript('pwd', 'lpwd', 'cd testDirectory', 'cd ..', 'pwd')
  838. def cmdOutput(output):
  839. """
  840. Callback function for handling command output.
  841. """
  842. cmds = []
  843. for cmd in output:
  844. if _PY3 and isinstance(cmd, bytes):
  845. cmd = cmd.decode("utf-8")
  846. cmds.append(cmd)
  847. return cmds[:3] + cmds[4:]
  848. d.addCallback(cmdOutput)
  849. d.addCallback(self.assertEqual,
  850. [homeDir.path, os.getcwd(), '', homeDir.path])
  851. return d
  852. def testChAttrs(self):
  853. """
  854. Check that 'ls -l' output includes the access permissions and that
  855. this output changes appropriately with 'chmod'.
  856. """
  857. def _check(results):
  858. self.flushLoggedErrors()
  859. self.assertTrue(results[0].startswith(b'-rw-r--r--'))
  860. self.assertEqual(results[1], b'')
  861. self.assertTrue(results[2].startswith(b'----------'), results[2])
  862. self.assertEqual(results[3], b'')
  863. d = self.runScript('ls -l testfile1', 'chmod 0 testfile1',
  864. 'ls -l testfile1', 'chmod 644 testfile1')
  865. return d.addCallback(_check)
  866. # XXX test chgrp/own
  867. def testList(self):
  868. """
  869. Check 'ls' works as expected. Checks for wildcards, hidden files,
  870. listing directories and listing empty directories.
  871. """
  872. def _check(results):
  873. self.assertEqual(results[0], [b'testDirectory', b'testRemoveFile',
  874. b'testRenameFile', b'testfile1'])
  875. self.assertEqual(results[1], [b'testDirectory', b'testRemoveFile',
  876. b'testRenameFile', b'testfile1'])
  877. self.assertEqual(results[2], [b'testRemoveFile', b'testRenameFile'])
  878. self.assertEqual(results[3], [b'.testHiddenFile', b'testRemoveFile',
  879. b'testRenameFile'])
  880. self.assertEqual(results[4], [b''])
  881. d = self.runScript('ls', 'ls ../' + self.testDir.basename(),
  882. 'ls *File', 'ls -a *File', 'ls -l testDirectory')
  883. d.addCallback(lambda xs: [x.split(b'\n') for x in xs])
  884. return d.addCallback(_check)
  885. def testHelp(self):
  886. """
  887. Check that running the '?' command returns help.
  888. """
  889. d = self.runCommand('?')
  890. helpText = cftp.StdioClient(None).cmd_HELP('').strip()
  891. if isinstance(helpText, unicode):
  892. helpText = helpText.encode("utf-8")
  893. d.addCallback(self.assertEqual, helpText)
  894. return d
  895. def assertFilesEqual(self, name1, name2, msg=None):
  896. """
  897. Assert that the files at C{name1} and C{name2} contain exactly the
  898. same data.
  899. """
  900. self.assertEqual(name1.getContent(), name2.getContent(), msg)
  901. def testGet(self):
  902. """
  903. Test that 'get' saves the remote file to the correct local location,
  904. that the output of 'get' is correct and that 'rm' actually removes
  905. the file.
  906. """
  907. # XXX - not actually a unit test
  908. expectedOutput = ("Transferred %s/testfile1 to %s/test file2"
  909. % (self.testDir.path, self.testDir.path))
  910. if isinstance(expectedOutput, unicode):
  911. expectedOutput = expectedOutput.encode("utf-8")
  912. def _checkGet(result):
  913. self.assertTrue(result.endswith(expectedOutput))
  914. self.assertFilesEqual(self.testDir.child('testfile1'),
  915. self.testDir.child('test file2'),
  916. "get failed")
  917. return self.runCommand('rm "test file2"')
  918. d = self.runCommand('get testfile1 "%s/test file2"' % (self.testDir.path,))
  919. d.addCallback(_checkGet)
  920. d.addCallback(lambda _: self.assertFalse(
  921. self.testDir.child('test file2').exists()))
  922. return d
  923. def testWildcardGet(self):
  924. """
  925. Test that 'get' works correctly when given wildcard parameters.
  926. """
  927. def _check(ignored):
  928. self.assertFilesEqual(self.testDir.child('testRemoveFile'),
  929. FilePath('testRemoveFile'),
  930. 'testRemoveFile get failed')
  931. self.assertFilesEqual(self.testDir.child('testRenameFile'),
  932. FilePath('testRenameFile'),
  933. 'testRenameFile get failed')
  934. d = self.runCommand('get testR*')
  935. return d.addCallback(_check)
  936. def testPut(self):
  937. """
  938. Check that 'put' uploads files correctly and that they can be
  939. successfully removed. Also check the output of the put command.
  940. """
  941. # XXX - not actually a unit test
  942. expectedOutput = (b'Transferred ' + self.testDir.asBytesMode().path +
  943. b'/testfile1 to ' + self.testDir.asBytesMode().path +
  944. b'/test"file2')
  945. def _checkPut(result):
  946. self.assertFilesEqual(self.testDir.child('testfile1'),
  947. self.testDir.child('test"file2'))
  948. self.assertTrue(result.endswith(expectedOutput))
  949. return self.runCommand('rm "test\\"file2"')
  950. d = self.runCommand('put %s/testfile1 "test\\"file2"'
  951. % (self.testDir.path,))
  952. d.addCallback(_checkPut)
  953. d.addCallback(lambda _: self.assertFalse(
  954. self.testDir.child('test"file2').exists()))
  955. return d
  956. def test_putOverLongerFile(self):
  957. """
  958. Check that 'put' uploads files correctly when overwriting a longer
  959. file.
  960. """
  961. # XXX - not actually a unit test
  962. with self.testDir.child('shorterFile').open(mode='w') as f:
  963. f.write(b"a")
  964. with self.testDir.child('longerFile').open(mode='w') as f:
  965. f.write(b"bb")
  966. def _checkPut(result):
  967. self.assertFilesEqual(self.testDir.child('shorterFile'),
  968. self.testDir.child('longerFile'))
  969. d = self.runCommand('put %s/shorterFile longerFile'
  970. % (self.testDir.path,))
  971. d.addCallback(_checkPut)
  972. return d
  973. def test_putMultipleOverLongerFile(self):
  974. """
  975. Check that 'put' uploads files correctly when overwriting a longer
  976. file and you use a wildcard to specify the files to upload.
  977. """
  978. # XXX - not actually a unit test
  979. someDir = self.testDir.child('dir')
  980. someDir.createDirectory()
  981. with someDir.child('file').open(mode='w') as f:
  982. f.write(b"a")
  983. with self.testDir.child('file').open(mode='w') as f:
  984. f.write(b"bb")
  985. def _checkPut(result):
  986. self.assertFilesEqual(someDir.child('file'),
  987. self.testDir.child('file'))
  988. d = self.runCommand('put %s/dir/*'
  989. % (self.testDir.path,))
  990. d.addCallback(_checkPut)
  991. return d
  992. def testWildcardPut(self):
  993. """
  994. What happens if you issue a 'put' command and include a wildcard (i.e.
  995. '*') in parameter? Check that all files matching the wildcard are
  996. uploaded to the correct directory.
  997. """
  998. def check(results):
  999. self.assertEqual(results[0], b'')
  1000. self.assertEqual(results[2], b'')
  1001. self.assertFilesEqual(self.testDir.child('testRemoveFile'),
  1002. self.testDir.parent().child('testRemoveFile'),
  1003. 'testRemoveFile get failed')
  1004. self.assertFilesEqual(self.testDir.child('testRenameFile'),
  1005. self.testDir.parent().child('testRenameFile'),
  1006. 'testRenameFile get failed')
  1007. d = self.runScript('cd ..',
  1008. 'put %s/testR*' % (self.testDir.path,),
  1009. 'cd %s' % self.testDir.basename())
  1010. d.addCallback(check)
  1011. return d
  1012. def testLink(self):
  1013. """
  1014. Test that 'ln' creates a file which appears as a link in the output of
  1015. 'ls'. Check that removing the new file succeeds without output.
  1016. """
  1017. def _check(results):
  1018. self.flushLoggedErrors()
  1019. self.assertEqual(results[0], b'')
  1020. self.assertTrue(results[1].startswith(b'l'), 'link failed')
  1021. return self.runCommand('rm testLink')
  1022. d = self.runScript('ln testLink testfile1', 'ls -l testLink')
  1023. d.addCallback(_check)
  1024. d.addCallback(self.assertEqual, b'')
  1025. return d
  1026. def testRemoteDirectory(self):
  1027. """
  1028. Test that we can create and remove directories with the cftp client.
  1029. """
  1030. def _check(results):
  1031. self.assertEqual(results[0], b'')
  1032. self.assertTrue(results[1].startswith(b'd'))
  1033. return self.runCommand('rmdir testMakeDirectory')
  1034. d = self.runScript('mkdir testMakeDirectory',
  1035. 'ls -l testMakeDirector?')
  1036. d.addCallback(_check)
  1037. d.addCallback(self.assertEqual, b'')
  1038. return d
  1039. def test_existingRemoteDirectory(self):
  1040. """
  1041. Test that a C{mkdir} on an existing directory fails with the
  1042. appropriate error, and doesn't log an useless error server side.
  1043. """
  1044. def _check(results):
  1045. self.assertEqual(results[0], b'')
  1046. self.assertEqual(results[1],
  1047. b'remote error 11: mkdir failed')
  1048. d = self.runScript('mkdir testMakeDirectory',
  1049. 'mkdir testMakeDirectory')
  1050. d.addCallback(_check)
  1051. return d
  1052. def testLocalDirectory(self):
  1053. """
  1054. Test that we can create a directory locally and remove it with the
  1055. cftp client. This test works because the 'remote' server is running
  1056. out of a local directory.
  1057. """
  1058. d = self.runCommand('lmkdir %s/testLocalDirectory' % (self.testDir.path,))
  1059. d.addCallback(self.assertEqual, b'')
  1060. d.addCallback(lambda _: self.runCommand('rmdir testLocalDirectory'))
  1061. d.addCallback(self.assertEqual, b'')
  1062. return d
  1063. def testRename(self):
  1064. """
  1065. Test that we can rename a file.
  1066. """
  1067. def _check(results):
  1068. self.assertEqual(results[0], b'')
  1069. self.assertEqual(results[1], b'testfile2')
  1070. return self.runCommand('rename testfile2 testfile1')
  1071. d = self.runScript('rename testfile1 testfile2', 'ls testfile?')
  1072. d.addCallback(_check)
  1073. d.addCallback(self.assertEqual, b'')
  1074. return d
  1075. class OurServerBatchFileTests(CFTPClientTestBase):
  1076. """
  1077. Functional tests which launch a SFTP server over localhost and checks csftp
  1078. in batch interface.
  1079. """
  1080. def setUp(self):
  1081. CFTPClientTestBase.setUp(self)
  1082. self.startServer()
  1083. def tearDown(self):
  1084. CFTPClientTestBase.tearDown(self)
  1085. return self.stopServer()
  1086. def _getBatchOutput(self, f):
  1087. fn = self.mktemp()
  1088. with open(fn, 'w') as fp:
  1089. fp.write(f)
  1090. port = self.server.getHost().port
  1091. cmds = ('-p %i -l testuser '
  1092. '--known-hosts kh_test '
  1093. '--user-authentications publickey '
  1094. '--host-key-algorithms ssh-rsa '
  1095. '-i dsa_test '
  1096. '-a '
  1097. '-v -b %s 127.0.0.1') % (port, fn)
  1098. cmds = test_conch._makeArgs(cmds.split(), mod='cftp')[1:]
  1099. log.msg('running %s %s' % (sys.executable, cmds))
  1100. env = os.environ.copy()
  1101. env['PYTHONPATH'] = os.pathsep.join(sys.path)
  1102. self.server.factory.expectedLoseConnection = 1
  1103. d = getProcessOutputAndValue(sys.executable, cmds, env=env)
  1104. def _cleanup(res):
  1105. os.remove(fn)
  1106. return res
  1107. d.addCallback(lambda res: res[0])
  1108. d.addBoth(_cleanup)
  1109. return d
  1110. def testBatchFile(self):
  1111. """
  1112. Test whether batch file function of cftp ('cftp -b batchfile').
  1113. This works by treating the file as a list of commands to be run.
  1114. """
  1115. cmds = """pwd
  1116. ls
  1117. exit
  1118. """
  1119. def _cbCheckResult(res):
  1120. res = res.split(b'\n')
  1121. log.msg('RES %s' % repr(res))
  1122. self.assertIn(self.testDir.asBytesMode().path, res[1])
  1123. self.assertEqual(res[3:-2], [b'testDirectory', b'testRemoveFile',
  1124. b'testRenameFile', b'testfile1'])
  1125. d = self._getBatchOutput(cmds)
  1126. d.addCallback(_cbCheckResult)
  1127. return d
  1128. def testError(self):
  1129. """
  1130. Test that an error in the batch file stops running the batch.
  1131. """
  1132. cmds = """chown 0 missingFile
  1133. pwd
  1134. exit
  1135. """
  1136. def _cbCheckResult(res):
  1137. self.assertNotIn(self.testDir.asBytesMode().path, res)
  1138. d = self._getBatchOutput(cmds)
  1139. d.addCallback(_cbCheckResult)
  1140. return d
  1141. def testIgnoredError(self):
  1142. """
  1143. Test that a minus sign '-' at the front of a line ignores
  1144. any errors.
  1145. """
  1146. cmds = """-chown 0 missingFile
  1147. pwd
  1148. exit
  1149. """
  1150. def _cbCheckResult(res):
  1151. self.assertIn(self.testDir.asBytesMode().path, res)
  1152. d = self._getBatchOutput(cmds)
  1153. d.addCallback(_cbCheckResult)
  1154. return d
  1155. class OurServerSftpClientTests(CFTPClientTestBase):
  1156. """
  1157. Test the sftp server against sftp command line client.
  1158. """
  1159. def setUp(self):
  1160. CFTPClientTestBase.setUp(self)
  1161. return self.startServer()
  1162. def tearDown(self):
  1163. return self.stopServer()
  1164. def test_extendedAttributes(self):
  1165. """
  1166. Test the return of extended attributes by the server: the sftp client
  1167. should ignore them, but still be able to parse the response correctly.
  1168. This test is mainly here to check that
  1169. L{filetransfer.FILEXFER_ATTR_EXTENDED} has the correct value.
  1170. """
  1171. fn = self.mktemp()
  1172. with open(fn, 'w') as f:
  1173. f.write("ls .\nexit")
  1174. port = self.server.getHost().port
  1175. oldGetAttr = FileTransferForTestAvatar._getAttrs
  1176. def _getAttrs(self, s):
  1177. attrs = oldGetAttr(self, s)
  1178. attrs["ext_foo"] = "bar"
  1179. return attrs
  1180. self.patch(FileTransferForTestAvatar, "_getAttrs", _getAttrs)
  1181. self.server.factory.expectedLoseConnection = True
  1182. # PubkeyAcceptedKeyTypes does not exist prior to OpenSSH 7.0 so we
  1183. # first need to check if we can set it. If we can, -V will just print
  1184. # the version without doing anything else; if we can't, we will get a
  1185. # configuration error.
  1186. d = getProcessValue(
  1187. 'ssh', ('-o', 'PubkeyAcceptedKeyTypes=ssh-dss', '-V'))
  1188. def hasPAKT(status):
  1189. if status == 0:
  1190. args = ('-o', 'PubkeyAcceptedKeyTypes=ssh-dss')
  1191. else:
  1192. args = ()
  1193. # Pass -F /dev/null to avoid the user's configuration file from
  1194. # being loaded, as it may contain settings that cause our tests to
  1195. # fail or hang.
  1196. args += ('-F', '/dev/null',
  1197. '-o', 'IdentityFile=dsa_test',
  1198. '-o', 'UserKnownHostsFile=kh_test',
  1199. '-o', 'HostKeyAlgorithms=ssh-rsa',
  1200. '-o', 'Port=%i' % (port,), '-b', fn, 'testuser@127.0.0.1')
  1201. return args
  1202. def check(result):
  1203. self.assertEqual(result[2], 0)
  1204. for i in [b'testDirectory', b'testRemoveFile',
  1205. b'testRenameFile', b'testfile1']:
  1206. self.assertIn(i, result[0])
  1207. d.addCallback(hasPAKT)
  1208. d.addCallback(lambda args: getProcessOutputAndValue('sftp', args))
  1209. return d.addCallback(check)
  1210. if None in (unix, cryptography, pyasn1,
  1211. interfaces.IReactorProcess(reactor, None)):
  1212. if _reason is None:
  1213. _reason = "don't run w/o spawnProcess or cryptography or pyasn1"
  1214. OurServerCmdLineClientTests.skip = _reason
  1215. OurServerBatchFileTests.skip = _reason
  1216. OurServerSftpClientTests.skip = _reason
  1217. StdioClientTests.skip = _reason
  1218. SSHSessionTests.skip = _reason
  1219. else:
  1220. from twisted.python.procutils import which
  1221. if not which('sftp'):
  1222. OurServerSftpClientTests.skip = "no sftp command-line client available"