~certify-web-dev/twisted/certify-trunk

« back to all changes in this revision

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

  • Committer: Bazaar Package Importer
  • Author(s): Matthias Klose
  • Date: 2007-01-17 14:52:35 UTC
  • mfrom: (1.1.5 upstream) (2.1.2 etch)
  • Revision ID: james.westby@ubuntu.com-20070117145235-btmig6qfmqfen0om
Tags: 2.5.0-0ubuntu1
New upstream version, compatible with python2.5.

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