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

Revision 22687, 22.7 kB (checked in by therve, 1 year ago)

Merge kill-raise-string-2063-4

Authors: jerub, therve
Reviewers: dreid, exarkun
Fixes #2063

Remove string exceptions raised in the whole twisted codebase.

The first merge added an useless dependency on tk in tests, corrected since.

Line 
1 # -*- test-case-name: twisted.conch.test.test_scripts -*-
2 # Copyright (c) 2001-2007 Twisted Matrix Laboratories.
3 # See LICENSE for details.
4
5 #
6 # $Id: tkconch.py,v 1.6 2003/02/22 08:10:15 z3p Exp $
7
8 """ Implementation module for the `tkconch` command.
9 """
10
11 from __future__ import nested_scopes
12
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
20
21 import os, sys, getpass, struct, base64, signal
22
23 class TkConchMenu(Tkinter.Frame):
24     def __init__(self, *args, **params):
25         ## Standard heading: initialization
26         apply(Tkinter.Frame.__init__, (self,) + args, params)
27
28         self.master.title('TkConch')
29         self.localRemoteVar = Tkinter.StringVar()
30         self.localRemoteVar.set('local')
31
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')
35
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')
39
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')
43
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')
47
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')
52
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)
68
69         Tkinter.Label(self, text='Advanced Options').grid(column=1, columnspan=3, row=10, sticky='nesw')
70
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')
74
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')
78
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')
83
84         # Resize behavior(s)
85         self.grid_rowconfigure(6, weight=1, minsize=64)
86         self.grid_columnconfigure(2, weight=1, minsize=2)
87
88         self.master.protocol("WM_DELETE_WINDOW", sys.exit)
89        
90
91     def getIdentityFile(self):
92         r = tkFileDialog.askopenfilename()
93         if r:
94             self.identity.delete(0, Tkinter.END)
95             self.identity.insert(Tkinter.END, r)
96
97     def addForward(self):
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))
104         else:
105             self.forwards.insert(Tkinter.END, 'R:%s:%s' % (port, host))
106
107     def removeForward(self):
108         cur = self.forwards.curselection()
109         if cur:
110             self.forwards.remove(cur[0])
111
112     def doConnect(self):
113         finished = 1
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()
119         mac = self.mac.get()
120         escape = self.escape.get()
121         if cipher:
122             if cipher in SSHClientTransport.supportedCiphers:
123                 SSHClientTransport.supportedCiphers = [cipher]
124             else:
125                 tkMessageBox.showerror('TkConch', 'Bad cipher.')
126                 finished = 0
127
128         if mac:
129             if mac in SSHClientTransport.supportedMACs:
130                 SSHClientTransport.supportedMACs = [mac]
131             elif finished:
132                 tkMessageBox.showerror('TkConch', 'Bad MAC.')
133                 finished = 0
134
135         if escape:
136             if escape == 'none':
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
142             elif finished:
143                 tkMessageBox.showerror('TkConch', "Bad escape character '%s'." % escape)
144                 finished = 0
145
146         if self.identity.get():
147             options.identitys.append(self.identity.get())
148
149         for line in self.forwards.get(0,Tkinter.END):
150             if line[0]=='L':
151                 options.opt_localforward(line[2:])
152             else:
153                 options.opt_remoteforward(line[2:])
154
155         if '@' in options['host']:
156             options['user'], options['host'] = options['host'].split('@',1)
157
158         if (not options['host'] or not options['user']) and finished:
159             tkMessageBox.showerror('TkConch', 'Missing host or username.')
160             finished = 0
161         if finished:
162             self.master.quit()
163             self.master.destroy()       
164             if options['log']:
165                 realout = sys.stdout
166                 log.startLogging(sys.stderr)
167                 sys.stdout = realout
168             else:
169                 log.discardLogs()
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)
175             log.msg((host,port))
176             reactor.connectTCP(host, port, SSHClientFactory())
177             frame.master.deiconify()
178             frame.master.title('%s@%s - TkConch' % (options['user'], options['host']))
179         else:
180             self.focus()
181
182 class GeneralOptions(usage.Options):
183     synopsis = """Usage:    tkconch [options] host [command]
184  """
185
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'],
194                     ]
195    
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']]
204
205     #zsh_altArgDescr = {"foo":"use this description for foo instead"}
206     #zsh_multiUse = ["foo", "bar"]
207     zsh_mutuallyExclusive = [("tty", "notty")]
208     zsh_actions = {"cipher":"(%s)" % " ".join(transport.SSHClientTransport.supportedCiphers),
209                    "macs":"(%s)" % " ".join(transport.SSHClientTransport.supportedMACs)}
210     zsh_actionDescr = {"localforward":"listen-port:host:port",
211                        "remoteforward":"listen-port:host:port"}
212     # user, host, or user@host completion similar to zsh's ssh completion
213     zsh_extras = ['1:host | user@host:{_ssh;if compset -P "*@"; then _wanted hosts expl "remote host name" _ssh_hosts && ret=0 elif compset -S "@*"; then _wanted users expl "login name" _ssh_users -S "" && ret=0 else if (( $+opt_args[-l] )); then tmp=() else tmp=( "users:login name:_ssh_users -qS@" ) fi; _alternative "hosts:remote host name:_ssh_hosts" "$tmp[@]" && ret=0 fi}',
214                   '*:command: ']
215
216     identitys = []
217     localForwards = []
218     remoteForwards = []
219
220     def opt_identity(self, i):
221         self.identitys.append(i)
222
223     def opt_localforward(self, f):
224         localPort, remoteHost, remotePort = f.split(':') # doesn't do v6 yet
225         localPort = int(localPort)
226         remotePort = int(remotePort)
227         self.localForwards.append((localPort, (remoteHost, remotePort)))
228
229     def opt_remoteforward(self, f):
230         remotePort, connHost, connPort = f.split(':') # doesn't do v6 yet
231         remotePort = int(remotePort)
232         connPort = int(connPort)
233         self.remoteForwards.append((remotePort, (connHost, connPort)))
234
235     def opt_compress(self):
236         SSHClientTransport.supportedCompressions[0:1] = ['zlib']
237
238     def parseArgs(self, *args):
239         if args:
240             self['host'] = args[0]
241             self['command'] = ' '.join(args[1:])
242         else:
243             self['host'] = ''
244             self['command'] = ''
245
246 # Rest of code in "run"
247 options = None
248 menu = None
249 exitStatus = 0
250 frame = None
251
252 def deferredAskFrame(question, echo):
253     if frame.callback:
254         raise ValueError("can't ask 2 questions at once!")
255     d = defer.Deferred()
256     resp = []
257     def gotChar(ch, resp=resp):
258         if not ch: return
259         if ch=='\x03': # C-c
260             reactor.stop()
261         if ch=='\r':
262             frame.write('\r\n')
263             stresp = ''.join(resp)
264             del resp
265             frame.callback = None
266             d.callback(stresp)
267             return
268         elif 32 <= ord(ch) < 127:
269             resp.append(ch)
270             if echo:
271                 frame.write(ch)
272         elif ord(ch) == 8 and resp: # BS
273             if echo: frame.write('\x08 \x08')
274             resp.pop()
275     frame.callback = gotChar
276     frame.write(question)
277     frame.canvas.focus_force()
278     return d
279
280 def run():
281     global menu, options, frame
282     args = sys.argv[1:]
283     if '-l' in args: # cvs is an idiot
284         i = args.index('-l')
285         args = args[i:i+2]+args
286         del args[i+2:i+4]
287     for arg in args[:]:
288         try:
289             i = args.index(arg)
290             if arg[:2] == '-o' and args[i+1][0]!='-':
291                 args[i:i+2] = [] # suck on it scp
292         except ValueError:
293             pass
294     root = Tkinter.Tk()
295     root.withdraw()
296     top = Tkinter.Toplevel()
297     menu = TkConchMenu(top)
298     menu.pack(side=Tkinter.TOP, fill=Tkinter.BOTH, expand=1)
299     options = GeneralOptions()
300     try:
301         options.parseOptions(args)
302     except usage.UsageError, u:
303         print 'ERROR: %s' % u
304         options.opt_help()
305         sys.exit(1)
306     for k,v in options.items():
307         if v and hasattr(menu, k):
308             getattr(menu,k).insert(Tkinter.END, v)
309     for (p, (rh, rp)) in options.localForwards:
310         menu.forwards.insert(Tkinter.END, 'L:%s:%s:%s' % (p, rh, rp))
311     options.localForwards = []
312     for (p, (rh, rp)) in options.remoteForwards:
313         menu.forwards.insert(Tkinter.END, 'R:%s:%s:%s' % (p, rh, rp))
314     options.remoteForwards = []
315     frame = tkvt100.VT100Frame(root, callback=None)
316     root.geometry('%dx%d'%(tkvt100.fontWidth*frame.width+3, tkvt100.fontHeight*frame.height+3))
317     frame.pack(side = Tkinter.TOP)
318     tksupport.install(root)
319     root.withdraw()
320     if (options['host'] and options['user']) or '@' in options['host']:
321         menu.doConnect()
322     else:
323         top.mainloop()
324     reactor.run()
325     sys.exit(exitStatus)
326
327 def handleError():
328     from twisted.python import failure
329     global exitStatus
330     exitStatus = 2
331     log.err(failure.Failure())
332     reactor.stop()
333     raise
334
335 class SSHClientFactory(protocol.ClientFactory):
336     noisy = 1
337
338     def stopFactory(self):
339         reactor.stop()
340
341     def buildProtocol(self, addr):
342         return SSHClientTransport()
343
344     def clientConnectionFailed(self, connector, reason):
345         tkMessageBox.showwarning('TkConch','Connection Failed, Reason:\n %s: %s' % (reason.type, reason.value))
346
347 class SSHClientTransport(transport.SSHClientTransport):
348
349     def receiveError(self, code, desc):
350         global exitStatus
351         exitStatus = 'conch:\tRemote side disconnected with error code %i\nconch:\treason: %s' % (code, desc)
352
353     def sendDisconnect(self, code, reason):
354         global exitStatus
355         exitStatus = 'conch:\tSending disconnect with error code %i\nconch:\treason: %s' % (code, reason)
356         transport.SSHClientTransport.sendDisconnect(self, code, reason)
357
358     def receiveDebug(self, alwaysDisplay, message, lang):
359         global options
360         if alwaysDisplay or options['log']:
361             log.msg('Received Debug Message: %s' % message)
362
363     def verifyHostKey(self, pubKey, fingerprint):
364         #d = defer.Deferred()
365         #d.addCallback(lambda x:defer.succeed(1))
366         #d.callback(2)
367         #return d
368         goodKey = isInKnownHosts(options['host'], pubKey, {'known-hosts': None})
369         if goodKey == 1: # good key
370             return defer.succeed(1)
371         elif goodKey == 2: # AAHHHHH changed
372             return defer.fail(error.ConchError('bad host key'))
373         else:
374             if options['host'] == self.transport.getPeer()[1]:
375                 host = options['host']
376                 khHost = options['host']
377             else:
378                 host = '%s (%s)' % (options['host'],
379                                     self.transport.getPeer()[1])
380                 khHost = '%s,%s' % (options['host'],
381                                     self.transport.getPeer()[1])
382             keyType = common.getNS(pubKey)[0]
383             ques = """The authenticity of host '%s' can't be established.\r
384 %s key fingerprint is %s.""" % (host,
385                                 {'ssh-dss':'DSA', 'ssh-rsa':'RSA'}[keyType],
386                                 fingerprint)
387             ques+='\r\nAre you sure you want to continue connecting (yes/no)? '
388             return deferredAskFrame(ques, 1).addCallback(self._cbVerifyHostKey, pubKey, khHost, keyType)
389
390     def _cbVerifyHostKey(self, ans, pubKey, khHost, keyType):
391         if ans.lower() not in ('yes', 'no'):
392             return deferredAskFrame("Please type  'yes' or 'no': ",1).addCallback(self._cbVerifyHostKey, pubKey, khHost, keyType)
393         if ans.lower() == 'no':
394             frame.write('Host key verification failed.\r\n')
395             raise error.ConchError('bad host key')
396         try:
397             frame.write("Warning: Permanently added '%s' (%s) to the list of known hosts.\r\n" % (khHost, {'ssh-dss':'DSA', 'ssh-rsa':'RSA'}[keyType]))
398             known_hosts = open(os.path.expanduser('~/.ssh/known_hosts'), 'a')
399             encodedKey = base64.encodestring(pubKey).replace('\n', '')
400             known_hosts.write('\n%s %s %s' % (khHost, keyType, encodedKey))
401             known_hosts.close()
402         except:
403             log.deferr()
404             raise error.ConchError
405
406     def connectionSecure(self):
407         if options['user']:
408             user = options['user']
409         else:
410             user = getpass.getuser()
411         self.requestService(SSHUserAuthClient(user, SSHConnection()))
412
413 class SSHUserAuthClient(userauth.SSHUserAuthClient):
414     usedFiles = []
415
416     def getPassword(self, prompt = None):
417         if not prompt:
418             prompt = "%s@%s's password: " % (self.user, options['host'])
419         return deferredAskFrame(prompt,0)
420
421     def getPublicKey(self):
422         files = [x for x in options.identitys if x not in self.usedFiles]
423         if not files:
424             return None
425         file = files[0]
426         log.msg(file)
427         self.usedFiles.append(file)
428         file = os.path.expanduser(file)
429         file += '.pub'
430         if not os.path.exists(file):
431             return
432         try:
433             return keys.getPublicKeyString(file)
434         except:
435             return self.getPublicKey() # try again
436     
437     def getPrivateKey(self):
438         file = os.path.expanduser(self.usedFiles[-1])
439         if not os.path.exists(file):
440             return None
441         try:
442             return defer.succeed(keys.getPrivateKeyObject(file))
443         except keys.BadKeyError, e:
444             if e.args[0] == 'encrypted key with no password':
445                 prompt = "Enter passphrase for key '%s': " % \
446                        self.usedFiles[-1]
447                 return deferredAskFrame(prompt, 0).addCallback(self._cbGetPrivateKey, 0)
448     def _cbGetPrivateKey(self, ans, count):
449         file = os.path.expanduser(self.usedFiles[-1])
450         try:
451             return keys.getPrivateKeyObject(file, password = ans)
452         except keys.BadKeyError:
453             if count == 2:
454                 raise
455             prompt = "Enter passphrase for key '%s': " % \
456                    self.usedFiles[-1]
457             return deferredAskFrame(prompt, 0).addCallback(self._cbGetPrivateKey, count+1)
458
459 class SSHConnection(connection.SSHConnection):
460     def serviceStarted(self):
461         if not options['noshell']:
462             self.openChannel(SSHSession())
463         if options.localForwards:
464             for localPort, hostport in options.localForwards:
465                 reactor.listenTCP(localPort,
466                             forwarding.SSHListenForwardingFactory(self,
467                                 hostport,
468                                 forwarding.SSHListenClientForwardingChannel))
469         if options.remoteForwards:
470             for remotePort, hostport in options.remoteForwards:
471                 log.msg('asking for remote forwarding for %s:%s' %
472                         (remotePort, hostport))
473                 data = forwarding.packGlobal_tcpip_forward(
474                     ('0.0.0.0', remotePort))
475                 d = self.sendGlobalRequest('tcpip-forward', data)
476                 self.remoteForwards[remotePort] = hostport
477
478 class SSHSession(channel.SSHChannel):
479
480     name = 'session'
481    
482     def channelOpen(self, foo):
483         #global globalSession
484         #globalSession = self
485         # turn off local echo
486         self.escapeMode = 1
487         c = session.SSHSessionClient()
488         if options['escape']:
489             c.dataReceived = self.handleInput
490         else:
491             c.dataReceived = self.write
492         c.connectionLost = self.sendEOF
493         frame.callback = c.dataReceived
494         frame.canvas.focus_force()
495         if options['subsystem']:
496             self.conn.sendRequest(self, 'subsystem', \
497                 common.NS(options['command']))
498         elif options['command']:
499             if options['tty']:
500                 term = os.environ.get('TERM', 'xterm')
501                 #winsz = fcntl.ioctl(fd, tty.TIOCGWINSZ, '12345678')
502                 winSize = (25,80,0,0) #struct.unpack('4H', winsz)
503                 ptyReqData = session.packRequest_pty_req(term, winSize, '')
504                 self.conn.sendRequest(self, 'pty-req', ptyReqData)               
505             self.conn.sendRequest(self, 'exec', \
506                 common.NS(options['command']))
507         else:
508             if not options['notty']:
509                 term = os.environ.get('TERM', 'xterm')
510                 #winsz = fcntl.ioctl(fd, tty.TIOCGWINSZ, '12345678')
511                 winSize = (25,80,0,0) #struct.unpack('4H', winsz)
512                 ptyReqData = session.packRequest_pty_req(term, winSize, '')
513                 self.conn.sendRequest(self, 'pty-req', ptyReqData)
514             self.conn.sendRequest(self, 'shell', '')
515         self.conn.transport.transport.setTcpNoDelay(1)
516
517     def handleInput(self, char):
518         #log.msg('handling %s' % repr(char))
519         if char in ('\n', '\r'):
520             self.escapeMode = 1
521             self.write(char)
522         elif self.escapeMode == 1 and char == options['escape']:
523             self.escapeMode = 2
524         elif self.escapeMode == 2:
525             self.escapeMode = 1 # so we can chain escapes together
526             if char == '.': # disconnect
527                 log.msg('disconnecting from escape')
528                 reactor.stop()
529                 return
530             elif char == '\x1a': # ^Z, suspend
531                 # following line courtesy of Erwin@freenode
532                 os.kill(os.getpid(), signal.SIGSTOP)
533                 return
534             elif char == 'R': # rekey connection
535                 log.msg('rekeying connection')
536                 self.conn.transport.sendKexInit()
537                 return
538             self.write('~' + char)
539         else:
540             self.escapeMode = 0
541             self.write(char)
542
543     def dataReceived(self, data):
544         if options['ansilog']:
545             print repr(data)
546         frame.write(data)
547
548     def extReceived(self, t, data):
549         if t==connection.EXTENDED_DATA_STDERR:
550             log.msg('got %s stderr data' % len(data))
551             sys.stderr.write(data)
552             sys.stderr.flush()
553
554     def eofReceived(self):
555         log.msg('got eof')
556         sys.stdin.close()
557
558     def closed(self):
559         log.msg('closed %s' % self)
560         if len(self.conn.channels) == 1: # just us left
561             reactor.stop()
562
563     def request_exit_status(self, data):
564         global exitStatus
565         exitStatus = int(struct.unpack('>L', data)[0])
566         log.msg('exit status: %s' % exitStatus)
567
568     def sendEOF(self):
569         self.conn.sendEOF(self)
570
571 if __name__=="__main__":
572     run()
Note: See TracBrowser for help on using the browser.