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

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