1
# -*- test-case-name: twisted.conch.test.test_scripts -*-
2
# Copyright (c) 2001-2007 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
#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}',
220
def opt_identity(self, i):
221
self.identitys.append(i)
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)))
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)))
235
def opt_compress(self):
236
SSHClientTransport.supportedCompressions[0:1] = ['zlib']
238
def parseArgs(self, *args):
240
self['host'] = args[0]
241
self['command'] = ' '.join(args[1:])
246
# Rest of code in "run"
252
def deferredAskFrame(question, echo):
254
raise ValueError("can't ask 2 questions at once!")
257
def gotChar(ch, resp=resp):
263
stresp = ''.join(resp)
265
frame.callback = None
268
elif 32 <= ord(ch) < 127:
272
elif ord(ch) == 8 and resp: # BS
273
if echo: frame.write('\x08 \x08')
275
frame.callback = gotChar
276
frame.write(question)
277
frame.canvas.focus_force()
281
global menu, options, frame
283
if '-l' in args: # cvs is an idiot
285
args = args[i:i+2]+args
290
if arg[:2] == '-o' and args[i+1][0]!='-':
291
args[i:i+2] = [] # suck on it scp
296
top = Tkinter.Toplevel()
297
menu = TkConchMenu(top)
298
menu.pack(side=Tkinter.TOP, fill=Tkinter.BOTH, expand=1)
299
options = GeneralOptions()
301
options.parseOptions(args)
302
except usage.UsageError, u:
303
print 'ERROR: %s' % u
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)
320
if (options['host'] and options['user']) or '@' in options['host']:
328
from twisted.python import failure
331
log.err(failure.Failure())
335
class SSHClientFactory(protocol.ClientFactory):
338
def stopFactory(self):
341
def buildProtocol(self, addr):
342
return SSHClientTransport()
344
def clientConnectionFailed(self, connector, reason):
345
tkMessageBox.showwarning('TkConch','Connection Failed, Reason:\n %s: %s' % (reason.type, reason.value))
347
class SSHClientTransport(transport.SSHClientTransport):
349
def receiveError(self, code, desc):
351
exitStatus = 'conch:\tRemote side disconnected with error code %i\nconch:\treason: %s' % (code, desc)
353
def sendDisconnect(self, code, reason):
355
exitStatus = 'conch:\tSending disconnect with error code %i\nconch:\treason: %s' % (code, reason)
356
transport.SSHClientTransport.sendDisconnect(self, code, reason)
358
def receiveDebug(self, alwaysDisplay, message, lang):
360
if alwaysDisplay or options['log']:
361
log.msg('Received Debug Message: %s' % message)
363
def verifyHostKey(self, pubKey, fingerprint):
364
#d = defer.Deferred()
365
#d.addCallback(lambda x:defer.succeed(1))
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'))
374
if options['host'] == self.transport.getPeer()[1]:
375
host = options['host']
376
khHost = options['host']
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],
387
ques+='\r\nAre you sure you want to continue connecting (yes/no)? '
388
return deferredAskFrame(ques, 1).addCallback(self._cbVerifyHostKey, pubKey, khHost, keyType)
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')
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))
404
raise error.ConchError
406
def connectionSecure(self):
408
user = options['user']
410
user = getpass.getuser()
411
self.requestService(SSHUserAuthClient(user, SSHConnection()))
413
class SSHUserAuthClient(userauth.SSHUserAuthClient):
416
def getPassword(self, prompt = None):
418
prompt = "%s@%s's password: " % (self.user, options['host'])
419
return deferredAskFrame(prompt,0)
421
def getPublicKey(self):
422
files = [x for x in options.identitys if x not in self.usedFiles]
427
self.usedFiles.append(file)
428
file = os.path.expanduser(file)
430
if not os.path.exists(file):
433
return keys.getPublicKeyString(file)
435
return self.getPublicKey() # try again
437
def getPrivateKey(self):
438
file = os.path.expanduser(self.usedFiles[-1])
439
if not os.path.exists(file):
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': " % \
447
return deferredAskFrame(prompt, 0).addCallback(self._cbGetPrivateKey, 0)
448
def _cbGetPrivateKey(self, ans, count):
449
file = os.path.expanduser(self.usedFiles[-1])
451
return keys.getPrivateKeyObject(file, password = ans)
452
except keys.BadKeyError:
455
prompt = "Enter passphrase for key '%s': " % \
457
return deferredAskFrame(prompt, 0).addCallback(self._cbGetPrivateKey, count+1)
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,
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
478
class SSHSession(channel.SSHChannel):
482
def channelOpen(self, foo):
483
#global globalSession
484
#globalSession = self
485
# turn off local echo
487
c = session.SSHSessionClient()
488
if options['escape']:
489
c.dataReceived = self.handleInput
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']:
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']))
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)
517
def handleInput(self, char):
518
#log.msg('handling %s' % repr(char))
519
if char in ('\n', '\r'):
522
elif self.escapeMode == 1 and char == options['escape']:
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')
530
elif char == '\x1a': # ^Z, suspend
531
# following line courtesy of Erwin@freenode
532
os.kill(os.getpid(), signal.SIGSTOP)
534
elif char == 'R': # rekey connection
535
log.msg('rekeying connection')
536
self.conn.transport.sendKexInit()
538
self.write('~' + char)
543
def dataReceived(self, data):
544
if options['ansilog']:
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)
554
def eofReceived(self):
559
log.msg('closed %s' % self)
560
if len(self.conn.channels) == 1: # just us left
563
def request_exit_status(self, data):
565
exitStatus = int(struct.unpack('>L', data)[0])
566
log.msg('exit status: %s' % exitStatus)
569
self.conn.sendEOF(self)
571
if __name__=="__main__":