1
# -*- test-case-name: twisted.conch.test.test_conch -*-
3
# Copyright (c) 2001-2004 Twisted Matrix Laboratories.
4
# See LICENSE for details.
7
# $Id: conch.py,v 1.65 2004/03/11 00:29:14 z3p Exp $
9
#""" Implementation module for the `conch` command.
11
from twisted.conch.client import connect, default, options
12
from twisted.conch.error import ConchError
13
from twisted.conch.ssh import connection, common
14
from twisted.conch.ssh import session, forwarding, channel
15
from twisted.internet import reactor, stdio, task
16
from twisted.python import log, usage
18
import os, sys, getpass, struct, tty, fcntl, signal
20
class ClientOptions(options.ConchOptions):
22
synopsis = """Usage: conch [options] host [command]
24
longdesc = ("conch is a SSHv2 client that allows logging into a remote "
25
"machine and executing commands.")
27
optParameters = [['escape', 'e', '~'],
28
['localforward', 'L', None, 'listen-port:host:port Forward local port to remote address'],
29
['remoteforward', 'R', None, 'listen-port:host:port Forward remote port to local address'],
32
optFlags = [['null', 'n', 'Redirect input from /dev/null.'],
33
['fork', 'f', 'Fork to background after authentication.'],
34
['tty', 't', 'Tty; allocate a tty even if command is given.'],
35
['notty', 'T', 'Do not allocate a tty.'],
36
['noshell', 'N', 'Do not execute a shell or command.'],
37
['subsystem', 's', 'Invoke command (mandatory) as SSH2 subsystem.'],
40
#zsh_altArgDescr = {"foo":"use this description for foo instead"}
41
#zsh_multiUse = ["foo", "bar"]
42
#zsh_mutuallyExclusive = [("foo", "bar"), ("bar", "baz")]
43
#zsh_actions = {"foo":'_files -g "*.foo"', "bar":"(one two three)"}
44
zsh_actionDescr = {"localforward":"listen-port:host:port",
45
"remoteforward":"listen-port:host:port"}
46
zsh_extras = ["*:command: "]
51
def opt_escape(self, esc):
52
"Set escape character; ``none'' = disable"
55
elif esc[0] == '^' and len(esc) == 2:
56
self['escape'] = chr(ord(esc[1])-64)
60
sys.exit("Bad escape character '%s'." % esc)
62
def opt_localforward(self, f):
63
"Forward local port to remote address (lport:host:port)"
64
localPort, remoteHost, remotePort = f.split(':') # doesn't do v6 yet
65
localPort = int(localPort)
66
remotePort = int(remotePort)
67
self.localForwards.append((localPort, (remoteHost, remotePort)))
69
def opt_remoteforward(self, f):
70
"""Forward remote port to local address (rport:host:port)"""
71
remotePort, connHost, connPort = f.split(':') # doesn't do v6 yet
72
remotePort = int(remotePort)
73
connPort = int(connPort)
74
self.remoteForwards.append((remotePort, (connHost, connPort)))
76
def parseArgs(self, host, *command):
78
self['command'] = ' '.join(command)
80
# Rest of code in "run"
91
if '-l' in args: # cvs is an idiot
93
args = args[i:i+2]+args
98
if arg[:2] == '-o' and args[i+1][0]!='-':
99
args[i:i+2] = [] # suck on it scp
102
options = ClientOptions()
104
options.parseOptions(args)
105
except usage.UsageError, u:
106
print 'ERROR: %s' % u
110
if options['logfile']:
111
if options['logfile'] == '-':
114
f = file(options['logfile'], 'a+')
123
fd = sys.stdin.fileno()
125
old = tty.tcgetattr(fd)
129
oldUSR1 = signal.signal(signal.SIGUSR1, lambda *a: reactor.callLater(0, reConnect))
136
tty.tcsetattr(fd, tty.TCSANOW, old)
138
signal.signal(signal.SIGUSR1, oldUSR1)
139
if (options['command'] and options['tty']) or not options['notty']:
140
signal.signal(signal.SIGWINCH, signal.SIG_DFL)
141
if sys.stdout.isatty() and not options['command']:
142
print 'Connection to %s closed.' % options['host']
146
from twisted.python import failure
149
reactor.callLater(0.01, _stopReactor)
150
log.err(failure.Failure())
159
# log.deferr = handleError # HACK
160
if '@' in options['host']:
161
options['user'], options['host'] = options['host'].split('@',1)
162
if not options.identitys:
163
options.identitys = ['~/.ssh/id_rsa', '~/.ssh/id_dsa']
164
host = options['host']
165
if not options['user']:
166
options['user'] = getpass.getuser()
167
if not options['port']:
170
options['port'] = int(options['port'])
171
host = options['host']
172
port = options['port']
173
vhk = default.verifyHostKey
174
uao = default.SSHUserAuthClient(options['user'], options, SSHConnection())
175
connect.connect(host, port, options, vhk, uao).addErrback(_ebExit)
179
if hasattr(f.value, 'value'):
183
exitStatus = "conch: exiting with error %s" % f
184
reactor.callLater(0.1, _stopReactor)
187
# if keyAgent and options['agent']:
188
# cc = protocol.ClientCreator(reactor, SSHAgentForwardingLocal, conn)
189
# cc.connectUNIX(os.environ['SSH_AUTH_SOCK'])
190
if hasattr(conn.transport, 'sendIgnore'):
192
if options.localForwards:
193
for localPort, hostport in options.localForwards:
194
s = reactor.listenTCP(localPort,
195
forwarding.SSHListenForwardingFactory(conn,
197
SSHListenClientForwardingChannel))
198
conn.localForwards.append(s)
199
if options.remoteForwards:
200
for remotePort, hostport in options.remoteForwards:
201
log.msg('asking for remote forwarding for %s:%s' %
202
(remotePort, hostport))
203
conn.requestRemoteForwarding(remotePort, hostport)
204
reactor.addSystemEventTrigger('before', 'shutdown', beforeShutdown)
205
if not options['noshell'] or options['agent']:
206
conn.openChannel(SSHSession())
216
if e.errno != errno.EBADF:
221
conn.transport.transport.loseConnection()
223
def beforeShutdown():
224
remoteForwards = options.remoteForwards
225
for remotePort, hostport in remoteForwards:
226
log.msg('cancelling %s:%s' % (remotePort, hostport))
227
conn.cancelRemoteForwarding(remotePort)
229
def stopConnection():
230
if not options['reconnect']:
231
reactor.callLater(0.1, _stopReactor)
235
def __init__(self, conn):
237
self.globalTimeout = None
238
self.lc = task.LoopingCall(self.sendGlobal)
241
def sendGlobal(self):
242
d = self.conn.sendGlobalRequest("conch-keep-alive@twistedmatrix.com",
244
d.addBoth(self._cbGlobal)
245
self.globalTimeout = reactor.callLater(30, self._ebGlobal)
247
def _cbGlobal(self, res):
248
if self.globalTimeout:
249
self.globalTimeout.cancel()
250
self.globalTimeout = None
253
if self.globalTimeout:
254
self.globalTimeout = None
255
self.conn.transport.loseConnection()
257
class SSHConnection(connection.SSHConnection):
258
def serviceStarted(self):
261
self.localForwards = []
262
self.remoteForwards = {}
263
if not isinstance(self, connection.SSHConnection):
264
# make these fall through
265
del self.__class__.requestRemoteForwarding
266
del self.__class__.cancelRemoteForwarding
269
def serviceStopped(self):
270
lf = self.localForwards
271
self.localForwards = []
276
def requestRemoteForwarding(self, remotePort, hostport):
277
data = forwarding.packGlobal_tcpip_forward(('0.0.0.0', remotePort))
278
d = self.sendGlobalRequest('tcpip-forward', data,
280
log.msg('requesting remote forwarding %s:%s' %(remotePort, hostport))
281
d.addCallback(self._cbRemoteForwarding, remotePort, hostport)
282
d.addErrback(self._ebRemoteForwarding, remotePort, hostport)
284
def _cbRemoteForwarding(self, result, remotePort, hostport):
285
log.msg('accepted remote forwarding %s:%s' % (remotePort, hostport))
286
self.remoteForwards[remotePort] = hostport
287
log.msg(repr(self.remoteForwards))
289
def _ebRemoteForwarding(self, f, remotePort, hostport):
290
log.msg('remote forwarding %s:%s failed' % (remotePort, hostport))
293
def cancelRemoteForwarding(self, remotePort):
294
data = forwarding.packGlobal_tcpip_forward(('0.0.0.0', remotePort))
295
self.sendGlobalRequest('cancel-tcpip-forward', data)
296
log.msg('cancelling remote forwarding %s' % remotePort)
298
del self.remoteForwards[remotePort]
301
log.msg(repr(self.remoteForwards))
303
def channel_forwarded_tcpip(self, windowSize, maxPacket, data):
304
log.msg('%s %s' % ('FTCP', repr(data)))
305
remoteHP, origHP = forwarding.unpackOpen_forwarded_tcpip(data)
306
log.msg(self.remoteForwards)
308
if self.remoteForwards.has_key(remoteHP[1]):
309
connectHP = self.remoteForwards[remoteHP[1]]
310
log.msg('connect forwarding %s' % (connectHP,))
311
return SSHConnectForwardingChannel(connectHP,
312
remoteWindow = windowSize,
313
remoteMaxPacket = maxPacket,
316
raise ConchError(connection.OPEN_CONNECT_FAILED, "don't know about that port")
318
# def channel_auth_agent_openssh_com(self, windowSize, maxPacket, data):
319
# if options['agent'] and keyAgent:
320
# return agent.SSHAgentForwardingChannel(remoteWindow = windowSize,
321
# remoteMaxPacket = maxPacket,
324
# return connection.OPEN_CONNECT_FAILED, "don't have an agent"
326
def channelClosed(self, channel):
327
log.msg('connection closing %s' % channel)
328
log.msg(self.channels)
329
if len(self.channels) == 1: # just us left
330
log.msg('stopping connection')
333
# because of the unix thing
334
self.__class__.__bases__[0].channelClosed(self, channel)
336
class SSHSession(channel.SSHChannel):
340
def channelOpen(self, foo):
341
log.msg('session %s open' % self.id)
343
d = self.conn.sendRequest(self, 'auth-agent-req@openssh.com', '', wantReply=1)
344
d.addBoth(lambda x:log.msg(x))
345
if options['noshell']: return
346
if (options['command'] and options['tty']) or not options['notty']:
348
c = session.SSHSessionClient()
349
if options['escape'] and not options['notty']:
351
c.dataReceived = self.handleInput
353
c.dataReceived = self.write
354
c.connectionLost = lambda x=None,s=self:s.sendEOF()
355
self.stdio = stdio.StandardIO(c)
357
if options['subsystem']:
358
self.conn.sendRequest(self, 'subsystem', \
359
common.NS(options['command']))
360
elif options['command']:
362
term = os.environ['TERM']
363
winsz = fcntl.ioctl(fd, tty.TIOCGWINSZ, '12345678')
364
winSize = struct.unpack('4H', winsz)
365
ptyReqData = session.packRequest_pty_req(term, winSize, '')
366
self.conn.sendRequest(self, 'pty-req', ptyReqData)
367
signal.signal(signal.SIGWINCH, self._windowResized)
368
self.conn.sendRequest(self, 'exec', \
369
common.NS(options['command']))
371
if not options['notty']:
372
term = os.environ['TERM']
373
winsz = fcntl.ioctl(fd, tty.TIOCGWINSZ, '12345678')
374
winSize = struct.unpack('4H', winsz)
375
ptyReqData = session.packRequest_pty_req(term, winSize, '')
376
self.conn.sendRequest(self, 'pty-req', ptyReqData)
377
signal.signal(signal.SIGWINCH, self._windowResized)
378
self.conn.sendRequest(self, 'shell', '')
379
#if hasattr(conn.transport, 'transport'):
380
# conn.transport.transport.setTcpNoDelay(1)
382
def handleInput(self, char):
383
#log.msg('handling %s' % repr(char))
384
if char in ('\n', '\r'):
387
elif self.escapeMode == 1 and char == options['escape']:
389
elif self.escapeMode == 2:
390
self.escapeMode = 1 # so we can chain escapes together
391
if char == '.': # disconnect
392
log.msg('disconnecting from escape')
395
elif char == '\x1a': # ^Z, suspend
400
os.kill(os.getpid(), signal.SIGTSTP)
402
reactor.callLater(0, _)
404
elif char == 'R': # rekey connection
405
log.msg('rekeying connection')
406
self.conn.transport.sendKexInit()
408
elif char == '#': # display connections
409
self.stdio.write('\r\nThe following connections are open:\r\n')
410
channels = self.conn.channels.keys()
412
for channelId in channels:
413
self.stdio.write(' #%i %s\r\n' % (channelId, str(self.conn.channels[channelId])))
415
self.write('~' + char)
420
def dataReceived(self, data):
421
self.stdio.write(data)
423
def extReceived(self, t, data):
424
if t==connection.EXTENDED_DATA_STDERR:
425
log.msg('got %s stderr data' % len(data))
426
sys.stderr.write(data)
428
def eofReceived(self):
430
self.stdio.loseWriteConnection()
432
def closeReceived(self):
433
log.msg('remote side closed %s' % self)
434
self.conn.sendClose(self)
438
log.msg('closed %s' % self)
439
log.msg(repr(self.conn.channels))
441
def request_exit_status(self, data):
443
exitStatus = int(struct.unpack('>L', data)[0])
444
log.msg('exit status: %s' % exitStatus)
447
self.conn.sendEOF(self)
449
def stopWriting(self):
450
self.stdio.pauseProducing()
452
def startWriting(self):
453
self.stdio.resumeProducing()
455
def _windowResized(self, *args):
456
winsz = fcntl.ioctl(0, tty.TIOCGWINSZ, '12345678')
457
winSize = struct.unpack('4H', winsz)
458
newSize = winSize[1], winSize[0], winSize[2], winSize[3]
459
self.conn.sendRequest(self, 'window-change', struct.pack('!4L', *newSize))
462
class SSHListenClientForwardingChannel(forwarding.SSHListenClientForwardingChannel): pass
463
class SSHConnectForwardingChannel(forwarding.SSHConnectForwardingChannel): pass
469
fd = sys.stdin.fileno()
470
tty.tcsetattr(fd, tty.TCSANOW, _savedMode)
474
global _inRawMode, _savedMode
477
fd = sys.stdin.fileno()
479
old = tty.tcgetattr(fd)
482
log.msg('not a typewriter!')
485
new[0] = new[0] | tty.IGNPAR
486
new[0] = new[0] & ~(tty.ISTRIP | tty.INLCR | tty.IGNCR | tty.ICRNL |
487
tty.IXON | tty.IXANY | tty.IXOFF)
488
if hasattr(tty, 'IUCLC'):
489
new[0] = new[0] & ~tty.IUCLC
492
new[3] = new[3] & ~(tty.ISIG | tty.ICANON | tty.ECHO | tty.ECHO |
493
tty.ECHOE | tty.ECHOK | tty.ECHONL)
494
if hasattr(tty, 'IEXTEN'):
495
new[3] = new[3] & ~tty.IEXTEN
498
new[1] = new[1] & ~tty.OPOST
501
new[6][tty.VTIME] = 0
504
tty.tcsetattr(fd, tty.TCSANOW, new)
508
if __name__ == '__main__':