RAHHH
This commit is contained in:
@@ -0,0 +1 @@
|
||||
"conch scripts"
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
1002
backend/lib/python3.12/site-packages/twisted/conch/scripts/cftp.py
Normal file
1002
backend/lib/python3.12/site-packages/twisted/conch/scripts/cftp.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,400 @@
|
||||
# -*- test-case-name: twisted.conch.test.test_ckeygen -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Implementation module for the `ckeygen` command.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import getpass
|
||||
import os
|
||||
import platform
|
||||
import socket
|
||||
import sys
|
||||
from collections.abc import Callable
|
||||
from functools import wraps
|
||||
from importlib import reload
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from twisted.conch.ssh import keys
|
||||
from twisted.python import failure, filepath, log, usage
|
||||
|
||||
if getpass.getpass == getpass.unix_getpass: # type: ignore[attr-defined]
|
||||
try:
|
||||
import termios # hack around broken termios
|
||||
|
||||
termios.tcgetattr, termios.tcsetattr
|
||||
except (ImportError, AttributeError):
|
||||
sys.modules["termios"] = None # type: ignore[assignment]
|
||||
reload(getpass)
|
||||
|
||||
supportedKeyTypes = dict()
|
||||
|
||||
|
||||
def _keyGenerator(keyType):
|
||||
def assignkeygenerator(keygenerator):
|
||||
@wraps(keygenerator)
|
||||
def wrapper(*args, **kwargs):
|
||||
return keygenerator(*args, **kwargs)
|
||||
|
||||
supportedKeyTypes[keyType] = wrapper
|
||||
return wrapper
|
||||
|
||||
return assignkeygenerator
|
||||
|
||||
|
||||
class GeneralOptions(usage.Options):
|
||||
synopsis = """Usage: ckeygen [options]
|
||||
"""
|
||||
|
||||
longdesc = "ckeygen manipulates public/private keys in various ways."
|
||||
|
||||
optParameters = [
|
||||
["bits", "b", None, "Number of bits in the key to create."],
|
||||
["filename", "f", None, "Filename of the key file."],
|
||||
["type", "t", None, "Specify type of key to create."],
|
||||
["comment", "C", None, "Provide new comment."],
|
||||
["newpass", "N", None, "Provide new passphrase."],
|
||||
["pass", "P", None, "Provide old passphrase."],
|
||||
["format", "o", "sha256-base64", "Fingerprint format of key file."],
|
||||
[
|
||||
"private-key-subtype",
|
||||
None,
|
||||
None,
|
||||
'OpenSSH private key subtype to write ("PEM" or "v1").',
|
||||
],
|
||||
]
|
||||
|
||||
optFlags = [
|
||||
["fingerprint", "l", "Show fingerprint of key file."],
|
||||
["changepass", "p", "Change passphrase of private key file."],
|
||||
["quiet", "q", "Quiet."],
|
||||
["no-passphrase", None, "Create the key with no passphrase."],
|
||||
["showpub", "y", "Read private key file and print public key."],
|
||||
]
|
||||
|
||||
compData = usage.Completions(
|
||||
optActions={
|
||||
"type": usage.CompleteList(list(supportedKeyTypes.keys())),
|
||||
"private-key-subtype": usage.CompleteList(["PEM", "v1"]),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def run():
|
||||
options = GeneralOptions()
|
||||
try:
|
||||
options.parseOptions(sys.argv[1:])
|
||||
except usage.UsageError as u:
|
||||
print("ERROR: %s" % u)
|
||||
options.opt_help()
|
||||
sys.exit(1)
|
||||
log.discardLogs()
|
||||
log.deferr = handleError # HACK
|
||||
if options["type"]:
|
||||
if options["type"].lower() in supportedKeyTypes:
|
||||
print("Generating public/private %s key pair." % (options["type"]))
|
||||
supportedKeyTypes[options["type"].lower()](options)
|
||||
else:
|
||||
sys.exit(
|
||||
"Key type was %s, must be one of %s"
|
||||
% (options["type"], ", ".join(supportedKeyTypes.keys()))
|
||||
)
|
||||
elif options["fingerprint"]:
|
||||
printFingerprint(options)
|
||||
elif options["changepass"]:
|
||||
changePassPhrase(options)
|
||||
elif options["showpub"]:
|
||||
displayPublicKey(options)
|
||||
else:
|
||||
options.opt_help()
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def enumrepresentation(options):
|
||||
if options["format"] == "md5-hex":
|
||||
options["format"] = keys.FingerprintFormats.MD5_HEX
|
||||
return options
|
||||
elif options["format"] == "sha256-base64":
|
||||
options["format"] = keys.FingerprintFormats.SHA256_BASE64
|
||||
return options
|
||||
else:
|
||||
raise keys.BadFingerPrintFormat(
|
||||
f"Unsupported fingerprint format: {options['format']}"
|
||||
)
|
||||
|
||||
|
||||
def handleError():
|
||||
global exitStatus
|
||||
exitStatus = 2
|
||||
log.err(failure.Failure())
|
||||
raise
|
||||
|
||||
|
||||
@_keyGenerator("rsa")
|
||||
def generateRSAkey(options):
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives.asymmetric import rsa
|
||||
|
||||
if not options["bits"]:
|
||||
options["bits"] = 2048
|
||||
keyPrimitive = rsa.generate_private_key(
|
||||
key_size=int(options["bits"]),
|
||||
public_exponent=65537,
|
||||
backend=default_backend(),
|
||||
)
|
||||
key = keys.Key(keyPrimitive)
|
||||
_saveKey(key, options)
|
||||
|
||||
|
||||
@_keyGenerator("dsa")
|
||||
def generateDSAkey(options):
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives.asymmetric import dsa
|
||||
|
||||
if not options["bits"]:
|
||||
options["bits"] = 1024
|
||||
keyPrimitive = dsa.generate_private_key(
|
||||
key_size=int(options["bits"]),
|
||||
backend=default_backend(),
|
||||
)
|
||||
key = keys.Key(keyPrimitive)
|
||||
_saveKey(key, options)
|
||||
|
||||
|
||||
@_keyGenerator("ecdsa")
|
||||
def generateECDSAkey(options):
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives.asymmetric import ec
|
||||
|
||||
if not options["bits"]:
|
||||
options["bits"] = 256
|
||||
# OpenSSH supports only mandatory sections of RFC5656.
|
||||
# See https://www.openssh.com/txt/release-5.7
|
||||
curve = b"ecdsa-sha2-nistp" + str(options["bits"]).encode("ascii")
|
||||
keyPrimitive = ec.generate_private_key(
|
||||
curve=keys._curveTable[curve], backend=default_backend()
|
||||
)
|
||||
key = keys.Key(keyPrimitive)
|
||||
_saveKey(key, options)
|
||||
|
||||
|
||||
@_keyGenerator("ed25519")
|
||||
def generateEd25519key(options):
|
||||
keyPrimitive = keys.Ed25519PrivateKey.generate()
|
||||
key = keys.Key(keyPrimitive)
|
||||
_saveKey(key, options)
|
||||
|
||||
|
||||
def _defaultPrivateKeySubtype(keyType):
|
||||
"""
|
||||
Return a reasonable default private key subtype for a given key type.
|
||||
|
||||
@type keyType: L{str}
|
||||
@param keyType: A key type, as returned by
|
||||
L{twisted.conch.ssh.keys.Key.type}.
|
||||
|
||||
@rtype: L{str}
|
||||
@return: A private OpenSSH key subtype (C{'PEM'} or C{'v1'}).
|
||||
"""
|
||||
if keyType == "Ed25519":
|
||||
# No PEM format is defined for Ed25519 keys.
|
||||
return "v1"
|
||||
else:
|
||||
return "PEM"
|
||||
|
||||
|
||||
def _getKeyOrDefault(
|
||||
options: Dict[Any, Any],
|
||||
inputCollector: Optional[Callable[[str], str]] = None,
|
||||
keyTypeName: str = "rsa",
|
||||
) -> str:
|
||||
"""
|
||||
If C{options["filename"]} is None, prompt the user to enter a path
|
||||
or attempt to set it to .ssh/id_rsa
|
||||
@param options: command line options
|
||||
@param inputCollector: dependency injection for testing
|
||||
@param keyTypeName: key type or "rsa"
|
||||
"""
|
||||
if inputCollector is None:
|
||||
inputCollector = input
|
||||
filename = options["filename"]
|
||||
if not filename:
|
||||
filename = os.path.expanduser(f"~/.ssh/id_{keyTypeName}")
|
||||
if platform.system() == "Windows":
|
||||
filename = os.path.expanduser(Rf"%HOMEPATH %\.ssh\id_{keyTypeName}")
|
||||
filename = (
|
||||
inputCollector("Enter file in which the key is (%s): " % filename)
|
||||
or filename
|
||||
)
|
||||
return str(filename)
|
||||
|
||||
|
||||
def printFingerprint(options: Dict[Any, Any]) -> None:
|
||||
filename = _getKeyOrDefault(options)
|
||||
if os.path.exists(filename + ".pub"):
|
||||
filename += ".pub"
|
||||
options = enumrepresentation(options)
|
||||
try:
|
||||
key = keys.Key.fromFile(filename)
|
||||
print(
|
||||
"%s %s %s"
|
||||
% (
|
||||
key.size(),
|
||||
key.fingerprint(options["format"]),
|
||||
os.path.basename(filename),
|
||||
)
|
||||
)
|
||||
except keys.BadKeyError:
|
||||
sys.exit("bad key")
|
||||
except FileNotFoundError:
|
||||
sys.exit(f"{filename} could not be opened, please specify a file.")
|
||||
|
||||
|
||||
def changePassPhrase(options):
|
||||
filename = _getKeyOrDefault(options)
|
||||
try:
|
||||
key = keys.Key.fromFile(filename)
|
||||
except keys.EncryptedKeyError:
|
||||
# Raised if password not supplied for an encrypted key
|
||||
if not options.get("pass"):
|
||||
options["pass"] = getpass.getpass("Enter old passphrase: ")
|
||||
try:
|
||||
key = keys.Key.fromFile(filename, passphrase=options["pass"])
|
||||
except keys.BadKeyError:
|
||||
sys.exit("Could not change passphrase: old passphrase error")
|
||||
except keys.EncryptedKeyError as e:
|
||||
sys.exit(f"Could not change passphrase: {e}")
|
||||
except keys.BadKeyError as e:
|
||||
sys.exit(f"Could not change passphrase: {e}")
|
||||
except FileNotFoundError:
|
||||
sys.exit(f"{filename} could not be opened, please specify a file.")
|
||||
|
||||
if not options.get("newpass"):
|
||||
while 1:
|
||||
p1 = getpass.getpass("Enter new passphrase (empty for no passphrase): ")
|
||||
p2 = getpass.getpass("Enter same passphrase again: ")
|
||||
if p1 == p2:
|
||||
break
|
||||
print("Passphrases do not match. Try again.")
|
||||
options["newpass"] = p1
|
||||
|
||||
if options.get("private-key-subtype") is None:
|
||||
options["private-key-subtype"] = _defaultPrivateKeySubtype(key.type())
|
||||
|
||||
try:
|
||||
newkeydata = key.toString(
|
||||
"openssh",
|
||||
subtype=options["private-key-subtype"],
|
||||
passphrase=options["newpass"],
|
||||
)
|
||||
except Exception as e:
|
||||
sys.exit(f"Could not change passphrase: {e}")
|
||||
|
||||
try:
|
||||
keys.Key.fromString(newkeydata, passphrase=options["newpass"])
|
||||
except (keys.EncryptedKeyError, keys.BadKeyError) as e:
|
||||
sys.exit(f"Could not change passphrase: {e}")
|
||||
|
||||
with open(filename, "wb") as fd:
|
||||
fd.write(newkeydata)
|
||||
|
||||
print("Your identification has been saved with the new passphrase.")
|
||||
|
||||
|
||||
def displayPublicKey(options):
|
||||
filename = _getKeyOrDefault(options)
|
||||
try:
|
||||
key = keys.Key.fromFile(filename)
|
||||
except FileNotFoundError:
|
||||
sys.exit(f"{filename} could not be opened, please specify a file.")
|
||||
except keys.EncryptedKeyError:
|
||||
if not options.get("pass"):
|
||||
options["pass"] = getpass.getpass("Enter passphrase: ")
|
||||
key = keys.Key.fromFile(filename, passphrase=options["pass"])
|
||||
displayKey = key.public().toString("openssh").decode("ascii")
|
||||
print(displayKey)
|
||||
|
||||
|
||||
def _inputSaveFile(prompt: str) -> str:
|
||||
"""
|
||||
Ask the user where to save the key.
|
||||
|
||||
This needs to be a separate function so the unit test can patch it.
|
||||
"""
|
||||
return input(prompt)
|
||||
|
||||
|
||||
def _saveKey(
|
||||
key: keys.Key,
|
||||
options: Dict[Any, Any],
|
||||
inputCollector: Optional[Callable[[str], str]] = None,
|
||||
) -> None:
|
||||
"""
|
||||
Persist a SSH key on local filesystem.
|
||||
|
||||
@param key: Key which is persisted on local filesystem.
|
||||
|
||||
@param options:
|
||||
|
||||
@param inputCollector: Dependency injection for testing.
|
||||
"""
|
||||
if inputCollector is None:
|
||||
inputCollector = input
|
||||
KeyTypeMapping = {"EC": "ecdsa", "Ed25519": "ed25519", "RSA": "rsa", "DSA": "dsa"}
|
||||
keyTypeName = KeyTypeMapping[key.type()]
|
||||
filename = options["filename"]
|
||||
if not filename:
|
||||
defaultPath = _getKeyOrDefault(options, inputCollector, keyTypeName)
|
||||
newPath = _inputSaveFile(
|
||||
f"Enter file in which to save the key ({defaultPath}): "
|
||||
)
|
||||
|
||||
filename = newPath.strip() or defaultPath
|
||||
|
||||
if os.path.exists(filename):
|
||||
print(f"{filename} already exists.")
|
||||
yn = inputCollector("Overwrite (y/n)? ")
|
||||
if yn[0].lower() != "y":
|
||||
sys.exit()
|
||||
|
||||
if options.get("no-passphrase"):
|
||||
options["pass"] = b""
|
||||
elif not options["pass"]:
|
||||
while 1:
|
||||
p1 = getpass.getpass("Enter passphrase (empty for no passphrase): ")
|
||||
p2 = getpass.getpass("Enter same passphrase again: ")
|
||||
if p1 == p2:
|
||||
break
|
||||
print("Passphrases do not match. Try again.")
|
||||
options["pass"] = p1
|
||||
|
||||
if options.get("private-key-subtype") is None:
|
||||
options["private-key-subtype"] = _defaultPrivateKeySubtype(key.type())
|
||||
|
||||
comment = f"{getpass.getuser()}@{socket.gethostname()}"
|
||||
|
||||
fp = filepath.FilePath(filename)
|
||||
fp.setContent(
|
||||
key.toString(
|
||||
"openssh",
|
||||
subtype=options["private-key-subtype"],
|
||||
passphrase=options["pass"],
|
||||
)
|
||||
)
|
||||
fp.chmod(0o100600)
|
||||
|
||||
filepath.FilePath(filename + ".pub").setContent(
|
||||
key.public().toString("openssh", comment=comment)
|
||||
)
|
||||
options = enumrepresentation(options)
|
||||
|
||||
print(f"Your identification has been saved in {filename}")
|
||||
print(f"Your public key has been saved in {filename}.pub")
|
||||
print(f"The key fingerprint in {options['format']} is:")
|
||||
print(key.fingerprint(options["format"]))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run()
|
||||
@@ -0,0 +1,578 @@
|
||||
# -*- test-case-name: twisted.conch.test.test_conch -*-
|
||||
#
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
#
|
||||
# $Id: conch.py,v 1.65 2004/03/11 00:29:14 z3p Exp $
|
||||
|
||||
# Implementation module for the `conch` command.
|
||||
#
|
||||
|
||||
import fcntl
|
||||
import getpass
|
||||
import os
|
||||
import signal
|
||||
import struct
|
||||
import sys
|
||||
import tty
|
||||
from typing import List, Tuple
|
||||
|
||||
from twisted.conch.client import connect, default
|
||||
from twisted.conch.client.options import ConchOptions
|
||||
from twisted.conch.error import ConchError
|
||||
from twisted.conch.ssh import channel, common, connection, forwarding, session
|
||||
from twisted.internet import reactor, stdio, task
|
||||
from twisted.python import log, usage
|
||||
from twisted.python.compat import ioType, networkString
|
||||
|
||||
|
||||
class ClientOptions(ConchOptions):
|
||||
synopsis = """Usage: conch [options] host [command]
|
||||
"""
|
||||
longdesc = (
|
||||
"conch is a SSHv2 client that allows logging into a remote "
|
||||
"machine and executing commands."
|
||||
)
|
||||
|
||||
optParameters = [
|
||||
["escape", "e", "~"],
|
||||
[
|
||||
"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 = [
|
||||
["null", "n", "Redirect input from /dev/null."],
|
||||
["fork", "f", "Fork to background after authentication."],
|
||||
["tty", "t", "Tty; allocate a tty even if command is given."],
|
||||
["notty", "T", "Do not allocate a tty."],
|
||||
["noshell", "N", "Do not execute a shell or command."],
|
||||
["subsystem", "s", "Invoke command (mandatory) as SSH2 subsystem."],
|
||||
]
|
||||
|
||||
compData = usage.Completions(
|
||||
mutuallyExclusive=[("tty", "notty")],
|
||||
optActions={
|
||||
"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),
|
||||
],
|
||||
)
|
||||
|
||||
localForwards: List[Tuple[int, Tuple[int, int]]] = []
|
||||
remoteForwards: List[Tuple[int, Tuple[int, int]]] = []
|
||||
|
||||
def opt_escape(self, esc):
|
||||
"""
|
||||
Set escape character; ``none'' = disable
|
||||
"""
|
||||
if esc == "none":
|
||||
self["escape"] = None
|
||||
elif esc[0] == "^" and len(esc) == 2:
|
||||
self["escape"] = chr(ord(esc[1]) - 64)
|
||||
elif len(esc) == 1:
|
||||
self["escape"] = esc
|
||||
else:
|
||||
sys.exit(f"Bad escape character '{esc}'.")
|
||||
|
||||
def opt_localforward(self, f):
|
||||
"""
|
||||
Forward local port to remote address (lport:host:port)
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
Forward remote port to local address (rport:host:port)
|
||||
"""
|
||||
remotePort, connHost, connPort = f.split(":") # Doesn't do v6 yet
|
||||
remotePort = int(remotePort)
|
||||
connPort = int(connPort)
|
||||
self.remoteForwards.append((remotePort, (connHost, connPort)))
|
||||
|
||||
def parseArgs(self, host, *command):
|
||||
self["host"] = host
|
||||
self["command"] = " ".join(command)
|
||||
|
||||
|
||||
# Rest of code in "run"
|
||||
options = None
|
||||
conn = None
|
||||
exitStatus = 0
|
||||
old = None
|
||||
_inRawMode = 0
|
||||
_savedRawMode = None
|
||||
|
||||
|
||||
def run():
|
||||
global options, old
|
||||
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
|
||||
options = ClientOptions()
|
||||
try:
|
||||
options.parseOptions(args)
|
||||
except usage.UsageError as u:
|
||||
print(f"ERROR: {u}")
|
||||
options.opt_help()
|
||||
sys.exit(1)
|
||||
if options["log"]:
|
||||
if options["logfile"]:
|
||||
if options["logfile"] == "-":
|
||||
f = sys.stdout
|
||||
else:
|
||||
f = open(options["logfile"], "a+")
|
||||
else:
|
||||
f = sys.stderr
|
||||
realout = sys.stdout
|
||||
log.startLogging(f)
|
||||
sys.stdout = realout
|
||||
else:
|
||||
log.discardLogs()
|
||||
doConnect()
|
||||
fd = sys.stdin.fileno()
|
||||
try:
|
||||
old = tty.tcgetattr(fd)
|
||||
except BaseException:
|
||||
old = None
|
||||
try:
|
||||
oldUSR1 = signal.signal(
|
||||
signal.SIGUSR1, lambda *a: reactor.callLater(0, reConnect)
|
||||
)
|
||||
except BaseException:
|
||||
oldUSR1 = None
|
||||
try:
|
||||
reactor.run()
|
||||
finally:
|
||||
if old:
|
||||
tty.tcsetattr(fd, tty.TCSANOW, old)
|
||||
if oldUSR1:
|
||||
signal.signal(signal.SIGUSR1, oldUSR1)
|
||||
if (options["command"] and options["tty"]) or not options["notty"]:
|
||||
signal.signal(signal.SIGWINCH, signal.SIG_DFL)
|
||||
if sys.stdout.isatty() and not options["command"]:
|
||||
print("Connection to {} closed.".format(options["host"]))
|
||||
sys.exit(exitStatus)
|
||||
|
||||
|
||||
def handleError():
|
||||
from twisted.python import failure
|
||||
|
||||
global exitStatus
|
||||
exitStatus = 2
|
||||
reactor.callLater(0.01, _stopReactor)
|
||||
log.err(failure.Failure())
|
||||
raise
|
||||
|
||||
|
||||
def _stopReactor():
|
||||
try:
|
||||
reactor.stop()
|
||||
except BaseException:
|
||||
pass
|
||||
|
||||
|
||||
def doConnect():
|
||||
if "@" in options["host"]:
|
||||
options["user"], options["host"] = options["host"].split("@", 1)
|
||||
if not options.identitys:
|
||||
options.identitys = ["~/.ssh/id_rsa", "~/.ssh/id_dsa"]
|
||||
host = options["host"]
|
||||
if not options["user"]:
|
||||
options["user"] = getpass.getuser()
|
||||
if not options["port"]:
|
||||
options["port"] = 22
|
||||
else:
|
||||
options["port"] = int(options["port"])
|
||||
host = options["host"]
|
||||
port = options["port"]
|
||||
vhk = default.verifyHostKey
|
||||
if not options["host-key-algorithms"]:
|
||||
options["host-key-algorithms"] = default.getHostKeyAlgorithms(host, options)
|
||||
uao = default.SSHUserAuthClient(options["user"], options, SSHConnection())
|
||||
connect.connect(host, port, options, vhk, uao).addErrback(_ebExit)
|
||||
|
||||
|
||||
def _ebExit(f):
|
||||
global exitStatus
|
||||
exitStatus = f"conch: exiting with error {f}"
|
||||
reactor.callLater(0.1, _stopReactor)
|
||||
|
||||
|
||||
def onConnect():
|
||||
# if keyAgent and options['agent']:
|
||||
# cc = protocol.ClientCreator(reactor, SSHAgentForwardingLocal, conn)
|
||||
# cc.connectUNIX(os.environ['SSH_AUTH_SOCK'])
|
||||
if hasattr(conn.transport, "sendIgnore"):
|
||||
_KeepAlive(conn)
|
||||
if options.localForwards:
|
||||
for localPort, hostport in options.localForwards:
|
||||
s = reactor.listenTCP(
|
||||
localPort,
|
||||
forwarding.SSHListenForwardingFactory(
|
||||
conn, hostport, SSHListenClientForwardingChannel
|
||||
),
|
||||
)
|
||||
conn.localForwards.append(s)
|
||||
if options.remoteForwards:
|
||||
for remotePort, hostport in options.remoteForwards:
|
||||
log.msg(f"asking for remote forwarding for {remotePort}:{hostport}")
|
||||
conn.requestRemoteForwarding(remotePort, hostport)
|
||||
reactor.addSystemEventTrigger("before", "shutdown", beforeShutdown)
|
||||
if not options["noshell"] or options["agent"]:
|
||||
conn.openChannel(SSHSession())
|
||||
if options["fork"]:
|
||||
if os.fork():
|
||||
os._exit(0)
|
||||
os.setsid()
|
||||
for i in range(3):
|
||||
try:
|
||||
os.close(i)
|
||||
except OSError as e:
|
||||
import errno
|
||||
|
||||
if e.errno != errno.EBADF:
|
||||
raise
|
||||
|
||||
|
||||
def reConnect():
|
||||
beforeShutdown()
|
||||
conn.transport.transport.loseConnection()
|
||||
|
||||
|
||||
def beforeShutdown():
|
||||
remoteForwards = options.remoteForwards
|
||||
for remotePort, hostport in remoteForwards:
|
||||
log.msg(f"cancelling {remotePort}:{hostport}")
|
||||
conn.cancelRemoteForwarding(remotePort)
|
||||
|
||||
|
||||
def stopConnection():
|
||||
if not options["reconnect"]:
|
||||
reactor.callLater(0.1, _stopReactor)
|
||||
|
||||
|
||||
class _KeepAlive:
|
||||
def __init__(self, conn):
|
||||
self.conn = conn
|
||||
self.globalTimeout = None
|
||||
self.lc = task.LoopingCall(self.sendGlobal)
|
||||
self.lc.start(300)
|
||||
|
||||
def sendGlobal(self):
|
||||
d = self.conn.sendGlobalRequest(
|
||||
b"conch-keep-alive@twistedmatrix.com", b"", wantReply=1
|
||||
)
|
||||
d.addBoth(self._cbGlobal)
|
||||
self.globalTimeout = reactor.callLater(30, self._ebGlobal)
|
||||
|
||||
def _cbGlobal(self, res):
|
||||
if self.globalTimeout:
|
||||
self.globalTimeout.cancel()
|
||||
self.globalTimeout = None
|
||||
|
||||
def _ebGlobal(self):
|
||||
if self.globalTimeout:
|
||||
self.globalTimeout = None
|
||||
self.conn.transport.loseConnection()
|
||||
|
||||
|
||||
class SSHConnection(connection.SSHConnection):
|
||||
def serviceStarted(self):
|
||||
global conn
|
||||
conn = self
|
||||
self.localForwards = []
|
||||
self.remoteForwards = {}
|
||||
onConnect()
|
||||
|
||||
def serviceStopped(self):
|
||||
lf = self.localForwards
|
||||
self.localForwards = []
|
||||
for s in lf:
|
||||
s.loseConnection()
|
||||
stopConnection()
|
||||
|
||||
def requestRemoteForwarding(self, remotePort, hostport):
|
||||
data = forwarding.packGlobal_tcpip_forward(("0.0.0.0", remotePort))
|
||||
d = self.sendGlobalRequest(b"tcpip-forward", data, wantReply=1)
|
||||
log.msg(f"requesting remote forwarding {remotePort}:{hostport}")
|
||||
d.addCallback(self._cbRemoteForwarding, remotePort, hostport)
|
||||
d.addErrback(self._ebRemoteForwarding, remotePort, hostport)
|
||||
|
||||
def _cbRemoteForwarding(self, result, remotePort, hostport):
|
||||
log.msg(f"accepted remote forwarding {remotePort}:{hostport}")
|
||||
self.remoteForwards[remotePort] = hostport
|
||||
log.msg(repr(self.remoteForwards))
|
||||
|
||||
def _ebRemoteForwarding(self, f, remotePort, hostport):
|
||||
log.msg(f"remote forwarding {remotePort}:{hostport} failed")
|
||||
log.msg(f)
|
||||
|
||||
def cancelRemoteForwarding(self, remotePort):
|
||||
data = forwarding.packGlobal_tcpip_forward(("0.0.0.0", remotePort))
|
||||
self.sendGlobalRequest(b"cancel-tcpip-forward", data)
|
||||
log.msg(f"cancelling remote forwarding {remotePort}")
|
||||
try:
|
||||
del self.remoteForwards[remotePort]
|
||||
except Exception:
|
||||
pass
|
||||
log.msg(repr(self.remoteForwards))
|
||||
|
||||
def channel_forwarded_tcpip(self, windowSize, maxPacket, data):
|
||||
log.msg(f"FTCP {data!r}")
|
||||
remoteHP, origHP = forwarding.unpackOpen_forwarded_tcpip(data)
|
||||
log.msg(self.remoteForwards)
|
||||
log.msg(remoteHP)
|
||||
if remoteHP[1] in self.remoteForwards:
|
||||
connectHP = self.remoteForwards[remoteHP[1]]
|
||||
log.msg(f"connect forwarding {connectHP}")
|
||||
return SSHConnectForwardingChannel(
|
||||
connectHP, remoteWindow=windowSize, remoteMaxPacket=maxPacket, conn=self
|
||||
)
|
||||
else:
|
||||
raise ConchError(
|
||||
connection.OPEN_CONNECT_FAILED, "don't know about that port"
|
||||
)
|
||||
|
||||
def channelClosed(self, channel):
|
||||
log.msg(f"connection closing {channel}")
|
||||
log.msg(self.channels)
|
||||
if len(self.channels) == 1: # Just us left
|
||||
log.msg("stopping connection")
|
||||
stopConnection()
|
||||
else:
|
||||
# Because of the unix thing
|
||||
self.__class__.__bases__[0].channelClosed(self, channel)
|
||||
|
||||
|
||||
class SSHSession(channel.SSHChannel):
|
||||
name = b"session"
|
||||
|
||||
def channelOpen(self, foo):
|
||||
log.msg(f"session {self.id} open")
|
||||
if options["agent"]:
|
||||
d = self.conn.sendRequest(
|
||||
self, b"auth-agent-req@openssh.com", b"", wantReply=1
|
||||
)
|
||||
d.addBoth(lambda x: log.msg(x))
|
||||
if options["noshell"]:
|
||||
return
|
||||
if (options["command"] and options["tty"]) or not options["notty"]:
|
||||
_enterRawMode()
|
||||
c = session.SSHSessionClient()
|
||||
if options["escape"] and not options["notty"]:
|
||||
self.escapeMode = 1
|
||||
c.dataReceived = self.handleInput
|
||||
else:
|
||||
c.dataReceived = self.write
|
||||
c.connectionLost = lambda x: self.sendEOF()
|
||||
self.stdio = stdio.StandardIO(c)
|
||||
fd = 0
|
||||
if options["subsystem"]:
|
||||
self.conn.sendRequest(self, b"subsystem", common.NS(options["command"]))
|
||||
elif options["command"]:
|
||||
if options["tty"]:
|
||||
term = os.environ["TERM"]
|
||||
winsz = fcntl.ioctl(fd, tty.TIOCGWINSZ, "12345678")
|
||||
winSize = struct.unpack("4H", winsz)
|
||||
ptyReqData = session.packRequest_pty_req(term, winSize, "")
|
||||
self.conn.sendRequest(self, b"pty-req", ptyReqData)
|
||||
signal.signal(signal.SIGWINCH, self._windowResized)
|
||||
self.conn.sendRequest(self, b"exec", common.NS(options["command"]))
|
||||
else:
|
||||
if not options["notty"]:
|
||||
term = os.environ["TERM"]
|
||||
winsz = fcntl.ioctl(fd, tty.TIOCGWINSZ, "12345678")
|
||||
winSize = struct.unpack("4H", winsz)
|
||||
ptyReqData = session.packRequest_pty_req(term, winSize, "")
|
||||
self.conn.sendRequest(self, b"pty-req", ptyReqData)
|
||||
signal.signal(signal.SIGWINCH, self._windowResized)
|
||||
self.conn.sendRequest(self, b"shell", b"")
|
||||
# if hasattr(conn.transport, 'transport'):
|
||||
# conn.transport.transport.setTcpNoDelay(1)
|
||||
|
||||
def handleInput(self, char):
|
||||
if char in (b"\n", b"\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 == b".": # Disconnect
|
||||
log.msg("disconnecting from escape")
|
||||
stopConnection()
|
||||
return
|
||||
elif char == b"\x1a": # ^Z, suspend
|
||||
|
||||
def _():
|
||||
_leaveRawMode()
|
||||
sys.stdout.flush()
|
||||
sys.stdin.flush()
|
||||
os.kill(os.getpid(), signal.SIGTSTP)
|
||||
_enterRawMode()
|
||||
|
||||
reactor.callLater(0, _)
|
||||
return
|
||||
elif char == b"R": # Rekey connection
|
||||
log.msg("rekeying connection")
|
||||
self.conn.transport.sendKexInit()
|
||||
return
|
||||
elif char == b"#": # Display connections
|
||||
self.stdio.write(b"\r\nThe following connections are open:\r\n")
|
||||
channels = self.conn.channels.keys()
|
||||
channels.sort()
|
||||
for channelId in channels:
|
||||
self.stdio.write(
|
||||
networkString(
|
||||
" #{} {}\r\n".format(
|
||||
channelId, self.conn.channels[channelId]
|
||||
)
|
||||
)
|
||||
)
|
||||
return
|
||||
self.write(b"~" + char)
|
||||
else:
|
||||
self.escapeMode = 0
|
||||
self.write(char)
|
||||
|
||||
def dataReceived(self, data):
|
||||
self.stdio.write(data)
|
||||
|
||||
def extReceived(self, t, data):
|
||||
if t == connection.EXTENDED_DATA_STDERR:
|
||||
log.msg(f"got {len(data)} stderr data")
|
||||
if ioType(sys.stderr) == str:
|
||||
sys.stderr.buffer.write(data)
|
||||
else:
|
||||
sys.stderr.write(data)
|
||||
|
||||
def eofReceived(self):
|
||||
log.msg("got eof")
|
||||
self.stdio.loseWriteConnection()
|
||||
|
||||
def closeReceived(self):
|
||||
log.msg(f"remote side closed {self}")
|
||||
self.conn.sendClose(self)
|
||||
|
||||
def closed(self):
|
||||
global old
|
||||
log.msg(f"closed {self}")
|
||||
log.msg(repr(self.conn.channels))
|
||||
|
||||
def request_exit_status(self, data):
|
||||
global exitStatus
|
||||
exitStatus = int(struct.unpack(">L", data)[0])
|
||||
log.msg(f"exit status: {exitStatus}")
|
||||
|
||||
def sendEOF(self):
|
||||
self.conn.sendEOF(self)
|
||||
|
||||
def stopWriting(self):
|
||||
self.stdio.pauseProducing()
|
||||
|
||||
def startWriting(self):
|
||||
self.stdio.resumeProducing()
|
||||
|
||||
def _windowResized(self, *args):
|
||||
winsz = fcntl.ioctl(0, tty.TIOCGWINSZ, "12345678")
|
||||
winSize = struct.unpack("4H", winsz)
|
||||
newSize = winSize[1], winSize[0], winSize[2], winSize[3]
|
||||
self.conn.sendRequest(self, b"window-change", struct.pack("!4L", *newSize))
|
||||
|
||||
|
||||
class SSHListenClientForwardingChannel(forwarding.SSHListenClientForwardingChannel):
|
||||
pass
|
||||
|
||||
|
||||
class SSHConnectForwardingChannel(forwarding.SSHConnectForwardingChannel):
|
||||
pass
|
||||
|
||||
|
||||
def _leaveRawMode():
|
||||
global _inRawMode
|
||||
if not _inRawMode:
|
||||
return
|
||||
fd = sys.stdin.fileno()
|
||||
tty.tcsetattr(fd, tty.TCSANOW, _savedRawMode)
|
||||
_inRawMode = 0
|
||||
|
||||
|
||||
def _enterRawMode():
|
||||
global _inRawMode, _savedRawMode
|
||||
if _inRawMode:
|
||||
return
|
||||
fd = sys.stdin.fileno()
|
||||
try:
|
||||
old = tty.tcgetattr(fd)
|
||||
new = old[:]
|
||||
except BaseException:
|
||||
log.msg("not a typewriter!")
|
||||
else:
|
||||
# iflage
|
||||
new[0] = new[0] | tty.IGNPAR
|
||||
new[0] = new[0] & ~(
|
||||
tty.ISTRIP
|
||||
| tty.INLCR
|
||||
| tty.IGNCR
|
||||
| tty.ICRNL
|
||||
| tty.IXON
|
||||
| tty.IXANY
|
||||
| tty.IXOFF
|
||||
)
|
||||
if hasattr(tty, "IUCLC"):
|
||||
new[0] = new[0] & ~tty.IUCLC
|
||||
|
||||
# lflag
|
||||
new[3] = new[3] & ~(
|
||||
tty.ISIG
|
||||
| tty.ICANON
|
||||
| tty.ECHO
|
||||
| tty.ECHO
|
||||
| tty.ECHOE
|
||||
| tty.ECHOK
|
||||
| tty.ECHONL
|
||||
)
|
||||
if hasattr(tty, "IEXTEN"):
|
||||
new[3] = new[3] & ~tty.IEXTEN
|
||||
|
||||
# oflag
|
||||
new[1] = new[1] & ~tty.OPOST
|
||||
|
||||
new[6][tty.VMIN] = 1
|
||||
new[6][tty.VTIME] = 0
|
||||
|
||||
_savedRawMode = old
|
||||
tty.tcsetattr(fd, tty.TCSANOW, new)
|
||||
# tty.setraw(fd)
|
||||
_inRawMode = 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run()
|
||||
@@ -0,0 +1,673 @@
|
||||
# -*- 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()
|
||||
Reference in New Issue
Block a user