# Twisted, the Framework of Your Internet
# Copyright (C) 2001 Matthew W. Lefkowitz
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of version 2.1 of the GNU Lesser General Public
# License as published by the Free Software Foundation.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# $Id: conch.py,v 1.65 2004/03/11 00:29:14 z3p Exp $
#""" Implementation module for the `conch` command.
#"""
from twisted.conch.client import agent, connect, default, options
from twisted.conch.error import ConchError
from twisted.conch.ssh import userauth, connection, common
from twisted.conch.ssh import session, forwarding, channel
from twisted.internet import reactor, stdio, defer, protocol
from twisted.internet.error import CannotListenError
from twisted.python import log, usage
import os, sys, getpass, struct, tty, fcntl, base64, signal, stat, errno
try:
import cPickle as pickle
except ImportError:
import pickle
class ClientOptions(options.ConchOptions):
synopsis = """Usage: conch [options] host [command]
"""
optParameters = options.ConchOptions.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 = options.ConchOptions.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.'],
]
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 '%s'." % 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
keyAgent = None
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, u:
options.opt_help()
print 'ERROR: %s' % u
sys.exit(1)
if options['log']:
realout = sys.stdout
log.startLogging(sys.stderr)
sys.stdout = realout
else:
log.discardLogs()
doConnect()
fd = sys.stdin.fileno()
try:
old = tty.tcgetattr(fd)
except:
old = None
try:
reactor.run()
finally:
if old:
tty.tcsetattr(fd, tty.TCSANOW, old)
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 %s closed.' % options['host']
sys.exit(exitStatus)
def handleError():
from twisted.python import failure
global exitStatus
exitStatus = 2
reactor.stop()
log.err(failure.Failure())
raise
def doConnect():
log.deferr = handleError # HACK
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
uao = default.SSHUserAuthClient(options['user'], options, SSHConnection())
connect.connect(host, port, options, vhk, uao).addErrback(_ebExit)
def _ebExit(f):
global exitStatus
if hasattr(f.value, 'value'):
s = f.value.value
else:
s = str(f)
exitStatus = "conch: exiting with error %s" % f
reactor.stop()
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()
if options.localForwards:
for localPort, hostport in options.localForwards:
reactor.listenTCP(localPort,
forwarding.SSHListenForwardingFactory(conn,
hostport,
SSHListenClientForwardingChannel))
if options.remoteForwards:
for remotePort, hostport in options.remoteForwards:
log.msg('asking for remote forwarding for %s:%s' %
(remotePort, hostport))
conn.requestRemoteForwarding(remotePort, hostport)
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, e:
import errno
if e.errno != errno.EBADF:
raise
def keepAlive():
conn.transport.sendIgnore('')
reactor.callLater(300, keepAlive)
def stopConnection():
if options.remoteForwards:
for remotePort, hostport in options.remoteForwards:
log.msg('cancelling %s:%s' % (remotePort, hostport))
conn.cancelRemoteForwarding(remotePort)
reactor.stop()
class SSHConnection(connection.SSHConnection):
def serviceStarted(self):
global conn, keyAgent
conn = self
self.remoteForwards = {}
if keyAgent:
keyAgent.transport.loseConnection()
onConnect()
def serviceStopped(self):
stopConnection()
def requestRemoteForwarding(self, remotePort, hostport):
data = forwarding.packGlobal_tcpip_forward(('0.0.0.0', remotePort))
self.sendGlobalRequest('tcpip-forward', data)
self.remoteForwards[remotePort] = hostport
log.msg('requesting remote forwarding %s:%s' %(remotePort, hostport))
log.msg(repr(self.remoteForwards))
def cancelRemoteForwarding(self, remotePort):
data = forwarding.packGlobal_tcpip_forward(('0.0.0.0', remotePort))
self.sendGlobalRequest('cancel-tcpip-forward', data)
log.msg('cancelling remote forwarding %s' % remotePort)
del self.remoteForwards[remotePort]
log.msg(repr(self.remoteForwards))
def channel_forwarded_tcpip(self, windowSize, maxPacket, data):
remoteHP, origHP = forwarding.unpackOpen_forwarded_tcpip(data)
log.msg(self.remoteForwards)
if self.remoteForwards.has_key(remoteHP[1]):
connectHP = self.remoteForwards[remoteHP[1]]
log.msg('connect forwarding %s' % (connectHP,))
return SSHConnectForwardingChannel(connectHP,
remoteWindow = windowSize,
remoteMaxPacket = maxPacket,
conn = self)
else:
return connection.OPEN_CONNECT_FAILED, "don't know about that port"
def channel_auth_agent_openssh_com(self, windowSize, maxPacket, data):
if options['agent'] and keyAgent:
return agent.SSHAgentForwardingChannel(remoteWindow = windowSize,
remoteMaxPacket = maxPacket,
conn = self)
else:
return connection.OPEN_CONNECT_FAILED, "don't have an agent"
class SSHSession(channel.SSHChannel):
name = 'session'
def channelOpen(self, foo):
#global globalSession
#globalSession = self
# turn off local echo
log.msg('session %s open' % self.id)
if options['agent']:
d = self.conn.sendRequest(self, 'auth-agent-req@openssh.com', '', 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=None,s=self:s.sendEOF()
self.stdio = stdio.StandardIO(c)
fd = 0
if options['subsystem']:
self.conn.sendRequest(self, '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, 'pty-req', ptyReqData)
signal.signal(signal.SIGWINCH, self._windowResized)
self.conn.sendRequest(self, '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, 'pty-req', ptyReqData)
signal.signal(signal.SIGWINCH, self._windowResized)
self.conn.sendRequest(self, 'shell', '')
#if hasattr(conn.transport, 'transport'):
# conn.transport.transport.setTcpNoDelay(1)
def handleInput(self, char):
#log.msg('handling %s' % repr(char))
if char in ('\n', '\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 == '.': # disconnect
log.msg('disconnecting from escape')
stopConnection()
return
elif char == '\x1a': # ^Z, suspend
def _():
_leaveRawMode()
sys.stdout.flush()
sys.stdin.flush()
os.kill(os.getpid(), signal.SIGTSTP)
_enterRawMode()
reactor.callLater(0, _)
return
elif char == 'R': # rekey connection
log.msg('rekeying connection')
self.conn.transport.sendKexInit()
return
elif char == '#': # display connections
self.stdio.write('\r\nThe following connections are open:\r\n')
channels = self.conn.channels.keys()
channels.sort()
for channelId in channels:
self.stdio.write(' #%i %s\r\n' % (channelId, str(self.conn.channels[channelId])))
return
self.write('~' + 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 %s stderr data' % len(data))
sys.stderr.write(data)
sys.stderr.flush()
def eofReceived(self):
log.msg('got eof')
self.stdio.closeStdin()
def closed(self):
global old
log.msg('closed %s' % self)
if len(self.conn.channels) == 1 and not (options['noshell'] and not options['nocache']): # just us left
stopConnection()
elif not options['nocache']: # fork into the background
if os.fork():
if old:
fd = sys.stdin.fileno()
tty.tcsetattr(fd, tty.TCSANOW, old)
if (options['command'] and options['tty']) or \
not options['notty']:
signal.signal(signal.SIGWINCH, signal.SIG_DFL)
os._exit(0)
os.setsid()
for i in range(3):
try:
os.close(i)
except OSError, e:
import errno
if e.errno != errno.EBADF:
raise
def request_exit_status(self, data):
global exitStatus
exitStatus = int(struct.unpack('>L', data)[0])
log.msg('exit status: %s' % exitStatus)
def sendEOF(self):
self.conn.sendEOF(self)
def stopWriting(self):
self.stdio.stopReading()
def startWriting(self):
self.stdio.startReading()
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, 'window-change', struct.pack('!4L', *newSize))
class SSHListenClientForwardingChannel(forwarding.SSHListenClientForwardingChannel):
def closed(self):
forwarding.SSHListenClientForwardingChannel.closed(self)
if len(self.conn.channels) == 1 and not (options['noshell'] and not options['nocache']): # just us left
stopConnection()
class SSHConnectForwardingChannel(forwarding.SSHConnectForwardingChannel):
def closed(self):
forwarding.SSHConnectForwardingChannel.closed(self)
if len(self.conn.channels) == 1 and not (options['noshell'] and not options['nocache']): # just us left
stopConnection()
def _leaveRawMode():
global _inRawMode
if not _inRawMode:
return
fd = sys.stdin.fileno()
tty.tcsetattr(fd, tty.TCSANOW, _savedMode)
_inRawMode = 0
def _enterRawMode():
global _inRawMode, _savedMode
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
_savedMode = old
tty.tcsetattr(fd, tty.TCSANOW, new)
#tty.setraw(fd)
_inRawMode = 1
if __name__ == '__main__':
run()
syntax highlighted by Code2HTML, v. 0.9.1