OLD | NEW |
| (Empty) |
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 Forwa
rd local port to remote address'], | |
27 ['remoteforward', 'R', None, 'listen-port:host:port Forw
ard 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 subsyste
m.'], | |
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 t
hat 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', '', wa
ntReply=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.SSHListenClientForwardingChann
el): 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 | |
OLD | NEW |