This commit is contained in:
2024-12-17 14:36:15 -08:00
parent b2dbf46d28
commit 06d106de53
17731 changed files with 3037186 additions and 144 deletions

View File

@@ -0,0 +1 @@
"conch scripts"

File diff suppressed because it is too large Load Diff

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()