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 agent, 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, defer, task
16
from twisted.python import log, usage
18
import os, sys, getpass, struct, tty, fcntl, base64, signal, stat, errno
20
class ClientOptions(options.ConchOptions):
22
synopsis = """Usage: conch [options] host [command]
25
optParameters = [['escape', 'e', '~'],
26
['localforward', 'L', None, 'listen-port:host:port Forward local port to remote address'],
27
['remoteforward', 'R', None, 'listen-port:host:port Forward remote port to local address'],
30
optFlags = [['null', 'n', 'Redirect input from /dev/null.'],
31
['fork', 'f', 'Fork to background after authentication.'],
32
['tty', 't', 'Tty; allocate a tty even if command is given.'],
33
['notty', 'T', 'Do not allocate a tty.'],
34
['noshell', 'N', 'Do not execute a shell or command.'],
35
['subsystem', 's', 'Invoke command (mandatory) as SSH2 subsystem.'],
38
#zsh_altArgDescr = {"foo":"use this description for foo instead"}
39
#zsh_multiUse = ["foo", "bar"]
40
#zsh_mutuallyExclusive = [("foo", "bar"), ("bar", "baz")]
41
#zsh_actions = {"foo":'_files -g "*.foo"', "bar":"(one two three)"}
42
zsh_actionDescr = {"localforward":"listen-port:host:port",
43
"remoteforward":"listen-port:host:port"}
44
zsh_extras = ["*:command: "]
49
def opt_escape(self, esc):
50
"Set escape character; ``none'' = disable"
53
elif esc[0] == '^' and len(esc) == 2:
54
self['escape'] = chr(ord(esc[1])-64)
58
sys.exit("Bad escape character '%s'." % esc)
60
def opt_localforward(self, f):
61
"Forward local port to remote address (lport:host:port)"
62
localPort, remoteHost, remotePort = f.split(':') # doesn't do v6 yet
63
localPort = int(localPort)
64
remotePort = int(remotePort)
65
self.localForwards.append((localPort, (remoteHost, remotePort)))
67
def opt_remoteforward(self, f):
68
"""Forward remote port to local address (rport:host:port)"""
69
remotePort, connHost, connPort = f.split(':') # doesn't do v6 yet
70
remotePort = int(remotePort)
71
connPort = int(connPort)
72
self.remoteForwards.append((remotePort, (connHost, connPort)))
74
def parseArgs(self, host, *command):
76
self['command'] = ' '.join(command)
78
# Rest of code in "run"
89
if '-l' in args: # cvs is an idiot
91
args = args[i:i+2]+args
96
if arg[:2] == '-o' and args[i+1][0]!='-':
97
args[i:i+2] = [] # suck on it scp
100
options = ClientOptions()
102
options.parseOptions(args)
103
except usage.UsageError, u:
104
print 'ERROR: %s' % u
108
if options['logfile']:
109
if options['logfile'] == '-':
112
f = file(options['logfile'], 'a+')
121
fd = sys.stdin.fileno()
123
old = tty.tcgetattr(fd)
127
oldUSR1 = signal.signal(signal.SIGUSR1, lambda *a: reactor.callLater(0, reConnect))
134
tty.tcsetattr(fd, tty.TCSANOW, old)
136
signal.signal(signal.SIGUSR1, oldUSR1)
137
if (options['command'] and options['tty']) or not options['notty']:
138
signal.signal(signal.SIGWINCH, signal.SIG_DFL)
139
if sys.stdout.isatty() and not options['command']:
140
print 'Connection to %s closed.' % options['host']
144
from twisted.python import failure
147
reactor.callLater(0.01, _stopReactor)
148
log.err(failure.Failure())
157
# log.deferr = handleError # HACK
158
if '@' in options['host']:
159
options['user'], options['host'] = options['host'].split('@',1)
160
if not options.identitys:
161
options.identitys = ['~/.ssh/id_rsa', '~/.ssh/id_dsa']
162
host = options['host']
163
if not options['user']:
164
options['user'] = getpass.getuser()
165
if not options['port']:
168
options['port'] = int(options['port'])
169
host = options['host']
170
port = options['port']
171
vhk = default.verifyHostKey
172
uao = default.SSHUserAuthClient(options['user'], options, SSHConnection())
173
connect.connect(host, port, options, vhk, uao).addErrback(_ebExit)
177
if hasattr(f.value, 'value'):
181
exitStatus = "conch: exiting with error %s" % f
182
reactor.callLater(0.1, _stopReactor)
185
# if keyAgent and options['agent']:
186
# cc = protocol.ClientCreator(reactor, SSHAgentForwardingLocal, conn)
187
# cc.connectUNIX(os.environ['SSH_AUTH_SOCK'])
188
if hasattr(conn.transport, 'sendIgnore'):
190
if options.localForwards:
191
for localPort, hostport in options.localForwards:
192
s = reactor.listenTCP(localPort,
193
forwarding.SSHListenForwardingFactory(conn,
195
SSHListenClientForwardingChannel))
196
conn.localForwards.append(s)
197
if options.remoteForwards:
198
for remotePort, hostport in options.remoteForwards:
199
log.msg('asking for remote forwarding for %s:%s' %
200
(remotePort, hostport))
201
conn.requestRemoteForwarding(remotePort, hostport)
202
reactor.addSystemEventTrigger('before', 'shutdown', beforeShutdown)
203
if not options['noshell'] or options['agent']:
204
conn.openChannel(SSHSession())
214
if e.errno != errno.EBADF:
219
conn.transport.transport.loseConnection()
221
def beforeShutdown():
222
remoteForwards = options.remoteForwards
223
for remotePort, hostport in remoteForwards:
224
log.msg('cancelling %s:%s' % (remotePort, hostport))
225
conn.cancelRemoteForwarding(remotePort)
227
def stopConnection():
228
if not options['reconnect']:
229
reactor.callLater(0.1, _stopReactor)
233
def __init__(self, conn):
235
self.globalTimeout = None
236
self.lc = task.LoopingCall(self.sendGlobal)
239
def sendGlobal(self):
240
d = self.conn.sendGlobalRequest("conch-keep-alive@twistedmatrix.com",
242
d.addBoth(self._cbGlobal)
243
self.globalTimeout = reactor.callLater(30, self._ebGlobal)
245
def _cbGlobal(self, res):
246
if self.globalTimeout:
247
self.globalTimeout.cancel()
248
self.globalTimeout = None
251
if self.globalTimeout:
252
self.globalTimeout = None
253
self.conn.transport.loseConnection()
255
class SSHConnection(connection.SSHConnection):
256
def serviceStarted(self):
259
self.localForwards = []
260
self.remoteForwards = {}
261
if not isinstance(self, connection.SSHConnection):
262
# make these fall through
263
del self.__class__.requestRemoteForwarding
264
del self.__class__.cancelRemoteForwarding
267
def serviceStopped(self):
268
lf = self.localForwards
269
self.localForwards = []
274
def requestRemoteForwarding(self, remotePort, hostport):
275
data = forwarding.packGlobal_tcpip_forward(('0.0.0.0', remotePort))
276
d = self.sendGlobalRequest('tcpip-forward', data,
278
log.msg('requesting remote forwarding %s:%s' %(remotePort, hostport))
279
d.addCallback(self._cbRemoteForwarding, remotePort, hostport)
280
d.addErrback(self._ebRemoteForwarding, remotePort, hostport)
282
def _cbRemoteForwarding(self, result, remotePort, hostport):
283
log.msg('accepted remote forwarding %s:%s' % (remotePort, hostport))
284
self.remoteForwards[remotePort] = hostport
285
log.msg(repr(self.remoteForwards))
287
def _ebRemoteForwarding(self, f, remotePort, hostport):
288
log.msg('remote forwarding %s:%s failed' % (remotePort, hostport))
291
def cancelRemoteForwarding(self, remotePort):
292
data = forwarding.packGlobal_tcpip_forward(('0.0.0.0', remotePort))
293
self.sendGlobalRequest('cancel-tcpip-forward', data)
294
log.msg('cancelling remote forwarding %s' % remotePort)
296
del self.remoteForwards[remotePort]
299
log.msg(repr(self.remoteForwards))
301
def channel_forwarded_tcpip(self, windowSize, maxPacket, data):
302
log.msg('%s %s' % ('FTCP', repr(data)))
303
remoteHP, origHP = forwarding.unpackOpen_forwarded_tcpip(data)
304
log.msg(self.remoteForwards)
306
if self.remoteForwards.has_key(remoteHP[1]):
307
connectHP = self.remoteForwards[remoteHP[1]]
308
log.msg('connect forwarding %s' % (connectHP,))
309
return SSHConnectForwardingChannel(connectHP,
310
remoteWindow = windowSize,
311
remoteMaxPacket = maxPacket,
314
raise ConchError(connection.OPEN_CONNECT_FAILED, "don't know about that port")
316
# def channel_auth_agent_openssh_com(self, windowSize, maxPacket, data):
317
# if options['agent'] and keyAgent:
318
# return agent.SSHAgentForwardingChannel(remoteWindow = windowSize,
319
# remoteMaxPacket = maxPacket,
322
# return connection.OPEN_CONNECT_FAILED, "don't have an agent"
324
def channelClosed(self, channel):
325
log.msg('connection closing %s' % channel)
326
log.msg(self.channels)
327
if len(self.channels) == 1 and not (options['noshell'] and not options['nocache']): # just us left
328
log.msg('stopping connection')
331
# because of the unix thing
332
self.__class__.__bases__[0].channelClosed(self, channel)
334
class SSHSession(channel.SSHChannel):
338
def channelOpen(self, foo):
339
log.msg('session %s open' % self.id)
341
d = self.conn.sendRequest(self, 'auth-agent-req@openssh.com', '', wantReply=1)
342
d.addBoth(lambda x:log.msg(x))
343
if options['noshell']: return
344
if (options['command'] and options['tty']) or not options['notty']:
346
c = session.SSHSessionClient()
347
if options['escape'] and not options['notty']:
349
c.dataReceived = self.handleInput
351
c.dataReceived = self.write
352
c.connectionLost = lambda x=None,s=self:s.sendEOF()
353
self.stdio = stdio.StandardIO(c)
355
if options['subsystem']:
356
self.conn.sendRequest(self, 'subsystem', \
357
common.NS(options['command']))
358
elif options['command']:
360
term = os.environ['TERM']
361
winsz = fcntl.ioctl(fd, tty.TIOCGWINSZ, '12345678')
362
winSize = struct.unpack('4H', winsz)
363
ptyReqData = session.packRequest_pty_req(term, winSize, '')
364
self.conn.sendRequest(self, 'pty-req', ptyReqData)
365
signal.signal(signal.SIGWINCH, self._windowResized)
366
self.conn.sendRequest(self, 'exec', \
367
common.NS(options['command']))
369
if not options['notty']:
370
term = os.environ['TERM']
371
winsz = fcntl.ioctl(fd, tty.TIOCGWINSZ, '12345678')
372
winSize = struct.unpack('4H', winsz)
373
ptyReqData = session.packRequest_pty_req(term, winSize, '')
374
self.conn.sendRequest(self, 'pty-req', ptyReqData)
375
signal.signal(signal.SIGWINCH, self._windowResized)
376
self.conn.sendRequest(self, 'shell', '')
377
#if hasattr(conn.transport, 'transport'):
378
# conn.transport.transport.setTcpNoDelay(1)
380
def handleInput(self, char):
381
#log.msg('handling %s' % repr(char))
382
if char in ('\n', '\r'):
385
elif self.escapeMode == 1 and char == options['escape']:
387
elif self.escapeMode == 2:
388
self.escapeMode = 1 # so we can chain escapes together
389
if char == '.': # disconnect
390
log.msg('disconnecting from escape')
393
elif char == '\x1a': # ^Z, suspend
398
os.kill(os.getpid(), signal.SIGTSTP)
400
reactor.callLater(0, _)
402
elif char == 'R': # rekey connection
403
log.msg('rekeying connection')
404
self.conn.transport.sendKexInit()
406
elif char == '#': # display connections
407
self.stdio.write('\r\nThe following connections are open:\r\n')
408
channels = self.conn.channels.keys()
410
for channelId in channels:
411
self.stdio.write(' #%i %s\r\n' % (channelId, str(self.conn.channels[channelId])))
413
self.write('~' + char)
418
def dataReceived(self, data):
419
self.stdio.write(data)
421
def extReceived(self, t, data):
422
if t==connection.EXTENDED_DATA_STDERR:
423
log.msg('got %s stderr data' % len(data))
424
sys.stderr.write(data)
426
def eofReceived(self):
428
self.stdio.closeStdin()
430
def closeReceived(self):
431
log.msg('remote side closed %s' % self)
432
self.conn.sendClose(self)
436
log.msg('closed %s' % self)
437
log.msg(repr(self.conn.channels))
438
if not options['nocache']: # fork into the background
441
fd = sys.stdin.fileno()
442
tty.tcsetattr(fd, tty.TCSANOW, old)
443
if (options['command'] and options['tty']) or \
444
not options['notty']:
445
signal.signal(signal.SIGWINCH, signal.SIG_DFL)
453
if e.errno != errno.EBADF:
456
def request_exit_status(self, data):
458
exitStatus = int(struct.unpack('>L', data)[0])
459
log.msg('exit status: %s' % exitStatus)
462
self.conn.sendEOF(self)
464
def stopWriting(self):
465
self.stdio.pauseProducing()
467
def startWriting(self):
468
self.stdio.resumeProducing()
470
def _windowResized(self, *args):
471
winsz = fcntl.ioctl(0, tty.TIOCGWINSZ, '12345678')
472
winSize = struct.unpack('4H', winsz)
473
newSize = winSize[1], winSize[0], winSize[2], winSize[3]
474
self.conn.sendRequest(self, 'window-change', struct.pack('!4L', *newSize))
477
class SSHListenClientForwardingChannel(forwarding.SSHListenClientForwardingChannel): pass
478
class SSHConnectForwardingChannel(forwarding.SSHConnectForwardingChannel): pass
484
fd = sys.stdin.fileno()
485
tty.tcsetattr(fd, tty.TCSANOW, _savedMode)
489
global _inRawMode, _savedMode
492
fd = sys.stdin.fileno()
494
old = tty.tcgetattr(fd)
497
log.msg('not a typewriter!')
500
new[0] = new[0] | tty.IGNPAR
501
new[0] = new[0] & ~(tty.ISTRIP | tty.INLCR | tty.IGNCR | tty.ICRNL |
502
tty.IXON | tty.IXANY | tty.IXOFF)
503
if hasattr(tty, 'IUCLC'):
504
new[0] = new[0] & ~tty.IUCLC
507
new[3] = new[3] & ~(tty.ISIG | tty.ICANON | tty.ECHO | tty.ECHO |
508
tty.ECHOE | tty.ECHOK | tty.ECHONL)
509
if hasattr(tty, 'IEXTEN'):
510
new[3] = new[3] & ~tty.IEXTEN
513
new[1] = new[1] & ~tty.OPOST
516
new[6][tty.VTIME] = 0
519
tty.tcsetattr(fd, tty.TCSANOW, new)
523
if __name__ == '__main__':