endpoints.py 28 KB

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