123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585 |
- # -*- test-case-name: twisted.conch.test.test_conch -*-
- #
- # Copyright (c) Twisted Matrix Laboratories.
- # See LICENSE for details.
-
- #
- # $Id: conch.py,v 1.65 2004/03/11 00:29:14 z3p Exp $
-
- #""" Implementation module for the `conch` command.
- #"""
- from __future__ import print_function
-
- from twisted.conch.client import connect, default, options
- from twisted.conch.error import ConchError
- from twisted.conch.ssh import connection, common
- from twisted.conch.ssh import session, forwarding, channel
- from twisted.internet import reactor, stdio, task
- from twisted.python import log, usage
- from twisted.python.compat import ioType, networkString, unicode
-
- import os
- import sys
- import getpass
- import struct
- import tty
- import fcntl
- import signal
-
-
-
- class ClientOptions(options.ConchOptions):
-
- synopsis = """Usage: conch [options] host [command]
- """
- longdesc = ("conch is a SSHv2 client that allows logging into a remote "
- "machine and executing commands.")
-
- optParameters = [['escape', 'e', '~'],
- ['localforward', 'L', None, 'listen-port:host:port Forward local port to remote address'],
- ['remoteforward', 'R', None, 'listen-port:host:port Forward remote port to local address'],
- ]
-
- optFlags = [['null', 'n', 'Redirect input from /dev/null.'],
- ['fork', 'f', 'Fork to background after authentication.'],
- ['tty', 't', 'Tty; allocate a tty even if command is given.'],
- ['notty', 'T', 'Do not allocate a tty.'],
- ['noshell', 'N', 'Do not execute a shell or command.'],
- ['subsystem', 's', 'Invoke command (mandatory) as SSH2 subsystem.'],
- ]
-
- compData = usage.Completions(
- mutuallyExclusive=[("tty", "notty")],
- optActions={
- "localforward": usage.Completer(descr="listen-port:host:port"),
- "remoteforward": usage.Completer(descr="listen-port:host:port")},
- extraActions=[usage.CompleteUserAtHost(),
- usage.Completer(descr="command"),
- usage.Completer(descr="argument", repeat=True)]
- )
-
- localForwards = []
- remoteForwards = []
-
- def opt_escape(self, esc):
- """
- Set escape character; ``none'' = disable
- """
- if esc == 'none':
- self['escape'] = None
- elif esc[0] == '^' and len(esc) == 2:
- self['escape'] = chr(ord(esc[1])-64)
- elif len(esc) == 1:
- self['escape'] = esc
- else:
- sys.exit("Bad escape character '{}'.".format(esc))
-
-
- def opt_localforward(self, f):
- """
- Forward local port to remote address (lport:host:port)
- """
- localPort, remoteHost, remotePort = f.split(':') # Doesn't do v6 yet
- localPort = int(localPort)
- remotePort = int(remotePort)
- self.localForwards.append((localPort, (remoteHost, remotePort)))
-
-
- def opt_remoteforward(self, f):
- """
- Forward remote port to local address (rport:host:port)
- """
- remotePort, connHost, connPort = f.split(':') # Doesn't do v6 yet
- remotePort = int(remotePort)
- connPort = int(connPort)
- self.remoteForwards.append((remotePort, (connHost, connPort)))
-
-
- def parseArgs(self, host, *command):
- self['host'] = host
- self['command'] = ' '.join(command)
-
-
-
- # Rest of code in "run"
- options = None
- conn = None
- exitStatus = 0
- old = None
- _inRawMode = 0
- _savedRawMode = None
-
-
-
- def run():
- global options, old
- args = sys.argv[1:]
- if '-l' in args: # CVS is an idiot
- i = args.index('-l')
- args = args[i:i+2]+args
- del args[i+2:i+4]
- for arg in args[:]:
- try:
- i = args.index(arg)
- if arg[:2] == '-o' and args[i+1][0] != '-':
- args[i:i+2] = [] # Suck on it scp
- except ValueError:
- pass
- options = ClientOptions()
- try:
- options.parseOptions(args)
- except usage.UsageError as u:
- print('ERROR: {}'.format(u))
- options.opt_help()
- sys.exit(1)
- if options['log']:
- if options['logfile']:
- if options['logfile'] == '-':
- f = sys.stdout
- else:
- f = open(options['logfile'], 'a+')
- else:
- f = sys.stderr
- realout = sys.stdout
- log.startLogging(f)
- sys.stdout = realout
- else:
- log.discardLogs()
- doConnect()
- fd = sys.stdin.fileno()
- try:
- old = tty.tcgetattr(fd)
- except:
- old = None
- try:
- oldUSR1 = signal.signal(signal.SIGUSR1, lambda *a: reactor.callLater(0, reConnect))
- except:
- oldUSR1 = None
- try:
- reactor.run()
- finally:
- if old:
- tty.tcsetattr(fd, tty.TCSANOW, old)
- if oldUSR1:
- signal.signal(signal.SIGUSR1, oldUSR1)
- if (options['command'] and options['tty']) or not options['notty']:
- signal.signal(signal.SIGWINCH, signal.SIG_DFL)
- if sys.stdout.isatty() and not options['command']:
- print('Connection to {} closed.'.format(options['host']))
- sys.exit(exitStatus)
-
-
-
- def handleError():
- from twisted.python import failure
- global exitStatus
- exitStatus = 2
- reactor.callLater(0.01, _stopReactor)
- log.err(failure.Failure())
- raise
-
-
-
- def _stopReactor():
- try:
- reactor.stop()
- except: pass
-
-
-
- def doConnect():
- if '@' in options['host']:
- options['user'], options['host'] = options['host'].split('@', 1)
- if not options.identitys:
- options.identitys = ['~/.ssh/id_rsa', '~/.ssh/id_dsa']
- host = options['host']
- if not options['user']:
- options['user'] = getpass.getuser()
- if not options['port']:
- options['port'] = 22
- else:
- options['port'] = int(options['port'])
- host = options['host']
- port = options['port']
- vhk = default.verifyHostKey
- if not options['host-key-algorithms']:
- options['host-key-algorithms'] = default.getHostKeyAlgorithms(
- host, options)
- uao = default.SSHUserAuthClient(options['user'], options, SSHConnection())
- connect.connect(host, port, options, vhk, uao).addErrback(_ebExit)
-
-
-
- def _ebExit(f):
- global exitStatus
- exitStatus = "conch: exiting with error {}".format(f)
- reactor.callLater(0.1, _stopReactor)
-
-
-
- def onConnect():
- # if keyAgent and options['agent']:
- # cc = protocol.ClientCreator(reactor, SSHAgentForwardingLocal, conn)
- # cc.connectUNIX(os.environ['SSH_AUTH_SOCK'])
- if hasattr(conn.transport, 'sendIgnore'):
- _KeepAlive(conn)
- if options.localForwards:
- for localPort, hostport in options.localForwards:
- s = reactor.listenTCP(localPort,
- forwarding.SSHListenForwardingFactory(conn,
- hostport,
- SSHListenClientForwardingChannel))
- conn.localForwards.append(s)
- if options.remoteForwards:
- for remotePort, hostport in options.remoteForwards:
- log.msg('asking for remote forwarding for {}:{}'.format(
- remotePort, hostport))
- conn.requestRemoteForwarding(remotePort, hostport)
- reactor.addSystemEventTrigger('before', 'shutdown', beforeShutdown)
- if not options['noshell'] or options['agent']:
- conn.openChannel(SSHSession())
- if options['fork']:
- if os.fork():
- os._exit(0)
- os.setsid()
- for i in range(3):
- try:
- os.close(i)
- except OSError as e:
- import errno
- if e.errno != errno.EBADF:
- raise
-
-
-
- def reConnect():
- beforeShutdown()
- conn.transport.transport.loseConnection()
-
-
-
- def beforeShutdown():
- remoteForwards = options.remoteForwards
- for remotePort, hostport in remoteForwards:
- log.msg('cancelling {}:{}'.format(remotePort, hostport))
- conn.cancelRemoteForwarding(remotePort)
-
-
-
- def stopConnection():
- if not options['reconnect']:
- reactor.callLater(0.1, _stopReactor)
-
-
-
- class _KeepAlive:
-
- def __init__(self, conn):
- self.conn = conn
- self.globalTimeout = None
- self.lc = task.LoopingCall(self.sendGlobal)
- self.lc.start(300)
-
-
- def sendGlobal(self):
- d = self.conn.sendGlobalRequest(b"conch-keep-alive@twistedmatrix.com",
- b"", wantReply=1)
- d.addBoth(self._cbGlobal)
- self.globalTimeout = reactor.callLater(30, self._ebGlobal)
-
-
- def _cbGlobal(self, res):
- if self.globalTimeout:
- self.globalTimeout.cancel()
- self.globalTimeout = None
-
-
- def _ebGlobal(self):
- if self.globalTimeout:
- self.globalTimeout = None
- self.conn.transport.loseConnection()
-
-
-
- class SSHConnection(connection.SSHConnection):
- def serviceStarted(self):
- global conn
- conn = self
- self.localForwards = []
- self.remoteForwards = {}
- if not isinstance(self, connection.SSHConnection):
- # make these fall through
- del self.__class__.requestRemoteForwarding
- del self.__class__.cancelRemoteForwarding
- onConnect()
-
-
- def serviceStopped(self):
- lf = self.localForwards
- self.localForwards = []
- for s in lf:
- s.loseConnection()
- stopConnection()
-
-
- def requestRemoteForwarding(self, remotePort, hostport):
- data = forwarding.packGlobal_tcpip_forward(('0.0.0.0', remotePort))
- d = self.sendGlobalRequest(b'tcpip-forward', data,
- wantReply=1)
- log.msg('requesting remote forwarding {}:{}'.format(
- remotePort, hostport))
- d.addCallback(self._cbRemoteForwarding, remotePort, hostport)
- d.addErrback(self._ebRemoteForwarding, remotePort, hostport)
-
-
- def _cbRemoteForwarding(self, result, remotePort, hostport):
- log.msg('accepted remote forwarding {}:{}'.format(
- remotePort, hostport))
- self.remoteForwards[remotePort] = hostport
- log.msg(repr(self.remoteForwards))
-
-
- def _ebRemoteForwarding(self, f, remotePort, hostport):
- log.msg('remote forwarding {}:{} failed'.format(
- remotePort, hostport))
- log.msg(f)
-
-
- def cancelRemoteForwarding(self, remotePort):
- data = forwarding.packGlobal_tcpip_forward(('0.0.0.0', remotePort))
- self.sendGlobalRequest(b'cancel-tcpip-forward', data)
- log.msg('cancelling remote forwarding {}'.format(remotePort))
- try:
- del self.remoteForwards[remotePort]
- except Exception:
- pass
- log.msg(repr(self.remoteForwards))
-
-
- def channel_forwarded_tcpip(self, windowSize, maxPacket, data):
- log.msg('FTCP {!r}'.format(data))
- remoteHP, origHP = forwarding.unpackOpen_forwarded_tcpip(data)
- log.msg(self.remoteForwards)
- log.msg(remoteHP)
- if remoteHP[1] in self.remoteForwards:
- connectHP = self.remoteForwards[remoteHP[1]]
- log.msg('connect forwarding {}'.format(connectHP))
- return SSHConnectForwardingChannel(connectHP,
- remoteWindow=windowSize,
- remoteMaxPacket=maxPacket,
- conn=self)
- else:
- raise ConchError(connection.OPEN_CONNECT_FAILED,
- "don't know about that port")
-
-
- def channelClosed(self, channel):
- log.msg('connection closing {}'.format(channel))
- log.msg(self.channels)
- if len(self.channels) == 1: # Just us left
- log.msg('stopping connection')
- stopConnection()
- else:
- # Because of the unix thing
- self.__class__.__bases__[0].channelClosed(self, channel)
-
-
-
- class SSHSession(channel.SSHChannel):
-
- name = b'session'
-
- def channelOpen(self, foo):
- log.msg('session {} open'.format(self.id))
- if options['agent']:
- d = self.conn.sendRequest(self, b'auth-agent-req@openssh.com',
- b'', wantReply=1)
- d.addBoth(lambda x: log.msg(x))
- if options['noshell']:
- return
- if (options['command'] and options['tty']) or not options['notty']:
- _enterRawMode()
- c = session.SSHSessionClient()
- if options['escape'] and not options['notty']:
- self.escapeMode = 1
- c.dataReceived = self.handleInput
- else:
- c.dataReceived = self.write
- c.connectionLost = lambda x: self.sendEOF()
- self.stdio = stdio.StandardIO(c)
- fd = 0
- if options['subsystem']:
- self.conn.sendRequest(self, b'subsystem',
- common.NS(options['command']))
- elif options['command']:
- if options['tty']:
- term = os.environ['TERM']
- winsz = fcntl.ioctl(fd, tty.TIOCGWINSZ, '12345678')
- winSize = struct.unpack('4H', winsz)
- ptyReqData = session.packRequest_pty_req(term, winSize, '')
- self.conn.sendRequest(self, b'pty-req', ptyReqData)
- signal.signal(signal.SIGWINCH, self._windowResized)
- self.conn.sendRequest(self, b'exec', common.NS(options['command']))
- else:
- if not options['notty']:
- term = os.environ['TERM']
- winsz = fcntl.ioctl(fd, tty.TIOCGWINSZ, '12345678')
- winSize = struct.unpack('4H', winsz)
- ptyReqData = session.packRequest_pty_req(term, winSize, '')
- self.conn.sendRequest(self, b'pty-req', ptyReqData)
- signal.signal(signal.SIGWINCH, self._windowResized)
- self.conn.sendRequest(self, b'shell', b'')
- #if hasattr(conn.transport, 'transport'):
- # conn.transport.transport.setTcpNoDelay(1)
-
-
- def handleInput(self, char):
- if char in (b'\n', b'\r'):
- self.escapeMode = 1
- self.write(char)
- elif self.escapeMode == 1 and char == options['escape']:
- self.escapeMode = 2
- elif self.escapeMode == 2:
- self.escapeMode = 1 # So we can chain escapes together
- if char == b'.': # Disconnect
- log.msg('disconnecting from escape')
- stopConnection()
- return
- elif char == b'\x1a': # ^Z, suspend
- def _():
- _leaveRawMode()
- sys.stdout.flush()
- sys.stdin.flush()
- os.kill(os.getpid(), signal.SIGTSTP)
- _enterRawMode()
- reactor.callLater(0, _)
- return
- elif char == b'R': # Rekey connection
- log.msg('rekeying connection')
- self.conn.transport.sendKexInit()
- return
- elif char == b'#': # Display connections
- self.stdio.write(
- b'\r\nThe following connections are open:\r\n')
- channels = self.conn.channels.keys()
- channels.sort()
- for channelId in channels:
- self.stdio.write(networkString(' #{} {}\r\n'.format(
- channelId,
- self.conn.channels[channelId])))
- return
- self.write(b'~' + char)
- else:
- self.escapeMode = 0
- self.write(char)
-
-
- def dataReceived(self, data):
- self.stdio.write(data)
-
-
- def extReceived(self, t, data):
- if t == connection.EXTENDED_DATA_STDERR:
- log.msg('got {} stderr data'.format(len(data)))
- if ioType(sys.stderr) == unicode:
- sys.stderr.buffer.write(data)
- else:
- sys.stderr.write(data)
-
-
- def eofReceived(self):
- log.msg('got eof')
- self.stdio.loseWriteConnection()
-
-
- def closeReceived(self):
- log.msg('remote side closed {}'.format(self))
- self.conn.sendClose(self)
-
-
- def closed(self):
- global old
- log.msg('closed {}'.format(self))
- log.msg(repr(self.conn.channels))
-
-
- def request_exit_status(self, data):
- global exitStatus
- exitStatus = int(struct.unpack('>L', data)[0])
- log.msg('exit status: {}'.format(exitStatus))
-
-
- def sendEOF(self):
- self.conn.sendEOF(self)
-
-
- def stopWriting(self):
- self.stdio.pauseProducing()
-
-
- def startWriting(self):
- self.stdio.resumeProducing()
-
-
- def _windowResized(self, *args):
- winsz = fcntl.ioctl(0, tty.TIOCGWINSZ, '12345678')
- winSize = struct.unpack('4H', winsz)
- newSize = winSize[1], winSize[0], winSize[2], winSize[3]
- self.conn.sendRequest(self, b'window-change', struct.pack('!4L', *newSize))
-
-
-
- class SSHListenClientForwardingChannel(forwarding.SSHListenClientForwardingChannel): pass
- class SSHConnectForwardingChannel(forwarding.SSHConnectForwardingChannel): pass
-
-
-
- def _leaveRawMode():
- global _inRawMode
- if not _inRawMode:
- return
- fd = sys.stdin.fileno()
- tty.tcsetattr(fd, tty.TCSANOW, _savedRawMode)
- _inRawMode = 0
-
-
-
- def _enterRawMode():
- global _inRawMode, _savedRawMode
- if _inRawMode:
- return
- fd = sys.stdin.fileno()
- try:
- old = tty.tcgetattr(fd)
- new = old[:]
- except:
- log.msg('not a typewriter!')
- else:
- # iflage
- new[0] = new[0] | tty.IGNPAR
- new[0] = new[0] & ~(tty.ISTRIP | tty.INLCR | tty.IGNCR | tty.ICRNL |
- tty.IXON | tty.IXANY | tty.IXOFF)
- if hasattr(tty, 'IUCLC'):
- new[0] = new[0] & ~tty.IUCLC
-
- # lflag
- new[3] = new[3] & ~(tty.ISIG | tty.ICANON | tty.ECHO | tty.ECHO |
- tty.ECHOE | tty.ECHOK | tty.ECHONL)
- if hasattr(tty, 'IEXTEN'):
- new[3] = new[3] & ~tty.IEXTEN
-
- #oflag
- new[1] = new[1] & ~tty.OPOST
-
- new[6][tty.VMIN] = 1
- new[6][tty.VTIME] = 0
-
- _savedRawMode = old
- tty.tcsetattr(fd, tty.TCSANOW, new)
- #tty.setraw(fd)
- _inRawMode = 1
-
-
-
- if __name__ == '__main__':
- run()
|