Funktionierender Prototyp des Serious Games zur Vermittlung von Wissen zu Software-Engineering-Arbeitsmodellen.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

endpoints.py 29KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875
  1. # -*- test-case-name: twisted.conch.test.test_endpoints -*-
  2. # Copyright (c) Twisted Matrix Laboratories.
  3. # See LICENSE for details.
  4. """
  5. Endpoint implementations of various SSH interactions.
  6. """
  7. __all__ = ["AuthenticationFailed", "SSHCommandAddress", "SSHCommandClientEndpoint"]
  8. import signal
  9. from os.path import expanduser
  10. from struct import unpack
  11. from zope.interface import Interface, implementer
  12. from twisted.conch.client.agent import SSHAgentClient
  13. from twisted.conch.client.default import _KNOWN_HOSTS
  14. from twisted.conch.client.knownhosts import ConsoleUI, KnownHostsFile
  15. from twisted.conch.ssh.channel import SSHChannel
  16. from twisted.conch.ssh.common import NS, getNS
  17. from twisted.conch.ssh.connection import SSHConnection
  18. from twisted.conch.ssh.keys import Key
  19. from twisted.conch.ssh.transport import SSHClientTransport
  20. from twisted.conch.ssh.userauth import SSHUserAuthClient
  21. from twisted.internet.defer import CancelledError, Deferred, succeed
  22. from twisted.internet.endpoints import TCP4ClientEndpoint, connectProtocol
  23. from twisted.internet.error import ConnectionDone, ProcessTerminated
  24. from twisted.internet.interfaces import IStreamClientEndpoint
  25. from twisted.internet.protocol import Factory
  26. from twisted.logger import Logger
  27. from twisted.python.compat import nativeString, networkString
  28. from twisted.python.failure import Failure
  29. from twisted.python.filepath import FilePath
  30. class AuthenticationFailed(Exception):
  31. """
  32. An SSH session could not be established because authentication was not
  33. successful.
  34. """
  35. # This should be public. See #6541.
  36. class _ISSHConnectionCreator(Interface):
  37. """
  38. An L{_ISSHConnectionCreator} knows how to create SSH connections somehow.
  39. """
  40. def secureConnection():
  41. """
  42. Return a new, connected, secured, but not yet authenticated instance of
  43. L{twisted.conch.ssh.transport.SSHServerTransport} or
  44. L{twisted.conch.ssh.transport.SSHClientTransport}.
  45. """
  46. def cleanupConnection(connection, immediate):
  47. """
  48. Perform cleanup necessary for a connection object previously returned
  49. from this creator's C{secureConnection} method.
  50. @param connection: An L{twisted.conch.ssh.transport.SSHServerTransport}
  51. or L{twisted.conch.ssh.transport.SSHClientTransport} returned by a
  52. previous call to C{secureConnection}. It is no longer needed by
  53. the caller of that method and may be closed or otherwise cleaned up
  54. as necessary.
  55. @param immediate: If C{True} don't wait for any network communication,
  56. just close the connection immediately and as aggressively as
  57. necessary.
  58. """
  59. class SSHCommandAddress:
  60. """
  61. An L{SSHCommandAddress} instance represents the address of an SSH server, a
  62. username which was used to authenticate with that server, and a command
  63. which was run there.
  64. @ivar server: See L{__init__}
  65. @ivar username: See L{__init__}
  66. @ivar command: See L{__init__}
  67. """
  68. def __init__(self, server, username, command):
  69. """
  70. @param server: The address of the SSH server on which the command is
  71. running.
  72. @type server: L{IAddress} provider
  73. @param username: An authentication username which was used to
  74. authenticate against the server at the given address.
  75. @type username: L{bytes}
  76. @param command: A command which was run in a session channel on the
  77. server at the given address.
  78. @type command: L{bytes}
  79. """
  80. self.server = server
  81. self.username = username
  82. self.command = command
  83. class _CommandChannel(SSHChannel):
  84. """
  85. A L{_CommandChannel} executes a command in a session channel and connects
  86. its input and output to an L{IProtocol} provider.
  87. @ivar _creator: See L{__init__}
  88. @ivar _command: See L{__init__}
  89. @ivar _protocolFactory: See L{__init__}
  90. @ivar _commandConnected: See L{__init__}
  91. @ivar _protocol: An L{IProtocol} provider created using C{_protocolFactory}
  92. which is hooked up to the running command's input and output streams.
  93. """
  94. name = b"session"
  95. _log = Logger()
  96. def __init__(self, creator, command, protocolFactory, commandConnected):
  97. """
  98. @param creator: The L{_ISSHConnectionCreator} provider which was used
  99. to get the connection which this channel exists on.
  100. @type creator: L{_ISSHConnectionCreator} provider
  101. @param command: The command to be executed.
  102. @type command: L{bytes}
  103. @param protocolFactory: A client factory to use to build a L{IProtocol}
  104. provider to use to associate with the running command.
  105. @param commandConnected: A L{Deferred} to use to signal that execution
  106. of the command has failed or that it has succeeded and the command
  107. is now running.
  108. @type commandConnected: L{Deferred}
  109. """
  110. SSHChannel.__init__(self)
  111. self._creator = creator
  112. self._command = command
  113. self._protocolFactory = protocolFactory
  114. self._commandConnected = commandConnected
  115. self._reason = None
  116. def openFailed(self, reason):
  117. """
  118. When the request to open a new channel to run this command in fails,
  119. fire the C{commandConnected} deferred with a failure indicating that.
  120. """
  121. self._commandConnected.errback(reason)
  122. def channelOpen(self, ignored):
  123. """
  124. When the request to open a new channel to run this command in succeeds,
  125. issue an C{"exec"} request to run the command.
  126. """
  127. command = self.conn.sendRequest(
  128. self, b"exec", NS(self._command), wantReply=True
  129. )
  130. command.addCallbacks(self._execSuccess, self._execFailure)
  131. def _execFailure(self, reason):
  132. """
  133. When the request to execute the command in this channel fails, fire the
  134. C{commandConnected} deferred with a failure indicating this.
  135. @param reason: The cause of the command execution failure.
  136. @type reason: L{Failure}
  137. """
  138. self._commandConnected.errback(reason)
  139. def _execSuccess(self, ignored):
  140. """
  141. When the request to execute the command in this channel succeeds, use
  142. C{protocolFactory} to build a protocol to handle the command's input
  143. and output and connect the protocol to a transport representing those
  144. streams.
  145. Also fire C{commandConnected} with the created protocol after it is
  146. connected to its transport.
  147. @param ignored: The (ignored) result of the execute request
  148. """
  149. self._protocol = self._protocolFactory.buildProtocol(
  150. SSHCommandAddress(
  151. self.conn.transport.transport.getPeer(),
  152. self.conn.transport.creator.username,
  153. self.conn.transport.creator.command,
  154. )
  155. )
  156. self._protocol.makeConnection(self)
  157. self._commandConnected.callback(self._protocol)
  158. def dataReceived(self, data):
  159. """
  160. When the command's stdout data arrives over the channel, deliver it to
  161. the protocol instance.
  162. @param data: The bytes from the command's stdout.
  163. @type data: L{bytes}
  164. """
  165. self._protocol.dataReceived(data)
  166. def request_exit_status(self, data):
  167. """
  168. When the server sends the command's exit status, record it for later
  169. delivery to the protocol.
  170. @param data: The network-order four byte representation of the exit
  171. status of the command.
  172. @type data: L{bytes}
  173. """
  174. (status,) = unpack(">L", data)
  175. if status != 0:
  176. self._reason = ProcessTerminated(status, None, None)
  177. def request_exit_signal(self, data):
  178. """
  179. When the server sends the command's exit status, record it for later
  180. delivery to the protocol.
  181. @param data: The network-order four byte representation of the exit
  182. signal of the command.
  183. @type data: L{bytes}
  184. """
  185. shortSignalName, data = getNS(data)
  186. coreDumped, data = bool(ord(data[0:1])), data[1:]
  187. errorMessage, data = getNS(data)
  188. languageTag, data = getNS(data)
  189. signalName = f"SIG{nativeString(shortSignalName)}"
  190. signalID = getattr(signal, signalName, -1)
  191. self._log.info(
  192. "Process exited with signal {shortSignalName!r};"
  193. " core dumped: {coreDumped};"
  194. " error message: {errorMessage};"
  195. " language: {languageTag!r}",
  196. shortSignalName=shortSignalName,
  197. coreDumped=coreDumped,
  198. errorMessage=errorMessage.decode("utf-8"),
  199. languageTag=languageTag,
  200. )
  201. self._reason = ProcessTerminated(None, signalID, None)
  202. def closed(self):
  203. """
  204. When the channel closes, deliver disconnection notification to the
  205. protocol.
  206. """
  207. self._creator.cleanupConnection(self.conn, False)
  208. if self._reason is None:
  209. reason = ConnectionDone("ssh channel closed")
  210. else:
  211. reason = self._reason
  212. self._protocol.connectionLost(Failure(reason))
  213. class _ConnectionReady(SSHConnection):
  214. """
  215. L{_ConnectionReady} is an L{SSHConnection} (an SSH service) which only
  216. propagates the I{serviceStarted} event to a L{Deferred} to be handled
  217. elsewhere.
  218. """
  219. def __init__(self, ready):
  220. """
  221. @param ready: A L{Deferred} which should be fired when
  222. I{serviceStarted} happens.
  223. """
  224. SSHConnection.__init__(self)
  225. self._ready = ready
  226. def serviceStarted(self):
  227. """
  228. When the SSH I{connection} I{service} this object represents is ready
  229. to be used, fire the C{connectionReady} L{Deferred} to publish that
  230. event to some other interested party.
  231. """
  232. self._ready.callback(self)
  233. del self._ready
  234. class _UserAuth(SSHUserAuthClient):
  235. """
  236. L{_UserAuth} implements the client part of SSH user authentication in the
  237. convenient way a user might expect if they are familiar with the
  238. interactive I{ssh} command line client.
  239. L{_UserAuth} supports key-based authentication, password-based
  240. authentication, and delegating authentication to an agent.
  241. """
  242. password = None
  243. keys = None
  244. agent = None
  245. def getPublicKey(self):
  246. """
  247. Retrieve the next public key object to offer to the server, possibly
  248. delegating to an authentication agent if there is one.
  249. @return: The public part of a key pair that could be used to
  250. authenticate with the server, or L{None} if there are no more
  251. public keys to try.
  252. @rtype: L{twisted.conch.ssh.keys.Key} or L{None}
  253. """
  254. if self.agent is not None:
  255. return self.agent.getPublicKey()
  256. if self.keys:
  257. self.key = self.keys.pop(0)
  258. else:
  259. self.key = None
  260. return self.key.public()
  261. def signData(self, publicKey, signData):
  262. """
  263. Extend the base signing behavior by using an SSH agent to sign the
  264. data, if one is available.
  265. @type publicKey: L{Key}
  266. @type signData: L{str}
  267. """
  268. if self.agent is not None:
  269. return self.agent.signData(publicKey.blob(), signData)
  270. else:
  271. return SSHUserAuthClient.signData(self, publicKey, signData)
  272. def getPrivateKey(self):
  273. """
  274. Get the private part of a key pair to use for authentication. The key
  275. corresponds to the public part most recently returned from
  276. C{getPublicKey}.
  277. @return: A L{Deferred} which fires with the private key.
  278. @rtype: L{Deferred}
  279. """
  280. return succeed(self.key)
  281. def getPassword(self):
  282. """
  283. Get the password to use for authentication.
  284. @return: A L{Deferred} which fires with the password, or L{None} if the
  285. password was not specified.
  286. """
  287. if self.password is None:
  288. return
  289. return succeed(self.password)
  290. def ssh_USERAUTH_SUCCESS(self, packet):
  291. """
  292. Handle user authentication success in the normal way, but also make a
  293. note of the state change on the L{_CommandTransport}.
  294. """
  295. self.transport._state = b"CHANNELLING"
  296. return SSHUserAuthClient.ssh_USERAUTH_SUCCESS(self, packet)
  297. def connectToAgent(self, endpoint):
  298. """
  299. Set up a connection to the authentication agent and trigger its
  300. initialization.
  301. @param endpoint: An endpoint which can be used to connect to the
  302. authentication agent.
  303. @type endpoint: L{IStreamClientEndpoint} provider
  304. @return: A L{Deferred} which fires when the agent connection is ready
  305. for use.
  306. """
  307. factory = Factory()
  308. factory.protocol = SSHAgentClient
  309. d = endpoint.connect(factory)
  310. def connected(agent):
  311. self.agent = agent
  312. return agent.getPublicKeys()
  313. d.addCallback(connected)
  314. return d
  315. def loseAgentConnection(self):
  316. """
  317. Disconnect the agent.
  318. """
  319. if self.agent is None:
  320. return
  321. self.agent.transport.loseConnection()
  322. class _CommandTransport(SSHClientTransport):
  323. """
  324. L{_CommandTransport} is an SSH client I{transport} which includes a host
  325. key verification step before it will proceed to secure the connection.
  326. L{_CommandTransport} also knows how to set up a connection to an
  327. authentication agent if it is told where it can connect to one.
  328. @ivar _userauth: The L{_UserAuth} instance which is in charge of the
  329. overall authentication process or L{None} if the SSH connection has not
  330. reach yet the C{user-auth} service.
  331. @type _userauth: L{_UserAuth}
  332. """
  333. # STARTING -> SECURING -> AUTHENTICATING -> CHANNELLING -> RUNNING
  334. _state = b"STARTING"
  335. _hostKeyFailure = None
  336. _userauth = None
  337. def __init__(self, creator):
  338. """
  339. @param creator: The L{_NewConnectionHelper} that created this
  340. connection.
  341. @type creator: L{_NewConnectionHelper}.
  342. """
  343. self.connectionReady = Deferred(lambda d: self.transport.abortConnection())
  344. # Clear the reference to that deferred to help the garbage collector
  345. # and to signal to other parts of this implementation (in particular
  346. # connectionLost) that it has already been fired and does not need to
  347. # be fired again.
  348. def readyFired(result):
  349. self.connectionReady = None
  350. return result
  351. self.connectionReady.addBoth(readyFired)
  352. self.creator = creator
  353. def verifyHostKey(self, hostKey, fingerprint):
  354. """
  355. Ask the L{KnownHostsFile} provider available on the factory which
  356. created this protocol this protocol to verify the given host key.
  357. @return: A L{Deferred} which fires with the result of
  358. L{KnownHostsFile.verifyHostKey}.
  359. """
  360. hostname = self.creator.hostname
  361. ip = networkString(self.transport.getPeer().host)
  362. self._state = b"SECURING"
  363. d = self.creator.knownHosts.verifyHostKey(
  364. self.creator.ui, hostname, ip, Key.fromString(hostKey)
  365. )
  366. d.addErrback(self._saveHostKeyFailure)
  367. return d
  368. def _saveHostKeyFailure(self, reason):
  369. """
  370. When host key verification fails, record the reason for the failure in
  371. order to fire a L{Deferred} with it later.
  372. @param reason: The cause of the host key verification failure.
  373. @type reason: L{Failure}
  374. @return: C{reason}
  375. @rtype: L{Failure}
  376. """
  377. self._hostKeyFailure = reason
  378. return reason
  379. def connectionSecure(self):
  380. """
  381. When the connection is secure, start the authentication process.
  382. """
  383. self._state = b"AUTHENTICATING"
  384. command = _ConnectionReady(self.connectionReady)
  385. self._userauth = _UserAuth(self.creator.username, command)
  386. self._userauth.password = self.creator.password
  387. if self.creator.keys:
  388. self._userauth.keys = list(self.creator.keys)
  389. if self.creator.agentEndpoint is not None:
  390. d = self._userauth.connectToAgent(self.creator.agentEndpoint)
  391. else:
  392. d = succeed(None)
  393. def maybeGotAgent(ignored):
  394. self.requestService(self._userauth)
  395. d.addBoth(maybeGotAgent)
  396. def connectionLost(self, reason):
  397. """
  398. When the underlying connection to the SSH server is lost, if there were
  399. any connection setup errors, propagate them. Also, clean up the
  400. connection to the ssh agent if one was created.
  401. """
  402. if self._userauth:
  403. self._userauth.loseAgentConnection()
  404. if self._state == b"RUNNING" or self.connectionReady is None:
  405. return
  406. if self._state == b"SECURING" and self._hostKeyFailure is not None:
  407. reason = self._hostKeyFailure
  408. elif self._state == b"AUTHENTICATING":
  409. reason = Failure(
  410. AuthenticationFailed("Connection lost while authenticating")
  411. )
  412. self.connectionReady.errback(reason)
  413. @implementer(IStreamClientEndpoint)
  414. class SSHCommandClientEndpoint:
  415. """
  416. L{SSHCommandClientEndpoint} exposes the command-executing functionality of
  417. SSH servers.
  418. L{SSHCommandClientEndpoint} can set up a new SSH connection, authenticate
  419. it in any one of a number of different ways (keys, passwords, agents),
  420. launch a command over that connection and then associate its input and
  421. output with a protocol.
  422. It can also re-use an existing, already-authenticated SSH connection
  423. (perhaps one which already has some SSH channels being used for other
  424. purposes). In this case it creates a new SSH channel to use to execute the
  425. command. Notably this means it supports multiplexing several different
  426. command invocations over a single SSH connection.
  427. """
  428. def __init__(self, creator, command):
  429. """
  430. @param creator: An L{_ISSHConnectionCreator} provider which will be
  431. used to set up the SSH connection which will be used to run a
  432. command.
  433. @type creator: L{_ISSHConnectionCreator} provider
  434. @param command: The command line to execute on the SSH server. This
  435. byte string is interpreted by a shell on the SSH server, so it may
  436. have a value like C{"ls /"}. Take care when trying to run a
  437. command like C{"/Volumes/My Stuff/a-program"} - spaces (and other
  438. special bytes) may require escaping.
  439. @type command: L{bytes}
  440. """
  441. self._creator = creator
  442. self._command = command
  443. @classmethod
  444. def newConnection(
  445. cls,
  446. reactor,
  447. command,
  448. username,
  449. hostname,
  450. port=None,
  451. keys=None,
  452. password=None,
  453. agentEndpoint=None,
  454. knownHosts=None,
  455. ui=None,
  456. ):
  457. """
  458. Create and return a new endpoint which will try to create a new
  459. connection to an SSH server and run a command over it. It will also
  460. close the connection if there are problems leading up to the command
  461. being executed, after the command finishes, or if the connection
  462. L{Deferred} is cancelled.
  463. @param reactor: The reactor to use to establish the connection.
  464. @type reactor: L{IReactorTCP} provider
  465. @param command: See L{__init__}'s C{command} argument.
  466. @param username: The username with which to authenticate to the SSH
  467. server.
  468. @type username: L{bytes}
  469. @param hostname: The hostname of the SSH server.
  470. @type hostname: L{bytes}
  471. @param port: The port number of the SSH server. By default, the
  472. standard SSH port number is used.
  473. @type port: L{int}
  474. @param keys: Private keys with which to authenticate to the SSH server,
  475. if key authentication is to be attempted (otherwise L{None}).
  476. @type keys: L{list} of L{Key}
  477. @param password: The password with which to authenticate to the SSH
  478. server, if password authentication is to be attempted (otherwise
  479. L{None}).
  480. @type password: L{bytes} or L{None}
  481. @param agentEndpoint: An L{IStreamClientEndpoint} provider which may be
  482. used to connect to an SSH agent, if one is to be used to help with
  483. authentication.
  484. @type agentEndpoint: L{IStreamClientEndpoint} provider
  485. @param knownHosts: The currently known host keys, used to check the
  486. host key presented by the server we actually connect to.
  487. @type knownHosts: L{KnownHostsFile}
  488. @param ui: An object for interacting with users to make decisions about
  489. whether to accept the server host keys. If L{None}, a L{ConsoleUI}
  490. connected to /dev/tty will be used; if /dev/tty is unavailable, an
  491. object which answers C{b"no"} to all prompts will be used.
  492. @type ui: L{None} or L{ConsoleUI}
  493. @return: A new instance of C{cls} (probably
  494. L{SSHCommandClientEndpoint}).
  495. """
  496. helper = _NewConnectionHelper(
  497. reactor,
  498. hostname,
  499. port,
  500. command,
  501. username,
  502. keys,
  503. password,
  504. agentEndpoint,
  505. knownHosts,
  506. ui,
  507. )
  508. return cls(helper, command)
  509. @classmethod
  510. def existingConnection(cls, connection, command):
  511. """
  512. Create and return a new endpoint which will try to open a new channel
  513. on an existing SSH connection and run a command over it. It will
  514. B{not} close the connection if there is a problem executing the command
  515. or after the command finishes.
  516. @param connection: An existing connection to an SSH server.
  517. @type connection: L{SSHConnection}
  518. @param command: See L{SSHCommandClientEndpoint.newConnection}'s
  519. C{command} parameter.
  520. @type command: L{bytes}
  521. @return: A new instance of C{cls} (probably
  522. L{SSHCommandClientEndpoint}).
  523. """
  524. helper = _ExistingConnectionHelper(connection)
  525. return cls(helper, command)
  526. def connect(self, protocolFactory):
  527. """
  528. Set up an SSH connection, use a channel from that connection to launch
  529. a command, and hook the stdin and stdout of that command up as a
  530. transport for a protocol created by the given factory.
  531. @param protocolFactory: A L{Factory} to use to create the protocol
  532. which will be connected to the stdin and stdout of the command on
  533. the SSH server.
  534. @return: A L{Deferred} which will fire with an error if the connection
  535. cannot be set up for any reason or with the protocol instance
  536. created by C{protocolFactory} once it has been connected to the
  537. command.
  538. """
  539. d = self._creator.secureConnection()
  540. d.addCallback(self._executeCommand, protocolFactory)
  541. return d
  542. def _executeCommand(self, connection, protocolFactory):
  543. """
  544. Given a secured SSH connection, try to execute a command in a new
  545. channel created on it and associate the result with a protocol from the
  546. given factory.
  547. @param connection: See L{SSHCommandClientEndpoint.existingConnection}'s
  548. C{connection} parameter.
  549. @param protocolFactory: See L{SSHCommandClientEndpoint.connect}'s
  550. C{protocolFactory} parameter.
  551. @return: See L{SSHCommandClientEndpoint.connect}'s return value.
  552. """
  553. commandConnected = Deferred()
  554. def disconnectOnFailure(passthrough):
  555. # Close the connection immediately in case of cancellation, since
  556. # that implies user wants it gone immediately (e.g. a timeout):
  557. immediate = passthrough.check(CancelledError)
  558. self._creator.cleanupConnection(connection, immediate)
  559. return passthrough
  560. commandConnected.addErrback(disconnectOnFailure)
  561. channel = _CommandChannel(
  562. self._creator, self._command, protocolFactory, commandConnected
  563. )
  564. connection.openChannel(channel)
  565. return commandConnected
  566. class _ReadFile:
  567. """
  568. A weakly file-like object which can be used with L{KnownHostsFile} to
  569. respond in the negative to all prompts for decisions.
  570. """
  571. def __init__(self, contents):
  572. """
  573. @param contents: L{bytes} which will be returned from every C{readline}
  574. call.
  575. """
  576. self._contents = contents
  577. def write(self, data):
  578. """
  579. No-op.
  580. @param data: ignored
  581. """
  582. def readline(self, count=-1):
  583. """
  584. Always give back the byte string that this L{_ReadFile} was initialized
  585. with.
  586. @param count: ignored
  587. @return: A fixed byte-string.
  588. @rtype: L{bytes}
  589. """
  590. return self._contents
  591. def close(self):
  592. """
  593. No-op.
  594. """
  595. @implementer(_ISSHConnectionCreator)
  596. class _NewConnectionHelper:
  597. """
  598. L{_NewConnectionHelper} implements L{_ISSHConnectionCreator} by
  599. establishing a brand new SSH connection, securing it, and authenticating.
  600. """
  601. _KNOWN_HOSTS = _KNOWN_HOSTS
  602. port = 22
  603. def __init__(
  604. self,
  605. reactor,
  606. hostname,
  607. port,
  608. command,
  609. username,
  610. keys,
  611. password,
  612. agentEndpoint,
  613. knownHosts,
  614. ui,
  615. tty=FilePath(b"/dev/tty"),
  616. ):
  617. """
  618. @param tty: The path of the tty device to use in case C{ui} is L{None}.
  619. @type tty: L{FilePath}
  620. @see: L{SSHCommandClientEndpoint.newConnection}
  621. """
  622. self.reactor = reactor
  623. self.hostname = hostname
  624. if port is not None:
  625. self.port = port
  626. self.command = command
  627. self.username = username
  628. self.keys = keys
  629. self.password = password
  630. self.agentEndpoint = agentEndpoint
  631. if knownHosts is None:
  632. knownHosts = self._knownHosts()
  633. self.knownHosts = knownHosts
  634. if ui is None:
  635. ui = ConsoleUI(self._opener)
  636. self.ui = ui
  637. self.tty = tty
  638. def _opener(self):
  639. """
  640. Open the tty if possible, otherwise give back a file-like object from
  641. which C{b"no"} can be read.
  642. For use as the opener argument to L{ConsoleUI}.
  643. """
  644. try:
  645. return self.tty.open("rb+")
  646. except BaseException:
  647. # Give back a file-like object from which can be read a byte string
  648. # that KnownHostsFile recognizes as rejecting some option (b"no").
  649. return _ReadFile(b"no")
  650. @classmethod
  651. def _knownHosts(cls):
  652. """
  653. @return: A L{KnownHostsFile} instance pointed at the user's personal
  654. I{known hosts} file.
  655. @rtype: L{KnownHostsFile}
  656. """
  657. return KnownHostsFile.fromPath(FilePath(expanduser(cls._KNOWN_HOSTS)))
  658. def secureConnection(self):
  659. """
  660. Create and return a new SSH connection which has been secured and on
  661. which authentication has already happened.
  662. @return: A L{Deferred} which fires with the ready-to-use connection or
  663. with a failure if something prevents the connection from being
  664. setup, secured, or authenticated.
  665. """
  666. protocol = _CommandTransport(self)
  667. ready = protocol.connectionReady
  668. sshClient = TCP4ClientEndpoint(
  669. self.reactor, nativeString(self.hostname), self.port
  670. )
  671. d = connectProtocol(sshClient, protocol)
  672. d.addCallback(lambda ignored: ready)
  673. return d
  674. def cleanupConnection(self, connection, immediate):
  675. """
  676. Clean up the connection by closing it. The command running on the
  677. endpoint has ended so the connection is no longer needed.
  678. @param connection: The L{SSHConnection} to close.
  679. @type connection: L{SSHConnection}
  680. @param immediate: Whether to close connection immediately.
  681. @type immediate: L{bool}.
  682. """
  683. if immediate:
  684. # We're assuming the underlying connection is an ITCPTransport,
  685. # which is what the current implementation is restricted to:
  686. connection.transport.transport.abortConnection()
  687. else:
  688. connection.transport.loseConnection()
  689. @implementer(_ISSHConnectionCreator)
  690. class _ExistingConnectionHelper:
  691. """
  692. L{_ExistingConnectionHelper} implements L{_ISSHConnectionCreator} by
  693. handing out an existing SSH connection which is supplied to its
  694. initializer.
  695. """
  696. def __init__(self, connection):
  697. """
  698. @param connection: See L{SSHCommandClientEndpoint.existingConnection}'s
  699. C{connection} parameter.
  700. """
  701. self.connection = connection
  702. def secureConnection(self):
  703. """
  704. @return: A L{Deferred} that fires synchronously with the
  705. already-established connection object.
  706. """
  707. return succeed(self.connection)
  708. def cleanupConnection(self, connection, immediate):
  709. """
  710. Do not do any cleanup on the connection. Leave that responsibility to
  711. whatever code created it in the first place.
  712. @param connection: The L{SSHConnection} which will not be modified in
  713. any way.
  714. @type connection: L{SSHConnection}
  715. @param immediate: An argument which will be ignored.
  716. @type immediate: L{bool}.
  717. """