test_session.py 38 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181
  1. # Copyright (c) Twisted Matrix Laboratories.
  2. # See LICENSE for details.
  3. """
  4. Tests for the 'session' channel implementation in twisted.conch.ssh.session.
  5. See also RFC 4254.
  6. """
  7. from __future__ import division, absolute_import
  8. import os, signal, sys, struct
  9. from zope.interface import implementer
  10. from twisted.internet.address import IPv4Address
  11. from twisted.internet.error import ProcessTerminated, ProcessDone
  12. from twisted.python.failure import Failure
  13. from twisted.conch.ssh import common, session, connection
  14. from twisted.internet import defer, protocol, error
  15. from twisted.python import components, failure
  16. from twisted.trial import unittest
  17. class SubsystemOnlyAvatar(object):
  18. """
  19. A stub class representing an avatar that is only useful for
  20. getting a subsystem.
  21. """
  22. def lookupSubsystem(self, name, data):
  23. """
  24. If the other side requests the 'subsystem' subsystem, allow it by
  25. returning a MockProtocol to implement it. Otherwise raise an assertion.
  26. """
  27. assert name == b'subsystem'
  28. return MockProtocol()
  29. class StubAvatar:
  30. """
  31. A stub class representing the avatar representing the authenticated user.
  32. It implements the I{ISession} interface.
  33. """
  34. def lookupSubsystem(self, name, data):
  35. """
  36. If the user requests the TestSubsystem subsystem, connect them to a
  37. MockProtocol. If they request neither, then None is returned which is
  38. interpreted by SSHSession as a failure.
  39. """
  40. if name == b'TestSubsystem':
  41. self.subsystem = MockProtocol()
  42. self.subsystem.packetData = data
  43. return self.subsystem
  44. @implementer(session.ISession)
  45. class StubSessionForStubAvatar(object):
  46. """
  47. A stub ISession implementation for our StubAvatar. The instance
  48. variables generally keep track of method invocations so that we can test
  49. that the methods were called.
  50. @ivar avatar: the L{StubAvatar} we are adapting.
  51. @ivar ptyRequest: if present, the terminal, window size, and modes passed
  52. to the getPty method.
  53. @ivar windowChange: if present, the window size passed to the
  54. windowChangned method.
  55. @ivar shellProtocol: if present, the L{SSHSessionProcessProtocol} passed
  56. to the openShell method.
  57. @ivar shellTransport: if present, the L{EchoTransport} connected to
  58. shellProtocol.
  59. @ivar execProtocol: if present, the L{SSHSessionProcessProtocol} passed
  60. to the execCommand method.
  61. @ivar execTransport: if present, the L{EchoTransport} connected to
  62. execProtocol.
  63. @ivar execCommandLine: if present, the command line passed to the
  64. execCommand method.
  65. @ivar gotEOF: if present, an EOF message was received.
  66. @ivar gotClosed: if present, a closed message was received.
  67. """
  68. def __init__(self, avatar):
  69. """
  70. Store the avatar we're adapting.
  71. """
  72. self.avatar = avatar
  73. self.shellProtocol = None
  74. def getPty(self, terminal, window, modes):
  75. """
  76. If the terminal is 'bad', fail. Otherwise, store the information in
  77. the ptyRequest variable.
  78. """
  79. if terminal != b'bad':
  80. self.ptyRequest = (terminal, window, modes)
  81. else:
  82. raise RuntimeError('not getting a pty')
  83. def windowChanged(self, window):
  84. """
  85. If all the window sizes are 0, fail. Otherwise, store the size in the
  86. windowChange variable.
  87. """
  88. if window == (0, 0, 0, 0):
  89. raise RuntimeError('not changing the window size')
  90. else:
  91. self.windowChange = window
  92. def openShell(self, pp):
  93. """
  94. If we have gotten a shell request before, fail. Otherwise, store the
  95. process protocol in the shellProtocol variable, connect it to the
  96. EchoTransport and store that as shellTransport.
  97. """
  98. if self.shellProtocol is not None:
  99. raise RuntimeError('not getting a shell this time')
  100. else:
  101. self.shellProtocol = pp
  102. self.shellTransport = EchoTransport(pp)
  103. def execCommand(self, pp, command):
  104. """
  105. If the command is 'true', store the command, the process protocol, and
  106. the transport we connect to the process protocol. Otherwise, just
  107. store the command and raise an error.
  108. """
  109. self.execCommandLine = command
  110. if command == b'success':
  111. self.execProtocol = pp
  112. elif command[:6] == b'repeat':
  113. self.execProtocol = pp
  114. self.execTransport = EchoTransport(pp)
  115. pp.outReceived(command[7:])
  116. else:
  117. raise RuntimeError('not getting a command')
  118. def eofReceived(self):
  119. """
  120. Note that EOF has been received.
  121. """
  122. self.gotEOF = True
  123. def closed(self):
  124. """
  125. Note that close has been received.
  126. """
  127. self.gotClosed = True
  128. components.registerAdapter(StubSessionForStubAvatar, StubAvatar,
  129. session.ISession)
  130. class EchoTransport:
  131. """
  132. A transport for a ProcessProtocol which echos data that is sent to it with
  133. a Window newline (CR LF) appended to it. If a null byte is in the data,
  134. disconnect. When we are asked to disconnect, disconnect the
  135. C{ProcessProtocol} with a 0 exit code.
  136. @ivar proto: the C{ProcessProtocol} connected to us.
  137. @ivar data: a L{bytes} of data written to us.
  138. """
  139. def __init__(self, processProtocol):
  140. """
  141. Initialize our instance variables.
  142. @param processProtocol: a C{ProcessProtocol} to connect to ourself.
  143. """
  144. self.proto = processProtocol
  145. self.closed = False
  146. self.data = b''
  147. processProtocol.makeConnection(self)
  148. def write(self, data):
  149. """
  150. We got some data. Give it back to our C{ProcessProtocol} with
  151. a newline attached. Disconnect if there's a null byte.
  152. """
  153. self.data += data
  154. self.proto.outReceived(data)
  155. self.proto.outReceived(b'\r\n')
  156. if b'\x00' in data: # mimic 'exit' for the shell test
  157. self.loseConnection()
  158. def loseConnection(self):
  159. """
  160. If we're asked to disconnect (and we haven't already) shut down
  161. the C{ProcessProtocol} with a 0 exit code.
  162. """
  163. if self.closed:
  164. return
  165. self.closed = 1
  166. self.proto.inConnectionLost()
  167. self.proto.outConnectionLost()
  168. self.proto.errConnectionLost()
  169. self.proto.processEnded(failure.Failure(
  170. error.ProcessTerminated(0, None, None)))
  171. class MockProtocol(protocol.Protocol):
  172. """
  173. A sample Protocol which stores the data passed to it.
  174. @ivar packetData: a L{bytes} of data to be sent when the connection is
  175. made.
  176. @ivar data: a L{bytes} of the data passed to us.
  177. @ivar open: True if the channel is open.
  178. @ivar reason: if not None, the reason the protocol was closed.
  179. """
  180. packetData = b''
  181. def connectionMade(self):
  182. """
  183. Set up the instance variables. If we have any packetData, send it
  184. along.
  185. """
  186. self.data = b''
  187. self.open = True
  188. self.reason = None
  189. if self.packetData:
  190. self.dataReceived(self.packetData)
  191. def dataReceived(self, data):
  192. """
  193. Store the received data and write it back with a tilde appended.
  194. The tilde is appended so that the tests can verify that we processed
  195. the data.
  196. """
  197. self.data += data
  198. self.transport.write(data + b'~')
  199. def connectionLost(self, reason):
  200. """
  201. Close the protocol and store the reason.
  202. """
  203. self.open = False
  204. self.reason = reason
  205. class StubConnection(object):
  206. """
  207. A stub for twisted.conch.ssh.connection.SSHConnection. Record the data
  208. that channels send, and when they try to close the connection.
  209. @ivar data: a L{dict} mapping C{SSHChannel}s to a C{list} of L{bytes} of
  210. data they sent.
  211. @ivar extData: a L{dict} mapping L{SSHChannel}s to a C{list} of L{tuple} of
  212. (L{int}, L{bytes}) of extended data they sent.
  213. @ivar requests: a L{dict} mapping L{SSHChannel}s to a C{list} of L{tuple}
  214. of (L{str}, L{bytes}) of channel requests they made.
  215. @ivar eofs: a L{dict} mapping L{SSHChannel}s to C{true} if they have sent
  216. an EOF.
  217. @ivar closes: a L{dict} mapping L{SSHChannel}s to C{true} if they have sent
  218. a close.
  219. """
  220. def __init__(self, transport=None):
  221. """
  222. Initialize our instance variables.
  223. """
  224. self.data = {}
  225. self.extData = {}
  226. self.requests = {}
  227. self.eofs = {}
  228. self.closes = {}
  229. self.transport = transport
  230. def logPrefix(self):
  231. """
  232. Return our logging prefix.
  233. """
  234. return "MockConnection"
  235. def sendData(self, channel, data):
  236. """
  237. Record the sent data.
  238. """
  239. self.data.setdefault(channel, []).append(data)
  240. def sendExtendedData(self, channel, type, data):
  241. """
  242. Record the sent extended data.
  243. """
  244. self.extData.setdefault(channel, []).append((type, data))
  245. def sendRequest(self, channel, request, data, wantReply=False):
  246. """
  247. Record the sent channel request.
  248. """
  249. self.requests.setdefault(channel, []).append((request, data,
  250. wantReply))
  251. if wantReply:
  252. return defer.succeed(None)
  253. def sendEOF(self, channel):
  254. """
  255. Record the sent EOF.
  256. """
  257. self.eofs[channel] = True
  258. def sendClose(self, channel):
  259. """
  260. Record the sent close.
  261. """
  262. self.closes[channel] = True
  263. class StubTransport:
  264. """
  265. A stub transport which records the data written.
  266. @ivar buf: the data sent to the transport.
  267. @type buf: L{bytes}
  268. @ivar close: flags indicating if the transport has been closed.
  269. @type close: L{bool}
  270. """
  271. buf = b''
  272. close = False
  273. def getPeer(self):
  274. """
  275. Return an arbitrary L{IAddress}.
  276. """
  277. return IPv4Address('TCP', 'remotehost', 8888)
  278. def getHost(self):
  279. """
  280. Return an arbitrary L{IAddress}.
  281. """
  282. return IPv4Address('TCP', 'localhost', 9999)
  283. def write(self, data):
  284. """
  285. Record data in the buffer.
  286. """
  287. self.buf += data
  288. def loseConnection(self):
  289. """
  290. Note that the connection was closed.
  291. """
  292. self.close = True
  293. def setTcpNoDelay(self, enabled):
  294. """
  295. Pretend to set C{TCP_NODELAY}.
  296. """
  297. # Required for testing SSHSessionForUnixConchUser.
  298. class StubTransportWithWriteErr(StubTransport):
  299. """
  300. A version of StubTransport which records the error data sent to it.
  301. @ivar err: the extended data sent to the transport.
  302. @type err: L{bytes}
  303. """
  304. err = b''
  305. def writeErr(self, data):
  306. """
  307. Record the extended data in the buffer. This was an old interface
  308. that allowed the Transports from ISession.openShell() or
  309. ISession.execCommand() to receive extended data from the client.
  310. """
  311. self.err += data
  312. class StubClient(object):
  313. """
  314. A stub class representing the client to a SSHSession.
  315. @ivar transport: A L{StubTransport} object which keeps track of the data
  316. passed to it.
  317. """
  318. def __init__(self):
  319. self.transport = StubTransportWithWriteErr()
  320. class SessionInterfaceTests(unittest.TestCase):
  321. """
  322. Tests for the SSHSession class interface. This interface is not ideal, but
  323. it is tested in order to maintain backwards compatibility.
  324. """
  325. def setUp(self):
  326. """
  327. Make an SSHSession object to test. Give the channel some window
  328. so that it's allowed to send packets. 500 and 100 are arbitrary
  329. values.
  330. """
  331. self.session = session.SSHSession(remoteWindow=500,
  332. remoteMaxPacket=100, conn=StubConnection(),
  333. avatar=StubAvatar())
  334. def assertSessionIsStubSession(self):
  335. """
  336. Asserts that self.session.session is an instance of
  337. StubSessionForStubOldAvatar.
  338. """
  339. self.assertIsInstance(self.session.session,
  340. StubSessionForStubAvatar)
  341. def test_init(self):
  342. """
  343. SSHSession initializes its buffer (buf), client, and ISession adapter.
  344. The avatar should not need to be adaptable to an ISession immediately.
  345. """
  346. s = session.SSHSession(avatar=object) # use object because it doesn't
  347. # have an adapter
  348. self.assertEqual(s.buf, b'')
  349. self.assertIsNone(s.client)
  350. self.assertIsNone(s.session)
  351. def test_client_dataReceived(self):
  352. """
  353. SSHSession.dataReceived() passes data along to a client. If the data
  354. comes before there is a client, the data should be discarded.
  355. """
  356. self.session.dataReceived(b'1')
  357. self.session.client = StubClient()
  358. self.session.dataReceived(b'2')
  359. self.assertEqual(self.session.client.transport.buf, b'2')
  360. def test_client_extReceived(self):
  361. """
  362. SSHSession.extReceived() passed data of type EXTENDED_DATA_STDERR along
  363. to the client. If the data comes before there is a client, or if the
  364. data is not of type EXTENDED_DATA_STDERR, it is discared.
  365. """
  366. self.session.extReceived(connection.EXTENDED_DATA_STDERR, b'1')
  367. self.session.extReceived(255, b'2') # 255 is arbitrary
  368. self.session.client = StubClient()
  369. self.session.extReceived(connection.EXTENDED_DATA_STDERR, b'3')
  370. self.assertEqual(self.session.client.transport.err, b'3')
  371. def test_client_extReceivedWithoutWriteErr(self):
  372. """
  373. SSHSession.extReceived() should handle the case where the transport
  374. on the client doesn't have a writeErr method.
  375. """
  376. client = self.session.client = StubClient()
  377. client.transport = StubTransport() # doesn't have writeErr
  378. # should not raise an error
  379. self.session.extReceived(connection.EXTENDED_DATA_STDERR, b'ignored')
  380. def test_client_closed(self):
  381. """
  382. SSHSession.closed() should tell the transport connected to the client
  383. that the connection was lost.
  384. """
  385. self.session.client = StubClient()
  386. self.session.closed()
  387. self.assertTrue(self.session.client.transport.close)
  388. self.session.client.transport.close = False
  389. def test_badSubsystemDoesNotCreateClient(self):
  390. """
  391. When a subsystem request fails, SSHSession.client should not be set.
  392. """
  393. ret = self.session.requestReceived(
  394. b'subsystem', common.NS(b'BadSubsystem'))
  395. self.assertFalse(ret)
  396. self.assertIsNone(self.session.client)
  397. def test_lookupSubsystem(self):
  398. """
  399. When a client requests a subsystem, the SSHSession object should get
  400. the subsystem by calling avatar.lookupSubsystem, and attach it as
  401. the client.
  402. """
  403. ret = self.session.requestReceived(
  404. b'subsystem', common.NS(b'TestSubsystem') + b'data')
  405. self.assertTrue(ret)
  406. self.assertIsInstance(self.session.client, protocol.ProcessProtocol)
  407. self.assertIs(self.session.client.transport.proto,
  408. self.session.avatar.subsystem)
  409. def test_lookupSubsystemDoesNotNeedISession(self):
  410. """
  411. Previously, if one only wanted to implement a subsystem, an ISession
  412. adapter wasn't needed because subsystems were looked up using the
  413. lookupSubsystem method on the avatar.
  414. """
  415. s = session.SSHSession(avatar=SubsystemOnlyAvatar(),
  416. conn=StubConnection())
  417. ret = s.request_subsystem(
  418. common.NS(b'subsystem') + b'data')
  419. self.assertTrue(ret)
  420. self.assertIsNotNone(s.client)
  421. self.assertIsNone(s.conn.closes.get(s))
  422. s.eofReceived()
  423. self.assertTrue(s.conn.closes.get(s))
  424. # these should not raise errors
  425. s.loseConnection()
  426. s.closed()
  427. def test_lookupSubsystem_data(self):
  428. """
  429. After having looked up a subsystem, data should be passed along to the
  430. client. Additionally, subsystems were passed the entire request packet
  431. as data, instead of just the additional data.
  432. We check for the additional tidle to verify that the data passed
  433. through the client.
  434. """
  435. #self.session.dataReceived('1')
  436. # subsystems didn't get extended data
  437. #self.session.extReceived(connection.EXTENDED_DATA_STDERR, '2')
  438. self.session.requestReceived(b'subsystem',
  439. common.NS(b'TestSubsystem') + b'data')
  440. self.assertEqual(self.session.conn.data[self.session],
  441. [b'\x00\x00\x00\x0dTestSubsystemdata~'])
  442. self.session.dataReceived(b'more data')
  443. self.assertEqual(self.session.conn.data[self.session][-1],
  444. b'more data~')
  445. def test_lookupSubsystem_closeReceived(self):
  446. """
  447. SSHSession.closeReceived() should sent a close message to the remote
  448. side.
  449. """
  450. self.session.requestReceived(b'subsystem',
  451. common.NS(b'TestSubsystem') + b'data')
  452. self.session.closeReceived()
  453. self.assertTrue(self.session.conn.closes[self.session])
  454. def assertRequestRaisedRuntimeError(self):
  455. """
  456. Assert that the request we just made raised a RuntimeError (and only a
  457. RuntimeError).
  458. """
  459. errors = self.flushLoggedErrors(RuntimeError)
  460. self.assertEqual(len(errors), 1, "Multiple RuntimeErrors raised: %s" %
  461. '\n'.join([repr(error) for error in errors]))
  462. errors[0].trap(RuntimeError)
  463. def test_requestShell(self):
  464. """
  465. When a client requests a shell, the SSHSession object should get
  466. the shell by getting an ISession adapter for the avatar, then
  467. calling openShell() with a ProcessProtocol to attach.
  468. """
  469. # gets a shell the first time
  470. ret = self.session.requestReceived(b'shell', b'')
  471. self.assertTrue(ret)
  472. self.assertSessionIsStubSession()
  473. self.assertIsInstance(self.session.client,
  474. session.SSHSessionProcessProtocol)
  475. self.assertIs(self.session.session.shellProtocol, self.session.client)
  476. # doesn't get a shell the second time
  477. self.assertFalse(self.session.requestReceived(b'shell', b''))
  478. self.assertRequestRaisedRuntimeError()
  479. def test_requestShellWithData(self):
  480. """
  481. When a client executes a shell, it should be able to give pass data
  482. back and forth between the local and the remote side.
  483. """
  484. ret = self.session.requestReceived(b'shell', b'')
  485. self.assertTrue(ret)
  486. self.assertSessionIsStubSession()
  487. self.session.dataReceived(b'some data\x00')
  488. self.assertEqual(self.session.session.shellTransport.data,
  489. b'some data\x00')
  490. self.assertEqual(self.session.conn.data[self.session],
  491. [b'some data\x00', b'\r\n'])
  492. self.assertTrue(self.session.session.shellTransport.closed)
  493. self.assertEqual(self.session.conn.requests[self.session],
  494. [(b'exit-status', b'\x00\x00\x00\x00', False)])
  495. def test_requestExec(self):
  496. """
  497. When a client requests a command, the SSHSession object should get
  498. the command by getting an ISession adapter for the avatar, then
  499. calling execCommand with a ProcessProtocol to attach and the
  500. command line.
  501. """
  502. ret = self.session.requestReceived(b'exec',
  503. common.NS(b'failure'))
  504. self.assertFalse(ret)
  505. self.assertRequestRaisedRuntimeError()
  506. self.assertIsNone(self.session.client)
  507. self.assertTrue(self.session.requestReceived(b'exec',
  508. common.NS(b'success')))
  509. self.assertSessionIsStubSession()
  510. self.assertIsInstance(self.session.client,
  511. session.SSHSessionProcessProtocol)
  512. self.assertIs(self.session.session.execProtocol, self.session.client)
  513. self.assertEqual(self.session.session.execCommandLine,
  514. b'success')
  515. def test_requestExecWithData(self):
  516. """
  517. When a client executes a command, it should be able to give pass data
  518. back and forth.
  519. """
  520. ret = self.session.requestReceived(b'exec',
  521. common.NS(b'repeat hello'))
  522. self.assertTrue(ret)
  523. self.assertSessionIsStubSession()
  524. self.session.dataReceived(b'some data')
  525. self.assertEqual(self.session.session.execTransport.data, b'some data')
  526. self.assertEqual(self.session.conn.data[self.session],
  527. [b'hello', b'some data', b'\r\n'])
  528. self.session.eofReceived()
  529. self.session.closeReceived()
  530. self.session.closed()
  531. self.assertTrue(self.session.session.execTransport.closed)
  532. self.assertEqual(self.session.conn.requests[self.session],
  533. [(b'exit-status', b'\x00\x00\x00\x00', False)])
  534. def test_requestPty(self):
  535. """
  536. When a client requests a PTY, the SSHSession object should make
  537. the request by getting an ISession adapter for the avatar, then
  538. calling getPty with the terminal type, the window size, and any modes
  539. the client gave us.
  540. """
  541. # 'bad' terminal type fails
  542. ret = self.session.requestReceived(
  543. b'pty_req', session.packRequest_pty_req(
  544. b'bad', (1, 2, 3, 4), b''))
  545. self.assertFalse(ret)
  546. self.assertSessionIsStubSession()
  547. self.assertRequestRaisedRuntimeError()
  548. # 'good' terminal type succeeds
  549. self.assertTrue(self.session.requestReceived(b'pty_req',
  550. session.packRequest_pty_req(b'good', (1, 2, 3, 4), b'')))
  551. self.assertEqual(self.session.session.ptyRequest,
  552. (b'good', (1, 2, 3, 4), []))
  553. def test_requestWindowChange(self):
  554. """
  555. When the client requests to change the window size, the SSHSession
  556. object should make the request by getting an ISession adapter for the
  557. avatar, then calling windowChanged with the new window size.
  558. """
  559. ret = self.session.requestReceived(
  560. b'window_change',
  561. session.packRequest_window_change((0, 0, 0, 0)))
  562. self.assertFalse(ret)
  563. self.assertRequestRaisedRuntimeError()
  564. self.assertSessionIsStubSession()
  565. self.assertTrue(self.session.requestReceived(b'window_change',
  566. session.packRequest_window_change((1, 2, 3, 4))))
  567. self.assertEqual(self.session.session.windowChange,
  568. (1, 2, 3, 4))
  569. def test_eofReceived(self):
  570. """
  571. When an EOF is received and an ISession adapter is present, it should
  572. be notified of the EOF message.
  573. """
  574. self.session.session = session.ISession(self.session.avatar)
  575. self.session.eofReceived()
  576. self.assertTrue(self.session.session.gotEOF)
  577. def test_closeReceived(self):
  578. """
  579. When a close is received, the session should send a close message.
  580. """
  581. ret = self.session.closeReceived()
  582. self.assertIsNone(ret)
  583. self.assertTrue(self.session.conn.closes[self.session])
  584. def test_closed(self):
  585. """
  586. When a close is received and an ISession adapter is present, it should
  587. be notified of the close message.
  588. """
  589. self.session.session = session.ISession(self.session.avatar)
  590. self.session.closed()
  591. self.assertTrue(self.session.session.gotClosed)
  592. class SessionWithNoAvatarTests(unittest.TestCase):
  593. """
  594. Test for the SSHSession interface. Several of the methods (request_shell,
  595. request_exec, request_pty_req, request_window_change) would create a
  596. 'session' instance variable from the avatar if one didn't exist when they
  597. were called.
  598. """
  599. def setUp(self):
  600. self.session = session.SSHSession()
  601. self.session.avatar = StubAvatar()
  602. self.assertIsNone(self.session.session)
  603. def assertSessionProvidesISession(self):
  604. """
  605. self.session.session should provide I{ISession}.
  606. """
  607. self.assertTrue(session.ISession.providedBy(self.session.session),
  608. "ISession not provided by %r" % self.session.session)
  609. def test_requestShellGetsSession(self):
  610. """
  611. If an ISession adapter isn't already present, request_shell should get
  612. one.
  613. """
  614. self.session.requestReceived(b'shell', b'')
  615. self.assertSessionProvidesISession()
  616. def test_requestExecGetsSession(self):
  617. """
  618. If an ISession adapter isn't already present, request_exec should get
  619. one.
  620. """
  621. self.session.requestReceived(b'exec',
  622. common.NS(b'success'))
  623. self.assertSessionProvidesISession()
  624. def test_requestPtyReqGetsSession(self):
  625. """
  626. If an ISession adapter isn't already present, request_pty_req should
  627. get one.
  628. """
  629. self.session.requestReceived(b'pty_req',
  630. session.packRequest_pty_req(
  631. b'term', (0, 0, 0, 0), b''))
  632. self.assertSessionProvidesISession()
  633. def test_requestWindowChangeGetsSession(self):
  634. """
  635. If an ISession adapter isn't already present, request_window_change
  636. should get one.
  637. """
  638. self.session.requestReceived(
  639. b'window_change',
  640. session.packRequest_window_change(
  641. (1, 1, 1, 1)))
  642. self.assertSessionProvidesISession()
  643. class WrappersTests(unittest.TestCase):
  644. """
  645. A test for the wrapProtocol and wrapProcessProtocol functions.
  646. """
  647. def test_wrapProtocol(self):
  648. """
  649. L{wrapProtocol}, when passed a L{Protocol} should return something that
  650. has write(), writeSequence(), loseConnection() methods which call the
  651. Protocol's dataReceived() and connectionLost() methods, respectively.
  652. """
  653. protocol = MockProtocol()
  654. protocol.transport = StubTransport()
  655. protocol.connectionMade()
  656. wrapped = session.wrapProtocol(protocol)
  657. wrapped.dataReceived(b'dataReceived')
  658. self.assertEqual(protocol.transport.buf, b'dataReceived')
  659. wrapped.write(b'data')
  660. wrapped.writeSequence([b'1', b'2'])
  661. wrapped.loseConnection()
  662. self.assertEqual(protocol.data, b'data12')
  663. protocol.reason.trap(error.ConnectionDone)
  664. def test_wrapProcessProtocol_Protocol(self):
  665. """
  666. L{wrapPRocessProtocol}, when passed a L{Protocol} should return
  667. something that follows the L{IProcessProtocol} interface, with
  668. connectionMade() mapping to connectionMade(), outReceived() mapping to
  669. dataReceived() and processEnded() mapping to connectionLost().
  670. """
  671. protocol = MockProtocol()
  672. protocol.transport = StubTransport()
  673. process_protocol = session.wrapProcessProtocol(protocol)
  674. process_protocol.connectionMade()
  675. process_protocol.outReceived(b'data')
  676. self.assertEqual(protocol.transport.buf, b'data~')
  677. process_protocol.processEnded(failure.Failure(
  678. error.ProcessTerminated(0, None, None)))
  679. protocol.reason.trap(error.ProcessTerminated)
  680. class HelpersTests(unittest.TestCase):
  681. """
  682. Tests for the 4 helper functions: parseRequest_* and packRequest_*.
  683. """
  684. def test_parseRequest_pty_req(self):
  685. """
  686. The payload of a pty-req message is::
  687. string terminal
  688. uint32 columns
  689. uint32 rows
  690. uint32 x pixels
  691. uint32 y pixels
  692. string modes
  693. Modes are::
  694. byte mode number
  695. uint32 mode value
  696. """
  697. self.assertEqual(session.parseRequest_pty_req(common.NS(b'xterm') +
  698. struct.pack('>4L',
  699. 1, 2, 3, 4)
  700. + common.NS(
  701. struct.pack('>BL', 5, 6))),
  702. (b'xterm', (2, 1, 3, 4), [(5, 6)]))
  703. def test_packRequest_pty_req_old(self):
  704. """
  705. See test_parseRequest_pty_req for the payload format.
  706. """
  707. packed = session.packRequest_pty_req(b'xterm', (2, 1, 3, 4),
  708. b'\x05\x00\x00\x00\x06')
  709. self.assertEqual(packed,
  710. common.NS(b'xterm') +
  711. struct.pack('>4L', 1, 2, 3, 4) +
  712. common.NS(struct.pack('>BL', 5, 6)))
  713. def test_packRequest_pty_req(self):
  714. """
  715. See test_parseRequest_pty_req for the payload format.
  716. """
  717. packed = session.packRequest_pty_req(b'xterm', (2, 1, 3, 4),
  718. b'\x05\x00\x00\x00\x06')
  719. self.assertEqual(packed,
  720. common.NS(b'xterm') +
  721. struct.pack('>4L', 1, 2, 3, 4) +
  722. common.NS(struct.pack('>BL', 5, 6)))
  723. def test_parseRequest_window_change(self):
  724. """
  725. The payload of a window_change request is::
  726. uint32 columns
  727. uint32 rows
  728. uint32 x pixels
  729. uint32 y pixels
  730. parseRequest_window_change() returns (rows, columns, x pixels,
  731. y pixels).
  732. """
  733. self.assertEqual(session.parseRequest_window_change(
  734. struct.pack('>4L', 1, 2, 3, 4)), (2, 1, 3, 4))
  735. def test_packRequest_window_change(self):
  736. """
  737. See test_parseRequest_window_change for the payload format.
  738. """
  739. self.assertEqual(session.packRequest_window_change((2, 1, 3, 4)),
  740. struct.pack('>4L', 1, 2, 3, 4))
  741. class SSHSessionProcessProtocolTests(unittest.TestCase):
  742. """
  743. Tests for L{SSHSessionProcessProtocol}.
  744. """
  745. def setUp(self):
  746. self.transport = StubTransport()
  747. self.session = session.SSHSession(
  748. conn=StubConnection(self.transport), remoteWindow=500,
  749. remoteMaxPacket=100)
  750. self.pp = session.SSHSessionProcessProtocol(self.session)
  751. self.pp.makeConnection(self.transport)
  752. def assertSessionClosed(self):
  753. """
  754. Assert that C{self.session} is closed.
  755. """
  756. self.assertTrue(self.session.conn.closes[self.session])
  757. def assertRequestsEqual(self, expectedRequests):
  758. """
  759. Assert that C{self.session} has sent the C{expectedRequests}.
  760. """
  761. self.assertEqual(
  762. self.session.conn.requests[self.session],
  763. expectedRequests)
  764. def test_init(self):
  765. """
  766. SSHSessionProcessProtocol should set self.session to the session passed
  767. to the __init__ method.
  768. """
  769. self.assertEqual(self.pp.session, self.session)
  770. def test_getHost(self):
  771. """
  772. SSHSessionProcessProtocol.getHost() just delegates to its
  773. session.conn.transport.getHost().
  774. """
  775. self.assertEqual(
  776. self.session.conn.transport.getHost(), self.pp.getHost())
  777. def test_getPeer(self):
  778. """
  779. SSHSessionProcessProtocol.getPeer() just delegates to its
  780. session.conn.transport.getPeer().
  781. """
  782. self.assertEqual(
  783. self.session.conn.transport.getPeer(), self.pp.getPeer())
  784. def test_connectionMade(self):
  785. """
  786. SSHSessionProcessProtocol.connectionMade() should check if there's a
  787. 'buf' attribute on its session and write it to the transport if so.
  788. """
  789. self.session.buf = b'buffer'
  790. self.pp.connectionMade()
  791. self.assertEqual(self.transport.buf, b'buffer')
  792. def test_getSignalName(self):
  793. """
  794. _getSignalName should return the name of a signal when given the
  795. signal number.
  796. """
  797. for signalName in session.SUPPORTED_SIGNALS:
  798. signalName = 'SIG' + signalName
  799. signalValue = getattr(signal, signalName)
  800. sshName = self.pp._getSignalName(signalValue)
  801. self.assertEqual(sshName, signalName,
  802. "%i: %s != %s" % (signalValue, sshName,
  803. signalName))
  804. def test_getSignalNameWithLocalSignal(self):
  805. """
  806. If there are signals in the signal module which aren't in the SSH RFC,
  807. we map their name to [signal name]@[platform].
  808. """
  809. signal.SIGTwistedTest = signal.NSIG + 1 # value can't exist normally
  810. # Force reinitialization of signals
  811. self.pp._signalValuesToNames = None
  812. self.assertEqual(self.pp._getSignalName(signal.SIGTwistedTest),
  813. 'SIGTwistedTest@' + sys.platform)
  814. if getattr(signal, 'SIGALRM', None) is None:
  815. test_getSignalName.skip = test_getSignalNameWithLocalSignal.skip = \
  816. "Not all signals available"
  817. def test_outReceived(self):
  818. """
  819. When data is passed to the outReceived method, it should be sent to
  820. the session's write method.
  821. """
  822. self.pp.outReceived(b'test data')
  823. self.assertEqual(self.session.conn.data[self.session],
  824. [b'test data'])
  825. def test_write(self):
  826. """
  827. When data is passed to the write method, it should be sent to the
  828. session channel's write method.
  829. """
  830. self.pp.write(b'test data')
  831. self.assertEqual(self.session.conn.data[self.session],
  832. [b'test data'])
  833. def test_writeSequence(self):
  834. """
  835. When a sequence is passed to the writeSequence method, it should be
  836. joined together and sent to the session channel's write method.
  837. """
  838. self.pp.writeSequence([b'test ', b'data'])
  839. self.assertEqual(self.session.conn.data[self.session],
  840. [b'test data'])
  841. def test_errReceived(self):
  842. """
  843. When data is passed to the errReceived method, it should be sent to
  844. the session's writeExtended method.
  845. """
  846. self.pp.errReceived(b'test data')
  847. self.assertEqual(self.session.conn.extData[self.session],
  848. [(1, b'test data')])
  849. def test_outConnectionLost(self):
  850. """
  851. When outConnectionLost and errConnectionLost are both called, we should
  852. send an EOF message.
  853. """
  854. self.pp.outConnectionLost()
  855. self.assertFalse(self.session in self.session.conn.eofs)
  856. self.pp.errConnectionLost()
  857. self.assertTrue(self.session.conn.eofs[self.session])
  858. def test_errConnectionLost(self):
  859. """
  860. Make sure reverse ordering of events in test_outConnectionLost also
  861. sends EOF.
  862. """
  863. self.pp.errConnectionLost()
  864. self.assertFalse(self.session in self.session.conn.eofs)
  865. self.pp.outConnectionLost()
  866. self.assertTrue(self.session.conn.eofs[self.session])
  867. def test_loseConnection(self):
  868. """
  869. When loseConnection() is called, it should call loseConnection
  870. on the session channel.
  871. """
  872. self.pp.loseConnection()
  873. self.assertTrue(self.session.conn.closes[self.session])
  874. def test_connectionLost(self):
  875. """
  876. When connectionLost() is called, it should call loseConnection()
  877. on the session channel.
  878. """
  879. self.pp.connectionLost(failure.Failure(
  880. ProcessDone(0)))
  881. def test_processEndedWithExitCode(self):
  882. """
  883. When processEnded is called, if there is an exit code in the reason
  884. it should be sent in an exit-status method. The connection should be
  885. closed.
  886. """
  887. self.pp.processEnded(Failure(ProcessDone(None)))
  888. self.assertRequestsEqual(
  889. [(b'exit-status', struct.pack('>I', 0) , False)])
  890. self.assertSessionClosed()
  891. def test_processEndedWithExitSignalCoreDump(self):
  892. """
  893. When processEnded is called, if there is an exit signal in the reason
  894. it should be sent in an exit-signal message. The connection should be
  895. closed.
  896. """
  897. self.pp.processEnded(
  898. Failure(ProcessTerminated(1,
  899. signal.SIGTERM, 1 << 7))) # 7th bit means core dumped
  900. self.assertRequestsEqual(
  901. [(b'exit-signal',
  902. common.NS(b'TERM') # signal name
  903. + b'\x01' # core dumped is true
  904. + common.NS(b'') # error message
  905. + common.NS(b''), # language tag
  906. False)])
  907. self.assertSessionClosed()
  908. def test_processEndedWithExitSignalNoCoreDump(self):
  909. """
  910. When processEnded is called, if there is an exit signal in the
  911. reason it should be sent in an exit-signal message. If no
  912. core was dumped, don't set the core-dump bit.
  913. """
  914. self.pp.processEnded(
  915. Failure(ProcessTerminated(1, signal.SIGTERM, 0)))
  916. # see comments in test_processEndedWithExitSignalCoreDump for the
  917. # meaning of the parts in the request
  918. self.assertRequestsEqual(
  919. [(b'exit-signal', common.NS(b'TERM') + b'\x00' + common.NS(b'') +
  920. common.NS(b''), False)])
  921. self.assertSessionClosed()
  922. if getattr(os, 'WCOREDUMP', None) is None:
  923. skipMsg = "can't run this w/o os.WCOREDUMP"
  924. test_processEndedWithExitSignalCoreDump.skip = skipMsg
  925. test_processEndedWithExitSignalNoCoreDump.skip = skipMsg
  926. class SSHSessionClientTests(unittest.TestCase):
  927. """
  928. SSHSessionClient is an obsolete class used to connect standard IO to
  929. an SSHSession.
  930. """
  931. def test_dataReceived(self):
  932. """
  933. When data is received, it should be sent to the transport.
  934. """
  935. client = session.SSHSessionClient()
  936. client.transport = StubTransport()
  937. client.dataReceived(b'test data')
  938. self.assertEqual(client.transport.buf, b'test data')