test_ssh.py 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988
  1. # Copyright (c) Twisted Matrix Laboratories.
  2. # See LICENSE for details.
  3. """
  4. Tests for L{twisted.conch.ssh}.
  5. """
  6. from __future__ import division, absolute_import
  7. import struct
  8. try:
  9. import cryptography
  10. except ImportError:
  11. cryptography = None
  12. try:
  13. import pyasn1
  14. except ImportError:
  15. pyasn1 = None
  16. from twisted.conch.ssh import common, session, forwarding, _kex
  17. from twisted.conch import avatar, error
  18. from twisted.conch.test.keydata import publicRSA_openssh, privateRSA_openssh
  19. from twisted.conch.test.keydata import publicDSA_openssh, privateDSA_openssh
  20. from twisted.cred import portal
  21. from twisted.cred.error import UnauthorizedLogin
  22. from twisted.internet import defer, protocol, reactor
  23. from twisted.internet.error import ProcessTerminated
  24. from twisted.python import failure, log
  25. from twisted.trial import unittest
  26. from twisted.conch.test.loopback import LoopbackRelay
  27. class ConchTestRealm(object):
  28. """
  29. A realm which expects a particular avatarId to log in once and creates a
  30. L{ConchTestAvatar} for that request.
  31. @ivar expectedAvatarID: The only avatarID that this realm will produce an
  32. avatar for.
  33. @ivar avatar: A reference to the avatar after it is requested.
  34. """
  35. avatar = None
  36. def __init__(self, expectedAvatarID):
  37. self.expectedAvatarID = expectedAvatarID
  38. def requestAvatar(self, avatarID, mind, *interfaces):
  39. """
  40. Return a new L{ConchTestAvatar} if the avatarID matches the expected one
  41. and this is the first avatar request.
  42. """
  43. if avatarID == self.expectedAvatarID:
  44. if self.avatar is not None:
  45. raise UnauthorizedLogin("Only one login allowed")
  46. self.avatar = ConchTestAvatar()
  47. return interfaces[0], self.avatar, self.avatar.logout
  48. raise UnauthorizedLogin(
  49. "Only %r may log in, not %r" % (self.expectedAvatarID, avatarID))
  50. class ConchTestAvatar(avatar.ConchUser):
  51. """
  52. An avatar against which various SSH features can be tested.
  53. @ivar loggedOut: A flag indicating whether the avatar logout method has been
  54. called.
  55. """
  56. loggedOut = False
  57. def __init__(self):
  58. avatar.ConchUser.__init__(self)
  59. self.listeners = {}
  60. self.globalRequests = {}
  61. self.channelLookup.update(
  62. {b'session': session.SSHSession,
  63. b'direct-tcpip':forwarding.openConnectForwardingClient})
  64. self.subsystemLookup.update({b'crazy': CrazySubsystem})
  65. def global_foo(self, data):
  66. self.globalRequests['foo'] = data
  67. return 1
  68. def global_foo_2(self, data):
  69. self.globalRequests['foo_2'] = data
  70. return 1, b'data'
  71. def global_tcpip_forward(self, data):
  72. host, port = forwarding.unpackGlobal_tcpip_forward(data)
  73. try:
  74. listener = reactor.listenTCP(
  75. port, forwarding.SSHListenForwardingFactory(
  76. self.conn, (host, port),
  77. forwarding.SSHListenServerForwardingChannel),
  78. interface=host)
  79. except:
  80. log.err(None, "something went wrong with remote->local forwarding")
  81. return 0
  82. else:
  83. self.listeners[(host, port)] = listener
  84. return 1
  85. def global_cancel_tcpip_forward(self, data):
  86. host, port = forwarding.unpackGlobal_tcpip_forward(data)
  87. listener = self.listeners.get((host, port), None)
  88. if not listener:
  89. return 0
  90. del self.listeners[(host, port)]
  91. listener.stopListening()
  92. return 1
  93. def logout(self):
  94. self.loggedOut = True
  95. for listener in self.listeners.values():
  96. log.msg('stopListening %s' % listener)
  97. listener.stopListening()
  98. class ConchSessionForTestAvatar(object):
  99. """
  100. An ISession adapter for ConchTestAvatar.
  101. """
  102. def __init__(self, avatar):
  103. """
  104. Initialize the session and create a reference to it on the avatar for
  105. later inspection.
  106. """
  107. self.avatar = avatar
  108. self.avatar._testSession = self
  109. self.cmd = None
  110. self.proto = None
  111. self.ptyReq = False
  112. self.eof = 0
  113. self.onClose = defer.Deferred()
  114. def getPty(self, term, windowSize, attrs):
  115. log.msg('pty req')
  116. self._terminalType = term
  117. self._windowSize = windowSize
  118. self.ptyReq = True
  119. def openShell(self, proto):
  120. log.msg('opening shell')
  121. self.proto = proto
  122. EchoTransport(proto)
  123. self.cmd = b'shell'
  124. def execCommand(self, proto, cmd):
  125. self.cmd = cmd
  126. self.proto = proto
  127. f = cmd.split()[0]
  128. if f == b'false':
  129. t = FalseTransport(proto)
  130. # Avoid disconnecting this immediately. If the channel is closed
  131. # before execCommand even returns the caller gets confused.
  132. reactor.callLater(0, t.loseConnection)
  133. elif f == b'echo':
  134. t = EchoTransport(proto)
  135. t.write(cmd[5:])
  136. t.loseConnection()
  137. elif f == b'secho':
  138. t = SuperEchoTransport(proto)
  139. t.write(cmd[6:])
  140. t.loseConnection()
  141. elif f == b'eecho':
  142. t = ErrEchoTransport(proto)
  143. t.write(cmd[6:])
  144. t.loseConnection()
  145. else:
  146. raise error.ConchError('bad exec')
  147. self.avatar.conn.transport.expectedLoseConnection = 1
  148. def eofReceived(self):
  149. self.eof = 1
  150. def closed(self):
  151. log.msg('closed cmd "%s"' % self.cmd)
  152. self.remoteWindowLeftAtClose = self.proto.session.remoteWindowLeft
  153. self.onClose.callback(None)
  154. from twisted.python import components
  155. components.registerAdapter(ConchSessionForTestAvatar, ConchTestAvatar, session.ISession)
  156. class CrazySubsystem(protocol.Protocol):
  157. def __init__(self, *args, **kw):
  158. pass
  159. def connectionMade(self):
  160. """
  161. good ... good
  162. """
  163. class FalseTransport:
  164. """
  165. False transport should act like a /bin/false execution, i.e. just exit with
  166. nonzero status, writing nothing to the terminal.
  167. @ivar proto: The protocol associated with this transport.
  168. @ivar closed: A flag tracking whether C{loseConnection} has been called yet.
  169. """
  170. def __init__(self, p):
  171. """
  172. @type p L{twisted.conch.ssh.session.SSHSessionProcessProtocol} instance
  173. """
  174. self.proto = p
  175. p.makeConnection(self)
  176. self.closed = 0
  177. def loseConnection(self):
  178. """
  179. Disconnect the protocol associated with this transport.
  180. """
  181. if self.closed:
  182. return
  183. self.closed = 1
  184. self.proto.inConnectionLost()
  185. self.proto.outConnectionLost()
  186. self.proto.errConnectionLost()
  187. self.proto.processEnded(failure.Failure(ProcessTerminated(255, None, None)))
  188. class EchoTransport:
  189. def __init__(self, p):
  190. self.proto = p
  191. p.makeConnection(self)
  192. self.closed = 0
  193. def write(self, data):
  194. log.msg(repr(data))
  195. self.proto.outReceived(data)
  196. self.proto.outReceived(b'\r\n')
  197. if b'\x00' in data: # mimic 'exit' for the shell test
  198. self.loseConnection()
  199. def loseConnection(self):
  200. if self.closed: return
  201. self.closed = 1
  202. self.proto.inConnectionLost()
  203. self.proto.outConnectionLost()
  204. self.proto.errConnectionLost()
  205. self.proto.processEnded(failure.Failure(ProcessTerminated(0, None, None)))
  206. class ErrEchoTransport:
  207. def __init__(self, p):
  208. self.proto = p
  209. p.makeConnection(self)
  210. self.closed = 0
  211. def write(self, data):
  212. self.proto.errReceived(data)
  213. self.proto.errReceived(b'\r\n')
  214. def loseConnection(self):
  215. if self.closed: return
  216. self.closed = 1
  217. self.proto.inConnectionLost()
  218. self.proto.outConnectionLost()
  219. self.proto.errConnectionLost()
  220. self.proto.processEnded(failure.Failure(ProcessTerminated(0, None, None)))
  221. class SuperEchoTransport:
  222. def __init__(self, p):
  223. self.proto = p
  224. p.makeConnection(self)
  225. self.closed = 0
  226. def write(self, data):
  227. self.proto.outReceived(data)
  228. self.proto.outReceived(b'\r\n')
  229. self.proto.errReceived(data)
  230. self.proto.errReceived(b'\r\n')
  231. def loseConnection(self):
  232. if self.closed: return
  233. self.closed = 1
  234. self.proto.inConnectionLost()
  235. self.proto.outConnectionLost()
  236. self.proto.errConnectionLost()
  237. self.proto.processEnded(failure.Failure(ProcessTerminated(0, None, None)))
  238. if cryptography is not None and pyasn1 is not None:
  239. from twisted.conch import checkers
  240. from twisted.conch.ssh import channel, connection, factory, keys
  241. from twisted.conch.ssh import transport, userauth
  242. class ConchTestPasswordChecker:
  243. credentialInterfaces = checkers.IUsernamePassword,
  244. def requestAvatarId(self, credentials):
  245. if credentials.username == b'testuser' and credentials.password == b'testpass':
  246. return defer.succeed(credentials.username)
  247. return defer.fail(Exception("Bad credentials"))
  248. class ConchTestSSHChecker(checkers.SSHProtocolChecker):
  249. def areDone(self, avatarId):
  250. if avatarId != b'testuser' or len(self.successfulCredentials[avatarId]) < 2:
  251. return False
  252. return True
  253. class ConchTestServerFactory(factory.SSHFactory):
  254. noisy = 0
  255. services = {
  256. b'ssh-userauth':userauth.SSHUserAuthServer,
  257. b'ssh-connection':connection.SSHConnection
  258. }
  259. def buildProtocol(self, addr):
  260. proto = ConchTestServer()
  261. proto.supportedPublicKeys = self.privateKeys.keys()
  262. proto.factory = self
  263. if hasattr(self, 'expectedLoseConnection'):
  264. proto.expectedLoseConnection = self.expectedLoseConnection
  265. self.proto = proto
  266. return proto
  267. def getPublicKeys(self):
  268. return {
  269. b'ssh-rsa': keys.Key.fromString(publicRSA_openssh),
  270. b'ssh-dss': keys.Key.fromString(publicDSA_openssh)
  271. }
  272. def getPrivateKeys(self):
  273. return {
  274. b'ssh-rsa': keys.Key.fromString(privateRSA_openssh),
  275. b'ssh-dss': keys.Key.fromString(privateDSA_openssh)
  276. }
  277. def getPrimes(self):
  278. """
  279. Diffie-Hellman primes that can be used for the
  280. diffie-hellman-group-exchange-sha1 key exchange.
  281. @return: The primes and generators.
  282. @rtype: L{dict} mapping the key size to a C{list} of
  283. C{(generator, prime)} tupple.
  284. """
  285. # In these tests, we hardwire the prime values to those defined by
  286. # the diffie-hellman-group14-sha1 key exchange algorithm, to avoid
  287. # requiring a moduli file when running tests.
  288. # See OpenSSHFactory.getPrimes.
  289. return {
  290. 2048: [
  291. _kex.getDHGeneratorAndPrime(
  292. b'diffie-hellman-group14-sha1')]
  293. }
  294. def getService(self, trans, name):
  295. return factory.SSHFactory.getService(self, trans, name)
  296. class ConchTestBase:
  297. done = 0
  298. def connectionLost(self, reason):
  299. if self.done:
  300. return
  301. if not hasattr(self, 'expectedLoseConnection'):
  302. raise unittest.FailTest(
  303. 'unexpectedly lost connection %s\n%s' % (self, reason))
  304. self.done = 1
  305. def receiveError(self, reasonCode, desc):
  306. self.expectedLoseConnection = 1
  307. # Some versions of OpenSSH (for example, OpenSSH_5.3p1) will
  308. # send a DISCONNECT_BY_APPLICATION error before closing the
  309. # connection. Other, older versions (for example,
  310. # OpenSSH_5.1p1), won't. So accept this particular error here,
  311. # but no others.
  312. if reasonCode != transport.DISCONNECT_BY_APPLICATION:
  313. log.err(
  314. Exception(
  315. 'got disconnect for %s: reason %s, desc: %s' % (
  316. self, reasonCode, desc)))
  317. self.loseConnection()
  318. def receiveUnimplemented(self, seqID):
  319. raise unittest.FailTest('got unimplemented: seqid %s' % (seqID,))
  320. self.expectedLoseConnection = 1
  321. self.loseConnection()
  322. class ConchTestServer(ConchTestBase, transport.SSHServerTransport):
  323. def connectionLost(self, reason):
  324. ConchTestBase.connectionLost(self, reason)
  325. transport.SSHServerTransport.connectionLost(self, reason)
  326. class ConchTestClient(ConchTestBase, transport.SSHClientTransport):
  327. """
  328. @ivar _channelFactory: A callable which accepts an SSH connection and
  329. returns a channel which will be attached to a new channel on that
  330. connection.
  331. """
  332. def __init__(self, channelFactory):
  333. self._channelFactory = channelFactory
  334. def connectionLost(self, reason):
  335. ConchTestBase.connectionLost(self, reason)
  336. transport.SSHClientTransport.connectionLost(self, reason)
  337. def verifyHostKey(self, key, fp):
  338. keyMatch = key == keys.Key.fromString(publicRSA_openssh).blob()
  339. fingerprintMatch = (
  340. fp == b'3d:13:5f:cb:c9:79:8a:93:06:27:65:bc:3d:0b:8f:af')
  341. if keyMatch and fingerprintMatch:
  342. return defer.succeed(1)
  343. return defer.fail(Exception("Key or fingerprint mismatch"))
  344. def connectionSecure(self):
  345. self.requestService(ConchTestClientAuth(b'testuser',
  346. ConchTestClientConnection(self._channelFactory)))
  347. class ConchTestClientAuth(userauth.SSHUserAuthClient):
  348. hasTriedNone = 0 # have we tried the 'none' auth yet?
  349. canSucceedPublicKey = 0 # can we succeed with this yet?
  350. canSucceedPassword = 0
  351. def ssh_USERAUTH_SUCCESS(self, packet):
  352. if not self.canSucceedPassword and self.canSucceedPublicKey:
  353. raise unittest.FailTest(
  354. 'got USERAUTH_SUCCESS before password and publickey')
  355. userauth.SSHUserAuthClient.ssh_USERAUTH_SUCCESS(self, packet)
  356. def getPassword(self):
  357. self.canSucceedPassword = 1
  358. return defer.succeed(b'testpass')
  359. def getPrivateKey(self):
  360. self.canSucceedPublicKey = 1
  361. return defer.succeed(keys.Key.fromString(privateDSA_openssh))
  362. def getPublicKey(self):
  363. return keys.Key.fromString(publicDSA_openssh)
  364. class ConchTestClientConnection(connection.SSHConnection):
  365. """
  366. @ivar _completed: A L{Deferred} which will be fired when the number of
  367. results collected reaches C{totalResults}.
  368. """
  369. name = b'ssh-connection'
  370. results = 0
  371. totalResults = 8
  372. def __init__(self, channelFactory):
  373. connection.SSHConnection.__init__(self)
  374. self._channelFactory = channelFactory
  375. def serviceStarted(self):
  376. self.openChannel(self._channelFactory(conn=self))
  377. class SSHTestChannel(channel.SSHChannel):
  378. def __init__(self, name, opened, *args, **kwargs):
  379. self.name = name
  380. self._opened = opened
  381. self.received = []
  382. self.receivedExt = []
  383. self.onClose = defer.Deferred()
  384. channel.SSHChannel.__init__(self, *args, **kwargs)
  385. def openFailed(self, reason):
  386. self._opened.errback(reason)
  387. def channelOpen(self, ignore):
  388. self._opened.callback(self)
  389. def dataReceived(self, data):
  390. self.received.append(data)
  391. def extReceived(self, dataType, data):
  392. if dataType == connection.EXTENDED_DATA_STDERR:
  393. self.receivedExt.append(data)
  394. else:
  395. log.msg("Unrecognized extended data: %r" % (dataType,))
  396. def request_exit_status(self, status):
  397. [self.status] = struct.unpack('>L', status)
  398. def eofReceived(self):
  399. self.eofCalled = True
  400. def closed(self):
  401. self.onClose.callback(None)
  402. def conchTestPublicKeyChecker():
  403. """
  404. Produces a SSHPublicKeyChecker with an in-memory key mapping with
  405. a single use: 'testuser'
  406. @return: L{twisted.conch.checkers.SSHPublicKeyChecker}
  407. """
  408. conchTestPublicKeyDB = checkers.InMemorySSHKeyDB(
  409. {b'testuser': [keys.Key.fromString(publicDSA_openssh)]})
  410. return checkers.SSHPublicKeyChecker(conchTestPublicKeyDB)
  411. class SSHProtocolTests(unittest.TestCase):
  412. """
  413. Tests for communication between L{SSHServerTransport} and
  414. L{SSHClientTransport}.
  415. """
  416. if not cryptography:
  417. skip = "can't run without cryptography"
  418. if not pyasn1:
  419. skip = "Cannot run without PyASN1"
  420. def _ourServerOurClientTest(self, name=b'session', **kwargs):
  421. """
  422. Create a connected SSH client and server protocol pair and return a
  423. L{Deferred} which fires with an L{SSHTestChannel} instance connected to
  424. a channel on that SSH connection.
  425. """
  426. result = defer.Deferred()
  427. self.realm = ConchTestRealm(b'testuser')
  428. p = portal.Portal(self.realm)
  429. sshpc = ConchTestSSHChecker()
  430. sshpc.registerChecker(ConchTestPasswordChecker())
  431. sshpc.registerChecker(conchTestPublicKeyChecker())
  432. p.registerChecker(sshpc)
  433. fac = ConchTestServerFactory()
  434. fac.portal = p
  435. fac.startFactory()
  436. self.server = fac.buildProtocol(None)
  437. self.clientTransport = LoopbackRelay(self.server)
  438. self.client = ConchTestClient(
  439. lambda conn: SSHTestChannel(name, result, conn=conn, **kwargs))
  440. self.serverTransport = LoopbackRelay(self.client)
  441. self.server.makeConnection(self.serverTransport)
  442. self.client.makeConnection(self.clientTransport)
  443. return result
  444. def test_subsystemsAndGlobalRequests(self):
  445. """
  446. Run the Conch server against the Conch client. Set up several different
  447. channels which exercise different behaviors and wait for them to
  448. complete. Verify that the channels with errors log them.
  449. """
  450. channel = self._ourServerOurClientTest()
  451. def cbSubsystem(channel):
  452. self.channel = channel
  453. return self.assertFailure(
  454. channel.conn.sendRequest(
  455. channel, b'subsystem', common.NS(b'not-crazy'), 1),
  456. Exception)
  457. channel.addCallback(cbSubsystem)
  458. def cbNotCrazyFailed(ignored):
  459. channel = self.channel
  460. return channel.conn.sendRequest(
  461. channel, b'subsystem', common.NS(b'crazy'), 1)
  462. channel.addCallback(cbNotCrazyFailed)
  463. def cbGlobalRequests(ignored):
  464. channel = self.channel
  465. d1 = channel.conn.sendGlobalRequest(b'foo', b'bar', 1)
  466. d2 = channel.conn.sendGlobalRequest(b'foo-2', b'bar2', 1)
  467. d2.addCallback(self.assertEqual, b'data')
  468. d3 = self.assertFailure(
  469. channel.conn.sendGlobalRequest(b'bar', b'foo', 1),
  470. Exception)
  471. return defer.gatherResults([d1, d2, d3])
  472. channel.addCallback(cbGlobalRequests)
  473. def disconnect(ignored):
  474. self.assertEqual(
  475. self.realm.avatar.globalRequests,
  476. {"foo": b"bar", "foo_2": b"bar2"})
  477. channel = self.channel
  478. channel.conn.transport.expectedLoseConnection = True
  479. channel.conn.serviceStopped()
  480. channel.loseConnection()
  481. channel.addCallback(disconnect)
  482. return channel
  483. def test_shell(self):
  484. """
  485. L{SSHChannel.sendRequest} can open a shell with a I{pty-req} request,
  486. specifying a terminal type and window size.
  487. """
  488. channel = self._ourServerOurClientTest()
  489. data = session.packRequest_pty_req(
  490. b'conch-test-term', (24, 80, 0, 0), b'')
  491. def cbChannel(channel):
  492. self.channel = channel
  493. return channel.conn.sendRequest(channel, b'pty-req', data, 1)
  494. channel.addCallback(cbChannel)
  495. def cbPty(ignored):
  496. # The server-side object corresponding to our client side channel.
  497. session = self.realm.avatar.conn.channels[0].session
  498. self.assertIs(session.avatar, self.realm.avatar)
  499. self.assertEqual(session._terminalType, b'conch-test-term')
  500. self.assertEqual(session._windowSize, (24, 80, 0, 0))
  501. self.assertTrue(session.ptyReq)
  502. channel = self.channel
  503. return channel.conn.sendRequest(channel, b'shell', b'', 1)
  504. channel.addCallback(cbPty)
  505. def cbShell(ignored):
  506. self.channel.write(b'testing the shell!\x00')
  507. self.channel.conn.sendEOF(self.channel)
  508. return defer.gatherResults([
  509. self.channel.onClose,
  510. self.realm.avatar._testSession.onClose])
  511. channel.addCallback(cbShell)
  512. def cbExited(ignored):
  513. if self.channel.status != 0:
  514. log.msg(
  515. 'shell exit status was not 0: %i' % (self.channel.status,))
  516. self.assertEqual(
  517. b"".join(self.channel.received),
  518. b'testing the shell!\x00\r\n')
  519. self.assertTrue(self.channel.eofCalled)
  520. self.assertTrue(
  521. self.realm.avatar._testSession.eof)
  522. channel.addCallback(cbExited)
  523. return channel
  524. def test_failedExec(self):
  525. """
  526. If L{SSHChannel.sendRequest} issues an exec which the server responds to
  527. with an error, the L{Deferred} it returns fires its errback.
  528. """
  529. channel = self._ourServerOurClientTest()
  530. def cbChannel(channel):
  531. self.channel = channel
  532. return self.assertFailure(
  533. channel.conn.sendRequest(
  534. channel, b'exec', common.NS(b'jumboliah'), 1),
  535. Exception)
  536. channel.addCallback(cbChannel)
  537. def cbFailed(ignored):
  538. # The server logs this exception when it cannot perform the
  539. # requested exec.
  540. errors = self.flushLoggedErrors(error.ConchError)
  541. self.assertEqual(errors[0].value.args, ('bad exec', None))
  542. channel.addCallback(cbFailed)
  543. return channel
  544. def test_falseChannel(self):
  545. """
  546. When the process started by a L{SSHChannel.sendRequest} exec request
  547. exits, the exit status is reported to the channel.
  548. """
  549. channel = self._ourServerOurClientTest()
  550. def cbChannel(channel):
  551. self.channel = channel
  552. return channel.conn.sendRequest(
  553. channel, b'exec', common.NS(b'false'), 1)
  554. channel.addCallback(cbChannel)
  555. def cbExec(ignored):
  556. return self.channel.onClose
  557. channel.addCallback(cbExec)
  558. def cbClosed(ignored):
  559. # No data is expected
  560. self.assertEqual(self.channel.received, [])
  561. self.assertNotEqual(self.channel.status, 0)
  562. channel.addCallback(cbClosed)
  563. return channel
  564. def test_errorChannel(self):
  565. """
  566. Bytes sent over the extended channel for stderr data are delivered to
  567. the channel's C{extReceived} method.
  568. """
  569. channel = self._ourServerOurClientTest(localWindow=4, localMaxPacket=5)
  570. def cbChannel(channel):
  571. self.channel = channel
  572. return channel.conn.sendRequest(
  573. channel, b'exec', common.NS(b'eecho hello'), 1)
  574. channel.addCallback(cbChannel)
  575. def cbExec(ignored):
  576. return defer.gatherResults([
  577. self.channel.onClose,
  578. self.realm.avatar._testSession.onClose])
  579. channel.addCallback(cbExec)
  580. def cbClosed(ignored):
  581. self.assertEqual(self.channel.received, [])
  582. self.assertEqual(b"".join(self.channel.receivedExt), b"hello\r\n")
  583. self.assertEqual(self.channel.status, 0)
  584. self.assertTrue(self.channel.eofCalled)
  585. self.assertEqual(self.channel.localWindowLeft, 4)
  586. self.assertEqual(
  587. self.channel.localWindowLeft,
  588. self.realm.avatar._testSession.remoteWindowLeftAtClose)
  589. channel.addCallback(cbClosed)
  590. return channel
  591. def test_unknownChannel(self):
  592. """
  593. When an attempt is made to open an unknown channel type, the L{Deferred}
  594. returned by L{SSHChannel.sendRequest} fires its errback.
  595. """
  596. d = self.assertFailure(
  597. self._ourServerOurClientTest(b'crazy-unknown-channel'), Exception)
  598. def cbFailed(ignored):
  599. errors = self.flushLoggedErrors(error.ConchError)
  600. self.assertEqual(errors[0].value.args, (3, 'unknown channel'))
  601. self.assertEqual(len(errors), 1)
  602. d.addCallback(cbFailed)
  603. return d
  604. def test_maxPacket(self):
  605. """
  606. An L{SSHChannel} can be configured with a maximum packet size to
  607. receive.
  608. """
  609. # localWindow needs to be at least 11 otherwise the assertion about it
  610. # in cbClosed is invalid.
  611. channel = self._ourServerOurClientTest(
  612. localWindow=11, localMaxPacket=1)
  613. def cbChannel(channel):
  614. self.channel = channel
  615. return channel.conn.sendRequest(
  616. channel, b'exec', common.NS(b'secho hello'), 1)
  617. channel.addCallback(cbChannel)
  618. def cbExec(ignored):
  619. return self.channel.onClose
  620. channel.addCallback(cbExec)
  621. def cbClosed(ignored):
  622. self.assertEqual(self.channel.status, 0)
  623. self.assertEqual(b"".join(self.channel.received), b"hello\r\n")
  624. self.assertEqual(b"".join(self.channel.receivedExt), b"hello\r\n")
  625. self.assertEqual(self.channel.localWindowLeft, 11)
  626. self.assertTrue(self.channel.eofCalled)
  627. channel.addCallback(cbClosed)
  628. return channel
  629. def test_echo(self):
  630. """
  631. Normal standard out bytes are sent to the channel's C{dataReceived}
  632. method.
  633. """
  634. channel = self._ourServerOurClientTest(localWindow=4, localMaxPacket=5)
  635. def cbChannel(channel):
  636. self.channel = channel
  637. return channel.conn.sendRequest(
  638. channel, b'exec', common.NS(b'echo hello'), 1)
  639. channel.addCallback(cbChannel)
  640. def cbEcho(ignored):
  641. return defer.gatherResults([
  642. self.channel.onClose,
  643. self.realm.avatar._testSession.onClose])
  644. channel.addCallback(cbEcho)
  645. def cbClosed(ignored):
  646. self.assertEqual(self.channel.status, 0)
  647. self.assertEqual(b"".join(self.channel.received), b"hello\r\n")
  648. self.assertEqual(self.channel.localWindowLeft, 4)
  649. self.assertTrue(self.channel.eofCalled)
  650. self.assertEqual(
  651. self.channel.localWindowLeft,
  652. self.realm.avatar._testSession.remoteWindowLeftAtClose)
  653. channel.addCallback(cbClosed)
  654. return channel
  655. class SSHFactoryTests(unittest.TestCase):
  656. if not cryptography:
  657. skip = "can't run without cryptography"
  658. if not pyasn1:
  659. skip = "Cannot run without PyASN1"
  660. def makeSSHFactory(self, primes=None):
  661. sshFactory = factory.SSHFactory()
  662. gpk = lambda: {'ssh-rsa' : keys.Key(None)}
  663. sshFactory.getPrimes = lambda: primes
  664. sshFactory.getPublicKeys = sshFactory.getPrivateKeys = gpk
  665. sshFactory.startFactory()
  666. return sshFactory
  667. def test_buildProtocol(self):
  668. """
  669. By default, buildProtocol() constructs an instance of
  670. SSHServerTransport.
  671. """
  672. factory = self.makeSSHFactory()
  673. protocol = factory.buildProtocol(None)
  674. self.assertIsInstance(protocol, transport.SSHServerTransport)
  675. def test_buildProtocolRespectsProtocol(self):
  676. """
  677. buildProtocol() calls 'self.protocol()' to construct a protocol
  678. instance.
  679. """
  680. calls = []
  681. def makeProtocol(*args):
  682. calls.append(args)
  683. return transport.SSHServerTransport()
  684. factory = self.makeSSHFactory()
  685. factory.protocol = makeProtocol
  686. factory.buildProtocol(None)
  687. self.assertEqual([()], calls)
  688. def test_buildProtocolNoPrimes(self):
  689. """
  690. Group key exchanges are not supported when we don't have the primes
  691. database.
  692. """
  693. f1 = self.makeSSHFactory(primes=None)
  694. p1 = f1.buildProtocol(None)
  695. self.assertNotIn(
  696. b'diffie-hellman-group-exchange-sha1', p1.supportedKeyExchanges)
  697. self.assertNotIn(
  698. b'diffie-hellman-group-exchange-sha256', p1.supportedKeyExchanges)
  699. def test_buildProtocolWithPrimes(self):
  700. """
  701. Group key exchanges are supported when we have the primes database.
  702. """
  703. f2 = self.makeSSHFactory(primes={1:(2,3)})
  704. p2 = f2.buildProtocol(None)
  705. self.assertIn(
  706. b'diffie-hellman-group-exchange-sha1', p2.supportedKeyExchanges)
  707. self.assertIn(
  708. b'diffie-hellman-group-exchange-sha256', p2.supportedKeyExchanges)
  709. class MPTests(unittest.TestCase):
  710. """
  711. Tests for L{common.getMP}.
  712. @cvar getMP: a method providing a MP parser.
  713. @type getMP: C{callable}
  714. """
  715. getMP = staticmethod(common.getMP)
  716. if not cryptography:
  717. skip = "can't run without cryptography"
  718. if not pyasn1:
  719. skip = "Cannot run without PyASN1"
  720. def test_getMP(self):
  721. """
  722. L{common.getMP} should parse the a multiple precision integer from a
  723. string: a 4-byte length followed by length bytes of the integer.
  724. """
  725. self.assertEqual(
  726. self.getMP(b'\x00\x00\x00\x04\x00\x00\x00\x01'),
  727. (1, b''))
  728. def test_getMPBigInteger(self):
  729. """
  730. L{common.getMP} should be able to parse a big enough integer
  731. (that doesn't fit on one byte).
  732. """
  733. self.assertEqual(
  734. self.getMP(b'\x00\x00\x00\x04\x01\x02\x03\x04'),
  735. (16909060, b''))
  736. def test_multipleGetMP(self):
  737. """
  738. L{common.getMP} has the ability to parse multiple integer in the same
  739. string.
  740. """
  741. self.assertEqual(
  742. self.getMP(b'\x00\x00\x00\x04\x00\x00\x00\x01'
  743. b'\x00\x00\x00\x04\x00\x00\x00\x02', 2),
  744. (1, 2, b''))
  745. def test_getMPRemainingData(self):
  746. """
  747. When more data than needed is sent to L{common.getMP}, it should return
  748. the remaining data.
  749. """
  750. self.assertEqual(
  751. self.getMP(b'\x00\x00\x00\x04\x00\x00\x00\x01foo'),
  752. (1, b'foo'))
  753. def test_notEnoughData(self):
  754. """
  755. When the string passed to L{common.getMP} doesn't even make 5 bytes,
  756. it should raise a L{struct.error}.
  757. """
  758. self.assertRaises(struct.error, self.getMP, b'\x02\x00')
  759. class GMPYInstallDeprecationTests(unittest.TestCase):
  760. """
  761. Tests for the deprecation of former GMPY accidental public API.
  762. """
  763. def test_deprecated(self):
  764. """
  765. L{twisted.conch.ssh.common.install} is deprecated.
  766. """
  767. common.install()
  768. warnings = self.flushWarnings([self.test_deprecated])
  769. self.assertEqual(len(warnings), 1)
  770. self.assertEqual(
  771. warnings[0]["message"],
  772. "twisted.conch.ssh.common.install was deprecated in Twisted 16.5.0"
  773. )