|
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674 |
- # -*- test-case-name: twisted.conch.test.test_scripts -*-
- # Copyright (c) Twisted Matrix Laboratories.
- # See LICENSE for details.
-
- """
- Implementation module for the `tkconch` command.
- """
-
-
- import base64
- import getpass
- import os
- import signal
- import struct
- import sys
- import tkinter as Tkinter
- import tkinter.filedialog as tkFileDialog
- import tkinter.messagebox as tkMessageBox
- from typing import List, Tuple
-
- from twisted.conch import error
- from twisted.conch.client.default import isInKnownHosts
- from twisted.conch.ssh import (
- channel,
- common,
- connection,
- forwarding,
- keys,
- session,
- transport,
- userauth,
- )
- from twisted.conch.ui import tkvt100
- from twisted.internet import defer, protocol, reactor, tksupport
- from twisted.python import log, usage
-
-
- class TkConchMenu(Tkinter.Frame):
- def __init__(self, *args, **params):
- ## Standard heading: initialization
- Tkinter.Frame.__init__(self, *args, **params)
-
- self.master.title("TkConch")
- self.localRemoteVar = Tkinter.StringVar()
- self.localRemoteVar.set("local")
-
- Tkinter.Label(self, anchor="w", justify="left", text="Hostname").grid(
- column=1, row=1, sticky="w"
- )
- self.host = Tkinter.Entry(self)
- self.host.grid(column=2, columnspan=2, row=1, sticky="nesw")
-
- Tkinter.Label(self, anchor="w", justify="left", text="Port").grid(
- column=1, row=2, sticky="w"
- )
- self.port = Tkinter.Entry(self)
- self.port.grid(column=2, columnspan=2, row=2, sticky="nesw")
-
- Tkinter.Label(self, anchor="w", justify="left", text="Username").grid(
- column=1, row=3, sticky="w"
- )
- self.user = Tkinter.Entry(self)
- self.user.grid(column=2, columnspan=2, row=3, sticky="nesw")
-
- Tkinter.Label(self, anchor="w", justify="left", text="Command").grid(
- column=1, row=4, sticky="w"
- )
- self.command = Tkinter.Entry(self)
- self.command.grid(column=2, columnspan=2, row=4, sticky="nesw")
-
- Tkinter.Label(self, anchor="w", justify="left", text="Identity").grid(
- column=1, row=5, sticky="w"
- )
- self.identity = Tkinter.Entry(self)
- self.identity.grid(column=2, row=5, sticky="nesw")
- Tkinter.Button(self, command=self.getIdentityFile, text="Browse").grid(
- column=3, row=5, sticky="nesw"
- )
-
- Tkinter.Label(self, text="Port Forwarding").grid(column=1, row=6, sticky="w")
- self.forwards = Tkinter.Listbox(self, height=0, width=0)
- self.forwards.grid(column=2, columnspan=2, row=6, sticky="nesw")
- Tkinter.Button(self, text="Add", command=self.addForward).grid(column=1, row=7)
- Tkinter.Button(self, text="Remove", command=self.removeForward).grid(
- column=1, row=8
- )
- self.forwardPort = Tkinter.Entry(self)
- self.forwardPort.grid(column=2, row=7, sticky="nesw")
- Tkinter.Label(self, text="Port").grid(column=3, row=7, sticky="nesw")
- self.forwardHost = Tkinter.Entry(self)
- self.forwardHost.grid(column=2, row=8, sticky="nesw")
- Tkinter.Label(self, text="Host").grid(column=3, row=8, sticky="nesw")
- self.localForward = Tkinter.Radiobutton(
- self, text="Local", variable=self.localRemoteVar, value="local"
- )
- self.localForward.grid(column=2, row=9)
- self.remoteForward = Tkinter.Radiobutton(
- self, text="Remote", variable=self.localRemoteVar, value="remote"
- )
- self.remoteForward.grid(column=3, row=9)
-
- Tkinter.Label(self, text="Advanced Options").grid(
- column=1, columnspan=3, row=10, sticky="nesw"
- )
-
- Tkinter.Label(self, anchor="w", justify="left", text="Cipher").grid(
- column=1, row=11, sticky="w"
- )
- self.cipher = Tkinter.Entry(self, name="cipher")
- self.cipher.grid(column=2, columnspan=2, row=11, sticky="nesw")
-
- Tkinter.Label(self, anchor="w", justify="left", text="MAC").grid(
- column=1, row=12, sticky="w"
- )
- self.mac = Tkinter.Entry(self, name="mac")
- self.mac.grid(column=2, columnspan=2, row=12, sticky="nesw")
-
- Tkinter.Label(self, anchor="w", justify="left", text="Escape Char").grid(
- column=1, row=13, sticky="w"
- )
- self.escape = Tkinter.Entry(self, name="escape")
- self.escape.grid(column=2, columnspan=2, row=13, sticky="nesw")
- Tkinter.Button(self, text="Connect!", command=self.doConnect).grid(
- column=1, columnspan=3, row=14, sticky="nesw"
- )
-
- # Resize behavior(s)
- self.grid_rowconfigure(6, weight=1, minsize=64)
- self.grid_columnconfigure(2, weight=1, minsize=2)
-
- self.master.protocol("WM_DELETE_WINDOW", sys.exit)
-
- def getIdentityFile(self):
- r = tkFileDialog.askopenfilename()
- if r:
- self.identity.delete(0, Tkinter.END)
- self.identity.insert(Tkinter.END, r)
-
- def addForward(self):
- port = self.forwardPort.get()
- self.forwardPort.delete(0, Tkinter.END)
- host = self.forwardHost.get()
- self.forwardHost.delete(0, Tkinter.END)
- if self.localRemoteVar.get() == "local":
- self.forwards.insert(Tkinter.END, f"L:{port}:{host}")
- else:
- self.forwards.insert(Tkinter.END, f"R:{port}:{host}")
-
- def removeForward(self):
- cur = self.forwards.curselection()
- if cur:
- self.forwards.remove(cur[0])
-
- def doConnect(self):
- finished = 1
- options["host"] = self.host.get()
- options["port"] = self.port.get()
- options["user"] = self.user.get()
- options["command"] = self.command.get()
- cipher = self.cipher.get()
- mac = self.mac.get()
- escape = self.escape.get()
- if cipher:
- if cipher in SSHClientTransport.supportedCiphers:
- SSHClientTransport.supportedCiphers = [cipher]
- else:
- tkMessageBox.showerror("TkConch", "Bad cipher.")
- finished = 0
-
- if mac:
- if mac in SSHClientTransport.supportedMACs:
- SSHClientTransport.supportedMACs = [mac]
- elif finished:
- tkMessageBox.showerror("TkConch", "Bad MAC.")
- finished = 0
-
- if escape:
- if escape == "none":
- options["escape"] = None
- elif escape[0] == "^" and len(escape) == 2:
- options["escape"] = chr(ord(escape[1]) - 64)
- elif len(escape) == 1:
- options["escape"] = escape
- elif finished:
- tkMessageBox.showerror("TkConch", "Bad escape character '%s'." % escape)
- finished = 0
-
- if self.identity.get():
- options.identitys.append(self.identity.get())
-
- for line in self.forwards.get(0, Tkinter.END):
- if line[0] == "L":
- options.opt_localforward(line[2:])
- else:
- options.opt_remoteforward(line[2:])
-
- if "@" in options["host"]:
- options["user"], options["host"] = options["host"].split("@", 1)
-
- if (not options["host"] or not options["user"]) and finished:
- tkMessageBox.showerror("TkConch", "Missing host or username.")
- finished = 0
- if finished:
- self.master.quit()
- self.master.destroy()
- if options["log"]:
- realout = sys.stdout
- log.startLogging(sys.stderr)
- sys.stdout = realout
- else:
- log.discardLogs()
- log.deferr = handleError # HACK
- if not options.identitys:
- options.identitys = ["~/.ssh/id_rsa", "~/.ssh/id_dsa"]
- host = options["host"]
- port = int(options["port"] or 22)
- log.msg((host, port))
- reactor.connectTCP(host, port, SSHClientFactory())
- frame.master.deiconify()
- frame.master.title(
- "{}@{} - TkConch".format(options["user"], options["host"])
- )
- else:
- self.focus()
-
-
- class GeneralOptions(usage.Options):
- synopsis = """Usage: tkconch [options] host [command]
- """
-
- optParameters = [
- ["user", "l", None, "Log in using this user name."],
- ["identity", "i", "~/.ssh/identity", "Identity for public key authentication"],
- ["escape", "e", "~", "Set escape character; ``none'' = disable"],
- ["cipher", "c", None, "Select encryption algorithm."],
- ["macs", "m", None, "Specify MAC algorithms for protocol version 2."],
- ["port", "p", None, "Connect to this port. Server must be on the same port."],
- [
- "localforward",
- "L",
- None,
- "listen-port:host:port Forward local port to remote address",
- ],
- [
- "remoteforward",
- "R",
- None,
- "listen-port:host:port Forward remote port to local address",
- ],
- ]
-
- optFlags = [
- ["tty", "t", "Tty; allocate a tty even if command is given."],
- ["notty", "T", "Do not allocate a tty."],
- ["version", "V", "Display version number only."],
- ["compress", "C", "Enable compression."],
- ["noshell", "N", "Do not execute a shell or command."],
- ["subsystem", "s", "Invoke command (mandatory) as SSH2 subsystem."],
- ["log", "v", "Log to stderr"],
- ["ansilog", "a", "Print the received data to stdout"],
- ]
-
- _ciphers = transport.SSHClientTransport.supportedCiphers
- _macs = transport.SSHClientTransport.supportedMACs
-
- compData = usage.Completions(
- mutuallyExclusive=[("tty", "notty")],
- optActions={
- "cipher": usage.CompleteList([v.decode() for v in _ciphers]),
- "macs": usage.CompleteList([v.decode() for v in _macs]),
- "localforward": usage.Completer(descr="listen-port:host:port"),
- "remoteforward": usage.Completer(descr="listen-port:host:port"),
- },
- extraActions=[
- usage.CompleteUserAtHost(),
- usage.Completer(descr="command"),
- usage.Completer(descr="argument", repeat=True),
- ],
- )
-
- identitys: List[str] = []
- localForwards: List[Tuple[int, Tuple[int, int]]] = []
- remoteForwards: List[Tuple[int, Tuple[int, int]]] = []
-
- def opt_identity(self, i):
- self.identitys.append(i)
-
- def opt_localforward(self, f):
- localPort, remoteHost, remotePort = f.split(":") # doesn't do v6 yet
- localPort = int(localPort)
- remotePort = int(remotePort)
- self.localForwards.append((localPort, (remoteHost, remotePort)))
-
- def opt_remoteforward(self, f):
- remotePort, connHost, connPort = f.split(":") # doesn't do v6 yet
- remotePort = int(remotePort)
- connPort = int(connPort)
- self.remoteForwards.append((remotePort, (connHost, connPort)))
-
- def opt_compress(self):
- SSHClientTransport.supportedCompressions[0:1] = ["zlib"]
-
- def parseArgs(self, *args):
- if args:
- self["host"] = args[0]
- self["command"] = " ".join(args[1:])
- else:
- self["host"] = ""
- self["command"] = ""
-
-
- # Rest of code in "run"
- options = None
- menu = None
- exitStatus = 0
- frame = None
-
-
- def deferredAskFrame(question, echo):
- if frame.callback:
- raise ValueError("can't ask 2 questions at once!")
- d = defer.Deferred()
- resp = []
-
- def gotChar(ch, resp=resp):
- if not ch:
- return
- if ch == "\x03": # C-c
- reactor.stop()
- if ch == "\r":
- frame.write("\r\n")
- stresp = "".join(resp)
- del resp
- frame.callback = None
- d.callback(stresp)
- return
- elif 32 <= ord(ch) < 127:
- resp.append(ch)
- if echo:
- frame.write(ch)
- elif ord(ch) == 8 and resp: # BS
- if echo:
- frame.write("\x08 \x08")
- resp.pop()
-
- frame.callback = gotChar
- frame.write(question)
- frame.canvas.focus_force()
- return d
-
-
- def run():
- global menu, options, frame
- args = sys.argv[1:]
- if "-l" in args: # cvs is an idiot
- i = args.index("-l")
- args = args[i : i + 2] + args
- del args[i + 2 : i + 4]
- for arg in args[:]:
- try:
- i = args.index(arg)
- if arg[:2] == "-o" and args[i + 1][0] != "-":
- args[i : i + 2] = [] # suck on it scp
- except ValueError:
- pass
- root = Tkinter.Tk()
- root.withdraw()
- top = Tkinter.Toplevel()
- menu = TkConchMenu(top)
- menu.pack(side=Tkinter.TOP, fill=Tkinter.BOTH, expand=1)
- options = GeneralOptions()
- try:
- options.parseOptions(args)
- except usage.UsageError as u:
- print("ERROR: %s" % u)
- options.opt_help()
- sys.exit(1)
- for k, v in options.items():
- if v and hasattr(menu, k):
- getattr(menu, k).insert(Tkinter.END, v)
- for (p, (rh, rp)) in options.localForwards:
- menu.forwards.insert(Tkinter.END, f"L:{p}:{rh}:{rp}")
- options.localForwards = []
- for (p, (rh, rp)) in options.remoteForwards:
- menu.forwards.insert(Tkinter.END, f"R:{p}:{rh}:{rp}")
- options.remoteForwards = []
- frame = tkvt100.VT100Frame(root, callback=None)
- root.geometry(
- "%dx%d"
- % (tkvt100.fontWidth * frame.width + 3, tkvt100.fontHeight * frame.height + 3)
- )
- frame.pack(side=Tkinter.TOP)
- tksupport.install(root)
- root.withdraw()
- if (options["host"] and options["user"]) or "@" in options["host"]:
- menu.doConnect()
- else:
- top.mainloop()
- reactor.run()
- sys.exit(exitStatus)
-
-
- def handleError():
- from twisted.python import failure
-
- global exitStatus
- exitStatus = 2
- log.err(failure.Failure())
- reactor.stop()
- raise
-
-
- class SSHClientFactory(protocol.ClientFactory):
- noisy = True
-
- def stopFactory(self):
- reactor.stop()
-
- def buildProtocol(self, addr):
- return SSHClientTransport()
-
- def clientConnectionFailed(self, connector, reason):
- tkMessageBox.showwarning(
- "TkConch",
- f"Connection Failed, Reason:\n {reason.type}: {reason.value}",
- )
-
-
- class SSHClientTransport(transport.SSHClientTransport):
- def receiveError(self, code, desc):
- global exitStatus
- exitStatus = (
- "conch:\tRemote side disconnected with error code %i\nconch:\treason: %s"
- % (code, desc)
- )
-
- def sendDisconnect(self, code, reason):
- global exitStatus
- exitStatus = (
- "conch:\tSending disconnect with error code %i\nconch:\treason: %s"
- % (code, reason)
- )
- transport.SSHClientTransport.sendDisconnect(self, code, reason)
-
- def receiveDebug(self, alwaysDisplay, message, lang):
- global options
- if alwaysDisplay or options["log"]:
- log.msg("Received Debug Message: %s" % message)
-
- def verifyHostKey(self, pubKey, fingerprint):
- # d = defer.Deferred()
- # d.addCallback(lambda x:defer.succeed(1))
- # d.callback(2)
- # return d
- goodKey = isInKnownHosts(options["host"], pubKey, {"known-hosts": None})
- if goodKey == 1: # good key
- return defer.succeed(1)
- elif goodKey == 2: # AAHHHHH changed
- return defer.fail(error.ConchError("bad host key"))
- else:
- if options["host"] == self.transport.getPeer().host:
- host = options["host"]
- khHost = options["host"]
- else:
- host = "{} ({})".format(options["host"], self.transport.getPeer().host)
- khHost = "{},{}".format(options["host"], self.transport.getPeer().host)
- keyType = common.getNS(pubKey)[0]
- ques = """The authenticity of host '{}' can't be established.\r
- {} key fingerprint is {}.""".format(
- host,
- {b"ssh-dss": "DSA", b"ssh-rsa": "RSA"}[keyType],
- fingerprint,
- )
- ques += "\r\nAre you sure you want to continue connecting (yes/no)? "
- return deferredAskFrame(ques, 1).addCallback(
- self._cbVerifyHostKey, pubKey, khHost, keyType
- )
-
- def _cbVerifyHostKey(self, ans, pubKey, khHost, keyType):
- if ans.lower() not in ("yes", "no"):
- return deferredAskFrame("Please type 'yes' or 'no': ", 1).addCallback(
- self._cbVerifyHostKey, pubKey, khHost, keyType
- )
- if ans.lower() == "no":
- frame.write("Host key verification failed.\r\n")
- raise error.ConchError("bad host key")
- try:
- frame.write(
- "Warning: Permanently added '%s' (%s) to the list of "
- "known hosts.\r\n"
- % (khHost, {b"ssh-dss": "DSA", b"ssh-rsa": "RSA"}[keyType])
- )
- with open(os.path.expanduser("~/.ssh/known_hosts"), "a") as known_hosts:
- encodedKey = base64.b64encode(pubKey)
- known_hosts.write(f"\n{khHost} {keyType} {encodedKey}")
- except BaseException:
- log.deferr()
- raise error.ConchError
-
- def connectionSecure(self):
- if options["user"]:
- user = options["user"]
- else:
- user = getpass.getuser()
- self.requestService(SSHUserAuthClient(user, SSHConnection()))
-
-
- class SSHUserAuthClient(userauth.SSHUserAuthClient):
- usedFiles: List[str] = []
-
- def getPassword(self, prompt=None):
- if not prompt:
- prompt = "{}@{}'s password: ".format(self.user, options["host"])
- return deferredAskFrame(prompt, 0)
-
- def getPublicKey(self):
- files = [x for x in options.identitys if x not in self.usedFiles]
- if not files:
- return None
- file = files[0]
- log.msg(file)
- self.usedFiles.append(file)
- file = os.path.expanduser(file)
- file += ".pub"
- if not os.path.exists(file):
- return
- try:
- return keys.Key.fromFile(file).blob()
- except BaseException:
- return self.getPublicKey() # try again
-
- def getPrivateKey(self):
- file = os.path.expanduser(self.usedFiles[-1])
- if not os.path.exists(file):
- return None
- try:
- return defer.succeed(keys.Key.fromFile(file).keyObject)
- except keys.BadKeyError as e:
- if e.args[0] == "encrypted key with no password":
- prompt = "Enter passphrase for key '%s': " % self.usedFiles[-1]
- return deferredAskFrame(prompt, 0).addCallback(self._cbGetPrivateKey, 0)
-
- def _cbGetPrivateKey(self, ans, count):
- file = os.path.expanduser(self.usedFiles[-1])
- try:
- return keys.Key.fromFile(file, password=ans).keyObject
- except keys.BadKeyError:
- if count == 2:
- raise
- prompt = "Enter passphrase for key '%s': " % self.usedFiles[-1]
- return deferredAskFrame(prompt, 0).addCallback(
- self._cbGetPrivateKey, count + 1
- )
-
-
- class SSHConnection(connection.SSHConnection):
- def serviceStarted(self):
- if not options["noshell"]:
- self.openChannel(SSHSession())
- if options.localForwards:
- for localPort, hostport in options.localForwards:
- reactor.listenTCP(
- localPort,
- forwarding.SSHListenForwardingFactory(
- self, hostport, forwarding.SSHListenClientForwardingChannel
- ),
- )
- if options.remoteForwards:
- for remotePort, hostport in options.remoteForwards:
- log.msg(
- "asking for remote forwarding for {}:{}".format(
- remotePort, hostport
- )
- )
- data = forwarding.packGlobal_tcpip_forward(("0.0.0.0", remotePort))
- self.sendGlobalRequest("tcpip-forward", data)
- self.remoteForwards[remotePort] = hostport
-
-
- class SSHSession(channel.SSHChannel):
-
- name = b"session"
-
- def channelOpen(self, foo):
- # global globalSession
- # globalSession = self
- # turn off local echo
- self.escapeMode = 1
- c = session.SSHSessionClient()
- if options["escape"]:
- c.dataReceived = self.handleInput
- else:
- c.dataReceived = self.write
- c.connectionLost = self.sendEOF
- frame.callback = c.dataReceived
- frame.canvas.focus_force()
- if options["subsystem"]:
- self.conn.sendRequest(self, b"subsystem", common.NS(options["command"]))
- elif options["command"]:
- if options["tty"]:
- term = os.environ.get("TERM", "xterm")
- # winsz = fcntl.ioctl(fd, tty.TIOCGWINSZ, '12345678')
- winSize = (25, 80, 0, 0) # struct.unpack('4H', winsz)
- ptyReqData = session.packRequest_pty_req(term, winSize, "")
- self.conn.sendRequest(self, b"pty-req", ptyReqData)
- self.conn.sendRequest(self, "exec", common.NS(options["command"]))
- else:
- if not options["notty"]:
- term = os.environ.get("TERM", "xterm")
- # winsz = fcntl.ioctl(fd, tty.TIOCGWINSZ, '12345678')
- winSize = (25, 80, 0, 0) # struct.unpack('4H', winsz)
- ptyReqData = session.packRequest_pty_req(term, winSize, "")
- self.conn.sendRequest(self, b"pty-req", ptyReqData)
- self.conn.sendRequest(self, b"shell", b"")
- self.conn.transport.transport.setTcpNoDelay(1)
-
- def handleInput(self, char):
- # log.msg('handling %s' % repr(char))
- if char in ("\n", "\r"):
- self.escapeMode = 1
- self.write(char)
- elif self.escapeMode == 1 and char == options["escape"]:
- self.escapeMode = 2
- elif self.escapeMode == 2:
- self.escapeMode = 1 # so we can chain escapes together
- if char == ".": # disconnect
- log.msg("disconnecting from escape")
- reactor.stop()
- return
- elif char == "\x1a": # ^Z, suspend
- # following line courtesy of Erwin@freenode
- os.kill(os.getpid(), signal.SIGSTOP)
- return
- elif char == "R": # rekey connection
- log.msg("rekeying connection")
- self.conn.transport.sendKexInit()
- return
- self.write("~" + char)
- else:
- self.escapeMode = 0
- self.write(char)
-
- def dataReceived(self, data):
- data = data.decode("utf-8")
- if options["ansilog"]:
- print(repr(data))
- frame.write(data)
-
- def extReceived(self, t, data):
- if t == connection.EXTENDED_DATA_STDERR:
- log.msg("got %s stderr data" % len(data))
- sys.stderr.write(data)
- sys.stderr.flush()
-
- def eofReceived(self):
- log.msg("got eof")
- sys.stdin.close()
-
- def closed(self):
- log.msg("closed %s" % self)
- if len(self.conn.channels) == 1: # just us left
- reactor.stop()
-
- def request_exit_status(self, data):
- global exitStatus
- exitStatus = int(struct.unpack(">L", data)[0])
- log.msg("exit status: %s" % exitStatus)
-
- def sendEOF(self):
- self.conn.sendEOF(self)
-
-
- if __name__ == "__main__":
- run()
|