root / trunk / twisted / conch / scripts / conch.py

Revision 26756, 17.5 kB (checked in by exarkun, 2 months ago)

Merge disable-connection-sharing-3498-2

Author: z3p
Reviewer: exarkun, therve, glyph
Fixes: #3498
Refs #3483, #3497, #716

Remove the Conch SSH connection sharing functionality. This feature is not well
tested and has numerous subtle bugs which can interfer with the normal operation
of the Conch SSH client.

The Conch command line arguments which supported configuring this functionality
have been removed (supplying them is now an error) has has the module primarily
responsible for the implementation.

It is expected that this feature will be re-introduced at some point, without
the problems afflicting this implementation.

Line 
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 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
17
18 import os, sys, getpass, struct, tty, fcntl, signal
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: # 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.loseWriteConnection()
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
439     def request_exit_status(self, data):
440         global exitStatus
441         exitStatus = int(struct.unpack('>L', data)[0])
442         log.msg('exit status: %s' % exitStatus)
443
444     def sendEOF(self):
445         self.conn.sendEOF(self)
446
447     def stopWriting(self):
448         self.stdio.pauseProducing()
449
450     def startWriting(self):
451         self.stdio.resumeProducing()
452
453     def _windowResized(self, *args):
454         winsz = fcntl.ioctl(0, tty.TIOCGWINSZ, '12345678')
455         winSize = struct.unpack('4H', winsz)
456         newSize = winSize[1], winSize[0], winSize[2], winSize[3]
457         self.conn.sendRequest(self, 'window-change', struct.pack('!4L', *newSize))
458
459
460 class SSHListenClientForwardingChannel(forwarding.SSHListenClientForwardingChannel): pass
461 class SSHConnectForwardingChannel(forwarding.SSHConnectForwardingChannel): pass
462
463 def _leaveRawMode():
464     global _inRawMode
465     if not _inRawMode:
466         return
467     fd = sys.stdin.fileno()
468     tty.tcsetattr(fd, tty.TCSANOW, _savedMode)
469     _inRawMode = 0
470
471 def _enterRawMode():
472     global _inRawMode, _savedMode
473     if _inRawMode:
474         return
475     fd = sys.stdin.fileno()
476     try:
477         old = tty.tcgetattr(fd)
478         new = old[:]
479     except:
480         log.msg('not a typewriter!')
481     else:
482         # iflage
483         new[0] = new[0] | tty.IGNPAR
484         new[0] = new[0] & ~(tty.ISTRIP | tty.INLCR | tty.IGNCR | tty.ICRNL |
485                             tty.IXON | tty.IXANY | tty.IXOFF)
486         if hasattr(tty, 'IUCLC'):
487             new[0] = new[0] & ~tty.IUCLC
488
489         # lflag
490         new[3] = new[3] & ~(tty.ISIG | tty.ICANON | tty.ECHO | tty.ECHO |
491                             tty.ECHOE | tty.ECHOK | tty.ECHONL)
492         if hasattr(tty, 'IEXTEN'):
493             new[3] = new[3] & ~tty.IEXTEN
494
495         #oflag
496         new[1] = new[1] & ~tty.OPOST
497
498         new[6][tty.VMIN] = 1
499         new[6][tty.VTIME] = 0
500
501         _savedMode = old
502         tty.tcsetattr(fd, tty.TCSANOW, new)
503         #tty.setraw(fd)
504         _inRawMode = 1
505
506 if __name__ == '__main__':
507     run()
508
Note: See TracBrowser for help on using the browser.