~landscape/zope3/newer-from-ztk

« back to all changes in this revision

Viewing changes to src/twisted/conch/scripts/conch.py

  • Committer: Thomas Hervé
  • Date: 2009-07-08 13:52:04 UTC
  • Revision ID: thomas@canonical.com-20090708135204-df5eesrthifpylf8
Remove twisted copy

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# -*- test-case-name: twisted.conch.test.test_conch -*-
2
 
#
3
 
# Copyright (c) 2001-2004 Twisted Matrix Laboratories.
4
 
# See LICENSE for details.
5
 
 
6
 
#
7
 
# $Id: conch.py,v 1.65 2004/03/11 00:29:14 z3p Exp $
8
 
 
9
 
#""" Implementation module for the `conch` command.
10
 
#"""
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
17
 
 
18
 
import os, sys, getpass, struct, tty, fcntl, base64, signal, stat, errno
19
 
 
20
 
class ClientOptions(options.ConchOptions):
21
 
    
22
 
    synopsis = """Usage:   conch [options] host [command]
23
 
"""
24
 
    
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'],
28
 
                     ]
29
 
 
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.'],
36
 
                ]
37
 
 
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: "]
45
 
 
46
 
    localForwards = []
47
 
    remoteForwards = []
48
 
 
49
 
    def opt_escape(self, esc):
50
 
        "Set escape character; ``none'' = disable"
51
 
        if esc == 'none':
52
 
            self['escape'] = None
53
 
        elif esc[0] == '^' and len(esc) == 2:
54
 
            self['escape'] = chr(ord(esc[1])-64)
55
 
        elif len(esc) == 1:
56
 
            self['escape'] = esc
57
 
        else:
58
 
            sys.exit("Bad escape character '%s'." % esc)
59
 
 
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)))
66
 
 
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)))
73
 
 
74
 
    def parseArgs(self, host, *command):
75
 
        self['host'] = host
76
 
        self['command'] = ' '.join(command)
77
 
 
78
 
# Rest of code in "run"
79
 
options = None
80
 
conn = None
81
 
exitStatus = 0
82
 
old = None
83
 
_inRawMode = 0
84
 
_savedRawMode = None
85
 
 
86
 
def run():
87
 
    global options, old
88
 
    args = sys.argv[1:]
89
 
    if '-l' in args: # cvs is an idiot
90
 
        i = args.index('-l')
91
 
        args = args[i:i+2]+args
92
 
        del args[i+2:i+4]
93
 
    for arg in args[:]:
94
 
        try:
95
 
            i = args.index(arg)
96
 
            if arg[:2] == '-o' and args[i+1][0]!='-':
97
 
                args[i:i+2] = [] # suck on it scp
98
 
        except ValueError:
99
 
            pass
100
 
    options = ClientOptions()
101
 
    try:
102
 
        options.parseOptions(args)
103
 
    except usage.UsageError, u:
104
 
        print 'ERROR: %s' % u
105
 
        options.opt_help()
106
 
        sys.exit(1)
107
 
    if options['log']:
108
 
        if options['logfile']:
109
 
            if options['logfile'] == '-':
110
 
                f = sys.stdout
111
 
            else:
112
 
                f = file(options['logfile'], 'a+')
113
 
        else:
114
 
            f = sys.stderr
115
 
        realout = sys.stdout
116
 
        log.startLogging(f)
117
 
        sys.stdout = realout
118
 
    else:
119
 
        log.discardLogs()
120
 
    doConnect()
121
 
    fd = sys.stdin.fileno()
122
 
    try:
123
 
        old = tty.tcgetattr(fd)
124
 
    except:
125
 
        old = None
126
 
    try:
127
 
        oldUSR1 = signal.signal(signal.SIGUSR1, lambda *a: reactor.callLater(0, reConnect))
128
 
    except:
129
 
        oldUSR1 = None
130
 
    try:
131
 
        reactor.run()
132
 
    finally:
133
 
        if old:
134
 
            tty.tcsetattr(fd, tty.TCSANOW, old)
135
 
        if oldUSR1:
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']
141
 
    sys.exit(exitStatus)
142
 
 
143
 
def handleError():
144
 
    from twisted.python import failure
145
 
    global exitStatus
146
 
    exitStatus = 2
147
 
    reactor.callLater(0.01, _stopReactor)
148
 
    log.err(failure.Failure())
149
 
    raise
150
 
 
151
 
def _stopReactor():
152
 
    try:
153
 
        reactor.stop()
154
 
    except: pass
155
 
 
156
 
def doConnect():
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']:
166
 
        options['port'] = 22
167
 
    else:
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)
174
 
 
175
 
def _ebExit(f):
176
 
    global exitStatus
177
 
    if hasattr(f.value, 'value'):
178
 
        s = f.value.value
179
 
    else:
180
 
        s = str(f)
181
 
    exitStatus = "conch: exiting with error %s" % f
182
 
    reactor.callLater(0.1, _stopReactor)
183
 
 
184
 
def onConnect():
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'):
189
 
        _KeepAlive(conn)
190
 
    if options.localForwards:
191
 
        for localPort, hostport in options.localForwards:
192
 
            s = reactor.listenTCP(localPort,
193
 
                        forwarding.SSHListenForwardingFactory(conn,
194
 
                            hostport,
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())
205
 
    if options['fork']:
206
 
        if os.fork():
207
 
            os._exit(0)
208
 
        os.setsid()
209
 
        for i in range(3):
210
 
            try:
211
 
                os.close(i)
212
 
            except OSError, e:
213
 
                import errno
214
 
                if e.errno != errno.EBADF:
215
 
                    raise
216
 
 
217
 
def reConnect():
218
 
    beforeShutdown()
219
 
    conn.transport.transport.loseConnection()
220
 
 
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)
226
 
 
227
 
def stopConnection():
228
 
    if not options['reconnect']:
229
 
        reactor.callLater(0.1, _stopReactor)
230
 
 
231
 
class _KeepAlive:
232
 
 
233
 
    def __init__(self, conn):
234
 
        self.conn = conn
235
 
        self.globalTimeout = None
236
 
        self.lc = task.LoopingCall(self.sendGlobal)
237
 
        self.lc.start(300)
238
 
 
239
 
    def sendGlobal(self):
240
 
        d = self.conn.sendGlobalRequest("conch-keep-alive@twistedmatrix.com",
241
 
                "", wantReply = 1)
242
 
        d.addBoth(self._cbGlobal)
243
 
        self.globalTimeout = reactor.callLater(30, self._ebGlobal)
244
 
 
245
 
    def _cbGlobal(self, res):
246
 
        if self.globalTimeout:
247
 
            self.globalTimeout.cancel()
248
 
            self.globalTimeout = None
249
 
 
250
 
    def _ebGlobal(self):
251
 
        if self.globalTimeout:
252
 
            self.globalTimeout = None
253
 
            self.conn.transport.loseConnection()
254
 
 
255
 
class SSHConnection(connection.SSHConnection):
256
 
    def serviceStarted(self):
257
 
        global conn
258
 
        conn = 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
265
 
        onConnect()
266
 
 
267
 
    def serviceStopped(self):
268
 
        lf = self.localForwards
269
 
        self.localForwards = []
270
 
        for s in lf:
271
 
            s.loseConnection()
272
 
        stopConnection()
273
 
 
274
 
    def requestRemoteForwarding(self, remotePort, hostport):
275
 
        data = forwarding.packGlobal_tcpip_forward(('0.0.0.0', remotePort))
276
 
        d = self.sendGlobalRequest('tcpip-forward', data, 
277
 
                                   wantReply=1)
278
 
        log.msg('requesting remote forwarding %s:%s' %(remotePort, hostport))
279
 
        d.addCallback(self._cbRemoteForwarding, remotePort, hostport)
280
 
        d.addErrback(self._ebRemoteForwarding, remotePort, hostport)
281
 
 
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))
286
 
    
287
 
    def _ebRemoteForwarding(self, f, remotePort, hostport):
288
 
        log.msg('remote forwarding %s:%s failed' % (remotePort, hostport))
289
 
        log.msg(f)
290
 
 
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)
295
 
        try:
296
 
            del self.remoteForwards[remotePort]
297
 
        except:
298
 
            pass
299
 
        log.msg(repr(self.remoteForwards))
300
 
 
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)
305
 
        log.msg(remoteHP)
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,
312
 
                                            conn = self)
313
 
        else:
314
 
            raise ConchError(connection.OPEN_CONNECT_FAILED, "don't know about that port")
315
 
 
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,
320
 
#                                             conn = self)
321
 
#        else:
322
 
#            return connection.OPEN_CONNECT_FAILED, "don't have an agent"
323
 
 
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')
329
 
            stopConnection()
330
 
        else:
331
 
            # because of the unix thing
332
 
            self.__class__.__bases__[0].channelClosed(self, channel)
333
 
 
334
 
class SSHSession(channel.SSHChannel):
335
 
 
336
 
    name = 'session'
337
 
 
338
 
    def channelOpen(self, foo):
339
 
        log.msg('session %s open' % self.id)
340
 
        if options['agent']:
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']:
345
 
            _enterRawMode()
346
 
        c = session.SSHSessionClient()
347
 
        if options['escape'] and not options['notty']:
348
 
            self.escapeMode = 1
349
 
            c.dataReceived = self.handleInput
350
 
        else:
351
 
            c.dataReceived = self.write
352
 
        c.connectionLost = lambda x=None,s=self:s.sendEOF()
353
 
        self.stdio = stdio.StandardIO(c)
354
 
        fd = 0
355
 
        if options['subsystem']:
356
 
            self.conn.sendRequest(self, 'subsystem', \
357
 
                common.NS(options['command']))
358
 
        elif options['command']:
359
 
            if options['tty']:
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']))
368
 
        else:
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)
379
 
 
380
 
    def handleInput(self, char):
381
 
        #log.msg('handling %s' % repr(char))
382
 
        if char in ('\n', '\r'):
383
 
            self.escapeMode = 1
384
 
            self.write(char)
385
 
        elif self.escapeMode == 1 and char == options['escape']:
386
 
            self.escapeMode = 2
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')
391
 
                stopConnection()
392
 
                return
393
 
            elif char == '\x1a': # ^Z, suspend
394
 
                def _():
395
 
                    _leaveRawMode()
396
 
                    sys.stdout.flush()
397
 
                    sys.stdin.flush()
398
 
                    os.kill(os.getpid(), signal.SIGTSTP)
399
 
                    _enterRawMode()
400
 
                reactor.callLater(0, _)
401
 
                return
402
 
            elif char == 'R': # rekey connection
403
 
                log.msg('rekeying connection')
404
 
                self.conn.transport.sendKexInit()
405
 
                return
406
 
            elif char == '#': # display connections
407
 
                self.stdio.write('\r\nThe following connections are open:\r\n')
408
 
                channels = self.conn.channels.keys()
409
 
                channels.sort()
410
 
                for channelId in channels:
411
 
                    self.stdio.write('  #%i %s\r\n' % (channelId, str(self.conn.channels[channelId])))
412
 
                return
413
 
            self.write('~' + char)
414
 
        else:
415
 
            self.escapeMode = 0
416
 
            self.write(char)
417
 
 
418
 
    def dataReceived(self, data):
419
 
        self.stdio.write(data)
420
 
 
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)
425
 
 
426
 
    def eofReceived(self):
427
 
        log.msg('got eof')
428
 
        self.stdio.closeStdin()
429
 
    
430
 
    def closeReceived(self):
431
 
        log.msg('remote side closed %s' % self)
432
 
        self.conn.sendClose(self)
433
 
 
434
 
    def closed(self):
435
 
        global old
436
 
        log.msg('closed %s' % self)
437
 
        log.msg(repr(self.conn.channels))
438
 
        if not options['nocache']: # fork into the background
439
 
            if os.fork():
440
 
                if old:
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)
446
 
                os._exit(0)
447
 
            os.setsid()
448
 
            for i in range(3):
449
 
                try:
450
 
                    os.close(i)
451
 
                except OSError, e:
452
 
                    import errno
453
 
                    if e.errno != errno.EBADF:
454
 
                        raise
455
 
 
456
 
    def request_exit_status(self, data):
457
 
        global exitStatus
458
 
        exitStatus = int(struct.unpack('>L', data)[0])
459
 
        log.msg('exit status: %s' % exitStatus)
460
 
 
461
 
    def sendEOF(self):
462
 
        self.conn.sendEOF(self)
463
 
 
464
 
    def stopWriting(self):
465
 
        self.stdio.pauseProducing()
466
 
 
467
 
    def startWriting(self):
468
 
        self.stdio.resumeProducing()
469
 
 
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))
475
 
           
476
 
 
477
 
class SSHListenClientForwardingChannel(forwarding.SSHListenClientForwardingChannel): pass
478
 
class SSHConnectForwardingChannel(forwarding.SSHConnectForwardingChannel): pass
479
 
 
480
 
def _leaveRawMode():
481
 
    global _inRawMode
482
 
    if not _inRawMode:
483
 
        return
484
 
    fd = sys.stdin.fileno()
485
 
    tty.tcsetattr(fd, tty.TCSANOW, _savedMode)
486
 
    _inRawMode = 0
487
 
 
488
 
def _enterRawMode():
489
 
    global _inRawMode, _savedMode
490
 
    if _inRawMode:
491
 
        return
492
 
    fd = sys.stdin.fileno()
493
 
    try:
494
 
        old = tty.tcgetattr(fd)
495
 
        new = old[:]
496
 
    except:
497
 
        log.msg('not a typewriter!')
498
 
    else:
499
 
        # iflage
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
505
 
 
506
 
        # lflag
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
511
 
 
512
 
        #oflag
513
 
        new[1] = new[1] & ~tty.OPOST
514
 
 
515
 
        new[6][tty.VMIN] = 1
516
 
        new[6][tty.VTIME] = 0
517
 
 
518
 
        _savedMode = old
519
 
        tty.tcsetattr(fd, tty.TCSANOW, new)
520
 
        #tty.setraw(fd)
521
 
        _inRawMode = 1
522
 
 
523
 
if __name__ == '__main__':
524
 
    run()
525