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.

conch.py 18KB

1 year ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580
  1. # -*- test-case-name: twisted.conch.test.test_conch -*-
  2. #
  3. # Copyright (c) Twisted Matrix Laboratories.
  4. # See LICENSE for details.
  5. #
  6. # $Id: conch.py,v 1.65 2004/03/11 00:29:14 z3p Exp $
  7. # Implementation module for the `conch` command.
  8. #
  9. import fcntl
  10. import getpass
  11. import os
  12. import signal
  13. import struct
  14. import sys
  15. import tty
  16. from typing import List, Tuple
  17. from twisted.conch.client import connect, default
  18. from twisted.conch.client.options import ConchOptions
  19. from twisted.conch.error import ConchError
  20. from twisted.conch.ssh import channel, common, connection, forwarding, session
  21. from twisted.internet import reactor, stdio, task
  22. from twisted.python import log, usage
  23. from twisted.python.compat import ioType, networkString
  24. class ClientOptions(ConchOptions):
  25. synopsis = """Usage: conch [options] host [command]
  26. """
  27. longdesc = (
  28. "conch is a SSHv2 client that allows logging into a remote "
  29. "machine and executing commands."
  30. )
  31. optParameters = [
  32. ["escape", "e", "~"],
  33. [
  34. "localforward",
  35. "L",
  36. None,
  37. "listen-port:host:port Forward local port to remote address",
  38. ],
  39. [
  40. "remoteforward",
  41. "R",
  42. None,
  43. "listen-port:host:port Forward remote port to local address",
  44. ],
  45. ]
  46. optFlags = [
  47. ["null", "n", "Redirect input from /dev/null."],
  48. ["fork", "f", "Fork to background after authentication."],
  49. ["tty", "t", "Tty; allocate a tty even if command is given."],
  50. ["notty", "T", "Do not allocate a tty."],
  51. ["noshell", "N", "Do not execute a shell or command."],
  52. ["subsystem", "s", "Invoke command (mandatory) as SSH2 subsystem."],
  53. ]
  54. compData = usage.Completions(
  55. mutuallyExclusive=[("tty", "notty")],
  56. optActions={
  57. "localforward": usage.Completer(descr="listen-port:host:port"),
  58. "remoteforward": usage.Completer(descr="listen-port:host:port"),
  59. },
  60. extraActions=[
  61. usage.CompleteUserAtHost(),
  62. usage.Completer(descr="command"),
  63. usage.Completer(descr="argument", repeat=True),
  64. ],
  65. )
  66. localForwards: List[Tuple[int, Tuple[int, int]]] = []
  67. remoteForwards: List[Tuple[int, Tuple[int, int]]] = []
  68. def opt_escape(self, esc):
  69. """
  70. Set escape character; ``none'' = disable
  71. """
  72. if esc == "none":
  73. self["escape"] = None
  74. elif esc[0] == "^" and len(esc) == 2:
  75. self["escape"] = chr(ord(esc[1]) - 64)
  76. elif len(esc) == 1:
  77. self["escape"] = esc
  78. else:
  79. sys.exit(f"Bad escape character '{esc}'.")
  80. def opt_localforward(self, f):
  81. """
  82. Forward local port to remote address (lport:host:port)
  83. """
  84. localPort, remoteHost, remotePort = f.split(":") # Doesn't do v6 yet
  85. localPort = int(localPort)
  86. remotePort = int(remotePort)
  87. self.localForwards.append((localPort, (remoteHost, remotePort)))
  88. def opt_remoteforward(self, f):
  89. """
  90. Forward remote port to local address (rport:host:port)
  91. """
  92. remotePort, connHost, connPort = f.split(":") # Doesn't do v6 yet
  93. remotePort = int(remotePort)
  94. connPort = int(connPort)
  95. self.remoteForwards.append((remotePort, (connHost, connPort)))
  96. def parseArgs(self, host, *command):
  97. self["host"] = host
  98. self["command"] = " ".join(command)
  99. # Rest of code in "run"
  100. options = None
  101. conn = None
  102. exitStatus = 0
  103. old = None
  104. _inRawMode = 0
  105. _savedRawMode = None
  106. def run():
  107. global options, old
  108. args = sys.argv[1:]
  109. if "-l" in args: # CVS is an idiot
  110. i = args.index("-l")
  111. args = args[i : i + 2] + args
  112. del args[i + 2 : i + 4]
  113. for arg in args[:]:
  114. try:
  115. i = args.index(arg)
  116. if arg[:2] == "-o" and args[i + 1][0] != "-":
  117. args[i : i + 2] = [] # Suck on it scp
  118. except ValueError:
  119. pass
  120. options = ClientOptions()
  121. try:
  122. options.parseOptions(args)
  123. except usage.UsageError as u:
  124. print(f"ERROR: {u}")
  125. options.opt_help()
  126. sys.exit(1)
  127. if options["log"]:
  128. if options["logfile"]:
  129. if options["logfile"] == "-":
  130. f = sys.stdout
  131. else:
  132. f = open(options["logfile"], "a+")
  133. else:
  134. f = sys.stderr
  135. realout = sys.stdout
  136. log.startLogging(f)
  137. sys.stdout = realout
  138. else:
  139. log.discardLogs()
  140. doConnect()
  141. fd = sys.stdin.fileno()
  142. try:
  143. old = tty.tcgetattr(fd)
  144. except BaseException:
  145. old = None
  146. try:
  147. oldUSR1 = signal.signal(
  148. signal.SIGUSR1, lambda *a: reactor.callLater(0, reConnect)
  149. )
  150. except BaseException:
  151. oldUSR1 = None
  152. try:
  153. reactor.run()
  154. finally:
  155. if old:
  156. tty.tcsetattr(fd, tty.TCSANOW, old)
  157. if oldUSR1:
  158. signal.signal(signal.SIGUSR1, oldUSR1)
  159. if (options["command"] and options["tty"]) or not options["notty"]:
  160. signal.signal(signal.SIGWINCH, signal.SIG_DFL)
  161. if sys.stdout.isatty() and not options["command"]:
  162. print("Connection to {} closed.".format(options["host"]))
  163. sys.exit(exitStatus)
  164. def handleError():
  165. from twisted.python import failure
  166. global exitStatus
  167. exitStatus = 2
  168. reactor.callLater(0.01, _stopReactor)
  169. log.err(failure.Failure())
  170. raise
  171. def _stopReactor():
  172. try:
  173. reactor.stop()
  174. except BaseException:
  175. pass
  176. def doConnect():
  177. if "@" in options["host"]:
  178. options["user"], options["host"] = options["host"].split("@", 1)
  179. if not options.identitys:
  180. options.identitys = ["~/.ssh/id_rsa", "~/.ssh/id_dsa"]
  181. host = options["host"]
  182. if not options["user"]:
  183. options["user"] = getpass.getuser()
  184. if not options["port"]:
  185. options["port"] = 22
  186. else:
  187. options["port"] = int(options["port"])
  188. host = options["host"]
  189. port = options["port"]
  190. vhk = default.verifyHostKey
  191. if not options["host-key-algorithms"]:
  192. options["host-key-algorithms"] = default.getHostKeyAlgorithms(host, options)
  193. uao = default.SSHUserAuthClient(options["user"], options, SSHConnection())
  194. connect.connect(host, port, options, vhk, uao).addErrback(_ebExit)
  195. def _ebExit(f):
  196. global exitStatus
  197. exitStatus = f"conch: exiting with error {f}"
  198. reactor.callLater(0.1, _stopReactor)
  199. def onConnect():
  200. # if keyAgent and options['agent']:
  201. # cc = protocol.ClientCreator(reactor, SSHAgentForwardingLocal, conn)
  202. # cc.connectUNIX(os.environ['SSH_AUTH_SOCK'])
  203. if hasattr(conn.transport, "sendIgnore"):
  204. _KeepAlive(conn)
  205. if options.localForwards:
  206. for localPort, hostport in options.localForwards:
  207. s = reactor.listenTCP(
  208. localPort,
  209. forwarding.SSHListenForwardingFactory(
  210. conn, hostport, SSHListenClientForwardingChannel
  211. ),
  212. )
  213. conn.localForwards.append(s)
  214. if options.remoteForwards:
  215. for remotePort, hostport in options.remoteForwards:
  216. log.msg(f"asking for remote forwarding for {remotePort}:{hostport}")
  217. conn.requestRemoteForwarding(remotePort, hostport)
  218. reactor.addSystemEventTrigger("before", "shutdown", beforeShutdown)
  219. if not options["noshell"] or options["agent"]:
  220. conn.openChannel(SSHSession())
  221. if options["fork"]:
  222. if os.fork():
  223. os._exit(0)
  224. os.setsid()
  225. for i in range(3):
  226. try:
  227. os.close(i)
  228. except OSError as e:
  229. import errno
  230. if e.errno != errno.EBADF:
  231. raise
  232. def reConnect():
  233. beforeShutdown()
  234. conn.transport.transport.loseConnection()
  235. def beforeShutdown():
  236. remoteForwards = options.remoteForwards
  237. for remotePort, hostport in remoteForwards:
  238. log.msg(f"cancelling {remotePort}:{hostport}")
  239. conn.cancelRemoteForwarding(remotePort)
  240. def stopConnection():
  241. if not options["reconnect"]:
  242. reactor.callLater(0.1, _stopReactor)
  243. class _KeepAlive:
  244. def __init__(self, conn):
  245. self.conn = conn
  246. self.globalTimeout = None
  247. self.lc = task.LoopingCall(self.sendGlobal)
  248. self.lc.start(300)
  249. def sendGlobal(self):
  250. d = self.conn.sendGlobalRequest(
  251. b"conch-keep-alive@twistedmatrix.com", b"", wantReply=1
  252. )
  253. d.addBoth(self._cbGlobal)
  254. self.globalTimeout = reactor.callLater(30, self._ebGlobal)
  255. def _cbGlobal(self, res):
  256. if self.globalTimeout:
  257. self.globalTimeout.cancel()
  258. self.globalTimeout = None
  259. def _ebGlobal(self):
  260. if self.globalTimeout:
  261. self.globalTimeout = None
  262. self.conn.transport.loseConnection()
  263. class SSHConnection(connection.SSHConnection):
  264. def serviceStarted(self):
  265. global conn
  266. conn = self
  267. self.localForwards = []
  268. self.remoteForwards = {}
  269. onConnect()
  270. def serviceStopped(self):
  271. lf = self.localForwards
  272. self.localForwards = []
  273. for s in lf:
  274. s.loseConnection()
  275. stopConnection()
  276. def requestRemoteForwarding(self, remotePort, hostport):
  277. data = forwarding.packGlobal_tcpip_forward(("0.0.0.0", remotePort))
  278. d = self.sendGlobalRequest(b"tcpip-forward", data, wantReply=1)
  279. log.msg(f"requesting remote forwarding {remotePort}:{hostport}")
  280. d.addCallback(self._cbRemoteForwarding, remotePort, hostport)
  281. d.addErrback(self._ebRemoteForwarding, remotePort, hostport)
  282. def _cbRemoteForwarding(self, result, remotePort, hostport):
  283. log.msg(f"accepted remote forwarding {remotePort}:{hostport}")
  284. self.remoteForwards[remotePort] = hostport
  285. log.msg(repr(self.remoteForwards))
  286. def _ebRemoteForwarding(self, f, remotePort, hostport):
  287. log.msg(f"remote forwarding {remotePort}:{hostport} failed")
  288. log.msg(f)
  289. def cancelRemoteForwarding(self, remotePort):
  290. data = forwarding.packGlobal_tcpip_forward(("0.0.0.0", remotePort))
  291. self.sendGlobalRequest(b"cancel-tcpip-forward", data)
  292. log.msg(f"cancelling remote forwarding {remotePort}")
  293. try:
  294. del self.remoteForwards[remotePort]
  295. except Exception:
  296. pass
  297. log.msg(repr(self.remoteForwards))
  298. def channel_forwarded_tcpip(self, windowSize, maxPacket, data):
  299. log.msg(f"FTCP {data!r}")
  300. remoteHP, origHP = forwarding.unpackOpen_forwarded_tcpip(data)
  301. log.msg(self.remoteForwards)
  302. log.msg(remoteHP)
  303. if remoteHP[1] in self.remoteForwards:
  304. connectHP = self.remoteForwards[remoteHP[1]]
  305. log.msg(f"connect forwarding {connectHP}")
  306. return SSHConnectForwardingChannel(
  307. connectHP, remoteWindow=windowSize, remoteMaxPacket=maxPacket, conn=self
  308. )
  309. else:
  310. raise ConchError(
  311. connection.OPEN_CONNECT_FAILED, "don't know about that port"
  312. )
  313. def channelClosed(self, channel):
  314. log.msg(f"connection closing {channel}")
  315. log.msg(self.channels)
  316. if len(self.channels) == 1: # Just us left
  317. log.msg("stopping connection")
  318. stopConnection()
  319. else:
  320. # Because of the unix thing
  321. self.__class__.__bases__[0].channelClosed(self, channel)
  322. class SSHSession(channel.SSHChannel):
  323. name = b"session"
  324. def channelOpen(self, foo):
  325. log.msg(f"session {self.id} open")
  326. if options["agent"]:
  327. d = self.conn.sendRequest(
  328. self, b"auth-agent-req@openssh.com", b"", wantReply=1
  329. )
  330. d.addBoth(lambda x: log.msg(x))
  331. if options["noshell"]:
  332. return
  333. if (options["command"] and options["tty"]) or not options["notty"]:
  334. _enterRawMode()
  335. c = session.SSHSessionClient()
  336. if options["escape"] and not options["notty"]:
  337. self.escapeMode = 1
  338. c.dataReceived = self.handleInput
  339. else:
  340. c.dataReceived = self.write
  341. c.connectionLost = lambda x: self.sendEOF()
  342. self.stdio = stdio.StandardIO(c)
  343. fd = 0
  344. if options["subsystem"]:
  345. self.conn.sendRequest(self, b"subsystem", common.NS(options["command"]))
  346. elif options["command"]:
  347. if options["tty"]:
  348. term = os.environ["TERM"]
  349. winsz = fcntl.ioctl(fd, tty.TIOCGWINSZ, "12345678")
  350. winSize = struct.unpack("4H", winsz)
  351. ptyReqData = session.packRequest_pty_req(term, winSize, "")
  352. self.conn.sendRequest(self, b"pty-req", ptyReqData)
  353. signal.signal(signal.SIGWINCH, self._windowResized)
  354. self.conn.sendRequest(self, b"exec", common.NS(options["command"]))
  355. else:
  356. if not options["notty"]:
  357. term = os.environ["TERM"]
  358. winsz = fcntl.ioctl(fd, tty.TIOCGWINSZ, "12345678")
  359. winSize = struct.unpack("4H", winsz)
  360. ptyReqData = session.packRequest_pty_req(term, winSize, "")
  361. self.conn.sendRequest(self, b"pty-req", ptyReqData)
  362. signal.signal(signal.SIGWINCH, self._windowResized)
  363. self.conn.sendRequest(self, b"shell", b"")
  364. # if hasattr(conn.transport, 'transport'):
  365. # conn.transport.transport.setTcpNoDelay(1)
  366. def handleInput(self, char):
  367. if char in (b"\n", b"\r"):
  368. self.escapeMode = 1
  369. self.write(char)
  370. elif self.escapeMode == 1 and char == options["escape"]:
  371. self.escapeMode = 2
  372. elif self.escapeMode == 2:
  373. self.escapeMode = 1 # So we can chain escapes together
  374. if char == b".": # Disconnect
  375. log.msg("disconnecting from escape")
  376. stopConnection()
  377. return
  378. elif char == b"\x1a": # ^Z, suspend
  379. def _():
  380. _leaveRawMode()
  381. sys.stdout.flush()
  382. sys.stdin.flush()
  383. os.kill(os.getpid(), signal.SIGTSTP)
  384. _enterRawMode()
  385. reactor.callLater(0, _)
  386. return
  387. elif char == b"R": # Rekey connection
  388. log.msg("rekeying connection")
  389. self.conn.transport.sendKexInit()
  390. return
  391. elif char == b"#": # Display connections
  392. self.stdio.write(b"\r\nThe following connections are open:\r\n")
  393. channels = self.conn.channels.keys()
  394. channels.sort()
  395. for channelId in channels:
  396. self.stdio.write(
  397. networkString(
  398. " #{} {}\r\n".format(
  399. channelId, self.conn.channels[channelId]
  400. )
  401. )
  402. )
  403. return
  404. self.write(b"~" + char)
  405. else:
  406. self.escapeMode = 0
  407. self.write(char)
  408. def dataReceived(self, data):
  409. self.stdio.write(data)
  410. def extReceived(self, t, data):
  411. if t == connection.EXTENDED_DATA_STDERR:
  412. log.msg(f"got {len(data)} stderr data")
  413. if ioType(sys.stderr) == str:
  414. sys.stderr.buffer.write(data)
  415. else:
  416. sys.stderr.write(data)
  417. def eofReceived(self):
  418. log.msg("got eof")
  419. self.stdio.loseWriteConnection()
  420. def closeReceived(self):
  421. log.msg(f"remote side closed {self}")
  422. self.conn.sendClose(self)
  423. def closed(self):
  424. global old
  425. log.msg(f"closed {self}")
  426. log.msg(repr(self.conn.channels))
  427. def request_exit_status(self, data):
  428. global exitStatus
  429. exitStatus = int(struct.unpack(">L", data)[0])
  430. log.msg(f"exit status: {exitStatus}")
  431. def sendEOF(self):
  432. self.conn.sendEOF(self)
  433. def stopWriting(self):
  434. self.stdio.pauseProducing()
  435. def startWriting(self):
  436. self.stdio.resumeProducing()
  437. def _windowResized(self, *args):
  438. winsz = fcntl.ioctl(0, tty.TIOCGWINSZ, "12345678")
  439. winSize = struct.unpack("4H", winsz)
  440. newSize = winSize[1], winSize[0], winSize[2], winSize[3]
  441. self.conn.sendRequest(self, b"window-change", struct.pack("!4L", *newSize))
  442. class SSHListenClientForwardingChannel(forwarding.SSHListenClientForwardingChannel):
  443. pass
  444. class SSHConnectForwardingChannel(forwarding.SSHConnectForwardingChannel):
  445. pass
  446. def _leaveRawMode():
  447. global _inRawMode
  448. if not _inRawMode:
  449. return
  450. fd = sys.stdin.fileno()
  451. tty.tcsetattr(fd, tty.TCSANOW, _savedRawMode)
  452. _inRawMode = 0
  453. def _enterRawMode():
  454. global _inRawMode, _savedRawMode
  455. if _inRawMode:
  456. return
  457. fd = sys.stdin.fileno()
  458. try:
  459. old = tty.tcgetattr(fd)
  460. new = old[:]
  461. except BaseException:
  462. log.msg("not a typewriter!")
  463. else:
  464. # iflage
  465. new[0] = new[0] | tty.IGNPAR
  466. new[0] = new[0] & ~(
  467. tty.ISTRIP
  468. | tty.INLCR
  469. | tty.IGNCR
  470. | tty.ICRNL
  471. | tty.IXON
  472. | tty.IXANY
  473. | tty.IXOFF
  474. )
  475. if hasattr(tty, "IUCLC"):
  476. new[0] = new[0] & ~tty.IUCLC
  477. # lflag
  478. new[3] = new[3] & ~(
  479. tty.ISIG
  480. | tty.ICANON
  481. | tty.ECHO
  482. | tty.ECHO
  483. | tty.ECHOE
  484. | tty.ECHOK
  485. | tty.ECHONL
  486. )
  487. if hasattr(tty, "IEXTEN"):
  488. new[3] = new[3] & ~tty.IEXTEN
  489. # oflag
  490. new[1] = new[1] & ~tty.OPOST
  491. new[6][tty.VMIN] = 1
  492. new[6][tty.VTIME] = 0
  493. _savedRawMode = old
  494. tty.tcsetattr(fd, tty.TCSANOW, new)
  495. # tty.setraw(fd)
  496. _inRawMode = 1
  497. if __name__ == "__main__":
  498. run()