1 # -*- test-case-name: twisted.conch.test.test_conch -*-
3 # Copyright (c) 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 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
18 import os, sys, getpass, struct, tty, fcntl, signal
20 class ClientOptions(options.ConchOptions):
22 synopsis = """Usage: conch [options] host [command]
24 longdesc = ("conch is a SSHv2 client that allows logging into a remote "
25 "machine and executing commands.")
27 optParameters = [['escape', 'e', '~'],
28 ['localforward', 'L', None, 'listen-port:host:port Forward local port to remote address'],
29 ['remoteforward', 'R', None, 'listen-port:host:port Forward remote port to local address'],
32 optFlags = [['null', 'n', 'Redirect input from /dev/null.'],
33 ['fork', 'f', 'Fork to background after authentication.'],
34 ['tty', 't', 'Tty; allocate a tty even if command is given.'],
35 ['notty', 'T', 'Do not allocate a tty.'],
36 ['noshell', 'N', 'Do not execute a shell or command.'],
37 ['subsystem', 's', 'Invoke command (mandatory) as SSH2 subsystem.'],
40 compData = usage.Completions(
41 mutuallyExclusive=[("tty", "notty")],
43 "localforward": usage.Completer(descr="listen-port:host:port"),
44 "remoteforward": usage.Completer(descr="listen-port:host:port")},
45 extraActions=[usage.CompleteUserAtHost(),
46 usage.Completer(descr="command"),
47 usage.Completer(descr="argument", repeat=True)]
53 def opt_escape(self, esc):
54 "Set escape character; ``none'' = disable"
57 elif esc[0] == '^' and len(esc) == 2:
58 self['escape'] = chr(ord(esc[1])-64)
62 sys.exit("Bad escape character '%s'." % esc)
64 def opt_localforward(self, f):
65 "Forward local port to remote address (lport:host:port)"
66 localPort, remoteHost, remotePort = f.split(':') # doesn't do v6 yet
67 localPort = int(localPort)
68 remotePort = int(remotePort)
69 self.localForwards.append((localPort, (remoteHost, remotePort)))
71 def opt_remoteforward(self, f):
72 """Forward remote port to local address (rport:host:port)"""
73 remotePort, connHost, connPort = f.split(':') # doesn't do v6 yet
74 remotePort = int(remotePort)
75 connPort = int(connPort)
76 self.remoteForwards.append((remotePort, (connHost, connPort)))
78 def parseArgs(self, host, *command):
80 self['command'] = ' '.join(command)
82 # Rest of code in "run"
93 if '-l' in args: # cvs is an idiot
95 args = args[i:i+2]+args
100 if arg[:2] == '-o' and args[i+1][0]!='-':
101 args[i:i+2] = [] # suck on it scp
104 options = ClientOptions()
106 options.parseOptions(args)
107 except usage.UsageError, u:
108 print 'ERROR: %s' % u
112 if options['logfile']:
113 if options['logfile'] == '-':
116 f = file(options['logfile'], 'a+')
125 fd = sys.stdin.fileno()
127 old = tty.tcgetattr(fd)
131 oldUSR1 = signal.signal(signal.SIGUSR1, lambda *a: reactor.callLater(0, reConnect))
138 tty.tcsetattr(fd, tty.TCSANOW, old)
140 signal.signal(signal.SIGUSR1, oldUSR1)
141 if (options['command'] and options['tty']) or not options['notty']:
142 signal.signal(signal.SIGWINCH, signal.SIG_DFL)
143 if sys.stdout.isatty() and not options['command']:
144 print 'Connection to %s closed.' % options['host']
148 from twisted.python import failure
151 reactor.callLater(0.01, _stopReactor)
152 log.err(failure.Failure())
161 # log.deferr = handleError # HACK
162 if '@' in options['host']:
163 options['user'], options['host'] = options['host'].split('@',1)
164 if not options.identitys:
165 options.identitys = ['~/.ssh/id_rsa', '~/.ssh/id_dsa']
166 host = options['host']
167 if not options['user']:
168 options['user'] = getpass.getuser()
169 if not options['port']:
172 options['port'] = int(options['port'])
173 host = options['host']
174 port = options['port']
175 vhk = default.verifyHostKey
176 uao = default.SSHUserAuthClient(options['user'], options, SSHConnection())
177 connect.connect(host, port, options, vhk, uao).addErrback(_ebExit)
181 if hasattr(f.value, 'value'):
185 exitStatus = "conch: exiting with error %s" % f
186 reactor.callLater(0.1, _stopReactor)
189 # if keyAgent and options['agent']:
190 # cc = protocol.ClientCreator(reactor, SSHAgentForwardingLocal, conn)
191 # cc.connectUNIX(os.environ['SSH_AUTH_SOCK'])
192 if hasattr(conn.transport, 'sendIgnore'):
194 if options.localForwards:
195 for localPort, hostport in options.localForwards:
196 s = reactor.listenTCP(localPort,
197 forwarding.SSHListenForwardingFactory(conn,
199 SSHListenClientForwardingChannel))
200 conn.localForwards.append(s)
201 if options.remoteForwards:
202 for remotePort, hostport in options.remoteForwards:
203 log.msg('asking for remote forwarding for %s:%s' %
204 (remotePort, hostport))
205 conn.requestRemoteForwarding(remotePort, hostport)
206 reactor.addSystemEventTrigger('before', 'shutdown', beforeShutdown)
207 if not options['noshell'] or options['agent']:
208 conn.openChannel(SSHSession())
218 if e.errno != errno.EBADF:
223 conn.transport.transport.loseConnection()
225 def beforeShutdown():
226 remoteForwards = options.remoteForwards
227 for remotePort, hostport in remoteForwards:
228 log.msg('cancelling %s:%s' % (remotePort, hostport))
229 conn.cancelRemoteForwarding(remotePort)
231 def stopConnection():
232 if not options['reconnect']:
233 reactor.callLater(0.1, _stopReactor)
237 def __init__(self, conn):
239 self.globalTimeout = None
240 self.lc = task.LoopingCall(self.sendGlobal)
243 def sendGlobal(self):
244 d = self.conn.sendGlobalRequest("conch-keep-alive@twistedmatrix.com",
246 d.addBoth(self._cbGlobal)
247 self.globalTimeout = reactor.callLater(30, self._ebGlobal)
249 def _cbGlobal(self, res):
250 if self.globalTimeout:
251 self.globalTimeout.cancel()
252 self.globalTimeout = None
255 if self.globalTimeout:
256 self.globalTimeout = None
257 self.conn.transport.loseConnection()
259 class SSHConnection(connection.SSHConnection):
260 def serviceStarted(self):
263 self.localForwards = []
264 self.remoteForwards = {}
265 if not isinstance(self, connection.SSHConnection):
266 # make these fall through
267 del self.__class__.requestRemoteForwarding
268 del self.__class__.cancelRemoteForwarding
271 def serviceStopped(self):
272 lf = self.localForwards
273 self.localForwards = []
278 def requestRemoteForwarding(self, remotePort, hostport):
279 data = forwarding.packGlobal_tcpip_forward(('0.0.0.0', remotePort))
280 d = self.sendGlobalRequest('tcpip-forward', data,
282 log.msg('requesting remote forwarding %s:%s' %(remotePort, hostport))
283 d.addCallback(self._cbRemoteForwarding, remotePort, hostport)
284 d.addErrback(self._ebRemoteForwarding, remotePort, hostport)
286 def _cbRemoteForwarding(self, result, remotePort, hostport):
287 log.msg('accepted remote forwarding %s:%s' % (remotePort, hostport))
288 self.remoteForwards[remotePort] = hostport
289 log.msg(repr(self.remoteForwards))
291 def _ebRemoteForwarding(self, f, remotePort, hostport):
292 log.msg('remote forwarding %s:%s failed' % (remotePort, hostport))
295 def cancelRemoteForwarding(self, remotePort):
296 data = forwarding.packGlobal_tcpip_forward(('0.0.0.0', remotePort))
297 self.sendGlobalRequest('cancel-tcpip-forward', data)
298 log.msg('cancelling remote forwarding %s' % remotePort)
300 del self.remoteForwards[remotePort]
303 log.msg(repr(self.remoteForwards))
305 def channel_forwarded_tcpip(self, windowSize, maxPacket, data):
306 log.msg('%s %s' % ('FTCP', repr(data)))
307 remoteHP, origHP = forwarding.unpackOpen_forwarded_tcpip(data)
308 log.msg(self.remoteForwards)
310 if self.remoteForwards.has_key(remoteHP[1]):
311 connectHP = self.remoteForwards[remoteHP[1]]
312 log.msg('connect forwarding %s' % (connectHP,))
313 return SSHConnectForwardingChannel(connectHP,
314 remoteWindow = windowSize,
315 remoteMaxPacket = maxPacket,
318 raise ConchError(connection.OPEN_CONNECT_FAILED, "don't know about that port")
320 # def channel_auth_agent_openssh_com(self, windowSize, maxPacket, data):
321 # if options['agent'] and keyAgent:
322 # return agent.SSHAgentForwardingChannel(remoteWindow = windowSize,
323 # remoteMaxPacket = maxPacket,
326 # return connection.OPEN_CONNECT_FAILED, "don't have an agent"
328 def channelClosed(self, channel):
329 log.msg('connection closing %s' % channel)
330 log.msg(self.channels)
331 if len(self.channels) == 1: # just us left
332 log.msg('stopping connection')
335 # because of the unix thing
336 self.__class__.__bases__[0].channelClosed(self, channel)
338 class SSHSession(channel.SSHChannel):
342 def channelOpen(self, foo):
343 log.msg('session %s open' % self.id)
345 d = self.conn.sendRequest(self, 'auth-agent-req@openssh.com', '', wantReply=1)
346 d.addBoth(lambda x:log.msg(x))
347 if options['noshell']: return
348 if (options['command'] and options['tty']) or not options['notty']:
350 c = session.SSHSessionClient()
351 if options['escape'] and not options['notty']:
353 c.dataReceived = self.handleInput
355 c.dataReceived = self.write
356 c.connectionLost = lambda x=None,s=self:s.sendEOF()
357 self.stdio = stdio.StandardIO(c)
359 if options['subsystem']:
360 self.conn.sendRequest(self, 'subsystem', \
361 common.NS(options['command']))
362 elif options['command']:
364 term = os.environ['TERM']
365 winsz = fcntl.ioctl(fd, tty.TIOCGWINSZ, '12345678')
366 winSize = struct.unpack('4H', winsz)
367 ptyReqData = session.packRequest_pty_req(term, winSize, '')
368 self.conn.sendRequest(self, 'pty-req', ptyReqData)
369 signal.signal(signal.SIGWINCH, self._windowResized)
370 self.conn.sendRequest(self, 'exec', \
371 common.NS(options['command']))
373 if not options['notty']:
374 term = os.environ['TERM']
375 winsz = fcntl.ioctl(fd, tty.TIOCGWINSZ, '12345678')
376 winSize = struct.unpack('4H', winsz)
377 ptyReqData = session.packRequest_pty_req(term, winSize, '')
378 self.conn.sendRequest(self, 'pty-req', ptyReqData)
379 signal.signal(signal.SIGWINCH, self._windowResized)
380 self.conn.sendRequest(self, 'shell', '')
381 #if hasattr(conn.transport, 'transport'):
382 # conn.transport.transport.setTcpNoDelay(1)
384 def handleInput(self, char):
385 #log.msg('handling %s' % repr(char))
386 if char in ('\n', '\r'):
389 elif self.escapeMode == 1 and char == options['escape']:
391 elif self.escapeMode == 2:
392 self.escapeMode = 1 # so we can chain escapes together
393 if char == '.': # disconnect
394 log.msg('disconnecting from escape')
397 elif char == '\x1a': # ^Z, suspend
402 os.kill(os.getpid(), signal.SIGTSTP)
404 reactor.callLater(0, _)
406 elif char == 'R': # rekey connection
407 log.msg('rekeying connection')
408 self.conn.transport.sendKexInit()
410 elif char == '#': # display connections
411 self.stdio.write('\r\nThe following connections are open:\r\n')
412 channels = self.conn.channels.keys()
414 for channelId in channels:
415 self.stdio.write(' #%i %s\r\n' % (channelId, str(self.conn.channels[channelId])))
417 self.write('~' + char)
422 def dataReceived(self, data):
423 self.stdio.write(data)
425 def extReceived(self, t, data):
426 if t==connection.EXTENDED_DATA_STDERR:
427 log.msg('got %s stderr data' % len(data))
428 sys.stderr.write(data)
430 def eofReceived(self):
432 self.stdio.loseWriteConnection()
434 def closeReceived(self):
435 log.msg('remote side closed %s' % self)
436 self.conn.sendClose(self)
440 log.msg('closed %s' % self)
441 log.msg(repr(self.conn.channels))
443 def request_exit_status(self, data):
445 exitStatus = int(struct.unpack('>L', data)[0])
446 log.msg('exit status: %s' % exitStatus)
449 self.conn.sendEOF(self)
451 def stopWriting(self):
452 self.stdio.pauseProducing()
454 def startWriting(self):
455 self.stdio.resumeProducing()
457 def _windowResized(self, *args):
458 winsz = fcntl.ioctl(0, tty.TIOCGWINSZ, '12345678')
459 winSize = struct.unpack('4H', winsz)
460 newSize = winSize[1], winSize[0], winSize[2], winSize[3]
461 self.conn.sendRequest(self, 'window-change', struct.pack('!4L', *newSize))
464 class SSHListenClientForwardingChannel(forwarding.SSHListenClientForwardingChannel): pass
465 class SSHConnectForwardingChannel(forwarding.SSHConnectForwardingChannel): pass
471 fd = sys.stdin.fileno()
472 tty.tcsetattr(fd, tty.TCSANOW, _savedMode)
476 global _inRawMode, _savedMode
479 fd = sys.stdin.fileno()
481 old = tty.tcgetattr(fd)
484 log.msg('not a typewriter!')
487 new[0] = new[0] | tty.IGNPAR
488 new[0] = new[0] & ~(tty.ISTRIP | tty.INLCR | tty.IGNCR | tty.ICRNL |
489 tty.IXON | tty.IXANY | tty.IXOFF)
490 if hasattr(tty, 'IUCLC'):
491 new[0] = new[0] & ~tty.IUCLC
494 new[3] = new[3] & ~(tty.ISIG | tty.ICANON | tty.ECHO | tty.ECHO |
495 tty.ECHOE | tty.ECHOK | tty.ECHONL)
496 if hasattr(tty, 'IEXTEN'):
497 new[3] = new[3] & ~tty.IEXTEN
500 new[1] = new[1] & ~tty.OPOST
503 new[6][tty.VTIME] = 0
506 tty.tcsetattr(fd, tty.TCSANOW, new)
510 if __name__ == '__main__':