RAHHH
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
# -*- test-case-name: twisted.conch.test -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Twisted Conch: The Twisted Shell. Terminal emulation, SSHv2 and telnet.
|
||||
"""
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
56
backend/lib/python3.12/site-packages/twisted/conch/avatar.py
Normal file
56
backend/lib/python3.12/site-packages/twisted/conch/avatar.py
Normal file
@@ -0,0 +1,56 @@
|
||||
# -*- test-case-name: twisted.conch.test.test_conch -*-
|
||||
|
||||
|
||||
from zope.interface import implementer
|
||||
|
||||
from twisted.conch.error import ConchError
|
||||
from twisted.conch.interfaces import IConchUser
|
||||
from twisted.conch.ssh.connection import OPEN_UNKNOWN_CHANNEL_TYPE
|
||||
from twisted.logger import Logger
|
||||
from twisted.python.compat import nativeString
|
||||
|
||||
|
||||
@implementer(IConchUser)
|
||||
class ConchUser:
|
||||
_log = Logger()
|
||||
|
||||
def __init__(self):
|
||||
self.channelLookup = {}
|
||||
self.subsystemLookup = {}
|
||||
|
||||
@property
|
||||
def conn(self):
|
||||
return self._conn
|
||||
|
||||
@conn.setter
|
||||
def conn(self, value):
|
||||
self._conn = value
|
||||
|
||||
def lookupChannel(self, channelType, windowSize, maxPacket, data):
|
||||
klass = self.channelLookup.get(channelType, None)
|
||||
if not klass:
|
||||
raise ConchError(OPEN_UNKNOWN_CHANNEL_TYPE, "unknown channel")
|
||||
else:
|
||||
return klass(
|
||||
remoteWindow=windowSize,
|
||||
remoteMaxPacket=maxPacket,
|
||||
data=data,
|
||||
avatar=self,
|
||||
)
|
||||
|
||||
def lookupSubsystem(self, subsystem, data):
|
||||
self._log.debug(
|
||||
"Subsystem lookup: {subsystem!r}", subsystem=self.subsystemLookup
|
||||
)
|
||||
klass = self.subsystemLookup.get(subsystem, None)
|
||||
if not klass:
|
||||
return False
|
||||
return klass(data, avatar=self)
|
||||
|
||||
def gotGlobalRequest(self, requestType, data):
|
||||
# XXX should this use method dispatch?
|
||||
requestType = nativeString(requestType.replace(b"-", b"_"))
|
||||
f = getattr(self, "global_%s" % requestType, None)
|
||||
if not f:
|
||||
return 0
|
||||
return f(data)
|
||||
640
backend/lib/python3.12/site-packages/twisted/conch/checkers.py
Normal file
640
backend/lib/python3.12/site-packages/twisted/conch/checkers.py
Normal file
@@ -0,0 +1,640 @@
|
||||
# -*- test-case-name: twisted.conch.test.test_checkers -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Provide L{ICredentialsChecker} implementations to be used in Conch protocols.
|
||||
"""
|
||||
|
||||
|
||||
import binascii
|
||||
import errno
|
||||
import sys
|
||||
from base64 import decodebytes
|
||||
from typing import IO, Any, Callable, Iterable, Iterator, Mapping, Optional, Tuple, cast
|
||||
|
||||
from zope.interface import Interface, implementer, providedBy
|
||||
|
||||
from incremental import Version
|
||||
from typing_extensions import Literal, Protocol
|
||||
|
||||
from twisted.conch import error
|
||||
from twisted.conch.ssh import keys
|
||||
from twisted.cred.checkers import ICredentialsChecker
|
||||
from twisted.cred.credentials import ISSHPrivateKey, IUsernamePassword
|
||||
from twisted.cred.error import UnauthorizedLogin, UnhandledCredentials
|
||||
from twisted.internet import defer
|
||||
from twisted.logger import Logger
|
||||
from twisted.plugins.cred_unix import verifyCryptedPassword
|
||||
from twisted.python import failure, reflect
|
||||
from twisted.python.deprecate import deprecatedModuleAttribute
|
||||
from twisted.python.filepath import FilePath
|
||||
from twisted.python.util import runAsEffectiveUser
|
||||
|
||||
_log = Logger()
|
||||
|
||||
|
||||
class UserRecord(Tuple[str, str, int, int, str, str, str]):
|
||||
"""
|
||||
A record in a UNIX-style password database. See L{pwd} for field details.
|
||||
|
||||
This corresponds to the undocumented type L{pwd.struct_passwd}, but lacks named
|
||||
field accessors.
|
||||
"""
|
||||
|
||||
@property
|
||||
def pw_dir(self) -> str: # type: ignore[empty-body]
|
||||
...
|
||||
|
||||
|
||||
class UserDB(Protocol):
|
||||
"""
|
||||
A database of users by name, like the stdlib L{pwd} module.
|
||||
|
||||
See L{twisted.python.fakepwd} for an in-memory implementation.
|
||||
"""
|
||||
|
||||
def getpwnam(self, username: str) -> UserRecord:
|
||||
"""
|
||||
Lookup a user record by name.
|
||||
|
||||
@raises KeyError: when no such user exists
|
||||
"""
|
||||
|
||||
|
||||
pwd: Optional[UserDB]
|
||||
try:
|
||||
import pwd as _pwd
|
||||
except ImportError:
|
||||
pwd = None
|
||||
else:
|
||||
pwd = cast(UserDB, _pwd)
|
||||
|
||||
|
||||
try:
|
||||
import spwd as _spwd
|
||||
except ImportError:
|
||||
spwd = None
|
||||
else:
|
||||
spwd = _spwd
|
||||
|
||||
|
||||
class CryptedPasswordRecord(Protocol):
|
||||
"""
|
||||
A sequence where the item at index 1 may be a crypted password.
|
||||
|
||||
Both L{pwd.struct_passwd} and L{spwd.struct_spwd} conform to this protocol.
|
||||
"""
|
||||
|
||||
def __getitem__(self, index: Literal[1]) -> str:
|
||||
"""
|
||||
Get the crypted password.
|
||||
"""
|
||||
|
||||
|
||||
def _lookupUser(userdb: UserDB, username: bytes) -> UserRecord:
|
||||
"""
|
||||
Lookup a user by name in a L{pwd}-style database.
|
||||
|
||||
@param userdb: The user database.
|
||||
|
||||
@param username: Identifying name in bytes. This will be decoded according
|
||||
to the filesystem encoding, as the L{pwd} module does internally.
|
||||
|
||||
@raises KeyError: when the user doesn't exist
|
||||
"""
|
||||
return userdb.getpwnam(username.decode(sys.getfilesystemencoding()))
|
||||
|
||||
|
||||
def _pwdGetByName(username: str) -> Optional[CryptedPasswordRecord]:
|
||||
"""
|
||||
Look up a user in the /etc/passwd database using the pwd module. If the
|
||||
pwd module is not available, return None.
|
||||
|
||||
@param username: the username of the user to return the passwd database
|
||||
information for.
|
||||
|
||||
@returns: A L{pwd.struct_passwd}, where field 1 may contain a crypted
|
||||
password, or L{None} when the L{pwd} database is unavailable.
|
||||
|
||||
@raises KeyError: when no such user exists
|
||||
"""
|
||||
if pwd is None:
|
||||
return None
|
||||
return cast(CryptedPasswordRecord, pwd.getpwnam(username))
|
||||
|
||||
|
||||
def _shadowGetByName(username: str) -> Optional[CryptedPasswordRecord]:
|
||||
"""
|
||||
Look up a user in the /etc/shadow database using the spwd module. If it is
|
||||
not available, return L{None}.
|
||||
|
||||
@param username: the username of the user to return the shadow database
|
||||
information for.
|
||||
@type username: L{str}
|
||||
|
||||
@returns: A L{spwd.struct_spwd}, where field 1 may contain a crypted
|
||||
password, or L{None} when the L{spwd} database is unavailable.
|
||||
|
||||
@raises KeyError: when no such user exists
|
||||
"""
|
||||
if spwd is not None:
|
||||
f = spwd.getspnam
|
||||
else:
|
||||
return None
|
||||
return cast(CryptedPasswordRecord, runAsEffectiveUser(0, 0, f, username))
|
||||
|
||||
|
||||
@implementer(ICredentialsChecker)
|
||||
class UNIXPasswordDatabase:
|
||||
"""
|
||||
A checker which validates users out of the UNIX password databases, or
|
||||
databases of a compatible format.
|
||||
|
||||
@ivar _getByNameFunctions: a C{list} of functions which are called in order
|
||||
to validate a user. The default value is such that the C{/etc/passwd}
|
||||
database will be tried first, followed by the C{/etc/shadow} database.
|
||||
"""
|
||||
|
||||
credentialInterfaces = (IUsernamePassword,)
|
||||
|
||||
def __init__(self, getByNameFunctions=None):
|
||||
if getByNameFunctions is None:
|
||||
getByNameFunctions = [_pwdGetByName, _shadowGetByName]
|
||||
self._getByNameFunctions = getByNameFunctions
|
||||
|
||||
def requestAvatarId(self, credentials):
|
||||
# We get bytes, but the Py3 pwd module uses str. So attempt to decode
|
||||
# it using the same method that CPython does for the file on disk.
|
||||
username = credentials.username.decode(sys.getfilesystemencoding())
|
||||
password = credentials.password.decode(sys.getfilesystemencoding())
|
||||
|
||||
for func in self._getByNameFunctions:
|
||||
try:
|
||||
pwnam = func(username)
|
||||
except KeyError:
|
||||
return defer.fail(UnauthorizedLogin("invalid username"))
|
||||
else:
|
||||
if pwnam is not None:
|
||||
crypted = pwnam[1]
|
||||
if crypted == "":
|
||||
continue
|
||||
|
||||
if verifyCryptedPassword(crypted, password):
|
||||
return defer.succeed(credentials.username)
|
||||
# fallback
|
||||
return defer.fail(UnauthorizedLogin("unable to verify password"))
|
||||
|
||||
|
||||
@implementer(ICredentialsChecker)
|
||||
class SSHPublicKeyDatabase:
|
||||
"""
|
||||
Checker that authenticates SSH public keys, based on public keys listed in
|
||||
authorized_keys and authorized_keys2 files in user .ssh/ directories.
|
||||
"""
|
||||
|
||||
credentialInterfaces = (ISSHPrivateKey,)
|
||||
|
||||
_userdb: UserDB = cast(UserDB, pwd)
|
||||
|
||||
def requestAvatarId(self, credentials):
|
||||
d = defer.maybeDeferred(self.checkKey, credentials)
|
||||
d.addCallback(self._cbRequestAvatarId, credentials)
|
||||
d.addErrback(self._ebRequestAvatarId)
|
||||
return d
|
||||
|
||||
def _cbRequestAvatarId(self, validKey, credentials):
|
||||
"""
|
||||
Check whether the credentials themselves are valid, now that we know
|
||||
if the key matches the user.
|
||||
|
||||
@param validKey: A boolean indicating whether or not the public key
|
||||
matches a key in the user's authorized_keys file.
|
||||
|
||||
@param credentials: The credentials offered by the user.
|
||||
@type credentials: L{ISSHPrivateKey} provider
|
||||
|
||||
@raise UnauthorizedLogin: (as a failure) if the key does not match the
|
||||
user in C{credentials}. Also raised if the user provides an invalid
|
||||
signature.
|
||||
|
||||
@raise ValidPublicKey: (as a failure) if the key matches the user but
|
||||
the credentials do not include a signature. See
|
||||
L{error.ValidPublicKey} for more information.
|
||||
|
||||
@return: The user's username, if authentication was successful.
|
||||
"""
|
||||
if not validKey:
|
||||
return failure.Failure(UnauthorizedLogin("invalid key"))
|
||||
if not credentials.signature:
|
||||
return failure.Failure(error.ValidPublicKey())
|
||||
else:
|
||||
try:
|
||||
pubKey = keys.Key.fromString(credentials.blob)
|
||||
if pubKey.verify(credentials.signature, credentials.sigData):
|
||||
return credentials.username
|
||||
except Exception: # any error should be treated as a failed login
|
||||
_log.failure("Error while verifying key")
|
||||
return failure.Failure(UnauthorizedLogin("error while verifying key"))
|
||||
return failure.Failure(UnauthorizedLogin("unable to verify key"))
|
||||
|
||||
def getAuthorizedKeysFiles(self, credentials):
|
||||
"""
|
||||
Return a list of L{FilePath} instances for I{authorized_keys} files
|
||||
which might contain information about authorized keys for the given
|
||||
credentials.
|
||||
|
||||
On OpenSSH servers, the default location of the file containing the
|
||||
list of authorized public keys is
|
||||
U{$HOME/.ssh/authorized_keys<http://www.openbsd.org/cgi-bin/man.cgi?query=sshd_config>}.
|
||||
|
||||
I{$HOME/.ssh/authorized_keys2} is also returned, though it has been
|
||||
U{deprecated by OpenSSH since
|
||||
2001<http://marc.info/?m=100508718416162>}.
|
||||
|
||||
@return: A list of L{FilePath} instances to files with the authorized keys.
|
||||
"""
|
||||
pwent = _lookupUser(self._userdb, credentials.username)
|
||||
root = FilePath(pwent.pw_dir).child(".ssh")
|
||||
files = ["authorized_keys", "authorized_keys2"]
|
||||
return [root.child(f) for f in files]
|
||||
|
||||
def checkKey(self, credentials):
|
||||
"""
|
||||
Retrieve files containing authorized keys and check against user
|
||||
credentials.
|
||||
"""
|
||||
ouid, ogid = _lookupUser(self._userdb, credentials.username)[2:4]
|
||||
for filepath in self.getAuthorizedKeysFiles(credentials):
|
||||
if not filepath.exists():
|
||||
continue
|
||||
try:
|
||||
lines = filepath.open()
|
||||
except OSError as e:
|
||||
if e.errno == errno.EACCES:
|
||||
lines = runAsEffectiveUser(ouid, ogid, filepath.open)
|
||||
else:
|
||||
raise
|
||||
with lines:
|
||||
for l in lines:
|
||||
l2 = l.split()
|
||||
if len(l2) < 2:
|
||||
continue
|
||||
try:
|
||||
if decodebytes(l2[1]) == credentials.blob:
|
||||
return True
|
||||
except binascii.Error:
|
||||
continue
|
||||
return False
|
||||
|
||||
def _ebRequestAvatarId(self, f):
|
||||
if not f.check(UnauthorizedLogin):
|
||||
_log.error(
|
||||
"Unauthorized login due to internal error: {error}", error=f.value
|
||||
)
|
||||
return failure.Failure(UnauthorizedLogin("unable to get avatar id"))
|
||||
return f
|
||||
|
||||
|
||||
@implementer(ICredentialsChecker)
|
||||
class SSHProtocolChecker:
|
||||
"""
|
||||
SSHProtocolChecker is a checker that requires multiple authentications
|
||||
to succeed. To add a checker, call my registerChecker method with
|
||||
the checker and the interface.
|
||||
|
||||
After each successful authenticate, I call my areDone method with the
|
||||
avatar id. To get a list of the successful credentials for an avatar id,
|
||||
use C{SSHProcotolChecker.successfulCredentials[avatarId]}. If L{areDone}
|
||||
returns True, the authentication has succeeded.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.checkers = {}
|
||||
self.successfulCredentials = {}
|
||||
|
||||
@property
|
||||
def credentialInterfaces(self):
|
||||
return list(self.checkers.keys())
|
||||
|
||||
def registerChecker(self, checker, *credentialInterfaces):
|
||||
if not credentialInterfaces:
|
||||
credentialInterfaces = checker.credentialInterfaces
|
||||
for credentialInterface in credentialInterfaces:
|
||||
self.checkers[credentialInterface] = checker
|
||||
|
||||
def requestAvatarId(self, credentials):
|
||||
"""
|
||||
Part of the L{ICredentialsChecker} interface. Called by a portal with
|
||||
some credentials to check if they'll authenticate a user. We check the
|
||||
interfaces that the credentials provide against our list of acceptable
|
||||
checkers. If one of them matches, we ask that checker to verify the
|
||||
credentials. If they're valid, we call our L{_cbGoodAuthentication}
|
||||
method to continue.
|
||||
|
||||
@param credentials: the credentials the L{Portal} wants us to verify
|
||||
"""
|
||||
ifac = providedBy(credentials)
|
||||
for i in ifac:
|
||||
c = self.checkers.get(i)
|
||||
if c is not None:
|
||||
d = defer.maybeDeferred(c.requestAvatarId, credentials)
|
||||
return d.addCallback(self._cbGoodAuthentication, credentials)
|
||||
return defer.fail(
|
||||
UnhandledCredentials(
|
||||
"No checker for %s" % ", ".join(map(reflect.qual, ifac))
|
||||
)
|
||||
)
|
||||
|
||||
def _cbGoodAuthentication(self, avatarId, credentials):
|
||||
"""
|
||||
Called if a checker has verified the credentials. We call our
|
||||
L{areDone} method to see if the whole of the successful authentications
|
||||
are enough. If they are, we return the avatar ID returned by the first
|
||||
checker.
|
||||
"""
|
||||
if avatarId not in self.successfulCredentials:
|
||||
self.successfulCredentials[avatarId] = []
|
||||
self.successfulCredentials[avatarId].append(credentials)
|
||||
if self.areDone(avatarId):
|
||||
del self.successfulCredentials[avatarId]
|
||||
return avatarId
|
||||
else:
|
||||
raise error.NotEnoughAuthentication()
|
||||
|
||||
def areDone(self, avatarId):
|
||||
"""
|
||||
Override to determine if the authentication is finished for a given
|
||||
avatarId.
|
||||
|
||||
@param avatarId: the avatar returned by the first checker. For
|
||||
this checker to function correctly, all the checkers must
|
||||
return the same avatar ID.
|
||||
"""
|
||||
return True
|
||||
|
||||
|
||||
deprecatedModuleAttribute(
|
||||
Version("Twisted", 15, 0, 0),
|
||||
(
|
||||
"Please use twisted.conch.checkers.SSHPublicKeyChecker, "
|
||||
"initialized with an instance of "
|
||||
"twisted.conch.checkers.UNIXAuthorizedKeysFiles instead."
|
||||
),
|
||||
__name__,
|
||||
"SSHPublicKeyDatabase",
|
||||
)
|
||||
|
||||
|
||||
class IAuthorizedKeysDB(Interface):
|
||||
"""
|
||||
An object that provides valid authorized ssh keys mapped to usernames.
|
||||
|
||||
@since: 15.0
|
||||
"""
|
||||
|
||||
def getAuthorizedKeys(avatarId):
|
||||
"""
|
||||
Gets an iterable of authorized keys that are valid for the given
|
||||
C{avatarId}.
|
||||
|
||||
@param avatarId: the ID of the avatar
|
||||
@type avatarId: valid return value of
|
||||
L{twisted.cred.checkers.ICredentialsChecker.requestAvatarId}
|
||||
|
||||
@return: an iterable of L{twisted.conch.ssh.keys.Key}
|
||||
"""
|
||||
|
||||
|
||||
def readAuthorizedKeyFile(
|
||||
fileobj: IO[bytes], parseKey: Callable[[bytes], keys.Key] = keys.Key.fromString
|
||||
) -> Iterator[keys.Key]:
|
||||
"""
|
||||
Reads keys from an authorized keys file. Any non-comment line that cannot
|
||||
be parsed as a key will be ignored, although that particular line will
|
||||
be logged.
|
||||
|
||||
@param fileobj: something from which to read lines which can be parsed
|
||||
as keys
|
||||
@param parseKey: a callable that takes bytes and returns a
|
||||
L{twisted.conch.ssh.keys.Key}, mainly to be used for testing. The
|
||||
default is L{twisted.conch.ssh.keys.Key.fromString}.
|
||||
@return: an iterable of L{twisted.conch.ssh.keys.Key}
|
||||
@since: 15.0
|
||||
"""
|
||||
for line in fileobj:
|
||||
line = line.strip()
|
||||
if line and not line.startswith(b"#"): # for comments
|
||||
try:
|
||||
yield parseKey(line)
|
||||
except keys.BadKeyError as e:
|
||||
_log.error(
|
||||
"Unable to parse line {line!r} as a key: {error!s}",
|
||||
line=line,
|
||||
error=e,
|
||||
)
|
||||
|
||||
|
||||
def _keysFromFilepaths(
|
||||
filepaths: Iterable[FilePath[Any]], parseKey: Callable[[bytes], keys.Key]
|
||||
) -> Iterable[keys.Key]:
|
||||
"""
|
||||
Helper function that turns an iterable of filepaths into a generator of
|
||||
keys. If any file cannot be read, a message is logged but it is
|
||||
otherwise ignored.
|
||||
|
||||
@param filepaths: iterable of L{twisted.python.filepath.FilePath}.
|
||||
@type filepaths: iterable
|
||||
|
||||
@param parseKey: a callable that takes a string and returns a
|
||||
L{twisted.conch.ssh.keys.Key}
|
||||
@type parseKey: L{callable}
|
||||
|
||||
@return: generator of L{twisted.conch.ssh.keys.Key}
|
||||
|
||||
@since: 15.0
|
||||
"""
|
||||
for fp in filepaths:
|
||||
if fp.exists():
|
||||
try:
|
||||
with fp.open() as f:
|
||||
yield from readAuthorizedKeyFile(f, parseKey)
|
||||
except OSError as e:
|
||||
_log.error("Unable to read {path!r}: {error!s}", path=fp.path, error=e)
|
||||
|
||||
|
||||
@implementer(IAuthorizedKeysDB)
|
||||
class InMemorySSHKeyDB:
|
||||
"""
|
||||
Object that provides SSH public keys based on a dictionary of usernames
|
||||
mapped to L{twisted.conch.ssh.keys.Key}s.
|
||||
|
||||
@since: 15.0
|
||||
"""
|
||||
|
||||
def __init__(self, mapping: Mapping[bytes, Iterable[keys.Key]]) -> None:
|
||||
"""
|
||||
Initializes a new L{InMemorySSHKeyDB}.
|
||||
|
||||
@param mapping: mapping of usernames to iterables of
|
||||
L{twisted.conch.ssh.keys.Key}s
|
||||
|
||||
"""
|
||||
self._mapping = mapping
|
||||
|
||||
def getAuthorizedKeys(self, username: bytes) -> Iterable[keys.Key]:
|
||||
"""
|
||||
Look up the authorized keys for a user.
|
||||
|
||||
@param username: Name of the user
|
||||
"""
|
||||
return self._mapping.get(username, [])
|
||||
|
||||
|
||||
@implementer(IAuthorizedKeysDB)
|
||||
class UNIXAuthorizedKeysFiles:
|
||||
"""
|
||||
Object that provides SSH public keys based on public keys listed in
|
||||
authorized_keys and authorized_keys2 files in UNIX user .ssh/ directories.
|
||||
If any of the files cannot be read, a message is logged but that file is
|
||||
otherwise ignored.
|
||||
|
||||
@since: 15.0
|
||||
"""
|
||||
|
||||
_userdb: UserDB
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
userdb: Optional[UserDB] = None,
|
||||
parseKey: Callable[[bytes], keys.Key] = keys.Key.fromString,
|
||||
):
|
||||
"""
|
||||
Initializes a new L{UNIXAuthorizedKeysFiles}.
|
||||
|
||||
@param userdb: access to the Unix user account and password database
|
||||
(default is the Python module L{pwd}, if available)
|
||||
|
||||
@param parseKey: a callable that takes a string and returns a
|
||||
L{twisted.conch.ssh.keys.Key}, mainly to be used for testing. The
|
||||
default is L{twisted.conch.ssh.keys.Key.fromString}.
|
||||
"""
|
||||
if userdb is not None:
|
||||
self._userdb = userdb
|
||||
elif pwd is not None:
|
||||
self._userdb = pwd
|
||||
else:
|
||||
raise ValueError("No pwd module found, and no userdb argument passed.")
|
||||
self._parseKey = parseKey
|
||||
|
||||
def getAuthorizedKeys(self, username: bytes) -> Iterable[keys.Key]:
|
||||
try:
|
||||
passwd = _lookupUser(self._userdb, username)
|
||||
except KeyError:
|
||||
return ()
|
||||
|
||||
root = FilePath(passwd.pw_dir).child(".ssh")
|
||||
files = ["authorized_keys", "authorized_keys2"]
|
||||
return _keysFromFilepaths((root.child(f) for f in files), self._parseKey)
|
||||
|
||||
|
||||
@implementer(ICredentialsChecker)
|
||||
class SSHPublicKeyChecker:
|
||||
"""
|
||||
Checker that authenticates SSH public keys, based on public keys listed in
|
||||
authorized_keys and authorized_keys2 files in user .ssh/ directories.
|
||||
|
||||
Initializing this checker with a L{UNIXAuthorizedKeysFiles} should be
|
||||
used instead of L{twisted.conch.checkers.SSHPublicKeyDatabase}.
|
||||
|
||||
@since: 15.0
|
||||
"""
|
||||
|
||||
credentialInterfaces = (ISSHPrivateKey,)
|
||||
|
||||
def __init__(self, keydb: IAuthorizedKeysDB) -> None:
|
||||
"""
|
||||
Initializes a L{SSHPublicKeyChecker}.
|
||||
|
||||
@param keydb: a provider of L{IAuthorizedKeysDB}
|
||||
"""
|
||||
self._keydb = keydb
|
||||
|
||||
def requestAvatarId(self, credentials):
|
||||
d = defer.execute(self._sanityCheckKey, credentials)
|
||||
d.addCallback(self._checkKey, credentials)
|
||||
d.addCallback(self._verifyKey, credentials)
|
||||
return d
|
||||
|
||||
def _sanityCheckKey(self, credentials):
|
||||
"""
|
||||
Checks whether the provided credentials are a valid SSH key with a
|
||||
signature (does not actually verify the signature).
|
||||
|
||||
@param credentials: the credentials offered by the user
|
||||
@type credentials: L{ISSHPrivateKey} provider
|
||||
|
||||
@raise ValidPublicKey: the credentials do not include a signature. See
|
||||
L{error.ValidPublicKey} for more information.
|
||||
|
||||
@raise BadKeyError: The key included with the credentials is not
|
||||
recognized as a key.
|
||||
|
||||
@return: the key in the credentials
|
||||
@rtype: L{twisted.conch.ssh.keys.Key}
|
||||
"""
|
||||
if not credentials.signature:
|
||||
raise error.ValidPublicKey()
|
||||
|
||||
return keys.Key.fromString(credentials.blob)
|
||||
|
||||
def _checkKey(self, pubKey, credentials):
|
||||
"""
|
||||
Checks the public key against all authorized keys (if any) for the
|
||||
user.
|
||||
|
||||
@param pubKey: the key in the credentials (just to prevent it from
|
||||
having to be calculated again)
|
||||
@type pubKey:
|
||||
|
||||
@param credentials: the credentials offered by the user
|
||||
@type credentials: L{ISSHPrivateKey} provider
|
||||
|
||||
@raise UnauthorizedLogin: If the key is not authorized, or if there
|
||||
was any error obtaining a list of authorized keys for the user.
|
||||
|
||||
@return: C{pubKey} if the key is authorized
|
||||
@rtype: L{twisted.conch.ssh.keys.Key}
|
||||
"""
|
||||
if any(
|
||||
key == pubKey for key in self._keydb.getAuthorizedKeys(credentials.username)
|
||||
):
|
||||
return pubKey
|
||||
|
||||
raise UnauthorizedLogin("Key not authorized")
|
||||
|
||||
def _verifyKey(self, pubKey, credentials):
|
||||
"""
|
||||
Checks whether the credentials themselves are valid, now that we know
|
||||
if the key matches the user.
|
||||
|
||||
@param pubKey: the key in the credentials (just to prevent it from
|
||||
having to be calculated again)
|
||||
@type pubKey: L{twisted.conch.ssh.keys.Key}
|
||||
|
||||
@param credentials: the credentials offered by the user
|
||||
@type credentials: L{ISSHPrivateKey} provider
|
||||
|
||||
@raise UnauthorizedLogin: If the key signature is invalid or there
|
||||
was any error verifying the signature.
|
||||
|
||||
@return: The user's username, if authentication was successful
|
||||
@rtype: L{bytes}
|
||||
"""
|
||||
try:
|
||||
if pubKey.verify(credentials.signature, credentials.sigData):
|
||||
return credentials.username
|
||||
except Exception as e: # Any error should be treated as a failed login
|
||||
raise UnauthorizedLogin("Error while verifying key") from e
|
||||
|
||||
raise UnauthorizedLogin("Key signature invalid.")
|
||||
@@ -0,0 +1,9 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
#
|
||||
"""
|
||||
Client support code for Conch.
|
||||
|
||||
Maintainer: Paul Swartz
|
||||
"""
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,65 @@
|
||||
# -*- test-case-name: twisted.conch.test.test_default -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Accesses the key agent for user authentication.
|
||||
|
||||
Maintainer: Paul Swartz
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from twisted.conch.ssh import agent, channel, keys
|
||||
from twisted.internet import protocol, reactor
|
||||
from twisted.logger import Logger
|
||||
|
||||
|
||||
class SSHAgentClient(agent.SSHAgentClient):
|
||||
_log = Logger()
|
||||
|
||||
def __init__(self):
|
||||
agent.SSHAgentClient.__init__(self)
|
||||
self.blobs = []
|
||||
|
||||
def getPublicKeys(self):
|
||||
return self.requestIdentities().addCallback(self._cbPublicKeys)
|
||||
|
||||
def _cbPublicKeys(self, blobcomm):
|
||||
self._log.debug("got {num_keys} public keys", num_keys=len(blobcomm))
|
||||
self.blobs = [x[0] for x in blobcomm]
|
||||
|
||||
def getPublicKey(self):
|
||||
"""
|
||||
Return a L{Key} from the first blob in C{self.blobs}, if any, or
|
||||
return L{None}.
|
||||
"""
|
||||
if self.blobs:
|
||||
return keys.Key.fromString(self.blobs.pop(0))
|
||||
return None
|
||||
|
||||
|
||||
class SSHAgentForwardingChannel(channel.SSHChannel):
|
||||
def channelOpen(self, specificData):
|
||||
cc = protocol.ClientCreator(reactor, SSHAgentForwardingLocal)
|
||||
d = cc.connectUNIX(os.environ["SSH_AUTH_SOCK"])
|
||||
d.addCallback(self._cbGotLocal)
|
||||
d.addErrback(lambda x: self.loseConnection())
|
||||
self.buf = ""
|
||||
|
||||
def _cbGotLocal(self, local):
|
||||
self.local = local
|
||||
self.dataReceived = self.local.transport.write
|
||||
self.local.dataReceived = self.write
|
||||
|
||||
def dataReceived(self, data):
|
||||
self.buf += data
|
||||
|
||||
def closed(self):
|
||||
if self.local:
|
||||
self.local.loseConnection()
|
||||
self.local = None
|
||||
|
||||
|
||||
class SSHAgentForwardingLocal(protocol.Protocol):
|
||||
pass
|
||||
@@ -0,0 +1,24 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
#
|
||||
from twisted.conch.client import direct
|
||||
|
||||
connectTypes = {"direct": direct.connect}
|
||||
|
||||
|
||||
def connect(host, port, options, verifyHostKey, userAuthObject):
|
||||
useConnects = ["direct"]
|
||||
return _ebConnect(
|
||||
None, useConnects, host, port, options, verifyHostKey, userAuthObject
|
||||
)
|
||||
|
||||
|
||||
def _ebConnect(f, useConnects, host, port, options, vhk, uao):
|
||||
if not useConnects:
|
||||
return f
|
||||
connectType = useConnects.pop(0)
|
||||
f = connectTypes[connectType]
|
||||
d = f(host, port, options, vhk, uao)
|
||||
d.addErrback(_ebConnect, useConnects, host, port, options, vhk, uao)
|
||||
return d
|
||||
@@ -0,0 +1,331 @@
|
||||
# -*- test-case-name: twisted.conch.test.test_knownhosts,twisted.conch.test.test_default -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Various classes and functions for implementing user-interaction in the
|
||||
command-line conch client.
|
||||
|
||||
You probably shouldn't use anything in this module directly, since it assumes
|
||||
you are sitting at an interactive terminal. For example, to programmatically
|
||||
interact with a known_hosts database, use L{twisted.conch.client.knownhosts}.
|
||||
"""
|
||||
|
||||
import contextlib
|
||||
import getpass
|
||||
import io
|
||||
import os
|
||||
import sys
|
||||
from base64 import decodebytes
|
||||
|
||||
from twisted.conch.client import agent
|
||||
from twisted.conch.client.knownhosts import ConsoleUI, KnownHostsFile
|
||||
from twisted.conch.error import ConchError
|
||||
from twisted.conch.ssh import common, keys, userauth
|
||||
from twisted.internet import defer, protocol, reactor
|
||||
from twisted.python.compat import nativeString
|
||||
from twisted.python.filepath import FilePath
|
||||
|
||||
# The default location of the known hosts file (probably should be parsed out
|
||||
# of an ssh config file someday).
|
||||
_KNOWN_HOSTS = "~/.ssh/known_hosts"
|
||||
|
||||
|
||||
# This name is bound so that the unit tests can use 'patch' to override it.
|
||||
_open = open
|
||||
_input = input
|
||||
|
||||
|
||||
def verifyHostKey(transport, host, pubKey, fingerprint):
|
||||
"""
|
||||
Verify a host's key.
|
||||
|
||||
This function is a gross vestige of some bad factoring in the client
|
||||
internals. The actual implementation, and a better signature of this logic
|
||||
is in L{KnownHostsFile.verifyHostKey}. This function is not deprecated yet
|
||||
because the callers have not yet been rehabilitated, but they should
|
||||
eventually be changed to call that method instead.
|
||||
|
||||
However, this function does perform two functions not implemented by
|
||||
L{KnownHostsFile.verifyHostKey}. It determines the path to the user's
|
||||
known_hosts file based on the options (which should really be the options
|
||||
object's job), and it provides an opener to L{ConsoleUI} which opens
|
||||
'/dev/tty' so that the user will be prompted on the tty of the process even
|
||||
if the input and output of the process has been redirected. This latter
|
||||
part is, somewhat obviously, not portable, but I don't know of a portable
|
||||
equivalent that could be used.
|
||||
|
||||
@param host: Due to a bug in L{SSHClientTransport.verifyHostKey}, this is
|
||||
always the dotted-quad IP address of the host being connected to.
|
||||
@type host: L{str}
|
||||
|
||||
@param transport: the client transport which is attempting to connect to
|
||||
the given host.
|
||||
@type transport: L{SSHClientTransport}
|
||||
|
||||
@param fingerprint: the fingerprint of the given public key, in
|
||||
xx:xx:xx:... format. This is ignored in favor of getting the fingerprint
|
||||
from the key itself.
|
||||
@type fingerprint: L{str}
|
||||
|
||||
@param pubKey: The public key of the server being connected to.
|
||||
@type pubKey: L{str}
|
||||
|
||||
@return: a L{Deferred} which fires with C{1} if the key was successfully
|
||||
verified, or fails if the key could not be successfully verified. Failure
|
||||
types may include L{HostKeyChanged}, L{UserRejectedKey}, L{IOError} or
|
||||
L{KeyboardInterrupt}.
|
||||
"""
|
||||
actualHost = transport.factory.options["host"]
|
||||
actualKey = keys.Key.fromString(pubKey)
|
||||
kh = KnownHostsFile.fromPath(
|
||||
FilePath(
|
||||
transport.factory.options["known-hosts"] or os.path.expanduser(_KNOWN_HOSTS)
|
||||
)
|
||||
)
|
||||
ui = ConsoleUI(lambda: _open("/dev/tty", "r+b", buffering=0))
|
||||
return kh.verifyHostKey(ui, actualHost, host, actualKey)
|
||||
|
||||
|
||||
def isInKnownHosts(host, pubKey, options):
|
||||
"""
|
||||
Checks to see if host is in the known_hosts file for the user.
|
||||
|
||||
@return: 0 if it isn't, 1 if it is and is the same, 2 if it's changed.
|
||||
@rtype: L{int}
|
||||
"""
|
||||
keyType = common.getNS(pubKey)[0]
|
||||
retVal = 0
|
||||
|
||||
if not options["known-hosts"] and not os.path.exists(os.path.expanduser("~/.ssh/")):
|
||||
print("Creating ~/.ssh directory...")
|
||||
os.mkdir(os.path.expanduser("~/.ssh"))
|
||||
kh_file = options["known-hosts"] or _KNOWN_HOSTS
|
||||
try:
|
||||
known_hosts = open(os.path.expanduser(kh_file), "rb")
|
||||
except OSError:
|
||||
return 0
|
||||
with known_hosts:
|
||||
for line in known_hosts.readlines():
|
||||
split = line.split()
|
||||
if len(split) < 3:
|
||||
continue
|
||||
hosts, hostKeyType, encodedKey = split[:3]
|
||||
if host not in hosts.split(b","): # incorrect host
|
||||
continue
|
||||
if hostKeyType != keyType: # incorrect type of key
|
||||
continue
|
||||
try:
|
||||
decodedKey = decodebytes(encodedKey)
|
||||
except BaseException:
|
||||
continue
|
||||
if decodedKey == pubKey:
|
||||
return 1
|
||||
else:
|
||||
retVal = 2
|
||||
return retVal
|
||||
|
||||
|
||||
def getHostKeyAlgorithms(host, options):
|
||||
"""
|
||||
Look in known_hosts for a key corresponding to C{host}.
|
||||
This can be used to change the order of supported key types
|
||||
in the KEXINIT packet.
|
||||
|
||||
@type host: L{str}
|
||||
@param host: the host to check in known_hosts
|
||||
@type options: L{twisted.conch.client.options.ConchOptions}
|
||||
@param options: options passed to client
|
||||
@return: L{list} of L{str} representing key types or L{None}.
|
||||
"""
|
||||
knownHosts = KnownHostsFile.fromPath(
|
||||
FilePath(options["known-hosts"] or os.path.expanduser(_KNOWN_HOSTS))
|
||||
)
|
||||
keyTypes = []
|
||||
for entry in knownHosts.iterentries():
|
||||
if entry.matchesHost(host):
|
||||
if entry.keyType not in keyTypes:
|
||||
keyTypes.append(entry.keyType)
|
||||
return keyTypes or None
|
||||
|
||||
|
||||
class SSHUserAuthClient(userauth.SSHUserAuthClient):
|
||||
def __init__(self, user, options, *args):
|
||||
userauth.SSHUserAuthClient.__init__(self, user, *args)
|
||||
self.keyAgent = None
|
||||
self.options = options
|
||||
self.usedFiles = []
|
||||
if not options.identitys:
|
||||
options.identitys = ["~/.ssh/id_rsa", "~/.ssh/id_dsa"]
|
||||
|
||||
def serviceStarted(self):
|
||||
if "SSH_AUTH_SOCK" in os.environ and not self.options["noagent"]:
|
||||
self._log.debug(
|
||||
"using SSH agent {authSock!r}", authSock=os.environ["SSH_AUTH_SOCK"]
|
||||
)
|
||||
cc = protocol.ClientCreator(reactor, agent.SSHAgentClient)
|
||||
d = cc.connectUNIX(os.environ["SSH_AUTH_SOCK"])
|
||||
d.addCallback(self._setAgent)
|
||||
d.addErrback(self._ebSetAgent)
|
||||
else:
|
||||
userauth.SSHUserAuthClient.serviceStarted(self)
|
||||
|
||||
def serviceStopped(self):
|
||||
if self.keyAgent:
|
||||
self.keyAgent.transport.loseConnection()
|
||||
self.keyAgent = None
|
||||
|
||||
def _setAgent(self, a):
|
||||
self.keyAgent = a
|
||||
d = self.keyAgent.getPublicKeys()
|
||||
d.addBoth(self._ebSetAgent)
|
||||
return d
|
||||
|
||||
def _ebSetAgent(self, f):
|
||||
userauth.SSHUserAuthClient.serviceStarted(self)
|
||||
|
||||
def _getPassword(self, prompt):
|
||||
"""
|
||||
Prompt for a password using L{getpass.getpass}.
|
||||
|
||||
@param prompt: Written on tty to ask for the input.
|
||||
@type prompt: L{str}
|
||||
@return: The input.
|
||||
@rtype: L{str}
|
||||
"""
|
||||
with self._replaceStdoutStdin():
|
||||
try:
|
||||
p = getpass.getpass(prompt)
|
||||
return p
|
||||
except (KeyboardInterrupt, OSError):
|
||||
print()
|
||||
raise ConchError("PEBKAC")
|
||||
|
||||
def getPassword(self, prompt=None):
|
||||
if prompt:
|
||||
prompt = nativeString(prompt)
|
||||
else:
|
||||
prompt = "{}@{}'s password: ".format(
|
||||
nativeString(self.user),
|
||||
self.transport.transport.getPeer().host,
|
||||
)
|
||||
try:
|
||||
# We don't know the encoding the other side is using,
|
||||
# signaling that is not part of the SSH protocol. But
|
||||
# using our defaultencoding is better than just going for
|
||||
# ASCII.
|
||||
p = self._getPassword(prompt).encode(sys.getdefaultencoding())
|
||||
return defer.succeed(p)
|
||||
except ConchError:
|
||||
return defer.fail()
|
||||
|
||||
def getPublicKey(self):
|
||||
"""
|
||||
Get a public key from the key agent if possible, otherwise look in
|
||||
the next configured identity file for one.
|
||||
"""
|
||||
if self.keyAgent:
|
||||
key = self.keyAgent.getPublicKey()
|
||||
if key is not None:
|
||||
return key
|
||||
files = [x for x in self.options.identitys if x not in self.usedFiles]
|
||||
self._log.debug(
|
||||
"public key identities: {identities}\n{files}",
|
||||
identities=self.options.identitys,
|
||||
files=files,
|
||||
)
|
||||
if not files:
|
||||
return None
|
||||
file = files[0]
|
||||
self.usedFiles.append(file)
|
||||
file = os.path.expanduser(file)
|
||||
file += ".pub"
|
||||
if not os.path.exists(file):
|
||||
return self.getPublicKey() # try again
|
||||
try:
|
||||
return keys.Key.fromFile(file)
|
||||
except keys.BadKeyError:
|
||||
return self.getPublicKey() # try again
|
||||
|
||||
def signData(self, publicKey, signData):
|
||||
"""
|
||||
Extend the base signing behavior by using an SSH agent to sign the
|
||||
data, if one is available.
|
||||
|
||||
@type publicKey: L{Key}
|
||||
@type signData: L{bytes}
|
||||
"""
|
||||
if not self.usedFiles: # agent key
|
||||
return self.keyAgent.signData(publicKey.blob(), signData)
|
||||
else:
|
||||
return userauth.SSHUserAuthClient.signData(self, publicKey, signData)
|
||||
|
||||
def getPrivateKey(self):
|
||||
"""
|
||||
Try to load the private key from the last used file identified by
|
||||
C{getPublicKey}, potentially asking for the passphrase if the key is
|
||||
encrypted.
|
||||
"""
|
||||
file = os.path.expanduser(self.usedFiles[-1])
|
||||
if not os.path.exists(file):
|
||||
return None
|
||||
try:
|
||||
return defer.succeed(keys.Key.fromFile(file))
|
||||
except keys.EncryptedKeyError:
|
||||
for i in range(3):
|
||||
prompt = "Enter passphrase for key '%s': " % self.usedFiles[-1]
|
||||
try:
|
||||
p = self._getPassword(prompt).encode(sys.getfilesystemencoding())
|
||||
return defer.succeed(keys.Key.fromFile(file, passphrase=p))
|
||||
except (keys.BadKeyError, ConchError):
|
||||
pass
|
||||
return defer.fail(ConchError("bad password"))
|
||||
raise
|
||||
except KeyboardInterrupt:
|
||||
print()
|
||||
reactor.stop()
|
||||
|
||||
def getGenericAnswers(self, name, instruction, prompts):
|
||||
responses = []
|
||||
with self._replaceStdoutStdin():
|
||||
if name:
|
||||
print(name.decode("utf-8"))
|
||||
if instruction:
|
||||
print(instruction.decode("utf-8"))
|
||||
for prompt, echo in prompts:
|
||||
prompt = prompt.decode("utf-8")
|
||||
if echo:
|
||||
responses.append(_input(prompt))
|
||||
else:
|
||||
responses.append(getpass.getpass(prompt))
|
||||
return defer.succeed(responses)
|
||||
|
||||
@classmethod
|
||||
def _openTty(cls):
|
||||
"""
|
||||
Open /dev/tty as two streams one in read, one in write mode,
|
||||
and return them.
|
||||
|
||||
@return: File objects for reading and writing to /dev/tty,
|
||||
corresponding to standard input and standard output.
|
||||
@rtype: A L{tuple} of L{io.TextIOWrapper} on Python 3.
|
||||
"""
|
||||
stdin = io.TextIOWrapper(open("/dev/tty", "rb"))
|
||||
stdout = io.TextIOWrapper(open("/dev/tty", "wb"))
|
||||
return stdin, stdout
|
||||
|
||||
@classmethod
|
||||
@contextlib.contextmanager
|
||||
def _replaceStdoutStdin(cls):
|
||||
"""
|
||||
Contextmanager that replaces stdout and stdin with /dev/tty
|
||||
and resets them when it is done.
|
||||
"""
|
||||
oldout, oldin = sys.stdout, sys.stdin
|
||||
sys.stdin, sys.stdout = cls._openTty()
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
sys.stdout.close()
|
||||
sys.stdin.close()
|
||||
sys.stdout, sys.stdin = oldout, oldin
|
||||
@@ -0,0 +1,98 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
|
||||
from twisted.conch import error
|
||||
from twisted.conch.ssh import transport
|
||||
from twisted.internet import defer, protocol, reactor
|
||||
|
||||
|
||||
class SSHClientFactory(protocol.ClientFactory):
|
||||
def __init__(self, d, options, verifyHostKey, userAuthObject):
|
||||
self.d = d
|
||||
self.options = options
|
||||
self.verifyHostKey = verifyHostKey
|
||||
self.userAuthObject = userAuthObject
|
||||
|
||||
def clientConnectionLost(self, connector, reason):
|
||||
if self.options["reconnect"]:
|
||||
connector.connect()
|
||||
|
||||
def clientConnectionFailed(self, connector, reason):
|
||||
if self.d is None:
|
||||
return
|
||||
d, self.d = self.d, None
|
||||
d.errback(reason)
|
||||
|
||||
def buildProtocol(self, addr):
|
||||
trans = SSHClientTransport(self)
|
||||
if self.options["ciphers"]:
|
||||
trans.supportedCiphers = self.options["ciphers"]
|
||||
if self.options["macs"]:
|
||||
trans.supportedMACs = self.options["macs"]
|
||||
if self.options["compress"]:
|
||||
trans.supportedCompressions[0:1] = ["zlib"]
|
||||
if self.options["host-key-algorithms"]:
|
||||
trans.supportedPublicKeys = self.options["host-key-algorithms"]
|
||||
return trans
|
||||
|
||||
|
||||
class SSHClientTransport(transport.SSHClientTransport):
|
||||
def __init__(self, factory):
|
||||
self.factory = factory
|
||||
self.unixServer = None
|
||||
|
||||
def connectionLost(self, reason):
|
||||
if self.unixServer:
|
||||
d = self.unixServer.stopListening()
|
||||
self.unixServer = None
|
||||
else:
|
||||
d = defer.succeed(None)
|
||||
d.addCallback(
|
||||
lambda x: transport.SSHClientTransport.connectionLost(self, reason)
|
||||
)
|
||||
|
||||
def receiveError(self, code, desc):
|
||||
if self.factory.d is None:
|
||||
return
|
||||
d, self.factory.d = self.factory.d, None
|
||||
d.errback(error.ConchError(desc, code))
|
||||
|
||||
def sendDisconnect(self, code, reason):
|
||||
if self.factory.d is None:
|
||||
return
|
||||
d, self.factory.d = self.factory.d, None
|
||||
transport.SSHClientTransport.sendDisconnect(self, code, reason)
|
||||
d.errback(error.ConchError(reason, code))
|
||||
|
||||
def receiveDebug(self, alwaysDisplay, message, lang):
|
||||
self._log.debug(
|
||||
"Received Debug Message: {message}",
|
||||
message=message,
|
||||
alwaysDisplay=alwaysDisplay,
|
||||
lang=lang,
|
||||
)
|
||||
if alwaysDisplay: # XXX what should happen here?
|
||||
print(message)
|
||||
|
||||
def verifyHostKey(self, pubKey, fingerprint):
|
||||
return self.factory.verifyHostKey(
|
||||
self, self.transport.getPeer().host, pubKey, fingerprint
|
||||
)
|
||||
|
||||
def setService(self, service):
|
||||
self._log.info("setting client server to {service}", service=service)
|
||||
transport.SSHClientTransport.setService(self, service)
|
||||
if service.name != "ssh-userauth" and self.factory.d is not None:
|
||||
d, self.factory.d = self.factory.d, None
|
||||
d.callback(None)
|
||||
|
||||
def connectionSecure(self):
|
||||
self.requestService(self.factory.userAuthObject)
|
||||
|
||||
|
||||
def connect(host, port, options, verifyHostKey, userAuthObject):
|
||||
d = defer.Deferred()
|
||||
factory = SSHClientFactory(d, options, verifyHostKey, userAuthObject)
|
||||
reactor.connectTCP(host, port, factory)
|
||||
return d
|
||||
@@ -0,0 +1,622 @@
|
||||
# -*- test-case-name: twisted.conch.test.test_knownhosts -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
An implementation of the OpenSSH known_hosts database.
|
||||
|
||||
@since: 8.2
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hmac
|
||||
import sys
|
||||
from binascii import Error as DecodeError, a2b_base64, b2a_base64
|
||||
from contextlib import closing
|
||||
from hashlib import sha1
|
||||
from typing import IO, Callable, Literal
|
||||
|
||||
from zope.interface import implementer
|
||||
|
||||
from twisted.conch.error import HostKeyChanged, InvalidEntry, UserRejectedKey
|
||||
from twisted.conch.interfaces import IKnownHostEntry
|
||||
from twisted.conch.ssh.keys import BadKeyError, FingerprintFormats, Key
|
||||
from twisted.internet import defer
|
||||
from twisted.internet.defer import Deferred
|
||||
from twisted.logger import Logger
|
||||
from twisted.python.compat import nativeString
|
||||
from twisted.python.filepath import FilePath
|
||||
from twisted.python.randbytes import secureRandom
|
||||
from twisted.python.util import FancyEqMixin
|
||||
|
||||
log = Logger()
|
||||
|
||||
|
||||
def _b64encode(s):
|
||||
"""
|
||||
Encode a binary string as base64 with no trailing newline.
|
||||
|
||||
@param s: The string to encode.
|
||||
@type s: L{bytes}
|
||||
|
||||
@return: The base64-encoded string.
|
||||
@rtype: L{bytes}
|
||||
"""
|
||||
return b2a_base64(s).strip()
|
||||
|
||||
|
||||
def _extractCommon(string):
|
||||
"""
|
||||
Extract common elements of base64 keys from an entry in a hosts file.
|
||||
|
||||
@param string: A known hosts file entry (a single line).
|
||||
@type string: L{bytes}
|
||||
|
||||
@return: a 4-tuple of hostname data (L{bytes}), ssh key type (L{bytes}), key
|
||||
(L{Key}), and comment (L{bytes} or L{None}). The hostname data is
|
||||
simply the beginning of the line up to the first occurrence of
|
||||
whitespace.
|
||||
@rtype: L{tuple}
|
||||
"""
|
||||
elements = string.split(None, 2)
|
||||
if len(elements) != 3:
|
||||
raise InvalidEntry()
|
||||
hostnames, keyType, keyAndComment = elements
|
||||
splitkey = keyAndComment.split(None, 1)
|
||||
if len(splitkey) == 2:
|
||||
keyString, comment = splitkey
|
||||
comment = comment.rstrip(b"\n")
|
||||
else:
|
||||
keyString = splitkey[0]
|
||||
comment = None
|
||||
key = Key.fromString(a2b_base64(keyString))
|
||||
return hostnames, keyType, key, comment
|
||||
|
||||
|
||||
class _BaseEntry:
|
||||
"""
|
||||
Abstract base of both hashed and non-hashed entry objects, since they
|
||||
represent keys and key types the same way.
|
||||
|
||||
@ivar keyType: The type of the key; either ssh-dss or ssh-rsa.
|
||||
@type keyType: L{bytes}
|
||||
|
||||
@ivar publicKey: The server public key indicated by this line.
|
||||
@type publicKey: L{twisted.conch.ssh.keys.Key}
|
||||
|
||||
@ivar comment: Trailing garbage after the key line.
|
||||
@type comment: L{bytes}
|
||||
"""
|
||||
|
||||
def __init__(self, keyType, publicKey, comment):
|
||||
self.keyType = keyType
|
||||
self.publicKey = publicKey
|
||||
self.comment = comment
|
||||
|
||||
def matchesKey(self, keyObject):
|
||||
"""
|
||||
Check to see if this entry matches a given key object.
|
||||
|
||||
@param keyObject: A public key object to check.
|
||||
@type keyObject: L{Key}
|
||||
|
||||
@return: C{True} if this entry's key matches C{keyObject}, C{False}
|
||||
otherwise.
|
||||
@rtype: L{bool}
|
||||
"""
|
||||
return self.publicKey == keyObject
|
||||
|
||||
|
||||
@implementer(IKnownHostEntry)
|
||||
class PlainEntry(_BaseEntry):
|
||||
"""
|
||||
A L{PlainEntry} is a representation of a plain-text entry in a known_hosts
|
||||
file.
|
||||
|
||||
@ivar _hostnames: the list of all host-names associated with this entry.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, hostnames: list[bytes], keyType: bytes, publicKey: Key, comment: bytes
|
||||
):
|
||||
self._hostnames: list[bytes] = hostnames
|
||||
super().__init__(keyType, publicKey, comment)
|
||||
|
||||
@classmethod
|
||||
def fromString(cls, string: bytes) -> PlainEntry:
|
||||
"""
|
||||
Parse a plain-text entry in a known_hosts file, and return a
|
||||
corresponding L{PlainEntry}.
|
||||
|
||||
@param string: a space-separated string formatted like "hostname
|
||||
key-type base64-key-data comment".
|
||||
|
||||
@raise DecodeError: if the key is not valid encoded as valid base64.
|
||||
|
||||
@raise InvalidEntry: if the entry does not have the right number of
|
||||
elements and is therefore invalid.
|
||||
|
||||
@raise BadKeyError: if the key, once decoded from base64, is not
|
||||
actually an SSH key.
|
||||
|
||||
@return: an IKnownHostEntry representing the hostname and key in the
|
||||
input line.
|
||||
|
||||
@rtype: L{PlainEntry}
|
||||
"""
|
||||
hostnames, keyType, key, comment = _extractCommon(string)
|
||||
self = cls(hostnames.split(b","), keyType, key, comment)
|
||||
return self
|
||||
|
||||
def matchesHost(self, hostname: bytes | str) -> bool:
|
||||
"""
|
||||
Check to see if this entry matches a given hostname.
|
||||
|
||||
@param hostname: A hostname or IP address literal to check against this
|
||||
entry.
|
||||
|
||||
@return: C{True} if this entry is for the given hostname or IP address,
|
||||
C{False} otherwise.
|
||||
"""
|
||||
if isinstance(hostname, str):
|
||||
hostname = hostname.encode("utf-8")
|
||||
return hostname in self._hostnames
|
||||
|
||||
def toString(self) -> bytes:
|
||||
"""
|
||||
Implement L{IKnownHostEntry.toString} by recording the comma-separated
|
||||
hostnames, key type, and base-64 encoded key.
|
||||
|
||||
@return: The string representation of this entry, with unhashed hostname
|
||||
information.
|
||||
"""
|
||||
fields = [
|
||||
b",".join(self._hostnames),
|
||||
self.keyType,
|
||||
_b64encode(self.publicKey.blob()),
|
||||
]
|
||||
if self.comment is not None:
|
||||
fields.append(self.comment)
|
||||
return b" ".join(fields)
|
||||
|
||||
|
||||
@implementer(IKnownHostEntry)
|
||||
class UnparsedEntry:
|
||||
"""
|
||||
L{UnparsedEntry} is an entry in a L{KnownHostsFile} which can't actually be
|
||||
parsed; therefore it matches no keys and no hosts.
|
||||
"""
|
||||
|
||||
def __init__(self, string):
|
||||
"""
|
||||
Create an unparsed entry from a line in a known_hosts file which cannot
|
||||
otherwise be parsed.
|
||||
"""
|
||||
self._string = string
|
||||
|
||||
def matchesHost(self, hostname):
|
||||
"""
|
||||
Always returns False.
|
||||
"""
|
||||
return False
|
||||
|
||||
def matchesKey(self, key):
|
||||
"""
|
||||
Always returns False.
|
||||
"""
|
||||
return False
|
||||
|
||||
def toString(self):
|
||||
"""
|
||||
Returns the input line, without its newline if one was given.
|
||||
|
||||
@return: The string representation of this entry, almost exactly as was
|
||||
used to initialize this entry but without a trailing newline.
|
||||
@rtype: L{bytes}
|
||||
"""
|
||||
return self._string.rstrip(b"\n")
|
||||
|
||||
|
||||
def _hmacedString(key, string):
|
||||
"""
|
||||
Return the SHA-1 HMAC hash of the given key and string.
|
||||
|
||||
@param key: The HMAC key.
|
||||
@type key: L{bytes}
|
||||
|
||||
@param string: The string to be hashed.
|
||||
@type string: L{bytes}
|
||||
|
||||
@return: The keyed hash value.
|
||||
@rtype: L{bytes}
|
||||
"""
|
||||
hash = hmac.HMAC(key, digestmod=sha1)
|
||||
if isinstance(string, str):
|
||||
string = string.encode("utf-8")
|
||||
hash.update(string)
|
||||
return hash.digest()
|
||||
|
||||
|
||||
@implementer(IKnownHostEntry)
|
||||
class HashedEntry(_BaseEntry, FancyEqMixin):
|
||||
"""
|
||||
A L{HashedEntry} is a representation of an entry in a known_hosts file
|
||||
where the hostname has been hashed and salted.
|
||||
|
||||
@ivar _hostSalt: the salt to combine with a hostname for hashing.
|
||||
|
||||
@ivar _hostHash: the hashed representation of the hostname.
|
||||
|
||||
@cvar MAGIC: the 'hash magic' string used to identify a hashed line in a
|
||||
known_hosts file as opposed to a plaintext one.
|
||||
"""
|
||||
|
||||
MAGIC = b"|1|"
|
||||
|
||||
compareAttributes = ("_hostSalt", "_hostHash", "keyType", "publicKey", "comment")
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hostSalt: bytes,
|
||||
hostHash: bytes,
|
||||
keyType: bytes,
|
||||
publicKey: Key,
|
||||
comment: bytes | None,
|
||||
) -> None:
|
||||
self._hostSalt = hostSalt
|
||||
self._hostHash = hostHash
|
||||
super().__init__(keyType, publicKey, comment)
|
||||
|
||||
@classmethod
|
||||
def fromString(cls, string: bytes) -> HashedEntry:
|
||||
"""
|
||||
Load a hashed entry from a string representing a line in a known_hosts
|
||||
file.
|
||||
|
||||
@param string: A complete single line from a I{known_hosts} file,
|
||||
formatted as defined by OpenSSH.
|
||||
|
||||
@raise DecodeError: if the key, the hostname, or the is not valid
|
||||
encoded as valid base64
|
||||
|
||||
@raise InvalidEntry: if the entry does not have the right number of
|
||||
elements and is therefore invalid, or the host/hash portion
|
||||
contains more items than just the host and hash.
|
||||
|
||||
@raise BadKeyError: if the key, once decoded from base64, is not
|
||||
actually an SSH key.
|
||||
|
||||
@return: The newly created L{HashedEntry} instance, initialized with
|
||||
the information from C{string}.
|
||||
"""
|
||||
stuff, keyType, key, comment = _extractCommon(string)
|
||||
saltAndHash = stuff[len(cls.MAGIC) :].split(b"|")
|
||||
if len(saltAndHash) != 2:
|
||||
raise InvalidEntry()
|
||||
hostSalt, hostHash = saltAndHash
|
||||
self = cls(a2b_base64(hostSalt), a2b_base64(hostHash), keyType, key, comment)
|
||||
return self
|
||||
|
||||
def matchesHost(self, hostname):
|
||||
"""
|
||||
Implement L{IKnownHostEntry.matchesHost} to compare the hash of the
|
||||
input to the stored hash.
|
||||
|
||||
@param hostname: A hostname or IP address literal to check against this
|
||||
entry.
|
||||
@type hostname: L{bytes}
|
||||
|
||||
@return: C{True} if this entry is for the given hostname or IP address,
|
||||
C{False} otherwise.
|
||||
@rtype: L{bool}
|
||||
"""
|
||||
return hmac.compare_digest(
|
||||
_hmacedString(self._hostSalt, hostname), self._hostHash
|
||||
)
|
||||
|
||||
def toString(self):
|
||||
"""
|
||||
Implement L{IKnownHostEntry.toString} by base64-encoding the salt, host
|
||||
hash, and key.
|
||||
|
||||
@return: The string representation of this entry, with the hostname part
|
||||
hashed.
|
||||
@rtype: L{bytes}
|
||||
"""
|
||||
fields = [
|
||||
self.MAGIC
|
||||
+ b"|".join([_b64encode(self._hostSalt), _b64encode(self._hostHash)]),
|
||||
self.keyType,
|
||||
_b64encode(self.publicKey.blob()),
|
||||
]
|
||||
if self.comment is not None:
|
||||
fields.append(self.comment)
|
||||
return b" ".join(fields)
|
||||
|
||||
|
||||
class KnownHostsFile:
|
||||
"""
|
||||
A structured representation of an OpenSSH-format ~/.ssh/known_hosts file.
|
||||
|
||||
@ivar _added: A list of L{IKnownHostEntry} providers which have been added
|
||||
to this instance in memory but not yet saved.
|
||||
|
||||
@ivar _clobber: A flag indicating whether the current contents of the save
|
||||
path will be disregarded and potentially overwritten or not. If
|
||||
C{True}, this will be done. If C{False}, entries in the save path will
|
||||
be read and new entries will be saved by appending rather than
|
||||
overwriting.
|
||||
@type _clobber: L{bool}
|
||||
|
||||
@ivar _savePath: See C{savePath} parameter of L{__init__}.
|
||||
"""
|
||||
|
||||
def __init__(self, savePath: FilePath[str]) -> None:
|
||||
"""
|
||||
Create a new, empty KnownHostsFile.
|
||||
|
||||
Unless you want to erase the current contents of C{savePath}, you want
|
||||
to use L{KnownHostsFile.fromPath} instead.
|
||||
|
||||
@param savePath: The L{FilePath} to which to save new entries.
|
||||
@type savePath: L{FilePath}
|
||||
"""
|
||||
self._added: list[IKnownHostEntry] = []
|
||||
self._savePath = savePath
|
||||
self._clobber = True
|
||||
|
||||
@property
|
||||
def savePath(self) -> FilePath[str]:
|
||||
"""
|
||||
@see: C{savePath} parameter of L{__init__}
|
||||
"""
|
||||
return self._savePath
|
||||
|
||||
def iterentries(self):
|
||||
"""
|
||||
Iterate over the host entries in this file.
|
||||
|
||||
@return: An iterable the elements of which provide L{IKnownHostEntry}.
|
||||
There is an element for each entry in the file as well as an element
|
||||
for each added but not yet saved entry.
|
||||
@rtype: iterable of L{IKnownHostEntry} providers
|
||||
"""
|
||||
for entry in self._added:
|
||||
yield entry
|
||||
|
||||
if self._clobber:
|
||||
return
|
||||
|
||||
try:
|
||||
fp = self._savePath.open()
|
||||
except OSError:
|
||||
return
|
||||
|
||||
with fp:
|
||||
for line in fp:
|
||||
try:
|
||||
if line.startswith(HashedEntry.MAGIC):
|
||||
entry = HashedEntry.fromString(line)
|
||||
else:
|
||||
entry = PlainEntry.fromString(line)
|
||||
except (DecodeError, InvalidEntry, BadKeyError):
|
||||
entry = UnparsedEntry(line)
|
||||
yield entry
|
||||
|
||||
def hasHostKey(self, hostname, key):
|
||||
"""
|
||||
Check for an entry with matching hostname and key.
|
||||
|
||||
@param hostname: A hostname or IP address literal to check for.
|
||||
@type hostname: L{bytes}
|
||||
|
||||
@param key: The public key to check for.
|
||||
@type key: L{Key}
|
||||
|
||||
@return: C{True} if the given hostname and key are present in this file,
|
||||
C{False} if they are not.
|
||||
@rtype: L{bool}
|
||||
|
||||
@raise HostKeyChanged: if the host key found for the given hostname
|
||||
does not match the given key.
|
||||
"""
|
||||
for lineidx, entry in enumerate(self.iterentries(), -len(self._added)):
|
||||
if entry.matchesHost(hostname) and entry.keyType == key.sshType():
|
||||
if entry.matchesKey(key):
|
||||
return True
|
||||
else:
|
||||
# Notice that lineidx is 0-based but HostKeyChanged.lineno
|
||||
# is 1-based.
|
||||
if lineidx < 0:
|
||||
line = None
|
||||
path = None
|
||||
else:
|
||||
line = lineidx + 1
|
||||
path = self._savePath
|
||||
raise HostKeyChanged(entry, path, line)
|
||||
return False
|
||||
|
||||
def verifyHostKey(
|
||||
self, ui: ConsoleUI, hostname: bytes, ip: bytes, key: Key
|
||||
) -> Deferred[bool]:
|
||||
"""
|
||||
Verify the given host key for the given IP and host, asking for
|
||||
confirmation from, and notifying, the given UI about changes to this
|
||||
file.
|
||||
|
||||
@param ui: The user interface to request an IP address from.
|
||||
|
||||
@param hostname: The hostname that the user requested to connect to.
|
||||
|
||||
@param ip: The string representation of the IP address that is actually
|
||||
being connected to.
|
||||
|
||||
@param key: The public key of the server.
|
||||
|
||||
@return: a L{Deferred} that fires with True when the key has been
|
||||
verified, or fires with an errback when the key either cannot be
|
||||
verified or has changed.
|
||||
@rtype: L{Deferred}
|
||||
"""
|
||||
hhk = defer.execute(self.hasHostKey, hostname, key)
|
||||
|
||||
def gotHasKey(result: bool) -> bool | Deferred[bool]:
|
||||
if result:
|
||||
if not self.hasHostKey(ip, key):
|
||||
addMessage = (
|
||||
f"Warning: Permanently added the {key.type()} host key"
|
||||
f" for IP address '{ip.decode()}' to the list of known"
|
||||
" hosts.\n"
|
||||
)
|
||||
ui.warn(addMessage.encode("utf-8"))
|
||||
self.addHostKey(ip, key)
|
||||
self.save()
|
||||
return result
|
||||
else:
|
||||
|
||||
def promptResponse(response: bool) -> bool:
|
||||
if response:
|
||||
self.addHostKey(hostname, key)
|
||||
self.addHostKey(ip, key)
|
||||
self.save()
|
||||
return response
|
||||
else:
|
||||
raise UserRejectedKey()
|
||||
|
||||
keytype: str = key.type()
|
||||
|
||||
if keytype == "EC":
|
||||
keytype = "ECDSA"
|
||||
|
||||
prompt = (
|
||||
"The authenticity of host '%s (%s)' "
|
||||
"can't be established.\n"
|
||||
"%s key fingerprint is SHA256:%s.\n"
|
||||
"Are you sure you want to continue connecting (yes/no)? "
|
||||
% (
|
||||
nativeString(hostname),
|
||||
nativeString(ip),
|
||||
keytype,
|
||||
key.fingerprint(format=FingerprintFormats.SHA256_BASE64),
|
||||
)
|
||||
)
|
||||
proceed = ui.prompt(prompt.encode(sys.getdefaultencoding()))
|
||||
return proceed.addCallback(promptResponse)
|
||||
|
||||
return hhk.addCallback(gotHasKey)
|
||||
|
||||
def addHostKey(self, hostname: bytes, key: Key) -> HashedEntry:
|
||||
"""
|
||||
Add a new L{HashedEntry} to the key database.
|
||||
|
||||
Note that you still need to call L{KnownHostsFile.save} if you wish
|
||||
these changes to be persisted.
|
||||
|
||||
@param hostname: A hostname or IP address literal to associate with the
|
||||
new entry.
|
||||
@type hostname: L{bytes}
|
||||
|
||||
@param key: The public key to associate with the new entry.
|
||||
@type key: L{Key}
|
||||
|
||||
@return: The L{HashedEntry} that was added.
|
||||
@rtype: L{HashedEntry}
|
||||
"""
|
||||
salt = secureRandom(20)
|
||||
keyType = key.sshType()
|
||||
entry = HashedEntry(salt, _hmacedString(salt, hostname), keyType, key, None)
|
||||
self._added.append(entry)
|
||||
return entry
|
||||
|
||||
def save(self) -> None:
|
||||
"""
|
||||
Save this L{KnownHostsFile} to the path it was loaded from.
|
||||
"""
|
||||
p = self._savePath.parent()
|
||||
if not p.isdir():
|
||||
p.makedirs()
|
||||
|
||||
mode: Literal["a", "w"] = "w" if self._clobber else "a"
|
||||
with self._savePath.open(mode) as hostsFileObj:
|
||||
if self._added:
|
||||
hostsFileObj.write(
|
||||
b"\n".join([entry.toString() for entry in self._added]) + b"\n"
|
||||
)
|
||||
self._added = []
|
||||
self._clobber = False
|
||||
|
||||
@classmethod
|
||||
def fromPath(cls, path: FilePath[str]) -> KnownHostsFile:
|
||||
"""
|
||||
Create a new L{KnownHostsFile}, potentially reading existing known
|
||||
hosts information from the given file.
|
||||
|
||||
@param path: A path object to use for both reading contents from and
|
||||
later saving to. If no file exists at this path, it is not an
|
||||
error; a L{KnownHostsFile} with no entries is returned.
|
||||
|
||||
@return: A L{KnownHostsFile} initialized with entries from C{path}.
|
||||
"""
|
||||
knownHosts = cls(path)
|
||||
knownHosts._clobber = False
|
||||
return knownHosts
|
||||
|
||||
|
||||
class ConsoleUI:
|
||||
"""
|
||||
A UI object that can ask true/false questions and post notifications on the
|
||||
console, to be used during key verification.
|
||||
"""
|
||||
|
||||
def __init__(self, opener: Callable[[], IO[bytes]]):
|
||||
"""
|
||||
@param opener: A no-argument callable which should open a console
|
||||
binary-mode file-like object to be used for reading and writing.
|
||||
This initializes the C{opener} attribute.
|
||||
@type opener: callable taking no arguments and returning a read/write
|
||||
file-like object
|
||||
"""
|
||||
self.opener = opener
|
||||
|
||||
def prompt(self, text: bytes) -> Deferred[bool]:
|
||||
"""
|
||||
Write the given text as a prompt to the console output, then read a
|
||||
result from the console input.
|
||||
|
||||
@param text: Something to present to a user to solicit a yes or no
|
||||
response.
|
||||
@type text: L{bytes}
|
||||
|
||||
@return: a L{Deferred} which fires with L{True} when the user answers
|
||||
'yes' and L{False} when the user answers 'no'. It may errback if
|
||||
there were any I/O errors.
|
||||
"""
|
||||
d = defer.succeed(None)
|
||||
|
||||
def body(ignored):
|
||||
with closing(self.opener()) as f:
|
||||
f.write(text)
|
||||
while True:
|
||||
answer = f.readline().strip().lower()
|
||||
if answer == b"yes":
|
||||
return True
|
||||
elif answer in {b"no", b""}:
|
||||
return False
|
||||
else:
|
||||
f.write(b"Please type 'yes' or 'no': ")
|
||||
|
||||
return d.addCallback(body)
|
||||
|
||||
def warn(self, text: bytes) -> None:
|
||||
"""
|
||||
Notify the user (non-interactively) of the provided text, by writing it
|
||||
to the console.
|
||||
|
||||
@param text: Some information the user is to be made aware of.
|
||||
"""
|
||||
try:
|
||||
with closing(self.opener()) as f:
|
||||
f.write(text)
|
||||
except Exception:
|
||||
log.failure("Failed to write to console")
|
||||
@@ -0,0 +1,109 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
import sys
|
||||
from typing import List, Optional, Union
|
||||
|
||||
#
|
||||
from twisted.conch.ssh.transport import SSHCiphers, SSHClientTransport
|
||||
from twisted.python import usage
|
||||
|
||||
|
||||
class ConchOptions(usage.Options):
|
||||
optParameters: List[List[Optional[Union[str, int]]]] = [
|
||||
["user", "l", None, "Log in using this user name."],
|
||||
["identity", "i", None],
|
||||
["ciphers", "c", None],
|
||||
["macs", "m", None],
|
||||
["port", "p", None, "Connect to this port. Server must be on the same port."],
|
||||
["option", "o", None, "Ignored OpenSSH options"],
|
||||
["host-key-algorithms", "", None],
|
||||
["known-hosts", "", None, "File to check for host keys"],
|
||||
["user-authentications", "", None, "Types of user authentications to use."],
|
||||
["logfile", "", None, "File to log to, or - for stdout"],
|
||||
]
|
||||
|
||||
optFlags = [
|
||||
["version", "V", "Display version number only."],
|
||||
["compress", "C", "Enable compression."],
|
||||
["log", "v", "Enable logging (defaults to stderr)"],
|
||||
["nox11", "x", "Disable X11 connection forwarding (default)"],
|
||||
["agent", "A", "Enable authentication agent forwarding"],
|
||||
["noagent", "a", "Disable authentication agent forwarding (default)"],
|
||||
["reconnect", "r", "Reconnect to the server if the connection is lost."],
|
||||
]
|
||||
|
||||
compData = usage.Completions(
|
||||
mutuallyExclusive=[("agent", "noagent")],
|
||||
optActions={
|
||||
"user": usage.CompleteUsernames(),
|
||||
"ciphers": usage.CompleteMultiList(
|
||||
[v.decode() for v in SSHCiphers.cipherMap.keys()],
|
||||
descr="ciphers to choose from",
|
||||
),
|
||||
"macs": usage.CompleteMultiList(
|
||||
[v.decode() for v in SSHCiphers.macMap.keys()],
|
||||
descr="macs to choose from",
|
||||
),
|
||||
"host-key-algorithms": usage.CompleteMultiList(
|
||||
[v.decode() for v in SSHClientTransport.supportedPublicKeys],
|
||||
descr="host key algorithms to choose from",
|
||||
),
|
||||
# "user-authentications": usage.CompleteMultiList(?
|
||||
# descr='user authentication types' ),
|
||||
},
|
||||
extraActions=[
|
||||
usage.CompleteUserAtHost(),
|
||||
usage.Completer(descr="command"),
|
||||
usage.Completer(descr="argument", repeat=True),
|
||||
],
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kw):
|
||||
usage.Options.__init__(self, *args, **kw)
|
||||
self.identitys = []
|
||||
self.conns = None
|
||||
|
||||
def opt_identity(self, i):
|
||||
"""Identity for public-key authentication"""
|
||||
self.identitys.append(i)
|
||||
|
||||
def opt_ciphers(self, ciphers):
|
||||
"Select encryption algorithms"
|
||||
ciphers = ciphers.split(",")
|
||||
for cipher in ciphers:
|
||||
if cipher not in SSHCiphers.cipherMap:
|
||||
sys.exit("Unknown cipher type '%s'" % cipher)
|
||||
self["ciphers"] = ciphers
|
||||
|
||||
def opt_macs(self, macs):
|
||||
"Specify MAC algorithms"
|
||||
if isinstance(macs, str):
|
||||
macs = macs.encode("utf-8")
|
||||
macs = macs.split(b",")
|
||||
for mac in macs:
|
||||
if mac not in SSHCiphers.macMap:
|
||||
sys.exit("Unknown mac type '%r'" % mac)
|
||||
self["macs"] = macs
|
||||
|
||||
def opt_host_key_algorithms(self, hkas):
|
||||
"Select host key algorithms"
|
||||
if isinstance(hkas, str):
|
||||
hkas = hkas.encode("utf-8")
|
||||
hkas = hkas.split(b",")
|
||||
for hka in hkas:
|
||||
if hka not in SSHClientTransport.supportedPublicKeys:
|
||||
sys.exit("Unknown host key type '%r'" % hka)
|
||||
self["host-key-algorithms"] = hkas
|
||||
|
||||
def opt_user_authentications(self, uas):
|
||||
"Choose how to authenticate to the remote server"
|
||||
if isinstance(uas, str):
|
||||
uas = uas.encode("utf-8")
|
||||
self["user-authentications"] = uas.split(b",")
|
||||
|
||||
|
||||
# def opt_compress(self):
|
||||
# "Enable compression"
|
||||
# self.enableCompression = 1
|
||||
# SSHClientTransport.supportedCompressions[0:1] = ['zlib']
|
||||
845
backend/lib/python3.12/site-packages/twisted/conch/endpoints.py
Normal file
845
backend/lib/python3.12/site-packages/twisted/conch/endpoints.py
Normal file
@@ -0,0 +1,845 @@
|
||||
# -*- test-case-name: twisted.conch.test.test_endpoints -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Endpoint implementations of various SSH interactions.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
__all__ = [
|
||||
"AuthenticationFailed",
|
||||
"SSHCommandAddress",
|
||||
"SSHCommandClientEndpoint",
|
||||
]
|
||||
|
||||
import signal
|
||||
from io import BytesIO
|
||||
from os.path import expanduser
|
||||
from struct import unpack
|
||||
from typing import IO, Any
|
||||
|
||||
from zope.interface import Interface, implementer
|
||||
|
||||
from twisted.conch.client.agent import SSHAgentClient
|
||||
from twisted.conch.client.default import _KNOWN_HOSTS
|
||||
from twisted.conch.client.knownhosts import ConsoleUI, KnownHostsFile
|
||||
from twisted.conch.ssh.channel import SSHChannel
|
||||
from twisted.conch.ssh.common import NS, getNS
|
||||
from twisted.conch.ssh.connection import SSHConnection
|
||||
from twisted.conch.ssh.keys import Key
|
||||
from twisted.conch.ssh.transport import SSHClientTransport
|
||||
from twisted.conch.ssh.userauth import SSHUserAuthClient
|
||||
from twisted.internet.defer import CancelledError, Deferred, succeed
|
||||
from twisted.internet.endpoints import TCP4ClientEndpoint, connectProtocol
|
||||
from twisted.internet.error import ConnectionDone, ProcessTerminated
|
||||
from twisted.internet.interfaces import IStreamClientEndpoint
|
||||
from twisted.internet.protocol import Factory
|
||||
from twisted.logger import Logger
|
||||
from twisted.python.compat import nativeString, networkString
|
||||
from twisted.python.failure import Failure
|
||||
from twisted.python.filepath import FilePath
|
||||
|
||||
|
||||
class AuthenticationFailed(Exception):
|
||||
"""
|
||||
An SSH session could not be established because authentication was not
|
||||
successful.
|
||||
"""
|
||||
|
||||
|
||||
# This should be public. See #6541.
|
||||
class _ISSHConnectionCreator(Interface):
|
||||
"""
|
||||
An L{_ISSHConnectionCreator} knows how to create SSH connections somehow.
|
||||
"""
|
||||
|
||||
def secureConnection():
|
||||
"""
|
||||
Return a new, connected, secured, but not yet authenticated instance of
|
||||
L{twisted.conch.ssh.transport.SSHServerTransport} or
|
||||
L{twisted.conch.ssh.transport.SSHClientTransport}.
|
||||
"""
|
||||
|
||||
def cleanupConnection(connection, immediate):
|
||||
"""
|
||||
Perform cleanup necessary for a connection object previously returned
|
||||
from this creator's C{secureConnection} method.
|
||||
|
||||
@param connection: An L{twisted.conch.ssh.transport.SSHServerTransport}
|
||||
or L{twisted.conch.ssh.transport.SSHClientTransport} returned by a
|
||||
previous call to C{secureConnection}. It is no longer needed by
|
||||
the caller of that method and may be closed or otherwise cleaned up
|
||||
as necessary.
|
||||
|
||||
@param immediate: If C{True} don't wait for any network communication,
|
||||
just close the connection immediately and as aggressively as
|
||||
necessary.
|
||||
"""
|
||||
|
||||
|
||||
class SSHCommandAddress:
|
||||
"""
|
||||
An L{SSHCommandAddress} instance represents the address of an SSH server, a
|
||||
username which was used to authenticate with that server, and a command
|
||||
which was run there.
|
||||
|
||||
@ivar server: See L{__init__}
|
||||
@ivar username: See L{__init__}
|
||||
@ivar command: See L{__init__}
|
||||
"""
|
||||
|
||||
def __init__(self, server, username, command):
|
||||
"""
|
||||
@param server: The address of the SSH server on which the command is
|
||||
running.
|
||||
@type server: L{IAddress} provider
|
||||
|
||||
@param username: An authentication username which was used to
|
||||
authenticate against the server at the given address.
|
||||
@type username: L{bytes}
|
||||
|
||||
@param command: A command which was run in a session channel on the
|
||||
server at the given address.
|
||||
@type command: L{bytes}
|
||||
"""
|
||||
self.server = server
|
||||
self.username = username
|
||||
self.command = command
|
||||
|
||||
|
||||
class _CommandChannel(SSHChannel):
|
||||
"""
|
||||
A L{_CommandChannel} executes a command in a session channel and connects
|
||||
its input and output to an L{IProtocol} provider.
|
||||
|
||||
@ivar _creator: See L{__init__}
|
||||
@ivar _command: See L{__init__}
|
||||
@ivar _protocolFactory: See L{__init__}
|
||||
@ivar _commandConnected: See L{__init__}
|
||||
@ivar _protocol: An L{IProtocol} provider created using C{_protocolFactory}
|
||||
which is hooked up to the running command's input and output streams.
|
||||
"""
|
||||
|
||||
name = b"session"
|
||||
_log = Logger()
|
||||
|
||||
def __init__(self, creator, command, protocolFactory, commandConnected):
|
||||
"""
|
||||
@param creator: The L{_ISSHConnectionCreator} provider which was used
|
||||
to get the connection which this channel exists on.
|
||||
@type creator: L{_ISSHConnectionCreator} provider
|
||||
|
||||
@param command: The command to be executed.
|
||||
@type command: L{bytes}
|
||||
|
||||
@param protocolFactory: A client factory to use to build a L{IProtocol}
|
||||
provider to use to associate with the running command.
|
||||
|
||||
@param commandConnected: A L{Deferred} to use to signal that execution
|
||||
of the command has failed or that it has succeeded and the command
|
||||
is now running.
|
||||
@type commandConnected: L{Deferred}
|
||||
"""
|
||||
SSHChannel.__init__(self)
|
||||
self._creator = creator
|
||||
self._command = command
|
||||
self._protocolFactory = protocolFactory
|
||||
self._commandConnected = commandConnected
|
||||
self._reason = None
|
||||
|
||||
def openFailed(self, reason):
|
||||
"""
|
||||
When the request to open a new channel to run this command in fails,
|
||||
fire the C{commandConnected} deferred with a failure indicating that.
|
||||
"""
|
||||
self._commandConnected.errback(reason)
|
||||
|
||||
def channelOpen(self, ignored):
|
||||
"""
|
||||
When the request to open a new channel to run this command in succeeds,
|
||||
issue an C{"exec"} request to run the command.
|
||||
"""
|
||||
command = self.conn.sendRequest(
|
||||
self, b"exec", NS(self._command), wantReply=True
|
||||
)
|
||||
command.addCallbacks(self._execSuccess, self._execFailure)
|
||||
|
||||
def _execFailure(self, reason):
|
||||
"""
|
||||
When the request to execute the command in this channel fails, fire the
|
||||
C{commandConnected} deferred with a failure indicating this.
|
||||
|
||||
@param reason: The cause of the command execution failure.
|
||||
@type reason: L{Failure}
|
||||
"""
|
||||
self._commandConnected.errback(reason)
|
||||
|
||||
def _execSuccess(self, ignored):
|
||||
"""
|
||||
When the request to execute the command in this channel succeeds, use
|
||||
C{protocolFactory} to build a protocol to handle the command's input
|
||||
and output and connect the protocol to a transport representing those
|
||||
streams.
|
||||
|
||||
Also fire C{commandConnected} with the created protocol after it is
|
||||
connected to its transport.
|
||||
|
||||
@param ignored: The (ignored) result of the execute request
|
||||
"""
|
||||
self._protocol = self._protocolFactory.buildProtocol(
|
||||
SSHCommandAddress(
|
||||
self.conn.transport.transport.getPeer(),
|
||||
self.conn.transport.creator.username,
|
||||
self.conn.transport.creator.command,
|
||||
)
|
||||
)
|
||||
self._protocol.makeConnection(self)
|
||||
self._commandConnected.callback(self._protocol)
|
||||
|
||||
def dataReceived(self, data):
|
||||
"""
|
||||
When the command's stdout data arrives over the channel, deliver it to
|
||||
the protocol instance.
|
||||
|
||||
@param data: The bytes from the command's stdout.
|
||||
@type data: L{bytes}
|
||||
"""
|
||||
self._protocol.dataReceived(data)
|
||||
|
||||
def request_exit_status(self, data):
|
||||
"""
|
||||
When the server sends the command's exit status, record it for later
|
||||
delivery to the protocol.
|
||||
|
||||
@param data: The network-order four byte representation of the exit
|
||||
status of the command.
|
||||
@type data: L{bytes}
|
||||
"""
|
||||
(status,) = unpack(">L", data)
|
||||
if status != 0:
|
||||
self._reason = ProcessTerminated(status, None, None)
|
||||
|
||||
def request_exit_signal(self, data):
|
||||
"""
|
||||
When the server sends the command's exit status, record it for later
|
||||
delivery to the protocol.
|
||||
|
||||
@param data: The network-order four byte representation of the exit
|
||||
signal of the command.
|
||||
@type data: L{bytes}
|
||||
"""
|
||||
shortSignalName, data = getNS(data)
|
||||
coreDumped, data = bool(ord(data[0:1])), data[1:]
|
||||
errorMessage, data = getNS(data)
|
||||
languageTag, data = getNS(data)
|
||||
signalName = f"SIG{nativeString(shortSignalName)}"
|
||||
signalID = getattr(signal, signalName, -1)
|
||||
self._log.info(
|
||||
"Process exited with signal {shortSignalName!r};"
|
||||
" core dumped: {coreDumped};"
|
||||
" error message: {errorMessage};"
|
||||
" language: {languageTag!r}",
|
||||
shortSignalName=shortSignalName,
|
||||
coreDumped=coreDumped,
|
||||
errorMessage=errorMessage.decode("utf-8"),
|
||||
languageTag=languageTag,
|
||||
)
|
||||
self._reason = ProcessTerminated(None, signalID, None)
|
||||
|
||||
def closed(self):
|
||||
"""
|
||||
When the channel closes, deliver disconnection notification to the
|
||||
protocol.
|
||||
"""
|
||||
self._creator.cleanupConnection(self.conn, False)
|
||||
if self._reason is None:
|
||||
reason = ConnectionDone("ssh channel closed")
|
||||
else:
|
||||
reason = self._reason
|
||||
self._protocol.connectionLost(Failure(reason))
|
||||
|
||||
|
||||
class _ConnectionReady(SSHConnection):
|
||||
"""
|
||||
L{_ConnectionReady} is an L{SSHConnection} (an SSH service) which only
|
||||
propagates the I{serviceStarted} event to a L{Deferred} to be handled
|
||||
elsewhere.
|
||||
"""
|
||||
|
||||
def __init__(self, ready):
|
||||
"""
|
||||
@param ready: A L{Deferred} which should be fired when
|
||||
I{serviceStarted} happens.
|
||||
"""
|
||||
SSHConnection.__init__(self)
|
||||
self._ready = ready
|
||||
|
||||
def serviceStarted(self):
|
||||
"""
|
||||
When the SSH I{connection} I{service} this object represents is ready
|
||||
to be used, fire the C{connectionReady} L{Deferred} to publish that
|
||||
event to some other interested party.
|
||||
|
||||
"""
|
||||
self._ready.callback(self)
|
||||
del self._ready
|
||||
|
||||
|
||||
class _UserAuth(SSHUserAuthClient):
|
||||
"""
|
||||
L{_UserAuth} implements the client part of SSH user authentication in the
|
||||
convenient way a user might expect if they are familiar with the
|
||||
interactive I{ssh} command line client.
|
||||
|
||||
L{_UserAuth} supports key-based authentication, password-based
|
||||
authentication, and delegating authentication to an agent.
|
||||
"""
|
||||
|
||||
password = None
|
||||
keys = None
|
||||
agent = None
|
||||
|
||||
def getPublicKey(self):
|
||||
"""
|
||||
Retrieve the next public key object to offer to the server, possibly
|
||||
delegating to an authentication agent if there is one.
|
||||
|
||||
@return: The public part of a key pair that could be used to
|
||||
authenticate with the server, or L{None} if there are no more
|
||||
public keys to try.
|
||||
@rtype: L{twisted.conch.ssh.keys.Key} or L{None}
|
||||
"""
|
||||
if self.agent is not None:
|
||||
return self.agent.getPublicKey()
|
||||
|
||||
if self.keys:
|
||||
self.key = self.keys.pop(0)
|
||||
else:
|
||||
self.key = None
|
||||
return self.key.public()
|
||||
|
||||
def signData(self, publicKey, signData):
|
||||
"""
|
||||
Extend the base signing behavior by using an SSH agent to sign the
|
||||
data, if one is available.
|
||||
|
||||
@type publicKey: L{Key}
|
||||
@type signData: L{str}
|
||||
"""
|
||||
if self.agent is not None:
|
||||
return self.agent.signData(publicKey.blob(), signData)
|
||||
else:
|
||||
return SSHUserAuthClient.signData(self, publicKey, signData)
|
||||
|
||||
def getPrivateKey(self):
|
||||
"""
|
||||
Get the private part of a key pair to use for authentication. The key
|
||||
corresponds to the public part most recently returned from
|
||||
C{getPublicKey}.
|
||||
|
||||
@return: A L{Deferred} which fires with the private key.
|
||||
@rtype: L{Deferred}
|
||||
"""
|
||||
return succeed(self.key)
|
||||
|
||||
def getPassword(self):
|
||||
"""
|
||||
Get the password to use for authentication.
|
||||
|
||||
@return: A L{Deferred} which fires with the password, or L{None} if the
|
||||
password was not specified.
|
||||
"""
|
||||
if self.password is None:
|
||||
return
|
||||
return succeed(self.password)
|
||||
|
||||
def ssh_USERAUTH_SUCCESS(self, packet):
|
||||
"""
|
||||
Handle user authentication success in the normal way, but also make a
|
||||
note of the state change on the L{_CommandTransport}.
|
||||
"""
|
||||
self.transport._state = b"CHANNELLING"
|
||||
return SSHUserAuthClient.ssh_USERAUTH_SUCCESS(self, packet)
|
||||
|
||||
def connectToAgent(self, endpoint):
|
||||
"""
|
||||
Set up a connection to the authentication agent and trigger its
|
||||
initialization.
|
||||
|
||||
@param endpoint: An endpoint which can be used to connect to the
|
||||
authentication agent.
|
||||
@type endpoint: L{IStreamClientEndpoint} provider
|
||||
|
||||
@return: A L{Deferred} which fires when the agent connection is ready
|
||||
for use.
|
||||
"""
|
||||
factory = Factory()
|
||||
factory.protocol = SSHAgentClient
|
||||
d = endpoint.connect(factory)
|
||||
|
||||
def connected(agent):
|
||||
self.agent = agent
|
||||
return agent.getPublicKeys()
|
||||
|
||||
d.addCallback(connected)
|
||||
return d
|
||||
|
||||
def loseAgentConnection(self):
|
||||
"""
|
||||
Disconnect the agent.
|
||||
"""
|
||||
if self.agent is None:
|
||||
return
|
||||
self.agent.transport.loseConnection()
|
||||
|
||||
|
||||
class _CommandTransport(SSHClientTransport):
|
||||
"""
|
||||
L{_CommandTransport} is an SSH client I{transport} which includes a host
|
||||
key verification step before it will proceed to secure the connection.
|
||||
|
||||
L{_CommandTransport} also knows how to set up a connection to an
|
||||
authentication agent if it is told where it can connect to one.
|
||||
|
||||
@ivar _userauth: The L{_UserAuth} instance which is in charge of the
|
||||
overall authentication process or L{None} if the SSH connection has not
|
||||
reach yet the C{user-auth} service.
|
||||
@type _userauth: L{_UserAuth}
|
||||
"""
|
||||
|
||||
# STARTING -> SECURING -> AUTHENTICATING -> CHANNELLING -> RUNNING
|
||||
_state = b"STARTING"
|
||||
|
||||
_hostKeyFailure = None
|
||||
|
||||
_userauth = None
|
||||
|
||||
def __init__(self, creator):
|
||||
"""
|
||||
@param creator: The L{_NewConnectionHelper} that created this
|
||||
connection.
|
||||
|
||||
@type creator: L{_NewConnectionHelper}.
|
||||
"""
|
||||
self.connectionReady = Deferred(lambda d: self.transport.abortConnection())
|
||||
# Clear the reference to that deferred to help the garbage collector
|
||||
# and to signal to other parts of this implementation (in particular
|
||||
# connectionLost) that it has already been fired and does not need to
|
||||
# be fired again.
|
||||
|
||||
def readyFired(result):
|
||||
self.connectionReady = None
|
||||
return result
|
||||
|
||||
self.connectionReady.addBoth(readyFired)
|
||||
self.creator = creator
|
||||
|
||||
def verifyHostKey(self, hostKey, fingerprint):
|
||||
"""
|
||||
Ask the L{KnownHostsFile} provider available on the factory which
|
||||
created this protocol this protocol to verify the given host key.
|
||||
|
||||
@return: A L{Deferred} which fires with the result of
|
||||
L{KnownHostsFile.verifyHostKey}.
|
||||
"""
|
||||
hostname = self.creator.hostname
|
||||
ip = networkString(self.transport.getPeer().host)
|
||||
|
||||
self._state = b"SECURING"
|
||||
d = self.creator.knownHosts.verifyHostKey(
|
||||
self.creator.ui, hostname, ip, Key.fromString(hostKey)
|
||||
)
|
||||
d.addErrback(self._saveHostKeyFailure)
|
||||
return d
|
||||
|
||||
def _saveHostKeyFailure(self, reason):
|
||||
"""
|
||||
When host key verification fails, record the reason for the failure in
|
||||
order to fire a L{Deferred} with it later.
|
||||
|
||||
@param reason: The cause of the host key verification failure.
|
||||
@type reason: L{Failure}
|
||||
|
||||
@return: C{reason}
|
||||
@rtype: L{Failure}
|
||||
"""
|
||||
self._hostKeyFailure = reason
|
||||
return reason
|
||||
|
||||
def connectionSecure(self):
|
||||
"""
|
||||
When the connection is secure, start the authentication process.
|
||||
"""
|
||||
self._state = b"AUTHENTICATING"
|
||||
|
||||
command = _ConnectionReady(self.connectionReady)
|
||||
|
||||
self._userauth = _UserAuth(self.creator.username, command)
|
||||
self._userauth.password = self.creator.password
|
||||
if self.creator.keys:
|
||||
self._userauth.keys = list(self.creator.keys)
|
||||
|
||||
if self.creator.agentEndpoint is not None:
|
||||
d = self._userauth.connectToAgent(self.creator.agentEndpoint)
|
||||
else:
|
||||
d = succeed(None)
|
||||
|
||||
def maybeGotAgent(ignored):
|
||||
self.requestService(self._userauth)
|
||||
|
||||
d.addBoth(maybeGotAgent)
|
||||
|
||||
def connectionLost(self, reason):
|
||||
"""
|
||||
When the underlying connection to the SSH server is lost, if there were
|
||||
any connection setup errors, propagate them. Also, clean up the
|
||||
connection to the ssh agent if one was created.
|
||||
"""
|
||||
if self._userauth:
|
||||
self._userauth.loseAgentConnection()
|
||||
|
||||
if self._state == b"RUNNING" or self.connectionReady is None:
|
||||
return
|
||||
if self._state == b"SECURING" and self._hostKeyFailure is not None:
|
||||
reason = self._hostKeyFailure
|
||||
elif self._state == b"AUTHENTICATING":
|
||||
reason = Failure(
|
||||
AuthenticationFailed("Connection lost while authenticating")
|
||||
)
|
||||
self.connectionReady.errback(reason)
|
||||
|
||||
|
||||
@implementer(IStreamClientEndpoint)
|
||||
class SSHCommandClientEndpoint:
|
||||
"""
|
||||
L{SSHCommandClientEndpoint} exposes the command-executing functionality of
|
||||
SSH servers.
|
||||
|
||||
L{SSHCommandClientEndpoint} can set up a new SSH connection, authenticate
|
||||
it in any one of a number of different ways (keys, passwords, agents),
|
||||
launch a command over that connection and then associate its input and
|
||||
output with a protocol.
|
||||
|
||||
It can also re-use an existing, already-authenticated SSH connection
|
||||
(perhaps one which already has some SSH channels being used for other
|
||||
purposes). In this case it creates a new SSH channel to use to execute the
|
||||
command. Notably this means it supports multiplexing several different
|
||||
command invocations over a single SSH connection.
|
||||
"""
|
||||
|
||||
def __init__(self, creator, command):
|
||||
"""
|
||||
@param creator: An L{_ISSHConnectionCreator} provider which will be
|
||||
used to set up the SSH connection which will be used to run a
|
||||
command.
|
||||
@type creator: L{_ISSHConnectionCreator} provider
|
||||
|
||||
@param command: The command line to execute on the SSH server. This
|
||||
byte string is interpreted by a shell on the SSH server, so it may
|
||||
have a value like C{"ls /"}. Take care when trying to run a
|
||||
command like C{"/Volumes/My Stuff/a-program"} - spaces (and other
|
||||
special bytes) may require escaping.
|
||||
@type command: L{bytes}
|
||||
|
||||
"""
|
||||
self._creator = creator
|
||||
self._command = command
|
||||
|
||||
@classmethod
|
||||
def newConnection(
|
||||
cls,
|
||||
reactor,
|
||||
command,
|
||||
username,
|
||||
hostname,
|
||||
port=None,
|
||||
keys=None,
|
||||
password=None,
|
||||
agentEndpoint=None,
|
||||
knownHosts=None,
|
||||
ui=None,
|
||||
):
|
||||
"""
|
||||
Create and return a new endpoint which will try to create a new
|
||||
connection to an SSH server and run a command over it. It will also
|
||||
close the connection if there are problems leading up to the command
|
||||
being executed, after the command finishes, or if the connection
|
||||
L{Deferred} is cancelled.
|
||||
|
||||
@param reactor: The reactor to use to establish the connection.
|
||||
@type reactor: L{IReactorTCP} provider
|
||||
|
||||
@param command: See L{__init__}'s C{command} argument.
|
||||
|
||||
@param username: The username with which to authenticate to the SSH
|
||||
server.
|
||||
@type username: L{bytes}
|
||||
|
||||
@param hostname: The hostname of the SSH server.
|
||||
@type hostname: L{bytes}
|
||||
|
||||
@param port: The port number of the SSH server. By default, the
|
||||
standard SSH port number is used.
|
||||
@type port: L{int}
|
||||
|
||||
@param keys: Private keys with which to authenticate to the SSH server,
|
||||
if key authentication is to be attempted (otherwise L{None}).
|
||||
@type keys: L{list} of L{Key}
|
||||
|
||||
@param password: The password with which to authenticate to the SSH
|
||||
server, if password authentication is to be attempted (otherwise
|
||||
L{None}).
|
||||
@type password: L{bytes} or L{None}
|
||||
|
||||
@param agentEndpoint: An L{IStreamClientEndpoint} provider which may be
|
||||
used to connect to an SSH agent, if one is to be used to help with
|
||||
authentication.
|
||||
@type agentEndpoint: L{IStreamClientEndpoint} provider
|
||||
|
||||
@param knownHosts: The currently known host keys, used to check the
|
||||
host key presented by the server we actually connect to.
|
||||
@type knownHosts: L{KnownHostsFile}
|
||||
|
||||
@param ui: An object for interacting with users to make decisions about
|
||||
whether to accept the server host keys. If L{None}, a L{ConsoleUI}
|
||||
connected to /dev/tty will be used; if /dev/tty is unavailable, an
|
||||
object which answers C{b"no"} to all prompts will be used.
|
||||
@type ui: L{None} or L{ConsoleUI}
|
||||
|
||||
@return: A new instance of C{cls} (probably
|
||||
L{SSHCommandClientEndpoint}).
|
||||
"""
|
||||
helper = _NewConnectionHelper(
|
||||
reactor,
|
||||
hostname,
|
||||
port,
|
||||
command,
|
||||
username,
|
||||
keys,
|
||||
password,
|
||||
agentEndpoint,
|
||||
knownHosts,
|
||||
ui,
|
||||
)
|
||||
return cls(helper, command)
|
||||
|
||||
@classmethod
|
||||
def existingConnection(cls, connection, command):
|
||||
"""
|
||||
Create and return a new endpoint which will try to open a new channel
|
||||
on an existing SSH connection and run a command over it. It will
|
||||
B{not} close the connection if there is a problem executing the command
|
||||
or after the command finishes.
|
||||
|
||||
@param connection: An existing connection to an SSH server.
|
||||
@type connection: L{SSHConnection}
|
||||
|
||||
@param command: See L{SSHCommandClientEndpoint.newConnection}'s
|
||||
C{command} parameter.
|
||||
@type command: L{bytes}
|
||||
|
||||
@return: A new instance of C{cls} (probably
|
||||
L{SSHCommandClientEndpoint}).
|
||||
"""
|
||||
helper = _ExistingConnectionHelper(connection)
|
||||
return cls(helper, command)
|
||||
|
||||
def connect(self, protocolFactory):
|
||||
"""
|
||||
Set up an SSH connection, use a channel from that connection to launch
|
||||
a command, and hook the stdin and stdout of that command up as a
|
||||
transport for a protocol created by the given factory.
|
||||
|
||||
@param protocolFactory: A L{Factory} to use to create the protocol
|
||||
which will be connected to the stdin and stdout of the command on
|
||||
the SSH server.
|
||||
|
||||
@return: A L{Deferred} which will fire with an error if the connection
|
||||
cannot be set up for any reason or with the protocol instance
|
||||
created by C{protocolFactory} once it has been connected to the
|
||||
command.
|
||||
"""
|
||||
d = self._creator.secureConnection()
|
||||
d.addCallback(self._executeCommand, protocolFactory)
|
||||
return d
|
||||
|
||||
def _executeCommand(self, connection, protocolFactory):
|
||||
"""
|
||||
Given a secured SSH connection, try to execute a command in a new
|
||||
channel created on it and associate the result with a protocol from the
|
||||
given factory.
|
||||
|
||||
@param connection: See L{SSHCommandClientEndpoint.existingConnection}'s
|
||||
C{connection} parameter.
|
||||
|
||||
@param protocolFactory: See L{SSHCommandClientEndpoint.connect}'s
|
||||
C{protocolFactory} parameter.
|
||||
|
||||
@return: See L{SSHCommandClientEndpoint.connect}'s return value.
|
||||
"""
|
||||
commandConnected = Deferred()
|
||||
|
||||
def disconnectOnFailure(passthrough):
|
||||
# Close the connection immediately in case of cancellation, since
|
||||
# that implies user wants it gone immediately (e.g. a timeout):
|
||||
immediate = passthrough.check(CancelledError)
|
||||
self._creator.cleanupConnection(connection, immediate)
|
||||
return passthrough
|
||||
|
||||
commandConnected.addErrback(disconnectOnFailure)
|
||||
|
||||
channel = _CommandChannel(
|
||||
self._creator, self._command, protocolFactory, commandConnected
|
||||
)
|
||||
connection.openChannel(channel)
|
||||
return commandConnected
|
||||
|
||||
|
||||
@implementer(_ISSHConnectionCreator)
|
||||
class _NewConnectionHelper:
|
||||
"""
|
||||
L{_NewConnectionHelper} implements L{_ISSHConnectionCreator} by
|
||||
establishing a brand new SSH connection, securing it, and authenticating.
|
||||
"""
|
||||
|
||||
_KNOWN_HOSTS = _KNOWN_HOSTS
|
||||
port = 22
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
reactor: Any,
|
||||
hostname: str,
|
||||
port: int,
|
||||
command: str,
|
||||
username: str,
|
||||
keys: str,
|
||||
password: str,
|
||||
agentEndpoint: str,
|
||||
knownHosts: str | None,
|
||||
ui: ConsoleUI | None,
|
||||
tty: FilePath[bytes] | FilePath[str] = FilePath(b"/dev/tty"),
|
||||
):
|
||||
"""
|
||||
@param tty: The path of the tty device to use in case C{ui} is L{None}.
|
||||
@type tty: L{FilePath}
|
||||
|
||||
@see: L{SSHCommandClientEndpoint.newConnection}
|
||||
"""
|
||||
self.reactor = reactor
|
||||
self.hostname = hostname
|
||||
if port is not None:
|
||||
self.port = port
|
||||
self.command = command
|
||||
self.username = username
|
||||
self.keys = keys
|
||||
self.password = password
|
||||
self.agentEndpoint = agentEndpoint
|
||||
if knownHosts is None:
|
||||
knownHosts = self._knownHosts()
|
||||
self.knownHosts = knownHosts
|
||||
|
||||
if ui is None:
|
||||
ui = ConsoleUI(self._opener)
|
||||
self.ui = ui
|
||||
self.tty: FilePath[bytes] | FilePath[str] = tty
|
||||
|
||||
def _opener(self) -> IO[bytes]:
|
||||
"""
|
||||
Open the tty if possible, otherwise give back a file-like object from
|
||||
which C{b"no"} can be read.
|
||||
|
||||
For use as the opener argument to L{ConsoleUI}.
|
||||
"""
|
||||
try:
|
||||
return self.tty.open("r+")
|
||||
except BaseException:
|
||||
# Give back a file-like object from which can be read a byte string
|
||||
# that KnownHostsFile recognizes as rejecting some option (b"no").
|
||||
return BytesIO(b"no")
|
||||
|
||||
@classmethod
|
||||
def _knownHosts(cls):
|
||||
"""
|
||||
|
||||
@return: A L{KnownHostsFile} instance pointed at the user's personal
|
||||
I{known hosts} file.
|
||||
@rtype: L{KnownHostsFile}
|
||||
"""
|
||||
return KnownHostsFile.fromPath(FilePath(expanduser(cls._KNOWN_HOSTS)))
|
||||
|
||||
def secureConnection(self):
|
||||
"""
|
||||
Create and return a new SSH connection which has been secured and on
|
||||
which authentication has already happened.
|
||||
|
||||
@return: A L{Deferred} which fires with the ready-to-use connection or
|
||||
with a failure if something prevents the connection from being
|
||||
setup, secured, or authenticated.
|
||||
"""
|
||||
protocol = _CommandTransport(self)
|
||||
ready = protocol.connectionReady
|
||||
|
||||
sshClient = TCP4ClientEndpoint(
|
||||
self.reactor, nativeString(self.hostname), self.port
|
||||
)
|
||||
|
||||
d = connectProtocol(sshClient, protocol)
|
||||
d.addCallback(lambda ignored: ready)
|
||||
return d
|
||||
|
||||
def cleanupConnection(self, connection, immediate):
|
||||
"""
|
||||
Clean up the connection by closing it. The command running on the
|
||||
endpoint has ended so the connection is no longer needed.
|
||||
|
||||
@param connection: The L{SSHConnection} to close.
|
||||
@type connection: L{SSHConnection}
|
||||
|
||||
@param immediate: Whether to close connection immediately.
|
||||
@type immediate: L{bool}.
|
||||
"""
|
||||
if immediate:
|
||||
# We're assuming the underlying connection is an ITCPTransport,
|
||||
# which is what the current implementation is restricted to:
|
||||
connection.transport.transport.abortConnection()
|
||||
else:
|
||||
connection.transport.loseConnection()
|
||||
|
||||
|
||||
@implementer(_ISSHConnectionCreator)
|
||||
class _ExistingConnectionHelper:
|
||||
"""
|
||||
L{_ExistingConnectionHelper} implements L{_ISSHConnectionCreator} by
|
||||
handing out an existing SSH connection which is supplied to its
|
||||
initializer.
|
||||
"""
|
||||
|
||||
def __init__(self, connection):
|
||||
"""
|
||||
@param connection: See L{SSHCommandClientEndpoint.existingConnection}'s
|
||||
C{connection} parameter.
|
||||
"""
|
||||
self.connection = connection
|
||||
|
||||
def secureConnection(self):
|
||||
"""
|
||||
|
||||
@return: A L{Deferred} that fires synchronously with the
|
||||
already-established connection object.
|
||||
"""
|
||||
return succeed(self.connection)
|
||||
|
||||
def cleanupConnection(self, connection, immediate):
|
||||
"""
|
||||
Do not do any cleanup on the connection. Leave that responsibility to
|
||||
whatever code created it in the first place.
|
||||
|
||||
@param connection: The L{SSHConnection} which will not be modified in
|
||||
any way.
|
||||
@type connection: L{SSHConnection}
|
||||
|
||||
@param immediate: An argument which will be ignored.
|
||||
@type immediate: L{bool}.
|
||||
"""
|
||||
96
backend/lib/python3.12/site-packages/twisted/conch/error.py
Normal file
96
backend/lib/python3.12/site-packages/twisted/conch/error.py
Normal file
@@ -0,0 +1,96 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
An error to represent bad things happening in Conch.
|
||||
|
||||
Maintainer: Paul Swartz
|
||||
"""
|
||||
|
||||
|
||||
from twisted.cred.error import UnauthorizedLogin
|
||||
|
||||
|
||||
class ConchError(Exception):
|
||||
def __init__(self, value, data=None):
|
||||
Exception.__init__(self, value, data)
|
||||
self.value = value
|
||||
self.data = data
|
||||
|
||||
|
||||
class NotEnoughAuthentication(Exception):
|
||||
"""
|
||||
This is thrown if the authentication is valid, but is not enough to
|
||||
successfully verify the user. i.e. don't retry this type of
|
||||
authentication, try another one.
|
||||
"""
|
||||
|
||||
|
||||
class ValidPublicKey(UnauthorizedLogin):
|
||||
"""
|
||||
Raised by public key checkers when they receive public key credentials
|
||||
that don't contain a signature at all, but are valid in every other way.
|
||||
(e.g. the public key matches one in the user's authorized_keys file).
|
||||
|
||||
Protocol code (eg
|
||||
L{SSHUserAuthServer<twisted.conch.ssh.userauth.SSHUserAuthServer>}) which
|
||||
attempts to log in using
|
||||
L{ISSHPrivateKey<twisted.cred.credentials.ISSHPrivateKey>} credentials
|
||||
should be prepared to handle a failure of this type by telling the user to
|
||||
re-authenticate using the same key and to include a signature with the new
|
||||
attempt.
|
||||
|
||||
See U{http://www.ietf.org/rfc/rfc4252.txt} section 7 for more details.
|
||||
"""
|
||||
|
||||
|
||||
class IgnoreAuthentication(Exception):
|
||||
"""
|
||||
This is thrown to let the UserAuthServer know it doesn't need to handle the
|
||||
authentication anymore.
|
||||
"""
|
||||
|
||||
|
||||
class MissingKeyStoreError(Exception):
|
||||
"""
|
||||
Raised if an SSHAgentServer starts receiving data without its factory
|
||||
providing a keys dict on which to read/write key data.
|
||||
"""
|
||||
|
||||
|
||||
class UserRejectedKey(Exception):
|
||||
"""
|
||||
The user interactively rejected a key.
|
||||
"""
|
||||
|
||||
|
||||
class InvalidEntry(Exception):
|
||||
"""
|
||||
An entry in a known_hosts file could not be interpreted as a valid entry.
|
||||
"""
|
||||
|
||||
|
||||
class HostKeyChanged(Exception):
|
||||
"""
|
||||
The host key of a remote host has changed.
|
||||
|
||||
@ivar offendingEntry: The entry which contains the persistent host key that
|
||||
disagrees with the given host key.
|
||||
|
||||
@type offendingEntry: L{twisted.conch.interfaces.IKnownHostEntry}
|
||||
|
||||
@ivar path: a reference to the known_hosts file that the offending entry
|
||||
was loaded from
|
||||
|
||||
@type path: L{twisted.python.filepath.FilePath}
|
||||
|
||||
@ivar lineno: The line number of the offending entry in the given path.
|
||||
|
||||
@type lineno: L{int}
|
||||
"""
|
||||
|
||||
def __init__(self, offendingEntry, path, lineno):
|
||||
Exception.__init__(self)
|
||||
self.offendingEntry = offendingEntry
|
||||
self.path = path
|
||||
self.lineno = lineno
|
||||
@@ -0,0 +1,4 @@
|
||||
"""
|
||||
Insults: a replacement for Curses/S-Lang.
|
||||
|
||||
Very basic at the moment."""
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,556 @@
|
||||
# -*- test-case-name: twisted.conch.test.test_helper -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Partial in-memory terminal emulator
|
||||
|
||||
@author: Jp Calderone
|
||||
"""
|
||||
|
||||
|
||||
import re
|
||||
import string
|
||||
|
||||
from zope.interface import implementer
|
||||
|
||||
from incremental import Version
|
||||
|
||||
from twisted.conch.insults import insults
|
||||
from twisted.internet import defer, protocol, reactor
|
||||
from twisted.logger import Logger
|
||||
from twisted.python import _textattributes
|
||||
from twisted.python.compat import iterbytes
|
||||
from twisted.python.deprecate import deprecated, deprecatedModuleAttribute
|
||||
|
||||
FOREGROUND = 30
|
||||
BACKGROUND = 40
|
||||
BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE, N_COLORS = range(9)
|
||||
|
||||
|
||||
class _FormattingState(_textattributes._FormattingStateMixin):
|
||||
"""
|
||||
Represents the formatting state/attributes of a single character.
|
||||
|
||||
Character set, intensity, underlinedness, blinkitude, video
|
||||
reversal, as well as foreground and background colors made up a
|
||||
character's attributes.
|
||||
"""
|
||||
|
||||
compareAttributes = (
|
||||
"charset",
|
||||
"bold",
|
||||
"underline",
|
||||
"blink",
|
||||
"reverseVideo",
|
||||
"foreground",
|
||||
"background",
|
||||
"_subtracting",
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
charset=insults.G0,
|
||||
bold=False,
|
||||
underline=False,
|
||||
blink=False,
|
||||
reverseVideo=False,
|
||||
foreground=WHITE,
|
||||
background=BLACK,
|
||||
_subtracting=False,
|
||||
):
|
||||
self.charset = charset
|
||||
self.bold = bold
|
||||
self.underline = underline
|
||||
self.blink = blink
|
||||
self.reverseVideo = reverseVideo
|
||||
self.foreground = foreground
|
||||
self.background = background
|
||||
self._subtracting = _subtracting
|
||||
|
||||
@deprecated(Version("Twisted", 13, 1, 0))
|
||||
def wantOne(self, **kw):
|
||||
"""
|
||||
Add a character attribute to a copy of this formatting state.
|
||||
|
||||
@param kw: An optional attribute name and value can be provided with
|
||||
a keyword argument.
|
||||
|
||||
@return: A formatting state instance with the new attribute.
|
||||
|
||||
@see: L{DefaultFormattingState._withAttribute}.
|
||||
"""
|
||||
k, v = kw.popitem()
|
||||
return self._withAttribute(k, v)
|
||||
|
||||
def toVT102(self):
|
||||
# Spit out a vt102 control sequence that will set up
|
||||
# all the attributes set here. Except charset.
|
||||
attrs = []
|
||||
if self._subtracting:
|
||||
attrs.append(0)
|
||||
if self.bold:
|
||||
attrs.append(insults.BOLD)
|
||||
if self.underline:
|
||||
attrs.append(insults.UNDERLINE)
|
||||
if self.blink:
|
||||
attrs.append(insults.BLINK)
|
||||
if self.reverseVideo:
|
||||
attrs.append(insults.REVERSE_VIDEO)
|
||||
if self.foreground != WHITE:
|
||||
attrs.append(FOREGROUND + self.foreground)
|
||||
if self.background != BLACK:
|
||||
attrs.append(BACKGROUND + self.background)
|
||||
if attrs:
|
||||
return "\x1b[" + ";".join(map(str, attrs)) + "m"
|
||||
return ""
|
||||
|
||||
|
||||
CharacterAttribute = _FormattingState
|
||||
|
||||
deprecatedModuleAttribute(
|
||||
Version("Twisted", 13, 1, 0),
|
||||
"Use twisted.conch.insults.text.assembleFormattedText instead.",
|
||||
"twisted.conch.insults.helper",
|
||||
"CharacterAttribute",
|
||||
)
|
||||
|
||||
|
||||
# XXX - need to support scroll regions and scroll history
|
||||
@implementer(insults.ITerminalTransport)
|
||||
class TerminalBuffer(protocol.Protocol):
|
||||
"""
|
||||
An in-memory terminal emulator.
|
||||
"""
|
||||
|
||||
for keyID in (
|
||||
b"UP_ARROW",
|
||||
b"DOWN_ARROW",
|
||||
b"RIGHT_ARROW",
|
||||
b"LEFT_ARROW",
|
||||
b"HOME",
|
||||
b"INSERT",
|
||||
b"DELETE",
|
||||
b"END",
|
||||
b"PGUP",
|
||||
b"PGDN",
|
||||
b"F1",
|
||||
b"F2",
|
||||
b"F3",
|
||||
b"F4",
|
||||
b"F5",
|
||||
b"F6",
|
||||
b"F7",
|
||||
b"F8",
|
||||
b"F9",
|
||||
b"F10",
|
||||
b"F11",
|
||||
b"F12",
|
||||
):
|
||||
execBytes = keyID + b" = object()"
|
||||
execStr = execBytes.decode("ascii")
|
||||
exec(execStr)
|
||||
|
||||
TAB = b"\t"
|
||||
BACKSPACE = b"\x7f"
|
||||
|
||||
width = 80
|
||||
height = 24
|
||||
|
||||
fill = b" "
|
||||
void = object()
|
||||
_log = Logger()
|
||||
|
||||
def getCharacter(self, x, y):
|
||||
return self.lines[y][x]
|
||||
|
||||
def connectionMade(self):
|
||||
self.reset()
|
||||
|
||||
def write(self, data):
|
||||
"""
|
||||
Add the given printable bytes to the terminal.
|
||||
|
||||
Line feeds in L{bytes} will be replaced with carriage return / line
|
||||
feed pairs.
|
||||
"""
|
||||
for b in iterbytes(data.replace(b"\n", b"\r\n")):
|
||||
self.insertAtCursor(b)
|
||||
|
||||
def _currentFormattingState(self):
|
||||
return _FormattingState(self.activeCharset, **self.graphicRendition)
|
||||
|
||||
def insertAtCursor(self, b):
|
||||
"""
|
||||
Add one byte to the terminal at the cursor and make consequent state
|
||||
updates.
|
||||
|
||||
If b is a carriage return, move the cursor to the beginning of the
|
||||
current row.
|
||||
|
||||
If b is a line feed, move the cursor to the next row or scroll down if
|
||||
the cursor is already in the last row.
|
||||
|
||||
Otherwise, if b is printable, put it at the cursor position (inserting
|
||||
or overwriting as dictated by the current mode) and move the cursor.
|
||||
"""
|
||||
if b == b"\r":
|
||||
self.x = 0
|
||||
elif b == b"\n":
|
||||
self._scrollDown()
|
||||
elif b in string.printable.encode("ascii"):
|
||||
if self.x >= self.width:
|
||||
self.nextLine()
|
||||
ch = (b, self._currentFormattingState())
|
||||
if self.modes.get(insults.modes.IRM):
|
||||
self.lines[self.y][self.x : self.x] = [ch]
|
||||
self.lines[self.y].pop()
|
||||
else:
|
||||
self.lines[self.y][self.x] = ch
|
||||
self.x += 1
|
||||
|
||||
def _emptyLine(self, width):
|
||||
return [(self.void, self._currentFormattingState()) for i in range(width)]
|
||||
|
||||
def _scrollDown(self):
|
||||
self.y += 1
|
||||
if self.y >= self.height:
|
||||
self.y -= 1
|
||||
del self.lines[0]
|
||||
self.lines.append(self._emptyLine(self.width))
|
||||
|
||||
def _scrollUp(self):
|
||||
self.y -= 1
|
||||
if self.y < 0:
|
||||
self.y = 0
|
||||
del self.lines[-1]
|
||||
self.lines.insert(0, self._emptyLine(self.width))
|
||||
|
||||
def cursorUp(self, n=1):
|
||||
self.y = max(0, self.y - n)
|
||||
|
||||
def cursorDown(self, n=1):
|
||||
self.y = min(self.height - 1, self.y + n)
|
||||
|
||||
def cursorBackward(self, n=1):
|
||||
self.x = max(0, self.x - n)
|
||||
|
||||
def cursorForward(self, n=1):
|
||||
self.x = min(self.width, self.x + n)
|
||||
|
||||
def cursorPosition(self, column, line):
|
||||
self.x = column
|
||||
self.y = line
|
||||
|
||||
def cursorHome(self):
|
||||
self.x = self.home.x
|
||||
self.y = self.home.y
|
||||
|
||||
def index(self):
|
||||
self._scrollDown()
|
||||
|
||||
def reverseIndex(self):
|
||||
self._scrollUp()
|
||||
|
||||
def nextLine(self):
|
||||
"""
|
||||
Update the cursor position attributes and scroll down if appropriate.
|
||||
"""
|
||||
self.x = 0
|
||||
self._scrollDown()
|
||||
|
||||
def saveCursor(self):
|
||||
self._savedCursor = (self.x, self.y)
|
||||
|
||||
def restoreCursor(self):
|
||||
self.x, self.y = self._savedCursor
|
||||
del self._savedCursor
|
||||
|
||||
def setModes(self, modes):
|
||||
for m in modes:
|
||||
self.modes[m] = True
|
||||
|
||||
def resetModes(self, modes):
|
||||
for m in modes:
|
||||
try:
|
||||
del self.modes[m]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
def setPrivateModes(self, modes):
|
||||
"""
|
||||
Enable the given modes.
|
||||
|
||||
Track which modes have been enabled so that the implementations of
|
||||
other L{insults.ITerminalTransport} methods can be properly implemented
|
||||
to respect these settings.
|
||||
|
||||
@see: L{resetPrivateModes}
|
||||
@see: L{insults.ITerminalTransport.setPrivateModes}
|
||||
"""
|
||||
for m in modes:
|
||||
self.privateModes[m] = True
|
||||
|
||||
def resetPrivateModes(self, modes):
|
||||
"""
|
||||
Disable the given modes.
|
||||
|
||||
@see: L{setPrivateModes}
|
||||
@see: L{insults.ITerminalTransport.resetPrivateModes}
|
||||
"""
|
||||
for m in modes:
|
||||
try:
|
||||
del self.privateModes[m]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
def applicationKeypadMode(self):
|
||||
self.keypadMode = "app"
|
||||
|
||||
def numericKeypadMode(self):
|
||||
self.keypadMode = "num"
|
||||
|
||||
def selectCharacterSet(self, charSet, which):
|
||||
self.charsets[which] = charSet
|
||||
|
||||
def shiftIn(self):
|
||||
self.activeCharset = insults.G0
|
||||
|
||||
def shiftOut(self):
|
||||
self.activeCharset = insults.G1
|
||||
|
||||
def singleShift2(self):
|
||||
oldActiveCharset = self.activeCharset
|
||||
self.activeCharset = insults.G2
|
||||
f = self.insertAtCursor
|
||||
|
||||
def insertAtCursor(b):
|
||||
f(b)
|
||||
del self.insertAtCursor
|
||||
self.activeCharset = oldActiveCharset
|
||||
|
||||
self.insertAtCursor = insertAtCursor
|
||||
|
||||
def singleShift3(self):
|
||||
oldActiveCharset = self.activeCharset
|
||||
self.activeCharset = insults.G3
|
||||
f = self.insertAtCursor
|
||||
|
||||
def insertAtCursor(b):
|
||||
f(b)
|
||||
del self.insertAtCursor
|
||||
self.activeCharset = oldActiveCharset
|
||||
|
||||
self.insertAtCursor = insertAtCursor
|
||||
|
||||
def selectGraphicRendition(self, *attributes):
|
||||
for a in attributes:
|
||||
if a == insults.NORMAL:
|
||||
self.graphicRendition = {
|
||||
"bold": False,
|
||||
"underline": False,
|
||||
"blink": False,
|
||||
"reverseVideo": False,
|
||||
"foreground": WHITE,
|
||||
"background": BLACK,
|
||||
}
|
||||
elif a == insults.BOLD:
|
||||
self.graphicRendition["bold"] = True
|
||||
elif a == insults.UNDERLINE:
|
||||
self.graphicRendition["underline"] = True
|
||||
elif a == insults.BLINK:
|
||||
self.graphicRendition["blink"] = True
|
||||
elif a == insults.REVERSE_VIDEO:
|
||||
self.graphicRendition["reverseVideo"] = True
|
||||
else:
|
||||
try:
|
||||
v = int(a)
|
||||
except ValueError:
|
||||
self._log.error(
|
||||
"Unknown graphic rendition attribute: {attr!r}", attr=a
|
||||
)
|
||||
else:
|
||||
if FOREGROUND <= v <= FOREGROUND + N_COLORS:
|
||||
self.graphicRendition["foreground"] = v - FOREGROUND
|
||||
elif BACKGROUND <= v <= BACKGROUND + N_COLORS:
|
||||
self.graphicRendition["background"] = v - BACKGROUND
|
||||
else:
|
||||
self._log.error(
|
||||
"Unknown graphic rendition attribute: {attr!r}", attr=a
|
||||
)
|
||||
|
||||
def eraseLine(self):
|
||||
self.lines[self.y] = self._emptyLine(self.width)
|
||||
|
||||
def eraseToLineEnd(self):
|
||||
width = self.width - self.x
|
||||
self.lines[self.y][self.x :] = self._emptyLine(width)
|
||||
|
||||
def eraseToLineBeginning(self):
|
||||
self.lines[self.y][: self.x + 1] = self._emptyLine(self.x + 1)
|
||||
|
||||
def eraseDisplay(self):
|
||||
self.lines = [self._emptyLine(self.width) for i in range(self.height)]
|
||||
|
||||
def eraseToDisplayEnd(self):
|
||||
self.eraseToLineEnd()
|
||||
height = self.height - self.y - 1
|
||||
self.lines[self.y + 1 :] = [self._emptyLine(self.width) for i in range(height)]
|
||||
|
||||
def eraseToDisplayBeginning(self):
|
||||
self.eraseToLineBeginning()
|
||||
self.lines[: self.y] = [self._emptyLine(self.width) for i in range(self.y)]
|
||||
|
||||
def deleteCharacter(self, n=1):
|
||||
del self.lines[self.y][self.x : self.x + n]
|
||||
self.lines[self.y].extend(self._emptyLine(min(self.width - self.x, n)))
|
||||
|
||||
def insertLine(self, n=1):
|
||||
self.lines[self.y : self.y] = [self._emptyLine(self.width) for i in range(n)]
|
||||
del self.lines[self.height :]
|
||||
|
||||
def deleteLine(self, n=1):
|
||||
del self.lines[self.y : self.y + n]
|
||||
self.lines.extend([self._emptyLine(self.width) for i in range(n)])
|
||||
|
||||
def reportCursorPosition(self):
|
||||
return (self.x, self.y)
|
||||
|
||||
def reset(self):
|
||||
self.home = insults.Vector(0, 0)
|
||||
self.x = self.y = 0
|
||||
self.modes = {}
|
||||
self.privateModes = {}
|
||||
self.setPrivateModes(
|
||||
[insults.privateModes.AUTO_WRAP, insults.privateModes.CURSOR_MODE]
|
||||
)
|
||||
self.numericKeypad = "app"
|
||||
self.activeCharset = insults.G0
|
||||
self.graphicRendition = {
|
||||
"bold": False,
|
||||
"underline": False,
|
||||
"blink": False,
|
||||
"reverseVideo": False,
|
||||
"foreground": WHITE,
|
||||
"background": BLACK,
|
||||
}
|
||||
self.charsets = {
|
||||
insults.G0: insults.CS_US,
|
||||
insults.G1: insults.CS_US,
|
||||
insults.G2: insults.CS_ALTERNATE,
|
||||
insults.G3: insults.CS_ALTERNATE_SPECIAL,
|
||||
}
|
||||
self.eraseDisplay()
|
||||
|
||||
def unhandledControlSequence(self, buf):
|
||||
print("Could not handle", repr(buf))
|
||||
|
||||
def __bytes__(self):
|
||||
lines = []
|
||||
for L in self.lines:
|
||||
buf = []
|
||||
length = 0
|
||||
for ch, attr in L:
|
||||
if ch is not self.void:
|
||||
buf.append(ch)
|
||||
length = len(buf)
|
||||
else:
|
||||
buf.append(self.fill)
|
||||
lines.append(b"".join(buf[:length]))
|
||||
return b"\n".join(lines)
|
||||
|
||||
def getHost(self):
|
||||
# ITransport.getHost
|
||||
raise NotImplementedError("Unimplemented: TerminalBuffer.getHost")
|
||||
|
||||
def getPeer(self):
|
||||
# ITransport.getPeer
|
||||
raise NotImplementedError("Unimplemented: TerminalBuffer.getPeer")
|
||||
|
||||
def loseConnection(self):
|
||||
# ITransport.loseConnection
|
||||
raise NotImplementedError("Unimplemented: TerminalBuffer.loseConnection")
|
||||
|
||||
def writeSequence(self, data):
|
||||
# ITransport.writeSequence
|
||||
raise NotImplementedError("Unimplemented: TerminalBuffer.writeSequence")
|
||||
|
||||
def horizontalTabulationSet(self):
|
||||
# ITerminalTransport.horizontalTabulationSet
|
||||
raise NotImplementedError(
|
||||
"Unimplemented: TerminalBuffer.horizontalTabulationSet"
|
||||
)
|
||||
|
||||
def tabulationClear(self):
|
||||
# TerminalTransport.tabulationClear
|
||||
raise NotImplementedError("Unimplemented: TerminalBuffer.tabulationClear")
|
||||
|
||||
def tabulationClearAll(self):
|
||||
# TerminalTransport.tabulationClearAll
|
||||
raise NotImplementedError("Unimplemented: TerminalBuffer.tabulationClearAll")
|
||||
|
||||
def doubleHeightLine(self, top=True):
|
||||
# ITerminalTransport.doubleHeightLine
|
||||
raise NotImplementedError("Unimplemented: TerminalBuffer.doubleHeightLine")
|
||||
|
||||
def singleWidthLine(self):
|
||||
# ITerminalTransport.singleWidthLine
|
||||
raise NotImplementedError("Unimplemented: TerminalBuffer.singleWidthLine")
|
||||
|
||||
def doubleWidthLine(self):
|
||||
# ITerminalTransport.doubleWidthLine
|
||||
raise NotImplementedError("Unimplemented: TerminalBuffer.doubleWidthLine")
|
||||
|
||||
|
||||
class ExpectationTimeout(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class ExpectableBuffer(TerminalBuffer):
|
||||
_mark = 0
|
||||
|
||||
def connectionMade(self):
|
||||
TerminalBuffer.connectionMade(self)
|
||||
self._expecting = []
|
||||
|
||||
def write(self, data):
|
||||
TerminalBuffer.write(self, data)
|
||||
self._checkExpected()
|
||||
|
||||
def cursorHome(self):
|
||||
TerminalBuffer.cursorHome(self)
|
||||
self._mark = 0
|
||||
|
||||
def _timeoutExpected(self, d):
|
||||
d.errback(ExpectationTimeout())
|
||||
self._checkExpected()
|
||||
|
||||
def _checkExpected(self):
|
||||
s = self.__bytes__()[self._mark :]
|
||||
while self._expecting:
|
||||
expr, timer, deferred = self._expecting[0]
|
||||
if timer and not timer.active():
|
||||
del self._expecting[0]
|
||||
continue
|
||||
for match in expr.finditer(s):
|
||||
if timer:
|
||||
timer.cancel()
|
||||
del self._expecting[0]
|
||||
self._mark += match.end()
|
||||
s = s[match.end() :]
|
||||
deferred.callback(match)
|
||||
break
|
||||
else:
|
||||
return
|
||||
|
||||
def expect(self, expression, timeout=None, scheduler=reactor):
|
||||
d = defer.Deferred()
|
||||
timer = None
|
||||
if timeout:
|
||||
timer = scheduler.callLater(timeout, self._timeoutExpected, d)
|
||||
self._expecting.append((re.compile(expression), timer, d))
|
||||
self._checkExpected()
|
||||
return d
|
||||
|
||||
|
||||
__all__ = ["CharacterAttribute", "TerminalBuffer", "ExpectableBuffer"]
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,176 @@
|
||||
# -*- test-case-name: twisted.conch.test.test_text -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Character attribute manipulation API.
|
||||
|
||||
This module provides a domain-specific language (using Python syntax)
|
||||
for the creation of text with additional display attributes associated
|
||||
with it. It is intended as an alternative to manually building up
|
||||
strings containing ECMA 48 character attribute control codes. It
|
||||
currently supports foreground and background colors (black, red,
|
||||
green, yellow, blue, magenta, cyan, and white), intensity selection,
|
||||
underlining, blinking and reverse video. Character set selection
|
||||
support is planned.
|
||||
|
||||
Character attributes are specified by using two Python operations:
|
||||
attribute lookup and indexing. For example, the string \"Hello
|
||||
world\" with red foreground and all other attributes set to their
|
||||
defaults, assuming the name twisted.conch.insults.text.attributes has
|
||||
been imported and bound to the name \"A\" (with the statement C{from
|
||||
twisted.conch.insults.text import attributes as A}, for example) one
|
||||
uses this expression::
|
||||
|
||||
A.fg.red[\"Hello world\"]
|
||||
|
||||
Other foreground colors are set by substituting their name for
|
||||
\"red\". To set both a foreground and a background color, this
|
||||
expression is used::
|
||||
|
||||
A.fg.red[A.bg.green[\"Hello world\"]]
|
||||
|
||||
Note that either A.bg.green can be nested within A.fg.red or vice
|
||||
versa. Also note that multiple items can be nested within a single
|
||||
index operation by separating them with commas::
|
||||
|
||||
A.bg.green[A.fg.red[\"Hello\"], " ", A.fg.blue[\"world\"]]
|
||||
|
||||
Other character attributes are set in a similar fashion. To specify a
|
||||
blinking version of the previous expression::
|
||||
|
||||
A.blink[A.bg.green[A.fg.red[\"Hello\"], " ", A.fg.blue[\"world\"]]]
|
||||
|
||||
C{A.reverseVideo}, C{A.underline}, and C{A.bold} are also valid.
|
||||
|
||||
A third operation is actually supported: unary negation. This turns
|
||||
off an attribute when an enclosing expression would otherwise have
|
||||
caused it to be on. For example::
|
||||
|
||||
A.underline[A.fg.red[\"Hello\", -A.underline[\" world\"]]]
|
||||
|
||||
A formatting structure can then be serialized into a string containing the
|
||||
necessary VT102 control codes with L{assembleFormattedText}.
|
||||
|
||||
@see: L{twisted.conch.insults.text._CharacterAttributes}
|
||||
@author: Jp Calderone
|
||||
"""
|
||||
|
||||
from incremental import Version
|
||||
|
||||
from twisted.conch.insults import helper, insults
|
||||
from twisted.python import _textattributes
|
||||
from twisted.python.deprecate import deprecatedModuleAttribute
|
||||
|
||||
flatten = _textattributes.flatten
|
||||
|
||||
deprecatedModuleAttribute(
|
||||
Version("Twisted", 13, 1, 0),
|
||||
"Use twisted.conch.insults.text.assembleFormattedText instead.",
|
||||
"twisted.conch.insults.text",
|
||||
"flatten",
|
||||
)
|
||||
|
||||
_TEXT_COLORS = {
|
||||
"black": helper.BLACK,
|
||||
"red": helper.RED,
|
||||
"green": helper.GREEN,
|
||||
"yellow": helper.YELLOW,
|
||||
"blue": helper.BLUE,
|
||||
"magenta": helper.MAGENTA,
|
||||
"cyan": helper.CYAN,
|
||||
"white": helper.WHITE,
|
||||
}
|
||||
|
||||
|
||||
class _CharacterAttributes(_textattributes.CharacterAttributesMixin):
|
||||
"""
|
||||
Factory for character attributes, including foreground and background color
|
||||
and non-color attributes such as bold, reverse video and underline.
|
||||
|
||||
Character attributes are applied to actual text by using object
|
||||
indexing-syntax (C{obj['abc']}) after accessing a factory attribute, for
|
||||
example::
|
||||
|
||||
attributes.bold['Some text']
|
||||
|
||||
These can be nested to mix attributes::
|
||||
|
||||
attributes.bold[attributes.underline['Some text']]
|
||||
|
||||
And multiple values can be passed::
|
||||
|
||||
attributes.normal[attributes.bold['Some'], ' text']
|
||||
|
||||
Non-color attributes can be accessed by attribute name, available
|
||||
attributes are:
|
||||
|
||||
- bold
|
||||
- blink
|
||||
- reverseVideo
|
||||
- underline
|
||||
|
||||
Available colors are:
|
||||
|
||||
0. black
|
||||
1. red
|
||||
2. green
|
||||
3. yellow
|
||||
4. blue
|
||||
5. magenta
|
||||
6. cyan
|
||||
7. white
|
||||
|
||||
@ivar fg: Foreground colors accessed by attribute name, see above
|
||||
for possible names.
|
||||
|
||||
@ivar bg: Background colors accessed by attribute name, see above
|
||||
for possible names.
|
||||
"""
|
||||
|
||||
fg = _textattributes._ColorAttribute(
|
||||
_textattributes._ForegroundColorAttr, _TEXT_COLORS
|
||||
)
|
||||
bg = _textattributes._ColorAttribute(
|
||||
_textattributes._BackgroundColorAttr, _TEXT_COLORS
|
||||
)
|
||||
|
||||
attrs = {
|
||||
"bold": insults.BOLD,
|
||||
"blink": insults.BLINK,
|
||||
"underline": insults.UNDERLINE,
|
||||
"reverseVideo": insults.REVERSE_VIDEO,
|
||||
}
|
||||
|
||||
|
||||
def assembleFormattedText(formatted):
|
||||
"""
|
||||
Assemble formatted text from structured information.
|
||||
|
||||
Currently handled formatting includes: bold, blink, reverse, underline and
|
||||
color codes.
|
||||
|
||||
For example::
|
||||
|
||||
from twisted.conch.insults.text import attributes as A
|
||||
assembleFormattedText(
|
||||
A.normal[A.bold['Time: '], A.fg.lightRed['Now!']])
|
||||
|
||||
Would produce "Time: " in bold formatting, followed by "Now!" with a
|
||||
foreground color of light red and without any additional formatting.
|
||||
|
||||
@param formatted: Structured text and attributes.
|
||||
|
||||
@rtype: L{str}
|
||||
@return: String containing VT102 control sequences that mimic those
|
||||
specified by C{formatted}.
|
||||
|
||||
@see: L{twisted.conch.insults.text._CharacterAttributes}
|
||||
@since: 13.1
|
||||
"""
|
||||
return _textattributes.flatten(formatted, helper._FormattingState(), "toVT102")
|
||||
|
||||
|
||||
attributes = _CharacterAttributes()
|
||||
|
||||
__all__ = ["attributes", "flatten"]
|
||||
@@ -0,0 +1,936 @@
|
||||
# -*- test-case-name: twisted.conch.test.test_window -*-
|
||||
|
||||
"""
|
||||
Simple insults-based widget library
|
||||
|
||||
@author: Jp Calderone
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import array
|
||||
|
||||
from twisted.conch.insults import helper, insults
|
||||
from twisted.python import text as tptext
|
||||
|
||||
|
||||
class YieldFocus(Exception):
|
||||
"""
|
||||
Input focus manipulation exception
|
||||
"""
|
||||
|
||||
|
||||
class BoundedTerminalWrapper:
|
||||
def __init__(self, terminal, width, height, xoff, yoff):
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.xoff = xoff
|
||||
self.yoff = yoff
|
||||
self.terminal = terminal
|
||||
self.cursorForward = terminal.cursorForward
|
||||
self.selectCharacterSet = terminal.selectCharacterSet
|
||||
self.selectGraphicRendition = terminal.selectGraphicRendition
|
||||
self.saveCursor = terminal.saveCursor
|
||||
self.restoreCursor = terminal.restoreCursor
|
||||
|
||||
def cursorPosition(self, x, y):
|
||||
return self.terminal.cursorPosition(
|
||||
self.xoff + min(self.width, x), self.yoff + min(self.height, y)
|
||||
)
|
||||
|
||||
def cursorHome(self):
|
||||
return self.terminal.cursorPosition(self.xoff, self.yoff)
|
||||
|
||||
def write(self, data):
|
||||
return self.terminal.write(data)
|
||||
|
||||
|
||||
class Widget:
|
||||
focused = False
|
||||
parent = None
|
||||
dirty = False
|
||||
width: int | None = None
|
||||
height: int | None = None
|
||||
|
||||
def repaint(self):
|
||||
if not self.dirty:
|
||||
self.dirty = True
|
||||
if self.parent is not None and not self.parent.dirty:
|
||||
self.parent.repaint()
|
||||
|
||||
def filthy(self):
|
||||
self.dirty = True
|
||||
|
||||
def redraw(self, width, height, terminal):
|
||||
self.filthy()
|
||||
self.draw(width, height, terminal)
|
||||
|
||||
def draw(self, width, height, terminal):
|
||||
if width != self.width or height != self.height or self.dirty:
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.dirty = False
|
||||
self.render(width, height, terminal)
|
||||
|
||||
def render(self, width, height, terminal):
|
||||
pass
|
||||
|
||||
def sizeHint(self):
|
||||
return None
|
||||
|
||||
def keystrokeReceived(self, keyID, modifier):
|
||||
if keyID == b"\t":
|
||||
self.tabReceived(modifier)
|
||||
elif keyID == b"\x7f":
|
||||
self.backspaceReceived()
|
||||
elif keyID in insults.FUNCTION_KEYS:
|
||||
self.functionKeyReceived(keyID, modifier)
|
||||
else:
|
||||
self.characterReceived(keyID, modifier)
|
||||
|
||||
def tabReceived(self, modifier):
|
||||
# XXX TODO - Handle shift+tab
|
||||
raise YieldFocus()
|
||||
|
||||
def focusReceived(self):
|
||||
"""
|
||||
Called when focus is being given to this widget.
|
||||
|
||||
May raise YieldFocus is this widget does not want focus.
|
||||
"""
|
||||
self.focused = True
|
||||
self.repaint()
|
||||
|
||||
def focusLost(self):
|
||||
self.focused = False
|
||||
self.repaint()
|
||||
|
||||
def backspaceReceived(self):
|
||||
pass
|
||||
|
||||
def functionKeyReceived(self, keyID, modifier):
|
||||
name = keyID
|
||||
if not isinstance(keyID, str):
|
||||
name = name.decode("utf-8")
|
||||
|
||||
# Peel off the square brackets added by the computed definition of
|
||||
# twisted.conch.insults.insults.FUNCTION_KEYS.
|
||||
methodName = "func_" + name[1:-1]
|
||||
|
||||
func = getattr(self, methodName, None)
|
||||
if func is not None:
|
||||
func(modifier)
|
||||
|
||||
def characterReceived(self, keyID, modifier):
|
||||
pass
|
||||
|
||||
|
||||
class ContainerWidget(Widget):
|
||||
"""
|
||||
@ivar focusedChild: The contained widget which currently has
|
||||
focus, or None.
|
||||
"""
|
||||
|
||||
focusedChild = None
|
||||
focused = False
|
||||
|
||||
def __init__(self):
|
||||
Widget.__init__(self)
|
||||
self.children = []
|
||||
|
||||
def addChild(self, child):
|
||||
assert child.parent is None
|
||||
child.parent = self
|
||||
self.children.append(child)
|
||||
if self.focusedChild is None and self.focused:
|
||||
try:
|
||||
child.focusReceived()
|
||||
except YieldFocus:
|
||||
pass
|
||||
else:
|
||||
self.focusedChild = child
|
||||
self.repaint()
|
||||
|
||||
def remChild(self, child):
|
||||
assert child.parent is self
|
||||
child.parent = None
|
||||
self.children.remove(child)
|
||||
self.repaint()
|
||||
|
||||
def filthy(self):
|
||||
for ch in self.children:
|
||||
ch.filthy()
|
||||
Widget.filthy(self)
|
||||
|
||||
def render(self, width, height, terminal):
|
||||
for ch in self.children:
|
||||
ch.draw(width, height, terminal)
|
||||
|
||||
def changeFocus(self):
|
||||
self.repaint()
|
||||
|
||||
if self.focusedChild is not None:
|
||||
self.focusedChild.focusLost()
|
||||
focusedChild = self.focusedChild
|
||||
self.focusedChild = None
|
||||
try:
|
||||
curFocus = self.children.index(focusedChild) + 1
|
||||
except ValueError:
|
||||
raise YieldFocus()
|
||||
else:
|
||||
curFocus = 0
|
||||
while curFocus < len(self.children):
|
||||
try:
|
||||
self.children[curFocus].focusReceived()
|
||||
except YieldFocus:
|
||||
curFocus += 1
|
||||
else:
|
||||
self.focusedChild = self.children[curFocus]
|
||||
return
|
||||
# None of our children wanted focus
|
||||
raise YieldFocus()
|
||||
|
||||
def focusReceived(self):
|
||||
self.changeFocus()
|
||||
self.focused = True
|
||||
|
||||
def keystrokeReceived(self, keyID, modifier):
|
||||
if self.focusedChild is not None:
|
||||
try:
|
||||
self.focusedChild.keystrokeReceived(keyID, modifier)
|
||||
except YieldFocus:
|
||||
self.changeFocus()
|
||||
self.repaint()
|
||||
else:
|
||||
Widget.keystrokeReceived(self, keyID, modifier)
|
||||
|
||||
|
||||
class TopWindow(ContainerWidget):
|
||||
"""
|
||||
A top-level container object which provides focus wrap-around and paint
|
||||
scheduling.
|
||||
|
||||
@ivar painter: A no-argument callable which will be invoked when this
|
||||
widget needs to be redrawn.
|
||||
|
||||
@ivar scheduler: A one-argument callable which will be invoked with a
|
||||
no-argument callable and should arrange for it to invoked at some point in
|
||||
the near future. The no-argument callable will cause this widget and all
|
||||
its children to be redrawn. It is typically beneficial for the no-argument
|
||||
callable to be invoked at the end of handling for whatever event is
|
||||
currently active; for example, it might make sense to call it at the end of
|
||||
L{twisted.conch.insults.insults.ITerminalProtocol.keystrokeReceived}.
|
||||
Note, however, that since calls to this may also be made in response to no
|
||||
apparent event, arrangements should be made for the function to be called
|
||||
even if an event handler such as C{keystrokeReceived} is not on the call
|
||||
stack (eg, using
|
||||
L{reactor.callLater<twisted.internet.interfaces.IReactorTime.callLater>}
|
||||
with a short timeout).
|
||||
"""
|
||||
|
||||
focused = True
|
||||
|
||||
def __init__(self, painter, scheduler):
|
||||
ContainerWidget.__init__(self)
|
||||
self.painter = painter
|
||||
self.scheduler = scheduler
|
||||
|
||||
_paintCall = None
|
||||
|
||||
def repaint(self):
|
||||
if self._paintCall is None:
|
||||
self._paintCall = object()
|
||||
self.scheduler(self._paint)
|
||||
ContainerWidget.repaint(self)
|
||||
|
||||
def _paint(self):
|
||||
self._paintCall = None
|
||||
self.painter()
|
||||
|
||||
def changeFocus(self):
|
||||
try:
|
||||
ContainerWidget.changeFocus(self)
|
||||
except YieldFocus:
|
||||
try:
|
||||
ContainerWidget.changeFocus(self)
|
||||
except YieldFocus:
|
||||
pass
|
||||
|
||||
def keystrokeReceived(self, keyID, modifier):
|
||||
try:
|
||||
ContainerWidget.keystrokeReceived(self, keyID, modifier)
|
||||
except YieldFocus:
|
||||
self.changeFocus()
|
||||
|
||||
|
||||
class AbsoluteBox(ContainerWidget):
|
||||
def moveChild(self, child, x, y):
|
||||
for n in range(len(self.children)):
|
||||
if self.children[n][0] is child:
|
||||
self.children[n] = (child, x, y)
|
||||
break
|
||||
else:
|
||||
raise ValueError("No such child", child)
|
||||
|
||||
def render(self, width, height, terminal):
|
||||
for ch, x, y in self.children:
|
||||
wrap = BoundedTerminalWrapper(terminal, width - x, height - y, x, y)
|
||||
ch.draw(width, height, wrap)
|
||||
|
||||
|
||||
class _Box(ContainerWidget):
|
||||
TOP, CENTER, BOTTOM = range(3)
|
||||
|
||||
def __init__(self, gravity=CENTER):
|
||||
ContainerWidget.__init__(self)
|
||||
self.gravity = gravity
|
||||
|
||||
def sizeHint(self):
|
||||
height = 0
|
||||
width = 0
|
||||
for ch in self.children:
|
||||
hint = ch.sizeHint()
|
||||
if hint is None:
|
||||
hint = (None, None)
|
||||
|
||||
if self.variableDimension == 0:
|
||||
if hint[0] is None:
|
||||
width = None
|
||||
elif width is not None:
|
||||
width += hint[0]
|
||||
if hint[1] is None:
|
||||
height = None
|
||||
elif height is not None:
|
||||
height = max(height, hint[1])
|
||||
else:
|
||||
if hint[0] is None:
|
||||
width = None
|
||||
elif width is not None:
|
||||
width = max(width, hint[0])
|
||||
if hint[1] is None:
|
||||
height = None
|
||||
elif height is not None:
|
||||
height += hint[1]
|
||||
|
||||
return width, height
|
||||
|
||||
def render(self, width, height, terminal):
|
||||
if not self.children:
|
||||
return
|
||||
|
||||
greedy = 0
|
||||
wants = []
|
||||
for ch in self.children:
|
||||
hint = ch.sizeHint()
|
||||
if hint is None:
|
||||
hint = (None, None)
|
||||
if hint[self.variableDimension] is None:
|
||||
greedy += 1
|
||||
wants.append(hint[self.variableDimension])
|
||||
|
||||
length = (width, height)[self.variableDimension]
|
||||
totalWant = sum(w for w in wants if w is not None)
|
||||
if greedy:
|
||||
leftForGreedy = int((length - totalWant) / greedy)
|
||||
|
||||
widthOffset = heightOffset = 0
|
||||
|
||||
for want, ch in zip(wants, self.children):
|
||||
if want is None:
|
||||
want = leftForGreedy
|
||||
|
||||
subWidth, subHeight = width, height
|
||||
if self.variableDimension == 0:
|
||||
subWidth = want
|
||||
else:
|
||||
subHeight = want
|
||||
|
||||
wrap = BoundedTerminalWrapper(
|
||||
terminal,
|
||||
subWidth,
|
||||
subHeight,
|
||||
widthOffset,
|
||||
heightOffset,
|
||||
)
|
||||
ch.draw(subWidth, subHeight, wrap)
|
||||
if self.variableDimension == 0:
|
||||
widthOffset += want
|
||||
else:
|
||||
heightOffset += want
|
||||
|
||||
|
||||
class HBox(_Box):
|
||||
variableDimension = 0
|
||||
|
||||
|
||||
class VBox(_Box):
|
||||
variableDimension = 1
|
||||
|
||||
|
||||
class Packer(ContainerWidget):
|
||||
def render(self, width, height, terminal):
|
||||
if not self.children:
|
||||
return
|
||||
|
||||
root = int(len(self.children) ** 0.5 + 0.5)
|
||||
boxes = [VBox() for n in range(root)]
|
||||
for n, ch in enumerate(self.children):
|
||||
boxes[n % len(boxes)].addChild(ch)
|
||||
h = HBox()
|
||||
map(h.addChild, boxes)
|
||||
h.render(width, height, terminal)
|
||||
|
||||
|
||||
class Canvas(Widget):
|
||||
focused = False
|
||||
|
||||
contents = None
|
||||
|
||||
def __init__(self):
|
||||
Widget.__init__(self)
|
||||
self.resize(1, 1)
|
||||
|
||||
def resize(self, width, height):
|
||||
contents = array.array("B", b" " * width * height)
|
||||
if self.contents is not None:
|
||||
for x in range(min(width, self._width)):
|
||||
for y in range(min(height, self._height)):
|
||||
contents[width * y + x] = self[x, y]
|
||||
self.contents = contents
|
||||
self._width = width
|
||||
self._height = height
|
||||
if self.x >= width:
|
||||
self.x = width - 1
|
||||
if self.y >= height:
|
||||
self.y = height - 1
|
||||
|
||||
def __getitem__(self, index):
|
||||
(x, y) = index
|
||||
return self.contents[(self._width * y) + x]
|
||||
|
||||
def __setitem__(self, index, value):
|
||||
(x, y) = index
|
||||
self.contents[(self._width * y) + x] = value
|
||||
|
||||
def clear(self):
|
||||
self.contents = array.array("B", b" " * len(self.contents))
|
||||
|
||||
def render(self, width, height, terminal):
|
||||
if not width or not height:
|
||||
return
|
||||
|
||||
if width != self._width or height != self._height:
|
||||
self.resize(width, height)
|
||||
for i in range(height):
|
||||
terminal.cursorPosition(0, i)
|
||||
text = self.contents[
|
||||
self._width * i : self._width * i + self._width
|
||||
].tobytes()
|
||||
text = text[:width]
|
||||
terminal.write(text)
|
||||
|
||||
|
||||
def horizontalLine(terminal, y, left, right):
|
||||
terminal.selectCharacterSet(insults.CS_DRAWING, insults.G0)
|
||||
terminal.cursorPosition(left, y)
|
||||
terminal.write(b"\161" * (right - left))
|
||||
terminal.selectCharacterSet(insults.CS_US, insults.G0)
|
||||
|
||||
|
||||
def verticalLine(terminal, x, top, bottom):
|
||||
terminal.selectCharacterSet(insults.CS_DRAWING, insults.G0)
|
||||
for n in range(top, bottom):
|
||||
terminal.cursorPosition(x, n)
|
||||
terminal.write(b"\170")
|
||||
terminal.selectCharacterSet(insults.CS_US, insults.G0)
|
||||
|
||||
|
||||
def rectangle(terminal, position, dimension):
|
||||
"""
|
||||
Draw a rectangle
|
||||
|
||||
@type position: L{tuple}
|
||||
@param position: A tuple of the (top, left) coordinates of the rectangle.
|
||||
@type dimension: L{tuple}
|
||||
@param dimension: A tuple of the (width, height) size of the rectangle.
|
||||
"""
|
||||
(top, left) = position
|
||||
(width, height) = dimension
|
||||
terminal.selectCharacterSet(insults.CS_DRAWING, insults.G0)
|
||||
|
||||
terminal.cursorPosition(top, left)
|
||||
terminal.write(b"\154")
|
||||
terminal.write(b"\161" * (width - 2))
|
||||
terminal.write(b"\153")
|
||||
for n in range(height - 2):
|
||||
terminal.cursorPosition(left, top + n + 1)
|
||||
terminal.write(b"\170")
|
||||
terminal.cursorForward(width - 2)
|
||||
terminal.write(b"\170")
|
||||
terminal.cursorPosition(0, top + height - 1)
|
||||
terminal.write(b"\155")
|
||||
terminal.write(b"\161" * (width - 2))
|
||||
terminal.write(b"\152")
|
||||
|
||||
terminal.selectCharacterSet(insults.CS_US, insults.G0)
|
||||
|
||||
|
||||
class Border(Widget):
|
||||
def __init__(self, containee):
|
||||
Widget.__init__(self)
|
||||
self.containee = containee
|
||||
self.containee.parent = self
|
||||
|
||||
def focusReceived(self):
|
||||
return self.containee.focusReceived()
|
||||
|
||||
def focusLost(self):
|
||||
return self.containee.focusLost()
|
||||
|
||||
def keystrokeReceived(self, keyID, modifier):
|
||||
return self.containee.keystrokeReceived(keyID, modifier)
|
||||
|
||||
def sizeHint(self):
|
||||
hint = self.containee.sizeHint()
|
||||
if hint is None:
|
||||
hint = (None, None)
|
||||
if hint[0] is None:
|
||||
x = None
|
||||
else:
|
||||
x = hint[0] + 2
|
||||
if hint[1] is None:
|
||||
y = None
|
||||
else:
|
||||
y = hint[1] + 2
|
||||
return x, y
|
||||
|
||||
def filthy(self):
|
||||
self.containee.filthy()
|
||||
Widget.filthy(self)
|
||||
|
||||
def render(self, width, height, terminal):
|
||||
if self.containee.focused:
|
||||
terminal.write(b"\x1b[31m")
|
||||
rectangle(terminal, (0, 0), (width, height))
|
||||
terminal.write(b"\x1b[0m")
|
||||
wrap = BoundedTerminalWrapper(terminal, width - 2, height - 2, 1, 1)
|
||||
self.containee.draw(width - 2, height - 2, wrap)
|
||||
|
||||
|
||||
class Button(Widget):
|
||||
def __init__(self, label, onPress):
|
||||
Widget.__init__(self)
|
||||
self.label = label
|
||||
self.onPress = onPress
|
||||
|
||||
def sizeHint(self):
|
||||
return len(self.label), 1
|
||||
|
||||
def characterReceived(self, keyID, modifier):
|
||||
if keyID == b"\r":
|
||||
self.onPress()
|
||||
|
||||
def render(self, width, height, terminal):
|
||||
terminal.cursorPosition(0, 0)
|
||||
if self.focused:
|
||||
terminal.write(b"\x1b[1m" + self.label + b"\x1b[0m")
|
||||
else:
|
||||
terminal.write(self.label)
|
||||
|
||||
|
||||
class TextInput(Widget):
|
||||
def __init__(self, maxwidth, onSubmit):
|
||||
Widget.__init__(self)
|
||||
self.onSubmit = onSubmit
|
||||
self.maxwidth = maxwidth
|
||||
self.buffer = b""
|
||||
self.cursor = 0
|
||||
|
||||
def setText(self, text):
|
||||
self.buffer = text[: self.maxwidth]
|
||||
self.cursor = len(self.buffer)
|
||||
self.repaint()
|
||||
|
||||
def func_LEFT_ARROW(self, modifier):
|
||||
if self.cursor > 0:
|
||||
self.cursor -= 1
|
||||
self.repaint()
|
||||
|
||||
def func_RIGHT_ARROW(self, modifier):
|
||||
if self.cursor < len(self.buffer):
|
||||
self.cursor += 1
|
||||
self.repaint()
|
||||
|
||||
def backspaceReceived(self):
|
||||
if self.cursor > 0:
|
||||
self.buffer = self.buffer[: self.cursor - 1] + self.buffer[self.cursor :]
|
||||
self.cursor -= 1
|
||||
self.repaint()
|
||||
|
||||
def characterReceived(self, keyID, modifier):
|
||||
if keyID == b"\r":
|
||||
self.onSubmit(self.buffer)
|
||||
else:
|
||||
if len(self.buffer) < self.maxwidth:
|
||||
self.buffer = (
|
||||
self.buffer[: self.cursor] + keyID + self.buffer[self.cursor :]
|
||||
)
|
||||
self.cursor += 1
|
||||
self.repaint()
|
||||
|
||||
def sizeHint(self):
|
||||
return self.maxwidth + 1, 1
|
||||
|
||||
def render(self, width, height, terminal):
|
||||
currentText = self._renderText()
|
||||
terminal.cursorPosition(0, 0)
|
||||
if self.focused:
|
||||
terminal.write(currentText[: self.cursor])
|
||||
cursor(terminal, currentText[self.cursor : self.cursor + 1] or b" ")
|
||||
terminal.write(currentText[self.cursor + 1 :])
|
||||
terminal.write(b" " * (self.maxwidth - len(currentText) + 1))
|
||||
else:
|
||||
more = self.maxwidth - len(currentText)
|
||||
terminal.write(currentText + b"_" * more)
|
||||
|
||||
def _renderText(self):
|
||||
return self.buffer
|
||||
|
||||
|
||||
class PasswordInput(TextInput):
|
||||
def _renderText(self):
|
||||
return "*" * len(self.buffer)
|
||||
|
||||
|
||||
class TextOutput(Widget):
|
||||
text = b""
|
||||
|
||||
def __init__(self, size=None):
|
||||
Widget.__init__(self)
|
||||
self.size = size
|
||||
|
||||
def sizeHint(self):
|
||||
return self.size
|
||||
|
||||
def render(self, width, height, terminal):
|
||||
terminal.cursorPosition(0, 0)
|
||||
text = self.text[:width]
|
||||
terminal.write(text + b" " * (width - len(text)))
|
||||
|
||||
def setText(self, text):
|
||||
self.text = text
|
||||
self.repaint()
|
||||
|
||||
def focusReceived(self):
|
||||
raise YieldFocus()
|
||||
|
||||
|
||||
class TextOutputArea(TextOutput):
|
||||
WRAP, TRUNCATE = range(2)
|
||||
|
||||
def __init__(self, size=None, longLines=WRAP):
|
||||
TextOutput.__init__(self, size)
|
||||
self.longLines = longLines
|
||||
|
||||
def render(self, width, height, terminal):
|
||||
n = 0
|
||||
inputLines = self.text.splitlines()
|
||||
outputLines = []
|
||||
while inputLines:
|
||||
if self.longLines == self.WRAP:
|
||||
line = inputLines.pop(0)
|
||||
if not isinstance(line, str):
|
||||
line = line.decode("utf-8")
|
||||
wrappedLines = []
|
||||
for wrappedLine in tptext.greedyWrap(line, width):
|
||||
if not isinstance(wrappedLine, bytes):
|
||||
wrappedLine = wrappedLine.encode("utf-8")
|
||||
wrappedLines.append(wrappedLine)
|
||||
outputLines.extend(wrappedLines or [b""])
|
||||
else:
|
||||
outputLines.append(inputLines.pop(0)[:width])
|
||||
if len(outputLines) >= height:
|
||||
break
|
||||
for n, L in enumerate(outputLines[:height]):
|
||||
terminal.cursorPosition(0, n)
|
||||
terminal.write(L)
|
||||
|
||||
|
||||
class Viewport(Widget):
|
||||
_xOffset = 0
|
||||
_yOffset = 0
|
||||
|
||||
@property
|
||||
def xOffset(self):
|
||||
return self._xOffset
|
||||
|
||||
@xOffset.setter
|
||||
def xOffset(self, value):
|
||||
if self._xOffset != value:
|
||||
self._xOffset = value
|
||||
self.repaint()
|
||||
|
||||
@property
|
||||
def yOffset(self):
|
||||
return self._yOffset
|
||||
|
||||
@yOffset.setter
|
||||
def yOffset(self, value):
|
||||
if self._yOffset != value:
|
||||
self._yOffset = value
|
||||
self.repaint()
|
||||
|
||||
_width = 160
|
||||
_height = 24
|
||||
|
||||
def __init__(self, containee):
|
||||
Widget.__init__(self)
|
||||
self.containee = containee
|
||||
self.containee.parent = self
|
||||
|
||||
self._buf = helper.TerminalBuffer()
|
||||
self._buf.width = self._width
|
||||
self._buf.height = self._height
|
||||
self._buf.connectionMade()
|
||||
|
||||
def filthy(self):
|
||||
self.containee.filthy()
|
||||
Widget.filthy(self)
|
||||
|
||||
def render(self, width, height, terminal):
|
||||
self.containee.draw(self._width, self._height, self._buf)
|
||||
|
||||
# XXX /Lame/
|
||||
for y, line in enumerate(
|
||||
self._buf.lines[self._yOffset : self._yOffset + height]
|
||||
):
|
||||
terminal.cursorPosition(0, y)
|
||||
n = 0
|
||||
for n, (ch, attr) in enumerate(line[self._xOffset : self._xOffset + width]):
|
||||
if ch is self._buf.void:
|
||||
ch = b" "
|
||||
terminal.write(ch)
|
||||
if n < width:
|
||||
terminal.write(b" " * (width - n - 1))
|
||||
|
||||
|
||||
class _Scrollbar(Widget):
|
||||
def __init__(self, onScroll):
|
||||
Widget.__init__(self)
|
||||
self.onScroll = onScroll
|
||||
self.percent = 0.0
|
||||
|
||||
def smaller(self):
|
||||
self.percent = min(1.0, max(0.0, self.onScroll(-1)))
|
||||
self.repaint()
|
||||
|
||||
def bigger(self):
|
||||
self.percent = min(1.0, max(0.0, self.onScroll(+1)))
|
||||
self.repaint()
|
||||
|
||||
|
||||
class HorizontalScrollbar(_Scrollbar):
|
||||
def sizeHint(self):
|
||||
return (None, 1)
|
||||
|
||||
def func_LEFT_ARROW(self, modifier):
|
||||
self.smaller()
|
||||
|
||||
def func_RIGHT_ARROW(self, modifier):
|
||||
self.bigger()
|
||||
|
||||
_left = "\N{BLACK LEFT-POINTING TRIANGLE}"
|
||||
_right = "\N{BLACK RIGHT-POINTING TRIANGLE}"
|
||||
_bar = "\N{LIGHT SHADE}"
|
||||
_slider = "\N{DARK SHADE}"
|
||||
|
||||
def render(self, width, height, terminal):
|
||||
terminal.cursorPosition(0, 0)
|
||||
n = width - 3
|
||||
before = int(n * self.percent)
|
||||
after = n - before
|
||||
me = (
|
||||
self._left
|
||||
+ (self._bar * before)
|
||||
+ self._slider
|
||||
+ (self._bar * after)
|
||||
+ self._right
|
||||
)
|
||||
terminal.write(me.encode("utf-8"))
|
||||
|
||||
|
||||
class VerticalScrollbar(_Scrollbar):
|
||||
def sizeHint(self):
|
||||
return (1, None)
|
||||
|
||||
def func_UP_ARROW(self, modifier):
|
||||
self.smaller()
|
||||
|
||||
def func_DOWN_ARROW(self, modifier):
|
||||
self.bigger()
|
||||
|
||||
_up = "\N{BLACK UP-POINTING TRIANGLE}"
|
||||
_down = "\N{BLACK DOWN-POINTING TRIANGLE}"
|
||||
_bar = "\N{LIGHT SHADE}"
|
||||
_slider = "\N{DARK SHADE}"
|
||||
|
||||
def render(self, width, height, terminal):
|
||||
terminal.cursorPosition(0, 0)
|
||||
knob = int(self.percent * (height - 2))
|
||||
terminal.write(self._up.encode("utf-8"))
|
||||
for i in range(1, height - 1):
|
||||
terminal.cursorPosition(0, i)
|
||||
if i != (knob + 1):
|
||||
terminal.write(self._bar.encode("utf-8"))
|
||||
else:
|
||||
terminal.write(self._slider.encode("utf-8"))
|
||||
terminal.cursorPosition(0, height - 1)
|
||||
terminal.write(self._down.encode("utf-8"))
|
||||
|
||||
|
||||
class ScrolledArea(Widget):
|
||||
"""
|
||||
A L{ScrolledArea} contains another widget wrapped in a viewport and
|
||||
vertical and horizontal scrollbars for moving the viewport around.
|
||||
"""
|
||||
|
||||
def __init__(self, containee):
|
||||
Widget.__init__(self)
|
||||
self._viewport = Viewport(containee)
|
||||
self._horiz = HorizontalScrollbar(self._horizScroll)
|
||||
self._vert = VerticalScrollbar(self._vertScroll)
|
||||
|
||||
for w in self._viewport, self._horiz, self._vert:
|
||||
w.parent = self
|
||||
|
||||
def _horizScroll(self, n):
|
||||
self._viewport.xOffset += n
|
||||
self._viewport.xOffset = max(0, self._viewport.xOffset)
|
||||
return self._viewport.xOffset / 25.0
|
||||
|
||||
def _vertScroll(self, n):
|
||||
self._viewport.yOffset += n
|
||||
self._viewport.yOffset = max(0, self._viewport.yOffset)
|
||||
return self._viewport.yOffset / 25.0
|
||||
|
||||
def func_UP_ARROW(self, modifier):
|
||||
self._vert.smaller()
|
||||
|
||||
def func_DOWN_ARROW(self, modifier):
|
||||
self._vert.bigger()
|
||||
|
||||
def func_LEFT_ARROW(self, modifier):
|
||||
self._horiz.smaller()
|
||||
|
||||
def func_RIGHT_ARROW(self, modifier):
|
||||
self._horiz.bigger()
|
||||
|
||||
def filthy(self):
|
||||
self._viewport.filthy()
|
||||
self._horiz.filthy()
|
||||
self._vert.filthy()
|
||||
Widget.filthy(self)
|
||||
|
||||
def render(self, width, height, terminal):
|
||||
wrapper = BoundedTerminalWrapper(terminal, width - 2, height - 2, 1, 1)
|
||||
self._viewport.draw(width - 2, height - 2, wrapper)
|
||||
if self.focused:
|
||||
terminal.write(b"\x1b[31m")
|
||||
horizontalLine(terminal, 0, 1, width - 1)
|
||||
verticalLine(terminal, 0, 1, height - 1)
|
||||
self._vert.draw(
|
||||
1, height - 1, BoundedTerminalWrapper(terminal, 1, height - 1, width - 1, 0)
|
||||
)
|
||||
self._horiz.draw(
|
||||
width, 1, BoundedTerminalWrapper(terminal, width, 1, 0, height - 1)
|
||||
)
|
||||
terminal.write(b"\x1b[0m")
|
||||
|
||||
|
||||
def cursor(terminal, ch):
|
||||
terminal.saveCursor()
|
||||
terminal.selectGraphicRendition(str(insults.REVERSE_VIDEO))
|
||||
terminal.write(ch)
|
||||
terminal.restoreCursor()
|
||||
terminal.cursorForward()
|
||||
|
||||
|
||||
class Selection(Widget):
|
||||
# Index into the sequence
|
||||
focusedIndex = 0
|
||||
|
||||
# Offset into the displayed subset of the sequence
|
||||
renderOffset = 0
|
||||
|
||||
def __init__(self, sequence, onSelect, minVisible=None):
|
||||
Widget.__init__(self)
|
||||
self.sequence = sequence
|
||||
self.onSelect = onSelect
|
||||
self.minVisible = minVisible
|
||||
if minVisible is not None:
|
||||
self._width = max(map(len, self.sequence))
|
||||
|
||||
def sizeHint(self):
|
||||
if self.minVisible is not None:
|
||||
return self._width, self.minVisible
|
||||
|
||||
def func_UP_ARROW(self, modifier):
|
||||
if self.focusedIndex > 0:
|
||||
self.focusedIndex -= 1
|
||||
if self.renderOffset > 0:
|
||||
self.renderOffset -= 1
|
||||
self.repaint()
|
||||
|
||||
def func_PGUP(self, modifier):
|
||||
if self.renderOffset != 0:
|
||||
self.focusedIndex -= self.renderOffset
|
||||
self.renderOffset = 0
|
||||
else:
|
||||
self.focusedIndex = max(0, self.focusedIndex - self.height)
|
||||
self.repaint()
|
||||
|
||||
def func_DOWN_ARROW(self, modifier):
|
||||
if self.focusedIndex < len(self.sequence) - 1:
|
||||
self.focusedIndex += 1
|
||||
if self.renderOffset < self.height - 1:
|
||||
self.renderOffset += 1
|
||||
self.repaint()
|
||||
|
||||
def func_PGDN(self, modifier):
|
||||
if self.renderOffset != self.height - 1:
|
||||
change = self.height - self.renderOffset - 1
|
||||
if change + self.focusedIndex >= len(self.sequence):
|
||||
change = len(self.sequence) - self.focusedIndex - 1
|
||||
self.focusedIndex += change
|
||||
self.renderOffset = self.height - 1
|
||||
else:
|
||||
self.focusedIndex = min(
|
||||
len(self.sequence) - 1, self.focusedIndex + self.height
|
||||
)
|
||||
self.repaint()
|
||||
|
||||
def characterReceived(self, keyID, modifier):
|
||||
if keyID == b"\r":
|
||||
self.onSelect(self.sequence[self.focusedIndex])
|
||||
|
||||
def render(self, width, height, terminal):
|
||||
self.height = height
|
||||
start = self.focusedIndex - self.renderOffset
|
||||
if start > len(self.sequence) - height:
|
||||
start = max(0, len(self.sequence) - height)
|
||||
|
||||
elements = self.sequence[start : start + height]
|
||||
|
||||
for n, ele in enumerate(elements):
|
||||
terminal.cursorPosition(0, n)
|
||||
if n == self.renderOffset:
|
||||
terminal.saveCursor()
|
||||
if self.focused:
|
||||
modes = str(insults.REVERSE_VIDEO), str(insults.BOLD)
|
||||
else:
|
||||
modes = (str(insults.REVERSE_VIDEO),)
|
||||
terminal.selectGraphicRendition(*modes)
|
||||
text = ele[:width]
|
||||
terminal.write(text + (b" " * (width - len(text))))
|
||||
if n == self.renderOffset:
|
||||
terminal.restoreCursor()
|
||||
456
backend/lib/python3.12/site-packages/twisted/conch/interfaces.py
Normal file
456
backend/lib/python3.12/site-packages/twisted/conch/interfaces.py
Normal file
@@ -0,0 +1,456 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
This module contains interfaces defined for the L{twisted.conch} package.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from zope.interface import Attribute, Interface
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from twisted.conch.ssh.keys import Key
|
||||
|
||||
|
||||
class IConchUser(Interface):
|
||||
"""
|
||||
A user who has been authenticated to Cred through Conch. This is
|
||||
the interface between the SSH connection and the user.
|
||||
"""
|
||||
|
||||
conn = Attribute("The SSHConnection object for this user.")
|
||||
|
||||
def lookupChannel(channelType, windowSize, maxPacket, data):
|
||||
"""
|
||||
The other side requested a channel of some sort.
|
||||
|
||||
C{channelType} is the type of channel being requested,
|
||||
as an ssh connection protocol channel type.
|
||||
C{data} is any other packet data (often nothing).
|
||||
|
||||
We return a subclass of L{SSHChannel<ssh.channel.SSHChannel>}. If
|
||||
the channel type is unknown, we return C{None}.
|
||||
|
||||
For other failures, we raise an exception. If a
|
||||
L{ConchError<error.ConchError>} is raised, the C{.value} will
|
||||
be the message, and the C{.data} will be the error code.
|
||||
|
||||
@param channelType: The requested channel type
|
||||
@type channelType: L{bytes}
|
||||
@param windowSize: The initial size of the remote window
|
||||
@type windowSize: L{int}
|
||||
@param maxPacket: The largest packet we should send
|
||||
@type maxPacket: L{int}
|
||||
@param data: Additional request data
|
||||
@type data: L{bytes}
|
||||
@rtype: a subclass of L{SSHChannel} or L{None}
|
||||
"""
|
||||
|
||||
def lookupSubsystem(subsystem, data):
|
||||
"""
|
||||
The other side requested a subsystem.
|
||||
|
||||
We return a L{Protocol} implementing the requested subsystem.
|
||||
If the subsystem is not available, we return C{None}.
|
||||
|
||||
@param subsystem: The name of the subsystem being requested
|
||||
@type subsystem: L{bytes}
|
||||
@param data: Additional request data (often nothing)
|
||||
@type data: L{bytes}
|
||||
@rtype: L{Protocol} or L{None}
|
||||
"""
|
||||
|
||||
def gotGlobalRequest(requestType, data):
|
||||
"""
|
||||
A global request was sent from the other side.
|
||||
|
||||
We return a true value on success or a false value on failure.
|
||||
If we indicate success by returning a tuple, its second item
|
||||
will be sent to the other side as additional response data.
|
||||
|
||||
@param requestType: The type of the request
|
||||
@type requestType: L{bytes}
|
||||
@param data: Additional request data
|
||||
@type data: L{bytes}
|
||||
@rtype: boolean or L{tuple}
|
||||
"""
|
||||
|
||||
|
||||
class ISession(Interface):
|
||||
def getPty(term, windowSize, modes):
|
||||
"""
|
||||
Get a pseudo-terminal for use by a shell or command.
|
||||
|
||||
If a pseudo-terminal is not available, or the request otherwise
|
||||
fails, raise an exception.
|
||||
"""
|
||||
|
||||
def openShell(proto):
|
||||
"""
|
||||
Open a shell and connect it to proto.
|
||||
|
||||
@param proto: a L{ProcessProtocol} instance.
|
||||
"""
|
||||
|
||||
def execCommand(proto, command):
|
||||
"""
|
||||
Execute a command.
|
||||
|
||||
@param proto: a L{ProcessProtocol} instance.
|
||||
"""
|
||||
|
||||
def windowChanged(newWindowSize):
|
||||
"""
|
||||
Called when the size of the remote screen has changed.
|
||||
"""
|
||||
|
||||
def eofReceived():
|
||||
"""
|
||||
Called when the other side has indicated no more data will be sent.
|
||||
"""
|
||||
|
||||
def closed():
|
||||
"""
|
||||
Called when the session is closed.
|
||||
"""
|
||||
|
||||
|
||||
class EnvironmentVariableNotPermitted(ValueError):
|
||||
"""Setting this environment variable in this session is not permitted."""
|
||||
|
||||
|
||||
class ISessionSetEnv(Interface):
|
||||
"""A session that can set environment variables."""
|
||||
|
||||
def setEnv(name, value):
|
||||
"""
|
||||
Set an environment variable for the shell or command to be started.
|
||||
|
||||
From U{RFC 4254, section 6.4
|
||||
<https://tools.ietf.org/html/rfc4254#section-6.4>}: "Uncontrolled
|
||||
setting of environment variables in a privileged process can be a
|
||||
security hazard. It is recommended that implementations either
|
||||
maintain a list of allowable variable names or only set environment
|
||||
variables after the server process has dropped sufficient
|
||||
privileges."
|
||||
|
||||
(OpenSSH refuses all environment variables by default, but has an
|
||||
C{AcceptEnv} configuration option to select specific variables to
|
||||
accept.)
|
||||
|
||||
@param name: The name of the environment variable to set.
|
||||
@type name: L{bytes}
|
||||
@param value: The value of the environment variable to set.
|
||||
@type value: L{bytes}
|
||||
@raise EnvironmentVariableNotPermitted: if setting this environment
|
||||
variable is not permitted.
|
||||
"""
|
||||
|
||||
|
||||
class ISFTPServer(Interface):
|
||||
"""
|
||||
SFTP subsystem for server-side communication.
|
||||
|
||||
Each method should check to verify that the user has permission for
|
||||
their actions.
|
||||
"""
|
||||
|
||||
avatar = Attribute(
|
||||
"""
|
||||
The avatar returned by the Realm that we are authenticated with,
|
||||
and represents the logged-in user.
|
||||
"""
|
||||
)
|
||||
|
||||
def gotVersion(otherVersion, extData):
|
||||
"""
|
||||
Called when the client sends their version info.
|
||||
|
||||
otherVersion is an integer representing the version of the SFTP
|
||||
protocol they are claiming.
|
||||
extData is a dictionary of extended_name : extended_data items.
|
||||
These items are sent by the client to indicate additional features.
|
||||
|
||||
This method should return a dictionary of extended_name : extended_data
|
||||
items. These items are the additional features (if any) supported
|
||||
by the server.
|
||||
"""
|
||||
return {}
|
||||
|
||||
def openFile(filename, flags, attrs):
|
||||
"""
|
||||
Called when the clients asks to open a file.
|
||||
|
||||
@param filename: a string representing the file to open.
|
||||
|
||||
@param flags: an integer of the flags to open the file with, ORed
|
||||
together. The flags and their values are listed at the bottom of
|
||||
L{twisted.conch.ssh.filetransfer} as FXF_*.
|
||||
|
||||
@param attrs: a list of attributes to open the file with. It is a
|
||||
dictionary, consisting of 0 or more keys. The possible keys are::
|
||||
|
||||
size: the size of the file in bytes
|
||||
uid: the user ID of the file as an integer
|
||||
gid: the group ID of the file as an integer
|
||||
permissions: the permissions of the file with as an integer.
|
||||
the bit representation of this field is defined by POSIX.
|
||||
atime: the access time of the file as seconds since the epoch.
|
||||
mtime: the modification time of the file as seconds since the epoch.
|
||||
ext_*: extended attributes. The server is not required to
|
||||
understand this, but it may.
|
||||
|
||||
NOTE: there is no way to indicate text or binary files. it is up
|
||||
to the SFTP client to deal with this.
|
||||
|
||||
This method returns an object that meets the ISFTPFile interface.
|
||||
Alternatively, it can return a L{Deferred} that will be called back
|
||||
with the object.
|
||||
"""
|
||||
|
||||
def removeFile(filename):
|
||||
"""
|
||||
Remove the given file.
|
||||
|
||||
This method returns when the remove succeeds, or a Deferred that is
|
||||
called back when it succeeds.
|
||||
|
||||
@param filename: the name of the file as a string.
|
||||
"""
|
||||
|
||||
def renameFile(oldpath, newpath):
|
||||
"""
|
||||
Rename the given file.
|
||||
|
||||
This method returns when the rename succeeds, or a L{Deferred} that is
|
||||
called back when it succeeds. If the rename fails, C{renameFile} will
|
||||
raise an implementation-dependent exception.
|
||||
|
||||
@param oldpath: the current location of the file.
|
||||
@param newpath: the new file name.
|
||||
"""
|
||||
|
||||
def makeDirectory(path, attrs):
|
||||
"""
|
||||
Make a directory.
|
||||
|
||||
This method returns when the directory is created, or a Deferred that
|
||||
is called back when it is created.
|
||||
|
||||
@param path: the name of the directory to create as a string.
|
||||
@param attrs: a dictionary of attributes to create the directory with.
|
||||
Its meaning is the same as the attrs in the L{openFile} method.
|
||||
"""
|
||||
|
||||
def removeDirectory(path):
|
||||
"""
|
||||
Remove a directory (non-recursively)
|
||||
|
||||
It is an error to remove a directory that has files or directories in
|
||||
it.
|
||||
|
||||
This method returns when the directory is removed, or a Deferred that
|
||||
is called back when it is removed.
|
||||
|
||||
@param path: the directory to remove.
|
||||
"""
|
||||
|
||||
def openDirectory(path):
|
||||
"""
|
||||
Open a directory for scanning.
|
||||
|
||||
This method returns an iterable object that has a close() method,
|
||||
or a Deferred that is called back with same.
|
||||
|
||||
The close() method is called when the client is finished reading
|
||||
from the directory. At this point, the iterable will no longer
|
||||
be used.
|
||||
|
||||
The iterable should return triples of the form (filename,
|
||||
longname, attrs) or Deferreds that return the same. The
|
||||
sequence must support __getitem__, but otherwise may be any
|
||||
'sequence-like' object.
|
||||
|
||||
filename is the name of the file relative to the directory.
|
||||
logname is an expanded format of the filename. The recommended format
|
||||
is:
|
||||
-rwxr-xr-x 1 mjos staff 348911 Mar 25 14:29 t-filexfer
|
||||
1234567890 123 12345678 12345678 12345678 123456789012
|
||||
|
||||
The first line is sample output, the second is the length of the field.
|
||||
The fields are: permissions, link count, user owner, group owner,
|
||||
size in bytes, modification time.
|
||||
|
||||
attrs is a dictionary in the format of the attrs argument to openFile.
|
||||
|
||||
@param path: the directory to open.
|
||||
"""
|
||||
|
||||
def getAttrs(path, followLinks):
|
||||
"""
|
||||
Return the attributes for the given path.
|
||||
|
||||
This method returns a dictionary in the same format as the attrs
|
||||
argument to openFile or a Deferred that is called back with same.
|
||||
|
||||
@param path: the path to return attributes for as a string.
|
||||
@param followLinks: a boolean. If it is True, follow symbolic links
|
||||
and return attributes for the real path at the base. If it is False,
|
||||
return attributes for the specified path.
|
||||
"""
|
||||
|
||||
def setAttrs(path, attrs):
|
||||
"""
|
||||
Set the attributes for the path.
|
||||
|
||||
This method returns when the attributes are set or a Deferred that is
|
||||
called back when they are.
|
||||
|
||||
@param path: the path to set attributes for as a string.
|
||||
@param attrs: a dictionary in the same format as the attrs argument to
|
||||
L{openFile}.
|
||||
"""
|
||||
|
||||
def readLink(path):
|
||||
"""
|
||||
Find the root of a set of symbolic links.
|
||||
|
||||
This method returns the target of the link, or a Deferred that
|
||||
returns the same.
|
||||
|
||||
@param path: the path of the symlink to read.
|
||||
"""
|
||||
|
||||
def makeLink(linkPath, targetPath):
|
||||
"""
|
||||
Create a symbolic link.
|
||||
|
||||
This method returns when the link is made, or a Deferred that
|
||||
returns the same.
|
||||
|
||||
@param linkPath: the pathname of the symlink as a string.
|
||||
@param targetPath: the path of the target of the link as a string.
|
||||
"""
|
||||
|
||||
def realPath(path):
|
||||
"""
|
||||
Convert any path to an absolute path.
|
||||
|
||||
This method returns the absolute path as a string, or a Deferred
|
||||
that returns the same.
|
||||
|
||||
@param path: the path to convert as a string.
|
||||
"""
|
||||
|
||||
def extendedRequest(extendedName, extendedData):
|
||||
"""
|
||||
This is the extension mechanism for SFTP. The other side can send us
|
||||
arbitrary requests.
|
||||
|
||||
If we don't implement the request given by extendedName, raise
|
||||
NotImplementedError.
|
||||
|
||||
The return value is a string, or a Deferred that will be called
|
||||
back with a string.
|
||||
|
||||
@param extendedName: the name of the request as a string.
|
||||
@param extendedData: the data the other side sent with the request,
|
||||
as a string.
|
||||
"""
|
||||
|
||||
|
||||
class IKnownHostEntry(Interface):
|
||||
"""
|
||||
A L{IKnownHostEntry} is an entry in an OpenSSH-formatted C{known_hosts}
|
||||
file.
|
||||
|
||||
@since: 8.2
|
||||
"""
|
||||
|
||||
def matchesKey(key: Key) -> bool:
|
||||
"""
|
||||
Return True if this entry matches the given Key object, False
|
||||
otherwise.
|
||||
|
||||
@param key: The key object to match against.
|
||||
"""
|
||||
|
||||
def matchesHost(hostname: bytes) -> bool:
|
||||
"""
|
||||
Return True if this entry matches the given hostname, False otherwise.
|
||||
|
||||
Note that this does no name resolution; if you want to match an IP
|
||||
address, you have to resolve it yourself, and pass it in as a dotted
|
||||
quad string.
|
||||
|
||||
@param hostname: The hostname to match against.
|
||||
"""
|
||||
|
||||
def toString() -> bytes:
|
||||
"""
|
||||
@return: a serialized string representation of this entry, suitable for
|
||||
inclusion in a known_hosts file. (Newline not included.)
|
||||
"""
|
||||
|
||||
|
||||
class ISFTPFile(Interface):
|
||||
"""
|
||||
This represents an open file on the server. An object adhering to this
|
||||
interface should be returned from L{openFile}().
|
||||
"""
|
||||
|
||||
def close():
|
||||
"""
|
||||
Close the file.
|
||||
|
||||
This method returns nothing if the close succeeds immediately, or a
|
||||
Deferred that is called back when the close succeeds.
|
||||
"""
|
||||
|
||||
def readChunk(offset, length):
|
||||
"""
|
||||
Read from the file.
|
||||
|
||||
If EOF is reached before any data is read, raise EOFError.
|
||||
|
||||
This method returns the data as a string, or a Deferred that is
|
||||
called back with same.
|
||||
|
||||
@param offset: an integer that is the index to start from in the file.
|
||||
@param length: the maximum length of data to return. The actual amount
|
||||
returned may less than this. For normal disk files, however,
|
||||
this should read the requested number (up to the end of the file).
|
||||
"""
|
||||
|
||||
def writeChunk(offset, data):
|
||||
"""
|
||||
Write to the file.
|
||||
|
||||
This method returns when the write completes, or a Deferred that is
|
||||
called when it completes.
|
||||
|
||||
@param offset: an integer that is the index to start from in the file.
|
||||
@param data: a string that is the data to write.
|
||||
"""
|
||||
|
||||
def getAttrs():
|
||||
"""
|
||||
Return the attributes for the file.
|
||||
|
||||
This method returns a dictionary in the same format as the attrs
|
||||
argument to L{openFile} or a L{Deferred} that is called back with same.
|
||||
"""
|
||||
|
||||
def setAttrs(attrs):
|
||||
"""
|
||||
Set the attributes for the file.
|
||||
|
||||
This method returns when the attributes are set or a Deferred that is
|
||||
called back when they are.
|
||||
|
||||
@param attrs: a dictionary in the same format as the attrs argument to
|
||||
L{openFile}.
|
||||
"""
|
||||
104
backend/lib/python3.12/site-packages/twisted/conch/ls.py
Normal file
104
backend/lib/python3.12/site-packages/twisted/conch/ls.py
Normal file
@@ -0,0 +1,104 @@
|
||||
# -*- test-case-name: twisted.conch.test.test_cftp -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
import array
|
||||
import stat
|
||||
from time import localtime, strftime, time
|
||||
|
||||
# Locale-independent month names to use instead of strftime's
|
||||
_MONTH_NAMES = dict(
|
||||
list(
|
||||
zip(
|
||||
list(range(1, 13)),
|
||||
"Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec".split(),
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def lsLine(name, s):
|
||||
"""
|
||||
Build an 'ls' line for a file ('file' in its generic sense, it
|
||||
can be of any type).
|
||||
"""
|
||||
mode = s.st_mode
|
||||
perms = array.array("B", b"-" * 10)
|
||||
ft = stat.S_IFMT(mode)
|
||||
if stat.S_ISDIR(ft):
|
||||
perms[0] = ord("d")
|
||||
elif stat.S_ISCHR(ft):
|
||||
perms[0] = ord("c")
|
||||
elif stat.S_ISBLK(ft):
|
||||
perms[0] = ord("b")
|
||||
elif stat.S_ISREG(ft):
|
||||
perms[0] = ord("-")
|
||||
elif stat.S_ISFIFO(ft):
|
||||
perms[0] = ord("f")
|
||||
elif stat.S_ISLNK(ft):
|
||||
perms[0] = ord("l")
|
||||
elif stat.S_ISSOCK(ft):
|
||||
perms[0] = ord("s")
|
||||
else:
|
||||
perms[0] = ord("!")
|
||||
# User
|
||||
if mode & stat.S_IRUSR:
|
||||
perms[1] = ord("r")
|
||||
if mode & stat.S_IWUSR:
|
||||
perms[2] = ord("w")
|
||||
if mode & stat.S_IXUSR:
|
||||
perms[3] = ord("x")
|
||||
# Group
|
||||
if mode & stat.S_IRGRP:
|
||||
perms[4] = ord("r")
|
||||
if mode & stat.S_IWGRP:
|
||||
perms[5] = ord("w")
|
||||
if mode & stat.S_IXGRP:
|
||||
perms[6] = ord("x")
|
||||
# Other
|
||||
if mode & stat.S_IROTH:
|
||||
perms[7] = ord("r")
|
||||
if mode & stat.S_IWOTH:
|
||||
perms[8] = ord("w")
|
||||
if mode & stat.S_IXOTH:
|
||||
perms[9] = ord("x")
|
||||
# Suid/sgid
|
||||
if mode & stat.S_ISUID:
|
||||
if perms[3] == ord("x"):
|
||||
perms[3] = ord("s")
|
||||
else:
|
||||
perms[3] = ord("S")
|
||||
if mode & stat.S_ISGID:
|
||||
if perms[6] == ord("x"):
|
||||
perms[6] = ord("s")
|
||||
else:
|
||||
perms[6] = ord("S")
|
||||
|
||||
if isinstance(name, bytes):
|
||||
name = name.decode("utf-8")
|
||||
lsPerms = perms.tobytes()
|
||||
lsPerms = lsPerms.decode("utf-8")
|
||||
|
||||
lsresult = [
|
||||
lsPerms,
|
||||
str(s.st_nlink).rjust(5),
|
||||
" ",
|
||||
str(s.st_uid).ljust(9),
|
||||
str(s.st_gid).ljust(9),
|
||||
str(s.st_size).rjust(8),
|
||||
" ",
|
||||
]
|
||||
# Need to specify the month manually, as strftime depends on locale
|
||||
ttup = localtime(s.st_mtime)
|
||||
sixmonths = 60 * 60 * 24 * 7 * 26
|
||||
if s.st_mtime + sixmonths < time(): # Last edited more than 6mo ago
|
||||
strtime = strftime("%%s %d %Y ", ttup)
|
||||
else:
|
||||
strtime = strftime("%%s %d %H:%M ", ttup)
|
||||
lsresult.append(strtime % (_MONTH_NAMES[ttup[1]],))
|
||||
|
||||
lsresult.append(name)
|
||||
return "".join(lsresult)
|
||||
|
||||
|
||||
__all__ = ["lsLine"]
|
||||
392
backend/lib/python3.12/site-packages/twisted/conch/manhole.py
Normal file
392
backend/lib/python3.12/site-packages/twisted/conch/manhole.py
Normal file
@@ -0,0 +1,392 @@
|
||||
# -*- test-case-name: twisted.conch.test.test_manhole -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Line-input oriented interactive interpreter loop.
|
||||
|
||||
Provides classes for handling Python source input and arbitrary output
|
||||
interactively from a Twisted application. Also included is syntax coloring
|
||||
code with support for VT102 terminals, control code handling (^C, ^D, ^Q),
|
||||
and reasonable handling of Deferreds.
|
||||
|
||||
@author: Jp Calderone
|
||||
"""
|
||||
|
||||
import code
|
||||
import sys
|
||||
import tokenize
|
||||
from io import BytesIO
|
||||
from traceback import format_exception
|
||||
from types import TracebackType
|
||||
from typing import Type
|
||||
|
||||
from twisted.conch import recvline
|
||||
from twisted.internet import defer
|
||||
from twisted.python.htmlizer import TokenPrinter
|
||||
from twisted.python.monkey import MonkeyPatcher
|
||||
|
||||
|
||||
class FileWrapper:
|
||||
"""
|
||||
Minimal write-file-like object.
|
||||
|
||||
Writes are translated into addOutput calls on an object passed to
|
||||
__init__. Newlines are also converted from network to local style.
|
||||
"""
|
||||
|
||||
softspace = 0
|
||||
state = "normal"
|
||||
|
||||
def __init__(self, o):
|
||||
self.o = o
|
||||
|
||||
def flush(self):
|
||||
pass
|
||||
|
||||
def write(self, data):
|
||||
self.o.addOutput(data.replace("\r\n", "\n"))
|
||||
|
||||
def writelines(self, lines):
|
||||
self.write("".join(lines))
|
||||
|
||||
|
||||
class ManholeInterpreter(code.InteractiveInterpreter):
|
||||
"""
|
||||
Interactive Interpreter with special output and Deferred support.
|
||||
|
||||
Aside from the features provided by L{code.InteractiveInterpreter}, this
|
||||
class captures sys.stdout output and redirects it to the appropriate
|
||||
location (the Manhole protocol instance). It also treats Deferreds
|
||||
which reach the top-level specially: each is formatted to the user with
|
||||
a unique identifier and a new callback and errback added to it, each of
|
||||
which will format the unique identifier and the result with which the
|
||||
Deferred fires and then pass it on to the next participant in the
|
||||
callback chain.
|
||||
"""
|
||||
|
||||
numDeferreds = 0
|
||||
|
||||
def __init__(self, handler, locals=None, filename="<console>"):
|
||||
code.InteractiveInterpreter.__init__(self, locals)
|
||||
self._pendingDeferreds = {}
|
||||
self.handler = handler
|
||||
self.filename = filename
|
||||
self.resetBuffer()
|
||||
|
||||
self.monkeyPatcher = MonkeyPatcher()
|
||||
self.monkeyPatcher.addPatch(sys, "displayhook", self.displayhook)
|
||||
self.monkeyPatcher.addPatch(sys, "excepthook", self.excepthook)
|
||||
self.monkeyPatcher.addPatch(sys, "stdout", FileWrapper(self.handler))
|
||||
|
||||
def resetBuffer(self):
|
||||
"""
|
||||
Reset the input buffer.
|
||||
"""
|
||||
self.buffer = []
|
||||
|
||||
def push(self, line):
|
||||
"""
|
||||
Push a line to the interpreter.
|
||||
|
||||
The line should not have a trailing newline; it may have
|
||||
internal newlines. The line is appended to a buffer and the
|
||||
interpreter's runsource() method is called with the
|
||||
concatenated contents of the buffer as source. If this
|
||||
indicates that the command was executed or invalid, the buffer
|
||||
is reset; otherwise, the command is incomplete, and the buffer
|
||||
is left as it was after the line was appended. The return
|
||||
value is 1 if more input is required, 0 if the line was dealt
|
||||
with in some way (this is the same as runsource()).
|
||||
|
||||
@param line: line of text
|
||||
@type line: L{bytes}
|
||||
@return: L{bool} from L{code.InteractiveInterpreter.runsource}
|
||||
"""
|
||||
self.buffer.append(line)
|
||||
source = b"\n".join(self.buffer)
|
||||
source = source.decode("utf-8")
|
||||
more = self.runsource(source, self.filename)
|
||||
if not more:
|
||||
self.resetBuffer()
|
||||
return more
|
||||
|
||||
def runcode(self, *a, **kw):
|
||||
with self.monkeyPatcher:
|
||||
code.InteractiveInterpreter.runcode(self, *a, **kw)
|
||||
|
||||
def excepthook(
|
||||
self,
|
||||
excType: Type[BaseException],
|
||||
excValue: BaseException,
|
||||
excTraceback: TracebackType,
|
||||
) -> None:
|
||||
"""
|
||||
Format exception tracebacks and write them to the output handler.
|
||||
"""
|
||||
code_obj = excTraceback.tb_frame.f_code
|
||||
if code_obj.co_filename == code.__file__ and code_obj.co_name == "runcode":
|
||||
traceback = excTraceback.tb_next
|
||||
else:
|
||||
# Workaround for https://github.com/python/cpython/issues/122478,
|
||||
# present e.g. in Python 3.12.6:
|
||||
traceback = excTraceback
|
||||
lines = format_exception(excType, excValue, traceback)
|
||||
self.write("".join(lines))
|
||||
|
||||
def displayhook(self, obj):
|
||||
self.locals["_"] = obj
|
||||
if isinstance(obj, defer.Deferred):
|
||||
# XXX Ick, where is my "hasFired()" interface?
|
||||
if hasattr(obj, "result"):
|
||||
self.write(repr(obj))
|
||||
elif id(obj) in self._pendingDeferreds:
|
||||
self.write("<Deferred #%d>" % (self._pendingDeferreds[id(obj)][0],))
|
||||
else:
|
||||
d = self._pendingDeferreds
|
||||
k = self.numDeferreds
|
||||
d[id(obj)] = (k, obj)
|
||||
self.numDeferreds += 1
|
||||
obj.addCallbacks(
|
||||
self._cbDisplayDeferred,
|
||||
self._ebDisplayDeferred,
|
||||
callbackArgs=(k, obj),
|
||||
errbackArgs=(k, obj),
|
||||
)
|
||||
self.write("<Deferred #%d>" % (k,))
|
||||
elif obj is not None:
|
||||
self.write(repr(obj))
|
||||
|
||||
def _cbDisplayDeferred(self, result, k, obj):
|
||||
self.write("Deferred #%d called back: %r" % (k, result), True)
|
||||
del self._pendingDeferreds[id(obj)]
|
||||
return result
|
||||
|
||||
def _ebDisplayDeferred(self, failure, k, obj):
|
||||
self.write("Deferred #%d failed: %r" % (k, failure.getErrorMessage()), True)
|
||||
del self._pendingDeferreds[id(obj)]
|
||||
return failure
|
||||
|
||||
def write(self, data, isAsync=None):
|
||||
self.handler.addOutput(data, isAsync)
|
||||
|
||||
|
||||
CTRL_C = b"\x03"
|
||||
CTRL_D = b"\x04"
|
||||
CTRL_BACKSLASH = b"\x1c"
|
||||
CTRL_L = b"\x0c"
|
||||
CTRL_A = b"\x01"
|
||||
CTRL_E = b"\x05"
|
||||
|
||||
|
||||
class Manhole(recvline.HistoricRecvLine):
|
||||
r"""
|
||||
Mediator between a fancy line source and an interactive interpreter.
|
||||
|
||||
This accepts lines from its transport and passes them on to a
|
||||
L{ManholeInterpreter}. Control commands (^C, ^D, ^\) are also handled
|
||||
with something approximating their normal terminal-mode behavior. It
|
||||
can optionally be constructed with a dict which will be used as the
|
||||
local namespace for any code executed.
|
||||
"""
|
||||
|
||||
namespace = None
|
||||
|
||||
def __init__(self, namespace=None):
|
||||
recvline.HistoricRecvLine.__init__(self)
|
||||
if namespace is not None:
|
||||
self.namespace = namespace.copy()
|
||||
|
||||
def connectionMade(self):
|
||||
recvline.HistoricRecvLine.connectionMade(self)
|
||||
self.interpreter = ManholeInterpreter(self, self.namespace)
|
||||
self.keyHandlers[CTRL_C] = self.handle_INT
|
||||
self.keyHandlers[CTRL_D] = self.handle_EOF
|
||||
self.keyHandlers[CTRL_L] = self.handle_FF
|
||||
self.keyHandlers[CTRL_A] = self.handle_HOME
|
||||
self.keyHandlers[CTRL_E] = self.handle_END
|
||||
self.keyHandlers[CTRL_BACKSLASH] = self.handle_QUIT
|
||||
|
||||
def handle_INT(self):
|
||||
"""
|
||||
Handle ^C as an interrupt keystroke by resetting the current input
|
||||
variables to their initial state.
|
||||
"""
|
||||
self.pn = 0
|
||||
self.lineBuffer = []
|
||||
self.lineBufferIndex = 0
|
||||
self.interpreter.resetBuffer()
|
||||
|
||||
self.terminal.nextLine()
|
||||
self.terminal.write(b"KeyboardInterrupt")
|
||||
self.terminal.nextLine()
|
||||
self.terminal.write(self.ps[self.pn])
|
||||
|
||||
def handle_EOF(self):
|
||||
if self.lineBuffer:
|
||||
self.terminal.write(b"\a")
|
||||
else:
|
||||
self.handle_QUIT()
|
||||
|
||||
def handle_FF(self):
|
||||
"""
|
||||
Handle a 'form feed' byte - generally used to request a screen
|
||||
refresh/redraw.
|
||||
"""
|
||||
self.terminal.eraseDisplay()
|
||||
self.terminal.cursorHome()
|
||||
self.drawInputLine()
|
||||
|
||||
def handle_QUIT(self):
|
||||
self.terminal.loseConnection()
|
||||
|
||||
def _needsNewline(self):
|
||||
w = self.terminal.lastWrite
|
||||
return not w.endswith(b"\n") and not w.endswith(b"\x1bE")
|
||||
|
||||
def addOutput(self, data, isAsync=None):
|
||||
if isAsync:
|
||||
self.terminal.eraseLine()
|
||||
self.terminal.cursorBackward(len(self.lineBuffer) + len(self.ps[self.pn]))
|
||||
|
||||
self.terminal.write(data)
|
||||
|
||||
if isAsync:
|
||||
if self._needsNewline():
|
||||
self.terminal.nextLine()
|
||||
|
||||
self.terminal.write(self.ps[self.pn])
|
||||
|
||||
if self.lineBuffer:
|
||||
oldBuffer = self.lineBuffer
|
||||
self.lineBuffer = []
|
||||
self.lineBufferIndex = 0
|
||||
|
||||
self._deliverBuffer(oldBuffer)
|
||||
|
||||
def lineReceived(self, line):
|
||||
more = self.interpreter.push(line)
|
||||
self.pn = bool(more)
|
||||
if self._needsNewline():
|
||||
self.terminal.nextLine()
|
||||
self.terminal.write(self.ps[self.pn])
|
||||
|
||||
|
||||
class VT102Writer:
|
||||
"""
|
||||
Colorizer for Python tokens.
|
||||
|
||||
A series of tokens are written to instances of this object. Each is
|
||||
colored in a particular way. The final line of the result of this is
|
||||
generally added to the output.
|
||||
"""
|
||||
|
||||
typeToColor = {
|
||||
"identifier": b"\x1b[31m",
|
||||
"keyword": b"\x1b[32m",
|
||||
"parameter": b"\x1b[33m",
|
||||
"variable": b"\x1b[1;33m",
|
||||
"string": b"\x1b[35m",
|
||||
"number": b"\x1b[36m",
|
||||
"op": b"\x1b[37m",
|
||||
}
|
||||
|
||||
normalColor = b"\x1b[0m"
|
||||
|
||||
def __init__(self):
|
||||
self.written = []
|
||||
|
||||
def color(self, type):
|
||||
r = self.typeToColor.get(type, b"")
|
||||
return r
|
||||
|
||||
def write(self, token, type=None):
|
||||
if token and token != b"\r":
|
||||
c = self.color(type)
|
||||
if c:
|
||||
self.written.append(c)
|
||||
self.written.append(token)
|
||||
if c:
|
||||
self.written.append(self.normalColor)
|
||||
|
||||
def __bytes__(self):
|
||||
s = b"".join(self.written)
|
||||
return s.strip(b"\n").splitlines()[-1]
|
||||
|
||||
|
||||
def lastColorizedLine(source):
|
||||
"""
|
||||
Tokenize and colorize the given Python source.
|
||||
|
||||
Returns a VT102-format colorized version of the last line of C{source}.
|
||||
|
||||
@param source: Python source code
|
||||
@type source: L{str} or L{bytes}
|
||||
@return: L{bytes} of colorized source
|
||||
"""
|
||||
if not isinstance(source, bytes):
|
||||
source = source.encode("utf-8")
|
||||
w = VT102Writer()
|
||||
p = TokenPrinter(w.write).printtoken
|
||||
s = BytesIO(source)
|
||||
|
||||
for token in tokenize.tokenize(s.readline):
|
||||
(tokenType, string, start, end, line) = token
|
||||
p(tokenType, string, start, end, line)
|
||||
|
||||
return bytes(w)
|
||||
|
||||
|
||||
class ColoredManhole(Manhole):
|
||||
"""
|
||||
A REPL which syntax colors input as users type it.
|
||||
"""
|
||||
|
||||
def getSource(self):
|
||||
"""
|
||||
Return a string containing the currently entered source.
|
||||
|
||||
This is only the code which will be considered for execution
|
||||
next.
|
||||
"""
|
||||
return b"\n".join(self.interpreter.buffer) + b"\n" + b"".join(self.lineBuffer)
|
||||
|
||||
def characterReceived(self, ch, moreCharactersComing):
|
||||
if self.mode == "insert":
|
||||
self.lineBuffer.insert(self.lineBufferIndex, ch)
|
||||
else:
|
||||
self.lineBuffer[self.lineBufferIndex : self.lineBufferIndex + 1] = [ch]
|
||||
self.lineBufferIndex += 1
|
||||
|
||||
if moreCharactersComing:
|
||||
# Skip it all, we'll get called with another character in
|
||||
# like 2 femtoseconds.
|
||||
return
|
||||
|
||||
if ch == b" ":
|
||||
# Don't bother to try to color whitespace
|
||||
self.terminal.write(ch)
|
||||
return
|
||||
|
||||
source = self.getSource()
|
||||
|
||||
# Try to write some junk
|
||||
try:
|
||||
coloredLine = lastColorizedLine(source)
|
||||
except tokenize.TokenError:
|
||||
# We couldn't do it. Strange. Oh well, just add the character.
|
||||
self.terminal.write(ch)
|
||||
else:
|
||||
# Success! Clear the source on this line.
|
||||
self.terminal.eraseLine()
|
||||
self.terminal.cursorBackward(
|
||||
len(self.lineBuffer) + len(self.ps[self.pn]) - 1
|
||||
)
|
||||
|
||||
# And write a new, colorized one.
|
||||
self.terminal.write(self.ps[self.pn] + coloredLine)
|
||||
|
||||
# And move the cursor to where it belongs
|
||||
n = len(self.lineBuffer) - self.lineBufferIndex
|
||||
if n:
|
||||
self.terminal.cursorBackward(n)
|
||||
@@ -0,0 +1,148 @@
|
||||
# -*- test-case-name: twisted.conch.test.test_manhole -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
insults/SSH integration support.
|
||||
|
||||
@author: Jp Calderone
|
||||
"""
|
||||
|
||||
from typing import Dict
|
||||
|
||||
from zope.interface import implementer
|
||||
|
||||
from twisted.conch import avatar, error as econch, interfaces as iconch
|
||||
from twisted.conch.insults import insults
|
||||
from twisted.conch.ssh import factory, session
|
||||
from twisted.python import components
|
||||
|
||||
|
||||
class _Glue:
|
||||
"""
|
||||
A feeble class for making one attribute look like another.
|
||||
|
||||
This should be replaced with a real class at some point, probably.
|
||||
Try not to write new code that uses it.
|
||||
"""
|
||||
|
||||
def __init__(self, **kw):
|
||||
self.__dict__.update(kw)
|
||||
|
||||
def __getattr__(self, name):
|
||||
raise AttributeError(self.name, "has no attribute", name)
|
||||
|
||||
|
||||
class TerminalSessionTransport:
|
||||
def __init__(self, proto, chainedProtocol, avatar, width, height):
|
||||
self.proto = proto
|
||||
self.avatar = avatar
|
||||
self.chainedProtocol = chainedProtocol
|
||||
|
||||
protoSession = self.proto.session
|
||||
|
||||
self.proto.makeConnection(
|
||||
_Glue(
|
||||
write=self.chainedProtocol.dataReceived,
|
||||
loseConnection=lambda: avatar.conn.sendClose(protoSession),
|
||||
name="SSH Proto Transport",
|
||||
)
|
||||
)
|
||||
|
||||
def loseConnection():
|
||||
self.proto.loseConnection()
|
||||
|
||||
self.chainedProtocol.makeConnection(
|
||||
_Glue(
|
||||
write=self.proto.write,
|
||||
loseConnection=loseConnection,
|
||||
name="Chained Proto Transport",
|
||||
)
|
||||
)
|
||||
|
||||
# XXX TODO
|
||||
# chainedProtocol is supposed to be an ITerminalTransport,
|
||||
# maybe. That means perhaps its terminalProtocol attribute is
|
||||
# an ITerminalProtocol, it could be. So calling terminalSize
|
||||
# on that should do the right thing But it'd be nice to clean
|
||||
# this bit up.
|
||||
self.chainedProtocol.terminalProtocol.terminalSize(width, height)
|
||||
|
||||
|
||||
@implementer(iconch.ISession)
|
||||
class TerminalSession(components.Adapter):
|
||||
transportFactory = TerminalSessionTransport
|
||||
chainedProtocolFactory = insults.ServerProtocol
|
||||
|
||||
def getPty(self, term, windowSize, attrs):
|
||||
self.height, self.width = windowSize[:2]
|
||||
|
||||
def openShell(self, proto):
|
||||
self.transportFactory(
|
||||
proto,
|
||||
self.chainedProtocolFactory(),
|
||||
iconch.IConchUser(self.original),
|
||||
self.width,
|
||||
self.height,
|
||||
)
|
||||
|
||||
def execCommand(self, proto, cmd):
|
||||
raise econch.ConchError("Cannot execute commands")
|
||||
|
||||
def windowChanged(self, newWindowSize):
|
||||
# ISession.windowChanged
|
||||
raise NotImplementedError("Unimplemented: TerminalSession.windowChanged")
|
||||
|
||||
def eofReceived(self):
|
||||
# ISession.eofReceived
|
||||
raise NotImplementedError("Unimplemented: TerminalSession.eofReceived")
|
||||
|
||||
def closed(self):
|
||||
# ISession.closed
|
||||
pass
|
||||
|
||||
|
||||
class TerminalUser(avatar.ConchUser, components.Adapter):
|
||||
def __init__(self, original, avatarId):
|
||||
components.Adapter.__init__(self, original)
|
||||
avatar.ConchUser.__init__(self)
|
||||
self.channelLookup[b"session"] = session.SSHSession
|
||||
|
||||
|
||||
class TerminalRealm:
|
||||
userFactory = TerminalUser
|
||||
sessionFactory = TerminalSession
|
||||
|
||||
transportFactory = TerminalSessionTransport
|
||||
chainedProtocolFactory = insults.ServerProtocol
|
||||
|
||||
def _getAvatar(self, avatarId):
|
||||
comp = components.Componentized()
|
||||
user = self.userFactory(comp, avatarId)
|
||||
sess = self.sessionFactory(comp)
|
||||
|
||||
sess.transportFactory = self.transportFactory
|
||||
sess.chainedProtocolFactory = self.chainedProtocolFactory
|
||||
|
||||
comp.setComponent(iconch.IConchUser, user)
|
||||
comp.setComponent(iconch.ISession, sess)
|
||||
|
||||
return user
|
||||
|
||||
def __init__(self, transportFactory=None):
|
||||
if transportFactory is not None:
|
||||
self.transportFactory = transportFactory
|
||||
|
||||
def requestAvatar(self, avatarId, mind, *interfaces):
|
||||
for i in interfaces:
|
||||
if i is iconch.IConchUser:
|
||||
return (iconch.IConchUser, self._getAvatar(avatarId), lambda: None)
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class ConchFactory(factory.SSHFactory):
|
||||
publicKeys: Dict[bytes, bytes] = {}
|
||||
privateKeys: Dict[bytes, bytes] = {}
|
||||
|
||||
def __init__(self, portal):
|
||||
self.portal = portal
|
||||
@@ -0,0 +1,180 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
TAP plugin for creating telnet- and ssh-accessible manhole servers.
|
||||
|
||||
@author: Jp Calderone
|
||||
"""
|
||||
|
||||
from zope.interface import implementer
|
||||
|
||||
from twisted.application import service, strports
|
||||
from twisted.conch import manhole, manhole_ssh, telnet
|
||||
from twisted.conch.insults import insults
|
||||
from twisted.conch.ssh import keys
|
||||
from twisted.cred import checkers, portal
|
||||
from twisted.internet import protocol
|
||||
from twisted.python import filepath, usage
|
||||
|
||||
|
||||
class makeTelnetProtocol:
|
||||
def __init__(self, portal):
|
||||
self.portal = portal
|
||||
|
||||
def __call__(self):
|
||||
auth = telnet.AuthenticatingTelnetProtocol
|
||||
args = (self.portal,)
|
||||
return telnet.TelnetTransport(auth, *args)
|
||||
|
||||
|
||||
class chainedProtocolFactory:
|
||||
def __init__(self, namespace):
|
||||
self.namespace = namespace
|
||||
|
||||
def __call__(self):
|
||||
return insults.ServerProtocol(manhole.ColoredManhole, self.namespace)
|
||||
|
||||
|
||||
@implementer(portal.IRealm)
|
||||
class _StupidRealm:
|
||||
def __init__(self, proto, *a, **kw):
|
||||
self.protocolFactory = proto
|
||||
self.protocolArgs = a
|
||||
self.protocolKwArgs = kw
|
||||
|
||||
def requestAvatar(self, avatarId, *interfaces):
|
||||
if telnet.ITelnetProtocol in interfaces:
|
||||
return (
|
||||
telnet.ITelnetProtocol,
|
||||
self.protocolFactory(*self.protocolArgs, **self.protocolKwArgs),
|
||||
lambda: None,
|
||||
)
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class Options(usage.Options):
|
||||
optParameters = [
|
||||
[
|
||||
"telnetPort",
|
||||
"t",
|
||||
None,
|
||||
(
|
||||
"strports description of the address on which to listen for telnet "
|
||||
"connections"
|
||||
),
|
||||
],
|
||||
[
|
||||
"sshPort",
|
||||
"s",
|
||||
None,
|
||||
(
|
||||
"strports description of the address on which to listen for ssh "
|
||||
"connections"
|
||||
),
|
||||
],
|
||||
[
|
||||
"passwd",
|
||||
"p",
|
||||
"/etc/passwd",
|
||||
"name of a passwd(5)-format username/password file",
|
||||
],
|
||||
[
|
||||
"sshKeyDir",
|
||||
None,
|
||||
"<USER DATA DIR>",
|
||||
"Directory where the autogenerated SSH key is kept.",
|
||||
],
|
||||
["sshKeyName", None, "server.key", "Filename of the autogenerated SSH key."],
|
||||
["sshKeySize", None, 4096, "Size of the automatically generated SSH key."],
|
||||
]
|
||||
|
||||
def __init__(self):
|
||||
usage.Options.__init__(self)
|
||||
self["namespace"] = None
|
||||
|
||||
def postOptions(self):
|
||||
if self["telnetPort"] is None and self["sshPort"] is None:
|
||||
raise usage.UsageError(
|
||||
"At least one of --telnetPort and --sshPort must be specified"
|
||||
)
|
||||
|
||||
|
||||
def makeService(options):
|
||||
"""
|
||||
Create a manhole server service.
|
||||
|
||||
@type options: L{dict}
|
||||
@param options: A mapping describing the configuration of
|
||||
the desired service. Recognized key/value pairs are::
|
||||
|
||||
"telnetPort": strports description of the address on which
|
||||
to listen for telnet connections. If None,
|
||||
no telnet service will be started.
|
||||
|
||||
"sshPort": strports description of the address on which to
|
||||
listen for ssh connections. If None, no ssh
|
||||
service will be started.
|
||||
|
||||
"namespace": dictionary containing desired initial locals
|
||||
for manhole connections. If None, an empty
|
||||
dictionary will be used.
|
||||
|
||||
"passwd": Name of a passwd(5)-format username/password file.
|
||||
|
||||
"sshKeyDir": The folder that the SSH server key will be kept in.
|
||||
|
||||
"sshKeyName": The filename of the key.
|
||||
|
||||
"sshKeySize": The size of the key, in bits. Default is 4096.
|
||||
|
||||
@rtype: L{twisted.application.service.IService}
|
||||
@return: A manhole service.
|
||||
"""
|
||||
svc = service.MultiService()
|
||||
|
||||
namespace = options["namespace"]
|
||||
if namespace is None:
|
||||
namespace = {}
|
||||
|
||||
checker = checkers.FilePasswordDB(options["passwd"])
|
||||
|
||||
if options["telnetPort"]:
|
||||
telnetRealm = _StupidRealm(
|
||||
telnet.TelnetBootstrapProtocol,
|
||||
insults.ServerProtocol,
|
||||
manhole.ColoredManhole,
|
||||
namespace,
|
||||
)
|
||||
|
||||
telnetPortal = portal.Portal(telnetRealm, [checker])
|
||||
|
||||
telnetFactory = protocol.ServerFactory()
|
||||
telnetFactory.protocol = makeTelnetProtocol(telnetPortal)
|
||||
telnetService = strports.service(options["telnetPort"], telnetFactory)
|
||||
telnetService.setServiceParent(svc)
|
||||
|
||||
if options["sshPort"]:
|
||||
sshRealm = manhole_ssh.TerminalRealm()
|
||||
sshRealm.chainedProtocolFactory = chainedProtocolFactory(namespace)
|
||||
|
||||
sshPortal = portal.Portal(sshRealm, [checker])
|
||||
sshFactory = manhole_ssh.ConchFactory(sshPortal)
|
||||
|
||||
if options["sshKeyDir"] != "<USER DATA DIR>":
|
||||
keyDir = options["sshKeyDir"]
|
||||
else:
|
||||
from twisted.python._appdirs import getDataDirectory
|
||||
|
||||
keyDir = getDataDirectory()
|
||||
|
||||
keyLocation = filepath.FilePath(keyDir).child(options["sshKeyName"])
|
||||
|
||||
sshKey = keys._getPersistentRSAKey(keyLocation, int(options["sshKeySize"]))
|
||||
sshFactory.publicKeys[b"ssh-rsa"] = sshKey
|
||||
sshFactory.privateKeys[b"ssh-rsa"] = sshKey
|
||||
|
||||
sshService = strports.service(options["sshPort"], sshFactory)
|
||||
sshService.setServiceParent(svc)
|
||||
|
||||
return svc
|
||||
54
backend/lib/python3.12/site-packages/twisted/conch/mixin.py
Normal file
54
backend/lib/python3.12/site-packages/twisted/conch/mixin.py
Normal file
@@ -0,0 +1,54 @@
|
||||
# -*- test-case-name: twisted.conch.test.test_mixin -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Experimental optimization
|
||||
|
||||
This module provides a single mixin class which allows protocols to
|
||||
collapse numerous small writes into a single larger one.
|
||||
|
||||
@author: Jp Calderone
|
||||
"""
|
||||
|
||||
from twisted.internet import reactor
|
||||
|
||||
|
||||
class BufferingMixin:
|
||||
"""
|
||||
Mixin which adds write buffering.
|
||||
"""
|
||||
|
||||
_delayedWriteCall = None
|
||||
data = None
|
||||
|
||||
DELAY = 0.0
|
||||
|
||||
def schedule(self):
|
||||
return reactor.callLater(self.DELAY, self.flush)
|
||||
|
||||
def reschedule(self, token):
|
||||
token.reset(self.DELAY)
|
||||
|
||||
def write(self, data):
|
||||
"""
|
||||
Buffer some bytes to be written soon.
|
||||
|
||||
Every call to this function delays the real write by C{self.DELAY}
|
||||
seconds. When the delay expires, all collected bytes are written
|
||||
to the underlying transport using L{ITransport.writeSequence}.
|
||||
"""
|
||||
if self._delayedWriteCall is None:
|
||||
self.data = []
|
||||
self._delayedWriteCall = self.schedule()
|
||||
else:
|
||||
self.reschedule(self._delayedWriteCall)
|
||||
self.data.append(data)
|
||||
|
||||
def flush(self):
|
||||
"""
|
||||
Flush the buffer immediately.
|
||||
"""
|
||||
self._delayedWriteCall = None
|
||||
self.transport.writeSequence(self.data)
|
||||
self.data = None
|
||||
1
backend/lib/python3.12/site-packages/twisted/conch/newsfragments/.gitignore
vendored
Normal file
1
backend/lib/python3.12/site-packages/twisted/conch/newsfragments/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
!.gitignore
|
||||
@@ -0,0 +1,10 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
#
|
||||
|
||||
"""
|
||||
Support for OpenSSH configuration files.
|
||||
|
||||
Maintainer: Paul Swartz
|
||||
"""
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,74 @@
|
||||
# -*- test-case-name: twisted.conch.test.test_openssh_compat -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Factory for reading openssh configuration files: public keys, private keys, and
|
||||
moduli file.
|
||||
"""
|
||||
|
||||
import errno
|
||||
import os
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
|
||||
from twisted.conch.openssh_compat import primes
|
||||
from twisted.conch.ssh import common, factory, keys
|
||||
from twisted.python.util import runAsEffectiveUser
|
||||
|
||||
|
||||
class OpenSSHFactory(factory.SSHFactory):
|
||||
dataRoot = "/usr/local/etc"
|
||||
# For openbsd which puts moduli in a different directory from keys.
|
||||
moduliRoot = "/usr/local/etc"
|
||||
|
||||
def getPublicKeys(self):
|
||||
"""
|
||||
Return the server public keys.
|
||||
"""
|
||||
ks = {}
|
||||
for filename in os.listdir(self.dataRoot):
|
||||
if filename[:9] == "ssh_host_" and filename[-8:] == "_key.pub":
|
||||
try:
|
||||
k = keys.Key.fromFile(os.path.join(self.dataRoot, filename))
|
||||
t = common.getNS(k.blob())[0]
|
||||
ks[t] = k
|
||||
except Exception as e:
|
||||
self._log.error(
|
||||
"bad public key file {filename}: {error}",
|
||||
filename=filename,
|
||||
error=e,
|
||||
)
|
||||
return ks
|
||||
|
||||
def getPrivateKeys(self):
|
||||
"""
|
||||
Return the server private keys.
|
||||
"""
|
||||
privateKeys = {}
|
||||
for filename in os.listdir(self.dataRoot):
|
||||
if filename[:9] == "ssh_host_" and filename[-4:] == "_key":
|
||||
fullPath = os.path.join(self.dataRoot, filename)
|
||||
try:
|
||||
key = keys.Key.fromFile(fullPath)
|
||||
except OSError as e:
|
||||
if e.errno == errno.EACCES:
|
||||
# Not allowed, let's switch to root
|
||||
key = runAsEffectiveUser(0, 0, keys.Key.fromFile, fullPath)
|
||||
privateKeys[key.sshType()] = key
|
||||
else:
|
||||
raise
|
||||
except Exception as e:
|
||||
self._log.error(
|
||||
"bad public key file {filename}: {error}",
|
||||
filename=filename,
|
||||
error=e,
|
||||
)
|
||||
else:
|
||||
privateKeys[key.sshType()] = key
|
||||
return privateKeys
|
||||
|
||||
def getPrimes(self) -> Optional[Dict[int, List[Tuple[int, int]]]]:
|
||||
try:
|
||||
return primes.parseModuliFile(self.moduliRoot + "/moduli")
|
||||
except OSError:
|
||||
return None
|
||||
@@ -0,0 +1,31 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
#
|
||||
|
||||
"""
|
||||
Parsing for the moduli file, which contains Diffie-Hellman prime groups.
|
||||
|
||||
Maintainer: Paul Swartz
|
||||
"""
|
||||
|
||||
|
||||
from typing import Dict, List, Tuple
|
||||
|
||||
|
||||
def parseModuliFile(filename: str) -> Dict[int, List[Tuple[int, int]]]:
|
||||
with open(filename) as f:
|
||||
lines = f.readlines()
|
||||
primes: Dict[int, List[Tuple[int, int]]] = {}
|
||||
for l in lines:
|
||||
l = l.strip()
|
||||
if not l or l[0] == "#":
|
||||
continue
|
||||
tim, typ, tst, tri, sizestr, genstr, modstr = l.split()
|
||||
size = int(sizestr) + 1
|
||||
gen = int(genstr)
|
||||
mod = int(modstr, 16)
|
||||
if size not in primes:
|
||||
primes[size] = []
|
||||
primes[size].append((gen, mod))
|
||||
return primes
|
||||
569
backend/lib/python3.12/site-packages/twisted/conch/recvline.py
Normal file
569
backend/lib/python3.12/site-packages/twisted/conch/recvline.py
Normal file
@@ -0,0 +1,569 @@
|
||||
# -*- test-case-name: twisted.conch.test.test_recvline -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Basic line editing support.
|
||||
|
||||
@author: Jp Calderone
|
||||
"""
|
||||
|
||||
import string
|
||||
from typing import Dict
|
||||
|
||||
from zope.interface import implementer
|
||||
|
||||
from twisted.conch.insults import helper, insults
|
||||
from twisted.logger import Logger
|
||||
from twisted.python import reflect
|
||||
from twisted.python.compat import iterbytes
|
||||
|
||||
_counters: Dict[str, int] = {}
|
||||
|
||||
|
||||
class Logging:
|
||||
"""
|
||||
Wrapper which logs attribute lookups.
|
||||
|
||||
This was useful in debugging something, I guess. I forget what.
|
||||
It can probably be deleted or moved somewhere more appropriate.
|
||||
Nothing special going on here, really.
|
||||
"""
|
||||
|
||||
def __init__(self, original):
|
||||
self.original = original
|
||||
key = reflect.qual(original.__class__)
|
||||
count = _counters.get(key, 0)
|
||||
_counters[key] = count + 1
|
||||
self._logFile = open(key + "-" + str(count), "w")
|
||||
|
||||
def __str__(self) -> str:
|
||||
return str(super().__getattribute__("original"))
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return repr(super().__getattribute__("original"))
|
||||
|
||||
def __getattribute__(self, name):
|
||||
original = super().__getattribute__("original")
|
||||
logFile = super().__getattribute__("_logFile")
|
||||
logFile.write(name + "\n")
|
||||
return getattr(original, name)
|
||||
|
||||
|
||||
@implementer(insults.ITerminalTransport)
|
||||
class TransportSequence:
|
||||
"""
|
||||
An L{ITerminalTransport} implementation which forwards calls to
|
||||
one or more other L{ITerminalTransport}s.
|
||||
|
||||
This is a cheap way for servers to keep track of the state they
|
||||
expect the client to see, since all terminal manipulations can be
|
||||
send to the real client and to a terminal emulator that lives in
|
||||
the server process.
|
||||
"""
|
||||
|
||||
for keyID in (
|
||||
b"UP_ARROW",
|
||||
b"DOWN_ARROW",
|
||||
b"RIGHT_ARROW",
|
||||
b"LEFT_ARROW",
|
||||
b"HOME",
|
||||
b"INSERT",
|
||||
b"DELETE",
|
||||
b"END",
|
||||
b"PGUP",
|
||||
b"PGDN",
|
||||
b"F1",
|
||||
b"F2",
|
||||
b"F3",
|
||||
b"F4",
|
||||
b"F5",
|
||||
b"F6",
|
||||
b"F7",
|
||||
b"F8",
|
||||
b"F9",
|
||||
b"F10",
|
||||
b"F11",
|
||||
b"F12",
|
||||
):
|
||||
execBytes = keyID + b" = object()"
|
||||
execStr = execBytes.decode("ascii")
|
||||
exec(execStr)
|
||||
|
||||
TAB = b"\t"
|
||||
BACKSPACE = b"\x7f"
|
||||
|
||||
def __init__(self, *transports):
|
||||
assert transports, "Cannot construct a TransportSequence with no transports"
|
||||
self.transports = transports
|
||||
|
||||
for method in insults.ITerminalTransport:
|
||||
exec(
|
||||
"""\
|
||||
def %s(self, *a, **kw):
|
||||
for tpt in self.transports:
|
||||
result = tpt.%s(*a, **kw)
|
||||
return result
|
||||
"""
|
||||
% (method, method)
|
||||
)
|
||||
|
||||
def getHost(self):
|
||||
# ITransport.getHost
|
||||
raise NotImplementedError("Unimplemented: TransportSequence.getHost")
|
||||
|
||||
def getPeer(self):
|
||||
# ITransport.getPeer
|
||||
raise NotImplementedError("Unimplemented: TransportSequence.getPeer")
|
||||
|
||||
def loseConnection(self):
|
||||
# ITransport.loseConnection
|
||||
raise NotImplementedError("Unimplemented: TransportSequence.loseConnection")
|
||||
|
||||
def write(self, data):
|
||||
# ITransport.write
|
||||
raise NotImplementedError("Unimplemented: TransportSequence.write")
|
||||
|
||||
def writeSequence(self, data):
|
||||
# ITransport.writeSequence
|
||||
raise NotImplementedError("Unimplemented: TransportSequence.writeSequence")
|
||||
|
||||
def cursorUp(self, n=1):
|
||||
# ITerminalTransport.cursorUp
|
||||
raise NotImplementedError("Unimplemented: TransportSequence.cursorUp")
|
||||
|
||||
def cursorDown(self, n=1):
|
||||
# ITerminalTransport.cursorDown
|
||||
raise NotImplementedError("Unimplemented: TransportSequence.cursorDown")
|
||||
|
||||
def cursorForward(self, n=1):
|
||||
# ITerminalTransport.cursorForward
|
||||
raise NotImplementedError("Unimplemented: TransportSequence.cursorForward")
|
||||
|
||||
def cursorBackward(self, n=1):
|
||||
# ITerminalTransport.cursorBackward
|
||||
raise NotImplementedError("Unimplemented: TransportSequence.cursorBackward")
|
||||
|
||||
def cursorPosition(self, column, line):
|
||||
# ITerminalTransport.cursorPosition
|
||||
raise NotImplementedError("Unimplemented: TransportSequence.cursorPosition")
|
||||
|
||||
def cursorHome(self):
|
||||
# ITerminalTransport.cursorHome
|
||||
raise NotImplementedError("Unimplemented: TransportSequence.cursorHome")
|
||||
|
||||
def index(self):
|
||||
# ITerminalTransport.index
|
||||
raise NotImplementedError("Unimplemented: TransportSequence.index")
|
||||
|
||||
def reverseIndex(self):
|
||||
# ITerminalTransport.reverseIndex
|
||||
raise NotImplementedError("Unimplemented: TransportSequence.reverseIndex")
|
||||
|
||||
def nextLine(self):
|
||||
# ITerminalTransport.nextLine
|
||||
raise NotImplementedError("Unimplemented: TransportSequence.nextLine")
|
||||
|
||||
def saveCursor(self):
|
||||
# ITerminalTransport.saveCursor
|
||||
raise NotImplementedError("Unimplemented: TransportSequence.saveCursor")
|
||||
|
||||
def restoreCursor(self):
|
||||
# ITerminalTransport.restoreCursor
|
||||
raise NotImplementedError("Unimplemented: TransportSequence.restoreCursor")
|
||||
|
||||
def setModes(self, modes):
|
||||
# ITerminalTransport.setModes
|
||||
raise NotImplementedError("Unimplemented: TransportSequence.setModes")
|
||||
|
||||
def resetModes(self, mode):
|
||||
# ITerminalTransport.resetModes
|
||||
raise NotImplementedError("Unimplemented: TransportSequence.resetModes")
|
||||
|
||||
def setPrivateModes(self, modes):
|
||||
# ITerminalTransport.setPrivateModes
|
||||
raise NotImplementedError("Unimplemented: TransportSequence.setPrivateModes")
|
||||
|
||||
def resetPrivateModes(self, modes):
|
||||
# ITerminalTransport.resetPrivateModes
|
||||
raise NotImplementedError("Unimplemented: TransportSequence.resetPrivateModes")
|
||||
|
||||
def applicationKeypadMode(self):
|
||||
# ITerminalTransport.applicationKeypadMode
|
||||
raise NotImplementedError(
|
||||
"Unimplemented: TransportSequence.applicationKeypadMode"
|
||||
)
|
||||
|
||||
def numericKeypadMode(self):
|
||||
# ITerminalTransport.numericKeypadMode
|
||||
raise NotImplementedError("Unimplemented: TransportSequence.numericKeypadMode")
|
||||
|
||||
def selectCharacterSet(self, charSet, which):
|
||||
# ITerminalTransport.selectCharacterSet
|
||||
raise NotImplementedError("Unimplemented: TransportSequence.selectCharacterSet")
|
||||
|
||||
def shiftIn(self):
|
||||
# ITerminalTransport.shiftIn
|
||||
raise NotImplementedError("Unimplemented: TransportSequence.shiftIn")
|
||||
|
||||
def shiftOut(self):
|
||||
# ITerminalTransport.shiftOut
|
||||
raise NotImplementedError("Unimplemented: TransportSequence.shiftOut")
|
||||
|
||||
def singleShift2(self):
|
||||
# ITerminalTransport.singleShift2
|
||||
raise NotImplementedError("Unimplemented: TransportSequence.singleShift2")
|
||||
|
||||
def singleShift3(self):
|
||||
# ITerminalTransport.singleShift3
|
||||
raise NotImplementedError("Unimplemented: TransportSequence.singleShift3")
|
||||
|
||||
def selectGraphicRendition(self, *attributes):
|
||||
# ITerminalTransport.selectGraphicRendition
|
||||
raise NotImplementedError(
|
||||
"Unimplemented: TransportSequence.selectGraphicRendition"
|
||||
)
|
||||
|
||||
def horizontalTabulationSet(self):
|
||||
# ITerminalTransport.horizontalTabulationSet
|
||||
raise NotImplementedError(
|
||||
"Unimplemented: TransportSequence.horizontalTabulationSet"
|
||||
)
|
||||
|
||||
def tabulationClear(self):
|
||||
# ITerminalTransport.tabulationClear
|
||||
raise NotImplementedError("Unimplemented: TransportSequence.tabulationClear")
|
||||
|
||||
def tabulationClearAll(self):
|
||||
# ITerminalTransport.tabulationClearAll
|
||||
raise NotImplementedError("Unimplemented: TransportSequence.tabulationClearAll")
|
||||
|
||||
def doubleHeightLine(self, top=True):
|
||||
# ITerminalTransport.doubleHeightLine
|
||||
raise NotImplementedError("Unimplemented: TransportSequence.doubleHeightLine")
|
||||
|
||||
def singleWidthLine(self):
|
||||
# ITerminalTransport.singleWidthLine
|
||||
raise NotImplementedError("Unimplemented: TransportSequence.singleWidthLine")
|
||||
|
||||
def doubleWidthLine(self):
|
||||
# ITerminalTransport.doubleWidthLine
|
||||
raise NotImplementedError("Unimplemented: TransportSequence.doubleWidthLine")
|
||||
|
||||
def eraseToLineEnd(self):
|
||||
# ITerminalTransport.eraseToLineEnd
|
||||
raise NotImplementedError("Unimplemented: TransportSequence.eraseToLineEnd")
|
||||
|
||||
def eraseToLineBeginning(self):
|
||||
# ITerminalTransport.eraseToLineBeginning
|
||||
raise NotImplementedError(
|
||||
"Unimplemented: TransportSequence.eraseToLineBeginning"
|
||||
)
|
||||
|
||||
def eraseLine(self):
|
||||
# ITerminalTransport.eraseLine
|
||||
raise NotImplementedError("Unimplemented: TransportSequence.eraseLine")
|
||||
|
||||
def eraseToDisplayEnd(self):
|
||||
# ITerminalTransport.eraseToDisplayEnd
|
||||
raise NotImplementedError("Unimplemented: TransportSequence.eraseToDisplayEnd")
|
||||
|
||||
def eraseToDisplayBeginning(self):
|
||||
# ITerminalTransport.eraseToDisplayBeginning
|
||||
raise NotImplementedError(
|
||||
"Unimplemented: TransportSequence.eraseToDisplayBeginning"
|
||||
)
|
||||
|
||||
def eraseDisplay(self):
|
||||
# ITerminalTransport.eraseDisplay
|
||||
raise NotImplementedError("Unimplemented: TransportSequence.eraseDisplay")
|
||||
|
||||
def deleteCharacter(self, n=1):
|
||||
# ITerminalTransport.deleteCharacter
|
||||
raise NotImplementedError("Unimplemented: TransportSequence.deleteCharacter")
|
||||
|
||||
def insertLine(self, n=1):
|
||||
# ITerminalTransport.insertLine
|
||||
raise NotImplementedError("Unimplemented: TransportSequence.insertLine")
|
||||
|
||||
def deleteLine(self, n=1):
|
||||
# ITerminalTransport.deleteLine
|
||||
raise NotImplementedError("Unimplemented: TransportSequence.deleteLine")
|
||||
|
||||
def reportCursorPosition(self):
|
||||
# ITerminalTransport.reportCursorPosition
|
||||
raise NotImplementedError(
|
||||
"Unimplemented: TransportSequence.reportCursorPosition"
|
||||
)
|
||||
|
||||
def reset(self):
|
||||
# ITerminalTransport.reset
|
||||
raise NotImplementedError("Unimplemented: TransportSequence.reset")
|
||||
|
||||
def unhandledControlSequence(self, seq):
|
||||
# ITerminalTransport.unhandledControlSequence
|
||||
raise NotImplementedError(
|
||||
"Unimplemented: TransportSequence.unhandledControlSequence"
|
||||
)
|
||||
|
||||
|
||||
class LocalTerminalBufferMixin:
|
||||
"""
|
||||
A mixin for RecvLine subclasses which records the state of the terminal.
|
||||
|
||||
This is accomplished by performing all L{ITerminalTransport} operations on both
|
||||
the transport passed to makeConnection and an instance of helper.TerminalBuffer.
|
||||
|
||||
@ivar terminalCopy: A L{helper.TerminalBuffer} instance which efforts
|
||||
will be made to keep up to date with the actual terminal
|
||||
associated with this protocol instance.
|
||||
"""
|
||||
|
||||
def makeConnection(self, transport):
|
||||
self.terminalCopy = helper.TerminalBuffer()
|
||||
self.terminalCopy.connectionMade()
|
||||
return super().makeConnection(TransportSequence(transport, self.terminalCopy))
|
||||
|
||||
def __str__(self) -> str:
|
||||
return str(self.terminalCopy)
|
||||
|
||||
|
||||
class RecvLine(insults.TerminalProtocol):
|
||||
"""
|
||||
L{TerminalProtocol} which adds line editing features.
|
||||
|
||||
Clients will be prompted for lines of input with all the usual
|
||||
features: character echoing, left and right arrow support for
|
||||
moving the cursor to different areas of the line buffer, backspace
|
||||
and delete for removing characters, and insert for toggling
|
||||
between typeover and insert mode. Tabs will be expanded to enough
|
||||
spaces to move the cursor to the next tabstop (every four
|
||||
characters by default). Enter causes the line buffer to be
|
||||
cleared and the line to be passed to the lineReceived() method
|
||||
which, by default, does nothing. Subclasses are responsible for
|
||||
redrawing the input prompt (this will probably change).
|
||||
"""
|
||||
|
||||
width = 80
|
||||
height = 24
|
||||
|
||||
TABSTOP = 4
|
||||
|
||||
ps = (b">>> ", b"... ")
|
||||
pn = 0
|
||||
_printableChars = string.printable.encode("ascii")
|
||||
|
||||
_log = Logger()
|
||||
|
||||
def connectionMade(self):
|
||||
# A list containing the characters making up the current line
|
||||
self.lineBuffer = []
|
||||
|
||||
# A zero-based (wtf else?) index into self.lineBuffer.
|
||||
# Indicates the current cursor position.
|
||||
self.lineBufferIndex = 0
|
||||
|
||||
t = self.terminal
|
||||
# A map of keyIDs to bound instance methods.
|
||||
self.keyHandlers = {
|
||||
t.LEFT_ARROW: self.handle_LEFT,
|
||||
t.RIGHT_ARROW: self.handle_RIGHT,
|
||||
t.TAB: self.handle_TAB,
|
||||
# Both of these should not be necessary, but figuring out
|
||||
# which is necessary is a huge hassle.
|
||||
b"\r": self.handle_RETURN,
|
||||
b"\n": self.handle_RETURN,
|
||||
t.BACKSPACE: self.handle_BACKSPACE,
|
||||
t.DELETE: self.handle_DELETE,
|
||||
t.INSERT: self.handle_INSERT,
|
||||
t.HOME: self.handle_HOME,
|
||||
t.END: self.handle_END,
|
||||
}
|
||||
|
||||
self.initializeScreen()
|
||||
|
||||
def initializeScreen(self):
|
||||
# Hmm, state sucks. Oh well.
|
||||
# For now we will just take over the whole terminal.
|
||||
self.terminal.reset()
|
||||
self.terminal.write(self.ps[self.pn])
|
||||
# XXX Note: I would prefer to default to starting in insert
|
||||
# mode, however this does not seem to actually work! I do not
|
||||
# know why. This is probably of interest to implementors
|
||||
# subclassing RecvLine.
|
||||
|
||||
# XXX XXX Note: But the unit tests all expect the initial mode
|
||||
# to be insert right now. Fuck, there needs to be a way to
|
||||
# query the current mode or something.
|
||||
# self.setTypeoverMode()
|
||||
self.setInsertMode()
|
||||
|
||||
def currentLineBuffer(self):
|
||||
s = b"".join(self.lineBuffer)
|
||||
return s[: self.lineBufferIndex], s[self.lineBufferIndex :]
|
||||
|
||||
def setInsertMode(self):
|
||||
self.mode = "insert"
|
||||
self.terminal.setModes([insults.modes.IRM])
|
||||
|
||||
def setTypeoverMode(self):
|
||||
self.mode = "typeover"
|
||||
self.terminal.resetModes([insults.modes.IRM])
|
||||
|
||||
def drawInputLine(self):
|
||||
"""
|
||||
Write a line containing the current input prompt and the current line
|
||||
buffer at the current cursor position.
|
||||
"""
|
||||
self.terminal.write(self.ps[self.pn] + b"".join(self.lineBuffer))
|
||||
|
||||
def terminalSize(self, width, height):
|
||||
# XXX - Clear the previous input line, redraw it at the new
|
||||
# cursor position
|
||||
self.terminal.eraseDisplay()
|
||||
self.terminal.cursorHome()
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.drawInputLine()
|
||||
|
||||
def unhandledControlSequence(self, seq):
|
||||
pass
|
||||
|
||||
def keystrokeReceived(self, keyID, modifier):
|
||||
m = self.keyHandlers.get(keyID)
|
||||
if m is not None:
|
||||
m()
|
||||
elif keyID in self._printableChars:
|
||||
self.characterReceived(keyID, False)
|
||||
else:
|
||||
self._log.warn("Received unhandled keyID: {keyID!r}", keyID=keyID)
|
||||
|
||||
def characterReceived(self, ch, moreCharactersComing):
|
||||
if self.mode == "insert":
|
||||
self.lineBuffer.insert(self.lineBufferIndex, ch)
|
||||
else:
|
||||
self.lineBuffer[self.lineBufferIndex : self.lineBufferIndex + 1] = [ch]
|
||||
self.lineBufferIndex += 1
|
||||
self.terminal.write(ch)
|
||||
|
||||
def handle_TAB(self):
|
||||
n = self.TABSTOP - (len(self.lineBuffer) % self.TABSTOP)
|
||||
self.terminal.cursorForward(n)
|
||||
self.lineBufferIndex += n
|
||||
self.lineBuffer.extend(iterbytes(b" " * n))
|
||||
|
||||
def handle_LEFT(self):
|
||||
if self.lineBufferIndex > 0:
|
||||
self.lineBufferIndex -= 1
|
||||
self.terminal.cursorBackward()
|
||||
|
||||
def handle_RIGHT(self):
|
||||
if self.lineBufferIndex < len(self.lineBuffer):
|
||||
self.lineBufferIndex += 1
|
||||
self.terminal.cursorForward()
|
||||
|
||||
def handle_HOME(self):
|
||||
if self.lineBufferIndex:
|
||||
self.terminal.cursorBackward(self.lineBufferIndex)
|
||||
self.lineBufferIndex = 0
|
||||
|
||||
def handle_END(self):
|
||||
offset = len(self.lineBuffer) - self.lineBufferIndex
|
||||
if offset:
|
||||
self.terminal.cursorForward(offset)
|
||||
self.lineBufferIndex = len(self.lineBuffer)
|
||||
|
||||
def handle_BACKSPACE(self):
|
||||
if self.lineBufferIndex > 0:
|
||||
self.lineBufferIndex -= 1
|
||||
del self.lineBuffer[self.lineBufferIndex]
|
||||
self.terminal.cursorBackward()
|
||||
self.terminal.deleteCharacter()
|
||||
|
||||
def handle_DELETE(self):
|
||||
if self.lineBufferIndex < len(self.lineBuffer):
|
||||
del self.lineBuffer[self.lineBufferIndex]
|
||||
self.terminal.deleteCharacter()
|
||||
|
||||
def handle_RETURN(self):
|
||||
line = b"".join(self.lineBuffer)
|
||||
self.lineBuffer = []
|
||||
self.lineBufferIndex = 0
|
||||
self.terminal.nextLine()
|
||||
self.lineReceived(line)
|
||||
|
||||
def handle_INSERT(self):
|
||||
assert self.mode in ("typeover", "insert")
|
||||
if self.mode == "typeover":
|
||||
self.setInsertMode()
|
||||
else:
|
||||
self.setTypeoverMode()
|
||||
|
||||
def lineReceived(self, line):
|
||||
pass
|
||||
|
||||
|
||||
class HistoricRecvLine(RecvLine):
|
||||
"""
|
||||
L{TerminalProtocol} which adds both basic line-editing features and input history.
|
||||
|
||||
Everything supported by L{RecvLine} is also supported by this class. In addition, the
|
||||
up and down arrows traverse the input history. Each received line is automatically
|
||||
added to the end of the input history.
|
||||
"""
|
||||
|
||||
def connectionMade(self):
|
||||
RecvLine.connectionMade(self)
|
||||
|
||||
self.historyLines = []
|
||||
self.historyPosition = 0
|
||||
|
||||
t = self.terminal
|
||||
self.keyHandlers.update(
|
||||
{t.UP_ARROW: self.handle_UP, t.DOWN_ARROW: self.handle_DOWN}
|
||||
)
|
||||
|
||||
def currentHistoryBuffer(self):
|
||||
b = tuple(self.historyLines)
|
||||
return b[: self.historyPosition], b[self.historyPosition :]
|
||||
|
||||
def _deliverBuffer(self, buf):
|
||||
if buf:
|
||||
for ch in iterbytes(buf[:-1]):
|
||||
self.characterReceived(ch, True)
|
||||
self.characterReceived(buf[-1:], False)
|
||||
|
||||
def handle_UP(self):
|
||||
if self.lineBuffer and self.historyPosition == len(self.historyLines):
|
||||
self.historyLines.append(b"".join(self.lineBuffer))
|
||||
if self.historyPosition > 0:
|
||||
self.handle_HOME()
|
||||
self.terminal.eraseToLineEnd()
|
||||
|
||||
self.historyPosition -= 1
|
||||
self.lineBuffer = []
|
||||
|
||||
self._deliverBuffer(self.historyLines[self.historyPosition])
|
||||
|
||||
def handle_DOWN(self):
|
||||
if self.historyPosition < len(self.historyLines) - 1:
|
||||
self.handle_HOME()
|
||||
self.terminal.eraseToLineEnd()
|
||||
|
||||
self.historyPosition += 1
|
||||
self.lineBuffer = []
|
||||
|
||||
self._deliverBuffer(self.historyLines[self.historyPosition])
|
||||
else:
|
||||
self.handle_HOME()
|
||||
self.terminal.eraseToLineEnd()
|
||||
|
||||
self.historyPosition = len(self.historyLines)
|
||||
self.lineBuffer = []
|
||||
self.lineBufferIndex = 0
|
||||
|
||||
def handle_RETURN(self):
|
||||
if self.lineBuffer:
|
||||
self.historyLines.append(b"".join(self.lineBuffer))
|
||||
self.historyPosition = len(self.historyLines)
|
||||
return RecvLine.handle_RETURN(self)
|
||||
@@ -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()
|
||||
@@ -0,0 +1,10 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
#
|
||||
|
||||
"""
|
||||
An SSHv2 implementation for Twisted. Part of the Twisted.Conch package.
|
||||
|
||||
Maintainer: Paul Swartz
|
||||
"""
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
293
backend/lib/python3.12/site-packages/twisted/conch/ssh/_kex.py
Normal file
293
backend/lib/python3.12/site-packages/twisted/conch/ssh/_kex.py
Normal file
@@ -0,0 +1,293 @@
|
||||
# -*- test-case-name: twisted.conch.test.test_transport -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
SSH key exchange handling.
|
||||
"""
|
||||
|
||||
|
||||
from hashlib import sha1, sha256, sha384, sha512
|
||||
|
||||
from zope.interface import Attribute, Interface, implementer
|
||||
|
||||
from twisted.conch import error
|
||||
|
||||
|
||||
class _IKexAlgorithm(Interface):
|
||||
"""
|
||||
An L{_IKexAlgorithm} describes a key exchange algorithm.
|
||||
"""
|
||||
|
||||
preference = Attribute(
|
||||
"An L{int} giving the preference of the algorithm when negotiating "
|
||||
"key exchange. Algorithms with lower precedence values are more "
|
||||
"preferred."
|
||||
)
|
||||
|
||||
hashProcessor = Attribute(
|
||||
"A callable hash algorithm constructor (e.g. C{hashlib.sha256}) "
|
||||
"suitable for use with this key exchange algorithm."
|
||||
)
|
||||
|
||||
|
||||
class _IFixedGroupKexAlgorithm(_IKexAlgorithm):
|
||||
"""
|
||||
An L{_IFixedGroupKexAlgorithm} describes a key exchange algorithm with a
|
||||
fixed prime / generator group.
|
||||
"""
|
||||
|
||||
prime = Attribute(
|
||||
"An L{int} giving the prime number used in Diffie-Hellman key "
|
||||
"exchange, or L{None} if not applicable."
|
||||
)
|
||||
|
||||
generator = Attribute(
|
||||
"An L{int} giving the generator number used in Diffie-Hellman key "
|
||||
"exchange, or L{None} if not applicable. (This is not related to "
|
||||
"Python generator functions.)"
|
||||
)
|
||||
|
||||
|
||||
class _IEllipticCurveExchangeKexAlgorithm(_IKexAlgorithm):
|
||||
"""
|
||||
An L{_IEllipticCurveExchangeKexAlgorithm} describes a key exchange algorithm
|
||||
that uses an elliptic curve exchange between the client and server.
|
||||
"""
|
||||
|
||||
|
||||
class _IGroupExchangeKexAlgorithm(_IKexAlgorithm):
|
||||
"""
|
||||
An L{_IGroupExchangeKexAlgorithm} describes a key exchange algorithm
|
||||
that uses group exchange between the client and server.
|
||||
|
||||
A prime / generator group should be chosen at run time based on the
|
||||
requested size. See RFC 4419.
|
||||
"""
|
||||
|
||||
|
||||
@implementer(_IEllipticCurveExchangeKexAlgorithm)
|
||||
class _Curve25519SHA256:
|
||||
"""
|
||||
Elliptic Curve Key Exchange using Curve25519 and SHA256. Defined in
|
||||
U{https://datatracker.ietf.org/doc/draft-ietf-curdle-ssh-curves/}.
|
||||
"""
|
||||
|
||||
preference = 1
|
||||
hashProcessor = sha256
|
||||
|
||||
|
||||
@implementer(_IEllipticCurveExchangeKexAlgorithm)
|
||||
class _Curve25519SHA256LibSSH:
|
||||
"""
|
||||
As L{_Curve25519SHA256}, but with a pre-standardized algorithm name.
|
||||
"""
|
||||
|
||||
preference = 2
|
||||
hashProcessor = sha256
|
||||
|
||||
|
||||
@implementer(_IEllipticCurveExchangeKexAlgorithm)
|
||||
class _ECDH256:
|
||||
"""
|
||||
Elliptic Curve Key Exchange with SHA-256 as HASH. Defined in
|
||||
RFC 5656.
|
||||
|
||||
Note that C{ecdh-sha2-nistp256} takes priority over nistp384 or nistp512.
|
||||
This is the same priority from OpenSSH.
|
||||
|
||||
C{ecdh-sha2-nistp256} is considered preety good cryptography.
|
||||
If you need something better consider using C{curve25519-sha256}.
|
||||
"""
|
||||
|
||||
preference = 3
|
||||
hashProcessor = sha256
|
||||
|
||||
|
||||
@implementer(_IEllipticCurveExchangeKexAlgorithm)
|
||||
class _ECDH384:
|
||||
"""
|
||||
Elliptic Curve Key Exchange with SHA-384 as HASH. Defined in
|
||||
RFC 5656.
|
||||
"""
|
||||
|
||||
preference = 4
|
||||
hashProcessor = sha384
|
||||
|
||||
|
||||
@implementer(_IEllipticCurveExchangeKexAlgorithm)
|
||||
class _ECDH512:
|
||||
"""
|
||||
Elliptic Curve Key Exchange with SHA-512 as HASH. Defined in
|
||||
RFC 5656.
|
||||
"""
|
||||
|
||||
preference = 5
|
||||
hashProcessor = sha512
|
||||
|
||||
|
||||
@implementer(_IGroupExchangeKexAlgorithm)
|
||||
class _DHGroupExchangeSHA256:
|
||||
"""
|
||||
Diffie-Hellman Group and Key Exchange with SHA-256 as HASH. Defined in
|
||||
RFC 4419, 4.2.
|
||||
"""
|
||||
|
||||
preference = 6
|
||||
hashProcessor = sha256
|
||||
|
||||
|
||||
@implementer(_IGroupExchangeKexAlgorithm)
|
||||
class _DHGroupExchangeSHA1:
|
||||
"""
|
||||
Diffie-Hellman Group and Key Exchange with SHA-1 as HASH. Defined in
|
||||
RFC 4419, 4.1.
|
||||
"""
|
||||
|
||||
preference = 7
|
||||
hashProcessor = sha1
|
||||
|
||||
|
||||
@implementer(_IFixedGroupKexAlgorithm)
|
||||
class _DHGroup14SHA1:
|
||||
"""
|
||||
Diffie-Hellman key exchange with SHA-1 as HASH and Oakley Group 14
|
||||
(2048-bit MODP Group). Defined in RFC 4253, 8.2.
|
||||
"""
|
||||
|
||||
preference = 8
|
||||
hashProcessor = sha1
|
||||
# Diffie-Hellman primes from Oakley Group 14 (RFC 3526, 3).
|
||||
prime = int(
|
||||
"323170060713110073003389139264238282488179412411402391128420"
|
||||
"097514007417066343542226196894173635693471179017379097041917"
|
||||
"546058732091950288537589861856221532121754125149017745202702"
|
||||
"357960782362488842461894775876411059286460994117232454266225"
|
||||
"221932305409190376805242355191256797158701170010580558776510"
|
||||
"388618472802579760549035697325615261670813393617995413364765"
|
||||
"591603683178967290731783845896806396719009772021941686472258"
|
||||
"710314113364293195361934716365332097170774482279885885653692"
|
||||
"086452966360772502689555059283627511211740969729980684105543"
|
||||
"595848665832916421362182310789909994486524682624169720359118"
|
||||
"52507045361090559"
|
||||
)
|
||||
generator = 2
|
||||
|
||||
|
||||
# Which ECDH hash function to use is dependent on the size.
|
||||
_kexAlgorithms = {
|
||||
b"curve25519-sha256": _Curve25519SHA256(),
|
||||
b"curve25519-sha256@libssh.org": _Curve25519SHA256LibSSH(),
|
||||
b"diffie-hellman-group-exchange-sha256": _DHGroupExchangeSHA256(),
|
||||
b"diffie-hellman-group-exchange-sha1": _DHGroupExchangeSHA1(),
|
||||
b"diffie-hellman-group14-sha1": _DHGroup14SHA1(),
|
||||
b"ecdh-sha2-nistp256": _ECDH256(),
|
||||
b"ecdh-sha2-nistp384": _ECDH384(),
|
||||
b"ecdh-sha2-nistp521": _ECDH512(),
|
||||
}
|
||||
|
||||
|
||||
def getKex(kexAlgorithm):
|
||||
"""
|
||||
Get a description of a named key exchange algorithm.
|
||||
|
||||
@param kexAlgorithm: The key exchange algorithm name.
|
||||
@type kexAlgorithm: L{bytes}
|
||||
|
||||
@return: A description of the key exchange algorithm named by
|
||||
C{kexAlgorithm}.
|
||||
@rtype: L{_IKexAlgorithm}
|
||||
|
||||
@raises ConchError: if the key exchange algorithm is not found.
|
||||
"""
|
||||
if kexAlgorithm not in _kexAlgorithms:
|
||||
raise error.ConchError(f"Unsupported key exchange algorithm: {kexAlgorithm}")
|
||||
return _kexAlgorithms[kexAlgorithm]
|
||||
|
||||
|
||||
def isEllipticCurve(kexAlgorithm):
|
||||
"""
|
||||
Returns C{True} if C{kexAlgorithm} is an elliptic curve.
|
||||
|
||||
@param kexAlgorithm: The key exchange algorithm name.
|
||||
@type kexAlgorithm: C{str}
|
||||
|
||||
@return: C{True} if C{kexAlgorithm} is an elliptic curve,
|
||||
otherwise C{False}.
|
||||
@rtype: C{bool}
|
||||
"""
|
||||
return _IEllipticCurveExchangeKexAlgorithm.providedBy(getKex(kexAlgorithm))
|
||||
|
||||
|
||||
def isFixedGroup(kexAlgorithm):
|
||||
"""
|
||||
Returns C{True} if C{kexAlgorithm} has a fixed prime / generator group.
|
||||
|
||||
@param kexAlgorithm: The key exchange algorithm name.
|
||||
@type kexAlgorithm: L{bytes}
|
||||
|
||||
@return: C{True} if C{kexAlgorithm} has a fixed prime / generator group,
|
||||
otherwise C{False}.
|
||||
@rtype: L{bool}
|
||||
"""
|
||||
return _IFixedGroupKexAlgorithm.providedBy(getKex(kexAlgorithm))
|
||||
|
||||
|
||||
def getHashProcessor(kexAlgorithm):
|
||||
"""
|
||||
Get the hash algorithm callable to use in key exchange.
|
||||
|
||||
@param kexAlgorithm: The key exchange algorithm name.
|
||||
@type kexAlgorithm: L{bytes}
|
||||
|
||||
@return: A callable hash algorithm constructor (e.g. C{hashlib.sha256}).
|
||||
@rtype: C{callable}
|
||||
"""
|
||||
kex = getKex(kexAlgorithm)
|
||||
return kex.hashProcessor
|
||||
|
||||
|
||||
def getDHGeneratorAndPrime(kexAlgorithm):
|
||||
"""
|
||||
Get the generator and the prime to use in key exchange.
|
||||
|
||||
@param kexAlgorithm: The key exchange algorithm name.
|
||||
@type kexAlgorithm: L{bytes}
|
||||
|
||||
@return: A L{tuple} containing L{int} generator and L{int} prime.
|
||||
@rtype: L{tuple}
|
||||
"""
|
||||
kex = getKex(kexAlgorithm)
|
||||
return kex.generator, kex.prime
|
||||
|
||||
|
||||
def getSupportedKeyExchanges():
|
||||
"""
|
||||
Get a list of supported key exchange algorithm names in order of
|
||||
preference.
|
||||
|
||||
@return: A C{list} of supported key exchange algorithm names.
|
||||
@rtype: C{list} of L{bytes}
|
||||
"""
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives.asymmetric import ec
|
||||
|
||||
from twisted.conch.ssh.keys import _curveTable
|
||||
|
||||
backend = default_backend()
|
||||
kexAlgorithms = _kexAlgorithms.copy()
|
||||
for keyAlgorithm in list(kexAlgorithms):
|
||||
if keyAlgorithm.startswith(b"ecdh"):
|
||||
keyAlgorithmDsa = keyAlgorithm.replace(b"ecdh", b"ecdsa")
|
||||
supported = backend.elliptic_curve_exchange_algorithm_supported(
|
||||
ec.ECDH(), _curveTable[keyAlgorithmDsa]
|
||||
)
|
||||
elif keyAlgorithm.startswith(b"curve25519-sha256"):
|
||||
supported = backend.x25519_supported()
|
||||
else:
|
||||
supported = True
|
||||
if not supported:
|
||||
kexAlgorithms.pop(keyAlgorithm)
|
||||
return sorted(
|
||||
kexAlgorithms, key=lambda kexAlgorithm: kexAlgorithms[kexAlgorithm].preference
|
||||
)
|
||||
@@ -0,0 +1,43 @@
|
||||
# -*- test-case-name: twisted.conch.test.test_address -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Address object for SSH network connections.
|
||||
|
||||
Maintainer: Paul Swartz
|
||||
|
||||
@since: 12.1
|
||||
"""
|
||||
|
||||
|
||||
from zope.interface import implementer
|
||||
|
||||
from twisted.internet.interfaces import IAddress
|
||||
from twisted.python import util
|
||||
|
||||
|
||||
@implementer(IAddress)
|
||||
class SSHTransportAddress(util.FancyEqMixin):
|
||||
"""
|
||||
Object representing an SSH Transport endpoint.
|
||||
|
||||
This is used to ensure that any code inspecting this address and
|
||||
attempting to construct a similar connection based upon it is not
|
||||
mislead into creating a transport which is not similar to the one it is
|
||||
indicating.
|
||||
|
||||
@ivar address: An instance of an object which implements I{IAddress} to
|
||||
which this transport address is connected.
|
||||
"""
|
||||
|
||||
compareAttributes = ("address",)
|
||||
|
||||
def __init__(self, address):
|
||||
self.address = address
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"SSHTransportAddress({self.address!r})"
|
||||
|
||||
def __hash__(self):
|
||||
return hash(("SSH", self.address))
|
||||
278
backend/lib/python3.12/site-packages/twisted/conch/ssh/agent.py
Normal file
278
backend/lib/python3.12/site-packages/twisted/conch/ssh/agent.py
Normal file
@@ -0,0 +1,278 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Implements the SSH v2 key agent protocol. This protocol is documented in the
|
||||
SSH source code, in the file
|
||||
U{PROTOCOL.agent<http://www.openbsd.org/cgi-bin/cvsweb/src/usr.bin/ssh/PROTOCOL.agent>}.
|
||||
|
||||
Maintainer: Paul Swartz
|
||||
"""
|
||||
|
||||
|
||||
import struct
|
||||
|
||||
from twisted.conch.error import ConchError, MissingKeyStoreError
|
||||
from twisted.conch.ssh import keys
|
||||
from twisted.conch.ssh.common import NS, getMP, getNS
|
||||
from twisted.internet import defer, protocol
|
||||
|
||||
|
||||
class SSHAgentClient(protocol.Protocol):
|
||||
"""
|
||||
The client side of the SSH agent protocol. This is equivalent to
|
||||
ssh-add(1) and can be used with either ssh-agent(1) or the SSHAgentServer
|
||||
protocol, also in this package.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.buf = b""
|
||||
self.deferreds = []
|
||||
|
||||
def dataReceived(self, data):
|
||||
self.buf += data
|
||||
while 1:
|
||||
if len(self.buf) <= 4:
|
||||
return
|
||||
packLen = struct.unpack("!L", self.buf[:4])[0]
|
||||
if len(self.buf) < 4 + packLen:
|
||||
return
|
||||
packet, self.buf = self.buf[4 : 4 + packLen], self.buf[4 + packLen :]
|
||||
reqType = ord(packet[0:1])
|
||||
d = self.deferreds.pop(0)
|
||||
if reqType == AGENT_FAILURE:
|
||||
d.errback(ConchError("agent failure"))
|
||||
elif reqType == AGENT_SUCCESS:
|
||||
d.callback(b"")
|
||||
else:
|
||||
d.callback(packet)
|
||||
|
||||
def sendRequest(self, reqType, data):
|
||||
pack = struct.pack("!LB", len(data) + 1, reqType) + data
|
||||
self.transport.write(pack)
|
||||
d = defer.Deferred()
|
||||
self.deferreds.append(d)
|
||||
return d
|
||||
|
||||
def requestIdentities(self):
|
||||
"""
|
||||
@return: A L{Deferred} which will fire with a list of all keys found in
|
||||
the SSH agent. The list of keys is comprised of (public key blob,
|
||||
comment) tuples.
|
||||
"""
|
||||
d = self.sendRequest(AGENTC_REQUEST_IDENTITIES, b"")
|
||||
d.addCallback(self._cbRequestIdentities)
|
||||
return d
|
||||
|
||||
def _cbRequestIdentities(self, data):
|
||||
"""
|
||||
Unpack a collection of identities into a list of tuples comprised of
|
||||
public key blobs and comments.
|
||||
"""
|
||||
if ord(data[0:1]) != AGENT_IDENTITIES_ANSWER:
|
||||
raise ConchError("unexpected response: %i" % ord(data[0:1]))
|
||||
numKeys = struct.unpack("!L", data[1:5])[0]
|
||||
result = []
|
||||
data = data[5:]
|
||||
for i in range(numKeys):
|
||||
blob, data = getNS(data)
|
||||
comment, data = getNS(data)
|
||||
result.append((blob, comment))
|
||||
return result
|
||||
|
||||
def addIdentity(self, blob, comment=b""):
|
||||
"""
|
||||
Add a private key blob to the agent's collection of keys.
|
||||
"""
|
||||
req = blob
|
||||
req += NS(comment)
|
||||
return self.sendRequest(AGENTC_ADD_IDENTITY, req)
|
||||
|
||||
def signData(self, blob, data):
|
||||
"""
|
||||
Request that the agent sign the given C{data} with the private key
|
||||
which corresponds to the public key given by C{blob}. The private
|
||||
key should have been added to the agent already.
|
||||
|
||||
@type blob: L{bytes}
|
||||
@type data: L{bytes}
|
||||
@return: A L{Deferred} which fires with a signature for given data
|
||||
created with the given key.
|
||||
"""
|
||||
req = NS(blob)
|
||||
req += NS(data)
|
||||
req += b"\000\000\000\000" # flags
|
||||
return self.sendRequest(AGENTC_SIGN_REQUEST, req).addCallback(self._cbSignData)
|
||||
|
||||
def _cbSignData(self, data):
|
||||
if ord(data[0:1]) != AGENT_SIGN_RESPONSE:
|
||||
raise ConchError("unexpected data: %i" % ord(data[0:1]))
|
||||
signature = getNS(data[1:])[0]
|
||||
return signature
|
||||
|
||||
def removeIdentity(self, blob):
|
||||
"""
|
||||
Remove the private key corresponding to the public key in blob from the
|
||||
running agent.
|
||||
"""
|
||||
req = NS(blob)
|
||||
return self.sendRequest(AGENTC_REMOVE_IDENTITY, req)
|
||||
|
||||
def removeAllIdentities(self):
|
||||
"""
|
||||
Remove all keys from the running agent.
|
||||
"""
|
||||
return self.sendRequest(AGENTC_REMOVE_ALL_IDENTITIES, b"")
|
||||
|
||||
|
||||
class SSHAgentServer(protocol.Protocol):
|
||||
"""
|
||||
The server side of the SSH agent protocol. This is equivalent to
|
||||
ssh-agent(1) and can be used with either ssh-add(1) or the SSHAgentClient
|
||||
protocol, also in this package.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.buf = b""
|
||||
|
||||
def dataReceived(self, data):
|
||||
self.buf += data
|
||||
while 1:
|
||||
if len(self.buf) <= 4:
|
||||
return
|
||||
packLen = struct.unpack("!L", self.buf[:4])[0]
|
||||
if len(self.buf) < 4 + packLen:
|
||||
return
|
||||
packet, self.buf = self.buf[4 : 4 + packLen], self.buf[4 + packLen :]
|
||||
reqType = ord(packet[0:1])
|
||||
reqName = messages.get(reqType, None)
|
||||
if not reqName:
|
||||
self.sendResponse(AGENT_FAILURE, b"")
|
||||
else:
|
||||
f = getattr(self, "agentc_%s" % reqName)
|
||||
if getattr(self.factory, "keys", None) is None:
|
||||
self.sendResponse(AGENT_FAILURE, b"")
|
||||
raise MissingKeyStoreError()
|
||||
f(packet[1:])
|
||||
|
||||
def sendResponse(self, reqType, data):
|
||||
pack = struct.pack("!LB", len(data) + 1, reqType) + data
|
||||
self.transport.write(pack)
|
||||
|
||||
def agentc_REQUEST_IDENTITIES(self, data):
|
||||
"""
|
||||
Return all of the identities that have been added to the server
|
||||
"""
|
||||
assert data == b""
|
||||
numKeys = len(self.factory.keys)
|
||||
resp = []
|
||||
|
||||
resp.append(struct.pack("!L", numKeys))
|
||||
for key, comment in self.factory.keys.values():
|
||||
resp.append(NS(key.blob())) # yes, wrapped in an NS
|
||||
resp.append(NS(comment))
|
||||
self.sendResponse(AGENT_IDENTITIES_ANSWER, b"".join(resp))
|
||||
|
||||
def agentc_SIGN_REQUEST(self, data):
|
||||
"""
|
||||
Data is a structure with a reference to an already added key object and
|
||||
some data that the clients wants signed with that key. If the key
|
||||
object wasn't loaded, return AGENT_FAILURE, else return the signature.
|
||||
"""
|
||||
blob, data = getNS(data)
|
||||
if blob not in self.factory.keys:
|
||||
return self.sendResponse(AGENT_FAILURE, b"")
|
||||
signData, data = getNS(data)
|
||||
assert data == b"\000\000\000\000"
|
||||
self.sendResponse(
|
||||
AGENT_SIGN_RESPONSE, NS(self.factory.keys[blob][0].sign(signData))
|
||||
)
|
||||
|
||||
def agentc_ADD_IDENTITY(self, data):
|
||||
"""
|
||||
Adds a private key to the agent's collection of identities. On
|
||||
subsequent interactions, the private key can be accessed using only the
|
||||
corresponding public key.
|
||||
"""
|
||||
|
||||
# need to pre-read the key data so we can get past it to the comment string
|
||||
keyType, rest = getNS(data)
|
||||
if keyType == b"ssh-rsa":
|
||||
nmp = 6
|
||||
elif keyType == b"ssh-dss":
|
||||
nmp = 5
|
||||
else:
|
||||
raise keys.BadKeyError("unknown blob type: %s" % keyType)
|
||||
|
||||
rest = getMP(rest, nmp)[
|
||||
-1
|
||||
] # ignore the key data for now, we just want the comment
|
||||
comment, rest = getNS(rest) # the comment, tacked onto the end of the key blob
|
||||
|
||||
k = keys.Key.fromString(data, type="private_blob") # not wrapped in NS here
|
||||
self.factory.keys[k.blob()] = (k, comment)
|
||||
self.sendResponse(AGENT_SUCCESS, b"")
|
||||
|
||||
def agentc_REMOVE_IDENTITY(self, data):
|
||||
"""
|
||||
Remove a specific key from the agent's collection of identities.
|
||||
"""
|
||||
blob, _ = getNS(data)
|
||||
k = keys.Key.fromString(blob, type="blob")
|
||||
del self.factory.keys[k.blob()]
|
||||
self.sendResponse(AGENT_SUCCESS, b"")
|
||||
|
||||
def agentc_REMOVE_ALL_IDENTITIES(self, data):
|
||||
"""
|
||||
Remove all keys from the agent's collection of identities.
|
||||
"""
|
||||
assert data == b""
|
||||
self.factory.keys = {}
|
||||
self.sendResponse(AGENT_SUCCESS, b"")
|
||||
|
||||
# v1 messages that we ignore because we don't keep v1 keys
|
||||
# open-ssh sends both v1 and v2 commands, so we have to
|
||||
# do no-ops for v1 commands or we'll get "bad request" errors
|
||||
|
||||
def agentc_REQUEST_RSA_IDENTITIES(self, data):
|
||||
"""
|
||||
v1 message for listing RSA1 keys; superseded by
|
||||
agentc_REQUEST_IDENTITIES, which handles different key types.
|
||||
"""
|
||||
self.sendResponse(AGENT_RSA_IDENTITIES_ANSWER, struct.pack("!L", 0))
|
||||
|
||||
def agentc_REMOVE_RSA_IDENTITY(self, data):
|
||||
"""
|
||||
v1 message for removing RSA1 keys; superseded by
|
||||
agentc_REMOVE_IDENTITY, which handles different key types.
|
||||
"""
|
||||
self.sendResponse(AGENT_SUCCESS, b"")
|
||||
|
||||
def agentc_REMOVE_ALL_RSA_IDENTITIES(self, data):
|
||||
"""
|
||||
v1 message for removing all RSA1 keys; superseded by
|
||||
agentc_REMOVE_ALL_IDENTITIES, which handles different key types.
|
||||
"""
|
||||
self.sendResponse(AGENT_SUCCESS, b"")
|
||||
|
||||
|
||||
AGENTC_REQUEST_RSA_IDENTITIES = 1
|
||||
AGENT_RSA_IDENTITIES_ANSWER = 2
|
||||
AGENT_FAILURE = 5
|
||||
AGENT_SUCCESS = 6
|
||||
|
||||
AGENTC_REMOVE_RSA_IDENTITY = 8
|
||||
AGENTC_REMOVE_ALL_RSA_IDENTITIES = 9
|
||||
|
||||
AGENTC_REQUEST_IDENTITIES = 11
|
||||
AGENT_IDENTITIES_ANSWER = 12
|
||||
AGENTC_SIGN_REQUEST = 13
|
||||
AGENT_SIGN_RESPONSE = 14
|
||||
AGENTC_ADD_IDENTITY = 17
|
||||
AGENTC_REMOVE_IDENTITY = 18
|
||||
AGENTC_REMOVE_ALL_IDENTITIES = 19
|
||||
|
||||
messages = {}
|
||||
for name, value in locals().copy().items():
|
||||
if name[:7] == "AGENTC_":
|
||||
messages[value] = name[7:] # doesn't handle doubles
|
||||
@@ -0,0 +1,312 @@
|
||||
# -*- test-case-name: twisted.conch.test.test_channel -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
The parent class for all the SSH Channels. Currently implemented channels
|
||||
are session, direct-tcp, and forwarded-tcp.
|
||||
|
||||
Maintainer: Paul Swartz
|
||||
"""
|
||||
|
||||
|
||||
from zope.interface import implementer
|
||||
|
||||
from twisted.internet import interfaces
|
||||
from twisted.logger import Logger
|
||||
from twisted.python import log
|
||||
|
||||
|
||||
@implementer(interfaces.ITransport)
|
||||
class SSHChannel(log.Logger):
|
||||
"""
|
||||
A class that represents a multiplexed channel over an SSH connection.
|
||||
The channel has a local window which is the maximum amount of data it will
|
||||
receive, and a remote which is the maximum amount of data the remote side
|
||||
will accept. There is also a maximum packet size for any individual data
|
||||
packet going each way.
|
||||
|
||||
@ivar name: the name of the channel.
|
||||
@type name: L{bytes}
|
||||
@ivar localWindowSize: the maximum size of the local window in bytes.
|
||||
@type localWindowSize: L{int}
|
||||
@ivar localWindowLeft: how many bytes are left in the local window.
|
||||
@type localWindowLeft: L{int}
|
||||
@ivar localMaxPacket: the maximum size of packet we will accept in bytes.
|
||||
@type localMaxPacket: L{int}
|
||||
@ivar remoteWindowLeft: how many bytes are left in the remote window.
|
||||
@type remoteWindowLeft: L{int}
|
||||
@ivar remoteMaxPacket: the maximum size of a packet the remote side will
|
||||
accept in bytes.
|
||||
@type remoteMaxPacket: L{int}
|
||||
@ivar conn: the connection this channel is multiplexed through.
|
||||
@type conn: L{SSHConnection}
|
||||
@ivar data: any data to send to the other side when the channel is
|
||||
requested.
|
||||
@type data: L{bytes}
|
||||
@ivar avatar: an avatar for the logged-in user (if a server channel)
|
||||
@ivar localClosed: True if we aren't accepting more data.
|
||||
@type localClosed: L{bool}
|
||||
@ivar remoteClosed: True if the other side isn't accepting more data.
|
||||
@type remoteClosed: L{bool}
|
||||
"""
|
||||
|
||||
_log = Logger()
|
||||
name: bytes = None # type: ignore[assignment] # only needed for client channels
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
localWindow=0,
|
||||
localMaxPacket=0,
|
||||
remoteWindow=0,
|
||||
remoteMaxPacket=0,
|
||||
conn=None,
|
||||
data=None,
|
||||
avatar=None,
|
||||
):
|
||||
self.localWindowSize = localWindow or 131072
|
||||
self.localWindowLeft = self.localWindowSize
|
||||
self.localMaxPacket = localMaxPacket or 32768
|
||||
self.remoteWindowLeft = remoteWindow
|
||||
self.remoteMaxPacket = remoteMaxPacket
|
||||
self.areWriting = 1
|
||||
self.conn = conn
|
||||
self.data = data
|
||||
self.avatar = avatar
|
||||
self.specificData = b""
|
||||
self.buf = b""
|
||||
self.extBuf = []
|
||||
self.closing = 0
|
||||
self.localClosed = 0
|
||||
self.remoteClosed = 0
|
||||
self.id = None # gets set later by SSHConnection
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.__bytes__().decode("ascii")
|
||||
|
||||
def __bytes__(self) -> bytes:
|
||||
"""
|
||||
Return a byte string representation of the channel
|
||||
"""
|
||||
name = self.name
|
||||
if not name:
|
||||
name = b"None"
|
||||
|
||||
return b"<SSHChannel %b (lw %d rw %d)>" % (
|
||||
name,
|
||||
self.localWindowLeft,
|
||||
self.remoteWindowLeft,
|
||||
)
|
||||
|
||||
def logPrefix(self):
|
||||
id = (self.id is not None and str(self.id)) or "unknown"
|
||||
if self.name:
|
||||
name = self.name.decode("ascii")
|
||||
else:
|
||||
name = "None"
|
||||
return f"SSHChannel {name} ({id}) on {self.conn.logPrefix()}"
|
||||
|
||||
def channelOpen(self, specificData):
|
||||
"""
|
||||
Called when the channel is opened. specificData is any data that the
|
||||
other side sent us when opening the channel.
|
||||
|
||||
@type specificData: L{bytes}
|
||||
"""
|
||||
self._log.info("channel open")
|
||||
|
||||
def openFailed(self, reason):
|
||||
"""
|
||||
Called when the open failed for some reason.
|
||||
reason.desc is a string descrption, reason.code the SSH error code.
|
||||
|
||||
@type reason: L{error.ConchError}
|
||||
"""
|
||||
self._log.error("other side refused open\nreason: {reason}", reason=reason)
|
||||
|
||||
def addWindowBytes(self, data):
|
||||
"""
|
||||
Called when bytes are added to the remote window. By default it clears
|
||||
the data buffers.
|
||||
|
||||
@type data: L{bytes}
|
||||
"""
|
||||
self.remoteWindowLeft = self.remoteWindowLeft + data
|
||||
if not self.areWriting and not self.closing:
|
||||
self.areWriting = True
|
||||
self.startWriting()
|
||||
if self.buf:
|
||||
b = self.buf
|
||||
self.buf = b""
|
||||
self.write(b)
|
||||
if self.extBuf:
|
||||
b = self.extBuf
|
||||
self.extBuf = []
|
||||
for type, data in b:
|
||||
self.writeExtended(type, data)
|
||||
|
||||
def requestReceived(self, requestType, data):
|
||||
"""
|
||||
Called when a request is sent to this channel. By default it delegates
|
||||
to self.request_<requestType>.
|
||||
If this function returns true, the request succeeded, otherwise it
|
||||
failed.
|
||||
|
||||
@type requestType: L{bytes}
|
||||
@type data: L{bytes}
|
||||
@rtype: L{bool}
|
||||
"""
|
||||
foo = requestType.replace(b"-", b"_").decode("ascii")
|
||||
f = getattr(self, "request_" + foo, None)
|
||||
if f:
|
||||
return f(data)
|
||||
self._log.info("unhandled request for {requestType}", requestType=requestType)
|
||||
return 0
|
||||
|
||||
def dataReceived(self, data):
|
||||
"""
|
||||
Called when we receive data.
|
||||
|
||||
@type data: L{bytes}
|
||||
"""
|
||||
self._log.debug("got data {data}", data=data)
|
||||
|
||||
def extReceived(self, dataType, data):
|
||||
"""
|
||||
Called when we receive extended data (usually standard error).
|
||||
|
||||
@type dataType: L{int}
|
||||
@type data: L{str}
|
||||
"""
|
||||
self._log.debug(
|
||||
"got extended data {dataType} {data!r}", dataType=dataType, data=data
|
||||
)
|
||||
|
||||
def eofReceived(self):
|
||||
"""
|
||||
Called when the other side will send no more data.
|
||||
"""
|
||||
self._log.info("remote eof")
|
||||
|
||||
def closeReceived(self):
|
||||
"""
|
||||
Called when the other side has closed the channel.
|
||||
"""
|
||||
self._log.info("remote close")
|
||||
self.loseConnection()
|
||||
|
||||
def closed(self):
|
||||
"""
|
||||
Called when the channel is closed. This means that both our side and
|
||||
the remote side have closed the channel.
|
||||
"""
|
||||
self._log.info("closed")
|
||||
|
||||
def write(self, data):
|
||||
"""
|
||||
Write some data to the channel. If there is not enough remote window
|
||||
available, buffer until it is. Otherwise, split the data into
|
||||
packets of length remoteMaxPacket and send them.
|
||||
|
||||
@type data: L{bytes}
|
||||
"""
|
||||
if self.buf:
|
||||
self.buf += data
|
||||
return
|
||||
top = len(data)
|
||||
if top > self.remoteWindowLeft:
|
||||
data, self.buf = (
|
||||
data[: self.remoteWindowLeft],
|
||||
data[self.remoteWindowLeft :],
|
||||
)
|
||||
self.areWriting = 0
|
||||
self.stopWriting()
|
||||
top = self.remoteWindowLeft
|
||||
rmp = self.remoteMaxPacket
|
||||
write = self.conn.sendData
|
||||
r = range(0, top, rmp)
|
||||
for offset in r:
|
||||
write(self, data[offset : offset + rmp])
|
||||
self.remoteWindowLeft -= top
|
||||
if self.closing and not self.buf:
|
||||
self.loseConnection() # try again
|
||||
|
||||
def writeExtended(self, dataType, data):
|
||||
"""
|
||||
Send extended data to this channel. If there is not enough remote
|
||||
window available, buffer until there is. Otherwise, split the data
|
||||
into packets of length remoteMaxPacket and send them.
|
||||
|
||||
@type dataType: L{int}
|
||||
@type data: L{bytes}
|
||||
"""
|
||||
if self.extBuf:
|
||||
if self.extBuf[-1][0] == dataType:
|
||||
self.extBuf[-1][1] += data
|
||||
else:
|
||||
self.extBuf.append([dataType, data])
|
||||
return
|
||||
if len(data) > self.remoteWindowLeft:
|
||||
data, self.extBuf = (
|
||||
data[: self.remoteWindowLeft],
|
||||
[[dataType, data[self.remoteWindowLeft :]]],
|
||||
)
|
||||
self.areWriting = 0
|
||||
self.stopWriting()
|
||||
while len(data) > self.remoteMaxPacket:
|
||||
self.conn.sendExtendedData(self, dataType, data[: self.remoteMaxPacket])
|
||||
data = data[self.remoteMaxPacket :]
|
||||
self.remoteWindowLeft -= self.remoteMaxPacket
|
||||
if data:
|
||||
self.conn.sendExtendedData(self, dataType, data)
|
||||
self.remoteWindowLeft -= len(data)
|
||||
if self.closing:
|
||||
self.loseConnection() # try again
|
||||
|
||||
def writeSequence(self, data):
|
||||
"""
|
||||
Part of the Transport interface. Write a list of strings to the
|
||||
channel.
|
||||
|
||||
@type data: C{list} of L{str}
|
||||
"""
|
||||
self.write(b"".join(data))
|
||||
|
||||
def loseConnection(self):
|
||||
"""
|
||||
Close the channel if there is no buferred data. Otherwise, note the
|
||||
request and return.
|
||||
"""
|
||||
self.closing = 1
|
||||
if not self.buf and not self.extBuf:
|
||||
self.conn.sendClose(self)
|
||||
|
||||
def getPeer(self):
|
||||
"""
|
||||
See: L{ITransport.getPeer}
|
||||
|
||||
@return: The remote address of this connection.
|
||||
@rtype: L{SSHTransportAddress}.
|
||||
"""
|
||||
return self.conn.transport.getPeer()
|
||||
|
||||
def getHost(self):
|
||||
"""
|
||||
See: L{ITransport.getHost}
|
||||
|
||||
@return: An address describing this side of the connection.
|
||||
@rtype: L{SSHTransportAddress}.
|
||||
"""
|
||||
return self.conn.transport.getHost()
|
||||
|
||||
def stopWriting(self):
|
||||
"""
|
||||
Called when the remote buffer is full, as a hint to stop writing.
|
||||
This can be ignored, but it can be helpful.
|
||||
"""
|
||||
|
||||
def startWriting(self):
|
||||
"""
|
||||
Called when the remote buffer has more room, as a hint to continue
|
||||
writing.
|
||||
"""
|
||||
@@ -0,0 +1,85 @@
|
||||
# -*- test-case-name: twisted.conch.test.test_ssh -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Common functions for the SSH classes.
|
||||
|
||||
Maintainer: Paul Swartz
|
||||
"""
|
||||
|
||||
|
||||
import struct
|
||||
|
||||
from cryptography.utils import int_to_bytes
|
||||
|
||||
from twisted.python.deprecate import deprecated
|
||||
from twisted.python.versions import Version
|
||||
|
||||
__all__ = ["NS", "getNS", "MP", "getMP", "ffs"]
|
||||
|
||||
|
||||
def NS(t):
|
||||
"""
|
||||
net string
|
||||
"""
|
||||
if isinstance(t, str):
|
||||
t = t.encode("utf-8")
|
||||
return struct.pack("!L", len(t)) + t
|
||||
|
||||
|
||||
def getNS(s, count=1):
|
||||
"""
|
||||
get net string
|
||||
"""
|
||||
ns = []
|
||||
c = 0
|
||||
for i in range(count):
|
||||
(l,) = struct.unpack("!L", s[c : c + 4])
|
||||
ns.append(s[c + 4 : 4 + l + c])
|
||||
c += 4 + l
|
||||
return tuple(ns) + (s[c:],)
|
||||
|
||||
|
||||
def MP(number):
|
||||
if number == 0:
|
||||
return b"\000" * 4
|
||||
assert number > 0
|
||||
bn = int_to_bytes(number)
|
||||
if ord(bn[0:1]) & 128:
|
||||
bn = b"\000" + bn
|
||||
return struct.pack(">L", len(bn)) + bn
|
||||
|
||||
|
||||
def getMP(data, count=1):
|
||||
"""
|
||||
Get multiple precision integer out of the string. A multiple precision
|
||||
integer is stored as a 4-byte length followed by length bytes of the
|
||||
integer. If count is specified, get count integers out of the string.
|
||||
The return value is a tuple of count integers followed by the rest of
|
||||
the data.
|
||||
"""
|
||||
mp = []
|
||||
c = 0
|
||||
for i in range(count):
|
||||
(length,) = struct.unpack(">L", data[c : c + 4])
|
||||
mp.append(int.from_bytes(data[c + 4 : c + 4 + length], "big"))
|
||||
c += 4 + length
|
||||
return tuple(mp) + (data[c:],)
|
||||
|
||||
|
||||
def ffs(c, s):
|
||||
"""
|
||||
first from second
|
||||
goes through the first list, looking for items in the second, returns the first one
|
||||
"""
|
||||
for i in c:
|
||||
if i in s:
|
||||
return i
|
||||
|
||||
|
||||
@deprecated(Version("Twisted", 16, 5, 0))
|
||||
def install():
|
||||
# This used to install gmpy, but is technically public API, so just do
|
||||
# nothing.
|
||||
pass
|
||||
@@ -0,0 +1,679 @@
|
||||
# -*- test-case-name: twisted.conch.test.test_connection -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
This module contains the implementation of the ssh-connection service, which
|
||||
allows access to the shell and port-forwarding.
|
||||
|
||||
Maintainer: Paul Swartz
|
||||
"""
|
||||
|
||||
import string
|
||||
import struct
|
||||
|
||||
import twisted.internet.error
|
||||
from twisted.conch import error
|
||||
from twisted.conch.ssh import common, service
|
||||
from twisted.internet import defer
|
||||
from twisted.logger import Logger
|
||||
from twisted.python.compat import nativeString, networkString
|
||||
|
||||
|
||||
class SSHConnection(service.SSHService):
|
||||
"""
|
||||
An implementation of the 'ssh-connection' service. It is used to
|
||||
multiplex multiple channels over the single SSH connection.
|
||||
|
||||
@ivar localChannelID: the next number to use as a local channel ID.
|
||||
@type localChannelID: L{int}
|
||||
@ivar channels: a L{dict} mapping a local channel ID to C{SSHChannel}
|
||||
subclasses.
|
||||
@type channels: L{dict}
|
||||
@ivar localToRemoteChannel: a L{dict} mapping a local channel ID to a
|
||||
remote channel ID.
|
||||
@type localToRemoteChannel: L{dict}
|
||||
@ivar channelsToRemoteChannel: a L{dict} mapping a C{SSHChannel} subclass
|
||||
to remote channel ID.
|
||||
@type channelsToRemoteChannel: L{dict}
|
||||
@ivar deferreds: a L{dict} mapping a local channel ID to a C{list} of
|
||||
C{Deferreds} for outstanding channel requests. Also, the 'global'
|
||||
key stores the C{list} of pending global request C{Deferred}s.
|
||||
"""
|
||||
|
||||
name = b"ssh-connection"
|
||||
_log = Logger()
|
||||
|
||||
def __init__(self):
|
||||
self.localChannelID = 0 # this is the current # to use for channel ID
|
||||
# local channel ID -> remote channel ID
|
||||
self.localToRemoteChannel = {}
|
||||
# local channel ID -> subclass of SSHChannel
|
||||
self.channels = {}
|
||||
# subclass of SSHChannel -> remote channel ID
|
||||
self.channelsToRemoteChannel = {}
|
||||
# local channel -> list of deferreds for pending requests
|
||||
# or 'global' -> list of deferreds for global requests
|
||||
self.deferreds = {"global": []}
|
||||
|
||||
self.transport = None # gets set later
|
||||
|
||||
def serviceStarted(self):
|
||||
if hasattr(self.transport, "avatar"):
|
||||
self.transport.avatar.conn = self
|
||||
|
||||
def serviceStopped(self):
|
||||
"""
|
||||
Called when the connection is stopped.
|
||||
"""
|
||||
# Close any fully open channels
|
||||
for channel in list(self.channelsToRemoteChannel.keys()):
|
||||
self.channelClosed(channel)
|
||||
# Indicate failure to any channels that were in the process of
|
||||
# opening but not yet open.
|
||||
while self.channels:
|
||||
(_, channel) = self.channels.popitem()
|
||||
channel.openFailed(twisted.internet.error.ConnectionLost())
|
||||
# Errback any unfinished global requests.
|
||||
self._cleanupGlobalDeferreds()
|
||||
|
||||
def _cleanupGlobalDeferreds(self):
|
||||
"""
|
||||
All pending requests that have returned a deferred must be errbacked
|
||||
when this service is stopped, otherwise they might be left uncalled and
|
||||
uncallable.
|
||||
"""
|
||||
for d in self.deferreds["global"]:
|
||||
d.errback(error.ConchError("Connection stopped."))
|
||||
del self.deferreds["global"][:]
|
||||
|
||||
# packet methods
|
||||
def ssh_GLOBAL_REQUEST(self, packet):
|
||||
"""
|
||||
The other side has made a global request. Payload::
|
||||
string request type
|
||||
bool want reply
|
||||
<request specific data>
|
||||
|
||||
This dispatches to self.gotGlobalRequest.
|
||||
"""
|
||||
requestType, rest = common.getNS(packet)
|
||||
wantReply, rest = ord(rest[0:1]), rest[1:]
|
||||
ret = self.gotGlobalRequest(requestType, rest)
|
||||
if wantReply:
|
||||
reply = MSG_REQUEST_FAILURE
|
||||
data = b""
|
||||
if ret:
|
||||
reply = MSG_REQUEST_SUCCESS
|
||||
if isinstance(ret, (tuple, list)):
|
||||
data = ret[1]
|
||||
self.transport.sendPacket(reply, data)
|
||||
|
||||
def ssh_REQUEST_SUCCESS(self, packet):
|
||||
"""
|
||||
Our global request succeeded. Get the appropriate Deferred and call
|
||||
it back with the packet we received.
|
||||
"""
|
||||
self._log.debug("global request success")
|
||||
self.deferreds["global"].pop(0).callback(packet)
|
||||
|
||||
def ssh_REQUEST_FAILURE(self, packet):
|
||||
"""
|
||||
Our global request failed. Get the appropriate Deferred and errback
|
||||
it with the packet we received.
|
||||
"""
|
||||
self._log.debug("global request failure")
|
||||
self.deferreds["global"].pop(0).errback(
|
||||
error.ConchError("global request failed", packet)
|
||||
)
|
||||
|
||||
def ssh_CHANNEL_OPEN(self, packet):
|
||||
"""
|
||||
The other side wants to get a channel. Payload::
|
||||
string channel name
|
||||
uint32 remote channel number
|
||||
uint32 remote window size
|
||||
uint32 remote maximum packet size
|
||||
<channel specific data>
|
||||
|
||||
We get a channel from self.getChannel(), give it a local channel number
|
||||
and notify the other side. Then notify the channel by calling its
|
||||
channelOpen method.
|
||||
"""
|
||||
channelType, rest = common.getNS(packet)
|
||||
senderChannel, windowSize, maxPacket = struct.unpack(">3L", rest[:12])
|
||||
packet = rest[12:]
|
||||
try:
|
||||
channel = self.getChannel(channelType, windowSize, maxPacket, packet)
|
||||
localChannel = self.localChannelID
|
||||
self.localChannelID += 1
|
||||
channel.id = localChannel
|
||||
self.channels[localChannel] = channel
|
||||
self.channelsToRemoteChannel[channel] = senderChannel
|
||||
self.localToRemoteChannel[localChannel] = senderChannel
|
||||
openConfirmPacket = (
|
||||
struct.pack(
|
||||
">4L",
|
||||
senderChannel,
|
||||
localChannel,
|
||||
channel.localWindowSize,
|
||||
channel.localMaxPacket,
|
||||
)
|
||||
+ channel.specificData
|
||||
)
|
||||
self.transport.sendPacket(MSG_CHANNEL_OPEN_CONFIRMATION, openConfirmPacket)
|
||||
channel.channelOpen(packet)
|
||||
except Exception as e:
|
||||
self._log.failure("channel open failed")
|
||||
if isinstance(e, error.ConchError):
|
||||
textualInfo, reason = e.args
|
||||
if isinstance(textualInfo, int):
|
||||
# See #3657 and #3071
|
||||
textualInfo, reason = reason, textualInfo
|
||||
else:
|
||||
reason = OPEN_CONNECT_FAILED
|
||||
textualInfo = "unknown failure"
|
||||
self.transport.sendPacket(
|
||||
MSG_CHANNEL_OPEN_FAILURE,
|
||||
struct.pack(">2L", senderChannel, reason)
|
||||
+ common.NS(networkString(textualInfo))
|
||||
+ common.NS(b""),
|
||||
)
|
||||
|
||||
def ssh_CHANNEL_OPEN_CONFIRMATION(self, packet):
|
||||
"""
|
||||
The other side accepted our MSG_CHANNEL_OPEN request. Payload::
|
||||
uint32 local channel number
|
||||
uint32 remote channel number
|
||||
uint32 remote window size
|
||||
uint32 remote maximum packet size
|
||||
<channel specific data>
|
||||
|
||||
Find the channel using the local channel number and notify its
|
||||
channelOpen method.
|
||||
"""
|
||||
(localChannel, remoteChannel, windowSize, maxPacket) = struct.unpack(
|
||||
">4L", packet[:16]
|
||||
)
|
||||
specificData = packet[16:]
|
||||
channel = self.channels[localChannel]
|
||||
channel.conn = self
|
||||
self.localToRemoteChannel[localChannel] = remoteChannel
|
||||
self.channelsToRemoteChannel[channel] = remoteChannel
|
||||
channel.remoteWindowLeft = windowSize
|
||||
channel.remoteMaxPacket = maxPacket
|
||||
channel.channelOpen(specificData)
|
||||
|
||||
def ssh_CHANNEL_OPEN_FAILURE(self, packet):
|
||||
"""
|
||||
The other side did not accept our MSG_CHANNEL_OPEN request. Payload::
|
||||
uint32 local channel number
|
||||
uint32 reason code
|
||||
string reason description
|
||||
|
||||
Find the channel using the local channel number and notify it by
|
||||
calling its openFailed() method.
|
||||
"""
|
||||
localChannel, reasonCode = struct.unpack(">2L", packet[:8])
|
||||
reasonDesc = common.getNS(packet[8:])[0]
|
||||
channel = self.channels[localChannel]
|
||||
del self.channels[localChannel]
|
||||
channel.conn = self
|
||||
reason = error.ConchError(reasonDesc, reasonCode)
|
||||
channel.openFailed(reason)
|
||||
|
||||
def ssh_CHANNEL_WINDOW_ADJUST(self, packet):
|
||||
"""
|
||||
The other side is adding bytes to its window. Payload::
|
||||
uint32 local channel number
|
||||
uint32 bytes to add
|
||||
|
||||
Call the channel's addWindowBytes() method to add new bytes to the
|
||||
remote window.
|
||||
"""
|
||||
localChannel, bytesToAdd = struct.unpack(">2L", packet[:8])
|
||||
channel = self.channels[localChannel]
|
||||
channel.addWindowBytes(bytesToAdd)
|
||||
|
||||
def ssh_CHANNEL_DATA(self, packet):
|
||||
"""
|
||||
The other side is sending us data. Payload::
|
||||
uint32 local channel number
|
||||
string data
|
||||
|
||||
Check to make sure the other side hasn't sent too much data (more
|
||||
than what's in the window, or more than the maximum packet size). If
|
||||
they have, close the channel. Otherwise, decrease the available
|
||||
window and pass the data to the channel's dataReceived().
|
||||
"""
|
||||
localChannel, dataLength = struct.unpack(">2L", packet[:8])
|
||||
channel = self.channels[localChannel]
|
||||
# XXX should this move to dataReceived to put client in charge?
|
||||
if (
|
||||
dataLength > channel.localWindowLeft or dataLength > channel.localMaxPacket
|
||||
): # more data than we want
|
||||
self._log.error("too much data")
|
||||
self.sendClose(channel)
|
||||
return
|
||||
# packet = packet[:channel.localWindowLeft+4]
|
||||
data = common.getNS(packet[4:])[0]
|
||||
channel.localWindowLeft -= dataLength
|
||||
if channel.localWindowLeft < channel.localWindowSize // 2:
|
||||
self.adjustWindow(
|
||||
channel, channel.localWindowSize - channel.localWindowLeft
|
||||
)
|
||||
channel.dataReceived(data)
|
||||
|
||||
def ssh_CHANNEL_EXTENDED_DATA(self, packet):
|
||||
"""
|
||||
The other side is sending us exteneded data. Payload::
|
||||
uint32 local channel number
|
||||
uint32 type code
|
||||
string data
|
||||
|
||||
Check to make sure the other side hasn't sent too much data (more
|
||||
than what's in the window, or than the maximum packet size). If
|
||||
they have, close the channel. Otherwise, decrease the available
|
||||
window and pass the data and type code to the channel's
|
||||
extReceived().
|
||||
"""
|
||||
localChannel, typeCode, dataLength = struct.unpack(">3L", packet[:12])
|
||||
channel = self.channels[localChannel]
|
||||
if dataLength > channel.localWindowLeft or dataLength > channel.localMaxPacket:
|
||||
self._log.error("too much extdata")
|
||||
self.sendClose(channel)
|
||||
return
|
||||
data = common.getNS(packet[8:])[0]
|
||||
channel.localWindowLeft -= dataLength
|
||||
if channel.localWindowLeft < channel.localWindowSize // 2:
|
||||
self.adjustWindow(
|
||||
channel, channel.localWindowSize - channel.localWindowLeft
|
||||
)
|
||||
channel.extReceived(typeCode, data)
|
||||
|
||||
def ssh_CHANNEL_EOF(self, packet):
|
||||
"""
|
||||
The other side is not sending any more data. Payload::
|
||||
uint32 local channel number
|
||||
|
||||
Notify the channel by calling its eofReceived() method.
|
||||
"""
|
||||
localChannel = struct.unpack(">L", packet[:4])[0]
|
||||
channel = self.channels[localChannel]
|
||||
channel.eofReceived()
|
||||
|
||||
def ssh_CHANNEL_CLOSE(self, packet):
|
||||
"""
|
||||
The other side is closing its end; it does not want to receive any
|
||||
more data. Payload::
|
||||
uint32 local channel number
|
||||
|
||||
Notify the channnel by calling its closeReceived() method. If
|
||||
the channel has also sent a close message, call self.channelClosed().
|
||||
"""
|
||||
localChannel = struct.unpack(">L", packet[:4])[0]
|
||||
channel = self.channels[localChannel]
|
||||
channel.closeReceived()
|
||||
channel.remoteClosed = True
|
||||
if channel.localClosed and channel.remoteClosed:
|
||||
self.channelClosed(channel)
|
||||
|
||||
def ssh_CHANNEL_REQUEST(self, packet):
|
||||
"""
|
||||
The other side is sending a request to a channel. Payload::
|
||||
uint32 local channel number
|
||||
string request name
|
||||
bool want reply
|
||||
<request specific data>
|
||||
|
||||
Pass the message to the channel's requestReceived method. If the
|
||||
other side wants a reply, add callbacks which will send the
|
||||
reply.
|
||||
"""
|
||||
localChannel = struct.unpack(">L", packet[:4])[0]
|
||||
requestType, rest = common.getNS(packet[4:])
|
||||
wantReply = ord(rest[0:1])
|
||||
channel = self.channels[localChannel]
|
||||
d = defer.maybeDeferred(channel.requestReceived, requestType, rest[1:])
|
||||
if wantReply:
|
||||
d.addCallback(self._cbChannelRequest, localChannel)
|
||||
d.addErrback(self._ebChannelRequest, localChannel)
|
||||
return d
|
||||
|
||||
def _cbChannelRequest(self, result, localChannel):
|
||||
"""
|
||||
Called back if the other side wanted a reply to a channel request. If
|
||||
the result is true, send a MSG_CHANNEL_SUCCESS. Otherwise, raise
|
||||
a C{error.ConchError}
|
||||
|
||||
@param result: the value returned from the channel's requestReceived()
|
||||
method. If it's False, the request failed.
|
||||
@type result: L{bool}
|
||||
@param localChannel: the local channel ID of the channel to which the
|
||||
request was made.
|
||||
@type localChannel: L{int}
|
||||
@raises ConchError: if the result is False.
|
||||
"""
|
||||
if not result:
|
||||
raise error.ConchError("failed request")
|
||||
self.transport.sendPacket(
|
||||
MSG_CHANNEL_SUCCESS,
|
||||
struct.pack(">L", self.localToRemoteChannel[localChannel]),
|
||||
)
|
||||
|
||||
def _ebChannelRequest(self, result, localChannel):
|
||||
"""
|
||||
Called if the other wisde wanted a reply to the channel requeset and
|
||||
the channel request failed.
|
||||
|
||||
@param result: a Failure, but it's not used.
|
||||
@param localChannel: the local channel ID of the channel to which the
|
||||
request was made.
|
||||
@type localChannel: L{int}
|
||||
"""
|
||||
self.transport.sendPacket(
|
||||
MSG_CHANNEL_FAILURE,
|
||||
struct.pack(">L", self.localToRemoteChannel[localChannel]),
|
||||
)
|
||||
|
||||
def ssh_CHANNEL_SUCCESS(self, packet):
|
||||
"""
|
||||
Our channel request to the other side succeeded. Payload::
|
||||
uint32 local channel number
|
||||
|
||||
Get the C{Deferred} out of self.deferreds and call it back.
|
||||
"""
|
||||
localChannel = struct.unpack(">L", packet[:4])[0]
|
||||
if self.deferreds.get(localChannel):
|
||||
d = self.deferreds[localChannel].pop(0)
|
||||
d.callback("")
|
||||
|
||||
def ssh_CHANNEL_FAILURE(self, packet):
|
||||
"""
|
||||
Our channel request to the other side failed. Payload::
|
||||
uint32 local channel number
|
||||
|
||||
Get the C{Deferred} out of self.deferreds and errback it with a
|
||||
C{error.ConchError}.
|
||||
"""
|
||||
localChannel = struct.unpack(">L", packet[:4])[0]
|
||||
if self.deferreds.get(localChannel):
|
||||
d = self.deferreds[localChannel].pop(0)
|
||||
d.errback(error.ConchError("channel request failed"))
|
||||
|
||||
# methods for users of the connection to call
|
||||
|
||||
def sendGlobalRequest(self, request, data, wantReply=0):
|
||||
"""
|
||||
Send a global request for this connection. Current this is only used
|
||||
for remote->local TCP forwarding.
|
||||
|
||||
@type request: L{bytes}
|
||||
@type data: L{bytes}
|
||||
@type wantReply: L{bool}
|
||||
@rtype: C{Deferred}/L{None}
|
||||
"""
|
||||
self.transport.sendPacket(
|
||||
MSG_GLOBAL_REQUEST,
|
||||
common.NS(request) + (wantReply and b"\xff" or b"\x00") + data,
|
||||
)
|
||||
if wantReply:
|
||||
d = defer.Deferred()
|
||||
self.deferreds["global"].append(d)
|
||||
return d
|
||||
|
||||
def openChannel(self, channel, extra=b""):
|
||||
"""
|
||||
Open a new channel on this connection.
|
||||
|
||||
@type channel: subclass of C{SSHChannel}
|
||||
@type extra: L{bytes}
|
||||
"""
|
||||
self._log.info(
|
||||
"opening channel {id} with {localWindowSize} {localMaxPacket}",
|
||||
id=self.localChannelID,
|
||||
localWindowSize=channel.localWindowSize,
|
||||
localMaxPacket=channel.localMaxPacket,
|
||||
)
|
||||
self.transport.sendPacket(
|
||||
MSG_CHANNEL_OPEN,
|
||||
common.NS(channel.name)
|
||||
+ struct.pack(
|
||||
">3L",
|
||||
self.localChannelID,
|
||||
channel.localWindowSize,
|
||||
channel.localMaxPacket,
|
||||
)
|
||||
+ extra,
|
||||
)
|
||||
channel.id = self.localChannelID
|
||||
self.channels[self.localChannelID] = channel
|
||||
self.localChannelID += 1
|
||||
|
||||
def sendRequest(self, channel, requestType, data, wantReply=0):
|
||||
"""
|
||||
Send a request to a channel.
|
||||
|
||||
@type channel: subclass of C{SSHChannel}
|
||||
@type requestType: L{bytes}
|
||||
@type data: L{bytes}
|
||||
@type wantReply: L{bool}
|
||||
@rtype: C{Deferred}/L{None}
|
||||
"""
|
||||
if channel.localClosed:
|
||||
return
|
||||
self._log.debug("sending request {requestType}", requestType=requestType)
|
||||
self.transport.sendPacket(
|
||||
MSG_CHANNEL_REQUEST,
|
||||
struct.pack(">L", self.channelsToRemoteChannel[channel])
|
||||
+ common.NS(requestType)
|
||||
+ (b"\1" if wantReply else b"\0")
|
||||
+ data,
|
||||
)
|
||||
if wantReply:
|
||||
d = defer.Deferred()
|
||||
self.deferreds.setdefault(channel.id, []).append(d)
|
||||
return d
|
||||
|
||||
def adjustWindow(self, channel, bytesToAdd):
|
||||
"""
|
||||
Tell the other side that we will receive more data. This should not
|
||||
normally need to be called as it is managed automatically.
|
||||
|
||||
@type channel: subclass of L{SSHChannel}
|
||||
@type bytesToAdd: L{int}
|
||||
"""
|
||||
if channel.localClosed:
|
||||
return # we're already closed
|
||||
packet = struct.pack(">2L", self.channelsToRemoteChannel[channel], bytesToAdd)
|
||||
self.transport.sendPacket(MSG_CHANNEL_WINDOW_ADJUST, packet)
|
||||
self._log.debug(
|
||||
"adding {bytesToAdd} to {localWindowLeft} in channel {id}",
|
||||
bytesToAdd=bytesToAdd,
|
||||
localWindowLeft=channel.localWindowLeft,
|
||||
id=channel.id,
|
||||
)
|
||||
channel.localWindowLeft += bytesToAdd
|
||||
|
||||
def sendData(self, channel, data):
|
||||
"""
|
||||
Send data to a channel. This should not normally be used: instead use
|
||||
channel.write(data) as it manages the window automatically.
|
||||
|
||||
@type channel: subclass of L{SSHChannel}
|
||||
@type data: L{bytes}
|
||||
"""
|
||||
if channel.localClosed:
|
||||
return # we're already closed
|
||||
self.transport.sendPacket(
|
||||
MSG_CHANNEL_DATA,
|
||||
struct.pack(">L", self.channelsToRemoteChannel[channel]) + common.NS(data),
|
||||
)
|
||||
|
||||
def sendExtendedData(self, channel, dataType, data):
|
||||
"""
|
||||
Send extended data to a channel. This should not normally be used:
|
||||
instead use channel.writeExtendedData(data, dataType) as it manages
|
||||
the window automatically.
|
||||
|
||||
@type channel: subclass of L{SSHChannel}
|
||||
@type dataType: L{int}
|
||||
@type data: L{bytes}
|
||||
"""
|
||||
if channel.localClosed:
|
||||
return # we're already closed
|
||||
self.transport.sendPacket(
|
||||
MSG_CHANNEL_EXTENDED_DATA,
|
||||
struct.pack(">2L", self.channelsToRemoteChannel[channel], dataType)
|
||||
+ common.NS(data),
|
||||
)
|
||||
|
||||
def sendEOF(self, channel):
|
||||
"""
|
||||
Send an EOF (End of File) for a channel.
|
||||
|
||||
@type channel: subclass of L{SSHChannel}
|
||||
"""
|
||||
if channel.localClosed:
|
||||
return # we're already closed
|
||||
self._log.debug("sending eof")
|
||||
self.transport.sendPacket(
|
||||
MSG_CHANNEL_EOF, struct.pack(">L", self.channelsToRemoteChannel[channel])
|
||||
)
|
||||
|
||||
def sendClose(self, channel):
|
||||
"""
|
||||
Close a channel.
|
||||
|
||||
@type channel: subclass of L{SSHChannel}
|
||||
"""
|
||||
if channel.localClosed:
|
||||
return # we're already closed
|
||||
self._log.info("sending close {id}", id=channel.id)
|
||||
self.transport.sendPacket(
|
||||
MSG_CHANNEL_CLOSE, struct.pack(">L", self.channelsToRemoteChannel[channel])
|
||||
)
|
||||
channel.localClosed = True
|
||||
if channel.localClosed and channel.remoteClosed:
|
||||
self.channelClosed(channel)
|
||||
|
||||
# methods to override
|
||||
def getChannel(self, channelType, windowSize, maxPacket, data):
|
||||
"""
|
||||
The other side requested a channel of some sort.
|
||||
channelType is the type of channel being requested,
|
||||
windowSize is the initial size of the remote window,
|
||||
maxPacket is the largest packet we should send,
|
||||
data is any other packet data (often nothing).
|
||||
|
||||
We return a subclass of L{SSHChannel}.
|
||||
|
||||
By default, this dispatches to a method 'channel_channelType' with any
|
||||
non-alphanumerics in the channelType replace with _'s. If it cannot
|
||||
find a suitable method, it returns an OPEN_UNKNOWN_CHANNEL_TYPE error.
|
||||
The method is called with arguments of windowSize, maxPacket, data.
|
||||
|
||||
@type channelType: L{bytes}
|
||||
@type windowSize: L{int}
|
||||
@type maxPacket: L{int}
|
||||
@type data: L{bytes}
|
||||
@rtype: subclass of L{SSHChannel}/L{tuple}
|
||||
"""
|
||||
self._log.debug("got channel {channelType!r} request", channelType=channelType)
|
||||
if hasattr(self.transport, "avatar"): # this is a server!
|
||||
chan = self.transport.avatar.lookupChannel(
|
||||
channelType, windowSize, maxPacket, data
|
||||
)
|
||||
else:
|
||||
channelType = channelType.translate(TRANSLATE_TABLE)
|
||||
attr = "channel_%s" % nativeString(channelType)
|
||||
f = getattr(self, attr, None)
|
||||
if f is not None:
|
||||
chan = f(windowSize, maxPacket, data)
|
||||
else:
|
||||
chan = None
|
||||
if chan is None:
|
||||
raise error.ConchError("unknown channel", OPEN_UNKNOWN_CHANNEL_TYPE)
|
||||
else:
|
||||
chan.conn = self
|
||||
return chan
|
||||
|
||||
def gotGlobalRequest(self, requestType, data):
|
||||
"""
|
||||
We got a global request. pretty much, this is just used by the client
|
||||
to request that we forward a port from the server to the client.
|
||||
Returns either:
|
||||
- 1: request accepted
|
||||
- 1, <data>: request accepted with request specific data
|
||||
- 0: request denied
|
||||
|
||||
By default, this dispatches to a method 'global_requestType' with
|
||||
-'s in requestType replaced with _'s. The found method is passed data.
|
||||
If this method cannot be found, this method returns 0. Otherwise, it
|
||||
returns the return value of that method.
|
||||
|
||||
@type requestType: L{bytes}
|
||||
@type data: L{bytes}
|
||||
@rtype: L{int}/L{tuple}
|
||||
"""
|
||||
self._log.debug("got global {requestType} request", requestType=requestType)
|
||||
if hasattr(self.transport, "avatar"): # this is a server!
|
||||
return self.transport.avatar.gotGlobalRequest(requestType, data)
|
||||
|
||||
requestType = nativeString(requestType.replace(b"-", b"_"))
|
||||
f = getattr(self, "global_%s" % requestType, None)
|
||||
if not f:
|
||||
return 0
|
||||
return f(data)
|
||||
|
||||
def channelClosed(self, channel):
|
||||
"""
|
||||
Called when a channel is closed.
|
||||
It clears the local state related to the channel, and calls
|
||||
channel.closed().
|
||||
MAKE SURE YOU CALL THIS METHOD, even if you subclass L{SSHConnection}.
|
||||
If you don't, things will break mysteriously.
|
||||
|
||||
@type channel: L{SSHChannel}
|
||||
"""
|
||||
if channel in self.channelsToRemoteChannel: # actually open
|
||||
channel.localClosed = channel.remoteClosed = True
|
||||
del self.localToRemoteChannel[channel.id]
|
||||
del self.channels[channel.id]
|
||||
del self.channelsToRemoteChannel[channel]
|
||||
for d in self.deferreds.pop(channel.id, []):
|
||||
d.errback(error.ConchError("Channel closed."))
|
||||
channel.closed()
|
||||
|
||||
|
||||
MSG_GLOBAL_REQUEST = 80
|
||||
MSG_REQUEST_SUCCESS = 81
|
||||
MSG_REQUEST_FAILURE = 82
|
||||
MSG_CHANNEL_OPEN = 90
|
||||
MSG_CHANNEL_OPEN_CONFIRMATION = 91
|
||||
MSG_CHANNEL_OPEN_FAILURE = 92
|
||||
MSG_CHANNEL_WINDOW_ADJUST = 93
|
||||
MSG_CHANNEL_DATA = 94
|
||||
MSG_CHANNEL_EXTENDED_DATA = 95
|
||||
MSG_CHANNEL_EOF = 96
|
||||
MSG_CHANNEL_CLOSE = 97
|
||||
MSG_CHANNEL_REQUEST = 98
|
||||
MSG_CHANNEL_SUCCESS = 99
|
||||
MSG_CHANNEL_FAILURE = 100
|
||||
|
||||
OPEN_ADMINISTRATIVELY_PROHIBITED = 1
|
||||
OPEN_CONNECT_FAILED = 2
|
||||
OPEN_UNKNOWN_CHANNEL_TYPE = 3
|
||||
OPEN_RESOURCE_SHORTAGE = 4
|
||||
|
||||
# From RFC 4254
|
||||
EXTENDED_DATA_STDERR = 1
|
||||
|
||||
messages = {}
|
||||
for name, value in locals().copy().items():
|
||||
if name[:4] == "MSG_":
|
||||
messages[value] = name # Doesn't handle doubles
|
||||
|
||||
alphanums = networkString(string.ascii_letters + string.digits)
|
||||
TRANSLATE_TABLE = bytes(i if i in alphanums else ord("_") for i in range(256))
|
||||
SSHConnection.protocolMessages = messages
|
||||
@@ -0,0 +1,129 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
A Factory for SSH servers.
|
||||
|
||||
See also L{twisted.conch.openssh_compat.factory} for OpenSSH compatibility.
|
||||
|
||||
Maintainer: Paul Swartz
|
||||
"""
|
||||
|
||||
|
||||
import random
|
||||
from itertools import chain
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
|
||||
from twisted.conch import error
|
||||
from twisted.conch.ssh import _kex, connection, transport, userauth
|
||||
from twisted.internet import protocol
|
||||
from twisted.logger import Logger
|
||||
|
||||
|
||||
class SSHFactory(protocol.Factory):
|
||||
"""
|
||||
A Factory for SSH servers.
|
||||
"""
|
||||
|
||||
primes: Optional[Dict[int, List[Tuple[int, int]]]]
|
||||
|
||||
_log = Logger()
|
||||
protocol = transport.SSHServerTransport
|
||||
|
||||
services = {
|
||||
b"ssh-userauth": userauth.SSHUserAuthServer,
|
||||
b"ssh-connection": connection.SSHConnection,
|
||||
}
|
||||
|
||||
def startFactory(self) -> None:
|
||||
"""
|
||||
Check for public and private keys.
|
||||
"""
|
||||
if not hasattr(self, "publicKeys"):
|
||||
self.publicKeys = self.getPublicKeys()
|
||||
if not hasattr(self, "privateKeys"):
|
||||
self.privateKeys = self.getPrivateKeys()
|
||||
if not self.publicKeys or not self.privateKeys:
|
||||
raise error.ConchError("no host keys, failing")
|
||||
if not hasattr(self, "primes"):
|
||||
self.primes = self.getPrimes()
|
||||
|
||||
def buildProtocol(self, addr):
|
||||
"""
|
||||
Create an instance of the server side of the SSH protocol.
|
||||
|
||||
@type addr: L{twisted.internet.interfaces.IAddress} provider
|
||||
@param addr: The address at which the server will listen.
|
||||
|
||||
@rtype: L{twisted.conch.ssh.transport.SSHServerTransport}
|
||||
@return: The built transport.
|
||||
"""
|
||||
t = protocol.Factory.buildProtocol(self, addr)
|
||||
t.supportedPublicKeys = list(
|
||||
chain.from_iterable(
|
||||
key.supportedSignatureAlgorithms() for key in self.privateKeys.values()
|
||||
)
|
||||
)
|
||||
if not self.primes:
|
||||
self._log.info(
|
||||
"disabling non-fixed-group key exchange algorithms "
|
||||
"because we cannot find moduli file"
|
||||
)
|
||||
t.supportedKeyExchanges = [
|
||||
kexAlgorithm
|
||||
for kexAlgorithm in t.supportedKeyExchanges
|
||||
if _kex.isFixedGroup(kexAlgorithm) or _kex.isEllipticCurve(kexAlgorithm)
|
||||
]
|
||||
return t
|
||||
|
||||
def getPublicKeys(self):
|
||||
"""
|
||||
Called when the factory is started to get the public portions of the
|
||||
servers host keys. Returns a dictionary mapping SSH key types to
|
||||
public key strings.
|
||||
|
||||
@rtype: L{dict}
|
||||
"""
|
||||
raise NotImplementedError("getPublicKeys unimplemented")
|
||||
|
||||
def getPrivateKeys(self):
|
||||
"""
|
||||
Called when the factory is started to get the private portions of the
|
||||
servers host keys. Returns a dictionary mapping SSH key types to
|
||||
L{twisted.conch.ssh.keys.Key} objects.
|
||||
|
||||
@rtype: L{dict}
|
||||
"""
|
||||
raise NotImplementedError("getPrivateKeys unimplemented")
|
||||
|
||||
def getPrimes(self) -> Optional[Dict[int, List[Tuple[int, int]]]]:
|
||||
"""
|
||||
Called when the factory is started to get Diffie-Hellman generators and
|
||||
primes to use. Returns a dictionary mapping number of bits to lists of
|
||||
tuple of (generator, prime).
|
||||
"""
|
||||
|
||||
def getDHPrime(self, bits: int) -> Tuple[int, int]:
|
||||
"""
|
||||
Return a tuple of (g, p) for a Diffe-Hellman process, with p being as
|
||||
close to C{bits} bits as possible.
|
||||
"""
|
||||
|
||||
def keyfunc(i: int) -> int:
|
||||
return abs(i - bits)
|
||||
|
||||
assert self.primes is not None, "Factory should have been started by now."
|
||||
primesKeys = sorted(self.primes.keys(), key=keyfunc)
|
||||
realBits = primesKeys[0]
|
||||
return random.choice(self.primes[realBits])
|
||||
|
||||
def getService(self, transport, service):
|
||||
"""
|
||||
Return a class to use as a service for the given transport.
|
||||
|
||||
@type transport: L{transport.SSHServerTransport}
|
||||
@type service: L{bytes}
|
||||
@rtype: subclass of L{service.SSHService}
|
||||
"""
|
||||
if service == b"ssh-userauth" or hasattr(transport, "avatar"):
|
||||
return self.services[service]
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,272 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
This module contains the implementation of the TCP forwarding, which allows
|
||||
clients and servers to forward arbitrary TCP data across the connection.
|
||||
|
||||
Maintainer: Paul Swartz
|
||||
"""
|
||||
|
||||
|
||||
import struct
|
||||
|
||||
from twisted.conch.ssh import channel, common
|
||||
from twisted.internet import protocol, reactor
|
||||
from twisted.internet.endpoints import HostnameEndpoint, connectProtocol
|
||||
|
||||
|
||||
class SSHListenForwardingFactory(protocol.Factory):
|
||||
def __init__(self, connection, hostport, klass):
|
||||
self.conn = connection
|
||||
self.hostport = hostport # tuple
|
||||
self.klass = klass
|
||||
|
||||
def buildProtocol(self, addr):
|
||||
channel = self.klass(conn=self.conn)
|
||||
client = SSHForwardingClient(channel)
|
||||
channel.client = client
|
||||
addrTuple = (addr.host, addr.port)
|
||||
channelOpenData = packOpen_direct_tcpip(self.hostport, addrTuple)
|
||||
self.conn.openChannel(channel, channelOpenData)
|
||||
return client
|
||||
|
||||
|
||||
class SSHListenForwardingChannel(channel.SSHChannel):
|
||||
def channelOpen(self, specificData):
|
||||
self._log.info("opened forwarding channel {id}", id=self.id)
|
||||
if len(self.client.buf) > 1:
|
||||
b = self.client.buf[1:]
|
||||
self.write(b)
|
||||
self.client.buf = b""
|
||||
|
||||
def openFailed(self, reason):
|
||||
self.closed()
|
||||
|
||||
def dataReceived(self, data):
|
||||
self.client.transport.write(data)
|
||||
|
||||
def eofReceived(self):
|
||||
self.client.transport.loseConnection()
|
||||
|
||||
def closed(self):
|
||||
if hasattr(self, "client"):
|
||||
self._log.info("closing local forwarding channel {id}", id=self.id)
|
||||
self.client.transport.loseConnection()
|
||||
del self.client
|
||||
|
||||
|
||||
class SSHListenClientForwardingChannel(SSHListenForwardingChannel):
|
||||
name = b"direct-tcpip"
|
||||
|
||||
|
||||
class SSHListenServerForwardingChannel(SSHListenForwardingChannel):
|
||||
name = b"forwarded-tcpip"
|
||||
|
||||
|
||||
class SSHConnectForwardingChannel(channel.SSHChannel):
|
||||
"""
|
||||
Channel used for handling server side forwarding request.
|
||||
It acts as a client for the remote forwarding destination.
|
||||
|
||||
@ivar hostport: C{(host, port)} requested by client as forwarding
|
||||
destination.
|
||||
@type hostport: L{tuple} or a C{sequence}
|
||||
|
||||
@ivar client: Protocol connected to the forwarding destination.
|
||||
@type client: L{protocol.Protocol}
|
||||
|
||||
@ivar clientBuf: Data received while forwarding channel is not yet
|
||||
connected.
|
||||
@type clientBuf: L{bytes}
|
||||
|
||||
@var _reactor: Reactor used for TCP connections.
|
||||
@type _reactor: A reactor.
|
||||
|
||||
@ivar _channelOpenDeferred: Deferred used in testing to check the
|
||||
result of C{channelOpen}.
|
||||
@type _channelOpenDeferred: L{twisted.internet.defer.Deferred}
|
||||
"""
|
||||
|
||||
_reactor = reactor
|
||||
|
||||
def __init__(self, hostport, *args, **kw):
|
||||
channel.SSHChannel.__init__(self, *args, **kw)
|
||||
self.hostport = hostport
|
||||
self.client = None
|
||||
self.clientBuf = b""
|
||||
|
||||
def channelOpen(self, specificData):
|
||||
"""
|
||||
See: L{channel.SSHChannel}
|
||||
"""
|
||||
self._log.info(
|
||||
"connecting to {host}:{port}", host=self.hostport[0], port=self.hostport[1]
|
||||
)
|
||||
ep = HostnameEndpoint(self._reactor, self.hostport[0], self.hostport[1])
|
||||
d = connectProtocol(ep, SSHForwardingClient(self))
|
||||
d.addCallbacks(self._setClient, self._close)
|
||||
self._channelOpenDeferred = d
|
||||
|
||||
def _setClient(self, client):
|
||||
"""
|
||||
Called when the connection was established to the forwarding
|
||||
destination.
|
||||
|
||||
@param client: Client protocol connected to the forwarding destination.
|
||||
@type client: L{protocol.Protocol}
|
||||
"""
|
||||
self.client = client
|
||||
self._log.info(
|
||||
"connected to {host}:{port}", host=self.hostport[0], port=self.hostport[1]
|
||||
)
|
||||
if self.clientBuf:
|
||||
self.client.transport.write(self.clientBuf)
|
||||
self.clientBuf = None
|
||||
if self.client.buf[1:]:
|
||||
self.write(self.client.buf[1:])
|
||||
self.client.buf = b""
|
||||
|
||||
def _close(self, reason):
|
||||
"""
|
||||
Called when failed to connect to the forwarding destination.
|
||||
|
||||
@param reason: Reason why connection failed.
|
||||
@type reason: L{twisted.python.failure.Failure}
|
||||
"""
|
||||
self._log.error(
|
||||
"failed to connect to {host}:{port}: {reason}",
|
||||
host=self.hostport[0],
|
||||
port=self.hostport[1],
|
||||
reason=reason,
|
||||
)
|
||||
self.loseConnection()
|
||||
|
||||
def dataReceived(self, data):
|
||||
"""
|
||||
See: L{channel.SSHChannel}
|
||||
"""
|
||||
if self.client:
|
||||
self.client.transport.write(data)
|
||||
else:
|
||||
self.clientBuf += data
|
||||
|
||||
def closed(self):
|
||||
"""
|
||||
See: L{channel.SSHChannel}
|
||||
"""
|
||||
if self.client:
|
||||
self._log.info("closed remote forwarding channel {id}", id=self.id)
|
||||
if self.client.channel:
|
||||
self.loseConnection()
|
||||
self.client.transport.loseConnection()
|
||||
del self.client
|
||||
|
||||
|
||||
def openConnectForwardingClient(remoteWindow, remoteMaxPacket, data, avatar):
|
||||
remoteHP, origHP = unpackOpen_direct_tcpip(data)
|
||||
return SSHConnectForwardingChannel(
|
||||
remoteHP,
|
||||
remoteWindow=remoteWindow,
|
||||
remoteMaxPacket=remoteMaxPacket,
|
||||
avatar=avatar,
|
||||
)
|
||||
|
||||
|
||||
class SSHForwardingClient(protocol.Protocol):
|
||||
def __init__(self, channel):
|
||||
self.channel = channel
|
||||
self.buf = b"\000"
|
||||
|
||||
def dataReceived(self, data):
|
||||
if self.buf:
|
||||
self.buf += data
|
||||
else:
|
||||
self.channel.write(data)
|
||||
|
||||
def connectionLost(self, reason):
|
||||
if self.channel:
|
||||
self.channel.loseConnection()
|
||||
self.channel = None
|
||||
|
||||
|
||||
def packOpen_direct_tcpip(destination, source):
|
||||
"""
|
||||
Pack the data suitable for sending in a CHANNEL_OPEN packet.
|
||||
|
||||
@type destination: L{tuple}
|
||||
@param destination: A tuple of the (host, port) of the destination host.
|
||||
|
||||
@type source: L{tuple}
|
||||
@param source: A tuple of the (host, port) of the source host.
|
||||
"""
|
||||
(connHost, connPort) = destination
|
||||
(origHost, origPort) = source
|
||||
if isinstance(connHost, str):
|
||||
connHost = connHost.encode("utf-8")
|
||||
if isinstance(origHost, str):
|
||||
origHost = origHost.encode("utf-8")
|
||||
conn = common.NS(connHost) + struct.pack(">L", connPort)
|
||||
orig = common.NS(origHost) + struct.pack(">L", origPort)
|
||||
return conn + orig
|
||||
|
||||
|
||||
packOpen_forwarded_tcpip = packOpen_direct_tcpip
|
||||
|
||||
|
||||
def unpackOpen_direct_tcpip(data):
|
||||
"""Unpack the data to a usable format."""
|
||||
connHost, rest = common.getNS(data)
|
||||
if isinstance(connHost, bytes):
|
||||
connHost = connHost.decode("utf-8")
|
||||
connPort = int(struct.unpack(">L", rest[:4])[0])
|
||||
origHost, rest = common.getNS(rest[4:])
|
||||
if isinstance(origHost, bytes):
|
||||
origHost = origHost.decode("utf-8")
|
||||
origPort = int(struct.unpack(">L", rest[:4])[0])
|
||||
return (connHost, connPort), (origHost, origPort)
|
||||
|
||||
|
||||
unpackOpen_forwarded_tcpip = unpackOpen_direct_tcpip
|
||||
|
||||
|
||||
def packGlobal_tcpip_forward(peer):
|
||||
"""
|
||||
Pack the data for tcpip forwarding.
|
||||
|
||||
@param peer: A tuple of the (host, port) .
|
||||
@type peer: L{tuple}
|
||||
"""
|
||||
(host, port) = peer
|
||||
return common.NS(host) + struct.pack(">L", port)
|
||||
|
||||
|
||||
def unpackGlobal_tcpip_forward(data):
|
||||
host, rest = common.getNS(data)
|
||||
if isinstance(host, bytes):
|
||||
host = host.decode("utf-8")
|
||||
port = int(struct.unpack(">L", rest[:4])[0])
|
||||
return host, port
|
||||
|
||||
|
||||
"""This is how the data -> eof -> close stuff /should/ work.
|
||||
|
||||
debug3: channel 1: waiting for connection
|
||||
debug1: channel 1: connected
|
||||
debug1: channel 1: read<=0 rfd 7 len 0
|
||||
debug1: channel 1: read failed
|
||||
debug1: channel 1: close_read
|
||||
debug1: channel 1: input open -> drain
|
||||
debug1: channel 1: ibuf empty
|
||||
debug1: channel 1: send eof
|
||||
debug1: channel 1: input drain -> closed
|
||||
debug1: channel 1: rcvd eof
|
||||
debug1: channel 1: output open -> drain
|
||||
debug1: channel 1: obuf empty
|
||||
debug1: channel 1: close_write
|
||||
debug1: channel 1: output drain -> closed
|
||||
debug1: channel 1: rcvd close
|
||||
debug3: channel 1: will not send data after close
|
||||
debug1: channel 1: send close
|
||||
debug1: channel 1: is dead
|
||||
"""
|
||||
1865
backend/lib/python3.12/site-packages/twisted/conch/ssh/keys.py
Normal file
1865
backend/lib/python3.12/site-packages/twisted/conch/ssh/keys.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,56 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
The parent class for all the SSH services. Currently implemented services
|
||||
are ssh-userauth and ssh-connection.
|
||||
|
||||
Maintainer: Paul Swartz
|
||||
"""
|
||||
|
||||
from typing import Dict
|
||||
|
||||
from twisted.logger import Logger
|
||||
|
||||
|
||||
class SSHService:
|
||||
# this is the ssh name for the service:
|
||||
name: bytes = None # type:ignore[assignment]
|
||||
|
||||
protocolMessages: Dict[int, str] = {} # map #'s -> protocol names
|
||||
transport = None # gets set later
|
||||
|
||||
_log = Logger()
|
||||
|
||||
def serviceStarted(self):
|
||||
"""
|
||||
called when the service is active on the transport.
|
||||
"""
|
||||
|
||||
def serviceStopped(self):
|
||||
"""
|
||||
called when the service is stopped, either by the connection ending
|
||||
or by another service being started
|
||||
"""
|
||||
|
||||
def logPrefix(self):
|
||||
return "SSHService {!r} on {}".format(
|
||||
self.name, self.transport.transport.logPrefix()
|
||||
)
|
||||
|
||||
def packetReceived(self, messageNum, packet):
|
||||
"""
|
||||
called when we receive a packet on the transport
|
||||
"""
|
||||
# print self.protocolMessages
|
||||
if messageNum in self.protocolMessages:
|
||||
messageType = self.protocolMessages[messageNum]
|
||||
f = getattr(self, "ssh_%s" % messageType[4:], None)
|
||||
if f is not None:
|
||||
return f(packet)
|
||||
self._log.info(
|
||||
"couldn't handle {messageNum} {packet!r}",
|
||||
messageNum=messageNum,
|
||||
packet=packet,
|
||||
)
|
||||
self.transport.sendUnimplemented()
|
||||
@@ -0,0 +1,440 @@
|
||||
# -*- test-case-name: twisted.conch.test.test_session -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
This module contains the implementation of SSHSession, which (by default)
|
||||
allows access to a shell and a python interpreter over SSH.
|
||||
|
||||
Maintainer: Paul Swartz
|
||||
"""
|
||||
|
||||
|
||||
import os
|
||||
import signal
|
||||
import struct
|
||||
import sys
|
||||
|
||||
from zope.interface import implementer
|
||||
|
||||
from twisted.conch.interfaces import (
|
||||
EnvironmentVariableNotPermitted,
|
||||
ISession,
|
||||
ISessionSetEnv,
|
||||
)
|
||||
from twisted.conch.ssh import channel, common, connection
|
||||
from twisted.internet import interfaces, protocol
|
||||
from twisted.logger import Logger
|
||||
from twisted.python.compat import networkString
|
||||
|
||||
log = Logger()
|
||||
|
||||
|
||||
class SSHSession(channel.SSHChannel):
|
||||
"""
|
||||
A generalized implementation of an SSH session.
|
||||
|
||||
See RFC 4254, section 6.
|
||||
|
||||
The precise implementation of the various operations that the remote end
|
||||
can send is left up to the avatar, usually via an adapter to an
|
||||
interface such as L{ISession}.
|
||||
|
||||
@ivar buf: a buffer for data received before making a connection to a
|
||||
client.
|
||||
@type buf: L{bytes}
|
||||
@ivar client: a protocol for communication with a shell, an application
|
||||
program, or a subsystem (see RFC 4254, section 6.5).
|
||||
@type client: L{SSHSessionProcessProtocol}
|
||||
@ivar session: an object providing concrete implementations of session
|
||||
operations.
|
||||
@type session: L{ISession}
|
||||
"""
|
||||
|
||||
name = b"session"
|
||||
|
||||
def __init__(self, *args, **kw):
|
||||
channel.SSHChannel.__init__(self, *args, **kw)
|
||||
self.buf = b""
|
||||
self.client = None
|
||||
self.session = None
|
||||
|
||||
def request_subsystem(self, data):
|
||||
subsystem, ignored = common.getNS(data)
|
||||
log.info('Asking for subsystem "{subsystem}"', subsystem=subsystem)
|
||||
client = self.avatar.lookupSubsystem(subsystem, data)
|
||||
if client:
|
||||
pp = SSHSessionProcessProtocol(self)
|
||||
proto = wrapProcessProtocol(pp)
|
||||
client.makeConnection(proto)
|
||||
pp.makeConnection(wrapProtocol(client))
|
||||
self.client = pp
|
||||
return 1
|
||||
else:
|
||||
log.error("Failed to get subsystem")
|
||||
return 0
|
||||
|
||||
def request_shell(self, data):
|
||||
log.info("Getting shell")
|
||||
if not self.session:
|
||||
self.session = ISession(self.avatar)
|
||||
try:
|
||||
pp = SSHSessionProcessProtocol(self)
|
||||
self.session.openShell(pp)
|
||||
except Exception:
|
||||
log.failure("Error getting shell")
|
||||
return 0
|
||||
else:
|
||||
self.client = pp
|
||||
return 1
|
||||
|
||||
def request_exec(self, data):
|
||||
if not self.session:
|
||||
self.session = ISession(self.avatar)
|
||||
f, data = common.getNS(data)
|
||||
log.info('Executing command "{f}"', f=f)
|
||||
try:
|
||||
pp = SSHSessionProcessProtocol(self)
|
||||
self.session.execCommand(pp, f)
|
||||
except Exception:
|
||||
log.failure('Error executing command "{f}"', f=f)
|
||||
return 0
|
||||
else:
|
||||
self.client = pp
|
||||
return 1
|
||||
|
||||
def request_pty_req(self, data):
|
||||
if not self.session:
|
||||
self.session = ISession(self.avatar)
|
||||
term, windowSize, modes = parseRequest_pty_req(data)
|
||||
log.info(
|
||||
"Handling pty request: {term!r} {windowSize!r}",
|
||||
term=term,
|
||||
windowSize=windowSize,
|
||||
)
|
||||
try:
|
||||
self.session.getPty(term, windowSize, modes)
|
||||
except Exception:
|
||||
log.failure("Error handling pty request")
|
||||
return 0
|
||||
else:
|
||||
return 1
|
||||
|
||||
def request_env(self, data):
|
||||
"""
|
||||
Process a request to pass an environment variable.
|
||||
|
||||
@param data: The environment variable name and value, each encoded
|
||||
as an SSH protocol string and concatenated.
|
||||
@type data: L{bytes}
|
||||
@return: A true value if the request to pass this environment
|
||||
variable was accepted, otherwise a false value.
|
||||
"""
|
||||
if not self.session:
|
||||
self.session = ISession(self.avatar)
|
||||
if not ISessionSetEnv.providedBy(self.session):
|
||||
return 0
|
||||
name, value, data = common.getNS(data, 2)
|
||||
try:
|
||||
self.session.setEnv(name, value)
|
||||
except EnvironmentVariableNotPermitted:
|
||||
return 0
|
||||
except Exception:
|
||||
log.failure("Error setting environment variable {name}", name=name)
|
||||
return 0
|
||||
else:
|
||||
return 1
|
||||
|
||||
def request_window_change(self, data):
|
||||
if not self.session:
|
||||
self.session = ISession(self.avatar)
|
||||
winSize = parseRequest_window_change(data)
|
||||
try:
|
||||
self.session.windowChanged(winSize)
|
||||
except Exception:
|
||||
log.failure("Error changing window size")
|
||||
return 0
|
||||
else:
|
||||
return 1
|
||||
|
||||
def dataReceived(self, data):
|
||||
if not self.client:
|
||||
# self.conn.sendClose(self)
|
||||
self.buf += data
|
||||
return
|
||||
self.client.transport.write(data)
|
||||
|
||||
def extReceived(self, dataType, data):
|
||||
if dataType == connection.EXTENDED_DATA_STDERR:
|
||||
if self.client and hasattr(self.client.transport, "writeErr"):
|
||||
self.client.transport.writeErr(data)
|
||||
else:
|
||||
log.warn("Weird extended data: {dataType}", dataType=dataType)
|
||||
|
||||
def eofReceived(self):
|
||||
# If we have a session, tell it that EOF has been received and
|
||||
# expect it to send a close message (it may need to send other
|
||||
# messages such as exit-status or exit-signal first). If we don't
|
||||
# have a session, then just send a close message directly.
|
||||
if self.session:
|
||||
self.session.eofReceived()
|
||||
elif self.client:
|
||||
self.conn.sendClose(self)
|
||||
|
||||
def closed(self):
|
||||
if self.client and self.client.transport:
|
||||
self.client.transport.loseConnection()
|
||||
if self.session:
|
||||
self.session.closed()
|
||||
|
||||
# def closeReceived(self):
|
||||
# self.loseConnection() # don't know what to do with this
|
||||
|
||||
def loseConnection(self):
|
||||
if self.client:
|
||||
self.client.transport.loseConnection()
|
||||
channel.SSHChannel.loseConnection(self)
|
||||
|
||||
|
||||
class _ProtocolWrapper(protocol.ProcessProtocol):
|
||||
"""
|
||||
This class wraps a L{Protocol} instance in a L{ProcessProtocol} instance.
|
||||
"""
|
||||
|
||||
def __init__(self, proto):
|
||||
self.proto = proto
|
||||
|
||||
def connectionMade(self):
|
||||
self.proto.connectionMade()
|
||||
|
||||
def outReceived(self, data):
|
||||
self.proto.dataReceived(data)
|
||||
|
||||
def processEnded(self, reason):
|
||||
self.proto.connectionLost(reason)
|
||||
|
||||
|
||||
class _DummyTransport:
|
||||
def __init__(self, proto):
|
||||
self.proto = proto
|
||||
|
||||
def dataReceived(self, data):
|
||||
self.proto.transport.write(data)
|
||||
|
||||
def write(self, data):
|
||||
self.proto.dataReceived(data)
|
||||
|
||||
def writeSequence(self, seq):
|
||||
self.write(b"".join(seq))
|
||||
|
||||
def loseConnection(self):
|
||||
self.proto.connectionLost(protocol.connectionDone)
|
||||
|
||||
|
||||
def wrapProcessProtocol(inst):
|
||||
if isinstance(inst, protocol.Protocol):
|
||||
return _ProtocolWrapper(inst)
|
||||
else:
|
||||
return inst
|
||||
|
||||
|
||||
def wrapProtocol(proto):
|
||||
return _DummyTransport(proto)
|
||||
|
||||
|
||||
# SUPPORTED_SIGNALS is a list of signals that every session channel is supposed
|
||||
# to accept. See RFC 4254
|
||||
SUPPORTED_SIGNALS = [
|
||||
"ABRT",
|
||||
"ALRM",
|
||||
"FPE",
|
||||
"HUP",
|
||||
"ILL",
|
||||
"INT",
|
||||
"KILL",
|
||||
"PIPE",
|
||||
"QUIT",
|
||||
"SEGV",
|
||||
"TERM",
|
||||
"USR1",
|
||||
"USR2",
|
||||
]
|
||||
|
||||
|
||||
@implementer(interfaces.ITransport)
|
||||
class SSHSessionProcessProtocol(protocol.ProcessProtocol):
|
||||
"""I am both an L{IProcessProtocol} and an L{ITransport}.
|
||||
|
||||
I am a transport to the remote endpoint and a process protocol to the
|
||||
local subsystem.
|
||||
"""
|
||||
|
||||
# once initialized, a dictionary mapping signal values to strings
|
||||
# that follow RFC 4254.
|
||||
_signalValuesToNames = None
|
||||
|
||||
def __init__(self, session):
|
||||
self.session = session
|
||||
self.lostOutOrErrFlag = False
|
||||
|
||||
def connectionMade(self):
|
||||
if self.session.buf:
|
||||
self.transport.write(self.session.buf)
|
||||
self.session.buf = None
|
||||
|
||||
def outReceived(self, data):
|
||||
self.session.write(data)
|
||||
|
||||
def errReceived(self, err):
|
||||
self.session.writeExtended(connection.EXTENDED_DATA_STDERR, err)
|
||||
|
||||
def outConnectionLost(self):
|
||||
"""
|
||||
EOF should only be sent when both STDOUT and STDERR have been closed.
|
||||
"""
|
||||
if self.lostOutOrErrFlag:
|
||||
self.session.conn.sendEOF(self.session)
|
||||
else:
|
||||
self.lostOutOrErrFlag = True
|
||||
|
||||
def errConnectionLost(self):
|
||||
"""
|
||||
See outConnectionLost().
|
||||
"""
|
||||
self.outConnectionLost()
|
||||
|
||||
def connectionLost(self, reason=None):
|
||||
self.session.loseConnection()
|
||||
|
||||
def _getSignalName(self, signum):
|
||||
"""
|
||||
Get a signal name given a signal number.
|
||||
"""
|
||||
if self._signalValuesToNames is None:
|
||||
self._signalValuesToNames = {}
|
||||
# make sure that the POSIX ones are the defaults
|
||||
for signame in SUPPORTED_SIGNALS:
|
||||
signame = "SIG" + signame
|
||||
sigvalue = getattr(signal, signame, None)
|
||||
if sigvalue is not None:
|
||||
self._signalValuesToNames[sigvalue] = signame
|
||||
for k, v in signal.__dict__.items():
|
||||
# Check for platform specific signals, ignoring Python specific
|
||||
# SIG_DFL and SIG_IGN
|
||||
if k.startswith("SIG") and not k.startswith("SIG_"):
|
||||
if v not in self._signalValuesToNames:
|
||||
self._signalValuesToNames[v] = k + "@" + sys.platform
|
||||
return self._signalValuesToNames[signum]
|
||||
|
||||
def processEnded(self, reason=None):
|
||||
"""
|
||||
When we are told the process ended, try to notify the other side about
|
||||
how the process ended using the exit-signal or exit-status requests.
|
||||
Also, close the channel.
|
||||
"""
|
||||
if reason is not None:
|
||||
err = reason.value
|
||||
if err.signal is not None:
|
||||
signame = self._getSignalName(err.signal)
|
||||
if getattr(os, "WCOREDUMP", None) is not None and os.WCOREDUMP(
|
||||
err.status
|
||||
):
|
||||
log.info("exitSignal: {signame} (core dumped)", signame=signame)
|
||||
coreDumped = True
|
||||
else:
|
||||
log.info("exitSignal: {}", signame=signame)
|
||||
coreDumped = False
|
||||
self.session.conn.sendRequest(
|
||||
self.session,
|
||||
b"exit-signal",
|
||||
common.NS(networkString(signame[3:]))
|
||||
+ (b"\1" if coreDumped else b"\0")
|
||||
+ common.NS(b"")
|
||||
+ common.NS(b""),
|
||||
)
|
||||
elif err.exitCode is not None:
|
||||
log.info("exitCode: {exitCode!r}", exitCode=err.exitCode)
|
||||
self.session.conn.sendRequest(
|
||||
self.session, b"exit-status", struct.pack(">L", err.exitCode)
|
||||
)
|
||||
self.session.loseConnection()
|
||||
|
||||
def getHost(self):
|
||||
"""
|
||||
Return the host from my session's transport.
|
||||
"""
|
||||
return self.session.conn.transport.getHost()
|
||||
|
||||
def getPeer(self):
|
||||
"""
|
||||
Return the peer from my session's transport.
|
||||
"""
|
||||
return self.session.conn.transport.getPeer()
|
||||
|
||||
def write(self, data):
|
||||
self.session.write(data)
|
||||
|
||||
def writeSequence(self, seq):
|
||||
self.session.write(b"".join(seq))
|
||||
|
||||
def loseConnection(self):
|
||||
self.session.loseConnection()
|
||||
|
||||
|
||||
class SSHSessionClient(protocol.Protocol):
|
||||
def dataReceived(self, data):
|
||||
if self.transport:
|
||||
self.transport.write(data)
|
||||
|
||||
|
||||
# methods factored out to make live easier on server writers
|
||||
def parseRequest_pty_req(data):
|
||||
"""Parse the data from a pty-req request into usable data.
|
||||
|
||||
@returns: a tuple of (terminal type, (rows, cols, xpixel, ypixel), modes)
|
||||
"""
|
||||
term, rest = common.getNS(data)
|
||||
cols, rows, xpixel, ypixel = struct.unpack(">4L", rest[:16])
|
||||
modes, ignored = common.getNS(rest[16:])
|
||||
winSize = (rows, cols, xpixel, ypixel)
|
||||
modes = [
|
||||
(ord(modes[i : i + 1]), struct.unpack(">L", modes[i + 1 : i + 5])[0])
|
||||
for i in range(0, len(modes) - 1, 5)
|
||||
]
|
||||
return term, winSize, modes
|
||||
|
||||
|
||||
def packRequest_pty_req(term, geometry, modes):
|
||||
"""
|
||||
Pack a pty-req request so that it is suitable for sending.
|
||||
|
||||
NOTE: modes must be packed before being sent here.
|
||||
|
||||
@type geometry: L{tuple}
|
||||
@param geometry: A tuple of (rows, columns, xpixel, ypixel)
|
||||
"""
|
||||
(rows, cols, xpixel, ypixel) = geometry
|
||||
termPacked = common.NS(term)
|
||||
winSizePacked = struct.pack(">4L", cols, rows, xpixel, ypixel)
|
||||
modesPacked = common.NS(modes) # depend on the client packing modes
|
||||
return termPacked + winSizePacked + modesPacked
|
||||
|
||||
|
||||
def parseRequest_window_change(data):
|
||||
"""Parse the data from a window-change request into usuable data.
|
||||
|
||||
@returns: a tuple of (rows, cols, xpixel, ypixel)
|
||||
"""
|
||||
cols, rows, xpixel, ypixel = struct.unpack(">4L", data)
|
||||
return rows, cols, xpixel, ypixel
|
||||
|
||||
|
||||
def packRequest_window_change(geometry):
|
||||
"""
|
||||
Pack a window-change request so that it is suitable for sending.
|
||||
|
||||
@type geometry: L{tuple}
|
||||
@param geometry: A tuple of (rows, columns, xpixel, ypixel)
|
||||
"""
|
||||
(rows, cols, xpixel, ypixel) = geometry
|
||||
return struct.pack(">4L", cols, rows, xpixel, ypixel)
|
||||
@@ -0,0 +1,40 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
|
||||
def parse(s):
|
||||
s = s.strip()
|
||||
expr = []
|
||||
while s:
|
||||
if s[0:1] == b"(":
|
||||
newSexp = []
|
||||
if expr:
|
||||
expr[-1].append(newSexp)
|
||||
expr.append(newSexp)
|
||||
s = s[1:]
|
||||
continue
|
||||
if s[0:1] == b")":
|
||||
aList = expr.pop()
|
||||
s = s[1:]
|
||||
if not expr:
|
||||
assert not s
|
||||
return aList
|
||||
continue
|
||||
i = 0
|
||||
while s[i : i + 1].isdigit():
|
||||
i += 1
|
||||
assert i
|
||||
length = int(s[:i])
|
||||
data = s[i + 1 : i + 1 + length]
|
||||
expr[-1].append(data)
|
||||
s = s[i + 1 + length :]
|
||||
assert False, "this should not happen"
|
||||
|
||||
|
||||
def pack(sexp):
|
||||
return b"".join(
|
||||
b"(%b)" % (pack(o),)
|
||||
if type(o) in (type(()), type([]))
|
||||
else b"%d:%b" % (len(o), o)
|
||||
for o in sexp
|
||||
)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user