Initial Commit
This commit is contained in:
@@ -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
|
||||
"""
|
||||
@@ -0,0 +1,293 @@
|
||||
# -*- test-case-name: twisted.conch.test.test_transport -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
SSH key exchange handling.
|
||||
"""
|
||||
|
||||
|
||||
from hashlib import sha1, sha256, sha384, sha512
|
||||
|
||||
from zope.interface import Attribute, Interface, implementer
|
||||
|
||||
from twisted.conch import error
|
||||
|
||||
|
||||
class _IKexAlgorithm(Interface):
|
||||
"""
|
||||
An L{_IKexAlgorithm} describes a key exchange algorithm.
|
||||
"""
|
||||
|
||||
preference = Attribute(
|
||||
"An L{int} giving the preference of the algorithm when negotiating "
|
||||
"key exchange. Algorithms with lower precedence values are more "
|
||||
"preferred."
|
||||
)
|
||||
|
||||
hashProcessor = Attribute(
|
||||
"A callable hash algorithm constructor (e.g. C{hashlib.sha256}) "
|
||||
"suitable for use with this key exchange algorithm."
|
||||
)
|
||||
|
||||
|
||||
class _IFixedGroupKexAlgorithm(_IKexAlgorithm):
|
||||
"""
|
||||
An L{_IFixedGroupKexAlgorithm} describes a key exchange algorithm with a
|
||||
fixed prime / generator group.
|
||||
"""
|
||||
|
||||
prime = Attribute(
|
||||
"An L{int} giving the prime number used in Diffie-Hellman key "
|
||||
"exchange, or L{None} if not applicable."
|
||||
)
|
||||
|
||||
generator = Attribute(
|
||||
"An L{int} giving the generator number used in Diffie-Hellman key "
|
||||
"exchange, or L{None} if not applicable. (This is not related to "
|
||||
"Python generator functions.)"
|
||||
)
|
||||
|
||||
|
||||
class _IEllipticCurveExchangeKexAlgorithm(_IKexAlgorithm):
|
||||
"""
|
||||
An L{_IEllipticCurveExchangeKexAlgorithm} describes a key exchange algorithm
|
||||
that uses an elliptic curve exchange between the client and server.
|
||||
"""
|
||||
|
||||
|
||||
class _IGroupExchangeKexAlgorithm(_IKexAlgorithm):
|
||||
"""
|
||||
An L{_IGroupExchangeKexAlgorithm} describes a key exchange algorithm
|
||||
that uses group exchange between the client and server.
|
||||
|
||||
A prime / generator group should be chosen at run time based on the
|
||||
requested size. See RFC 4419.
|
||||
"""
|
||||
|
||||
|
||||
@implementer(_IEllipticCurveExchangeKexAlgorithm)
|
||||
class _Curve25519SHA256:
|
||||
"""
|
||||
Elliptic Curve Key Exchange using Curve25519 and SHA256. Defined in
|
||||
U{https://datatracker.ietf.org/doc/draft-ietf-curdle-ssh-curves/}.
|
||||
"""
|
||||
|
||||
preference = 1
|
||||
hashProcessor = sha256
|
||||
|
||||
|
||||
@implementer(_IEllipticCurveExchangeKexAlgorithm)
|
||||
class _Curve25519SHA256LibSSH:
|
||||
"""
|
||||
As L{_Curve25519SHA256}, but with a pre-standardized algorithm name.
|
||||
"""
|
||||
|
||||
preference = 2
|
||||
hashProcessor = sha256
|
||||
|
||||
|
||||
@implementer(_IEllipticCurveExchangeKexAlgorithm)
|
||||
class _ECDH256:
|
||||
"""
|
||||
Elliptic Curve Key Exchange with SHA-256 as HASH. Defined in
|
||||
RFC 5656.
|
||||
|
||||
Note that C{ecdh-sha2-nistp256} takes priority over nistp384 or nistp512.
|
||||
This is the same priority from OpenSSH.
|
||||
|
||||
C{ecdh-sha2-nistp256} is considered preety good cryptography.
|
||||
If you need something better consider using C{curve25519-sha256}.
|
||||
"""
|
||||
|
||||
preference = 3
|
||||
hashProcessor = sha256
|
||||
|
||||
|
||||
@implementer(_IEllipticCurveExchangeKexAlgorithm)
|
||||
class _ECDH384:
|
||||
"""
|
||||
Elliptic Curve Key Exchange with SHA-384 as HASH. Defined in
|
||||
RFC 5656.
|
||||
"""
|
||||
|
||||
preference = 4
|
||||
hashProcessor = sha384
|
||||
|
||||
|
||||
@implementer(_IEllipticCurveExchangeKexAlgorithm)
|
||||
class _ECDH512:
|
||||
"""
|
||||
Elliptic Curve Key Exchange with SHA-512 as HASH. Defined in
|
||||
RFC 5656.
|
||||
"""
|
||||
|
||||
preference = 5
|
||||
hashProcessor = sha512
|
||||
|
||||
|
||||
@implementer(_IGroupExchangeKexAlgorithm)
|
||||
class _DHGroupExchangeSHA256:
|
||||
"""
|
||||
Diffie-Hellman Group and Key Exchange with SHA-256 as HASH. Defined in
|
||||
RFC 4419, 4.2.
|
||||
"""
|
||||
|
||||
preference = 6
|
||||
hashProcessor = sha256
|
||||
|
||||
|
||||
@implementer(_IGroupExchangeKexAlgorithm)
|
||||
class _DHGroupExchangeSHA1:
|
||||
"""
|
||||
Diffie-Hellman Group and Key Exchange with SHA-1 as HASH. Defined in
|
||||
RFC 4419, 4.1.
|
||||
"""
|
||||
|
||||
preference = 7
|
||||
hashProcessor = sha1
|
||||
|
||||
|
||||
@implementer(_IFixedGroupKexAlgorithm)
|
||||
class _DHGroup14SHA1:
|
||||
"""
|
||||
Diffie-Hellman key exchange with SHA-1 as HASH and Oakley Group 14
|
||||
(2048-bit MODP Group). Defined in RFC 4253, 8.2.
|
||||
"""
|
||||
|
||||
preference = 8
|
||||
hashProcessor = sha1
|
||||
# Diffie-Hellman primes from Oakley Group 14 (RFC 3526, 3).
|
||||
prime = int(
|
||||
"323170060713110073003389139264238282488179412411402391128420"
|
||||
"097514007417066343542226196894173635693471179017379097041917"
|
||||
"546058732091950288537589861856221532121754125149017745202702"
|
||||
"357960782362488842461894775876411059286460994117232454266225"
|
||||
"221932305409190376805242355191256797158701170010580558776510"
|
||||
"388618472802579760549035697325615261670813393617995413364765"
|
||||
"591603683178967290731783845896806396719009772021941686472258"
|
||||
"710314113364293195361934716365332097170774482279885885653692"
|
||||
"086452966360772502689555059283627511211740969729980684105543"
|
||||
"595848665832916421362182310789909994486524682624169720359118"
|
||||
"52507045361090559"
|
||||
)
|
||||
generator = 2
|
||||
|
||||
|
||||
# Which ECDH hash function to use is dependent on the size.
|
||||
_kexAlgorithms = {
|
||||
b"curve25519-sha256": _Curve25519SHA256(),
|
||||
b"curve25519-sha256@libssh.org": _Curve25519SHA256LibSSH(),
|
||||
b"diffie-hellman-group-exchange-sha256": _DHGroupExchangeSHA256(),
|
||||
b"diffie-hellman-group-exchange-sha1": _DHGroupExchangeSHA1(),
|
||||
b"diffie-hellman-group14-sha1": _DHGroup14SHA1(),
|
||||
b"ecdh-sha2-nistp256": _ECDH256(),
|
||||
b"ecdh-sha2-nistp384": _ECDH384(),
|
||||
b"ecdh-sha2-nistp521": _ECDH512(),
|
||||
}
|
||||
|
||||
|
||||
def getKex(kexAlgorithm):
|
||||
"""
|
||||
Get a description of a named key exchange algorithm.
|
||||
|
||||
@param kexAlgorithm: The key exchange algorithm name.
|
||||
@type kexAlgorithm: L{bytes}
|
||||
|
||||
@return: A description of the key exchange algorithm named by
|
||||
C{kexAlgorithm}.
|
||||
@rtype: L{_IKexAlgorithm}
|
||||
|
||||
@raises ConchError: if the key exchange algorithm is not found.
|
||||
"""
|
||||
if kexAlgorithm not in _kexAlgorithms:
|
||||
raise error.ConchError(f"Unsupported key exchange algorithm: {kexAlgorithm}")
|
||||
return _kexAlgorithms[kexAlgorithm]
|
||||
|
||||
|
||||
def isEllipticCurve(kexAlgorithm):
|
||||
"""
|
||||
Returns C{True} if C{kexAlgorithm} is an elliptic curve.
|
||||
|
||||
@param kexAlgorithm: The key exchange algorithm name.
|
||||
@type kexAlgorithm: C{str}
|
||||
|
||||
@return: C{True} if C{kexAlgorithm} is an elliptic curve,
|
||||
otherwise C{False}.
|
||||
@rtype: C{bool}
|
||||
"""
|
||||
return _IEllipticCurveExchangeKexAlgorithm.providedBy(getKex(kexAlgorithm))
|
||||
|
||||
|
||||
def isFixedGroup(kexAlgorithm):
|
||||
"""
|
||||
Returns C{True} if C{kexAlgorithm} has a fixed prime / generator group.
|
||||
|
||||
@param kexAlgorithm: The key exchange algorithm name.
|
||||
@type kexAlgorithm: L{bytes}
|
||||
|
||||
@return: C{True} if C{kexAlgorithm} has a fixed prime / generator group,
|
||||
otherwise C{False}.
|
||||
@rtype: L{bool}
|
||||
"""
|
||||
return _IFixedGroupKexAlgorithm.providedBy(getKex(kexAlgorithm))
|
||||
|
||||
|
||||
def getHashProcessor(kexAlgorithm):
|
||||
"""
|
||||
Get the hash algorithm callable to use in key exchange.
|
||||
|
||||
@param kexAlgorithm: The key exchange algorithm name.
|
||||
@type kexAlgorithm: L{bytes}
|
||||
|
||||
@return: A callable hash algorithm constructor (e.g. C{hashlib.sha256}).
|
||||
@rtype: C{callable}
|
||||
"""
|
||||
kex = getKex(kexAlgorithm)
|
||||
return kex.hashProcessor
|
||||
|
||||
|
||||
def getDHGeneratorAndPrime(kexAlgorithm):
|
||||
"""
|
||||
Get the generator and the prime to use in key exchange.
|
||||
|
||||
@param kexAlgorithm: The key exchange algorithm name.
|
||||
@type kexAlgorithm: L{bytes}
|
||||
|
||||
@return: A L{tuple} containing L{int} generator and L{int} prime.
|
||||
@rtype: L{tuple}
|
||||
"""
|
||||
kex = getKex(kexAlgorithm)
|
||||
return kex.generator, kex.prime
|
||||
|
||||
|
||||
def getSupportedKeyExchanges():
|
||||
"""
|
||||
Get a list of supported key exchange algorithm names in order of
|
||||
preference.
|
||||
|
||||
@return: A C{list} of supported key exchange algorithm names.
|
||||
@rtype: C{list} of L{bytes}
|
||||
"""
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives.asymmetric import ec
|
||||
|
||||
from twisted.conch.ssh.keys import _curveTable
|
||||
|
||||
backend = default_backend()
|
||||
kexAlgorithms = _kexAlgorithms.copy()
|
||||
for keyAlgorithm in list(kexAlgorithms):
|
||||
if keyAlgorithm.startswith(b"ecdh"):
|
||||
keyAlgorithmDsa = keyAlgorithm.replace(b"ecdh", b"ecdsa")
|
||||
supported = backend.elliptic_curve_exchange_algorithm_supported(
|
||||
ec.ECDH(), _curveTable[keyAlgorithmDsa]
|
||||
)
|
||||
elif keyAlgorithm.startswith(b"curve25519-sha256"):
|
||||
supported = backend.x25519_supported()
|
||||
else:
|
||||
supported = True
|
||||
if not supported:
|
||||
kexAlgorithms.pop(keyAlgorithm)
|
||||
return sorted(
|
||||
kexAlgorithms, key=lambda kexAlgorithm: kexAlgorithms[kexAlgorithm].preference
|
||||
)
|
||||
@@ -0,0 +1,104 @@
|
||||
# -*- test-case-name: twisted.conch.test.test_keys -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Optional PyNaCl fallback code for Ed25519 keys.
|
||||
"""
|
||||
|
||||
from cryptography.exceptions import InvalidSignature
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
from cryptography.hazmat.primitives.asymmetric import ed25519
|
||||
from nacl.exceptions import BadSignatureError
|
||||
from nacl.signing import SigningKey, VerifyKey
|
||||
|
||||
|
||||
class Ed25519PublicKey(ed25519.Ed25519PublicKey):
|
||||
def __init__(self, data: bytes):
|
||||
self._key = VerifyKey(data)
|
||||
|
||||
def __bytes__(self) -> bytes:
|
||||
return bytes(self._key)
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash(bytes(self))
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
if not isinstance(other, self.__class__):
|
||||
return False
|
||||
return self._key == other._key
|
||||
|
||||
def __ne__(self, other: object) -> bool:
|
||||
return not (self == other)
|
||||
|
||||
@classmethod
|
||||
def from_public_bytes(cls, data: bytes) -> ed25519.Ed25519PublicKey:
|
||||
return cls(data)
|
||||
|
||||
def public_bytes(
|
||||
self,
|
||||
encoding: serialization.Encoding,
|
||||
format: serialization.PublicFormat,
|
||||
) -> bytes:
|
||||
if (
|
||||
encoding is not serialization.Encoding.Raw
|
||||
or format is not serialization.PublicFormat.Raw
|
||||
):
|
||||
raise ValueError("Both encoding and format must be Raw")
|
||||
return bytes(self)
|
||||
|
||||
def verify(self, signature: bytes, data: bytes) -> None:
|
||||
try:
|
||||
self._key.verify(data, signature)
|
||||
except BadSignatureError as e:
|
||||
raise InvalidSignature(str(e))
|
||||
|
||||
|
||||
class Ed25519PrivateKey(ed25519.Ed25519PrivateKey):
|
||||
def __init__(self, data: bytes):
|
||||
self._key = SigningKey(data)
|
||||
|
||||
def __bytes__(self) -> bytes:
|
||||
return bytes(self._key)
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash(bytes(self))
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
if not isinstance(other, self.__class__):
|
||||
return False
|
||||
return self._key == other._key
|
||||
|
||||
def __ne__(self, other: object) -> bool:
|
||||
return not (self == other)
|
||||
|
||||
@classmethod
|
||||
def generate(cls) -> ed25519.Ed25519PrivateKey:
|
||||
return cls(bytes(SigningKey.generate()))
|
||||
|
||||
@classmethod
|
||||
def from_private_bytes(cls, data: bytes) -> ed25519.Ed25519PrivateKey:
|
||||
return cls(data)
|
||||
|
||||
def public_key(self) -> ed25519.Ed25519PublicKey:
|
||||
return Ed25519PublicKey(bytes(self._key.verify_key))
|
||||
|
||||
def private_bytes(
|
||||
self,
|
||||
encoding: serialization.Encoding,
|
||||
format: serialization.PrivateFormat,
|
||||
encryption_algorithm: serialization.KeySerializationEncryption,
|
||||
) -> bytes:
|
||||
if (
|
||||
encoding is not serialization.Encoding.Raw
|
||||
or format is not serialization.PrivateFormat.Raw
|
||||
or not isinstance(encryption_algorithm, serialization.NoEncryption)
|
||||
):
|
||||
raise ValueError(
|
||||
"Encoding and format must be Raw and "
|
||||
"encryption_algorithm must be NoEncryption"
|
||||
)
|
||||
return bytes(self)
|
||||
|
||||
def sign(self, data: bytes) -> bytes:
|
||||
return self._key.sign(data).signature
|
||||
@@ -0,0 +1,43 @@
|
||||
# -*- test-case-name: twisted.conch.test.test_address -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Address object for SSH network connections.
|
||||
|
||||
Maintainer: Paul Swartz
|
||||
|
||||
@since: 12.1
|
||||
"""
|
||||
|
||||
|
||||
from zope.interface import implementer
|
||||
|
||||
from twisted.internet.interfaces import IAddress
|
||||
from twisted.python import util
|
||||
|
||||
|
||||
@implementer(IAddress)
|
||||
class SSHTransportAddress(util.FancyEqMixin):
|
||||
"""
|
||||
Object representing an SSH Transport endpoint.
|
||||
|
||||
This is used to ensure that any code inspecting this address and
|
||||
attempting to construct a similar connection based upon it is not
|
||||
mislead into creating a transport which is not similar to the one it is
|
||||
indicating.
|
||||
|
||||
@ivar address: An instance of an object which implements I{IAddress} to
|
||||
which this transport address is connected.
|
||||
"""
|
||||
|
||||
compareAttributes = ("address",)
|
||||
|
||||
def __init__(self, address):
|
||||
self.address = address
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"SSHTransportAddress({self.address!r})"
|
||||
|
||||
def __hash__(self):
|
||||
return hash(("SSH", self.address))
|
||||
@@ -0,0 +1,278 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Implements the SSH v2 key agent protocol. This protocol is documented in the
|
||||
SSH source code, in the file
|
||||
U{PROTOCOL.agent<http://www.openbsd.org/cgi-bin/cvsweb/src/usr.bin/ssh/PROTOCOL.agent>}.
|
||||
|
||||
Maintainer: Paul Swartz
|
||||
"""
|
||||
|
||||
|
||||
import struct
|
||||
|
||||
from twisted.conch.error import ConchError, MissingKeyStoreError
|
||||
from twisted.conch.ssh import keys
|
||||
from twisted.conch.ssh.common import NS, getMP, getNS
|
||||
from twisted.internet import defer, protocol
|
||||
|
||||
|
||||
class SSHAgentClient(protocol.Protocol):
|
||||
"""
|
||||
The client side of the SSH agent protocol. This is equivalent to
|
||||
ssh-add(1) and can be used with either ssh-agent(1) or the SSHAgentServer
|
||||
protocol, also in this package.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.buf = b""
|
||||
self.deferreds = []
|
||||
|
||||
def dataReceived(self, data):
|
||||
self.buf += data
|
||||
while 1:
|
||||
if len(self.buf) <= 4:
|
||||
return
|
||||
packLen = struct.unpack("!L", self.buf[:4])[0]
|
||||
if len(self.buf) < 4 + packLen:
|
||||
return
|
||||
packet, self.buf = self.buf[4 : 4 + packLen], self.buf[4 + packLen :]
|
||||
reqType = ord(packet[0:1])
|
||||
d = self.deferreds.pop(0)
|
||||
if reqType == AGENT_FAILURE:
|
||||
d.errback(ConchError("agent failure"))
|
||||
elif reqType == AGENT_SUCCESS:
|
||||
d.callback(b"")
|
||||
else:
|
||||
d.callback(packet)
|
||||
|
||||
def sendRequest(self, reqType, data):
|
||||
pack = struct.pack("!LB", len(data) + 1, reqType) + data
|
||||
self.transport.write(pack)
|
||||
d = defer.Deferred()
|
||||
self.deferreds.append(d)
|
||||
return d
|
||||
|
||||
def requestIdentities(self):
|
||||
"""
|
||||
@return: A L{Deferred} which will fire with a list of all keys found in
|
||||
the SSH agent. The list of keys is comprised of (public key blob,
|
||||
comment) tuples.
|
||||
"""
|
||||
d = self.sendRequest(AGENTC_REQUEST_IDENTITIES, b"")
|
||||
d.addCallback(self._cbRequestIdentities)
|
||||
return d
|
||||
|
||||
def _cbRequestIdentities(self, data):
|
||||
"""
|
||||
Unpack a collection of identities into a list of tuples comprised of
|
||||
public key blobs and comments.
|
||||
"""
|
||||
if ord(data[0:1]) != AGENT_IDENTITIES_ANSWER:
|
||||
raise ConchError("unexpected response: %i" % ord(data[0:1]))
|
||||
numKeys = struct.unpack("!L", data[1:5])[0]
|
||||
result = []
|
||||
data = data[5:]
|
||||
for i in range(numKeys):
|
||||
blob, data = getNS(data)
|
||||
comment, data = getNS(data)
|
||||
result.append((blob, comment))
|
||||
return result
|
||||
|
||||
def addIdentity(self, blob, comment=b""):
|
||||
"""
|
||||
Add a private key blob to the agent's collection of keys.
|
||||
"""
|
||||
req = blob
|
||||
req += NS(comment)
|
||||
return self.sendRequest(AGENTC_ADD_IDENTITY, req)
|
||||
|
||||
def signData(self, blob, data):
|
||||
"""
|
||||
Request that the agent sign the given C{data} with the private key
|
||||
which corresponds to the public key given by C{blob}. The private
|
||||
key should have been added to the agent already.
|
||||
|
||||
@type blob: L{bytes}
|
||||
@type data: L{bytes}
|
||||
@return: A L{Deferred} which fires with a signature for given data
|
||||
created with the given key.
|
||||
"""
|
||||
req = NS(blob)
|
||||
req += NS(data)
|
||||
req += b"\000\000\000\000" # flags
|
||||
return self.sendRequest(AGENTC_SIGN_REQUEST, req).addCallback(self._cbSignData)
|
||||
|
||||
def _cbSignData(self, data):
|
||||
if ord(data[0:1]) != AGENT_SIGN_RESPONSE:
|
||||
raise ConchError("unexpected data: %i" % ord(data[0:1]))
|
||||
signature = getNS(data[1:])[0]
|
||||
return signature
|
||||
|
||||
def removeIdentity(self, blob):
|
||||
"""
|
||||
Remove the private key corresponding to the public key in blob from the
|
||||
running agent.
|
||||
"""
|
||||
req = NS(blob)
|
||||
return self.sendRequest(AGENTC_REMOVE_IDENTITY, req)
|
||||
|
||||
def removeAllIdentities(self):
|
||||
"""
|
||||
Remove all keys from the running agent.
|
||||
"""
|
||||
return self.sendRequest(AGENTC_REMOVE_ALL_IDENTITIES, b"")
|
||||
|
||||
|
||||
class SSHAgentServer(protocol.Protocol):
|
||||
"""
|
||||
The server side of the SSH agent protocol. This is equivalent to
|
||||
ssh-agent(1) and can be used with either ssh-add(1) or the SSHAgentClient
|
||||
protocol, also in this package.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.buf = b""
|
||||
|
||||
def dataReceived(self, data):
|
||||
self.buf += data
|
||||
while 1:
|
||||
if len(self.buf) <= 4:
|
||||
return
|
||||
packLen = struct.unpack("!L", self.buf[:4])[0]
|
||||
if len(self.buf) < 4 + packLen:
|
||||
return
|
||||
packet, self.buf = self.buf[4 : 4 + packLen], self.buf[4 + packLen :]
|
||||
reqType = ord(packet[0:1])
|
||||
reqName = messages.get(reqType, None)
|
||||
if not reqName:
|
||||
self.sendResponse(AGENT_FAILURE, b"")
|
||||
else:
|
||||
f = getattr(self, "agentc_%s" % reqName)
|
||||
if getattr(self.factory, "keys", None) is None:
|
||||
self.sendResponse(AGENT_FAILURE, b"")
|
||||
raise MissingKeyStoreError()
|
||||
f(packet[1:])
|
||||
|
||||
def sendResponse(self, reqType, data):
|
||||
pack = struct.pack("!LB", len(data) + 1, reqType) + data
|
||||
self.transport.write(pack)
|
||||
|
||||
def agentc_REQUEST_IDENTITIES(self, data):
|
||||
"""
|
||||
Return all of the identities that have been added to the server
|
||||
"""
|
||||
assert data == b""
|
||||
numKeys = len(self.factory.keys)
|
||||
resp = []
|
||||
|
||||
resp.append(struct.pack("!L", numKeys))
|
||||
for key, comment in self.factory.keys.values():
|
||||
resp.append(NS(key.blob())) # yes, wrapped in an NS
|
||||
resp.append(NS(comment))
|
||||
self.sendResponse(AGENT_IDENTITIES_ANSWER, b"".join(resp))
|
||||
|
||||
def agentc_SIGN_REQUEST(self, data):
|
||||
"""
|
||||
Data is a structure with a reference to an already added key object and
|
||||
some data that the clients wants signed with that key. If the key
|
||||
object wasn't loaded, return AGENT_FAILURE, else return the signature.
|
||||
"""
|
||||
blob, data = getNS(data)
|
||||
if blob not in self.factory.keys:
|
||||
return self.sendResponse(AGENT_FAILURE, b"")
|
||||
signData, data = getNS(data)
|
||||
assert data == b"\000\000\000\000"
|
||||
self.sendResponse(
|
||||
AGENT_SIGN_RESPONSE, NS(self.factory.keys[blob][0].sign(signData))
|
||||
)
|
||||
|
||||
def agentc_ADD_IDENTITY(self, data):
|
||||
"""
|
||||
Adds a private key to the agent's collection of identities. On
|
||||
subsequent interactions, the private key can be accessed using only the
|
||||
corresponding public key.
|
||||
"""
|
||||
|
||||
# need to pre-read the key data so we can get past it to the comment string
|
||||
keyType, rest = getNS(data)
|
||||
if keyType == b"ssh-rsa":
|
||||
nmp = 6
|
||||
elif keyType == b"ssh-dss":
|
||||
nmp = 5
|
||||
else:
|
||||
raise keys.BadKeyError("unknown blob type: %s" % keyType)
|
||||
|
||||
rest = getMP(rest, nmp)[
|
||||
-1
|
||||
] # ignore the key data for now, we just want the comment
|
||||
comment, rest = getNS(rest) # the comment, tacked onto the end of the key blob
|
||||
|
||||
k = keys.Key.fromString(data, type="private_blob") # not wrapped in NS here
|
||||
self.factory.keys[k.blob()] = (k, comment)
|
||||
self.sendResponse(AGENT_SUCCESS, b"")
|
||||
|
||||
def agentc_REMOVE_IDENTITY(self, data):
|
||||
"""
|
||||
Remove a specific key from the agent's collection of identities.
|
||||
"""
|
||||
blob, _ = getNS(data)
|
||||
k = keys.Key.fromString(blob, type="blob")
|
||||
del self.factory.keys[k.blob()]
|
||||
self.sendResponse(AGENT_SUCCESS, b"")
|
||||
|
||||
def agentc_REMOVE_ALL_IDENTITIES(self, data):
|
||||
"""
|
||||
Remove all keys from the agent's collection of identities.
|
||||
"""
|
||||
assert data == b""
|
||||
self.factory.keys = {}
|
||||
self.sendResponse(AGENT_SUCCESS, b"")
|
||||
|
||||
# v1 messages that we ignore because we don't keep v1 keys
|
||||
# open-ssh sends both v1 and v2 commands, so we have to
|
||||
# do no-ops for v1 commands or we'll get "bad request" errors
|
||||
|
||||
def agentc_REQUEST_RSA_IDENTITIES(self, data):
|
||||
"""
|
||||
v1 message for listing RSA1 keys; superseded by
|
||||
agentc_REQUEST_IDENTITIES, which handles different key types.
|
||||
"""
|
||||
self.sendResponse(AGENT_RSA_IDENTITIES_ANSWER, struct.pack("!L", 0))
|
||||
|
||||
def agentc_REMOVE_RSA_IDENTITY(self, data):
|
||||
"""
|
||||
v1 message for removing RSA1 keys; superseded by
|
||||
agentc_REMOVE_IDENTITY, which handles different key types.
|
||||
"""
|
||||
self.sendResponse(AGENT_SUCCESS, b"")
|
||||
|
||||
def agentc_REMOVE_ALL_RSA_IDENTITIES(self, data):
|
||||
"""
|
||||
v1 message for removing all RSA1 keys; superseded by
|
||||
agentc_REMOVE_ALL_IDENTITIES, which handles different key types.
|
||||
"""
|
||||
self.sendResponse(AGENT_SUCCESS, b"")
|
||||
|
||||
|
||||
AGENTC_REQUEST_RSA_IDENTITIES = 1
|
||||
AGENT_RSA_IDENTITIES_ANSWER = 2
|
||||
AGENT_FAILURE = 5
|
||||
AGENT_SUCCESS = 6
|
||||
|
||||
AGENTC_REMOVE_RSA_IDENTITY = 8
|
||||
AGENTC_REMOVE_ALL_RSA_IDENTITIES = 9
|
||||
|
||||
AGENTC_REQUEST_IDENTITIES = 11
|
||||
AGENT_IDENTITIES_ANSWER = 12
|
||||
AGENTC_SIGN_REQUEST = 13
|
||||
AGENT_SIGN_RESPONSE = 14
|
||||
AGENTC_ADD_IDENTITY = 17
|
||||
AGENTC_REMOVE_IDENTITY = 18
|
||||
AGENTC_REMOVE_ALL_IDENTITIES = 19
|
||||
|
||||
messages = {}
|
||||
for name, value in locals().copy().items():
|
||||
if name[:7] == "AGENTC_":
|
||||
messages[value] = name[7:] # doesn't handle doubles
|
||||
@@ -0,0 +1,312 @@
|
||||
# -*- test-case-name: twisted.conch.test.test_channel -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
The parent class for all the SSH Channels. Currently implemented channels
|
||||
are session, direct-tcp, and forwarded-tcp.
|
||||
|
||||
Maintainer: Paul Swartz
|
||||
"""
|
||||
|
||||
|
||||
from zope.interface import implementer
|
||||
|
||||
from twisted.internet import interfaces
|
||||
from twisted.logger import Logger
|
||||
from twisted.python import log
|
||||
|
||||
|
||||
@implementer(interfaces.ITransport)
|
||||
class SSHChannel(log.Logger):
|
||||
"""
|
||||
A class that represents a multiplexed channel over an SSH connection.
|
||||
The channel has a local window which is the maximum amount of data it will
|
||||
receive, and a remote which is the maximum amount of data the remote side
|
||||
will accept. There is also a maximum packet size for any individual data
|
||||
packet going each way.
|
||||
|
||||
@ivar name: the name of the channel.
|
||||
@type name: L{bytes}
|
||||
@ivar localWindowSize: the maximum size of the local window in bytes.
|
||||
@type localWindowSize: L{int}
|
||||
@ivar localWindowLeft: how many bytes are left in the local window.
|
||||
@type localWindowLeft: L{int}
|
||||
@ivar localMaxPacket: the maximum size of packet we will accept in bytes.
|
||||
@type localMaxPacket: L{int}
|
||||
@ivar remoteWindowLeft: how many bytes are left in the remote window.
|
||||
@type remoteWindowLeft: L{int}
|
||||
@ivar remoteMaxPacket: the maximum size of a packet the remote side will
|
||||
accept in bytes.
|
||||
@type remoteMaxPacket: L{int}
|
||||
@ivar conn: the connection this channel is multiplexed through.
|
||||
@type conn: L{SSHConnection}
|
||||
@ivar data: any data to send to the other side when the channel is
|
||||
requested.
|
||||
@type data: L{bytes}
|
||||
@ivar avatar: an avatar for the logged-in user (if a server channel)
|
||||
@ivar localClosed: True if we aren't accepting more data.
|
||||
@type localClosed: L{bool}
|
||||
@ivar remoteClosed: True if the other side isn't accepting more data.
|
||||
@type remoteClosed: L{bool}
|
||||
"""
|
||||
|
||||
_log = Logger()
|
||||
name: bytes = None # type: ignore[assignment] # only needed for client channels
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
localWindow=0,
|
||||
localMaxPacket=0,
|
||||
remoteWindow=0,
|
||||
remoteMaxPacket=0,
|
||||
conn=None,
|
||||
data=None,
|
||||
avatar=None,
|
||||
):
|
||||
self.localWindowSize = localWindow or 131072
|
||||
self.localWindowLeft = self.localWindowSize
|
||||
self.localMaxPacket = localMaxPacket or 32768
|
||||
self.remoteWindowLeft = remoteWindow
|
||||
self.remoteMaxPacket = remoteMaxPacket
|
||||
self.areWriting = 1
|
||||
self.conn = conn
|
||||
self.data = data
|
||||
self.avatar = avatar
|
||||
self.specificData = b""
|
||||
self.buf = b""
|
||||
self.extBuf = []
|
||||
self.closing = 0
|
||||
self.localClosed = 0
|
||||
self.remoteClosed = 0
|
||||
self.id = None # gets set later by SSHConnection
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.__bytes__().decode("ascii")
|
||||
|
||||
def __bytes__(self) -> bytes:
|
||||
"""
|
||||
Return a byte string representation of the channel
|
||||
"""
|
||||
name = self.name
|
||||
if not name:
|
||||
name = b"None"
|
||||
|
||||
return b"<SSHChannel %b (lw %d rw %d)>" % (
|
||||
name,
|
||||
self.localWindowLeft,
|
||||
self.remoteWindowLeft,
|
||||
)
|
||||
|
||||
def logPrefix(self):
|
||||
id = (self.id is not None and str(self.id)) or "unknown"
|
||||
if self.name:
|
||||
name = self.name.decode("ascii")
|
||||
else:
|
||||
name = "None"
|
||||
return f"SSHChannel {name} ({id}) on {self.conn.logPrefix()}"
|
||||
|
||||
def channelOpen(self, specificData):
|
||||
"""
|
||||
Called when the channel is opened. specificData is any data that the
|
||||
other side sent us when opening the channel.
|
||||
|
||||
@type specificData: L{bytes}
|
||||
"""
|
||||
self._log.info("channel open")
|
||||
|
||||
def openFailed(self, reason):
|
||||
"""
|
||||
Called when the open failed for some reason.
|
||||
reason.desc is a string descrption, reason.code the SSH error code.
|
||||
|
||||
@type reason: L{error.ConchError}
|
||||
"""
|
||||
self._log.error("other side refused open\nreason: {reason}", reason=reason)
|
||||
|
||||
def addWindowBytes(self, data):
|
||||
"""
|
||||
Called when bytes are added to the remote window. By default it clears
|
||||
the data buffers.
|
||||
|
||||
@type data: L{bytes}
|
||||
"""
|
||||
self.remoteWindowLeft = self.remoteWindowLeft + data
|
||||
if not self.areWriting and not self.closing:
|
||||
self.areWriting = True
|
||||
self.startWriting()
|
||||
if self.buf:
|
||||
b = self.buf
|
||||
self.buf = b""
|
||||
self.write(b)
|
||||
if self.extBuf:
|
||||
b = self.extBuf
|
||||
self.extBuf = []
|
||||
for (type, data) in b:
|
||||
self.writeExtended(type, data)
|
||||
|
||||
def requestReceived(self, requestType, data):
|
||||
"""
|
||||
Called when a request is sent to this channel. By default it delegates
|
||||
to self.request_<requestType>.
|
||||
If this function returns true, the request succeeded, otherwise it
|
||||
failed.
|
||||
|
||||
@type requestType: L{bytes}
|
||||
@type data: L{bytes}
|
||||
@rtype: L{bool}
|
||||
"""
|
||||
foo = requestType.replace(b"-", b"_").decode("ascii")
|
||||
f = getattr(self, "request_" + foo, None)
|
||||
if f:
|
||||
return f(data)
|
||||
self._log.info("unhandled request for {requestType}", requestType=requestType)
|
||||
return 0
|
||||
|
||||
def dataReceived(self, data):
|
||||
"""
|
||||
Called when we receive data.
|
||||
|
||||
@type data: L{bytes}
|
||||
"""
|
||||
self._log.debug("got data {data}", data=data)
|
||||
|
||||
def extReceived(self, dataType, data):
|
||||
"""
|
||||
Called when we receive extended data (usually standard error).
|
||||
|
||||
@type dataType: L{int}
|
||||
@type data: L{str}
|
||||
"""
|
||||
self._log.debug(
|
||||
"got extended data {dataType} {data!r}", dataType=dataType, data=data
|
||||
)
|
||||
|
||||
def eofReceived(self):
|
||||
"""
|
||||
Called when the other side will send no more data.
|
||||
"""
|
||||
self._log.info("remote eof")
|
||||
|
||||
def closeReceived(self):
|
||||
"""
|
||||
Called when the other side has closed the channel.
|
||||
"""
|
||||
self._log.info("remote close")
|
||||
self.loseConnection()
|
||||
|
||||
def closed(self):
|
||||
"""
|
||||
Called when the channel is closed. This means that both our side and
|
||||
the remote side have closed the channel.
|
||||
"""
|
||||
self._log.info("closed")
|
||||
|
||||
def write(self, data):
|
||||
"""
|
||||
Write some data to the channel. If there is not enough remote window
|
||||
available, buffer until it is. Otherwise, split the data into
|
||||
packets of length remoteMaxPacket and send them.
|
||||
|
||||
@type data: L{bytes}
|
||||
"""
|
||||
if self.buf:
|
||||
self.buf += data
|
||||
return
|
||||
top = len(data)
|
||||
if top > self.remoteWindowLeft:
|
||||
data, self.buf = (
|
||||
data[: self.remoteWindowLeft],
|
||||
data[self.remoteWindowLeft :],
|
||||
)
|
||||
self.areWriting = 0
|
||||
self.stopWriting()
|
||||
top = self.remoteWindowLeft
|
||||
rmp = self.remoteMaxPacket
|
||||
write = self.conn.sendData
|
||||
r = range(0, top, rmp)
|
||||
for offset in r:
|
||||
write(self, data[offset : offset + rmp])
|
||||
self.remoteWindowLeft -= top
|
||||
if self.closing and not self.buf:
|
||||
self.loseConnection() # try again
|
||||
|
||||
def writeExtended(self, dataType, data):
|
||||
"""
|
||||
Send extended data to this channel. If there is not enough remote
|
||||
window available, buffer until there is. Otherwise, split the data
|
||||
into packets of length remoteMaxPacket and send them.
|
||||
|
||||
@type dataType: L{int}
|
||||
@type data: L{bytes}
|
||||
"""
|
||||
if self.extBuf:
|
||||
if self.extBuf[-1][0] == dataType:
|
||||
self.extBuf[-1][1] += data
|
||||
else:
|
||||
self.extBuf.append([dataType, data])
|
||||
return
|
||||
if len(data) > self.remoteWindowLeft:
|
||||
data, self.extBuf = (
|
||||
data[: self.remoteWindowLeft],
|
||||
[[dataType, data[self.remoteWindowLeft :]]],
|
||||
)
|
||||
self.areWriting = 0
|
||||
self.stopWriting()
|
||||
while len(data) > self.remoteMaxPacket:
|
||||
self.conn.sendExtendedData(self, dataType, data[: self.remoteMaxPacket])
|
||||
data = data[self.remoteMaxPacket :]
|
||||
self.remoteWindowLeft -= self.remoteMaxPacket
|
||||
if data:
|
||||
self.conn.sendExtendedData(self, dataType, data)
|
||||
self.remoteWindowLeft -= len(data)
|
||||
if self.closing:
|
||||
self.loseConnection() # try again
|
||||
|
||||
def writeSequence(self, data):
|
||||
"""
|
||||
Part of the Transport interface. Write a list of strings to the
|
||||
channel.
|
||||
|
||||
@type data: C{list} of L{str}
|
||||
"""
|
||||
self.write(b"".join(data))
|
||||
|
||||
def loseConnection(self):
|
||||
"""
|
||||
Close the channel if there is no buferred data. Otherwise, note the
|
||||
request and return.
|
||||
"""
|
||||
self.closing = 1
|
||||
if not self.buf and not self.extBuf:
|
||||
self.conn.sendClose(self)
|
||||
|
||||
def getPeer(self):
|
||||
"""
|
||||
See: L{ITransport.getPeer}
|
||||
|
||||
@return: The remote address of this connection.
|
||||
@rtype: L{SSHTransportAddress}.
|
||||
"""
|
||||
return self.conn.transport.getPeer()
|
||||
|
||||
def getHost(self):
|
||||
"""
|
||||
See: L{ITransport.getHost}
|
||||
|
||||
@return: An address describing this side of the connection.
|
||||
@rtype: L{SSHTransportAddress}.
|
||||
"""
|
||||
return self.conn.transport.getHost()
|
||||
|
||||
def stopWriting(self):
|
||||
"""
|
||||
Called when the remote buffer is full, as a hint to stop writing.
|
||||
This can be ignored, but it can be helpful.
|
||||
"""
|
||||
|
||||
def startWriting(self):
|
||||
"""
|
||||
Called when the remote buffer has more room, as a hint to continue
|
||||
writing.
|
||||
"""
|
||||
@@ -0,0 +1,85 @@
|
||||
# -*- test-case-name: twisted.conch.test.test_ssh -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Common functions for the SSH classes.
|
||||
|
||||
Maintainer: Paul Swartz
|
||||
"""
|
||||
|
||||
|
||||
import struct
|
||||
|
||||
from cryptography.utils import int_to_bytes
|
||||
|
||||
from twisted.python.deprecate import deprecated
|
||||
from twisted.python.versions import Version
|
||||
|
||||
__all__ = ["NS", "getNS", "MP", "getMP", "ffs"]
|
||||
|
||||
|
||||
def NS(t):
|
||||
"""
|
||||
net string
|
||||
"""
|
||||
if isinstance(t, str):
|
||||
t = t.encode("utf-8")
|
||||
return struct.pack("!L", len(t)) + t
|
||||
|
||||
|
||||
def getNS(s, count=1):
|
||||
"""
|
||||
get net string
|
||||
"""
|
||||
ns = []
|
||||
c = 0
|
||||
for i in range(count):
|
||||
(l,) = struct.unpack("!L", s[c : c + 4])
|
||||
ns.append(s[c + 4 : 4 + l + c])
|
||||
c += 4 + l
|
||||
return tuple(ns) + (s[c:],)
|
||||
|
||||
|
||||
def MP(number):
|
||||
if number == 0:
|
||||
return b"\000" * 4
|
||||
assert number > 0
|
||||
bn = int_to_bytes(number)
|
||||
if ord(bn[0:1]) & 128:
|
||||
bn = b"\000" + bn
|
||||
return struct.pack(">L", len(bn)) + bn
|
||||
|
||||
|
||||
def getMP(data, count=1):
|
||||
"""
|
||||
Get multiple precision integer out of the string. A multiple precision
|
||||
integer is stored as a 4-byte length followed by length bytes of the
|
||||
integer. If count is specified, get count integers out of the string.
|
||||
The return value is a tuple of count integers followed by the rest of
|
||||
the data.
|
||||
"""
|
||||
mp = []
|
||||
c = 0
|
||||
for i in range(count):
|
||||
(length,) = struct.unpack(">L", data[c : c + 4])
|
||||
mp.append(int.from_bytes(data[c + 4 : c + 4 + length], "big"))
|
||||
c += 4 + length
|
||||
return tuple(mp) + (data[c:],)
|
||||
|
||||
|
||||
def ffs(c, s):
|
||||
"""
|
||||
first from second
|
||||
goes through the first list, looking for items in the second, returns the first one
|
||||
"""
|
||||
for i in c:
|
||||
if i in s:
|
||||
return i
|
||||
|
||||
|
||||
@deprecated(Version("Twisted", 16, 5, 0))
|
||||
def install():
|
||||
# This used to install gmpy, but is technically public API, so just do
|
||||
# nothing.
|
||||
pass
|
||||
@@ -0,0 +1,679 @@
|
||||
# -*- test-case-name: twisted.conch.test.test_connection -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
This module contains the implementation of the ssh-connection service, which
|
||||
allows access to the shell and port-forwarding.
|
||||
|
||||
Maintainer: Paul Swartz
|
||||
"""
|
||||
|
||||
import string
|
||||
import struct
|
||||
|
||||
import twisted.internet.error
|
||||
from twisted.conch import error
|
||||
from twisted.conch.ssh import common, service
|
||||
from twisted.internet import defer
|
||||
from twisted.logger import Logger
|
||||
from twisted.python.compat import nativeString, networkString
|
||||
|
||||
|
||||
class SSHConnection(service.SSHService):
|
||||
"""
|
||||
An implementation of the 'ssh-connection' service. It is used to
|
||||
multiplex multiple channels over the single SSH connection.
|
||||
|
||||
@ivar localChannelID: the next number to use as a local channel ID.
|
||||
@type localChannelID: L{int}
|
||||
@ivar channels: a L{dict} mapping a local channel ID to C{SSHChannel}
|
||||
subclasses.
|
||||
@type channels: L{dict}
|
||||
@ivar localToRemoteChannel: a L{dict} mapping a local channel ID to a
|
||||
remote channel ID.
|
||||
@type localToRemoteChannel: L{dict}
|
||||
@ivar channelsToRemoteChannel: a L{dict} mapping a C{SSHChannel} subclass
|
||||
to remote channel ID.
|
||||
@type channelsToRemoteChannel: L{dict}
|
||||
@ivar deferreds: a L{dict} mapping a local channel ID to a C{list} of
|
||||
C{Deferreds} for outstanding channel requests. Also, the 'global'
|
||||
key stores the C{list} of pending global request C{Deferred}s.
|
||||
"""
|
||||
|
||||
name = b"ssh-connection"
|
||||
_log = Logger()
|
||||
|
||||
def __init__(self):
|
||||
self.localChannelID = 0 # this is the current # to use for channel ID
|
||||
# local channel ID -> remote channel ID
|
||||
self.localToRemoteChannel = {}
|
||||
# local channel ID -> subclass of SSHChannel
|
||||
self.channels = {}
|
||||
# subclass of SSHChannel -> remote channel ID
|
||||
self.channelsToRemoteChannel = {}
|
||||
# local channel -> list of deferreds for pending requests
|
||||
# or 'global' -> list of deferreds for global requests
|
||||
self.deferreds = {"global": []}
|
||||
|
||||
self.transport = None # gets set later
|
||||
|
||||
def serviceStarted(self):
|
||||
if hasattr(self.transport, "avatar"):
|
||||
self.transport.avatar.conn = self
|
||||
|
||||
def serviceStopped(self):
|
||||
"""
|
||||
Called when the connection is stopped.
|
||||
"""
|
||||
# Close any fully open channels
|
||||
for channel in list(self.channelsToRemoteChannel.keys()):
|
||||
self.channelClosed(channel)
|
||||
# Indicate failure to any channels that were in the process of
|
||||
# opening but not yet open.
|
||||
while self.channels:
|
||||
(_, channel) = self.channels.popitem()
|
||||
channel.openFailed(twisted.internet.error.ConnectionLost())
|
||||
# Errback any unfinished global requests.
|
||||
self._cleanupGlobalDeferreds()
|
||||
|
||||
def _cleanupGlobalDeferreds(self):
|
||||
"""
|
||||
All pending requests that have returned a deferred must be errbacked
|
||||
when this service is stopped, otherwise they might be left uncalled and
|
||||
uncallable.
|
||||
"""
|
||||
for d in self.deferreds["global"]:
|
||||
d.errback(error.ConchError("Connection stopped."))
|
||||
del self.deferreds["global"][:]
|
||||
|
||||
# packet methods
|
||||
def ssh_GLOBAL_REQUEST(self, packet):
|
||||
"""
|
||||
The other side has made a global request. Payload::
|
||||
string request type
|
||||
bool want reply
|
||||
<request specific data>
|
||||
|
||||
This dispatches to self.gotGlobalRequest.
|
||||
"""
|
||||
requestType, rest = common.getNS(packet)
|
||||
wantReply, rest = ord(rest[0:1]), rest[1:]
|
||||
ret = self.gotGlobalRequest(requestType, rest)
|
||||
if wantReply:
|
||||
reply = MSG_REQUEST_FAILURE
|
||||
data = b""
|
||||
if ret:
|
||||
reply = MSG_REQUEST_SUCCESS
|
||||
if isinstance(ret, (tuple, list)):
|
||||
data = ret[1]
|
||||
self.transport.sendPacket(reply, data)
|
||||
|
||||
def ssh_REQUEST_SUCCESS(self, packet):
|
||||
"""
|
||||
Our global request succeeded. Get the appropriate Deferred and call
|
||||
it back with the packet we received.
|
||||
"""
|
||||
self._log.debug("global request success")
|
||||
self.deferreds["global"].pop(0).callback(packet)
|
||||
|
||||
def ssh_REQUEST_FAILURE(self, packet):
|
||||
"""
|
||||
Our global request failed. Get the appropriate Deferred and errback
|
||||
it with the packet we received.
|
||||
"""
|
||||
self._log.debug("global request failure")
|
||||
self.deferreds["global"].pop(0).errback(
|
||||
error.ConchError("global request failed", packet)
|
||||
)
|
||||
|
||||
def ssh_CHANNEL_OPEN(self, packet):
|
||||
"""
|
||||
The other side wants to get a channel. Payload::
|
||||
string channel name
|
||||
uint32 remote channel number
|
||||
uint32 remote window size
|
||||
uint32 remote maximum packet size
|
||||
<channel specific data>
|
||||
|
||||
We get a channel from self.getChannel(), give it a local channel number
|
||||
and notify the other side. Then notify the channel by calling its
|
||||
channelOpen method.
|
||||
"""
|
||||
channelType, rest = common.getNS(packet)
|
||||
senderChannel, windowSize, maxPacket = struct.unpack(">3L", rest[:12])
|
||||
packet = rest[12:]
|
||||
try:
|
||||
channel = self.getChannel(channelType, windowSize, maxPacket, packet)
|
||||
localChannel = self.localChannelID
|
||||
self.localChannelID += 1
|
||||
channel.id = localChannel
|
||||
self.channels[localChannel] = channel
|
||||
self.channelsToRemoteChannel[channel] = senderChannel
|
||||
self.localToRemoteChannel[localChannel] = senderChannel
|
||||
openConfirmPacket = (
|
||||
struct.pack(
|
||||
">4L",
|
||||
senderChannel,
|
||||
localChannel,
|
||||
channel.localWindowSize,
|
||||
channel.localMaxPacket,
|
||||
)
|
||||
+ channel.specificData
|
||||
)
|
||||
self.transport.sendPacket(MSG_CHANNEL_OPEN_CONFIRMATION, openConfirmPacket)
|
||||
channel.channelOpen(packet)
|
||||
except Exception as e:
|
||||
self._log.failure("channel open failed")
|
||||
if isinstance(e, error.ConchError):
|
||||
textualInfo, reason = e.args
|
||||
if isinstance(textualInfo, int):
|
||||
# See #3657 and #3071
|
||||
textualInfo, reason = reason, textualInfo
|
||||
else:
|
||||
reason = OPEN_CONNECT_FAILED
|
||||
textualInfo = "unknown failure"
|
||||
self.transport.sendPacket(
|
||||
MSG_CHANNEL_OPEN_FAILURE,
|
||||
struct.pack(">2L", senderChannel, reason)
|
||||
+ common.NS(networkString(textualInfo))
|
||||
+ common.NS(b""),
|
||||
)
|
||||
|
||||
def ssh_CHANNEL_OPEN_CONFIRMATION(self, packet):
|
||||
"""
|
||||
The other side accepted our MSG_CHANNEL_OPEN request. Payload::
|
||||
uint32 local channel number
|
||||
uint32 remote channel number
|
||||
uint32 remote window size
|
||||
uint32 remote maximum packet size
|
||||
<channel specific data>
|
||||
|
||||
Find the channel using the local channel number and notify its
|
||||
channelOpen method.
|
||||
"""
|
||||
(localChannel, remoteChannel, windowSize, maxPacket) = struct.unpack(
|
||||
">4L", packet[:16]
|
||||
)
|
||||
specificData = packet[16:]
|
||||
channel = self.channels[localChannel]
|
||||
channel.conn = self
|
||||
self.localToRemoteChannel[localChannel] = remoteChannel
|
||||
self.channelsToRemoteChannel[channel] = remoteChannel
|
||||
channel.remoteWindowLeft = windowSize
|
||||
channel.remoteMaxPacket = maxPacket
|
||||
channel.channelOpen(specificData)
|
||||
|
||||
def ssh_CHANNEL_OPEN_FAILURE(self, packet):
|
||||
"""
|
||||
The other side did not accept our MSG_CHANNEL_OPEN request. Payload::
|
||||
uint32 local channel number
|
||||
uint32 reason code
|
||||
string reason description
|
||||
|
||||
Find the channel using the local channel number and notify it by
|
||||
calling its openFailed() method.
|
||||
"""
|
||||
localChannel, reasonCode = struct.unpack(">2L", packet[:8])
|
||||
reasonDesc = common.getNS(packet[8:])[0]
|
||||
channel = self.channels[localChannel]
|
||||
del self.channels[localChannel]
|
||||
channel.conn = self
|
||||
reason = error.ConchError(reasonDesc, reasonCode)
|
||||
channel.openFailed(reason)
|
||||
|
||||
def ssh_CHANNEL_WINDOW_ADJUST(self, packet):
|
||||
"""
|
||||
The other side is adding bytes to its window. Payload::
|
||||
uint32 local channel number
|
||||
uint32 bytes to add
|
||||
|
||||
Call the channel's addWindowBytes() method to add new bytes to the
|
||||
remote window.
|
||||
"""
|
||||
localChannel, bytesToAdd = struct.unpack(">2L", packet[:8])
|
||||
channel = self.channels[localChannel]
|
||||
channel.addWindowBytes(bytesToAdd)
|
||||
|
||||
def ssh_CHANNEL_DATA(self, packet):
|
||||
"""
|
||||
The other side is sending us data. Payload::
|
||||
uint32 local channel number
|
||||
string data
|
||||
|
||||
Check to make sure the other side hasn't sent too much data (more
|
||||
than what's in the window, or more than the maximum packet size). If
|
||||
they have, close the channel. Otherwise, decrease the available
|
||||
window and pass the data to the channel's dataReceived().
|
||||
"""
|
||||
localChannel, dataLength = struct.unpack(">2L", packet[:8])
|
||||
channel = self.channels[localChannel]
|
||||
# XXX should this move to dataReceived to put client in charge?
|
||||
if (
|
||||
dataLength > channel.localWindowLeft or dataLength > channel.localMaxPacket
|
||||
): # more data than we want
|
||||
self._log.error("too much data")
|
||||
self.sendClose(channel)
|
||||
return
|
||||
# packet = packet[:channel.localWindowLeft+4]
|
||||
data = common.getNS(packet[4:])[0]
|
||||
channel.localWindowLeft -= dataLength
|
||||
if channel.localWindowLeft < channel.localWindowSize // 2:
|
||||
self.adjustWindow(
|
||||
channel, channel.localWindowSize - channel.localWindowLeft
|
||||
)
|
||||
channel.dataReceived(data)
|
||||
|
||||
def ssh_CHANNEL_EXTENDED_DATA(self, packet):
|
||||
"""
|
||||
The other side is sending us exteneded data. Payload::
|
||||
uint32 local channel number
|
||||
uint32 type code
|
||||
string data
|
||||
|
||||
Check to make sure the other side hasn't sent too much data (more
|
||||
than what's in the window, or than the maximum packet size). If
|
||||
they have, close the channel. Otherwise, decrease the available
|
||||
window and pass the data and type code to the channel's
|
||||
extReceived().
|
||||
"""
|
||||
localChannel, typeCode, dataLength = struct.unpack(">3L", packet[:12])
|
||||
channel = self.channels[localChannel]
|
||||
if dataLength > channel.localWindowLeft or dataLength > channel.localMaxPacket:
|
||||
self._log.error("too much extdata")
|
||||
self.sendClose(channel)
|
||||
return
|
||||
data = common.getNS(packet[8:])[0]
|
||||
channel.localWindowLeft -= dataLength
|
||||
if channel.localWindowLeft < channel.localWindowSize // 2:
|
||||
self.adjustWindow(
|
||||
channel, channel.localWindowSize - channel.localWindowLeft
|
||||
)
|
||||
channel.extReceived(typeCode, data)
|
||||
|
||||
def ssh_CHANNEL_EOF(self, packet):
|
||||
"""
|
||||
The other side is not sending any more data. Payload::
|
||||
uint32 local channel number
|
||||
|
||||
Notify the channel by calling its eofReceived() method.
|
||||
"""
|
||||
localChannel = struct.unpack(">L", packet[:4])[0]
|
||||
channel = self.channels[localChannel]
|
||||
channel.eofReceived()
|
||||
|
||||
def ssh_CHANNEL_CLOSE(self, packet):
|
||||
"""
|
||||
The other side is closing its end; it does not want to receive any
|
||||
more data. Payload::
|
||||
uint32 local channel number
|
||||
|
||||
Notify the channnel by calling its closeReceived() method. If
|
||||
the channel has also sent a close message, call self.channelClosed().
|
||||
"""
|
||||
localChannel = struct.unpack(">L", packet[:4])[0]
|
||||
channel = self.channels[localChannel]
|
||||
channel.closeReceived()
|
||||
channel.remoteClosed = True
|
||||
if channel.localClosed and channel.remoteClosed:
|
||||
self.channelClosed(channel)
|
||||
|
||||
def ssh_CHANNEL_REQUEST(self, packet):
|
||||
"""
|
||||
The other side is sending a request to a channel. Payload::
|
||||
uint32 local channel number
|
||||
string request name
|
||||
bool want reply
|
||||
<request specific data>
|
||||
|
||||
Pass the message to the channel's requestReceived method. If the
|
||||
other side wants a reply, add callbacks which will send the
|
||||
reply.
|
||||
"""
|
||||
localChannel = struct.unpack(">L", packet[:4])[0]
|
||||
requestType, rest = common.getNS(packet[4:])
|
||||
wantReply = ord(rest[0:1])
|
||||
channel = self.channels[localChannel]
|
||||
d = defer.maybeDeferred(channel.requestReceived, requestType, rest[1:])
|
||||
if wantReply:
|
||||
d.addCallback(self._cbChannelRequest, localChannel)
|
||||
d.addErrback(self._ebChannelRequest, localChannel)
|
||||
return d
|
||||
|
||||
def _cbChannelRequest(self, result, localChannel):
|
||||
"""
|
||||
Called back if the other side wanted a reply to a channel request. If
|
||||
the result is true, send a MSG_CHANNEL_SUCCESS. Otherwise, raise
|
||||
a C{error.ConchError}
|
||||
|
||||
@param result: the value returned from the channel's requestReceived()
|
||||
method. If it's False, the request failed.
|
||||
@type result: L{bool}
|
||||
@param localChannel: the local channel ID of the channel to which the
|
||||
request was made.
|
||||
@type localChannel: L{int}
|
||||
@raises ConchError: if the result is False.
|
||||
"""
|
||||
if not result:
|
||||
raise error.ConchError("failed request")
|
||||
self.transport.sendPacket(
|
||||
MSG_CHANNEL_SUCCESS,
|
||||
struct.pack(">L", self.localToRemoteChannel[localChannel]),
|
||||
)
|
||||
|
||||
def _ebChannelRequest(self, result, localChannel):
|
||||
"""
|
||||
Called if the other wisde wanted a reply to the channel requeset and
|
||||
the channel request failed.
|
||||
|
||||
@param result: a Failure, but it's not used.
|
||||
@param localChannel: the local channel ID of the channel to which the
|
||||
request was made.
|
||||
@type localChannel: L{int}
|
||||
"""
|
||||
self.transport.sendPacket(
|
||||
MSG_CHANNEL_FAILURE,
|
||||
struct.pack(">L", self.localToRemoteChannel[localChannel]),
|
||||
)
|
||||
|
||||
def ssh_CHANNEL_SUCCESS(self, packet):
|
||||
"""
|
||||
Our channel request to the other side succeeded. Payload::
|
||||
uint32 local channel number
|
||||
|
||||
Get the C{Deferred} out of self.deferreds and call it back.
|
||||
"""
|
||||
localChannel = struct.unpack(">L", packet[:4])[0]
|
||||
if self.deferreds.get(localChannel):
|
||||
d = self.deferreds[localChannel].pop(0)
|
||||
d.callback("")
|
||||
|
||||
def ssh_CHANNEL_FAILURE(self, packet):
|
||||
"""
|
||||
Our channel request to the other side failed. Payload::
|
||||
uint32 local channel number
|
||||
|
||||
Get the C{Deferred} out of self.deferreds and errback it with a
|
||||
C{error.ConchError}.
|
||||
"""
|
||||
localChannel = struct.unpack(">L", packet[:4])[0]
|
||||
if self.deferreds.get(localChannel):
|
||||
d = self.deferreds[localChannel].pop(0)
|
||||
d.errback(error.ConchError("channel request failed"))
|
||||
|
||||
# methods for users of the connection to call
|
||||
|
||||
def sendGlobalRequest(self, request, data, wantReply=0):
|
||||
"""
|
||||
Send a global request for this connection. Current this is only used
|
||||
for remote->local TCP forwarding.
|
||||
|
||||
@type request: L{bytes}
|
||||
@type data: L{bytes}
|
||||
@type wantReply: L{bool}
|
||||
@rtype: C{Deferred}/L{None}
|
||||
"""
|
||||
self.transport.sendPacket(
|
||||
MSG_GLOBAL_REQUEST,
|
||||
common.NS(request) + (wantReply and b"\xff" or b"\x00") + data,
|
||||
)
|
||||
if wantReply:
|
||||
d = defer.Deferred()
|
||||
self.deferreds["global"].append(d)
|
||||
return d
|
||||
|
||||
def openChannel(self, channel, extra=b""):
|
||||
"""
|
||||
Open a new channel on this connection.
|
||||
|
||||
@type channel: subclass of C{SSHChannel}
|
||||
@type extra: L{bytes}
|
||||
"""
|
||||
self._log.info(
|
||||
"opening channel {id} with {localWindowSize} {localMaxPacket}",
|
||||
id=self.localChannelID,
|
||||
localWindowSize=channel.localWindowSize,
|
||||
localMaxPacket=channel.localMaxPacket,
|
||||
)
|
||||
self.transport.sendPacket(
|
||||
MSG_CHANNEL_OPEN,
|
||||
common.NS(channel.name)
|
||||
+ struct.pack(
|
||||
">3L",
|
||||
self.localChannelID,
|
||||
channel.localWindowSize,
|
||||
channel.localMaxPacket,
|
||||
)
|
||||
+ extra,
|
||||
)
|
||||
channel.id = self.localChannelID
|
||||
self.channels[self.localChannelID] = channel
|
||||
self.localChannelID += 1
|
||||
|
||||
def sendRequest(self, channel, requestType, data, wantReply=0):
|
||||
"""
|
||||
Send a request to a channel.
|
||||
|
||||
@type channel: subclass of C{SSHChannel}
|
||||
@type requestType: L{bytes}
|
||||
@type data: L{bytes}
|
||||
@type wantReply: L{bool}
|
||||
@rtype: C{Deferred}/L{None}
|
||||
"""
|
||||
if channel.localClosed:
|
||||
return
|
||||
self._log.debug("sending request {requestType}", requestType=requestType)
|
||||
self.transport.sendPacket(
|
||||
MSG_CHANNEL_REQUEST,
|
||||
struct.pack(">L", self.channelsToRemoteChannel[channel])
|
||||
+ common.NS(requestType)
|
||||
+ (b"\1" if wantReply else b"\0")
|
||||
+ data,
|
||||
)
|
||||
if wantReply:
|
||||
d = defer.Deferred()
|
||||
self.deferreds.setdefault(channel.id, []).append(d)
|
||||
return d
|
||||
|
||||
def adjustWindow(self, channel, bytesToAdd):
|
||||
"""
|
||||
Tell the other side that we will receive more data. This should not
|
||||
normally need to be called as it is managed automatically.
|
||||
|
||||
@type channel: subclass of L{SSHChannel}
|
||||
@type bytesToAdd: L{int}
|
||||
"""
|
||||
if channel.localClosed:
|
||||
return # we're already closed
|
||||
packet = struct.pack(">2L", self.channelsToRemoteChannel[channel], bytesToAdd)
|
||||
self.transport.sendPacket(MSG_CHANNEL_WINDOW_ADJUST, packet)
|
||||
self._log.debug(
|
||||
"adding {bytesToAdd} to {localWindowLeft} in channel {id}",
|
||||
bytesToAdd=bytesToAdd,
|
||||
localWindowLeft=channel.localWindowLeft,
|
||||
id=channel.id,
|
||||
)
|
||||
channel.localWindowLeft += bytesToAdd
|
||||
|
||||
def sendData(self, channel, data):
|
||||
"""
|
||||
Send data to a channel. This should not normally be used: instead use
|
||||
channel.write(data) as it manages the window automatically.
|
||||
|
||||
@type channel: subclass of L{SSHChannel}
|
||||
@type data: L{bytes}
|
||||
"""
|
||||
if channel.localClosed:
|
||||
return # we're already closed
|
||||
self.transport.sendPacket(
|
||||
MSG_CHANNEL_DATA,
|
||||
struct.pack(">L", self.channelsToRemoteChannel[channel]) + common.NS(data),
|
||||
)
|
||||
|
||||
def sendExtendedData(self, channel, dataType, data):
|
||||
"""
|
||||
Send extended data to a channel. This should not normally be used:
|
||||
instead use channel.writeExtendedData(data, dataType) as it manages
|
||||
the window automatically.
|
||||
|
||||
@type channel: subclass of L{SSHChannel}
|
||||
@type dataType: L{int}
|
||||
@type data: L{bytes}
|
||||
"""
|
||||
if channel.localClosed:
|
||||
return # we're already closed
|
||||
self.transport.sendPacket(
|
||||
MSG_CHANNEL_EXTENDED_DATA,
|
||||
struct.pack(">2L", self.channelsToRemoteChannel[channel], dataType)
|
||||
+ common.NS(data),
|
||||
)
|
||||
|
||||
def sendEOF(self, channel):
|
||||
"""
|
||||
Send an EOF (End of File) for a channel.
|
||||
|
||||
@type channel: subclass of L{SSHChannel}
|
||||
"""
|
||||
if channel.localClosed:
|
||||
return # we're already closed
|
||||
self._log.debug("sending eof")
|
||||
self.transport.sendPacket(
|
||||
MSG_CHANNEL_EOF, struct.pack(">L", self.channelsToRemoteChannel[channel])
|
||||
)
|
||||
|
||||
def sendClose(self, channel):
|
||||
"""
|
||||
Close a channel.
|
||||
|
||||
@type channel: subclass of L{SSHChannel}
|
||||
"""
|
||||
if channel.localClosed:
|
||||
return # we're already closed
|
||||
self._log.info("sending close {id}", id=channel.id)
|
||||
self.transport.sendPacket(
|
||||
MSG_CHANNEL_CLOSE, struct.pack(">L", self.channelsToRemoteChannel[channel])
|
||||
)
|
||||
channel.localClosed = True
|
||||
if channel.localClosed and channel.remoteClosed:
|
||||
self.channelClosed(channel)
|
||||
|
||||
# methods to override
|
||||
def getChannel(self, channelType, windowSize, maxPacket, data):
|
||||
"""
|
||||
The other side requested a channel of some sort.
|
||||
channelType is the type of channel being requested,
|
||||
windowSize is the initial size of the remote window,
|
||||
maxPacket is the largest packet we should send,
|
||||
data is any other packet data (often nothing).
|
||||
|
||||
We return a subclass of L{SSHChannel}.
|
||||
|
||||
By default, this dispatches to a method 'channel_channelType' with any
|
||||
non-alphanumerics in the channelType replace with _'s. If it cannot
|
||||
find a suitable method, it returns an OPEN_UNKNOWN_CHANNEL_TYPE error.
|
||||
The method is called with arguments of windowSize, maxPacket, data.
|
||||
|
||||
@type channelType: L{bytes}
|
||||
@type windowSize: L{int}
|
||||
@type maxPacket: L{int}
|
||||
@type data: L{bytes}
|
||||
@rtype: subclass of L{SSHChannel}/L{tuple}
|
||||
"""
|
||||
self._log.debug("got channel {channelType!r} request", channelType=channelType)
|
||||
if hasattr(self.transport, "avatar"): # this is a server!
|
||||
chan = self.transport.avatar.lookupChannel(
|
||||
channelType, windowSize, maxPacket, data
|
||||
)
|
||||
else:
|
||||
channelType = channelType.translate(TRANSLATE_TABLE)
|
||||
attr = "channel_%s" % nativeString(channelType)
|
||||
f = getattr(self, attr, None)
|
||||
if f is not None:
|
||||
chan = f(windowSize, maxPacket, data)
|
||||
else:
|
||||
chan = None
|
||||
if chan is None:
|
||||
raise error.ConchError("unknown channel", OPEN_UNKNOWN_CHANNEL_TYPE)
|
||||
else:
|
||||
chan.conn = self
|
||||
return chan
|
||||
|
||||
def gotGlobalRequest(self, requestType, data):
|
||||
"""
|
||||
We got a global request. pretty much, this is just used by the client
|
||||
to request that we forward a port from the server to the client.
|
||||
Returns either:
|
||||
- 1: request accepted
|
||||
- 1, <data>: request accepted with request specific data
|
||||
- 0: request denied
|
||||
|
||||
By default, this dispatches to a method 'global_requestType' with
|
||||
-'s in requestType replaced with _'s. The found method is passed data.
|
||||
If this method cannot be found, this method returns 0. Otherwise, it
|
||||
returns the return value of that method.
|
||||
|
||||
@type requestType: L{bytes}
|
||||
@type data: L{bytes}
|
||||
@rtype: L{int}/L{tuple}
|
||||
"""
|
||||
self._log.debug("got global {requestType} request", requestType=requestType)
|
||||
if hasattr(self.transport, "avatar"): # this is a server!
|
||||
return self.transport.avatar.gotGlobalRequest(requestType, data)
|
||||
|
||||
requestType = nativeString(requestType.replace(b"-", b"_"))
|
||||
f = getattr(self, "global_%s" % requestType, None)
|
||||
if not f:
|
||||
return 0
|
||||
return f(data)
|
||||
|
||||
def channelClosed(self, channel):
|
||||
"""
|
||||
Called when a channel is closed.
|
||||
It clears the local state related to the channel, and calls
|
||||
channel.closed().
|
||||
MAKE SURE YOU CALL THIS METHOD, even if you subclass L{SSHConnection}.
|
||||
If you don't, things will break mysteriously.
|
||||
|
||||
@type channel: L{SSHChannel}
|
||||
"""
|
||||
if channel in self.channelsToRemoteChannel: # actually open
|
||||
channel.localClosed = channel.remoteClosed = True
|
||||
del self.localToRemoteChannel[channel.id]
|
||||
del self.channels[channel.id]
|
||||
del self.channelsToRemoteChannel[channel]
|
||||
for d in self.deferreds.pop(channel.id, []):
|
||||
d.errback(error.ConchError("Channel closed."))
|
||||
channel.closed()
|
||||
|
||||
|
||||
MSG_GLOBAL_REQUEST = 80
|
||||
MSG_REQUEST_SUCCESS = 81
|
||||
MSG_REQUEST_FAILURE = 82
|
||||
MSG_CHANNEL_OPEN = 90
|
||||
MSG_CHANNEL_OPEN_CONFIRMATION = 91
|
||||
MSG_CHANNEL_OPEN_FAILURE = 92
|
||||
MSG_CHANNEL_WINDOW_ADJUST = 93
|
||||
MSG_CHANNEL_DATA = 94
|
||||
MSG_CHANNEL_EXTENDED_DATA = 95
|
||||
MSG_CHANNEL_EOF = 96
|
||||
MSG_CHANNEL_CLOSE = 97
|
||||
MSG_CHANNEL_REQUEST = 98
|
||||
MSG_CHANNEL_SUCCESS = 99
|
||||
MSG_CHANNEL_FAILURE = 100
|
||||
|
||||
OPEN_ADMINISTRATIVELY_PROHIBITED = 1
|
||||
OPEN_CONNECT_FAILED = 2
|
||||
OPEN_UNKNOWN_CHANNEL_TYPE = 3
|
||||
OPEN_RESOURCE_SHORTAGE = 4
|
||||
|
||||
# From RFC 4254
|
||||
EXTENDED_DATA_STDERR = 1
|
||||
|
||||
messages = {}
|
||||
for name, value in locals().copy().items():
|
||||
if name[:4] == "MSG_":
|
||||
messages[value] = name # Doesn't handle doubles
|
||||
|
||||
alphanums = networkString(string.ascii_letters + string.digits)
|
||||
TRANSLATE_TABLE = bytes(i if i in alphanums else ord("_") for i in range(256))
|
||||
SSHConnection.protocolMessages = messages
|
||||
@@ -0,0 +1,126 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
A Factory for SSH servers.
|
||||
|
||||
See also L{twisted.conch.openssh_compat.factory} for OpenSSH compatibility.
|
||||
|
||||
Maintainer: Paul Swartz
|
||||
"""
|
||||
|
||||
|
||||
import random
|
||||
from itertools import chain
|
||||
|
||||
from twisted.conch import error
|
||||
from twisted.conch.ssh import _kex, connection, transport, userauth
|
||||
from twisted.internet import protocol
|
||||
from twisted.logger import Logger
|
||||
|
||||
|
||||
class SSHFactory(protocol.Factory):
|
||||
"""
|
||||
A Factory for SSH servers.
|
||||
"""
|
||||
|
||||
_log = Logger()
|
||||
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 = list(
|
||||
chain.from_iterable(
|
||||
key.supportedSignatureAlgorithms() for key in self.privateKeys.values()
|
||||
)
|
||||
)
|
||||
if not self.primes:
|
||||
self._log.info(
|
||||
"disabling non-fixed-group key exchange algorithms "
|
||||
"because we cannot find moduli file"
|
||||
)
|
||||
t.supportedKeyExchanges = [
|
||||
kexAlgorithm
|
||||
for kexAlgorithm in t.supportedKeyExchanges
|
||||
if _kex.isFixedGroup(kexAlgorithm) or _kex.isEllipticCurve(kexAlgorithm)
|
||||
]
|
||||
return t
|
||||
|
||||
def getPublicKeys(self):
|
||||
"""
|
||||
Called when the factory is started to get the public portions of the
|
||||
servers host keys. Returns a dictionary mapping SSH key types to
|
||||
public key strings.
|
||||
|
||||
@rtype: L{dict}
|
||||
"""
|
||||
raise NotImplementedError("getPublicKeys unimplemented")
|
||||
|
||||
def getPrivateKeys(self):
|
||||
"""
|
||||
Called when the factory is started to get the private portions of the
|
||||
servers host keys. Returns a dictionary mapping SSH key types to
|
||||
L{twisted.conch.ssh.keys.Key} objects.
|
||||
|
||||
@rtype: L{dict}
|
||||
"""
|
||||
raise NotImplementedError("getPrivateKeys unimplemented")
|
||||
|
||||
def getPrimes(self):
|
||||
"""
|
||||
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]
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,274 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
This module contains the implementation of the TCP forwarding, which allows
|
||||
clients and servers to forward arbitrary TCP data across the connection.
|
||||
|
||||
Maintainer: Paul Swartz
|
||||
"""
|
||||
|
||||
|
||||
import struct
|
||||
|
||||
from twisted.conch.ssh import channel, common
|
||||
from twisted.internet import protocol, reactor
|
||||
from twisted.internet.endpoints import HostnameEndpoint, connectProtocol
|
||||
|
||||
|
||||
class SSHListenForwardingFactory(protocol.Factory):
|
||||
def __init__(self, connection, hostport, klass):
|
||||
self.conn = connection
|
||||
self.hostport = hostport # tuple
|
||||
self.klass = klass
|
||||
|
||||
def buildProtocol(self, addr):
|
||||
channel = self.klass(conn=self.conn)
|
||||
client = SSHForwardingClient(channel)
|
||||
channel.client = client
|
||||
addrTuple = (addr.host, addr.port)
|
||||
channelOpenData = packOpen_direct_tcpip(self.hostport, addrTuple)
|
||||
self.conn.openChannel(channel, channelOpenData)
|
||||
return client
|
||||
|
||||
|
||||
class SSHListenForwardingChannel(channel.SSHChannel):
|
||||
def channelOpen(self, specificData):
|
||||
self._log.info("opened forwarding channel {id}", id=self.id)
|
||||
if len(self.client.buf) > 1:
|
||||
b = self.client.buf[1:]
|
||||
self.write(b)
|
||||
self.client.buf = b""
|
||||
|
||||
def openFailed(self, reason):
|
||||
self.closed()
|
||||
|
||||
def dataReceived(self, data):
|
||||
self.client.transport.write(data)
|
||||
|
||||
def eofReceived(self):
|
||||
self.client.transport.loseConnection()
|
||||
|
||||
def closed(self):
|
||||
if hasattr(self, "client"):
|
||||
self._log.info("closing local forwarding channel {id}", id=self.id)
|
||||
self.client.transport.loseConnection()
|
||||
del self.client
|
||||
|
||||
|
||||
class SSHListenClientForwardingChannel(SSHListenForwardingChannel):
|
||||
|
||||
name = b"direct-tcpip"
|
||||
|
||||
|
||||
class SSHListenServerForwardingChannel(SSHListenForwardingChannel):
|
||||
|
||||
name = b"forwarded-tcpip"
|
||||
|
||||
|
||||
class SSHConnectForwardingChannel(channel.SSHChannel):
|
||||
"""
|
||||
Channel used for handling server side forwarding request.
|
||||
It acts as a client for the remote forwarding destination.
|
||||
|
||||
@ivar hostport: C{(host, port)} requested by client as forwarding
|
||||
destination.
|
||||
@type hostport: L{tuple} or a C{sequence}
|
||||
|
||||
@ivar client: Protocol connected to the forwarding destination.
|
||||
@type client: L{protocol.Protocol}
|
||||
|
||||
@ivar clientBuf: Data received while forwarding channel is not yet
|
||||
connected.
|
||||
@type clientBuf: L{bytes}
|
||||
|
||||
@var _reactor: Reactor used for TCP connections.
|
||||
@type _reactor: A reactor.
|
||||
|
||||
@ivar _channelOpenDeferred: Deferred used in testing to check the
|
||||
result of C{channelOpen}.
|
||||
@type _channelOpenDeferred: L{twisted.internet.defer.Deferred}
|
||||
"""
|
||||
|
||||
_reactor = reactor
|
||||
|
||||
def __init__(self, hostport, *args, **kw):
|
||||
channel.SSHChannel.__init__(self, *args, **kw)
|
||||
self.hostport = hostport
|
||||
self.client = None
|
||||
self.clientBuf = b""
|
||||
|
||||
def channelOpen(self, specificData):
|
||||
"""
|
||||
See: L{channel.SSHChannel}
|
||||
"""
|
||||
self._log.info(
|
||||
"connecting to {host}:{port}", host=self.hostport[0], port=self.hostport[1]
|
||||
)
|
||||
ep = HostnameEndpoint(self._reactor, self.hostport[0], self.hostport[1])
|
||||
d = connectProtocol(ep, SSHForwardingClient(self))
|
||||
d.addCallbacks(self._setClient, self._close)
|
||||
self._channelOpenDeferred = d
|
||||
|
||||
def _setClient(self, client):
|
||||
"""
|
||||
Called when the connection was established to the forwarding
|
||||
destination.
|
||||
|
||||
@param client: Client protocol connected to the forwarding destination.
|
||||
@type client: L{protocol.Protocol}
|
||||
"""
|
||||
self.client = client
|
||||
self._log.info(
|
||||
"connected to {host}:{port}", host=self.hostport[0], port=self.hostport[1]
|
||||
)
|
||||
if self.clientBuf:
|
||||
self.client.transport.write(self.clientBuf)
|
||||
self.clientBuf = None
|
||||
if self.client.buf[1:]:
|
||||
self.write(self.client.buf[1:])
|
||||
self.client.buf = b""
|
||||
|
||||
def _close(self, reason):
|
||||
"""
|
||||
Called when failed to connect to the forwarding destination.
|
||||
|
||||
@param reason: Reason why connection failed.
|
||||
@type reason: L{twisted.python.failure.Failure}
|
||||
"""
|
||||
self._log.error(
|
||||
"failed to connect to {host}:{port}: {reason}",
|
||||
host=self.hostport[0],
|
||||
port=self.hostport[1],
|
||||
reason=reason,
|
||||
)
|
||||
self.loseConnection()
|
||||
|
||||
def dataReceived(self, data):
|
||||
"""
|
||||
See: L{channel.SSHChannel}
|
||||
"""
|
||||
if self.client:
|
||||
self.client.transport.write(data)
|
||||
else:
|
||||
self.clientBuf += data
|
||||
|
||||
def closed(self):
|
||||
"""
|
||||
See: L{channel.SSHChannel}
|
||||
"""
|
||||
if self.client:
|
||||
self._log.info("closed remote forwarding channel {id}", id=self.id)
|
||||
if self.client.channel:
|
||||
self.loseConnection()
|
||||
self.client.transport.loseConnection()
|
||||
del self.client
|
||||
|
||||
|
||||
def openConnectForwardingClient(remoteWindow, remoteMaxPacket, data, avatar):
|
||||
remoteHP, origHP = unpackOpen_direct_tcpip(data)
|
||||
return SSHConnectForwardingChannel(
|
||||
remoteHP,
|
||||
remoteWindow=remoteWindow,
|
||||
remoteMaxPacket=remoteMaxPacket,
|
||||
avatar=avatar,
|
||||
)
|
||||
|
||||
|
||||
class SSHForwardingClient(protocol.Protocol):
|
||||
def __init__(self, channel):
|
||||
self.channel = channel
|
||||
self.buf = b"\000"
|
||||
|
||||
def dataReceived(self, data):
|
||||
if self.buf:
|
||||
self.buf += data
|
||||
else:
|
||||
self.channel.write(data)
|
||||
|
||||
def connectionLost(self, reason):
|
||||
if self.channel:
|
||||
self.channel.loseConnection()
|
||||
self.channel = None
|
||||
|
||||
|
||||
def packOpen_direct_tcpip(destination, source):
|
||||
"""
|
||||
Pack the data suitable for sending in a CHANNEL_OPEN packet.
|
||||
|
||||
@type destination: L{tuple}
|
||||
@param destination: A tuple of the (host, port) of the destination host.
|
||||
|
||||
@type source: L{tuple}
|
||||
@param source: A tuple of the (host, port) of the source host.
|
||||
"""
|
||||
(connHost, connPort) = destination
|
||||
(origHost, origPort) = source
|
||||
if isinstance(connHost, str):
|
||||
connHost = connHost.encode("utf-8")
|
||||
if isinstance(origHost, str):
|
||||
origHost = origHost.encode("utf-8")
|
||||
conn = common.NS(connHost) + struct.pack(">L", connPort)
|
||||
orig = common.NS(origHost) + struct.pack(">L", origPort)
|
||||
return conn + orig
|
||||
|
||||
|
||||
packOpen_forwarded_tcpip = packOpen_direct_tcpip
|
||||
|
||||
|
||||
def unpackOpen_direct_tcpip(data):
|
||||
"""Unpack the data to a usable format."""
|
||||
connHost, rest = common.getNS(data)
|
||||
if isinstance(connHost, bytes):
|
||||
connHost = connHost.decode("utf-8")
|
||||
connPort = int(struct.unpack(">L", rest[:4])[0])
|
||||
origHost, rest = common.getNS(rest[4:])
|
||||
if isinstance(origHost, bytes):
|
||||
origHost = origHost.decode("utf-8")
|
||||
origPort = int(struct.unpack(">L", rest[:4])[0])
|
||||
return (connHost, connPort), (origHost, origPort)
|
||||
|
||||
|
||||
unpackOpen_forwarded_tcpip = unpackOpen_direct_tcpip
|
||||
|
||||
|
||||
def packGlobal_tcpip_forward(peer):
|
||||
"""
|
||||
Pack the data for tcpip forwarding.
|
||||
|
||||
@param peer: A tuple of the (host, port) .
|
||||
@type peer: L{tuple}
|
||||
"""
|
||||
(host, port) = peer
|
||||
return common.NS(host) + struct.pack(">L", port)
|
||||
|
||||
|
||||
def unpackGlobal_tcpip_forward(data):
|
||||
host, rest = common.getNS(data)
|
||||
if isinstance(host, bytes):
|
||||
host = host.decode("utf-8")
|
||||
port = int(struct.unpack(">L", rest[:4])[0])
|
||||
return host, port
|
||||
|
||||
|
||||
"""This is how the data -> eof -> close stuff /should/ work.
|
||||
|
||||
debug3: channel 1: waiting for connection
|
||||
debug1: channel 1: connected
|
||||
debug1: channel 1: read<=0 rfd 7 len 0
|
||||
debug1: channel 1: read failed
|
||||
debug1: channel 1: close_read
|
||||
debug1: channel 1: input open -> drain
|
||||
debug1: channel 1: ibuf empty
|
||||
debug1: channel 1: send eof
|
||||
debug1: channel 1: input drain -> closed
|
||||
debug1: channel 1: rcvd eof
|
||||
debug1: channel 1: output open -> drain
|
||||
debug1: channel 1: obuf empty
|
||||
debug1: channel 1: close_write
|
||||
debug1: channel 1: output drain -> closed
|
||||
debug1: channel 1: rcvd close
|
||||
debug3: channel 1: will not send data after close
|
||||
debug1: channel 1: send close
|
||||
debug1: channel 1: is dead
|
||||
"""
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,56 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
The parent class for all the SSH services. Currently implemented services
|
||||
are ssh-userauth and ssh-connection.
|
||||
|
||||
Maintainer: Paul Swartz
|
||||
"""
|
||||
|
||||
from typing import Dict
|
||||
|
||||
from twisted.logger import Logger
|
||||
|
||||
|
||||
class SSHService:
|
||||
# this is the ssh name for the service:
|
||||
name: bytes = None # type:ignore[assignment]
|
||||
|
||||
protocolMessages: Dict[int, str] = {} # map #'s -> protocol names
|
||||
transport = None # gets set later
|
||||
|
||||
_log = Logger()
|
||||
|
||||
def serviceStarted(self):
|
||||
"""
|
||||
called when the service is active on the transport.
|
||||
"""
|
||||
|
||||
def serviceStopped(self):
|
||||
"""
|
||||
called when the service is stopped, either by the connection ending
|
||||
or by another service being started
|
||||
"""
|
||||
|
||||
def logPrefix(self):
|
||||
return "SSHService {!r} on {}".format(
|
||||
self.name, self.transport.transport.logPrefix()
|
||||
)
|
||||
|
||||
def packetReceived(self, messageNum, packet):
|
||||
"""
|
||||
called when we receive a packet on the transport
|
||||
"""
|
||||
# print self.protocolMessages
|
||||
if messageNum in self.protocolMessages:
|
||||
messageType = self.protocolMessages[messageNum]
|
||||
f = getattr(self, "ssh_%s" % messageType[4:], None)
|
||||
if f is not None:
|
||||
return f(packet)
|
||||
self._log.info(
|
||||
"couldn't handle {messageNum} {packet!r}",
|
||||
messageNum=messageNum,
|
||||
packet=packet,
|
||||
)
|
||||
self.transport.sendUnimplemented()
|
||||
@@ -0,0 +1,440 @@
|
||||
# -*- test-case-name: twisted.conch.test.test_session -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
This module contains the implementation of SSHSession, which (by default)
|
||||
allows access to a shell and a python interpreter over SSH.
|
||||
|
||||
Maintainer: Paul Swartz
|
||||
"""
|
||||
|
||||
|
||||
import os
|
||||
import signal
|
||||
import struct
|
||||
import sys
|
||||
|
||||
from zope.interface import implementer
|
||||
|
||||
from twisted.conch.interfaces import (
|
||||
EnvironmentVariableNotPermitted,
|
||||
ISession,
|
||||
ISessionSetEnv,
|
||||
)
|
||||
from twisted.conch.ssh import channel, common, connection
|
||||
from twisted.internet import interfaces, protocol
|
||||
from twisted.logger import Logger
|
||||
from twisted.python.compat import networkString
|
||||
|
||||
log = Logger()
|
||||
|
||||
|
||||
class SSHSession(channel.SSHChannel):
|
||||
"""
|
||||
A generalized implementation of an SSH session.
|
||||
|
||||
See RFC 4254, section 6.
|
||||
|
||||
The precise implementation of the various operations that the remote end
|
||||
can send is left up to the avatar, usually via an adapter to an
|
||||
interface such as L{ISession}.
|
||||
|
||||
@ivar buf: a buffer for data received before making a connection to a
|
||||
client.
|
||||
@type buf: L{bytes}
|
||||
@ivar client: a protocol for communication with a shell, an application
|
||||
program, or a subsystem (see RFC 4254, section 6.5).
|
||||
@type client: L{SSHSessionProcessProtocol}
|
||||
@ivar session: an object providing concrete implementations of session
|
||||
operations.
|
||||
@type session: L{ISession}
|
||||
"""
|
||||
|
||||
name = b"session"
|
||||
|
||||
def __init__(self, *args, **kw):
|
||||
channel.SSHChannel.__init__(self, *args, **kw)
|
||||
self.buf = b""
|
||||
self.client = None
|
||||
self.session = None
|
||||
|
||||
def request_subsystem(self, data):
|
||||
subsystem, ignored = common.getNS(data)
|
||||
log.info('Asking for subsystem "{subsystem}"', subsystem=subsystem)
|
||||
client = self.avatar.lookupSubsystem(subsystem, data)
|
||||
if client:
|
||||
pp = SSHSessionProcessProtocol(self)
|
||||
proto = wrapProcessProtocol(pp)
|
||||
client.makeConnection(proto)
|
||||
pp.makeConnection(wrapProtocol(client))
|
||||
self.client = pp
|
||||
return 1
|
||||
else:
|
||||
log.error("Failed to get subsystem")
|
||||
return 0
|
||||
|
||||
def request_shell(self, data):
|
||||
log.info("Getting shell")
|
||||
if not self.session:
|
||||
self.session = ISession(self.avatar)
|
||||
try:
|
||||
pp = SSHSessionProcessProtocol(self)
|
||||
self.session.openShell(pp)
|
||||
except Exception:
|
||||
log.failure("Error getting shell")
|
||||
return 0
|
||||
else:
|
||||
self.client = pp
|
||||
return 1
|
||||
|
||||
def request_exec(self, data):
|
||||
if not self.session:
|
||||
self.session = ISession(self.avatar)
|
||||
f, data = common.getNS(data)
|
||||
log.info('Executing command "{f}"', f=f)
|
||||
try:
|
||||
pp = SSHSessionProcessProtocol(self)
|
||||
self.session.execCommand(pp, f)
|
||||
except Exception:
|
||||
log.failure('Error executing command "{f}"', f=f)
|
||||
return 0
|
||||
else:
|
||||
self.client = pp
|
||||
return 1
|
||||
|
||||
def request_pty_req(self, data):
|
||||
if not self.session:
|
||||
self.session = ISession(self.avatar)
|
||||
term, windowSize, modes = parseRequest_pty_req(data)
|
||||
log.info(
|
||||
"Handling pty request: {term!r} {windowSize!r}",
|
||||
term=term,
|
||||
windowSize=windowSize,
|
||||
)
|
||||
try:
|
||||
self.session.getPty(term, windowSize, modes)
|
||||
except Exception:
|
||||
log.failure("Error handling pty request")
|
||||
return 0
|
||||
else:
|
||||
return 1
|
||||
|
||||
def request_env(self, data):
|
||||
"""
|
||||
Process a request to pass an environment variable.
|
||||
|
||||
@param data: The environment variable name and value, each encoded
|
||||
as an SSH protocol string and concatenated.
|
||||
@type data: L{bytes}
|
||||
@return: A true value if the request to pass this environment
|
||||
variable was accepted, otherwise a false value.
|
||||
"""
|
||||
if not self.session:
|
||||
self.session = ISession(self.avatar)
|
||||
if not ISessionSetEnv.providedBy(self.session):
|
||||
return 0
|
||||
name, value, data = common.getNS(data, 2)
|
||||
try:
|
||||
self.session.setEnv(name, value)
|
||||
except EnvironmentVariableNotPermitted:
|
||||
return 0
|
||||
except Exception:
|
||||
log.failure("Error setting environment variable {name}", name=name)
|
||||
return 0
|
||||
else:
|
||||
return 1
|
||||
|
||||
def request_window_change(self, data):
|
||||
if not self.session:
|
||||
self.session = ISession(self.avatar)
|
||||
winSize = parseRequest_window_change(data)
|
||||
try:
|
||||
self.session.windowChanged(winSize)
|
||||
except Exception:
|
||||
log.failure("Error changing window size")
|
||||
return 0
|
||||
else:
|
||||
return 1
|
||||
|
||||
def dataReceived(self, data):
|
||||
if not self.client:
|
||||
# self.conn.sendClose(self)
|
||||
self.buf += data
|
||||
return
|
||||
self.client.transport.write(data)
|
||||
|
||||
def extReceived(self, dataType, data):
|
||||
if dataType == connection.EXTENDED_DATA_STDERR:
|
||||
if self.client and hasattr(self.client.transport, "writeErr"):
|
||||
self.client.transport.writeErr(data)
|
||||
else:
|
||||
log.warn("Weird extended data: {dataType}", dataType=dataType)
|
||||
|
||||
def eofReceived(self):
|
||||
# If we have a session, tell it that EOF has been received and
|
||||
# expect it to send a close message (it may need to send other
|
||||
# messages such as exit-status or exit-signal first). If we don't
|
||||
# have a session, then just send a close message directly.
|
||||
if self.session:
|
||||
self.session.eofReceived()
|
||||
elif self.client:
|
||||
self.conn.sendClose(self)
|
||||
|
||||
def closed(self):
|
||||
if self.client and self.client.transport:
|
||||
self.client.transport.loseConnection()
|
||||
if self.session:
|
||||
self.session.closed()
|
||||
|
||||
# def closeReceived(self):
|
||||
# self.loseConnection() # don't know what to do with this
|
||||
|
||||
def loseConnection(self):
|
||||
if self.client:
|
||||
self.client.transport.loseConnection()
|
||||
channel.SSHChannel.loseConnection(self)
|
||||
|
||||
|
||||
class _ProtocolWrapper(protocol.ProcessProtocol):
|
||||
"""
|
||||
This class wraps a L{Protocol} instance in a L{ProcessProtocol} instance.
|
||||
"""
|
||||
|
||||
def __init__(self, proto):
|
||||
self.proto = proto
|
||||
|
||||
def connectionMade(self):
|
||||
self.proto.connectionMade()
|
||||
|
||||
def outReceived(self, data):
|
||||
self.proto.dataReceived(data)
|
||||
|
||||
def processEnded(self, reason):
|
||||
self.proto.connectionLost(reason)
|
||||
|
||||
|
||||
class _DummyTransport:
|
||||
def __init__(self, proto):
|
||||
self.proto = proto
|
||||
|
||||
def dataReceived(self, data):
|
||||
self.proto.transport.write(data)
|
||||
|
||||
def write(self, data):
|
||||
self.proto.dataReceived(data)
|
||||
|
||||
def writeSequence(self, seq):
|
||||
self.write(b"".join(seq))
|
||||
|
||||
def loseConnection(self):
|
||||
self.proto.connectionLost(protocol.connectionDone)
|
||||
|
||||
|
||||
def wrapProcessProtocol(inst):
|
||||
if isinstance(inst, protocol.Protocol):
|
||||
return _ProtocolWrapper(inst)
|
||||
else:
|
||||
return inst
|
||||
|
||||
|
||||
def wrapProtocol(proto):
|
||||
return _DummyTransport(proto)
|
||||
|
||||
|
||||
# SUPPORTED_SIGNALS is a list of signals that every session channel is supposed
|
||||
# to accept. See RFC 4254
|
||||
SUPPORTED_SIGNALS = [
|
||||
"ABRT",
|
||||
"ALRM",
|
||||
"FPE",
|
||||
"HUP",
|
||||
"ILL",
|
||||
"INT",
|
||||
"KILL",
|
||||
"PIPE",
|
||||
"QUIT",
|
||||
"SEGV",
|
||||
"TERM",
|
||||
"USR1",
|
||||
"USR2",
|
||||
]
|
||||
|
||||
|
||||
@implementer(interfaces.ITransport)
|
||||
class SSHSessionProcessProtocol(protocol.ProcessProtocol):
|
||||
"""I am both an L{IProcessProtocol} and an L{ITransport}.
|
||||
|
||||
I am a transport to the remote endpoint and a process protocol to the
|
||||
local subsystem.
|
||||
"""
|
||||
|
||||
# once initialized, a dictionary mapping signal values to strings
|
||||
# that follow RFC 4254.
|
||||
_signalValuesToNames = None
|
||||
|
||||
def __init__(self, session):
|
||||
self.session = session
|
||||
self.lostOutOrErrFlag = False
|
||||
|
||||
def connectionMade(self):
|
||||
if self.session.buf:
|
||||
self.transport.write(self.session.buf)
|
||||
self.session.buf = None
|
||||
|
||||
def outReceived(self, data):
|
||||
self.session.write(data)
|
||||
|
||||
def errReceived(self, err):
|
||||
self.session.writeExtended(connection.EXTENDED_DATA_STDERR, err)
|
||||
|
||||
def outConnectionLost(self):
|
||||
"""
|
||||
EOF should only be sent when both STDOUT and STDERR have been closed.
|
||||
"""
|
||||
if self.lostOutOrErrFlag:
|
||||
self.session.conn.sendEOF(self.session)
|
||||
else:
|
||||
self.lostOutOrErrFlag = True
|
||||
|
||||
def errConnectionLost(self):
|
||||
"""
|
||||
See outConnectionLost().
|
||||
"""
|
||||
self.outConnectionLost()
|
||||
|
||||
def connectionLost(self, reason=None):
|
||||
self.session.loseConnection()
|
||||
|
||||
def _getSignalName(self, signum):
|
||||
"""
|
||||
Get a signal name given a signal number.
|
||||
"""
|
||||
if self._signalValuesToNames is None:
|
||||
self._signalValuesToNames = {}
|
||||
# make sure that the POSIX ones are the defaults
|
||||
for signame in SUPPORTED_SIGNALS:
|
||||
signame = "SIG" + signame
|
||||
sigvalue = getattr(signal, signame, None)
|
||||
if sigvalue is not None:
|
||||
self._signalValuesToNames[sigvalue] = signame
|
||||
for k, v in signal.__dict__.items():
|
||||
# Check for platform specific signals, ignoring Python specific
|
||||
# SIG_DFL and SIG_IGN
|
||||
if k.startswith("SIG") and not k.startswith("SIG_"):
|
||||
if v not in self._signalValuesToNames:
|
||||
self._signalValuesToNames[v] = k + "@" + sys.platform
|
||||
return self._signalValuesToNames[signum]
|
||||
|
||||
def processEnded(self, reason=None):
|
||||
"""
|
||||
When we are told the process ended, try to notify the other side about
|
||||
how the process ended using the exit-signal or exit-status requests.
|
||||
Also, close the channel.
|
||||
"""
|
||||
if reason is not None:
|
||||
err = reason.value
|
||||
if err.signal is not None:
|
||||
signame = self._getSignalName(err.signal)
|
||||
if getattr(os, "WCOREDUMP", None) is not None and os.WCOREDUMP(
|
||||
err.status
|
||||
):
|
||||
log.info("exitSignal: {signame} (core dumped)", signame=signame)
|
||||
coreDumped = True
|
||||
else:
|
||||
log.info("exitSignal: {}", signame=signame)
|
||||
coreDumped = False
|
||||
self.session.conn.sendRequest(
|
||||
self.session,
|
||||
b"exit-signal",
|
||||
common.NS(networkString(signame[3:]))
|
||||
+ (b"\1" if coreDumped else b"\0")
|
||||
+ common.NS(b"")
|
||||
+ common.NS(b""),
|
||||
)
|
||||
elif err.exitCode is not None:
|
||||
log.info("exitCode: {exitCode!r}", exitCode=err.exitCode)
|
||||
self.session.conn.sendRequest(
|
||||
self.session, b"exit-status", struct.pack(">L", err.exitCode)
|
||||
)
|
||||
self.session.loseConnection()
|
||||
|
||||
def getHost(self):
|
||||
"""
|
||||
Return the host from my session's transport.
|
||||
"""
|
||||
return self.session.conn.transport.getHost()
|
||||
|
||||
def getPeer(self):
|
||||
"""
|
||||
Return the peer from my session's transport.
|
||||
"""
|
||||
return self.session.conn.transport.getPeer()
|
||||
|
||||
def write(self, data):
|
||||
self.session.write(data)
|
||||
|
||||
def writeSequence(self, seq):
|
||||
self.session.write(b"".join(seq))
|
||||
|
||||
def loseConnection(self):
|
||||
self.session.loseConnection()
|
||||
|
||||
|
||||
class SSHSessionClient(protocol.Protocol):
|
||||
def dataReceived(self, data):
|
||||
if self.transport:
|
||||
self.transport.write(data)
|
||||
|
||||
|
||||
# methods factored out to make live easier on server writers
|
||||
def parseRequest_pty_req(data):
|
||||
"""Parse the data from a pty-req request into usable data.
|
||||
|
||||
@returns: a tuple of (terminal type, (rows, cols, xpixel, ypixel), modes)
|
||||
"""
|
||||
term, rest = common.getNS(data)
|
||||
cols, rows, xpixel, ypixel = struct.unpack(">4L", rest[:16])
|
||||
modes, ignored = common.getNS(rest[16:])
|
||||
winSize = (rows, cols, xpixel, ypixel)
|
||||
modes = [
|
||||
(ord(modes[i : i + 1]), struct.unpack(">L", modes[i + 1 : i + 5])[0])
|
||||
for i in range(0, len(modes) - 1, 5)
|
||||
]
|
||||
return term, winSize, modes
|
||||
|
||||
|
||||
def packRequest_pty_req(term, geometry, modes):
|
||||
"""
|
||||
Pack a pty-req request so that it is suitable for sending.
|
||||
|
||||
NOTE: modes must be packed before being sent here.
|
||||
|
||||
@type geometry: L{tuple}
|
||||
@param geometry: A tuple of (rows, columns, xpixel, ypixel)
|
||||
"""
|
||||
(rows, cols, xpixel, ypixel) = geometry
|
||||
termPacked = common.NS(term)
|
||||
winSizePacked = struct.pack(">4L", cols, rows, xpixel, ypixel)
|
||||
modesPacked = common.NS(modes) # depend on the client packing modes
|
||||
return termPacked + winSizePacked + modesPacked
|
||||
|
||||
|
||||
def parseRequest_window_change(data):
|
||||
"""Parse the data from a window-change request into usuable data.
|
||||
|
||||
@returns: a tuple of (rows, cols, xpixel, ypixel)
|
||||
"""
|
||||
cols, rows, xpixel, ypixel = struct.unpack(">4L", data)
|
||||
return rows, cols, xpixel, ypixel
|
||||
|
||||
|
||||
def packRequest_window_change(geometry):
|
||||
"""
|
||||
Pack a window-change request so that it is suitable for sending.
|
||||
|
||||
@type geometry: L{tuple}
|
||||
@param geometry: A tuple of (rows, columns, xpixel, ypixel)
|
||||
"""
|
||||
(rows, cols, xpixel, ypixel) = geometry
|
||||
return struct.pack(">4L", cols, rows, xpixel, ypixel)
|
||||
@@ -0,0 +1,40 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
|
||||
def parse(s):
|
||||
s = s.strip()
|
||||
expr = []
|
||||
while s:
|
||||
if s[0:1] == b"(":
|
||||
newSexp = []
|
||||
if expr:
|
||||
expr[-1].append(newSexp)
|
||||
expr.append(newSexp)
|
||||
s = s[1:]
|
||||
continue
|
||||
if s[0:1] == b")":
|
||||
aList = expr.pop()
|
||||
s = s[1:]
|
||||
if not expr:
|
||||
assert not s
|
||||
return aList
|
||||
continue
|
||||
i = 0
|
||||
while s[i : i + 1].isdigit():
|
||||
i += 1
|
||||
assert i
|
||||
length = int(s[:i])
|
||||
data = s[i + 1 : i + 1 + length]
|
||||
expr[-1].append(data)
|
||||
s = s[i + 1 + length :]
|
||||
assert False, "this should not happen"
|
||||
|
||||
|
||||
def pack(sexp):
|
||||
return b"".join(
|
||||
b"(%b)" % (pack(o),)
|
||||
if type(o) in (type(()), type([]))
|
||||
else b"%d:%b" % (len(o), o)
|
||||
for o in sexp
|
||||
)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,764 @@
|
||||
# -*- 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
|
||||
"""
|
||||
|
||||
|
||||
import struct
|
||||
|
||||
from twisted.conch import error, interfaces
|
||||
from twisted.conch.ssh import keys, service, transport
|
||||
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.logger import Logger
|
||||
from twisted.python import failure
|
||||
from twisted.python.compat import nativeString
|
||||
|
||||
|
||||
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",
|
||||
}
|
||||
_log = Logger()
|
||||
|
||||
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}
|
||||
"""
|
||||
self._log.debug("{user!r} trying auth {kind!r}", user=user, kind=kind)
|
||||
if kind not in self.supportedAuthentications:
|
||||
return defer.fail(error.ConchError("unsupported authentication, failing"))
|
||||
kind = nativeString(kind.replace(b"-", b"_"))
|
||||
f = getattr(self, f"auth_{kind}", None)
|
||||
if f:
|
||||
ret = f(data)
|
||||
if not ret:
|
||||
return defer.fail(
|
||||
error.ConchError(f"{kind} return None instead of a Deferred")
|
||||
)
|
||||
else:
|
||||
return ret
|
||||
return defer.fail(error.ConchError(f"bad auth type: {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(f"could not get next service: {self.nextService}")
|
||||
self._log.debug(
|
||||
"{user!r} authenticated with {method!r}", user=self.user, method=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":
|
||||
self._log.debug(
|
||||
"{user!r} failed auth {method!r}", user=self.user, method=self.method
|
||||
)
|
||||
if reason.check(UnauthorizedLogin):
|
||||
self._log.debug(
|
||||
"unauthorized login: {message}", message=reason.getErrorMessage()
|
||||
)
|
||||
elif reason.check(error.ConchError):
|
||||
self._log.debug("reason: {reason}", reason=reason.getErrorMessage())
|
||||
else:
|
||||
self._log.failure(
|
||||
"Error checking auth for user {user}",
|
||||
failure=reason,
|
||||
user=self.user,
|
||||
)
|
||||
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:
|
||||
keys.Key.fromString(blob)
|
||||
except keys.BadKeyError:
|
||||
error = "Unsupported key type {} or bad key".format(algName.decode("ascii"))
|
||||
self._log.error(error)
|
||||
return defer.fail(UnauthorizedLogin(error))
|
||||
|
||||
signature = hasSig and getNS(rest)[0] or None
|
||||
if hasSig:
|
||||
b = (
|
||||
NS(self.transport.sessionID)
|
||||
+ bytes((MSG_USERAUTH_REQUEST,))
|
||||
+ NS(self.user)
|
||||
+ NS(self.nextService)
|
||||
+ NS(b"publickey")
|
||||
+ bytes((hasSig,))
|
||||
+ NS(algName)
|
||||
+ 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"_"))
|
||||
self._log.debug("trying to auth with {kind}", kind=kind)
|
||||
f = getattr(self, "auth_" + 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,
|
||||
)
|
||||
|
||||
self._log.debug("can continue with: {methods}", methods=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)
|
||||
+ bytes((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)
|
||||
self._log.debug("using key of type {keyType}", keyType=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}
|
||||
"""
|
||||
self._log.debug("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 privateKey: 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
|
||||
Reference in New Issue
Block a user