default.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349
  1. # -*- test-case-name: twisted.conch.test.test_knownhosts,twisted.conch.test.test_default -*-
  2. # Copyright (c) Twisted Matrix Laboratories.
  3. # See LICENSE for details.
  4. """
  5. Various classes and functions for implementing user-interaction in the
  6. command-line conch client.
  7. You probably shouldn't use anything in this module directly, since it assumes
  8. you are sitting at an interactive terminal. For example, to programmatically
  9. interact with a known_hosts database, use L{twisted.conch.client.knownhosts}.
  10. """
  11. from __future__ import print_function
  12. from twisted.python import log
  13. from twisted.python.compat import (
  14. nativeString, raw_input, _PY3, _b64decodebytes as decodebytes)
  15. from twisted.python.filepath import FilePath
  16. from twisted.conch.error import ConchError
  17. from twisted.conch.ssh import common, keys, userauth
  18. from twisted.internet import defer, protocol, reactor
  19. from twisted.conch.client.knownhosts import KnownHostsFile, ConsoleUI
  20. from twisted.conch.client import agent
  21. import os, sys, getpass, contextlib
  22. if _PY3:
  23. import io
  24. # The default location of the known hosts file (probably should be parsed out
  25. # of an ssh config file someday).
  26. _KNOWN_HOSTS = "~/.ssh/known_hosts"
  27. # This name is bound so that the unit tests can use 'patch' to override it.
  28. _open = open
  29. def verifyHostKey(transport, host, pubKey, fingerprint):
  30. """
  31. Verify a host's key.
  32. This function is a gross vestige of some bad factoring in the client
  33. internals. The actual implementation, and a better signature of this logic
  34. is in L{KnownHostsFile.verifyHostKey}. This function is not deprecated yet
  35. because the callers have not yet been rehabilitated, but they should
  36. eventually be changed to call that method instead.
  37. However, this function does perform two functions not implemented by
  38. L{KnownHostsFile.verifyHostKey}. It determines the path to the user's
  39. known_hosts file based on the options (which should really be the options
  40. object's job), and it provides an opener to L{ConsoleUI} which opens
  41. '/dev/tty' so that the user will be prompted on the tty of the process even
  42. if the input and output of the process has been redirected. This latter
  43. part is, somewhat obviously, not portable, but I don't know of a portable
  44. equivalent that could be used.
  45. @param host: Due to a bug in L{SSHClientTransport.verifyHostKey}, this is
  46. always the dotted-quad IP address of the host being connected to.
  47. @type host: L{str}
  48. @param transport: the client transport which is attempting to connect to
  49. the given host.
  50. @type transport: L{SSHClientTransport}
  51. @param fingerprint: the fingerprint of the given public key, in
  52. xx:xx:xx:... format. This is ignored in favor of getting the fingerprint
  53. from the key itself.
  54. @type fingerprint: L{str}
  55. @param pubKey: The public key of the server being connected to.
  56. @type pubKey: L{str}
  57. @return: a L{Deferred} which fires with C{1} if the key was successfully
  58. verified, or fails if the key could not be successfully verified. Failure
  59. types may include L{HostKeyChanged}, L{UserRejectedKey}, L{IOError} or
  60. L{KeyboardInterrupt}.
  61. """
  62. actualHost = transport.factory.options['host']
  63. actualKey = keys.Key.fromString(pubKey)
  64. kh = KnownHostsFile.fromPath(FilePath(
  65. transport.factory.options['known-hosts']
  66. or os.path.expanduser(_KNOWN_HOSTS)
  67. ))
  68. ui = ConsoleUI(lambda : _open("/dev/tty", "r+b", buffering=0))
  69. return kh.verifyHostKey(ui, actualHost, host, actualKey)
  70. def isInKnownHosts(host, pubKey, options):
  71. """
  72. Checks to see if host is in the known_hosts file for the user.
  73. @return: 0 if it isn't, 1 if it is and is the same, 2 if it's changed.
  74. @rtype: L{int}
  75. """
  76. keyType = common.getNS(pubKey)[0]
  77. retVal = 0
  78. if not options['known-hosts'] and not os.path.exists(os.path.expanduser('~/.ssh/')):
  79. print('Creating ~/.ssh directory...')
  80. os.mkdir(os.path.expanduser('~/.ssh'))
  81. kh_file = options['known-hosts'] or _KNOWN_HOSTS
  82. try:
  83. known_hosts = open(os.path.expanduser(kh_file), 'rb')
  84. except IOError:
  85. return 0
  86. with known_hosts:
  87. for line in known_hosts.readlines():
  88. split = line.split()
  89. if len(split) < 3:
  90. continue
  91. hosts, hostKeyType, encodedKey = split[:3]
  92. if host not in hosts.split(b','): # incorrect host
  93. continue
  94. if hostKeyType != keyType: # incorrect type of key
  95. continue
  96. try:
  97. decodedKey = decodebytes(encodedKey)
  98. except:
  99. continue
  100. if decodedKey == pubKey:
  101. return 1
  102. else:
  103. retVal = 2
  104. return retVal
  105. def getHostKeyAlgorithms(host, options):
  106. """
  107. Look in known_hosts for a key corresponding to C{host}.
  108. This can be used to change the order of supported key types
  109. in the KEXINIT packet.
  110. @type host: L{str}
  111. @param host: the host to check in known_hosts
  112. @type options: L{twisted.conch.client.options.ConchOptions}
  113. @param options: options passed to client
  114. @return: L{list} of L{str} representing key types or L{None}.
  115. """
  116. knownHosts = KnownHostsFile.fromPath(FilePath(
  117. options['known-hosts']
  118. or os.path.expanduser(_KNOWN_HOSTS)
  119. ))
  120. keyTypes = []
  121. for entry in knownHosts.iterentries():
  122. if entry.matchesHost(host):
  123. if entry.keyType not in keyTypes:
  124. keyTypes.append(entry.keyType)
  125. return keyTypes or None
  126. class SSHUserAuthClient(userauth.SSHUserAuthClient):
  127. def __init__(self, user, options, *args):
  128. userauth.SSHUserAuthClient.__init__(self, user, *args)
  129. self.keyAgent = None
  130. self.options = options
  131. self.usedFiles = []
  132. if not options.identitys:
  133. options.identitys = ['~/.ssh/id_rsa', '~/.ssh/id_dsa']
  134. def serviceStarted(self):
  135. if 'SSH_AUTH_SOCK' in os.environ and not self.options['noagent']:
  136. log.msg('using agent')
  137. cc = protocol.ClientCreator(reactor, agent.SSHAgentClient)
  138. d = cc.connectUNIX(os.environ['SSH_AUTH_SOCK'])
  139. d.addCallback(self._setAgent)
  140. d.addErrback(self._ebSetAgent)
  141. else:
  142. userauth.SSHUserAuthClient.serviceStarted(self)
  143. def serviceStopped(self):
  144. if self.keyAgent:
  145. self.keyAgent.transport.loseConnection()
  146. self.keyAgent = None
  147. def _setAgent(self, a):
  148. self.keyAgent = a
  149. d = self.keyAgent.getPublicKeys()
  150. d.addBoth(self._ebSetAgent)
  151. return d
  152. def _ebSetAgent(self, f):
  153. userauth.SSHUserAuthClient.serviceStarted(self)
  154. def _getPassword(self, prompt):
  155. """
  156. Prompt for a password using L{getpass.getpass}.
  157. @param prompt: Written on tty to ask for the input.
  158. @type prompt: L{str}
  159. @return: The input.
  160. @rtype: L{str}
  161. """
  162. with self._replaceStdoutStdin():
  163. try:
  164. p = getpass.getpass(prompt)
  165. return p
  166. except (KeyboardInterrupt, IOError):
  167. print()
  168. raise ConchError('PEBKAC')
  169. def getPassword(self, prompt = None):
  170. if prompt:
  171. prompt = nativeString(prompt)
  172. else:
  173. prompt = ("%s@%s's password: " %
  174. (nativeString(self.user), self.transport.transport.getPeer().host))
  175. try:
  176. # We don't know the encoding the other side is using,
  177. # signaling that is not part of the SSH protocol. But
  178. # using our defaultencoding is better than just going for
  179. # ASCII.
  180. p = self._getPassword(prompt).encode(sys.getdefaultencoding())
  181. return defer.succeed(p)
  182. except ConchError:
  183. return defer.fail()
  184. def getPublicKey(self):
  185. """
  186. Get a public key from the key agent if possible, otherwise look in
  187. the next configured identity file for one.
  188. """
  189. if self.keyAgent:
  190. key = self.keyAgent.getPublicKey()
  191. if key is not None:
  192. return key
  193. files = [x for x in self.options.identitys if x not in self.usedFiles]
  194. log.msg(str(self.options.identitys))
  195. log.msg(str(files))
  196. if not files:
  197. return None
  198. file = files[0]
  199. log.msg(file)
  200. self.usedFiles.append(file)
  201. file = os.path.expanduser(file)
  202. file += '.pub'
  203. if not os.path.exists(file):
  204. return self.getPublicKey() # try again
  205. try:
  206. return keys.Key.fromFile(file)
  207. except keys.BadKeyError:
  208. return self.getPublicKey() # try again
  209. def signData(self, publicKey, signData):
  210. """
  211. Extend the base signing behavior by using an SSH agent to sign the
  212. data, if one is available.
  213. @type publicKey: L{Key}
  214. @type signData: L{bytes}
  215. """
  216. if not self.usedFiles: # agent key
  217. return self.keyAgent.signData(publicKey.blob(), signData)
  218. else:
  219. return userauth.SSHUserAuthClient.signData(self, publicKey, signData)
  220. def getPrivateKey(self):
  221. """
  222. Try to load the private key from the last used file identified by
  223. C{getPublicKey}, potentially asking for the passphrase if the key is
  224. encrypted.
  225. """
  226. file = os.path.expanduser(self.usedFiles[-1])
  227. if not os.path.exists(file):
  228. return None
  229. try:
  230. return defer.succeed(keys.Key.fromFile(file))
  231. except keys.EncryptedKeyError:
  232. for i in range(3):
  233. prompt = "Enter passphrase for key '%s': " % self.usedFiles[-1]
  234. try:
  235. p = self._getPassword(prompt).encode(
  236. sys.getfilesystemencoding())
  237. return defer.succeed(keys.Key.fromFile(file, passphrase=p))
  238. except (keys.BadKeyError, ConchError):
  239. pass
  240. return defer.fail(ConchError('bad password'))
  241. raise
  242. except KeyboardInterrupt:
  243. print()
  244. reactor.stop()
  245. def getGenericAnswers(self, name, instruction, prompts):
  246. responses = []
  247. with self._replaceStdoutStdin():
  248. if name:
  249. print(name.decode("utf-8"))
  250. if instruction:
  251. print(instruction.decode("utf-8"))
  252. for prompt, echo in prompts:
  253. prompt = prompt.decode("utf-8")
  254. if echo:
  255. responses.append(raw_input(prompt))
  256. else:
  257. responses.append(getpass.getpass(prompt))
  258. return defer.succeed(responses)
  259. @classmethod
  260. def _openTty(cls):
  261. """
  262. Open /dev/tty as two streams one in read, one in write mode,
  263. and return them.
  264. @return: File objects for reading and writing to /dev/tty,
  265. corresponding to standard input and standard output.
  266. @rtype: A L{tuple} of L{io.TextIOWrapper} on Python 3.
  267. A L{tuple} of binary files on Python 2.
  268. """
  269. stdin = open("/dev/tty", "rb")
  270. stdout = open("/dev/tty", "wb")
  271. if _PY3:
  272. stdin = io.TextIOWrapper(stdin)
  273. stdout = io.TextIOWrapper(stdout)
  274. return stdin, stdout
  275. @classmethod
  276. @contextlib.contextmanager
  277. def _replaceStdoutStdin(cls):
  278. """
  279. Contextmanager that replaces stdout and stdin with /dev/tty
  280. and resets them when it is done.
  281. """
  282. oldout, oldin = sys.stdout, sys.stdin
  283. sys.stdin, sys.stdout = cls._openTty()
  284. try:
  285. yield
  286. finally:
  287. sys.stdout.close()
  288. sys.stdin.close()
  289. sys.stdout, sys.stdin = oldout, oldin