diff options
author | shmel1k <shmel1k@ydb.tech> | 2023-11-26 18:16:14 +0300 |
---|---|---|
committer | shmel1k <shmel1k@ydb.tech> | 2023-11-26 18:43:30 +0300 |
commit | b8cf9e88f4c5c64d9406af533d8948deb050d695 (patch) | |
tree | 218eb61fb3c3b96ec08b4d8cdfef383104a87d63 /contrib/python/Twisted/py2/twisted/conch | |
parent | 523f645a83a0ec97a0332dbc3863bb354c92a328 (diff) | |
download | ydb-b8cf9e88f4c5c64d9406af533d8948deb050d695.tar.gz |
add kikimr_configure
Diffstat (limited to 'contrib/python/Twisted/py2/twisted/conch')
51 files changed, 18401 insertions, 0 deletions
diff --git a/contrib/python/Twisted/py2/twisted/conch/__init__.py b/contrib/python/Twisted/py2/twisted/conch/__init__.py new file mode 100644 index 0000000000..adc49d01e6 --- /dev/null +++ b/contrib/python/Twisted/py2/twisted/conch/__init__.py @@ -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. +""" diff --git a/contrib/python/Twisted/py2/twisted/conch/avatar.py b/contrib/python/Twisted/py2/twisted/conch/avatar.py new file mode 100644 index 0000000000..2f6850dbb8 --- /dev/null +++ b/contrib/python/Twisted/py2/twisted/conch/avatar.py @@ -0,0 +1,45 @@ +# -*- test-case-name: twisted.conch.test.test_conch -*- + +from __future__ import absolute_import, division + +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.python import log +from twisted.python.compat import nativeString + + +@implementer(IConchUser) +class ConchUser: + def __init__(self): + self.channelLookup = {} + self.subsystemLookup = {} + + + 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): + log.msg(repr(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) diff --git a/contrib/python/Twisted/py2/twisted/conch/checkers.py b/contrib/python/Twisted/py2/twisted/conch/checkers.py new file mode 100644 index 0000000000..d4fcf5bc78 --- /dev/null +++ b/contrib/python/Twisted/py2/twisted/conch/checkers.py @@ -0,0 +1,592 @@ +# -*- 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. +""" + +from __future__ import absolute_import, division + +import sys +import binascii +import errno + +try: + import pwd +except ImportError: + pwd = None +else: + import crypt + +try: + import spwd +except ImportError: + spwd = None + +from zope.interface import providedBy, implementer, Interface + +from incremental import Version + +from twisted.conch import error +from twisted.conch.ssh import keys +from twisted.cred.checkers import ICredentialsChecker +from twisted.cred.credentials import IUsernamePassword, ISSHPrivateKey +from twisted.cred.error import UnauthorizedLogin, UnhandledCredentials +from twisted.internet import defer +from twisted.python.compat import _keys, _PY3, _b64decodebytes +from twisted.python import failure, reflect, log +from twisted.python.deprecate import deprecatedModuleAttribute +from twisted.python.util import runAsEffectiveUser +from twisted.python.filepath import FilePath + + + + +def verifyCryptedPassword(crypted, pw): + """ + Check that the password, when crypted, matches the stored crypted password. + + @param crypted: The stored crypted password. + @type crypted: L{str} + @param pw: The password the user has given. + @type pw: L{str} + + @rtype: L{bool} + """ + return crypt.crypt(pw, crypted) == crypted + + + +def _pwdGetByName(username): + """ + 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. + @type username: L{str} + """ + if pwd is None: + return None + return pwd.getpwnam(username) + + + +def _shadowGetByName(username): + """ + 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} + """ + if spwd is not None: + f = spwd.getspnam + else: + return None + return 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 valid 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. + if _PY3: + username = credentials.username.decode(sys.getfilesystemencoding()) + password = credentials.password.decode(sys.getfilesystemencoding()) + else: + username = credentials.username + password = credentials.password + + 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 = 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: # any error should be treated as a failed login + log.err() + 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 = self._userdb.getpwnam(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 = self._userdb.getpwnam(credentials.username)[2:4] + for filepath in self.getAuthorizedKeysFiles(credentials): + if not filepath.exists(): + continue + try: + lines = filepath.open() + except IOError 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 _b64decodebytes(l2[1]) == credentials.blob: + return True + except binascii.Error: + continue + return False + + + def _ebRequestAvatarId(self, f): + if not f.check(UnauthorizedLogin): + log.msg(f) + 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 = {} + + + def get_credentialInterfaces(self): + return _keys(self.checkers) + + credentialInterfaces = property(get_credentialInterfaces) + + 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, parseKey=keys.Key.fromString): + """ + 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 + @type fileobj: L{file}-like object + + @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}. + @type parseKey: L{callable} + + @return: an iterable of L{twisted.conch.ssh.keys.Key} + @rtype: iterable + + @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.msg('Unable to parse line "{0}" as a key: {1!s}' + .format(line, e)) + + + +def _keysFromFilepaths(filepaths, parseKey): + """ + 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} + @rtype: generator + + @since: 15.0 + """ + for fp in filepaths: + if fp.exists(): + try: + with fp.open() as f: + for key in readAuthorizedKeyFile(f, parseKey): + yield key + except (IOError, OSError) as e: + log.msg("Unable to read {0}: {1!s}".format(fp.path, e)) + + + +@implementer(IAuthorizedKeysDB) +class InMemorySSHKeyDB(object): + """ + 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): + """ + Initializes a new L{InMemorySSHKeyDB}. + + @param mapping: mapping of usernames to iterables of + L{twisted.conch.ssh.keys.Key}s + @type mapping: L{dict} + + """ + self._mapping = mapping + + + def getAuthorizedKeys(self, username): + return self._mapping.get(username, []) + + + +@implementer(IAuthorizedKeysDB) +class UNIXAuthorizedKeysFiles(object): + """ + 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 + """ + def __init__(self, userdb=None, parseKey=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}) + @type userdb: L{pwd}-like object + + @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}. + @type parseKey: L{callable} + """ + self._userdb = userdb + self._parseKey = parseKey + if userdb is None: + self._userdb = pwd + + + def getAuthorizedKeys(self, username): + try: + passwd = self._userdb.getpwnam(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(object): + """ + 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): + """ + Initializes a L{SSHPublicKeyChecker}. + + @param keydb: a provider of L{IAuthorizedKeysDB} + @type keydb: L{IAuthorizedKeysDB} provider + """ + self._keydb = keydb + + + def requestAvatarId(self, credentials): + d = defer.maybeDeferred(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: # Any error should be treated as a failed login + log.err() + raise UnauthorizedLogin('Error while verifying key') + + raise UnauthorizedLogin("Key signature invalid.") diff --git a/contrib/python/Twisted/py2/twisted/conch/client/__init__.py b/contrib/python/Twisted/py2/twisted/conch/client/__init__.py new file mode 100644 index 0000000000..f55d474db4 --- /dev/null +++ b/contrib/python/Twisted/py2/twisted/conch/client/__init__.py @@ -0,0 +1,9 @@ +# Copyright (c) Twisted Matrix Laboratories. +# See LICENSE for details. + +# +""" +Client support code for Conch. + +Maintainer: Paul Swartz +""" diff --git a/contrib/python/Twisted/py2/twisted/conch/client/agent.py b/contrib/python/Twisted/py2/twisted/conch/client/agent.py new file mode 100644 index 0000000000..fdf08356f1 --- /dev/null +++ b/contrib/python/Twisted/py2/twisted/conch/client/agent.py @@ -0,0 +1,73 @@ +# -*- 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.python import log + + + +class SSHAgentClient(agent.SSHAgentClient): + + def __init__(self): + agent.SSHAgentClient.__init__(self) + self.blobs = [] + + + def getPublicKeys(self): + return self.requestIdentities().addCallback(self._cbPublicKeys) + + + def _cbPublicKeys(self, blobcomm): + log.msg('got %i public 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 diff --git a/contrib/python/Twisted/py2/twisted/conch/client/connect.py b/contrib/python/Twisted/py2/twisted/conch/client/connect.py new file mode 100644 index 0000000000..ac47187e49 --- /dev/null +++ b/contrib/python/Twisted/py2/twisted/conch/client/connect.py @@ -0,0 +1,21 @@ +# 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 diff --git a/contrib/python/Twisted/py2/twisted/conch/client/default.py b/contrib/python/Twisted/py2/twisted/conch/client/default.py new file mode 100644 index 0000000000..ff2d635314 --- /dev/null +++ b/contrib/python/Twisted/py2/twisted/conch/client/default.py @@ -0,0 +1,349 @@ +# -*- 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}. +""" + +from __future__ import print_function + +from twisted.python import log +from twisted.python.compat import ( + nativeString, raw_input, _PY3, _b64decodebytes as decodebytes) +from twisted.python.filepath import FilePath + +from twisted.conch.error import ConchError +from twisted.conch.ssh import common, keys, userauth +from twisted.internet import defer, protocol, reactor + +from twisted.conch.client.knownhosts import KnownHostsFile, ConsoleUI + +from twisted.conch.client import agent + +import os, sys, getpass, contextlib + +if _PY3: + import io + +# 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 + +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 IOError: + 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: + 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']: + log.msg('using agent') + 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, IOError): + print() + raise ConchError('PEBKAC') + + + def getPassword(self, prompt = None): + if prompt: + prompt = nativeString(prompt) + else: + prompt = ("%s@%s's password: " % + (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] + log.msg(str(self.options.identitys)) + log.msg(str(files)) + 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 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(raw_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. + A L{tuple} of binary files on Python 2. + """ + stdin = open("/dev/tty", "rb") + stdout = open("/dev/tty", "wb") + if _PY3: + stdin = io.TextIOWrapper(stdin) + stdout = io.TextIOWrapper(stdout) + 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 diff --git a/contrib/python/Twisted/py2/twisted/conch/client/direct.py b/contrib/python/Twisted/py2/twisted/conch/client/direct.py new file mode 100644 index 0000000000..601a9d2dc3 --- /dev/null +++ b/contrib/python/Twisted/py2/twisted/conch/client/direct.py @@ -0,0 +1,109 @@ +# Copyright (c) Twisted Matrix Laboratories. +# See LICENSE for details. + + +from __future__ import print_function + +from twisted.internet import defer, protocol, reactor +from twisted.conch import error +from twisted.conch.ssh import transport +from twisted.python import log + + + +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): + log.msg('Received Debug Message: %s' % message) + 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): + log.msg('setting client server to %s' % 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 diff --git a/contrib/python/Twisted/py2/twisted/conch/client/knownhosts.py b/contrib/python/Twisted/py2/twisted/conch/client/knownhosts.py new file mode 100644 index 0000000000..aa0a622d41 --- /dev/null +++ b/contrib/python/Twisted/py2/twisted/conch/client/knownhosts.py @@ -0,0 +1,630 @@ +# -*- 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 absolute_import, division + +import hmac +from binascii import Error as DecodeError, b2a_base64, a2b_base64 +from contextlib import closing +from hashlib import sha1 +import sys + +from zope.interface import implementer + +from twisted.conch.interfaces import IKnownHostEntry +from twisted.conch.error import HostKeyChanged, UserRejectedKey, InvalidEntry +from twisted.conch.ssh.keys import Key, BadKeyError, FingerprintFormats +from twisted.internet import defer +from twisted.python import log +from twisted.python.compat import nativeString, unicode +from twisted.python.randbytes import secureRandom +from twisted.python.util import FancyEqMixin + + +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(object): + """ + 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. + @type _hostnames: L{list} of L{bytes} + """ + + def __init__(self, hostnames, keyType, publicKey, comment): + self._hostnames = hostnames + super(PlainEntry, self).__init__(keyType, publicKey, comment) + + + @classmethod + def fromString(cls, string): + """ + 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". + + @type string: L{bytes} + + @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): + """ + Check to see if this entry matches a given hostname. + + @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} + """ + if isinstance(hostname, unicode): + hostname = hostname.encode("utf-8") + return hostname in self._hostnames + + + def toString(self): + """ + 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. + @rtype: L{bytes} + """ + 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(object): + """ + 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, unicode): + 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, hostHash, keyType, publicKey, comment): + self._hostSalt = hostSalt + self._hostHash = hostHash + super(HashedEntry, self).__init__(keyType, publicKey, comment) + + + @classmethod + def fromString(cls, string): + """ + 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. + @type string: L{bytes} + + @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 (_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(object): + """ + 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): + """ + 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 = [] + self._savePath = savePath + self._clobber = True + + + @property + def savePath(self): + """ + @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 IOError: + 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, hostname, ip, key): + """ + 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.maybeDeferred(self.hasHostKey, hostname, key) + def gotHasKey(result): + if result: + if not self.hasHostKey(ip, key): + ui.warn("Warning: Permanently added the %s host key for " + "IP address '%s' to the list of known hosts." % + (key.type(), nativeString(ip))) + self.addHostKey(ip, key) + self.save() + return result + else: + def promptResponse(response): + if response: + self.addHostKey(hostname, key) + self.addHostKey(ip, key) + self.save() + return response + else: + raise UserRejectedKey() + + keytype = 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, key): + """ + 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): + """ + Save this L{KnownHostsFile} to the path it was loaded from. + """ + p = self._savePath.parent() + if not p.isdir(): + p.makedirs() + + if self._clobber: + mode = "wb" + else: + mode = "ab" + + 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): + """ + 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. + @type path: L{FilePath} + + @return: A L{KnownHostsFile} initialized with entries from C{path}. + @rtype: L{KnownHostsFile} + """ + knownHosts = cls(path) + knownHosts._clobber = False + return knownHosts + + + +class ConsoleUI(object): + """ + 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): + """ + @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): + """ + 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 == b'no': + return False + else: + f.write(b"Please type 'yes' or 'no': ") + return d.addCallback(body) + + + def warn(self, text): + """ + 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. + @type text: L{bytes} + """ + try: + with closing(self.opener()) as f: + f.write(text) + except: + log.err() diff --git a/contrib/python/Twisted/py2/twisted/conch/client/options.py b/contrib/python/Twisted/py2/twisted/conch/client/options.py new file mode 100644 index 0000000000..5630fce250 --- /dev/null +++ b/contrib/python/Twisted/py2/twisted/conch/client/options.py @@ -0,0 +1,103 @@ +# Copyright (c) Twisted Matrix Laboratories. +# See LICENSE for details. + +# +from twisted.conch.ssh.transport import SSHClientTransport, SSHCiphers +from twisted.python import usage +from twisted.python.compat import unicode + +import sys + +class ConchOptions(usage.Options): + + optParameters = [['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( + SSHCiphers.cipherMap.keys(), + descr='ciphers to choose from'), + "macs": usage.CompleteMultiList( + SSHCiphers.macMap.keys(), + descr='macs to choose from'), + "host-key-algorithms": usage.CompleteMultiList( + 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, unicode): + 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, unicode): + 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, unicode): + 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'] diff --git a/contrib/python/Twisted/py2/twisted/conch/endpoints.py b/contrib/python/Twisted/py2/twisted/conch/endpoints.py new file mode 100644 index 0000000000..2e19d0870f --- /dev/null +++ b/contrib/python/Twisted/py2/twisted/conch/endpoints.py @@ -0,0 +1,872 @@ +# -*- test-case-name: twisted.conch.test.test_endpoints -*- +# Copyright (c) Twisted Matrix Laboratories. +# See LICENSE for details. + +""" +Endpoint implementations of various SSH interactions. +""" + +__all__ = [ + 'AuthenticationFailed', 'SSHCommandAddress', 'SSHCommandClientEndpoint'] + +from struct import unpack +from os.path import expanduser + +import signal + +from zope.interface import Interface, implementer + +from twisted.logger import Logger +from twisted.python.compat import nativeString, networkString +from twisted.python.filepath import FilePath +from twisted.python.failure import Failure +from twisted.internet.error import ConnectionDone, ProcessTerminated +from twisted.internet.interfaces import IStreamClientEndpoint +from twisted.internet.protocol import Factory +from twisted.internet.defer import Deferred, succeed, CancelledError +from twisted.internet.endpoints import TCP4ClientEndpoint, connectProtocol + +from twisted.conch.ssh.keys import Key +from twisted.conch.ssh.common import getNS, NS +from twisted.conch.ssh.transport import SSHClientTransport +from twisted.conch.ssh.connection import SSHConnection +from twisted.conch.ssh.userauth import SSHUserAuthClient +from twisted.conch.ssh.channel import SSHChannel +from twisted.conch.client.knownhosts import ConsoleUI, KnownHostsFile +from twisted.conch.client.agent import SSHAgentClient +from twisted.conch.client.default import _KNOWN_HOSTS + + +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(object): + """ + 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 = "SIG%s" % (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(object): + """ + 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 + + + +class _ReadFile(object): + """ + A weakly file-like object which can be used with L{KnownHostsFile} to + respond in the negative to all prompts for decisions. + """ + def __init__(self, contents): + """ + @param contents: L{bytes} which will be returned from every C{readline} + call. + """ + self._contents = contents + + + def write(self, data): + """ + No-op. + + @param data: ignored + """ + + + def readline(self, count=-1): + """ + Always give back the byte string that this L{_ReadFile} was initialized + with. + + @param count: ignored + + @return: A fixed byte-string. + @rtype: L{bytes} + """ + return self._contents + + + def close(self): + """ + No-op. + """ + + + +@implementer(_ISSHConnectionCreator) +class _NewConnectionHelper(object): + """ + 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, hostname, port, command, username, keys, + password, agentEndpoint, knownHosts, ui, + tty=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 = tty + + + def _opener(self): + """ + 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("rb+") + except: + # Give back a file-like object from which can be read a byte string + # that KnownHostsFile recognizes as rejecting some option (b"no"). + return _ReadFile(b"no") + + + @classmethod + def _knownHosts(cls): + """ + + @return: A L{KnownHostsFile} instance pointed at the user's personal + I{known hosts} file. + @type: 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(object): + """ + 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}. + """ diff --git a/contrib/python/Twisted/py2/twisted/conch/error.py b/contrib/python/Twisted/py2/twisted/conch/error.py new file mode 100644 index 0000000000..c8297c3964 --- /dev/null +++ b/contrib/python/Twisted/py2/twisted/conch/error.py @@ -0,0 +1,103 @@ +# Copyright (c) Twisted Matrix Laboratories. +# See LICENSE for details. + +""" +An error to represent bad things happening in Conch. + +Maintainer: Paul Swartz +""" + +from __future__ import absolute_import, division + +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 diff --git a/contrib/python/Twisted/py2/twisted/conch/insults/__init__.py b/contrib/python/Twisted/py2/twisted/conch/insults/__init__.py new file mode 100644 index 0000000000..3d83876698 --- /dev/null +++ b/contrib/python/Twisted/py2/twisted/conch/insults/__init__.py @@ -0,0 +1,4 @@ +""" +Insults: a replacement for Curses/S-Lang. + +Very basic at the moment.""" diff --git a/contrib/python/Twisted/py2/twisted/conch/insults/helper.py b/contrib/python/Twisted/py2/twisted/conch/insults/helper.py new file mode 100644 index 0000000000..0485bfdbe6 --- /dev/null +++ b/contrib/python/Twisted/py2/twisted/conch/insults/helper.py @@ -0,0 +1,517 @@ +# -*- 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 +""" + +from __future__ import print_function + +import re, string + +from zope.interface import implementer + +from incremental import Version + +from twisted.internet import defer, protocol, reactor +from twisted.python import log, _textattributes +from twisted.python.compat import iterbytes +from twisted.python.deprecate import deprecated, deprecatedModuleAttribute +from twisted.conch.insults import insults + +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() + + 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: + log.msg("Unknown graphic rendition attribute: " + repr(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: + log.msg("Unknown graphic rendition attribute: " + repr(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) + + + +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'] diff --git a/contrib/python/Twisted/py2/twisted/conch/insults/insults.py b/contrib/python/Twisted/py2/twisted/conch/insults/insults.py new file mode 100644 index 0000000000..a583174415 --- /dev/null +++ b/contrib/python/Twisted/py2/twisted/conch/insults/insults.py @@ -0,0 +1,1289 @@ +# -*- test-case-name: twisted.conch.test.test_insults -*- +# Copyright (c) Twisted Matrix Laboratories. +# See LICENSE for details. + +""" +VT102 and VT220 terminal manipulation. + +@author: Jp Calderone +""" + +from zope.interface import implementer, Interface + +from twisted.internet import protocol, defer, interfaces as iinternet +from twisted.python.compat import intToBytes, iterbytes, networkString + + + +class ITerminalProtocol(Interface): + def makeConnection(transport): + """ + Called with an L{ITerminalTransport} when a connection is established. + """ + + def keystrokeReceived(keyID, modifier): + """ + A keystroke was received. + + Each keystroke corresponds to one invocation of this method. + keyID is a string identifier for that key. Printable characters + are represented by themselves. Control keys, such as arrows and + function keys, are represented with symbolic constants on + L{ServerProtocol}. + """ + + def terminalSize(width, height): + """ + Called to indicate the size of the terminal. + + A terminal of 80x24 should be assumed if this method is not + called. This method might not be called for real terminals. + """ + + def unhandledControlSequence(seq): + """ + Called when an unsupported control sequence is received. + + @type seq: L{str} + @param seq: The whole control sequence which could not be interpreted. + """ + + def connectionLost(reason): + """ + Called when the connection has been lost. + + reason is a Failure describing why. + """ + + + +@implementer(ITerminalProtocol) +class TerminalProtocol(object): + def makeConnection(self, terminal): + # assert ITerminalTransport.providedBy(transport), "TerminalProtocol.makeConnection must be passed an ITerminalTransport implementor" + self.terminal = terminal + self.connectionMade() + + + def connectionMade(self): + """ + Called after a connection has been established. + """ + + + def keystrokeReceived(self, keyID, modifier): + pass + + + def terminalSize(self, width, height): + pass + + + def unhandledControlSequence(self, seq): + pass + + + def connectionLost(self, reason): + pass + + + +class ITerminalTransport(iinternet.ITransport): + def cursorUp(n=1): + """ + Move the cursor up n lines. + """ + + + def cursorDown(n=1): + """ + Move the cursor down n lines. + """ + + + def cursorForward(n=1): + """ + Move the cursor right n columns. + """ + + + def cursorBackward(n=1): + """ + Move the cursor left n columns. + """ + + + def cursorPosition(column, line): + """ + Move the cursor to the given line and column. + """ + + + def cursorHome(): + """ + Move the cursor home. + """ + + + def index(): + """ + Move the cursor down one line, performing scrolling if necessary. + """ + + + def reverseIndex(): + """ + Move the cursor up one line, performing scrolling if necessary. + """ + + + def nextLine(): + """ + Move the cursor to the first position on the next line, performing scrolling if necessary. + """ + + + def saveCursor(): + """ + Save the cursor position, character attribute, character set, and origin mode selection. + """ + + + def restoreCursor(): + """ + Restore the previously saved cursor position, character attribute, character set, and origin mode selection. + + If no cursor state was previously saved, move the cursor to the home position. + """ + + + def setModes(modes): + """ + Set the given modes on the terminal. + """ + + def resetModes(mode): + """ + Reset the given modes on the terminal. + """ + + + def setPrivateModes(modes): + """ + Set the given DEC private modes on the terminal. + """ + + + def resetPrivateModes(modes): + """ + Reset the given DEC private modes on the terminal. + """ + + + def applicationKeypadMode(): + """ + Cause keypad to generate control functions. + + Cursor key mode selects the type of characters generated by cursor keys. + """ + + + def numericKeypadMode(): + """ + Cause keypad to generate normal characters. + """ + + + def selectCharacterSet(charSet, which): + """ + Select a character set. + + charSet should be one of CS_US, CS_UK, CS_DRAWING, CS_ALTERNATE, or + CS_ALTERNATE_SPECIAL. + + which should be one of G0 or G1. + """ + + + def shiftIn(): + """ + Activate the G0 character set. + """ + + + def shiftOut(): + """ + Activate the G1 character set. + """ + + + def singleShift2(): + """ + Shift to the G2 character set for a single character. + """ + + + def singleShift3(): + """ + Shift to the G3 character set for a single character. + """ + + + def selectGraphicRendition(*attributes): + """ + Enabled one or more character attributes. + + Arguments should be one or more of UNDERLINE, REVERSE_VIDEO, BLINK, or BOLD. + NORMAL may also be specified to disable all character attributes. + """ + + + def horizontalTabulationSet(): + """ + Set a tab stop at the current cursor position. + """ + + + def tabulationClear(): + """ + Clear the tab stop at the current cursor position. + """ + + + def tabulationClearAll(): + """ + Clear all tab stops. + """ + + + def doubleHeightLine(top=True): + """ + Make the current line the top or bottom half of a double-height, double-width line. + + If top is True, the current line is the top half. Otherwise, it is the bottom half. + """ + + + def singleWidthLine(): + """ + Make the current line a single-width, single-height line. + """ + + + def doubleWidthLine(): + """ + Make the current line a double-width line. + """ + + + def eraseToLineEnd(): + """ + Erase from the cursor to the end of line, including cursor position. + """ + + + def eraseToLineBeginning(): + """ + Erase from the cursor to the beginning of the line, including the cursor position. + """ + + + def eraseLine(): + """ + Erase the entire cursor line. + """ + + + def eraseToDisplayEnd(): + """ + Erase from the cursor to the end of the display, including the cursor position. + """ + + + def eraseToDisplayBeginning(): + """ + Erase from the cursor to the beginning of the display, including the cursor position. + """ + + + def eraseDisplay(): + """ + Erase the entire display. + """ + + + def deleteCharacter(n=1): + """ + Delete n characters starting at the cursor position. + + Characters to the right of deleted characters are shifted to the left. + """ + + + def insertLine(n=1): + """ + Insert n lines at the cursor position. + + Lines below the cursor are shifted down. Lines moved past the bottom margin are lost. + This command is ignored when the cursor is outside the scroll region. + """ + + + def deleteLine(n=1): + """ + Delete n lines starting at the cursor position. + + Lines below the cursor are shifted up. This command is ignored when the cursor is outside + the scroll region. + """ + + + def reportCursorPosition(): + """ + Return a Deferred that fires with a two-tuple of (x, y) indicating the cursor position. + """ + + + def reset(): + """ + Reset the terminal to its initial state. + """ + + + def unhandledControlSequence(seq): + """ + Called when an unsupported control sequence is received. + + @type seq: L{str} + @param seq: The whole control sequence which could not be interpreted. + """ + + +CSI = b'\x1b' +CST = {b'~': b'tilde'} + +class modes: + """ + ECMA 48 standardized modes + """ + + # BREAKS YOPUR KEYBOARD MOFO + KEYBOARD_ACTION = KAM = 2 + + # When set, enables character insertion. New display characters + # move old display characters to the right. Characters moved past + # the right margin are lost. + + # When reset, enables replacement mode (disables character + # insertion). New display characters replace old display + # characters at cursor position. The old character is erased. + INSERTION_REPLACEMENT = IRM = 4 + + # Set causes a received linefeed, form feed, or vertical tab to + # move cursor to first column of next line. RETURN transmits both + # a carriage return and linefeed. This selection is also called + # new line option. + + # Reset causes a received linefeed, form feed, or vertical tab to + # move cursor to next line in current column. RETURN transmits a + # carriage return. + LINEFEED_NEWLINE = LNM = 20 + + + +class privateModes: + """ + ANSI-Compatible Private Modes + """ + ERROR = 0 + CURSOR_KEY = 1 + ANSI_VT52 = 2 + COLUMN = 3 + SCROLL = 4 + SCREEN = 5 + ORIGIN = 6 + AUTO_WRAP = 7 + AUTO_REPEAT = 8 + PRINTER_FORM_FEED = 18 + PRINTER_EXTENT = 19 + + # Toggle cursor visibility (reset hides it) + CURSOR_MODE = 25 + + +# Character sets +CS_US = b'CS_US' +CS_UK = b'CS_UK' +CS_DRAWING = b'CS_DRAWING' +CS_ALTERNATE = b'CS_ALTERNATE' +CS_ALTERNATE_SPECIAL = b'CS_ALTERNATE_SPECIAL' + +# Groupings (or something?? These are like variables that can be bound to character sets) +G0 = b'G0' +G1 = b'G1' + +# G2 and G3 cannot be changed, but they can be shifted to. +G2 = b'G2' +G3 = b'G3' + +# Character attributes + +NORMAL = 0 +BOLD = 1 +UNDERLINE = 4 +BLINK = 5 +REVERSE_VIDEO = 7 + +class Vector: + def __init__(self, x, y): + self.x = x + self.y = y + + + +def log(s): + with open('log', 'a') as f: + f.write(str(s) + '\n') + +# XXX TODO - These attributes are really part of the +# ITerminalTransport interface, I think. +_KEY_NAMES = ('UP_ARROW', 'DOWN_ARROW', 'RIGHT_ARROW', 'LEFT_ARROW', + 'HOME', 'INSERT', 'DELETE', 'END', 'PGUP', 'PGDN', 'NUMPAD_MIDDLE', + 'F1', 'F2', 'F3', 'F4', 'F5', 'F6', 'F7', 'F8', 'F9', + 'F10', 'F11', 'F12', + + 'ALT', 'SHIFT', 'CONTROL') + +class _const(object): + """ + @ivar name: A string naming this constant + """ + def __init__(self, name): + self.name = name + + + def __repr__(self): + return '[' + self.name + ']' + + + def __bytes__(self): + return ('[' + self.name + ']').encode("ascii") + + +FUNCTION_KEYS = [ + _const(_name).__bytes__() for _name in _KEY_NAMES] + + + +@implementer(ITerminalTransport) +class ServerProtocol(protocol.Protocol): + protocolFactory = None + terminalProtocol = None + + TAB = b'\t' + BACKSPACE = b'\x7f' + ## + + lastWrite = b'' + + state = b'data' + + termSize = Vector(80, 24) + cursorPos = Vector(0, 0) + scrollRegion = None + + # Factory who instantiated me + factory = None + + def __init__(self, protocolFactory=None, *a, **kw): + """ + @param protocolFactory: A callable which will be invoked with + *a, **kw and should return an ITerminalProtocol implementor. + This will be invoked when a connection to this ServerProtocol + is established. + + @param a: Any positional arguments to pass to protocolFactory. + @param kw: Any keyword arguments to pass to protocolFactory. + """ + # assert protocolFactory is None or ITerminalProtocol.implementedBy(protocolFactory), "ServerProtocol.__init__ must be passed an ITerminalProtocol implementor" + if protocolFactory is not None: + self.protocolFactory = protocolFactory + self.protocolArgs = a + self.protocolKwArgs = kw + + self._cursorReports = [] + + + def connectionMade(self): + if self.protocolFactory is not None: + self.terminalProtocol = self.protocolFactory(*self.protocolArgs, **self.protocolKwArgs) + + try: + factory = self.factory + except AttributeError: + pass + else: + self.terminalProtocol.factory = factory + + self.terminalProtocol.makeConnection(self) + + + def dataReceived(self, data): + for ch in iterbytes(data): + if self.state == b'data': + if ch == b'\x1b': + self.state = b'escaped' + else: + self.terminalProtocol.keystrokeReceived(ch, None) + elif self.state == b'escaped': + if ch == b'[': + self.state = b'bracket-escaped' + self.escBuf = [] + elif ch == b'O': + self.state = b'low-function-escaped' + else: + self.state = b'data' + self._handleShortControlSequence(ch) + elif self.state == b'bracket-escaped': + if ch == b'O': + self.state = b'low-function-escaped' + elif ch.isalpha() or ch == b'~': + self._handleControlSequence(b''.join(self.escBuf) + ch) + del self.escBuf + self.state = b'data' + else: + self.escBuf.append(ch) + elif self.state == b'low-function-escaped': + self._handleLowFunctionControlSequence(ch) + self.state = b'data' + else: + raise ValueError("Illegal state") + + + def _handleShortControlSequence(self, ch): + self.terminalProtocol.keystrokeReceived(ch, self.ALT) + + + def _handleControlSequence(self, buf): + buf = b'\x1b[' + buf + f = getattr(self.controlSequenceParser, + CST.get(buf[-1:], buf[-1:]).decode("ascii"), + None) + if f is None: + self.unhandledControlSequence(buf) + else: + f(self, self.terminalProtocol, buf[:-1]) + + + def unhandledControlSequence(self, buf): + self.terminalProtocol.unhandledControlSequence(buf) + + + def _handleLowFunctionControlSequence(self, ch): + functionKeys = {b'P': self.F1, b'Q': self.F2, + b'R': self.F3, b'S': self.F4} + keyID = functionKeys.get(ch) + if keyID is not None: + self.terminalProtocol.keystrokeReceived(keyID, None) + else: + self.terminalProtocol.unhandledControlSequence(b'\x1b[O' + ch) + + + class ControlSequenceParser: + def A(self, proto, handler, buf): + if buf == b'\x1b[': + handler.keystrokeReceived(proto.UP_ARROW, None) + else: + handler.unhandledControlSequence(buf + b'A') + + + def B(self, proto, handler, buf): + if buf == b'\x1b[': + handler.keystrokeReceived(proto.DOWN_ARROW, None) + else: + handler.unhandledControlSequence(buf + b'B') + + + def C(self, proto, handler, buf): + if buf == b'\x1b[': + handler.keystrokeReceived(proto.RIGHT_ARROW, None) + else: + handler.unhandledControlSequence(buf + b'C') + + + def D(self, proto, handler, buf): + if buf == b'\x1b[': + handler.keystrokeReceived(proto.LEFT_ARROW, None) + else: + handler.unhandledControlSequence(buf + b'D') + + + def E(self, proto, handler, buf): + if buf == b'\x1b[': + handler.keystrokeReceived(proto.NUMPAD_MIDDLE, None) + else: + handler.unhandledControlSequence(buf + b'E') + + + def F(self, proto, handler, buf): + if buf == b'\x1b[': + handler.keystrokeReceived(proto.END, None) + else: + handler.unhandledControlSequence(buf + b'F') + + + def H(self, proto, handler, buf): + if buf == b'\x1b[': + handler.keystrokeReceived(proto.HOME, None) + else: + handler.unhandledControlSequence(buf + b'H') + + + def R(self, proto, handler, buf): + if not proto._cursorReports: + handler.unhandledControlSequence(buf + b'R') + elif buf.startswith(b'\x1b['): + report = buf[2:] + parts = report.split(b';') + if len(parts) != 2: + handler.unhandledControlSequence(buf + b'R') + else: + Pl, Pc = parts + try: + Pl, Pc = int(Pl), int(Pc) + except ValueError: + handler.unhandledControlSequence(buf + b'R') + else: + d = proto._cursorReports.pop(0) + d.callback((Pc - 1, Pl - 1)) + else: + handler.unhandledControlSequence(buf + b'R') + + + def Z(self, proto, handler, buf): + if buf == b'\x1b[': + handler.keystrokeReceived(proto.TAB, proto.SHIFT) + else: + handler.unhandledControlSequence(buf + b'Z') + + + def tilde(self, proto, handler, buf): + map = {1: proto.HOME, 2: proto.INSERT, 3: proto.DELETE, + 4: proto.END, 5: proto.PGUP, 6: proto.PGDN, + + 15: proto.F5, 17: proto.F6, 18: proto.F7, + 19: proto.F8, 20: proto.F9, 21: proto.F10, + 23: proto.F11, 24: proto.F12} + + if buf.startswith(b'\x1b['): + ch = buf[2:] + try: + v = int(ch) + except ValueError: + handler.unhandledControlSequence(buf + b'~') + else: + symbolic = map.get(v) + if symbolic is not None: + handler.keystrokeReceived(map[v], None) + else: + handler.unhandledControlSequence(buf + b'~') + else: + handler.unhandledControlSequence(buf + b'~') + + controlSequenceParser = ControlSequenceParser() + + + # ITerminalTransport + def cursorUp(self, n=1): + assert n >= 1 + self.cursorPos.y = max(self.cursorPos.y - n, 0) + self.write(b'\x1b[' + intToBytes(n) + b'A') + + + def cursorDown(self, n=1): + assert n >= 1 + self.cursorPos.y = min(self.cursorPos.y + n, self.termSize.y - 1) + self.write(b'\x1b[' + intToBytes(n) + b'B') + + + def cursorForward(self, n=1): + assert n >= 1 + self.cursorPos.x = min(self.cursorPos.x + n, self.termSize.x - 1) + self.write(b'\x1b[' + intToBytes(n) + b'C') + + + def cursorBackward(self, n=1): + assert n >= 1 + self.cursorPos.x = max(self.cursorPos.x - n, 0) + self.write(b'\x1b[' + intToBytes(n) + b'D') + + + def cursorPosition(self, column, line): + self.write(b'\x1b[' + + intToBytes(line + 1) + + b';' + + intToBytes(column + 1) + + b'H') + + + def cursorHome(self): + self.cursorPos.x = self.cursorPos.y = 0 + self.write(b'\x1b[H') + + + def index(self): + # ECMA48 5th Edition removes this + self.cursorPos.y = min(self.cursorPos.y + 1, self.termSize.y - 1) + self.write(b'\x1bD') + + + def reverseIndex(self): + self.cursorPos.y = max(self.cursorPos.y - 1, 0) + self.write(b'\x1bM') + + + def nextLine(self): + self.cursorPos.x = 0 + self.cursorPos.y = min(self.cursorPos.y + 1, self.termSize.y - 1) + self.write(b'\n') + + + def saveCursor(self): + self._savedCursorPos = Vector(self.cursorPos.x, self.cursorPos.y) + self.write(b'\x1b7') + + + def restoreCursor(self): + self.cursorPos = self._savedCursorPos + del self._savedCursorPos + self.write(b'\x1b8') + + + def setModes(self, modes): + # XXX Support ANSI-Compatible private modes + modesBytes = b';'.join([intToBytes(mode) for mode in modes]) + self.write(b'\x1b[' + modesBytes + b'h') + + + def setPrivateModes(self, modes): + modesBytes = b';'.join([intToBytes(mode) for mode in modes]) + self.write(b'\x1b[?' + modesBytes + b'h') + + + def resetModes(self, modes): + # XXX Support ANSI-Compatible private modes + modesBytes = b';'.join([intToBytes(mode) for mode in modes]) + self.write(b'\x1b[' + modesBytes + b'l') + + + def resetPrivateModes(self, modes): + modesBytes = b';'.join([intToBytes(mode) for mode in modes]) + self.write(b'\x1b[?' + modesBytes + b'l') + + + def applicationKeypadMode(self): + self.write(b'\x1b=') + + + def numericKeypadMode(self): + self.write(b'\x1b>') + + + def selectCharacterSet(self, charSet, which): + # XXX Rewrite these as dict lookups + if which == G0: + which = b'(' + elif which == G1: + which = b')' + else: + raise ValueError("`which' argument to selectCharacterSet must be G0 or G1") + if charSet == CS_UK: + charSet = b'A' + elif charSet == CS_US: + charSet = b'B' + elif charSet == CS_DRAWING: + charSet = b'0' + elif charSet == CS_ALTERNATE: + charSet = b'1' + elif charSet == CS_ALTERNATE_SPECIAL: + charSet = b'2' + else: + raise ValueError("Invalid `charSet' argument to selectCharacterSet") + self.write(b'\x1b' + which + charSet) + + + def shiftIn(self): + self.write(b'\x15') + + + def shiftOut(self): + self.write(b'\x14') + + + def singleShift2(self): + self.write(b'\x1bN') + + + def singleShift3(self): + self.write(b'\x1bO') + + + def selectGraphicRendition(self, *attributes): + # each member of attributes must be a native string + attrs = [] + for a in attributes: + attrs.append(networkString(a)) + self.write(b'\x1b[' + + b';'.join(attrs) + + b'm') + + + def horizontalTabulationSet(self): + self.write(b'\x1bH') + + + def tabulationClear(self): + self.write(b'\x1b[q') + + + def tabulationClearAll(self): + self.write(b'\x1b[3q') + + + def doubleHeightLine(self, top=True): + if top: + self.write(b'\x1b#3') + else: + self.write(b'\x1b#4') + + + def singleWidthLine(self): + self.write(b'\x1b#5') + + + def doubleWidthLine(self): + self.write(b'\x1b#6') + + + def eraseToLineEnd(self): + self.write(b'\x1b[K') + + + def eraseToLineBeginning(self): + self.write(b'\x1b[1K') + + + def eraseLine(self): + self.write(b'\x1b[2K') + + + def eraseToDisplayEnd(self): + self.write(b'\x1b[J') + + + def eraseToDisplayBeginning(self): + self.write(b'\x1b[1J') + + + def eraseDisplay(self): + self.write(b'\x1b[2J') + + + def deleteCharacter(self, n=1): + self.write(b'\x1b[' + intToBytes(n) + b'P') + + + def insertLine(self, n=1): + self.write(b'\x1b[' + intToBytes(n) + b'L') + + + def deleteLine(self, n=1): + self.write(b'\x1b[' + intToBytes(n) + b'M') + + + def setScrollRegion(self, first=None, last=None): + if first is not None: + first = intToBytes(first) + else: + first = b'' + if last is not None: + last = intToBytes(last) + else: + last = b'' + self.write(b'\x1b[' + first + b';' + last + b'r') + + + def resetScrollRegion(self): + self.setScrollRegion() + + + def reportCursorPosition(self): + d = defer.Deferred() + self._cursorReports.append(d) + self.write(b'\x1b[6n') + return d + + + def reset(self): + self.cursorPos.x = self.cursorPos.y = 0 + try: + del self._savedCursorPos + except AttributeError: + pass + self.write(b'\x1bc') + + + # ITransport + def write(self, data): + if data: + if not isinstance(data, bytes): + data = data.encode("utf-8") + self.lastWrite = data + self.transport.write(b'\r\n'.join(data.split(b'\n'))) + + + def writeSequence(self, data): + self.write(b''.join(data)) + + + def loseConnection(self): + self.reset() + self.transport.loseConnection() + + + def connectionLost(self, reason): + if self.terminalProtocol is not None: + try: + self.terminalProtocol.connectionLost(reason) + finally: + self.terminalProtocol = None +# Add symbolic names for function keys +for name, const in zip(_KEY_NAMES, FUNCTION_KEYS): + setattr(ServerProtocol, name, const) + + + +class ClientProtocol(protocol.Protocol): + + terminalFactory = None + terminal = None + + state = b'data' + + _escBuf = None + + _shorts = { + b'D': b'index', + b'M': b'reverseIndex', + b'E': b'nextLine', + b'7': b'saveCursor', + b'8': b'restoreCursor', + b'=': b'applicationKeypadMode', + b'>': b'numericKeypadMode', + b'N': b'singleShift2', + b'O': b'singleShift3', + b'H': b'horizontalTabulationSet', + b'c': b'reset'} + + _longs = { + b'[': b'bracket-escape', + b'(': b'select-g0', + b')': b'select-g1', + b'#': b'select-height-width'} + + _charsets = { + b'A': CS_UK, + b'B': CS_US, + b'0': CS_DRAWING, + b'1': CS_ALTERNATE, + b'2': CS_ALTERNATE_SPECIAL} + + # Factory who instantiated me + factory = None + + def __init__(self, terminalFactory=None, *a, **kw): + """ + @param terminalFactory: A callable which will be invoked with + *a, **kw and should return an ITerminalTransport provider. + This will be invoked when this ClientProtocol establishes a + connection. + + @param a: Any positional arguments to pass to terminalFactory. + @param kw: Any keyword arguments to pass to terminalFactory. + """ + # assert terminalFactory is None or ITerminalTransport.implementedBy(terminalFactory), "ClientProtocol.__init__ must be passed an ITerminalTransport implementor" + if terminalFactory is not None: + self.terminalFactory = terminalFactory + self.terminalArgs = a + self.terminalKwArgs = kw + + + def connectionMade(self): + if self.terminalFactory is not None: + self.terminal = self.terminalFactory(*self.terminalArgs, **self.terminalKwArgs) + self.terminal.factory = self.factory + self.terminal.makeConnection(self) + + + def connectionLost(self, reason): + if self.terminal is not None: + try: + self.terminal.connectionLost(reason) + finally: + del self.terminal + + + def dataReceived(self, data): + """ + Parse the given data from a terminal server, dispatching to event + handlers defined by C{self.terminal}. + """ + toWrite = [] + for b in iterbytes(data): + if self.state == b'data': + if b == b'\x1b': + if toWrite: + self.terminal.write(b''.join(toWrite)) + del toWrite[:] + self.state = b'escaped' + elif b == b'\x14': + if toWrite: + self.terminal.write(b''.join(toWrite)) + del toWrite[:] + self.terminal.shiftOut() + elif b == b'\x15': + if toWrite: + self.terminal.write(b''.join(toWrite)) + del toWrite[:] + self.terminal.shiftIn() + elif b == b'\x08': + if toWrite: + self.terminal.write(b''.join(toWrite)) + del toWrite[:] + self.terminal.cursorBackward() + else: + toWrite.append(b) + elif self.state == b'escaped': + fName = self._shorts.get(b) + if fName is not None: + self.state = b'data' + getattr(self.terminal, fName.decode("ascii"))() + else: + state = self._longs.get(b) + if state is not None: + self.state = state + else: + self.terminal.unhandledControlSequence(b'\x1b' + b) + self.state = b'data' + elif self.state == b'bracket-escape': + if self._escBuf is None: + self._escBuf = [] + if b.isalpha() or b == b'~': + self._handleControlSequence(b''.join(self._escBuf), b) + del self._escBuf + self.state = b'data' + else: + self._escBuf.append(b) + elif self.state == b'select-g0': + self.terminal.selectCharacterSet(self._charsets.get(b, b), G0) + self.state = b'data' + elif self.state == b'select-g1': + self.terminal.selectCharacterSet(self._charsets.get(b, b), G1) + self.state = b'data' + elif self.state == b'select-height-width': + self._handleHeightWidth(b) + self.state = b'data' + else: + raise ValueError("Illegal state") + if toWrite: + self.terminal.write(b''.join(toWrite)) + + + def _handleControlSequence(self, buf, terminal): + f = getattr(self.controlSequenceParser, CST.get(terminal, terminal).decode("ascii"), None) + if f is None: + self.terminal.unhandledControlSequence(b'\x1b[' + buf + terminal) + else: + f(self, self.terminal, buf) + + + class ControlSequenceParser: + def _makeSimple(ch, fName): + n = 'cursor' + fName + def simple(self, proto, handler, buf): + if not buf: + getattr(handler, n)(1) + else: + try: + m = int(buf) + except ValueError: + handler.unhandledControlSequence(b'\x1b[' + buf + ch) + else: + getattr(handler, n)(m) + return simple + + for (ch, fName) in (('A', 'Up'), + ('B', 'Down'), + ('C', 'Forward'), + ('D', 'Backward')): + exec(ch + " = _makeSimple(ch, fName)") + del _makeSimple + + + def h(self, proto, handler, buf): + # XXX - Handle '?' to introduce ANSI-Compatible private modes. + try: + modes = [int(mode) for mode in buf.split(b';')] + except ValueError: + handler.unhandledControlSequence(b'\x1b[' + buf + b'h') + else: + handler.setModes(modes) + + + def l(self, proto, handler, buf): + # XXX - Handle '?' to introduce ANSI-Compatible private modes. + try: + modes = [int(mode) for mode in buf.split(b';')] + except ValueError: + handler.unhandledControlSequence(b'\x1b[' + buf + 'l') + else: + handler.resetModes(modes) + + + def r(self, proto, handler, buf): + parts = buf.split(b';') + if len(parts) == 1: + handler.setScrollRegion(None, None) + elif len(parts) == 2: + try: + if parts[0]: + pt = int(parts[0]) + else: + pt = None + if parts[1]: + pb = int(parts[1]) + else: + pb = None + except ValueError: + handler.unhandledControlSequence(b'\x1b[' + buf + b'r') + else: + handler.setScrollRegion(pt, pb) + else: + handler.unhandledControlSequence(b'\x1b[' + buf + b'r') + + def K(self, proto, handler, buf): + if not buf: + handler.eraseToLineEnd() + elif buf == b'1': + handler.eraseToLineBeginning() + elif buf == b'2': + handler.eraseLine() + else: + handler.unhandledControlSequence(b'\x1b[' + buf + b'K') + + + def H(self, proto, handler, buf): + handler.cursorHome() + + + def J(self, proto, handler, buf): + if not buf: + handler.eraseToDisplayEnd() + elif buf == b'1': + handler.eraseToDisplayBeginning() + elif buf == b'2': + handler.eraseDisplay() + else: + handler.unhandledControlSequence(b'\x1b[' + buf + b'J') + + + def P(self, proto, handler, buf): + if not buf: + handler.deleteCharacter(1) + else: + try: + n = int(buf) + except ValueError: + handler.unhandledControlSequence(b'\x1b[' + buf + b'P') + else: + handler.deleteCharacter(n) + + def L(self, proto, handler, buf): + if not buf: + handler.insertLine(1) + else: + try: + n = int(buf) + except ValueError: + handler.unhandledControlSequence(b'\x1b[' + buf + b'L') + else: + handler.insertLine(n) + + + def M(self, proto, handler, buf): + if not buf: + handler.deleteLine(1) + else: + try: + n = int(buf) + except ValueError: + handler.unhandledControlSequence(b'\x1b[' + buf + b'M') + else: + handler.deleteLine(n) + + + def n(self, proto, handler, buf): + if buf == b'6': + x, y = handler.reportCursorPosition() + proto.transport.write(b'\x1b[' + + intToBytes(x+1) + + b';' + + intToBytes(y+1) + + b'R') + else: + handler.unhandledControlSequence(b'\x1b[' + buf + b'n') + + + def m(self, proto, handler, buf): + if not buf: + handler.selectGraphicRendition(NORMAL) + else: + attrs = [] + for a in buf.split(b';'): + try: + a = int(a) + except ValueError: + pass + attrs.append(a) + handler.selectGraphicRendition(*attrs) + + controlSequenceParser = ControlSequenceParser() + + + def _handleHeightWidth(self, b): + if b == b'3': + self.terminal.doubleHeightLine(True) + elif b == b'4': + self.terminal.doubleHeightLine(False) + elif b == b'5': + self.terminal.singleWidthLine() + elif b == b'6': + self.terminal.doubleWidthLine() + else: + self.terminal.unhandledControlSequence(b'\x1b#' + b) + + +__all__ = [ + # Interfaces + 'ITerminalProtocol', 'ITerminalTransport', + + # Symbolic constants + 'modes', 'privateModes', 'FUNCTION_KEYS', + + 'CS_US', 'CS_UK', 'CS_DRAWING', 'CS_ALTERNATE', 'CS_ALTERNATE_SPECIAL', + 'G0', 'G1', 'G2', 'G3', + + 'UNDERLINE', 'REVERSE_VIDEO', 'BLINK', 'BOLD', 'NORMAL', + + # Protocol classes + 'ServerProtocol', 'ClientProtocol'] diff --git a/contrib/python/Twisted/py2/twisted/conch/insults/text.py b/contrib/python/Twisted/py2/twisted/conch/insults/text.py new file mode 100644 index 0000000000..54476f71a1 --- /dev/null +++ b/contrib/python/Twisted/py2/twisted/conch/insults/text.py @@ -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'] diff --git a/contrib/python/Twisted/py2/twisted/conch/insults/window.py b/contrib/python/Twisted/py2/twisted/conch/insults/window.py new file mode 100644 index 0000000000..d3caf7d3f4 --- /dev/null +++ b/contrib/python/Twisted/py2/twisted/conch/insults/window.py @@ -0,0 +1,1027 @@ +# -*- test-case-name: twisted.conch.test.test_window -*- + +""" +Simple insults-based widget library + +@author: Jp Calderone +""" + +import array + +from twisted.conch.insults import insults, helper +from twisted.python import text as tptext +from twisted.python.compat import (_PY3, _bytesChr as chr) + +class YieldFocus(Exception): + """ + Input focus manipulation exception + """ + + + +class BoundedTerminalWrapper(object): + 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(object): + focused = False + parent = None + dirty = False + width = height = 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") + func = getattr(self, 'func_' + name, 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) + if _PY3: + text = self.contents[self._width * i: + self._width * i + self._width + ].tobytes() + else: + text = self.contents[self._width * i: + self._width * i + self._width + ].tostring() + 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(chr(0o161) * (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(chr(0o170)) + 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(chr(0o154)) + terminal.write(chr(0o161) * (width - 2)) + terminal.write(chr(0o153)) + for n in range(height - 2): + terminal.cursorPosition(left, top + n + 1) + terminal.write(chr(0o170)) + terminal.cursorForward(width - 2) + terminal.write(chr(0o170)) + terminal.cursorPosition(0, top + height - 1) + terminal.write(chr(0o155)) + terminal.write(chr(0o161) * (width - 2)) + terminal.write(chr(0o152)) + + 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 + + def xOffset(): + def get(self): + return self._xOffset + def set(self, value): + if self._xOffset != value: + self._xOffset = value + self.repaint() + return get, set + xOffset = property(*xOffset()) + + + def yOffset(): + def get(self): + return self._yOffset + def set(self, value): + if self._yOffset != value: + self._yOffset = value + self.repaint() + return get, set + yOffset = property(*yOffset()) + + _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 = u'\N{BLACK LEFT-POINTING TRIANGLE}' + _right = u'\N{BLACK RIGHT-POINTING TRIANGLE}' + _bar = u'\N{LIGHT SHADE}' + _slider = u'\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 = u'\N{BLACK UP-POINTING TRIANGLE}' + _down = u'\N{BLACK DOWN-POINTING TRIANGLE}' + _bar = u'\N{LIGHT SHADE}' + _slider = u'\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() diff --git a/contrib/python/Twisted/py2/twisted/conch/interfaces.py b/contrib/python/Twisted/py2/twisted/conch/interfaces.py new file mode 100644 index 0000000000..cdf5489898 --- /dev/null +++ b/contrib/python/Twisted/py2/twisted/conch/interfaces.py @@ -0,0 +1,444 @@ +# Copyright (c) Twisted Matrix Laboratories. +# See LICENSE for details. + +""" +This module contains interfaces defined for the L{twisted.conch} package. +""" + +from zope.interface import Interface, Attribute + +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 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): + """ + Return True if this entry matches the given Key object, False + otherwise. + + @param key: The key object to match against. + @type key: L{twisted.conch.ssh.keys.Key} + """ + + + def matchesHost(hostname): + """ + 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. + @type hostname: L{str} + """ + + + def toString(): + """ + + @return: a serialized string representation of this entry, suitable for + inclusion in a known_hosts file. (Newline not included.) + + @rtype: L{str} + """ + + + +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}. + """ diff --git a/contrib/python/Twisted/py2/twisted/conch/ls.py b/contrib/python/Twisted/py2/twisted/conch/ls.py new file mode 100644 index 0000000000..85da665dc9 --- /dev/null +++ b/contrib/python/Twisted/py2/twisted/conch/ls.py @@ -0,0 +1,83 @@ +# -*- test-case-name: twisted.conch.test.test_cftp -*- +# Copyright (c) Twisted Matrix Laboratories. +# See LICENSE for details. + +import array +import stat + +from time import time, strftime, localtime +from twisted.python.compat import _PY3 + +# 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 _PY3: + if isinstance(name, bytes): + name = name.decode("utf-8") + lsPerms = perms.tobytes() + lsPerms = lsPerms.decode("utf-8") + else: + lsPerms = perms.tostring() + + 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'] diff --git a/contrib/python/Twisted/py2/twisted/conch/manhole.py b/contrib/python/Twisted/py2/twisted/conch/manhole.py new file mode 100644 index 0000000000..70e16b70cd --- /dev/null +++ b/contrib/python/Twisted/py2/twisted/conch/manhole.py @@ -0,0 +1,401 @@ +# -*- 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, sys, tokenize +from io import BytesIO + +from twisted.conch import recvline + +from twisted.internet import defer +from twisted.python.compat import _tokenize, _get_async_param +from twisted.python.htmlizer import TokenPrinter + + + +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() + + + 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): + orighook, sys.displayhook = sys.displayhook, self.displayhook + try: + origout, sys.stdout = sys.stdout, FileWrapper(self.handler) + try: + code.InteractiveInterpreter.runcode(self, *a, **kw) + finally: + sys.stdout = origout + finally: + sys.displayhook = orighook + + + 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, **kwargs): + isAsync = _get_async_param(isAsync, **kwargs) + 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): + """ + 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, **kwargs): + isAsync = _get_async_param(isAsync, **kwargs) + 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] + + if bytes == str: + # Compat with Python 2.7 + __str__ = __bytes__ + + + +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(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) diff --git a/contrib/python/Twisted/py2/twisted/conch/manhole_ssh.py b/contrib/python/Twisted/py2/twisted/conch/manhole_ssh.py new file mode 100644 index 0000000000..84b242f24e --- /dev/null +++ b/contrib/python/Twisted/py2/twisted/conch/manhole_ssh.py @@ -0,0 +1,141 @@ +# -*- 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 zope.interface import implementer + +from twisted.conch import avatar, interfaces as iconch, error as econch +from twisted.conch.ssh import factory, session +from twisted.python import components + +from twisted.conch.insults import insults + + +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 closed(self): + 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 = {} + privateKeys = {} + + def __init__(self, portal): + self.portal = portal diff --git a/contrib/python/Twisted/py2/twisted/conch/manhole_tap.py b/contrib/python/Twisted/py2/twisted/conch/manhole_tap.py new file mode 100644 index 0000000000..b5fb78b60c --- /dev/null +++ b/contrib/python/Twisted/py2/twisted/conch/manhole_tap.py @@ -0,0 +1,165 @@ +# 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.internet import protocol +from twisted.application import service, strports +from twisted.cred import portal, checkers +from twisted.python import usage, filepath + +from twisted.conch import manhole, manhole_ssh, telnet +from twisted.conch.insults import insults +from twisted.conch.ssh import keys + + + +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 diff --git a/contrib/python/Twisted/py2/twisted/conch/mixin.py b/contrib/python/Twisted/py2/twisted/conch/mixin.py new file mode 100644 index 0000000000..976e9ad18f --- /dev/null +++ b/contrib/python/Twisted/py2/twisted/conch/mixin.py @@ -0,0 +1,55 @@ +# -*- 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 diff --git a/contrib/python/Twisted/py2/twisted/conch/openssh_compat/__init__.py b/contrib/python/Twisted/py2/twisted/conch/openssh_compat/__init__.py new file mode 100644 index 0000000000..69d5927d1f --- /dev/null +++ b/contrib/python/Twisted/py2/twisted/conch/openssh_compat/__init__.py @@ -0,0 +1,11 @@ +# Copyright (c) Twisted Matrix Laboratories. +# See LICENSE for details. + +# + +""" +Support for OpenSSH configuration files. + +Maintainer: Paul Swartz +""" + diff --git a/contrib/python/Twisted/py2/twisted/conch/openssh_compat/factory.py b/contrib/python/Twisted/py2/twisted/conch/openssh_compat/factory.py new file mode 100644 index 0000000000..eeea823226 --- /dev/null +++ b/contrib/python/Twisted/py2/twisted/conch/openssh_compat/factory.py @@ -0,0 +1,72 @@ +# -*- 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 os, errno + +from twisted.python import log +from twisted.python.util import runAsEffectiveUser + +from twisted.conch.ssh import keys, factory, common +from twisted.conch.openssh_compat import primes + + + +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: + log.msg('bad public key file %s: %s' % (filename, 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 IOError 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: + log.msg('bad private key file %s: %s' % (filename, e)) + else: + privateKeys[key.sshType()] = key + return privateKeys + + + def getPrimes(self): + try: + return primes.parseModuliFile(self.moduliRoot+'/moduli') + except IOError: + return None diff --git a/contrib/python/Twisted/py2/twisted/conch/openssh_compat/primes.py b/contrib/python/Twisted/py2/twisted/conch/openssh_compat/primes.py new file mode 100644 index 0000000000..79cc7ff125 --- /dev/null +++ b/contrib/python/Twisted/py2/twisted/conch/openssh_compat/primes.py @@ -0,0 +1,30 @@ +# Copyright (c) Twisted Matrix Laboratories. +# See LICENSE for details. + +# + +""" +Parsing for the moduli file, which contains Diffie-Hellman prime groups. + +Maintainer: Paul Swartz +""" + +from twisted.python.compat import long + + +def parseModuliFile(filename): + with open(filename) as f: + lines = f.readlines() + primes = {} + for l in lines: + l = l.strip() + if not l or l[0]=='#': + continue + tim, typ, tst, tri, size, gen, mod = l.split() + size = int(size) + 1 + gen = long(gen) + mod = long(mod, 16) + if size not in primes: + primes[size] = [] + primes[size].append((gen, mod)) + return primes diff --git a/contrib/python/Twisted/py2/twisted/conch/recvline.py b/contrib/python/Twisted/py2/twisted/conch/recvline.py new file mode 100644 index 0000000000..f7801b720e --- /dev/null +++ b/contrib/python/Twisted/py2/twisted/conch/recvline.py @@ -0,0 +1,374 @@ +# -*- 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 zope.interface import implementer + +from twisted.conch.insults import insults, helper + +from twisted.python import log, reflect +from twisted.python.compat import iterbytes + +_counters = {} +class Logging(object): + """ + 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): + return str(super(Logging, self).__getattribute__('original')) + + + def __repr__(self): + return repr(super(Logging, self).__getattribute__('original')) + + + def __getattribute__(self, name): + original = super(Logging, self).__getattribute__('original') + logFile = super(Logging, self).__getattribute__('_logFile') + logFile.write(name + '\n') + return getattr(original, name) + + + +@implementer(insults.ITerminalTransport) +class TransportSequence(object): + """ + 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)) + + + +class LocalTerminalBufferMixin(object): + """ + 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(LocalTerminalBufferMixin, self).makeConnection( + TransportSequence(transport, self.terminalCopy)) + + + def __str__(self): + 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") + + 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: + log.msg("Received unhandled keyID: %r" % (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) diff --git a/contrib/python/Twisted/py2/twisted/conch/ssh/__init__.py b/contrib/python/Twisted/py2/twisted/conch/ssh/__init__.py new file mode 100644 index 0000000000..4b7f024b99 --- /dev/null +++ b/contrib/python/Twisted/py2/twisted/conch/ssh/__init__.py @@ -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 +""" diff --git a/contrib/python/Twisted/py2/twisted/conch/ssh/_kex.py b/contrib/python/Twisted/py2/twisted/conch/ssh/_kex.py new file mode 100644 index 0000000000..922cf8b6a1 --- /dev/null +++ b/contrib/python/Twisted/py2/twisted/conch/ssh/_kex.py @@ -0,0 +1,294 @@ +# -*- test-case-name: twisted.conch.test.test_transport -*- +# Copyright (c) Twisted Matrix Laboratories. +# See LICENSE for details. + +""" +SSH key exchange handling. +""" + +from __future__ import absolute_import, division + +from hashlib import sha1, sha256, sha384, sha512 + +from zope.interface import Attribute, implementer, Interface + +from twisted.conch import error +from twisted.python.compat import long + + +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( + "A L{long} giving the prime number used in Diffie-Hellman key " + "exchange, or L{None} if not applicable.") + + generator = Attribute( + "A L{long} 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(object): + """ + 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(object): + """ + As L{_Curve25519SHA256}, but with a pre-standardized algorithm name. + """ + preference = 2 + hashProcessor = sha256 + + + +@implementer(_IEllipticCurveExchangeKexAlgorithm) +class _ECDH256(object): + """ + Elliptic Curve Key Exchange with SHA-256 as HASH. Defined in + RFC 5656. + """ + preference = 3 + hashProcessor = sha256 + + + +@implementer(_IEllipticCurveExchangeKexAlgorithm) +class _ECDH384(object): + """ + Elliptic Curve Key Exchange with SHA-384 as HASH. Defined in + RFC 5656. + """ + preference = 4 + hashProcessor = sha384 + + + +@implementer(_IEllipticCurveExchangeKexAlgorithm) +class _ECDH512(object): + """ + Elliptic Curve Key Exchange with SHA-512 as HASH. Defined in + RFC 5656. + """ + preference = 5 + hashProcessor = sha512 + + + +@implementer(_IGroupExchangeKexAlgorithm) +class _DHGroupExchangeSHA256(object): + """ + 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(object): + """ + 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(object): + """ + 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 = long('32317006071311007300338913926423828248817941241140239112842' + '00975140074170663435422261968941736356934711790173790970419175460587' + '32091950288537589861856221532121754125149017745202702357960782362488' + '84246189477587641105928646099411723245426622522193230540919037680524' + '23551912567971587011700105805587765103886184728025797605490356973256' + '15261670813393617995413364765591603683178967290731783845896806396719' + '00977202194168647225871031411336429319536193471636533209717077448227' + '98858856536920864529663607725026895550592836275112117409697299806841' + '05543595848665832916421362182310789909994486524682624169720359118525' + '07045361090559') + 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( + "Unsupported key exchange algorithm: %s" % (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{long} generator and L{long} 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) diff --git a/contrib/python/Twisted/py2/twisted/conch/ssh/address.py b/contrib/python/Twisted/py2/twisted/conch/ssh/address.py new file mode 100644 index 0000000000..969740268c --- /dev/null +++ b/contrib/python/Twisted/py2/twisted/conch/ssh/address.py @@ -0,0 +1,47 @@ +# -*- 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 __future__ import division, absolute_import + +from zope.interface import implementer + +from twisted.internet.interfaces import IAddress +from twisted.python import util + + + +@implementer(IAddress) +class SSHTransportAddress(util.FancyEqMixin, object): + """ + 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): + return 'SSHTransportAddress(%r)' % (self.address,) + + + def __hash__(self): + return hash(('SSH', self.address)) diff --git a/contrib/python/Twisted/py2/twisted/conch/ssh/agent.py b/contrib/python/Twisted/py2/twisted/conch/ssh/agent.py new file mode 100644 index 0000000000..03c0de8068 --- /dev/null +++ b/contrib/python/Twisted/py2/twisted/conch/ssh/agent.py @@ -0,0 +1,296 @@ +# 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 +""" + +from __future__ import absolute_import, division + +import struct + +from twisted.conch.ssh.common import NS, getNS, getMP +from twisted.conch.error import ConchError, MissingKeyStoreError +from twisted.conch.ssh import keys +from twisted.internet import defer, protocol +from twisted.python.compat import itervalues + + + +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 itervalues(self.factory.keys): + 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 diff --git a/contrib/python/Twisted/py2/twisted/conch/ssh/channel.py b/contrib/python/Twisted/py2/twisted/conch/ssh/channel.py new file mode 100644 index 0000000000..51e2a2f914 --- /dev/null +++ b/contrib/python/Twisted/py2/twisted/conch/ssh/channel.py @@ -0,0 +1,320 @@ +# -*- 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 __future__ import division, absolute_import + +from zope.interface import implementer + +from twisted.python import log +from twisted.python.compat import nativeString, intToBytes +from twisted.internet import interfaces + + + +@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} + """ + + name = None # 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): + return nativeString(self.__bytes__()) + + + def __bytes__(self): + """ + Return a byte string representation of the channel + """ + name = self.name + if not name: + name = b'None' + + return (b'<SSHChannel ' + name + + b' (lw ' + intToBytes(self.localWindowLeft) + + b' rw ' + intToBytes(self.remoteWindowLeft) + + b')>') + + + def logPrefix(self): + id = (self.id is not None and str(self.id)) or "unknown" + name = self.name + if name: + name = nativeString(name) + return "SSHChannel %s (%s) on %s" % (name, id, + 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} + """ + log.msg('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} + """ + log.msg('other side refused open\nreason: %s'% 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 = nativeString(requestType.replace(b'-', b'_')) + f = getattr(self, 'request_%s'%foo, None) + if f: + return f(data) + log.msg('unhandled request for %s'%requestType) + return 0 + + + def dataReceived(self, data): + """ + Called when we receive data. + + @type data: L{bytes} + """ + log.msg('got data %s'%repr(data)) + + + def extReceived(self, dataType, data): + """ + Called when we receive extended data (usually standard error). + + @type dataType: L{int} + @type data: L{str} + """ + log.msg('got extended data %s %s'%(dataType, repr(data))) + + + def eofReceived(self): + """ + Called when the other side will send no more data. + """ + log.msg('remote eof') + + + def closeReceived(self): + """ + Called when the other side has closed the channel. + """ + log.msg('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. + """ + log.msg('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. + """ diff --git a/contrib/python/Twisted/py2/twisted/conch/ssh/common.py b/contrib/python/Twisted/py2/twisted/conch/ssh/common.py new file mode 100644 index 0000000000..8a0f136c36 --- /dev/null +++ b/contrib/python/Twisted/py2/twisted/conch/ssh/common.py @@ -0,0 +1,93 @@ +# -*- 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 +""" + +from __future__ import absolute_import, division + +import struct + +from cryptography.utils import int_from_bytes, int_to_bytes + +from twisted.python.compat import unicode +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, unicode): + 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 diff --git a/contrib/python/Twisted/py2/twisted/conch/ssh/connection.py b/contrib/python/Twisted/py2/twisted/conch/ssh/connection.py new file mode 100644 index 0000000000..16ef6444a0 --- /dev/null +++ b/contrib/python/Twisted/py2/twisted/conch/ssh/connection.py @@ -0,0 +1,653 @@ +# -*- 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 +""" +from __future__ import division, absolute_import + +import string +import struct + +import twisted.internet.error +from twisted.conch.ssh import service, common +from twisted.conch import error +from twisted.internet import defer +from twisted.python import log +from twisted.python.compat import ( + nativeString, networkString, long, _bytesChr as chr) + + + +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' + + def __init__(self): + self.localChannelID = 0 # this is the current # to use for channel ID + self.localToRemoteChannel = {} # local channel ID -> remote channel ID + self.channels = {} # local channel ID -> subclass of SSHChannel + self.channelsToRemoteChannel = {} # subclass of SSHChannel -> + # remote channel ID + self.deferreds = {"global": []} # local channel -> list of deferreds + # for pending requests or 'global' -> list of + # deferreds for global requests + 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() + log.callWithLogger(channel, 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. + """ + log.msg('RS') + 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. + """ + log.msg('RF') + 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 + self.transport.sendPacket(MSG_CHANNEL_OPEN_CONFIRMATION, + struct.pack('>4L', senderChannel, localChannel, + channel.localWindowSize, + channel.localMaxPacket)+channel.specificData) + log.callWithLogger(channel, channel.channelOpen, packet) + except Exception as e: + log.err(e, 'channel open failed') + if isinstance(e, error.ConchError): + textualInfo, reason = e.args + if isinstance(textualInfo, (int, long)): + # 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 + log.callWithLogger(channel, 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) + log.callWithLogger(channel, 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] + log.callWithLogger(channel, 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 + log.callWithLogger(channel, log.msg, '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) + #log.msg('local window left: %s/%s' % (channel.localWindowLeft, + # channel.localWindowSize)) + log.callWithLogger(channel, 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): + log.callWithLogger(channel, log.msg, '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) + log.callWithLogger(channel, 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] + log.callWithLogger(channel, 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] + log.callWithLogger(channel, 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(log.callWithLogger, channel, + 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) + log.callWithLogger(self.channels[localChannel], + 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) + log.callWithLogger(self.channels[localChannel], + 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} + """ + log.msg('opening channel %s with %s %s'%(self.localChannelID, + channel.localWindowSize, 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 + log.msg('sending request %r' % (requestType)) + self.transport.sendPacket(MSG_CHANNEL_REQUEST, struct.pack('>L', + self.channelsToRemoteChannel[channel]) + + common.NS(requestType)+chr(wantReply) + + 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 + self.transport.sendPacket(MSG_CHANNEL_WINDOW_ADJUST, struct.pack('>2L', + self.channelsToRemoteChannel[channel], + bytesToAdd)) + log.msg('adding %i to %i in channel %i' % (bytesToAdd, + channel.localWindowLeft, 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 + log.msg('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 + log.msg('sending close %i' % 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} + """ + log.msg('got channel %r request' % (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} + """ + log.msg('got global %s request' % 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.")) + log.callWithLogger(channel, 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 + +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 = b''.join([chr(i) in alphanums and chr(i) or b'_' + for i in range(256)]) +SSHConnection.protocolMessages = messages diff --git a/contrib/python/Twisted/py2/twisted/conch/ssh/factory.py b/contrib/python/Twisted/py2/twisted/conch/ssh/factory.py new file mode 100644 index 0000000000..16658e7c59 --- /dev/null +++ b/contrib/python/Twisted/py2/twisted/conch/ssh/factory.py @@ -0,0 +1,123 @@ +# 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 +""" + +from __future__ import division, absolute_import + +from twisted.internet import protocol +from twisted.python import log + +from twisted.conch import error +from twisted.conch.ssh import (_kex, transport, userauth, connection) + +import random + + +class SSHFactory(protocol.Factory): + """ + A Factory for SSH servers. + """ + protocol = transport.SSHServerTransport + + services = { + b'ssh-userauth':userauth.SSHUserAuthServer, + b'ssh-connection':connection.SSHConnection + } + def startFactory(self): + """ + 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 = self.privateKeys.keys() + if not self.primes: + log.msg('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): + """ + 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). + + @rtype: L{dict} + """ + + + def getDHPrime(self, bits): + """ + Return a tuple of (g, p) for a Diffe-Hellman process, with p being as + close to bits bits as possible. + + @type bits: L{int} + @rtype: L{tuple} + """ + primesKeys = sorted(self.primes.keys(), key=lambda i: abs(i - bits)) + 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] diff --git a/contrib/python/Twisted/py2/twisted/conch/ssh/filetransfer.py b/contrib/python/Twisted/py2/twisted/conch/ssh/filetransfer.py new file mode 100644 index 0000000000..cd739e5361 --- /dev/null +++ b/contrib/python/Twisted/py2/twisted/conch/ssh/filetransfer.py @@ -0,0 +1,1055 @@ +# -*- test-case-name: twisted.conch.test.test_filetransfer -*- +# +# Copyright (c) Twisted Matrix Laboratories. +# See LICENSE for details. + +from __future__ import division, absolute_import + +import errno +import struct +import warnings + +from zope.interface import implementer + +from twisted.conch.interfaces import ISFTPServer, ISFTPFile +from twisted.conch.ssh.common import NS, getNS +from twisted.internet import defer, protocol +from twisted.python import failure, log +from twisted.python.compat import ( + _PY3, range, itervalues, nativeString, networkString) + + + +class FileTransferBase(protocol.Protocol): + + versions = (3, ) + + packetTypes = {} + + def __init__(self): + self.buf = b'' + self.otherVersion = None # This gets set + + + def sendPacket(self, kind, data): + self.transport.write(struct.pack('!LB', len(data)+1, kind) + data) + + + def dataReceived(self, data): + self.buf += data + while len(self.buf) > 5: + length, kind = struct.unpack('!LB', self.buf[:5]) + if len(self.buf) < 4 + length: + return + data, self.buf = self.buf[5:4+length], self.buf[4+length:] + packetType = self.packetTypes.get(kind, None) + if not packetType: + log.msg('no packet type for', kind) + continue + f = getattr(self, 'packet_{}'.format(packetType), None) + if not f: + log.msg('not implemented: {}'.format(packetType)) + log.msg(repr(data[4:])) + reqId, = struct.unpack('!L', data[:4]) + self._sendStatus(reqId, FX_OP_UNSUPPORTED, + "don't understand {}".format(packetType)) + # XXX not implemented + continue + try: + f(data) + except Exception: + log.err() + continue + + + def _parseAttributes(self, data): + (flags,) = struct.unpack('!L', data[:4]) + attrs = {} + data = data[4:] + if flags & FILEXFER_ATTR_SIZE == FILEXFER_ATTR_SIZE: + (size,) = struct.unpack('!Q', data[:8]) + attrs['size'] = size + data = data[8:] + if flags & FILEXFER_ATTR_OWNERGROUP == FILEXFER_ATTR_OWNERGROUP: + uid, gid = struct.unpack('!2L', data[:8]) + attrs['uid'] = uid + attrs['gid'] = gid + data = data[8:] + if flags & FILEXFER_ATTR_PERMISSIONS == FILEXFER_ATTR_PERMISSIONS: + (perms,) = struct.unpack('!L', data[:4]) + attrs['permissions'] = perms + data = data[4:] + if flags & FILEXFER_ATTR_ACMODTIME == FILEXFER_ATTR_ACMODTIME: + atime, mtime = struct.unpack('!2L', data[:8]) + attrs['atime'] = atime + attrs['mtime'] = mtime + data = data[8:] + if flags & FILEXFER_ATTR_EXTENDED == FILEXFER_ATTR_EXTENDED: + (extendedCount,) = struct.unpack('!L', data[:4]) + data = data[4:] + for i in range(extendedCount): + (extendedType, data) = getNS(data) + (extendedData, data) = getNS(data) + attrs['ext_{}'.format(nativeString(extendedType))] = \ + extendedData + return attrs, data + + + def _packAttributes(self, attrs): + flags = 0 + data = b'' + if 'size' in attrs: + data += struct.pack('!Q', attrs['size']) + flags |= FILEXFER_ATTR_SIZE + if 'uid' in attrs and 'gid' in attrs: + data += struct.pack('!2L', attrs['uid'], attrs['gid']) + flags |= FILEXFER_ATTR_OWNERGROUP + if 'permissions' in attrs: + data += struct.pack('!L', attrs['permissions']) + flags |= FILEXFER_ATTR_PERMISSIONS + if 'atime' in attrs and 'mtime' in attrs: + data += struct.pack('!2L', attrs['atime'], attrs['mtime']) + flags |= FILEXFER_ATTR_ACMODTIME + extended = [] + for k in attrs: + if k.startswith('ext_'): + extType = NS(networkString(k[4:])) + extData = NS(attrs[k]) + extended.append(extType + extData) + if extended: + data += struct.pack('!L', len(extended)) + data += b''.join(extended) + flags |= FILEXFER_ATTR_EXTENDED + return struct.pack('!L', flags) + data + + + +class FileTransferServer(FileTransferBase): + + def __init__(self, data=None, avatar=None): + FileTransferBase.__init__(self) + self.client = ISFTPServer(avatar) # yay interfaces + self.openFiles = {} + self.openDirs = {} + + + def packet_INIT(self, data): + (version,) = struct.unpack('!L', data[:4]) + self.version = min(list(self.versions) + [version]) + data = data[4:] + ext = {} + while data: + extName, data = getNS(data) + extData, data = getNS(data) + ext[extName] = extData + ourExt = self.client.gotVersion(version, ext) + ourExtData = b"" + for (k, v) in ourExt.items(): + ourExtData += NS(k) + NS(v) + self.sendPacket(FXP_VERSION, struct.pack('!L', self.version) + + ourExtData) + + + def packet_OPEN(self, data): + requestId = data[:4] + data = data[4:] + filename, data = getNS(data) + (flags,) = struct.unpack('!L', data[:4]) + data = data[4:] + attrs, data = self._parseAttributes(data) + assert data == b'', 'still have data in OPEN: {!r}'.format(data) + d = defer.maybeDeferred(self.client.openFile, filename, flags, attrs) + d.addCallback(self._cbOpenFile, requestId) + d.addErrback(self._ebStatus, requestId, b"open failed") + + + def _cbOpenFile(self, fileObj, requestId): + fileId = networkString(str(hash(fileObj))) + if fileId in self.openFiles: + raise KeyError('id already open') + self.openFiles[fileId] = fileObj + self.sendPacket(FXP_HANDLE, requestId + NS(fileId)) + + + def packet_CLOSE(self, data): + requestId = data[:4] + data = data[4:] + handle, data = getNS(data) + assert data == b'', 'still have data in CLOSE: {!r}'.format(data) + if handle in self.openFiles: + fileObj = self.openFiles[handle] + d = defer.maybeDeferred(fileObj.close) + d.addCallback(self._cbClose, handle, requestId) + d.addErrback(self._ebStatus, requestId, b"close failed") + elif handle in self.openDirs: + dirObj = self.openDirs[handle][0] + d = defer.maybeDeferred(dirObj.close) + d.addCallback(self._cbClose, handle, requestId, 1) + d.addErrback(self._ebStatus, requestId, b"close failed") + else: + self._ebClose(failure.Failure(KeyError()), requestId) + + + def _cbClose(self, result, handle, requestId, isDir=0): + if isDir: + del self.openDirs[handle] + else: + del self.openFiles[handle] + self._sendStatus(requestId, FX_OK, b'file closed') + + + def packet_READ(self, data): + requestId = data[:4] + data = data[4:] + handle, data = getNS(data) + (offset, length), data = struct.unpack('!QL', data[:12]), data[12:] + assert data == b'', 'still have data in READ: {!r}'.format(data) + if handle not in self.openFiles: + self._ebRead(failure.Failure(KeyError()), requestId) + else: + fileObj = self.openFiles[handle] + d = defer.maybeDeferred(fileObj.readChunk, offset, length) + d.addCallback(self._cbRead, requestId) + d.addErrback(self._ebStatus, requestId, b"read failed") + + + def _cbRead(self, result, requestId): + if result == b'': # Python's read will return this for EOF + raise EOFError() + self.sendPacket(FXP_DATA, requestId + NS(result)) + + + def packet_WRITE(self, data): + requestId = data[:4] + data = data[4:] + handle, data = getNS(data) + offset, = struct.unpack('!Q', data[:8]) + data = data[8:] + writeData, data = getNS(data) + assert data == b'', 'still have data in WRITE: {!r}'.format(data) + if handle not in self.openFiles: + self._ebWrite(failure.Failure(KeyError()), requestId) + else: + fileObj = self.openFiles[handle] + d = defer.maybeDeferred(fileObj.writeChunk, offset, writeData) + d.addCallback(self._cbStatus, requestId, b"write succeeded") + d.addErrback(self._ebStatus, requestId, b"write failed") + + + def packet_REMOVE(self, data): + requestId = data[:4] + data = data[4:] + filename, data = getNS(data) + assert data == b'', 'still have data in REMOVE: {!r}'.format(data) + d = defer.maybeDeferred(self.client.removeFile, filename) + d.addCallback(self._cbStatus, requestId, b"remove succeeded") + d.addErrback(self._ebStatus, requestId, b"remove failed") + + + def packet_RENAME(self, data): + requestId = data[:4] + data = data[4:] + oldPath, data = getNS(data) + newPath, data = getNS(data) + assert data == b'', 'still have data in RENAME: {!r}'.format(data) + d = defer.maybeDeferred(self.client.renameFile, oldPath, newPath) + d.addCallback(self._cbStatus, requestId, b"rename succeeded") + d.addErrback(self._ebStatus, requestId, b"rename failed") + + + def packet_MKDIR(self, data): + requestId = data[:4] + data = data[4:] + path, data = getNS(data) + attrs, data = self._parseAttributes(data) + assert data == b'', 'still have data in MKDIR: {!r}'.format(data) + d = defer.maybeDeferred(self.client.makeDirectory, path, attrs) + d.addCallback(self._cbStatus, requestId, b"mkdir succeeded") + d.addErrback(self._ebStatus, requestId, b"mkdir failed") + + + def packet_RMDIR(self, data): + requestId = data[:4] + data = data[4:] + path, data = getNS(data) + assert data == b'', 'still have data in RMDIR: {!r}'.format(data) + d = defer.maybeDeferred(self.client.removeDirectory, path) + d.addCallback(self._cbStatus, requestId, b"rmdir succeeded") + d.addErrback(self._ebStatus, requestId, b"rmdir failed") + + + def packet_OPENDIR(self, data): + requestId = data[:4] + data = data[4:] + path, data = getNS(data) + assert data == b'', 'still have data in OPENDIR: {!r}'.format(data) + d = defer.maybeDeferred(self.client.openDirectory, path) + d.addCallback(self._cbOpenDirectory, requestId) + d.addErrback(self._ebStatus, requestId, b"opendir failed") + + + def _cbOpenDirectory(self, dirObj, requestId): + handle = networkString((str(hash(dirObj)))) + if handle in self.openDirs: + raise KeyError("already opened this directory") + self.openDirs[handle] = [dirObj, iter(dirObj)] + self.sendPacket(FXP_HANDLE, requestId + NS(handle)) + + + def packet_READDIR(self, data): + requestId = data[:4] + data = data[4:] + handle, data = getNS(data) + assert data == b'', 'still have data in READDIR: {!r}'.format(data) + if handle not in self.openDirs: + self._ebStatus(failure.Failure(KeyError()), requestId) + else: + dirObj, dirIter = self.openDirs[handle] + d = defer.maybeDeferred(self._scanDirectory, dirIter, []) + d.addCallback(self._cbSendDirectory, requestId) + d.addErrback(self._ebStatus, requestId, b"scan directory failed") + + + def _scanDirectory(self, dirIter, f): + while len(f) < 250: + try: + info = next(dirIter) + except StopIteration: + if not f: + raise EOFError + return f + if isinstance(info, defer.Deferred): + info.addCallback(self._cbScanDirectory, dirIter, f) + return + else: + f.append(info) + return f + + + def _cbScanDirectory(self, result, dirIter, f): + f.append(result) + return self._scanDirectory(dirIter, f) + + + def _cbSendDirectory(self, result, requestId): + data = b'' + for (filename, longname, attrs) in result: + data += NS(filename) + data += NS(longname) + data += self._packAttributes(attrs) + self.sendPacket(FXP_NAME, requestId + + struct.pack('!L', len(result))+data) + + + def packet_STAT(self, data, followLinks=1): + requestId = data[:4] + data = data[4:] + path, data = getNS(data) + assert data == b'', 'still have data in STAT/LSTAT: {!r}'.format(data) + d = defer.maybeDeferred(self.client.getAttrs, path, followLinks) + d.addCallback(self._cbStat, requestId) + d.addErrback(self._ebStatus, requestId, b'stat/lstat failed') + + + def packet_LSTAT(self, data): + self.packet_STAT(data, 0) + + + def packet_FSTAT(self, data): + requestId = data[:4] + data = data[4:] + handle, data = getNS(data) + assert data == b'', 'still have data in FSTAT: {!r}'.format(data) + if handle not in self.openFiles: + self._ebStatus(failure.Failure(KeyError( + '{} not in self.openFiles'.format(handle))), requestId) + else: + fileObj = self.openFiles[handle] + d = defer.maybeDeferred(fileObj.getAttrs) + d.addCallback(self._cbStat, requestId) + d.addErrback(self._ebStatus, requestId, b'fstat failed') + + + def _cbStat(self, result, requestId): + data = requestId + self._packAttributes(result) + self.sendPacket(FXP_ATTRS, data) + + + def packet_SETSTAT(self, data): + requestId = data[:4] + data = data[4:] + path, data = getNS(data) + attrs, data = self._parseAttributes(data) + if data != b'': + log.msg('WARN: still have data in SETSTAT: {!r}'.format(data)) + d = defer.maybeDeferred(self.client.setAttrs, path, attrs) + d.addCallback(self._cbStatus, requestId, b'setstat succeeded') + d.addErrback(self._ebStatus, requestId, b'setstat failed') + + + def packet_FSETSTAT(self, data): + requestId = data[:4] + data = data[4:] + handle, data = getNS(data) + attrs, data = self._parseAttributes(data) + assert data == b'', 'still have data in FSETSTAT: {!r}'.format(data) + if handle not in self.openFiles: + self._ebStatus(failure.Failure(KeyError()), requestId) + else: + fileObj = self.openFiles[handle] + d = defer.maybeDeferred(fileObj.setAttrs, attrs) + d.addCallback(self._cbStatus, requestId, b'fsetstat succeeded') + d.addErrback(self._ebStatus, requestId, b'fsetstat failed') + + + def packet_READLINK(self, data): + requestId = data[:4] + data = data[4:] + path, data = getNS(data) + assert data == b'', 'still have data in READLINK: {!r}'.format(data) + d = defer.maybeDeferred(self.client.readLink, path) + d.addCallback(self._cbReadLink, requestId) + d.addErrback(self._ebStatus, requestId, b'readlink failed') + + + def _cbReadLink(self, result, requestId): + self._cbSendDirectory([(result, b'', {})], requestId) + + + def packet_SYMLINK(self, data): + requestId = data[:4] + data = data[4:] + linkPath, data = getNS(data) + targetPath, data = getNS(data) + d = defer.maybeDeferred(self.client.makeLink, linkPath, targetPath) + d.addCallback(self._cbStatus, requestId, b'symlink succeeded') + d.addErrback(self._ebStatus, requestId, b'symlink failed') + + + def packet_REALPATH(self, data): + requestId = data[:4] + data = data[4:] + path, data = getNS(data) + assert data == b'', 'still have data in REALPATH: {!r}'.format(data) + d = defer.maybeDeferred(self.client.realPath, path) + d.addCallback(self._cbReadLink, requestId) # Same return format + d.addErrback(self._ebStatus, requestId, b'realpath failed') + + + def packet_EXTENDED(self, data): + requestId = data[:4] + data = data[4:] + extName, extData = getNS(data) + d = defer.maybeDeferred(self.client.extendedRequest, extName, extData) + d.addCallback(self._cbExtended, requestId) + d.addErrback(self._ebStatus, requestId, + b'extended ' + extName + b' failed') + + + def _cbExtended(self, data, requestId): + self.sendPacket(FXP_EXTENDED_REPLY, requestId + data) + + + def _cbStatus(self, result, requestId, msg=b"request succeeded"): + self._sendStatus(requestId, FX_OK, msg) + + + def _ebStatus(self, reason, requestId, msg=b"request failed"): + code = FX_FAILURE + message = msg + if isinstance(reason.value, (IOError, OSError)): + if reason.value.errno == errno.ENOENT: # No such file + code = FX_NO_SUCH_FILE + message = networkString(reason.value.strerror) + elif reason.value.errno == errno.EACCES: # Permission denied + code = FX_PERMISSION_DENIED + message = networkString(reason.value.strerror) + elif reason.value.errno == errno.EEXIST: + code = FX_FILE_ALREADY_EXISTS + else: + log.err(reason) + elif isinstance(reason.value, EOFError): # EOF + code = FX_EOF + if reason.value.args: + message = networkString(reason.value.args[0]) + elif isinstance(reason.value, NotImplementedError): + code = FX_OP_UNSUPPORTED + if reason.value.args: + message = networkString(reason.value.args[0]) + elif isinstance(reason.value, SFTPError): + code = reason.value.code + message = networkString(reason.value.message) + else: + log.err(reason) + self._sendStatus(requestId, code, message) + + + def _sendStatus(self, requestId, code, message, lang=b''): + """ + Helper method to send a FXP_STATUS message. + """ + data = requestId + struct.pack('!L', code) + data += NS(message) + data += NS(lang) + self.sendPacket(FXP_STATUS, data) + + + def connectionLost(self, reason): + """ + Clean all opened files and directories. + """ + for fileObj in self.openFiles.values(): + fileObj.close() + self.openFiles = {} + for (dirObj, dirIter) in self.openDirs.values(): + dirObj.close() + self.openDirs = {} + + + +class FileTransferClient(FileTransferBase): + + def __init__(self, extData={}): + """ + @param extData: a dict of extended_name : extended_data items + to be sent to the server. + """ + FileTransferBase.__init__(self) + self.extData = {} + self.counter = 0 + self.openRequests = {} # id -> Deferred + + + def connectionMade(self): + data = struct.pack('!L', max(self.versions)) + for (k, v) in itervalues(self.extData): + data += NS(k) + NS(v) + self.sendPacket(FXP_INIT, data) + + + def _sendRequest(self, msg, data): + data = struct.pack('!L', self.counter) + data + d = defer.Deferred() + self.openRequests[self.counter] = d + self.counter += 1 + self.sendPacket(msg, data) + return d + + + def _parseRequest(self, data): + (id,) = struct.unpack('!L', data[:4]) + d = self.openRequests[id] + del self.openRequests[id] + return d, data[4:] + + + def openFile(self, filename, flags, attrs): + """ + Open a file. + + This method returns a L{Deferred} that is called back with an object + that provides the L{ISFTPFile} interface. + + @type filename: L{bytes} + @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 this file. + + @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. + """ + data = NS(filename) + struct.pack('!L', flags) + self._packAttributes(attrs) + d = self._sendRequest(FXP_OPEN, data) + d.addCallback(self._cbOpenHandle, ClientFile, filename) + return d + + + def _cbOpenHandle(self, handle, handleClass, name): + """ + Callback invoked when an OPEN or OPENDIR request succeeds. + + @param handle: The handle returned by the server + @type handle: L{bytes} + @param handleClass: The class that will represent the + newly-opened file or directory to the user (either L{ClientFile} or + L{ClientDirectory}). + @param name: The name of the file or directory represented + by C{handle}. + @type name: L{bytes} + """ + cb = handleClass(self, handle) + cb.name = name + return cb + + + def removeFile(self, filename): + """ + Remove the given file. + + This method returns a Deferred that is called back when it succeeds. + + @type filename: L{bytes} + @param filename: the name of the file as a string. + """ + return self._sendRequest(FXP_REMOVE, NS(filename)) + + + def renameFile(self, oldpath, newpath): + """ + Rename the given file. + + This method returns a Deferred that is called back when it succeeds. + + @type oldpath: L{bytes} + @param oldpath: the current location of the file. + @type newpath: L{bytes} + @param newpath: the new file name. + """ + return self._sendRequest(FXP_RENAME, NS(oldpath)+NS(newpath)) + + + def makeDirectory(self, path, attrs): + """ + Make a directory. + + This method returns a Deferred that is called back when it is + created. + + @type path: L{bytes} + @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 openFile method. + """ + return self._sendRequest(FXP_MKDIR, NS(path)+self._packAttributes(attrs)) + + + def removeDirectory(self, path): + """ + Remove a directory (non-recursively) + + It is an error to remove a directory that has files or directories in + it. + + This method returns a Deferred that is called back when it is removed. + + @type path: L{bytes} + @param path: the directory to remove. + """ + return self._sendRequest(FXP_RMDIR, NS(path)) + + + def openDirectory(self, path): + """ + Open a directory for scanning. + + This method returns a Deferred that is called back with an iterable + object that has a close() method. + + 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 returns triples of the form (filename, longname, attrs) + or a Deferred that returns 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. + + @type path: L{bytes} + @param path: the directory to open. + """ + d = self._sendRequest(FXP_OPENDIR, NS(path)) + d.addCallback(self._cbOpenHandle, ClientDirectory, path) + return d + + + def getAttrs(self, path, followLinks=0): + """ + 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. + + @type path: L{bytes} + @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. + """ + if followLinks: m = FXP_STAT + else: m = FXP_LSTAT + return self._sendRequest(m, NS(path)) + + + def setAttrs(self, 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. + + @type path: L{bytes} + @param path: the path to set attributes for as a string. + @param attrs: a dictionary in the same format as the attrs argument to + openFile. + """ + data = NS(path) + self._packAttributes(attrs) + return self._sendRequest(FXP_SETSTAT, data) + + + def readLink(self, 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. + + @type path: L{bytes} + @param path: the path of the symlink to read. + """ + d = self._sendRequest(FXP_READLINK, NS(path)) + return d.addCallback(self._cbRealPath) + + + def makeLink(self, linkPath, targetPath): + """ + Create a symbolic link. + + This method returns when the link is made, or a Deferred that + returns the same. + + @type linkPath: L{bytes} + @param linkPath: the pathname of the symlink as a string + @type targetPath: L{bytes} + @param targetPath: the path of the target of the link as a string. + """ + return self._sendRequest(FXP_SYMLINK, NS(linkPath)+NS(targetPath)) + + + def realPath(self, path): + """ + Convert any path to an absolute path. + + This method returns the absolute path as a string, or a Deferred + that returns the same. + + @type path: L{bytes} + @param path: the path to convert as a string. + """ + d = self._sendRequest(FXP_REALPATH, NS(path)) + return d.addCallback(self._cbRealPath) + + + def _cbRealPath(self, result): + name, longname, attrs = result[0] + if _PY3: + name = name.decode("utf-8") + return name + + + def extendedRequest(self, request, data): + """ + Make an extended request of the server. + + The method returns a Deferred that is called back with + the result of the extended request. + + @type request: L{bytes} + @param request: the name of the extended request to make. + @type data: L{bytes} + @param data: any other data that goes along with the request. + """ + return self._sendRequest(FXP_EXTENDED, NS(request) + data) + + + def packet_VERSION(self, data): + version, = struct.unpack('!L', data[:4]) + data = data[4:] + d = {} + while data: + k, data = getNS(data) + v, data = getNS(data) + d[k]=v + self.version = version + self.gotServerVersion(version, d) + + + def packet_STATUS(self, data): + d, data = self._parseRequest(data) + code, = struct.unpack('!L', data[:4]) + data = data[4:] + if len(data) >= 4: + msg, data = getNS(data) + if len(data) >= 4: + lang, data = getNS(data) + else: + lang = b'' + else: + msg = b'' + lang = b'' + if code == FX_OK: + d.callback((msg, lang)) + elif code == FX_EOF: + d.errback(EOFError(msg)) + elif code == FX_OP_UNSUPPORTED: + d.errback(NotImplementedError(msg)) + else: + d.errback(SFTPError(code, nativeString(msg), lang)) + + + def packet_HANDLE(self, data): + d, data = self._parseRequest(data) + handle, _ = getNS(data) + d.callback(handle) + + + def packet_DATA(self, data): + d, data = self._parseRequest(data) + d.callback(getNS(data)[0]) + + + def packet_NAME(self, data): + d, data = self._parseRequest(data) + count, = struct.unpack('!L', data[:4]) + data = data[4:] + files = [] + for i in range(count): + filename, data = getNS(data) + longname, data = getNS(data) + attrs, data = self._parseAttributes(data) + files.append((filename, longname, attrs)) + d.callback(files) + + + def packet_ATTRS(self, data): + d, data = self._parseRequest(data) + d.callback(self._parseAttributes(data)[0]) + + + def packet_EXTENDED_REPLY(self, data): + d, data = self._parseRequest(data) + d.callback(data) + + + def gotServerVersion(self, serverVersion, extData): + """ + Called when the client sends their version info. + + @param otherVersion: an integer representing the version of the SFTP + protocol they are claiming. + @param extData: a dictionary of extended_name : extended_data items. + These items are sent by the client to indicate additional features. + """ + + + +@implementer(ISFTPFile) +class ClientFile: + def __init__(self, parent, handle): + self.parent = parent + self.handle = NS(handle) + + + def close(self): + return self.parent._sendRequest(FXP_CLOSE, self.handle) + + + def readChunk(self, offset, length): + data = self.handle + struct.pack("!QL", offset, length) + return self.parent._sendRequest(FXP_READ, data) + + + def writeChunk(self, offset, chunk): + data = self.handle + struct.pack("!Q", offset) + NS(chunk) + return self.parent._sendRequest(FXP_WRITE, data) + + + def getAttrs(self): + return self.parent._sendRequest(FXP_FSTAT, self.handle) + + + def setAttrs(self, attrs): + data = self.handle + self.parent._packAttributes(attrs) + return self.parent._sendRequest(FXP_FSTAT, data) + + + +class ClientDirectory: + + def __init__(self, parent, handle): + self.parent = parent + self.handle = NS(handle) + self.filesCache = [] + + + def read(self): + return self.parent._sendRequest(FXP_READDIR, self.handle) + + + def close(self): + if self.handle is None: + return defer.succeed(None) + d = self.parent._sendRequest(FXP_CLOSE, self.handle) + self.handle = None + return d + + + def __iter__(self): + return self + + + def __next__(self): + warnings.warn( + ('Using twisted.conch.ssh.filetransfer.ClientDirectory ' + 'as an iterator was deprecated in Twisted 18.9.0.'), + category=DeprecationWarning, + stacklevel=2) + if self.filesCache: + return self.filesCache.pop(0) + if self.filesCache is None: + raise StopIteration() + d = self.read() + d.addCallbacks(self._cbReadDir, self._ebReadDir) + return d + + next = __next__ + + + def _cbReadDir(self, names): + self.filesCache = names[1:] + return names[0] + + + def _ebReadDir(self, reason): + reason.trap(EOFError) + self.filesCache = None + return failure.Failure(StopIteration()) + + + +class SFTPError(Exception): + + def __init__(self, errorCode, errorMessage, lang=''): + Exception.__init__(self) + self.code = errorCode + self._message = errorMessage + self.lang = lang + + + def message(self): + """ + A string received over the network that explains the error to a human. + """ + # Python 2.6 deprecates assigning to the 'message' attribute of an + # exception. We define this read-only property here in order to + # prevent the warning about deprecation while maintaining backwards + # compatibility with object clients that rely on the 'message' + # attribute being set correctly. See bug #3897. + return self._message + message = property(message) + + + def __str__(self): + return 'SFTPError {}: {}'.format(self.code, self.message) + + + +FXP_INIT = 1 +FXP_VERSION = 2 +FXP_OPEN = 3 +FXP_CLOSE = 4 +FXP_READ = 5 +FXP_WRITE = 6 +FXP_LSTAT = 7 +FXP_FSTAT = 8 +FXP_SETSTAT = 9 +FXP_FSETSTAT = 10 +FXP_OPENDIR = 11 +FXP_READDIR = 12 +FXP_REMOVE = 13 +FXP_MKDIR = 14 +FXP_RMDIR = 15 +FXP_REALPATH = 16 +FXP_STAT = 17 +FXP_RENAME = 18 +FXP_READLINK = 19 +FXP_SYMLINK = 20 +FXP_STATUS = 101 +FXP_HANDLE = 102 +FXP_DATA = 103 +FXP_NAME = 104 +FXP_ATTRS = 105 +FXP_EXTENDED = 200 +FXP_EXTENDED_REPLY = 201 + +FILEXFER_ATTR_SIZE = 0x00000001 +FILEXFER_ATTR_UIDGID = 0x00000002 +FILEXFER_ATTR_OWNERGROUP = FILEXFER_ATTR_UIDGID +FILEXFER_ATTR_PERMISSIONS = 0x00000004 +FILEXFER_ATTR_ACMODTIME = 0x00000008 +FILEXFER_ATTR_EXTENDED = 0x80000000 + +FILEXFER_TYPE_REGULAR = 1 +FILEXFER_TYPE_DIRECTORY = 2 +FILEXFER_TYPE_SYMLINK = 3 +FILEXFER_TYPE_SPECIAL = 4 +FILEXFER_TYPE_UNKNOWN = 5 + +FXF_READ = 0x00000001 +FXF_WRITE = 0x00000002 +FXF_APPEND = 0x00000004 +FXF_CREAT = 0x00000008 +FXF_TRUNC = 0x00000010 +FXF_EXCL = 0x00000020 +FXF_TEXT = 0x00000040 + +FX_OK = 0 +FX_EOF = 1 +FX_NO_SUCH_FILE = 2 +FX_PERMISSION_DENIED = 3 +FX_FAILURE = 4 +FX_BAD_MESSAGE = 5 +FX_NO_CONNECTION = 6 +FX_CONNECTION_LOST = 7 +FX_OP_UNSUPPORTED = 8 +FX_FILE_ALREADY_EXISTS = 11 +# http://tools.ietf.org/wg/secsh/draft-ietf-secsh-filexfer/ defines more +# useful error codes, but so far OpenSSH doesn't implement them. We use them +# internally for clarity, but for now define them all as FX_FAILURE to be +# compatible with existing software. +FX_NOT_A_DIRECTORY = FX_FAILURE +FX_FILE_IS_A_DIRECTORY = FX_FAILURE + + +# initialize FileTransferBase.packetTypes: +g = globals() +for name in list(g.keys()): + if name.startswith('FXP_'): + value = g[name] + FileTransferBase.packetTypes[value] = name[4:] +del g, name, value diff --git a/contrib/python/Twisted/py2/twisted/conch/ssh/forwarding.py b/contrib/python/Twisted/py2/twisted/conch/ssh/forwarding.py new file mode 100644 index 0000000000..dd61e75f64 --- /dev/null +++ b/contrib/python/Twisted/py2/twisted/conch/ssh/forwarding.py @@ -0,0 +1,269 @@ +# 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 +""" + +from __future__ import division, absolute_import + +import struct + +from twisted.internet import protocol, reactor +from twisted.internet.endpoints import HostnameEndpoint, connectProtocol +from twisted.python import log +from twisted.python.compat import _PY3, unicode + +from twisted.conch.ssh import common, channel + +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): + log.msg('opened forwarding channel %s' % 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'): + log.msg('closing local forwarding channel %s' % 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} + """ + log.msg("connecting to %s:%i" % self.hostport) + 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 + log.msg("connected to %s:%i" % self.hostport) + 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} + """ + log.msg("failed to connect: %s" % 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: + log.msg('closed remote forwarding channel %s' % 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, unicode): + connHost = connHost.encode("utf-8") + if isinstance(origHost, unicode): + 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 _PY3 and isinstance(connHost, bytes): + connHost = connHost.decode("utf-8") + connPort = int(struct.unpack('>L', rest[:4])[0]) + origHost, rest = common.getNS(rest[4:]) + if _PY3 and 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 _PY3 and 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 +""" diff --git a/contrib/python/Twisted/py2/twisted/conch/ssh/keys.py b/contrib/python/Twisted/py2/twisted/conch/ssh/keys.py new file mode 100644 index 0000000000..fcbf9d2866 --- /dev/null +++ b/contrib/python/Twisted/py2/twisted/conch/ssh/keys.py @@ -0,0 +1,1678 @@ +# -*- test-case-name: twisted.conch.test.test_keys -*- +# Copyright (c) Twisted Matrix Laboratories. +# See LICENSE for details. + +""" +Handling of RSA, DSA, and EC keys. +""" + +from __future__ import absolute_import, division + +import binascii +import itertools + +from hashlib import md5, sha256 +import base64 +import struct +import warnings + +import bcrypt +from cryptography.exceptions import InvalidSignature +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import hashes, serialization +from cryptography.hazmat.primitives.asymmetric import dsa, rsa, padding, ec +from cryptography.hazmat.primitives.serialization import ( + load_pem_private_key, load_ssh_public_key) +from cryptography import utils + +try: + + from cryptography.hazmat.primitives.asymmetric.utils import ( + encode_dss_signature, decode_dss_signature) +except ImportError: + from cryptography.hazmat.primitives.asymmetric.utils import ( + encode_rfc6979_signature as encode_dss_signature, + decode_rfc6979_signature as decode_dss_signature) +from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes + +from pyasn1.error import PyAsn1Error +from pyasn1.type import univ +from pyasn1.codec.ber import decoder as berDecoder +from pyasn1.codec.ber import encoder as berEncoder + +from twisted.conch.ssh import common, sexpy +from twisted.conch.ssh.common import int_from_bytes, int_to_bytes +from twisted.python import randbytes +from twisted.python.compat import ( + iterbytes, long, izip, nativeString, unicode, _PY3, + _b64decodebytes as decodebytes, _b64encodebytes as encodebytes, + _bytesChr as chr) +from twisted.python.constants import NamedConstant, Names +from twisted.python.deprecate import _mutuallyExclusiveArguments + +# Curve lookup table +_curveTable = { + b'ecdsa-sha2-nistp256': ec.SECP256R1(), + b'ecdsa-sha2-nistp384': ec.SECP384R1(), + b'ecdsa-sha2-nistp521': ec.SECP521R1(), +} + +_secToNist = { + b'secp256r1' : b'nistp256', + b'secp384r1' : b'nistp384', + b'secp521r1' : b'nistp521', +} + + + + + +class BadKeyError(Exception): + """ + Raised when a key isn't what we expected from it. + + XXX: we really need to check for bad keys + """ + + + +class EncryptedKeyError(Exception): + """ + Raised when an encrypted key is presented to fromString/fromFile without + a password. + """ + + + +class BadFingerPrintFormat(Exception): + """ + Raises when unsupported fingerprint formats are presented to fingerprint. + """ + + + +class FingerprintFormats(Names): + """ + Constants representing the supported formats of key fingerprints. + + @cvar MD5_HEX: Named constant representing fingerprint format generated + using md5[RFC1321] algorithm in hexadecimal encoding. + @type MD5_HEX: L{twisted.python.constants.NamedConstant} + + @cvar SHA256_BASE64: Named constant representing fingerprint format + generated using sha256[RFC4634] algorithm in base64 encoding + @type SHA256_BASE64: L{twisted.python.constants.NamedConstant} + """ + MD5_HEX = NamedConstant() + SHA256_BASE64 = NamedConstant() + + + +class Key(object): + """ + An object representing a key. A key can be either a public or + private key. A public key can verify a signature; a private key can + create or verify a signature. To generate a string that can be stored + on disk, use the toString method. If you have a private key, but want + the string representation of the public key, use Key.public().toString(). + """ + + @classmethod + def fromFile(cls, filename, type=None, passphrase=None): + """ + Load a key from a file. + + @param filename: The path to load key data from. + + @type type: L{str} or L{None} + @param type: A string describing the format the key data is in, or + L{None} to attempt detection of the type. + + @type passphrase: L{bytes} or L{None} + @param passphrase: The passphrase the key is encrypted with, or L{None} + if there is no encryption. + + @rtype: L{Key} + @return: The loaded key. + """ + with open(filename, 'rb') as f: + return cls.fromString(f.read(), type, passphrase) + + + @classmethod + def fromString(cls, data, type=None, passphrase=None): + """ + Return a Key object corresponding to the string data. + type is optionally the type of string, matching a _fromString_* + method. Otherwise, the _guessStringType() classmethod will be used + to guess a type. If the key is encrypted, passphrase is used as + the decryption key. + + @type data: L{bytes} + @param data: The key data. + + @type type: L{str} or L{None} + @param type: A string describing the format the key data is in, or + L{None} to attempt detection of the type. + + @type passphrase: L{bytes} or L{None} + @param passphrase: The passphrase the key is encrypted with, or L{None} + if there is no encryption. + + @rtype: L{Key} + @return: The loaded key. + """ + if isinstance(data, unicode): + data = data.encode("utf-8") + if isinstance(passphrase, unicode): + passphrase = passphrase.encode("utf-8") + if type is None: + type = cls._guessStringType(data) + if type is None: + raise BadKeyError('cannot guess the type of %r' % (data,)) + method = getattr(cls, '_fromString_%s' % (type.upper(),), None) + if method is None: + raise BadKeyError('no _fromString method for %s' % (type,)) + if method.__code__.co_argcount == 2: # No passphrase + if passphrase: + raise BadKeyError('key not encrypted') + return method(data) + else: + return method(data, passphrase) + + + @classmethod + def _fromString_BLOB(cls, blob): + """ + Return a public key object corresponding to this public key blob. + The format of a RSA public key blob is:: + string 'ssh-rsa' + integer e + integer n + + The format of a DSA public key blob is:: + string 'ssh-dss' + integer p + integer q + integer g + integer y + + The format of ECDSA-SHA2-* public key blob is:: + string 'ecdsa-sha2-[identifier]' + integer x + integer y + + identifier is the standard NIST curve name. + + @type blob: L{bytes} + @param blob: The key data. + + @return: A new key. + @rtype: L{twisted.conch.ssh.keys.Key} + @raises BadKeyError: if the key type (the first string) is unknown. + """ + keyType, rest = common.getNS(blob) + if keyType == b'ssh-rsa': + e, n, rest = common.getMP(rest, 2) + return cls( + rsa.RSAPublicNumbers(e, n).public_key(default_backend())) + elif keyType == b'ssh-dss': + p, q, g, y, rest = common.getMP(rest, 4) + return cls( + dsa.DSAPublicNumbers( + y=y, + parameter_numbers=dsa.DSAParameterNumbers( + p=p, + q=q, + g=g + ) + ).public_key(default_backend()) + ) + elif keyType in _curveTable: + return cls( + ec.EllipticCurvePublicKey.from_encoded_point( + _curveTable[keyType], common.getNS(rest, 2)[1] + ) + ) + else: + raise BadKeyError('unknown blob type: %s' % (keyType,)) + + + @classmethod + def _fromString_PRIVATE_BLOB(cls, blob): + """ + Return a private key object corresponding to this private key blob. + The blob formats are as follows: + + RSA keys:: + string 'ssh-rsa' + integer n + integer e + integer d + integer u + integer p + integer q + + DSA keys:: + string 'ssh-dss' + integer p + integer q + integer g + integer y + integer x + + EC keys:: + string 'ecdsa-sha2-[identifier]' + string identifier + string q + integer privateValue + + identifier is the standard NIST curve name. + + + @type blob: L{bytes} + @param blob: The key data. + + @return: A new key. + @rtype: L{twisted.conch.ssh.keys.Key} + @raises BadKeyError: if + * the key type (the first string) is unknown + * the curve name of an ECDSA key does not match the key type + """ + keyType, rest = common.getNS(blob) + + if keyType == b'ssh-rsa': + n, e, d, u, p, q, rest = common.getMP(rest, 6) + return cls._fromRSAComponents(n=n, e=e, d=d, p=p, q=q) + elif keyType == b'ssh-dss': + p, q, g, y, x, rest = common.getMP(rest, 5) + return cls._fromDSAComponents(y=y, g=g, p=p, q=q, x=x) + elif keyType in _curveTable: + curve = _curveTable[keyType] + curveName, q, rest = common.getNS(rest, 2) + if curveName != _secToNist[curve.name.encode('ascii')]: + raise BadKeyError('ECDSA curve name %r does not match key ' + 'type %r' % (curveName, keyType)) + privateValue, rest = common.getMP(rest) + return cls._fromECEncodedPoint( + encodedPoint=q, curve=keyType, privateValue=privateValue) + else: + raise BadKeyError('unknown blob type: %s' % (keyType,)) + + + @classmethod + def _fromString_PUBLIC_OPENSSH(cls, data): + """ + Return a public key object corresponding to this OpenSSH public key + string. The format of an OpenSSH public key string is:: + <key type> <base64-encoded public key blob> + + @type data: L{bytes} + @param data: The key data. + + @return: A new key. + @rtype: L{twisted.conch.ssh.keys.Key} + @raises BadKeyError: if the blob type is unknown. + """ + # ECDSA keys don't need base64 decoding which is required + # for RSA or DSA key. + if data.startswith(b'ecdsa-sha2'): + return cls(load_ssh_public_key(data, default_backend())) + blob = decodebytes(data.split()[1]) + return cls._fromString_BLOB(blob) + + + @classmethod + def _fromPrivateOpenSSH_v1(cls, data, passphrase): + """ + Return a private key object corresponding to this OpenSSH private key + string, in the "openssh-key-v1" format introduced in OpenSSH 6.5. + + The format of an openssh-key-v1 private key string is:: + -----BEGIN OPENSSH PRIVATE KEY----- + <base64-encoded SSH protocol string> + -----END OPENSSH PRIVATE KEY----- + + The SSH protocol string is as described in + U{PROTOCOL.key<https://cvsweb.openbsd.org/cgi-bin/cvsweb/src/usr.bin/ssh/PROTOCOL.key>}. + + @type data: L{bytes} + @param data: The key data. + + @type passphrase: L{bytes} or L{None} + @param passphrase: The passphrase the key is encrypted with, or L{None} + if it is not encrypted. + + @return: A new key. + @rtype: L{twisted.conch.ssh.keys.Key} + @raises BadKeyError: if + * a passphrase is provided for an unencrypted key + * the SSH protocol encoding is incorrect + @raises EncryptedKeyError: if + * a passphrase is not provided for an encrypted key + """ + lines = data.strip().splitlines() + keyList = decodebytes(b''.join(lines[1:-1])) + if not keyList.startswith(b'openssh-key-v1\0'): + raise BadKeyError('unknown OpenSSH private key format') + keyList = keyList[len(b'openssh-key-v1\0'):] + cipher, kdf, kdfOptions, rest = common.getNS(keyList, 3) + n = struct.unpack('!L', rest[:4])[0] + if n != 1: + raise BadKeyError('only OpenSSH private key files containing ' + 'a single key are supported') + # Ignore public key + _, encPrivKeyList, _ = common.getNS(rest[4:], 2) + if cipher != b'none': + if not passphrase: + raise EncryptedKeyError('Passphrase must be provided ' + 'for an encrypted key') + # Determine cipher + if cipher in (b'aes128-ctr', b'aes192-ctr', b'aes256-ctr'): + algorithmClass = algorithms.AES + blockSize = 16 + keySize = int(cipher[3:6]) // 8 + ivSize = blockSize + else: + raise BadKeyError('unknown encryption type %r' % (cipher,)) + if kdf == b'bcrypt': + salt, rest = common.getNS(kdfOptions) + rounds = struct.unpack('!L', rest[:4])[0] + decKey = bcrypt.kdf( + passphrase, salt, keySize + ivSize, rounds, + # We can only use the number of rounds that OpenSSH used. + ignore_few_rounds=True) + else: + raise BadKeyError('unknown KDF type %r' % (kdf,)) + if (len(encPrivKeyList) % blockSize) != 0: + raise BadKeyError('bad padding') + decryptor = Cipher( + algorithmClass(decKey[:keySize]), + modes.CTR(decKey[keySize:keySize + ivSize]), + backend=default_backend() + ).decryptor() + privKeyList = ( + decryptor.update(encPrivKeyList) + decryptor.finalize()) + else: + if kdf != b'none': + raise BadKeyError('private key specifies KDF %r but no ' + 'cipher' % (kdf,)) + privKeyList = encPrivKeyList + check1 = struct.unpack('!L', privKeyList[:4])[0] + check2 = struct.unpack('!L', privKeyList[4:8])[0] + if check1 != check2: + raise BadKeyError('check values do not match: %d != %d' % + (check1, check2)) + return cls._fromString_PRIVATE_BLOB(privKeyList[8:]) + + + @classmethod + def _fromPrivateOpenSSH_PEM(cls, data, passphrase): + """ + Return a private key object corresponding to this OpenSSH private key + string, in the old PEM-based format. + + The format of a PEM-based OpenSSH private key string is:: + -----BEGIN <key type> PRIVATE KEY----- + [Proc-Type: 4,ENCRYPTED + DEK-Info: DES-EDE3-CBC,<initialization value>] + <base64-encoded ASN.1 structure> + ------END <key type> PRIVATE KEY------ + + The ASN.1 structure of a RSA key is:: + (0, n, e, d, p, q) + + The ASN.1 structure of a DSA key is:: + (0, p, q, g, y, x) + + The ASN.1 structure of a ECDSA key is:: + (ECParameters, OID, NULL) + + @type data: L{bytes} + @param data: The key data. + + @type passphrase: L{bytes} or L{None} + @param passphrase: The passphrase the key is encrypted with, or L{None} + if it is not encrypted. + + @return: A new key. + @rtype: L{twisted.conch.ssh.keys.Key} + @raises BadKeyError: if + * a passphrase is provided for an unencrypted key + * the ASN.1 encoding is incorrect + @raises EncryptedKeyError: if + * a passphrase is not provided for an encrypted key + """ + lines = data.strip().splitlines() + kind = lines[0][11:-17] + if lines[1].startswith(b'Proc-Type: 4,ENCRYPTED'): + if not passphrase: + raise EncryptedKeyError('Passphrase must be provided ' + 'for an encrypted key') + + # Determine cipher and initialization vector + try: + _, cipherIVInfo = lines[2].split(b' ', 1) + cipher, ivdata = cipherIVInfo.rstrip().split(b',', 1) + except ValueError: + raise BadKeyError('invalid DEK-info %r' % (lines[2],)) + + if cipher in (b'AES-128-CBC', b'AES-256-CBC'): + algorithmClass = algorithms.AES + keySize = int(cipher.split(b'-')[1]) // 8 + if len(ivdata) != 32: + raise BadKeyError('AES encrypted key with a bad IV') + elif cipher == b'DES-EDE3-CBC': + algorithmClass = algorithms.TripleDES + keySize = 24 + if len(ivdata) != 16: + raise BadKeyError('DES encrypted key with a bad IV') + else: + raise BadKeyError('unknown encryption type %r' % (cipher,)) + + # Extract keyData for decoding + iv = bytes(bytearray([int(ivdata[i:i + 2], 16) + for i in range(0, len(ivdata), 2)])) + ba = md5(passphrase + iv[:8]).digest() + bb = md5(ba + passphrase + iv[:8]).digest() + decKey = (ba + bb)[:keySize] + b64Data = decodebytes(b''.join(lines[3:-1])) + + decryptor = Cipher( + algorithmClass(decKey), + modes.CBC(iv), + backend=default_backend() + ).decryptor() + keyData = decryptor.update(b64Data) + decryptor.finalize() + + removeLen = ord(keyData[-1:]) + keyData = keyData[:-removeLen] + else: + b64Data = b''.join(lines[1:-1]) + keyData = decodebytes(b64Data) + + try: + decodedKey = berDecoder.decode(keyData)[0] + except PyAsn1Error as e: + raise BadKeyError( + 'Failed to decode key (Bad Passphrase?): %s' % (e,)) + + if kind == b'EC': + return cls( + load_pem_private_key(data, passphrase, default_backend())) + + if kind == b'RSA': + if len(decodedKey) == 2: # Alternate RSA key + decodedKey = decodedKey[0] + if len(decodedKey) < 6: + raise BadKeyError('RSA key failed to decode properly') + + n, e, d, p, q, dmp1, dmq1, iqmp = [ + long(value) for value in decodedKey[1:9] + ] + return cls( + rsa.RSAPrivateNumbers( + p=p, + q=q, + d=d, + dmp1=dmp1, + dmq1=dmq1, + iqmp=iqmp, + public_numbers=rsa.RSAPublicNumbers(e=e, n=n), + ).private_key(default_backend()) + ) + elif kind == b'DSA': + p, q, g, y, x = [long(value) for value in decodedKey[1: 6]] + if len(decodedKey) < 6: + raise BadKeyError('DSA key failed to decode properly') + return cls( + dsa.DSAPrivateNumbers( + x=x, + public_numbers=dsa.DSAPublicNumbers( + y=y, + parameter_numbers=dsa.DSAParameterNumbers( + p=p, + q=q, + g=g + ) + ) + ).private_key(backend=default_backend()) + ) + else: + raise BadKeyError("unknown key type %s" % (kind,)) + + + @classmethod + def _fromString_PRIVATE_OPENSSH(cls, data, passphrase): + """ + Return a private key object corresponding to this OpenSSH private key + string. If the key is encrypted, passphrase MUST be provided. + Providing a passphrase for an unencrypted key is an error. + + @type data: L{bytes} + @param data: The key data. + + @type passphrase: L{bytes} or L{None} + @param passphrase: The passphrase the key is encrypted with, or L{None} + if it is not encrypted. + + @return: A new key. + @rtype: L{twisted.conch.ssh.keys.Key} + @raises BadKeyError: if + * a passphrase is provided for an unencrypted key + * the encoding is incorrect + @raises EncryptedKeyError: if + * a passphrase is not provided for an encrypted key + """ + if data.strip().splitlines()[0][11:-17] == b'OPENSSH': + # New-format (openssh-key-v1) key + return cls._fromPrivateOpenSSH_v1(data, passphrase) + else: + # Old-format (PEM) key + return cls._fromPrivateOpenSSH_PEM(data, passphrase) + + @classmethod + def _fromString_PUBLIC_LSH(cls, data): + """ + Return a public key corresponding to this LSH public key string. + The LSH public key string format is:: + <s-expression: ('public-key', (<key type>, (<name, <value>)+))> + + The names for a RSA (key type 'rsa-pkcs1-sha1') key are: n, e. + The names for a DSA (key type 'dsa') key are: y, g, p, q. + + @type data: L{bytes} + @param data: The key data. + + @return: A new key. + @rtype: L{twisted.conch.ssh.keys.Key} + @raises BadKeyError: if the key type is unknown + """ + sexp = sexpy.parse(decodebytes(data[1:-1])) + assert sexp[0] == b'public-key' + kd = {} + for name, data in sexp[1][1:]: + kd[name] = common.getMP(common.NS(data))[0] + if sexp[1][0] == b'dsa': + return cls._fromDSAComponents( + y=kd[b'y'], g=kd[b'g'], p=kd[b'p'], q=kd[b'q']) + + elif sexp[1][0] == b'rsa-pkcs1-sha1': + return cls._fromRSAComponents(n=kd[b'n'], e=kd[b'e']) + else: + raise BadKeyError('unknown lsh key type %s' % (sexp[1][0],)) + + @classmethod + def _fromString_PRIVATE_LSH(cls, data): + """ + Return a private key corresponding to this LSH private key string. + The LSH private key string format is:: + <s-expression: ('private-key', (<key type>, (<name>, <value>)+))> + + The names for a RSA (key type 'rsa-pkcs1-sha1') key are: n, e, d, p, q. + The names for a DSA (key type 'dsa') key are: y, g, p, q, x. + + @type data: L{bytes} + @param data: The key data. + + @return: A new key. + @rtype: L{twisted.conch.ssh.keys.Key} + @raises BadKeyError: if the key type is unknown + """ + sexp = sexpy.parse(data) + assert sexp[0] == b'private-key' + kd = {} + for name, data in sexp[1][1:]: + kd[name] = common.getMP(common.NS(data))[0] + if sexp[1][0] == b'dsa': + assert len(kd) == 5, len(kd) + return cls._fromDSAComponents( + y=kd[b'y'], g=kd[b'g'], p=kd[b'p'], q=kd[b'q'], x=kd[b'x']) + elif sexp[1][0] == b'rsa-pkcs1': + assert len(kd) == 8, len(kd) + if kd[b'p'] > kd[b'q']: # Make p smaller than q + kd[b'p'], kd[b'q'] = kd[b'q'], kd[b'p'] + return cls._fromRSAComponents( + n=kd[b'n'], e=kd[b'e'], d=kd[b'd'], p=kd[b'p'], q=kd[b'q']) + + else: + raise BadKeyError('unknown lsh key type %s' % (sexp[1][0],)) + + @classmethod + def _fromString_AGENTV3(cls, data): + """ + Return a private key object corresponsing to the Secure Shell Key + Agent v3 format. + + The SSH Key Agent v3 format for a RSA key is:: + string 'ssh-rsa' + integer e + integer d + integer n + integer u + integer p + integer q + + The SSH Key Agent v3 format for a DSA key is:: + string 'ssh-dss' + integer p + integer q + integer g + integer y + integer x + + @type data: L{bytes} + @param data: The key data. + + @return: A new key. + @rtype: L{twisted.conch.ssh.keys.Key} + @raises BadKeyError: if the key type (the first string) is unknown + """ + keyType, data = common.getNS(data) + if keyType == b'ssh-dss': + p, data = common.getMP(data) + q, data = common.getMP(data) + g, data = common.getMP(data) + y, data = common.getMP(data) + x, data = common.getMP(data) + return cls._fromDSAComponents(y=y, g=g, p=p, q=q, x=x) + elif keyType == b'ssh-rsa': + e, data = common.getMP(data) + d, data = common.getMP(data) + n, data = common.getMP(data) + u, data = common.getMP(data) + p, data = common.getMP(data) + q, data = common.getMP(data) + return cls._fromRSAComponents(n=n, e=e, d=d, p=p, q=q, u=u) + else: + raise BadKeyError("unknown key type %s" % (keyType,)) + + @classmethod + def _guessStringType(cls, data): + """ + Guess the type of key in data. The types map to _fromString_* + methods. + + @type data: L{bytes} + @param data: The key data. + """ + if data.startswith(b'ssh-') or data.startswith(b'ecdsa-sha2-'): + return 'public_openssh' + elif data.startswith(b'-----BEGIN'): + return 'private_openssh' + elif data.startswith(b'{'): + return 'public_lsh' + elif data.startswith(b'('): + return 'private_lsh' + elif data.startswith(b'\x00\x00\x00\x07ssh-') or data.startswith(b'\x00\x00\x00\x13ecdsa-'): + ignored, rest = common.getNS(data) + count = 0 + while rest: + count += 1 + ignored, rest = common.getMP(rest) + if count > 4: + return 'agentv3' + else: + return 'blob' + + @classmethod + def _fromRSAComponents(cls, n, e, d=None, p=None, q=None, u=None): + """ + Build a key from RSA numerical components. + + @type n: L{int} + @param n: The 'n' RSA variable. + + @type e: L{int} + @param e: The 'e' RSA variable. + + @type d: L{int} or L{None} + @param d: The 'd' RSA variable (optional for a public key). + + @type p: L{int} or L{None} + @param p: The 'p' RSA variable (optional for a public key). + + @type q: L{int} or L{None} + @param q: The 'q' RSA variable (optional for a public key). + + @type u: L{int} or L{None} + @param u: The 'u' RSA variable. Ignored, as its value is determined by + p and q. + + @rtype: L{Key} + @return: An RSA key constructed from the values as given. + """ + publicNumbers = rsa.RSAPublicNumbers(e=e, n=n) + if d is None: + # We have public components. + keyObject = publicNumbers.public_key(default_backend()) + else: + privateNumbers = rsa.RSAPrivateNumbers( + p=p, + q=q, + d=d, + dmp1=rsa.rsa_crt_dmp1(d, p), + dmq1=rsa.rsa_crt_dmq1(d, q), + iqmp=rsa.rsa_crt_iqmp(p, q), + public_numbers=publicNumbers, + ) + keyObject = privateNumbers.private_key(default_backend()) + + return cls(keyObject) + + @classmethod + def _fromDSAComponents(cls, y, p, q, g, x=None): + """ + Build a key from DSA numerical components. + + @type y: L{int} + @param y: The 'y' DSA variable. + + @type p: L{int} + @param p: The 'p' DSA variable. + + @type q: L{int} + @param q: The 'q' DSA variable. + + @type g: L{int} + @param g: The 'g' DSA variable. + + @type x: L{int} or L{None} + @param x: The 'x' DSA variable (optional for a public key) + + @rtype: L{Key} + @return: A DSA key constructed from the values as given. + """ + publicNumbers = dsa.DSAPublicNumbers( + y=y, parameter_numbers=dsa.DSAParameterNumbers(p=p, q=q, g=g)) + if x is None: + # We have public components. + keyObject = publicNumbers.public_key(default_backend()) + else: + privateNumbers = dsa.DSAPrivateNumbers( + x=x, public_numbers=publicNumbers) + keyObject = privateNumbers.private_key(default_backend()) + + return cls(keyObject) + + @classmethod + def _fromECComponents(cls, x, y, curve, privateValue=None): + """ + Build a key from EC components. + + @param x: The affine x component of the public point used for verifying. + @type x: L{int} + + @param y: The affine y component of the public point used for verifying. + @type y: L{int} + + @param curve: NIST name of elliptic curve. + @type curve: L{bytes} + + @param privateValue: The private value. + @type privateValue: L{int} + """ + + publicNumbers = ec.EllipticCurvePublicNumbers( + x=x, y=y, curve=_curveTable[curve]) + if privateValue is None: + # We have public components. + keyObject = publicNumbers.public_key(default_backend()) + else: + privateNumbers = ec.EllipticCurvePrivateNumbers( + private_value=privateValue, public_numbers=publicNumbers) + keyObject = privateNumbers.private_key(default_backend()) + + return cls(keyObject) + + @classmethod + def _fromECEncodedPoint(cls, encodedPoint, curve, privateValue=None): + """ + Build a key from an EC encoded point. + + @param encodedPoint: The public point encoded as in SEC 1 v2.0 + section 2.3.3. + @type encodedPoint: L{bytes} + + @param curve: NIST name of elliptic curve. + @type curve: L{bytes} + + @param privateValue: The private value. + @type privateValue: L{int} + """ + + if privateValue is None: + # We have public components. + keyObject = ec.EllipticCurvePublicKey.from_encoded_point( + _curveTable[curve], encodedPoint + ) + else: + keyObject = ec.derive_private_key( + privateValue, _curveTable[curve], default_backend() + ) + + return cls(keyObject) + + def __init__(self, keyObject): + """ + Initialize with a private or public + C{cryptography.hazmat.primitives.asymmetric} key. + + @param keyObject: Low level key. + @type keyObject: C{cryptography.hazmat.primitives.asymmetric} key. + """ + self._keyObject = keyObject + + def __eq__(self, other): + """ + Return True if other represents an object with the same key. + """ + if type(self) == type(other): + return self.type() == other.type() and self.data() == other.data() + else: + return NotImplemented + + def __ne__(self, other): + """ + Return True if other represents anything other than this key. + """ + result = self.__eq__(other) + if result == NotImplemented: + return result + return not result + + def __repr__(self): + """ + Return a pretty representation of this object. + """ + if self.type() == 'EC': + data = self.data() + name = data['curve'].decode('utf-8') + + if self.isPublic(): + out = '<Elliptic Curve Public Key (%s bits)' % (name[-3:],) + else: + out = '<Elliptic Curve Private Key (%s bits)' % (name[-3:],) + + for k, v in sorted(data.items()): + if _PY3 and k == 'curve': + out += "\ncurve:\n\t%s" % (name,) + else: + out += "\n%s:\n\t%s" % (k, v) + + return out + ">\n" + else: + lines = [ + '<%s %s (%s bits)' % ( + nativeString(self.type()), + self.isPublic() and 'Public Key' or 'Private Key', + self._keyObject.key_size)] + for k, v in sorted(self.data().items()): + lines.append('attr %s:' % (k,)) + by = common.MP(v)[4:] + while by: + m = by[:15] + by = by[15:] + o = '' + for c in iterbytes(m): + o = o + '%02x:' % (ord(c),) + if len(m) < 15: + o = o[:-1] + lines.append('\t' + o) + lines[-1] = lines[-1] + '>' + return '\n'.join(lines) + + def isPublic(self): + """ + Check if this instance is a public key. + + @return: C{True} if this is a public key. + """ + return isinstance( + self._keyObject, + (rsa.RSAPublicKey, dsa.DSAPublicKey, ec.EllipticCurvePublicKey)) + + def public(self): + """ + Returns a version of this key containing only the public key data. + If this is a public key, this may or may not be the same object + as self. + + @rtype: L{Key} + @return: A public key. + """ + if self.isPublic(): + return self + else: + return Key(self._keyObject.public_key()) + + def fingerprint(self, format=FingerprintFormats.MD5_HEX): + """ + The fingerprint of a public key consists of the output of the + message-digest algorithm in the specified format. + Supported formats include L{FingerprintFormats.MD5_HEX} and + L{FingerprintFormats.SHA256_BASE64} + + The input to the algorithm is the public key data as specified by [RFC4253]. + + The output of sha256[RFC4634] algorithm is presented to the + user in the form of base64 encoded sha256 hashes. + Example: C{US5jTUa0kgX5ZxdqaGF0yGRu8EgKXHNmoT8jHKo1StM=} + + The output of the MD5[RFC1321](default) algorithm is presented to the user as + a sequence of 16 octets printed as hexadecimal with lowercase letters + and separated by colons. + Example: C{c1:b1:30:29:d7:b8:de:6c:97:77:10:d7:46:41:63:87} + + @param format: Format for fingerprint generation. Consists + hash function and representation format. + Default is L{FingerprintFormats.MD5_HEX} + + @since: 8.2 + + @return: the user presentation of this L{Key}'s fingerprint, as a + string. + + @rtype: L{str} + """ + if format is FingerprintFormats.SHA256_BASE64: + return nativeString(base64.b64encode( + sha256(self.blob()).digest())) + elif format is FingerprintFormats.MD5_HEX: + return nativeString( + b':'.join([binascii.hexlify(x) + for x in iterbytes(md5(self.blob()).digest())])) + else: + raise BadFingerPrintFormat( + 'Unsupported fingerprint format: %s' % (format,)) + + def type(self): + """ + Return the type of the object we wrap. Currently this can only be + 'RSA', 'DSA', or 'EC'. + + @rtype: L{str} + @raises RuntimeError: If the object type is unknown. + """ + if isinstance( + self._keyObject, (rsa.RSAPublicKey, rsa.RSAPrivateKey)): + return 'RSA' + elif isinstance( + self._keyObject, (dsa.DSAPublicKey, dsa.DSAPrivateKey)): + return 'DSA' + elif isinstance( + self._keyObject, (ec.EllipticCurvePublicKey, ec.EllipticCurvePrivateKey)): + return 'EC' + else: + raise RuntimeError( + 'unknown type of object: %r' % (self._keyObject,)) + + def sshType(self): + """ + Get the type of the object we wrap as defined in the SSH protocol, + defined in RFC 4253, Section 6.6. Currently this can only be b'ssh-rsa', + b'ssh-dss' or b'ecdsa-sha2-[identifier]'. + + identifier is the standard NIST curve name + + @return: The key type format. + @rtype: L{bytes} + """ + if self.type() == 'EC': + return b'ecdsa-sha2-' + _secToNist[self._keyObject.curve.name.encode('ascii')] + else: + return {'RSA': b'ssh-rsa', 'DSA': b'ssh-dss'}[self.type()] + + def size(self): + """ + Return the size of the object we wrap. + + @return: The size of the key. + @rtype: L{int} + """ + if self._keyObject is None: + return 0 + elif self.type() == 'EC': + return self._keyObject.curve.key_size + return self._keyObject.key_size + + def data(self): + """ + Return the values of the public key as a dictionary. + + @rtype: L{dict} + """ + if isinstance(self._keyObject, rsa.RSAPublicKey): + numbers = self._keyObject.public_numbers() + return { + "n": numbers.n, + "e": numbers.e, + } + elif isinstance(self._keyObject, rsa.RSAPrivateKey): + numbers = self._keyObject.private_numbers() + return { + "n": numbers.public_numbers.n, + "e": numbers.public_numbers.e, + "d": numbers.d, + "p": numbers.p, + "q": numbers.q, + # Use a trick: iqmp is q^-1 % p, u is p^-1 % q + "u": rsa.rsa_crt_iqmp(numbers.q, numbers.p), + } + elif isinstance(self._keyObject, dsa.DSAPublicKey): + numbers = self._keyObject.public_numbers() + return { + "y": numbers.y, + "g": numbers.parameter_numbers.g, + "p": numbers.parameter_numbers.p, + "q": numbers.parameter_numbers.q, + } + elif isinstance(self._keyObject, dsa.DSAPrivateKey): + numbers = self._keyObject.private_numbers() + return { + "x": numbers.x, + "y": numbers.public_numbers.y, + "g": numbers.public_numbers.parameter_numbers.g, + "p": numbers.public_numbers.parameter_numbers.p, + "q": numbers.public_numbers.parameter_numbers.q, + } + elif isinstance(self._keyObject, ec.EllipticCurvePublicKey): + numbers = self._keyObject.public_numbers() + return { + "x": numbers.x, + "y": numbers.y, + "curve": self.sshType(), + } + elif isinstance(self._keyObject, ec.EllipticCurvePrivateKey): + numbers = self._keyObject.private_numbers() + return { + "x": numbers.public_numbers.x, + "y": numbers.public_numbers.y, + "privateValue": numbers.private_value, + "curve": self.sshType(), + } + + else: + raise RuntimeError("Unexpected key type: %s" % (self._keyObject,)) + + def blob(self): + """ + Return the public key blob for this key. The blob is the + over-the-wire format for public keys. + + SECSH-TRANS RFC 4253 Section 6.6. + + RSA keys:: + string 'ssh-rsa' + integer e + integer n + + DSA keys:: + string 'ssh-dss' + integer p + integer q + integer g + integer y + + EC keys:: + string 'ecdsa-sha2-[identifier]' + integer x + integer y + + identifier is the standard NIST curve name + + @rtype: L{bytes} + """ + type = self.type() + data = self.data() + if type == 'RSA': + return (common.NS(b'ssh-rsa') + common.MP(data['e']) + + common.MP(data['n'])) + elif type == 'DSA': + return (common.NS(b'ssh-dss') + common.MP(data['p']) + + common.MP(data['q']) + common.MP(data['g']) + + common.MP(data['y'])) + else: # EC + byteLength = (self._keyObject.curve.key_size + 7) // 8 + return (common.NS(data['curve']) + common.NS(data["curve"][-8:]) + + common.NS(b'\x04' + utils.int_to_bytes(data['x'], byteLength) + + utils.int_to_bytes(data['y'], byteLength))) + + + def privateBlob(self): + """ + Return the private key blob for this key. The blob is the + over-the-wire format for private keys: + + Specification in OpenSSH PROTOCOL.agent + + RSA keys:: + string 'ssh-rsa' + integer n + integer e + integer d + integer u + integer p + integer q + + DSA keys:: + string 'ssh-dss' + integer p + integer q + integer g + integer y + integer x + + EC keys:: + string 'ecdsa-sha2-[identifier]' + integer x + integer y + integer privateValue + + identifier is the NIST standard curve name. + """ + type = self.type() + data = self.data() + if type == 'RSA': + iqmp = rsa.rsa_crt_iqmp(data['p'], data['q']) + return (common.NS(b'ssh-rsa') + common.MP(data['n']) + + common.MP(data['e']) + common.MP(data['d']) + + common.MP(iqmp) + common.MP(data['p']) + + common.MP(data['q'])) + elif type == 'DSA': + return (common.NS(b'ssh-dss') + common.MP(data['p']) + + common.MP(data['q']) + common.MP(data['g']) + + common.MP(data['y']) + common.MP(data['x'])) + else: # EC + encPub = self._keyObject.public_key().public_bytes( + serialization.Encoding.X962, + serialization.PublicFormat.UncompressedPoint + ) + return (common.NS(data['curve']) + common.NS(data['curve'][-8:]) + + common.NS(encPub) + common.MP(data['privateValue'])) + + @_mutuallyExclusiveArguments([ + ['extra', 'comment'], + ['extra', 'passphrase'], + ]) + def toString(self, type, extra=None, subtype=None, comment=None, + passphrase=None): + """ + Create a string representation of this key. If the key is a private + key and you want the representation of its public key, use + C{key.public().toString()}. type maps to a _toString_* method. + + @param type: The type of string to emit. Currently supported values + are C{'OPENSSH'}, C{'LSH'}, and C{'AGENTV3'}. + @type type: L{str} + + @param extra: Any extra data supported by the selected format which + is not part of the key itself. For public OpenSSH keys, this is + a comment. For private OpenSSH keys, this is a passphrase to + encrypt with. (Deprecated since Twisted 20.3.0; use C{comment} + or C{passphrase} as appropriate instead.) + @type extra: L{bytes} or L{unicode} or L{None} + + @param subtype: A subtype of the requested C{type} to emit. Only + supported for private OpenSSH keys, for which the currently + supported subtypes are C{'PEM'} and C{'v1'}. If not given, an + appropriate default is used. + @type subtype: L{str} or L{None} + + @param comment: A comment to include with the key. Only supported + for OpenSSH keys. + + Present since Twisted 20.3.0. + + @type comment: L{bytes} or L{unicode} or L{None} + + @param passphrase: A passphrase to encrypt the key with. Only + supported for private OpenSSH keys. + + Present since Twisted 20.3.0. + + @type passphrase: L{bytes} or L{unicode} or L{None} + + @rtype: L{bytes} + """ + if extra is not None: + # Compatibility with old parameter format. + warnings.warn( + "The 'extra' argument to " + "twisted.conch.ssh.keys.Key.toString was deprecated in " + "Twisted 20.3.0; use 'comment' or 'passphrase' instead.", + DeprecationWarning, stacklevel=3) + if self.isPublic(): + comment = extra + else: + passphrase = extra + if isinstance(comment, unicode): + comment = comment.encode("utf-8") + if isinstance(passphrase, unicode): + passphrase = passphrase.encode("utf-8") + method = getattr(self, '_toString_%s' % (type.upper(),), None) + if method is None: + raise BadKeyError('unknown key type: %s' % (type,)) + return method(subtype=subtype, comment=comment, passphrase=passphrase) + + def _toPublicOpenSSH(self, comment=None): + """ + Return a public OpenSSH key string. + + See _fromString_PUBLIC_OPENSSH for the string format. + + @type comment: L{bytes} or L{None} + @param comment: A comment to include with the key, or L{None} to + omit the comment. + """ + if self.type() == 'EC': + if not comment: + comment = b'' + return (self._keyObject.public_bytes( + serialization.Encoding.OpenSSH, + serialization.PublicFormat.OpenSSH + ) + b' ' + comment).strip() + + b64Data = encodebytes(self.blob()).replace(b'\n', b'') + if not comment: + comment = b'' + return (self.sshType() + b' ' + b64Data + b' ' + comment).strip() + + def _toPrivateOpenSSH_v1(self, comment=None, passphrase=None): + """ + Return a private OpenSSH key string, in the "openssh-key-v1" format + introduced in OpenSSH 6.5. + + See _fromPrivateOpenSSH_v1 for the string format. + + @type passphrase: L{bytes} or L{None} + @param passphrase: The passphrase to encrypt the key with, or L{None} + if it is not encrypted. + """ + if passphrase: + # For now we just hardcode the cipher to the one used by + # OpenSSH. We could make this configurable later if it's + # needed. + cipher = algorithms.AES + cipherName = b'aes256-ctr' + kdfName = b'bcrypt' + blockSize = cipher.block_size // 8 + keySize = 32 + ivSize = blockSize + salt = randbytes.secureRandom(ivSize) + rounds = 100 + kdfOptions = common.NS(salt) + struct.pack('!L', rounds) + else: + cipherName = b'none' + kdfName = b'none' + blockSize = 8 + kdfOptions = b'' + check = randbytes.secureRandom(4) + privKeyList = ( + check + check + self.privateBlob() + common.NS(comment or b'')) + padByte = 0 + while len(privKeyList) % blockSize: + padByte += 1 + privKeyList += chr(padByte & 0xFF) + if passphrase: + encKey = bcrypt.kdf(passphrase, salt, keySize + ivSize, 100) + encryptor = Cipher( + cipher(encKey[:keySize]), + modes.CTR(encKey[keySize:keySize + ivSize]), + backend=default_backend() + ).encryptor() + encPrivKeyList = ( + encryptor.update(privKeyList) + encryptor.finalize()) + else: + encPrivKeyList = privKeyList + blob = ( + b'openssh-key-v1\0' + + common.NS(cipherName) + + common.NS(kdfName) + common.NS(kdfOptions) + + struct.pack('!L', 1) + + common.NS(self.blob()) + + common.NS(encPrivKeyList)) + b64Data = encodebytes(blob).replace(b'\n', b'') + lines = ( + [b'-----BEGIN OPENSSH PRIVATE KEY-----'] + + [b64Data[i:i + 64] for i in range(0, len(b64Data), 64)] + + [b'-----END OPENSSH PRIVATE KEY-----']) + return b'\n'.join(lines) + b'\n' + + def _toPrivateOpenSSH_PEM(self, passphrase=None): + """ + Return a private OpenSSH key string, in the old PEM-based format. + + See _fromPrivateOpenSSH_PEM for the string format. + + @type passphrase: L{bytes} or L{None} + @param passphrase: The passphrase to encrypt the key with, or L{None} + if it is not encrypted. + """ + if self.type() == 'EC': + # EC keys has complex ASN.1 structure hence we do this this way. + if not passphrase: + # unencrypted private key + encryptor = serialization.NoEncryption() + else: + encryptor = serialization.BestAvailableEncryption(passphrase) + + return self._keyObject.private_bytes( + serialization.Encoding.PEM, + serialization.PrivateFormat.TraditionalOpenSSL, + encryptor) + + data = self.data() + lines = [b''.join((b'-----BEGIN ', self.type().encode('ascii'), + b' PRIVATE KEY-----'))] + if self.type() == 'RSA': + p, q = data['p'], data['q'] + iqmp = rsa.rsa_crt_iqmp(p, q) + objData = (0, data['n'], data['e'], data['d'], p, q, + data['d'] % (p - 1), data['d'] % (q - 1), + iqmp) + else: + objData = (0, data['p'], data['q'], data['g'], data['y'], + data['x']) + asn1Sequence = univ.Sequence() + for index, value in izip(itertools.count(), objData): + asn1Sequence.setComponentByPosition(index, univ.Integer(value)) + asn1Data = berEncoder.encode(asn1Sequence) + if passphrase: + iv = randbytes.secureRandom(8) + hexiv = ''.join(['%02X' % (ord(x),) for x in iterbytes(iv)]) + hexiv = hexiv.encode('ascii') + lines.append(b'Proc-Type: 4,ENCRYPTED') + lines.append(b'DEK-Info: DES-EDE3-CBC,' + hexiv + b'\n') + ba = md5(passphrase + iv).digest() + bb = md5(ba + passphrase + iv).digest() + encKey = (ba + bb)[:24] + padLen = 8 - (len(asn1Data) % 8) + asn1Data += chr(padLen) * padLen + + encryptor = Cipher( + algorithms.TripleDES(encKey), + modes.CBC(iv), + backend=default_backend() + ).encryptor() + + asn1Data = encryptor.update(asn1Data) + encryptor.finalize() + + b64Data = encodebytes(asn1Data).replace(b'\n', b'') + lines += [b64Data[i:i + 64] for i in range(0, len(b64Data), 64)] + lines.append(b''.join((b'-----END ', self.type().encode('ascii'), + b' PRIVATE KEY-----'))) + return b'\n'.join(lines) + + def _toString_OPENSSH(self, subtype=None, comment=None, passphrase=None): + """ + Return a public or private OpenSSH string. See + _fromString_PUBLIC_OPENSSH and _fromPrivateOpenSSH_PEM for the + string formats. If extra is present, it represents a comment for a + public key, or a passphrase for a private key. + + @param extra: Comment for a public key or passphrase for a + private key + @type extra: L{bytes} + + @rtype: L{bytes} + """ + if self.isPublic(): + return self._toPublicOpenSSH(comment=comment) + elif subtype is None or subtype == 'PEM': + return self._toPrivateOpenSSH_PEM(passphrase=passphrase) + elif subtype == 'v1': + return self._toPrivateOpenSSH_v1( + comment=comment, passphrase=passphrase) + else: + raise ValueError('unknown subtype %s' % (subtype,)) + + def _toString_LSH(self, **kwargs): + """ + Return a public or private LSH key. See _fromString_PUBLIC_LSH and + _fromString_PRIVATE_LSH for the key formats. + + @rtype: L{bytes} + """ + data = self.data() + type = self.type() + if self.isPublic(): + if type == 'RSA': + keyData = sexpy.pack([[b'public-key', + [b'rsa-pkcs1-sha1', + [b'n', common.MP(data['n'])[4:]], + [b'e', common.MP(data['e'])[4:]]]]]) + elif type == 'DSA': + keyData = sexpy.pack([[b'public-key', + [b'dsa', + [b'p', common.MP(data['p'])[4:]], + [b'q', common.MP(data['q'])[4:]], + [b'g', common.MP(data['g'])[4:]], + [b'y', common.MP(data['y'])[4:]]]]]) + else: + raise BadKeyError("unknown key type %s" % (type,)) + return (b'{' + encodebytes(keyData).replace(b'\n', b'') + + b'}') + else: + if type == 'RSA': + p, q = data['p'], data['q'] + iqmp = rsa.rsa_crt_iqmp(p, q) + return sexpy.pack([[b'private-key', + [b'rsa-pkcs1', + [b'n', common.MP(data['n'])[4:]], + [b'e', common.MP(data['e'])[4:]], + [b'd', common.MP(data['d'])[4:]], + [b'p', common.MP(q)[4:]], + [b'q', common.MP(p)[4:]], + [b'a', common.MP( + data['d'] % (q - 1))[4:]], + [b'b', common.MP( + data['d'] % (p - 1))[4:]], + [b'c', common.MP(iqmp)[4:]]]]]) + elif type == 'DSA': + return sexpy.pack([[b'private-key', + [b'dsa', + [b'p', common.MP(data['p'])[4:]], + [b'q', common.MP(data['q'])[4:]], + [b'g', common.MP(data['g'])[4:]], + [b'y', common.MP(data['y'])[4:]], + [b'x', common.MP(data['x'])[4:]]]]]) + else: + raise BadKeyError("unknown key type %s'" % (type,)) + + def _toString_AGENTV3(self, **kwargs): + """ + Return a private Secure Shell Agent v3 key. See + _fromString_AGENTV3 for the key format. + + @rtype: L{bytes} + """ + data = self.data() + if not self.isPublic(): + if self.type() == 'RSA': + values = (data['e'], data['d'], data['n'], data['u'], + data['p'], data['q']) + elif self.type() == 'DSA': + values = (data['p'], data['q'], data['g'], data['y'], + data['x']) + return common.NS(self.sshType()) + b''.join(map(common.MP, values)) + + def sign(self, data): + """ + Sign some data with this key. + + SECSH-TRANS RFC 4253 Section 6.6. + + @type data: L{bytes} + @param data: The data to sign. + + @rtype: L{bytes} + @return: A signature for the given data. + """ + keyType = self.type() + if keyType == 'RSA': + sig = self._keyObject.sign(data, padding.PKCS1v15(), hashes.SHA1()) + ret = common.NS(sig) + + elif keyType == 'DSA': + sig = self._keyObject.sign(data, hashes.SHA1()) + (r, s) = decode_dss_signature(sig) + # SSH insists that the DSS signature blob be two 160-bit integers + # concatenated together. The sig[0], [1] numbers from obj.sign + # are just numbers, and could be any length from 0 to 160 bits. + # Make sure they are padded out to 160 bits (20 bytes each) + ret = common.NS(int_to_bytes(r, 20) + int_to_bytes(s, 20)) + + elif keyType == 'EC': # Pragma: no branch + # Hash size depends on key size + keySize = self.size() + if keySize <= 256: + hashSize = hashes.SHA256() + elif keySize <= 384: + hashSize = hashes.SHA384() + else: + hashSize = hashes.SHA512() + signature = self._keyObject.sign(data, ec.ECDSA(hashSize)) + (r, s) = decode_dss_signature(signature) + + rb = int_to_bytes(r) + sb = int_to_bytes(s) + + # Int_to_bytes returns rb[0] as a str in python2 + # and an as int in python3 + if type(rb[0]) is str: + rcomp = ord(rb[0]) + else: + rcomp = rb[0] + + # If the MSB is set, prepend a null byte for correct formatting. + if rcomp & 0x80: + rb = b"\x00" + rb + + if type(sb[0]) is str: + scomp = ord(sb[0]) + else: + scomp = sb[0] + + if scomp & 0x80: + sb = b"\x00" + sb + + ret = common.NS(common.NS(rb) + common.NS(sb)) + return common.NS(self.sshType()) + ret + + def verify(self, signature, data): + """ + Verify a signature using this key. + + @type signature: L{bytes} + @param signature: The signature to verify. + + @type data: L{bytes} + @param data: The signed data. + + @rtype: L{bool} + @return: C{True} if the signature is valid. + """ + if len(signature) == 40: + # DSA key with no padding + signatureType, signature = b'ssh-dss', common.NS(signature) + else: + signatureType, signature = common.getNS(signature) + + if signatureType != self.sshType(): + return False + + keyType = self.type() + if keyType == 'RSA': + k = self._keyObject + if not self.isPublic(): + k = k.public_key() + args = ( + common.getNS(signature)[0], + data, + padding.PKCS1v15(), + hashes.SHA1(), + ) + elif keyType == 'DSA': + concatenatedSignature = common.getNS(signature)[0] + r = int_from_bytes(concatenatedSignature[:20], 'big') + s = int_from_bytes(concatenatedSignature[20:], 'big') + signature = encode_dss_signature(r, s) + k = self._keyObject + if not self.isPublic(): + k = k.public_key() + args = (signature, data, hashes.SHA1()) + + elif keyType == 'EC': # Pragma: no branch + concatenatedSignature = common.getNS(signature)[0] + rstr, sstr, rest = common.getNS(concatenatedSignature, 2) + r = int_from_bytes(rstr, 'big') + s = int_from_bytes(sstr, 'big') + signature = encode_dss_signature(r, s) + + k = self._keyObject + if not self.isPublic(): + k = k.public_key() + + keySize = self.size() + if keySize <= 256: # Hash size depends on key size + hashSize = hashes.SHA256() + elif keySize <= 384: + hashSize = hashes.SHA384() + else: + hashSize = hashes.SHA512() + args = (signature, data, ec.ECDSA(hashSize)) + + try: + k.verify(*args) + except InvalidSignature: + return False + else: + return True + + +def _getPersistentRSAKey(location, keySize=4096): + """ + This function returns a persistent L{Key}. + + The key is loaded from a PEM file in C{location}. If it does not exist, a + key with the key size of C{keySize} is generated and saved. + + @param location: Where the key is stored. + @type location: L{twisted.python.filepath.FilePath} + + @param keySize: The size of the key, if it needs to be generated. + @type keySize: L{int} + + @returns: A persistent key. + @rtype: L{Key} + """ + location.parent().makedirs(ignoreExistingDirectory=True) + + # If it doesn't exist, we want to generate a new key and save it + if not location.exists(): + privateKey = rsa.generate_private_key( + public_exponent=65537, + key_size=keySize, + backend=default_backend() + ) + + pem = privateKey.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.TraditionalOpenSSL, + encryption_algorithm=serialization.NoEncryption() + ) + + location.setContent(pem) + + # By this point (save any hilarious race conditions) we should have a + # working PEM file. Load it! + # (Future archaeological readers: I chose not to short circuit above, + # because then there's two exit paths to this code!) + with location.open("rb") as keyFile: + privateKey = serialization.load_pem_private_key( + keyFile.read(), + password=None, + backend=default_backend() + ) + return Key(privateKey) diff --git a/contrib/python/Twisted/py2/twisted/conch/ssh/service.py b/contrib/python/Twisted/py2/twisted/conch/ssh/service.py new file mode 100644 index 0000000000..94a34cce9b --- /dev/null +++ b/contrib/python/Twisted/py2/twisted/conch/ssh/service.py @@ -0,0 +1,48 @@ +# 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 __future__ import division, absolute_import + +from twisted.python import log + +class SSHService(log.Logger): + name = None # this is the ssh name for the service + protocolMessages = {} # these map #'s -> protocol names + transport = None # gets set later + + 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 %s" % (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) + log.msg("couldn't handle %r" % messageNum) + log.msg(repr(packet)) + self.transport.sendUnimplemented() diff --git a/contrib/python/Twisted/py2/twisted/conch/ssh/session.py b/contrib/python/Twisted/py2/twisted/conch/ssh/session.py new file mode 100644 index 0000000000..3a2f5d54c7 --- /dev/null +++ b/contrib/python/Twisted/py2/twisted/conch/ssh/session.py @@ -0,0 +1,362 @@ +# -*- 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 +""" + +from __future__ import division, absolute_import + +import struct +import signal +import sys +import os + +from zope.interface import implementer + +from twisted.internet import interfaces, protocol +from twisted.python import log +from twisted.python.compat import _bytesChr as chr, networkString +from twisted.conch.interfaces import ISession +from twisted.conch.ssh import common, channel, connection + + +class SSHSession(channel.SSHChannel): + + 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.msg('asking for subsystem "%s"' % 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.msg('failed to get subsystem') + return 0 + + def request_shell(self, data): + log.msg('getting shell') + if not self.session: + self.session = ISession(self.avatar) + try: + pp = SSHSessionProcessProtocol(self) + self.session.openShell(pp) + except: + log.deferr() + 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.msg('executing command "%s"' % f) + try: + pp = SSHSessionProcessProtocol(self) + self.session.execCommand(pp, f) + except: + log.deferr() + 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.msg('pty request: %r %r' % (term, windowSize)) + try: + self.session.getPty(term, windowSize, modes) + except: + log.err() + 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: + log.msg('error changing window size') + log.err() + 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.msg('weird extended data: %s'%dataType) + + def eofReceived(self): + if self.session: + self.session.eofReceived() + elif self.client: + self.conn.sendClose(self) + + def closed(self): + if self.session: + self.session.closed() + elif self.client: + self.client.transport.loseConnection() + + #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.msg('exitSignal: %s (core dumped)' % (signame,)) + coreDumped = 1 + else: + log.msg('exitSignal: %s' % (signame,)) + coreDumped = 0 + self.session.conn.sendRequest( + self.session, b'exit-signal', + common.NS(networkString(signame[3:])) + chr(coreDumped) + + common.NS(b'') + common.NS(b'')) + elif err.exitCode is not None: + log.msg('exitCode: %r' % (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) diff --git a/contrib/python/Twisted/py2/twisted/conch/ssh/sexpy.py b/contrib/python/Twisted/py2/twisted/conch/ssh/sexpy.py new file mode 100644 index 0000000000..c5f102e4f1 --- /dev/null +++ b/contrib/python/Twisted/py2/twisted/conch/ssh/sexpy.py @@ -0,0 +1,45 @@ +# Copyright (c) Twisted Matrix Laboratories. +# See LICENSE for details. + +from __future__ import absolute_import, division + +from twisted.python.compat import intToBytes + + +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 0, "this should not happen" + +def pack(sexp): + s = b"" + for o in sexp: + if type(o) in (type(()), type([])): + s+=b'(' + s+=pack(o) + s+=b')' + else: + s+=intToBytes(len(o)) + b":" + o + return s diff --git a/contrib/python/Twisted/py2/twisted/conch/ssh/transport.py b/contrib/python/Twisted/py2/twisted/conch/ssh/transport.py new file mode 100644 index 0000000000..e5d75eab18 --- /dev/null +++ b/contrib/python/Twisted/py2/twisted/conch/ssh/transport.py @@ -0,0 +1,2127 @@ +# -*- test-case-name: twisted.conch.test.test_transport -*- +# Copyright (c) Twisted Matrix Laboratories. +# See LICENSE for details. + +""" +The lowest level SSH protocol. This handles the key negotiation, the +encryption and the compression. The transport layer is described in +RFC 4253. + +Maintainer: Paul Swartz +""" + +from __future__ import absolute_import, division + +import binascii +import hmac +import struct +import zlib + +from hashlib import md5, sha1, sha256, sha384, sha512 + +from cryptography.exceptions import UnsupportedAlgorithm +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.ciphers import algorithms, modes, Cipher +from cryptography.hazmat.primitives.asymmetric import dh, ec, x25519 + +from twisted import __version__ as twisted_version +from twisted.internet import protocol, defer +from twisted.python import log, randbytes +from twisted.python.compat import iterbytes, _bytesChr as chr, networkString + +# This import is needed if SHA256 hashing is used. +# from twisted.python.compat import nativeString + +from twisted.conch.ssh import address, keys, _kex +from twisted.conch.ssh.common import ( + NS, getNS, MP, getMP, ffs, int_from_bytes +) + + + +def _mpFromBytes(data): + """Make an SSH multiple-precision integer from big-endian L{bytes}. + + Used in ECDH key exchange. + + @type data: L{bytes} + @param data: The input data, interpreted as a big-endian octet string. + + @rtype: L{bytes} + @return: The given data encoded as an SSH multiple-precision integer. + """ + return MP(int_from_bytes(data, 'big')) + + + +class _MACParams(tuple): + """ + L{_MACParams} represents the parameters necessary to compute SSH MAC + (Message Authenticate Codes). + + L{_MACParams} is a L{tuple} subclass to maintain compatibility with older + versions of the code. The elements of a L{_MACParams} are:: + + 0. The digest object used for the MAC + 1. The inner pad ("ipad") string + 2. The outer pad ("opad") string + 3. The size of the digest produced by the digest object + + L{_MACParams} is also an object lesson in why tuples are a bad type for + public APIs. + + @ivar key: The HMAC key which will be used. + """ + + + +class SSHCiphers: + """ + SSHCiphers represents all the encryption operations that need to occur + to encrypt and authenticate the SSH connection. + + @cvar cipherMap: A dictionary mapping SSH encryption names to 3-tuples of + (<cryptography.hazmat.primitives.interfaces.CipherAlgorithm>, + <block size>, <cryptography.hazmat.primitives.interfaces.Mode>) + @cvar macMap: A dictionary mapping SSH MAC names to hash modules. + + @ivar outCipType: the string type of the outgoing cipher. + @ivar inCipType: the string type of the incoming cipher. + @ivar outMACType: the string type of the incoming MAC. + @ivar inMACType: the string type of the incoming MAC. + @ivar encBlockSize: the block size of the outgoing cipher. + @ivar decBlockSize: the block size of the incoming cipher. + @ivar verifyDigestSize: the size of the incoming MAC. + @ivar outMAC: a tuple of (<hash module>, <inner key>, <outer key>, + <digest size>) representing the outgoing MAC. + @ivar inMAc: see outMAC, but for the incoming MAC. + """ + + cipherMap = { + b'3des-cbc': (algorithms.TripleDES, 24, modes.CBC), + b'blowfish-cbc': (algorithms.Blowfish, 16, modes.CBC), + b'aes256-cbc': (algorithms.AES, 32, modes.CBC), + b'aes192-cbc': (algorithms.AES, 24, modes.CBC), + b'aes128-cbc': (algorithms.AES, 16, modes.CBC), + b'cast128-cbc': (algorithms.CAST5, 16, modes.CBC), + b'aes128-ctr': (algorithms.AES, 16, modes.CTR), + b'aes192-ctr': (algorithms.AES, 24, modes.CTR), + b'aes256-ctr': (algorithms.AES, 32, modes.CTR), + b'3des-ctr': (algorithms.TripleDES, 24, modes.CTR), + b'blowfish-ctr': (algorithms.Blowfish, 16, modes.CTR), + b'cast128-ctr': (algorithms.CAST5, 16, modes.CTR), + b'none': (None, 0, modes.CBC), + } + macMap = { + b'hmac-sha2-512': sha512, + b'hmac-sha2-384': sha384, + b'hmac-sha2-256': sha256, + b'hmac-sha1': sha1, + b'hmac-md5': md5, + b'none': None + } + + + def __init__(self, outCip, inCip, outMac, inMac): + self.outCipType = outCip + self.inCipType = inCip + self.outMACType = outMac + self.inMACType = inMac + self.encBlockSize = 0 + self.decBlockSize = 0 + self.verifyDigestSize = 0 + self.outMAC = (None, b'', b'', 0) + self.inMAC = (None, b'', b'', 0) + + + def setKeys(self, outIV, outKey, inIV, inKey, outInteg, inInteg): + """ + Set up the ciphers and hashes using the given keys, + + @param outIV: the outgoing initialization vector + @param outKey: the outgoing encryption key + @param inIV: the incoming initialization vector + @param inKey: the incoming encryption key + @param outInteg: the outgoing integrity key + @param inInteg: the incoming integrity key. + """ + o = self._getCipher(self.outCipType, outIV, outKey) + self.encryptor = o.encryptor() + self.encBlockSize = o.algorithm.block_size // 8 + o = self._getCipher(self.inCipType, inIV, inKey) + self.decryptor = o.decryptor() + self.decBlockSize = o.algorithm.block_size // 8 + self.outMAC = self._getMAC(self.outMACType, outInteg) + self.inMAC = self._getMAC(self.inMACType, inInteg) + if self.inMAC: + self.verifyDigestSize = self.inMAC[3] + + + def _getCipher(self, cip, iv, key): + """ + Creates an initialized cipher object. + + @param cip: the name of the cipher, maps into cipherMap + @param iv: the initialzation vector + @param key: the encryption key + + @return: the cipher object. + """ + algorithmClass, keySize, modeClass = self.cipherMap[cip] + if algorithmClass is None: + return _DummyCipher() + + return Cipher( + algorithmClass(key[:keySize]), + modeClass(iv[:algorithmClass.block_size // 8]), + backend=default_backend(), + ) + + + def _getMAC(self, mac, key): + """ + Gets a 4-tuple representing the message authentication code. + (<hash module>, <inner hash value>, <outer hash value>, + <digest size>) + + @type mac: L{bytes} + @param mac: a key mapping into macMap + + @type key: L{bytes} + @param key: the MAC key. + + @rtype: L{bytes} + @return: The MAC components. + """ + mod = self.macMap[mac] + if not mod: + return (None, b'', b'', 0) + + # With stdlib we can only get attributes fron an instantiated object. + hashObject = mod() + digestSize = hashObject.digest_size + blockSize = hashObject.block_size + + # Truncation here appears to contravene RFC 2104, section 2. However, + # implementing the hashing behavior prescribed by the RFC breaks + # interoperability with OpenSSH (at least version 5.5p1). + key = key[:digestSize] + (b'\x00' * (blockSize - digestSize)) + i = key.translate(hmac.trans_36) + o = key.translate(hmac.trans_5C) + result = _MACParams((mod, i, o, digestSize)) + result.key = key + return result + + + def encrypt(self, blocks): + """ + Encrypt some data. + + @type blocks: L{bytes} + @param blocks: The data to encrypt. + + @rtype: L{bytes} + @return: The encrypted data. + """ + return self.encryptor.update(blocks) + + + def decrypt(self, blocks): + """ + Decrypt some data. + + @type blocks: L{bytes} + @param blocks: The data to decrypt. + + @rtype: L{bytes} + @return: The decrypted data. + """ + return self.decryptor.update(blocks) + + + def makeMAC(self, seqid, data): + """ + Create a message authentication code (MAC) for the given packet using + the outgoing MAC values. + + @type seqid: L{int} + @param seqid: The sequence ID of the outgoing packet. + + @type data: L{bytes} + @param data: The data to create a MAC for. + + @rtype: L{str} + @return: The serialized MAC. + """ + if not self.outMAC[0]: + return b'' + data = struct.pack('>L', seqid) + data + return hmac.HMAC(self.outMAC.key, data, self.outMAC[0]).digest() + + + def verify(self, seqid, data, mac): + """ + Verify an incoming MAC using the incoming MAC values. + + @type seqid: L{int} + @param seqid: The sequence ID of the incoming packet. + + @type data: L{bytes} + @param data: The packet data to verify. + + @type mac: L{bytes} + @param mac: The MAC sent with the packet. + + @rtype: L{bool} + @return: C{True} if the MAC is valid. + """ + if not self.inMAC[0]: + return mac == b'' + data = struct.pack('>L', seqid) + data + outer = hmac.HMAC(self.inMAC.key, data, self.inMAC[0]).digest() + return mac == outer + + + +def _getSupportedCiphers(): + """ + Build a list of ciphers that are supported by the backend in use. + + @return: a list of supported ciphers. + @rtype: L{list} of L{str} + """ + supportedCiphers = [] + cs = [b'aes256-ctr', b'aes256-cbc', b'aes192-ctr', b'aes192-cbc', + b'aes128-ctr', b'aes128-cbc', b'cast128-ctr', b'cast128-cbc', + b'blowfish-ctr', b'blowfish-cbc', b'3des-ctr', b'3des-cbc'] + for cipher in cs: + algorithmClass, keySize, modeClass = SSHCiphers.cipherMap[cipher] + try: + Cipher( + algorithmClass(b' ' * keySize), + modeClass(b' ' * (algorithmClass.block_size // 8)), + backend=default_backend(), + ).encryptor() + except UnsupportedAlgorithm: + pass + else: + supportedCiphers.append(cipher) + return supportedCiphers + + + +class SSHTransportBase(protocol.Protocol): + """ + Protocol supporting basic SSH functionality: sending/receiving packets + and message dispatch. To connect to or run a server, you must use + SSHClientTransport or SSHServerTransport. + + @ivar protocolVersion: A string representing the version of the SSH + protocol we support. Currently defaults to '2.0'. + + @ivar version: A string representing the version of the server or client. + Currently defaults to 'Twisted'. + + @ivar comment: An optional string giving more information about the + server or client. + + @ivar supportedCiphers: A list of strings representing the encryption + algorithms supported, in order from most-preferred to least. + + @ivar supportedMACs: A list of strings representing the message + authentication codes (hashes) supported, in order from most-preferred + to least. Both this and supportedCiphers can include 'none' to use + no encryption or authentication, but that must be done manually, + + @ivar supportedKeyExchanges: A list of strings representing the + key exchanges supported, in order from most-preferred to least. + + @ivar supportedPublicKeys: A list of strings representing the + public key types supported, in order from most-preferred to least. + + @ivar supportedCompressions: A list of strings representing compression + types supported, from most-preferred to least. + + @ivar supportedLanguages: A list of strings representing languages + supported, from most-preferred to least. + + @ivar supportedVersions: A container of strings representing supported ssh + protocol version numbers. + + @ivar isClient: A boolean indicating whether this is a client or server. + + @ivar gotVersion: A boolean indicating whether we have received the + version string from the other side. + + @ivar buf: Data we've received but hasn't been parsed into a packet. + + @ivar outgoingPacketSequence: the sequence number of the next packet we + will send. + + @ivar incomingPacketSequence: the sequence number of the next packet we + are expecting from the other side. + + @ivar outgoingCompression: an object supporting the .compress(str) and + .flush() methods, or None if there is no outgoing compression. Used to + compress outgoing data. + + @ivar outgoingCompressionType: A string representing the outgoing + compression type. + + @ivar incomingCompression: an object supporting the .decompress(str) + method, or None if there is no incoming compression. Used to + decompress incoming data. + + @ivar incomingCompressionType: A string representing the incoming + compression type. + + @ivar ourVersionString: the version string that we sent to the other side. + Used in the key exchange. + + @ivar otherVersionString: the version string sent by the other side. Used + in the key exchange. + + @ivar ourKexInitPayload: the MSG_KEXINIT payload we sent. Used in the key + exchange. + + @ivar otherKexInitPayload: the MSG_KEXINIT payload we received. Used in + the key exchange + + @ivar sessionID: a string that is unique to this SSH session. Created as + part of the key exchange, sessionID is used to generate the various + encryption and authentication keys. + + @ivar service: an SSHService instance, or None. If it's set to an object, + it's the currently running service. + + @ivar kexAlg: the agreed-upon key exchange algorithm. + + @ivar keyAlg: the agreed-upon public key type for the key exchange. + + @ivar currentEncryptions: an SSHCiphers instance. It represents the + current encryption and authentication options for the transport. + + @ivar nextEncryptions: an SSHCiphers instance. Held here until the + MSG_NEWKEYS messages are exchanged, when nextEncryptions is + transitioned to currentEncryptions. + + @ivar first: the first bytes of the next packet. In order to avoid + decrypting data twice, the first bytes are decrypted and stored until + the whole packet is available. + + @ivar _keyExchangeState: The current protocol state with respect to key + exchange. This is either C{_KEY_EXCHANGE_NONE} if no key exchange is + in progress (and returns to this value after any key exchange + completqes), C{_KEY_EXCHANGE_REQUESTED} if this side of the connection + initiated a key exchange, and C{_KEY_EXCHANGE_PROGRESSING} if the other + side of the connection initiated a key exchange. C{_KEY_EXCHANGE_NONE} + is the initial value (however SSH connections begin with key exchange, + so it will quickly change to another state). + + @ivar _blockedByKeyExchange: Whenever C{_keyExchangeState} is not + C{_KEY_EXCHANGE_NONE}, this is a C{list} of pending messages which were + passed to L{sendPacket} but could not be sent because it is not legal + to send them while a key exchange is in progress. When the key + exchange completes, another attempt is made to send these messages. + """ + protocolVersion = b'2.0' + version = b'Twisted_' + twisted_version.encode('ascii') + comment = b'' + ourVersionString = (b'SSH-' + protocolVersion + b'-' + version + b' ' + + comment).strip() + + # L{None} is supported as cipher and hmac. For security they are disabled + # by default. To enable them, subclass this class and add it, or do: + # SSHTransportBase.supportedCiphers.append('none') + # List ordered by preference. + supportedCiphers = _getSupportedCiphers() + supportedMACs = [ + b'hmac-sha2-512', + b'hmac-sha2-384', + b'hmac-sha2-256', + b'hmac-sha1', + b'hmac-md5', + # `none`, + ] + + supportedKeyExchanges = _kex.getSupportedKeyExchanges() + supportedPublicKeys = [] + + # Add the supported EC keys, and change the name from ecdh* to ecdsa* + for eckey in supportedKeyExchanges: + if eckey.find(b'ecdh') != -1: + supportedPublicKeys += [eckey.replace(b'ecdh', b'ecdsa')] + + supportedPublicKeys += [b'ssh-rsa', b'ssh-dss'] + + supportedCompressions = [b'none', b'zlib'] + supportedLanguages = () + supportedVersions = (b'1.99', b'2.0') + isClient = False + gotVersion = False + buf = b'' + outgoingPacketSequence = 0 + incomingPacketSequence = 0 + outgoingCompression = None + incomingCompression = None + sessionID = None + service = None + + # There is no key exchange activity in progress. + _KEY_EXCHANGE_NONE = '_KEY_EXCHANGE_NONE' + + # Key exchange is in progress and we started it. + _KEY_EXCHANGE_REQUESTED = '_KEY_EXCHANGE_REQUESTED' + + # Key exchange is in progress and both sides have sent KEXINIT messages. + _KEY_EXCHANGE_PROGRESSING = '_KEY_EXCHANGE_PROGRESSING' + + # There is a fourth conceptual state not represented here: KEXINIT received + # but not sent. Since we always send a KEXINIT as soon as we get it, we + # can't ever be in that state. + + # The current key exchange state. + _keyExchangeState = _KEY_EXCHANGE_NONE + _blockedByKeyExchange = None + + def connectionLost(self, reason): + """ + When the underlying connection is closed, stop the running service (if + any), and log out the avatar (if any). + + @type reason: L{twisted.python.failure.Failure} + @param reason: The cause of the connection being closed. + """ + if self.service: + self.service.serviceStopped() + if hasattr(self, 'avatar'): + self.logoutFunction() + log.msg('connection lost') + + + def connectionMade(self): + """ + Called when the connection is made to the other side. We sent our + version and the MSG_KEXINIT packet. + """ + self.transport.write(self.ourVersionString + b'\r\n') + self.currentEncryptions = SSHCiphers(b'none', b'none', b'none', + b'none') + self.currentEncryptions.setKeys(b'', b'', b'', b'', b'', b'') + self.sendKexInit() + + + def sendKexInit(self): + """ + Send a I{KEXINIT} message to initiate key exchange or to respond to a + key exchange initiated by the peer. + + @raise RuntimeError: If a key exchange has already been started and it + is not appropriate to send a I{KEXINIT} message at this time. + + @return: L{None} + """ + if self._keyExchangeState != self._KEY_EXCHANGE_NONE: + raise RuntimeError( + "Cannot send KEXINIT while key exchange state is %r" % ( + self._keyExchangeState,)) + + self.ourKexInitPayload = b''.join([ + chr(MSG_KEXINIT), + randbytes.secureRandom(16), + NS(b','.join(self.supportedKeyExchanges)), + NS(b','.join(self.supportedPublicKeys)), + NS(b','.join(self.supportedCiphers)), + NS(b','.join(self.supportedCiphers)), + NS(b','.join(self.supportedMACs)), + NS(b','.join(self.supportedMACs)), + NS(b','.join(self.supportedCompressions)), + NS(b','.join(self.supportedCompressions)), + NS(b','.join(self.supportedLanguages)), + NS(b','.join(self.supportedLanguages)), + b'\000\000\000\000\000']) + self.sendPacket(MSG_KEXINIT, self.ourKexInitPayload[1:]) + self._keyExchangeState = self._KEY_EXCHANGE_REQUESTED + self._blockedByKeyExchange = [] + + + def _allowedKeyExchangeMessageType(self, messageType): + """ + Determine if the given message type may be sent while key exchange is + in progress. + + @param messageType: The type of message + @type messageType: L{int} + + @return: C{True} if the given type of message may be sent while key + exchange is in progress, C{False} if it may not. + @rtype: L{bool} + + @see: U{http://tools.ietf.org/html/rfc4253#section-7.1} + """ + # Written somewhat peculularly to reflect the way the specification + # defines the allowed message types. + if 1 <= messageType <= 19: + return messageType not in (MSG_SERVICE_REQUEST, MSG_SERVICE_ACCEPT) + if 20 <= messageType <= 29: + return messageType not in (MSG_KEXINIT,) + return 30 <= messageType <= 49 + + + def sendPacket(self, messageType, payload): + """ + Sends a packet. If it's been set up, compress the data, encrypt it, + and authenticate it before sending. If key exchange is in progress and + the message is not part of key exchange, queue it to be sent later. + + @param messageType: The type of the packet; generally one of the + MSG_* values. + @type messageType: L{int} + @param payload: The payload for the message. + @type payload: L{str} + """ + if self._keyExchangeState != self._KEY_EXCHANGE_NONE: + if not self._allowedKeyExchangeMessageType(messageType): + self._blockedByKeyExchange.append((messageType, payload)) + return + + payload = chr(messageType) + payload + if self.outgoingCompression: + payload = (self.outgoingCompression.compress(payload) + + self.outgoingCompression.flush(2)) + bs = self.currentEncryptions.encBlockSize + # 4 for the packet length and 1 for the padding length + totalSize = 5 + len(payload) + lenPad = bs - (totalSize % bs) + if lenPad < 4: + lenPad = lenPad + bs + packet = (struct.pack('!LB', + totalSize + lenPad - 4, lenPad) + + payload + randbytes.secureRandom(lenPad)) + encPacket = ( + self.currentEncryptions.encrypt(packet) + + self.currentEncryptions.makeMAC( + self.outgoingPacketSequence, packet)) + self.transport.write(encPacket) + self.outgoingPacketSequence += 1 + + + def getPacket(self): + """ + Try to return a decrypted, authenticated, and decompressed packet + out of the buffer. If there is not enough data, return None. + + @rtype: L{str} or L{None} + @return: The decoded packet, if any. + """ + bs = self.currentEncryptions.decBlockSize + ms = self.currentEncryptions.verifyDigestSize + if len(self.buf) < bs: + # Not enough data for a block + return + if not hasattr(self, 'first'): + first = self.currentEncryptions.decrypt(self.buf[:bs]) + else: + first = self.first + del self.first + packetLen, paddingLen = struct.unpack('!LB', first[:5]) + if packetLen > 1048576: # 1024 ** 2 + self.sendDisconnect( + DISCONNECT_PROTOCOL_ERROR, + networkString('bad packet length {}'.format(packetLen))) + return + if len(self.buf) < packetLen + 4 + ms: + # Not enough data for a packet + self.first = first + return + if (packetLen + 4) % bs != 0: + self.sendDisconnect( + DISCONNECT_PROTOCOL_ERROR, + networkString( + 'bad packet mod (%i%%%i == %i)' % ( + packetLen + 4, bs, (packetLen + 4) % bs))) + return + encData, self.buf = self.buf[:4 + packetLen], self.buf[4 + packetLen:] + packet = first + self.currentEncryptions.decrypt(encData[bs:]) + if len(packet) != 4 + packetLen: + self.sendDisconnect(DISCONNECT_PROTOCOL_ERROR, + b'bad decryption') + return + if ms: + macData, self.buf = self.buf[:ms], self.buf[ms:] + if not self.currentEncryptions.verify(self.incomingPacketSequence, + packet, macData): + self.sendDisconnect(DISCONNECT_MAC_ERROR, b'bad MAC') + return + payload = packet[5:-paddingLen] + if self.incomingCompression: + try: + payload = self.incomingCompression.decompress(payload) + except: + # Tolerate any errors in decompression + log.err() + self.sendDisconnect(DISCONNECT_COMPRESSION_ERROR, + b'compression error') + return + self.incomingPacketSequence += 1 + return payload + + + def _unsupportedVersionReceived(self, remoteVersion): + """ + Called when an unsupported version of the ssh protocol is received from + the remote endpoint. + + @param remoteVersion: remote ssh protocol version which is unsupported + by us. + @type remoteVersion: L{str} + """ + self.sendDisconnect(DISCONNECT_PROTOCOL_VERSION_NOT_SUPPORTED, + b'bad version ' + remoteVersion) + + + def dataReceived(self, data): + """ + First, check for the version string (SSH-2.0-*). After that has been + received, this method adds data to the buffer, and pulls out any + packets. + + @type data: L{bytes} + @param data: The data that was received. + """ + self.buf = self.buf + data + if not self.gotVersion: + if self.buf.find(b'\n', self.buf.find(b'SSH-')) == -1: + return + + # RFC 4253 section 4.2 ask for strict `\r\n` line ending. + # Here we are a bit more relaxed and accept implementations ending + # only in '\n'. + # https://tools.ietf.org/html/rfc4253#section-4.2 + lines = self.buf.split(b'\n') + for p in lines: + if p.startswith(b'SSH-'): + self.gotVersion = True + # Since the line was split on '\n' and most of the time + # it uses '\r\n' we may get an extra '\r'. + self.otherVersionString = p.rstrip(b'\r') + remoteVersion = p.split(b'-')[1] + if remoteVersion not in self.supportedVersions: + self._unsupportedVersionReceived(remoteVersion) + return + i = lines.index(p) + self.buf = b'\n'.join(lines[i + 1:]) + packet = self.getPacket() + while packet: + messageNum = ord(packet[0:1]) + self.dispatchMessage(messageNum, packet[1:]) + packet = self.getPacket() + + + def dispatchMessage(self, messageNum, payload): + """ + Send a received message to the appropriate method. + + @type messageNum: L{int} + @param messageNum: The message number. + + @type payload: L{bytes} + @param payload: The message payload. + """ + if messageNum < 50 and messageNum in messages: + messageType = messages[messageNum][4:] + f = getattr(self, 'ssh_%s' % (messageType,), None) + if f is not None: + f(payload) + else: + log.msg("couldn't handle %s" % messageType) + log.msg(repr(payload)) + self.sendUnimplemented() + elif self.service: + log.callWithLogger(self.service, self.service.packetReceived, + messageNum, payload) + else: + log.msg("couldn't handle %s" % messageNum) + log.msg(repr(payload)) + self.sendUnimplemented() + + + def getPeer(self): + """ + Returns an L{SSHTransportAddress} corresponding to the other (peer) + side of this transport. + + @return: L{SSHTransportAddress} for the peer + @rtype: L{SSHTransportAddress} + @since: 12.1 + """ + return address.SSHTransportAddress(self.transport.getPeer()) + + + def getHost(self): + """ + Returns an L{SSHTransportAddress} corresponding to the this side of + transport. + + @return: L{SSHTransportAddress} for the peer + @rtype: L{SSHTransportAddress} + @since: 12.1 + """ + return address.SSHTransportAddress(self.transport.getHost()) + + + @property + def kexAlg(self): + """ + The key exchange algorithm name agreed between client and server. + """ + return self._kexAlg + + + @kexAlg.setter + def kexAlg(self, value): + """ + Set the key exchange algorithm name. + """ + self._kexAlg = value + + # Client-initiated rekeying looks like this: + # + # C> MSG_KEXINIT + # S> MSG_KEXINIT + # C> MSG_KEX_DH_GEX_REQUEST or MSG_KEXDH_INIT + # S> MSG_KEX_DH_GEX_GROUP or MSG_KEXDH_REPLY + # C> MSG_KEX_DH_GEX_INIT or -- + # S> MSG_KEX_DH_GEX_REPLY or -- + # C> MSG_NEWKEYS + # S> MSG_NEWKEYS + # + # Server-initiated rekeying is the same, only the first two messages are + # switched. + + + def ssh_KEXINIT(self, packet): + """ + Called when we receive a MSG_KEXINIT message. Payload:: + bytes[16] cookie + string keyExchangeAlgorithms + string keyAlgorithms + string incomingEncryptions + string outgoingEncryptions + string incomingAuthentications + string outgoingAuthentications + string incomingCompressions + string outgoingCompressions + string incomingLanguages + string outgoingLanguages + bool firstPacketFollows + unit32 0 (reserved) + + Starts setting up the key exchange, keys, encryptions, and + authentications. Extended by ssh_KEXINIT in SSHServerTransport and + SSHClientTransport. + + @type packet: L{bytes} + @param packet: The message data. + + @return: A L{tuple} of negotiated key exchange algorithms, key + algorithms, and unhandled data, or L{None} if something went wrong. + """ + self.otherKexInitPayload = chr(MSG_KEXINIT) + packet + # This is useless to us: + # cookie = packet[: 16] + k = getNS(packet[16:], 10) + strings, rest = k[:-1], k[-1] + (kexAlgs, keyAlgs, encCS, encSC, macCS, macSC, compCS, compSC, langCS, + langSC) = [s.split(b',') for s in strings] + # These are the server directions + outs = [encSC, macSC, compSC] + ins = [encCS, macSC, compCS] + if self.isClient: + outs, ins = ins, outs # Switch directions + server = (self.supportedKeyExchanges, self.supportedPublicKeys, + self.supportedCiphers, self.supportedCiphers, + self.supportedMACs, self.supportedMACs, + self.supportedCompressions, self.supportedCompressions) + client = (kexAlgs, keyAlgs, outs[0], ins[0], outs[1], ins[1], + outs[2], ins[2]) + if self.isClient: + server, client = client, server + self.kexAlg = ffs(client[0], server[0]) + self.keyAlg = ffs(client[1], server[1]) + self.nextEncryptions = SSHCiphers( + ffs(client[2], server[2]), + ffs(client[3], server[3]), + ffs(client[4], server[4]), + ffs(client[5], server[5])) + self.outgoingCompressionType = ffs(client[6], server[6]) + self.incomingCompressionType = ffs(client[7], server[7]) + if None in (self.kexAlg, self.keyAlg, self.outgoingCompressionType, + self.incomingCompressionType): + self.sendDisconnect(DISCONNECT_KEY_EXCHANGE_FAILED, + b"couldn't match all kex parts") + return + if None in self.nextEncryptions.__dict__.values(): + self.sendDisconnect(DISCONNECT_KEY_EXCHANGE_FAILED, + b"couldn't match all kex parts") + return + log.msg('kex alg, key alg: %r %r' % (self.kexAlg, self.keyAlg)) + log.msg('outgoing: %r %r %r' % (self.nextEncryptions.outCipType, + self.nextEncryptions.outMACType, + self.outgoingCompressionType)) + log.msg('incoming: %r %r %r' % (self.nextEncryptions.inCipType, + self.nextEncryptions.inMACType, + self.incomingCompressionType)) + + if self._keyExchangeState == self._KEY_EXCHANGE_REQUESTED: + self._keyExchangeState = self._KEY_EXCHANGE_PROGRESSING + else: + self.sendKexInit() + + return kexAlgs, keyAlgs, rest # For SSHServerTransport to use + + + def ssh_DISCONNECT(self, packet): + """ + Called when we receive a MSG_DISCONNECT message. Payload:: + long code + string description + + This means that the other side has disconnected. Pass the message up + and disconnect ourselves. + + @type packet: L{bytes} + @param packet: The message data. + """ + reasonCode = struct.unpack('>L', packet[: 4])[0] + description, foo = getNS(packet[4:]) + self.receiveError(reasonCode, description) + self.transport.loseConnection() + + + def ssh_IGNORE(self, packet): + """ + Called when we receive a MSG_IGNORE message. No payload. + This means nothing; we simply return. + + @type packet: L{bytes} + @param packet: The message data. + """ + + + def ssh_UNIMPLEMENTED(self, packet): + """ + Called when we receive a MSG_UNIMPLEMENTED message. Payload:: + long packet + + This means that the other side did not implement one of our packets. + + @type packet: L{bytes} + @param packet: The message data. + """ + seqnum, = struct.unpack('>L', packet) + self.receiveUnimplemented(seqnum) + + + def ssh_DEBUG(self, packet): + """ + Called when we receive a MSG_DEBUG message. Payload:: + bool alwaysDisplay + string message + string language + + This means the other side has passed along some debugging info. + + @type packet: L{bytes} + @param packet: The message data. + """ + alwaysDisplay = bool(ord(packet[0:1])) + message, lang, foo = getNS(packet[1:], 2) + self.receiveDebug(alwaysDisplay, message, lang) + + + def setService(self, service): + """ + Set our service to service and start it running. If we were + running a service previously, stop it first. + + @type service: C{SSHService} + @param service: The service to attach. + """ + log.msg('starting service %r' % (service.name,)) + if self.service: + self.service.serviceStopped() + self.service = service + service.transport = self + self.service.serviceStarted() + + + def sendDebug(self, message, alwaysDisplay=False, language=b''): + """ + Send a debug message to the other side. + + @param message: the message to send. + @type message: L{str} + @param alwaysDisplay: if True, tell the other side to always + display this message. + @type alwaysDisplay: L{bool} + @param language: optionally, the language the message is in. + @type language: L{str} + """ + self.sendPacket(MSG_DEBUG, chr(alwaysDisplay) + NS(message) + + NS(language)) + + + def sendIgnore(self, message): + """ + Send a message that will be ignored by the other side. This is + useful to fool attacks based on guessing packet sizes in the + encrypted stream. + + @param message: data to send with the message + @type message: L{str} + """ + self.sendPacket(MSG_IGNORE, NS(message)) + + + def sendUnimplemented(self): + """ + Send a message to the other side that the last packet was not + understood. + """ + seqnum = self.incomingPacketSequence + self.sendPacket(MSG_UNIMPLEMENTED, struct.pack('!L', seqnum)) + + + def sendDisconnect(self, reason, desc): + """ + Send a disconnect message to the other side and then disconnect. + + @param reason: the reason for the disconnect. Should be one of the + DISCONNECT_* values. + @type reason: L{int} + @param desc: a descrption of the reason for the disconnection. + @type desc: L{str} + """ + self.sendPacket( + MSG_DISCONNECT, struct.pack('>L', reason) + NS(desc) + NS(b'')) + log.msg('Disconnecting with error, code %s\nreason: %s' % (reason, + desc)) + self.transport.loseConnection() + + + def _startEphemeralDH(self): + """ + Prepares for a Diffie-Hellman key agreement exchange. + + Creates an ephemeral keypair in the group defined by (self.g, + self.p) and stores it. + """ + + numbers = dh.DHParameterNumbers(self.p, self.g) + parameters = numbers.parameters(default_backend()) + self.dhSecretKey = parameters.generate_private_key() + y = self.dhSecretKey.public_key().public_numbers().y + self.dhSecretKeyPublicMP = MP(y) + + + def _finishEphemeralDH(self, remoteDHpublicKey): + """ + Completes the Diffie-Hellman key agreement started by + _startEphemeralDH, and forgets the ephemeral secret key. + + @type remoteDHpublicKey: L{int} + @rtype: L{bytes} + @return: The new shared secret, in SSH C{mpint} format. + + """ + + remoteKey = dh.DHPublicNumbers( + remoteDHpublicKey, + dh.DHParameterNumbers(self.p, self.g) + ).public_key(default_backend()) + secret = self.dhSecretKey.exchange(remoteKey) + del self.dhSecretKey + + # The result of a Diffie-Hellman exchange is an integer, but + # the Cryptography module returns it as bytes in a form that + # is only vaguely documented. We fix it up to match the SSH + # MP-integer format as described in RFC4251. + secret = secret.lstrip(b'\x00') + ch = ord(secret[0:1]) + if ch & 0x80: # High bit set? + # Make room for the sign bit + prefix = struct.pack('>L', len(secret) + 1) + b'\x00' + else: + prefix = struct.pack('>L', len(secret)) + return prefix + secret + + + def _getKey(self, c, sharedSecret, exchangeHash): + """ + Get one of the keys for authentication/encryption. + + @type c: L{bytes} + @param c: The letter identifying which key this is. + + @type sharedSecret: L{bytes} + @param sharedSecret: The shared secret K. + + @type exchangeHash: L{bytes} + @param exchangeHash: The hash H from key exchange. + + @rtype: L{bytes} + @return: The derived key. + """ + hashProcessor = _kex.getHashProcessor(self.kexAlg) + k1 = hashProcessor(sharedSecret + exchangeHash + c + self.sessionID) + k1 = k1.digest() + k2 = hashProcessor(sharedSecret + exchangeHash + k1).digest() + k3 = hashProcessor(sharedSecret + exchangeHash + k1 + k2).digest() + k4 = hashProcessor(sharedSecret + exchangeHash + k1 + k2 + k3).digest() + return k1 + k2 + k3 + k4 + + + def _keySetup(self, sharedSecret, exchangeHash): + """ + Set up the keys for the connection and sends MSG_NEWKEYS when + finished, + + @param sharedSecret: a secret string agreed upon using a Diffie- + Hellman exchange, so it is only shared between + the server and the client. + @type sharedSecret: L{str} + @param exchangeHash: A hash of various data known by both sides. + @type exchangeHash: L{str} + """ + if not self.sessionID: + self.sessionID = exchangeHash + initIVCS = self._getKey(b'A', sharedSecret, exchangeHash) + initIVSC = self._getKey(b'B', sharedSecret, exchangeHash) + encKeyCS = self._getKey(b'C', sharedSecret, exchangeHash) + encKeySC = self._getKey(b'D', sharedSecret, exchangeHash) + integKeyCS = self._getKey(b'E', sharedSecret, exchangeHash) + integKeySC = self._getKey(b'F', sharedSecret, exchangeHash) + outs = [initIVSC, encKeySC, integKeySC] + ins = [initIVCS, encKeyCS, integKeyCS] + if self.isClient: # Reverse for the client + log.msg('REVERSE') + outs, ins = ins, outs + self.nextEncryptions.setKeys(outs[0], outs[1], ins[0], ins[1], + outs[2], ins[2]) + self.sendPacket(MSG_NEWKEYS, b'') + + + def _newKeys(self): + """ + Called back by a subclass once a I{MSG_NEWKEYS} message has been + received. This indicates key exchange has completed and new encryption + and compression parameters should be adopted. Any messages which were + queued during key exchange will also be flushed. + """ + log.msg('NEW KEYS') + self.currentEncryptions = self.nextEncryptions + if self.outgoingCompressionType == b'zlib': + self.outgoingCompression = zlib.compressobj(6) + if self.incomingCompressionType == b'zlib': + self.incomingCompression = zlib.decompressobj() + + self._keyExchangeState = self._KEY_EXCHANGE_NONE + messages = self._blockedByKeyExchange + self._blockedByKeyExchange = None + for (messageType, payload) in messages: + self.sendPacket(messageType, payload) + + + def isEncrypted(self, direction="out"): + """ + Check if the connection is encrypted in the given direction. + + @type direction: L{str} + @param direction: The direction: one of 'out', 'in', or 'both'. + + @rtype: L{bool} + @return: C{True} if it is encrypted. + """ + if direction == "out": + return self.currentEncryptions.outCipType != b'none' + elif direction == "in": + return self.currentEncryptions.inCipType != b'none' + elif direction == "both": + return self.isEncrypted("in") and self.isEncrypted("out") + else: + raise TypeError('direction must be "out", "in", or "both"') + + + def isVerified(self, direction="out"): + """ + Check if the connection is verified/authentication in the given direction. + + @type direction: L{str} + @param direction: The direction: one of 'out', 'in', or 'both'. + + @rtype: L{bool} + @return: C{True} if it is verified. + """ + if direction == "out": + return self.currentEncryptions.outMACType != b'none' + elif direction == "in": + return self.currentEncryptions.inMACType != b'none' + elif direction == "both": + return self.isVerified("in") and self.isVerified("out") + else: + raise TypeError('direction must be "out", "in", or "both"') + + + def loseConnection(self): + """ + Lose the connection to the other side, sending a + DISCONNECT_CONNECTION_LOST message. + """ + self.sendDisconnect(DISCONNECT_CONNECTION_LOST, + b"user closed connection") + + # Client methods + + + def receiveError(self, reasonCode, description): + """ + Called when we receive a disconnect error message from the other + side. + + @param reasonCode: the reason for the disconnect, one of the + DISCONNECT_ values. + @type reasonCode: L{int} + @param description: a human-readable description of the + disconnection. + @type description: L{str} + """ + log.msg('Got remote error, code %s\nreason: %s' % (reasonCode, + description)) + + + def receiveUnimplemented(self, seqnum): + """ + Called when we receive an unimplemented packet message from the other + side. + + @param seqnum: the sequence number that was not understood. + @type seqnum: L{int} + """ + log.msg('other side unimplemented packet #%s' % (seqnum,)) + + + def receiveDebug(self, alwaysDisplay, message, lang): + """ + Called when we receive a debug message from the other side. + + @param alwaysDisplay: if True, this message should always be + displayed. + @type alwaysDisplay: L{bool} + @param message: the debug message + @type message: L{str} + @param lang: optionally the language the message is in. + @type lang: L{str} + """ + if alwaysDisplay: + log.msg('Remote Debug Message: %s' % (message,)) + + + def _generateECPrivateKey(self): + """ + Generate an private key for ECDH key exchange. + + @rtype: The appropriate private key type matching C{self.kexAlg}: + L{EllipticCurvePrivateKey} for C{ecdh-sha2-nistp*}, or + L{X25519PrivateKey} for C{curve25519-sha256}. + @return: The generated private key. + """ + if self.kexAlg.startswith(b'ecdh-sha2-nistp'): + try: + curve = keys._curveTable[b'ecdsa' + self.kexAlg[4:]] + except KeyError: + raise UnsupportedAlgorithm('unused-key') + + return ec.generate_private_key(curve, default_backend()) + elif self.kexAlg in ( + b'curve25519-sha256', b'curve25519-sha256@libssh.org'): + return x25519.X25519PrivateKey.generate() + else: + raise UnsupportedAlgorithm( + 'Cannot generate elliptic curve private key for %r' % + (self.kexAlg,)) + + + def _encodeECPublicKey(self, ecPub): + """ + Encode an elliptic curve public key to bytes. + + @type ecPub: The appropriate public key type matching + C{self.kexAlg}: L{EllipticCurvePublicKey} for + C{ecdh-sha2-nistp*}, or L{X25519PublicKey} for + C{curve25519-sha256}. + @param ecPub: The public key to encode. + + @rtype: L{bytes} + @return: The encoded public key. + """ + if self.kexAlg.startswith(b'ecdh-sha2-nistp'): + return ecPub.public_bytes( + serialization.Encoding.X962, + serialization.PublicFormat.UncompressedPoint + ) + elif self.kexAlg in ( + b'curve25519-sha256', b'curve25519-sha256@libssh.org'): + return ecPub.public_bytes( + serialization.Encoding.Raw, + serialization.PublicFormat.Raw + ) + else: + raise UnsupportedAlgorithm( + 'Cannot encode elliptic curve public key for %r' % + (self.kexAlg,)) + + + def _generateECSharedSecret(self, ecPriv, theirECPubBytes): + """ + Generate a shared secret for ECDH key exchange. + + @type ecPriv: The appropriate private key type matching + C{self.kexAlg}: L{EllipticCurvePrivateKey} for + C{ecdh-sha2-nistp*}, or L{X25519PrivateKey} for + C{curve25519-sha256}. + @param ecPriv: Our private key. + + @rtype: L{bytes} + @return: The generated shared secret, as an SSH multiple-precision + integer. + """ + if self.kexAlg.startswith(b'ecdh-sha2-nistp'): + try: + curve = keys._curveTable[b'ecdsa' + self.kexAlg[4:]] + except KeyError: + raise UnsupportedAlgorithm('unused-key') + + theirECPub = ec.EllipticCurvePublicKey.from_encoded_point( + curve, theirECPubBytes) + sharedSecret = ecPriv.exchange(ec.ECDH(), theirECPub) + elif self.kexAlg in ( + b'curve25519-sha256', b'curve25519-sha256@libssh.org'): + theirECPub = x25519.X25519PublicKey.from_public_bytes( + theirECPubBytes) + sharedSecret = ecPriv.exchange(theirECPub) + else: + raise UnsupportedAlgorithm( + 'Cannot generate elliptic curve shared secret for %r' % + (self.kexAlg,)) + + return _mpFromBytes(sharedSecret) + + + +class SSHServerTransport(SSHTransportBase): + """ + SSHServerTransport implements the server side of the SSH protocol. + + @ivar isClient: since we are never the client, this is always False. + + @ivar ignoreNextPacket: if True, ignore the next key exchange packet. This + is set when the client sends a guessed key exchange packet but with + an incorrect guess. + + @ivar dhGexRequest: the KEX_DH_GEX_REQUEST(_OLD) that the client sent. + The key generation needs this to be stored. + + @ivar g: the Diffie-Hellman group generator. + + @ivar p: the Diffie-Hellman group prime. + """ + isClient = False + ignoreNextPacket = 0 + + + def ssh_KEXINIT(self, packet): + """ + Called when we receive a MSG_KEXINIT message. For a description + of the packet, see SSHTransportBase.ssh_KEXINIT(). Additionally, + this method checks if a guessed key exchange packet was sent. If + it was sent, and it guessed incorrectly, the next key exchange + packet MUST be ignored. + """ + retval = SSHTransportBase.ssh_KEXINIT(self, packet) + if not retval: # Disconnected + return + else: + kexAlgs, keyAlgs, rest = retval + if ord(rest[0:1]): # Flag first_kex_packet_follows? + if (kexAlgs[0] != self.supportedKeyExchanges[0] or + keyAlgs[0] != self.supportedPublicKeys[0]): + self.ignoreNextPacket = True # Guess was wrong + + + def _ssh_KEX_ECDH_INIT(self, packet): + """ + Called from L{ssh_KEX_DH_GEX_REQUEST_OLD} to handle + elliptic curve key exchanges. + + Payload:: + + string client Elliptic Curve Diffie-Hellman public key + + Just like L{_ssh_KEXDH_INIT} this message type is also not dispatched + directly. Extra check to determine if this is really KEX_ECDH_INIT + is required. + + First we load the host's public/private keys. + Then we generate the ECDH public/private keypair for the given curve. + With that we generate the shared secret key. + Then we compute the hash to sign and send back to the client + Along with the server's public key and the ECDH public key. + + @type packet: L{bytes} + @param packet: The message data. + + @return: None. + """ + # Get the raw client public key. + pktPub, packet = getNS(packet) + + # Get the host's public and private keys + pubHostKey = self.factory.publicKeys[self.keyAlg] + privHostKey = self.factory.privateKeys[self.keyAlg] + + # Generate the private key + ecPriv = self._generateECPrivateKey() + + # Get the public key + self.ecPub = ecPriv.public_key() + encPub = self._encodeECPublicKey(self.ecPub) + + # Generate the shared secret + sharedSecret = self._generateECSharedSecret(ecPriv, pktPub) + + # Finish update and digest + h = _kex.getHashProcessor(self.kexAlg)() + h.update(NS(self.otherVersionString)) + h.update(NS(self.ourVersionString)) + h.update(NS(self.otherKexInitPayload)) + h.update(NS(self.ourKexInitPayload)) + h.update(NS(pubHostKey.blob())) + h.update(NS(pktPub)) + h.update(NS(encPub)) + h.update(sharedSecret) + exchangeHash = h.digest() + + self.sendPacket( + MSG_KEXDH_REPLY, + NS(pubHostKey.blob()) + NS(encPub) + + NS(privHostKey.sign(exchangeHash))) + self._keySetup(sharedSecret, exchangeHash) + + + def _ssh_KEXDH_INIT(self, packet): + """ + Called to handle the beginning of a non-group key exchange. + + Unlike other message types, this is not dispatched automatically. It + is called from C{ssh_KEX_DH_GEX_REQUEST_OLD} because an extra check is + required to determine if this is really a KEXDH_INIT message or if it + is a KEX_DH_GEX_REQUEST_OLD message. + + The KEXDH_INIT payload:: + + integer e (the client's Diffie-Hellman public key) + + We send the KEXDH_REPLY with our host key and signature. + + @type packet: L{bytes} + @param packet: The message data. + """ + clientDHpublicKey, foo = getMP(packet) + self.g, self.p = _kex.getDHGeneratorAndPrime(self.kexAlg) + self._startEphemeralDH() + sharedSecret = self._finishEphemeralDH(clientDHpublicKey) + h = sha1() + h.update(NS(self.otherVersionString)) + h.update(NS(self.ourVersionString)) + h.update(NS(self.otherKexInitPayload)) + h.update(NS(self.ourKexInitPayload)) + h.update(NS(self.factory.publicKeys[self.keyAlg].blob())) + h.update(MP(clientDHpublicKey)) + h.update(self.dhSecretKeyPublicMP) + h.update(sharedSecret) + exchangeHash = h.digest() + self.sendPacket( + MSG_KEXDH_REPLY, + NS(self.factory.publicKeys[self.keyAlg].blob()) + + self.dhSecretKeyPublicMP + + NS(self.factory.privateKeys[self.keyAlg].sign(exchangeHash))) + self._keySetup(sharedSecret, exchangeHash) + + + def ssh_KEX_DH_GEX_REQUEST_OLD(self, packet): + """ + This represents different key exchange methods that share the same + integer value. If the message is determined to be a KEXDH_INIT, + L{_ssh_KEXDH_INIT} is called to handle it. If it is a KEX_ECDH_INIT, + L{_ssh_KEX_ECDH_INIT} is called. + Otherwise, for KEX_DH_GEX_REQUEST_OLD payload:: + + integer ideal (ideal size for the Diffie-Hellman prime) + + We send the KEX_DH_GEX_GROUP message with the group that is + closest in size to ideal. + + If we were told to ignore the next key exchange packet by ssh_KEXINIT, + drop it on the floor and return. + + @type packet: L{bytes} + @param packet: The message data. + """ + if self.ignoreNextPacket: + self.ignoreNextPacket = 0 + return + + # KEXDH_INIT, KEX_ECDH_INIT, and KEX_DH_GEX_REQUEST_OLD + # have the same value, so use another cue + # to decide what kind of message the peer sent us. + if _kex.isFixedGroup(self.kexAlg): + return self._ssh_KEXDH_INIT(packet) + elif _kex.isEllipticCurve(self.kexAlg): + return self._ssh_KEX_ECDH_INIT(packet) + else: + self.dhGexRequest = packet + ideal = struct.unpack('>L', packet)[0] + self.g, self.p = self.factory.getDHPrime(ideal) + self._startEphemeralDH() + self.sendPacket(MSG_KEX_DH_GEX_GROUP, MP(self.p) + MP(self.g)) + + + def ssh_KEX_DH_GEX_REQUEST(self, packet): + """ + Called when we receive a MSG_KEX_DH_GEX_REQUEST message. Payload:: + integer minimum + integer ideal + integer maximum + + The client is asking for a Diffie-Hellman group between minimum and + maximum size, and close to ideal if possible. We reply with a + MSG_KEX_DH_GEX_GROUP message. + + If we were told to ignore the next key exchange packet by ssh_KEXINIT, + drop it on the floor and return. + + @type packet: L{bytes} + @param packet: The message data. + """ + if self.ignoreNextPacket: + self.ignoreNextPacket = 0 + return + self.dhGexRequest = packet + min, ideal, max = struct.unpack('>3L', packet) + self.g, self.p = self.factory.getDHPrime(ideal) + self._startEphemeralDH() + self.sendPacket(MSG_KEX_DH_GEX_GROUP, MP(self.p) + MP(self.g)) + + + def ssh_KEX_DH_GEX_INIT(self, packet): + """ + Called when we get a MSG_KEX_DH_GEX_INIT message. Payload:: + integer e (client DH public key) + + We send the MSG_KEX_DH_GEX_REPLY message with our host key and + signature. + + @type packet: L{bytes} + @param packet: The message data. + """ + clientDHpublicKey, foo = getMP(packet) + # TODO: we should also look at the value they send to us and reject + # insecure values of f (if g==2 and f has a single '1' bit while the + # rest are '0's, then they must have used a small y also). + + # TODO: This could be computed when self.p is set up + # or do as openssh does and scan f for a single '1' bit instead + + sharedSecret = self._finishEphemeralDH(clientDHpublicKey) + h = _kex.getHashProcessor(self.kexAlg)() + h.update(NS(self.otherVersionString)) + h.update(NS(self.ourVersionString)) + h.update(NS(self.otherKexInitPayload)) + h.update(NS(self.ourKexInitPayload)) + h.update(NS(self.factory.publicKeys[self.keyAlg].blob())) + h.update(self.dhGexRequest) + h.update(MP(self.p)) + h.update(MP(self.g)) + h.update(MP(clientDHpublicKey)) + h.update(self.dhSecretKeyPublicMP) + h.update(sharedSecret) + exchangeHash = h.digest() + self.sendPacket( + MSG_KEX_DH_GEX_REPLY, + NS(self.factory.publicKeys[self.keyAlg].blob()) + + self.dhSecretKeyPublicMP + + NS(self.factory.privateKeys[self.keyAlg].sign(exchangeHash))) + self._keySetup(sharedSecret, exchangeHash) + + + def ssh_NEWKEYS(self, packet): + """ + Called when we get a MSG_NEWKEYS message. No payload. + When we get this, the keys have been set on both sides, and we + start using them to encrypt and authenticate the connection. + + @type packet: L{bytes} + @param packet: The message data. + """ + if packet != b'': + self.sendDisconnect(DISCONNECT_PROTOCOL_ERROR, + b"NEWKEYS takes no data") + return + self._newKeys() + + + def ssh_SERVICE_REQUEST(self, packet): + """ + Called when we get a MSG_SERVICE_REQUEST message. Payload:: + string serviceName + + The client has requested a service. If we can start the service, + start it; otherwise, disconnect with + DISCONNECT_SERVICE_NOT_AVAILABLE. + + @type packet: L{bytes} + @param packet: The message data. + """ + service, rest = getNS(packet) + cls = self.factory.getService(self, service) + if not cls: + self.sendDisconnect(DISCONNECT_SERVICE_NOT_AVAILABLE, + b"don't have service " + service) + return + else: + self.sendPacket(MSG_SERVICE_ACCEPT, NS(service)) + self.setService(cls()) + + + +class SSHClientTransport(SSHTransportBase): + """ + SSHClientTransport implements the client side of the SSH protocol. + + @ivar isClient: since we are always the client, this is always True. + + @ivar _gotNewKeys: if we receive a MSG_NEWKEYS message before we are + ready to transition to the new keys, this is set to True so we + can transition when the keys are ready locally. + + @ivar x: our Diffie-Hellman private key. + + @ivar e: our Diffie-Hellman public key. + + @ivar g: the Diffie-Hellman group generator. + + @ivar p: the Diffie-Hellman group prime + + @ivar instance: the SSHService object we are requesting. + + @ivar _dhMinimalGroupSize: Minimal acceptable group size advertised by the + client in MSG_KEX_DH_GEX_REQUEST. + @type _dhMinimalGroupSize: int + + @ivar _dhMaximalGroupSize: Maximal acceptable group size advertised by the + client in MSG_KEX_DH_GEX_REQUEST. + @type _dhMaximalGroupSize: int + + @ivar _dhPreferredGroupSize: Preferred group size advertised by the client + in MSG_KEX_DH_GEX_REQUEST. + @type _dhPreferredGroupSize: int + """ + isClient = True + + # Recommended minimal and maximal values from RFC 4419, 3. + _dhMinimalGroupSize = 1024 + _dhMaximalGroupSize = 8192 + # FIXME: https://twistedmatrix.com/trac/ticket/8103 + # This may need to be more dynamic; compare kexgex_client in + # OpenSSH. + _dhPreferredGroupSize = 2048 + + def connectionMade(self): + """ + Called when the connection is started with the server. Just sets + up a private instance variable. + """ + SSHTransportBase.connectionMade(self) + self._gotNewKeys = 0 + + + def ssh_KEXINIT(self, packet): + """ + Called when we receive a MSG_KEXINIT message. For a description + of the packet, see SSHTransportBase.ssh_KEXINIT(). Additionally, + this method sends the first key exchange packet. + + If the agreed-upon exchange is ECDH, generate a key pair for the + corresponding curve and send the public key. + + If the agreed-upon exchange has a fixed prime/generator group, + generate a public key and send it in a MSG_KEXDH_INIT message. + Otherwise, ask for a 2048 bit group with a MSG_KEX_DH_GEX_REQUEST + message. + """ + if SSHTransportBase.ssh_KEXINIT(self, packet) is None: + # Connection was disconnected while doing base processing. + # Maybe no common protocols were agreed. + return + # Are we using ECDH? + if _kex.isEllipticCurve(self.kexAlg): + # Generate the keys + self.ecPriv = self._generateECPrivateKey() + self.ecPub = self.ecPriv.public_key() + + # DH_GEX_REQUEST_OLD is the same number we need. + self.sendPacket( + MSG_KEX_DH_GEX_REQUEST_OLD, + NS(self._encodeECPublicKey(self.ecPub)) + ) + elif _kex.isFixedGroup(self.kexAlg): + # We agreed on a fixed group key exchange algorithm. + self.g, self.p = _kex.getDHGeneratorAndPrime(self.kexAlg) + self._startEphemeralDH() + self.sendPacket(MSG_KEXDH_INIT, self.dhSecretKeyPublicMP) + else: + # We agreed on a dynamic group. Tell the server what range of + # group sizes we accept, and what size we prefer; the server + # will then select a group. + self.sendPacket( + MSG_KEX_DH_GEX_REQUEST, + struct.pack( + '!LLL', + self._dhMinimalGroupSize, + self._dhPreferredGroupSize, + self._dhMaximalGroupSize, + )) + + + def _ssh_KEX_ECDH_REPLY(self, packet): + """ + Called to handle a reply to a ECDH exchange message(KEX_ECDH_INIT). + + Like the handler for I{KEXDH_INIT}, this message type has an + overlapping value. This method is called from C{ssh_KEX_DH_GEX_GROUP} + if that method detects a non-group key exchange is in progress. + + Payload:: + + string serverHostKey + string server Elliptic Curve Diffie-Hellman public key + string signature + + We verify the host key and continue if it passes verificiation. + Otherwise raise an exception and return. + + @type packet: L{bytes} + @param packet: The message data. + + @return: A deferred firing when key exchange is complete. + """ + def _continue_KEX_ECDH_REPLY(ignored, hostKey, pubKey, signature): + # Save off the host public key. + theirECHost = hostKey + + sharedSecret = self._generateECSharedSecret(self.ecPriv, pubKey) + + h = _kex.getHashProcessor(self.kexAlg)() + h.update(NS(self.ourVersionString)) + h.update(NS(self.otherVersionString)) + h.update(NS(self.ourKexInitPayload)) + h.update(NS(self.otherKexInitPayload)) + h.update(NS(theirECHost)) + h.update(NS(self._encodeECPublicKey(self.ecPub))) + h.update(NS(pubKey)) + h.update(sharedSecret) + + exchangeHash = h.digest() + + if not keys.Key.fromString(theirECHost).verify( + signature, exchangeHash): + self.sendDisconnect(DISCONNECT_KEY_EXCHANGE_FAILED, + b'bad signature') + else: + self._keySetup(sharedSecret, exchangeHash) + + # Get the host public key, + # the raw ECDH public key bytes and the signature + hostKey, pubKey, signature, packet = getNS(packet, 3) + + # Easier to comment this out for now than to update all of the tests. + #fingerprint = nativeString(base64.b64encode( + # sha256(hostKey).digest())) + + fingerprint = b':'.join( + [binascii.hexlify(ch) for ch in iterbytes(md5(hostKey).digest())]) + d = self.verifyHostKey(hostKey, fingerprint) + d.addCallback(_continue_KEX_ECDH_REPLY, hostKey, pubKey, signature) + d.addErrback( + lambda unused: self.sendDisconnect( + DISCONNECT_HOST_KEY_NOT_VERIFIABLE, b'bad host key')) + return d + + + def _ssh_KEXDH_REPLY(self, packet): + """ + Called to handle a reply to a non-group key exchange message + (KEXDH_INIT). + + Like the handler for I{KEXDH_INIT}, this message type has an + overlapping value. This method is called from C{ssh_KEX_DH_GEX_GROUP} + if that method detects a non-group key exchange is in progress. + + Payload:: + + string serverHostKey + integer f (server Diffie-Hellman public key) + string signature + + We verify the host key by calling verifyHostKey, then continue in + _continueKEXDH_REPLY. + + @type packet: L{bytes} + @param packet: The message data. + + @return: A deferred firing when key exchange is complete. + """ + pubKey, packet = getNS(packet) + f, packet = getMP(packet) + signature, packet = getNS(packet) + fingerprint = b':'.join([binascii.hexlify(ch) for ch in + iterbytes(md5(pubKey).digest())]) + d = self.verifyHostKey(pubKey, fingerprint) + d.addCallback(self._continueKEXDH_REPLY, pubKey, f, signature) + d.addErrback( + lambda unused: self.sendDisconnect( + DISCONNECT_HOST_KEY_NOT_VERIFIABLE, b'bad host key')) + return d + + + def ssh_KEX_DH_GEX_GROUP(self, packet): + """ + This handles different messages which share an integer value. + + If the key exchange does not have a fixed prime/generator group, + we generate a Diffie-Hellman public key and send it in a + MSG_KEX_DH_GEX_INIT message. + + Payload:: + string g (group generator) + string p (group prime) + + @type packet: L{bytes} + @param packet: The message data. + """ + if _kex.isFixedGroup(self.kexAlg): + return self._ssh_KEXDH_REPLY(packet) + elif _kex.isEllipticCurve(self.kexAlg): + return self._ssh_KEX_ECDH_REPLY(packet) + else: + self.p, rest = getMP(packet) + self.g, rest = getMP(rest) + self._startEphemeralDH() + self.sendPacket(MSG_KEX_DH_GEX_INIT, self.dhSecretKeyPublicMP) + + + def _continueKEXDH_REPLY(self, ignored, pubKey, f, signature): + """ + The host key has been verified, so we generate the keys. + + @param ignored: Ignored. + + @param pubKey: the public key blob for the server's public key. + @type pubKey: L{str} + @param f: the server's Diffie-Hellman public key. + @type f: L{long} + @param signature: the server's signature, verifying that it has the + correct private key. + @type signature: L{str} + """ + serverKey = keys.Key.fromString(pubKey) + sharedSecret = self._finishEphemeralDH(f) + h = sha1() + h.update(NS(self.ourVersionString)) + h.update(NS(self.otherVersionString)) + h.update(NS(self.ourKexInitPayload)) + h.update(NS(self.otherKexInitPayload)) + h.update(NS(pubKey)) + h.update(self.dhSecretKeyPublicMP) + h.update(MP(f)) + h.update(sharedSecret) + exchangeHash = h.digest() + if not serverKey.verify(signature, exchangeHash): + self.sendDisconnect(DISCONNECT_KEY_EXCHANGE_FAILED, + b'bad signature') + return + self._keySetup(sharedSecret, exchangeHash) + + + def ssh_KEX_DH_GEX_REPLY(self, packet): + """ + Called when we receive a MSG_KEX_DH_GEX_REPLY message. Payload:: + string server host key + integer f (server DH public key) + + We verify the host key by calling verifyHostKey, then continue in + _continueGEX_REPLY. + + @type packet: L{bytes} + @param packet: The message data. + + @return: A deferred firing once key exchange is complete. + """ + pubKey, packet = getNS(packet) + f, packet = getMP(packet) + signature, packet = getNS(packet) + fingerprint = b':'.join( + [binascii.hexlify(c) for c in iterbytes(md5(pubKey).digest())]) + d = self.verifyHostKey(pubKey, fingerprint) + d.addCallback(self._continueGEX_REPLY, pubKey, f, signature) + d.addErrback( + lambda unused: self.sendDisconnect( + DISCONNECT_HOST_KEY_NOT_VERIFIABLE, b'bad host key')) + return d + + + def _continueGEX_REPLY(self, ignored, pubKey, f, signature): + """ + The host key has been verified, so we generate the keys. + + @param ignored: Ignored. + + @param pubKey: the public key blob for the server's public key. + @type pubKey: L{str} + @param f: the server's Diffie-Hellman public key. + @type f: L{long} + @param signature: the server's signature, verifying that it has the + correct private key. + @type signature: L{str} + """ + serverKey = keys.Key.fromString(pubKey) + sharedSecret = self._finishEphemeralDH(f) + h = _kex.getHashProcessor(self.kexAlg)() + h.update(NS(self.ourVersionString)) + h.update(NS(self.otherVersionString)) + h.update(NS(self.ourKexInitPayload)) + h.update(NS(self.otherKexInitPayload)) + h.update(NS(pubKey)) + h.update(struct.pack( + '!LLL', + self._dhMinimalGroupSize, + self._dhPreferredGroupSize, + self._dhMaximalGroupSize, + )) + h.update(MP(self.p)) + h.update(MP(self.g)) + h.update(self.dhSecretKeyPublicMP) + h.update(MP(f)) + h.update(sharedSecret) + exchangeHash = h.digest() + if not serverKey.verify(signature, exchangeHash): + self.sendDisconnect(DISCONNECT_KEY_EXCHANGE_FAILED, + b'bad signature') + return + self._keySetup(sharedSecret, exchangeHash) + + + def _keySetup(self, sharedSecret, exchangeHash): + """ + See SSHTransportBase._keySetup(). + """ + SSHTransportBase._keySetup(self, sharedSecret, exchangeHash) + if self._gotNewKeys: + self.ssh_NEWKEYS(b'') + + + def ssh_NEWKEYS(self, packet): + """ + Called when we receive a MSG_NEWKEYS message. No payload. + If we've finished setting up our own keys, start using them. + Otherwise, remember that we've received this message. + + @type packet: L{bytes} + @param packet: The message data. + """ + if packet != b'': + self.sendDisconnect(DISCONNECT_PROTOCOL_ERROR, + b"NEWKEYS takes no data") + return + if not self.nextEncryptions.encBlockSize: + self._gotNewKeys = 1 + return + self._newKeys() + self.connectionSecure() + + + def ssh_SERVICE_ACCEPT(self, packet): + """ + Called when we receive a MSG_SERVICE_ACCEPT message. Payload:: + string service name + + Start the service we requested. + + @type packet: L{bytes} + @param packet: The message data. + """ + if packet == b'': + log.msg('got SERVICE_ACCEPT without payload') + else: + name = getNS(packet)[0] + if name != self.instance.name: + self.sendDisconnect( + DISCONNECT_PROTOCOL_ERROR, + b"received accept for service we did not request") + self.setService(self.instance) + + + def requestService(self, instance): + """ + Request that a service be run over this transport. + + @type instance: subclass of L{twisted.conch.ssh.service.SSHService} + @param instance: The service to run. + """ + self.sendPacket(MSG_SERVICE_REQUEST, NS(instance.name)) + self.instance = instance + + # Client methods + + + def verifyHostKey(self, hostKey, fingerprint): + """ + Returns a Deferred that gets a callback if it is a valid key, or + an errback if not. + + @type hostKey: L{bytes} + @param hostKey: The host key to verify. + + @type fingerprint: L{bytes} + @param fingerprint: The fingerprint of the key. + + @return: A deferred firing with C{True} if the key is valid. + """ + return defer.fail(NotImplementedError()) + + + def connectionSecure(self): + """ + Called when the encryption has been set up. Generally, + requestService() is called to run another service over the transport. + """ + raise NotImplementedError() + + + +class _NullEncryptionContext(object): + """ + An encryption context that does not actually encrypt anything. + """ + def update(self, data): + """ + 'Encrypt' new data by doing nothing. + + @type data: L{bytes} + @param data: The data to 'encrypt'. + + @rtype: L{bytes} + @return: The 'encrypted' data. + """ + return data + + + +class _DummyAlgorithm(object): + """ + An encryption algorithm that does not actually encrypt anything. + """ + block_size = 64 + + + +class _DummyCipher(object): + """ + A cipher for the none encryption method. + + @ivar block_size: the block size of the encryption. In the case of the + none cipher, this is 8 bytes. + """ + algorithm = _DummyAlgorithm() + + + def encryptor(self): + """ + Construct a noop encryptor. + + @return: The encryptor. + """ + return _NullEncryptionContext() + + + def decryptor(self): + """ + Construct a noop decryptor. + + @return: The decryptor. + """ + return _NullEncryptionContext() + + + +DH_GENERATOR, DH_PRIME = _kex.getDHGeneratorAndPrime( + b'diffie-hellman-group14-sha1') + + +MSG_DISCONNECT = 1 +MSG_IGNORE = 2 +MSG_UNIMPLEMENTED = 3 +MSG_DEBUG = 4 +MSG_SERVICE_REQUEST = 5 +MSG_SERVICE_ACCEPT = 6 +MSG_KEXINIT = 20 +MSG_NEWKEYS = 21 +MSG_KEXDH_INIT = 30 +MSG_KEXDH_REPLY = 31 +MSG_KEX_DH_GEX_REQUEST_OLD = 30 +MSG_KEX_DH_GEX_REQUEST = 34 +MSG_KEX_DH_GEX_GROUP = 31 +MSG_KEX_DH_GEX_INIT = 32 +MSG_KEX_DH_GEX_REPLY = 33 + + + +DISCONNECT_HOST_NOT_ALLOWED_TO_CONNECT = 1 +DISCONNECT_PROTOCOL_ERROR = 2 +DISCONNECT_KEY_EXCHANGE_FAILED = 3 +DISCONNECT_RESERVED = 4 +DISCONNECT_MAC_ERROR = 5 +DISCONNECT_COMPRESSION_ERROR = 6 +DISCONNECT_SERVICE_NOT_AVAILABLE = 7 +DISCONNECT_PROTOCOL_VERSION_NOT_SUPPORTED = 8 +DISCONNECT_HOST_KEY_NOT_VERIFIABLE = 9 +DISCONNECT_CONNECTION_LOST = 10 +DISCONNECT_BY_APPLICATION = 11 +DISCONNECT_TOO_MANY_CONNECTIONS = 12 +DISCONNECT_AUTH_CANCELLED_BY_USER = 13 +DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE = 14 +DISCONNECT_ILLEGAL_USER_NAME = 15 + + + +messages = {} +for name, value in list(globals().items()): + # Avoid legacy messages which overlap with never ones + if name.startswith('MSG_') and not name.startswith('MSG_KEXDH_'): + messages[value] = name +# Check for regressions (#5352) +if 'MSG_KEXDH_INIT' in messages or 'MSG_KEXDH_REPLY' in messages: + raise RuntimeError( + "legacy SSH mnemonics should not end up in messages dict") diff --git a/contrib/python/Twisted/py2/twisted/conch/ssh/userauth.py b/contrib/python/Twisted/py2/twisted/conch/ssh/userauth.py new file mode 100644 index 0000000000..8fab81603a --- /dev/null +++ b/contrib/python/Twisted/py2/twisted/conch/ssh/userauth.py @@ -0,0 +1,770 @@ +# -*- test-case-name: twisted.conch.test.test_userauth -*- +# Copyright (c) Twisted Matrix Laboratories. +# See LICENSE for details. + +""" +Implementation of the ssh-userauth service. +Currently implemented authentication types are public-key and password. + +Maintainer: Paul Swartz +""" + +from __future__ import absolute_import, division + +import struct + +from twisted.conch import error, interfaces +from twisted.conch.ssh import keys, transport, service +from twisted.conch.ssh.common import NS, getNS +from twisted.cred import credentials +from twisted.cred.error import UnauthorizedLogin +from twisted.internet import defer, reactor +from twisted.python import failure, log +from twisted.python.compat import nativeString, _bytesChr as chr + + + +class SSHUserAuthServer(service.SSHService): + """ + A service implementing the server side of the 'ssh-userauth' service. It + is used to authenticate the user on the other side as being able to access + this server. + + @ivar name: the name of this service: 'ssh-userauth' + @type name: L{bytes} + @ivar authenticatedWith: a list of authentication methods that have + already been used. + @type authenticatedWith: L{list} + @ivar loginTimeout: the number of seconds we wait before disconnecting + the user for taking too long to authenticate + @type loginTimeout: L{int} + @ivar attemptsBeforeDisconnect: the number of failed login attempts we + allow before disconnecting. + @type attemptsBeforeDisconnect: L{int} + @ivar loginAttempts: the number of login attempts that have been made + @type loginAttempts: L{int} + @ivar passwordDelay: the number of seconds to delay when the user gives + an incorrect password + @type passwordDelay: L{int} + @ivar interfaceToMethod: a L{dict} mapping credential interfaces to + authentication methods. The server checks to see which of the + cred interfaces have checkers and tells the client that those methods + are valid for authentication. + @type interfaceToMethod: L{dict} + @ivar supportedAuthentications: A list of the supported authentication + methods. + @type supportedAuthentications: L{list} of L{bytes} + @ivar user: the last username the client tried to authenticate with + @type user: L{bytes} + @ivar method: the current authentication method + @type method: L{bytes} + @ivar nextService: the service the user wants started after authentication + has been completed. + @type nextService: L{bytes} + @ivar portal: the L{twisted.cred.portal.Portal} we are using for + authentication + @type portal: L{twisted.cred.portal.Portal} + @ivar clock: an object with a callLater method. Stubbed out for testing. + """ + + name = b'ssh-userauth' + loginTimeout = 10 * 60 * 60 + # 10 minutes before we disconnect them + attemptsBeforeDisconnect = 20 + # 20 login attempts before a disconnect + passwordDelay = 1 # number of seconds to delay on a failed password + clock = reactor + interfaceToMethod = { + credentials.ISSHPrivateKey : b'publickey', + credentials.IUsernamePassword : b'password', + } + + + def serviceStarted(self): + """ + Called when the userauth service is started. Set up instance + variables, check if we should allow password authentication (only + allow if the outgoing connection is encrypted) and set up a login + timeout. + """ + self.authenticatedWith = [] + self.loginAttempts = 0 + self.user = None + self.nextService = None + self.portal = self.transport.factory.portal + + self.supportedAuthentications = [] + for i in self.portal.listCredentialsInterfaces(): + if i in self.interfaceToMethod: + self.supportedAuthentications.append(self.interfaceToMethod[i]) + + if not self.transport.isEncrypted('in'): + # don't let us transport password in plaintext + if b'password' in self.supportedAuthentications: + self.supportedAuthentications.remove(b'password') + self._cancelLoginTimeout = self.clock.callLater( + self.loginTimeout, + self.timeoutAuthentication) + + + def serviceStopped(self): + """ + Called when the userauth service is stopped. Cancel the login timeout + if it's still going. + """ + if self._cancelLoginTimeout: + self._cancelLoginTimeout.cancel() + self._cancelLoginTimeout = None + + + def timeoutAuthentication(self): + """ + Called when the user has timed out on authentication. Disconnect + with a DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE message. + """ + self._cancelLoginTimeout = None + self.transport.sendDisconnect( + transport.DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE, + b'you took too long') + + + def tryAuth(self, kind, user, data): + """ + Try to authenticate the user with the given method. Dispatches to a + auth_* method. + + @param kind: the authentication method to try. + @type kind: L{bytes} + @param user: the username the client is authenticating with. + @type user: L{bytes} + @param data: authentication specific data sent by the client. + @type data: L{bytes} + @return: A Deferred called back if the method succeeded, or erred back + if it failed. + @rtype: C{defer.Deferred} + """ + log.msg('%r trying auth %r' % (user, kind)) + if kind not in self.supportedAuthentications: + return defer.fail( + error.ConchError('unsupported authentication, failing')) + kind = nativeString(kind.replace(b'-', b'_')) + f = getattr(self, 'auth_%s' % (kind,), None) + if f: + ret = f(data) + if not ret: + return defer.fail( + error.ConchError( + '%s return None instead of a Deferred' + % (kind, ))) + else: + return ret + return defer.fail(error.ConchError('bad auth type: %s' % (kind,))) + + + def ssh_USERAUTH_REQUEST(self, packet): + """ + The client has requested authentication. Payload:: + string user + string next service + string method + <authentication specific data> + + @type packet: L{bytes} + """ + user, nextService, method, rest = getNS(packet, 3) + if user != self.user or nextService != self.nextService: + self.authenticatedWith = [] # clear auth state + self.user = user + self.nextService = nextService + self.method = method + d = self.tryAuth(method, user, rest) + if not d: + self._ebBadAuth( + failure.Failure(error.ConchError('auth returned none'))) + return + d.addCallback(self._cbFinishedAuth) + d.addErrback(self._ebMaybeBadAuth) + d.addErrback(self._ebBadAuth) + return d + + + def _cbFinishedAuth(self, result): + """ + The callback when user has successfully been authenticated. For a + description of the arguments, see L{twisted.cred.portal.Portal.login}. + We start the service requested by the user. + """ + (interface, avatar, logout) = result + self.transport.avatar = avatar + self.transport.logoutFunction = logout + service = self.transport.factory.getService(self.transport, + self.nextService) + if not service: + raise error.ConchError('could not get next service: %s' + % self.nextService) + log.msg('%r authenticated with %r' % (self.user, self.method)) + self.transport.sendPacket(MSG_USERAUTH_SUCCESS, b'') + self.transport.setService(service()) + + + def _ebMaybeBadAuth(self, reason): + """ + An intermediate errback. If the reason is + error.NotEnoughAuthentication, we send a MSG_USERAUTH_FAILURE, but + with the partial success indicator set. + + @type reason: L{twisted.python.failure.Failure} + """ + reason.trap(error.NotEnoughAuthentication) + self.transport.sendPacket(MSG_USERAUTH_FAILURE, + NS(b','.join(self.supportedAuthentications)) + b'\xff') + + + def _ebBadAuth(self, reason): + """ + The final errback in the authentication chain. If the reason is + error.IgnoreAuthentication, we simply return; the authentication + method has sent its own response. Otherwise, send a failure message + and (if the method is not 'none') increment the number of login + attempts. + + @type reason: L{twisted.python.failure.Failure} + """ + if reason.check(error.IgnoreAuthentication): + return + if self.method != b'none': + log.msg('%r failed auth %r' % (self.user, self.method)) + if reason.check(UnauthorizedLogin): + log.msg('unauthorized login: %s' % reason.getErrorMessage()) + elif reason.check(error.ConchError): + log.msg('reason: %s' % reason.getErrorMessage()) + else: + log.msg(reason.getTraceback()) + self.loginAttempts += 1 + if self.loginAttempts > self.attemptsBeforeDisconnect: + self.transport.sendDisconnect( + transport.DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE, + b'too many bad auths') + return + self.transport.sendPacket( + MSG_USERAUTH_FAILURE, + NS(b','.join(self.supportedAuthentications)) + b'\x00') + + + def auth_publickey(self, packet): + """ + Public key authentication. Payload:: + byte has signature + string algorithm name + string key blob + [string signature] (if has signature is True) + + Create a SSHPublicKey credential and verify it using our portal. + """ + hasSig = ord(packet[0:1]) + algName, blob, rest = getNS(packet[1:], 2) + + try: + pubKey = keys.Key.fromString(blob) + except keys.BadKeyError: + error = "Unsupported key type %s or bad key" % ( + algName.decode('ascii'),) + log.msg(error) + return defer.fail(UnauthorizedLogin(error)) + + signature = hasSig and getNS(rest)[0] or None + if hasSig: + b = (NS(self.transport.sessionID) + chr(MSG_USERAUTH_REQUEST) + + NS(self.user) + NS(self.nextService) + NS(b'publickey') + + chr(hasSig) + NS(pubKey.sshType()) + NS(blob)) + c = credentials.SSHPrivateKey(self.user, algName, blob, b, + signature) + return self.portal.login(c, None, interfaces.IConchUser) + else: + c = credentials.SSHPrivateKey(self.user, algName, blob, None, None) + return self.portal.login(c, None, + interfaces.IConchUser).addErrback(self._ebCheckKey, + packet[1:]) + + + def _ebCheckKey(self, reason, packet): + """ + Called back if the user did not sent a signature. If reason is + error.ValidPublicKey then this key is valid for the user to + authenticate with. Send MSG_USERAUTH_PK_OK. + """ + reason.trap(error.ValidPublicKey) + # if we make it here, it means that the publickey is valid + self.transport.sendPacket(MSG_USERAUTH_PK_OK, packet) + return failure.Failure(error.IgnoreAuthentication()) + + + def auth_password(self, packet): + """ + Password authentication. Payload:: + string password + + Make a UsernamePassword credential and verify it with our portal. + """ + password = getNS(packet[1:])[0] + c = credentials.UsernamePassword(self.user, password) + return self.portal.login(c, None, interfaces.IConchUser).addErrback( + self._ebPassword) + + + def _ebPassword(self, f): + """ + If the password is invalid, wait before sending the failure in order + to delay brute-force password guessing. + """ + d = defer.Deferred() + self.clock.callLater(self.passwordDelay, d.callback, f) + return d + + + +class SSHUserAuthClient(service.SSHService): + """ + A service implementing the client side of 'ssh-userauth'. + + This service will try all authentication methods provided by the server, + making callbacks for more information when necessary. + + @ivar name: the name of this service: 'ssh-userauth' + @type name: L{str} + @ivar preferredOrder: a list of authentication methods that should be used + first, in order of preference, if supported by the server + @type preferredOrder: L{list} + @ivar user: the name of the user to authenticate as + @type user: L{bytes} + @ivar instance: the service to start after authentication has finished + @type instance: L{service.SSHService} + @ivar authenticatedWith: a list of strings of authentication methods we've tried + @type authenticatedWith: L{list} of L{bytes} + @ivar triedPublicKeys: a list of public key objects that we've tried to + authenticate with + @type triedPublicKeys: L{list} of L{Key} + @ivar lastPublicKey: the last public key object we've tried to authenticate + with + @type lastPublicKey: L{Key} + """ + + name = b'ssh-userauth' + preferredOrder = [b'publickey', b'password', b'keyboard-interactive'] + + + def __init__(self, user, instance): + self.user = user + self.instance = instance + + + def serviceStarted(self): + self.authenticatedWith = [] + self.triedPublicKeys = [] + self.lastPublicKey = None + self.askForAuth(b'none', b'') + + + def askForAuth(self, kind, extraData): + """ + Send a MSG_USERAUTH_REQUEST. + + @param kind: the authentication method to try. + @type kind: L{bytes} + @param extraData: method-specific data to go in the packet + @type extraData: L{bytes} + """ + self.lastAuth = kind + self.transport.sendPacket(MSG_USERAUTH_REQUEST, NS(self.user) + + NS(self.instance.name) + NS(kind) + extraData) + + + def tryAuth(self, kind): + """ + Dispatch to an authentication method. + + @param kind: the authentication method + @type kind: L{bytes} + """ + kind = nativeString(kind.replace(b'-', b'_')) + log.msg('trying to auth with %s' % (kind,)) + f = getattr(self,'auth_%s' % (kind,), None) + if f: + return f() + + + def _ebAuth(self, ignored, *args): + """ + Generic callback for a failed authentication attempt. Respond by + asking for the list of accepted methods (the 'none' method) + """ + self.askForAuth(b'none', b'') + + + def ssh_USERAUTH_SUCCESS(self, packet): + """ + We received a MSG_USERAUTH_SUCCESS. The server has accepted our + authentication, so start the next service. + """ + self.transport.setService(self.instance) + + + def ssh_USERAUTH_FAILURE(self, packet): + """ + We received a MSG_USERAUTH_FAILURE. Payload:: + string methods + byte partial success + + If partial success is C{True}, then the previous method succeeded but is + not sufficient for authentication. C{methods} is a comma-separated list + of accepted authentication methods. + + We sort the list of methods by their position in C{self.preferredOrder}, + removing methods that have already succeeded. We then call + C{self.tryAuth} with the most preferred method. + + @param packet: the C{MSG_USERAUTH_FAILURE} payload. + @type packet: L{bytes} + + @return: a L{defer.Deferred} that will be callbacked with L{None} as + soon as all authentication methods have been tried, or L{None} if no + more authentication methods are available. + @rtype: C{defer.Deferred} or L{None} + """ + canContinue, partial = getNS(packet) + partial = ord(partial) + if partial: + self.authenticatedWith.append(self.lastAuth) + + def orderByPreference(meth): + """ + Invoked once per authentication method in order to extract a + comparison key which is then used for sorting. + + @param meth: the authentication method. + @type meth: L{bytes} + + @return: the comparison key for C{meth}. + @rtype: L{int} + """ + if meth in self.preferredOrder: + return self.preferredOrder.index(meth) + else: + # put the element at the end of the list. + return len(self.preferredOrder) + + canContinue = sorted([meth for meth in canContinue.split(b',') + if meth not in self.authenticatedWith], + key=orderByPreference) + + log.msg('can continue with: %s' % canContinue) + return self._cbUserauthFailure(None, iter(canContinue)) + + + def _cbUserauthFailure(self, result, iterator): + if result: + return + try: + method = next(iterator) + except StopIteration: + self.transport.sendDisconnect( + transport.DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE, + b'no more authentication methods available') + else: + d = defer.maybeDeferred(self.tryAuth, method) + d.addCallback(self._cbUserauthFailure, iterator) + return d + + + def ssh_USERAUTH_PK_OK(self, packet): + """ + This message (number 60) can mean several different messages depending + on the current authentication type. We dispatch to individual methods + in order to handle this request. + """ + func = getattr(self, 'ssh_USERAUTH_PK_OK_%s' % + nativeString(self.lastAuth.replace(b'-', b'_')), None) + if func is not None: + return func(packet) + else: + self.askForAuth(b'none', b'') + + + def ssh_USERAUTH_PK_OK_publickey(self, packet): + """ + This is MSG_USERAUTH_PK. Our public key is valid, so we create a + signature and try to authenticate with it. + """ + publicKey = self.lastPublicKey + b = (NS(self.transport.sessionID) + chr(MSG_USERAUTH_REQUEST) + + NS(self.user) + NS(self.instance.name) + NS(b'publickey') + + b'\x01' + NS(publicKey.sshType()) + NS(publicKey.blob())) + d = self.signData(publicKey, b) + if not d: + self.askForAuth(b'none', b'') + # this will fail, we'll move on + return + d.addCallback(self._cbSignedData) + d.addErrback(self._ebAuth) + + + def ssh_USERAUTH_PK_OK_password(self, packet): + """ + This is MSG_USERAUTH_PASSWD_CHANGEREQ. The password given has expired. + We ask for an old password and a new password, then send both back to + the server. + """ + prompt, language, rest = getNS(packet, 2) + self._oldPass = self._newPass = None + d = self.getPassword(b'Old Password: ') + d = d.addCallbacks(self._setOldPass, self._ebAuth) + d.addCallback(lambda ignored: self.getPassword(prompt)) + d.addCallbacks(self._setNewPass, self._ebAuth) + + + def ssh_USERAUTH_PK_OK_keyboard_interactive(self, packet): + """ + This is MSG_USERAUTH_INFO_RESPONSE. The server has sent us the + questions it wants us to answer, so we ask the user and sent the + responses. + """ + name, instruction, lang, data = getNS(packet, 3) + numPrompts = struct.unpack('!L', data[:4])[0] + data = data[4:] + prompts = [] + for i in range(numPrompts): + prompt, data = getNS(data) + echo = bool(ord(data[0:1])) + data = data[1:] + prompts.append((prompt, echo)) + d = self.getGenericAnswers(name, instruction, prompts) + d.addCallback(self._cbGenericAnswers) + d.addErrback(self._ebAuth) + + + def _cbSignedData(self, signedData): + """ + Called back out of self.signData with the signed data. Send the + authentication request with the signature. + + @param signedData: the data signed by the user's private key. + @type signedData: L{bytes} + """ + publicKey = self.lastPublicKey + self.askForAuth(b'publickey', b'\x01' + NS(publicKey.sshType()) + + NS(publicKey.blob()) + NS(signedData)) + + + def _setOldPass(self, op): + """ + Called back when we are choosing a new password. Simply store the old + password for now. + + @param op: the old password as entered by the user + @type op: L{bytes} + """ + self._oldPass = op + + + def _setNewPass(self, np): + """ + Called back when we are choosing a new password. Get the old password + and send the authentication message with both. + + @param np: the new password as entered by the user + @type np: L{bytes} + """ + op = self._oldPass + self._oldPass = None + self.askForAuth(b'password', b'\xff' + NS(op) + NS(np)) + + + def _cbGenericAnswers(self, responses): + """ + Called back when we are finished answering keyboard-interactive + questions. Send the info back to the server in a + MSG_USERAUTH_INFO_RESPONSE. + + @param responses: a list of L{bytes} responses + @type responses: L{list} + """ + data = struct.pack('!L', len(responses)) + for r in responses: + data += NS(r.encode('UTF8')) + self.transport.sendPacket(MSG_USERAUTH_INFO_RESPONSE, data) + + + def auth_publickey(self): + """ + Try to authenticate with a public key. Ask the user for a public key; + if the user has one, send the request to the server and return True. + Otherwise, return False. + + @rtype: L{bool} + """ + d = defer.maybeDeferred(self.getPublicKey) + d.addBoth(self._cbGetPublicKey) + return d + + + def _cbGetPublicKey(self, publicKey): + if not isinstance(publicKey, keys.Key): # failure or None + publicKey = None + if publicKey is not None: + self.lastPublicKey = publicKey + self.triedPublicKeys.append(publicKey) + log.msg('using key of type %s' % publicKey.type()) + self.askForAuth(b'publickey', b'\x00' + NS(publicKey.sshType()) + + NS(publicKey.blob())) + return True + else: + return False + + + def auth_password(self): + """ + Try to authenticate with a password. Ask the user for a password. + If the user will return a password, return True. Otherwise, return + False. + + @rtype: L{bool} + """ + d = self.getPassword() + if d: + d.addCallbacks(self._cbPassword, self._ebAuth) + return True + else: # returned None, don't do password auth + return False + + + def auth_keyboard_interactive(self): + """ + Try to authenticate with keyboard-interactive authentication. Send + the request to the server and return True. + + @rtype: L{bool} + """ + log.msg('authing with keyboard-interactive') + self.askForAuth(b'keyboard-interactive', NS(b'') + NS(b'')) + return True + + + def _cbPassword(self, password): + """ + Called back when the user gives a password. Send the request to the + server. + + @param password: the password the user entered + @type password: L{bytes} + """ + self.askForAuth(b'password', b'\x00' + NS(password)) + + + def signData(self, publicKey, signData): + """ + Sign the given data with the given public key. + + By default, this will call getPrivateKey to get the private key, + then sign the data using Key.sign(). + + This method is factored out so that it can be overridden to use + alternate methods, such as a key agent. + + @param publicKey: The public key object returned from L{getPublicKey} + @type publicKey: L{keys.Key} + + @param signData: the data to be signed by the private key. + @type signData: L{bytes} + @return: a Deferred that's called back with the signature + @rtype: L{defer.Deferred} + """ + key = self.getPrivateKey() + if not key: + return + return key.addCallback(self._cbSignData, signData) + + + def _cbSignData(self, privateKey, signData): + """ + Called back when the private key is returned. Sign the data and + return the signature. + + @param privateKey: the private key object + @type publicKey: L{keys.Key} + @param signData: the data to be signed by the private key. + @type signData: L{bytes} + @return: the signature + @rtype: L{bytes} + """ + return privateKey.sign(signData) + + + def getPublicKey(self): + """ + Return a public key for the user. If no more public keys are + available, return L{None}. + + This implementation always returns L{None}. Override it in a + subclass to actually find and return a public key object. + + @rtype: L{Key} or L{None} + """ + return None + + + def getPrivateKey(self): + """ + Return a L{Deferred} that will be called back with the private key + object corresponding to the last public key from getPublicKey(). + If the private key is not available, errback on the Deferred. + + @rtype: L{Deferred} called back with L{Key} + """ + return defer.fail(NotImplementedError()) + + + def getPassword(self, prompt = None): + """ + Return a L{Deferred} that will be called back with a password. + prompt is a string to display for the password, or None for a generic + 'user@hostname's password: '. + + @type prompt: L{bytes}/L{None} + @rtype: L{defer.Deferred} + """ + return defer.fail(NotImplementedError()) + + + def getGenericAnswers(self, name, instruction, prompts): + """ + Returns a L{Deferred} with the responses to the promopts. + + @param name: The name of the authentication currently in progress. + @param instruction: Describes what the authentication wants. + @param prompts: A list of (prompt, echo) pairs, where prompt is a + string to display and echo is a boolean indicating whether the + user's response should be echoed as they type it. + """ + return defer.fail(NotImplementedError()) + + +MSG_USERAUTH_REQUEST = 50 +MSG_USERAUTH_FAILURE = 51 +MSG_USERAUTH_SUCCESS = 52 +MSG_USERAUTH_BANNER = 53 +MSG_USERAUTH_INFO_RESPONSE = 61 +MSG_USERAUTH_PK_OK = 60 + +messages = {} +for k, v in list(locals().items()): + if k[:4] == 'MSG_': + messages[v] = k + +SSHUserAuthServer.protocolMessages = messages +SSHUserAuthClient.protocolMessages = messages +del messages +del v + +# Doubles, not included in the protocols' mappings +MSG_USERAUTH_PASSWD_CHANGEREQ = 60 +MSG_USERAUTH_INFO_REQUEST = 60 diff --git a/contrib/python/Twisted/py2/twisted/conch/stdio.py b/contrib/python/Twisted/py2/twisted/conch/stdio.py new file mode 100644 index 0000000000..78a88d8886 --- /dev/null +++ b/contrib/python/Twisted/py2/twisted/conch/stdio.py @@ -0,0 +1,120 @@ +# -*- test-case-name: twisted.conch.test.test_manhole -*- +# Copyright (c) Twisted Matrix Laboratories. +# See LICENSE for details. + +""" +Asynchronous local terminal input handling + +@author: Jp Calderone +""" + +import os, tty, sys, termios + +from twisted.internet import reactor, stdio, protocol, defer +from twisted.python import failure, reflect, log + +from twisted.conch.insults.insults import ServerProtocol +from twisted.conch.manhole import ColoredManhole + +class UnexpectedOutputError(Exception): + pass + + + +class TerminalProcessProtocol(protocol.ProcessProtocol): + def __init__(self, proto): + self.proto = proto + self.onConnection = defer.Deferred() + + + def connectionMade(self): + self.proto.makeConnection(self) + self.onConnection.callback(None) + self.onConnection = None + + + def write(self, data): + """ + Write to the terminal. + + @param data: Data to write. + @type data: L{bytes} + """ + self.transport.write(data) + + + def outReceived(self, data): + """ + Receive data from the terminal. + + @param data: Data received. + @type data: L{bytes} + """ + self.proto.dataReceived(data) + + + def errReceived(self, data): + """ + Report an error. + + @param data: Data to include in L{Failure}. + @type data: L{bytes} + """ + self.transport.loseConnection() + if self.proto is not None: + self.proto.connectionLost(failure.Failure(UnexpectedOutputError(data))) + self.proto = None + + + def childConnectionLost(self, childFD): + if self.proto is not None: + self.proto.childConnectionLost(childFD) + + + def processEnded(self, reason): + if self.proto is not None: + self.proto.connectionLost(reason) + self.proto = None + + + +class ConsoleManhole(ColoredManhole): + """ + A manhole protocol specifically for use with L{stdio.StandardIO}. + """ + def connectionLost(self, reason): + """ + When the connection is lost, there is nothing more to do. Stop the + reactor so that the process can exit. + """ + reactor.stop() + + + +def runWithProtocol(klass): + fd = sys.__stdin__.fileno() + oldSettings = termios.tcgetattr(fd) + tty.setraw(fd) + try: + stdio.StandardIO(ServerProtocol(klass)) + reactor.run() + finally: + termios.tcsetattr(fd, termios.TCSANOW, oldSettings) + os.write(fd, b"\r\x1bc\r") + + + +def main(argv=None): + log.startLogging(open('child.log', 'w')) + + if argv is None: + argv = sys.argv[1:] + if argv: + klass = reflect.namedClass(argv[0]) + else: + klass = ConsoleManhole + runWithProtocol(klass) + + +if __name__ == '__main__': + main() diff --git a/contrib/python/Twisted/py2/twisted/conch/tap.py b/contrib/python/Twisted/py2/twisted/conch/tap.py new file mode 100644 index 0000000000..f622854a06 --- /dev/null +++ b/contrib/python/Twisted/py2/twisted/conch/tap.py @@ -0,0 +1,86 @@ +# -*- test-case-name: twisted.conch.test.test_tap -*- +# Copyright (c) Twisted Matrix Laboratories. +# See LICENSE for details. + +""" +Support module for making SSH servers with twistd. +""" + +from twisted.conch import unix +from twisted.conch import checkers as conch_checkers +from twisted.conch.openssh_compat import factory +from twisted.cred import portal, strcred +from twisted.python import usage +from twisted.application import strports + + +class Options(usage.Options, strcred.AuthOptionMixin): + synopsis = "[-i <interface>] [-p <port>] [-d <dir>] " + longdesc = ("Makes a Conch SSH server. If no authentication methods are " + "specified, the default authentication methods are UNIX passwords " + "and SSH public keys. If --auth options are " + "passed, only the measures specified will be used.") + optParameters = [ + ["interface", "i", "", "local interface to which we listen"], + ["port", "p", "tcp:22", "Port on which to listen"], + ["data", "d", "/etc", "directory to look for host keys in"], + ["moduli", "", None, "directory to look for moduli in " + "(if different from --data)"] + ] + compData = usage.Completions( + optActions={"data": usage.CompleteDirs(descr="data directory"), + "moduli": usage.CompleteDirs(descr="moduli directory"), + "interface": usage.CompleteNetInterfaces()} + ) + + + def __init__(self, *a, **kw): + usage.Options.__init__(self, *a, **kw) + + # Call the default addCheckers (for backwards compatibility) that will + # be used if no --auth option is provided - note that conch's + # UNIXPasswordDatabase is used, instead of twisted.plugins.cred_unix's + # checker + super(Options, self).addChecker(conch_checkers.UNIXPasswordDatabase()) + super(Options, self).addChecker(conch_checkers.SSHPublicKeyChecker( + conch_checkers.UNIXAuthorizedKeysFiles())) + self._usingDefaultAuth = True + + + def addChecker(self, checker): + """ + Add the checker specified. If any checkers are added, the default + checkers are automatically cleared and the only checkers will be the + specified one(s). + """ + if self._usingDefaultAuth: + self['credCheckers'] = [] + self['credInterfaces'] = {} + self._usingDefaultAuth = False + super(Options, self).addChecker(checker) + + + +def makeService(config): + """ + Construct a service for operating a SSH server. + + @param config: An L{Options} instance specifying server options, including + where server keys are stored and what authentication methods to use. + + @return: A L{twisted.application.service.IService} provider which contains + the requested SSH server. + """ + + t = factory.OpenSSHFactory() + + r = unix.UnixSSHRealm() + t.portal = portal.Portal(r, config.get('credCheckers', [])) + t.dataRoot = config['data'] + t.moduliRoot = config['moduli'] or config['data'] + + port = config['port'] + if config['interface']: + # Add warning here + port += ':interface=' + config['interface'] + return strports.service(port, t) diff --git a/contrib/python/Twisted/py2/twisted/conch/telnet.py b/contrib/python/Twisted/py2/twisted/conch/telnet.py new file mode 100644 index 0000000000..daa27b8aba --- /dev/null +++ b/contrib/python/Twisted/py2/twisted/conch/telnet.py @@ -0,0 +1,1194 @@ +# -*- test-case-name: twisted.conch.test.test_telnet -*- +# Copyright (c) Twisted Matrix Laboratories. +# See LICENSE for details. + +""" +Telnet protocol implementation. + +@author: Jean-Paul Calderone +""" + +from __future__ import absolute_import, division + +import struct + +from zope.interface import implementer + +from twisted.internet import protocol, interfaces as iinternet, defer +from twisted.python import log +from twisted.python.compat import _bytesChr as chr, iterbytes + +MODE = chr(1) +EDIT = 1 +TRAPSIG = 2 +MODE_ACK = 4 +SOFT_TAB = 8 +LIT_ECHO = 16 + +# Characters gleaned from the various (and conflicting) RFCs. Not all of these are correct. + +NULL = chr(0) # No operation. +BEL = chr(7) # Produces an audible or + # visible signal (which does + # NOT move the print head). +BS = chr(8) # Moves the print head one + # character position towards + # the left margin. +HT = chr(9) # Moves the printer to the + # next horizontal tab stop. + # It remains unspecified how + # either party determines or + # establishes where such tab + # stops are located. +LF = chr(10) # Moves the printer to the + # next print line, keeping the + # same horizontal position. +VT = chr(11) # Moves the printer to the + # next vertical tab stop. It + # remains unspecified how + # either party determines or + # establishes where such tab + # stops are located. +FF = chr(12) # Moves the printer to the top + # of the next page, keeping + # the same horizontal position. +CR = chr(13) # Moves the printer to the left + # margin of the current line. + +ECHO = chr(1) # User-to-Server: Asks the server to send + # Echos of the transmitted data. +SGA = chr(3) # Suppress Go Ahead. Go Ahead is silly + # and most modern servers should suppress + # it. +NAWS = chr(31) # Negotiate About Window Size. Indicate that + # information about the size of the terminal + # can be communicated. +LINEMODE = chr(34) # Allow line buffering to be + # negotiated about. + +SE = chr(240) # End of subnegotiation parameters. +NOP = chr(241) # No operation. +DM = chr(242) # "Data Mark": The data stream portion + # of a Synch. This should always be + # accompanied by a TCP Urgent + # notification. +BRK = chr(243) # NVT character Break. +IP = chr(244) # The function Interrupt Process. +AO = chr(245) # The function Abort Output +AYT = chr(246) # The function Are You There. +EC = chr(247) # The function Erase Character. +EL = chr(248) # The function Erase Line +GA = chr(249) # The Go Ahead signal. +SB = chr(250) # Indicates that what follows is + # subnegotiation of the indicated + # option. +WILL = chr(251) # Indicates the desire to begin + # performing, or confirmation that + # you are now performing, the + # indicated option. +WONT = chr(252) # Indicates the refusal to perform, + # or continue performing, the + # indicated option. +DO = chr(253) # Indicates the request that the + # other party perform, or + # confirmation that you are expecting + # the other party to perform, the + # indicated option. +DONT = chr(254) # Indicates the demand that the + # other party stop performing, + # or confirmation that you are no + # longer expecting the other party + # to perform, the indicated option. +IAC = chr(255) # Data Byte 255. Introduces a + # telnet command. + +LINEMODE_MODE = chr(1) +LINEMODE_EDIT = chr(1) +LINEMODE_TRAPSIG = chr(2) +LINEMODE_MODE_ACK = chr(4) +LINEMODE_SOFT_TAB = chr(8) +LINEMODE_LIT_ECHO = chr(16) +LINEMODE_FORWARDMASK = chr(2) +LINEMODE_SLC = chr(3) +LINEMODE_SLC_SYNCH = chr(1) +LINEMODE_SLC_BRK = chr(2) +LINEMODE_SLC_IP = chr(3) +LINEMODE_SLC_AO = chr(4) +LINEMODE_SLC_AYT = chr(5) +LINEMODE_SLC_EOR = chr(6) +LINEMODE_SLC_ABORT = chr(7) +LINEMODE_SLC_EOF = chr(8) +LINEMODE_SLC_SUSP = chr(9) +LINEMODE_SLC_EC = chr(10) +LINEMODE_SLC_EL = chr(11) + +LINEMODE_SLC_EW = chr(12) +LINEMODE_SLC_RP = chr(13) +LINEMODE_SLC_LNEXT = chr(14) +LINEMODE_SLC_XON = chr(15) +LINEMODE_SLC_XOFF = chr(16) +LINEMODE_SLC_FORW1 = chr(17) +LINEMODE_SLC_FORW2 = chr(18) +LINEMODE_SLC_MCL = chr(19) +LINEMODE_SLC_MCR = chr(20) +LINEMODE_SLC_MCWL = chr(21) +LINEMODE_SLC_MCWR = chr(22) +LINEMODE_SLC_MCBOL = chr(23) +LINEMODE_SLC_MCEOL = chr(24) +LINEMODE_SLC_INSRT = chr(25) +LINEMODE_SLC_OVER = chr(26) +LINEMODE_SLC_ECR = chr(27) +LINEMODE_SLC_EWR = chr(28) +LINEMODE_SLC_EBOL = chr(29) +LINEMODE_SLC_EEOL = chr(30) + +LINEMODE_SLC_DEFAULT = chr(3) +LINEMODE_SLC_VALUE = chr(2) +LINEMODE_SLC_CANTCHANGE = chr(1) +LINEMODE_SLC_NOSUPPORT = chr(0) +LINEMODE_SLC_LEVELBITS = chr(3) + +LINEMODE_SLC_ACK = chr(128) +LINEMODE_SLC_FLUSHIN = chr(64) +LINEMODE_SLC_FLUSHOUT = chr(32) +LINEMODE_EOF = chr(236) +LINEMODE_SUSP = chr(237) +LINEMODE_ABORT = chr(238) + +class ITelnetProtocol(iinternet.IProtocol): + def unhandledCommand(command, argument): + """ + A command was received but not understood. + + @param command: the command received. + @type command: L{str}, a single character. + @param argument: the argument to the received command. + @type argument: L{str}, a single character, or None if the command that + was unhandled does not provide an argument. + """ + + + def unhandledSubnegotiation(command, data): + """ + A subnegotiation command was received but not understood. + + @param command: the command being subnegotiated. That is, the first + byte after the SB command. + @type command: L{str}, a single character. + @param data: all other bytes of the subneogation. That is, all but the + first bytes between SB and SE, with IAC un-escaping applied. + @type data: L{bytes}, each a single character + """ + + + def enableLocal(option): + """ + Enable the given option locally. + + This should enable the given option on this side of the + telnet connection and return True. If False is returned, + the option will be treated as still disabled and the peer + will be notified. + + @param option: the option to be enabled. + @type option: L{bytes}, a single character. + """ + + + def enableRemote(option): + """ + Indicate whether the peer should be allowed to enable this option. + + Returns True if the peer should be allowed to enable this option, + False otherwise. + + @param option: the option to be enabled. + @type option: L{bytes}, a single character. + """ + + + def disableLocal(option): + """ + Disable the given option locally. + + Unlike enableLocal, this method cannot fail. The option must be + disabled. + + @param option: the option to be disabled. + @type option: L{bytes}, a single character. + """ + + + def disableRemote(option): + """ + Indicate that the peer has disabled this option. + + @param option: the option to be disabled. + @type option: L{bytes}, a single character. + """ + + + +class ITelnetTransport(iinternet.ITransport): + def do(option): + """ + Indicate a desire for the peer to begin performing the given option. + + Returns a Deferred that fires with True when the peer begins performing + the option, or fails with L{OptionRefused} when the peer refuses to + perform it. If the peer is already performing the given option, the + Deferred will fail with L{AlreadyEnabled}. If a negotiation regarding + this option is already in progress, the Deferred will fail with + L{AlreadyNegotiating}. + + Note: It is currently possible that this Deferred will never fire, + if the peer never responds, or if the peer believes the option to + already be enabled. + """ + + + def dont(option): + """ + Indicate a desire for the peer to cease performing the given option. + + Returns a Deferred that fires with True when the peer ceases performing + the option. If the peer is not performing the given option, the + Deferred will fail with L{AlreadyDisabled}. If negotiation regarding + this option is already in progress, the Deferred will fail with + L{AlreadyNegotiating}. + + Note: It is currently possible that this Deferred will never fire, + if the peer never responds, or if the peer believes the option to + already be disabled. + """ + + + def will(option): + """ + Indicate our willingness to begin performing this option locally. + + Returns a Deferred that fires with True when the peer agrees to allow us + to begin performing this option, or fails with L{OptionRefused} if the + peer refuses to allow us to begin performing it. If the option is + already enabled locally, the Deferred will fail with L{AlreadyEnabled}. + If negotiation regarding this option is already in progress, the + Deferred will fail with L{AlreadyNegotiating}. + + Note: It is currently possible that this Deferred will never fire, + if the peer never responds, or if the peer believes the option to + already be enabled. + """ + + + def wont(option): + """ + Indicate that we will stop performing the given option. + + Returns a Deferred that fires with True when the peer acknowledges + we have stopped performing this option. If the option is already + disabled locally, the Deferred will fail with L{AlreadyDisabled}. + If negotiation regarding this option is already in progress, + the Deferred will fail with L{AlreadyNegotiating}. + + Note: It is currently possible that this Deferred will never fire, + if the peer never responds, or if the peer believes the option to + already be disabled. + """ + + + def requestNegotiation(about, data): + """ + Send a subnegotiation request. + + @param about: A byte indicating the feature being negotiated. + @param data: Any number of L{bytes} containing specific information + about the negotiation being requested. No values in this string + need to be escaped, as this function will escape any value which + requires it. + """ + + + +class TelnetError(Exception): + pass + + + +class NegotiationError(TelnetError): + def __str__(self): + return self.__class__.__module__ + '.' + self.__class__.__name__ + ':' + repr(self.args[0]) + + + +class OptionRefused(NegotiationError): + pass + + + +class AlreadyEnabled(NegotiationError): + pass + + + +class AlreadyDisabled(NegotiationError): + pass + + + +class AlreadyNegotiating(NegotiationError): + pass + + + +@implementer(ITelnetProtocol) +class TelnetProtocol(protocol.Protocol): + def unhandledCommand(self, command, argument): + pass + + + def unhandledSubnegotiation(self, command, data): + pass + + + def enableLocal(self, option): + pass + + + def enableRemote(self, option): + pass + + + def disableLocal(self, option): + pass + + + def disableRemote(self, option): + pass + + + +class Telnet(protocol.Protocol): + """ + @ivar commandMap: A mapping of bytes to callables. When a + telnet command is received, the command byte (the first byte + after IAC) is looked up in this dictionary. If a callable is + found, it is invoked with the argument of the command, or None + if the command takes no argument. Values should be added to + this dictionary if commands wish to be handled. By default, + only WILL, WONT, DO, and DONT are handled. These should not + be overridden, as this class handles them correctly and + provides an API for interacting with them. + + @ivar negotiationMap: A mapping of bytes to callables. When + a subnegotiation command is received, the command byte (the + first byte after SB) is looked up in this dictionary. If + a callable is found, it is invoked with the argument of the + subnegotiation. Values should be added to this dictionary if + subnegotiations are to be handled. By default, no values are + handled. + + @ivar options: A mapping of option bytes to their current + state. This state is likely of little use to user code. + Changes should not be made to it. + + @ivar state: A string indicating the current parse state. It + can take on the values "data", "escaped", "command", "newline", + "subnegotiation", and "subnegotiation-escaped". Changes + should not be made to it. + + @ivar transport: This protocol's transport object. + """ + + # One of a lot of things + state = 'data' + + def __init__(self): + self.options = {} + self.negotiationMap = {} + self.commandMap = { + WILL: self.telnet_WILL, + WONT: self.telnet_WONT, + DO: self.telnet_DO, + DONT: self.telnet_DONT} + + + def _write(self, data): + self.transport.write(data) + + + class _OptionState: + """ + Represents the state of an option on both sides of a telnet + connection. + + @ivar us: The state of the option on this side of the connection. + + @ivar him: The state of the option on the other side of the + connection. + """ + class _Perspective: + """ + Represents the state of an option on side of the telnet + connection. Some options can be enabled on a particular side of + the connection (RFC 1073 for example: only the client can have + NAWS enabled). Other options can be enabled on either or both + sides (such as RFC 1372: each side can have its own flow control + state). + + @ivar state: C{'yes'} or C{'no'} indicating whether or not this + option is enabled on one side of the connection. + + @ivar negotiating: A boolean tracking whether negotiation about + this option is in progress. + + @ivar onResult: When negotiation about this option has been + initiated by this side of the connection, a L{Deferred} + which will fire with the result of the negotiation. L{None} + at other times. + """ + state = 'no' + negotiating = False + onResult = None + + def __str__(self): + return self.state + ('*' * self.negotiating) + + + def __init__(self): + self.us = self._Perspective() + self.him = self._Perspective() + + + def __repr__(self): + return '<_OptionState us=%s him=%s>' % (self.us, self.him) + + + def getOptionState(self, opt): + return self.options.setdefault(opt, self._OptionState()) + + + def _do(self, option): + self._write(IAC + DO + option) + + + def _dont(self, option): + self._write(IAC + DONT + option) + + + def _will(self, option): + self._write(IAC + WILL + option) + + + def _wont(self, option): + self._write(IAC + WONT + option) + + + def will(self, option): + """ + Indicate our willingness to enable an option. + """ + s = self.getOptionState(option) + if s.us.negotiating or s.him.negotiating: + return defer.fail(AlreadyNegotiating(option)) + elif s.us.state == 'yes': + return defer.fail(AlreadyEnabled(option)) + else: + s.us.negotiating = True + s.us.onResult = d = defer.Deferred() + self._will(option) + return d + + + def wont(self, option): + """ + Indicate we are not willing to enable an option. + """ + s = self.getOptionState(option) + if s.us.negotiating or s.him.negotiating: + return defer.fail(AlreadyNegotiating(option)) + elif s.us.state == 'no': + return defer.fail(AlreadyDisabled(option)) + else: + s.us.negotiating = True + s.us.onResult = d = defer.Deferred() + self._wont(option) + return d + + + def do(self, option): + s = self.getOptionState(option) + if s.us.negotiating or s.him.negotiating: + return defer.fail(AlreadyNegotiating(option)) + elif s.him.state == 'yes': + return defer.fail(AlreadyEnabled(option)) + else: + s.him.negotiating = True + s.him.onResult = d = defer.Deferred() + self._do(option) + return d + + + def dont(self, option): + s = self.getOptionState(option) + if s.us.negotiating or s.him.negotiating: + return defer.fail(AlreadyNegotiating(option)) + elif s.him.state == 'no': + return defer.fail(AlreadyDisabled(option)) + else: + s.him.negotiating = True + s.him.onResult = d = defer.Deferred() + self._dont(option) + return d + + + def requestNegotiation(self, about, data): + """ + Send a negotiation message for the option C{about} with C{data} as the + payload. + + @param data: the payload + @type data: L{bytes} + @see: L{ITelnetTransport.requestNegotiation} + """ + data = data.replace(IAC, IAC * 2) + self._write(IAC + SB + about + data + IAC + SE) + + + def dataReceived(self, data): + appDataBuffer = [] + + for b in iterbytes(data): + if self.state == 'data': + if b == IAC: + self.state = 'escaped' + elif b == b'\r': + self.state = 'newline' + else: + appDataBuffer.append(b) + elif self.state == 'escaped': + if b == IAC: + appDataBuffer.append(b) + self.state = 'data' + elif b == SB: + self.state = 'subnegotiation' + self.commands = [] + elif b in (NOP, DM, BRK, IP, AO, AYT, EC, EL, GA): + self.state = 'data' + if appDataBuffer: + self.applicationDataReceived(b''.join(appDataBuffer)) + del appDataBuffer[:] + self.commandReceived(b, None) + elif b in (WILL, WONT, DO, DONT): + self.state = 'command' + self.command = b + else: + raise ValueError("Stumped", b) + elif self.state == 'command': + self.state = 'data' + command = self.command + del self.command + if appDataBuffer: + self.applicationDataReceived(b''.join(appDataBuffer)) + del appDataBuffer[:] + self.commandReceived(command, b) + elif self.state == 'newline': + self.state = 'data' + if b == b'\n': + appDataBuffer.append(b'\n') + elif b == b'\0': + appDataBuffer.append(b'\r') + elif b == IAC: + # IAC isn't really allowed after \r, according to the + # RFC, but handling it this way is less surprising than + # delivering the IAC to the app as application data. + # The purpose of the restriction is to allow terminals + # to unambiguously interpret the behavior of the CR + # after reading only one more byte. CR LF is supposed + # to mean one thing (cursor to next line, first column), + # CR NUL another (cursor to first column). Absent the + # NUL, it still makes sense to interpret this as CR and + # then apply all the usual interpretation to the IAC. + appDataBuffer.append(b'\r') + self.state = 'escaped' + else: + appDataBuffer.append(b'\r' + b) + elif self.state == 'subnegotiation': + if b == IAC: + self.state = 'subnegotiation-escaped' + else: + self.commands.append(b) + elif self.state == 'subnegotiation-escaped': + if b == SE: + self.state = 'data' + commands = self.commands + del self.commands + if appDataBuffer: + self.applicationDataReceived(b''.join(appDataBuffer)) + del appDataBuffer[:] + self.negotiate(commands) + else: + self.state = 'subnegotiation' + self.commands.append(b) + else: + raise ValueError("How'd you do this?") + + if appDataBuffer: + self.applicationDataReceived(b''.join(appDataBuffer)) + + + def connectionLost(self, reason): + for state in self.options.values(): + if state.us.onResult is not None: + d = state.us.onResult + state.us.onResult = None + d.errback(reason) + if state.him.onResult is not None: + d = state.him.onResult + state.him.onResult = None + d.errback(reason) + + + def applicationDataReceived(self, data): + """ + Called with application-level data. + """ + + def unhandledCommand(self, command, argument): + """ + Called for commands for which no handler is installed. + """ + + + def commandReceived(self, command, argument): + cmdFunc = self.commandMap.get(command) + if cmdFunc is None: + self.unhandledCommand(command, argument) + else: + cmdFunc(argument) + + + def unhandledSubnegotiation(self, command, data): + """ + Called for subnegotiations for which no handler is installed. + """ + + + def negotiate(self, data): + command, data = data[0], data[1:] + cmdFunc = self.negotiationMap.get(command) + if cmdFunc is None: + self.unhandledSubnegotiation(command, data) + else: + cmdFunc(data) + + + def telnet_WILL(self, option): + s = self.getOptionState(option) + self.willMap[s.him.state, s.him.negotiating](self, s, option) + + + def will_no_false(self, state, option): + # He is unilaterally offering to enable an option. + if self.enableRemote(option): + state.him.state = 'yes' + self._do(option) + else: + self._dont(option) + + + def will_no_true(self, state, option): + # Peer agreed to enable an option in response to our request. + state.him.state = 'yes' + state.him.negotiating = False + d = state.him.onResult + state.him.onResult = None + d.callback(True) + assert self.enableRemote(option), "enableRemote must return True in this context (for option %r)" % (option,) + + + def will_yes_false(self, state, option): + # He is unilaterally offering to enable an already-enabled option. + # Ignore this. + pass + + + def will_yes_true(self, state, option): + # This is a bogus state. It is here for completeness. It will + # never be entered. + assert False, "will_yes_true can never be entered, but was called with %r, %r" % (state, option) + + willMap = {('no', False): will_no_false, ('no', True): will_no_true, + ('yes', False): will_yes_false, ('yes', True): will_yes_true} + + + def telnet_WONT(self, option): + s = self.getOptionState(option) + self.wontMap[s.him.state, s.him.negotiating](self, s, option) + + + def wont_no_false(self, state, option): + # He is unilaterally demanding that an already-disabled option be/remain disabled. + # Ignore this (although we could record it and refuse subsequent enable attempts + # from our side - he can always refuse them again though, so we won't) + pass + + + def wont_no_true(self, state, option): + # Peer refused to enable an option in response to our request. + state.him.negotiating = False + d = state.him.onResult + state.him.onResult = None + d.errback(OptionRefused(option)) + + + def wont_yes_false(self, state, option): + # Peer is unilaterally demanding that an option be disabled. + state.him.state = 'no' + self.disableRemote(option) + self._dont(option) + + + def wont_yes_true(self, state, option): + # Peer agreed to disable an option at our request. + state.him.state = 'no' + state.him.negotiating = False + d = state.him.onResult + state.him.onResult = None + d.callback(True) + self.disableRemote(option) + + wontMap = {('no', False): wont_no_false, ('no', True): wont_no_true, + ('yes', False): wont_yes_false, ('yes', True): wont_yes_true} + + + def telnet_DO(self, option): + s = self.getOptionState(option) + self.doMap[s.us.state, s.us.negotiating](self, s, option) + + + def do_no_false(self, state, option): + # Peer is unilaterally requesting that we enable an option. + if self.enableLocal(option): + state.us.state = 'yes' + self._will(option) + else: + self._wont(option) + + + def do_no_true(self, state, option): + # Peer agreed to allow us to enable an option at our request. + state.us.state = 'yes' + state.us.negotiating = False + d = state.us.onResult + state.us.onResult = None + d.callback(True) + self.enableLocal(option) + + + def do_yes_false(self, state, option): + # Peer is unilaterally requesting us to enable an already-enabled option. + # Ignore this. + pass + + + def do_yes_true(self, state, option): + # This is a bogus state. It is here for completeness. It will never be + # entered. + assert False, "do_yes_true can never be entered, but was called with %r, %r" % (state, option) + + doMap = {('no', False): do_no_false, ('no', True): do_no_true, + ('yes', False): do_yes_false, ('yes', True): do_yes_true} + + + def telnet_DONT(self, option): + s = self.getOptionState(option) + self.dontMap[s.us.state, s.us.negotiating](self, s, option) + + + def dont_no_false(self, state, option): + # Peer is unilaterally demanding us to disable an already-disabled option. + # Ignore this. + pass + + + def dont_no_true(self, state, option): + # Offered option was refused. Fail the Deferred returned by the + # previous will() call. + state.us.negotiating = False + d = state.us.onResult + state.us.onResult = None + d.errback(OptionRefused(option)) + + + def dont_yes_false(self, state, option): + # Peer is unilaterally demanding we disable an option. + state.us.state = 'no' + self.disableLocal(option) + self._wont(option) + + + def dont_yes_true(self, state, option): + # Peer acknowledged our notice that we will disable an option. + state.us.state = 'no' + state.us.negotiating = False + d = state.us.onResult + state.us.onResult = None + d.callback(True) + self.disableLocal(option) + + dontMap = {('no', False): dont_no_false, ('no', True): dont_no_true, + ('yes', False): dont_yes_false, ('yes', True): dont_yes_true} + + + def enableLocal(self, option): + """ + Reject all attempts to enable options. + """ + return False + + + def enableRemote(self, option): + """ + Reject all attempts to enable options. + """ + return False + + + def disableLocal(self, option): + """ + Signal a programming error by raising an exception. + + L{enableLocal} must return true for the given value of C{option} in + order for this method to be called. If a subclass of L{Telnet} + overrides enableLocal to allow certain options to be enabled, it must + also override disableLocal to disable those options. + + @raise NotImplementedError: Always raised. + """ + raise NotImplementedError( + "Don't know how to disable local telnet option %r" % (option,)) + + + def disableRemote(self, option): + """ + Signal a programming error by raising an exception. + + L{enableRemote} must return true for the given value of C{option} in + order for this method to be called. If a subclass of L{Telnet} + overrides enableRemote to allow certain options to be enabled, it must + also override disableRemote tto disable those options. + + @raise NotImplementedError: Always raised. + """ + raise NotImplementedError( + "Don't know how to disable remote telnet option %r" % (option,)) + + + +class ProtocolTransportMixin: + def write(self, data): + self.transport.write(data.replace(b'\n', b'\r\n')) + + + def writeSequence(self, seq): + self.transport.writeSequence(seq) + + + def loseConnection(self): + self.transport.loseConnection() + + + def getHost(self): + return self.transport.getHost() + + + def getPeer(self): + return self.transport.getPeer() + + + +class TelnetTransport(Telnet, ProtocolTransportMixin): + """ + @ivar protocol: An instance of the protocol to which this + transport is connected, or None before the connection is + established and after it is lost. + + @ivar protocolFactory: A callable which returns protocol instances + which provide L{ITelnetProtocol}. This will be invoked when a + connection is established. It is passed *protocolArgs and + **protocolKwArgs. + + @ivar protocolArgs: A tuple of additional arguments to + pass to protocolFactory. + + @ivar protocolKwArgs: A dictionary of additional arguments + to pass to protocolFactory. + """ + + disconnecting = False + + protocolFactory = None + protocol = None + + def __init__(self, protocolFactory=None, *a, **kw): + Telnet.__init__(self) + if protocolFactory is not None: + self.protocolFactory = protocolFactory + self.protocolArgs = a + self.protocolKwArgs = kw + + + def connectionMade(self): + if self.protocolFactory is not None: + self.protocol = self.protocolFactory(*self.protocolArgs, **self.protocolKwArgs) + assert ITelnetProtocol.providedBy(self.protocol) + try: + factory = self.factory + except AttributeError: + pass + else: + self.protocol.factory = factory + self.protocol.makeConnection(self) + + + def connectionLost(self, reason): + Telnet.connectionLost(self, reason) + if self.protocol is not None: + try: + self.protocol.connectionLost(reason) + finally: + del self.protocol + + + def enableLocal(self, option): + return self.protocol.enableLocal(option) + + + def enableRemote(self, option): + return self.protocol.enableRemote(option) + + + def disableLocal(self, option): + return self.protocol.disableLocal(option) + + + def disableRemote(self, option): + return self.protocol.disableRemote(option) + + + def unhandledSubnegotiation(self, command, data): + self.protocol.unhandledSubnegotiation(command, data) + + + def unhandledCommand(self, command, argument): + self.protocol.unhandledCommand(command, argument) + + + def applicationDataReceived(self, data): + self.protocol.dataReceived(data) + + + def write(self, data): + ProtocolTransportMixin.write(self, data.replace(b'\xff', b'\xff\xff')) + + + +class TelnetBootstrapProtocol(TelnetProtocol, ProtocolTransportMixin): + protocol = None + + def __init__(self, protocolFactory, *args, **kw): + self.protocolFactory = protocolFactory + self.protocolArgs = args + self.protocolKwArgs = kw + + + def connectionMade(self): + self.transport.negotiationMap[NAWS] = self.telnet_NAWS + self.transport.negotiationMap[LINEMODE] = self.telnet_LINEMODE + + for opt in (LINEMODE, NAWS, SGA): + self.transport.do(opt).addErrback(log.err) + for opt in (ECHO,): + self.transport.will(opt).addErrback(log.err) + + self.protocol = self.protocolFactory(*self.protocolArgs, **self.protocolKwArgs) + + try: + factory = self.factory + except AttributeError: + pass + else: + self.protocol.factory = factory + + self.protocol.makeConnection(self) + + + def connectionLost(self, reason): + if self.protocol is not None: + try: + self.protocol.connectionLost(reason) + finally: + del self.protocol + + + def dataReceived(self, data): + self.protocol.dataReceived(data) + + + def enableLocal(self, opt): + if opt == ECHO: + return True + elif opt == SGA: + return True + else: + return False + + + def enableRemote(self, opt): + if opt == LINEMODE: + self.transport.requestNegotiation(LINEMODE, MODE + chr(TRAPSIG)) + return True + elif opt == NAWS: + return True + elif opt == SGA: + return True + else: + return False + + + def telnet_NAWS(self, data): + # NAWS is client -> server *only*. self.protocol will + # therefore be an ITerminalTransport, the `.protocol' + # attribute of which will be an ITerminalProtocol. Maybe. + # You know what, XXX TODO clean this up. + if len(data) == 4: + width, height = struct.unpack('!HH', b''.join(data)) + self.protocol.terminalProtocol.terminalSize(width, height) + else: + log.msg("Wrong number of NAWS bytes") + + linemodeSubcommands = { + LINEMODE_SLC: 'SLC'} + def telnet_LINEMODE(self, data): + linemodeSubcommand = data[0] + if 0: + # XXX TODO: This should be enabled to parse linemode subnegotiation. + getattr(self, 'linemode_' + self.linemodeSubcommands[linemodeSubcommand])(data[1:]) + + + def linemode_SLC(self, data): + chunks = zip(*[iter(data)]*3) + for slcFunction, slcValue, slcWhat in chunks: + # Later, we should parse stuff. + 'SLC', ord(slcFunction), ord(slcValue), ord(slcWhat) + + +from twisted.protocols import basic + +class StatefulTelnetProtocol(basic.LineReceiver, TelnetProtocol): + delimiter = b'\n' + + state = 'Discard' + + def connectionLost(self, reason): + basic.LineReceiver.connectionLost(self, reason) + TelnetProtocol.connectionLost(self, reason) + + + def lineReceived(self, line): + oldState = self.state + newState = getattr(self, "telnet_" + oldState)(line) + if newState is not None: + if self.state == oldState: + self.state = newState + else: + log.msg("Warning: state changed and new state returned") + + + def telnet_Discard(self, line): + pass + + +from twisted.cred import credentials + +class AuthenticatingTelnetProtocol(StatefulTelnetProtocol): + """ + A protocol which prompts for credentials and attempts to authenticate them. + + Username and password prompts are given (the password is obscured). When the + information is collected, it is passed to a portal and an avatar implementing + L{ITelnetProtocol} is requested. If an avatar is returned, it connected to this + protocol's transport, and this protocol's transport is connected to it. + Otherwise, the user is re-prompted for credentials. + """ + + state = "User" + protocol = None + + def __init__(self, portal): + self.portal = portal + + + def connectionMade(self): + self.transport.write(b"Username: ") + + + def connectionLost(self, reason): + StatefulTelnetProtocol.connectionLost(self, reason) + if self.protocol is not None: + try: + self.protocol.connectionLost(reason) + self.logout() + finally: + del self.protocol, self.logout + + + def telnet_User(self, line): + self.username = line + self.transport.will(ECHO) + self.transport.write(b"Password: ") + return 'Password' + + + def telnet_Password(self, line): + username, password = self.username, line + del self.username + def login(ignored): + creds = credentials.UsernamePassword(username, password) + d = self.portal.login(creds, None, ITelnetProtocol) + d.addCallback(self._cbLogin) + d.addErrback(self._ebLogin) + self.transport.wont(ECHO).addCallback(login) + return 'Discard' + + + def _cbLogin(self, ial): + interface, protocol, logout = ial + assert interface is ITelnetProtocol + self.protocol = protocol + self.logout = logout + self.state = 'Command' + + protocol.makeConnection(self.transport) + self.transport.protocol = protocol + + + def _ebLogin(self, failure): + self.transport.write(b"\nAuthentication failed\n") + self.transport.write(b"Username: ") + self.state = "User" + + +__all__ = [ + # Exceptions + 'TelnetError', 'NegotiationError', 'OptionRefused', + 'AlreadyNegotiating', 'AlreadyEnabled', 'AlreadyDisabled', + + # Interfaces + 'ITelnetProtocol', 'ITelnetTransport', + + # Other stuff, protocols, etc. + 'Telnet', 'TelnetProtocol', 'TelnetTransport', + 'TelnetBootstrapProtocol', + + ] diff --git a/contrib/python/Twisted/py2/twisted/conch/ttymodes.py b/contrib/python/Twisted/py2/twisted/conch/ttymodes.py new file mode 100644 index 0000000000..00b4495f3a --- /dev/null +++ b/contrib/python/Twisted/py2/twisted/conch/ttymodes.py @@ -0,0 +1,121 @@ +# Copyright (c) Twisted Matrix Laboratories. +# See LICENSE for details. + +# + +import tty +# this module was autogenerated. + +VINTR = 1 +VQUIT = 2 +VERASE = 3 +VKILL = 4 +VEOF = 5 +VEOL = 6 +VEOL2 = 7 +VSTART = 8 +VSTOP = 9 +VSUSP = 10 +VDSUSP = 11 +VREPRINT = 12 +VWERASE = 13 +VLNEXT = 14 +VFLUSH = 15 +VSWTCH = 16 +VSTATUS = 17 +VDISCARD = 18 +IGNPAR = 30 +PARMRK = 31 +INPCK = 32 +ISTRIP = 33 +INLCR = 34 +IGNCR = 35 +ICRNL = 36 +IUCLC = 37 +IXON = 38 +IXANY = 39 +IXOFF = 40 +IMAXBEL = 41 +ISIG = 50 +ICANON = 51 +XCASE = 52 +ECHO = 53 +ECHOE = 54 +ECHOK = 55 +ECHONL = 56 +NOFLSH = 57 +TOSTOP = 58 +IEXTEN = 59 +ECHOCTL = 60 +ECHOKE = 61 +PENDIN = 62 +OPOST = 70 +OLCUC = 71 +ONLCR = 72 +OCRNL = 73 +ONOCR = 74 +ONLRET = 75 +CS7 = 90 +CS8 = 91 +PARENB = 92 +PARODD = 93 +TTY_OP_ISPEED = 128 +TTY_OP_OSPEED = 129 + +TTYMODES = { + 1 : 'VINTR', + 2 : 'VQUIT', + 3 : 'VERASE', + 4 : 'VKILL', + 5 : 'VEOF', + 6 : 'VEOL', + 7 : 'VEOL2', + 8 : 'VSTART', + 9 : 'VSTOP', + 10 : 'VSUSP', + 11 : 'VDSUSP', + 12 : 'VREPRINT', + 13 : 'VWERASE', + 14 : 'VLNEXT', + 15 : 'VFLUSH', + 16 : 'VSWTCH', + 17 : 'VSTATUS', + 18 : 'VDISCARD', + 30 : (tty.IFLAG, 'IGNPAR'), + 31 : (tty.IFLAG, 'PARMRK'), + 32 : (tty.IFLAG, 'INPCK'), + 33 : (tty.IFLAG, 'ISTRIP'), + 34 : (tty.IFLAG, 'INLCR'), + 35 : (tty.IFLAG, 'IGNCR'), + 36 : (tty.IFLAG, 'ICRNL'), + 37 : (tty.IFLAG, 'IUCLC'), + 38 : (tty.IFLAG, 'IXON'), + 39 : (tty.IFLAG, 'IXANY'), + 40 : (tty.IFLAG, 'IXOFF'), + 41 : (tty.IFLAG, 'IMAXBEL'), + 50 : (tty.LFLAG, 'ISIG'), + 51 : (tty.LFLAG, 'ICANON'), + 52 : (tty.LFLAG, 'XCASE'), + 53 : (tty.LFLAG, 'ECHO'), + 54 : (tty.LFLAG, 'ECHOE'), + 55 : (tty.LFLAG, 'ECHOK'), + 56 : (tty.LFLAG, 'ECHONL'), + 57 : (tty.LFLAG, 'NOFLSH'), + 58 : (tty.LFLAG, 'TOSTOP'), + 59 : (tty.LFLAG, 'IEXTEN'), + 60 : (tty.LFLAG, 'ECHOCTL'), + 61 : (tty.LFLAG, 'ECHOKE'), + 62 : (tty.LFLAG, 'PENDIN'), + 70 : (tty.OFLAG, 'OPOST'), + 71 : (tty.OFLAG, 'OLCUC'), + 72 : (tty.OFLAG, 'ONLCR'), + 73 : (tty.OFLAG, 'OCRNL'), + 74 : (tty.OFLAG, 'ONOCR'), + 75 : (tty.OFLAG, 'ONLRET'), +# 90 : (tty.CFLAG, 'CS7'), +# 91 : (tty.CFLAG, 'CS8'), + 92 : (tty.CFLAG, 'PARENB'), + 93 : (tty.CFLAG, 'PARODD'), + 128 : 'ISPEED', + 129 : 'OSPEED' +} diff --git a/contrib/python/Twisted/py2/twisted/conch/ui/__init__.py b/contrib/python/Twisted/py2/twisted/conch/ui/__init__.py new file mode 100644 index 0000000000..ea0eea8318 --- /dev/null +++ b/contrib/python/Twisted/py2/twisted/conch/ui/__init__.py @@ -0,0 +1,11 @@ +# Copyright (c) Twisted Matrix Laboratories. +# See LICENSE for details. + +# + + +""" +twisted.conch.ui is home to the UI elements for tkconch. + +Maintainer: Paul Swartz +""" diff --git a/contrib/python/Twisted/py2/twisted/conch/ui/ansi.py b/contrib/python/Twisted/py2/twisted/conch/ui/ansi.py new file mode 100644 index 0000000000..b073532642 --- /dev/null +++ b/contrib/python/Twisted/py2/twisted/conch/ui/ansi.py @@ -0,0 +1,240 @@ +# Copyright (c) Twisted Matrix Laboratories. +# See LICENSE for details. + +# +"""Module to parse ANSI escape sequences + +Maintainer: Jean-Paul Calderone +""" + +import string + +# Twisted imports +from twisted.python import log + +class ColorText: + """ + Represents an element of text along with the texts colors and + additional attributes. + """ + + # The colors to use + COLORS = ('b', 'r', 'g', 'y', 'l', 'm', 'c', 'w') + BOLD_COLORS = tuple([x.upper() for x in COLORS]) + BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = range(len(COLORS)) + + # Color names + COLOR_NAMES = ( + 'Black', 'Red', 'Green', 'Yellow', 'Blue', 'Magenta', 'Cyan', 'White' + ) + + def __init__(self, text, fg, bg, display, bold, underline, flash, reverse): + self.text, self.fg, self.bg = text, fg, bg + self.display = display + self.bold = bold + self.underline = underline + self.flash = flash + self.reverse = reverse + if self.reverse: + self.fg, self.bg = self.bg, self.fg + + +class AnsiParser: + """ + Parser class for ANSI codes. + """ + + # Terminators for cursor movement ansi controls - unsupported + CURSOR_SET = ('H', 'f', 'A', 'B', 'C', 'D', 'R', 's', 'u', 'd','G') + + # Terminators for erasure ansi controls - unsupported + ERASE_SET = ('J', 'K', 'P') + + # Terminators for mode change ansi controls - unsupported + MODE_SET = ('h', 'l') + + # Terminators for keyboard assignment ansi controls - unsupported + ASSIGN_SET = ('p',) + + # Terminators for color change ansi controls - supported + COLOR_SET = ('m',) + + SETS = (CURSOR_SET, ERASE_SET, MODE_SET, ASSIGN_SET, COLOR_SET) + + def __init__(self, defaultFG, defaultBG): + self.defaultFG, self.defaultBG = defaultFG, defaultBG + self.currentFG, self.currentBG = self.defaultFG, self.defaultBG + self.bold, self.flash, self.underline, self.reverse = 0, 0, 0, 0 + self.display = 1 + self.prepend = '' + + + def stripEscapes(self, string): + """ + Remove all ANSI color escapes from the given string. + """ + result = '' + show = 1 + i = 0 + L = len(string) + while i < L: + if show == 0 and string[i] in _sets: + show = 1 + elif show: + n = string.find('\x1B', i) + if n == -1: + return result + string[i:] + else: + result = result + string[i:n] + i = n + show = 0 + i = i + 1 + return result + + def writeString(self, colorstr): + pass + + def parseString(self, str): + """ + Turn a string input into a list of L{ColorText} elements. + """ + + if self.prepend: + str = self.prepend + str + self.prepend = '' + parts = str.split('\x1B') + + if len(parts) == 1: + self.writeString(self.formatText(parts[0])) + else: + self.writeString(self.formatText(parts[0])) + for s in parts[1:]: + L = len(s) + i = 0 + type = None + while i < L: + if s[i] not in string.digits+'[;?': + break + i+=1 + if not s: + self.prepend = '\x1b' + return + if s[0]!='[': + self.writeString(self.formatText(s[i+1:])) + continue + else: + s=s[1:] + i-=1 + if i==L-1: + self.prepend = '\x1b[' + return + type = _setmap.get(s[i], None) + if type is None: + continue + + if type == AnsiParser.COLOR_SET: + self.parseColor(s[:i + 1]) + s = s[i + 1:] + self.writeString(self.formatText(s)) + elif type == AnsiParser.CURSOR_SET: + cursor, s = s[:i+1], s[i+1:] + self.parseCursor(cursor) + self.writeString(self.formatText(s)) + elif type == AnsiParser.ERASE_SET: + erase, s = s[:i+1], s[i+1:] + self.parseErase(erase) + self.writeString(self.formatText(s)) + elif type == AnsiParser.MODE_SET: + s = s[i+1:] + #self.parseErase('2J') + self.writeString(self.formatText(s)) + elif i == L: + self.prepend = '\x1B[' + s + else: + log.msg('Unhandled ANSI control type: %c' % (s[i],)) + s = s[i + 1:] + self.writeString(self.formatText(s)) + + def parseColor(self, str): + """ + Handle a single ANSI color sequence + """ + # Drop the trailing 'm' + str = str[:-1] + + if not str: + str = '0' + + try: + parts = map(int, str.split(';')) + except ValueError: + log.msg('Invalid ANSI color sequence (%d): %s' % (len(str), str)) + self.currentFG, self.currentBG = self.defaultFG, self.defaultBG + return + + for x in parts: + if x == 0: + self.currentFG, self.currentBG = self.defaultFG, self.defaultBG + self.bold, self.flash, self.underline, self.reverse = 0, 0, 0, 0 + self.display = 1 + elif x == 1: + self.bold = 1 + elif 30 <= x <= 37: + self.currentFG = x - 30 + elif 40 <= x <= 47: + self.currentBG = x - 40 + elif x == 39: + self.currentFG = self.defaultFG + elif x == 49: + self.currentBG = self.defaultBG + elif x == 4: + self.underline = 1 + elif x == 5: + self.flash = 1 + elif x == 7: + self.reverse = 1 + elif x == 8: + self.display = 0 + elif x == 22: + self.bold = 0 + elif x == 24: + self.underline = 0 + elif x == 25: + self.blink = 0 + elif x == 27: + self.reverse = 0 + elif x == 28: + self.display = 1 + else: + log.msg('Unrecognised ANSI color command: %d' % (x,)) + + def parseCursor(self, cursor): + pass + + def parseErase(self, erase): + pass + + + def pickColor(self, value, mode, BOLD = ColorText.BOLD_COLORS): + if mode: + return ColorText.COLORS[value] + else: + return self.bold and BOLD[value] or ColorText.COLORS[value] + + + def formatText(self, text): + return ColorText( + text, + self.pickColor(self.currentFG, 0), + self.pickColor(self.currentBG, 1), + self.display, self.bold, self.underline, self.flash, self.reverse + ) + + +_sets = ''.join(map(''.join, AnsiParser.SETS)) + +_setmap = {} +for s in AnsiParser.SETS: + for r in s: + _setmap[r] = s +del s diff --git a/contrib/python/Twisted/py2/twisted/conch/ui/tkvt100.py b/contrib/python/Twisted/py2/twisted/conch/ui/tkvt100.py new file mode 100644 index 0000000000..0ce4db2915 --- /dev/null +++ b/contrib/python/Twisted/py2/twisted/conch/ui/tkvt100.py @@ -0,0 +1,202 @@ +# Copyright (c) Twisted Matrix Laboratories. +# See LICENSE for details. + +# + +"""Module to emulate a VT100 terminal in Tkinter. + +Maintainer: Paul Swartz +""" + +try: + import tkinter as Tkinter + import tkinter.font as tkFont +except ImportError: + import Tkinter, tkFont +import string +from . import ansi + +ttyFont = None#tkFont.Font(family = 'Courier', size = 10) +fontWidth, fontHeight = None,None#max(map(ttyFont.measure, string.letters+string.digits)), int(ttyFont.metrics()['linespace']) + +colorKeys = ( + 'b', 'r', 'g', 'y', 'l', 'm', 'c', 'w', + 'B', 'R', 'G', 'Y', 'L', 'M', 'C', 'W' +) + +colorMap = { + 'b': '#000000', 'r': '#c40000', 'g': '#00c400', 'y': '#c4c400', + 'l': '#000080', 'm': '#c400c4', 'c': '#00c4c4', 'w': '#c4c4c4', + 'B': '#626262', 'R': '#ff0000', 'G': '#00ff00', 'Y': '#ffff00', + 'L': '#0000ff', 'M': '#ff00ff', 'C': '#00ffff', 'W': '#ffffff', +} + +class VT100Frame(Tkinter.Frame): + def __init__(self, *args, **kw): + global ttyFont, fontHeight, fontWidth + ttyFont = tkFont.Font(family = 'Courier', size = 10) + fontWidth = max(map(ttyFont.measure, string.ascii_letters+string.digits)) + fontHeight = int(ttyFont.metrics()['linespace']) + self.width = kw.get('width', 80) + self.height = kw.get('height', 25) + self.callback = kw['callback'] + del kw['callback'] + kw['width'] = w = fontWidth * self.width + kw['height'] = h = fontHeight * self.height + Tkinter.Frame.__init__(self, *args, **kw) + self.canvas = Tkinter.Canvas(bg='#000000', width=w, height=h) + self.canvas.pack(side=Tkinter.TOP, fill=Tkinter.BOTH, expand=1) + self.canvas.bind('<Key>', self.keyPressed) + self.canvas.bind('<1>', lambda x: 'break') + self.canvas.bind('<Up>', self.upPressed) + self.canvas.bind('<Down>', self.downPressed) + self.canvas.bind('<Left>', self.leftPressed) + self.canvas.bind('<Right>', self.rightPressed) + self.canvas.focus() + + self.ansiParser = ansi.AnsiParser(ansi.ColorText.WHITE, ansi.ColorText.BLACK) + self.ansiParser.writeString = self.writeString + self.ansiParser.parseCursor = self.parseCursor + self.ansiParser.parseErase = self.parseErase + #for (a, b) in colorMap.items(): + # self.canvas.tag_config(a, foreground=b) + # self.canvas.tag_config('b'+a, background=b) + #self.canvas.tag_config('underline', underline=1) + + self.x = 0 + self.y = 0 + self.cursor = self.canvas.create_rectangle(0,0,fontWidth-1,fontHeight-1,fill='green',outline='green') + + def _delete(self, sx, sy, ex, ey): + csx = sx*fontWidth + 1 + csy = sy*fontHeight + 1 + cex = ex*fontWidth + 3 + cey = ey*fontHeight + 3 + items = self.canvas.find_overlapping(csx,csy, cex,cey) + for item in items: + self.canvas.delete(item) + + def _write(self, ch, fg, bg): + if self.x == self.width: + self.x = 0 + self.y+=1 + if self.y == self.height: + [self.canvas.move(x,0,-fontHeight) for x in self.canvas.find_all()] + self.y-=1 + canvasX = self.x*fontWidth + 1 + canvasY = self.y*fontHeight + 1 + items = self.canvas.find_overlapping(canvasX, canvasY, canvasX+2, canvasY+2) + if items: + [self.canvas.delete(item) for item in items] + if bg: + self.canvas.create_rectangle(canvasX, canvasY, canvasX+fontWidth-1, canvasY+fontHeight-1, fill=bg, outline=bg) + self.canvas.create_text(canvasX, canvasY, anchor=Tkinter.NW, font=ttyFont, text=ch, fill=fg) + self.x+=1 + + def write(self, data): + #print self.x,self.y,repr(data) + #if len(data)>5: raw_input() + self.ansiParser.parseString(data) + self.canvas.delete(self.cursor) + canvasX = self.x*fontWidth + 1 + canvasY = self.y*fontHeight + 1 + self.cursor = self.canvas.create_rectangle(canvasX,canvasY,canvasX+fontWidth-1,canvasY+fontHeight-1, fill='green', outline='green') + self.canvas.lower(self.cursor) + + def writeString(self, i): + if not i.display: + return + fg = colorMap[i.fg] + bg = i.bg != 'b' and colorMap[i.bg] + for ch in i.text: + b = ord(ch) + if b == 7: # bell + self.bell() + elif b == 8: # BS + if self.x: + self.x-=1 + elif b == 9: # TAB + [self._write(' ',fg,bg) for index in range(8)] + elif b == 10: + if self.y == self.height-1: + self._delete(0,0,self.width,0) + [self.canvas.move(x,0,-fontHeight) for x in self.canvas.find_all()] + else: + self.y+=1 + elif b == 13: + self.x = 0 + elif 32 <= b < 127: + self._write(ch, fg, bg) + + def parseErase(self, erase): + if ';' in erase: + end = erase[-1] + parts = erase[:-1].split(';') + [self.parseErase(x+end) for x in parts] + return + start = 0 + x,y = self.x, self.y + if len(erase) > 1: + start = int(erase[:-1]) + if erase[-1] == 'J': + if start == 0: + self._delete(x,y,self.width,self.height) + else: + self._delete(0,0,self.width,self.height) + self.x = 0 + self.y = 0 + elif erase[-1] == 'K': + if start == 0: + self._delete(x,y,self.width,y) + elif start == 1: + self._delete(0,y,x,y) + self.x = 0 + else: + self._delete(0,y,self.width,y) + self.x = 0 + elif erase[-1] == 'P': + self._delete(x,y,x+start,y) + + def parseCursor(self, cursor): + #if ';' in cursor and cursor[-1]!='H': + # end = cursor[-1] + # parts = cursor[:-1].split(';') + # [self.parseCursor(x+end) for x in parts] + # return + start = 1 + if len(cursor) > 1 and cursor[-1]!='H': + start = int(cursor[:-1]) + if cursor[-1] == 'C': + self.x+=start + elif cursor[-1] == 'D': + self.x-=start + elif cursor[-1]=='d': + self.y=start-1 + elif cursor[-1]=='G': + self.x=start-1 + elif cursor[-1]=='H': + if len(cursor)>1: + y,x = map(int, cursor[:-1].split(';')) + y-=1 + x-=1 + else: + x,y=0,0 + self.x = x + self.y = y + + def keyPressed(self, event): + if self.callback and event.char: + self.callback(event.char) + return 'break' + + def upPressed(self, event): + self.callback('\x1bOA') + + def downPressed(self, event): + self.callback('\x1bOB') + + def rightPressed(self, event): + self.callback('\x1bOC') + + def leftPressed(self, event): + self.callback('\x1bOD') diff --git a/contrib/python/Twisted/py2/twisted/conch/unix.py b/contrib/python/Twisted/py2/twisted/conch/unix.py new file mode 100644 index 0000000000..d9c3f05c89 --- /dev/null +++ b/contrib/python/Twisted/py2/twisted/conch/unix.py @@ -0,0 +1,535 @@ +# Copyright (c) Twisted Matrix Laboratories. +# See LICENSE for details. + +""" +A UNIX SSH server. +""" + +import fcntl +import grp +import os +import pty +import pwd +import socket +import struct +import time +import tty + +from zope.interface import implementer + +from twisted.conch import ttymodes +from twisted.conch.avatar import ConchUser +from twisted.conch.error import ConchError +from twisted.conch.ls import lsLine +from twisted.conch.ssh import session, forwarding, filetransfer +from twisted.conch.ssh.filetransfer import ( + FXF_READ, FXF_WRITE, FXF_APPEND, FXF_CREAT, FXF_TRUNC, FXF_EXCL +) +from twisted.conch.interfaces import ISession, ISFTPServer, ISFTPFile +from twisted.cred import portal +from twisted.internet.error import ProcessExitedAlready +from twisted.python import components, log +from twisted.python.compat import _bytesChr as chr, nativeString + +try: + import utmp +except ImportError: + utmp = None + + + +@implementer(portal.IRealm) +class UnixSSHRealm: + def requestAvatar(self, username, mind, *interfaces): + user = UnixConchUser(username) + return interfaces[0], user, user.logout + + + +class UnixConchUser(ConchUser): + + def __init__(self, username): + ConchUser.__init__(self) + self.username = username + self.pwdData = pwd.getpwnam(self.username) + l = [self.pwdData[3]] + for groupname, password, gid, userlist in grp.getgrall(): + if username in userlist: + l.append(gid) + self.otherGroups = l + self.listeners = {} # Dict mapping (interface, port) -> listener + self.channelLookup.update( + {b"session": session.SSHSession, + b"direct-tcpip": forwarding.openConnectForwardingClient}) + + self.subsystemLookup.update( + {b"sftp": filetransfer.FileTransferServer}) + + + def getUserGroupId(self): + return self.pwdData[2:4] + + + def getOtherGroups(self): + return self.otherGroups + + + def getHomeDir(self): + return self.pwdData[5] + + + def getShell(self): + return self.pwdData[6] + + + def global_tcpip_forward(self, data): + hostToBind, portToBind = forwarding.unpackGlobal_tcpip_forward(data) + from twisted.internet import reactor + try: + listener = self._runAsUser( + reactor.listenTCP, portToBind, + forwarding.SSHListenForwardingFactory( + self.conn, + (hostToBind, portToBind), + forwarding.SSHListenServerForwardingChannel), + interface=hostToBind) + except: + return 0 + else: + self.listeners[(hostToBind, portToBind)] = listener + if portToBind == 0: + portToBind = listener.getHost()[2] # The port + return 1, struct.pack('>L', portToBind) + else: + return 1 + + + def global_cancel_tcpip_forward(self, data): + hostToBind, portToBind = forwarding.unpackGlobal_tcpip_forward(data) + listener = self.listeners.get((hostToBind, portToBind), None) + if not listener: + return 0 + del self.listeners[(hostToBind, portToBind)] + self._runAsUser(listener.stopListening) + return 1 + + + def logout(self): + # Remove all listeners. + for listener in self.listeners.values(): + self._runAsUser(listener.stopListening) + log.msg( + 'avatar %s logging out (%i)' + % (self.username, len(self.listeners))) + + + def _runAsUser(self, f, *args, **kw): + euid = os.geteuid() + egid = os.getegid() + groups = os.getgroups() + uid, gid = self.getUserGroupId() + os.setegid(0) + os.seteuid(0) + os.setgroups(self.getOtherGroups()) + os.setegid(gid) + os.seteuid(uid) + try: + f = iter(f) + except TypeError: + f = [(f, args, kw)] + try: + for i in f: + func = i[0] + args = len(i) > 1 and i[1] or () + kw = len(i) > 2 and i[2] or {} + r = func(*args, **kw) + finally: + os.setegid(0) + os.seteuid(0) + os.setgroups(groups) + os.setegid(egid) + os.seteuid(euid) + return r + + + +@implementer(ISession) +class SSHSessionForUnixConchUser: + def __init__(self, avatar, reactor=None): + """ + Construct an C{SSHSessionForUnixConchUser}. + + @param avatar: The L{UnixConchUser} for whom this is an SSH session. + @param reactor: An L{IReactorProcess} used to handle shell and exec + requests. Uses the default reactor if None. + """ + if reactor is None: + from twisted.internet import reactor + self._reactor = reactor + self.avatar = avatar + self.environ = {'PATH': '/bin:/usr/bin:/usr/local/bin'} + self.pty = None + self.ptyTuple = 0 + + + def addUTMPEntry(self, loggedIn=1): + if not utmp: + return + ipAddress = self.avatar.conn.transport.transport.getPeer().host + packedIp, = struct.unpack('L', socket.inet_aton(ipAddress)) + ttyName = self.ptyTuple[2][5:] + t = time.time() + t1 = int(t) + t2 = int((t-t1) * 1e6) + entry = utmp.UtmpEntry() + entry.ut_type = loggedIn and utmp.USER_PROCESS or utmp.DEAD_PROCESS + entry.ut_pid = self.pty.pid + entry.ut_line = ttyName + entry.ut_id = ttyName[-4:] + entry.ut_tv = (t1, t2) + if loggedIn: + entry.ut_user = self.avatar.username + entry.ut_host = socket.gethostbyaddr(ipAddress)[0] + entry.ut_addr_v6 = (packedIp, 0, 0, 0) + a = utmp.UtmpRecord(utmp.UTMP_FILE) + a.pututline(entry) + a.endutent() + b = utmp.UtmpRecord(utmp.WTMP_FILE) + b.pututline(entry) + b.endutent() + + + def getPty(self, term, windowSize, modes): + self.environ['TERM'] = term + self.winSize = windowSize + self.modes = modes + master, slave = pty.openpty() + ttyname = os.ttyname(slave) + self.environ['SSH_TTY'] = ttyname + self.ptyTuple = (master, slave, ttyname) + + + def openShell(self, proto): + if not self.ptyTuple: # We didn't get a pty-req. + log.msg('tried to get shell without pty, failing') + raise ConchError("no pty") + uid, gid = self.avatar.getUserGroupId() + homeDir = self.avatar.getHomeDir() + shell = self.avatar.getShell() + self.environ['USER'] = self.avatar.username + self.environ['HOME'] = homeDir + self.environ['SHELL'] = shell + shellExec = os.path.basename(shell) + peer = self.avatar.conn.transport.transport.getPeer() + host = self.avatar.conn.transport.transport.getHost() + self.environ['SSH_CLIENT'] = '%s %s %s' % ( + peer.host, peer.port, host.port) + self.getPtyOwnership() + self.pty = self._reactor.spawnProcess( + proto, shell, ['-%s' % (shellExec,)], self.environ, homeDir, uid, + gid, usePTY=self.ptyTuple) + self.addUTMPEntry() + fcntl.ioctl(self.pty.fileno(), tty.TIOCSWINSZ, + struct.pack('4H', *self.winSize)) + if self.modes: + self.setModes() + self.oldWrite = proto.transport.write + proto.transport.write = self._writeHack + self.avatar.conn.transport.transport.setTcpNoDelay(1) + + + def execCommand(self, proto, cmd): + uid, gid = self.avatar.getUserGroupId() + homeDir = self.avatar.getHomeDir() + shell = self.avatar.getShell() or '/bin/sh' + self.environ['HOME'] = homeDir + command = (shell, '-c', cmd) + peer = self.avatar.conn.transport.transport.getPeer() + host = self.avatar.conn.transport.transport.getHost() + self.environ['SSH_CLIENT'] = '%s %s %s' % ( + peer.host, peer.port, host.port) + if self.ptyTuple: + self.getPtyOwnership() + self.pty = self._reactor.spawnProcess( + proto, shell, command, self.environ, homeDir, uid, gid, + usePTY=self.ptyTuple or 0) + if self.ptyTuple: + self.addUTMPEntry() + if self.modes: + self.setModes() + self.avatar.conn.transport.transport.setTcpNoDelay(1) + + + def getPtyOwnership(self): + ttyGid = os.stat(self.ptyTuple[2])[5] + uid, gid = self.avatar.getUserGroupId() + euid, egid = os.geteuid(), os.getegid() + os.setegid(0) + os.seteuid(0) + try: + os.chown(self.ptyTuple[2], uid, ttyGid) + finally: + os.setegid(egid) + os.seteuid(euid) + + + def setModes(self): + pty = self.pty + attr = tty.tcgetattr(pty.fileno()) + for mode, modeValue in self.modes: + if mode not in ttymodes.TTYMODES: + continue + ttyMode = ttymodes.TTYMODES[mode] + if len(ttyMode) == 2: # Flag. + flag, ttyAttr = ttyMode + if not hasattr(tty, ttyAttr): + continue + ttyval = getattr(tty, ttyAttr) + if modeValue: + attr[flag] = attr[flag] | ttyval + else: + attr[flag] = attr[flag] & ~ttyval + elif ttyMode == 'OSPEED': + attr[tty.OSPEED] = getattr(tty, 'B%s' % (modeValue,)) + elif ttyMode == 'ISPEED': + attr[tty.ISPEED] = getattr(tty, 'B%s' % (modeValue,)) + else: + if not hasattr(tty, ttyMode): + continue + ttyval = getattr(tty, ttyMode) + attr[tty.CC][ttyval] = chr(modeValue) + tty.tcsetattr(pty.fileno(), tty.TCSANOW, attr) + + + def eofReceived(self): + if self.pty: + self.pty.closeStdin() + + + def closed(self): + if self.ptyTuple and os.path.exists(self.ptyTuple[2]): + ttyGID = os.stat(self.ptyTuple[2])[5] + os.chown(self.ptyTuple[2], 0, ttyGID) + if self.pty: + try: + self.pty.signalProcess('HUP') + except (OSError, ProcessExitedAlready): + pass + self.pty.loseConnection() + self.addUTMPEntry(0) + log.msg('shell closed') + + + def windowChanged(self, winSize): + self.winSize = winSize + fcntl.ioctl( + self.pty.fileno(), tty.TIOCSWINSZ, + struct.pack('4H', *self.winSize)) + + + def _writeHack(self, data): + """ + Hack to send ignore messages when we aren't echoing. + """ + if self.pty is not None: + attr = tty.tcgetattr(self.pty.fileno())[3] + if not attr & tty.ECHO and attr & tty.ICANON: # No echo. + self.avatar.conn.transport.sendIgnore('\x00'*(8+len(data))) + self.oldWrite(data) + + + +@implementer(ISFTPServer) +class SFTPServerForUnixConchUser: + def __init__(self, avatar): + self.avatar = avatar + + + def _setAttrs(self, path, attrs): + """ + NOTE: this function assumes it runs as the logged-in user: + i.e. under _runAsUser() + """ + if "uid" in attrs and "gid" in attrs: + os.chown(path, attrs["uid"], attrs["gid"]) + if "permissions" in attrs: + os.chmod(path, attrs["permissions"]) + if "atime" in attrs and "mtime" in attrs: + os.utime(path, (attrs["atime"], attrs["mtime"])) + + + def _getAttrs(self, s): + return { + "size": s.st_size, + "uid": s.st_uid, + "gid": s.st_gid, + "permissions": s.st_mode, + "atime": int(s.st_atime), + "mtime": int(s.st_mtime) + } + + + def _absPath(self, path): + home = self.avatar.getHomeDir() + return os.path.join(nativeString(home.path), nativeString(path)) + + + def gotVersion(self, otherVersion, extData): + return {} + + + def openFile(self, filename, flags, attrs): + return UnixSFTPFile(self, self._absPath(filename), flags, attrs) + + + def removeFile(self, filename): + filename = self._absPath(filename) + return self.avatar._runAsUser(os.remove, filename) + + + def renameFile(self, oldpath, newpath): + oldpath = self._absPath(oldpath) + newpath = self._absPath(newpath) + return self.avatar._runAsUser(os.rename, oldpath, newpath) + + + def makeDirectory(self, path, attrs): + path = self._absPath(path) + return self.avatar._runAsUser( + [(os.mkdir, (path,)), (self._setAttrs, (path, attrs))]) + + + def removeDirectory(self, path): + path = self._absPath(path) + self.avatar._runAsUser(os.rmdir, path) + + + def openDirectory(self, path): + return UnixSFTPDirectory(self, self._absPath(path)) + + + def getAttrs(self, path, followLinks): + path = self._absPath(path) + if followLinks: + s = self.avatar._runAsUser(os.stat, path) + else: + s = self.avatar._runAsUser(os.lstat, path) + return self._getAttrs(s) + + + def setAttrs(self, path, attrs): + path = self._absPath(path) + self.avatar._runAsUser(self._setAttrs, path, attrs) + + + def readLink(self, path): + path = self._absPath(path) + return self.avatar._runAsUser(os.readlink, path) + + + def makeLink(self, linkPath, targetPath): + linkPath = self._absPath(linkPath) + targetPath = self._absPath(targetPath) + return self.avatar._runAsUser(os.symlink, targetPath, linkPath) + + + def realPath(self, path): + return os.path.realpath(self._absPath(path)) + + + def extendedRequest(self, extName, extData): + raise NotImplementedError + + + +@implementer(ISFTPFile) +class UnixSFTPFile: + def __init__(self, server, filename, flags, attrs): + self.server = server + openFlags = 0 + if flags & FXF_READ == FXF_READ and flags & FXF_WRITE == 0: + openFlags = os.O_RDONLY + if flags & FXF_WRITE == FXF_WRITE and flags & FXF_READ == 0: + openFlags = os.O_WRONLY + if flags & FXF_WRITE == FXF_WRITE and flags & FXF_READ == FXF_READ: + openFlags = os.O_RDWR + if flags & FXF_APPEND == FXF_APPEND: + openFlags |= os.O_APPEND + if flags & FXF_CREAT == FXF_CREAT: + openFlags |= os.O_CREAT + if flags & FXF_TRUNC == FXF_TRUNC: + openFlags |= os.O_TRUNC + if flags & FXF_EXCL == FXF_EXCL: + openFlags |= os.O_EXCL + if "permissions" in attrs: + mode = attrs["permissions"] + del attrs["permissions"] + else: + mode = 0o777 + fd = server.avatar._runAsUser(os.open, filename, openFlags, mode) + if attrs: + server.avatar._runAsUser(server._setAttrs, filename, attrs) + self.fd = fd + + + def close(self): + return self.server.avatar._runAsUser(os.close, self.fd) + + + def readChunk(self, offset, length): + return self.server.avatar._runAsUser( + [(os.lseek, (self.fd, offset, 0)), + (os.read, (self.fd, length))]) + + + def writeChunk(self, offset, data): + return self.server.avatar._runAsUser( + [(os.lseek, (self.fd, offset, 0)), + (os.write, (self.fd, data))]) + + + def getAttrs(self): + s = self.server.avatar._runAsUser(os.fstat, self.fd) + return self.server._getAttrs(s) + + + def setAttrs(self, attrs): + raise NotImplementedError + + + +class UnixSFTPDirectory: + + def __init__(self, server, directory): + self.server = server + self.files = server.avatar._runAsUser(os.listdir, directory) + self.dir = directory + + + def __iter__(self): + return self + + + def __next__(self): + try: + f = self.files.pop(0) + except IndexError: + raise StopIteration + else: + s = self.server.avatar._runAsUser( + os.lstat, os.path.join(self.dir, f)) + longname = lsLine(f, s) + attrs = self.server._getAttrs(s) + return (f, longname, attrs) + + next = __next__ + + def close(self): + self.files = [] + + + +components.registerAdapter( + SFTPServerForUnixConchUser, UnixConchUser, filetransfer.ISFTPServer) +components.registerAdapter( + SSHSessionForUnixConchUser, UnixConchUser, session.ISession) |