Initial import to Tizen
[profile/ivi/python-twisted.git] / twisted / conch / scripts / tkconch.py
1 # -*- test-case-name: twisted.conch.test.test_scripts -*-
2 # Copyright (c) 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     _ciphers = transport.SSHClientTransport.supportedCiphers
206     _macs = transport.SSHClientTransport.supportedMACs
207
208     compData = usage.Completions(
209         mutuallyExclusive=[("tty", "notty")],
210         optActions={
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)]
218         )
219
220     identitys = []
221     localForwards = []
222     remoteForwards = []
223
224     def opt_identity(self, i):
225         self.identitys.append(i)
226
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)))
232
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)))
238
239     def opt_compress(self):
240         SSHClientTransport.supportedCompressions[0:1] = ['zlib']
241
242     def parseArgs(self, *args):
243         if args:
244             self['host'] = args[0]
245             self['command'] = ' '.join(args[1:])
246         else:
247             self['host'] = ''
248             self['command'] = ''
249
250 # Rest of code in "run"
251 options = None
252 menu = None
253 exitStatus = 0
254 frame = None
255
256 def deferredAskFrame(question, echo):
257     if frame.callback:
258         raise ValueError("can't ask 2 questions at once!")
259     d = defer.Deferred()
260     resp = []
261     def gotChar(ch, resp=resp):
262         if not ch: return
263         if ch=='\x03': # C-c
264             reactor.stop()
265         if ch=='\r':
266             frame.write('\r\n')
267             stresp = ''.join(resp)
268             del resp
269             frame.callback = None
270             d.callback(stresp)
271             return
272         elif 32 <= ord(ch) < 127:
273             resp.append(ch)
274             if echo:
275                 frame.write(ch)
276         elif ord(ch) == 8 and resp: # BS
277             if echo: frame.write('\x08 \x08')
278             resp.pop()
279     frame.callback = gotChar
280     frame.write(question)
281     frame.canvas.focus_force()
282     return d
283
284 def run():
285     global menu, options, frame
286     args = sys.argv[1:]
287     if '-l' in args: # cvs is an idiot
288         i = args.index('-l')
289         args = args[i:i+2]+args
290         del args[i+2:i+4]
291     for arg in args[:]:
292         try:
293             i = args.index(arg)
294             if arg[:2] == '-o' and args[i+1][0]!='-':
295                 args[i:i+2] = [] # suck on it scp
296         except ValueError:
297             pass
298     root = Tkinter.Tk()
299     root.withdraw()
300     top = Tkinter.Toplevel()
301     menu = TkConchMenu(top)
302     menu.pack(side=Tkinter.TOP, fill=Tkinter.BOTH, expand=1)
303     options = GeneralOptions()
304     try:
305         options.parseOptions(args)
306     except usage.UsageError, u:
307         print 'ERROR: %s' % u
308         options.opt_help()
309         sys.exit(1)
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)
323     root.withdraw()
324     if (options['host'] and options['user']) or '@' in options['host']:
325         menu.doConnect()
326     else:
327         top.mainloop()
328     reactor.run()
329     sys.exit(exitStatus)
330
331 def handleError():
332     from twisted.python import failure
333     global exitStatus
334     exitStatus = 2
335     log.err(failure.Failure())
336     reactor.stop()
337     raise
338
339 class SSHClientFactory(protocol.ClientFactory):
340     noisy = 1 
341
342     def stopFactory(self):
343         reactor.stop()
344
345     def buildProtocol(self, addr):
346         return SSHClientTransport()
347
348     def clientConnectionFailed(self, connector, reason):
349         tkMessageBox.showwarning('TkConch','Connection Failed, Reason:\n %s: %s' % (reason.type, reason.value))
350
351 class SSHClientTransport(transport.SSHClientTransport):
352
353     def receiveError(self, code, desc):
354         global exitStatus
355         exitStatus = 'conch:\tRemote side disconnected with error code %i\nconch:\treason: %s' % (code, desc)
356
357     def sendDisconnect(self, code, reason):
358         global exitStatus
359         exitStatus = 'conch:\tSending disconnect with error code %i\nconch:\treason: %s' % (code, reason)
360         transport.SSHClientTransport.sendDisconnect(self, code, reason)
361
362     def receiveDebug(self, alwaysDisplay, message, lang):
363         global options
364         if alwaysDisplay or options['log']:
365             log.msg('Received Debug Message: %s' % message)
366
367     def verifyHostKey(self, pubKey, fingerprint):
368         #d = defer.Deferred()
369         #d.addCallback(lambda x:defer.succeed(1))
370         #d.callback(2)
371         #return d
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'))
377         else:
378             if options['host'] == self.transport.getPeer()[1]:
379                 host = options['host']
380                 khHost = options['host']
381             else:
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], 
390                                 fingerprint) 
391             ques+='\r\nAre you sure you want to continue connecting (yes/no)? '
392             return deferredAskFrame(ques, 1).addCallback(self._cbVerifyHostKey, pubKey, khHost, keyType)
393
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')
400         try:
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))
405             known_hosts.close()
406         except:
407             log.deferr()
408             raise error.ConchError 
409
410     def connectionSecure(self):
411         if options['user']:
412             user = options['user']
413         else:
414             user = getpass.getuser()
415         self.requestService(SSHUserAuthClient(user, SSHConnection()))
416
417 class SSHUserAuthClient(userauth.SSHUserAuthClient):
418     usedFiles = []
419
420     def getPassword(self, prompt = None):
421         if not prompt:
422             prompt = "%s@%s's password: " % (self.user, options['host'])
423         return deferredAskFrame(prompt,0) 
424
425     def getPublicKey(self):
426         files = [x for x in options.identitys if x not in self.usedFiles]
427         if not files:
428             return None
429         file = files[0]
430         log.msg(file)
431         self.usedFiles.append(file)
432         file = os.path.expanduser(file) 
433         file += '.pub'
434         if not os.path.exists(file):
435             return
436         try:
437             return keys.Key.fromFile(file).blob() 
438         except:
439             return self.getPublicKey() # try again
440     
441     def getPrivateKey(self):
442         file = os.path.expanduser(self.usedFiles[-1])
443         if not os.path.exists(file):
444             return None
445         try:
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': " % \
450                        self.usedFiles[-1]
451                 return deferredAskFrame(prompt, 0).addCallback(self._cbGetPrivateKey, 0)
452     def _cbGetPrivateKey(self, ans, count):
453         file = os.path.expanduser(self.usedFiles[-1])
454         try:
455             return keys.Key.fromFile(file, password = ans).keyObject
456         except keys.BadKeyError:
457             if count == 2:
458                 raise
459             prompt = "Enter passphrase for key '%s': " % \
460                    self.usedFiles[-1]
461             return deferredAskFrame(prompt, 0).addCallback(self._cbGetPrivateKey, count+1)
462
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, 
471                                 hostport,
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
481
482 class SSHSession(channel.SSHChannel):
483
484     name = 'session'
485     
486     def channelOpen(self, foo):
487         #global globalSession
488         #globalSession = self
489         # turn off local echo
490         self.escapeMode = 1
491         c = session.SSHSessionClient()
492         if options['escape']:
493             c.dataReceived = self.handleInput
494         else:
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']:
503             if options['tty']:
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']))
511         else:
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)
520
521     def handleInput(self, char):
522         #log.msg('handling %s' % repr(char))
523         if char in ('\n', '\r'):
524             self.escapeMode = 1
525             self.write(char)
526         elif self.escapeMode == 1 and char == options['escape']:
527             self.escapeMode = 2
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')
532                 reactor.stop()
533                 return
534             elif char == '\x1a': # ^Z, suspend
535                 # following line courtesy of Erwin@freenode
536                 os.kill(os.getpid(), signal.SIGSTOP)
537                 return
538             elif char == 'R': # rekey connection
539                 log.msg('rekeying connection')
540                 self.conn.transport.sendKexInit()
541                 return
542             self.write('~' + char)
543         else:
544             self.escapeMode = 0
545             self.write(char)
546
547     def dataReceived(self, data):
548         if options['ansilog']:
549             print repr(data)
550         frame.write(data)
551
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)
556             sys.stderr.flush()
557
558     def eofReceived(self):
559         log.msg('got eof')
560         sys.stdin.close()
561
562     def closed(self):
563         log.msg('closed %s' % self)
564         if len(self.conn.channels) == 1: # just us left
565             reactor.stop()
566
567     def request_exit_status(self, data):
568         global exitStatus
569         exitStatus = int(struct.unpack('>L', data)[0])
570         log.msg('exit status: %s' % exitStatus)
571
572     def sendEOF(self):
573         self.conn.sendEOF(self)
574
575 if __name__=="__main__":
576     run()