sshtunnel.py 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286
  1. # -*- coding: utf-8 -*-
  2. #
  3. # AWL simulator - SSH tunnel helper
  4. #
  5. # Copyright 2016-2017 Michael Buesch <m@bues.ch>
  6. #
  7. # This program is free software; you can redistribute it and/or modify
  8. # it under the terms of the GNU General Public License as published by
  9. # the Free Software Foundation; either version 2 of the License, or
  10. # (at your option) any later version.
  11. #
  12. # This program is distributed in the hope that it will be useful,
  13. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. # GNU General Public License for more details.
  16. #
  17. # You should have received a copy of the GNU General Public License along
  18. # with this program; if not, write to the Free Software Foundation, Inc.,
  19. # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
  20. #
  21. from __future__ import division, absolute_import, print_function, unicode_literals
  22. #from awlsim.common.cython_support cimport * #@cy
  23. from awlsim.common.compat import *
  24. from awlsim.common.exceptions import *
  25. from awlsim.common.net import *
  26. from awlsim.common.env import *
  27. from awlsim.common.util import *
  28. from awlsim.common.subprocess_wrapper import *
  29. from awlsim.common.monotonic import * #+cimport
  30. if not osIsWindows:
  31. import pty
  32. import os
  33. import select
  34. import signal
  35. import time
  36. class SSHTunnel(object):
  37. """SSH tunnel helper.
  38. """
  39. SSH_DEFAULT_USER = "pi"
  40. SSH_PORT = 22
  41. SSH_LOCAL_PORT_START = 4151 + 10
  42. SSH_LOCAL_PORT_END = SSH_LOCAL_PORT_START + 4096
  43. SSH_DEFAULT_EXECUTABLE = "ssh"
  44. def __init__(self, remoteHost, remotePort,
  45. sshUser=SSH_DEFAULT_USER,
  46. localPort=None,
  47. sshExecutable=SSH_DEFAULT_EXECUTABLE,
  48. sshPort=SSH_PORT):
  49. """Create an SSH tunnel.
  50. """
  51. if osIsWindows:
  52. # win magic: translate "ssh" to "plink".
  53. if sshExecutable == "ssh":
  54. sshExecutable = "plink.exe"
  55. self.remoteHost = remoteHost
  56. self.remotePort = remotePort
  57. self.sshUser = sshUser
  58. self.localPort = localPort
  59. self.sshExecutable = sshExecutable
  60. self.sshPort = sshPort
  61. self.__sshPid = None
  62. self.__sshProc = None
  63. def connect(self, timeout=10.0):
  64. """Establish the SSH tunnel.
  65. """
  66. localPort = self.localPort
  67. if localPort is None:
  68. localPort = self.SSH_LOCAL_PORT_START
  69. while not netPortIsUnused("localhost", localPort):
  70. localPort += 1
  71. if localPort > self.SSH_LOCAL_PORT_END:
  72. raise AwlSimError("Failed to find an "
  73. "unused local port for the "
  74. "SSH tunnel.")
  75. actualLocalPort = localPort
  76. self.sshMessage("Establishing SSH tunnel to '%s@%s'...\n" %(
  77. self.sshUser, self.remoteHost),
  78. isDebug=False)
  79. self.__sshPid = None
  80. try:
  81. # Prepare SSH environment and arguments.
  82. env = AwlSimEnv.clearLang(AwlSimEnv.getEnv())
  83. if osIsWindows and "plink" in self.sshExecutable.lower():
  84. # Run plink.exe (PuTTY)
  85. pw = self.getPassphrase("%s's Password:" % self.remoteHost)
  86. argv = [ self.sshExecutable,
  87. "-ssh",
  88. "-pw", None,
  89. "-P", "%d" % self.sshPort,
  90. "-l", self.sshUser,
  91. "-L", "localhost:%d:localhost:%d" % (
  92. localPort, self.remotePort),
  93. "-N",
  94. "-x",
  95. "-v",
  96. self.remoteHost, ]
  97. pwArgIdx = 2
  98. if pw is None:
  99. del argv[pwArgIdx : pwArgIdx + 2]
  100. pwArgIdx = None
  101. else:
  102. argv[pwArgIdx + 1] = pw.decode("UTF-8")
  103. else:
  104. # Run OpenSSH
  105. argv = [ self.sshExecutable,
  106. "-p", "%d" % self.sshPort,
  107. "-l", self.sshUser,
  108. "-L", "localhost:%d:localhost:%d" % (
  109. localPort, self.remotePort),
  110. "-N",
  111. "-x",
  112. "-v",
  113. self.remoteHost, ]
  114. pwArgIdx = None
  115. printArgv = argv[:]
  116. if pwArgIdx is not None:
  117. printArgv[pwArgIdx + 1] = "*" * len(printArgv[pwArgIdx + 1])
  118. self.sshMessage("Running command:\n %s\n" % " ".join(printArgv),
  119. isDebug=False)
  120. if osIsWindows:
  121. # Start SSH tunnel as subprocess.
  122. proc = PopenWrapper(argv, env=env, stdio=True)
  123. self.__sshProc = proc
  124. self.sshMessage("Starting %s..." % argv[0],
  125. isDebug=False)
  126. self.sleep(1.0)
  127. proc.stdin.write(b"n\n") # Do not cache host auth.
  128. proc.stdin.flush()
  129. for i in range(3):
  130. self.sshMessage(".", isDebug=False)
  131. self.sleep(1.0)
  132. if self.__sshProc.returncode is not None:
  133. raise AwlSimError("%s exited with "
  134. "error." % argv[0])
  135. else:
  136. # Create a PTY and fork.
  137. childPid, ptyMasterFd = pty.fork()
  138. if childPid == pty.CHILD:
  139. # Run SSH
  140. execargs = argv + [env]
  141. os.execlpe(argv[0], *execargs)
  142. assert(0) # unreachable
  143. self.__sshPid = childPid
  144. self.__handshake(ptyMasterFd, timeout)
  145. except (OSError, ValueError, IOError) as e:
  146. with suppressAllExc:
  147. self.shutdown()
  148. raise AwlSimError("Failed to execute SSH to "
  149. "establish SSH tunnel:\n%s" %\
  150. str(e))
  151. except KeyboardInterrupt as e:
  152. with suppressAllExc:
  153. self.shutdown()
  154. raise AwlSimError("Interrupted by user.")
  155. return "localhost", actualLocalPort
  156. def shutdown(self):
  157. if self.__sshProc:
  158. try:
  159. with suppressAllExc:
  160. self.__sshProc.terminate()
  161. finally:
  162. self.__sshProc = None
  163. if self.__sshPid is not None:
  164. try:
  165. with suppressAllExc:
  166. os.kill(self.__sshPid, signal.SIGTERM)
  167. finally:
  168. self.__sshPid = None
  169. @staticmethod
  170. def __read(fd):
  171. data = []
  172. while True:
  173. rfds, wfds, xfds = select.select([fd], [], [], 0)
  174. if fd not in rfds:
  175. break
  176. d = os.read(fd, 1024)
  177. if not d:
  178. break
  179. data.append(d)
  180. return b''.join(data)
  181. @staticmethod
  182. def __write(fd, data):
  183. while data:
  184. count = os.write(fd, data)
  185. data = data[count:]
  186. PROMPT_PW = "'s Password:"
  187. PROMPT_AUTH = "The authenticity of host "
  188. PROMPT_YESNO = " (yes/no)?"
  189. AUTH_FINISH = "Authenticated to "
  190. def __handshake(self, ptyMasterFd, timeout):
  191. timeoutEnd = monotonic_time() + (timeout or 0)
  192. sentPw, authReq, finished = False, [], False
  193. while not finished:
  194. self.sleep(0.1)
  195. if timeout and monotonic_time() >= timeoutEnd:
  196. raise AwlSimError("Timeout establishing SSH tunnel.")
  197. fromSsh = self.__read(ptyMasterFd)
  198. try:
  199. fromSsh = fromSsh.decode("UTF-8", "ignore")
  200. except UnicodeError:
  201. fromSsh = ""
  202. for line in fromSsh.splitlines():
  203. if not line:
  204. continue
  205. lineLow = line.lower()
  206. isDebug = lineLow.strip().startswith("debug")
  207. self.sshMessage(line, isDebug)
  208. if isDebug:
  209. continue
  210. if authReq:
  211. authReq.append(line)
  212. if self.PROMPT_PW.lower() in lineLow:
  213. if sentPw:
  214. # Second try.
  215. raise AwlSimError("SSH tunnel passphrase "
  216. "was not accepted.")
  217. passphrase = self.getPassphrase(line)
  218. if passphrase is None:
  219. raise AwlSimError("SSH tunnel connection "
  220. "requires a passphrase, but "
  221. "no passphrase was given.")
  222. self.__write(ptyMasterFd, passphrase)
  223. if not passphrase.endswith(b"\n"):
  224. self.__write(ptyMasterFd, b"\n")
  225. sentPw = True
  226. timeoutEnd = monotonic_time() + (timeout or 0)
  227. continue
  228. if self.PROMPT_AUTH.lower() in lineLow:
  229. authReq.append(line)
  230. continue
  231. if self.PROMPT_YESNO.lower() in lineLow and authReq:
  232. ok = self.hostAuth("\n".join(authReq))
  233. if not ok:
  234. raise AwlSimError("SSH tunnel host "
  235. "authentication failed.")
  236. self.__write(ptyMasterFd, b"yes\n")
  237. authReq = []
  238. timeoutEnd = monotonic_time() + (timeout or 0)
  239. continue
  240. if self.AUTH_FINISH.lower() in lineLow:
  241. # Successfully authenticated.
  242. finished = True
  243. continue
  244. def sleep(self, seconds):
  245. """Sleep for a number of seconds.
  246. """
  247. time.sleep(seconds)
  248. def sshMessage(self, message, isDebug):
  249. """Print a SSH log message.
  250. """
  251. if not isDebug:
  252. printInfo("[SSH]: %s" % message)
  253. def getPassphrase(self, prompt):
  254. """Get a password from the user.
  255. """
  256. try:
  257. return input(prompt).encode("UTF-8", "ignore")
  258. except UnicodeError:
  259. return b""
  260. def hostAuth(self, prompt):
  261. """Get the user answer to the host authentication question.
  262. This function returns a boolean.
  263. """
  264. return str2bool(input(prompt))