1 # -*- test-case-name: twisted.conch.test.test_scripts -*-
2 # Copyright (c) Twisted Matrix Laboratories.
3 # See LICENSE for details.
6 # $Id: tkconch.py,v 1.6 2003/02/22 08:10:15 z3p Exp $
8 """ Implementation module for the `tkconch` command.
11 from __future__ import nested_scopes
13 import Tkinter, tkFileDialog, tkFont, tkMessageBox, string
14 from twisted.conch.ui import tkvt100
15 from twisted.conch.ssh import transport, userauth, connection, common, keys
16 from twisted.conch.ssh import session, forwarding, channel
17 from twisted.conch.client.default import isInKnownHosts
18 from twisted.internet import reactor, defer, protocol, tksupport
19 from twisted.python import usage, log
21 import os, sys, getpass, struct, base64, signal
23 class TkConchMenu(Tkinter.Frame):
24 def __init__(self, *args, **params):
25 ## Standard heading: initialization
26 apply(Tkinter.Frame.__init__, (self,) + args, params)
28 self.master.title('TkConch')
29 self.localRemoteVar = Tkinter.StringVar()
30 self.localRemoteVar.set('local')
32 Tkinter.Label(self, anchor='w', justify='left', text='Hostname').grid(column=1, row=1, sticky='w')
33 self.host = Tkinter.Entry(self)
34 self.host.grid(column=2, columnspan=2, row=1, sticky='nesw')
36 Tkinter.Label(self, anchor='w', justify='left', text='Port').grid(column=1, row=2, sticky='w')
37 self.port = Tkinter.Entry(self)
38 self.port.grid(column=2, columnspan=2, row=2, sticky='nesw')
40 Tkinter.Label(self, anchor='w', justify='left', text='Username').grid(column=1, row=3, sticky='w')
41 self.user = Tkinter.Entry(self)
42 self.user.grid(column=2, columnspan=2, row=3, sticky='nesw')
44 Tkinter.Label(self, anchor='w', justify='left', text='Command').grid(column=1, row=4, sticky='w')
45 self.command = Tkinter.Entry(self)
46 self.command.grid(column=2, columnspan=2, row=4, sticky='nesw')
48 Tkinter.Label(self, anchor='w', justify='left', text='Identity').grid(column=1, row=5, sticky='w')
49 self.identity = Tkinter.Entry(self)
50 self.identity.grid(column=2, row=5, sticky='nesw')
51 Tkinter.Button(self, command=self.getIdentityFile, text='Browse').grid(column=3, row=5, sticky='nesw')
53 Tkinter.Label(self, text='Port Forwarding').grid(column=1, row=6, sticky='w')
54 self.forwards = Tkinter.Listbox(self, height=0, width=0)
55 self.forwards.grid(column=2, columnspan=2, row=6, sticky='nesw')
56 Tkinter.Button(self, text='Add', command=self.addForward).grid(column=1, row=7)
57 Tkinter.Button(self, text='Remove', command=self.removeForward).grid(column=1, row=8)
58 self.forwardPort = Tkinter.Entry(self)
59 self.forwardPort.grid(column=2, row=7, sticky='nesw')
60 Tkinter.Label(self, text='Port').grid(column=3, row=7, sticky='nesw')
61 self.forwardHost = Tkinter.Entry(self)
62 self.forwardHost.grid(column=2, row=8, sticky='nesw')
63 Tkinter.Label(self, text='Host').grid(column=3, row=8, sticky='nesw')
64 self.localForward = Tkinter.Radiobutton(self, text='Local', variable=self.localRemoteVar, value='local')
65 self.localForward.grid(column=2, row=9)
66 self.remoteForward = Tkinter.Radiobutton(self, text='Remote', variable=self.localRemoteVar, value='remote')
67 self.remoteForward.grid(column=3, row=9)
69 Tkinter.Label(self, text='Advanced Options').grid(column=1, columnspan=3, row=10, sticky='nesw')
71 Tkinter.Label(self, anchor='w', justify='left', text='Cipher').grid(column=1, row=11, sticky='w')
72 self.cipher = Tkinter.Entry(self, name='cipher')
73 self.cipher.grid(column=2, columnspan=2, row=11, sticky='nesw')
75 Tkinter.Label(self, anchor='w', justify='left', text='MAC').grid(column=1, row=12, sticky='w')
76 self.mac = Tkinter.Entry(self, name='mac')
77 self.mac.grid(column=2, columnspan=2, row=12, sticky='nesw')
79 Tkinter.Label(self, anchor='w', justify='left', text='Escape Char').grid(column=1, row=13, sticky='w')
80 self.escape = Tkinter.Entry(self, name='escape')
81 self.escape.grid(column=2, columnspan=2, row=13, sticky='nesw')
82 Tkinter.Button(self, text='Connect!', command=self.doConnect).grid(column=1, columnspan=3, row=14, sticky='nesw')
85 self.grid_rowconfigure(6, weight=1, minsize=64)
86 self.grid_columnconfigure(2, weight=1, minsize=2)
88 self.master.protocol("WM_DELETE_WINDOW", sys.exit)
91 def getIdentityFile(self):
92 r = tkFileDialog.askopenfilename()
94 self.identity.delete(0, Tkinter.END)
95 self.identity.insert(Tkinter.END, r)
98 port = self.forwardPort.get()
99 self.forwardPort.delete(0, Tkinter.END)
100 host = self.forwardHost.get()
101 self.forwardHost.delete(0, Tkinter.END)
102 if self.localRemoteVar.get() == 'local':
103 self.forwards.insert(Tkinter.END, 'L:%s:%s' % (port, host))
105 self.forwards.insert(Tkinter.END, 'R:%s:%s' % (port, host))
107 def removeForward(self):
108 cur = self.forwards.curselection()
110 self.forwards.remove(cur[0])
114 options['host'] = self.host.get()
115 options['port'] = self.port.get()
116 options['user'] = self.user.get()
117 options['command'] = self.command.get()
118 cipher = self.cipher.get()
120 escape = self.escape.get()
122 if cipher in SSHClientTransport.supportedCiphers:
123 SSHClientTransport.supportedCiphers = [cipher]
125 tkMessageBox.showerror('TkConch', 'Bad cipher.')
129 if mac in SSHClientTransport.supportedMACs:
130 SSHClientTransport.supportedMACs = [mac]
132 tkMessageBox.showerror('TkConch', 'Bad MAC.')
137 options['escape'] = None
138 elif escape[0] == '^' and len(escape) == 2:
139 options['escape'] = chr(ord(escape[1])-64)
140 elif len(escape) == 1:
141 options['escape'] = escape
143 tkMessageBox.showerror('TkConch', "Bad escape character '%s'." % escape)
146 if self.identity.get():
147 options.identitys.append(self.identity.get())
149 for line in self.forwards.get(0,Tkinter.END):
151 options.opt_localforward(line[2:])
153 options.opt_remoteforward(line[2:])
155 if '@' in options['host']:
156 options['user'], options['host'] = options['host'].split('@',1)
158 if (not options['host'] or not options['user']) and finished:
159 tkMessageBox.showerror('TkConch', 'Missing host or username.')
163 self.master.destroy()
166 log.startLogging(sys.stderr)
170 log.deferr = handleError # HACK
171 if not options.identitys:
172 options.identitys = ['~/.ssh/id_rsa', '~/.ssh/id_dsa']
173 host = options['host']
174 port = int(options['port'] or 22)
176 reactor.connectTCP(host, port, SSHClientFactory())
177 frame.master.deiconify()
178 frame.master.title('%s@%s - TkConch' % (options['user'], options['host']))
182 class GeneralOptions(usage.Options):
183 synopsis = """Usage: tkconch [options] host [command]
186 optParameters = [['user', 'l', None, 'Log in using this user name.'],
187 ['identity', 'i', '~/.ssh/identity', 'Identity for public key authentication'],
188 ['escape', 'e', '~', "Set escape character; ``none'' = disable"],
189 ['cipher', 'c', None, 'Select encryption algorithm.'],
190 ['macs', 'm', None, 'Specify MAC algorithms for protocol version 2.'],
191 ['port', 'p', None, 'Connect to this port. Server must be on the same port.'],
192 ['localforward', 'L', None, 'listen-port:host:port Forward local port to remote address'],
193 ['remoteforward', 'R', None, 'listen-port:host:port Forward remote port to local address'],
196 optFlags = [['tty', 't', 'Tty; allocate a tty even if command is given.'],
197 ['notty', 'T', 'Do not allocate a tty.'],
198 ['version', 'V', 'Display version number only.'],
199 ['compress', 'C', 'Enable compression.'],
200 ['noshell', 'N', 'Do not execute a shell or command.'],
201 ['subsystem', 's', 'Invoke command (mandatory) as SSH2 subsystem.'],
202 ['log', 'v', 'Log to stderr'],
203 ['ansilog', 'a', 'Print the receieved data to stdout']]
205 _ciphers = transport.SSHClientTransport.supportedCiphers
206 _macs = transport.SSHClientTransport.supportedMACs
208 compData = usage.Completions(
209 mutuallyExclusive=[("tty", "notty")],
211 "cipher": usage.CompleteList(_ciphers),
212 "macs": usage.CompleteList(_macs),
213 "localforward": usage.Completer(descr="listen-port:host:port"),
214 "remoteforward": usage.Completer(descr="listen-port:host:port")},
215 extraActions=[usage.CompleteUserAtHost(),
216 usage.Completer(descr="command"),
217 usage.Completer(descr="argument", repeat=True)]
224 def opt_identity(self, i):
225 self.identitys.append(i)
227 def opt_localforward(self, f):
228 localPort, remoteHost, remotePort = f.split(':') # doesn't do v6 yet
229 localPort = int(localPort)
230 remotePort = int(remotePort)
231 self.localForwards.append((localPort, (remoteHost, remotePort)))
233 def opt_remoteforward(self, f):
234 remotePort, connHost, connPort = f.split(':') # doesn't do v6 yet
235 remotePort = int(remotePort)
236 connPort = int(connPort)
237 self.remoteForwards.append((remotePort, (connHost, connPort)))
239 def opt_compress(self):
240 SSHClientTransport.supportedCompressions[0:1] = ['zlib']
242 def parseArgs(self, *args):
244 self['host'] = args[0]
245 self['command'] = ' '.join(args[1:])
250 # Rest of code in "run"
256 def deferredAskFrame(question, echo):
258 raise ValueError("can't ask 2 questions at once!")
261 def gotChar(ch, resp=resp):
267 stresp = ''.join(resp)
269 frame.callback = None
272 elif 32 <= ord(ch) < 127:
276 elif ord(ch) == 8 and resp: # BS
277 if echo: frame.write('\x08 \x08')
279 frame.callback = gotChar
280 frame.write(question)
281 frame.canvas.focus_force()
285 global menu, options, frame
287 if '-l' in args: # cvs is an idiot
289 args = args[i:i+2]+args
294 if arg[:2] == '-o' and args[i+1][0]!='-':
295 args[i:i+2] = [] # suck on it scp
300 top = Tkinter.Toplevel()
301 menu = TkConchMenu(top)
302 menu.pack(side=Tkinter.TOP, fill=Tkinter.BOTH, expand=1)
303 options = GeneralOptions()
305 options.parseOptions(args)
306 except usage.UsageError, u:
307 print 'ERROR: %s' % u
310 for k,v in options.items():
311 if v and hasattr(menu, k):
312 getattr(menu,k).insert(Tkinter.END, v)
313 for (p, (rh, rp)) in options.localForwards:
314 menu.forwards.insert(Tkinter.END, 'L:%s:%s:%s' % (p, rh, rp))
315 options.localForwards = []
316 for (p, (rh, rp)) in options.remoteForwards:
317 menu.forwards.insert(Tkinter.END, 'R:%s:%s:%s' % (p, rh, rp))
318 options.remoteForwards = []
319 frame = tkvt100.VT100Frame(root, callback=None)
320 root.geometry('%dx%d'%(tkvt100.fontWidth*frame.width+3, tkvt100.fontHeight*frame.height+3))
321 frame.pack(side = Tkinter.TOP)
322 tksupport.install(root)
324 if (options['host'] and options['user']) or '@' in options['host']:
332 from twisted.python import failure
335 log.err(failure.Failure())
339 class SSHClientFactory(protocol.ClientFactory):
342 def stopFactory(self):
345 def buildProtocol(self, addr):
346 return SSHClientTransport()
348 def clientConnectionFailed(self, connector, reason):
349 tkMessageBox.showwarning('TkConch','Connection Failed, Reason:\n %s: %s' % (reason.type, reason.value))
351 class SSHClientTransport(transport.SSHClientTransport):
353 def receiveError(self, code, desc):
355 exitStatus = 'conch:\tRemote side disconnected with error code %i\nconch:\treason: %s' % (code, desc)
357 def sendDisconnect(self, code, reason):
359 exitStatus = 'conch:\tSending disconnect with error code %i\nconch:\treason: %s' % (code, reason)
360 transport.SSHClientTransport.sendDisconnect(self, code, reason)
362 def receiveDebug(self, alwaysDisplay, message, lang):
364 if alwaysDisplay or options['log']:
365 log.msg('Received Debug Message: %s' % message)
367 def verifyHostKey(self, pubKey, fingerprint):
368 #d = defer.Deferred()
369 #d.addCallback(lambda x:defer.succeed(1))
372 goodKey = isInKnownHosts(options['host'], pubKey, {'known-hosts': None})
373 if goodKey == 1: # good key
374 return defer.succeed(1)
375 elif goodKey == 2: # AAHHHHH changed
376 return defer.fail(error.ConchError('bad host key'))
378 if options['host'] == self.transport.getPeer()[1]:
379 host = options['host']
380 khHost = options['host']
382 host = '%s (%s)' % (options['host'],
383 self.transport.getPeer()[1])
384 khHost = '%s,%s' % (options['host'],
385 self.transport.getPeer()[1])
386 keyType = common.getNS(pubKey)[0]
387 ques = """The authenticity of host '%s' can't be established.\r
388 %s key fingerprint is %s.""" % (host,
389 {'ssh-dss':'DSA', 'ssh-rsa':'RSA'}[keyType],
391 ques+='\r\nAre you sure you want to continue connecting (yes/no)? '
392 return deferredAskFrame(ques, 1).addCallback(self._cbVerifyHostKey, pubKey, khHost, keyType)
394 def _cbVerifyHostKey(self, ans, pubKey, khHost, keyType):
395 if ans.lower() not in ('yes', 'no'):
396 return deferredAskFrame("Please type 'yes' or 'no': ",1).addCallback(self._cbVerifyHostKey, pubKey, khHost, keyType)
397 if ans.lower() == 'no':
398 frame.write('Host key verification failed.\r\n')
399 raise error.ConchError('bad host key')
401 frame.write("Warning: Permanently added '%s' (%s) to the list of known hosts.\r\n" % (khHost, {'ssh-dss':'DSA', 'ssh-rsa':'RSA'}[keyType]))
402 known_hosts = open(os.path.expanduser('~/.ssh/known_hosts'), 'a')
403 encodedKey = base64.encodestring(pubKey).replace('\n', '')
404 known_hosts.write('\n%s %s %s' % (khHost, keyType, encodedKey))
408 raise error.ConchError
410 def connectionSecure(self):
412 user = options['user']
414 user = getpass.getuser()
415 self.requestService(SSHUserAuthClient(user, SSHConnection()))
417 class SSHUserAuthClient(userauth.SSHUserAuthClient):
420 def getPassword(self, prompt = None):
422 prompt = "%s@%s's password: " % (self.user, options['host'])
423 return deferredAskFrame(prompt,0)
425 def getPublicKey(self):
426 files = [x for x in options.identitys if x not in self.usedFiles]
431 self.usedFiles.append(file)
432 file = os.path.expanduser(file)
434 if not os.path.exists(file):
437 return keys.Key.fromFile(file).blob()
439 return self.getPublicKey() # try again
441 def getPrivateKey(self):
442 file = os.path.expanduser(self.usedFiles[-1])
443 if not os.path.exists(file):
446 return defer.succeed(keys.Key.fromFile(file).keyObject)
447 except keys.BadKeyError, e:
448 if e.args[0] == 'encrypted key with no password':
449 prompt = "Enter passphrase for key '%s': " % \
451 return deferredAskFrame(prompt, 0).addCallback(self._cbGetPrivateKey, 0)
452 def _cbGetPrivateKey(self, ans, count):
453 file = os.path.expanduser(self.usedFiles[-1])
455 return keys.Key.fromFile(file, password = ans).keyObject
456 except keys.BadKeyError:
459 prompt = "Enter passphrase for key '%s': " % \
461 return deferredAskFrame(prompt, 0).addCallback(self._cbGetPrivateKey, count+1)
463 class SSHConnection(connection.SSHConnection):
464 def serviceStarted(self):
465 if not options['noshell']:
466 self.openChannel(SSHSession())
467 if options.localForwards:
468 for localPort, hostport in options.localForwards:
469 reactor.listenTCP(localPort,
470 forwarding.SSHListenForwardingFactory(self,
472 forwarding.SSHListenClientForwardingChannel))
473 if options.remoteForwards:
474 for remotePort, hostport in options.remoteForwards:
475 log.msg('asking for remote forwarding for %s:%s' %
476 (remotePort, hostport))
477 data = forwarding.packGlobal_tcpip_forward(
478 ('0.0.0.0', remotePort))
479 d = self.sendGlobalRequest('tcpip-forward', data)
480 self.remoteForwards[remotePort] = hostport
482 class SSHSession(channel.SSHChannel):
486 def channelOpen(self, foo):
487 #global globalSession
488 #globalSession = self
489 # turn off local echo
491 c = session.SSHSessionClient()
492 if options['escape']:
493 c.dataReceived = self.handleInput
495 c.dataReceived = self.write
496 c.connectionLost = self.sendEOF
497 frame.callback = c.dataReceived
498 frame.canvas.focus_force()
499 if options['subsystem']:
500 self.conn.sendRequest(self, 'subsystem', \
501 common.NS(options['command']))
502 elif options['command']:
504 term = os.environ.get('TERM', 'xterm')
505 #winsz = fcntl.ioctl(fd, tty.TIOCGWINSZ, '12345678')
506 winSize = (25,80,0,0) #struct.unpack('4H', winsz)
507 ptyReqData = session.packRequest_pty_req(term, winSize, '')
508 self.conn.sendRequest(self, 'pty-req', ptyReqData)
509 self.conn.sendRequest(self, 'exec', \
510 common.NS(options['command']))
512 if not options['notty']:
513 term = os.environ.get('TERM', 'xterm')
514 #winsz = fcntl.ioctl(fd, tty.TIOCGWINSZ, '12345678')
515 winSize = (25,80,0,0) #struct.unpack('4H', winsz)
516 ptyReqData = session.packRequest_pty_req(term, winSize, '')
517 self.conn.sendRequest(self, 'pty-req', ptyReqData)
518 self.conn.sendRequest(self, 'shell', '')
519 self.conn.transport.transport.setTcpNoDelay(1)
521 def handleInput(self, char):
522 #log.msg('handling %s' % repr(char))
523 if char in ('\n', '\r'):
526 elif self.escapeMode == 1 and char == options['escape']:
528 elif self.escapeMode == 2:
529 self.escapeMode = 1 # so we can chain escapes together
530 if char == '.': # disconnect
531 log.msg('disconnecting from escape')
534 elif char == '\x1a': # ^Z, suspend
535 # following line courtesy of Erwin@freenode
536 os.kill(os.getpid(), signal.SIGSTOP)
538 elif char == 'R': # rekey connection
539 log.msg('rekeying connection')
540 self.conn.transport.sendKexInit()
542 self.write('~' + char)
547 def dataReceived(self, data):
548 if options['ansilog']:
552 def extReceived(self, t, data):
553 if t==connection.EXTENDED_DATA_STDERR:
554 log.msg('got %s stderr data' % len(data))
555 sys.stderr.write(data)
558 def eofReceived(self):
563 log.msg('closed %s' % self)
564 if len(self.conn.channels) == 1: # just us left
567 def request_exit_status(self, data):
569 exitStatus = int(struct.unpack('>L', data)[0])
570 log.msg('exit status: %s' % exitStatus)
573 self.conn.sendEOF(self)
575 if __name__=="__main__":