1
# Copyright (c) 2001-2004 Twisted Matrix Laboratories.
2
# See LICENSE for details.
5
# $Id: tkconch.py,v 1.6 2003/02/22 08:10:15 z3p Exp $
7
""" Implementation module for the `tkconch` command.
10
from __future__ import nested_scopes
12
import Tkinter, tkFileDialog, tkFont, tkMessageBox, string
13
from twisted.conch.ui import tkvt100
14
from twisted.conch.ssh import transport, userauth, connection, common, keys
15
from twisted.conch.ssh import session, forwarding, channel
16
from twisted.conch.client.default import isInKnownHosts
17
from twisted.internet import reactor, defer, protocol, tksupport
18
from twisted.python import usage, log
20
import os, sys, getpass, struct, base64, signal
22
class TkConchMenu(Tkinter.Frame):
23
def __init__(self, *args, **params):
24
## Standard heading: initialization
25
apply(Tkinter.Frame.__init__, (self,) + args, params)
27
self.master.title('TkConch')
28
self.localRemoteVar = Tkinter.StringVar()
29
self.localRemoteVar.set('local')
31
Tkinter.Label(self, anchor='w', justify='left', text='Hostname').grid(column=1, row=1, sticky='w')
32
self.host = Tkinter.Entry(self)
33
self.host.grid(column=2, columnspan=2, row=1, sticky='nesw')
35
Tkinter.Label(self, anchor='w', justify='left', text='Port').grid(column=1, row=2, sticky='w')
36
self.port = Tkinter.Entry(self)
37
self.port.grid(column=2, columnspan=2, row=2, sticky='nesw')
39
Tkinter.Label(self, anchor='w', justify='left', text='Username').grid(column=1, row=3, sticky='w')
40
self.user = Tkinter.Entry(self)
41
self.user.grid(column=2, columnspan=2, row=3, sticky='nesw')
43
Tkinter.Label(self, anchor='w', justify='left', text='Command').grid(column=1, row=4, sticky='w')
44
self.command = Tkinter.Entry(self)
45
self.command.grid(column=2, columnspan=2, row=4, sticky='nesw')
47
Tkinter.Label(self, anchor='w', justify='left', text='Identity').grid(column=1, row=5, sticky='w')
48
self.identity = Tkinter.Entry(self)
49
self.identity.grid(column=2, row=5, sticky='nesw')
50
Tkinter.Button(self, command=self.getIdentityFile, text='Browse').grid(column=3, row=5, sticky='nesw')
52
Tkinter.Label(self, text='Port Forwarding').grid(column=1, row=6, sticky='w')
53
self.forwards = Tkinter.Listbox(self, height=0, width=0)
54
self.forwards.grid(column=2, columnspan=2, row=6, sticky='nesw')
55
Tkinter.Button(self, text='Add', command=self.addForward).grid(column=1, row=7)
56
Tkinter.Button(self, text='Remove', command=self.removeForward).grid(column=1, row=8)
57
self.forwardPort = Tkinter.Entry(self)
58
self.forwardPort.grid(column=2, row=7, sticky='nesw')
59
Tkinter.Label(self, text='Port').grid(column=3, row=7, sticky='nesw')
60
self.forwardHost = Tkinter.Entry(self)
61
self.forwardHost.grid(column=2, row=8, sticky='nesw')
62
Tkinter.Label(self, text='Host').grid(column=3, row=8, sticky='nesw')
63
self.localForward = Tkinter.Radiobutton(self, text='Local', variable=self.localRemoteVar, value='local')
64
self.localForward.grid(column=2, row=9)
65
self.remoteForward = Tkinter.Radiobutton(self, text='Remote', variable=self.localRemoteVar, value='remote')
66
self.remoteForward.grid(column=3, row=9)
68
Tkinter.Label(self, text='Advanced Options').grid(column=1, columnspan=3, row=10, sticky='nesw')
70
Tkinter.Label(self, anchor='w', justify='left', text='Cipher').grid(column=1, row=11, sticky='w')
71
self.cipher = Tkinter.Entry(self, name='cipher')
72
self.cipher.grid(column=2, columnspan=2, row=11, sticky='nesw')
74
Tkinter.Label(self, anchor='w', justify='left', text='MAC').grid(column=1, row=12, sticky='w')
75
self.mac = Tkinter.Entry(self, name='mac')
76
self.mac.grid(column=2, columnspan=2, row=12, sticky='nesw')
78
Tkinter.Label(self, anchor='w', justify='left', text='Escape Char').grid(column=1, row=13, sticky='w')
79
self.escape = Tkinter.Entry(self, name='escape')
80
self.escape.grid(column=2, columnspan=2, row=13, sticky='nesw')
81
Tkinter.Button(self, text='Connect!', command=self.doConnect).grid(column=1, columnspan=3, row=14, sticky='nesw')
84
self.grid_rowconfigure(6, weight=1, minsize=64)
85
self.grid_columnconfigure(2, weight=1, minsize=2)
87
self.master.protocol("WM_DELETE_WINDOW", sys.exit)
90
def getIdentityFile(self):
91
r = tkFileDialog.askopenfilename()
93
self.identity.delete(0, Tkinter.END)
94
self.identity.insert(Tkinter.END, r)
97
port = self.forwardPort.get()
98
self.forwardPort.delete(0, Tkinter.END)
99
host = self.forwardHost.get()
100
self.forwardHost.delete(0, Tkinter.END)
101
if self.localRemoteVar.get() == 'local':
102
self.forwards.insert(Tkinter.END, 'L:%s:%s' % (port, host))
104
self.forwards.insert(Tkinter.END, 'R:%s:%s' % (port, host))
106
def removeForward(self):
107
cur = self.forwards.curselection()
109
self.forwards.remove(cur[0])
113
options['host'] = self.host.get()
114
options['port'] = self.port.get()
115
options['user'] = self.user.get()
116
options['command'] = self.command.get()
117
cipher = self.cipher.get()
119
escape = self.escape.get()
121
if cipher in SSHClientTransport.supportedCiphers:
122
SSHClientTransport.supportedCiphers = [cipher]
124
tkMessageBox.showerror('TkConch', 'Bad cipher.')
128
if mac in SSHClientTransport.supportedMACs:
129
SSHClientTransport.supportedMACs = [mac]
131
tkMessageBox.showerror('TkConch', 'Bad MAC.')
136
options['escape'] = None
137
elif escape[0] == '^' and len(escape) == 2:
138
options['escape'] = chr(ord(escape[1])-64)
139
elif len(escape) == 1:
140
options['escape'] = escape
142
tkMessageBox.showerror('TkConch', "Bad escape character '%s'." % escape)
145
if self.identity.get():
146
options.identitys.append(self.identity.get())
148
for line in self.forwards.get(0,Tkinter.END):
150
options.opt_localforward(line[2:])
152
options.opt_remoteforward(line[2:])
154
if '@' in options['host']:
155
options['user'], options['host'] = options['host'].split('@',1)
157
if (not options['host'] or not options['user']) and finished:
158
tkMessageBox.showerror('TkConch', 'Missing host or username.')
162
self.master.destroy()
165
log.startLogging(sys.stderr)
169
log.deferr = handleError # HACK
170
if not options.identitys:
171
options.identitys = ['~/.ssh/id_rsa', '~/.ssh/id_dsa']
172
host = options['host']
173
port = int(options['port'] or 22)
175
reactor.connectTCP(host, port, SSHClientFactory())
176
frame.master.deiconify()
177
frame.master.title('%s@%s - TkConch' % (options['user'], options['host']))
181
class GeneralOptions(usage.Options):
182
synopsis = """Usage: tkconch [options] host [command]
185
optParameters = [['user', 'l', None, 'Log in using this user name.'],
186
['identity', 'i', '~/.ssh/identity', 'Identity for public key authentication'],
187
['escape', 'e', '~', "Set escape character; ``none'' = disable"],
188
['cipher', 'c', None, 'Select encryption algorithm.'],
189
['macs', 'm', None, 'Specify MAC algorithms for protocol version 2.'],
190
['port', 'p', None, 'Connect to this port. Server must be on the same port.'],
191
['localforward', 'L', None, 'listen-port:host:port Forward local port to remote address'],
192
['remoteforward', 'R', None, 'listen-port:host:port Forward remote port to local address'],
195
optFlags = [['tty', 't', 'Tty; allocate a tty even if command is given.'],
196
['notty', 'T', 'Do not allocate a tty.'],
197
['version', 'V', 'Display version number only.'],
198
['compress', 'C', 'Enable compression.'],
199
['noshell', 'N', 'Do not execute a shell or command.'],
200
['subsystem', 's', 'Invoke command (mandatory) as SSH2 subsystem.'],
201
['log', 'v', 'Log to stderr'],
202
['ansilog', 'a', 'Print the receieved data to stdout']]
204
#zsh_altArgDescr = {"foo":"use this description for foo instead"}
205
#zsh_multiUse = ["foo", "bar"]
206
zsh_mutuallyExclusive = [("tty", "notty")]
207
zsh_actions = {"cipher":"(%s)" % " ".join(transport.SSHClientTransport.supportedCiphers),
208
"macs":"(%s)" % " ".join(transport.SSHClientTransport.supportedMACs)}
209
zsh_actionDescr = {"localforward":"listen-port:host:port",
210
"remoteforward":"listen-port:host:port"}
211
# user, host, or user@host completion similar to zsh's ssh completion
212
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}',
219
def opt_identity(self, i):
220
self.identitys.append(i)
222
def opt_localforward(self, f):
223
localPort, remoteHost, remotePort = f.split(':') # doesn't do v6 yet
224
localPort = int(localPort)
225
remotePort = int(remotePort)
226
self.localForwards.append((localPort, (remoteHost, remotePort)))
228
def opt_remoteforward(self, f):
229
remotePort, connHost, connPort = f.split(':') # doesn't do v6 yet
230
remotePort = int(remotePort)
231
connPort = int(connPort)
232
self.remoteForwards.append((remotePort, (connHost, connPort)))
234
def opt_compress(self):
235
SSHClientTransport.supportedCompressions[0:1] = ['zlib']
237
def parseArgs(self, *args):
239
self['host'] = args[0]
240
self['command'] = ' '.join(args[1:])
245
# Rest of code in "run"
251
def deferredAskFrame(question, echo):
253
raise "can't ask 2 questions at once!"
256
def gotChar(ch, resp=resp):
262
stresp = ''.join(resp)
264
frame.callback = None
267
elif 32 <= ord(ch) < 127:
271
elif ord(ch) == 8 and resp: # BS
272
if echo: frame.write('\x08 \x08')
274
frame.callback = gotChar
275
frame.write(question)
276
frame.canvas.focus_force()
280
global menu, options, frame
282
if '-l' in args: # cvs is an idiot
284
args = args[i:i+2]+args
289
if arg[:2] == '-o' and args[i+1][0]!='-':
290
args[i:i+2] = [] # suck on it scp
295
top = Tkinter.Toplevel()
296
menu = TkConchMenu(top)
297
menu.pack(side=Tkinter.TOP, fill=Tkinter.BOTH, expand=1)
298
options = GeneralOptions()
300
options.parseOptions(args)
301
except usage.UsageError, u:
302
print 'ERROR: %s' % u
305
for k,v in options.items():
306
if v and hasattr(menu, k):
307
getattr(menu,k).insert(Tkinter.END, v)
308
for (p, (rh, rp)) in options.localForwards:
309
menu.forwards.insert(Tkinter.END, 'L:%s:%s:%s' % (p, rh, rp))
310
options.localForwards = []
311
for (p, (rh, rp)) in options.remoteForwards:
312
menu.forwards.insert(Tkinter.END, 'R:%s:%s:%s' % (p, rh, rp))
313
options.remoteForwards = []
314
frame = tkvt100.VT100Frame(root, callback=None)
315
root.geometry('%dx%d'%(tkvt100.fontWidth*frame.width+3, tkvt100.fontHeight*frame.height+3))
316
frame.pack(side = Tkinter.TOP)
317
tksupport.install(root)
319
if (options['host'] and options['user']) or '@' in options['host']:
327
from twisted.python import failure
330
log.err(failure.Failure())
334
class SSHClientFactory(protocol.ClientFactory):
337
def stopFactory(self):
340
def buildProtocol(self, addr):
341
return SSHClientTransport()
343
def clientConnectionFailed(self, connector, reason):
344
tkMessageBox.showwarning('TkConch','Connection Failed, Reason:\n %s: %s' % (reason.type, reason.value))
346
class SSHClientTransport(transport.SSHClientTransport):
348
def receiveError(self, code, desc):
350
exitStatus = 'conch:\tRemote side disconnected with error code %i\nconch:\treason: %s' % (code, desc)
352
def sendDisconnect(self, code, reason):
354
exitStatus = 'conch:\tSending disconnect with error code %i\nconch:\treason: %s' % (code, reason)
355
transport.SSHClientTransport.sendDisconnect(self, code, reason)
357
def receiveDebug(self, alwaysDisplay, message, lang):
359
if alwaysDisplay or options['log']:
360
log.msg('Received Debug Message: %s' % message)
362
def verifyHostKey(self, pubKey, fingerprint):
363
#d = defer.Deferred()
364
#d.addCallback(lambda x:defer.succeed(1))
367
goodKey = isInKnownHosts(options['host'], pubKey, {'known-hosts': None})
368
if goodKey == 1: # good key
369
return defer.succeed(1)
370
elif goodKey == 2: # AAHHHHH changed
371
return defer.fail(error.ConchError('bad host key'))
373
if options['host'] == self.transport.getPeer()[1]:
374
host = options['host']
375
khHost = options['host']
377
host = '%s (%s)' % (options['host'],
378
self.transport.getPeer()[1])
379
khHost = '%s,%s' % (options['host'],
380
self.transport.getPeer()[1])
381
keyType = common.getNS(pubKey)[0]
382
ques = """The authenticity of host '%s' can't be established.\r
383
%s key fingerprint is %s.""" % (host,
384
{'ssh-dss':'DSA', 'ssh-rsa':'RSA'}[keyType],
386
ques+='\r\nAre you sure you want to continue connecting (yes/no)? '
387
return deferredAskFrame(ques, 1).addCallback(self._cbVerifyHostKey, pubKey, khHost, keyType)
389
def _cbVerifyHostKey(self, ans, pubKey, khHost, keyType):
390
if ans.lower() not in ('yes', 'no'):
391
return deferredAskFrame("Please type 'yes' or 'no': ",1).addCallback(self._cbVerifyHostKey, pubKey, khHost, keyType)
392
if ans.lower() == 'no':
393
frame.write('Host key verification failed.\r\n')
394
raise error.ConchError('bad host key')
396
frame.write("Warning: Permanently added '%s' (%s) to the list of known hosts.\r\n" % (khHost, {'ssh-dss':'DSA', 'ssh-rsa':'RSA'}[keyType]))
397
known_hosts = open(os.path.expanduser('~/.ssh/known_hosts'), 'a')
398
encodedKey = base64.encodestring(pubKey).replace('\n', '')
399
known_hosts.write('\n%s %s %s' % (khHost, keyType, encodedKey))
403
raise error.ConchError
405
def connectionSecure(self):
407
user = options['user']
409
user = getpass.getuser()
410
self.requestService(SSHUserAuthClient(user, SSHConnection()))
412
class SSHUserAuthClient(userauth.SSHUserAuthClient):
415
def getPassword(self, prompt = None):
417
prompt = "%s@%s's password: " % (self.user, options['host'])
418
return deferredAskFrame(prompt,0)
420
def getPublicKey(self):
421
files = [x for x in options.identitys if x not in self.usedFiles]
426
self.usedFiles.append(file)
427
file = os.path.expanduser(file)
429
if not os.path.exists(file):
432
return keys.getPublicKeyString(file)
434
return self.getPublicKey() # try again
436
def getPrivateKey(self):
437
file = os.path.expanduser(self.usedFiles[-1])
438
if not os.path.exists(file):
441
return defer.succeed(keys.getPrivateKeyObject(file))
442
except keys.BadKeyError, e:
443
if e.args[0] == 'encrypted key with no password':
444
prompt = "Enter passphrase for key '%s': " % \
446
return deferredAskFrame(prompt, 0).addCallback(self._cbGetPrivateKey, 0)
447
def _cbGetPrivateKey(self, ans, count):
448
file = os.path.expanduser(self.usedFiles[-1])
450
return keys.getPrivateKeyObject(file, password = ans)
451
except keys.BadKeyError:
454
prompt = "Enter passphrase for key '%s': " % \
456
return deferredAskFrame(prompt, 0).addCallback(self._cbGetPrivateKey, count+1)
458
class SSHConnection(connection.SSHConnection):
459
def serviceStarted(self):
460
if not options['noshell']:
461
self.openChannel(SSHSession())
462
if options.localForwards:
463
for localPort, hostport in options.localForwards:
464
reactor.listenTCP(localPort,
465
forwarding.SSHListenForwardingFactory(self,
467
forwarding.SSHListenClientForwardingChannel))
468
if options.remoteForwards:
469
for remotePort, hostport in options.remoteForwards:
470
log.msg('asking for remote forwarding for %s:%s' %
471
(remotePort, hostport))
472
data = forwarding.packGlobal_tcpip_forward(
473
('0.0.0.0', remotePort))
474
d = self.sendGlobalRequest('tcpip-forward', data)
475
self.remoteForwards[remotePort] = hostport
477
class SSHSession(channel.SSHChannel):
481
def channelOpen(self, foo):
482
#global globalSession
483
#globalSession = self
484
# turn off local echo
486
c = session.SSHSessionClient()
487
if options['escape']:
488
c.dataReceived = self.handleInput
490
c.dataReceived = self.write
491
c.connectionLost = self.sendEOF
492
frame.callback = c.dataReceived
493
frame.canvas.focus_force()
494
if options['subsystem']:
495
self.conn.sendRequest(self, 'subsystem', \
496
common.NS(options['command']))
497
elif options['command']:
499
term = os.environ.get('TERM', 'xterm')
500
#winsz = fcntl.ioctl(fd, tty.TIOCGWINSZ, '12345678')
501
winSize = (25,80,0,0) #struct.unpack('4H', winsz)
502
ptyReqData = session.packRequest_pty_req(term, winSize, '')
503
self.conn.sendRequest(self, 'pty-req', ptyReqData)
504
self.conn.sendRequest(self, 'exec', \
505
common.NS(options['command']))
507
if not options['notty']:
508
term = os.environ.get('TERM', 'xterm')
509
#winsz = fcntl.ioctl(fd, tty.TIOCGWINSZ, '12345678')
510
winSize = (25,80,0,0) #struct.unpack('4H', winsz)
511
ptyReqData = session.packRequest_pty_req(term, winSize, '')
512
self.conn.sendRequest(self, 'pty-req', ptyReqData)
513
self.conn.sendRequest(self, 'shell', '')
514
self.conn.transport.transport.setTcpNoDelay(1)
516
def handleInput(self, char):
517
#log.msg('handling %s' % repr(char))
518
if char in ('\n', '\r'):
521
elif self.escapeMode == 1 and char == options['escape']:
523
elif self.escapeMode == 2:
524
self.escapeMode = 1 # so we can chain escapes together
525
if char == '.': # disconnect
526
log.msg('disconnecting from escape')
529
elif char == '\x1a': # ^Z, suspend
530
# following line courtesy of Erwin@freenode
531
os.kill(os.getpid(), signal.SIGSTOP)
533
elif char == 'R': # rekey connection
534
log.msg('rekeying connection')
535
self.conn.transport.sendKexInit()
537
self.write('~' + char)
542
def dataReceived(self, data):
543
if options['ansilog']:
547
def extReceived(self, t, data):
548
if t==connection.EXTENDED_DATA_STDERR:
549
log.msg('got %s stderr data' % len(data))
550
sys.stderr.write(data)
553
def eofReceived(self):
558
log.msg('closed %s' % self)
559
if len(self.conn.channels) == 1: # just us left
562
def request_exit_status(self, data):
564
exitStatus = int(struct.unpack('>L', data)[0])
565
log.msg('exit status: %s' % exitStatus)
568
self.conn.sendEOF(self)
570
if __name__=="__main__":