From 7a0aaa57736fb833033c86aa0d66301d775efca0 Mon Sep 17 00:00:00 2001 From: Leite Machado <jorge.leite-machado@etu.hesge.ch> Date: Fri, 26 May 2023 20:59:15 +0200 Subject: [PATCH] Deporting ssh signature by tweaking paramiko source code --- .gitignore | 1 + auth.py | 141 + client.py | 109 + gen_keys.py | 28 + paramiko/__init__.py | 143 + paramiko/__pycache__/__init__.cpython-311.pyc | Bin 0 -> 4197 bytes paramiko/__pycache__/_version.cpython-311.pyc | Bin 0 -> 362 bytes paramiko/__pycache__/agent.cpython-311.pyc | Bin 0 -> 22508 bytes .../__pycache__/auth_handler.cpython-311.pyc | Bin 0 -> 47384 bytes paramiko/__pycache__/ber.cpython-311.pyc | Bin 0 -> 6716 bytes .../__pycache__/buffered_pipe.cpython-311.pyc | Bin 0 -> 9686 bytes paramiko/__pycache__/channel.cpython-311.pyc | Bin 0 -> 66597 bytes paramiko/__pycache__/client.cpython-311.pyc | Bin 0 -> 34752 bytes paramiko/__pycache__/common.cpython-311.pyc | Bin 0 -> 7992 bytes paramiko/__pycache__/compress.cpython-311.pyc | Bin 0 -> 1743 bytes paramiko/__pycache__/config.cpython-311.pyc | Bin 0 -> 25965 bytes paramiko/__pycache__/dsskey.cpython-311.pyc | Bin 0 -> 12015 bytes paramiko/__pycache__/ecdsakey.cpython-311.pyc | Bin 0 -> 16430 bytes .../__pycache__/ed25519key.cpython-311.pyc | Bin 0 -> 9155 bytes paramiko/__pycache__/file.cpython-311.pyc | Bin 0 -> 21478 bytes paramiko/__pycache__/hostkeys.cpython-311.pyc | Bin 0 -> 19771 bytes .../kex_curve25519.cpython-311.pyc | Bin 0 -> 8386 bytes .../__pycache__/kex_ecdh_nist.cpython-311.pyc | Bin 0 -> 9198 bytes paramiko/__pycache__/kex_gex.cpython-311.pyc | Bin 0 -> 14430 bytes .../__pycache__/kex_group1.cpython-311.pyc | Bin 0 -> 7998 bytes .../__pycache__/kex_group14.cpython-311.pyc | Bin 0 -> 1402 bytes .../__pycache__/kex_group16.cpython-311.pyc | Bin 0 -> 1397 bytes paramiko/__pycache__/kex_gss.cpython-311.pyc | Bin 0 -> 32255 bytes paramiko/__pycache__/message.cpython-311.pyc | Bin 0 -> 13496 bytes paramiko/__pycache__/packet.cpython-311.pyc | Bin 0 -> 25065 bytes paramiko/__pycache__/pipe.cpython-311.pyc | Bin 0 -> 6790 bytes paramiko/__pycache__/pkey.cpython-311.pyc | Bin 0 -> 33030 bytes paramiko/__pycache__/primes.cpython-311.pyc | Bin 0 -> 4915 bytes paramiko/__pycache__/proxy.cpython-311.pyc | Bin 0 -> 5414 bytes paramiko/__pycache__/rsakey.cpython-311.pyc | Bin 0 -> 10053 bytes paramiko/__pycache__/server.cpython-311.pyc | Bin 0 -> 34312 bytes paramiko/__pycache__/sftp.cpython-311.pyc | Bin 0 -> 7975 bytes .../__pycache__/sftp_attr.cpython-311.pyc | Bin 0 -> 10463 bytes .../__pycache__/sftp_client.cpython-311.pyc | Bin 0 -> 45270 bytes .../__pycache__/sftp_file.cpython-311.pyc | Bin 0 -> 26361 bytes .../__pycache__/sftp_handle.cpython-311.pyc | Bin 0 -> 8588 bytes .../__pycache__/sftp_server.cpython-311.pyc | Bin 0 -> 26615 bytes paramiko/__pycache__/sftp_si.cpython-311.pyc | Bin 0 -> 13863 bytes .../__pycache__/ssh_exception.cpython-311.pyc | Bin 0 -> 10342 bytes paramiko/__pycache__/ssh_gss.cpython-311.pyc | Bin 0 -> 29820 bytes .../__pycache__/transport.cpython-311.pyc | Bin 0 -> 117148 bytes paramiko/__pycache__/util.cpython-311.pyc | Bin 0 -> 14990 bytes paramiko/_version.py | 2 + paramiko/_winapi.py | 413 +++ paramiko/agent.py | 450 +++ paramiko/auth_handler.py | 1056 ++++++ paramiko/ber.py | 139 + paramiko/buffered_pipe.py | 212 ++ paramiko/channel.py | 1390 +++++++ paramiko/client.py | 865 +++++ paramiko/common.py | 248 ++ paramiko/compress.py | 40 + paramiko/config.py | 679 ++++ paramiko/dsskey.py | 255 ++ paramiko/ecdsakey.py | 333 ++ paramiko/ed25519key.py | 209 ++ paramiko/file.py | 528 +++ paramiko/hostkeys.py | 389 ++ paramiko/kex_curve25519.py | 131 + paramiko/kex_ecdh_nist.py | 151 + paramiko/kex_gex.py | 288 ++ paramiko/kex_group1.py | 155 + paramiko/kex_group14.py | 40 + paramiko/kex_group16.py | 35 + paramiko/kex_gss.py | 686 ++++ paramiko/message.py | 318 ++ paramiko/packet.py | 634 ++++ paramiko/pipe.py | 148 + paramiko/pkey.py | 747 ++++ paramiko/primes.py | 148 + paramiko/proxy.py | 134 + paramiko/rsakey.py | 217 ++ paramiko/server.py | 732 ++++ paramiko/sftp.py | 224 ++ paramiko/sftp_attr.py | 239 ++ paramiko/sftp_client.py | 930 +++++ paramiko/sftp_file.py | 570 +++ paramiko/sftp_handle.py | 196 + paramiko/sftp_server.py | 537 +++ paramiko/sftp_si.py | 316 ++ paramiko/ssh_exception.py | 235 ++ paramiko/ssh_gss.py | 778 ++++ paramiko/transport.py | 3217 +++++++++++++++++ paramiko/util.py | 337 ++ paramiko/win_openssh.py | 56 + paramiko/win_pageant.py | 138 + ssh-keys/hello.txt | 2 + ssh-keys/hello2.txt | 0 ssh-keys/test | 39 + ssh-keys/test.pub | 1 + ssh-keys/yeeee | 39 + ssh-keys/yeeee.pub | 1 + 97 files changed, 20049 insertions(+) create mode 100644 .gitignore create mode 100644 auth.py create mode 100644 client.py create mode 100644 gen_keys.py create mode 100644 paramiko/__init__.py create mode 100644 paramiko/__pycache__/__init__.cpython-311.pyc create mode 100644 paramiko/__pycache__/_version.cpython-311.pyc create mode 100644 paramiko/__pycache__/agent.cpython-311.pyc create mode 100644 paramiko/__pycache__/auth_handler.cpython-311.pyc create mode 100644 paramiko/__pycache__/ber.cpython-311.pyc create mode 100644 paramiko/__pycache__/buffered_pipe.cpython-311.pyc create mode 100644 paramiko/__pycache__/channel.cpython-311.pyc create mode 100644 paramiko/__pycache__/client.cpython-311.pyc create mode 100644 paramiko/__pycache__/common.cpython-311.pyc create mode 100644 paramiko/__pycache__/compress.cpython-311.pyc create mode 100644 paramiko/__pycache__/config.cpython-311.pyc create mode 100644 paramiko/__pycache__/dsskey.cpython-311.pyc create mode 100644 paramiko/__pycache__/ecdsakey.cpython-311.pyc create mode 100644 paramiko/__pycache__/ed25519key.cpython-311.pyc create mode 100644 paramiko/__pycache__/file.cpython-311.pyc create mode 100644 paramiko/__pycache__/hostkeys.cpython-311.pyc create mode 100644 paramiko/__pycache__/kex_curve25519.cpython-311.pyc create mode 100644 paramiko/__pycache__/kex_ecdh_nist.cpython-311.pyc create mode 100644 paramiko/__pycache__/kex_gex.cpython-311.pyc create mode 100644 paramiko/__pycache__/kex_group1.cpython-311.pyc create mode 100644 paramiko/__pycache__/kex_group14.cpython-311.pyc create mode 100644 paramiko/__pycache__/kex_group16.cpython-311.pyc create mode 100644 paramiko/__pycache__/kex_gss.cpython-311.pyc create mode 100644 paramiko/__pycache__/message.cpython-311.pyc create mode 100644 paramiko/__pycache__/packet.cpython-311.pyc create mode 100644 paramiko/__pycache__/pipe.cpython-311.pyc create mode 100644 paramiko/__pycache__/pkey.cpython-311.pyc create mode 100644 paramiko/__pycache__/primes.cpython-311.pyc create mode 100644 paramiko/__pycache__/proxy.cpython-311.pyc create mode 100644 paramiko/__pycache__/rsakey.cpython-311.pyc create mode 100644 paramiko/__pycache__/server.cpython-311.pyc create mode 100644 paramiko/__pycache__/sftp.cpython-311.pyc create mode 100644 paramiko/__pycache__/sftp_attr.cpython-311.pyc create mode 100644 paramiko/__pycache__/sftp_client.cpython-311.pyc create mode 100644 paramiko/__pycache__/sftp_file.cpython-311.pyc create mode 100644 paramiko/__pycache__/sftp_handle.cpython-311.pyc create mode 100644 paramiko/__pycache__/sftp_server.cpython-311.pyc create mode 100644 paramiko/__pycache__/sftp_si.cpython-311.pyc create mode 100644 paramiko/__pycache__/ssh_exception.cpython-311.pyc create mode 100644 paramiko/__pycache__/ssh_gss.cpython-311.pyc create mode 100644 paramiko/__pycache__/transport.cpython-311.pyc create mode 100644 paramiko/__pycache__/util.cpython-311.pyc create mode 100644 paramiko/_version.py create mode 100644 paramiko/_winapi.py create mode 100644 paramiko/agent.py create mode 100644 paramiko/auth_handler.py create mode 100644 paramiko/ber.py create mode 100644 paramiko/buffered_pipe.py create mode 100644 paramiko/channel.py create mode 100644 paramiko/client.py create mode 100644 paramiko/common.py create mode 100644 paramiko/compress.py create mode 100644 paramiko/config.py create mode 100644 paramiko/dsskey.py create mode 100644 paramiko/ecdsakey.py create mode 100644 paramiko/ed25519key.py create mode 100644 paramiko/file.py create mode 100644 paramiko/hostkeys.py create mode 100644 paramiko/kex_curve25519.py create mode 100644 paramiko/kex_ecdh_nist.py create mode 100644 paramiko/kex_gex.py create mode 100644 paramiko/kex_group1.py create mode 100644 paramiko/kex_group14.py create mode 100644 paramiko/kex_group16.py create mode 100644 paramiko/kex_gss.py create mode 100644 paramiko/message.py create mode 100644 paramiko/packet.py create mode 100644 paramiko/pipe.py create mode 100644 paramiko/pkey.py create mode 100644 paramiko/primes.py create mode 100644 paramiko/proxy.py create mode 100644 paramiko/rsakey.py create mode 100644 paramiko/server.py create mode 100644 paramiko/sftp.py create mode 100644 paramiko/sftp_attr.py create mode 100644 paramiko/sftp_client.py create mode 100644 paramiko/sftp_file.py create mode 100644 paramiko/sftp_handle.py create mode 100644 paramiko/sftp_server.py create mode 100644 paramiko/sftp_si.py create mode 100644 paramiko/ssh_exception.py create mode 100644 paramiko/ssh_gss.py create mode 100644 paramiko/transport.py create mode 100644 paramiko/util.py create mode 100644 paramiko/win_openssh.py create mode 100644 paramiko/win_pageant.py create mode 100644 ssh-keys/hello.txt create mode 100644 ssh-keys/hello2.txt create mode 100644 ssh-keys/test create mode 100644 ssh-keys/test.pub create mode 100644 ssh-keys/yeeee create mode 100644 ssh-keys/yeeee.pub diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..191611f --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +./ssh-keys/* \ No newline at end of file diff --git a/auth.py b/auth.py new file mode 100644 index 0000000..e60dd5f --- /dev/null +++ b/auth.py @@ -0,0 +1,141 @@ +import socket +import sys +import json +import os +import base64 + + +# ajouter le chemin vers le répertoire libs à PYTHONPATH +libs_path = os.path.join(os.getcwd(), 'libs') +if libs_path not in sys.path: + sys.path.append(libs_path) + +# maintenant, vous pouvez importer votre bibliothèque +import paramiko + + +def _get_key_type_and_bits_o(key): + print("_get_key_type_and_bits") + """ + Given any key, return its type/algorithm & bits-to-sign. + + Intended for input to or verification of, key signatures. + """ + # Use certificate contents, if available, plain pubkey otherwise + if key.public_blob: + return key.public_blob.key_type, key.public_blob.key_blob + else: + return key.get_name(), key + +def _get_session_blob(key, service, username, algorithm, session_id, userauth_request): + print("_get_session_blob ********************************") + m = paramiko.Message() + m.add_string(session_id) + # problématique car comment avoir la session_id avant de s'authentifier ? + # print("session_id: ", self.transport.session_id) + m.add_byte(userauth_request) + # print("cMSG_USERAUTH_REQUEST", cMSG_USERAUTH_REQUEST) + m.add_string(username) + print("username: ", username) + m.add_string(service) + print("service: ", service) + m.add_string("publickey") + m.add_boolean(True) + _, bits = _get_key_type_and_bits_o(key) + m.add_string(algorithm) + m.add_string(bits) + print("*****************************") + return m.asbytes() + +# Variables pour le serveur de signature +hostname = 'localhost' +port = 12340 + +# Variables pour la clé privée +private_key_file = 'ssh-keys/test' +password = None + +# Lire la clé privée au format OpenSSH +privateKey = paramiko.RSAKey.from_private_key_file(private_key_file) + +# Démarrer le serveur de signatu +signer_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) +signer_server.bind((hostname, port)) +signer_server.listen(1) + +while True: + print("En attente d'une connexion...") + # Attendre une connexion + client, address = signer_server.accept() + print("Connexion recue !") + # print("Adresse :", address) + # print("Client :", client) + # Lire le défi + data = client.recv(1024) + + info_dict = json.loads(data.decode()) + + # Vous pouvez maintenant accéder aux informations dans info_dict + algorithm = info_dict['algorithm'] + session_id = base64.b64decode(info_dict['session_id']) + userauth_request = base64.b64decode(info_dict['userauth_request']) + username = info_dict['username'] + + m = paramiko.Message() + + + print("algorithm: ", algorithm) + print("session_id: ", session_id) + print("userauth_request: ", userauth_request) + print("username: ", username) + + m.add_byte(userauth_request) + m.add_string(username) + m.add_string("ssh-connection") + m.add_string("publickey") + m.add_boolean(True) + + + sessionblob = _get_session_blob(privateKey, "ssh-connection", username, algorithm, session_id, userauth_request) + + keyType, bits = _get_key_type_and_bits_o(privateKey) + + m.add_string("rsa-sha2-512") + m.add_string(bits) + + print("sessionblob: ", sessionblob) + + sig = privateKey.sign_ssh_data(sessionblob, algorithm) + print("signature: ", sig) + + m.add_string(sig) + + # info_dict_retour = { + # 'signature': base64.b64encode(sig.asbytes()).decode(), + # 'key_type': keyType, + # 'algorithm': algorithm, + # 'bits': base64.b64encode(bits.asbytes()).decode(), + # } + # print("bits decode: ", base64.b64encode(bits).decode().encode()) + # # print("Défi reçu :", challenge) + + # if challenge != b'': + # print("Défi reçu :", challenge) + # Signer le défi + + # Envoyer la signature au client + # sig_bytes = sig.asbytes() + try: + # Convertir le message en bytes + message_bytes = m.asbytes() + + # Envoyer le message + client.sendall(message_bytes) + + finally: + # Fermer la connexion + client.close() + # print(json.dumps(info_dict_retour).encode()) + # client.send(json.dumps(info_dict_retour).encode()) + + client.close() diff --git a/client.py b/client.py new file mode 100644 index 0000000..0c8d5c1 --- /dev/null +++ b/client.py @@ -0,0 +1,109 @@ +import socket +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.backends import default_backend + +import sys +import os + + +# ajouter le chemin vers le répertoire libs à PYTHONPATH +libs_path = os.path.join(os.getcwd(), 'libs') +if libs_path not in sys.path: + sys.path.append(libs_path) + +# maintenant, vous pouvez importer votre bibliothèque +import paramiko +from paramiko.auth_handler import AuthHandler +import logging +# +# logging.basicConfig(level=logging.DEBUG) +# logging.basicConfig(level=logging.ERROR) + + + + + +algorithm = "rsa-sha2-512" + + + +# Variables pour la connexion SSH +hostname = '172.20.10.4' +username = 'parallels' +port = 22 + +# Variables pour la connexion au deuxième dispositif +signer_hostname = 'localhost' +signer_port = 12340 + +# Variables pour la clé publique +public_key_file = 'ssh-keys/test.pub' + +# Lire la clé publique au format OpenSSH +with open(public_key_file, 'rb') as key_file: + public_key = serialization.load_ssh_public_key(key_file.read(), backend=default_backend()) + +public_key_pem = public_key.public_bytes( + encoding=serialization.Encoding.OpenSSH, + format=serialization.PublicFormat.OpenSSH +) + +print("Contenu de la clé publique :\n", public_key_pem.decode()) +print("Clé publique chargée :", public_key) + +# Créer une nouvelle instance de transport +transport = paramiko.Transport((hostname, port)) + +# Créer une nouvelle instance de client de signature +clePriv = paramiko.RSAKey.from_private_key_file('ssh-keys/test') + + +# Essayer d'ouvrir la session SSH +def handler(title, instructions, prompt_list): + print("*" * 80) + print("Handeling interactive authentication") + print("*" * 80) + signer_client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + signer_client.connect((signer_hostname, signer_port)) + + resp = [] + print("Prompt list :", prompt_list) + print("Instructions :", instructions) + print("Title :", title) + for pr in prompt_list: + print(pr) + if pr[0].startswith('Password'): + + # Transmettre le défi au deuxième dispositif + + print("Envoi du défi : ", pr[0].encode()) + + signer_client.sendall(pr[0].encode()) + + # Attendre la réponse du deuxième dispositif + signed_challenge = signer_client.recv(1024) + + print("Défi signé :", signed_challenge) + # resp.append("Jdlm1209") + resp.append(signed_challenge) + else: + resp.append('') + return resp + + +print("Connexion au serveur SSH...") +transport.connect() + +print("Connexion réussie !") +print("Authentification for username {}...".format(username)) + +# transport.auth_publickey(username, public_key, handler) + +try: + transport.auth_publickey(username, public_key) +except paramiko.ssh_exception.AuthenticationException: + print("Échec de l'authentification") +else: + print("Authentification réussie !") + + diff --git a/gen_keys.py b/gen_keys.py new file mode 100644 index 0000000..8cfdd3d --- /dev/null +++ b/gen_keys.py @@ -0,0 +1,28 @@ +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric import rsa +from cryptography.hazmat.backends import default_backend + +# Génération de la clé privée +private_key = rsa.generate_private_key( + public_exponent=65537, + key_size=2048, + backend=default_backend() +) + +# Sauvegarde de la clé privée au format PEM +with open('private_key.pem', 'wb') as f: + f.write(private_key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.PKCS8, + encryption_algorithm=serialization.NoEncryption() + )) + +# Génération de la clé publique +public_key = private_key.public_key() + +# Sauvegarde de la clé publique au format PEM +with open('public_key.pem', 'wb') as f: + f.write(public_key.public_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PublicFormat.SubjectPublicKeyInfo + )) diff --git a/paramiko/__init__.py b/paramiko/__init__.py new file mode 100644 index 0000000..cbc240a --- /dev/null +++ b/paramiko/__init__.py @@ -0,0 +1,143 @@ +# Copyright (C) 2003-2011 Robey Pointer <robeypointer@gmail.com> +# +# This file is part of paramiko. +# +# Paramiko is free software; you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free +# Software Foundation; either version 2.1 of the License, or (at your option) +# any later version. +# +# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Paramiko; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +# flake8: noqa +import sys +from paramiko._version import __version__, __version_info__ +from paramiko.transport import SecurityOptions, Transport +from paramiko.client import ( + SSHClient, + MissingHostKeyPolicy, + AutoAddPolicy, + RejectPolicy, + WarningPolicy, +) +from paramiko.auth_handler import AuthHandler +from paramiko.ssh_gss import GSSAuth, GSS_AUTH_AVAILABLE, GSS_EXCEPTIONS +from paramiko.channel import ( + Channel, + ChannelFile, + ChannelStderrFile, + ChannelStdinFile, +) +from paramiko.ssh_exception import ( + AuthenticationException, + BadAuthenticationType, + BadHostKeyException, + ChannelException, + ConfigParseError, + CouldNotCanonicalize, + IncompatiblePeer, + PasswordRequiredException, + ProxyCommandFailure, + SSHException, +) +from paramiko.server import ServerInterface, SubsystemHandler, InteractiveQuery +from paramiko.rsakey import RSAKey +from paramiko.dsskey import DSSKey +from paramiko.ecdsakey import ECDSAKey +from paramiko.ed25519key import Ed25519Key +from paramiko.sftp import SFTPError, BaseSFTP +from paramiko.sftp_client import SFTP, SFTPClient +from paramiko.sftp_server import SFTPServer +from paramiko.sftp_attr import SFTPAttributes +from paramiko.sftp_handle import SFTPHandle +from paramiko.sftp_si import SFTPServerInterface +from paramiko.sftp_file import SFTPFile +from paramiko.message import Message +from paramiko.packet import Packetizer +from paramiko.file import BufferedFile +from paramiko.agent import Agent, AgentKey +from paramiko.pkey import PKey, PublicBlob +from paramiko.hostkeys import HostKeys +from paramiko.config import SSHConfig, SSHConfigDict +from paramiko.proxy import ProxyCommand + +from paramiko.common import ( + AUTH_SUCCESSFUL, + AUTH_PARTIALLY_SUCCESSFUL, + AUTH_FAILED, + OPEN_SUCCEEDED, + OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED, + OPEN_FAILED_CONNECT_FAILED, + OPEN_FAILED_UNKNOWN_CHANNEL_TYPE, + OPEN_FAILED_RESOURCE_SHORTAGE, +) + +from paramiko.sftp import ( + SFTP_OK, + SFTP_EOF, + SFTP_NO_SUCH_FILE, + SFTP_PERMISSION_DENIED, + SFTP_FAILURE, + SFTP_BAD_MESSAGE, + SFTP_NO_CONNECTION, + SFTP_CONNECTION_LOST, + SFTP_OP_UNSUPPORTED, +) + +from paramiko.common import io_sleep + + +__author__ = "Jeff Forcier <jeff@bitprophet.org>" +__license__ = "GNU Lesser General Public License (LGPL)" + +__all__ = [ + "Agent", + "AgentKey", + "AuthenticationException", + "AutoAddPolicy", + "BadAuthenticationType", + "BadHostKeyException", + "BufferedFile", + "Channel", + "ChannelException", + "ConfigParseError", + "CouldNotCanonicalize", + "DSSKey", + "ECDSAKey", + "Ed25519Key", + "HostKeys", + "Message", + "MissingHostKeyPolicy", + "PKey", + "PasswordRequiredException", + "ProxyCommand", + "ProxyCommandFailure", + "RSAKey", + "RejectPolicy", + "SFTP", + "SFTPAttributes", + "SFTPClient", + "SFTPError", + "SFTPFile", + "SFTPHandle", + "SFTPServer", + "SFTPServerInterface", + "SSHClient", + "SSHConfig", + "SSHConfigDict", + "SSHException", + "SecurityOptions", + "ServerInterface", + "SubsystemHandler", + "Transport", + "WarningPolicy", + "io_sleep", + "util", +] diff --git a/paramiko/__pycache__/__init__.cpython-311.pyc b/paramiko/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..23ab9003f981a48630ccaa9c6ae7f47ab34ba21d GIT binary patch literal 4197 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 Ybw?k6jT{W!h`?>S@O2db1c!<6A6P-e>Hq)$ literal 0 HcmV?d00001 diff --git a/paramiko/__pycache__/_version.cpython-311.pyc b/paramiko/__pycache__/_version.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b46a253dae2474e01ab408cb680575a8a643076f GIT binary patch literal 362 zcmZ3^%ge<81RE^PQv!hWV-N=h*r1Hh9ze!)h7^V<h7`sHj4)|rnu#HmDU~^uVHpzx z!)hRgfMr0ntKsUSSW=jR88n$SnO`yk<zIp%UV;?<V$}1~WW6O2A77SQRGgWg7oVAz zmLDH~izO>RGw&92ZeqbL=Hil~Tigg0@hcfVgZ0MhhZX~M>1X8^rKjd5CTAq3<m<cS zCzs}?=9Ludd!!a*Ch7;L=B5G-NL4V?FDS~-N-c>`0;){S$uH6`0ZQoSWF{5s7bF%X z=4NK+>%*L(S5Wzj!zMRBr8Fniu80q49mvVWx<KLsGb1D84PluJ3<5X!1p70(GA0yH fD4t<_LBZgnxZxFX!;5@ISNMz?xIwUp2dD!8C0bxQ literal 0 HcmV?d00001 diff --git a/paramiko/__pycache__/agent.cpython-311.pyc b/paramiko/__pycache__/agent.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..37891438a455492bb856136e200af361036ba2aa GIT binary patch literal 22508 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�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 BYpnnP literal 0 HcmV?d00001 diff --git a/paramiko/__pycache__/auth_handler.cpython-311.pyc b/paramiko/__pycache__/auth_handler.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..af91acee2405b7c154019e9255c17551bf74819e GIT binary patch literal 47384 zcmeIb3vgRkdL{@E1bIPT5FiPHZxVbHqWBQ?qGY`&k(8(hWm~e_rn?&r5s*ZQ0ObqF zmSNIScXefHmy)Ku?6vGw$4ugM%V{UA+3KXck4)UXlkKFEP42~97qQ4{SCdp%ZEChs zbh&${$F(!N-+%5K4~h0Vshz61qJG?a-jDzM=Rg1X&wqa4c30T&{M-L{Ei(SD&Gz^7 zp*^MQ%coD<ZML^;5!<XSVvm$8*k|qRw`8`2{W@kH?6-8bl>Itqo$R-4whX_Hh4L6T z%f(!?u2{uvMXYkRGRDvHG54%HRyA7{^UQi;-dS&~dbT=NGg}j@ovn@eW__``*}9m2 z)*q{%t&cU#HpCid8)HqgO|j<L=2**YORROaHP$xU7Hgkvj|FA}v5wh}Sm$hKtZTL_ z);-%D>zVC|_0IOj`eyrL{j>eC;A}9qZFZZT>QK5c5ZgYxoxM9324h3BL$Tr6VfJ0N zFcRA_yCXI_I~v<LyEC?Hb{BImU)UYnGrNbqa|?T8W3ywieY5-Qwi4TMTg3HkTcqOq zB{tg+@n3mn_cNbL`0zPCPee-2*@Etrx8fT^<JXgyPlV%<g{bh2ihrP}Gr_K`Z|=;w z<DqktPd__-Y%=up<WtW~o;#oQFxQFcbH~ok%uF6TpA8uAdf=I}=R@Pqj!&N+KYDsH z8#INOIU722_UXyc_%r8Egw9N!KXG>AoGEA>OZ^#S$^vMa)>?`(e(c!fljpO}iOHkS z9M5`K$aBxY@!YwoXHI8Jr)Q?lW^+<f{d1kCP{wDTp3F9~M4lXf`uz0x>C?}LOvyD_ zi$sa*jZyxU@uz2|XO3qpS$s-uaw4mgR<Fd9r$T2>Wt$5obMDErGv_9=wS`)`#_Xup z*+*`A%3AYSUA3xOUA3CBtn?I)j-wNw&Nf<-wN$aGke6EQR%@M(pF21H<TM&b_0DNI z)%*O}Q<F2<hJsPgOdrd(STf8_(^^tXk_EjcpMLu6(^+dJ)L8IBS+ezo@}q*d$YW;w z%;Y(BDCD$S(4H+n6HO$-m!iRvto=gPemz*4<<FfvF?nMydM!D>7|+(z(2B;B^K)VP zHazN%h9g$L^RHZsX6v2|3(5KLg4GK-d8gyas1Tk@&c76W>Uvao1zDC;MUZ~B3NN9_ z=V(++1E1#9(a(MuZN=NCWsHJGZJ?YsqMD$XCEvDzUOE7sky602h!e0pQU=IH$^l&w z4zME96{+~PeYP?JN;1nwctCf=4OkVa0`x>YfZm7~usTu=SQDuMtc}zH`XWBSx=0<M zKjH_hkJJM;L>d4aBaMJfktV?ANHbtdqy?}w(hArXX#;GJv;zhr0l<!EXS6Qb6>W}o zM+5VY*`8=`v?1CTZHe|rTcg2fTXb8recm}c5ZxZ>__po)sQM4}_Lv=vbb{)2W4sz9 zFLkUUfQL13JiHK?TL>o-f#hP~G9!rrbYLJBox|7sTq3Y|F%Y)&=@5%Or_)?y=1~8C z`j2>d%a%mTYtI$^eaZfPy#LVnwuSUGBDN)4h*Dus#Evg*Cd3?wFQ@KTg0C|Di~Pzh zU!3L3W%<%@LrJsJ;>YV>4y5VUzbJ<){i_sT9{tOPwsp=-Wo-i0<b2SZEq^&0zA8j7 zW}VlB#T&0=T}dGvPh49Rl3DJ0A}Ykgv1rz9YS3sTTS1?p=t~$j*-G{vizY8GMzUoW z!tr=i$a2@vK`$=~k!<BPVg99XG8(!XeI;8vZw!%;(sx3(B60nK5<Q{h6VkKwu(U6R z=NCeAi`S7EcPWtwU0zHivmSbjL>Ho$LUTeilE7dqO+*(i3VTqm1XV5&_$)m93~G=V zzOpD>ipIh~kjUcj#Nyob7)qWPJ`ue(A09pzjYShlAsW~-j25^OO@=NYWOQLs7*4`t zcwzoRV)$BE2*>8HE)KI2sYM^U_DYruh34b)$xtZe8#fSkNOL_;rFj+LrtLvv;BM>s zD^lYQxpBuz>8+|vLkoV~nbyvgQkiSYG_~Q=lWA_H&xVY@37?f(pyqbPwX2VQ18U%h ztsP3-_5l61JXDix?_!@K*Mh%Kk-cqB$6|aC-VzRedI-RZ{gE7;C(dQbW{im|`UqIE z|0ZEm$xOC1w6J(7=pYP22eTd!rjc^6M3}%&wlplDx0F5+CgH+-Q(jAhA&HCdO0)x@ zu{ih2#n;`p+$-*kr*kEF>owWaxjrF!f+80bjw4K7g{V@W4gox_Lgwly74Xg{Le#P9 zG!TuT<R_7mFb43xoz)^i4H!_0e+I9q7IX1)6S#s9i6#J66|pKHaUC+(A#xoVPp#~s zmq*lsu;%mB!sryC9JSNQ(mCq7QY6M6rLO!XV7NBIf=;ELG-$Gw7%MMDg-~oU5*3cX zMc7MV2EbhTC-I$XFjwB3Vw^%#FDQK&m$+7$YZbZHOr_`6A-S?=)8=q>WNPc>+8&Yb zd01P&c2ML4N@M5+M|>(a>ryb&U$8OO&FLXI<+08;cALw#L=;+YrU-rM)O(32yH35A zZ0kDh*3zTj+%(N7W^~TFPU#;NH-b^sRb<R<ErpkDHyqE~UbY7vGg%kmbtt|Vk7i2= zzV9;ic<I98+|_J(c<#mP^FmbEhhho)2~fvp-60|R;&qTfHa4>50;p{`5q;mm#)A^Z z$tHmWH8dt@WptdwN2<<@AbJ_D!z=Nd019?omAEEQJ`u!^<8M{TT=NFkmFBu6u1Ds2 zM6PGEq|CJ~)7G^zaqDcRy=P@Y=36o?-S|9{;k_GtTbgf^_<+m@fI|;?@2we`Z{Ohi z(tMx92W37eYJa~%oWh>J`qi&KrHS=N!SONMPsRdA8ys^wGQeF&#D#x)M5Kva&-;;x zNJy%qqM3aG>gn!Q_QejQ+8P2CL0_FUL96IbF6uuLeYcc?8GWl5>Rh{iVPSrbI0%>i z8mur7S$G!35xzpeio?P|a(j-z^8mq86Y4sUp6~)(6V&3sUKM@6j;~Zx9{TDT-=rw_ z094Z6CUNaD*DiAHMUj{@A@Mc~61R7<&j)<<YPVd|De+x0-zD;0UnX`Zh*A8}c<Iqv z+fQo)M|T!M?}`84qW6D#Xv_HJkj4ZH2hagq!&eGf7++Z;h4A$PqNE!0@m1x(e*@l$ z?*b?!cTwWnWUfu*+KOUpxd~f?7Hs|UDQ|*N-jVUcwx1jh9Bp?9J5ka(efGr%4Nk@f zoZkX|VlFSpt8SSS_<{s@iB?(q><)b05}_+nR(;Kfn(NQ!BC}gc!AxeojLe!=XP@~% z9WN|~g~*`EHCVY7noElc;V=P%@H$O|w_II5F=S=|6V%~EfmIS5!KanK&Mn{D2$A?b z0F?+E{Pku@iHqMXv$@=b845Ek-fh9|z9IVT&b0M2cH>LrFBq2J-+Q#&_S5pf(LP6E z1Xl#;=D9q7F;)a+w#w)dn$mz`&(`^iB^nn038S}<p%2!e;cN4um*>F{$L8lgBM|}| z2FYO%4qL<Sqi8$f7=eP=Eg1fcx5&Zl%ZQi?<PqzfqP>q$iQfWHvAbJmh>H?!Gg^;W z&{`$hU8Ce0_)-}QM!fePEwlZ!EO4~fF=s|YLNi9U|MU9o^R58Rl)2iBx}eF?8=vti z-m)*@8ML!7O9(@)t&`BJ_(-)DLo^okk15i308}jNkho5n>lC@pqF82LjSX6`>`SDU z!dFqI!r~noKA%T0^37&5!CWU1RNhl)W~D_8Nw%Ef3lM|`9m=#vYm2NS5l!aFL`dq9 zs<yN?Mf?ez6SS6Ac&|&ZSKX>wsUk_pVX?9oL-E$x4Zc0iw@ZA7%y)>|-=@RH_aVe1 zL`0iN^>HncG>u1VEtl;}_QI`^EJMQTlec`duhXc`dm<&dA<dImt`wb?p;vScZ{3ui zk#K$-)+3G?Msz>p6N$@%B*UV?5iD1Frx($)TsRU5U3di>F;sU5s__Zo?tJ_b6H7v- zk%-14AxL~_Q|NtFKxvOzq?1J0tsJ#Q67uKpPDBBeUNt!C2Yl`8)3>Hq>>1AWor*Uq zKCbPPxPF=I7rB0N;@{v`zb<jzGS@9~-9LwjGyFPti(BE?c-!E*(_HuZQxexFbA6)r z_Xr%gumz6zp9=?)CZ01F8-QyCdpRF(iiULLrD+m#6c%da;}Gyp7maL<LuXi5I+N`P zMdlN8i`d(kO9F$0m*(f95UN2k_7XNH!WR~zsk(EDa{$f(-8owljcY4ELQp{<K%)p^ zH{d48%V{hK2?8XB6_VuXoLg8#K1^OlR7>NGZ{R&OXeKm|Et!NUi8TO~%<qu6QJEVR zxzSvF{qA9j>yo)Hk?SIKb-m&GxMo!1cFNpNk=v<auvoWU;s#}IP~-+Rq&{zz<K<zp zir+11u0>ydVg-u7M_~82P~?Ap3TKGRp~Ioh0uAOijxRz$E<~A3R1=@)^sECto8NWI z)OFp(y3U;J-%^k1(}q~pFL6Pcqn(dO_t-7x3j3!ts(G>_Vo3i#y#?^NM#Uo9Zfy7n z4dyR{m9e<$6U{}atY8oqoF=}Hynl%Q%Clsrur$f~L$rYamL!zK273rwx}gj6$%NXt z*g<5{ztDw+#S2+Z3#Pm&ZWNuiDb@WZ;Ty=oLeOu*A=PPV`hr>h9Ys6|pbnK@ofdDF zlwwuM+jvafw1d%?sr9cNmTL#@o%<jp4^E1;lOjL)u-5<9fj1A_KD2gdeWzI4EAr|T zu1|MJo7#r;#lJ)kJ+3bvLB5?!&cahT$ei9E20G^wQ1cW`E5U+sP(njh2@|(--CPq? zM0t|>Ry4LkFew>Z=61{0dc<J~nV&jb@}H$V;%_N?Dg3vZ(jys_h;ylAt0_gwa_fn< z(I*#k9U|p<A&W@ib0@5?qE^dg%jHG;xil~3Vi=KE21^Ta1=87q01KnnHIo`;qc;JD z3fePlkOsCr=1K8d@cGF8fI!Wbs?fG1_Xkogb%ups2!7{Nt&dF5GQaBcDD(={GR;Sn zS(oyGx&kqXi;LLGiVHW%r97M<hA|Q36dBbwn4?+*WtYnpUbwU<Kui!5ULs$6C|gP- zDnT+4EV<ajHovmfdCg&6h2?n@BcB++piybgQHeVybH_yPn1$FA?|6Rg>vG+Y#0|^b zu*eM)0~x;)xf_?;_DbBC%#DfM7){BH^)yWyoLj8w`MB?Z#2u8mgCciOo423SXJguL zU$}F^Bxg|A)vxILw-n;)9b{zU5U5Zu7(3g@+Wm%mH6n4%*vb>R=1h%$qh>H&Gk9-K zsu`7QMqe*qv9IjXvVY#RbE0HtFcQd4tQwTKA(<NzxuJ~5`<++bc;)q1Z@r4$!u$zN zC#v@_XdXvYBerECcxZFwF($Qd29l;hY>>p9sjFbz6cSr5p~<uG^cTq^W9BjxCal(c z6H}HLWm@^ozt$MbjwQ!8o07WzkIlB^xI#$Fo@9;o2C{7-p@Jt_XZS^O2o0tBEm}d6 z{3q|RmO!w_gG(jf>|H9^O3PkleUg*A(@LJcWxw^`K*DaCa*QHa^G=-kBJFkaF?BE6 znxyWbJ~^#fj#jVI{J<cg^)3-_SvHfcemM;7p^J+GmYC4GiYKDyFTL|NJ^$_Dl%H4@ zLpL!%no!ZmP%1}fF_4^(MHjCphk_nMKA+{owDYfeWlQNLTS~cQU8JqT^c1sh;)9~P zGGxVk0@_$iy)Nqth1g3dl;tMRPE88Jq9ANTEwW{ZPf`$4gAulqi#xoqu=sK`!Z^5u zw#`Zc&}>;Od?S=hWbM(MeO8h@S!rWlQ<MT75{C`iXp&G>*R7PPDkZ&Q<>-UD#<k~f zzp(bg`mXm5$z6w}x<e~dn<W*lX?upR{?5raPOc_iKYQ!!%2}wMtR32@9Z1&>NVS7< z?ck=Z)ZLKrHLT5S_(syc5y>|y`$kvo8DHJ%^zUuE_tXb3{7Jvmc}(s+wmSWwVOShF zAvH|P4b!XR8Grq1dB)@W&e9u8ufKNdwN0Dd?az4YGM+lo)3JW>k4k^+`WKZyu1xnn zA@)A;z&|Js9hdwkWd8}#dt$SWO1Ify^ZGVC-Dyv^<mr_?y_+`9<;!?|YgHTG?P>3J z$vY%_hgQZx0aspm;2RLPPe{H=**Cdr|7DrY+jOVn?f}tt&q3L9kP6V3sc%^OHPPFf zsi|E#p7B(#xbo>GF^G%_y}Si_iIJ|;O2pG2>u6&%6{#tYK|6(iTq=o_TxCAO|B*B? z7CG^WqI_=l5^+ovTgx4EE|1j%EznE%IV1*poGW_C*G;kxBS&j(81v>>26HMbe4#q~ zO(~=G%`A1%I_J+Y#abtaqBEJCUem3%4y}*3Mv2w6H1v`D*cBHNQC*0lsu&3`qPjsh zBVW++1W`&xZ>Xw-Pr*y*CBTFyG<_-J5+*5e6Q>5PwZ#ipAe_loP+Ua|Rp_CRBr(BK za)RE8c@thI@B~1XTL{N5U56YncmMHw@Xc8-7A$pu=#$5wPsVx?WACkPZ*IH2eQo<n zxuN>zcGce7dq18Ym=Fghe(tTm?OJnTX!CBOP#@QCllTFd9}xKgaytFS={xnhWSFGI zGaGz=n(x0?ZG>y@y8D{k{=`Q6RJwgiYCj>jpOE;Lm8sPeAVQlqr+egqule@$+B687 zxB9J$H!D7F9+tc#vUf!Ej*t`oCV%Hy$=fY^yG3ue=JK56?UlW~L`Do+bnhw2J1l#L zMelHiuUVOT$nhK8P?`fzIwEr;qV}iI8+{Rq)-1z+MsHfsT94z;%(J#JX2EsX*Uf1n zN#%)@fVTqn8cWqB$4ho$G*XK3XDiH&ndOyBrG@2_xeGFD9tuw9XvgZbXvyBF70;He z)?f4Jl9jgsPvD%%HerzfEoxP3I&@K3jA;Vs&*}!}qC#@;rI8&+7OzF)5QGoSEykGI zMCt&s3jrZY10jl)6$B>`mjv|1OQ7%7<-Wl6c;fms((jH&0*gWbf=!ZNMuKL=!0Kzz z2#*R-tPUxOBuM@5+CmtaPopvdaemn{ur0AL_z5PwnUCX>hN(8%-k>n$C;^kwlrOUL z7Ky9_>E;f&-$l?=Z;pVbNE7U$LAQbk>Vj#T#0|*YfXEFztnsf6VU<B6&bLw7lCEqK zTX)^RAXV;@EBAd=FIJww-$U;V_E+3q=m@y0zcc;D^mik7j=g*Gos-bbZrLHX?2!0T znI9GTQLqZDyE2|pMg6$%o=f(O{?XnK_ewjD$vclpo(b7AA#xM>vn!D(hV3^npFIxS zA$+j|TLBq0MeXvRB&~1MC*mkP3Lb~87(J#`wA5_2T<%`Dku>r3S7_w22jUTv{M=YY zSVx8Me54GV0Ijd|u@KT{PK?IK*RR}~B2!y@d@Qa=tK{$jd1*rkN$2v+<-MZM?-Mzi zy`Vu`sX;}NvgFLqd1Y}VJjr^kz0ot3R_gFAd(t2())MQdKi2hihLH|Z;*+FTuRamC zHC<~ttbWU-Y_(h}JOGvSYAKk1BUL#ufG1n%Wlxbd%31AM%LZ{`myvgpCV2H(a$c#` z-*aYzobr_|l?i*7$|7E)F^PBGLYcoHj>X^HQtH*Yb&b?4mnVZ-t-#Zk7iqJcGaJ_W zVx{h|)-_VQ%x$GaOJA_QTS+_O%dPE}=@Z1Xk<t_}{t<MBwifDWCcQY117q{4s5*~@ z^vN*m5E9|k$duxb4dVRi67yMrCQuwGqn(Mss~y6!@E-aj)jW6>lkd56Cqn&)_rI`X zaL?D4_I`Efg^|I%Ukj#MXm=Wk7o4e6K{e)qy+dPxfxy$}#sf5?2Mz|1-5ixejoy*! z7{4S$F-u>ET!3YsS44}J1W|Vy1D}#0;w{@HP>8Q_%Q#Dj`Nj65vL*ZNl3Omci(U9b z1b82NFWHizk*xiOd9UQNiUSMti6nT6Yll+%Ei-b_rW`tea|Gdq#38(Balb~hbSv_? zX%lN_l)pP2AMO70@sIX@w11VskM^^vGu3+bx{z130423Q@al4EYCO)GiMrzDa3YYn z7M+{FI3JB{4_vsO#C|LD&1H5HfiTW=VbAvB{H5z)WI9qFWu-8fn7=d#G)OR}=`;KG zRNFbWgn&mRYT}l(0<fcUso=~aQj5jV75Q03Z=kRpfr(epe?zH*&%}wPq63Oy>cIfw zg!5uie+N*OIQ4m0>0eZ_BlWye9B^h4wwX*C3kZ-?de%Nq1R~Lk;p+=YN(0#~q9HHN z3yEYvXW&?c0{w)$!PFX#eNXNWys~&b5DUK&fXtnpo$S+?Kx23$`T{`jV+->TKMkMP zjsynitXtsf%Q(<Q*{RWxMmR~l<RYY1g3*GB!2CtZ3)N%|9EdDN6H1j74wN#Fbp-1a zsTE@^iB%AO0FdQS?F8xOWlMx;wk#113v-thEsiQ78e7DfLah5}*CE>wV(qLkt41w^ z-=pM+gbMwXX0=ifNW7BZoe>r6C>D%DA9*?x*A^fhE2Vx1Z#%yLEs`t`DcE8>8J>?P zLLnyp0;4=Z34V*>(B>pMjpdl-V#-S*tD+inCjFBvc0AZBmB>#5hjIul=n$en8=_f9 zEO9A8HCMM1wB7zcfKv`zTyzi~p#k^TSOw*?#g*PWoLCvaU#7Mh5-^UZy?vZH$nb|Y z_{lUsnejG!;wW)f{lW(DiPPz-_=ODs>qmeF8*gyaQsr*Ba`&dq<!b+v&i^5p;kVtJ z_+UofK8^TKAQk}g1b5xMZ{<{G&$x2%W+&FKjM<KgtyrN>%lx#+Ppgg(ymeyz{*C(a zbp7~815*8rTt8!!XOjwr^nC3i-$uV@?Vu-jlC<zWj5n!vMy{P<S~OIz@Aim|V;hZ! z(~XBe;-toDxp7+JPRiU#kvsX&Q%}3MRS#;~GT!HqMJ-jiwu)u(OU~BZ!j{=GzkP!r zN%JG(j-!7z{?Ub>O#Z7$i9adxClRHYYS~<|;z3dBn^!6`l}($rR##P~dD}+wNV<9C zey!BJTW;RHa@t(Iy&Lt1()EWvT$1Wf%k`&os`pFY*4X;JXV*{5-3LFsA+?^6(Mn9p zxH6sbG;G=`-4pgqTi-qV-G$XkEXm(;z3F=3Ys~oeZukzReTP1LRq~yZeWyNgI;v}a zVFN%#HBe!zuA>Ow>k&InOWrfG_l)R0L*;+gzUqKn^4{E!uKfO$4;!SRqw>&E$$w1t z9}~UD5TdzjeaGDabYx9z<(S@wch0>#`_AmWN~v|1+`3ERcgy^4k>739ceBJ+eW?V6 zA)Eq0VF0*7m}GYWH|oXaQLMX=&*(uMd&N<(;f#k4hUG7=Xj$RF|4*L-tNXac6_|>G zONZ|x0UCPBvs_}?56xLJU;zRFgCf9OEfanU_C^<#X(`aJS#=JJDvLk@j4O?A^~uTI z{s=!3dSDTm|5$d?QO7L_2yLci+ZN+Qau{;7MlqqH%@C1VSx_ck)axH9Q-#4tkdnUC zGS@JF&)mE$!ctA&{ITW(aZj<b<q14jo$;R_C8JcF+QW9C-hvG-YaS5$R3w{?(i$5( zNKM~pyP_|Ztg+Wkq97v;D?SuXy?B3bB~6r#gc9tDRNgIJcA6JYSM-g*WVaT|W-}I4 z7FVAZ!iwR`Wf6Y4JmSXEqHNu?d8mb^<;xOxF@J28t(!#MM(E_Q@qVR2dtb`I#&u&2 zrv<^UwLPsp09-6vH*M7!(ef6uF7vot%E5!>ilvHW`)X<Yw=7uXQE_`mF-(kj^U6~U zXKhz>Jj;!Pd@oq?tyZzuA^eNvKBG?NFnA}1oIN>xSBcWLX@2J4rOI`a;K1;CObeSv z!g|1nWxVAIs~CkaMo)(pa=lY~Gyht1XIm{iRGRtZlo3kphdrRyCY(0>|A|U)(#S#W zkrII@X0PFHyjkn9>^9?Dj$iT3u^Cf(IW(OqBj{o{BOJzJ<vKCMc@J<Ts5vgV@ugFZ z?_<vWA<et&$;HR`_AXoUyyk1Ob=!pY<tnuOfZ=AmE#-)e=1QK2vgLBsYgG`&vEP^N z+93#(LC1@&;u1RssK>hUMX`zWz|}{^rhmD`p7fvupSy*ZJl`)x8{@z7EPMY6Th)8h z9)Ir3#<yZLZz-SVnR|6mtFieve|+Bhw_m8d7mQSMv9@S=E7wPjSjL+f7mLmCxp}PG z<G%sQbkRsf#U)SjYla)dVV1EAp&g&RZ1}5jk|^<R!_|1pp%&F!3|E72y+$g=TTXhl zTMGv+*IcK%b<-Sfq`g$VRI^n3D0;$bzhJ~rYhMp~LTb@@Pd={D*0iN8wS~)){DzU( z5^pdQ*50swp>$@x(XW>7k_9`)v~-0REbjW;yzE=@t?$=DnST-9pt=^Uz%1$e;+VNy zhh8i^8`N2AXt%|8TIyJ@*3lmCv$SeM;Z`-rwWe(SVlAoD>8(<CRqf#MYp}*5rOV+# zt-O$`s?ao(t;<nW2+xttxNKQ68YP2KFQU&<RoW?}z<eTb_S9!pB)nA*ia@-?4((4d zE!I?BG#((+IwS~$H47$L-XBcyBq>f@4v!A*-Z2WzDcUs^ejOm|)za2Be^q5vCTt}m zSo6C^Y_@qDkVS}6tephr0nVp<Pe<pXFhmy#JQ<$53Ka(?NmT`%*dk6|7vk^?#{<}n zh2$=|cr_YN{g<#(HwXiP0jBJ=zd{#BnO@F>VNEM=E*VZ<Pf((T1I@=DABbfVQ0?;- zO-dj-AP{SjYIr7oHNN;VgsHU8t=J3;biTUWnd&$P@fyzLpk~Zs4RZ}B9foT59}Whe zP=t(hs#e*;rj?&bt%L@KqE<pAMlpl)U(j10fdc?!-7FE^vn%VmuxD2^K1XJf_K+v* zt+GgzTp*}~O`T{mTRuJ&nx2_FpRGK1_SmV=x${p?j-SbvtGGeN%Hol1>6HY;m(Ixb z*tJBqOi43aDn#dAQV!4$QS!$qZx_>BCOu`y5)+q0I0G75oR1`w<3bhJ(96*z_BkMG z6ry4MgquJlO;@_n5lU5%A=X~em{HHj(fT0kqx6|kvqW@`Nk2&y=P0}cqUYI0)pQ%I zenINbN<xZ>5V8va@`tR`IAW&=Uw=T6i47NMl>tjrR0AmL;MBE19KJv-TDF{==tLAP z_p<fs@Y4&eW|F1zuh2NSK~}6_U=DBOFqgcMq^wQGt^7G!KiN{8`x{mI(nabDIM)M} zAvX!(p+Pjqgx!$cDxWmMnDM(x)q&Z>D~*JcVU`d&4Anr^uzN<^L*xhn^?yxecO`_E zLTpTBE8=K9?L8L00&y@6ooq#HKCZo&V-SWQ1J0HymC3r$FiM;REsjmY&-AKTTlyLN zu~UQ*aYC^VJ5-Qt-NHnY_|r07JlrF3y)xG;a-=3`s49_`_vP0;w>&GJOl8e#JYCr> zRdz#gr>aUodOIlbLoz=k@<R_a1*QkJO}F>0?OU19HJP?c{GiMaiu|Ca$>h7+D)D_X z&b9J=4;Aeuu`wv|+hl&5$Zykw$!15gmDaw#KkXS5J%gm0<bKnAC#oweJ!o#ZbN=qv zq~-y+d0>^x*Ij}VQ>MC3uI^ZWAzeKpRgaLQ**~c0REplA%_>{x$b-&)xpVaXt{)$g zcb=9y&&Zu;HagFzJI_m<&&r+8KIk8k`^P@sHzW0*mHW?b^go;Ke^%;$PVRp$6X<@= zz3D1zg|HQ1)%D97TSwn}?mxKrLHtiH%7>nm0#C_-r&isW&cRg=>{yZLwsphXllJzk z|JsL@V(*0Hos_+kq8B1~cyy+{oxiv1z5Vy*rLHl#Yiy(IaJuWT)HN=5jZ5C6viGRy zJ-T_?Zfoe-s1K&=gZH|n`eC_#c(oMDpbhPJlDC)FmRCzrue!!n`)xaVuEvl0i#<=I ztDg|7pU~RqX~`Rqy#di1&|J>n4N2ZX**hqD(LjSryNNw}($#yU>OHVtxbBlXcHG}3 z?>sIwo{$?)td>#K$~P<TOl`Cbr(1?q^%?J|>>U-oqYn!!jy1R6JtQ~p+Grk6H;+rr z$K>W?x4Bj4>Wi7$hL4d>?XXxo41L@U-*DPDeBZ9<<BGn~jH2r{w$VPBZl9FekIU`H zZ=YD5yt9MF981@ZNws6BaZBsF2i`gG_My9nRwo`bw7>1T>w>PXukNjrZ=U?PeV^pp zFZ=e3zWtje<#prsOhe<l{5yQ6rE{ZYZ@OhK>(qP6`{#fBH3;cj+aT?4ZTrMg+Sv39 z8vxXWRq(4{R@hp$LFE>Sm?6r!(S9)9e(=Mv)P6*6KO#2d{=w$huQnYN5ibu7-Q)Y! zl5eN%+bQ~XZk9CFHD$W`NS*n>M%Q$@Yg+0$C3l^Ao4ez@^J1ns@NxISbn`*6`Cz6$ z_#^)J`S+_nsJb(~S;DnK>3MMH#^8zc;E7Ck|9fWu)Zf8RT+X3RZ0-zou2-yA{0jRH z-J>WxqL^W(Z(yTuGTk@%QCR9bA@`jS1G#@{6%di?Qeg{h6We!W0zE(P9{BC)_omnF znLx)6Dt@Qp<AF&jFeL}3#K06e@xR00o0S4%a$rmhj8S7(Z3Lc32cGzFTnZeL1Bb-G zq0K?m{g-=eje(rL+bj9TWZ#(R8+(ZEJpJbBb^k`k&UDAl`x8H&k$0VuI%edK8Oe86 z_MH`dXYrD$Yx=ltEL}Gy){UXRn)}4Q<5JTJx#@)HJHb5nrt9{Kb$c@%oj+*&oyOm8 zey@4;r1hh%{oRFk7T%8EjjtXjG(x_&XVzv`XC5^5;$UE7!@uKfjqUH0Wm<agRop)g ztRY+h_@t6+_W!~L0Ilvi|7MNN-~4ggXu57xtQ%!0P73FyPS5z7R;Pa6)SGD-+GyCF zZrJ@t^B+0oJyTM{ak+sKrsM#yUxnm2Z0leQug|2}cw;*3vTqpLZPh!mb<@;#w@q%` zNyF5?;q6L$yVfV(J0oeslOg<2x@G8I@`Kmp;S*BJwA?bi>eA@J^OCnu_V$VFPo<*I zrmF|U>H+LO{oLP~@owAj?nrxg-0zl0k4oNSviBH@SdZ-|02C41Po!g4Ky4KouK0t& zKH3tr?Edvh^(W<e+GkUDBbD7hwgpHaL7doMdv~36+Wy8_adyb@Hy+>FpyO}4?DQUV z0!r@Ar$%hjNX65Q4r#ydX`drqWygEk=LEFa_QQOr>?QpsBwLT$OM1jvOsILARrJ)= zQpmE8khAI_iDzMTo}|9|gW{H>@a|8pdVB5~rkF}=5_@LaV_l13-D5Fg760G1RK6l7 zLLXN?OUvY+r7WV5YdNr#yD}~<yqAQvsIJav>>I5^eJKCQl_TV~5PG@Ah^6*&kEJeo zCoRzv`U=;Gi=NHfk&5>e4V!;L({{sdNT4DvQiqx=3F*5|=2{otA6m-GA93{fk&3Hq z*HQ2R3-sL~bNRoI4`T-v*j#7?kogt8_1AR;y+@@(t9$w%tei=C71b9-t2qHRmlU%I z9Y|cqQaq8kczq#NL!58m5)@QOpSl0l<>1iyR4sGV6rmN@6tl^vYe%OV*&-5Z{bbC2 z;T2PwQ~v<evi$??Oa^_*s<9XE0y!Ak&rC^0IgvoSk<3)EKv-om<?t>(v(>b!(auLv zrU`)tp}-cG<Mc-77=&dC>t<#JLNHiz=~7hC6sc)JigQ$|iEW${Lrbu;3^{7lIDE>M zUX%$PC!+?lm;lQ2J_jL6f_BJNr4hr7+Rt@cCY9CS4PhA|R(3;UM6BtO_-<H96JaP# z)#!+bEh7r+^kJFQa#(ITEb&KV{)osQDYPEwU}jv~pv$4s|L-D!9UCoU>6Wn%{Zh*j zx#ft&kIVeH$d8j@%wdV|l=)7P?<|y@vXo$l6lCxA4ez0}_YgEFyeDMuiQHKP=cW7~ zH;$$~qoQY&Ed_QM)1kV$h)CWp+1n+uzlV8C4rGK=5;!Yy&n|g`%4=csEjaZdSGKQ@ zrYn2I${sY<1|LlGSn+5_JdB2gr#4CI3nHwE@JE(_5WPEJ4T6RzZO42XKp*~pn7W{= z{djeTJVIEqu5VMI4Eh735W2F$=ZHhoFnhKd$9x9UN{9-iV46;~e4wmhB1R}RjcxW= z<6FN#<V+zaY+oCLK6tT~z}*eS#K}mfr105UXzDtO$GWu-<WyIEC^CiRu^Eh*md>QA zO@<r;kdg_nFvUjU-=p5aDifV!r>tnsVw4PKqv-|!9tv<t;W|!@lG2=$otPI=2$Xd! zMB~|Vn9h$~OC}Vm#-w6I)zp)qS^JCX!TD>}L!j9jlJ{u0W`tO&Qf)-Nbzw%H|02<> ze}lwTnq?dcHE5Q02;Ovn98Ppx{i(HQC9Vag@?jWXJBR+%nqiemb^Go!x4*LX75(r# zxsqyG*J^le{`Sxs4m@e*+^cKW8o{Hw0t_gJx#;9p^_?AeeG=a$^F->1a-DhO%$<%q z<9E9yz8z+&MfO*ib|}`L@&`T1EZLy{3{%MC20f64CNlOI$EXr<^FeIu;9+LL3Jz@Z zajE=4QTX^&LGw=KT8YfRk2xA&Z~*7>YKq(*b*46Ak5IyP3X13$<<uO<Zwm8|IpIhj zOD7guPV}1-jt-vboR2wNQrBf69<azD#(DxAG<w>!;|{d{A^s~5@mmq+Rl)_E;7492 zzFrN2A8VQP7Sj3AZyrE<>0e7FYI-}0mOWsk4GSo_C9)OfGz*rZ1Qtb&oXwc3bDLY? z7nhw&rTXFG4hzN`>G+IN8*jNQl;yHjXZ*iIYUL_l|F6;01@)M$^?`jBdtnWxoNyd; z0=V^hqXfoVB!|CW=Hg!HQ($*Td!SF@u-l%u#Y=U)h;rc@Mf=H&W3Ie@+FE&CD6gwn zd2_EZTXsFZ#K}>uHe}ZZt&PJ}CJJTrS3&J894X_hH9;Ds@qdFJt5kYywM5vy#XhUp z;wUD)sDj#QIep8M#>u*_t!31cG;Nl0E_PdX$A1^StM3pqZJFatTc$9@FzpgY_`(z; zdC<r+SG&eaudQuJG-27TY2U=ZqG{hG4;kqJ_fR@$_IZlpo2i6Lp28(us=5-;(lj)3 zP*w{Sv3}*0j?+q~j{)~hqJ3M?snltoF(VrI)~S`t{0n?@TT8QygJWhKR;lHtW&NL{ z*#tUW@danxDWf#j9wauj@Qk~Zb71fhA^E&CO=I!%7yvMH6I&ZC)|{8U=)rEIuErbm z&oB$3PT)vPT2a+Y?)4rcl<@|=60fz#5}jMY*oem(3tB8zN>vj7Lr@v7Ixk#6Eqjez zjkg?{P*a4u^%*gYH`KGnT8ibe<Wna9vgFgomo=n7dNJD!U*l~lPqIcF(P~M23iZ)6 zM(xm0ajx2h=aOGB#mtcoEt3Y&cEjIz%c(WAS|T-984oE8Z>jZbt+fHpZgE6!u^;M| zYS#yi@)~bTIa)8v{w056Yh}(qytvge*RBs4B{1G1xn-$0kE@(ECSB0NT>96I6s@C- zQ34Y`VT4<%&kdQJGx^$}sb!(iW~>hB<HW4rX4YFvUNO?TXqoRrv`A!+ImORId4R*3 z?$w_zIfXyjB0e;j^SeT_HTD3*Yj3t$M?NsoG)rqCF{hW{!iO>|f6gwFs$Jh_gfiZi zta<9QB}$_6z`3c#2f-s)XW>9DW%_*O`<6|cH<K?JWy7pyqWs25fOXnv$$L=qJib5P zUL=Jr^4*`W7EGK`581G$7ozF>Fdd<(6uT-{a|`s7X2wBCh1n5AXb9nbJDq!E_Zs3u z_zToAb&5m|x@<UbAvzaE7<M*N(FzHKlgTIq9FQ^N+#}sIc`2$VgHw@#*nDL0;==r; z%SonRl5#$Cerj+mHJ~P}XeAj@wX0zgfqqR)9TaGoE4pG#YO9xrS@KX-#+V-0K1*@; z=bH40p>(k5VOBJQV4PwGQ|!7%)sUES5rHY2`S?pXEFEE|QlX>*g`OxCX`TRyiBe+| zx*6pC0bRw6?BUWG&^QjdMgy7-+1cp{?NZJ}EVa8}OhnQ0EEtWBebTu}rUVu|s0gW; zbcyChllGP&cxPgDCQ+xMEBqFLX#yk{P=xk3=<Nsr+V5bJcY%rOYohGnt|EkoW)E(M zz-`KxqjOgkCw7fmCCT$u3zDk2A)FzHH0`gESeAbVcaTC4j48JX#F^r<VIu{t--X|$ zoPz|2=Z95!91?Zum(L3S6*=N=Ns_(60GrR8gmxL0Kpd>_8x*n%Z8U%Jl@RGwL0d_9 zK#qP>B$E^^3nOnN%nQeH?wlq@fdf-4(6l7{9ZQT-QQ|U<rvHU}Xx~f7kaxpz)-Yee z$=oyZb0%4xYH<#DK-W|=T`<8<(TEOYy*PA>gWDKE&`%;|HR0#v_y3Te+ms_T!hW4R zNXU|{H&sFD4>C0qy%B6Qsdf<wSFAey2a3b)2n}OA3;&TmUCLmEVxXe<RT+=Itn~u) z#pW-CFtq8y&xoRW`97ug4FaUeCHxrxdT0I;EPlcOv7(mukL2j2#(_3oB$~kGd8BbS zcR5>5Ftm6zTcsP>VIIQ2LzF}#fJuSN`l{Fu&C_~QH5yZ0rk2)JyfhN1DTpp(rcV4C zL~ME6IS)OxbcJSc(^l@P%{M%iude0Suhr8;mpvvIeY?VJii#{ku}zOw5{1@2WZ$*E zxAwoe|Mr2k12~{-)-=mh*SyvAX4CDKwH8?NGaX4ju)!Zo^T(jHaHo9z5yz5YjJa0y z_ogdh|Bvoc*x*OgJPwiWlKEXCzl#~Df-{r9LcNS$)?q!1c2#u6gZhm3Yx##%-PO5U zHLNPFJ?ySky0Y?+Tsyo`do*2pRH~hjYbRE?hn3Z<&!#Jz#meRkY!Sjx>yC}O@pRqz zNBvUWDY@>{%GB#q8F%e!D(&tN-5sQL<bKEfw&$(~Cw%K0-dcKd>Go@DudSTeEOEN) zGR<A@9)9QWy}eTNPPuvKD!0o0>OpNg9ixR+FCCOY0|eS$<!~S^9M0y2EV{~F!&F1G z6TdT(_pIzaD|*jnOqwi}n<b9wMkuCjbnZ%b?z;b#59g)M6LRN?)l;jde)XWf3;ICS zNKFIKE?J#`&Qf(Fa{75=_iEYgvWJbWq++srqj5UjIGype+_{V`w<b~mX{w<B(Eq6N zt-hLecS0#6)7<jz!FLY6efaL-mD4C;W7E48?^L|a-{sK){`$8T-dwmHUyH9ChYFLs z8Ta3A_;;uMyYJ6`R4@6bW&iZ5W7VOwLW#SX^g<M%o`(Kjw-`9Q5tvE`rv5DP=R1D# z(!Y913e3m>Q}z8#l!W$=G?*ULwor2-Aq`~wPpvxiB-b6J!*p-Q2T#ZYhd-*80>|XQ zG08h2dnZKi1j?u-M!%FG^+hxNqAzM!UPVG1-oZ3!)M<v(y#Cd)hdPQ(rR%0}`u<KA z8qQCL(fw60TuB?1-)n|VNYT@ceah9CuJZVhI)<xmhPq##cGRe@Jf><Xtv5=(0U0(U zeFK_Hz2pnZzM$v}GIb%v<Yle~(gT0%ov^0!B>4wr|KO_QVSV#P{b0I&FypJgQ@`GS zkB<B|P+vFH00*Id2!BN<@xDW<-zC@YLLXIAK5tFGIsLte*gYlrj?2E|qVM>F*8bJ0 ze~<AXB1!k4=pM`f9a`RLc^gVgD<_R1bRu1Mg4$=BxqTGFm%Sf%j!K@LvS%mtUOm(e zH@tmmZ{NK#Yh!<cE~!U<RQbgE{<OPaa`)eR4&Xsu%bjO$ht@)9aL!ier&8WNwRUP{ zlG?v+^~E<{S$Rdpga<yD2%X72Tu%yEx!nsSSCtQO{@>rS;U7r*2kxDg{QG48zLnz- zYoS31t9<5k^A7D7NcUlD*GB6|x^?7!gVefTZr#5!gI1_+$gv=3K8x@E&Z##}-Ko;G zZXawrid<JOq9q3i(*~*((+qtud{DjZfftz8wOL*|tU3=ssjr~&mO@!U%3e#mdqj87 zy?y{yi}LNNyHyy?3VIJ?KJe77yvC>n8wtH>Z}0kz`+n^6dymN8Bck^RlrYI=Dm2u( z(=b2iYgG(8jD6TG?<Xq>WX*sy$#n25)Z2$C#^Ht<oCY2cK`NF7EV9zD^kZ0}s{S4n zi#FV4zJG&1l7^N7@T(VD<!hWFbW=fE&MY6crMb2{H}3iG`*ri%u)vMK#Cc5gKWnTz zK5YBhaK-Thj-NeY2mJFL{%Owk7hJ`3o#QWjcEG<Z_n&IB{bgIlnM01h>_2#Vo8zyx zx$*v2dk>!3=lGxZIr09tuHF|$Y=1kl?W;`=%N_P*$b>e4KCJ@(_V~>}QuD}v%xY_q zlEMmUSl;R42CJ~ZDjI;0l`e{NYPldDm61J#4<6Bq=Z9(tn~g+2#?dEscay&LxAps4 z)cx+PUoqLK^GK@FP^z66DU(rU(it*o|8$(!?8&oL199ids)1JRik1&sfc)R|NvnNf zk-&8TvmK@U;|p5;k7VT3K%?l_1hK&?BWE%!s&PtQv1aE+&Hi-F{ttJ_Pn?o!PRlhk zUdVI{0I^^dpkr<7<??jDvT`qrq4@tf@1X*7sXw%h%W#k2V{vbbMmQkAG3TLYX2p_a zhehCCQsDXr2iqE>^5s2N<;q<Ag(#wMELx4IPoy+St4;OE-9}8oJvUXewn}bzGsYWs z665c|UC(o=1oj~>N1TN>l+7Wnqcc)gBu0*o$i))qva~W=sgJEa)CSQ<=8foEZDy;4 zk#f|~5~4^Aalo)hTkB&U8{}M(8kkzgT4!tCphzxIB)z|23(Xd)()-RNeq+=|7seH~ zbZEsxWlM=sn+n)9&Y?EyVWpHy)iA`h7R@<?hwMtt$8o{>(9lrIb%v?;;YeMoFDD$c zJ;$gNT`hGjD#Yd?jDi{X+Hu9|x_T6j9mh-69}VMNpOwT>3ZLa>&r(z^%1;HWnQTQd zy-WtM1UCr<3=-%<qY2gIM!Nut87@yEj1z{2`FQ2f+z-fqfB+M~u+_OvCpDGEm*9`f z+E7epZL3^uCAQqo(P|X_9$_E7{8bzK0I)al63UoQHFKAtgKrMr9$p(>DbKssq7tk| z4XimC%8mOZe!tA`7y12~WAx4of3Wl3jrR|Ja8PO;llXlyzfa`%Js3Rt(U>%NS{^*D za8~SKle=1Wx2``278idHz56!2N7CLS;EH8mw~4<2GuNwFd<S#t-6MPVY`UBcFq~B8 zhD~^YRR``L((E{`*Gk?V+1n$sKV+Qo2Jej_xgp~0hEx`gX%K2>w;n)q(Z6%Uzd!9) zrwspT*`I5Oil&S%w$9Bow7?RRn~Ya~p4$q|6ub;QM-=7LKL+^HW(iUq(R0}n6)E{* zO7v`O!)AG1&e&E?Po_LHP6NsZ6Rwsi@<|fDF$3eK;X{K0*GNu~7vVnhx8B(Kzrwko z(Q%!FQL=iTuzUuARV+81<bz11<;|AcZEI~1srae;DpkqKce}-g;6}qpx?u#|YQvb^ zFeY*PWNx3R{pBm0>Am+6TKloRXWB%7LJsx14flBHydWKAuu^Iq{w`wA+e&9)<#TJ{ zNHwdJ^38p$>9MUaQ@M=L)^N+EmuxVTq|Y7L_Q}~4#jJS{+b2aXe%e~85y~{*6)Q`= z*`!;CfcZp}^B!!`IAA6TJ@7-;3lso$WKF6}IrXG|AT~GeF{`Rs`;N7tsb!3)$yVci zDd#XmUK-_S!Fm{svKKxg0fiqB^B>~B^4M*+Xd{h;_UtLF_)%CfFo;_mn{&o7I|8g- zKoCWeslfEYLiAF20lN|x7xhgfGQh5yung8HW1KApf{3DQFwhE25uQ7765$d~frqcb zKx{;rMQIn$v{ylz$4qrrp!-cs#|3EDLbEQ(CP-&;J_%8J2?cWkz}zHLvkkFH(>|Yd zYnR5TE<yn-lG&ErgVg4Fr}~MC>DsoBXrXTq-FX6~Q0dNq#BG<k?IO3G34NQz_TA~q z-D2f#7*`bA$EBL1a?MeZ$IQg89k|V}@heV}bf(@+-CkN-TH#e)TlH342#14j25%3n z4Zwc8zhR~FLA8JNWg=z}Lb{I7`9wfcu&v+V+tQHc+DSI|+x&a{z3`7>zaM*l@q<ON z{gA{TmifaX`@<Zj5ItN1vwC2?853k9t&bWjR@@K;xP3YIwHlXe85+Masis@5F<rr3 z%?bp$q21MD()U%amtp2LwWU^(%Ut!z3;j)mh=%E>{}WpF^AK5+Mz6=LE0C*o%gF2& z$l`Jd$eqJ%`aO54g25W33r&jX9u=`IVkI_t19}?<G-|Fq#^S#SY0H^HK%PxxZONJR zYboU}!kCVXt~zgo0#VPgD5q(P713g|8TS4l?8qUrN?al}Erkb?a>G-qBZtf=&H)ph zVH>aJ>zo9(;mIh@-=X-zUNo98Mu6>bvhB-u({7n*!;g*vv74oYdI~>C(b<+C?Y*&m z&|sOtnPlzptV@fL5NN=&jkH7=LMuB~%1%{l5wfDzF}MY~q6B@32+RM8)K$V_x?fh` zwX(Y=?2N7)m1>U3HOEA35J9CLr$=8ub?X#vT=w}_TtqNlyxp({bE$UM00T5W>o)k7 zG~aUPDY9Am_E+zI)qKVV2V2+L<m$nV>NDx;GYAY4Q9~r6redWGl1ARYdhz47A*pg$ zt{jHEk@mH))zp>dyVj?Svpj5rh;Bn6nV_=SBnI|;SccRr0vTO2^9Xip7ES(GN<cP< z5Kei%Bz8F0j6CR+d7)pF)i#Z*qUTjj*FjimK;DAgatf6#*mX7sb=HL<-)uC06=q<p zv_~DCxL~D0*`v-kV3MkV8Uh)>QCF12)(b;u=cypCO#FM$;=%|GifMYIqAB7*CMYyy zggMJz<)U%6rEHcG3jaIer+O?yrC8Zsr(v>&@aixzE_loYw<$Jrdo^w!CXfd=8h51| zcin$VYTPR~?$ssKS`aJ<c5gHsNH-k#@VtERjMOkAH_S-fS(!U4a%VHGs@(ALi@*$5 zwNj>_vyPA`CB3tWjauw2lI-GFc?`rA%Hgh0hjjmmac|f;+xzwz93WyXPSg(unN%y< zs)|7<NoLmJtmx>dW@!Y)S)e6Sf=Urk=v9@~Kh;#A)z!FvNKM^=s5fm7vHU$GSN71* zAHsz&xG)YzO|S>7ys&!Dz@+|8gboe~wa74A5yBPGk?RYjxdHp(FJ2EXsCpdbp-^OT z4u-_15M05aGxYW(fpY}DPT&H82!YE4t`dk7c#Z&5Us<NNn*@G?z;_6IkHGH|_yYnz zBfw}pqxStMnlM1%@5zIfo5CUboFG7m$P_gcl?_s57pBRBCRm}KKm!3%VGx=KoP=+d z149alRPq(!6a~Id9>)nx64;@Evv^6+Vbh!Xzs-^oCkH7t0OxgfZ`yRwUE+jQ4IOYL z&QAJN0q1mfU<XYDRW1sw1Airb>Y%BDK6TJgMW0p>&;zq*O?px+QNM%o)j?IcvmYuH z8mQr%*pk<Quha>7jSi|Roqbdm6|~eivCXW5{w60RO*$B>aqb{r74%m)C&BTWV8cl} z#W%s{N+ukXp$X2EH90Zy>0pefqUxX}QR3XUiG2zkUU70%K^^#-=~D;$OP%|vMk=WC zQF1!iUhS;dwCSMN<@8Wjs9>ATh3l7aZG4N^wmZXhirwSbU2H8AOB*-6Hdiwa{c3(n zh-MbLL+m=dSy`rp^V!O(U}3{KM1>go#DULmKW8{FR|Ve))#mVDf3#Kgr$2?$Tehfe z7B^wcmPG8cIGH(H8gYEvHtUR(0+vO~BhGK<9G05p;98dF>Vj)|o@)hMxjffOx>snH zk5mA<Bb9(v5gyPJaRYjz)lpBhChCpWM%__gv}(R|whrl3<)!0~cyJuvD>Pz+rS6X_ z_g}C}O(FrJVF8sKvI`{?EvZ4eH(?M)q$(<iWt+GzunQtCGwnQ@-<WNx051MSTxc@B z91X<jrjamNsAG3)(A^H#!pXVI0Y+rEvvkaQk%|(RaxqH$dh7zOVqiCJkc|3zd>#sq zLo5!{9-MRN{bI8by{GR_J6X>xft3mA-jH6{La3<nkOWRRflu63;tEBgv^d9c(SL-m zDox9B7FVlCji_v@qnN(hP)M<I7Wz|^CP7MSH*J~54w_0;;n_;%N@f1crkDS#h(a^l zI)3w?T;>CFn_jQ2>~m=~cVyd$gHJ-qQf~o89Z~If;c29+%nvzp9&=2!6>Wg<1zZF7 zKT_@cQGV2(IM{(Sho$|X%ngd%V1_vG4Nq6v(<OO&WKWOC_2jn*)r<8gO$^26daXVs zN_p9|o?BjWOm!A5Z&JI5<Y!c7H!HKM+17_6vtLfZ*o>K5Fmg<7D_XET!wP?yS3tV4 z`^zcd5tJpDyJ5??RBzD&hCwIDULI3>{+qnQ(d~I(PT`IgDjYdJVjNzA`989?_pkHH zMfU0P%cT!M!dff3nFpD%Dl*83!-RZIG;DG^`WZN;_7-i&M~}n}s(?yJdbhn7SGe*n zk)uDQ9#nSiDZ>k5*go9`SJcD4uK8Ggt%QWGDl-VlvI8d%9fiq>nVBZCYEt4rt|sAd zY5WD8HPBa9>^$NOPS(<ee^f^HgvzzIq*r^RbDYJ*(AH9uYDeL7)8=yFT^B^>pRf29 zomF(c<}}#`Fa74r=FMkop6oO`5sAFVY{kdA8fAiQeH+*uF<1DHvls!xKiO$|?>64q znHQrCN%YGebIYz1#^gPP#n=WW*D)n;HWg<5q(7}#I|U`mvDtY9NeeZE>x|^GwQDuG z5vnWega1IEy#xdTB*It3(gAwwBH$;`Ofiyp3znM%M!dTGVmyUXAgA)qG^j}nJ3+{! z2#?g&Se{TCL;&jciqXr|?&1S5N3dp&mZjlGD=^GtCXHGidz!czw0Wg$v&7}9fsM5d z-;T6z2h_#Z+;^7lotFm>LrzmiJEwJ3E0Zgeq+&*zv`!aTbPZ$%htyr&2Xz6aLbkyV zr+HYX*&*{gM1BXvJ{x>zn(tiSC3o$<pZxKwA3;KNO6E_A{3!^anR;j?k|Xs{XfIcb zl`XmnP2IGO>H1N{w9TD5-Co4cy}<|GK4|J~MNLn!v9?)j<6Dstvm^Swj>EP;tvFim z_=#)Wb+pFu(;6o}3M-ZU6z%v$X%etor*p;CO01)mou6{7mjY4p9!9-LQqCTOCSgok zC>iWl9|a|=(n2yywp*oSdi;>KG_aZNi7`rMqJc&lIP3K}>5_TljGeF2^S+|fqrw&& z*ts&aU5?!C3zf^D6&{0DFh)Tc`Ykk?8BO?gR8}Do26bR;5F-qfxpF?HY0fywq7FP- zXpk9SGeuyB9T*{C7ORzm6ott^Q?3wMO%^Gpc79$WfJna}67YYaz#svJrN$K(iFrW) zm;k4TT@{wY=tAclq=B#5-~(wMHc=bz>|$n0?~_J*o?@s*AWDfqXrK^*VB}!z-uVw+ z_^?+uYNgQv7h;l5s@GQ2!OlR?y;t2E+~G8LIKwNNx)$j%XLGp~5i`_IeIGR6zbJ7B zW$vJ;{Uu0H^I^w$lkF!>6-OH#Kj|Jh>T~?m=ft~Zn4_5#)?*1Tj}<l-xf?Zi0Go&X zI^~e}<W6c>*Ottr6#WXe!)mhO4bk68B%t6bTjDVJ)^)g9M5yGN8h#wEkmL}FYbRxR z(3u2jCFLto6k7xBN_$9}>e(xM_KMuze0EN6Q<C8kTe^<l{HH*xVIV3`5k@XYFt4qo z9HzEm(!IR4A=lK%<Jv}7Fj4oytEq9ep+t{Z5$;ZVcHa+6o-x@&8qfJmI@O6<<Wu4n ztxURBetWh*RIF?vtF$(cPbYC>s;^k=A#3aws*ti=q;nZQ*`qV$Gr@W$v=v}f*(RJJ zFhhV*z9|%4Sy-PXH%2QNT|7b_M+q<{iUbzI1p*93Szi!Sr*KUHdg~%^k%Caka|NX& z+F|mYKSq-!_TcNL{*T065R?+&ZB^|zr#4GVoOHxW0}zlxLre!8lbP8`QU{>R%*dfk z^%^0s5tG-5?zNq~wwt`R>t2v!+MRtSufEMPyAyXo>Y%K_iTFC$<#!(1wCP~P;T$7b zstPKY6jBEjR&gPT0W(~?*zp9^;Ofh6p53fsg?Ess7(g$E=D=JT8nT8A9#Hym7q$Cs z0)Io`j|s>G{t6)L0XG?r&0k#{Qm%Ft2xA3c3-m1rlo24_RUn>0+3jJ|A=~YteJ{o2 zBXJx8EgO^*rHs91r!$$Z2SWuWNB2{ZLc(+=Z*L*rRswAV+6e>*bP%8whJr@j^wtBA z_3Gt^W%~rKd0<?8)~!2VPtGqWHyF@bnQ5si^`#X9#DBQZg087x`^q%p6~)c%6xL`P z^$hNyHAhO}QX-)o937^(B!gv*pM&0?aR-$BkV8TYsEt2c;=c!zgVSVo`(|&6y%gvR zz}f7cn^p8LW2?N${$*_Lo9ti4=DNxL<$G<GcH2ufZT}hk^R)r?e(XJ>t=s&Uv9*bX z{&1d=y61}l-ew;ZZQIR%8QU(g(BEcR#BIk$L1Fj_bN_|ecc%G@-41F}7=FUse^JDj H5t{!OUP$a< literal 0 HcmV?d00001 diff --git a/paramiko/__pycache__/ber.cpython-311.pyc b/paramiko/__pycache__/ber.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..76771d85eaac81f40f2a66318a318d8ab40edeec GIT binary patch literal 6716 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 Pi{Uf9{reqybqW6u^uQe` literal 0 HcmV?d00001 diff --git a/paramiko/__pycache__/buffered_pipe.cpython-311.pyc b/paramiko/__pycache__/buffered_pipe.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..12f24856d4cfc6d5cf8c828190c1fe28ca9c16eb GIT binary patch literal 9686 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 literal 0 HcmV?d00001 diff --git a/paramiko/__pycache__/channel.cpython-311.pyc b/paramiko/__pycache__/channel.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a1884c849942a3fadd8f8b5e61cf10840679428e GIT binary patch literal 66597 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 literal 0 HcmV?d00001 diff --git a/paramiko/__pycache__/client.cpython-311.pyc b/paramiko/__pycache__/client.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..98281b550163336402c5d1cc95c95247a43de8d3 GIT binary patch literal 34752 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 HF;4zJ#+P^b literal 0 HcmV?d00001 diff --git a/paramiko/__pycache__/common.cpython-311.pyc b/paramiko/__pycache__/common.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bed45dac8a0e8745654f83f0694846966f4f5db2 GIT binary patch literal 7992 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 T!3p(1|3GW*EjY;!y&C@)Dy@>; literal 0 HcmV?d00001 diff --git a/paramiko/__pycache__/compress.cpython-311.pyc b/paramiko/__pycache__/compress.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8d367f511b7483896e13821060afb16d95ffb237 GIT binary patch literal 1743 zcmb_c&1(}u6rb7brZN49@%sy0ZA~di7erbF@nTJTD80m90y1o~TTR?#<IJ{T2s8%| zMG7A4v52&UUcC5!G!jr2Jc$Qyk$URM_ja;t5_1w~+26jIH}Ac9@BL;E#>bNc#)o_J zmZA~z4JYAHyTWl6gk55hJh7;yI5bZcvPi7hD`Khp3L%H^%8^$BjRu<dfF@=sIWjX+ zn@v7+%T;a*QFJSMv0QcRvR&~^52iw2ce!rrkGWY9RhN6S$sg!=rO@>sd}w!ad<BGE zVv{_DvndvRMe+~@P_<$ZOZ6x6PH`;^=kh{mi4O40F?f&X0CtJjqbo)7KIjhNm7`8r zl&M8>q)At1V!m2~ApU5fo0Yp(@@cI}eO1`bx<FU@$rEE$*jyMTmp`@3X5pD>xyFK9 z*ud0<v1C_^rjdg}2#?$PoKfX&$@bV9?6e)18y+YO2qTQD$<1={xod>+W~(p#1Y^ZY z(PON3u|MP4P;nKGAua$k$oKU5cKSvuJ=soAZmT=-ZFr6`!9tWAOtASuc^V+>k~%5D zSB|}?JmCs{>vSL14<lWlMX%?`$cslJDTQPs{a$LYB}X{!oYD9x`1px1b6;C`HpDYN zf~#YBmR((5X4&P{+)`8FqqwMAre_KauIqd(FlMY^I?m8aNyT;8CT;^X$d}aFoy|8} zueaWMt<+RIHMQrx-#n<ltAAW*-+k1Y&bFtstyK0Xk>wL$u+r496BV;;Gv+55E4$W) zgYp<-FE&g^YWO(p<>vvKB(Muk#<2haAgjeZKqGoOilR+*NCXM0Ez%Adj?yQ(`^Q^z z8acFJhh_f%_eQoeCL1?y{fV?z3R=wZpCoUNtPNJIc6snv5xJ*uyCnBlB==-1eXE_m z6_PtBNFTY8*gbc@kE#)YVNWy$$sJNsk(5mS$$_5*qh87dogR94$+v0Te+i(=x6dj4 zQ%c`E*GgsDsmxI#6C8hqV}2aDA^VGL=GQ`)1VP*Z=;aL6GRToG#Hluo(?O8#&=dSH zM;No*0%JjIBqf6w{c%qc_a(I-@S9+V6%%!UPK;98$tY9>t^y<o9czr@(}~9^GIlur Y4Ay`3YR_mC4*ECz#I;{T6hRul0p{{*00000 literal 0 HcmV?d00001 diff --git a/paramiko/__pycache__/config.cpython-311.pyc b/paramiko/__pycache__/config.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f37b507e006bfa43a51eb3fa0fef1d437fb47f0d GIT binary patch literal 25965 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 C4|}En literal 0 HcmV?d00001 diff --git a/paramiko/__pycache__/dsskey.cpython-311.pyc b/paramiko/__pycache__/dsskey.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..28a9f970ef39224f7fdacea0d65009b0ea4ba7ef GIT binary patch literal 12015 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 RK24u|L`$BmQ%*Vhe*j)v#vuRz literal 0 HcmV?d00001 diff --git a/paramiko/__pycache__/ecdsakey.cpython-311.pyc b/paramiko/__pycache__/ecdsakey.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4a81c78e6f75ec90ce185002d9224c3776ed165d GIT binary patch literal 16430 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 A0ssI2 literal 0 HcmV?d00001 diff --git a/paramiko/__pycache__/ed25519key.cpython-311.pyc b/paramiko/__pycache__/ed25519key.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cb7085ce756c252b6e78240bf47c549a5261d1cd GIT binary patch literal 9155 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 literal 0 HcmV?d00001 diff --git a/paramiko/__pycache__/file.cpython-311.pyc b/paramiko/__pycache__/file.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..adcc6db4b684fa6911f001c7af7b62a416d26265 GIT binary patch literal 21478 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 literal 0 HcmV?d00001 diff --git a/paramiko/__pycache__/hostkeys.cpython-311.pyc b/paramiko/__pycache__/hostkeys.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7703287d721feb59c18fcd6c57d3aef8daf873fe GIT binary patch literal 19771 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 literal 0 HcmV?d00001 diff --git a/paramiko/__pycache__/kex_curve25519.cpython-311.pyc b/paramiko/__pycache__/kex_curve25519.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5915562984b567b9242ccb6e3d82502d5ea9d200 GIT binary patch literal 8386 zcmd5>U2NM{mcEoMiIOQ<w({e|v7^}Wk8D>;vPqjCBW;@4wI`{!t=mDPnO%YwSzC_& z=8{frue=C~JUD@Yq(BGR76B?O2Au?*0-3=8JCC!#KI|hUFt9+tzyOOr^vyv!gQ8D! z4k=QiWM_ieeb{UH@bcXMd(XMwcQ1b(3<d~<C+A)h-|r;kfAGZ$o?7I&3XumyBqEa` zN%P4h8F+VPTv>P0&7jVmY0G+&o~$?NHQw7YzAT$$v;L&tc=u!i+4f|+5%*@eY%m!# z;=W8M+mY;Gh>J*Jkrn-4xd{0h{>|`nh$TCdU1H$&9yzX%uORE!Rsmv^h$Q#9$Xi5g zzfDBWSz~g)$jp&g@H;Fq8)Ni+ALQiaLLpBTNxYO<$kVj4n2ovgU@D)J6(OhaN;)g) zL0O_{A(LJclyp7^>ERDgojG&z+&eUVQ&6O<(yHE3(=5+t(kW0t<-WHiSr!(g$BfR- zuPPFsTBM-n=H{j+S5i^|>-w$_dh%TqM2Z1n1)P63{68ND@qjFlBKfDzbw+8(Q)Iq^ zx36u@r>@PeIr5lLKNNv3y5c8vCMDx+Mx#Hb5+A@A<it{*E=XA+wJ3=B#Fcz%IV<H9 zIWa92(n4ZR%1W|ArRb}P0?jW;3O^4yrA(eC6i7&9((`hnAW$KjzL8JdkXHEAGQBAo zQxPw${ur3Zq-7<Q&lWDy2($}UMPwY{EuxOjn!i7s{O0VFOOG%7=7Ozy4mn5)g%vCl zc>Yt+JRlN5lS{fJ_fngcCFv2F+a&3ge4^`ilNlsg(G6*Su?=KE^nh#^y&yT!2QnzK zU<7_WcvV`tY!4u1HHs}@+?fA<h&&)grbu|y8$pEC;O%)A3Cub(3p4IlsF0J<m~{6I zumG3dCQF%*=piUoTL;<&ah>J)bS|y%{MrFW7vh%wAePt%;ufhM>fdPF3{@is;lXae zAH@WRwcw-}y?qBF4~U;M+R2jLQ>U+h2ZApQ5XGKSLXH#K#*pja-E-t)vf{o@K4xO> z+4rYnZt8`)b#_6@Nfb<;VtZiB?o>vWv4yBPG%hXk!c8Ha5#}?}+F|GL8q<29jC={i zEwa<*@po@?M>KA*%nfVYu*wbZuw-BN{nJ1E*+;_1cL?J@R2S7q_x&^X&+NEC`@;`E z$Q;!2N#r1T5Eyj-iWzh}R??oEPaqepBw2!|8A|rdSQ>mb`_;Njv35g9UBYo2q39AB z=k7u~6!g7X5M56H{DdjOtio){Q7g*}o@?=fB++3xTKBMM!Pr}(H-*fy6kQbL=*Kjl zTZrbVDAAaQ9){|4R$2ky&Min32X5{=GB5ptw!<r|B2K%`V%<ELsP2`e6qOViM-`3M z*07T=Nc5vT&GJ@dYe$_6*^=Qnl$WPLfWda||Eznfv%k{WUpiav9M?L>H@w^J-CFyx z($J&va{Fsq`)fPI?GJ5t^;N@fJafAuq3;QZ9pVaxcDy8fU?bRS<Y<3}k%z!JAH0-_ zgPWow8vz#HUb^}&LXS1-OAQNcl_Nw!`>F-!k|K!B5v9XQB~AO#I#XmyY(2FeQ*a-s zh2}b;i~-D6y7h#f+M_Aqa<&AggO?imUC*kZnbg8k$d1`#aHNM@)14;Y()Wg0F5&jy z5}MtML6@{wW{(v>^QwjB7I6h=HzdX)#uh~pZwy~B_FcUlVDBbadp0jhdI&7KAf*(( zAf#?cir&e;J@*!Wb@Ic>%U7oPnc125DK3}J3Zlp(Wb0k2mMnSzQvoN_xdlC7I;G)E zTrG|TWs0_>vy-`0Uc`*<_k@pi?j6I0r@*%aMGx|_loNTtTx1D)m!qZkCf~VsJ?1hU zmdQHs!w=MiD$wT2+Cisd+6l-TWDUeEvK<-FBID&qT#LjJL#pi^n-{hAL2x&Jq{@Xi zXScXmg^N8My-?;RG;TuWCaPT5#?-f5<j&0I%m!0s{r3WQ15YD^Wp+qohg5b5Roq<; zi%)NHM=RXXGIvblj%{)A3K!oF_iEuGEj;?o+uHb)HvSWE@*p~SF!apb<_~;NK<pSI zK>WeY&=z~V!X7WZTV{tfc35SHt1O4w!3sP0bm(lEJ*TnfRO@GM4z|(2C~dy<7kF{* z@^$>9(IV4`b~g-nqWg=a)L0kPV1{DT4nF5Vk9xf_*4yCb4fXE2p|sYk1g$m+pmNDq zf2+ru9mnYipD;1k>{{Gm4N*b9A&F5X4`7iKGg8#F`lzy6kfQ%`UR;l{hL6w@XoZe~ z&?Dd(RF=@Rq?EX5&=h(aQ}#8bP^pkvHBd$OB31~Bg)W`~HbBonDJQN3%(`>Zin7-2 z=xMy3GJ(a`K>(82hU)g8cy#9J>B-92q&hYUTxcsYR*8%~x@20l%Eb|N`n1k5WC<sp z?0<UxTIJ+5_2e}p?|3CL{^;FuB%wtTDwo&}9j<Z{TioReclnQ#W$q^$hX&dKW(fk_ zB4ApWz1>20Z1}kdYl@7p1j^Rn+=78SG+0)B`);;?b@!e&=ytC2dRrsMP#R!h343US zVw<x}Gf#m!?Crh<vOGo4?vMrBw9RgoY=ClT?cO-FHViqmw#9$d2B#SKH-iBOb~_~- zpsaL!I_J35FqV2QXP$L0v~7dg?!e?MQEC9_dacS(J-&oA+z3nit(YVIfTgXcJHd0^ z2Xl>7za<pC&J~1N*(+z$9`%BpzFq1>;A{!a_JeCu!b40;fL^`C>^s{nH4u<`h0fVu zXB%y$grv146x)idb3XqL+d0xwEhvhP)|Yy%JkHkE{V(t$Bz{OlT7q)1u5!)`^qzTX zYuGDi>l*Ks)8Ep1gMXKz^<LIv%?Q*q-^c1X>oMoLnuPhWYxX;I##p}|Sb$3glS)xU zJ-zcIn$PkMB85JuyVE%ZPBZD8Ag9u4-M1*MWYQn4nsm(#gv?}nAu46_aMJ>ePlnqS zJ($U-gpB>#QA-ARGoVcmnd$X+qdz8N#Pg9)kWDs+MoZ%;5-1SY=?N4kQJg~Y3JBeg z+%Q|f-umH)w#W-`tE79y^a5O<z`<XJ@m}Sp;YKOFkmF^@APS102N&})61r91z|JwO zD&nM`1|g*XfGJ(PkW!4BCqBQd%;%SLVk}@VMFVy9kafN@IIK*MV<G0M?pn<1;Tj*t zwv3_aUDhejXy4Q^)4CgaDC0g4=fYYrj`;K}=+=%o@w<r};O)jFWRrUULh%mc@Rvuv zIP!Vl!@iBscBEg6oPP4p<;WE+a%F=B3c1DgS2#Q^a^o5|zQJsB9lvC?aJ1A}4i9MI zfzo?rZbaioHkfaN`|e!dyuRV8vVnWvyIzC7YFw=L9H~7=E&VgU2iYcw9Yb*BNc7>w zt)8=$p0nki^IFgOt)4e4J#UtKu4p}1RJOa?d+gzdTfHYLy(h}Or?lQvTfMJWdSBn> z4ryG!#tnTF)JERaMy7x-^dVp93#ja2IJ5fO?*;D$)dNEf3^stO`?)eZsIh}8JBWQY zGL4kkn8wCbHijyo(f8lI)3MpH(E;7QH+y&X-%fpb{)_XUPduD3$GycRDqNz>oz%FK z$i@w7e6+$IEit8&_3A@io4KvfSS2)84!xp<UfBv=sDv(5x!{Jf`L9~XSe@f+xc|%& zm`W$RZ|u9l?$!M*gkRl1TxLf!c0^@IYG;7^b7l6B#vW4HL$$8$&gEkjHdbcGG<Hn2 zeyubMHvyzf&;J7Q<!RPS(lIyW+x61NxlI8<Y9Q|Q(`m7pjClYUZR!HOwnNe5++Nqc zja03X?l0M7zZvvfXq#7QI8itC*jMzGY}#aZ10n#VHd$M0BxrSQBSEWcZGz^g!6sxa zw9U6W6=<fq02+Jc{C`N%oU8bPzBgHhk@ID#UBe2Mb|+*&U-uwjzL%j>&Qa`6r=0fW zq&f!B+m=z<oiw?CHo|US|9sO_%BE3`*6D23qz4o!Qml`@0S}WPAnemsgCNjLpru17 z-b8U3#T68I0H+8#=9%^_j7_1KK{1U25!$>OdNGn;!+igU;%yXHQOttS+mT5C?LefY zbD)6}G?G&jb|VJt+DL8^hxai#i2_lWet-hMYRK4MC$OFa&07Bpfz>#5FJkdeVIKoz z?K}Eq=8MedxraHG?Q!62HPmr0b2oD*x0%~;f7=AQfBHWFT?=e2@U@<!t)Oe6Bkcd9 z8at}8qcto<oc>?m9~scwgtLvH>cGp>zrOhI7oYV1$LQ}ye>?vB@$$gy+Q92u15=fO zsq(<IHZWagXEb(3WoK$&)?Z=!)qz+4;4ZTh8att~6SaiB@N6~~!Y@2IAuI8`9^mon zaXEu=j^}@|EM#gaKIDh+NewRH7ll)2UNwykol9>|WdvEyO3Gqhq*2hA7XK>7CQu*; zqi8P_&tC?Nn&b=*ofHM0PUTNQ+_L|w;e(%rZq4kpxjfx)%V7zYcsqAoUVH`d%nh%% ze`YGeUe5v4)<ihuIln_}5%qb1R@tKKm}h*4*kX$5!S_~S_7?jXJ7D_nJjC^bDY{xv z@(WZbEUw0ZlbRn$W#dbe-sebN6xQIvDPExIY#N9#+_tts73P)ei5KUL3-W3fW|pQ? z@fG9CrFm^^oIb+V$IW={Qr0|Qbl7V6wwcZ6DDH*&0b7eS-tMq@C+2}qf$$M#6uX7@ zXB01F>3b-QC2}s?kL-kLBo}E0vLHo~4}pYFTntks9k-0XDhb^({{BSHs?J}Pyru5; z+i|%W7kq5_n<7A%L6sbD{8gQ=kW?iJ)%gSF8;d{kJeq&f|2X)YV3|NAooZyTYJBef vqX+)x|GWP`gZ~wTuf65bSq&r^R!Nt-Z+OR>XBe=M-Q_bw|NS1Rh7J7{m~3<< literal 0 HcmV?d00001 diff --git a/paramiko/__pycache__/kex_ecdh_nist.cpython-311.pyc b/paramiko/__pycache__/kex_ecdh_nist.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..778bafef551b9dbfc35c166338a008a96498e26f GIT binary patch literal 9198 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 literal 0 HcmV?d00001 diff --git a/paramiko/__pycache__/kex_gex.cpython-311.pyc b/paramiko/__pycache__/kex_gex.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2d11d42d3c38907e88e25f4b3eda654268d24029 GIT binary patch literal 14430 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 CSlw6v literal 0 HcmV?d00001 diff --git a/paramiko/__pycache__/kex_group1.cpython-311.pyc b/paramiko/__pycache__/kex_group1.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b6016acac1e90da55cf6df50cbdafcbd69608890 GIT binary patch literal 7998 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 literal 0 HcmV?d00001 diff --git a/paramiko/__pycache__/kex_group14.cpython-311.pyc b/paramiko/__pycache__/kex_group14.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1a3736a7276365057bd50313e2d8b5295f3451fc GIT binary patch literal 1402 zcmZ`(TWl0n7(O#Qd+mj7*<MVQ9v0nnNxRc+meSUWDHeChD$y=&(h-=>?m6AtT$Y(# zx?NEA!5G0r2niB>At7Eu8WN34VuFTviHXq!B{7?c4<^P3d||QpLP+qO*)0XB=gfTb zpYzY0^Z(!X&-tjW%>zjLczh=cIsy2D4%V8zX{;V1#yNn101$&%nNe7QWvFeFZ3-uF zie0cXfCW*2xYq%)WA;NE04wBfG{Ir{9K`3uj{o?a$dv#=cjc5PQIu39=?EqgF__28 z5SLRKNljxo;Lqc-A7&TfvQ~milB!D3&`K(TDNU1uAqc0li;G!26vMKtNGjA8JtM=B zF__F2t(7v8d;}YVuw-P_G?ZXo)s|G~FZf|W&ng&_jiEp7hiR;0T`FpN$a90n9%M{+ z9G9cIRw{&ptjQS}DQr3jj_epsHPf>mOmnchg&5}m1_DC}V-X|R5G!!VCfKn<a3YQ* zYd1aj7mdg#$Ore1H~Oi-+1U%-(eW;?r<h6(cN~2Az|aR1Up;wtd^of)`<Zwp`SiRm zv9o+|Prm=;mW7^)GBT=LovnjQlhK=Fmk%dV%S@~2ZT-Pj%Y3)_Dq2Ao9pB84#@{Kl z&+I??Ti2}X+uW7j_V}f3Uqp*teaHTJ;r;%<HoX;jFR?OKgTFjKUsxI(2<-x2KXW<u zX>{bdGw5!rQ*?En55=Di<V^96)R(VbVk)m3INme&xfp&iG=1XGC;fc*$c~o1J?^)2 zzs?1YU2i+d|E>7uuFAbL*C$7}{drVnBkil+fjyn`<Kgf3-(;DIAJe{$F?Y=U(^OyK zLT@S=+5PkGH*>GW?lyV22X3&0mP#LL=$dKhI+V7AhzwB~_61$0OB7W}!J=q-L{ZUD zNv3wIC>}0Ja?``SWHM2cqa&Roi<<6?WMo81PHPvzDwzOx7Wi2M>jt0G^fXo^Qguk< zr?pf`!D`XqW4Mr&_ykt4QPeRU<qNu&!$mPk-o&z|^F`v|<!sX63z9A=*}TS+f)$(5 zh6>9%`L}=tj}yaqia_;#uRDM{?y64JT<+@B?=1t>>6+J9-AlXPvG(1@#nw&S4;LHe z1Uq6$k=ab|dXXh!Q>0)kPy9cf#Jw-*(A{)DdL-yA6xu1!b3orj!Arpksgqj1H3$&H z=q0e8RIOz@&7=FlJm~vWHK!;dEk%Udyhc}Ooi3do8>W+LL>RRcYeZmCXl?m$7<XKg z4dr*~Gz9u18t)RQ+Zcwa2W$*i2NXPj*?tWKAG~S+UfbxZJDf~c9XtxBAG<|oZ?C(2 UEYnj5kHYE7EjoL91DD0%zrc!vhX4Qo literal 0 HcmV?d00001 diff --git a/paramiko/__pycache__/kex_group16.cpython-311.pyc b/paramiko/__pycache__/kex_group16.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..39827c0e8b915b318ea1461101aa461b804fc888 GIT binary patch literal 1397 zcmZuxeQZ-z6u+;1?fSt+S2k8Qghv@;-RQc)R+x;!zUW|VanzAnJ6>P=?%KD$kMdsM zz8E+Z2oe?0ghezFbpb(03>w@&Y?w?Y|0o8QjG|@)V<a(H0}(O|dSAPtM(@YDzjM#K z=XcJ{x$kslrU9V#N!409sRQ7SqRchTEV%DRU<lB_2WX5|u^6ZFsW1hrSv80Ia15w` z2hjL_Kx?4tlp27K(M&g=hSqw4Eu;U4!5gP|ni6Tk>un^Wu#bSfp$Nr?AyI6N!d@%E zbP|1nO!QDZPZ5$J^E43>1lCqY5cN!FCj;$`kYzcFCxlKzMR}Er2r}`MrwGM%LunNu zOAH?-C?d)WJv?EJS&5j)aF9U6gf(m>!jOj|6&J)Z!(Aml8<x;F!CsFj$T25Uv{Hns zbgmA~2A^86S>?VJfgu2a4?|a>qA{PER{3yRjZ#DyYJC|rj#AYmOix|9yp1);U;i_7 zI{rvHetS4Y=(4BjSTo}I^QCluZ_yRdEavw&&)?TvD~|3FEF-%^%eTp&E$~);vBRxr zzzMb4Z(9F-L5$pK?9mRHe_8&x?|kvWWj&h@WgWz4=<Tk~yy?mo&xPLiEp6S8YhETl zY#Q+VfM3-7%5M(W+Z(E80%p#b%Q7chZv>XN?99Bf<@UCs7vsBYBd^u<a(~3m;7h9V zF;nfZ>eYoKbhdTlx+nHqb-^9Sz3M%MSvCGvS4ZHG{$9JRxxQY`9>`NSUNTxr-G#?& zb=_-LUe0nY!W(XCACTh?DepX0-#;RKUA|)LW!vihktJ`HztbAsGa34kYBsw4?{X7? zlhL-ta)Uwm+?k#8UAU!aDD<%FXvs%w9uV0TBbMvF1MO$6c8{1#7=8|oms>kN@cUNC z?k%}exj%5WVyybh^%rtZtxa|uj=BvS0%gx8>7PJdhGo^g>NkX4t~<f6jKlLrO_r_M z?cX-6+;p|{sx7nbQ`hl@x3h+y|J`5OW~!g8c4i&7d#aAaO9~#zZx?RtT54qSPnYg$ zui5_k3)4OA#eO%a-R(NI=E&-U=Bon}=22bY(vGIRMK^q7+KCO1?dtxE{wI0oD#x5B z$`0u-IX60j-Cf2v^UPV3JI)t9@)j6K@m9{cO*bvW-agl6_>jjd7E8;D51TJk1wCg3 z4Hn<_O^$V)xzXJGO2M?%85*zH-+Dkdx%cS6TxDS3t_qc4i%pl%kt9!XkR%fZlH>$h zW)(b-BzMUaJ8Qv}HGw^sz&s+3&L#8_N{Wya8y3!h`$|#I1RQM=6eUNOAci4Fq1Q+Y zj(Q;^bC8cqjz$<`D2EqvP>PF?a5-Y4&;{dU5Pb<*L3G5C!ND>?$q}POiesXJ13hGN zwz|t=eWDi8q{Lzbq!J_spY|zZ$zXjF2l^({V9k_HKUg!Z>`a*!4sKDpXqr3RV$&uv zNRk#pD9Z(NTxEZAiOT;bk)b4r!bzo@rk=03NcqV|Xhv6^zAU$xi&W)hOAHc8HHKly rA~l95fg%h5E4c=2{~S{w|JuJtQk#M0CBc8i(Z1i5-3R|;N`>+lIWV`) literal 0 HcmV?d00001 diff --git a/paramiko/__pycache__/kex_gss.cpython-311.pyc b/paramiko/__pycache__/kex_gss.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..960909a05a12802afd7b95a0714a31c1a38b9ca5 GIT binary patch literal 32255 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 literal 0 HcmV?d00001 diff --git a/paramiko/__pycache__/message.cpython-311.pyc b/paramiko/__pycache__/message.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1a746fa86fe49b1f47853120508f4703e2651874 GIT binary patch literal 13496 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 P=JdZ=`R}%P%jWXG*0{c8 literal 0 HcmV?d00001 diff --git a/paramiko/__pycache__/packet.cpython-311.pyc b/paramiko/__pycache__/packet.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..aaa5448f0aab738c25a9e73bbed9fd57216bb779 GIT binary patch literal 25065 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 literal 0 HcmV?d00001 diff --git a/paramiko/__pycache__/pipe.cpython-311.pyc b/paramiko/__pycache__/pipe.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6179797ea1819a7a657af6fe4e6caaec169b1ea1 GIT binary patch literal 6790 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 Y6QS3B@)gy6-Dnt$i=EqopKwO~7l8vBApigX literal 0 HcmV?d00001 diff --git a/paramiko/__pycache__/pkey.cpython-311.pyc b/paramiko/__pycache__/pkey.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..89f61c7b4b36da420774383e15356e1c1f2224c0 GIT binary patch literal 33030 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 XsiOO`PKO1sarg;~|7?>~wnY9P8O7Ze literal 0 HcmV?d00001 diff --git a/paramiko/__pycache__/primes.cpython-311.pyc b/paramiko/__pycache__/primes.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1e7574750adaab8b011125b086219f2cfddb693c GIT binary patch literal 4915 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 C>1xRU literal 0 HcmV?d00001 diff --git a/paramiko/__pycache__/proxy.cpython-311.pyc b/paramiko/__pycache__/proxy.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9c90951459940d2ebd66617f11db143a189cea87 GIT binary patch literal 5414 zcma(VTWB2D_0G(`wR%{q$69vekuA&K+FoJBu~Q`^O>Fr=630XiNj9eI(ax1L((KIU z&TRDPDySv}7xbZkz$v((rmm7yrkH-EC505y-?B0bUIqk#wjcdlV;fAqdd{8Q*<Cr0 z-kCjfpXZ))?s?t)w6!%tpxpWCDfMtCA^*Zgt@x{z2e+YegQ!FmbdqwvLQ3FmF(txR z)TOMHk_30mm-6v3f6A{)S42WmfmAT;js;Qyqw4^np+Ul;sW1(vg6{y{RQTPn5putC z!W@~G-XU|igHMlSTT(3?vtN&9TT`t95y?5C20kQekhX3}glxjkEvYtD7$=F)f3bNn zAvghx>NH~~#EaO9U1H|^!f7*`RSfm4qUm`?p}YIEZd#f#b=ov+I&Z(G7|Il7nJP~H z0WNrXa0Na$2qnlr@GhhzRs4_u#}Yul>H`>1{Q!eBgfwoz{7rW)K;Sr}2A}ASs3BO< zqK1L?h!bt3Qu(7mNJNJ67E^MTtjJb=BFD@OwJe$C4Fl+w8y6f?nf5+4WSBF>l&qDE z@YR>6HA~Lu3arwyIh|&yVIwwsn#z`$nWgqIU7Mw{1|ndSN`}e?r7BhBi3Oa)aa>Ja zW{P3uOlDt|L4xwt<Rt}{YKxRzb+J<`DZh!rgjMw&4a=sADw~tC!Z9&AmDd$k-C52m zvlQsE<(z3*+JsJJ+mvlZpOxn{dm5;`Wag-GNR>10(E)D7hA~~|*nrh}ppq$*lUhce zp!OUE0U&1?T?0Njtspqc-lt5SHh~xw7=>;VCNbDPGp!l4Dya)VtYUzCk=zC*vdRMB zmh%=YRzQ}nvcO`lEg)w+q)bgw@W_Mkli>H@-vDk98(^(02knNL*95y}Y%pO4*YHvh zHsJI(;a6>ww69jTy<S-1tuBz6`i$CC_|61sjYcYdtXCCpOB;ZDvjKk^x#C3?3M3t> zw?KJ)icnKml*VaB6`R8O8FWsrnjP0wkiIMAq<k5Usn(J+d_h&|G>eNA5xI(+Q4BnR z94N7S2GCa6R33GiYYd=j(ow#wdaQ_MG^c<`$d=7&nv~Yz;77fEa1z&>Mm@8p&AbjY zQJK!uOdgd7mA|G4IG<~vs{x4OBE!Q?_#D4wtC|5`$X3m~&A*f}CpeLxw7_T*VJA{I zXQ#JbPlJ(ynWbU2$pFo?PUqp1(>2?XGFjE}bCYm<mo8iybAqrxIs_+ZYgr0AB}B&u zo^R5LRxO6}X(3gz{B3IZO|S)Pc*bN?6qR35&EYd<h6~afK2LL+GCU400t7|nlf!_0 zhT7=~m`Qb$4cjm<tZNh2Fqfw`YvPpog=B8Q38mATq1oy5V)vy6XPc~bKMf>UKL@Z( z9(9mtPqF2x?ao9on%IuT*Pi>huoXL6iXFY~TM2FV4wQNim7^Ui=eIlet_^R;o?3g6 zgK}4Qr6mxL+$R7YB7phPN*jsq#maSf9>5Ny$r#Z9xG{bMl^Y~Xz*L~-mKo2O)*H;S zHcH4&LllVd;sIiNMnwq3vymmy4%gSH!n$WR^<Kc$V2nG=Tb%KXYXj)m^k}&zsN$0N zfmn}?_O3e4oz#ILN{fH>l=>~Eff2fD4c;AWh0zR!Afd{W%*^tU@$vIDXNj&CR2uD5 zRha6b(TrT)c6}n1CrvgB>#N%JeI~|t8%2BL0l`EYvKoxF8H=~mw}N~z5oTS$Awz*W z{yAj930`=84DErTi*tN4re-h{gA+nU<(4Ap)JRCI2YZ9uMH{vwPzz5U43owAN}xeq z+|{I?*mVvDtvLY8q|z1)pA}H~UM)rY??ngiMhDm5+KL`4MUNHv`*^#(r$YQNs}Vkm zkl4_A=F{m<ra!Z9&u<MqTN--yi-9kHveh|O>KrRZ#~wqewBS0ZJc^Qz*y^cY7gh?z z&>jmpzP4{a`6TjEzjXW9OS`4N@AgBrNs-axaz*|Vs0S*tDuCj73Jks;Cb*?J`B;){ zTvsgxafk8=vwVcz16A{Zl6uPb%NPD1>;-8NQpqKM!QZ%cmYV>J*`8tpeIDj!`mRAd zsgD)>9G|bzq+r}Lse0_15~j!$Dfl)(EjCd#c)0?+E(Ml?OQAx5r3wL6+Fct$6Z{T5 zvz#t=rXbqw-c&N<MY!)?wccRz^;ONB6@(v>Jo4FW?u)HJyZA$E3f0rPbFUzdlV(>` zmmxT%kU$C|^A*I5_zoE-*NBZuRxR9Vh3(#2n?S<79<8_PHQ*g+a?1|A3D2~7BOt*Y zPAmLDxbCzP{>6WKR{jQNgq{Pcxf>ax*OPt*!V4y|Px={1S7nT(qmb6kVJ@~HLpWoW zTo2liQmAeY3=TjZWz^0yW3=kyxMPAmZ(;$08u&#;Ju1&lYnf@l#nU~&!yqGu>5yzN z`d%JlAFVq@)(5=1;hyb<YNFloVGL#i&|uvFAn5b-nxXBvJ`K*BIHGAeKHSg=Olsh@ zOxBMRP=DDz1nAkA3?LD4{h1FLbpnt(Oj67VY=zlQn|r}YR|CHjq&i#^sOkvJ5#}6W zo*e)b7H)~`yQS&|Aq>2~!wq8B+qhr~916I>j$U+w-SO*Zx4YtN7e0Dr?Un0e+nwF3 zlOG*fJF@P(+c{Y594rqUTx~1Ide@#W#*S?F@4u=2X7=XnXJcFa$5+pld;8Z1Zymkc zn=JMwxBCZg8e9FzQh%}-PnHiRZe6)^ZtLK4s~7*%C*!oz{%48<C%+KCc=@Zgt-jYw zeXoBMToqQ2uPWtOSA|HsdOqIu@$s8o>s`0@mIhAT-F>3C`$V~`@3#{-r^@lYH>0Ka zk$drxyYZ2&_=!^d#2xi-<6rgs<=rpe-8y;xz9jDJc}M`<Ct`Qc>e)x#B-Yc+OQnx= z^<94r+<W)R2NmKAch*6<y}Lrh*8XpLpIU9jrySkI!+^lUKpk#(9{lkjAmMxn0u~<p zwKL?5M81|{V_nkM@d(2G$Hv;Ff3*9do@sFJ1XTo;=>Z-TTq_#J>Uj(ptmkIy7Yf^R zW{sDIX6J)<?Q!zCa4`{JhhYcDHwzanuDOns(?DM>ZOTw|CpdoL+~u)1Ui)0&`e`8@ z)d=g)z>S(Nb~c4r-nRggtN{SaWIGgD39kCqqNNV`4@0+-TcP8n(D7pJ<)+odI5NZ; zM}gh=o&-)MdH4%}Jp)q`qDytewfT&xQZ@?xjg$sBRqA5XUjs?Evw(BqUlE}7oA%gB zq0~O~nObZgDTYR#Bo4Xf#C1TWnYbD`0($|*o**HZuF}xeB(7@jGAyysCxbNxNJrPo zFN&c)cYE)EVbqfix_8(f?`BYQ3m&K|5rO+85DaEWyk5N&xGo`LQM>9%7JHkhsSo@L zR$3SgmI;`4E#Y<@VIqnb07J>rblQoe(^*r^>sXJb)9>XKy*d(1r&TkPPBRS2Y!Ct7 z3)pi2oDews9A)-`E3Kd5z{>zWCwvc1D8qDugF{fUeh*;Tdn#hsAF2=pJ)Qo&72<)z z?b!6d{(XMT-WuS`g5V#i5Dx@MpsgYb{-?3;>H88iKYZKmaEC<y9`FeMJ>C#@garRS zT(A?k^dsqsc5Ye>^9=SZ0#qG_%&;K@C|`zg)oHoFZ-Q>(djNZSYPYVWWlg8)e1_(1 z%{1JsxYZlX+nUZW&2fcw9%~q_xB$NmJ@1~)3+z?sL*=y)RwO|XDu;;>Sq|e<Ceda7 zlu7F{f68QcvH5KnYFXw_B}5J#-y#Q=BgN=}iYN(k+1FiccpsO8;8q1eu7h&_zM^l> zj+ZCPK$46}bASk^isWSDTPDNB=C=|^3jzrKyJ0bQ_&y(c_&pOWwBFzOhRBBhl(Z?1 JNPmX!{{U?(4CDX+ literal 0 HcmV?d00001 diff --git a/paramiko/__pycache__/rsakey.cpython-311.pyc b/paramiko/__pycache__/rsakey.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..267b4464adcdbfd562dd450b437575a301293871 GIT binary patch literal 10053 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 IawZD@19YT|OaK4? literal 0 HcmV?d00001 diff --git a/paramiko/__pycache__/server.cpython-311.pyc b/paramiko/__pycache__/server.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d88ebaff20e85dd78f7fa9f8fd10b3fb6f9c9ac1 GIT binary patch literal 34312 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 literal 0 HcmV?d00001 diff --git a/paramiko/__pycache__/sftp.cpython-311.pyc b/paramiko/__pycache__/sftp.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d3f96961d6cb036ecd830a58ce3cf8e638c9d794 GIT binary patch literal 7975 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 literal 0 HcmV?d00001 diff --git a/paramiko/__pycache__/sftp_attr.cpython-311.pyc b/paramiko/__pycache__/sftp_attr.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..51edae30b21d17693ea887062cfb8dfa87c0e390 GIT binary patch literal 10463 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 literal 0 HcmV?d00001 diff --git a/paramiko/__pycache__/sftp_client.cpython-311.pyc b/paramiko/__pycache__/sftp_client.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b48ddb93a0e02c837878315fd1f67a08c77dfaff GIT binary patch literal 45270 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 Q+-FQ>aG%-oh+(h)7Ya<OzW@LL literal 0 HcmV?d00001 diff --git a/paramiko/__pycache__/sftp_file.cpython-311.pyc b/paramiko/__pycache__/sftp_file.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..eba11b42b53d8243e45a9bbc011afb445642e9c5 GIT binary patch literal 26361 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�`Nb?M;-KC%pmBKKiOB?KO1Alg3(4--f`GEj~{Z@WM!%>ZxXXA_l1h zPde&6I7XN!+r#w8lkO&Z<Vi2j$4YEJMh)4M>yAbbR5E+gv)dEh5bVi|j$Jg3JbB6C zrg8Hm==C&i2s{b<JP=oS($wSuMdL}YhkCL((cpkUAi7QY_xQo_q=|G~ok}FwrmRXp zcZ0&Eud?&>l~V*Sos|5L@`ns#G?}O&rfcU$*YUSx7tsEH0p>fzJ15zZ(iQU^9m}E+ zch>|*lX<Z-VHn$E+aAe}r-UhKpo9dbN+WMgJKP1^!`OaE4Yuv@jw56>j6JCoQI_bP z`U+BT5Qk%9x6|Q<;)$eIaBM3G9oE0HfH$_kvQT%E{VNOMg7H@t0yo*evaqvY|9d3t yF4+Id!q$TQ4@XEloB+Uoz@%Mp^cRFa>)(cV%;x|IJwDAZe$2l7_6gq^nEf9|Ef*aC literal 0 HcmV?d00001 diff --git a/paramiko/__pycache__/sftp_handle.cpython-311.pyc b/paramiko/__pycache__/sftp_handle.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f29b2b213c110e45bec50cc81d75c070e60bd1a0 GIT binary patch literal 8588 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 Y@_&n0{}XSpkY?IMi(P&8Pt5uM0Mx_IH~;_u literal 0 HcmV?d00001 diff --git a/paramiko/__pycache__/sftp_server.cpython-311.pyc b/paramiko/__pycache__/sftp_server.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ee69cdc99ec72548f22170cae4116624c54017cb GIT binary patch literal 26615 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 literal 0 HcmV?d00001 diff --git a/paramiko/__pycache__/sftp_si.cpython-311.pyc b/paramiko/__pycache__/sftp_si.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8853e2a57420adaeafa8d7984395e6c9c9af05a4 GIT binary patch literal 13863 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 M`~TSIuB_$10WQ^fHvj+t literal 0 HcmV?d00001 diff --git a/paramiko/__pycache__/ssh_exception.cpython-311.pyc b/paramiko/__pycache__/ssh_exception.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cd1c1583124e31ab8acf2d86f3a230f846e0209c GIT binary patch literal 10342 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 K8%<DbbN?HW52qCX literal 0 HcmV?d00001 diff --git a/paramiko/__pycache__/ssh_gss.cpython-311.pyc b/paramiko/__pycache__/ssh_gss.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ed1afa2cd65ad7768cada839d28c0d8346e4777d GIT binary patch literal 29820 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 literal 0 HcmV?d00001 diff --git a/paramiko/__pycache__/transport.cpython-311.pyc b/paramiko/__pycache__/transport.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d64e8e4af7275502bbcfce408c18f417fc829368 GIT binary patch literal 117148 zcmdqK33waVohOKc2S9=ZNP_nTQ4~c;JVc73?t>yJi8?6Tl6=5+K!{Z&Aprzk04<3o zZ8>o|l$%Ltx7=nj%7jUrwA^;4HGRygXR_aybkg&6_s;eL+?pl2x;3lm*-W>4FXmHw z*P}hMzyGTPRRAbaNqTy|Em03uuikt0j{p0=|NCF>w~C4iOt}8w-%N4u{nTvw5A>j3 zdCJX$I;+X_zKJvWOq`jsgv>rO`?dHi?APkEvfmtE4*SjZ<+9&AUmkv~q5QDTXA9eX z_Hco(AYAAxG*dY_AxF5#SH$jfL&agI&lz_4T;UR535&CYN`0kBmlrAxm-)(ATz<$M zF87tQdt0a?T<NQ1_x4a#xY}3E?h8US;aXp9xXxD>uJ_f48+;AnMqgvt<MV_!`8I`{ zd`;oazRlrgUvqeiZ%eqv*Am|9+Zt~5wT8VuZ+M$;TX?%~d$`Tl7H;>ohdX>7;Z9#? zc!zICxXafS?)G(uclvgQdwf0NUA|r6USDr`w{LfNk8e+SuWxU7pKo7yzi)r|fbT%~ zpzmP#knd3Vu<vmAi0??a&({||>N^_l_w|Pdd;{S@-(dKd?^yV_?|Arx??iaWHxxeU zJIUHn7&;X`?K>Sl<2%Ei9iidyS>IW9Ule*O{Iu_Bc3&KNCVb9!j@>&$=ff9#7sAi_ zo@LLj&~xGEeb2M|lF$obpU)S5(f1;IE)BgDe%bdjyDtlUF6{UD!y~?taKIN}aqiH? z@ThMz%=x%5@8elqd1x#=?i*+K6`_f6&=(9}@?B!jm7&YwkT1mUt3u)Mq;HblSBE0u zDc=;kuL->p7JLG`uMI`RF<*?`*M+9TulimMU-4ZDU-exLU-MlH&-iB8yZX>;;aT5o zc+NK$e%<%F*)(qQy}>oSW%6BTQ1+(pO^b<t^U~*4u;s6R&ulV%g#YqoHd#!^O<dz! zCeFj>T>63<&(D4@2kG!%zI<=7beoW_>3bHu!GGn=7ulQ5c+;Hr=GQpOIg@wGOhZ9` zMBqJBLL?R$jf6bG@KlHo^OLbaEEt(A_=E}?_U1^|Ft<y}iB1H%q&$S(yL#F0uC8vc zS#omxSYSF7^Ir^%Ugjq`uT?4tgvKL6Fg6j6N_qXksR>??a>Eghk0O1}bSxOcZ(-kb zY~n;<k_+(y9`lc%J4Z2e+dmPQoa95^9g_X(m~u%aqi4<?_xGRZ8y+4!?LYg};ILH4 zqK1wSpM7RfDq|1FPoF*7ciR8V;L{fd&z+ZY2L_K`I4%`Z)>DJe4-F5UmkQ`X$y7}D z14HNf&!W`+^OBuCvpl(j&pdPX87cp{zGsGqhL207to#eZLuZ~kJ$Pnt`2662lruDZ z?5yNqZ`9)Dr{TfpP7S_rPX4m`;ARPxy7|vt=<gpqcTO_K9_t%AeF4p>P^vU^cGypG zg9HBlvjc<DR%*E#(>HJiO*(h}nZENw&kmk`!T;1VXHN_r9iqAwvxMv;g9CW3W*^u0 zA1j5vmF1OMxl-zov`#Ifxb#*SBGpDzQA)!n)p9f6sCiuJc_db}T8dhLAu?;;fxh#7 zQnkLc!ROBp4x=ypnXjH38Xh?NoWF12<OP6@p>P$JoQ9	+OJ+5&fsno*R^$7!SWv z*0~{{67f`D|Ea<AekG#sc|(e_fx%;a7fzoyL{_SiXOLQ-z4pY=@e_b-|Aljd&-7h5 zf5LwTFw}Q^&_6UFSI7vV44{YmkVtY+b<qWW2DC*C9JEK<#cMI%9}zgOMJo1%f*1RN z00ll8jR=y9o(K3*Ee3NmZ{XayQ~WjD6%2CSyLNT$m7j`E@mKq&g;#kNhr;p@ah$&@ zIpp7SC;EUskj+ldLS%ZX3yBJ}haSn5y4Mn?;(8HBc*iOgBHeH>8k-_?q&@81)02AG z)!l`J>`Uj)X_de&`>sTF1=f+DdI<5HL4yP1Jk{ta!e+7H3{Y3;Fwb+(@R#{(gI7oS zDZ-6NTzDqJO^2qVln{@3&z$R%yTCTsKcL<_&++IBVQ4bO3uA#%9tjK19XtP2e<%nv z>&=xM=gytbN|nlw1~@{3co!UHNcjA<DPD3Z2r-q=H8e@Xfg~40{8KzCS@x6=xq1zw z76wW^76^u>1s;tr?+-<y!O8Ld2&!;3b|x^1_6fk8p+I=b|7svK%}eHsqsp4Wen2;w z=>LPG2;MivSfm^bm|m`!GtHWowRKAO()2w%e56H~O$(Kr>7409^Dz3;=?{#?g0J%a z*o43bxTsVZjYX!EC-jOH@lh((^YC+>7oxln?YtBb#`$nybRxh-ItL=7)3h>0J5TUa z!9eFZtW(h#7O&n;EMJ#+w){-+p@`5K!;4NpY_xMKAOym}%aP8Q5SWZkMTA(#)HSKn ze<>22^oP(ZynsNhk}!tP&*6Wx1HpCEJ!iw3$(&omf_ZDwR`^xt+s=foTC`Qi+25K~ zef<DAOom=8r5Icf+CY)MZ{kg$KTSk?`m9*_bBulE%gqv($60*&oORhI7i0{1`z1yL zQ>>A4@D*}7J_ncUE8_Be#rW^!@_jCpUvf#UQ`$%gxtxu6&7dToi?jR6xB{P>FTYfw zevz-dRHfc?h4|;VmGg-AMUQ!3oaKG>W6IaR&sM(vy;^>aS|;kBdhg^|elF)$zOR<A z<6K<HEen22*>62pZ1`>9%Gk5|+lbav`bIfD=e}k0d4eXcoZs{tX0C#Tl`O1cVKobD zSXj$9Ax#|%>-o*NZ(v~~3q35{#KI;PZswbjrkO(D7QThw%D3`fejC4?Z{yqf4!)D$ z!FO?6ZaIA2j~-pVgzx5B4xl^uoqW%jMGm>GT<a~XZx`qF^>W*MyZJr*UT!<r#(BB6 zu{^H*EwgVQ*8z%kKYxJhd@BuRd<PM`BO~?@V!JY84<oiaBlZYlcV@))A+{$Y_9(Xt zHR<Pi5e{&>5e{;1b9+$7V`#@-{2s^eKK!2G_T%jkcL3o@{uKA?+`+fZX|wG#cL;CK zaEB2Nb4L)KwVSvFt`GU2LXM;OeVXe>{4?AD!gF?$`j7iEH;A|Ak?I(JFL1~4{#ot> z!sie-gt+IqlX!Z8JB854eT6%XxEJy641Qly%SrumU**o?&CA?VNdGzRX@q|68H6L; zIfMc3Ji?3I1%#t0?OCMdP}+0&<+(-fdHjxX?{F{RcbxMf)dcq<!XWn&!b{xC2rqM= zLm1-x2*cdF+z8T6asfO=xQhs<xKV_!a2!H`;}J%=F@!O09N{!Kf$&u>i0}$`3E@@l zGQw+I2;mGDM)(>xiEx&SAe`f-5Wdd6g76JaKzN;tB7BpJA^bcyjqnTHs|erXt|0s( zcNO8UalgS`L%qJl&ERRCdkx_YZWiI&+#JGR=Uzv+z`cR+%iMK@U*X<F_*L%n2p738 zAbf{=3*o!m7ZLsj_iG4ma$iFD9ygEhH@O=K-{+V3FY{mHzruf=U*!J@{|<kPf0zFT z{~P>2<!|!e<lp0$`L}|0-?#X0^WVAjfrdlAizfmjcJaT3r`y`ohj{vJ?dc;teXKow zA5Z^Gd-@$b-O--@Ii7x3eG1xqzsGs8FS{w(&#UWI;4Q3)1s+^|lOB)?9_@`M5Q|}L z<fEQg#1rt)S{Mj>TBoDafl%lg#YBjJ@wRy;`SD0B7{IdV2~4uG_{mY>+Ek3^JW&ul zAZOY<ffUujNAWnxd58@0jG|b|sgxg^ri~dAjN&Ui$4VQ)3e93Y2Ral&<<JO<7!@K> z)NX=TztZ9HoWVvhh)t$P%QG67^jzdwL1E?PRa8vLjb>nlMtY^9o>r<^WGsfN1q8m$ z6P#pU7!5>uM0;qBXJw5=gew7oL*}Trqku&XO-5sZ$x$>031bt%s0Tz4Y8Vb&Lk&FB zQTi+*Wl9Orj8uwtu*?sw-5on-3RQ~2|Js~4f2JV7N4vWBw2#IF`F?M=b`PplyJu-G zj;7Km_bd(WZ9B0wZXdlk>a{&AngGeuPP^=Oyw*R0?0r~oc-zj$!bc)g{A4sb(E)l; z{~D<?Ufb2&T@P*S7W?{#4lS~MZ|}oGH69PBO(9Bd=Vb!_VHrQlMFVo(CKbv%GZGYd zW+W)_hYmr_Of5-0Q8V-DJ9sbOaM7sO{;(9En@IawdK`T$J&wLr&jrWEf_ytD!Vm_s zeVkGJy;K@~ygA$JcK#|s<v7p2iArTJq(^<-^w>hW9@b_{)5VJDdRUY7B^BT0H9xEy z)wWT}7mpB8i;)Ih?6p4JG9Hb#kMmbq`u14lxih=Yg|6%xyt3!aE0+V;wg*Dp;|C8u z^s;2E`SNGiPuiMF`shMG1Q%yAGRc490`dZXD3?tnDTi1*4+}1afO*=Z*g8G5Vb8?w zJ|P0~6AuMM&VJ%X;I#<9jvp|Vkn|G=MIWJPn-IZ|@CpT&DF{;#qF|DODGCG%t|6GI zQ$VMElow*{ukPs5&j{fvCE27w9-DF5(p@p8quG<SJff22PXb7@C!r0~Oly|8l0Er! z*5m-}?8#rvnjA2nJ$aOpdvvonYcj7x_yQ6N#Eul+q~Hw-u2b+j1z)1z*C_ZR1+P&s zO9Abdu*B174iA55edwA2km^T3%ApwH29gZ_&DXz1*B>4}c79a9tI5MlEB-&YidgXM z&9f#y9_5R(;7QwW2C@6}Ydp?azM9Hpnl;CCWg0o2!slkq%ekqvsXNZf<rve>*=B8A zuC`&I?`dC;6{T{{+Kl@yF6T0<mr!WT&*hC-=FF4z#@LusUlNy-`kt{=${Q=ur8OJh z$Fx12$uw;e?#-HCLM`U(a|N?@ea*T2xx!d^s+>!O>OImG#ArS$*Cm>X>@vh^EjRL? z=Nz*Ra0Om}3B5a4G*>+5oO8{U%ob<QtMt&=Y|&h)p*GrHcD6Ko&a^Qy+cDnD#cECU zzto;Fi?Nr?CbMbs>}HdpY;6RZO+xitS<Id)Wwy+if3ENa6K9z#d%bMoRr&Xd`HJbP z^##)v^THdkO{tumZCN*3sd&_?(AWm!7d~%}HK$TvDpT*5bt9w30oK#@{E#`&Xlvs; zOEXB@zt5Udd->(cR4J)Dq|kQw#(J4ev7NefY0!tSXuE8#0HxHbX$enqg%l#TP9KXr z_3AU{z*9r&=_=K`QGLd}N4@7P?48!rc-NHnZnOG~cUoU6`L^iZwW#+DvurgywHlth z>eFlv%G{<tG5oY$dm{YQW_W5>pU`$Km*9=Ptj42nie__-@SraR`8w6KxZk1P%l+xs zo>>dJ)VJ)Z+wi1~9jkMX_Kx~_m*J^beTr$Y2hI{AKT=;1Cfuz)#IznYnJ(>7V>8n4 zRo}#DnNzNeRQuI847(Gff?e$Cp!&q+;`@ixr&%kW4r@;|21nE<)(?G#r=y0ae#6s% z`ebO+pc<P|+hgh*L;Cgm;JBKeD@H9(XirW&4XIDD_tZP|=PC6bW3Qz!&P(JytxKiN z!sRn+to|3}XsM0m0CP~^4Xdfy(^<pQQ`(aYDW5hxJ)=F9;N3axsT5D=)u-9KDKi_( z3tCJ*-ao58G1z=gd!qV0uRgJSFQ`xB1cN^H$<Y5Vs<CLR26e`^BJWFTDpsyGADKL~ zEI<ni+wIYYyPxfRJpt^WuduDRx&@7Zp^pu+hrfzF5H^^~PP@$$5tQ5ZQ1G(6rKN%* z7ccRnF>HR%)1EUjiQT!ntESEINQbtkA5qfo_XJ}eF2YAAx5ltF9S_Reb}Ec=5FbFT z%r%}$2E*Jlf{p7WK7>L9&j_}<Ld-wP#0(?EB4EX!HFA{JN{s=K$^_;+T4=~5N2woG z_S00IA=*h}=gpevLGvafG3?!AMkWNd=`l}uIvVp#1YYG?j<HB66uE-l(`pjtV-pcB zx=-G$Q{nBNk&!4Q%B>`uih4&z_IaLzR2duiF(DH6bWyWLMne3gmcYYO%Iph)k4+1c zQLTIrjahIq<`H-jY>|isV@I{0oDN^akb*0~vU{%JBVaI~U!Tb+V+ZxF5D22*@a@5~ z#|8x<B8-f9f^ttNe3A(;5-#$@KY{p#=VTs&+9rV?eYF)eVJ&)wq#=d|;SI`@t3uok zr6E++Ffk|sx+dXsXho`~o+qrIuBdD^WpS;no+L*?o2kEK&XTcBQ9c$6hWW^JtaXy5 zKgY*VRZr_!C=!TyJ<3afzh|iPtOraBfyuYPUeW4s3|-4Bj2v)d+C0}H(~LV42t^~F zDdN?D-GfmKa2$yy!3?5L#HPVe^3u2jJQI<q%>SXjmOF>KjA{*7!oCze8xY2sC<(pR z+Q)LJDFMH75tD&1@3{g627FDcUW7^h0wKm^^0Y>IVg?Zt?cy~Q-n6f2WW?*aG7%h| zK-N%*cuJU(m`aoeg9<jCRu-!>LEOklE7h4k*v1M*=XH3&?o)bMUlh%56orC_d!?17 z_6ETUV`72#g)fK0)02u25iQk3hYGBvdV~kGX=M>xPi2TfA{V(rQ-*yG9KR^98?25F zn(O?&eOg}Xa>zu3W7pJ)MIXT&=OS0oj~qsjIFjJ#%>a<X!SM+N9|SmbQZ$7ysUUuA z46P+}Nl<|Hs60j)b#n5IVO+eFa1R)k_bBQphH5c6Ck7YtLaI}gh8JL=Fb~gygGM-Y z6zmJnMFK#oEkji_hYclN1ltwtNFX5$g|0jHG0vbmXas4-j#2rsX^Vu4IWeYk!{qwO zTv-pTG^uH(e1cGzs&XTDuAh3;_&H2R8S0gk>M&4B>Qm|kUB9&|$&e>vXm0n8y}P@1 zb$hkQER_fcuKMMn-AF}{S`P?MQkk9IyL<O!ROMm>noHnZ`7iTVjh*n^1eO#ac62U3 z8GxYNLpnxJ4SB$o!s;zE`!HjHrj-{!0Dy_9P~cjs+X)&6WEH1<n6AfwjCjn^^d8Zw z!SNw}+>g<uwTv>W*SgiAw6q)nwXn<#F=p1wP#CMDzFy~r>1@^Ff};SJV3eBA`BQpX z#;Rc%ABS)rasakU19JtC2?G1bWI(tEgfKNlb6U&pi9s!a`c;P>shL=u1=i8PY^lUS z);h3b1qcp|2vV(?hj11K%h1M9a10t4n5v9luLs!Fnn7QM+64p@6Ori<hpNVae+j(7 z+mCs!K%+&S=8Q?X)pKz=2pI$x5miUc@J$&pW0DKOi_@6pZJy{f3IwtMiXUaFY*8S3 zYP(W*58>`8CK!;B5m$_i_(5rm@dC!zujCyW+0W>+ktTp^T0zkW(LmJEn2y0oMZ7{S zq$@ZXMMt6<x<XL~K+*Fosc~psz+VLc1|n1LiZD;a4aq!=?L-f;1Z-T@=IRg{eUdH6 zSD`2pB1y{_TaPE93!`hjq0o_$tQ>8X?T&6U%)C|*0TY2KqLfAhSh);k$ylE-ax4(K zOo$0EsnDS^3~Abmn6w1rSTO`lBea~zLqlOtDT*MFhDE3NQ7lpDM-}#Tv*O@uY*>tw z<Hf9u{w}puO)t-_hYsZG0xOQwH`Yf7MC4*B0Ve?`djL(OghVY)>G&vgJKzzZE0=3% zA+g>uxfsdf31MSGlO{SnMFKGl*wpkz>Us_4qa7OVFcdQ~qT&%P8Vfxw&p-t-!sPbt z^K^A|?a~qur1WtdfdzmH{i^7L>J}%Wy3oYbyR<b!M}bgjDut%V-YDvcYoV+N+L!5B zhnfi%As`%o%qO}#de?7fCRSBTO|1^u>fPP3cilqMKqj-mW5W;4`#vF#v;ao0=@a7e z&D0D>FxbHu!@Vj!I0oIP(6yP`3-a2>z=`b;39??B+lT+>ALfAYX@6*LmrOGbbWva` z*bYG9ua3(b<wHkiN>!O>hej}bLaftIh;J8kVax3OoRlYPcS$ytn3YPDmH?(9p9J(o zrGg-Yxv~savVu65a-&nBU`(=N3ny7Y*GM_Eu1L8I2}8x0NwB5DaXv<E3QmqiBzxa6 z|ApamPYw1<h3C%ppYlWbb+GS@l&|c5rCgxfh#(LHLCV9b0wgEdWu-HqIw|iw(?64J zet&Q>81wt3JTAb8Ba;wA;d4?!Y9k|+t2%2CvyWc(D_a<WI3QAiO!0u65+pmR-JU;7 z8khzC{?kLy4W|1d$iV_3zcP8GVga9lj-~qSkYCH|f>fluRUV7wm%#6=nvzp_twq@7 z1dtU=1<E_RFHs6rR5j7<QmI<9E)K0^a({o2lZt4_Ks;UZv(9HDNRLt>P_kc{MiK!B zQCNQ3VMv9zqlnSi)tF?XJ1RsV8G%%yTc7X-DWzgv6g?MELHs268c;qwA(>A}E*cCF zK<pD7M2k`$RP-oqkzO-_lU#oODlj;3nnH6*Ij3pda?o&$UcbCeI1`w{sHl4esaU!5 z^Pq8JQK?9M<Bx*AN15vT(a1DuYD$$V4okyRQknWfhuowJKj4hC4g5gclQBQ)KuYbB z!+*7_OZOBTozkA1QE);yG)7S<m2wBEZm3#_4+#EP1XY{lpmeT(^s`2m8{-tPlx(5M zxSwbdfkZn}0cH)-kCV1%p7NUy;HEgqL7kea%Xlbq5q*z6^%UkQ^i65q34!*COv+s* z6<;J357bNE<4YyfMhL1eL!ncNlFL+|oNAY<5oKAHTgjP*?w}RQm`{FM&ZHvcLFO)L zi3wDRIF*W(_kmIB#HeKNk4$m^BLKb2@X9~>DsoR>j9$a8E$nA#((A^I=R;${UG$c4 zpMw99`Ym4pbje9Kx)nqCdrI>urLjzegb(N@hn80<51hg%K5AF76s#U(<SW?-smMr; zSy@PS=(~VVhhdW6Q5MM(7?aF=ltewW)}>zpPhb8Q2xdyptC~R_Dgrx(wIF&Zhcs5# zLKahH-F(hMVX}&Tijtc)&*zA?hMyLeFLK}LPZWB@LeHAXX5YS+XDY3DzvkVVrT*oT zM9F5cWb^#dWL?A3E4K#c2NurUbCxc4-?*}HWogsfv-7jbs+y&)Te(ZZ;%kdJi#hk@ z$8SWJyO#qW?E2cwt(kb`w#A%#h0evC8~YaaeWN^H*nq$L^_y-C&!1T2l6B2gY_hy- z{@A^$2C-^e(p9z8{M|g!v*Qn1#O{HgSuNGYe{VuCKZMHlnNOP6OvOc|EJ(NxEaolQ zmbculzH=g3R{M=t#m26cvaWbpSF*Zs@#s%$H{CjTyY&0ditUFJwMWF-BZ~uTmfZ3N z^usqDUw14|+{uY=*_Ej36{~vVRlRFAQ+2~q?q{Foo9dcw9e{>!d21@Tx2bvg>2Fnh zP_dMk`u7<w==+wp(r=N;-Ti5Wsd97D-L>l8v*O-!x9W$z3HM3SeG(0+EJZ^qQFTj6 z>8Cc6y;QVSt=cxN*fuRUCv00q+t#>kYqFw-jg-3zKXx?PUbJ9ewbiZI>Xy!n^}7?c zJ)&(-+_vY_JX7tK<@1S}ZDP$fq;)3iH!pJ^OeX4k?nD#y`|h6qfj?1yng+Ti+2Eny z+GN+xxNRrhRxb^RbsfoFd*imf?9~z{HgqSqcgAg<D6DwNeXB9K|8U%PnBF!lyTwg= zk~IzTcQ#R6jRH)lVjTmgs{3{9MRl^akrGs?&{pUa3v1D8`&sk7#+K#R@4S&{JT5jK zpC1q%b;+g{v1!lU9&!J0qG_0-8c`yZfEJaM<0nt~x$oIR#nvY)Y7twQtZ!n~xaZmO zt^5!2*{h$r>u(Mx+|8o98A<HuqMGKf)!ibXFV^~?p4F}<>0!04Pi{R#d7G1)d+FDc zY(Z~|w#~^p)@Rk~@BK#B%DQ{?Ew|9I9b$b4rK`EuwEcsQL{qoe)J@Ng$t_;`ZAw<P z;j=~eDr#;<FfQRlMUPm~!)k7GEI3zfjVrds<(v-+6Sg+d))u$5rGM$Z2dzMZ?<=E+ zeraMc4fpFX5*Wn$jV<(Bhq}i%AGpgp#FK1f6|19xVNI+`)+-~{$bRe8uDIv%ik_W! z27Z4i;W;FFSU=P!l|*q{9sZ(Kz%M^>R}QpWe$wt7EYJPP0q3AA_m5q<xF01_Q@!$V zW5xdmZz1@S=}TtV2oOE0TsNS<v8;Pm<4vX;Y0_h({BzC{%T1-7H_e;JES%*n+pJ|E zO%7?4>Of=CIy_UPBQai^8`XcG>PvFa2PANM&vFX70PjPN^z!C8^Q`%X<&yT%b!u%L zHyLaCs`+;sXU)%=-Z#U>p_yzPga8VGJ`*hwk`*g2l%gyhJ0$Z};s2qiTv`JBQ6OoP z$8-HUL+B6f2S6==nh_4oq(nd+{xPs8u+nN<wT=Ufri&ix(9&08IHDxz`|NK`e@l4Z zlv7^7g8QzLq|>#OBUbHLt?F5+>bY~_?$JcmVX^9P+)00Hxu&Ax`|MVU$XD|UMDxRB zB%kPQa0G<k!?W-)i)76ZT<BIQUm<~_1OwQgu*QJuORx7#t!}MKe_2i~_#6uO>@yfz zlvXb_FFRK~y(^yHJJSizLD6$CQF=%$Jv5(x&*@GzFYWGrdEHtrs{b=;|KC%9I~U!# zs5|ThtVsr7ph`2~C5b(O!7_d`FD?yK0c4h8Y%*j9(1&Khjjq5Cjjn9AqI5$B!$;o@ z887S64G(AscinW~S+?rjvf|vb985TOh|V2x+m2sx^>eU8hzc-1HVIV-^z|-IRgDtO zRHeqw_+}FgTzZYi63%wf*&esGXVy3$tz#YjI&L=J;m~@bT*?LAF5MHb0<?b~jTJ~! zA`707azH&s4G=+-lA(tqxY3Y9^&HG~mJySA&8ESIf(U9jGeOBr<!f_Ub?k<{W;UaM zCW>hYoHPXJO<)9>z=H+3X=Z!2j;A_jK?5B$7Nq?lV}W?tN&MaIi@V!b-90Pro;yz` z+`C2hZux~il&89dL9sl;Ei$6A$rBa4YhBGG9duAtvO@>G*+MlUO#|R0hsj;FTIO9T z^WJVrly!+^UGp|f=A^@YueeOE%nz&Zn<zUeA}l^B7K3&&7o9Ty%xa}cj9|@V{nbxm znHRRsL?}styOJ*Z1Xf;lp}|584JRQ_16&cnGQj^uFpU*k-|REBC{;nEnW@YUFIRWb zNTknGKH+Q=okW5BimO{l8%7ezV(*o`+U2Qg>(b6NW~;s~!QO1m2q&C9qO&J%>&a|} zMx6W+2E|C2=ut?z0$q%-fO5ftKMi#N7nqlI2uMvyN~cC7GY93BOIlgcyjhSiGhmC% zStfr2Z!`jjvtDN93A>RZXPvTj>5-RO#x8w1DYa3($|{#PgD)A07P%H!?+77hT;D@X zO3_Q$j#}ksua!X=)pSWynl$!<@k!|cC<UdA76s}XoAM?X_jaX*7Op_O13oQ$_>{&g zRkSfY0aaeFX>!OCGbSb?6&t3F(#B*;9Di|o9E_&%6iard`hdpL)W8qsG~kEM6vKiB zmbHvc%5z>}8T~R->1mZ0exzvgyta><^IQzZqF&)Ecq_a^N$R1C!0cQ1BMoUcK=VUp zMYTq~-kDmIpccB{6SxupmxAPJ<nLgnNPCGm??dw;Z=qzJ0PD*V<OJfh%7oZ==<bII zz*my11J)ooqOM@nZ{U#$A5zM{q(DUAwaTd78sz+v6$%=Ra08D)rdYrTrEc{5jl2ap z?jTJ+E9S#>6ZWyr()XGc&%e`pv-KM<BuZNn#aqSVtuTx&_O2H1SSj9drz}ytODx`n zC}-KiYilN_eQVO;jF)XrI5yAs&-a7I5uKjpqbtsqxU(hcDvMX{SgqW>Qn@?nt`gl_ z<Zzi2Td-{HxmzQ;2Y&cU!hParxz;LdsS*6lWGyLOv||76a<4j@SDeiWXN%};Su^Dq zZDj@au2vpgsXTbkUAdHVt2p7_D!R8~OHTO_{EW8bi&m6C{134N8Ldjjo`kE1-rRSU zExwU(wJegw?|k$|0X3&|J_{|0&XF<v2yTe9dE8dQILpjLeWGtOuk>^>Wf7oBrA>zO zybumfLjMF(7r=Z9kVyuFQD&M)1Y{(XQP{oE133%vHVneAYURj0F_J4mWUDKN<`IE0 zH%%Hw<Rb)@Q&1j>2$%8P=D8Swb`0hbn3h^uB;y&K2to|Y6kxy~R1_|zCplgSF})C( zsgL}qBGZKfvsWt}%rUJnFq&E2m5GCu#nY;_4IKVysN)!$=SBN2uw1CTtWKttM$!+k zQYSsrQ-)?LnrmvoU{dM}MjN3&ptM~l98F1ez`x^ViP)}=o!Zx6@y_^)WElgssSI*E zFA$@SoN;Nx)*;6{OA~G@AOZxGn{k8;Hs?v3<EsU47u;|xIOf^E%mvVj%=G`k5d_8s zFnv>FhE?mM7Dai}<0eLOl!rA^Fm}x^D?vqJ(Vsg7z=%oka3H@r%haQy>1`GE0cvL& zQVq>k9Pt;TEvPUygSMi0F}P$~MBA3QZ40<~cE_g<ldEwlmT)$U&gQtSS#Gtq%p#xG zYI&ET=SO5~^{h2&p?&+T`8qUF;R;B(z!9(r6|?3y-FjZcB`Td=8OuF52+%6kYC>hw zM$tixc2z9m%@y|J3yfndd>`eo9roBO+@$d5c=G1Rn`^p<q)LFAq@RPd%!T|A#{oOP z0^vRc9t!(F-7XxWeyvFLtNxQ=>QB;>W&JrnnXuJ^y%e|AuUX3Mj<szj7r4M?ds)&^ zI)4nP(BY03)+Q@!Z_dr1xL=43x7og9sVZLGey_6r=IhH7iONo~vJ>yBz$#lPn#VtR z478rXx3n?%kEq$>A&+SqYdw!XuV;NpNR?ecj{4Ho=rmps^q0}+dfSZ@k!3`q%zi*C z9V5NL@@Fk?R_N`1QiPbX{8<a_IVZ7goU_`=IwdQi#aWG{5iJ5K_=|2ut#npQTBFli zJ(n|^^QL7s2Ua6xsWh-;LVtdw_37sUoy!HO_o+K$&7;+5WA%pGXuZrxSb@!+g{U`| zU|pwbd|vn(zVfhaSg|=KtUU&2B+!-f7RuA$J4hh>?+76NrJTa=Q^ZdxB8P-UQVx+v zQW4<^O+2Q^b3y6=%rT*4t{M~PN%^XX$wtyTdJqcG2T`gi$Q*UP^drtNUv$-8r1?Uk zoy_^N3G-#ZY^s!5(S@#fVcq>^FR`yTB^|D>o_zb{jnfOK=TEb#1U|I4`(tOKb+6c} zGTiHza}8)a*|sy@cKE%Xn|t5ce{=tGWumM#Ue@}<^YgYfOM$&2=_sK>7o#`ME}We| zn=GrmxgWf&qJ1EaR^2Tt?v{kRRdly5nv-Sj#e>P3+QsAdDw^V(_arLziWPg~uDwa8 z`>V5W&)#@_;dLT!Dw3|sxNGzGb3V3zSom?_ovSOYhvTh>@4LzubJxo8$wy4(AE477 zKb2=K*NgQFi4nZ!um=K?)TZ7nJ!w;$z0(k=h6WgzVL*sR%A?=(%lHii4G^`gT$i+I zWK7A>fL1y~0~;!zO=)enqke^cvrcKPMQloMU=q$*XRVmhR!nJ~iB{Gr?a+T+y*IW} znV>%11eGPC@&pwC(I#(^JRylKF8m)9{GSwP^HUf^qSWjZj??1_3P`y^7@{JSwd*AA zp|8PYxANqp_NV6FFy3d*Jvse<LvxR&f9Bk4#oT+wY{DXS=w5kqqP#^cZ<#-)NuBDM zoTf$<r2Pu!=9<O!tDk{pnt?ReN>I2w*R;8SW>ZhmY<NI;a07D<3s-8v0D{+-g%ae; z2n}RWwP90PkU#n_a2Cp)7H6e6T6h`3lQS$8gAN92Cc_cZ9bsnI!WAS^iJ#AK4^0u^ zbf%&}^%Kt1dxh}`hSxPc&ryJ%NRyt?X#7AidMtVw`4xh(Az^ETNG@(`BqZust1#_g z($Bi3bKiXF>o0xnbGJSR=^OU2j*7+6Z}cV#o5aE<l2q<a?(8LG?pba~ICdBw;*K5o zTgycvmQo%%R7<<Gp$j4E@k0j`q8~cbC2i<ZY@Bl@SgGm|5^Phk|9-(VnWNg9sx)S? zOh~I;j;Y9~OzLRiPZ3DAQ*c<ul%dkN{lbN`zBQJ#nfjKLoU-(75&E{yYA?aEB$jQx zog=pH6U+9;9s57K=d2{Mu_P5>d7K}B$lP86kvX>h^RXylu6IkCO&=6=S?`#;tOkZD zA!RlZNVf9$-dumd(KGY(3k_!-hc)%A=}mz6GL2ov#lg<g$TkCg21&!qJ#!Z9m2`Ph zdxGqC<+E8Vkhxqox;pf&j_yKNYIOBq(bYZ=JB$2bsTAfk+DwC%%ycL?<Lu;Pqn)tv z?^GhnJEx;UCled8S8_Dd7d(H=n=b>(Ika8~B9Mwm-G*G*F_kLPyuwU|)*RGYI1=me zQvT3c21$?(l7s--^q|x1bO$$T(U@czn>28h1bA<Sn}pQnB(v6g3Ccdk=dYW7N?X3B zgl#i~@p0ScHA^M7d^=2=JHFNaLHn2OnAi8qTR^eI-ACr_$-=UDVPn!!zUru3anvn6 zy=+T3yrRP!cX*Sf&_2kum$P90=$a*`sQjL*T6Aq)b#<+{y6!Y3T)RZquDEL#Ham;4 z#n@-}%E-T45wfU3(&b*v0oWFmlcq<WY4cWA1rSy9(^0av|4~6>Uz_DeZ3TUMbAPnQ zjL-na1SAZM9UB5;JxWkErKXMGbh8no>$uL@0n)f47&i*ctsiX`bchG7P%dLma_fgW zYnz*mY*%FJ3>#x%H~PeD*KGtw@ewHR0f-pKQp%G}b)<a%^dvm=U6$3eST<#)$RWn6 zVX2_EGtLySF`mmP!PlE8T_!8WZxk;S&lj&*EWluS;2sixQr!WAK3}y}t=Os(wi+mU z#G&iqu7YAr!)kTMN_9t~dWTrOBjN56-CdLkK!#v`faw3zZ=b#~yf8eEe<)?aZeZ8Q zUIH`M4FV4aBE&F!Jf_owh(P=bZQY1E^~*Y!hI6q0(}4x>hjmygCtR;+gr?SOY}jBU zquH~{f*f_}{thb1ILR{I_h<B&zM_oL5a@(CS-%pYA?ZhUWntNpu(gP`mbk5D%~FVA zsWO$6-z=og?Oi@EZrR7~?v*yg8+#I^yTsC6amOy=EYCq*<q@4q*6~l)sWQ{XPK<QM z#0OEr)>78;^ri->sbwi>({jB@%_HXpGQ?fhfN9N=ht?G{paX2lRVOr|UNr5H+4P_X z!J}m4Aj7al_}F6f!9gP%_$~_2_>a%XJR^^6ysStfNbOr@1{2Kyb7fepk{J&STNM5< z>`}l%Jx}~(oqgdYGTbGr%TN$zFyLD<=U>Eo9U%^}sysWQSRWvvVqmGaIWc}DWxoSX zs%%F14`Mo~<9#dkbzFzuNYInTFi`76+P-YqNDy{|k2DM<t3930u+hRqvn-WUFbhe= zA__yHGVisD>1^=Q^pn>q7O4z2cQl4fYM>RJDqFTT-lu@hi0aX8BsFCFWb<d^w?;Z( zNT3;+j(FPJRf}kae@VvS0#BCkFcguEz0{V$I*&`4vr@N1SPZ>=c(4s!OMN%RbSHow zSJ>`dJ3D$hVUN<&v6FuETzD#}PiL5-@QFrrCcs*|*C~8`D;M2hQi066C2cfxAJUnb zNYKJ&f^ra3g}PECXIggQ6fg~3<2v-SOx<zAWj!2YQ9!ATtYIaG@)`ZUo0X)*F92ag z+ktQ(YS_cXX`U6Q2ZU9!3M!G?lkVCzyUDfbXQo_I#y~(+l_CYx79eCNM24bF56Dl% zM(}QSnHTs#=8vsL;v0w!drKFqHL21oh)lBL0UcwD3Zs7P3xy9Ad=20I>V}dz6E;u@ zui)iOO$u+Pm-;)@AEZsAtWKVUZIfu*6t`_!vp9giYqUOEF27S1cX|@eeWG(;+_vu# zYtiXnvQLb3n!JS9u$p5I>r4)mo_Sb7lHhh`hH!+EVN*bk0R<9>WGvdRQjavJ^+?uo zf0%+5CZE}sux%G@+vB$F>UNvNu>?QyO?%V&g<wOyv@PB*4J&|_@1f+6w9@6isnhoj z>?>e$5u8%t=Alz*9M8407pz+p`8Wd@$qq-1F$Pn{{meD$s+R0smHwIkjyg*RNNovQ zyJ#crIASk3)=D)%if`(^Gj#WA!g)e;o`~B{$bh7g*61~Da{W`#2u2YzXg{s-V8)~= zqJZ|xW3nx6s!k$iE)Nu;mTQ?x&$!abed1bV-%`w`$r~V*Sch2)Uq|S`kMLi<=HSaG zyL`m>^|^e#x=GqQ_-R|t%8=E2`7(YNyLsC(na5;Y>5@H`Qnyo2Wth#+QfBs)Y5ntv zGSm8pxDHSeVq*s-BAh#;cuotiTYrmQ0cgWDv~YPT-cy!b+55`+3w_J_sBcXkKr8c= zR`#yj%71__$26a1U~{I8?Hs(ST`E?`epx48P~#}itlhY5VQaoA_5M<cdT)eZ5=30m zy!XxK6H9fjfU_Z1lj+H^v-XX8fkVo2^=tZH_%BQIHU<dOV*=6?%(#s8g$KQc&A&h{ zm9vEzuM=#fK9=rB5xqyqQ@M10=@64rQJX6SUMg5l0k4_?znnb-l69&%OdwOEORn7- z+e@AeuN!e}fw5dl0X$o<tb-t}ur!ISV4mDITR59XGUAQ2k9gZyvW?#_G=Bf{=7rB2 z;IocpN(-{Uv>k#bJG386%fG^Y@HzrQ-jnS?J3J%(vX)#_?>n0Ov}lC6tS`hoYAK3+ z(Ug}P+TUTz4sTPkHxqaOplvM|hy_#+(nyjjOD4QKJTEAYg_r`8x<P?&EV(kC0Q^e} zn%`{N=_($xA<otcgR5<}2Uhfxvbf#yvTcD*HxKEJN<{y_TVJ(3iqD*Xq^&PldmiYF zamEcJ4IRbOc&YVNMgki|&5yZ(Z_uW7K5gyrDvh24|DuhexgcC@#bg?f#bLjRXzUvB zl;J=|cKM+Zewo#zSEOo3$_%vKAU(KN^=7;54vG~5Csc}Oam^1oRg^BJrhovZX*V)L zZN~-?yW*_vz!o{Zv1zUHv<Al~aeNnCA)$G)rkdK3#Mg;tG*sO#?UUI+aH}+m>7z7A zU-Puurd8xIHGljFXyxe`8M=+IaRC1Dc_Z=wdOS*PnL{(xpV>NfRX7bygOZShhFaQG z*G-<Cy*qXQjJhVgDGtZJ^_VZ@B?*3LNCQ)!nz^AfR^f^GFl`Dykg+2k$%RokHc;mT zz{2I4Z1bJjzEti0^D$H&R)F=S@sbn=gz#oGERZJuLx+CeFd3L|i<VX#=x36(A!aI5 zE;4BUsN$mz$e+_1s!XsBZ@sK1!89QrP;q~OK*|q{zA_Eu1J@DJ0C_Z+;r%+u)<^3+ zW<dZ4J3x4Al$RfYHph|;6Vp#l00kF47SJOwDWC~o!95s?0#T;2*u*+Dty82D{)%4Q zqkP4rFy)V_R^w73dzR}Y<%7XS4)>%|#7=VL+d=Rv?q`H!=pUWV2dR4qIQWv(Mws?h z4&edeHHs!nGQr4^lJclEa4%sF3mm;>Dk3&nS1cBll|*0|VNz!o`;`r$h>erMwNg6g zudsWK($SvaT0zLD%5nBGF~ROc=^n9kPu#I5xvlGV=VG&1+7d5qxqD?kp9yDHZPhEb z>V&Np7SeHB?V6?9?t!iIYE92dP0yVd6E%HeP2c>fWJA+x!=9CfJ$D-u4MSqX5Y({B zt5?gnu9R<0lzYW;FKpOdmG9VZ+UNU{jv~_6`nQ#>DKR9XAQxJ4Cmao;qap5SphQD& z4=s)*9QC53KJKVjV=gBgO`@YI?r2hDA_+%}=xB*Mpdjy#S9w=l-nh$qJ9g*wmCnI< z=U~!R^1kC;$I=VSuiSbm;o2s;w#8krrZ0Kl{;qw=yWDrHE#cY<nP=Pujd(fj^UHx- zFD6`Gk(H&UZC&oVwH=!7qN^p&{$PK-RQ_)LV*Qikg9LS{_npHx4>N(-!tko2Va3s~ z^y+df;b;>bZE;82z3%<L-<jy{7rXo8j(%qPy_}bDY`wiD>20S+BuA?uPg`{~t~g*U zop5Xs9b4j#Ei8@+dSATVnQ-hA9sA;refJ%%g`rhP{feVL;b?><d7S-y1{VX_qCfkL zN%)u@wmD&Ig3)%|)^rc5bxa3O*1S8sp5|RuR?WM;_o|xXTMo&#wTpepips_OG{w8a zQ2R-iR4*Nee}(UI9|XU3`Gd<VC0+57E~c<|Fi~+xtT+^R9U?8f!_Zj)G{3&;+_d7{ zwA_+#dPOG;%#(#q(#<Wa1YA=8Q0FEb9%!u0o0El(`MvjCb@BS%glo6x+6{0lcFyNB zJ&PY4-;O!^r{>zBI_s#O)1xur>M+l~v;mgw4V~5sxK5N`xiTe=dU<5J4#b@0hABJG ziL;Vq3-;bQALkm`dZg)!U6}rxdFi_#GFDPJ2kzON?7XV9y&a@z*YoF1V`k3wmJQsf zTvETgVe(@GBVV(0LaKdOaw{~cpC~%j&x<gJo_kotc!=!^Wp^Idz@#BJQ-8kyDIEa? zp$F67#=<`t<T<tgjRJpZ7v0l_{{eaKeh<d<H>}IJ%e>NU)?}$VGd;(F9F=~2W}3&k z0Ddo433WArC3?QPun?Y>iZx1?wTU5fReo??wh3~oUT%QG$&<~@?iA-UTYsc%Q`0gA z0e4I!`Weu59>lw=uC^6d+wJExJq)N~;EyprRmGI=py{%x`L}T=6>HvUSU)o5dA9of z2fX#>$%_#!iA0wRW@=)-Qg6woy0j6N5Fe${9nmc`i!@$3(}Zko+fPmS*T{$_WY{J& zf}MM0{Z)vZsPx>pwr~y8xU6!utYxJPNUnBid^xt{ztgj3HNiH?WN|~jf&gep(Zpwq z*`Y+~VX^dZ+;JEvV6p4%z4LqF`DC?h^9t;#%C?AQTYzv2OXm--<yeb)NzpeWC@@vF z%pXt6UOTEGI-f6;S<HI))6fC2CN?6!NogO-`YIe42|DfGb=2xfzQd-t_2V4l6j!uE zph{i_`=3!hWSDCY-EyS7o6}~S@Gizq)p=tOL*O8MjbdekH{qY)p235WmH$l|^!gM) zXZfZ;V}1eUDMP+FVQYrt5Llk9S@JRDE{*PxEhtof9&DAVT>)`X*L~-?yWE|#e}YW4 z3ENrGb~dj5JwjY`azjI)=liF0LDJC!@E0smWZ_wKECwlveCDWC)@^tWpOT8nQ(}rb z%#aX$oBAqUW~M9bP3kCbs-yHWGl)7qg{Y%Obhh4hC!8Ikvm<WnkQ=WFC6HBvKTM<z zz@NVH-$QmCOVBcF$*wu2SEw{D9>i$BzEy8N64SA~Q>Gs23!SkP&N>6s!sXBmA`_zQ zqK)h&rwbm8B@>%AyM|Tz;+mR8@Sz1-US<x+Tb)JJ>yrWms_GT(FoZAA{2=DEa0r29 ziA1IRX>1g@NSL{@V!My1@HUdEHlzi#A@MUWX~LHgZ=6}*Ma)cjYG&!`@f8A;FXJ<^ zZcI2~tAjTi=<%#s?0^%usmOg}XkiE>jm=ILZtv~a6nc||@Fi7$ucqm{-5>0`9r)J4 z4-O`3y2P5UJ4X{Wy?2`u)d%KJQntdk3*S3>^W@S@!o5v&r$m(5Xg`pD_<O`mJchPB z9vSL^F*Os7V5v8Dz+j>yGB0Bevdc4Y=XKOiDsNV^1Aw7?F)p`WlBm8&<4Xfv&EN)` zVRoR&DF~E}V8}Gdv_&Jk)Xmri@Vm_mqfA#Bh}{J<QS}r;(hra7%@7J?gru1P$;Lsn zaRnH|#{m}mU`*bLGDL4QL4fR^wxjZflJJUZ@AEN1qYihaSK(U(l6R0Hi@YD&XrES@ zHf{UBnecRpo-XE{NyF|sYdU5w;DRmCs-tDa(Xt#(IJS$9?QzHUU(9d^_R-C&&h0DC z?XpW1sK7Dn0cJY@$+6uzpO=Y338rXt9=wg<7lqpN^#ioA<t;OFXpkEDtk6pfd%6US zjXd-X;lq!dOTt%v2rQXVjEQzpS&Z=AftyEZy>XVTiXk&4TDeC;f$@$>K6Xqd3(PVd zV3vKX&^I+>cZI~8@#@i(u7)X6RyDl*Q`9qBhcd34U~whWU-9PMcX_gFFB=fc2I7tZ zHYSR~q?CV!4_t=hXEKq+ncTv^LT2F(s-dD$8<;n+l?a4{-$kquiBdYfR}Vw~{ebH7 z5~~Yn(fLTi)&QSvaa)6oMk`EJjW_4uDz0*ySh<bVCkNp~FC%2G=HQoX%ZhCaJmc9C zw!NZ_%+=&S8NO(A)QhkRgReGBu*bvKtW~IbQp~7df{mDjc7WOUbTOUO-H5NSZ02NN zgwvN>mQphnQyWabvzeBfmNyG5C^_ZyV%G8%6I-Hbam~15I;kpWS=QAyl{CFR>GKFH z264$Uu5{fC)IzaoPFp)P>9xLybo@?jxTHz9RW)>-rie?QTfH}q6T|q2P>!~afC9}O zW>ayd_8gEoal+-zoVF=$dTp*rUJJaX*_Hvu3zaDq5|s;YVRlG`j5P|k|FGwmtay-e z$g8P9$VB#)K{ShQ#bCV4{|#>y%TeLishAuR6UwO=EkmoUZgWUm3B3W5aU1&Q3@ads zV!og^KyqS>;4wK|H1&G#HMf6oHqpFGY~IC;M(;Vw43y5lEo(`+gaj4ic-ygfUQ-<= zx~AyuqD4+)S}Ob?N6CEO!r)@hJei)tD}eoN`+HmB)xE3L2Un^O-sKY2{bF^0!Zsk< z2I97XY%~z|gH*#RtW=9VWM>cC(?>03=At@gHGzEVtx|<31dg9vZqZ4W9FJJ3{v2Jm z0ZgLfJY^^CjWEbO8tAAVK%<7oj2MEmTxM^CT78=AcqX0rhclnE$VFI@T^q$T2Hw~| zRLx+S&DL!6uTs0L*e$)^6y1&|%%$me5Vs7gJ=}nkD_+LrQui7p7!lq;!LnbP|Aa@$ zIu)4`{x5pLj7o3dt-5PJMJY*QU|be|fEa_j4ECW<X%>>4L*7<Q+;A>9=bf^eU4`OM zHI3z8X!kI-jEH&X+$uV^-Uh#Qd)&Dlu#H7?wX$QSvg7tEiOMdqvWxk;1vI<1th%<X zxVGKiop9|GT|48ho%hNr7GubeDBCWUZND8@DeH`vb;34PtnFB>?Ov(vzSEVc-6huU zS{#5gTDa)+EDk(5-<o(fjGg<@QA>Zm>BsrC{Y_S38BHG5uQ_zjR_MP17TdtQGQYW3 zze2;mo;FX?9Y+ZT<2WPk+t5}@S))zXtY<-IgsX8wF05r)cQUiNI2|*83i^ql^U#Wv zQYTwmwRwg%YM1dlNM~L&zxm#5{-QbhF219~0a}*yolgNYcXX{@FWtskD!cQFZbO+H zm~SX^1E~}jVSk7AJXX&UD>`Vik2J;#>$%70%3V(zaw9zx(`jFAtVKo~Y^<^IX@m98 zTc`dT8d*dAH#BZfQh&W(VLH7<`{@mgY5qE`TgQ6UI42%eSM+Yq^*qQxa%S_Qmf3t^ zWj0UPv!OcKvW!z!8k5{Sq-aWL`K8V5$t8TZYeV16(62<UtQ2V`Q*4rlr<CpueWf5v z%QRXxg&nXnfajI;w38caN!l5}49Ybf(6n!)8AB)xJ_p>;_RGHHuw-6A9@*Ib8txGB z=M-^-*3dp$F{_RSaFQ9$W?(1puwxTttG^l7pmL4^odZJuXI#f|b|YO{OvMQ`Ck)1d zIKT;tS+C7`XWII3K4Mt*zlD>t+UZO!QgiEo(gz(PPp62GN3Hg7aMU~F-9T#ag!rr8 znR;DPc-RaDN9i;Ywd|Q%{c9Wt$WC`sKI|=%_YX7)m}ddWsdEWUJSPFJB*5#Wpr3*< z1kk03vXd5tKcpya2O;H<13V^$lHBwK^5#U|n5bEPG>dh#RHA>y+5k@olv=JlV&wBH zU|22J7EW-O@<cw33B9(F&V_s&fSLxU3gC_W0d}?ql_>nb)UKZ+fM*;WOrZIR63(IG zQDR!Nv6BN?kK&&ra;7B3=uk8jORxw>*RWE+sb7`JUOXR6*s9>gJ#MR7vvgwbG6I(y zFtTody98Iw($??xCY;+uC-@2ZMF(J0-3rI{YOt6ix|-vzW)S_}9jmQ-R$BKY;1Zy9 zUwrF+_U=BJ))EPpEQde$&B^-ac>RvswzYhdy#~&j?c}W44$~qE9H7|JRWu5~8HR}e zb|r#jX~k-3+e&F$qO?OS?SMZ)R~e~7yi<I$czNq;Q};?!ccQ6BZ0cEY^~7C0$+C*~ z_rJUUor5<IE+7A`v)@*>VJl_3;$^$;yWFHO@r^C<P5W`Yfap39cO8Jn1x4O-S0~*q ztM2v{cYCs`akZ*-rK&YiwGF2he1G6h<$u-qyN!viqhi<5L|ebu)(`iB<rNU&l~?>M zH@5_i1`)tm3V{P6&&&dA=}3aF72hiUe$MT4zw_d6zxd(HAHSSv-7mK8U$-c@K&#vw z-<;AJz+R11O76MJ<CUGOmAxyKy?3sx?moV<`*>pakhps&QF&6Vq|*qB=valKV)(E? zU^5k!#2t0Zt?{PbyL%A)@Ct$?Id5<l?M}5nUOR-W)EETFia?g%l0Hh(UAM&D3g7mp zCsERsR^yBbtu*T5(+SrZ(RC*7I)hKFx>{CTEz4IEt{tLlN1XjZ^(0=hN%=nIIFfMn ziLSo5tB>W_yyDuteBw5z(cd7!bwhY4;W{k34#!>4xj^|%E3T&HJ-44vxOzobZ`{>; z-&U+LPBC4yi^xAlHvdn!*#O!63v5TR2+|kWMLwnVWHcD3`Vu!ljGEzdg(=}{t=I^m zW;UrCHHZXfijLzDPR&$Pwen7F)2J^5+L8(Xj8F@alqyf1Snqn~B$;_@KDyo|yb?od zc{8SGPRcG><O43!_G7tF!J-{jGjvxrZzUFq2W-|S-Y78)_>r^EZTgYBwy)D_P_dwn zCGOUPzee!*VP+h(R0nG`H8N|Lk=DXG4vmu8oH3kAo*M31^8uuP@*yVP`7b-j<>xTS z=hP!_JSSvDjt>=AgsH*)zfnUr*a~Y*%3=O_ZlU>74xNuC<;mxyrMXj+%84<ZI#Wm0 z6w&zr$eQ4{2uO%(QKT+<9{|kCG6(RGMY{_MeRq29R>ZwW7>F!mQD|8QNT3V0z9JZd zlXqMC%1l2htL^JBdZ3|+%FqEp@#En{KeCZcc+wz+GfkT#Rr;7^4GExV?J`0Uli1I= zhN7N6toU%OO2_xY3$fza=R3$I{I3*rV{|fP^%SX>ccr@p{sHy;5VB{Hcac&<DOQGs zQ?yF#ViR)HZFk(&zJ~RK)=JT}Y1P%e;_8N%T+!9DmjCF}6`s5UjgBp@GY5meSO+=? zzrhbIBdanx4=0%ca8;@+qZj`N>P4MbO?~2jLp|89_MpM5X3D_>W2r91#Ks-*#yyO+ z%J`+8RcHH(vpwPL6rFVRj?5ax$Fz$8g#JJ9A$T(2dtBe8P?8K{1=K@F*@<46{gWpj z^_AW;@_}OVyBT+isH6^(WB-D>XCEpr<NuJxvQAf;smjtlV8r1}q(?A}H|7*dKP8R5 z@;Fy1dI8jWE<|xejZPSoE-=N)uCbj^5A8lXt0$Y#)Q~GH^Co?R%h;|ncET5)VR%v& z#sN}ZMWW6ShKK=?wdsDJ?tV%ENun4gfu0BLR&XL2FO`tnP110L;8Q*a?n6b9>Q7Ln zWD8$Yj?W_?#Wq9UWBet}7o9g0t-QY>X!sJ6%Fv+qhC(iATQ}hA<z89c&BKYZX0fap zM@Q7b-B_ZoU94+oCn7<jfpd{oZJSqYL>NvaY`aC<?znBYI^DWgoZWZ!B%B9C=YhEG zfDBrwly=cQn{a=IHhjtQ%S^e)0T(ze?hOH0?j-1lx!hkRnBB0z_7m+ia8==IavW3! zqjXY$njPfQY?GM}Gq=`@CipNr3cK<g!456nyi1z;)T?H}{CRv~-U3a69BA3XbJ2P- zP}nG9?M!hmM=dYFwcuRd>+pJ(%^%)u-sjCKNh_zjqCG?AI8($X#8b+qn@x%tH#XH5 zBRHk8ok`ziPd^4Sk${0IBwWP}Y2xbLNT<&{&a4QKj3$~HE|?x!hGOC^u)(-t9j6my za)z1G)=VQ#8V_9zj9zB!4}}`g`O}q!EEZX+LUt(7^v#V0aXP)^pkr2rX?A`$okbYr zLtIq&Q-HZq(N`ccZ>BYc%-QT!`4A~p2!*&inTG=U@TJc%rz=V8|FxU1EkFJ48;ft; zYuFawemc=`Mr=3}uR4=@8(68_8n4{CR$%fpE!lAj;L^UhdkdS4s{hgNphTU2sVrzp z_}8eX@K+SPfW+XbghG)k<adGW+$0yPwr*6tHd!Z#5EpB<Vtq2E^r-Of=tThq)B{oC z#d|y>Kx!&lqYvsgI);wPp?)C274<`!8b&*C+e>Bzxmh(wR=9(2;Ic-8E2~1+{K0#b zo||tZDqF?M*7*}~u#MvzPppUPwyxN=-nQLoy?Y{I8x(DWaob>0c6wS?jzbgSHjiYv zz?Q3Jy(?wCcdjPNj)-MP;*KNm&e*(WDz{hZf;^ME4#Y}PMcmVSH}Hc<+;awhFj-i$ zxQeRQa_n?CLY4qZCUvWoZ7Y>+x1YarB~f`;tfYN++8-S2H4-g}FRH<PGLe8g&7vwe z)}w1}D{6$_C@BE_=s@++3ez7|6hOZ9M;=Fiz4edUYx;{UKh8HJ=Ep@v{WZBiuCXHi z$Mw00$!^kc45MP4x9N^Cn1~pu)GJ-MODx$-+WSY+5NQe`VCrua&F3&QpYy9k^NcCI z4w~NpU6H2n$MDdMQ%0kV5$ibxZe^J~4_E}yDZ8GK<PiQCoy+F3s*J&ADRD8`EM;>u zZL`Vp=yfrS)$q}LBxWa@j`8Bggv}$`JaL<6%>wt`p8_cC#qr{l7B}`Y<Y&&JO;6nY zG|kW4lTOao6<h1=syjRHLY=BlwDrYpeY(l{NTMW1g+=_LDKb$)Zs#5k$s2~%%ymh( z*sn|P{>mW~N>ym>M_P;7Y<nC5k_nqQ@X^I!bBGk_<fZ&c;qa$f;gHq?90ERlQ9eH; zniU)=tA4A^z)`pUe3bz5H&XzqSBv*zfjF-&5RZbu4N<?_R&3jFqT1s4($(8F;KmX^ z9Rb010T{NVh=ZMHR#I3S1JqL^g*UkOv_!3R2p=h$ETb<nFWTK;tPS``-m8?SkDF&; zI(*SUhf!@99oB)e-#3lpK!sV8u*pD4t@GXQA#2Ja;CsmbkybyznmMhMbq*?hOeq%l zOC5R%1nXQ5NOi4zu=;Yc_lB`N&I|{R267iQvu0Os(yI&^OG$6r!pT`H>*0rH4}7Cp zp>L<`?dhBaAvgu)oGBPLb5d?_64t`7oci~uy71Qsq(UgFLY^l(^Ua5Q)hoOJ!cmUJ zbeM=-ffhD$F%v3ACn@_snlCdu(kMvXRPb{c-3&@Ho`(r?6JrIP+L}5TPL_dAFqe2C zcv;%=&Fx>`{<Zd7?eaMXad&HyNC|Vv4tm?RTGO^t)0U{|5NkRT?oQF&8FzOkx9tMR zm$b1U;ci(xwsewGw|ukh>uq1_xYcocH16(BxVuGnH*VnD?)~9+hrhe)Tf0Blop5g# z-P_~rFIm~JSS%B#ic2|~>gY$NV$K#SYOwHk2vky9Q7tY{M*^(ndY`CS6Z|ptgpMnx z=ZE6t8}b?o(M+Me5d>Kgx<L!bT2wv(Yx()xSMC-hoc*G+KW^)nH@f=SjZgglcufJ@ z{a3Z7xEXGOM4JTKB<b~#r1!~KD#DKmK>murMn0xQU|_@K@+7{^x}Z=Vy&)n4l{AU- zJ%W_?*qq6*Lo5cQG$@;nUwTdHSh01`nliEMyi@gLYl_AIM>PooX-ECQjmG_>Q!@XB za_&m>5C@7Hr+z*mxcx*0H{l-cj7pRAPJeu<9<12MJ|mc#Pr=kC%{YF|!d(6Ts$gpS zif#Mt6L-L+J1pAhu=z(4#Ck9y5Mp5T0d6(`Mx=3wX3Lk}Zal>VPuICe4h>y`pw@VV z+1;Tdg_b&f5oT5|5V7J=w3r7f>8d9Y)wKB&R7)TZnecZM{Ld6H>yZBicd8{*oW_y5 zKw(BmPLiadG}WYgraHCWroD23mW~c1Z<ehP4DE2HTy?$fu=+&BcCljnqmHV_S+S2k zE><?)(NjU$cHH`X3aAw7B9cMqyGU7ZSr?`G+?JdqWmu;N(q4I}2Rzw(K$Fs>Jcwrn zwGC3iIzB;m>Mb466csMCzK{R}XMBP>nu69A^EOU^Ai;Pu=2u@R9`Ey&XFX5|Kc~5E zFt*h4ZKp2SsdmA<-q=$1KMFxS%zqf?{5Xz95!o0&cBIdv#0^Zq$M(wiP)>?Qm!YAM zph0c6p+9JNl^5wEDZzr#)AxZc?H=lb?OFRkv*3PX)wylOxh?Hb<g5^Ja${{bg9%Qj zTWJW05CYiZX))mO&Nv~V^yp4H7HGdH5H9x0GeN4*>96S$;x$ttv#D`<u#lXPY{EcS zJB?Y7E7D#KP#2MgLKa*hD{MG$5}?)IliW5;Lieq4_IH2VPTiACZEEdBY1&@$5*{88 zF9tPqy$Br=VB`LgOjRKl#x4g&Smq2ULZ33tmMNZm_t60*g@6Zoq6ZvUnLj;wc`|Zk zQf8bhKvs&Xvoj{tZncb0={r=gWx|+R=Cjm+ht&==SmUApKCLhnmoJ{b`NDG1?O39y zM=a`@w<HUT=J#Pgvv?rsESVQ>#1>+zPX>J-YKtCe;5@DmjSvS+t3mKGm6LH9dyY6T z%i4mbPi#yld~w#4-GbeiZrUuwPzQ94e5KA|i(b;#EoDR(9YGDqfFa|`X5ze|x<VOu zgLQ3Qr>-^G>-sy-vKdXYdY%6?w+qk3B2)0KMhEB|V<#dS4nJfoJ{?*j*+@x)3}e|b zYf>>zQl$fR{c=|M2-$o%ZonC`96V~tzd4s8!O09<IEvoW<H9Japp-K~aQgRnFN-Tu z>p`+YKGhHhmtZ}pU$NC|R`>96QB?BPGjE@vV+hx*dG=Z`O}{ZD*7YRndc``@sly*U zBi$Ur!OiewQnLF#9?rt>ek~UdOwo|}yc(6yM_DWU<qDKPxR#5WteFhKeO49ueP$>l zDE&kr1V!Na;9npxPA&s7N@JNm!a}BL)&iDLJ^8;*QJL8R6^yYA-SLG60wy3eUG5Z( zNvyPSFw?$me#S)Brj(XB{qg2X1;Hp-m7^gDIt5x%g#e*5A}b{;$R$;$D>sCbGDDIz z932-Dlp>qGBPl@76sIUlqPJ!Vas(6y$SEv;k6YULPVi>%yY56`i&zNpP>#JbSx&5* zBC<9v+J-|ZiM9T|^Ihk1?(!>|$1ylCqZ5_nMBqyn)xlHOsfDvkgUdVPjt*!VIXcq* zG8Y!qPu>ZT>c7z!I*17K%nqz?oaf>-NGh%#=daRnCu8Jb6z3GfrycW>IW9zS{xdWd zA<-P?y=LJB>Wy@2v&2t?g#Jt{{U$2o<rk=v3Rx$y1J3ElfXv!!D19ZP_H-s$O60UL zQu586$b*vPi(_Zc!VL+U17bssCWzARf03_ze`8-^V(XczYw)T|Cw@;vLd@2eT)g5C zvdgkSJNWaT|C}ro{vB$>oE9=~g6gJc0W~+QyPrpdUIJ@yk@zL*@>fvMb<_QlavbA6 zpO-8yU6@=g_N)|pmMap)tzvO&ytpU1#fyW4?mnH^a_EPxiRP0;xom<nKe2op4nEE` z;WVMlOaRcP7I9PW-IBzneRn4ko@28A2WPo@?6J1Oq6*r;?!$u-EVFZiv+N3}11&R$ zRR9in`T?MmJiHo5z+MbTUWMPySd<Tqd6)?Yb*wD(P&b$!Izfw09sf&ILdxUd*Aaq| zYSdHTsW6F<Pu0q{lT3THNL}0@cQMw7VtIX78q-eZ#^K!flL0+ze~?as5xr*a44)kE zb$cjs5Q1~wqd4xuu!q9w+wdHs__0*zI^kcY0T`Ta>XR8jP5<I1^+Ox$2a@&c7EgKA zY+kW7!{dT&n`qk>SN}45g(xh7lm|Za)#G}_cr1GgH5e%^I+76mr}ceyYA+iNm0Db) z^`N~ph6n1WU3%vVnZ|{*2;tl48~Mtv<7TWcll7%)tCK&frNHzQx3rMS7hDRQFnlE| zm1xWZ_wl;Le!aefbjp-cRhq%)G;N-0_3LnhlcxFvhwFbxeZG!MhO{F%SbI}Fw4u6F zzcSVDC(NfRLwWOtXG++-04@<|a?tEh+}!RM5F%4k<b{E~ls(XTF7l%RJYz2kjKfrh z$j2`S(zRoz8$$nfb-83-f8not8YfvnRglUd&(mNerd*{*CcVWnHv>X<lkgwWmpVTT zDI2T5p%=_3e2CI9YlFX~IOb5d5m15|&4eyAZ`n!2M%L!rh}Iu4nd!iG30)W^rD$b) ztLJT6u|2a9Z`1xx;myMN+%-$Fy_9)BXjpY@UU6(*?oT*cMMrDg0qK)+0(T{78^xF> zUVA3p^aa*2m5X_-jLoZ#?JJJ$2}ir=XpcME8LxBGs<U;)*{YgS$!>kJCclS8>DrCg z7G7JrK*!)6PShV>aUPC455sF{(tQFSt|YUZO2sUv8dYCwu-d`1SKNax!Siaf=xB~R znxP^uI@0TdD&Y@0@>ySzLJld2{<yJxz-ju4vvy#c(V9YA@Gc_N<53ytR~`F<CZ66| zV2WP8ZfEbb$xBm#{u8qhM`p!5r#vTAZRP)-K!pe?;ZG1Kpdy(sOXdj!m`FCjMa+N7 ze`2OoN7*WIzef;q1O>?ZBE1=$d`e_#8Cauv$&ym`Q|Mec94~B2HZ+2G#nJ5Z*cX`` zn-P_r+M_A_fOxTw?;}0RN+%EW{iXIt5zmY_l|$Xs#qVQ$OISsq@-p(Kn1Y;v1Sp2V z&J^Z3JL{E?>9uN%Z~klQo$V-1?j5~34|)gMY@1q^$G%ncK@lU4;<kGH$sM5$Au{LT zKRrJ<ikbMhj$m6Lq?0fG+Ll7fdWm>?R~gyy<kA(cV|D9Hx~>;#Y-L~l@NKF&y9{4U z+Y}n*_)&u=LC%`p4S~L0#u^)+sCF29n_3Sx_~0%SFk{?ngm`DlFUUNtl#MTq_{`>W zaR0{+(DTTayD%gn)lstE^^65@ZZFrN_~(%9<d6;0ec<vx#ixX)C?GT}70Vy;;}9`_ zG&n9#xZk7Ke}+JniqpMhrT5Gdl?WYD5Rg5|7W$PkSY>rnFctk1g0pWRmkehqvw>vs zj(Kija_PKS-zgUF_=C#3Rf^SRalcsHKW|MI7SHczzEXP=rMtz_-EjxBze^Pl%FQ3N ze1Gem%^$XX+?H_e7M;84{JLT!Pr7!kx(=+s+S6Snx?56+{AbpjGT2HYz|I!7k_D#f zIue%5<6kCZYkbZEH2HCmZKUXo6br%WI-5QnL%;|dOk;$)I^zO#Y4lu{JQ`H%GsZO6 zPuM=DQ88J{Sns<Uq^9SIkv5NVp?(@A#AWh$b1-XYFP(Ae^If9#iCvd8dyI6p3Y<FU zTzACk)Rd&HGn)$^oyY;LoyJuBEYW$OI8y58(KgU{#AV31Voj;2%p;XzTXb(xDwk*E zV4I1_lY`OL^+zg)5h{#*nEe1KV~(rlxjY=0MkrgkX470{&sr|CH^O)H3y!gr|E$oM z9g@-x;rfT|wK>~t-mHy5MIMgwlJ7Rui+8>=n+JPd%NH%F)d}QEKKQ>RfSHcrfE(z{ z+9v|QpMlVLWTpk)EOE@ncyN-3!v#+i>#*!qK}+I!ZIduJQ_%(_9m4U~<2Y6p1_0m! zL?;4WkO+Y%hl5Z3EIs%EGQED@TOrfjgt}$dtznS)F@cduA7Vk3QTG?<j+jG?Y7&^} zBSDXZ)3FYK(F{hS-w&$EPcjp~pBy1GciPcFEGDFUB+wbAF&6bBlnH)Aa2#jkFjE99 zD~xX6!Cp+t&J~E?D0E<sNlsnW{lt&#p>(;?$XGxSb|FQS)@xco*d?zb4kpn;htg7| zx6aeYCukk{Z&8}Oj_5b)hYU{F;3sx*;N~&SJWcT(n-NEDXuB4ld1u$nT?tz?jE!)d zcPi~!%_lTSq4K(=fm>&9zj(Jf;XW+75965nnxm^VLn}2ypvo7{;w%Hv(Q(I)wWgLd zG;0gsr3%iz;n}wCURB*ZQq{&AcPAXX=g;1A!m(7mepkZTD>~uQ+hX5`GKiJ_9>3(* zHWg^n;y}En`_4eZu~&5LjXU-xD;gJr%jVy*f7|}8!Vd})6>VZg+iJzmm5QB-id|yG zuDEU2ymg+xS6s5V`NqV;#8SmdaU=Al4To8rdi&H;&Mh3zpq#5f=TfW}mzz4_qP$`8 zg{4=%dF|`hzV_O!*AnI1#q#Z|<=rdg-HGxZvAk#gSkmEK?7eYjz8?oIEIxbV?EK)T zP7^$&8>#}igR5+D_u{d|-3znuoh`4s+KL9)pcRZ!lrLedAGhw(<|D-efJsuRUL}Nl zlGEPbh-={rmq>TzN~1~5skzQsQ}^lCL669lqsx_ML7=BqjaWJ3SOlzY`jvj#B)DgD zvn#9^O9I~}H#=RLo{o441>l5xMuSrmI0T?Vr8Q+o;!3nf_;vJ@uz&z+r}>1hNcGev zW27#c_-s#4(^{Z@1$G%6?tkWmr_M`u<yf_dKnJgm3fHD$5kYb3FOV7xBPk_I@Kwo< zgLwo-#6k|n>XWXDilc3aZLd^Trx~G(hT9QSl*B$6v?g5a!<$=cypaCn!(=d-8&gjB ze_CFfbnPIYCYzR<ak|RweE6=oQzjo|kaTtG-sFj{&N~*_Q^mcit%)kHSmlMqbVV^d zo>UNpSy2q`bAicK4YYA4?)2g>Sy{hYxfRw@iAt|nNk_ii&xnyH1CXS4(L`XAfs`{I zf0oCr`Fb%s^}+&sJh)58i5KW)`J(N`tT{afUj1gOY4@!hbKzlU3wsocuu&~H%Ak4> zVUs$@!n};f%q!h+RwvUEZlXkNUw@MZpVkq1?|NM}Hl;%xW)A9h5www4D&}Vu!^+4` ztVn4br=3V)1(Euo9tD2})njrM@`&>&6pf^RkJKM3+$g5~OVwa(h7om}Q^VK{S!<#{ zV0ryiKPSK>UID7bv^%{TM_D=J!A!?HWC`+2+xO}8G45~c(mnkn0Sqaud2rgE1}5ns zYXlR^G8&$FJr+!?3{2o+S~&(>cb*BsqQ|2Lo1JN3gX%pNSeyVBVGxyP{RpsRdRl*t zvV~)aHNt?4jjpa(y@g2ny8@JK81IzL(tY_%k1RvYh97*F9W=sph!p<rdzjA@<tcN# ziGX7Wq76X%*ws?ER%YLk8rO&nNA>izj4ORd$@t>hioTIGi{(K5<!d9WmyA-<oT$+1 zlX0cDNaLh~?O4H%)bXfC>Zrmtb#eO(6ecsV{}bJ<p@z(<sg#F9=z?R{WWOqke|o}b zdPbb^9wKHo>7X+E`8^c`DQ}~E1u^Ko1v?h(>He}Tvi}quiKTd?zmjltijK~>qmwb4 z;i$fK#euWRwuz2yamTh_*oVq*@9f)S`q7@+qh;1n{q&}RWdJvUo5up2nVnIt4egv+ zPoC+I?SO|;bcA{tx79L_*sY$p2hDw)VR6M1#p!#{fM9d>K5`15qo9X^UIeOOkj||$ z49x%6-n)Reb=`M@7a#!w1W14+_y7r#;G5t}qDWDq-qeE<^`L4?abm?bLkW;fnIh#2 zNVaIwkyB4gX*D(Fk=L}VteK3H(1|l)GHyrRb-HrWJle@lFOcgHL914?J88e}ZfEJw zP1lot{bqmvbKe&LMaj;#-|SvNf1Z2pIp?0&J?DS^&&xD0Iy*KQUHR40uymssc~}}% zzG%`_2HR<HD<@WNm_2mIQ~AZ6i-*2j8}|%}o*}_8l>UwWGw3pz7o_j9zod?0UAEEO zW#`OY_CnS!)44fpTBXY>8DYI%tl2^bAnLNC-yMm2j)<Nkg5!wPWi+dli^6P{wPEhv zYnB=9Iq|hdYqw9K?ifoSNIv>EC~q>eBtfz|#$`v;K|Zm>R00>+itO8fmFb+!3tg;G zeUa%H0)N|IMOivBlw8*DPFpkE4mB=iAa_|ai`ALZ*|OHHEPLIMM<v2(sX#t7u}m4E zGGoeErIycl0AqP{{i3Ba6Yy6_X*ya_N~{wkBpR8BUVxMeDMP>?+X6~YStAo6kF-+! zJ?{B_0>nF%sNv0Y%cv(o3K4}Z<)Fhx3~oF5*C|~iJ@^+Cs;3aysh1Bj@B%?J`Ba|F zj7`~KT5|jnvq8@@u@^gX1lc8#JHVo0RUDD3Xk^jopS05#<;1I`<BXm~LxSEua5zP8 za3#m)thnp(y<gFj@bn0t9`GY2&9S(rS@bl6iv@#DFb7aE*9%%kQ3Z2sX}jz4zVB;J zxS9nQoR$^6G<<dV{VKR%TraxU3+#8dp!of==0riWP|%z#sWiDJD-FCl^72T$v{@`| zro+EnLT}u?QFLz<+#4wh0*3R=ad#c0as&wGNHOnM4#eHNMfYyOy&F->Zsz~-Vae;E z0Q+!i+;P==CRv>?opnnq1$soiXxCsX#Z`Ps_gX1px}8kVwbaZ0WnLxHkyZT~p%+Fq zr_>8tOrhq+Z-Gf|sfY`|4yzQkDD8?BYf<nup<98pcrzkqBzj}L=~`3iIS*F4Q?fSW zQtFd={m@%Y27yCe1tjNM#=bG$;R|0&-`KL$UiU6zx#nKNTXgIar8Q>J6{?LUV3L`c zfD}bae&ah?TXZ9;q{{S`%*=#Xt1b^_l|c=fJ6ozeoy*}LG1OGI<|}E;Z)H9c%9W>V z=Uh`=**>g|e}z(l{;zRwVoKK5nVbnPtN?A$(!u6Xj)i-ayLMVZ*Qc#9!tCTr-YG5D zE+U?gJ!5=1#xS77>Hntte3`RO{19c3Y@d9|WtX805|2x}(#yE^2eF-6GW2#(xigM> z*e(kponzujYATQ|TVs2aJP=FD%Q}bEJRnG7E&GPLo*CZ+66ZdxJY%0@-P4s4c09$7 zYJ3B{tU4uJiHFeG*#;&50&RV17g~^Jzk1GcZ3k;vlal-*$3wttf%ez5E4>8r1jN(W zwWK9R1Q>v(c|K>E_z@$-s_&cDv1+Et!;hhlEF5zD8&*U0>p=~_16(JR>5ayI1H+L! z+oq&jAS5B}GGj-un`M6iQkuxAzR^%e*l5pC$H(9?fjirw6puQtN3Yy_U&zlgZ%jB_ zhBr23E&qg43(QN`1D6l%`)rp|(hG;#7^wGW)$v11$-_{FC;DES$wQCA+L)Fuy%+EE zi?q&Juiczjh|zgSz6@`4gxy)%ZiVH0!z`O|X6?(*DP?0er`x=@#!hO_W#15=jzzRM z(|jcV7CqWiEfjlF3!A?g<e}qU+BKbXR$WKa@?`rW^V3u6_QXwTFF}N{4Qv<H?Nv*U z;0ygZ_JZA7UhD-IXIxqPR6jopvW$^Vkd?QF3nwgL&$P)>zi|v#!8LU6i#@H?@+=)G z*kx>CTDfS+h`QS_D|oKwU1;?auycpdGnUvzr6i6++dEfjyKYwwd)NG?kou-)wKtvJ zu7vT<H)mY28RZ^x)pb_g-;5(3+pDDli|Uv@CEpn?$}%3h^1{V_wFrIguw9Ecwrs{d z(Hwh0&4k(+>vX;__M6$CB6pttJu~h#&8(7X_Y?{>)&#R8<7iGitM>4VT20WePp&eS zx_K4$t=7Jb^~kn6=t}+MX6uAQX`h0f8O~2SOh#+H`BVfk?koDIx}9~O7O#a&UzGt9 zfNoY9?X(MnT6!&H%xU4S0mIPnWBwn)rT8M%H3-i%40gX(iWV}Lha3l#r!V;RcMC&G zy#8;7{OW$k@~d|X2le@saEAPclxGWT=sVV11=9s<tKGG7hSeWd-*7|;>HlV^)3EY< z;iwYQ|ILs?dk4w6q+RA5vPZM~%%#j&jMGEPD;Cx?&O{e%;Mk6Z*(Y6RA67yyz!oa4 z=Et~aa~|cMv4qNoQ#t&FiK)6MoDec$dP(Us*eW?_2}1uY5@L>7(v!#>crA&wAb+az z@Z_n{aT559Le_KLSeVQ{O`M@3n9B21qvZ01#EN8kFViL>k)w{_890A=ZcBKE|1zrl zH-zs_H4mMcn4}^UJpv~0PnrP=CxqpgYE`tAWap|ZnXGJWOj&bu@_e)-cws_f(}sgY z86#1sR;om3s(9bC<UdXkRb^E_irysM*D)x@PL=OFItYGDFa|%>FjCCNWG&k%Co2T? z4@uTji;qF^3|vSkFGvM$2~L$L0t~vy!O$^A_k#p0Jj4)!CGm0A;jR370(98t|DJ%R zzCZ#~%y37_`3ye>xg<zyNs?86h$m1jI`w4a)KjuuY}oIhA!gi0h;cm?;Xk14-=xA@ ztN=c`{nwN+e<A|uE<OS=C;okkaFYtLqRPb6Y~pDt;>+k^5oEY|sQZV<qO$S^Pp9&f zTZxaHyl_Uc54%j6m>kC26pB-_d@1(?Xqc%&g%>&!8$CH5;UA%Tks1bM&PZ7jx@e{p zjOt0+W=xHbze6E9(MuNUw^E39XnqR;`d}pjY9)n;P70Z?$kP*(sbZ3qBW*QZ|IiAX z%8#8Kk!Em||4$U*z}WBO`AMGtCKd7>0^cCOR9e4Aq3=<_PIVHc?2_`Fq!xRUI-DdH zc+$O=yiDhSQ=PJnp2SSz|C~zrIRU1a@(&a`LjAzBQ*PlFG~CJXh4DxfN-qCM_c^h# zFn^5Fkc1oLS*UtUtc?h}%#9(lFw2Bl;1_t5pTOtDmr{;=%8?r(8MuQ8Mhj7(L<Lr$ zB=qnZB%UhJ_=H$A9>!5Gx)DI4`$n#~ueoR4zw~mxa>y$?K*V)cEIVovjvA<fI2uJq zqu^+S)YmKZbH}f@-e~>eqw(VAcwviJ*g{g&9?=Dz>Kx~=MHMs?s&_3{?@v_k|8Y*d z`mk7iIPN|ox{nC#2cDrMj+Q;uiuGYDT!C7jwG@O%CD^m<CC4$}tXteGcI>!a7Vp>} zZ#y8i9f*5}MDLK`9nzF*lD_rJzD)_=rdzq<=7VwHA<=i}16vOHq5(if0Z4gAa`O`R zLK=s;Ao7wz4_>q`0r8t$as6Q2yI=I~7rgtKpa*%7iF=#i%Te$)sWD$Uq<W#bTUL4F zsSmirmZE<6g1qth<?`-Cd3U_LM=bAw=tXJQt-bHUKLP?tSir4lPgJzWD>}uBPNAZ6 z#hO!E2md8+?p$uznP}J<Zx|3823~W_+vcC1fBK_4Rh^WvP6Nq`;JgDm(WQ0Jw}I8) zujD7n+l2Bq#3%hVU+GHt*9-pj>_uIP3UWwU(JfYV3l-fzuc)RJp@hF(@VB!Rp+rS! zu`XWGAy#w<6&)yp3^4bKRqG`;mP-SP@{L0I#$;7+F;{HeE>zL)PJLH0IJg`<l7QLY z##XVh|JFtbDAv_OK(VeK!tf0>5LIfZfiQe^%`fx0?!Is5y^}YeixwvXbqf!_erW#C zVq}T``csQfB?G~mN0$SAi9p}dg<GTXz%DVcO9<@xd7u%ETgVlIJwiGC@S^3$O$qqV zw%vL<-Z&sO4!|sQ6|58I_;*=Q%yOVB5$IaH6c6-^fqo&-kHFnP-~3)Nux`l)%L7X> zsPhJPih-R%U?&3T3<zY^Zx{UZOV-tY&HrWpm#W^Znztuw8*V<oT-%+f?Otk**Y=CG z{qwnS(J555Cj&dENK``%?6?)XjUxApf&D^YKLW|>hJ~2e(l1og50!dj==GsH!KP&J zz;f_tB6u{}*rw|#o2?o>1pp0$>Y5K&sF16!Q*#|$4nC9!KD25sH&<PMw;}*ZF`;53 z^)FPv`oyMvLV$jEp^#kPBi8S}y-}<mn$Lq*s`)(X)^2zit=K45Y(%a*Rrub;+Fr@+ z=q*d4s$Zz;#}I(S6WH?0LM|9uEEQ|lp%3bJFV_zx>WAn>y=Y29B?i5ray}1yAb<56 z&exq^X@RrTiXO3|N2ut*#CgAQW1?!KP_^++edtS}H$(G#l7ZSc4!?f*{m|BUV4E1& zCIq%o)S=f8En42riwD+=f%QUQy*8%Nc%VlN^az0-vKw)9+1How^~HUgMc-z@w|V7J zD~Hcc{sRBYN56!otS?B8q(U0duM(}xzI6%Mex#|}D^%>Bcg#C(wcZ{;bME1e>x)fa z@x56auiGHj!Jmoj&^6i8N-})3l&C)}F8DlQ%NpyWmFl4YxgO-GYD?U^RrGEZyjxc- z!GB{LS#Lxc3b<2JK5w~czrO9pwy(e=Su1|Y@`@W5@hOycCu`f}iCgzh^R1$HLbo1z z_mSHJ?>-u@-7nVepU<7o{Y7p4LicMIZ(bB?*MHNuIQmxk+vOC0w<>V6Z*H%yiW_g` z#_P7lz1v0acEP(nQ$5jSsim+9o<p@%fUcOn@0Q$p=nu-?EsNLfjC*&9-d%!sm->8- zT`Iq5B@Zva8=;0-5LkBCC*1W5U=7MTmyn5`KYsIx<?0QI>J9PgUa`7Yn&ntP$qFXG zTrRy?_Wm|kI9CX+=C$WmxPpS#WKF|d?hO~7({|95@HWN0EfCohye-T=I(oX7>HdTy z7jK#wQD!||C|S>*HZA+w6TbGiuT%7O3h)G1T_;wrU#{MisNNK>?iZ{3QIR6dP=L98 zNuU4Kp_hlQAG&d94*%dMuc(!#`OvbjDdB61`&vX_i{NWfUc5fxTdx`<CDSS9gh=}l zWYRN5fAkSal@Gmi^y<;LyIPe6gE-hnD^@%9U5J+Yu01PyYRRuLEV_fk+kqf{=<?_y zFpckXdv+dc;C{pf_E&R13Royyoxi^==SMA83WqEdZacIIH$Sbjer`SY(}w&L{W(AF z9BBVkP0srjc0|2jQ-G-V+v^`G$@y6U#r&+K7%@L<+Fki*ZccKi4N*VOr9?k38mKwZ zo0D2cQK?=VqEh{Px^eRt?HfimaerA|iSS=;s`^}U&fVew!gtquMy*-Qy#EKZ;9k63 za6y4qrC(+)D9K4oD~=f8$70&bltFSQ{L`e2l(~#F?uL{Hv?5?@>6by0Db4;1oeCkD zkg~y8#_0G|F8Sk}Dq%O<pTp6Nkl0uRK}G$Q9Y565h-&5KnJ33iJ;@UfTS9zc0fI11 zlQDJR`eaFO^b8+~oTD>Ug@#)ipL|4U<``Ndp_yl{6kaQwB^QF8(wEL%JvYBEURW;{ z))NZpog2Kmb9N_*_;0*&cy`b1p1W=j7PRG(b#RQmWLvrzFWDiM?1;N}K>lCxS3h48 zcT|f^b5yTCs_(*Fygmp2KO}_uUPF#UyE<H%2f|DwV8jb5pIr;|wzSKf$M6<gT5?2~ zbEG*}UsEm}DZ;M2<uZvW<tcaizvg#lBU_rMKO<N;m69?OWX)x>2@Cni!ifmT24iqi zMw(7h#mLoPqcrIZ^;Za`Y^TN}Q0`%;sPEAuI{A`!R(5!WN)(h&$70f%)F~a65&4!Z zH%SL8y?7ons&6HAps3!$o#~Ms?TWsOSIR<qHL7_lxFr=GwaX52sS$UCL`O()gn+~| zEqc)nD*%lNcO!hviSA~>O`hg5>&lZPZU*&wtg`G_RRh@><Xv{xCfv1}m<#zt4I~_a z`G?dB;hP{ETy+Tt>=ZPJjs`*bG1Ed{_YYKa?^Ra~Y&LqV!xt)j9)E~Qe6P<#uk>e@ zqyp9NRWh&_R#rj>rn&kd&R8d`;au6^$UQz6vnmX@_glfS(MhC$(rvYHU*PnTFNkZL z$<-^OhHbGTv`)Uv++9ZH#?;W{8omJMnlri2=W3pSrgOqv*sfR43+Kt)d%S<57N3YZ z@s;<`-xv{O<SXo$&SCFDUufKPP>JU=md^TyFEzBoDU}e;&s1lo;;mAY*Hj}{X2}`w zI1T-g#*4_wY_=rqHs+IO0b8Q@d}`&iMc;=^L6nFUX;-+wY$(R0Xlvp#hYO!#W$+$D ze|e1cff~Fm?A5qEpyWf1S9hNWHOwNJAKHd+u^es;m&`a~by_v29a(zLq$X>Af7q8T zpV@B}lu5JAsd{}q%wxkA)Y9Q8H=`Ru=@SGu{%^v_!YH?YLm%Q)9V31L8yIEQwT>M- z0MllZvB^`D<4O+Nzxe-w5<7OYGJr_b4NX9|im6NyX-~@|6N-cNzp~U%RXxm*jLg*I zzs1Dj-$1c{L-Tv8cxd7xtQLlYR0XN7645ocyQ{ySKTBy^hbD-y2aPRyr}8S;9{}|> z8Y2?&<mupsuJxg*I@y~!ITu&b5!NHqxPkSAiX{@Q4Mg8F5q<^bOT<?P^##;qBm83& zN9&bD<YdG^6+z<vXL?FVQ!1A%!tsOvOLWoCQm6_*R$EP3#`r^MZ7S#Fh0`*LE*gOs z)I7OrJlSuO&?;Ix6jB{Vg-UuH4-J;2^mJe!?M6<dkkF|K75Di?QQ2JddgYDE6|Ts+ zj)B<$9K^jva|2f|&R%4)<ehVQb9r|wtKdS(npaSq1hHyzxvVQu*0uOdylj(LwrMVB zE(b@|W;z~~X#n^?T#3+uj%a5xV5Vd?eYfG(=pQt{+Z^}p!l7AjRff)jD>+C_f8hqa zWNyRUz}$vw&&)R`JVC(&pS`7XHkDjBe`2vAUfmgYcR|fnGUP|b8TdKNEPrj&-06fX zK$@=PVrTd5WAWMpKkkXw4#$g+ip56-_tCpGb!0@ON<|C>f8Vlyd&0l{)~UFEx9Hy; zckB@z<lTVmN_t;9aP`34u`5Te9htQxUG6!{)xO!jWO3=-$+@TJPTpt%_US2p>C)9p z^Ck16aZeCVP-b)QphB@L{nz^Ep1!&rj<={XJ@MjRvA9=o_rkaM+|$UJ2S?O%qc_T7 zP|j7XRD7X)abKdYTd3<!7H^x~bEmj|q33H`zPx4eOuV5_Y|vSsDC}M?+?*)fe5?F6 z2&_Y5;SjK68QpE1&xtz%q9Y(EzbKLGzf(C-V12K^x7%iWuda4CXZs#!L)f5Nr*89) z0TcXf`@J@Lqrm%rJD4!Dbc-I4E@oF*#f1&X0;gG*H7@2%oK{6e_;<p&SM9U*)0VL9 zMaQ&duL*M}|27)C_wqwAZP$U`zp-Rhcy9+QFAwEqu@h#*rZo}Qw70`9$lA&nQMY4# zFX(Mt=;P5QuhNGLIuuKZGoN3K?L4arm7tswC694KGv=7doe1dD;=pamY|kR>Fy@31 zK)dn=bFCn5XImgPSlSiq(xS}eArCRB(ymyK7Il_*RO|{njromtI62uCXzxh7GMurp zrA!lA%vvUQHlVy{&GmS&Jk<~Io(IWO^8oKzEl;gdp8juaK)W-3c^P`~<CmaAH?{Yg z4_j>J)N9YR5G=+3`_K+GEM<fCTnic7@+=uyYgC>h+@yuGh}W8saoMhBJ``k_{XjBR zLg(_q<5P9vvD5G&+WBN;eEi(#L?^SNtC|Ju1W((rS<%UX7KRUZ0FWEiGmvk6+)w9< z1;nIZS4>Okx5}q2*R9tok8y8tZ&~1eirD8-^2Y-;>_YSpUk@xQRebI|SmjS&7~{b+ zKhH-_k3B0Jf0Oinf0e(DtfCScx?`$CN91DWJr%lxrYWwa{td#^Xyy6nGAEQCm45RB zi=*Ef|N6MF^+>#HSnL``^xUZWm}#CcZ2gn%j3u=*_<xC3^S?u24#3b>{$<46Un7rG zBezM71dFc%*r5XMR``=ufj73jzD;P|60h1SR&9m(f{Nm#52R!z5lhZm{i}S45WuVM z>B-Jh{8f|~YLRrQjD#Tm5>=g%fqsHSOu#|n%(v2*G7*bMDfAXS-9n)j0P<fu4pJun z5k=Qhj2otECMRN}I7+}qO+BS#%D0-B0WFpMTlC;-1QscmV>BFQ9?p0_#Z?n%CqT+t zDVt=e=HF2S)8l%NLf=C<DeuTQNL<kv$W1b>iW#WPlVkv#6ieoP;4qDX=gy~c&m)j3 z9lZ!kYjoy<_BW6710z>4Ndf*k4Itqz(g9+ZbbKJfn1LK?n5nyH!2T05Vpf{n&cdK2 zFk@%IubZe9V2HXOrw<4kxbFBW=6#|cERqh<*8!t9vM52&*`BQ1E4XWt%EyDxWWf&* zFFh)j9)&s>JQI5B1#jE$l`K8-?W%XGZaw{-+NIhRyJ2>*DR#HIZn?TIQQa4>-Yn8@ z+lmdTU`nF`5&-~nx!`y7Y?{lJWDMfoc9DMT?liXE@s`PT>|FM5NccCz{k@{UH}2ag z(r@=lF$Y`FzLpP4T_j!r@Ig*N6D)K9Kpzc1xYdFQ=|-5AtHWCg%4B7<$5aLCU${%I z3|$+-LB!#FiA)A<Q@xMM#)4kiedFLfe|`AIuu9pJ)6Vs)XFxdyBy*J<sU+$ZOLW#C zq_@mvi`1)!v-(iVjFFvjN0;d665#AhE7KV1zUqGEQ`bLz<I{#}W{xemS}+$;&o@-0 z^Qv>MP7+PI+qdc8jeTeA+fTjo6#SyHv)+7Itm(ngXxUMja8%BBtFMqa0gUSTwtt|O zd#|=?U`rMYq;#~o*I{Uk+(%kr9%oI?9<kwCG9Q>SY~q&8x`4*585Fb}T4qtMN;$=? zdZne#y|2`^`$}!Uuhe<>mD+J%sh#(gI{&^>yY4HsTTV^Hv$QL1bPD9uj6P~kZcL52 zL^{WX!&9XoRE>{cg4iYG^}s8UP%E}qrV5n(L-M}q)AkCst4OgG5;Y8p!OTjM{)wDE z{fSS`b8hsMF1=E|G1f1qC1i<*(P&);SpQ+0SB6JkIoCMoa#B6La$+sUMa?@sNe<hi z(1<2$43h3N?YAi>X;HJCSBfNrjrr{vIXO8Aw$TLBu2xKq5-%uK1k<LId}QSG7>_NP zm11Z88a8?4ECY5(y>4POO$BwRV8#@`NgHhy-X*h*G@qQwq7otuk>7NupA4fNyn67; z;cJIy57UB2hUIo&@m%xFdXn`GUu*kv+m|}t?3lIBejaR|WT@?1m0z!XEAVz;)-HIc z%w%WRw@!Th#9NQO{TM~jBA+QLL&gkIUF*Je_UmWg8h?A7%4&-{+LIZgnwr0M;>#z# z^w^v1jbsu#nIS4z_qBpA7ktU{rU#Xu!OAQz=T}_UT(hpENA-h%K2k6GAZUU6kO#Qe zq71r*KCfDc-uwq>2I_)oJQanlZ=2NOB-Q>B%%-4(tEnH^ad`RoH`pc=-uV*3l052b z6f*n%@G)y)CVWrtnlsb$S82k~X-A$gnx}E5YEMqan7KaD7n1OXTVWE(X#aq-ddVE1 zS8~`aDX*he>P3tp)~C&&@H}XrnyC?F_$#bagrx0JZ_ADZA`A=`8}4ly=?e@Pt6u$P zCJ*;{y5|nuJ9PIj>>YOM?(=b<ue*2Q-le;D<KC^iFTiZk_lnv*da;UdfsqD<^K9XC z&S@)DxpD!$(>B1OX**!?bRJ;Iv;)vL?F1~H&Ic@;b^(@8yH8tB=Yt`7jHw?N&UmIh z;fgaBnRS>G_MfrLc*2zn88?h+6Z;Ce*HZhcEOw|1Q6Ac^u^x?gU_>5YQ~7}`<rf&g zEOY5=D!ba83oJu!#I6hz5TF`uG&4&tglmoU;Qdg#C8~GYW!z#7@6l0xp)jqg?0}(5 zbawf|s^V6{I_trv^~6}48Sg|(*79<6rRd5w_KCT?m`X1LV(2wK4F~0!=FwAd!*#|~ zU$8K>VU?0Q?F|RDUe-cp#VYieO7mUQ=+<YMIfnNc>t<{_&>;QDOl0dxeLQlgbmq(k z4vh1__<qBPsLXt0t&BNliu7+c%$FivX(rJvQyImp%#vJPDcsq$)v4GpONxze%Q8zE zjH$ppFV@csjK2C^7)FH@yfaeMjr|OJ`Z^kExH0W*+=A{~Q@%5(hBuk&VW^eSoR?|7 z9L+j1+*y^Duj^g=s^eU8pQV{BgB#mB<C_@HTAoc;CtXb+uy*zbD9vY>KPASxEAuB> zVo2r7oC+cu`f+bIrot>MG0ie{0h;!io~Yav%(%>Rr3Eun%g<^2n73OWc&4a)8eKo; ztvVK^Sx1&To2fLDn!mA!X3AEXDS5g&>B?TUc4b*>=Xik9N)2<M%vkp<Go>_hD$`5} z8B<}Vl$qv3Kv^ZGOHEJIJu>S|X~Rs>@^fd^93Nw*s2nC;Kbnj&7~3>co^>p^vzbcE zUv*?FR+%YIU7d7guU=l(+T}k$Y2}8QQeo_gEHkA%b1Hs)xP8Vy?GJY_;c35qg_-te z!8$6YNj+QbRpWYKd=KVYg=t2qYuI$T=}AzXh1pi6PP|vkuQTG4X|Ao|ywYVHHSoH3 zD|KMR!g}pa3mJD~5-7xu-k>~ZVr1(emusRky@3-1PgUZ-MBvK={xyL&34DbBRfhi> zfkgsu0Zi3z-@YBrG%u+RGlS<RqtUUGW8?7t1&^9|JVo4(Ame^a<x8|55(S<rlqf@R zcEIRWQ-$&~<vw424l0zIkHQ`~ULiJ3!IUsjpHxrT@U6f!fpN^AI5P!r>Fg=p@C)dM zDf_mG$%)90DQib?%1(mx&&^DEsE#rTibd3>HB6O;XRZ1hxT^fGQ%;quC8<yM7)09O z6eq}BO}9==!WkoJ88h)Zr9Yq+{N%aNRNkS;v+SK+Ys&rPWHd${7@FFeCH<*UV41;l zqp?#@Lg)-OV<Al!HoE@_rScA-9E{x5y0lW{9y8TiHmM%sLCGM?sH8-|U!+OUvBnZG zdeKlS=g7dGsq~J@@=}@D2zBq!l!v*#&_K$yM|(UKpb^^#caM-7q#h>yai&X6lQ7gM zu^Sm_N0a*VQX81mpBI!sQlWr@tC-~J=p}mgU67z8w&N&;UZFU~iu~^>^cH%8|11FW zkVK3>*!L#IX^jynu^#`J;yy(sJVW^ybMZFCy+?qt8UKhvKOn#ujz6T(j|kA_l_Dei zBhkqba)I%EM1(3O7aBzF<iAB_KG+AGvP)d?lnvsV7b2;AHp|1r#^k?<VpA?P;!-r_ zlW&;biEKtc3MMA$iI7e@>p*#)q>7EvH2A3^Ljf$xZ%n3q)fn(pg(0=>i6mS}Sfa!m z%>*TO=@i;vkp4Gn6nv4Waz6o+Ri?^m(*Kb0TQ2HI6m={<6fasQ7OewOx1whL^J2xi z*@9$klhC|PtlbVVy<|g3Xxl3`>=PV8Ls*|iA8u?D+6TqP{cr&cZBH#+RVUO#9~T}t zDw6teSz!Kgv8;X81%(7+n7z^j9oD#Ghv?WLICk7EsF;VVzSd-cU#Nl!=k*W@Eri%2 zxRINa1=UJGeyFBO7SsqqI0V_E3see$4su0UAO}|b1vQ{L7CE;w0Jj{2bNf~TFrqnM zJfC}YW_Bj&uba<@jHj=H^xEblabKh8Yn<Ju34MO%jgi+!79UyqRJ?ktSiLpw-X^-Y z3GAoHJfb;m%l__!zk8`Y?%yH$cf=h#MaNFTu~V01OWd(lbZiwIThYL}CgKlPCtVfu z#iA>)>}pB4S{7Y#SFh;mCBpmWWMlU-v{4&3#v3<*HA^}eb+G>l9SzcB&x4aMe>FJ2 z<j?`G6#8xz#Jz)}cMu}3m0;}x%pROQ2*tz78cH=ImUl1piaq<%Q$dm5?U|jrvh&)` z`O$={R&doKTS-IGyAOqtuP^{o3N1vvt2WO!-t1beTpD=0TC9Zfdc1PGSh*d1a*x{7 zdH=E>irb;Zwxx&O?u`34i~h}VN5AOk7aaX}W$w8L4Y)V!n&}a?XnOG<<k*OD4FFd4 zFWp=fyt<SneIe1;Cu}~9>Joz+0D9e3NCz-`X!ekjP$+K~%QxR@6t^6Sdxu5uFfvdw z03>4F@Lb$gg$|y7I^hZmt{{Y9lU@=VHg!WyXtC(#6EuXCia0=yvv9$;T-TeZ>y6j- z!LhPXw}WLTCz_`0&Vpq}Rl-p<KeX`NQumwBFFkj=`<>_GjzQ5eC^!a{KJufF8d_Kj z;lr&>^leJ|It-oBNS)D$&IqE7O`+A2YW+rCSIDu>M|q;JpY?LvJqyl~R4W*DfIgh3 zA?|7vU5$&DgexStLf<?lbPvWm_luqTf83quJS=n`X8l4c%%*;+?p`W_?lkqQKUuwD zsavexD0MQi(5c6gZXap1cP@p+z5`${I0IlWIElUBl-LUp5RpHT0`y*86HR`6B5h*% zCZS&&_IeujdURY3#yxAQI!aZCROER89|cR8X%=Y-QCxj9UYaSD;iUy$!CNO;_FWiF zxLX8w3-#2Vc;{ZRbMKEWiOwORbBOiSj`W_YY+vja{hj#k3X_#s;l;{z@;9jTQ_;Nd zX3bLWJD$~hiRM^qhca=xMBmn=uhTFULNv!h=!7PW@9IfgDQPQ`l2=yDF*!9<i?^^j z)@3MIOO1j<ovaR7lIrSE2+?@K3)}u7(H&ZRD8WSW?|6!Z5_mxD6g{0wMG4Oa!L#9R z@qY5c1O-i~xHU3?_N2Gu)%=(9G3(Jtxkh=pv}OQ&kYlIm2(ZH0vDg)I^;>S$iFMm( z2G%6&x852M>vzyiEtBYwrlTJ+^<pvk&lOAB;CY88WG+oe07&)#<U?Xf+4VNX9h*hR zX2G$U3>9=pvUYDxygd=GWv`K?=2tQBoC2rpUOY0RWIRrEKp~_{B{*>g-Wd6hA`Tq8 z_&uORWaRol7Tsbz&ynnSmbAGj6R4eZWvPRX-)1T|XJU8OlI{0V3o?-TF=~N2WE6qW zw+Hg;BtdUi+WJ-W-8a<!o5_W8ozC>)MfhdJy~_2`Khuqlo|9-_LwY@@k=m?{W@FZV zbddp3<5^p$QGhw4@#AJ36GS^#Tcxy>{)XN6Qx58;Y9?gyfeFcZ7&e8!!fD&v1z$zK zenahBMm1MCTPREQjH&CpLM;q4q|O%}(x(f1B*nQX^^&Slzz?8x84X^?f?8WMn3vSI zM&&qZIXDh|<23fMk$xjdBoj@8(KU*OVeo1c4$3b=7!#+llam*q%JnZ%V#+d*vh3l1 zhXr@1EPH?DiNwzBg2%84oYK2+w&4F2o-s#dDa*c;We`%uIY`2^*7#TP>{pG)AYXjy zNiuc@Ke=-0D%F7ul67@;$%<$^^J>OUcT8xt9}|M#4FK0>d-pNdX6&$sgCkBs?BJMk zNiGuMM|UbBDX$fi^D6m8@^K3d)E$JR6DK<+{*0=xo@`TPhOF{28J^SfC5hDhlQ_dY z#a@-N4LyQ#NHUdt{HE-Z_euUwS#7CyRH0Npg``KYafL9Fr<fN@CJY2QTbh~LNlZF_ z{SbvrRq3Q@%%k%Zaz(93C7;0bF4!&3s=JWAENy0%cEr*xx3-C;2WImXq|xat#5N=u z&saFMxFzoE11gK-N2!0gv^i1Qj2&z4xIX_Dv6OIpa<B$4I|vtR4v2StoFahM7ndy; z)g_AHn*b`{H4FAd+hXkP8N8>MP^;qnSvwMWf(yNG?i30)K-&xM;&&`GzPU~;*|5|n zmTZ}IpvpI%SlIg}L|=QCJ}s7RpUp=e85hfO?zcdkcDbx2QP#4Uvv^{W2?U9q`{HGT zV%gyA{@MMoOts2$Y`LU5-j;y)kh^&9;6nc5fu%FIdkDKB5fcLrXorm*c!gX%4opfc z+jo0ZEIR;^9`adJ&zjgEwDc#KJIPze#qH3}>xTlnV(x2C+>OH)BzyNF?X{!J?hOg| zhNZ1bTfe>io$Yb=fao3&l;4U|a*m#4g{AM?L}{Y6_`};+!|uIr>y_7ys|Hgnz$8n* z^ztF*wI$n{ZI+f{>#DlbVP#ZTTx~5iU<|b5kz_Z!7{;LPXi6;o1;$`9(GEGBEM{86 zgfZN!4%sn=T!t}Zt_5Uy%=kstTF}1mF=~NwY0T3ueV>wdWZbL!pb2BJF^qwvqSytD zfiM95uBSfJjgB5;pf=nqy&hvQU?@Cc44M0pFb0QlUTQnAsfV2mW5`?%VGL_7N8fV` z>W;1S4*i8x6mUFEpJ*l@dDPuJi(3lFdjZ49n|4m;Put%v{3>SiH`E!;z6W(bM`@~@ zhOWmwS{NpcT`wHfrwe;^cm^p7v5Vi1zE$NSlvw&3Yt!z+MaI5lwb9y>QGx+I>N1Xu zu}@58WaLR3^GEX_L&lA$(g$dr+CThX;7)Bmzl=MT;~g%MC3}7n_JvEc=nm>Qn#OvL zS!Uc1v<hy~`p;M*T5ieg#KK@6-((=dT!vo5ZrZ(2r@c)J8T-#z0wnF$F-g0EnL^a3 zUdyG0rcLvh)jV^`LSyNWkR#^^8S(Q>YyCol_Es$vJD`Ql<umLQ=6s7a>~!)%OhZrk zKgQ<7{|Ny?QF-{JfCp{@|DJ$~n)3e(-Tnk%D!20#+-?5{y2pP`LQ$IxD5~-bGmbjQ z-=Gp?zFB8<>`W)vbJ1T}Izmknn?%!+B6c~m@+x7px9Qf1(em4<4u3=s5)}F=K*|9J zzf2uZLS#z_8&#?!W3v4Fl=_F1&xE^{8&j*uD?dUx-lrVDByfvv84ITgD;U3>;+S66 zTL`7>QCMt^gi|&c-#r&)8eEbsUE&PGmDfK~a>hSdrW)IU1*3+kJVJvJ;8RpOA;J8g z5%_ZgcK~3OHjJ)MxfMK|Wh4DIoF#Ayh)QmW-$l|WEjt=Y%t8`gd>2or0-5k)rI+Ri zG2RXAM@Ee6Rn4FktHl{uRi`LjV+Q!8j0bNPOZ#tih^70Dc(j5tFGLp)#!EMerJJCE zrQno$JeuWi6HAG_T~2s(c_HDSz@w3xVcic_gc*;no_}W14hmdBCOmqFP}r^E(OqH* zjCYD9{Td!UESB~x?H5b8eZqJ&>7>;yd)pG;w#B}<cZ2BNFl&>s>-Gc$>NV{8)`cH8 z5Psc}j$gxuXL;vhSnS#>mJ@zmeqh#}bd?LP28LhP3(cDo?#<xf+}bN{8^&K9ehpvl z`$YFX!A)Ei!moQ0?w+N?ONYNb{LXOPy+?HK5!`#o(&6WqJ)s0t20ZPer=7SW!DMCa z?A~jK(A5P+qC2pX<962Hr6x75*bqcKd4p?O3?#f81@FeYppzc^)@Q!{8KM^-j&~jr zJC6wd;n{-*XYkN(cx5I|-s_t{<s^*<@<V@WzU}(tjY-u!)}7j>g|o}eeTnA2c=Kkl zd9!3|l{h0X><Vz*HJ3XVNqUROVB`GpxVN5Z|AB)b#e6F6Z4kW;0{fxq%kG+lyJr4r z)npdfV-ofDi-+QlR?*QaD8KZB1ugaLVDbA{IPP_@U?wGyPP(vJ<J*-k)e_dH5KHB; zOP@mB<vD}6JV=I_w;R0;tguP2kRc&<<t>?oq=Cp-R@+Ef87f+-*8kI{`l~5)^|x!` z?1W20{Y}Dirut_@^ihB7>h)KNJ&s`*Q789@Bd}qA21b95vprDS4gVv?NFv<`2!ZOs zT)eV<hV3|P?@_5GY?I=DL3w_J{^fs0d1QCq{|CkX1%RwfEt~QA&xkRY@zK}h-_zPH zpd_|-&uUH=VIP!nWEhXCPH5VcaBUJ?n_zEPtm<BB6gM0YtA>7DCsvWVkanhC<2O&t zLAG|dHVdxJ%p}nIC7;-RK&+%fD-X{e(iM7N2lYG-M|9WbboCROkL*k8UBj0IJxi+| zJ<X;^x!MdMR1nADHEBvRGa~e@r(MQ3kf9J9XqZ+dLj~0%tUjaO79A{ys_48v4Q4O; zK|4&P9Yt{TA5p(9h8E@wtA-90*^?>nez;oL_nG4(L&JkdNj3<!r`QaBk!B;atN$=U zvRDZ7;h@p}m_Q5v6;w-hFDe<6|34{%WkiRa(AY`XiG-A_(46+`kP=B)La9}fW;LOr zGDf2LBge*3Rxz7@hm2Ic`rOOUU7xuz6EA8Ki<)3g$|-@_fn-V9tJ_}Qc74Z<9kY9r zzVcUxUmjK^25K8;56;En?ttJ9sJw6(FPC((m$sNB1AuPScQDqH;HbebePXIh{CTwM zUQ0ZdV|9Vo52xNK4U~IP(l_v5x@b9DP#1kAmi`(i7tyGh?G1GzVhX4e%%}&$?$n8e zMOmFH%%P&XWM1~^B{nnYZ>T9zhDz_inG}?h5%nI=R5}N98>8N3HUnsG7xJqVh0OGS zV-CC@RH_k8A4TWfazUjLtk#o^H>c7A)?7=K7Ll2HV5}v~KGra=@b{TZvBk<hYTGbu z=t`f@MC2J9DWX$l(%CE+n+zg~B>1AlFgQLKDwV!%Z9DoQCK<CU!G9mU!T*#%oB%s{ z{Vj!PsCbdU|3zS&a{Nye(#}YKO}ETX#a~f~S=E?EI=!h>{_hZBuqR=t^k35_--S;@ z{^WJ?QJOFk^zt`|jtzoi1LN$HPYKn&!QG7{wn86j6>0NtfLIkc?RM-<jNvYevsj%Q z7JM+Y4om632(~ZojR(77ww{D`s)5Lg9>Nfvp}UQ(Opb?bo{%r;z-9)qo@IAa!rc^i zw}?y(;O=UeFDiHs^A}`g91L%fG2wxBXSUr+^~hq=)loQWkJ9LTzsbM1g8QS2s=Ylq zr_jslMc+5uG7|{gb7fkSNdh2bSxgcb=Qg7sXI!&l>PiEOpfwBMHqE59c}0{rG=7w3 zRpnmQ=}fTo6kHWCS3t~Pk*wvd?PSvKMLH8~_cCa6=ThEOZa5M>WmMmmUZdwONt0J| zc4Ng_7kvb!%JWy_E+su)vi^7FxogkO=F-j7)u}7f*QRl!*y0l^dY3D<CMvex+Izb( zUa?=S*dKQs5FG~u<(Ize*p;z;2fh4>jqO^rL#+$y{(?HF*F6yuOXjgopEnv;BifCf zPd}s8x9PblFpT~{HG&c0+=<_Zey>fYgbrbDXWX4lb+jR!x_7EoLco;HAQ6y--(Z_I z>4*W9%;j^Yb5C1PyECYrU9R2WueEkqf734@tESok$H+sys`MB`7Wn|I6vnTXWGrU# z40a1m7UyCrb(-Vgq7gkYlSl8H&f!n%bv;b|>_Cq-!Jw{6d)K!_FBxD&nJvWicW7>} zr}KV?!=7B3wWLO^Tysg7rz*CfrGiF8niWCySm_*2mC8H!neoY!qvOng7)0o&0uM({ zMaC{dlwNwE$kqqzpPQ+lYI`_xHUiNDNC(ipISq58Cds&W4f5A9pLv?p$C<^hkWaz@ z2&XsW@!zC4w(m2v|NllaQk9a;SaNO#yBR!uMv?S26ZB72LHhp`TybT5=#w%U8zy<k zdK(4noN$pE3xA8M8Kh7xY9ZUdm1w5_9Ui6fpBbHq;mzmAFQu%LPw~G?=`HlchC>5C zn$-yIe?f6mwP_~}W4#=-c>f7XlNWEDjg3s}HROmlE&F#Q{P5Ki_wN<`d*hCMq62c- z5C|xH_3@V<zdmwf1XNGLDBvKZ*tuJAW_8C~n)Gg4_U=h|_uSqq`I~~UT>;3O0Av!E zTpARY&+eYxoh&Gt8%Y#23I&bHvhr8YzI^uj_>J+|{mByFtNkzcU*CFTD^53{qZKtQ z7wt$C?YQ;Kk8|TiN5rBd$m}U1tGiXnvWmG+%~?M}VM1wpqM%(UXkVo?LPKD0eoMl= z<<=%t&^;u&hXnT!VN<BmUHNUB6GfYE`QS6DXh19)z}vjVv$i`f&z$wj=4+c@xd8jR z_$7U15RssqNmnV!LO0$i_FoU(2tnSDs+U{PLy~@=!o6SJEs;0{?}nted@kok-hAnt zhyLBokV2)&MdR)U;75Y;Tk+r}>`mz#CT$T68KU`j&kggzj%lE_ji^WumPmD5dVv;9 z^=>U2p(bz?!nwUnUMS>mZzOsFN<Pu(RONoQa;OW09Dn-4c-LE!eH-0Z{-5dl(vbKx z4BdjN8w}D&NRH{9o%FGsKvD9?qPORzsts(6JNiUNpWx_A*23H|><lwenyk1qq-ya4 z5!>Afbo-xV@Ysp?|CPZz4NH6%_{db5Y4F04iLpqyi?^Z{eA?*bu0A?)p}lJv9o;#q zw&4>P9lP>r{Sj8%dwyDL!?Ea`ffYIZGVT3Dpj%5ivh%x^bR_kOR!CQ7s*4%ZC3n7; ztAPQiAGngA{u&6?X_Otp2JlAeL3XVLue5NWPS`Wcen4U+ciLvuL}m1&%yrYzLV!hQ z#sSwrXw)b_7hxw8?ZGURF1=&=%#I6;<H<<(y4>51y`##JD&^B(a|tq4MeEZw<uXuJ z-~g#gUUC(Wqa?wpuDeagqy<>^F01K2%BkyupWV6h@u|vVkyB93lq1=xG)U#(D~H!k zMrSp-E@h+{;(ut)KT{<z*-6Ncr1T~^K;;SBl<2R2P9a8?A;L>YvIHvOPUNR31rxfG z%{1g33#TCJ$!?#4^MT1{B#}$VaI&~_qtBw%<f~Sa4Gkl|&N8UXk+9QB)Rcx(W$LJ{ zv>fe0De`8~9(Qzzjt;@mkt`~H)&H{pdex08Qh4)|o4?mwH(k(qtFC$D(bpe+?Xx#O zD?o2Y^PvdpD{&j2U-r}`Jar4raZgC}gm8{_d1kjM{Dw0^?O@!!Uv%#m-21^K1#=l9 ziOyw*KjHAl9aW$Y36822s}oF0!XwDFPSOkYwQiwj2pnKS#Yl%rR-`H@Qp64Wm#cRr zs&~O%X!QZH`hbQSc}mIdC|sJ+kJ%D!Sb&OGyX5>c?(G)6-GaB9=`pk{+6CV_P<yTo zExW4|?&`R^R&>`2?Dr8^rkSGQzlYI!Xc~X7XJF$#EB6Cy)xK&Yv4_Yiw0(W}y?>fT zSY0$x2lX<3nv6@bhICLr*B!4lya~peObaLp>1W2K3_O&a%*!fqV$6%B@yN=SBb8v4 z`3P@pBQ4Gbl6el^fHrI7d;?*e1T=|;$1W_M2XU`1SDD4=%-av@XfB18V6{}k5}fgf zY;J)uH(P?WK2VA3O0DUyxdeF$zRz51T7nhqsjyY`>P^ncr<${@wZXxGp~L(35^sy~ zw~BP@E;|O-q5hKJXtwy0Af2?Z{+vRLtWD@H^ZP9MW&Sg|t-_${mR-9}%!m0~hF2$? zTVc~NI*EM)VmvN69K(q<YDW=zj+lE5YpUGxhiOgSckeaTu)yM6dgA31*B`s_7|x}V ziCxJW?wzs@v26WPnb1EJFFPof9fSb3M3=ndE=sz4m)%<v?yX60*?j)OA#Hv9z-A*+ zZUFe11#m-qD#I%4a0c%*^eheDIuvg>AT}Hza&{0l8;vXGjY7#Y=ECW?w^Q_X3f|6S ziGTh$JX{JT^i$SO(YJ2d*OTz|NJf(d-$u9`7Ax8p2jLg?<E*MX2U^jiKd@Bo3m8{b zm4yCPe3hRdYP*J2knx_^l8#W@wWPC!t;}4a;#KVmzH{0!*!98})`Y;CjE=GOk>)bH zGOx1cwSib`nUPmxN|<ugBgQJDQF$BvHRiBzb2$@78B0y8)0*-rE02yC`~$pQMTlSc z!!;fs;J9T?W8qv=Z`-qwRvl1Q!iZ~oit+BB-Kbb6Ui_G&qWH@DTm9bj!e6a%RPxt2 zDy}t__sNd3<G+znwoF|ASfi}Ne4_np`aYaldhQ-a)^PreJ*JxK;<y=j?V9K@tTsCQ zQ(JG$tIe--B}+TR-tRi*lG#R2uW?QntnnKmv3MXK>6fv0;C*n-E8#trK=Pmh0@JwD z1=EG6tw!R;jGHl|RJ=B`6DeTGCK<sQ_q03KsI@}&)0kB}-$2H3Klr?xw)6ig)}p=F z)N3&Bn^|;3uXvKieDY*AmudFGiG3kkN|RazOPNvm#H_@#O3F0l2Yl12nu^u~V3L{4 z6ic*ky&Eiy9_E}T?Kb0mu&-59f61cok_Q90hMg64AvGm+wV2M^hN@3{7gY0phL&)~ zdZKr{nUz{lW<~>U=C>n%o&F6<I2)l)M@n4Nit+K*nN?;od05J<LbX?E1MoM)sG_$l zYo5v3QkrJUDrM?MV9oW=nc7RslWlfeGNjB*j#cqZh{kxbtT8ZfZk!{=JG2)D)aA9m z9ABJ@;cLB4^VeJ_Mu^r#Au5qmR~s$wv{zb-&2N?SD|lM7ykdJbgMcVerAcVzQC;M; z`DF6kxVlR#0dneAYAtW(R)oC6Ael<MD?LqekbefVja<+|pN6>SGDAB!dIp~7p}GV= zw_vMJPVhXXYgE%c+uf~u8e|N15EGu;5}x6wD5+#lkBW?)I<FK9?!8u|A=6a5iG#1u zhh;LdlvrlrALpN^SF9<EWR0+kUqBb}4`CUbs?9(Yf#2JZN`;oyuoxkPk)r=2<<$fP zH5$klPz_16?|-Ev|CYf2Mu3S7)KiFw4Ae`Uf2HTc@Q8LQHiC{BlRTtBz#&y?Oe43O zZ=ganjW8N>NzIC!VoADTBnHYe`pa)o+#S08Z3@j2xJ$QxNgxl?o^L`Z<(h=h{@BC_ z9B%RaD@YY8(vfONx`1b7@)B||HwGyT9bue5hsP;9&jc<~&U2SWs99rSz68aqC6fR| zO1A#UUZdX76LK%k@y}5~PDqx;n7o7}D9y<0B>X8+f^<+4llI~*Q`M^y+Mgm~&~M?i zD-+oD-X8R#W(HzVP3;?FuaCX<)J-@*s!CQ?y%Bmn^jiDPcDM>DDx0-~kRtdx5^mUN zljzy)f_D=l6-xLnRk<D%^fZb!9M?l&ArM>+bR_~^@xXd9uzt=7<)0hQWp7;q#*`HG z8U#L?%T0@h;tjd1FD#ymTs=2?ZlO`E?-A;EQa#&VZ+ordW(Twjbd^?A4VY|2-?Ddm z!n-}`t6iu9wMwxwX0s7p9{|)U05{Z<evK6uU_YU-MJQ|`&8wY47prcTy058KyOzEC z6W;y*l&vDkkGd7E&RHXKT3B~)U-s@zc=z6h7S%z~doW#;1-{t^??5?j@XBB&`rO(l z3(bn`lf_dq_k6<B1Xnh91J!RFeEr~Shi@L9b0%>!^!m_ihi)Fi4W|9s*U!E-esg@z zMK|&{fE#}`9LFvDhZFwcq^~+z8eC`>d|k=XngyreYx^KCw~}m6RTj_L=j@m~l{GB+ zK*B!&%H(_q6O9r8;4xffmwmA&;b{>(E#L;d>U`OG$LCM_29|vX621dJ_Qid}qHkC; zu?4B0qGCw>RMyPFoe$(<9L^F=P{!?f>G0LV@55W1yGL~Q2<{%(%6jS0)kE`km3Ndm zX87vxJaW}QqKCZSAs5B;#@#{CU}1`mvA>vVn<hrX7-qlll+9g=d+J0Fbcix@&s>aq zc5PVJjk~l6+g<BKH_Sq=OLnX$DsRpFCD{IA@2E|<YZr2LihE$Y1rpG{Acerz#YVA> zOr@kT-W1+knE(Rsh&csZZ>N94;G<<{qQUEp!8zXhutetQ6dj#{qf>Da2VoXJjM@pX zYbQI6gCiu_O|}x#C>E1PBhk?=Kn+t)hRCkCW1Z+&Cpgxj&(b`}X+(9Qq<6VwYocW9 zt)bhoc*$Y0ge)SO45MHPE@@A=+65OhJ<(q+Fg>@3KI{}BA*1|ALhMslAD?}kB_Vd= ze0brrOYK7Aj<|cL=-w$PKPLXwoG54(3YwD;eE+Oa+fSnuP;+81=AK(PB!CyN^|njc zKy@+<iN+PBu3vVyCfu!yKE0Q9_*##WYV`LSeS1o{@0V2V>B{-RI^SRo_rsd1!Ty{- zuJ;`*;{K$l>R?CCPn^EPo4B8Bsyb4f^V8r!_ov#q_uH!;u^XxHDo-<j?emkNzO(ZH zP^0wAbP`{qpgYRTf@7~m&^@N|`Z%501~Z4xh^uG9ydfjTpCp8@U~lB)g)_l((KC$M z<&c<NOtkX@JlA1%41ps_3t%gwr{NZqDR(K#h)i9*6giB_i0mC@X&6bEUi!ZiDsmPD zGE`)CLUp1@-v~T1%Ff6NJui)49ltVhZGybfJ70na$18=`3TF%DFXWY``Pyawx`cn- z@8yc?cik?Duiq!G-xv1}ivGd4W54KtL0kE^ilA8A8fFYr@<BT3S_|EtW)Dv8z{{lT zqJ^&s!>8~VcbZJaXl3OpDfEAhGt451{-|Y5TQt4b2PuouUhlI83g07SB^g6z`uW1i z=p~r(J~u*Ut;gZ&_D@kJ|0@84I!_JZrh4yn88d%Cb7CHSkR~j+Vzo<qh*{zRA|CUw zPP|U=&@WkDHUIEJ@!T|1B~QcIu;8!QIA4WWg4Izh>CA*<thy;g3H`p_Jg|v-ugFbs zQ`PR`oRIZ6<~W|<yBZ!2*#PjzI($6j9A>90+zt*NCwFcOl*%f_hAfB~486l|z=xB{ z9~n6}8NL9IyCdWz_~{Fy;}fIj=q_($Bs_Tv0sG`h66KZ*Tg2)6d>6&BvsKE06YS`@ zv8N_eu8|Q~ohOSAa4`H`B!G1fGNz5OfiY~w3bscV2dimO{x*ipkn;S6lcWpAlfE#& zhstq}!MUR17?}T4iqEB5(7Kz-8Q6D>CmKL1XV3A60e0_6Igj&W=f@*^@r2o(qiN2Q ziXo392*=Z0mTYqzr_ca_PZ8KnAVftEQfNPcy#)3VI6z>Cz%YTM1db3mNZ=5G!vy|- z3fM`ZT?CkF^`mGd|3A|0CIXuY{5}C6HQ#ZbhrAUZyQFio%v>}xJ08rwdOH=ck-%mG zTL|nRKw?Y$FoA~%kWuxN8}7ejW2eqVVoy$n`Oi@NCIY7kj1ib5@HByo1TGPnATUSZ z4FX>#@D&1Y6Idef8i98Se48qNfI>eduz|qO2>hJDpAq;Wfgcd~1%baO@O=V*MBpC? z{3C(?N`Oq>@pb|Z0;j0>KcENYbjwcl0Sb}eJ%5!RG*YOAKnDTUq9DJYZVwTlb2NXM zzzYN(p}5ZwU{>d6DD*giZ_?8x3VnwF8A<1VNZ>93Qf=o+7nmsrG9@dX1onB7MCM80 zl_yy<o`fTKIt;Ttj;E!ECn_vY23GiH0z^^gTM2{+v=L}0KqN>$Ooet(i0B9Kp>2|B z<~u3cqehdYMI_93QLJs`G;GfE>nL2v!eqH0R%Z~oUW$|{2BYD#7qE+VOJzyRr&1)p zfnr_qqB;_pJk9q|Oo0+}YJ3tDp9ocyxmG`erxF!lj6wu{8~>w^;fuVi{#&s+Yz~|l z0DL%i%wD|m?6qevA0V%oa`3LV^fJWvYnF?e5=Bi5Pm0Z3;ze7<qOF$?t`s^d?LuzZ zO0l!te);H1nes=9_2rk@FAuMjagO@SGh$v{Jg-5_Yk*dP?XV^3-*W4Txc|^>;fgia zk3vdeKewhu$gRXdrYv~liKNeeV<=e}NLEy?IBn%5B?3_R&+M648j>qoB~`SPDhhDO z((bdbZ~&JNuRLbqf{lw03#IGvOV&3pSs`tLAB^xXZ4mr@_$7P#Z*LGngZO2Mt*Kkw zfHNf>E9)8;PYUJT_^lK<i|`tN%R?(=r3k|caq(Q=ii^UKse!!UN&$rnIcNF2eZ@mz zFHF5GKC)6o;bJMigu*^4zLdgcQaR-mu8_*{Q@E0I*33V<Qbpka=j>TJuu@Io8Y#Y( z!a*s%j>7d)d;^6WrT8WaH*?Oixv7;F3b(?v<Kn@U5W;B2dah-CvZXuO)tg+uEg9;k zU+=Eu#@)&8Ey<dY^n=BJWW+gwas<+67!+oD2WFi@@%p%<8y!*8A#`pR0y~mLwL%@F zDWN42SSiT!qYMC=e&js;@{weqR><|E7pvDNgPW4jwy6k!76rIcbR@@Czrv-#C+v0` zMqLG7&W=fKv31i-@BSbMw=aH1im=O%y691t?ok)86UybJk^t046j65X0z0X|PBj<8 za)B1xMtZbS_h{paz1&u^!l__`%LeTl6;wKGu!EoiU!9G}HU=<g>9>`wa7J*^GGL+D zG&pE2v29x6RM6(L`Bpd;ba3|K6>AICK><|T;ujy2Vjs5T+bUN$6;ya_)hnC|8l5)y zNl-xrw{wqWC8v%$(g+4E9^2Lx&IsxYC{6`6F58fW`a*?4uB03)NFdYDOz`?vJday! z-W4tlK5yMf%`$=sOM&e-P@oRt73=5?GN>%FVM#QCX1~p|!l|IoYuiE%mO*2a4Jt}T zFl?!$mm9%0*hqqpXR3Wc5y7G|c|w;}u9SY>LN7^!yK_ryK^iO>w5cH$*H%a!tAe1* zww@XzgG#rpb%j$wpvne^94gpy++wR(;nLu^wTIFf!C6Z$MH<1lrOdV$-I)g0`zf&s z`pW251?vm(J)qJu2s&v3si4e74^&W@Pq!+-Z0n`Y*Mmkcb+!s{iy5ebRvV423JR^X zYN)`GN8gAFimkS0dMX2l)du@lDscF1SSnP|=b%whfzL)yRZ!@%!IifPDy!*M1v?6C zZ8Q&MP~)_1pgxd6Sw0P%3M%sGMJg!HwH-vS>!3T=Hq2sF=x(A9SOvqDu$3CBgNJST zwi<eg3<7SN$0`W4(Wt24Gqjo@r49~Si>XK>Xw9VwsRH-m92$BZe9Ycp+l{*F;6W^3 zAzHp<aM9voNp!HXz}7+CC4(CEV`QIYUoKl9bkN;EjZwiti;X%|1%+ye#TAy)x2J-x za=KN)h8YXZART-@$7VZ#Ht1lXIt|!_z;<0aH<@&l%sr8Gz$;F75^4(7Jqip8_1lvU z304S|AXk>mjjWVN2pYEZ!2GA-f&;1PZ*S63G2e0%iRu<Mi4FbQU*&Q6d^r>rk`Dj; zCb4QgkdPW|p_S5)kyIGEc?#&JdI2wG*N1(;=wIa`+zCsVJ7Eo5PCzFAL~c0eMec+x zoC|0V+W_+-j<EejlYHh0XT+(=REG01Ja>d0X|b+|OP9-;;khH6pBC$mxOKT)8J;`B z?zGr~NP#X_L5Am!aA8_(VWd!(%ai^*?1fYKB7Pe>Z*&X#GM{*T$Hva#EFF|+0K_bg zje-YBT;wMs!D!^v1-Nay6qHW-@KpyVcW^j1IuRsqZ!vy!B1-JVE>_g>CnM2FaFmY( zV;4}Slv48pC({Tzf|B9)Xa^aE3C1p+k01y#58bkIBF_?o^9+g_KLfrgyv8BH_!x8R zHZ~EQ<il`(hgxWr>dI#s_fR&a?$i{p)T31Nx=CbEvY}z9=IOD>co+{)p9bwC%32HS z`4N8n5(;3YeXdI-F@7%Cdh$XHcJRk91;<VYFHK%x>QtyA3Og4(837j`y%ihfnMS<S zb#M#_H*Qkb3`v<{JQ@t=c;PU0q1rr@G8zjiq?TZKG7=@1cHmi0o|%9(`VOf+pL>Ly zmhI#DN&a)63qCV8K8`mB`O&c`RJnpd(>w6`GZ>~xA`?)boQy=r!1@i2vclL1QC;90 zkouCUEOkjo@N=I#jzP)t9+n$qC3Wl##$M<P#le{SkWRd>GCJi5`(z1_bcRIGvhbUc z3SRa85z#59T9$#hM+B6q3djzyDR}feNkv4vMqspmEH*OI1Tl^%5w0(D$x7&tUh~Z2 zpG1qr1XnMr4XaTf0<Usm%YynyAJEe}jg)SDGgiJ6MNTzrQ--L^R4b-|_go!6vjb@v zO`F=sS0GoEDkJNx5rHRDi(pcK9~o(-7Lo0V%N)+H-i^zitqBjTvAlaU?l~-a4hxRM zQuA_k6(@=pYaUT{e%oebPnbpTV#)%FOt5U>iK%wEAOMqNZ1zk}jA|}l=BIi~VHYeY zDNjj>VK#{xeZitPf){gYDbeX_4a@>X3)Hf->#K<Qh8k<E@m1Tb?KDt^7ah|%drgb} zOm0jS&O}>H7C#`W#QGHNAWE!!g-xl@juPb=%Fx%{g8soMe1k@UU6RNUXtq;jk!R0i zg@GWB^vPi*qNVJVu5&qhITigO3HZHYc@?z$Y3^|cneo43y=eIsoY-8qUUTB>VN@9c zV38D}h?Hdl<VEI#6bmlBJ{Y_#QEXmDs;O4fMC~Fi&aQ3DqhWLh0#p@P9Ny7Oldl2! zKf25bzD?3^zI)-J#V3WPKK$k$lKuipyEDzDsX|r#Y3%%y5%RY|qr;D)YW&*>fmW+5 zC44bOvhSjnLT}OId~HeNd#PEn29rQ-%O#;1-a|nr7TA+xVJN5bJJCu_b7>cAY$})9 zHRa3LT58$v;bmwUS%<J(*>r8wE8*FlvpXR@FgJSLal<h?pg#Fx6MWj?cc-{Y2=vB_ zH;Tm@1^31~CFNhtTgZLWv*e3c^obRHlC7k8#dfh`d%R?aSh54=c$JPr_bpfe;=WCA z#5U`^<0_oJbY;i29rFVTSGC}(zEd0!sypMwUEtV)V|mBnfmi{AZWo@8J3^u(Bq+c1 z1r6OHeetwZV2S0<;;(dRtRE~|mZ`w8OXslq^JAwJud`S|gNkEQt27o(=+3l-sthlg zDmVA0oU@-gl)fgcQ;;+8UHiP~=@uN_QdMbMC>MR)G$%jo1)!=f;BWdB#zzY?JxH1M z0fD2)DoAwxw=Dc#q<Kr)HQ2l`>=&z6dIwdSWL+<Fu$)USwuIv5#ll;bxMPdx*di#u zkJI15y<=w}TNP}@p4$1$=q31MjXoLDw)rSF2R8qkGxT{XPezN<`(1vp8%_Ao)bCw_ zqwCky@2rvu3@5*zs(S#y(A@_Sk+rV-sjjB(Zh%w(ERyw#j$T3eeVpzt{f!GJ$Hz`} z5*DeVQfeO)<H<a)Q}E4vX@p^%Q-1U8Rv#Tg-T6b*W+HzZ(3?+WLbIANHdN-0JxU#$ zy?Vbv)jOhAkCp@FqA(tCAKr6mC_kikVx8W5j5}vWUs_Q9R7rZjJ$oEgGOq|E(DG~Q zKvp@%hRfW6S?l>I)zdU9>lY3!6~!GrqN7JpejjI6mhI7;G-sHWjI(L(SP~PMsxo)1 zVsIae_Tw2k7@l)C{+c?NRgy6_Gk5Sk>wA*w8^lc1t+s7K$Ijd6={=%jkD&Zm1BT(A zfSA>MEzRm6fjR&<3mb`!PsXBPI5TD{<B!r7z;_a$^&V~yBC!~F(6k!y^yTn0`aIRp z%)+8`FFX8}QSC0`?y~xC#hOnnECLl=Q8oAhxfUYEsKB0UJHm)nDs(%E4yFQ$z-}dC z7=TJDrw9jJM85b0QSU(}2B3#NB|@UvTZvc$pv$*TmrtX=%lSZmX9WFh`8*;hX?UTQ zu}qCoa)Rz>)iDqdrZ+lYJV6;jssy0wc$EkxUmxY`)77+(@)e1;a>_`gRf6=q!W1Vx zVPnn);uL0xdSN`+^&v4S^~4<zazNez%s}8F^3DlII1l)agHIu&0k0-p1XF_h@H8t@ zf1uG&CF79-b`ewzMt`W$>_4QXSC25-`PR<=TQnzC^6ALvQw+o1JH~|iNBK)YRnLu{ z=ifn%_#^;WyX2eByj8IoKUG2$jfu$kk<s(&!S_j;y-mn@nfryW3d-nlU$^M%#tGPY zr?lqA6Y<g}u@oMoujS9;pR__yCn#4g0<2TsMZkzZE+}K5q-B4N4Q6(K$kc<{P1BZb zNT<g`7xeE_%QBZk-NnR()D&yWr-%zlq=a1lvv`|qA%Q~7{)}wRp4mWP0`62JP`jj~ z(Ql!hQ-!*|J%1r~j=J@;NCkQVY_;S|vXiRZC1ix1b;)2OSx1>W9d}ogkk3I$@>CUO zg*+>MB>b7-3B<{xE|mYf`2!sH9#=I`WfU`1$AzZx1CEPsR5JJFGzg|f(LKYsQ617Q z^SGsr+M041N6ls)wE_%@q}bud#4;e7^vEz;5b`3!Dp8^=tu|WlJJ|o~_LsLWltDza z1a4EhvJBe2Myztc!@cLJ8feQhVt<Z`JlKda;XmD%w4IyqHgI~ovW*aEvT%=b6S+zc z-v5YY_|iU!F?%HYn2}wQsUqDM7LJTZVv*-)*nV@BVY55yNE)A5r8kD=PYb>#{64x< z0^XRV0K6dukf3nxN)B%ESz#}P7<PtFZuO6`4)wU{XyV?q40yS(<ZsCNmKA|-TQ=l; zCkKJ|yj273Ifng&I*k^R4_5#lY`_g9H=PXU!Wss|NbWLB19cBV#nkJbAMToRDbr3` zZtc1cmr;36(3*Qvoe5^tmYx_U!bPHCNB;#)ga~T7%J&PB%Y5!l=W<1BqM|ik(I!^3 z#Y@`7lJ*Duikk*1x%Voo1~z6H)SsX_pX{KnGGJ>MZW7Hk?T>59WgIa5N=qa+o(&kY zj&B+(k_a7#dAX6PV%>);$6ccV`}DmAtZXiJ;}Utic)%el8E|m#IjRO4a)x1k*K{Ue zX8_u|B|AyX^bwnQqcjCg@@lk+GKBC9ReXX~{4!=6{lf}FymA%f+ek1)1>mz9ZXI;1 z0x--CPmQ-~DH4)PwFvz-JI(N4qndw{s`XhcjuHYmNO25R%p+RS_}EEFdX>gmlW0u^ zPVtw{$0pD4qvxNz)b-@()Va}E*GV#w2e)%Pk=|3ynUb96$Id~W@M0v|CDZPhP%Td+ zcqUZKH&E9RR+1`Jc^k~Nk0dzAGsg=lulfvJzmYT1C{K(RUGymlO_c;2J!)#4V9!C5 zm1tJHmjF@Mc{2AXSq@}kk)?E7MxdMk>E`py_FyH2n2r1Zg{lcKSxY*>^Fac21nLPi z5NISosPR3-F*_&;Nn0}EN+wmwucup*nq)$fOfHfqvL(Nf05Okrvc3IuyM@430)+oc z{`htvlq%A90GO2Mubq@)7d_N^CmfA}=EM(BY>ApB65g<JV|PD2*-cN%)hACyo*e-% z>|%r^kYuN|k)fw&BF}09&AKw#Gk)^I`EGuY%Jdn^Jj0&!O7hSo1)cGEACrwvl^L=| zPKBQwnE(&LBqXhy8tmQZfg>nbldRSyIEVg$@-tJp)%A{K0g``+vOP?d(mIZ&btK9k zGsg1KQR=kg6swH@YAD5%1h}pYNwJxKgkrVsh@Os}ACVN8d0Omr&83+S#&m?SA^FeX zA^#|W69nkoz&{3%Dp339LTqfDf1DmXL4c4|{v?6Z04Y0Mwm&--OF8(+8T2107{sDr zA`k3n%+@Z`qV_AtHc6I$2md_2TdW;i^fm0^z=JH7l~$`I7bkN72WQE@?4*B5uHZ8J zm*ny<vwum>D=5Dt=eo@PCAq@O>|c`W6z=uA%WW2nza)29$o{*_c`mbmNzN}Af9bDF za*cxVmmafX?Y3A~xc?vgf2j{}h_~z%xE<-gBv&sOe@U)QF#eKUw_yCG$4Cj1Tu3nf zlH7J7(=WY*BzHi_{!4OugzUfcX`bY^2$_CKu0=5ZGW9^Sz!`o?u3a$x(qo_vMIYA3 z1%<g(%KZQ@S5kV}vtqYeAfu~-e9lrRtzNW**kAMr-j+DmDsrs?*ScaK>ay5ZxXf_Q zDS9EOw&?@*{O|Oso_NeMY_aUMtZ>=?d>}phJ7bcQ);0@JzjW{c#r<7|2Mh`Pe*o-J BF--sf literal 0 HcmV?d00001 diff --git a/paramiko/__pycache__/util.cpython-311.pyc b/paramiko/__pycache__/util.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..50fd1c067765f34a72f76633e01bd6581962ecd1 GIT binary patch literal 14990 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 literal 0 HcmV?d00001 diff --git a/paramiko/_version.py b/paramiko/_version.py new file mode 100644 index 0000000..ba3f1b3 --- /dev/null +++ b/paramiko/_version.py @@ -0,0 +1,2 @@ +__version_info__ = (3, 1, 0) +__version__ = ".".join(map(str, __version_info__)) diff --git a/paramiko/_winapi.py b/paramiko/_winapi.py new file mode 100644 index 0000000..4295457 --- /dev/null +++ b/paramiko/_winapi.py @@ -0,0 +1,413 @@ +""" +Windows API functions implemented as ctypes functions and classes as found +in jaraco.windows (3.4.1). + +If you encounter issues with this module, please consider reporting the issues +in jaraco.windows and asking the author to port the fixes back here. +""" + +import builtins +import ctypes.wintypes + +from paramiko.util import u + + +###################### +# jaraco.windows.error + + +def format_system_message(errno): + """ + Call FormatMessage with a system error number to retrieve + the descriptive error message. + """ + # first some flags used by FormatMessageW + ALLOCATE_BUFFER = 0x100 + FROM_SYSTEM = 0x1000 + + # Let FormatMessageW allocate the buffer (we'll free it below) + # Also, let it know we want a system error message. + flags = ALLOCATE_BUFFER | FROM_SYSTEM + source = None + message_id = errno + language_id = 0 + result_buffer = ctypes.wintypes.LPWSTR() + buffer_size = 0 + arguments = None + bytes = ctypes.windll.kernel32.FormatMessageW( + flags, + source, + message_id, + language_id, + ctypes.byref(result_buffer), + buffer_size, + arguments, + ) + # note the following will cause an infinite loop if GetLastError + # repeatedly returns an error that cannot be formatted, although + # this should not happen. + handle_nonzero_success(bytes) + message = result_buffer.value + ctypes.windll.kernel32.LocalFree(result_buffer) + return message + + +class WindowsError(builtins.WindowsError): + """more info about errors at + http://msdn.microsoft.com/en-us/library/ms681381(VS.85).aspx""" + + def __init__(self, value=None): + if value is None: + value = ctypes.windll.kernel32.GetLastError() + strerror = format_system_message(value) + args = 0, strerror, None, value + super().__init__(*args) + + @property + def message(self): + return self.strerror + + @property + def code(self): + return self.winerror + + def __str__(self): + return self.message + + def __repr__(self): + return "{self.__class__.__name__}({self.winerror})".format(**vars()) + + +def handle_nonzero_success(result): + if result == 0: + raise WindowsError() + + +########################### +# jaraco.windows.api.memory + +GMEM_MOVEABLE = 0x2 + +GlobalAlloc = ctypes.windll.kernel32.GlobalAlloc +GlobalAlloc.argtypes = ctypes.wintypes.UINT, ctypes.c_size_t +GlobalAlloc.restype = ctypes.wintypes.HANDLE + +GlobalLock = ctypes.windll.kernel32.GlobalLock +GlobalLock.argtypes = (ctypes.wintypes.HGLOBAL,) +GlobalLock.restype = ctypes.wintypes.LPVOID + +GlobalUnlock = ctypes.windll.kernel32.GlobalUnlock +GlobalUnlock.argtypes = (ctypes.wintypes.HGLOBAL,) +GlobalUnlock.restype = ctypes.wintypes.BOOL + +GlobalSize = ctypes.windll.kernel32.GlobalSize +GlobalSize.argtypes = (ctypes.wintypes.HGLOBAL,) +GlobalSize.restype = ctypes.c_size_t + +CreateFileMapping = ctypes.windll.kernel32.CreateFileMappingW +CreateFileMapping.argtypes = [ + ctypes.wintypes.HANDLE, + ctypes.c_void_p, + ctypes.wintypes.DWORD, + ctypes.wintypes.DWORD, + ctypes.wintypes.DWORD, + ctypes.wintypes.LPWSTR, +] +CreateFileMapping.restype = ctypes.wintypes.HANDLE + +MapViewOfFile = ctypes.windll.kernel32.MapViewOfFile +MapViewOfFile.restype = ctypes.wintypes.HANDLE + +UnmapViewOfFile = ctypes.windll.kernel32.UnmapViewOfFile +UnmapViewOfFile.argtypes = (ctypes.wintypes.HANDLE,) + +RtlMoveMemory = ctypes.windll.kernel32.RtlMoveMemory +RtlMoveMemory.argtypes = (ctypes.c_void_p, ctypes.c_void_p, ctypes.c_size_t) + +ctypes.windll.kernel32.LocalFree.argtypes = (ctypes.wintypes.HLOCAL,) + +##################### +# jaraco.windows.mmap + + +class MemoryMap: + """ + A memory map object which can have security attributes overridden. + """ + + def __init__(self, name, length, security_attributes=None): + self.name = name + self.length = length + self.security_attributes = security_attributes + self.pos = 0 + + def __enter__(self): + p_SA = ( + ctypes.byref(self.security_attributes) + if self.security_attributes + else None + ) + INVALID_HANDLE_VALUE = -1 + PAGE_READWRITE = 0x4 + FILE_MAP_WRITE = 0x2 + filemap = ctypes.windll.kernel32.CreateFileMappingW( + INVALID_HANDLE_VALUE, + p_SA, + PAGE_READWRITE, + 0, + self.length, + u(self.name), + ) + handle_nonzero_success(filemap) + if filemap == INVALID_HANDLE_VALUE: + raise Exception("Failed to create file mapping") + self.filemap = filemap + self.view = MapViewOfFile(filemap, FILE_MAP_WRITE, 0, 0, 0) + return self + + def seek(self, pos): + self.pos = pos + + def write(self, msg): + assert isinstance(msg, bytes) + n = len(msg) + if self.pos + n >= self.length: # A little safety. + raise ValueError(f"Refusing to write {n} bytes") + dest = self.view + self.pos + length = ctypes.c_size_t(n) + ctypes.windll.kernel32.RtlMoveMemory(dest, msg, length) + self.pos += n + + def read(self, n): + """ + Read n bytes from mapped view. + """ + out = ctypes.create_string_buffer(n) + source = self.view + self.pos + length = ctypes.c_size_t(n) + ctypes.windll.kernel32.RtlMoveMemory(out, source, length) + self.pos += n + return out.raw + + def __exit__(self, exc_type, exc_val, tb): + ctypes.windll.kernel32.UnmapViewOfFile(self.view) + ctypes.windll.kernel32.CloseHandle(self.filemap) + + +############################# +# jaraco.windows.api.security + +# from WinNT.h +READ_CONTROL = 0x00020000 +STANDARD_RIGHTS_REQUIRED = 0x000F0000 +STANDARD_RIGHTS_READ = READ_CONTROL +STANDARD_RIGHTS_WRITE = READ_CONTROL +STANDARD_RIGHTS_EXECUTE = READ_CONTROL +STANDARD_RIGHTS_ALL = 0x001F0000 + +# from NTSecAPI.h +POLICY_VIEW_LOCAL_INFORMATION = 0x00000001 +POLICY_VIEW_AUDIT_INFORMATION = 0x00000002 +POLICY_GET_PRIVATE_INFORMATION = 0x00000004 +POLICY_TRUST_ADMIN = 0x00000008 +POLICY_CREATE_ACCOUNT = 0x00000010 +POLICY_CREATE_SECRET = 0x00000020 +POLICY_CREATE_PRIVILEGE = 0x00000040 +POLICY_SET_DEFAULT_QUOTA_LIMITS = 0x00000080 +POLICY_SET_AUDIT_REQUIREMENTS = 0x00000100 +POLICY_AUDIT_LOG_ADMIN = 0x00000200 +POLICY_SERVER_ADMIN = 0x00000400 +POLICY_LOOKUP_NAMES = 0x00000800 +POLICY_NOTIFICATION = 0x00001000 + +POLICY_ALL_ACCESS = ( + STANDARD_RIGHTS_REQUIRED + | POLICY_VIEW_LOCAL_INFORMATION + | POLICY_VIEW_AUDIT_INFORMATION + | POLICY_GET_PRIVATE_INFORMATION + | POLICY_TRUST_ADMIN + | POLICY_CREATE_ACCOUNT + | POLICY_CREATE_SECRET + | POLICY_CREATE_PRIVILEGE + | POLICY_SET_DEFAULT_QUOTA_LIMITS + | POLICY_SET_AUDIT_REQUIREMENTS + | POLICY_AUDIT_LOG_ADMIN + | POLICY_SERVER_ADMIN + | POLICY_LOOKUP_NAMES +) + + +POLICY_READ = ( + STANDARD_RIGHTS_READ + | POLICY_VIEW_AUDIT_INFORMATION + | POLICY_GET_PRIVATE_INFORMATION +) + +POLICY_WRITE = ( + STANDARD_RIGHTS_WRITE + | POLICY_TRUST_ADMIN + | POLICY_CREATE_ACCOUNT + | POLICY_CREATE_SECRET + | POLICY_CREATE_PRIVILEGE + | POLICY_SET_DEFAULT_QUOTA_LIMITS + | POLICY_SET_AUDIT_REQUIREMENTS + | POLICY_AUDIT_LOG_ADMIN + | POLICY_SERVER_ADMIN +) + +POLICY_EXECUTE = ( + STANDARD_RIGHTS_EXECUTE + | POLICY_VIEW_LOCAL_INFORMATION + | POLICY_LOOKUP_NAMES +) + + +class TokenAccess: + TOKEN_QUERY = 0x8 + + +class TokenInformationClass: + TokenUser = 1 + + +class TOKEN_USER(ctypes.Structure): + num = 1 + _fields_ = [ + ("SID", ctypes.c_void_p), + ("ATTRIBUTES", ctypes.wintypes.DWORD), + ] + + +class SECURITY_DESCRIPTOR(ctypes.Structure): + """ + typedef struct _SECURITY_DESCRIPTOR + { + UCHAR Revision; + UCHAR Sbz1; + SECURITY_DESCRIPTOR_CONTROL Control; + PSID Owner; + PSID Group; + PACL Sacl; + PACL Dacl; + } SECURITY_DESCRIPTOR; + """ + + SECURITY_DESCRIPTOR_CONTROL = ctypes.wintypes.USHORT + REVISION = 1 + + _fields_ = [ + ("Revision", ctypes.c_ubyte), + ("Sbz1", ctypes.c_ubyte), + ("Control", SECURITY_DESCRIPTOR_CONTROL), + ("Owner", ctypes.c_void_p), + ("Group", ctypes.c_void_p), + ("Sacl", ctypes.c_void_p), + ("Dacl", ctypes.c_void_p), + ] + + +class SECURITY_ATTRIBUTES(ctypes.Structure): + """ + typedef struct _SECURITY_ATTRIBUTES { + DWORD nLength; + LPVOID lpSecurityDescriptor; + BOOL bInheritHandle; + } SECURITY_ATTRIBUTES; + """ + + _fields_ = [ + ("nLength", ctypes.wintypes.DWORD), + ("lpSecurityDescriptor", ctypes.c_void_p), + ("bInheritHandle", ctypes.wintypes.BOOL), + ] + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.nLength = ctypes.sizeof(SECURITY_ATTRIBUTES) + + @property + def descriptor(self): + return self._descriptor + + @descriptor.setter + def descriptor(self, value): + self._descriptor = value + self.lpSecurityDescriptor = ctypes.addressof(value) + + +ctypes.windll.advapi32.SetSecurityDescriptorOwner.argtypes = ( + ctypes.POINTER(SECURITY_DESCRIPTOR), + ctypes.c_void_p, + ctypes.wintypes.BOOL, +) + +######################### +# jaraco.windows.security + + +def GetTokenInformation(token, information_class): + """ + Given a token, get the token information for it. + """ + data_size = ctypes.wintypes.DWORD() + ctypes.windll.advapi32.GetTokenInformation( + token, information_class.num, 0, 0, ctypes.byref(data_size) + ) + data = ctypes.create_string_buffer(data_size.value) + handle_nonzero_success( + ctypes.windll.advapi32.GetTokenInformation( + token, + information_class.num, + ctypes.byref(data), + ctypes.sizeof(data), + ctypes.byref(data_size), + ) + ) + return ctypes.cast(data, ctypes.POINTER(TOKEN_USER)).contents + + +def OpenProcessToken(proc_handle, access): + result = ctypes.wintypes.HANDLE() + proc_handle = ctypes.wintypes.HANDLE(proc_handle) + handle_nonzero_success( + ctypes.windll.advapi32.OpenProcessToken( + proc_handle, access, ctypes.byref(result) + ) + ) + return result + + +def get_current_user(): + """ + Return a TOKEN_USER for the owner of this process. + """ + process = OpenProcessToken( + ctypes.windll.kernel32.GetCurrentProcess(), TokenAccess.TOKEN_QUERY + ) + return GetTokenInformation(process, TOKEN_USER) + + +def get_security_attributes_for_user(user=None): + """ + Return a SECURITY_ATTRIBUTES structure with the SID set to the + specified user (uses current user if none is specified). + """ + if user is None: + user = get_current_user() + + assert isinstance(user, TOKEN_USER), "user must be TOKEN_USER instance" + + SD = SECURITY_DESCRIPTOR() + SA = SECURITY_ATTRIBUTES() + # by attaching the actual security descriptor, it will be garbage- + # collected with the security attributes + SA.descriptor = SD + SA.bInheritHandle = 1 + + ctypes.windll.advapi32.InitializeSecurityDescriptor( + ctypes.byref(SD), SECURITY_DESCRIPTOR.REVISION + ) + ctypes.windll.advapi32.SetSecurityDescriptorOwner( + ctypes.byref(SD), user.SID, 0 + ) + return SA diff --git a/paramiko/agent.py b/paramiko/agent.py new file mode 100644 index 0000000..30ec159 --- /dev/null +++ b/paramiko/agent.py @@ -0,0 +1,450 @@ +# Copyright (C) 2003-2007 John Rochester <john@jrochester.org> +# +# This file is part of paramiko. +# +# Paramiko is free software; you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free +# Software Foundation; either version 2.1 of the License, or (at your option) +# any later version. +# +# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Paramiko; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +""" +SSH Agent interface +""" + +import os +import socket +import struct +import sys +import threading +import time +import tempfile +import stat +from select import select +from paramiko.common import io_sleep, byte_chr + +from paramiko.ssh_exception import SSHException, AuthenticationException +from paramiko.message import Message +from paramiko.pkey import PKey +from paramiko.util import asbytes + +cSSH2_AGENTC_REQUEST_IDENTITIES = byte_chr(11) +SSH2_AGENT_IDENTITIES_ANSWER = 12 +cSSH2_AGENTC_SIGN_REQUEST = byte_chr(13) +SSH2_AGENT_SIGN_RESPONSE = 14 + +SSH_AGENT_RSA_SHA2_256 = 2 +SSH_AGENT_RSA_SHA2_512 = 4 +# NOTE: RFC mildly confusing; while these flags are OR'd together, OpenSSH at +# least really treats them like "AND"s, in the sense that if it finds the +# SHA256 flag set it won't continue looking at the SHA512 one; it +# short-circuits right away. +# Thus, we never want to eg submit 6 to say "either's good". +ALGORITHM_FLAG_MAP = { + "rsa-sha2-256": SSH_AGENT_RSA_SHA2_256, + "rsa-sha2-512": SSH_AGENT_RSA_SHA2_512, +} + + +class AgentSSH: + def __init__(self): + self._conn = None + self._keys = () + + def get_keys(self): + """ + Return the list of keys available through the SSH agent, if any. If + no SSH agent was running (or it couldn't be contacted), an empty list + will be returned. + + This method performs no IO, just returns the list of keys retrieved + when the connection was made. + + :return: + a tuple of `.AgentKey` objects representing keys available on the + SSH agent + """ + return self._keys + + def _connect(self, conn): + self._conn = conn + ptype, result = self._send_message(cSSH2_AGENTC_REQUEST_IDENTITIES) + if ptype != SSH2_AGENT_IDENTITIES_ANSWER: + raise SSHException("could not get keys from ssh-agent") + keys = [] + for i in range(result.get_int()): + keys.append(AgentKey(self, result.get_binary())) + result.get_string() + self._keys = tuple(keys) + + def _close(self): + if self._conn is not None: + self._conn.close() + self._conn = None + self._keys = () + + def _send_message(self, msg): + msg = asbytes(msg) + self._conn.send(struct.pack(">I", len(msg)) + msg) + data = self._read_all(4) + msg = Message(self._read_all(struct.unpack(">I", data)[0])) + return ord(msg.get_byte()), msg + + def _read_all(self, wanted): + result = self._conn.recv(wanted) + while len(result) < wanted: + if len(result) == 0: + raise SSHException("lost ssh-agent") + extra = self._conn.recv(wanted - len(result)) + if len(extra) == 0: + raise SSHException("lost ssh-agent") + result += extra + return result + + +class AgentProxyThread(threading.Thread): + """ + Class in charge of communication between two channels. + """ + + def __init__(self, agent): + threading.Thread.__init__(self, target=self.run) + self._agent = agent + self._exit = False + + def run(self): + try: + (r, addr) = self.get_connection() + # Found that r should be either + # a socket from the socket library or None + self.__inr = r + # The address should be an IP address as a string? or None + self.__addr = addr + self._agent.connect() + if not isinstance(self._agent, int) and ( + self._agent._conn is None + or not hasattr(self._agent._conn, "fileno") + ): + raise AuthenticationException("Unable to connect to SSH agent") + self._communicate() + except: + # XXX Not sure what to do here ... raise or pass ? + raise + + def _communicate(self): + import fcntl + + oldflags = fcntl.fcntl(self.__inr, fcntl.F_GETFL) + fcntl.fcntl(self.__inr, fcntl.F_SETFL, oldflags | os.O_NONBLOCK) + while not self._exit: + events = select([self._agent._conn, self.__inr], [], [], 0.5) + for fd in events[0]: + if self._agent._conn == fd: + data = self._agent._conn.recv(512) + if len(data) != 0: + self.__inr.send(data) + else: + self._close() + break + elif self.__inr == fd: + data = self.__inr.recv(512) + if len(data) != 0: + self._agent._conn.send(data) + else: + self._close() + break + time.sleep(io_sleep) + + def _close(self): + self._exit = True + self.__inr.close() + self._agent._conn.close() + + +class AgentLocalProxy(AgentProxyThread): + """ + Class to be used when wanting to ask a local SSH Agent being + asked from a remote fake agent (so use a unix socket for ex.) + """ + + def __init__(self, agent): + AgentProxyThread.__init__(self, agent) + + def get_connection(self): + """ + Return a pair of socket object and string address. + + May block! + """ + conn = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + try: + conn.bind(self._agent._get_filename()) + conn.listen(1) + (r, addr) = conn.accept() + return r, addr + except: + raise + + +class AgentRemoteProxy(AgentProxyThread): + """ + Class to be used when wanting to ask a remote SSH Agent + """ + + def __init__(self, agent, chan): + AgentProxyThread.__init__(self, agent) + self.__chan = chan + + def get_connection(self): + return self.__chan, None + + +def get_agent_connection(): + """ + Returns some SSH agent object, or None if none were found/supported. + + .. versionadded:: 2.10 + """ + if ("SSH_AUTH_SOCK" in os.environ) and (sys.platform != "win32"): + conn = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + try: + conn.connect(os.environ["SSH_AUTH_SOCK"]) + return conn + except: + # probably a dangling env var: the ssh agent is gone + return + elif sys.platform == "win32": + from . import win_pageant, win_openssh + + conn = None + if win_pageant.can_talk_to_agent(): + conn = win_pageant.PageantConnection() + elif win_openssh.can_talk_to_agent(): + conn = win_openssh.OpenSSHAgentConnection() + return conn + else: + # no agent support + return + + +class AgentClientProxy: + """ + Class proxying request as a client: + + #. client ask for a request_forward_agent() + #. server creates a proxy and a fake SSH Agent + #. server ask for establishing a connection when needed, + calling the forward_agent_handler at client side. + #. the forward_agent_handler launch a thread for connecting + the remote fake agent and the local agent + #. Communication occurs ... + """ + + def __init__(self, chanRemote): + self._conn = None + self.__chanR = chanRemote + self.thread = AgentRemoteProxy(self, chanRemote) + self.thread.start() + + def __del__(self): + self.close() + + def connect(self): + """ + Method automatically called by ``AgentProxyThread.run``. + """ + conn = get_agent_connection() + if not conn: + return + self._conn = conn + + def close(self): + """ + Close the current connection and terminate the agent + Should be called manually + """ + if hasattr(self, "thread"): + self.thread._exit = True + self.thread.join(1000) + if self._conn is not None: + self._conn.close() + + +class AgentServerProxy(AgentSSH): + """ + Allows an SSH server to access a forwarded agent. + + This also creates a unix domain socket on the system to allow external + programs to also access the agent. For this reason, you probably only want + to create one of these. + + :meth:`connect` must be called before it is usable. This will also load the + list of keys the agent contains. You must also call :meth:`close` in + order to clean up the unix socket and the thread that maintains it. + (:class:`contextlib.closing` might be helpful to you.) + + :param .Transport t: Transport used for SSH Agent communication forwarding + + :raises: `.SSHException` -- mostly if we lost the agent + """ + + def __init__(self, t): + AgentSSH.__init__(self) + self.__t = t + self._dir = tempfile.mkdtemp("sshproxy") + os.chmod(self._dir, stat.S_IRWXU) + self._file = self._dir + "/sshproxy.ssh" + self.thread = AgentLocalProxy(self) + self.thread.start() + + def __del__(self): + self.close() + + def connect(self): + conn_sock = self.__t.open_forward_agent_channel() + if conn_sock is None: + raise SSHException("lost ssh-agent") + conn_sock.set_name("auth-agent") + self._connect(conn_sock) + + def close(self): + """ + Terminate the agent, clean the files, close connections + Should be called manually + """ + os.remove(self._file) + os.rmdir(self._dir) + self.thread._exit = True + self.thread.join(1000) + self._close() + + def get_env(self): + """ + Helper for the environment under unix + + :return: + a dict containing the ``SSH_AUTH_SOCK`` environment variables + """ + return {"SSH_AUTH_SOCK": self._get_filename()} + + def _get_filename(self): + return self._file + + +class AgentRequestHandler: + """ + Primary/default implementation of SSH agent forwarding functionality. + + Simply instantiate this class, handing it a live command-executing session + object, and it will handle forwarding any local SSH agent processes it + finds. + + For example:: + + # Connect + client = SSHClient() + client.connect(host, port, username) + # Obtain session + session = client.get_transport().open_session() + # Forward local agent + AgentRequestHandler(session) + # Commands executed after this point will see the forwarded agent on + # the remote end. + session.exec_command("git clone https://my.git.repository/") + """ + + def __init__(self, chanClient): + self._conn = None + self.__chanC = chanClient + chanClient.request_forward_agent(self._forward_agent_handler) + self.__clientProxys = [] + + def _forward_agent_handler(self, chanRemote): + self.__clientProxys.append(AgentClientProxy(chanRemote)) + + def __del__(self): + self.close() + + def close(self): + for p in self.__clientProxys: + p.close() + + +class Agent(AgentSSH): + """ + Client interface for using private keys from an SSH agent running on the + local machine. If an SSH agent is running, this class can be used to + connect to it and retrieve `.PKey` objects which can be used when + attempting to authenticate to remote SSH servers. + + Upon initialization, a session with the local machine's SSH agent is + opened, if one is running. If no agent is running, initialization will + succeed, but `get_keys` will return an empty tuple. + + :raises: `.SSHException` -- + if an SSH agent is found, but speaks an incompatible protocol + + .. versionchanged:: 2.10 + Added support for native openssh agent on windows (extending previous + putty pageant support) + """ + + def __init__(self): + AgentSSH.__init__(self) + + conn = get_agent_connection() + if not conn: + return + self._connect(conn) + + def close(self): + """ + Close the SSH agent connection. + """ + self._close() + + +class AgentKey(PKey): + """ + Private key held in a local SSH agent. This type of key can be used for + authenticating to a remote server (signing). Most other key operations + work as expected. + """ + + def __init__(self, agent, blob): + self.agent = agent + self.blob = blob + self.public_blob = None + self.name = Message(blob).get_text() + + def asbytes(self): + return self.blob + + def __str__(self): + return self.asbytes() + + def get_name(self): + return self.name + + @property + def _fields(self): + raise NotImplementedError + + def sign_ssh_data(self, data, algorithm=None): + msg = Message() + msg.add_byte(cSSH2_AGENTC_SIGN_REQUEST) + msg.add_string(self.blob) + msg.add_string(data) + msg.add_int(ALGORITHM_FLAG_MAP.get(algorithm, 0)) + ptype, result = self.agent._send_message(msg) + if ptype != SSH2_AGENT_SIGN_RESPONSE: + raise SSHException("key cannot be used for signing") + return result.get_binary() diff --git a/paramiko/auth_handler.py b/paramiko/auth_handler.py new file mode 100644 index 0000000..8c626b4 --- /dev/null +++ b/paramiko/auth_handler.py @@ -0,0 +1,1056 @@ +# Copyright (C) 2003-2007 Robey Pointer <robeypointer@gmail.com> +# +# This file is part of paramiko. +# +# Paramiko is free software; you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free +# Software Foundation; either version 2.1 of the License, or (at your option) +# any later version. +# +# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Paramiko; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +""" +`.AuthHandler` +""" + +import weakref +import time +import socket +import re +import json +import base64 + +from paramiko.common import ( + cMSG_SERVICE_REQUEST, + cMSG_DISCONNECT, + DISCONNECT_SERVICE_NOT_AVAILABLE, + DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE, + cMSG_USERAUTH_REQUEST, + cMSG_SERVICE_ACCEPT, + DEBUG, + AUTH_SUCCESSFUL, + INFO, + cMSG_USERAUTH_SUCCESS, + cMSG_USERAUTH_FAILURE, + AUTH_PARTIALLY_SUCCESSFUL, + cMSG_USERAUTH_INFO_REQUEST, + WARNING, + AUTH_FAILED, + cMSG_USERAUTH_PK_OK, + cMSG_USERAUTH_INFO_RESPONSE, + MSG_SERVICE_REQUEST, + MSG_SERVICE_ACCEPT, + MSG_USERAUTH_REQUEST, + MSG_USERAUTH_SUCCESS, + MSG_USERAUTH_FAILURE, + MSG_USERAUTH_BANNER, + MSG_USERAUTH_INFO_REQUEST, + MSG_USERAUTH_INFO_RESPONSE, + cMSG_USERAUTH_GSSAPI_RESPONSE, + cMSG_USERAUTH_GSSAPI_TOKEN, + cMSG_USERAUTH_GSSAPI_MIC, + MSG_USERAUTH_GSSAPI_RESPONSE, + MSG_USERAUTH_GSSAPI_TOKEN, + MSG_USERAUTH_GSSAPI_ERROR, + MSG_USERAUTH_GSSAPI_ERRTOK, + MSG_USERAUTH_GSSAPI_MIC, + MSG_NAMES, + cMSG_USERAUTH_BANNER, +) +from paramiko.message import Message +from paramiko.util import b, u +from paramiko.ssh_exception import ( + SSHException, + AuthenticationException, + BadAuthenticationType, + PartialAuthentication, +) +from paramiko.server import InteractiveQuery +from paramiko.ssh_gss import GSSAuth, GSS_EXCEPTIONS + + +class AuthHandler: + """ + Internal class to handle the mechanics of authentication. + """ + + def __init__(self, transport): + self.transport = weakref.proxy(transport) + self.username = None + self.authenticated = False + self.auth_event = None + self.auth_method = "" + self.banner = None + self.password = None + self.private_key = None + self.interactive_handler = None + self.submethods = None + # for server mode: + self.auth_username = None + self.auth_fail_count = 0 + # for GSSAPI + self.gss_host = None + self.gss_deleg_creds = True + + def _log(self, *args): + print("_log") + return self.transport._log(*args) + + def is_authenticated(self): + print("is_authenticated") + return self.authenticated + + def get_username(self): + print("get_username") + if self.transport.server_mode: + return self.auth_username + else: + return self.username + + def auth_none(self, username, event): + print("auth_none") + self.transport.lock.acquire() + try: + self.auth_event = event + self.auth_method = "none" + self.username = username + self._request_auth() + finally: + self.transport.lock.release() + + def auth_publickey(self, username, key, event): + print("auth_publickey") + self.transport.lock.acquire() + try: + self.auth_event = event + self.auth_method = "publickey" + self.username = username + self.private_key = key + print(self.private_key) + self._request_auth() + finally: + self.transport.lock.release() + + def auth_password(self, username, password, event): + print("auth_password") + self.transport.lock.acquire() + try: + self.auth_event = event + self.auth_method = "password" + self.username = username + self.password = password + self._request_auth() + finally: + self.transport.lock.release() + + def auth_interactive(self, username, handler, event, submethods=""): + print("auth_interactive") + """ + response_list = handler(title, instructions, prompt_list) + """ + self.transport.lock.acquire() + try: + self.auth_event = event + self.auth_method = "keyboard-interactive" + self.username = username + self.interactive_handler = handler + self.submethods = submethods + self._request_auth() + finally: + self.transport.lock.release() + + def auth_gssapi_with_mic(self, username, gss_host, gss_deleg_creds, event): + print("auth_gssapi_with_mic") + self.transport.lock.acquire() + try: + self.auth_event = event + self.auth_method = "gssapi-with-mic" + self.username = username + self.gss_host = gss_host + self.gss_deleg_creds = gss_deleg_creds + self._request_auth() + finally: + self.transport.lock.release() + + def auth_gssapi_keyex(self, username, event): + print("auth_gssapi_keyex") + self.transport.lock.acquire() + try: + self.auth_event = event + self.auth_method = "gssapi-keyex" + self.username = username + self._request_auth() + finally: + self.transport.lock.release() + + def abort(self): + print("abort") + if self.auth_event is not None: + self.auth_event.set() + + # ...internals... + + def _request_auth(self): + print("_request_auth") + m = Message() + m.add_byte(cMSG_SERVICE_REQUEST) + m.add_string("ssh-userauth") + + print(m) + self.transport._send_message(m) + + def _disconnect_service_not_available(self): + print("_disconnect_service_not_available") + m = Message() + m.add_byte(cMSG_DISCONNECT) + m.add_int(DISCONNECT_SERVICE_NOT_AVAILABLE) + m.add_string("Service not available") + m.add_string("en") + self.transport._send_message(m) + self.transport.close() + + def _disconnect_no_more_auth(self): + print("_disconnect_no_more_auth") + m = Message() + m.add_byte(cMSG_DISCONNECT) + m.add + + + def _get_key_type_and_bits(self, key): + print("_get_key_type_and_bits") + """ + Given any key, return its type/algorithm & bits-to-sign. + + Intended for input to or verification of, key signatures. + """ + # Use certificate contents, if available, plain pubkey otherwise + if key.public_blob: + return key.public_blob.key_type, key.public_blob.key_blob + else: + return key.get_name(), key + + # LE CHALLENGE ********************************************* + def _get_session_blob(self, key, service, username, algorithm): + print("_get_session_blob ********************************") + m = Message() + m.add_string(self.transport.session_id) + # problématique car comment avoir la session_id avant de s'authentifier ? + print("session_id: ", self.transport.session_id) + m.add_byte(cMSG_USERAUTH_REQUEST) + print("cMSG_USERAUTH_REQUEST", cMSG_USERAUTH_REQUEST) + m.add_string(username) + print("username: ", username) + m.add_string(service) + print("service: ", service) + m.add_string("publickey") + m.add_boolean(True) + _, bits = self._get_key_type_and_bits(key) + m.add_string(algorithm) + m.add_string(bits) + print("*****************************") + return m.asbytes() + + def wait_for_response(self, event): + print("wait_for_response") + max_ts = None + if self.transport.auth_timeout is not None: + max_ts = time.time() + self.transport.auth_timeout + while True: + event.wait(0.1) + if not self.transport.is_active(): + e = self.transport.get_exception() + if (e is None) or issubclass(e.__class__, EOFError): + e = AuthenticationException("Authentication failed.") + raise e + if event.is_set(): + break + if max_ts is not None and max_ts <= time.time(): + raise AuthenticationException("Authentication timeout.") + + if not self.is_authenticated(): + e = self.transport.get_exception() + if e is None: + e = AuthenticationException("Authentication failed.") + # this is horrible. Python Exception isn't yet descended from + # object, so type(e) won't work. :( + if issubclass(e.__class__, PartialAuthentication): + return e.allowed_types + raise e + return [] + + def _parse_service_request(self, m): + print("_parse_service_request") + service = m.get_text() + if self.transport.server_mode and (service == "ssh-userauth"): + # accepted + m = Message() + m.add_byte(cMSG_SERVICE_ACCEPT) + m.add_string(service) + self.transport._send_message(m) + banner, language = self.transport.server_object.get_banner() + if banner: + m = Message() + m.add_byte(cMSG_USERAUTH_BANNER) + m.add_string(banner) + m.add_string(language) + self.transport._send_message(m) + return + # dunno this one + self._disconnect_service_not_available() + + def _generate_key_from_request(self, algorithm, keyblob): + print("_generate_key_from_request") + # For use in server mode. + options = self.transport.preferred_pubkeys + if algorithm.replace("-cert-v01@openssh.com", "") not in options: + err = ( + "Auth rejected: pubkey algorithm '{}' unsupported or disabled" + ) + self._log(INFO, err.format(algorithm)) + return None + return self.transport._key_info[algorithm](Message(keyblob)) + + def _finalize_pubkey_algorithm(self, key_type): + print("_finalize_pubkey_algorithm") + # Short-circuit for non-RSA keys + if "rsa" not in key_type: + return key_type + self._log( + DEBUG, + "Finalizing pubkey algorithm for key of type {!r}".format( + key_type + ), + ) + # NOTE re #2017: When the key is an RSA cert and the remote server is + # OpenSSH 7.7 or earlier, always use ssh-rsa-cert-v01@openssh.com. + # Those versions of the server won't support rsa-sha2 family sig algos + # for certs specifically, and in tandem with various server bugs + # regarding server-sig-algs, it's impossible to fit this into the rest + # of the logic here. + if key_type.endswith("-cert-v01@openssh.com") and re.search( + r"-OpenSSH_(?:[1-6]|7\.[0-7])", self.transport.remote_version + ): + pubkey_algo = "ssh-rsa-cert-v01@openssh.com" + self.transport._agreed_pubkey_algorithm = pubkey_algo + self._log(DEBUG, "OpenSSH<7.8 + RSA cert = forcing ssh-rsa!") + self._log( + DEBUG, "Agreed upon {!r} pubkey algorithm".format(pubkey_algo) + ) + return pubkey_algo + # Normal attempts to handshake follow from here. + # Only consider RSA algos from our list, lest we agree on another! + my_algos = [x for x in self.transport.preferred_pubkeys if "rsa" in x] + self._log(DEBUG, "Our pubkey algorithm list: {}".format(my_algos)) + # Short-circuit negatively if user disabled all RSA algos (heh) + if not my_algos: + raise SSHException( + "An RSA key was specified, but no RSA pubkey algorithms are configured!" # noqa + ) + # Check for server-sig-algs if supported & sent + server_algo_str = u( + self.transport.server_extensions.get("server-sig-algs", b("")) + ) + pubkey_algo = None + if server_algo_str: + server_algos = server_algo_str.split(",") + self._log( + DEBUG, "Server-side algorithm list: {}".format(server_algos) + ) + # Only use algos from our list that the server likes, in our own + # preference order. (NOTE: purposefully using same style as in + # Transport...expect to refactor later) + agreement = list(filter(server_algos.__contains__, my_algos)) + if agreement: + pubkey_algo = agreement[0] + self._log( + DEBUG, + "Agreed upon {!r} pubkey algorithm".format(pubkey_algo), + ) + else: + self._log(DEBUG, "No common pubkey algorithms exist! Dying.") + # TODO: MAY want to use IncompatiblePeer again here but that's + # technically for initial key exchange, not pubkey auth. + err = "Unable to agree on a pubkey algorithm for signing a {!r} key!" # noqa + raise AuthenticationException(err.format(key_type)) + else: + # Fallback: first one in our (possibly tweaked by caller) list + pubkey_algo = my_algos[0] + msg = "Server did not send a server-sig-algs list; defaulting to our first preferred algo ({!r})" # noqa + self._log(DEBUG, msg.format(pubkey_algo)) + self._log( + DEBUG, + "NOTE: you may use the 'disabled_algorithms' SSHClient/Transport init kwarg to disable that or other algorithms if your server does not support them!", # noqa + ) + if key_type.endswith("-cert-v01@openssh.com"): + pubkey_algo += "-cert-v01@openssh.com" + self.transport._agreed_pubkey_algorithm = pubkey_algo + return pubkey_algo + + def _parse_service_accept(self, m): + print("_parse_service_accept") + service = m.get_text() + print("teeest") + if service == "ssh-userauth": + # TODO 4.0: this message sucks ass. change it to something more + # obvious. it always appears to mean "we already authed" but no! it + # just means "we are allowed to TRY authing!" + self._log(DEBUG, "userauth is OK") + m = Message() + mes2 = Message() + m.add_byte(cMSG_USERAUTH_REQUEST) + m.add_string(self.username) + + m.add_string("ssh-connection") + m.add_string(self.auth_method) + if self.auth_method == "password": + m.add_boolean(False) + password = b(self.password) + m.add_string(password) + elif self.auth_method == "publickey": + print("en mode public key :)") + + + # m.add_boolean(True) + # TODO : aller chercher la clé privée autre part ?? + + info_dict = { + "algorithm": "rsa-sha2-512", + "session_id": base64.b64encode(self.transport.session_id).decode(), # decode to convert bytes to string + "userauth_request": base64.b64encode(cMSG_USERAUTH_REQUEST).decode(), + "username": self.username, + # "public_key" : self.private_key.get_base64(), # here it's a public key!! + } + + + signer_client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + signer_hostname = "localhost" + signer_port = 12340 + signer_client.connect((signer_hostname, signer_port)) + signer_client.send(json.dumps(info_dict).encode()) + + # faire dans auth : + # key_type, bits = self._get_key_type_and_bits(self.private_key) + # print("key type : ", key_type) + # print("bits : ", bits) + + # hard code pour l'instant + # TODO : faire en sorte que ça marche avec d'autres clés + # algorithm = self._finalize_pubkey_algorithm(key_type) + # print("algorithm : ", algorithm) + + # faire dans auth : + # m.add_string("rsa-sha2-512") + # m.add_string(bits) + + # TODO : envoyer le session ID au serveur !! + # print("session ID : ", self.transport.session_id) + # TODO : envoyer le cMSG_USERAUTH_REQUEST au serveur !! + # print("cMSG_USERAUTH_REQUEST : ", cMSG_USERAUTH_REQUEST) + + # faire dans auth : + # blob = self._get_session_blob( + # self.private_key, + # "ssh-connection", + # self.username, + # algorithm, + # ) + + + # print("session blob : ", blob) + # faire dans auth : + # sig = self.private_key.sign_ssh_data(blob, algorithm) + # print("signature : ", sig) + + + # TODO : recupérer le message du serveur + + data = signer_client.recv(2048) + # + # Reconstruct the Paramiko Message object + mess = Message(data) + # print("data : ", data) + # info_dict_retour = json.loads(data.decode()) + + # print("signature : ", Message(sig)) + # sig = self.private_key.sign_ssh_data(blob, algorithm) + + m = mess + + + # m.add_string() + elif self.auth_method == "keyboard-interactive": + m.add_string("") + m.add_string(self.submethods) + elif self.auth_method == "gssapi-with-mic": + sshgss = GSSAuth(self.auth_method, self.gss_deleg_creds) + m.add_bytes(sshgss.ssh_gss_oids()) + # send the supported GSSAPI OIDs to the server + self.transport._send_message(m) + ptype, m = self.transport.packetizer.read_message() + if ptype == MSG_USERAUTH_BANNER: + self._parse_userauth_banner(m) + ptype, m = self.transport.packetizer.read_message() + if ptype == MSG_USERAUTH_GSSAPI_RESPONSE: + # Read the mechanism selected by the server. We send just + # the Kerberos V5 OID, so the server can only respond with + # this OID. + mech = m.get_string() + m = Message() + m.add_byte(cMSG_USERAUTH_GSSAPI_TOKEN) + try: + m.add_string( + sshgss.ssh_init_sec_context( + self.gss_host, mech, self.username + ) + ) + except GSS_EXCEPTIONS as e: + return self._handle_local_gss_failure(e) + self.transport._send_message(m) + while True: + ptype, m = self.transport.packetizer.read_message() + if ptype == MSG_USERAUTH_GSSAPI_TOKEN: + srv_token = m.get_string() + try: + next_token = sshgss.ssh_init_sec_context( + self.gss_host, + mech, + self.username, + srv_token, + ) + except GSS_EXCEPTIONS as e: + return self._handle_local_gss_failure(e) + # After this step the GSSAPI should not return any + # token. If it does, we keep sending the token to + # the server until no more token is returned. + if next_token is None: + break + else: + m = Message() + m.add_byte(cMSG_USERAUTH_GSSAPI_TOKEN) + m.add_string(next_token) + self.transport.send_message(m) + else: + raise SSHException( + "Received Package: {}".format(MSG_NAMES[ptype]) + ) + m = Message() + m.add_byte(cMSG_USERAUTH_GSSAPI_MIC) + # send the MIC to the server + m.add_string(sshgss.ssh_get_mic(self.transport.session_id)) + elif ptype == MSG_USERAUTH_GSSAPI_ERRTOK: + # RFC 4462 says we are not required to implement GSS-API + # error messages. + # See RFC 4462 Section 3.8 in + # http://www.ietf.org/rfc/rfc4462.txt + raise SSHException("Server returned an error token") + elif ptype == MSG_USERAUTH_GSSAPI_ERROR: + maj_status = m.get_int() + min_status = m.get_int() + err_msg = m.get_string() + m.get_string() # Lang tag - discarded + raise SSHException( + """GSS-API Error: + Major Status: {} + Minor Status: {} + Error Message: {} + """.format( + maj_status, min_status, err_msg + ) + ) + elif ptype == MSG_USERAUTH_FAILURE: + self._parse_userauth_failure(m) + return + else: + raise SSHException( + "Received Package: {}".format(MSG_NAMES[ptype]) + ) + elif ( + self.auth_method == "gssapi-keyex" + and self.transport.gss_kex_used + ): + kexgss = self.transport.kexgss_ctxt + kexgss.set_username(self.username) + mic_token = kexgss.ssh_get_mic(self.transport.session_id) + m.add_string(mic_token) + elif self.auth_method == "none": + pass + else: + raise SSHException( + 'Unknown auth method "{}"'.format(self.auth_method) + ) + self.transport._send_message(m) + else: + self._log( + DEBUG, 'Service request "{}" accepted (?)'.format(service) + ) + + + def _send_auth_result(self, username, method, result): + print("send auth result") + # okay, send result + m = Message() + if result == AUTH_SUCCESSFUL: + print("auth successful") + self._log(INFO, "Auth granted ({}).".format(method)) + m.add_byte(cMSG_USERAUTH_SUCCESS) + self.authenticated = True + else: + self._log(INFO, "Auth rejected ({}).".format(method)) + print("auth rejected") + m.add_byte(cMSG_USERAUTH_FAILURE) + m.add_string( + self.transport.server_object.get_allowed_auths(username) + ) + if result == AUTH_PARTIALLY_SUCCESSFUL: + print("auth partially successful") + m.add_boolean(True) + else: + m.add_boolean(False) + self.auth_fail_count += 1 + self.transport._send_message(m) + if self.auth_fail_count >= 10: + self._disconnect_no_more_auth() + if result == AUTH_SUCCESSFUL: + self.transport._auth_trigger() + + def _interactive_query(self, q): + print("interactive query") + # make interactive query instead of response + m = Message() + m.add_byte(cMSG_USERAUTH_INFO_REQUEST) + m.add_string(q.name) + m.add_string(q.instructions) + m.add_string(bytes()) + m.add_int(len(q.prompts)) + for p in q.prompts: + m.add_string(p[0]) + m.add_boolean(p[1]) + self.transport._send_message(m) + + def _parse_userauth_request(self, m): + print("userauth request") + + if not self.transport.server_mode: + # er, uh... what? + m = Message() + m.add_byte(cMSG_USERAUTH_FAILURE) + m.add_string("none") + m.add_boolean(False) + self.transport._send_message(m) + return + if self.authenticated: + # ignore + return + username = m.get_text() + service = m.get_text() + method = m.get_text() + self._log( + DEBUG, + "Auth request (type={}) service={}, username={}".format( + method, service, username + ), + ) + if service != "ssh-connection": + self._disconnect_service_not_available() + return + if (self.auth_username is not None) and ( + self.auth_username != username + ): + self._log( + WARNING, + "Auth rejected because the client attempted to change username in mid-flight", # noqa + ) + self._disconnect_no_more_auth() + return + self.auth_username = username + # check if GSS-API authentication is enabled + gss_auth = self.transport.server_object.enable_auth_gssapi() + + if method == "none": + result = self.transport.server_object.check_auth_none(username) + elif method == "password": + changereq = m.get_boolean() + password = m.get_binary() + try: + password = password.decode("UTF-8") + except UnicodeError: + # some clients/servers expect non-utf-8 passwords! + # in this case, just return the raw byte string. + pass + if changereq: + # always treated as failure, since we don't support changing + # passwords, but collect the list of valid auth types from + # the callback anyway + self._log(DEBUG, "Auth request to change passwords (rejected)") + newpassword = m.get_binary() + try: + newpassword = newpassword.decode("UTF-8", "replace") + except UnicodeError: + pass + result = AUTH_FAILED + else: + result = self.transport.server_object.check_auth_password( + username, password + ) + elif method == "publickey": + sig_attached = m.get_boolean() + # NOTE: server never wants to guess a client's algo, they're + # telling us directly. No need for _finalize_pubkey_algorithm + # anywhere in this flow. + algorithm = m.get_text() + keyblob = m.get_binary() + try: + key = self._generate_key_from_request(algorithm, keyblob) + except SSHException as e: + self._log(INFO, "Auth rejected: public key: {}".format(str(e))) + key = None + except Exception as e: + msg = "Auth rejected: unsupported or mangled public key ({}: {})" # noqa + self._log(INFO, msg.format(e.__class__.__name__, e)) + key = None + if key is None: + self._disconnect_no_more_auth() + return + # first check if this key is okay... if not, we can skip the verify + result = self.transport.server_object.check_auth_publickey( + username, key + ) + if result != AUTH_FAILED: + print("auth failed") + # key is okay, verify it + if not sig_attached: + # client wants to know if this key is acceptable, before it + # signs anything... send special "ok" message + m = Message() + m.add_byte(cMSG_USERAUTH_PK_OK) + m.add_string(algorithm) + m.add_string(keyblob) + self.transport._send_message(m) + return + sig = Message(m.get_binary()) + # + blob = self._get_session_blob( + key, service, username, algorithm + ) + if not key.verify_ssh_sig(blob, sig): + self._log(INFO, "Auth rejected: invalid signature") + result = AUTH_FAILED + elif method == "keyboard-interactive": + submethods = m.get_string() + result = self.transport.server_object.check_auth_interactive( + username, submethods + ) + if isinstance(result, InteractiveQuery): + # make interactive query instead of response + self._interactive_query(result) + return + elif method == "gssapi-with-mic" and gss_auth: + sshgss = GSSAuth(method) + # Read the number of OID mechanisms supported by the client. + # OpenSSH sends just one OID. It's the Kerveros V5 OID and that's + # the only OID we support. + mechs = m.get_int() + # We can't accept more than one OID, so if the SSH client sends + # more than one, disconnect. + if mechs > 1: + self._log( + INFO, + "Disconnect: Received more than one GSS-API OID mechanism", + ) + self._disconnect_no_more_auth() + desired_mech = m.get_string() + mech_ok = sshgss.ssh_check_mech(desired_mech) + # if we don't support the mechanism, disconnect. + if not mech_ok: + self._log( + INFO, + "Disconnect: Received an invalid GSS-API OID mechanism", + ) + self._disconnect_no_more_auth() + # send the Kerberos V5 GSSAPI OID to the client + supported_mech = sshgss.ssh_gss_oids("server") + # RFC 4462 says we are not required to implement GSS-API error + # messages. See section 3.8 in http://www.ietf.org/rfc/rfc4462.txt + m = Message() + m.add_byte(cMSG_USERAUTH_GSSAPI_RESPONSE) + m.add_bytes(supported_mech) + self.transport.auth_handler = GssapiWithMicAuthHandler( + self, sshgss + ) + self.transport._expected_packet = ( + MSG_USERAUTH_GSSAPI_TOKEN, + MSG_USERAUTH_REQUEST, + MSG_SERVICE_REQUEST, + ) + self.transport._send_message(m) + return + elif method == "gssapi-keyex" and gss_auth: + mic_token = m.get_string() + sshgss = self.transport.kexgss_ctxt + if sshgss is None: + # If there is no valid context, we reject the authentication + result = AUTH_FAILED + self._send_auth_result(username, method, result) + try: + sshgss.ssh_check_mic( + mic_token, self.transport.session_id, self.auth_username + ) + except Exception: + result = AUTH_FAILED + self._send_auth_result(username, method, result) + raise + result = AUTH_SUCCESSFUL + self.transport.server_object.check_auth_gssapi_keyex( + username, result + ) + else: + result = self.transport.server_object.check_auth_none(username) + # okay, send result + self._send_auth_result(username, method, result) + + def _parse_userauth_success(self, m): + print("parse_userauth_success") + self._log( + INFO, "Authentication ({}) successful!".format(self.auth_method) + ) + self.authenticated = True + self.transport._auth_trigger() + if self.auth_event is not None: + self.auth_event.set() + + def _parse_userauth_failure(self, m): + print("parse_userauth_failure") + authlist = m.get_list() + partial = m.get_boolean() + print("partial: ", partial) + if partial: + self._log(INFO, "Authentication continues...") + self._log(DEBUG, "Methods: " + str(authlist)) + self.transport.saved_exception = PartialAuthentication(authlist) + elif self.auth_method not in authlist: + for msg in ( + "Authentication type ({}) not permitted.".format( + self.auth_method + ), + "Allowed methods: {}".format(authlist), + ): + self._log(DEBUG, msg) + self.transport.saved_exception = BadAuthenticationType( + "Bad authentication type", authlist + ) + else: + self._log( + INFO, "Authentication ({}) failed.".format(self.auth_method) + ) + self.authenticated = False + self.username = None + if self.auth_event is not None: + self.auth_event.set() + + def _parse_userauth_banner(self, m): + print("parse_userauth_banner") + banner = m.get_string() + self.banner = banner + self._log(INFO, "Auth banner: {}".format(banner)) + # who cares. + + def _parse_userauth_info_request(self, m): + print("parse_userauth_info_request") + if self.auth_method != "keyboard-interactive": + raise SSHException("Illegal info request from server") + title = m.get_text() + instructions = m.get_text() + m.get_binary() # lang + prompts = m.get_int() + prompt_list = [] + for i in range(prompts): + prompt_list.append((m.get_text(), m.get_boolean())) + response_list = self.interactive_handler( + title, instructions, prompt_list + ) + + m = Message() + m.add_byte(cMSG_USERAUTH_INFO_RESPONSE) + m.add_int(len(response_list)) + for r in response_list: + m.add_string(r) + self.transport._send_message(m) + + def _parse_userauth_info_response(self, m): + print("parse_userauth_info_response") + if not self.transport.server_mode: + raise SSHException("Illegal info response from server") + n = m.get_int() + responses = [] + for i in range(n): + responses.append(m.get_text()) + result = self.transport.server_object.check_auth_interactive_response( + responses + ) + if isinstance(result, InteractiveQuery): + # make interactive query instead of response + self._interactive_query(result) + return + self._send_auth_result( + self.auth_username, "keyboard-interactive", result + ) + + def _handle_local_gss_failure(self, e): + print("handle_local_gss_failure") + self.transport.saved_exception = e + self._log(DEBUG, "GSSAPI failure: {}".format(e)) + self._log(INFO, "Authentication ({}) failed.".format(self.auth_method)) + self.authenticated = False + self.username = None + if self.auth_event is not None: + self.auth_event.set() + return + + # TODO: do the same to the other tables, in Transport. + # TODO 4.0: MAY make sense to make these tables into actual + # classes/instances that can be fed a mode bool or whatever. Or, + # alternately (both?) make the message types small classes or enums that + # embed this info within themselves (which could also then tidy up the + # current 'integer -> human readable short string' stuff in common.py). + # TODO: if we do that, also expose 'em publicly. + + # Messages which should be handled _by_ servers (sent by clients) + _server_handler_table = { + MSG_SERVICE_REQUEST: _parse_service_request, + MSG_USERAUTH_REQUEST: _parse_userauth_request, + MSG_USERAUTH_INFO_RESPONSE: _parse_userauth_info_response, + } + + # Messages which should be handled _by_ clients (sent by servers) + _client_handler_table = { + MSG_SERVICE_ACCEPT: _parse_service_accept, + MSG_USERAUTH_SUCCESS: _parse_userauth_success, + MSG_USERAUTH_FAILURE: _parse_userauth_failure, + MSG_USERAUTH_BANNER: _parse_userauth_banner, + MSG_USERAUTH_INFO_REQUEST: _parse_userauth_info_request, + } + + # NOTE: prior to the fix for #1283, this was a static dict instead of a + # property. Should be backwards compatible in most/all cases. + @property + def _handler_table(self): + if self.transport.server_mode: + return self._server_handler_table + else: + return self._client_handler_table + + +class GssapiWithMicAuthHandler: + """A specialized Auth handler for gssapi-with-mic + + During the GSSAPI token exchange we need a modified dispatch table, + because the packet type numbers are not unique. + """ + + method = "gssapi-with-mic" + + def __init__(self, delegate, sshgss): + self._delegate = delegate + self.sshgss = sshgss + + def abort(self): + self._restore_delegate_auth_handler() + return self._delegate.abort() + + @property + def transport(self): + return self._delegate.transport + + @property + def _send_auth_result(self): + return self._delegate._send_auth_result + + @property + def auth_username(self): + return self._delegate.auth_username + + @property + def gss_host(self): + return self._delegate.gss_host + + def _restore_delegate_auth_handler(self): + self.transport.auth_handler = self._delegate + + def _parse_userauth_gssapi_token(self, m): + client_token = m.get_string() + # use the client token as input to establish a secure + # context. + sshgss = self.sshgss + try: + token = sshgss.ssh_accept_sec_context( + self.gss_host, client_token, self.auth_username + ) + except Exception as e: + self.transport.saved_exception = e + result = AUTH_FAILED + self._restore_delegate_auth_handler() + self._send_auth_result(self.auth_username, self.method, result) + raise + if token is not None: + m = Message() + m.add_byte(cMSG_USERAUTH_GSSAPI_TOKEN) + m.add_string(token) + self.transport._expected_packet = ( + MSG_USERAUTH_GSSAPI_TOKEN, + MSG_USERAUTH_GSSAPI_MIC, + MSG_USERAUTH_REQUEST, + ) + self.transport._send_message(m) + + def _parse_userauth_gssapi_mic(self, m): + mic_token = m.get_string() + sshgss = self.sshgss + username = self.auth_username + self._restore_delegate_auth_handler() + try: + sshgss.ssh_check_mic( + mic_token, self.transport.session_id, username + ) + except Exception as e: + self.transport.saved_exception = e + result = AUTH_FAILED + self._send_auth_result(username, self.method, result) + raise + # TODO: Implement client credential saving. + # The OpenSSH server is able to create a TGT with the delegated + # client credentials, but this is not supported by GSS-API. + result = AUTH_SUCCESSFUL + self.transport.server_object.check_auth_gssapi_with_mic( + username, result + ) + # okay, send result + self._send_auth_result(username, self.method, result) + + def _parse_service_request(self, m): + print("parse service request") + self._restore_delegate_auth_handler() + return self._delegate._parse_service_request(m) + + def _parse_userauth_request(self, m): + self._restore_delegate_auth_handler() + return self._delegate._parse_userauth_request(m) + + __handler_table = { + MSG_SERVICE_REQUEST: _parse_service_request, + MSG_USERAUTH_REQUEST: _parse_userauth_request, + MSG_USERAUTH_GSSAPI_TOKEN: _parse_userauth_gssapi_token, + MSG_USERAUTH_GSSAPI_MIC: _parse_userauth_gssapi_mic, + } + + @property + def _handler_table(self): + # TODO: determine if we can cut this up like we did for the primary + # AuthHandler class. + return self.__handler_table diff --git a/paramiko/ber.py b/paramiko/ber.py new file mode 100644 index 0000000..b8287f5 --- /dev/null +++ b/paramiko/ber.py @@ -0,0 +1,139 @@ +# Copyright (C) 2003-2007 Robey Pointer <robeypointer@gmail.com> +# +# This file is part of paramiko. +# +# Paramiko is free software; you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free +# Software Foundation; either version 2.1 of the License, or (at your option) +# any later version. +# +# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Paramiko; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +from paramiko.common import max_byte, zero_byte, byte_ord, byte_chr + +import paramiko.util as util +from paramiko.util import b +from paramiko.sftp import int64 + + +class BERException(Exception): + pass + + +class BER: + """ + Robey's tiny little attempt at a BER decoder. + """ + + def __init__(self, content=bytes()): + self.content = b(content) + self.idx = 0 + + def asbytes(self): + return self.content + + def __str__(self): + return self.asbytes() + + def __repr__(self): + return "BER('" + repr(self.content) + "')" + + def decode(self): + return self.decode_next() + + def decode_next(self): + if self.idx >= len(self.content): + return None + ident = byte_ord(self.content[self.idx]) + self.idx += 1 + if (ident & 31) == 31: + # identifier > 30 + ident = 0 + while self.idx < len(self.content): + t = byte_ord(self.content[self.idx]) + self.idx += 1 + ident = (ident << 7) | (t & 0x7F) + if not (t & 0x80): + break + if self.idx >= len(self.content): + return None + # now fetch length + size = byte_ord(self.content[self.idx]) + self.idx += 1 + if size & 0x80: + # more complimicated... + # FIXME: theoretically should handle indefinite-length (0x80) + t = size & 0x7F + if self.idx + t > len(self.content): + return None + size = util.inflate_long( + self.content[self.idx : self.idx + t], True + ) + self.idx += t + if self.idx + size > len(self.content): + # can't fit + return None + data = self.content[self.idx : self.idx + size] + self.idx += size + # now switch on id + if ident == 0x30: + # sequence + return self.decode_sequence(data) + elif ident == 2: + # int + return util.inflate_long(data) + else: + # 1: boolean (00 false, otherwise true) + msg = "Unknown ber encoding type {:d} (robey is lazy)" + raise BERException(msg.format(ident)) + + @staticmethod + def decode_sequence(data): + out = [] + ber = BER(data) + while True: + x = ber.decode_next() + if x is None: + break + out.append(x) + return out + + def encode_tlv(self, ident, val): + # no need to support ident > 31 here + self.content += byte_chr(ident) + if len(val) > 0x7F: + lenstr = util.deflate_long(len(val)) + self.content += byte_chr(0x80 + len(lenstr)) + lenstr + else: + self.content += byte_chr(len(val)) + self.content += val + + def encode(self, x): + if type(x) is bool: + if x: + self.encode_tlv(1, max_byte) + else: + self.encode_tlv(1, zero_byte) + elif (type(x) is int) or (type(x) is int64): + self.encode_tlv(2, util.deflate_long(x)) + elif type(x) is str: + self.encode_tlv(4, x) + elif (type(x) is list) or (type(x) is tuple): + self.encode_tlv(0x30, self.encode_sequence(x)) + else: + raise BERException( + "Unknown type for encoding: {!r}".format(type(x)) + ) + + @staticmethod + def encode_sequence(data): + ber = BER() + for item in data: + ber.encode(item) + return ber.asbytes() diff --git a/paramiko/buffered_pipe.py b/paramiko/buffered_pipe.py new file mode 100644 index 0000000..c19279c --- /dev/null +++ b/paramiko/buffered_pipe.py @@ -0,0 +1,212 @@ +# Copyright (C) 2006-2007 Robey Pointer <robeypointer@gmail.com> +# +# This file is part of paramiko. +# +# Paramiko is free software; you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free +# Software Foundation; either version 2.1 of the License, or (at your option) +# any later version. +# +# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Paramiko; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +""" +Attempt to generalize the "feeder" part of a `.Channel`: an object which can be +read from and closed, but is reading from a buffer fed by another thread. The +read operations are blocking and can have a timeout set. +""" + +import array +import threading +import time +from paramiko.util import b + + +class PipeTimeout(IOError): + """ + Indicates that a timeout was reached on a read from a `.BufferedPipe`. + """ + + pass + + +class BufferedPipe: + """ + A buffer that obeys normal read (with timeout) & close semantics for a + file or socket, but is fed data from another thread. This is used by + `.Channel`. + """ + + def __init__(self): + self._lock = threading.Lock() + self._cv = threading.Condition(self._lock) + self._event = None + self._buffer = array.array("B") + self._closed = False + + def _buffer_frombytes(self, data): + self._buffer.frombytes(data) + + def _buffer_tobytes(self, limit=None): + return self._buffer[:limit].tobytes() + + def set_event(self, event): + """ + Set an event on this buffer. When data is ready to be read (or the + buffer has been closed), the event will be set. When no data is + ready, the event will be cleared. + + :param threading.Event event: the event to set/clear + """ + self._lock.acquire() + try: + self._event = event + # Make sure the event starts in `set` state if we appear to already + # be closed; otherwise, if we start in `clear` state & are closed, + # nothing will ever call `.feed` and the event (& OS pipe, if we're + # wrapping one - see `Channel.fileno`) will permanently stay in + # `clear`, causing deadlock if e.g. `select`ed upon. + if self._closed or len(self._buffer) > 0: + event.set() + else: + event.clear() + finally: + self._lock.release() + + def feed(self, data): + """ + Feed new data into this pipe. This method is assumed to be called + from a separate thread, so synchronization is done. + + :param data: the data to add, as a ``str`` or ``bytes`` + """ + self._lock.acquire() + try: + if self._event is not None: + self._event.set() + self._buffer_frombytes(b(data)) + self._cv.notify_all() + finally: + self._lock.release() + + def read_ready(self): + """ + Returns true if data is buffered and ready to be read from this + feeder. A ``False`` result does not mean that the feeder has closed; + it means you may need to wait before more data arrives. + + :return: + ``True`` if a `read` call would immediately return at least one + byte; ``False`` otherwise. + """ + self._lock.acquire() + try: + if len(self._buffer) == 0: + return False + return True + finally: + self._lock.release() + + def read(self, nbytes, timeout=None): + """ + Read data from the pipe. The return value is a string representing + the data received. The maximum amount of data to be received at once + is specified by ``nbytes``. If a string of length zero is returned, + the pipe has been closed. + + The optional ``timeout`` argument can be a nonnegative float expressing + seconds, or ``None`` for no timeout. If a float is given, a + `.PipeTimeout` will be raised if the timeout period value has elapsed + before any data arrives. + + :param int nbytes: maximum number of bytes to read + :param float timeout: + maximum seconds to wait (or ``None``, the default, to wait forever) + :return: the read data, as a ``str`` or ``bytes`` + + :raises: + `.PipeTimeout` -- if a timeout was specified and no data was ready + before that timeout + """ + out = bytes() + self._lock.acquire() + try: + if len(self._buffer) == 0: + if self._closed: + return out + # should we block? + if timeout == 0.0: + raise PipeTimeout() + # loop here in case we get woken up but a different thread has + # grabbed everything in the buffer. + while (len(self._buffer) == 0) and not self._closed: + then = time.time() + self._cv.wait(timeout) + if timeout is not None: + timeout -= time.time() - then + if timeout <= 0.0: + raise PipeTimeout() + + # something's in the buffer and we have the lock! + if len(self._buffer) <= nbytes: + out = self._buffer_tobytes() + del self._buffer[:] + if (self._event is not None) and not self._closed: + self._event.clear() + else: + out = self._buffer_tobytes(nbytes) + del self._buffer[:nbytes] + finally: + self._lock.release() + + return out + + def empty(self): + """ + Clear out the buffer and return all data that was in it. + + :return: + any data that was in the buffer prior to clearing it out, as a + `str` + """ + self._lock.acquire() + try: + out = self._buffer_tobytes() + del self._buffer[:] + if (self._event is not None) and not self._closed: + self._event.clear() + return out + finally: + self._lock.release() + + def close(self): + """ + Close this pipe object. Future calls to `read` after the buffer + has been emptied will return immediately with an empty string. + """ + self._lock.acquire() + try: + self._closed = True + self._cv.notify_all() + if self._event is not None: + self._event.set() + finally: + self._lock.release() + + def __len__(self): + """ + Return the number of bytes buffered. + + :return: number (`int`) of bytes buffered + """ + self._lock.acquire() + try: + return len(self._buffer) + finally: + self._lock.release() diff --git a/paramiko/channel.py b/paramiko/channel.py new file mode 100644 index 0000000..2757450 --- /dev/null +++ b/paramiko/channel.py @@ -0,0 +1,1390 @@ +# Copyright (C) 2003-2007 Robey Pointer <robeypointer@gmail.com> +# +# This file is part of paramiko. +# +# Paramiko is free software; you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free +# Software Foundation; either version 2.1 of the License, or (at your option) +# any later version. +# +# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Paramiko; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +""" +Abstraction for an SSH2 channel. +""" + +import binascii +import os +import socket +import time +import threading + +from functools import wraps + +from paramiko import util +from paramiko.common import ( + cMSG_CHANNEL_REQUEST, + cMSG_CHANNEL_WINDOW_ADJUST, + cMSG_CHANNEL_DATA, + cMSG_CHANNEL_EXTENDED_DATA, + DEBUG, + ERROR, + cMSG_CHANNEL_SUCCESS, + cMSG_CHANNEL_FAILURE, + cMSG_CHANNEL_EOF, + cMSG_CHANNEL_CLOSE, +) +from paramiko.message import Message +from paramiko.ssh_exception import SSHException +from paramiko.file import BufferedFile +from paramiko.buffered_pipe import BufferedPipe, PipeTimeout +from paramiko import pipe +from paramiko.util import ClosingContextManager + + +def open_only(func): + """ + Decorator for `.Channel` methods which performs an openness check. + + :raises: + `.SSHException` -- If the wrapped method is called on an unopened + `.Channel`. + """ + + @wraps(func) + def _check(self, *args, **kwds): + if ( + self.closed + or self.eof_received + or self.eof_sent + or not self.active + ): + raise SSHException("Channel is not open") + return func(self, *args, **kwds) + + return _check + + +class Channel(ClosingContextManager): + """ + A secure tunnel across an SSH `.Transport`. A Channel is meant to behave + like a socket, and has an API that should be indistinguishable from the + Python socket API. + + Because SSH2 has a windowing kind of flow control, if you stop reading data + from a Channel and its buffer fills up, the server will be unable to send + you any more data until you read some of it. (This won't affect other + channels on the same transport -- all channels on a single transport are + flow-controlled independently.) Similarly, if the server isn't reading + data you send, calls to `send` may block, unless you set a timeout. This + is exactly like a normal network socket, so it shouldn't be too surprising. + + Instances of this class may be used as context managers. + """ + + def __init__(self, chanid): + """ + Create a new channel. The channel is not associated with any + particular session or `.Transport` until the Transport attaches it. + Normally you would only call this method from the constructor of a + subclass of `.Channel`. + + :param int chanid: + the ID of this channel, as passed by an existing `.Transport`. + """ + #: Channel ID + self.chanid = chanid + #: Remote channel ID + self.remote_chanid = 0 + #: `.Transport` managing this channel + self.transport = None + #: Whether the connection is presently active + self.active = False + self.eof_received = 0 + self.eof_sent = 0 + self.in_buffer = BufferedPipe() + self.in_stderr_buffer = BufferedPipe() + self.timeout = None + #: Whether the connection has been closed + self.closed = False + self.ultra_debug = False + self.lock = threading.Lock() + self.out_buffer_cv = threading.Condition(self.lock) + self.in_window_size = 0 + self.out_window_size = 0 + self.in_max_packet_size = 0 + self.out_max_packet_size = 0 + self.in_window_threshold = 0 + self.in_window_sofar = 0 + self.status_event = threading.Event() + self._name = str(chanid) + self.logger = util.get_logger("paramiko.transport") + self._pipe = None + self.event = threading.Event() + self.event_ready = False + self.combine_stderr = False + self.exit_status = -1 + self.origin_addr = None + + def __del__(self): + try: + self.close() + except: + pass + + def __repr__(self): + """ + Return a string representation of this object, for debugging. + """ + out = "<paramiko.Channel {}".format(self.chanid) + if self.closed: + out += " (closed)" + elif self.active: + if self.eof_received: + out += " (EOF received)" + if self.eof_sent: + out += " (EOF sent)" + out += " (open) window={}".format(self.out_window_size) + if len(self.in_buffer) > 0: + out += " in-buffer={}".format(len(self.in_buffer)) + out += " -> " + repr(self.transport) + out += ">" + return out + + @open_only + def get_pty( + self, + term="vt100", + width=80, + height=24, + width_pixels=0, + height_pixels=0, + ): + """ + Request a pseudo-terminal from the server. This is usually used right + after creating a client channel, to ask the server to provide some + basic terminal semantics for a shell invoked with `invoke_shell`. + It isn't necessary (or desirable) to call this method if you're going + to execute a single command with `exec_command`. + + :param str term: the terminal type to emulate + (for example, ``'vt100'``) + :param int width: width (in characters) of the terminal screen + :param int height: height (in characters) of the terminal screen + :param int width_pixels: width (in pixels) of the terminal screen + :param int height_pixels: height (in pixels) of the terminal screen + + :raises: + `.SSHException` -- if the request was rejected or the channel was + closed + """ + m = Message() + m.add_byte(cMSG_CHANNEL_REQUEST) + m.add_int(self.remote_chanid) + m.add_string("pty-req") + m.add_boolean(True) + m.add_string(term) + m.add_int(width) + m.add_int(height) + m.add_int(width_pixels) + m.add_int(height_pixels) + m.add_string(bytes()) + self._event_pending() + self.transport._send_user_message(m) + self._wait_for_event() + + @open_only + def invoke_shell(self): + """ + Request an interactive shell session on this channel. If the server + allows it, the channel will then be directly connected to the stdin, + stdout, and stderr of the shell. + + Normally you would call `get_pty` before this, in which case the + shell will operate through the pty, and the channel will be connected + to the stdin and stdout of the pty. + + When the shell exits, the channel will be closed and can't be reused. + You must open a new channel if you wish to open another shell. + + :raises: + `.SSHException` -- if the request was rejected or the channel was + closed + """ + m = Message() + m.add_byte(cMSG_CHANNEL_REQUEST) + m.add_int(self.remote_chanid) + m.add_string("shell") + m.add_boolean(True) + self._event_pending() + self.transport._send_user_message(m) + self._wait_for_event() + + @open_only + def exec_command(self, command): + """ + Execute a command on the server. If the server allows it, the channel + will then be directly connected to the stdin, stdout, and stderr of + the command being executed. + + When the command finishes executing, the channel will be closed and + can't be reused. You must open a new channel if you wish to execute + another command. + + :param str command: a shell command to execute. + + :raises: + `.SSHException` -- if the request was rejected or the channel was + closed + """ + m = Message() + m.add_byte(cMSG_CHANNEL_REQUEST) + m.add_int(self.remote_chanid) + m.add_string("exec") + m.add_boolean(True) + m.add_string(command) + self._event_pending() + self.transport._send_user_message(m) + self._wait_for_event() + + @open_only + def invoke_subsystem(self, subsystem): + """ + Request a subsystem on the server (for example, ``sftp``). If the + server allows it, the channel will then be directly connected to the + requested subsystem. + + When the subsystem finishes, the channel will be closed and can't be + reused. + + :param str subsystem: name of the subsystem being requested. + + :raises: + `.SSHException` -- if the request was rejected or the channel was + closed + """ + m = Message() + m.add_byte(cMSG_CHANNEL_REQUEST) + m.add_int(self.remote_chanid) + m.add_string("subsystem") + m.add_boolean(True) + m.add_string(subsystem) + self._event_pending() + self.transport._send_user_message(m) + self._wait_for_event() + + @open_only + def resize_pty(self, width=80, height=24, width_pixels=0, height_pixels=0): + """ + Resize the pseudo-terminal. This can be used to change the width and + height of the terminal emulation created in a previous `get_pty` call. + + :param int width: new width (in characters) of the terminal screen + :param int height: new height (in characters) of the terminal screen + :param int width_pixels: new width (in pixels) of the terminal screen + :param int height_pixels: new height (in pixels) of the terminal screen + + :raises: + `.SSHException` -- if the request was rejected or the channel was + closed + """ + m = Message() + m.add_byte(cMSG_CHANNEL_REQUEST) + m.add_int(self.remote_chanid) + m.add_string("window-change") + m.add_boolean(False) + m.add_int(width) + m.add_int(height) + m.add_int(width_pixels) + m.add_int(height_pixels) + self.transport._send_user_message(m) + + @open_only + def update_environment(self, environment): + """ + Updates this channel's remote shell environment. + + .. note:: + This operation is additive - i.e. the current environment is not + reset before the given environment variables are set. + + .. warning:: + Servers may silently reject some environment variables; see the + warning in `set_environment_variable` for details. + + :param dict environment: + a dictionary containing the name and respective values to set + :raises: + `.SSHException` -- if any of the environment variables was rejected + by the server or the channel was closed + """ + for name, value in environment.items(): + try: + self.set_environment_variable(name, value) + except SSHException as e: + err = 'Failed to set environment variable "{}".' + raise SSHException(err.format(name), e) + + @open_only + def set_environment_variable(self, name, value): + """ + Set the value of an environment variable. + + .. warning:: + The server may reject this request depending on its ``AcceptEnv`` + setting; such rejections will fail silently (which is common client + practice for this particular request type). Make sure you + understand your server's configuration before using! + + :param str name: name of the environment variable + :param str value: value of the environment variable + + :raises: + `.SSHException` -- if the request was rejected or the channel was + closed + """ + m = Message() + m.add_byte(cMSG_CHANNEL_REQUEST) + m.add_int(self.remote_chanid) + m.add_string("env") + m.add_boolean(False) + m.add_string(name) + m.add_string(value) + self.transport._send_user_message(m) + + def exit_status_ready(self): + """ + Return true if the remote process has exited and returned an exit + status. You may use this to poll the process status if you don't + want to block in `recv_exit_status`. Note that the server may not + return an exit status in some cases (like bad servers). + + :return: + ``True`` if `recv_exit_status` will return immediately, else + ``False``. + + .. versionadded:: 1.7.3 + """ + return self.closed or self.status_event.is_set() + + def recv_exit_status(self): + """ + Return the exit status from the process on the server. This is + mostly useful for retrieving the results of an `exec_command`. + If the command hasn't finished yet, this method will wait until + it does, or until the channel is closed. If no exit status is + provided by the server, -1 is returned. + + .. warning:: + In some situations, receiving remote output larger than the current + `.Transport` or session's ``window_size`` (e.g. that set by the + ``default_window_size`` kwarg for `.Transport.__init__`) will cause + `.recv_exit_status` to hang indefinitely if it is called prior to a + sufficiently large `.Channel.recv` (or if there are no threads + calling `.Channel.recv` in the background). + + In these cases, ensuring that `.recv_exit_status` is called *after* + `.Channel.recv` (or, again, using threads) can avoid the hang. + + :return: the exit code (as an `int`) of the process on the server. + + .. versionadded:: 1.2 + """ + self.status_event.wait() + assert self.status_event.is_set() + return self.exit_status + + def send_exit_status(self, status): + """ + Send the exit status of an executed command to the client. (This + really only makes sense in server mode.) Many clients expect to + get some sort of status code back from an executed command after + it completes. + + :param int status: the exit code of the process + + .. versionadded:: 1.2 + """ + # in many cases, the channel will not still be open here. + # that's fine. + m = Message() + m.add_byte(cMSG_CHANNEL_REQUEST) + m.add_int(self.remote_chanid) + m.add_string("exit-status") + m.add_boolean(False) + m.add_int(status) + self.transport._send_user_message(m) + + @open_only + def request_x11( + self, + screen_number=0, + auth_protocol=None, + auth_cookie=None, + single_connection=False, + handler=None, + ): + """ + Request an x11 session on this channel. If the server allows it, + further x11 requests can be made from the server to the client, + when an x11 application is run in a shell session. + + From :rfc:`4254`:: + + It is RECOMMENDED that the 'x11 authentication cookie' that is + sent be a fake, random cookie, and that the cookie be checked and + replaced by the real cookie when a connection request is received. + + If you omit the auth_cookie, a new secure random 128-bit value will be + generated, used, and returned. You will need to use this value to + verify incoming x11 requests and replace them with the actual local + x11 cookie (which requires some knowledge of the x11 protocol). + + If a handler is passed in, the handler is called from another thread + whenever a new x11 connection arrives. The default handler queues up + incoming x11 connections, which may be retrieved using + `.Transport.accept`. The handler's calling signature is:: + + handler(channel: Channel, (address: str, port: int)) + + :param int screen_number: the x11 screen number (0, 10, etc.) + :param str auth_protocol: + the name of the X11 authentication method used; if none is given, + ``"MIT-MAGIC-COOKIE-1"`` is used + :param str auth_cookie: + hexadecimal string containing the x11 auth cookie; if none is + given, a secure random 128-bit value is generated + :param bool single_connection: + if True, only a single x11 connection will be forwarded (by + default, any number of x11 connections can arrive over this + session) + :param handler: + an optional callable handler to use for incoming X11 connections + :return: the auth_cookie used + """ + if auth_protocol is None: + auth_protocol = "MIT-MAGIC-COOKIE-1" + if auth_cookie is None: + auth_cookie = binascii.hexlify(os.urandom(16)) + + m = Message() + m.add_byte(cMSG_CHANNEL_REQUEST) + m.add_int(self.remote_chanid) + m.add_string("x11-req") + m.add_boolean(True) + m.add_boolean(single_connection) + m.add_string(auth_protocol) + m.add_string(auth_cookie) + m.add_int(screen_number) + self._event_pending() + self.transport._send_user_message(m) + self._wait_for_event() + self.transport._set_x11_handler(handler) + return auth_cookie + + @open_only + def request_forward_agent(self, handler): + """ + Request for a forward SSH Agent on this channel. + This is only valid for an ssh-agent from OpenSSH !!! + + :param handler: + a required callable handler to use for incoming SSH Agent + connections + + :return: True if we are ok, else False + (at that time we always return ok) + + :raises: SSHException in case of channel problem. + """ + m = Message() + m.add_byte(cMSG_CHANNEL_REQUEST) + m.add_int(self.remote_chanid) + m.add_string("auth-agent-req@openssh.com") + m.add_boolean(False) + self.transport._send_user_message(m) + self.transport._set_forward_agent_handler(handler) + return True + + def get_transport(self): + """ + Return the `.Transport` associated with this channel. + """ + return self.transport + + def set_name(self, name): + """ + Set a name for this channel. Currently it's only used to set the name + of the channel in logfile entries. The name can be fetched with the + `get_name` method. + + :param str name: new channel name + """ + self._name = name + + def get_name(self): + """ + Get the name of this channel that was previously set by `set_name`. + """ + return self._name + + def get_id(self): + """ + Return the `int` ID # for this channel. + + The channel ID is unique across a `.Transport` and usually a small + number. It's also the number passed to + `.ServerInterface.check_channel_request` when determining whether to + accept a channel request in server mode. + """ + return self.chanid + + def set_combine_stderr(self, combine): + """ + Set whether stderr should be combined into stdout on this channel. + The default is ``False``, but in some cases it may be convenient to + have both streams combined. + + If this is ``False``, and `exec_command` is called (or ``invoke_shell`` + with no pty), output to stderr will not show up through the `recv` + and `recv_ready` calls. You will have to use `recv_stderr` and + `recv_stderr_ready` to get stderr output. + + If this is ``True``, data will never show up via `recv_stderr` or + `recv_stderr_ready`. + + :param bool combine: + ``True`` if stderr output should be combined into stdout on this + channel. + :return: the previous setting (a `bool`). + + .. versionadded:: 1.1 + """ + data = bytes() + self.lock.acquire() + try: + old = self.combine_stderr + self.combine_stderr = combine + if combine and not old: + # copy old stderr buffer into primary buffer + data = self.in_stderr_buffer.empty() + finally: + self.lock.release() + if len(data) > 0: + self._feed(data) + return old + + # ...socket API... + + def settimeout(self, timeout): + """ + Set a timeout on blocking read/write operations. The ``timeout`` + argument can be a nonnegative float expressing seconds, or ``None``. + If a float is given, subsequent channel read/write operations will + raise a timeout exception if the timeout period value has elapsed + before the operation has completed. Setting a timeout of ``None`` + disables timeouts on socket operations. + + ``chan.settimeout(0.0)`` is equivalent to ``chan.setblocking(0)``; + ``chan.settimeout(None)`` is equivalent to ``chan.setblocking(1)``. + + :param float timeout: + seconds to wait for a pending read/write operation before raising + ``socket.timeout``, or ``None`` for no timeout. + """ + self.timeout = timeout + + def gettimeout(self): + """ + Returns the timeout in seconds (as a float) associated with socket + operations, or ``None`` if no timeout is set. This reflects the last + call to `setblocking` or `settimeout`. + """ + return self.timeout + + def setblocking(self, blocking): + """ + Set blocking or non-blocking mode of the channel: if ``blocking`` is 0, + the channel is set to non-blocking mode; otherwise it's set to blocking + mode. Initially all channels are in blocking mode. + + In non-blocking mode, if a `recv` call doesn't find any data, or if a + `send` call can't immediately dispose of the data, an error exception + is raised. In blocking mode, the calls block until they can proceed. An + EOF condition is considered "immediate data" for `recv`, so if the + channel is closed in the read direction, it will never block. + + ``chan.setblocking(0)`` is equivalent to ``chan.settimeout(0)``; + ``chan.setblocking(1)`` is equivalent to ``chan.settimeout(None)``. + + :param int blocking: + 0 to set non-blocking mode; non-0 to set blocking mode. + """ + if blocking: + self.settimeout(None) + else: + self.settimeout(0.0) + + def getpeername(self): + """ + Return the address of the remote side of this Channel, if possible. + + This simply wraps `.Transport.getpeername`, used to provide enough of a + socket-like interface to allow asyncore to work. (asyncore likes to + call ``'getpeername'``.) + """ + return self.transport.getpeername() + + def close(self): + """ + Close the channel. All future read/write operations on the channel + will fail. The remote end will receive no more data (after queued data + is flushed). Channels are automatically closed when their `.Transport` + is closed or when they are garbage collected. + """ + self.lock.acquire() + try: + # only close the pipe when the user explicitly closes the channel. + # otherwise they will get unpleasant surprises. (and do it before + # checking self.closed, since the remote host may have already + # closed the connection.) + if self._pipe is not None: + self._pipe.close() + self._pipe = None + + if not self.active or self.closed: + return + msgs = self._close_internal() + finally: + self.lock.release() + for m in msgs: + if m is not None: + self.transport._send_user_message(m) + + def recv_ready(self): + """ + Returns true if data is buffered and ready to be read from this + channel. A ``False`` result does not mean that the channel has closed; + it means you may need to wait before more data arrives. + + :return: + ``True`` if a `recv` call on this channel would immediately return + at least one byte; ``False`` otherwise. + """ + return self.in_buffer.read_ready() + + def recv(self, nbytes): + """ + Receive data from the channel. The return value is a string + representing the data received. The maximum amount of data to be + received at once is specified by ``nbytes``. If a string of + length zero is returned, the channel stream has closed. + + :param int nbytes: maximum number of bytes to read. + :return: received data, as a `bytes`. + + :raises socket.timeout: + if no data is ready before the timeout set by `settimeout`. + """ + try: + out = self.in_buffer.read(nbytes, self.timeout) + except PipeTimeout: + raise socket.timeout() + + ack = self._check_add_window(len(out)) + # no need to hold the channel lock when sending this + if ack > 0: + m = Message() + m.add_byte(cMSG_CHANNEL_WINDOW_ADJUST) + m.add_int(self.remote_chanid) + m.add_int(ack) + self.transport._send_user_message(m) + + return out + + def recv_stderr_ready(self): + """ + Returns true if data is buffered and ready to be read from this + channel's stderr stream. Only channels using `exec_command` or + `invoke_shell` without a pty will ever have data on the stderr + stream. + + :return: + ``True`` if a `recv_stderr` call on this channel would immediately + return at least one byte; ``False`` otherwise. + + .. versionadded:: 1.1 + """ + return self.in_stderr_buffer.read_ready() + + def recv_stderr(self, nbytes): + """ + Receive data from the channel's stderr stream. Only channels using + `exec_command` or `invoke_shell` without a pty will ever have data + on the stderr stream. The return value is a string representing the + data received. The maximum amount of data to be received at once is + specified by ``nbytes``. If a string of length zero is returned, the + channel stream has closed. + + :param int nbytes: maximum number of bytes to read. + :return: received data as a `bytes` + + :raises socket.timeout: if no data is ready before the timeout set by + `settimeout`. + + .. versionadded:: 1.1 + """ + try: + out = self.in_stderr_buffer.read(nbytes, self.timeout) + except PipeTimeout: + raise socket.timeout() + + ack = self._check_add_window(len(out)) + # no need to hold the channel lock when sending this + if ack > 0: + m = Message() + m.add_byte(cMSG_CHANNEL_WINDOW_ADJUST) + m.add_int(self.remote_chanid) + m.add_int(ack) + self.transport._send_user_message(m) + + return out + + def send_ready(self): + """ + Returns true if data can be written to this channel without blocking. + This means the channel is either closed (so any write attempt would + return immediately) or there is at least one byte of space in the + outbound buffer. If there is at least one byte of space in the + outbound buffer, a `send` call will succeed immediately and return + the number of bytes actually written. + + :return: + ``True`` if a `send` call on this channel would immediately succeed + or fail + """ + self.lock.acquire() + try: + if self.closed or self.eof_sent: + return True + return self.out_window_size > 0 + finally: + self.lock.release() + + def send(self, s): + """ + Send data to the channel. Returns the number of bytes sent, or 0 if + the channel stream is closed. Applications are responsible for + checking that all data has been sent: if only some of the data was + transmitted, the application needs to attempt delivery of the remaining + data. + + :param bytes s: data to send + :return: number of bytes actually sent, as an `int` + + :raises socket.timeout: if no data could be sent before the timeout set + by `settimeout`. + """ + + m = Message() + m.add_byte(cMSG_CHANNEL_DATA) + m.add_int(self.remote_chanid) + return self._send(s, m) + + def send_stderr(self, s): + """ + Send data to the channel on the "stderr" stream. This is normally + only used by servers to send output from shell commands -- clients + won't use this. Returns the number of bytes sent, or 0 if the channel + stream is closed. Applications are responsible for checking that all + data has been sent: if only some of the data was transmitted, the + application needs to attempt delivery of the remaining data. + + :param bytes s: data to send. + :return: number of bytes actually sent, as an `int`. + + :raises socket.timeout: + if no data could be sent before the timeout set by `settimeout`. + + .. versionadded:: 1.1 + """ + + m = Message() + m.add_byte(cMSG_CHANNEL_EXTENDED_DATA) + m.add_int(self.remote_chanid) + m.add_int(1) + return self._send(s, m) + + def sendall(self, s): + """ + Send data to the channel, without allowing partial results. Unlike + `send`, this method continues to send data from the given string until + either all data has been sent or an error occurs. Nothing is returned. + + :param bytes s: data to send. + + :raises socket.timeout: + if sending stalled for longer than the timeout set by `settimeout`. + :raises socket.error: + if an error occurred before the entire string was sent. + + .. note:: + If the channel is closed while only part of the data has been + sent, there is no way to determine how much data (if any) was sent. + This is irritating, but identically follows Python's API. + """ + while s: + sent = self.send(s) + s = s[sent:] + return None + + def sendall_stderr(self, s): + """ + Send data to the channel's "stderr" stream, without allowing partial + results. Unlike `send_stderr`, this method continues to send data + from the given bytestring until all data has been sent or an error + occurs. Nothing is returned. + + :param bytes s: data to send to the client as "stderr" output. + + :raises socket.timeout: + if sending stalled for longer than the timeout set by `settimeout`. + :raises socket.error: + if an error occurred before the entire string was sent. + + .. versionadded:: 1.1 + """ + while s: + sent = self.send_stderr(s) + s = s[sent:] + return None + + def makefile(self, *params): + """ + Return a file-like object associated with this channel. The optional + ``mode`` and ``bufsize`` arguments are interpreted the same way as by + the built-in ``file()`` function in Python. + + :return: `.ChannelFile` object which can be used for Python file I/O. + """ + return ChannelFile(*([self] + list(params))) + + def makefile_stderr(self, *params): + """ + Return a file-like object associated with this channel's stderr + stream. Only channels using `exec_command` or `invoke_shell` + without a pty will ever have data on the stderr stream. + + The optional ``mode`` and ``bufsize`` arguments are interpreted the + same way as by the built-in ``file()`` function in Python. For a + client, it only makes sense to open this file for reading. For a + server, it only makes sense to open this file for writing. + + :returns: + `.ChannelStderrFile` object which can be used for Python file I/O. + + .. versionadded:: 1.1 + """ + return ChannelStderrFile(*([self] + list(params))) + + def makefile_stdin(self, *params): + """ + Return a file-like object associated with this channel's stdin + stream. + + The optional ``mode`` and ``bufsize`` arguments are interpreted the + same way as by the built-in ``file()`` function in Python. For a + client, it only makes sense to open this file for writing. For a + server, it only makes sense to open this file for reading. + + :returns: + `.ChannelStdinFile` object which can be used for Python file I/O. + + .. versionadded:: 2.6 + """ + return ChannelStdinFile(*([self] + list(params))) + + def fileno(self): + """ + Returns an OS-level file descriptor which can be used for polling, but + but not for reading or writing. This is primarily to allow Python's + ``select`` module to work. + + The first time ``fileno`` is called on a channel, a pipe is created to + simulate real OS-level file descriptor (FD) behavior. Because of this, + two OS-level FDs are created, which will use up FDs faster than normal. + (You won't notice this effect unless you have hundreds of channels + open at the same time.) + + :return: an OS-level file descriptor (`int`) + + .. warning:: + This method causes channel reads to be slightly less efficient. + """ + self.lock.acquire() + try: + if self._pipe is not None: + return self._pipe.fileno() + # create the pipe and feed in any existing data + self._pipe = pipe.make_pipe() + p1, p2 = pipe.make_or_pipe(self._pipe) + self.in_buffer.set_event(p1) + self.in_stderr_buffer.set_event(p2) + return self._pipe.fileno() + finally: + self.lock.release() + + def shutdown(self, how): + """ + Shut down one or both halves of the connection. If ``how`` is 0, + further receives are disallowed. If ``how`` is 1, further sends + are disallowed. If ``how`` is 2, further sends and receives are + disallowed. This closes the stream in one or both directions. + + :param int how: + 0 (stop receiving), 1 (stop sending), or 2 (stop receiving and + sending). + """ + if (how == 0) or (how == 2): + # feign "read" shutdown + self.eof_received = 1 + if (how == 1) or (how == 2): + self.lock.acquire() + try: + m = self._send_eof() + finally: + self.lock.release() + if m is not None: + self.transport._send_user_message(m) + + def shutdown_read(self): + """ + Shutdown the receiving side of this socket, closing the stream in + the incoming direction. After this call, future reads on this + channel will fail instantly. This is a convenience method, equivalent + to ``shutdown(0)``, for people who don't make it a habit to + memorize unix constants from the 1970s. + + .. versionadded:: 1.2 + """ + self.shutdown(0) + + def shutdown_write(self): + """ + Shutdown the sending side of this socket, closing the stream in + the outgoing direction. After this call, future writes on this + channel will fail instantly. This is a convenience method, equivalent + to ``shutdown(1)``, for people who don't make it a habit to + memorize unix constants from the 1970s. + + .. versionadded:: 1.2 + """ + self.shutdown(1) + + @property + def _closed(self): + # Concession to Python 3's socket API, which has a private ._closed + # attribute instead of a semipublic .closed attribute. + return self.closed + + # ...calls from Transport + + def _set_transport(self, transport): + self.transport = transport + self.logger = util.get_logger(self.transport.get_log_channel()) + + def _set_window(self, window_size, max_packet_size): + self.in_window_size = window_size + self.in_max_packet_size = max_packet_size + # threshold of bytes we receive before we bother to send + # a window update + self.in_window_threshold = window_size // 10 + self.in_window_sofar = 0 + self._log(DEBUG, "Max packet in: {} bytes".format(max_packet_size)) + + def _set_remote_channel(self, chanid, window_size, max_packet_size): + self.remote_chanid = chanid + self.out_window_size = window_size + self.out_max_packet_size = self.transport._sanitize_packet_size( + max_packet_size + ) + self.active = 1 + self._log( + DEBUG, "Max packet out: {} bytes".format(self.out_max_packet_size) + ) + + def _request_success(self, m): + self._log(DEBUG, "Sesch channel {} request ok".format(self.chanid)) + self.event_ready = True + self.event.set() + return + + def _request_failed(self, m): + self.lock.acquire() + try: + msgs = self._close_internal() + finally: + self.lock.release() + for m in msgs: + if m is not None: + self.transport._send_user_message(m) + + def _feed(self, m): + if isinstance(m, bytes): + # passed from _feed_extended + s = m + else: + s = m.get_binary() + self.in_buffer.feed(s) + + def _feed_extended(self, m): + code = m.get_int() + s = m.get_binary() + if code != 1: + self._log( + ERROR, "unknown extended_data type {}; discarding".format(code) + ) + return + if self.combine_stderr: + self._feed(s) + else: + self.in_stderr_buffer.feed(s) + + def _window_adjust(self, m): + nbytes = m.get_int() + self.lock.acquire() + try: + if self.ultra_debug: + self._log(DEBUG, "window up {}".format(nbytes)) + self.out_window_size += nbytes + self.out_buffer_cv.notify_all() + finally: + self.lock.release() + + def _handle_request(self, m): + key = m.get_text() + want_reply = m.get_boolean() + server = self.transport.server_object + ok = False + if key == "exit-status": + self.exit_status = m.get_int() + self.status_event.set() + ok = True + elif key == "xon-xoff": + # ignore + ok = True + elif key == "pty-req": + term = m.get_string() + width = m.get_int() + height = m.get_int() + pixelwidth = m.get_int() + pixelheight = m.get_int() + modes = m.get_string() + if server is None: + ok = False + else: + ok = server.check_channel_pty_request( + self, term, width, height, pixelwidth, pixelheight, modes + ) + elif key == "shell": + if server is None: + ok = False + else: + ok = server.check_channel_shell_request(self) + elif key == "env": + name = m.get_string() + value = m.get_string() + if server is None: + ok = False + else: + ok = server.check_channel_env_request(self, name, value) + elif key == "exec": + cmd = m.get_string() + if server is None: + ok = False + else: + ok = server.check_channel_exec_request(self, cmd) + elif key == "subsystem": + name = m.get_text() + if server is None: + ok = False + else: + ok = server.check_channel_subsystem_request(self, name) + elif key == "window-change": + width = m.get_int() + height = m.get_int() + pixelwidth = m.get_int() + pixelheight = m.get_int() + if server is None: + ok = False + else: + ok = server.check_channel_window_change_request( + self, width, height, pixelwidth, pixelheight + ) + elif key == "x11-req": + single_connection = m.get_boolean() + auth_proto = m.get_text() + auth_cookie = m.get_binary() + screen_number = m.get_int() + if server is None: + ok = False + else: + ok = server.check_channel_x11_request( + self, + single_connection, + auth_proto, + auth_cookie, + screen_number, + ) + elif key == "auth-agent-req@openssh.com": + if server is None: + ok = False + else: + ok = server.check_channel_forward_agent_request(self) + else: + self._log(DEBUG, 'Unhandled channel request "{}"'.format(key)) + ok = False + if want_reply: + m = Message() + if ok: + m.add_byte(cMSG_CHANNEL_SUCCESS) + else: + m.add_byte(cMSG_CHANNEL_FAILURE) + m.add_int(self.remote_chanid) + self.transport._send_user_message(m) + + def _handle_eof(self, m): + self.lock.acquire() + try: + if not self.eof_received: + self.eof_received = True + self.in_buffer.close() + self.in_stderr_buffer.close() + if self._pipe is not None: + self._pipe.set_forever() + finally: + self.lock.release() + self._log(DEBUG, "EOF received ({})".format(self._name)) + + def _handle_close(self, m): + self.lock.acquire() + try: + msgs = self._close_internal() + self.transport._unlink_channel(self.chanid) + finally: + self.lock.release() + for m in msgs: + if m is not None: + self.transport._send_user_message(m) + + # ...internals... + + def _send(self, s, m): + size = len(s) + self.lock.acquire() + try: + if self.closed: + # this doesn't seem useful, but it is the documented behavior + # of Socket + raise socket.error("Socket is closed") + size = self._wait_for_send_window(size) + if size == 0: + # eof or similar + return 0 + m.add_string(s[:size]) + finally: + self.lock.release() + # Note: We release self.lock before calling _send_user_message. + # Otherwise, we can deadlock during re-keying. + self.transport._send_user_message(m) + return size + + def _log(self, level, msg, *args): + self.logger.log(level, "[chan " + self._name + "] " + msg, *args) + + def _event_pending(self): + self.event.clear() + self.event_ready = False + + def _wait_for_event(self): + self.event.wait() + assert self.event.is_set() + if self.event_ready: + return + e = self.transport.get_exception() + if e is None: + e = SSHException("Channel closed.") + raise e + + def _set_closed(self): + # you are holding the lock. + self.closed = True + self.in_buffer.close() + self.in_stderr_buffer.close() + self.out_buffer_cv.notify_all() + # Notify any waiters that we are closed + self.event.set() + self.status_event.set() + if self._pipe is not None: + self._pipe.set_forever() + + def _send_eof(self): + # you are holding the lock. + if self.eof_sent: + return None + m = Message() + m.add_byte(cMSG_CHANNEL_EOF) + m.add_int(self.remote_chanid) + self.eof_sent = True + self._log(DEBUG, "EOF sent ({})".format(self._name)) + return m + + def _close_internal(self): + # you are holding the lock. + if not self.active or self.closed: + return None, None + m1 = self._send_eof() + m2 = Message() + m2.add_byte(cMSG_CHANNEL_CLOSE) + m2.add_int(self.remote_chanid) + self._set_closed() + # can't unlink from the Transport yet -- the remote side may still + # try to send meta-data (exit-status, etc) + return m1, m2 + + def _unlink(self): + # server connection could die before we become active: + # still signal the close! + if self.closed: + return + self.lock.acquire() + try: + self._set_closed() + self.transport._unlink_channel(self.chanid) + finally: + self.lock.release() + + def _check_add_window(self, n): + self.lock.acquire() + try: + if self.closed or self.eof_received or not self.active: + return 0 + if self.ultra_debug: + self._log(DEBUG, "addwindow {}".format(n)) + self.in_window_sofar += n + if self.in_window_sofar <= self.in_window_threshold: + return 0 + if self.ultra_debug: + self._log( + DEBUG, "addwindow send {}".format(self.in_window_sofar) + ) + out = self.in_window_sofar + self.in_window_sofar = 0 + return out + finally: + self.lock.release() + + def _wait_for_send_window(self, size): + """ + (You are already holding the lock.) + Wait for the send window to open up, and allocate up to ``size`` bytes + for transmission. If no space opens up before the timeout, a timeout + exception is raised. Returns the number of bytes available to send + (may be less than requested). + """ + # you are already holding the lock + if self.closed or self.eof_sent: + return 0 + if self.out_window_size == 0: + # should we block? + if self.timeout == 0.0: + raise socket.timeout() + # loop here in case we get woken up but a different thread has + # filled the buffer + timeout = self.timeout + while self.out_window_size == 0: + if self.closed or self.eof_sent: + return 0 + then = time.time() + self.out_buffer_cv.wait(timeout) + if timeout is not None: + timeout -= time.time() - then + if timeout <= 0.0: + raise socket.timeout() + # we have some window to squeeze into + if self.closed or self.eof_sent: + return 0 + if self.out_window_size < size: + size = self.out_window_size + if self.out_max_packet_size - 64 < size: + size = self.out_max_packet_size - 64 + self.out_window_size -= size + if self.ultra_debug: + self._log(DEBUG, "window down to {}".format(self.out_window_size)) + return size + + +class ChannelFile(BufferedFile): + """ + A file-like wrapper around `.Channel`. A ChannelFile is created by calling + `Channel.makefile`. + + .. warning:: + To correctly emulate the file object created from a socket's `makefile + <python:socket.socket.makefile>` method, a `.Channel` and its + `.ChannelFile` should be able to be closed or garbage-collected + independently. Currently, closing the `ChannelFile` does nothing but + flush the buffer. + """ + + def __init__(self, channel, mode="r", bufsize=-1): + self.channel = channel + BufferedFile.__init__(self) + self._set_mode(mode, bufsize) + + def __repr__(self): + """ + Returns a string representation of this object, for debugging. + """ + return "<paramiko.ChannelFile from " + repr(self.channel) + ">" + + def _read(self, size): + return self.channel.recv(size) + + def _write(self, data): + self.channel.sendall(data) + return len(data) + + +class ChannelStderrFile(ChannelFile): + """ + A file-like wrapper around `.Channel` stderr. + + See `Channel.makefile_stderr` for details. + """ + + def _read(self, size): + return self.channel.recv_stderr(size) + + def _write(self, data): + self.channel.sendall_stderr(data) + return len(data) + + +class ChannelStdinFile(ChannelFile): + """ + A file-like wrapper around `.Channel` stdin. + + See `Channel.makefile_stdin` for details. + """ + + def close(self): + super().close() + self.channel.shutdown_write() diff --git a/paramiko/client.py b/paramiko/client.py new file mode 100644 index 0000000..1fe14b0 --- /dev/null +++ b/paramiko/client.py @@ -0,0 +1,865 @@ +# Copyright (C) 2006-2007 Robey Pointer <robeypointer@gmail.com> +# +# This file is part of paramiko. +# +# Paramiko is free software; you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free +# Software Foundation; either version 2.1 of the License, or (at your option) +# any later version. +# +# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Paramiko; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +""" +SSH client & key policies +""" + +from binascii import hexlify +import getpass +import inspect +import os +import socket +import warnings +from errno import ECONNREFUSED, EHOSTUNREACH + +from paramiko.agent import Agent +from paramiko.common import DEBUG +from paramiko.config import SSH_PORT +from paramiko.dsskey import DSSKey +from paramiko.ecdsakey import ECDSAKey +from paramiko.ed25519key import Ed25519Key +from paramiko.hostkeys import HostKeys +from paramiko.rsakey import RSAKey +from paramiko.ssh_exception import ( + SSHException, + BadHostKeyException, + NoValidConnectionsError, +) +from paramiko.transport import Transport +from paramiko.util import ClosingContextManager + + +class SSHClient(ClosingContextManager): + """ + A high-level representation of a session with an SSH server. This class + wraps `.Transport`, `.Channel`, and `.SFTPClient` to take care of most + aspects of authenticating and opening channels. A typical use case is:: + + client = SSHClient() + client.load_system_host_keys() + client.connect('ssh.example.com') + stdin, stdout, stderr = client.exec_command('ls -l') + + You may pass in explicit overrides for authentication and server host key + checking. The default mechanism is to try to use local key files or an + SSH agent (if one is running). + + Instances of this class may be used as context managers. + + .. versionadded:: 1.6 + """ + + def __init__(self): + """ + Create a new SSHClient. + """ + self._system_host_keys = HostKeys() + self._host_keys = HostKeys() + self._host_keys_filename = None + self._log_channel = None + self._policy = RejectPolicy() + self._transport = None + self._agent = None + + def load_system_host_keys(self, filename=None): + """ + Load host keys from a system (read-only) file. Host keys read with + this method will not be saved back by `save_host_keys`. + + This method can be called multiple times. Each new set of host keys + will be merged with the existing set (new replacing old if there are + conflicts). + + If ``filename`` is left as ``None``, an attempt will be made to read + keys from the user's local "known hosts" file, as used by OpenSSH, + and no exception will be raised if the file can't be read. This is + probably only useful on posix. + + :param str filename: the filename to read, or ``None`` + + :raises: ``IOError`` -- + if a filename was provided and the file could not be read + """ + if filename is None: + # try the user's .ssh key file, and mask exceptions + filename = os.path.expanduser("~/.ssh/known_hosts") + try: + self._system_host_keys.load(filename) + except IOError: + pass + return + self._system_host_keys.load(filename) + + def load_host_keys(self, filename): + """ + Load host keys from a local host-key file. Host keys read with this + method will be checked after keys loaded via `load_system_host_keys`, + but will be saved back by `save_host_keys` (so they can be modified). + The missing host key policy `.AutoAddPolicy` adds keys to this set and + saves them, when connecting to a previously-unknown server. + + This method can be called multiple times. Each new set of host keys + will be merged with the existing set (new replacing old if there are + conflicts). When automatically saving, the last hostname is used. + + :param str filename: the filename to read + + :raises: ``IOError`` -- if the filename could not be read + """ + self._host_keys_filename = filename + self._host_keys.load(filename) + + def save_host_keys(self, filename): + """ + Save the host keys back to a file. Only the host keys loaded with + `load_host_keys` (plus any added directly) will be saved -- not any + host keys loaded with `load_system_host_keys`. + + :param str filename: the filename to save to + + :raises: ``IOError`` -- if the file could not be written + """ + + # update local host keys from file (in case other SSH clients + # have written to the known_hosts file meanwhile. + if self._host_keys_filename is not None: + self.load_host_keys(self._host_keys_filename) + + with open(filename, "w") as f: + for hostname, keys in self._host_keys.items(): + for keytype, key in keys.items(): + f.write( + "{} {} {}\n".format( + hostname, keytype, key.get_base64() + ) + ) + + def get_host_keys(self): + """ + Get the local `.HostKeys` object. This can be used to examine the + local host keys or change them. + + :return: the local host keys as a `.HostKeys` object. + """ + return self._host_keys + + def set_log_channel(self, name): + """ + Set the channel for logging. The default is ``"paramiko.transport"`` + but it can be set to anything you want. + + :param str name: new channel name for logging + """ + self._log_channel = name + + def set_missing_host_key_policy(self, policy): + """ + Set policy to use when connecting to servers without a known host key. + + Specifically: + + * A **policy** is a "policy class" (or instance thereof), namely some + subclass of `.MissingHostKeyPolicy` such as `.RejectPolicy` (the + default), `.AutoAddPolicy`, `.WarningPolicy`, or a user-created + subclass. + * A host key is **known** when it appears in the client object's cached + host keys structures (those manipulated by `load_system_host_keys` + and/or `load_host_keys`). + + :param .MissingHostKeyPolicy policy: + the policy to use when receiving a host key from a + previously-unknown server + """ + if inspect.isclass(policy): + policy = policy() + self._policy = policy + + def _families_and_addresses(self, hostname, port): + """ + Yield pairs of address families and addresses to try for connecting. + + :param str hostname: the server to connect to + :param int port: the server port to connect to + :returns: Yields an iterable of ``(family, address)`` tuples + """ + guess = True + addrinfos = socket.getaddrinfo( + hostname, port, socket.AF_UNSPEC, socket.SOCK_STREAM + ) + for (family, socktype, proto, canonname, sockaddr) in addrinfos: + if socktype == socket.SOCK_STREAM: + yield family, sockaddr + guess = False + + # some OS like AIX don't indicate SOCK_STREAM support, so just + # guess. :( We only do this if we did not get a single result marked + # as socktype == SOCK_STREAM. + if guess: + for family, _, _, _, sockaddr in addrinfos: + yield family, sockaddr + + def connect( + self, + hostname, + port=SSH_PORT, + username=None, + password=None, + pkey=None, + key_filename=None, + timeout=None, + allow_agent=True, + look_for_keys=True, + compress=False, + sock=None, + gss_auth=False, + gss_kex=False, + gss_deleg_creds=True, + gss_host=None, + banner_timeout=None, + auth_timeout=None, + channel_timeout=None, + gss_trust_dns=True, + passphrase=None, + disabled_algorithms=None, + transport_factory=None, + ): + """ + Connect to an SSH server and authenticate to it. The server's host key + is checked against the system host keys (see `load_system_host_keys`) + and any local host keys (`load_host_keys`). If the server's hostname + is not found in either set of host keys, the missing host key policy + is used (see `set_missing_host_key_policy`). The default policy is + to reject the key and raise an `.SSHException`. + + Authentication is attempted in the following order of priority: + + - The ``pkey`` or ``key_filename`` passed in (if any) + + - ``key_filename`` may contain OpenSSH public certificate paths + as well as regular private-key paths; when files ending in + ``-cert.pub`` are found, they are assumed to match a private + key, and both components will be loaded. (The private key + itself does *not* need to be listed in ``key_filename`` for + this to occur - *just* the certificate.) + + - Any key we can find through an SSH agent + - Any "id_rsa", "id_dsa" or "id_ecdsa" key discoverable in + ``~/.ssh/`` + + - When OpenSSH-style public certificates exist that match an + existing such private key (so e.g. one has ``id_rsa`` and + ``id_rsa-cert.pub``) the certificate will be loaded alongside + the private key and used for authentication. + + - Plain username/password auth, if a password was given + + If a private key requires a password to unlock it, and a password is + passed in, that password will be used to attempt to unlock the key. + + :param str hostname: the server to connect to + :param int port: the server port to connect to + :param str username: + the username to authenticate as (defaults to the current local + username) + :param str password: + Used for password authentication; is also used for private key + decryption if ``passphrase`` is not given. + :param str passphrase: + Used for decrypting private keys. + :param .PKey pkey: an optional private key to use for authentication + :param str key_filename: + the filename, or list of filenames, of optional private key(s) + and/or certs to try for authentication + :param float timeout: + an optional timeout (in seconds) for the TCP connect + :param bool allow_agent: + set to False to disable connecting to the SSH agent + :param bool look_for_keys: + set to False to disable searching for discoverable private key + files in ``~/.ssh/`` + :param bool compress: set to True to turn on compression + :param socket sock: + an open socket or socket-like object (such as a `.Channel`) to use + for communication to the target host + :param bool gss_auth: + ``True`` if you want to use GSS-API authentication + :param bool gss_kex: + Perform GSS-API Key Exchange and user authentication + :param bool gss_deleg_creds: Delegate GSS-API client credentials or not + :param str gss_host: + The targets name in the kerberos database. default: hostname + :param bool gss_trust_dns: + Indicates whether or not the DNS is trusted to securely + canonicalize the name of the host being connected to (default + ``True``). + :param float banner_timeout: an optional timeout (in seconds) to wait + for the SSH banner to be presented. + :param float auth_timeout: an optional timeout (in seconds) to wait for + an authentication response. + :param float channel_timeout: an optional timeout (in seconds) to wait + for a channel open response. + :param dict disabled_algorithms: + an optional dict passed directly to `.Transport` and its keyword + argument of the same name. + :param transport_factory: + an optional callable which is handed a subset of the constructor + arguments (primarily those related to the socket, GSS + functionality, and algorithm selection) and generates a + `.Transport` instance to be used by this client. Defaults to + `.Transport.__init__`. + + :raises BadHostKeyException: + if the server's host key could not be verified. + :raises AuthenticationException: if authentication failed. + :raises socket.error: + if a socket error (other than connection-refused or + host-unreachable) occurred while connecting. + :raises NoValidConnectionsError: + if all valid connection targets for the requested hostname (eg IPv4 + and IPv6) yielded connection-refused or host-unreachable socket + errors. + :raises SSHException: + if there was any other error connecting or establishing an SSH + session. + + .. versionchanged:: 1.15 + Added the ``banner_timeout``, ``gss_auth``, ``gss_kex``, + ``gss_deleg_creds`` and ``gss_host`` arguments. + .. versionchanged:: 2.3 + Added the ``gss_trust_dns`` argument. + .. versionchanged:: 2.4 + Added the ``passphrase`` argument. + .. versionchanged:: 2.6 + Added the ``disabled_algorithms`` argument. + .. versionchanged:: 2.12 + Added the ``transport_factory`` argument. + """ + if not sock: + errors = {} + # Try multiple possible address families (e.g. IPv4 vs IPv6) + to_try = list(self._families_and_addresses(hostname, port)) + for af, addr in to_try: + try: + sock = socket.socket(af, socket.SOCK_STREAM) + if timeout is not None: + try: + sock.settimeout(timeout) + except: + pass + sock.connect(addr) + # Break out of the loop on success + break + except socket.error as e: + # As mentioned in socket docs it is better + # to close sockets explicitly + if sock: + sock.close() + # Raise anything that isn't a straight up connection error + # (such as a resolution error) + if e.errno not in (ECONNREFUSED, EHOSTUNREACH): + raise + # Capture anything else so we know how the run looks once + # iteration is complete. Retain info about which attempt + # this was. + errors[addr] = e + + # Make sure we explode usefully if no address family attempts + # succeeded. We've no way of knowing which error is the "right" + # one, so we construct a hybrid exception containing all the real + # ones, of a subclass that client code should still be watching for + # (socket.error) + if len(errors) == len(to_try): + raise NoValidConnectionsError(errors) + + if transport_factory is None: + transport_factory = Transport + t = self._transport = transport_factory( + sock, + gss_kex=gss_kex, + gss_deleg_creds=gss_deleg_creds, + disabled_algorithms=disabled_algorithms, + ) + t.use_compression(compress=compress) + t.set_gss_host( + # t.hostname may be None, but GSS-API requires a target name. + # Therefore use hostname as fallback. + gss_host=gss_host or hostname, + trust_dns=gss_trust_dns, + gssapi_requested=gss_auth or gss_kex, + ) + if self._log_channel is not None: + t.set_log_channel(self._log_channel) + if banner_timeout is not None: + t.banner_timeout = banner_timeout + if auth_timeout is not None: + t.auth_timeout = auth_timeout + if channel_timeout is not None: + t.channel_timeout = channel_timeout + + if port == SSH_PORT: + server_hostkey_name = hostname + else: + server_hostkey_name = "[{}]:{}".format(hostname, port) + our_server_keys = None + + our_server_keys = self._system_host_keys.get(server_hostkey_name) + if our_server_keys is None: + our_server_keys = self._host_keys.get(server_hostkey_name) + if our_server_keys is not None: + keytype = our_server_keys.keys()[0] + sec_opts = t.get_security_options() + other_types = [x for x in sec_opts.key_types if x != keytype] + sec_opts.key_types = [keytype] + other_types + + t.start_client(timeout=timeout) + + # If GSS-API Key Exchange is performed we are not required to check the + # host key, because the host is authenticated via GSS-API / SSPI as + # well as our client. + if not self._transport.gss_kex_used: + server_key = t.get_remote_server_key() + if our_server_keys is None: + # will raise exception if the key is rejected + self._policy.missing_host_key( + self, server_hostkey_name, server_key + ) + else: + our_key = our_server_keys.get(server_key.get_name()) + if our_key != server_key: + if our_key is None: + our_key = list(our_server_keys.values())[0] + raise BadHostKeyException(hostname, server_key, our_key) + + if username is None: + username = getpass.getuser() + + if key_filename is None: + key_filenames = [] + elif isinstance(key_filename, str): + key_filenames = [key_filename] + else: + key_filenames = key_filename + + self._auth( + username, + password, + pkey, + key_filenames, + allow_agent, + look_for_keys, + gss_auth, + gss_kex, + gss_deleg_creds, + t.gss_host, + passphrase, + ) + + def close(self): + """ + Close this SSHClient and its underlying `.Transport`. + + This should be called anytime you are done using the client object. + + .. warning:: + Paramiko registers garbage collection hooks that will try to + automatically close connections for you, but this is not presently + reliable. Failure to explicitly close your client after use may + lead to end-of-process hangs! + """ + if self._transport is None: + return + self._transport.close() + self._transport = None + + if self._agent is not None: + self._agent.close() + self._agent = None + + def exec_command( + self, + command, + bufsize=-1, + timeout=None, + get_pty=False, + environment=None, + ): + """ + Execute a command on the SSH server. A new `.Channel` is opened and + the requested command is executed. The command's input and output + streams are returned as Python ``file``-like objects representing + stdin, stdout, and stderr. + + :param str command: the command to execute + :param int bufsize: + interpreted the same way as by the built-in ``file()`` function in + Python + :param int timeout: + set command's channel timeout. See `.Channel.settimeout` + :param bool get_pty: + Request a pseudo-terminal from the server (default ``False``). + See `.Channel.get_pty` + :param dict environment: + a dict of shell environment variables, to be merged into the + default environment that the remote command executes within. + + .. warning:: + Servers may silently reject some environment variables; see the + warning in `.Channel.set_environment_variable` for details. + + :return: + the stdin, stdout, and stderr of the executing command, as a + 3-tuple + + :raises: `.SSHException` -- if the server fails to execute the command + + .. versionchanged:: 1.10 + Added the ``get_pty`` kwarg. + """ + chan = self._transport.open_session(timeout=timeout) + if get_pty: + chan.get_pty() + chan.settimeout(timeout) + if environment: + chan.update_environment(environment) + chan.exec_command(command) + stdin = chan.makefile_stdin("wb", bufsize) + stdout = chan.makefile("r", bufsize) + stderr = chan.makefile_stderr("r", bufsize) + return stdin, stdout, stderr + + def invoke_shell( + self, + term="vt100", + width=80, + height=24, + width_pixels=0, + height_pixels=0, + environment=None, + ): + """ + Start an interactive shell session on the SSH server. A new `.Channel` + is opened and connected to a pseudo-terminal using the requested + terminal type and size. + + :param str term: + the terminal type to emulate (for example, ``"vt100"``) + :param int width: the width (in characters) of the terminal window + :param int height: the height (in characters) of the terminal window + :param int width_pixels: the width (in pixels) of the terminal window + :param int height_pixels: the height (in pixels) of the terminal window + :param dict environment: the command's environment + :return: a new `.Channel` connected to the remote shell + + :raises: `.SSHException` -- if the server fails to invoke a shell + """ + chan = self._transport.open_session() + chan.get_pty(term, width, height, width_pixels, height_pixels) + chan.invoke_shell() + return chan + + def open_sftp(self): + """ + Open an SFTP session on the SSH server. + + :return: a new `.SFTPClient` session object + """ + return self._transport.open_sftp_client() + + def get_transport(self): + """ + Return the underlying `.Transport` object for this SSH connection. + This can be used to perform lower-level tasks, like opening specific + kinds of channels. + + :return: the `.Transport` for this connection + """ + return self._transport + + def _key_from_filepath(self, filename, klass, password): + """ + Attempt to derive a `.PKey` from given string path ``filename``: + + - If ``filename`` appears to be a cert, the matching private key is + loaded. + - Otherwise, the filename is assumed to be a private key, and the + matching public cert will be loaded if it exists. + """ + cert_suffix = "-cert.pub" + # Assume privkey, not cert, by default + if filename.endswith(cert_suffix): + key_path = filename[: -len(cert_suffix)] + cert_path = filename + else: + key_path = filename + cert_path = filename + cert_suffix + # Blindly try the key path; if no private key, nothing will work. + key = klass.from_private_key_file(key_path, password) + # TODO: change this to 'Loading' instead of 'Trying' sometime; probably + # when #387 is released, since this is a critical log message users are + # likely testing/filtering for (bah.) + msg = "Trying discovered key {} in {}".format( + hexlify(key.get_fingerprint()), key_path + ) + self._log(DEBUG, msg) + # Attempt to load cert if it exists. + if os.path.isfile(cert_path): + key.load_certificate(cert_path) + self._log(DEBUG, "Adding public certificate {}".format(cert_path)) + return key + + def _auth( + self, + username, + password, + pkey, + key_filenames, + allow_agent, + look_for_keys, + gss_auth, + gss_kex, + gss_deleg_creds, + gss_host, + passphrase, + ): + """ + Try, in order: + + - The key(s) passed in, if one was passed in. + - Any key we can find through an SSH agent (if allowed). + - Any "id_rsa", "id_dsa" or "id_ecdsa" key discoverable in ~/.ssh/ + (if allowed). + - Plain username/password auth, if a password was given. + + (The password might be needed to unlock a private key [if 'passphrase' + isn't also given], or for two-factor authentication [for which it is + required].) + """ + saved_exception = None + two_factor = False + allowed_types = set() + two_factor_types = {"keyboard-interactive", "password"} + if passphrase is None and password is not None: + passphrase = password + + # If GSS-API support and GSS-PI Key Exchange was performed, we attempt + # authentication with gssapi-keyex. + if gss_kex and self._transport.gss_kex_used: + try: + self._transport.auth_gssapi_keyex(username) + return + except Exception as e: + saved_exception = e + + # Try GSS-API authentication (gssapi-with-mic) only if GSS-API Key + # Exchange is not performed, because if we use GSS-API for the key + # exchange, there is already a fully established GSS-API context, so + # why should we do that again? + if gss_auth: + try: + return self._transport.auth_gssapi_with_mic( + username, gss_host, gss_deleg_creds + ) + except Exception as e: + saved_exception = e + + if pkey is not None: + try: + self._log( + DEBUG, + "Trying SSH key {}".format( + hexlify(pkey.get_fingerprint()) + ), + ) + allowed_types = set( + self._transport.auth_publickey(username, pkey) + ) + two_factor = allowed_types & two_factor_types + if not two_factor: + return + except SSHException as e: + saved_exception = e + + if not two_factor: + for key_filename in key_filenames: + for pkey_class in (RSAKey, DSSKey, ECDSAKey, Ed25519Key): + try: + key = self._key_from_filepath( + key_filename, pkey_class, passphrase + ) + allowed_types = set( + self._transport.auth_publickey(username, key) + ) + two_factor = allowed_types & two_factor_types + if not two_factor: + return + break + except SSHException as e: + saved_exception = e + + if not two_factor and allow_agent: + if self._agent is None: + self._agent = Agent() + + for key in self._agent.get_keys(): + try: + id_ = hexlify(key.get_fingerprint()) + self._log(DEBUG, "Trying SSH agent key {}".format(id_)) + # for 2-factor auth a successfully auth'd key password + # will return an allowed 2fac auth method + allowed_types = set( + self._transport.auth_publickey(username, key) + ) + two_factor = allowed_types & two_factor_types + if not two_factor: + return + break + except SSHException as e: + saved_exception = e + + if not two_factor: + keyfiles = [] + + for keytype, name in [ + (RSAKey, "rsa"), + (DSSKey, "dsa"), + (ECDSAKey, "ecdsa"), + (Ed25519Key, "ed25519"), + ]: + # ~/ssh/ is for windows + for directory in [".ssh", "ssh"]: + full_path = os.path.expanduser( + "~/{}/id_{}".format(directory, name) + ) + if os.path.isfile(full_path): + # TODO: only do this append if below did not run + keyfiles.append((keytype, full_path)) + if os.path.isfile(full_path + "-cert.pub"): + keyfiles.append((keytype, full_path + "-cert.pub")) + + if not look_for_keys: + keyfiles = [] + + for pkey_class, filename in keyfiles: + try: + key = self._key_from_filepath( + filename, pkey_class, passphrase + ) + # for 2-factor auth a successfully auth'd key will result + # in ['password'] + allowed_types = set( + self._transport.auth_publickey(username, key) + ) + two_factor = allowed_types & two_factor_types + if not two_factor: + return + break + except (SSHException, IOError) as e: + saved_exception = e + + if password is not None: + try: + self._transport.auth_password(username, password) + return + except SSHException as e: + saved_exception = e + elif two_factor: + try: + self._transport.auth_interactive_dumb(username) + return + except SSHException as e: + saved_exception = e + + # if we got an auth-failed exception earlier, re-raise it + if saved_exception is not None: + raise saved_exception + raise SSHException("No authentication methods available") + + def _log(self, level, msg): + self._transport._log(level, msg) + + +class MissingHostKeyPolicy: + """ + Interface for defining the policy that `.SSHClient` should use when the + SSH server's hostname is not in either the system host keys or the + application's keys. Pre-made classes implement policies for automatically + adding the key to the application's `.HostKeys` object (`.AutoAddPolicy`), + and for automatically rejecting the key (`.RejectPolicy`). + + This function may be used to ask the user to verify the key, for example. + """ + + def missing_host_key(self, client, hostname, key): + """ + Called when an `.SSHClient` receives a server key for a server that + isn't in either the system or local `.HostKeys` object. To accept + the key, simply return. To reject, raised an exception (which will + be passed to the calling application). + """ + pass + + +class AutoAddPolicy(MissingHostKeyPolicy): + """ + Policy for automatically adding the hostname and new host key to the + local `.HostKeys` object, and saving it. This is used by `.SSHClient`. + """ + + def missing_host_key(self, client, hostname, key): + client._host_keys.add(hostname, key.get_name(), key) + if client._host_keys_filename is not None: + client.save_host_keys(client._host_keys_filename) + client._log( + DEBUG, + "Adding {} host key for {}: {}".format( + key.get_name(), hostname, hexlify(key.get_fingerprint()) + ), + ) + + +class RejectPolicy(MissingHostKeyPolicy): + """ + Policy for automatically rejecting the unknown hostname & key. This is + used by `.SSHClient`. + """ + + def missing_host_key(self, client, hostname, key): + client._log( + DEBUG, + "Rejecting {} host key for {}: {}".format( + key.get_name(), hostname, hexlify(key.get_fingerprint()) + ), + ) + raise SSHException( + "Server {!r} not found in known_hosts".format(hostname) + ) + + +class WarningPolicy(MissingHostKeyPolicy): + """ + Policy for logging a Python-style warning for an unknown host key, but + accepting it. This is used by `.SSHClient`. + """ + + def missing_host_key(self, client, hostname, key): + warnings.warn( + "Unknown {} host key for {}: {}".format( + key.get_name(), hostname, hexlify(key.get_fingerprint()) + ) + ) diff --git a/paramiko/common.py b/paramiko/common.py new file mode 100644 index 0000000..a9a4e7a --- /dev/null +++ b/paramiko/common.py @@ -0,0 +1,248 @@ +# Copyright (C) 2003-2007 Robey Pointer <robeypointer@gmail.com> +# +# This file is part of paramiko. +# +# Paramiko is free software; you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free +# Software Foundation; either version 2.1 of the License, or (at your option) +# any later version. +# +# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Paramiko; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +""" +Common constants and global variables. +""" +import logging +import struct + +# +# Formerly of py3compat.py. May be fully delete'able with a deeper look? +# + + +def byte_chr(c): + print("byte_chr") + print(c==MSG_SERVICE_REQUEST) + print(c) + assert isinstance(c, int) + return struct.pack("B", c) + + +def byte_mask(c, mask): + assert isinstance(c, int) + return struct.pack("B", c & mask) + + +def byte_ord(c): + # In case we're handed a string instead of an int. + if not isinstance(c, int): + c = ord(c) + return c + + +( + MSG_DISCONNECT, + MSG_IGNORE, + MSG_UNIMPLEMENTED, + MSG_DEBUG, + MSG_SERVICE_REQUEST, + MSG_SERVICE_ACCEPT, + MSG_EXT_INFO, +) = range(1, 8) +(MSG_KEXINIT, MSG_NEWKEYS) = range(20, 22) +( + MSG_USERAUTH_REQUEST, + MSG_USERAUTH_FAILURE, + MSG_USERAUTH_SUCCESS, + MSG_USERAUTH_BANNER, +) = range(50, 54) +MSG_USERAUTH_PK_OK = 60 +(MSG_USERAUTH_INFO_REQUEST, MSG_USERAUTH_INFO_RESPONSE) = range(60, 62) +(MSG_USERAUTH_GSSAPI_RESPONSE, MSG_USERAUTH_GSSAPI_TOKEN) = range(60, 62) +( + MSG_USERAUTH_GSSAPI_EXCHANGE_COMPLETE, + MSG_USERAUTH_GSSAPI_ERROR, + MSG_USERAUTH_GSSAPI_ERRTOK, + MSG_USERAUTH_GSSAPI_MIC, +) = range(63, 67) +HIGHEST_USERAUTH_MESSAGE_ID = 79 +(MSG_GLOBAL_REQUEST, MSG_REQUEST_SUCCESS, MSG_REQUEST_FAILURE) = range(80, 83) +( + MSG_CHANNEL_OPEN, + MSG_CHANNEL_OPEN_SUCCESS, + MSG_CHANNEL_OPEN_FAILURE, + MSG_CHANNEL_WINDOW_ADJUST, + MSG_CHANNEL_DATA, + MSG_CHANNEL_EXTENDED_DATA, + MSG_CHANNEL_EOF, + MSG_CHANNEL_CLOSE, + MSG_CHANNEL_REQUEST, + MSG_CHANNEL_SUCCESS, + MSG_CHANNEL_FAILURE, +) = range(90, 101) + +cMSG_DISCONNECT = byte_chr(MSG_DISCONNECT) +cMSG_IGNORE = byte_chr(MSG_IGNORE) +cMSG_UNIMPLEMENTED = byte_chr(MSG_UNIMPLEMENTED) +cMSG_DEBUG = byte_chr(MSG_DEBUG) +cMSG_SERVICE_REQUEST = byte_chr(MSG_SERVICE_REQUEST) +cMSG_SERVICE_ACCEPT = byte_chr(MSG_SERVICE_ACCEPT) +cMSG_EXT_INFO = byte_chr(MSG_EXT_INFO) +cMSG_KEXINIT = byte_chr(MSG_KEXINIT) +cMSG_NEWKEYS = byte_chr(MSG_NEWKEYS) +cMSG_USERAUTH_REQUEST = byte_chr(MSG_USERAUTH_REQUEST) +cMSG_USERAUTH_FAILURE = byte_chr(MSG_USERAUTH_FAILURE) +cMSG_USERAUTH_SUCCESS = byte_chr(MSG_USERAUTH_SUCCESS) +cMSG_USERAUTH_BANNER = byte_chr(MSG_USERAUTH_BANNER) +cMSG_USERAUTH_PK_OK = byte_chr(MSG_USERAUTH_PK_OK) +cMSG_USERAUTH_INFO_REQUEST = byte_chr(MSG_USERAUTH_INFO_REQUEST) +cMSG_USERAUTH_INFO_RESPONSE = byte_chr(MSG_USERAUTH_INFO_RESPONSE) +cMSG_USERAUTH_GSSAPI_RESPONSE = byte_chr(MSG_USERAUTH_GSSAPI_RESPONSE) +cMSG_USERAUTH_GSSAPI_TOKEN = byte_chr(MSG_USERAUTH_GSSAPI_TOKEN) +cMSG_USERAUTH_GSSAPI_EXCHANGE_COMPLETE = byte_chr( + MSG_USERAUTH_GSSAPI_EXCHANGE_COMPLETE +) +cMSG_USERAUTH_GSSAPI_ERROR = byte_chr(MSG_USERAUTH_GSSAPI_ERROR) +cMSG_USERAUTH_GSSAPI_ERRTOK = byte_chr(MSG_USERAUTH_GSSAPI_ERRTOK) +cMSG_USERAUTH_GSSAPI_MIC = byte_chr(MSG_USERAUTH_GSSAPI_MIC) +cMSG_GLOBAL_REQUEST = byte_chr(MSG_GLOBAL_REQUEST) +cMSG_REQUEST_SUCCESS = byte_chr(MSG_REQUEST_SUCCESS) +cMSG_REQUEST_FAILURE = byte_chr(MSG_REQUEST_FAILURE) +cMSG_CHANNEL_OPEN = byte_chr(MSG_CHANNEL_OPEN) +cMSG_CHANNEL_OPEN_SUCCESS = byte_chr(MSG_CHANNEL_OPEN_SUCCESS) +cMSG_CHANNEL_OPEN_FAILURE = byte_chr(MSG_CHANNEL_OPEN_FAILURE) +cMSG_CHANNEL_WINDOW_ADJUST = byte_chr(MSG_CHANNEL_WINDOW_ADJUST) +cMSG_CHANNEL_DATA = byte_chr(MSG_CHANNEL_DATA) +cMSG_CHANNEL_EXTENDED_DATA = byte_chr(MSG_CHANNEL_EXTENDED_DATA) +cMSG_CHANNEL_EOF = byte_chr(MSG_CHANNEL_EOF) +cMSG_CHANNEL_CLOSE = byte_chr(MSG_CHANNEL_CLOSE) +cMSG_CHANNEL_REQUEST = byte_chr(MSG_CHANNEL_REQUEST) +cMSG_CHANNEL_SUCCESS = byte_chr(MSG_CHANNEL_SUCCESS) +cMSG_CHANNEL_FAILURE = byte_chr(MSG_CHANNEL_FAILURE) + +# for debugging: +MSG_NAMES = { + MSG_DISCONNECT: "disconnect", + MSG_IGNORE: "ignore", + MSG_UNIMPLEMENTED: "unimplemented", + MSG_DEBUG: "debug", + MSG_SERVICE_REQUEST: "service-request", + MSG_SERVICE_ACCEPT: "service-accept", + MSG_KEXINIT: "kexinit", + MSG_EXT_INFO: "ext-info", + MSG_NEWKEYS: "newkeys", + 30: "kex30", + 31: "kex31", + 32: "kex32", + 33: "kex33", + 34: "kex34", + 40: "kex40", + 41: "kex41", + MSG_USERAUTH_REQUEST: "userauth-request", + MSG_USERAUTH_FAILURE: "userauth-failure", + MSG_USERAUTH_SUCCESS: "userauth-success", + MSG_USERAUTH_BANNER: "userauth--banner", + MSG_USERAUTH_PK_OK: "userauth-60(pk-ok/info-request)", + MSG_USERAUTH_INFO_RESPONSE: "userauth-info-response", + MSG_GLOBAL_REQUEST: "global-request", + MSG_REQUEST_SUCCESS: "request-success", + MSG_REQUEST_FAILURE: "request-failure", + MSG_CHANNEL_OPEN: "channel-open", + MSG_CHANNEL_OPEN_SUCCESS: "channel-open-success", + MSG_CHANNEL_OPEN_FAILURE: "channel-open-failure", + MSG_CHANNEL_WINDOW_ADJUST: "channel-window-adjust", + MSG_CHANNEL_DATA: "channel-data", + MSG_CHANNEL_EXTENDED_DATA: "channel-extended-data", + MSG_CHANNEL_EOF: "channel-eof", + MSG_CHANNEL_CLOSE: "channel-close", + MSG_CHANNEL_REQUEST: "channel-request", + MSG_CHANNEL_SUCCESS: "channel-success", + MSG_CHANNEL_FAILURE: "channel-failure", + MSG_USERAUTH_GSSAPI_RESPONSE: "userauth-gssapi-response", + MSG_USERAUTH_GSSAPI_TOKEN: "userauth-gssapi-token", + MSG_USERAUTH_GSSAPI_EXCHANGE_COMPLETE: "userauth-gssapi-exchange-complete", + MSG_USERAUTH_GSSAPI_ERROR: "userauth-gssapi-error", + MSG_USERAUTH_GSSAPI_ERRTOK: "userauth-gssapi-error-token", + MSG_USERAUTH_GSSAPI_MIC: "userauth-gssapi-mic", +} + + +# authentication request return codes: +AUTH_SUCCESSFUL, AUTH_PARTIALLY_SUCCESSFUL, AUTH_FAILED = range(3) + + +# channel request failed reasons: +( + OPEN_SUCCEEDED, + OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED, + OPEN_FAILED_CONNECT_FAILED, + OPEN_FAILED_UNKNOWN_CHANNEL_TYPE, + OPEN_FAILED_RESOURCE_SHORTAGE, +) = range(0, 5) + + +CONNECTION_FAILED_CODE = { + 1: "Administratively prohibited", + 2: "Connect failed", + 3: "Unknown channel type", + 4: "Resource shortage", +} + + +( + DISCONNECT_SERVICE_NOT_AVAILABLE, + DISCONNECT_AUTH_CANCELLED_BY_USER, + DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE, +) = (7, 13, 14) + +zero_byte = byte_chr(0) +one_byte = byte_chr(1) +four_byte = byte_chr(4) +max_byte = byte_chr(0xFF) +cr_byte = byte_chr(13) +linefeed_byte = byte_chr(10) +crlf = cr_byte + linefeed_byte +cr_byte_value = 13 +linefeed_byte_value = 10 + + +xffffffff = 0xFFFFFFFF +x80000000 = 0x80000000 +o666 = 438 +o660 = 432 +o644 = 420 +o600 = 384 +o777 = 511 +o700 = 448 +o70 = 56 + +DEBUG = logging.DEBUG +INFO = logging.INFO +WARNING = logging.WARNING +ERROR = logging.ERROR +CRITICAL = logging.CRITICAL + +# Common IO/select/etc sleep period, in seconds +io_sleep = 0.01 + +DEFAULT_WINDOW_SIZE = 64 * 2**15 +DEFAULT_MAX_PACKET_SIZE = 2**15 + +# lower bound on the max packet size we'll accept from the remote host +# Minimum packet size is 32768 bytes according to +# http://www.ietf.org/rfc/rfc4254.txt +MIN_WINDOW_SIZE = 2**15 + +# However, according to http://www.ietf.org/rfc/rfc4253.txt it is perfectly +# legal to accept a size much smaller, as OpenSSH client does as size 16384. +MIN_PACKET_SIZE = 2**12 + +# Max windows size according to http://www.ietf.org/rfc/rfc4254.txt +MAX_WINDOW_SIZE = 2**32 - 1 diff --git a/paramiko/compress.py b/paramiko/compress.py new file mode 100644 index 0000000..18ff484 --- /dev/null +++ b/paramiko/compress.py @@ -0,0 +1,40 @@ +# Copyright (C) 2003-2007 Robey Pointer <robeypointer@gmail.com> +# +# This file is part of paramiko. +# +# Paramiko is free software; you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free +# Software Foundation; either version 2.1 of the License, or (at your option) +# any later version. +# +# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Paramiko; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +""" +Compression implementations for a Transport. +""" + +import zlib + + +class ZlibCompressor: + def __init__(self): + # Use the default level of zlib compression + self.z = zlib.compressobj() + + def __call__(self, data): + return self.z.compress(data) + self.z.flush(zlib.Z_FULL_FLUSH) + + +class ZlibDecompressor: + def __init__(self): + self.z = zlib.decompressobj() + + def __call__(self, data): + return self.z.decompress(data) diff --git a/paramiko/config.py b/paramiko/config.py new file mode 100644 index 0000000..48bcb10 --- /dev/null +++ b/paramiko/config.py @@ -0,0 +1,679 @@ +# Copyright (C) 2006-2007 Robey Pointer <robeypointer@gmail.com> +# Copyright (C) 2012 Olle Lundberg <geek@nerd.sh> +# +# This file is part of paramiko. +# +# Paramiko is free software; you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free +# Software Foundation; either version 2.1 of the License, or (at your option) +# any later version. +# +# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Paramiko; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +""" +Configuration file (aka ``ssh_config``) support. +""" + +import fnmatch +import getpass +import os +import re +import shlex +import socket +from hashlib import sha1 +from io import StringIO +from functools import partial + +invoke, invoke_import_error = None, None +try: + import invoke +except ImportError as e: + invoke_import_error = e + +from .ssh_exception import CouldNotCanonicalize, ConfigParseError + + +SSH_PORT = 22 + + +class SSHConfig: + """ + Representation of config information as stored in the format used by + OpenSSH. Queries can be made via `lookup`. The format is described in + OpenSSH's ``ssh_config`` man page. This class is provided primarily as a + convenience to posix users (since the OpenSSH format is a de-facto + standard on posix) but should work fine on Windows too. + + .. versionadded:: 1.6 + """ + + SETTINGS_REGEX = re.compile(r"(\w+)(?:\s*=\s*|\s+)(.+)") + + # TODO: do a full scan of ssh.c & friends to make sure we're fully + # compatible across the board, e.g. OpenSSH 8.1 added %n to ProxyCommand. + TOKENS_BY_CONFIG_KEY = { + "controlpath": ["%C", "%h", "%l", "%L", "%n", "%p", "%r", "%u"], + "hostname": ["%h"], + "identityfile": ["%C", "~", "%d", "%h", "%l", "%u", "%r"], + "proxycommand": ["~", "%h", "%p", "%r"], + "proxyjump": ["%h", "%p", "%r"], + # Doesn't seem worth making this 'special' for now, it will fit well + # enough (no actual match-exec config key to be confused with). + "match-exec": ["%C", "%d", "%h", "%L", "%l", "%n", "%p", "%r", "%u"], + } + + def __init__(self): + """ + Create a new OpenSSH config object. + + Note: the newer alternate constructors `from_path`, `from_file` and + `from_text` are simpler to use, as they parse on instantiation. For + example, instead of:: + + config = SSHConfig() + config.parse(open("some-path.config") + + you could:: + + config = SSHConfig.from_file(open("some-path.config")) + # Or more directly: + config = SSHConfig.from_path("some-path.config") + # Or if you have arbitrary ssh_config text from some other source: + config = SSHConfig.from_text("Host foo\\n\\tUser bar") + """ + self._config = [] + + @classmethod + def from_text(cls, text): + """ + Create a new, parsed `SSHConfig` from ``text`` string. + + .. versionadded:: 2.7 + """ + return cls.from_file(StringIO(text)) + + @classmethod + def from_path(cls, path): + """ + Create a new, parsed `SSHConfig` from the file found at ``path``. + + .. versionadded:: 2.7 + """ + with open(path) as flo: + return cls.from_file(flo) + + @classmethod + def from_file(cls, flo): + """ + Create a new, parsed `SSHConfig` from file-like object ``flo``. + + .. versionadded:: 2.7 + """ + obj = cls() + obj.parse(flo) + return obj + + def parse(self, file_obj): + """ + Read an OpenSSH config from the given file object. + + :param file_obj: a file-like object to read the config file from + """ + # Start out w/ implicit/anonymous global host-like block to hold + # anything not contained by an explicit one. + context = {"host": ["*"], "config": {}} + for line in file_obj: + # Strip any leading or trailing whitespace from the line. + # Refer to https://github.com/paramiko/paramiko/issues/499 + line = line.strip() + # Skip blanks, comments + if not line or line.startswith("#"): + continue + + # Parse line into key, value + match = re.match(self.SETTINGS_REGEX, line) + if not match: + raise ConfigParseError("Unparsable line {}".format(line)) + key = match.group(1).lower() + value = match.group(2) + + # Host keyword triggers switch to new block/context + if key in ("host", "match"): + self._config.append(context) + context = {"config": {}} + if key == "host": + # TODO 4.0: make these real objects or at least name this + # "hosts" to acknowledge it's an iterable. (Doing so prior + # to 3.0, despite it being a private API, feels bad - + # surely such an old codebase has folks actually relying on + # these keys.) + context["host"] = self._get_hosts(value) + else: + context["matches"] = self._get_matches(value) + # Special-case for noop ProxyCommands + elif key == "proxycommand" and value.lower() == "none": + # Store 'none' as None - not as a string implying that the + # proxycommand is the literal shell command "none"! + context["config"][key] = None + # All other keywords get stored, directly or via append + else: + if value.startswith('"') and value.endswith('"'): + value = value[1:-1] + + # identityfile, localforward, remoteforward keys are special + # cases, since they are allowed to be specified multiple times + # and they should be tried in order of specification. + if key in ["identityfile", "localforward", "remoteforward"]: + if key in context["config"]: + context["config"][key].append(value) + else: + context["config"][key] = [value] + elif key not in context["config"]: + context["config"][key] = value + # Store last 'open' block and we're done + self._config.append(context) + + def lookup(self, hostname): + """ + Return a dict (`SSHConfigDict`) of config options for a given hostname. + + The host-matching rules of OpenSSH's ``ssh_config`` man page are used: + For each parameter, the first obtained value will be used. The + configuration files contain sections separated by ``Host`` and/or + ``Match`` specifications, and that section is only applied for hosts + which match the given patterns or keywords + + Since the first obtained value for each parameter is used, more host- + specific declarations should be given near the beginning of the file, + and general defaults at the end. + + The keys in the returned dict are all normalized to lowercase (look for + ``"port"``, not ``"Port"``. The values are processed according to the + rules for substitution variable expansion in ``ssh_config``. + + Finally, please see the docs for `SSHConfigDict` for deeper info on + features such as optional type conversion methods, e.g.:: + + conf = my_config.lookup('myhost') + assert conf['passwordauthentication'] == 'yes' + assert conf.as_bool('passwordauthentication') is True + + .. note:: + If there is no explicitly configured ``HostName`` value, it will be + set to the being-looked-up hostname, which is as close as we can + get to OpenSSH's behavior around that particular option. + + :param str hostname: the hostname to lookup + + .. versionchanged:: 2.5 + Returns `SSHConfigDict` objects instead of dict literals. + .. versionchanged:: 2.7 + Added canonicalization support. + .. versionchanged:: 2.7 + Added ``Match`` support. + """ + # First pass + options = self._lookup(hostname=hostname) + # Inject HostName if it was not set (this used to be done incidentally + # during tokenization, for some reason). + if "hostname" not in options: + options["hostname"] = hostname + # Handle canonicalization + canon = options.get("canonicalizehostname", None) in ("yes", "always") + maxdots = int(options.get("canonicalizemaxdots", 1)) + if canon and hostname.count(".") <= maxdots: + # NOTE: OpenSSH manpage does not explicitly state this, but its + # implementation for CanonicalDomains is 'split on any whitespace'. + domains = options["canonicaldomains"].split() + hostname = self.canonicalize(hostname, options, domains) + # Overwrite HostName again here (this is also what OpenSSH does) + options["hostname"] = hostname + options = self._lookup(hostname, options, canonical=True) + return options + + def _lookup(self, hostname, options=None, canonical=False): + # Init + if options is None: + options = SSHConfigDict() + # Iterate all stanzas, applying any that match, in turn (so that things + # like Match can reference currently understood state) + for context in self._config: + if not ( + self._pattern_matches(context.get("host", []), hostname) + or self._does_match( + context.get("matches", []), hostname, canonical, options + ) + ): + continue + for key, value in context["config"].items(): + if key not in options: + # Create a copy of the original value, + # else it will reference the original list + # in self._config and update that value too + # when the extend() is being called. + options[key] = value[:] if value is not None else value + elif key == "identityfile": + options[key].extend( + x for x in value if x not in options[key] + ) + # Expand variables in resulting values (besides 'Match exec' which was + # already handled above) + options = self._expand_variables(options, hostname) + return options + + def canonicalize(self, hostname, options, domains): + """ + Return canonicalized version of ``hostname``. + + :param str hostname: Target hostname. + :param options: An `SSHConfigDict` from a previous lookup pass. + :param domains: List of domains (e.g. ``["paramiko.org"]``). + + :returns: A canonicalized hostname if one was found, else ``None``. + + .. versionadded:: 2.7 + """ + found = False + for domain in domains: + candidate = "{}.{}".format(hostname, domain) + family_specific = _addressfamily_host_lookup(candidate, options) + if family_specific is not None: + # TODO: would we want to dig deeper into other results? e.g. to + # find something that satisfies PermittedCNAMEs when that is + # implemented? + found = family_specific[0] + else: + # TODO: what does ssh use here and is there a reason to use + # that instead of gethostbyname? + try: + found = socket.gethostbyname(candidate) + except socket.gaierror: + pass + if found: + # TODO: follow CNAME (implied by found != candidate?) if + # CanonicalizePermittedCNAMEs allows it + return candidate + # If we got here, it means canonicalization failed. + # When CanonicalizeFallbackLocal is undefined or 'yes', we just spit + # back the original hostname. + if options.get("canonicalizefallbacklocal", "yes") == "yes": + return hostname + # And here, we failed AND fallback was set to a non-yes value, so we + # need to get mad. + raise CouldNotCanonicalize(hostname) + + def get_hostnames(self): + """ + Return the set of literal hostnames defined in the SSH config (both + explicit hostnames and wildcard entries). + """ + hosts = set() + for entry in self._config: + hosts.update(entry["host"]) + return hosts + + def _pattern_matches(self, patterns, target): + # Convenience auto-splitter if not already a list + if hasattr(patterns, "split"): + patterns = patterns.split(",") + match = False + for pattern in patterns: + # Short-circuit if target matches a negated pattern + if pattern.startswith("!") and fnmatch.fnmatch( + target, pattern[1:] + ): + return False + # Flag a match, but continue (in case of later negation) if regular + # match occurs + elif fnmatch.fnmatch(target, pattern): + match = True + return match + + def _does_match(self, match_list, target_hostname, canonical, options): + matched = [] + candidates = match_list[:] + local_username = getpass.getuser() + while candidates: + candidate = candidates.pop(0) + passed = None + # Obtain latest host/user value every loop, so later Match may + # reference values assigned within a prior Match. + configured_host = options.get("hostname", None) + configured_user = options.get("user", None) + type_, param = candidate["type"], candidate["param"] + # Canonical is a hard pass/fail based on whether this is a + # canonicalized re-lookup. + if type_ == "canonical": + if self._should_fail(canonical, candidate): + return False + # The parse step ensures we only see this by itself or after + # canonical, so it's also an easy hard pass. (No negation here as + # that would be uh, pretty weird?) + elif type_ == "all": + return True + # From here, we are testing various non-hard criteria, + # short-circuiting only on fail + elif type_ == "host": + hostval = configured_host or target_hostname + passed = self._pattern_matches(param, hostval) + elif type_ == "originalhost": + passed = self._pattern_matches(param, target_hostname) + elif type_ == "user": + user = configured_user or local_username + passed = self._pattern_matches(param, user) + elif type_ == "localuser": + passed = self._pattern_matches(param, local_username) + elif type_ == "exec": + exec_cmd = self._tokenize( + options, target_hostname, "match-exec", param + ) + # This is the laziest spot in which we can get mad about an + # inability to import Invoke. + if invoke is None: + raise invoke_import_error + # Like OpenSSH, we 'redirect' stdout but let stderr bubble up + passed = invoke.run(exec_cmd, hide="stdout", warn=True).ok + # Tackle any 'passed, but was negated' results from above + if passed is not None and self._should_fail(passed, candidate): + return False + # Made it all the way here? Everything matched! + matched.append(candidate) + # Did anything match? (To be treated as bool, usually.) + return matched + + def _should_fail(self, would_pass, candidate): + return would_pass if candidate["negate"] else not would_pass + + def _tokenize(self, config, target_hostname, key, value): + """ + Tokenize a string based on current config/hostname data. + + :param config: Current config data. + :param target_hostname: Original target connection hostname. + :param key: Config key being tokenized (used to filter token list). + :param value: Config value being tokenized. + + :returns: The tokenized version of the input ``value`` string. + """ + allowed_tokens = self._allowed_tokens(key) + # Short-circuit if no tokenization possible + if not allowed_tokens: + return value + # Obtain potentially configured hostname, for use with %h. + # Special-case where we are tokenizing the hostname itself, to avoid + # replacing %h with a %h-bearing value, etc. + configured_hostname = target_hostname + if key != "hostname": + configured_hostname = config.get("hostname", configured_hostname) + # Ditto the rest of the source values + if "port" in config: + port = config["port"] + else: + port = SSH_PORT + user = getpass.getuser() + if "user" in config: + remoteuser = config["user"] + else: + remoteuser = user + local_hostname = socket.gethostname().split(".")[0] + local_fqdn = LazyFqdn(config, local_hostname) + homedir = os.path.expanduser("~") + tohash = local_hostname + target_hostname + repr(port) + remoteuser + # The actual tokens! + replacements = { + # TODO: %%??? + "%C": sha1(tohash.encode()).hexdigest(), + "%d": homedir, + "%h": configured_hostname, + # TODO: %i? + "%L": local_hostname, + "%l": local_fqdn, + # also this is pseudo buggy when not in Match exec mode so document + # that. also WHY is that the case?? don't we do all of this late? + "%n": target_hostname, + "%p": port, + "%r": remoteuser, + # TODO: %T? don't believe this is possible however + "%u": user, + "~": homedir, + } + # Do the thing with the stuff + tokenized = value + for find, replace in replacements.items(): + if find not in allowed_tokens: + continue + tokenized = tokenized.replace(find, str(replace)) + # TODO: log? eg that value -> tokenized + return tokenized + + def _allowed_tokens(self, key): + """ + Given config ``key``, return list of token strings to tokenize. + + .. note:: + This feels like it wants to eventually go away, but is used to + preserve as-strict-as-possible compatibility with OpenSSH, which + for whatever reason only applies some tokens to some config keys. + """ + return self.TOKENS_BY_CONFIG_KEY.get(key, []) + + def _expand_variables(self, config, target_hostname): + """ + Return a dict of config options with expanded substitutions + for a given original & current target hostname. + + Please refer to :doc:`/api/config` for details. + + :param dict config: the currently parsed config + :param str hostname: the hostname whose config is being looked up + """ + for k in config: + if config[k] is None: + continue + tokenizer = partial(self._tokenize, config, target_hostname, k) + if isinstance(config[k], list): + for i, value in enumerate(config[k]): + config[k][i] = tokenizer(value) + else: + config[k] = tokenizer(config[k]) + return config + + def _get_hosts(self, host): + """ + Return a list of host_names from host value. + """ + try: + return shlex.split(host) + except ValueError: + raise ConfigParseError("Unparsable host {}".format(host)) + + def _get_matches(self, match): + """ + Parse a specific Match config line into a list-of-dicts for its values. + + Performs some parse-time validation as well. + """ + matches = [] + tokens = shlex.split(match) + while tokens: + match = {"type": None, "param": None, "negate": False} + type_ = tokens.pop(0) + # Handle per-keyword negation + if type_.startswith("!"): + match["negate"] = True + type_ = type_[1:] + match["type"] = type_ + # all/canonical have no params (everything else does) + if type_ in ("all", "canonical"): + matches.append(match) + continue + if not tokens: + raise ConfigParseError( + "Missing parameter to Match '{}' keyword".format(type_) + ) + match["param"] = tokens.pop(0) + matches.append(match) + # Perform some (easier to do now than in the middle) validation that is + # better handled here than at lookup time. + keywords = [x["type"] for x in matches] + if "all" in keywords: + allowable = ("all", "canonical") + ok, bad = ( + list(filter(lambda x: x in allowable, keywords)), + list(filter(lambda x: x not in allowable, keywords)), + ) + err = None + if any(bad): + err = "Match does not allow 'all' mixed with anything but 'canonical'" # noqa + elif "canonical" in ok and ok.index("canonical") > ok.index("all"): + err = "Match does not allow 'all' before 'canonical'" + if err is not None: + raise ConfigParseError(err) + return matches + + +def _addressfamily_host_lookup(hostname, options): + """ + Try looking up ``hostname`` in an IPv4 or IPv6 specific manner. + + This is an odd duck due to needing use in two divergent use cases. It looks + up ``AddressFamily`` in ``options`` and if it is ``inet`` or ``inet6``, + this function uses `socket.getaddrinfo` to perform a family-specific + lookup, returning the result if successful. + + In any other situation -- lookup failure, or ``AddressFamily`` being + unspecified or ``any`` -- ``None`` is returned instead and the caller is + expected to do something situation-appropriate like calling + `socket.gethostbyname`. + + :param str hostname: Hostname to look up. + :param options: `SSHConfigDict` instance w/ parsed options. + :returns: ``getaddrinfo``-style tuples, or ``None``, depending. + """ + address_family = options.get("addressfamily", "any").lower() + if address_family == "any": + return + try: + family = socket.AF_INET6 + if address_family == "inet": + family = socket.AF_INET + return socket.getaddrinfo( + hostname, + None, + family, + socket.SOCK_DGRAM, + socket.IPPROTO_IP, + socket.AI_CANONNAME, + ) + except socket.gaierror: + pass + + +class LazyFqdn: + """ + Returns the host's fqdn on request as string. + """ + + def __init__(self, config, host=None): + self.fqdn = None + self.config = config + self.host = host + + def __str__(self): + if self.fqdn is None: + # + # If the SSH config contains AddressFamily, use that when + # determining the local host's FQDN. Using socket.getfqdn() from + # the standard library is the most general solution, but can + # result in noticeable delays on some platforms when IPv6 is + # misconfigured or not available, as it calls getaddrinfo with no + # address family specified, so both IPv4 and IPv6 are checked. + # + + # Handle specific option + fqdn = None + results = _addressfamily_host_lookup(self.host, self.config) + if results is not None: + for res in results: + af, socktype, proto, canonname, sa = res + if canonname and "." in canonname: + fqdn = canonname + break + # Handle 'any' / unspecified / lookup failure + if fqdn is None: + fqdn = socket.getfqdn() + # Cache + self.fqdn = fqdn + return self.fqdn + + +class SSHConfigDict(dict): + """ + A dictionary wrapper/subclass for per-host configuration structures. + + This class introduces some usage niceties for consumers of `SSHConfig`, + specifically around the issue of variable type conversions: normal value + access yields strings, but there are now methods such as `as_bool` and + `as_int` that yield casted values instead. + + For example, given the following ``ssh_config`` file snippet:: + + Host foo.example.com + PasswordAuthentication no + Compression yes + ServerAliveInterval 60 + + the following code highlights how you can access the raw strings as well as + usefully Python type-casted versions (recalling that keys are all + normalized to lowercase first):: + + my_config = SSHConfig() + my_config.parse(open('~/.ssh/config')) + conf = my_config.lookup('foo.example.com') + + assert conf['passwordauthentication'] == 'no' + assert conf.as_bool('passwordauthentication') is False + assert conf['compression'] == 'yes' + assert conf.as_bool('compression') is True + assert conf['serveraliveinterval'] == '60' + assert conf.as_int('serveraliveinterval') == 60 + + .. versionadded:: 2.5 + """ + + def as_bool(self, key): + """ + Express given key's value as a boolean type. + + Typically, this is used for ``ssh_config``'s pseudo-boolean values + which are either ``"yes"`` or ``"no"``. In such cases, ``"yes"`` yields + ``True`` and any other value becomes ``False``. + + .. note:: + If (for whatever reason) the stored value is already boolean in + nature, it's simply returned. + + .. versionadded:: 2.5 + """ + val = self[key] + if isinstance(val, bool): + return val + return val.lower() == "yes" + + def as_int(self, key): + """ + Express given key's value as an integer, if possible. + + This method will raise ``ValueError`` or similar if the value is not + int-appropriate, same as the builtin `int` type. + + .. versionadded:: 2.5 + """ + return int(self[key]) diff --git a/paramiko/dsskey.py b/paramiko/dsskey.py new file mode 100644 index 0000000..5a0f85e --- /dev/null +++ b/paramiko/dsskey.py @@ -0,0 +1,255 @@ +# Copyright (C) 2003-2007 Robey Pointer <robeypointer@gmail.com> +# +# This file is part of paramiko. +# +# Paramiko is free software; you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free +# Software Foundation; either version 2.1 of the License, or (at your option) +# any later version. +# +# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Paramiko; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +""" +DSS keys. +""" + +from cryptography.exceptions import InvalidSignature +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import hashes, serialization +from cryptography.hazmat.primitives.asymmetric import dsa +from cryptography.hazmat.primitives.asymmetric.utils import ( + decode_dss_signature, + encode_dss_signature, +) + +from paramiko import util +from paramiko.common import zero_byte +from paramiko.ssh_exception import SSHException +from paramiko.message import Message +from paramiko.ber import BER, BERException +from paramiko.pkey import PKey + + +class DSSKey(PKey): + """ + Representation of a DSS key which can be used to sign an verify SSH2 + data. + """ + + def __init__( + self, + msg=None, + data=None, + filename=None, + password=None, + vals=None, + file_obj=None, + ): + self.p = None + self.q = None + self.g = None + self.y = None + self.x = None + self.public_blob = None + if file_obj is not None: + self._from_private_key(file_obj, password) + return + if filename is not None: + self._from_private_key_file(filename, password) + return + if (msg is None) and (data is not None): + msg = Message(data) + if vals is not None: + self.p, self.q, self.g, self.y = vals + else: + self._check_type_and_load_cert( + msg=msg, + key_type="ssh-dss", + cert_type="ssh-dss-cert-v01@openssh.com", + ) + self.p = msg.get_mpint() + self.q = msg.get_mpint() + self.g = msg.get_mpint() + self.y = msg.get_mpint() + self.size = util.bit_length(self.p) + + def asbytes(self): + m = Message() + m.add_string("ssh-dss") + m.add_mpint(self.p) + m.add_mpint(self.q) + m.add_mpint(self.g) + m.add_mpint(self.y) + return m.asbytes() + + def __str__(self): + return self.asbytes() + + @property + def _fields(self): + return (self.get_name(), self.p, self.q, self.g, self.y) + + def get_name(self): + return "ssh-dss" + + def get_bits(self): + return self.size + + def can_sign(self): + return self.x is not None + + def sign_ssh_data(self, data, algorithm=None): + key = dsa.DSAPrivateNumbers( + x=self.x, + public_numbers=dsa.DSAPublicNumbers( + y=self.y, + parameter_numbers=dsa.DSAParameterNumbers( + p=self.p, q=self.q, g=self.g + ), + ), + ).private_key(backend=default_backend()) + sig = key.sign(data, hashes.SHA1()) + r, s = decode_dss_signature(sig) + + m = Message() + m.add_string("ssh-dss") + # apparently, in rare cases, r or s may be shorter than 20 bytes! + rstr = util.deflate_long(r, 0) + sstr = util.deflate_long(s, 0) + if len(rstr) < 20: + rstr = zero_byte * (20 - len(rstr)) + rstr + if len(sstr) < 20: + sstr = zero_byte * (20 - len(sstr)) + sstr + m.add_string(rstr + sstr) + return m + + def verify_ssh_sig(self, data, msg): + if len(msg.asbytes()) == 40: + # spies.com bug: signature has no header + sig = msg.asbytes() + else: + kind = msg.get_text() + if kind != "ssh-dss": + return 0 + sig = msg.get_binary() + + # pull out (r, s) which are NOT encoded as mpints + sigR = util.inflate_long(sig[:20], 1) + sigS = util.inflate_long(sig[20:], 1) + + signature = encode_dss_signature(sigR, sigS) + + key = dsa.DSAPublicNumbers( + y=self.y, + parameter_numbers=dsa.DSAParameterNumbers( + p=self.p, q=self.q, g=self.g + ), + ).public_key(backend=default_backend()) + try: + key.verify(signature, data, hashes.SHA1()) + except InvalidSignature: + return False + else: + return True + + def write_private_key_file(self, filename, password=None): + key = dsa.DSAPrivateNumbers( + x=self.x, + public_numbers=dsa.DSAPublicNumbers( + y=self.y, + parameter_numbers=dsa.DSAParameterNumbers( + p=self.p, q=self.q, g=self.g + ), + ), + ).private_key(backend=default_backend()) + + self._write_private_key_file( + filename, + key, + serialization.PrivateFormat.TraditionalOpenSSL, + password=password, + ) + + def write_private_key(self, file_obj, password=None): + key = dsa.DSAPrivateNumbers( + x=self.x, + public_numbers=dsa.DSAPublicNumbers( + y=self.y, + parameter_numbers=dsa.DSAParameterNumbers( + p=self.p, q=self.q, g=self.g + ), + ), + ).private_key(backend=default_backend()) + + self._write_private_key( + file_obj, + key, + serialization.PrivateFormat.TraditionalOpenSSL, + password=password, + ) + + @staticmethod + def generate(bits=1024, progress_func=None): + """ + Generate a new private DSS key. This factory function can be used to + generate a new host key or authentication key. + + :param int bits: number of bits the generated key should be. + :param progress_func: Unused + :return: new `.DSSKey` private key + """ + numbers = dsa.generate_private_key( + bits, backend=default_backend() + ).private_numbers() + key = DSSKey( + vals=( + numbers.public_numbers.parameter_numbers.p, + numbers.public_numbers.parameter_numbers.q, + numbers.public_numbers.parameter_numbers.g, + numbers.public_numbers.y, + ) + ) + key.x = numbers.x + return key + + # ...internals... + + def _from_private_key_file(self, filename, password): + data = self._read_private_key_file("DSA", filename, password) + self._decode_key(data) + + def _from_private_key(self, file_obj, password): + data = self._read_private_key("DSA", file_obj, password) + self._decode_key(data) + + def _decode_key(self, data): + pkformat, data = data + # private key file contains: + # DSAPrivateKey = { version = 0, p, q, g, y, x } + if pkformat == self._PRIVATE_KEY_FORMAT_ORIGINAL: + try: + keylist = BER(data).decode() + except BERException as e: + raise SSHException("Unable to parse key file: {}".format(e)) + elif pkformat == self._PRIVATE_KEY_FORMAT_OPENSSH: + keylist = self._uint32_cstruct_unpack(data, "iiiii") + keylist = [0] + list(keylist) + else: + self._got_bad_key_format_id(pkformat) + if type(keylist) is not list or len(keylist) < 6 or keylist[0] != 0: + raise SSHException( + "not a valid DSA private key file (bad ber encoding)" + ) + self.p = keylist[1] + self.q = keylist[2] + self.g = keylist[3] + self.y = keylist[4] + self.x = keylist[5] + self.size = util.bit_length(self.p) diff --git a/paramiko/ecdsakey.py b/paramiko/ecdsakey.py new file mode 100644 index 0000000..e227975 --- /dev/null +++ b/paramiko/ecdsakey.py @@ -0,0 +1,333 @@ +# Copyright (C) 2003-2007 Robey Pointer <robeypointer@gmail.com> +# +# This file is part of paramiko. +# +# Paramiko is free software; you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free +# Software Foundation; either version 2.1 of the License, or (at your option) +# any later version. +# +# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Paramiko; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +""" +ECDSA keys +""" + +from cryptography.exceptions import InvalidSignature, UnsupportedAlgorithm +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import hashes, serialization +from cryptography.hazmat.primitives.asymmetric import ec +from cryptography.hazmat.primitives.asymmetric.utils import ( + decode_dss_signature, + encode_dss_signature, +) + +from paramiko.common import four_byte +from paramiko.message import Message +from paramiko.pkey import PKey +from paramiko.ssh_exception import SSHException +from paramiko.util import deflate_long + + +class _ECDSACurve: + """ + Represents a specific ECDSA Curve (nistp256, nistp384, etc). + + Handles the generation of the key format identifier and the selection of + the proper hash function. Also grabs the proper curve from the 'ecdsa' + package. + """ + + def __init__(self, curve_class, nist_name): + self.nist_name = nist_name + self.key_length = curve_class.key_size + + # Defined in RFC 5656 6.2 + self.key_format_identifier = "ecdsa-sha2-" + self.nist_name + + # Defined in RFC 5656 6.2.1 + if self.key_length <= 256: + self.hash_object = hashes.SHA256 + elif self.key_length <= 384: + self.hash_object = hashes.SHA384 + else: + self.hash_object = hashes.SHA512 + + self.curve_class = curve_class + + +class _ECDSACurveSet: + """ + A collection to hold the ECDSA curves. Allows querying by oid and by key + format identifier. The two ways in which ECDSAKey needs to be able to look + up curves. + """ + + def __init__(self, ecdsa_curves): + self.ecdsa_curves = ecdsa_curves + + def get_key_format_identifier_list(self): + return [curve.key_format_identifier for curve in self.ecdsa_curves] + + def get_by_curve_class(self, curve_class): + for curve in self.ecdsa_curves: + if curve.curve_class == curve_class: + return curve + + def get_by_key_format_identifier(self, key_format_identifier): + for curve in self.ecdsa_curves: + if curve.key_format_identifier == key_format_identifier: + return curve + + def get_by_key_length(self, key_length): + for curve in self.ecdsa_curves: + if curve.key_length == key_length: + return curve + + +class ECDSAKey(PKey): + """ + Representation of an ECDSA key which can be used to sign and verify SSH2 + data. + """ + + _ECDSA_CURVES = _ECDSACurveSet( + [ + _ECDSACurve(ec.SECP256R1, "nistp256"), + _ECDSACurve(ec.SECP384R1, "nistp384"), + _ECDSACurve(ec.SECP521R1, "nistp521"), + ] + ) + + def __init__( + self, + msg=None, + data=None, + filename=None, + password=None, + vals=None, + file_obj=None, + validate_point=True, + ): + self.verifying_key = None + self.signing_key = None + self.public_blob = None + if file_obj is not None: + self._from_private_key(file_obj, password) + return + if filename is not None: + self._from_private_key_file(filename, password) + return + if (msg is None) and (data is not None): + msg = Message(data) + if vals is not None: + self.signing_key, self.verifying_key = vals + c_class = self.signing_key.curve.__class__ + self.ecdsa_curve = self._ECDSA_CURVES.get_by_curve_class(c_class) + else: + # Must set ecdsa_curve first; subroutines called herein may need to + # spit out our get_name(), which relies on this. + key_type = msg.get_text() + # But this also means we need to hand it a real key/curve + # identifier, so strip out any cert business. (NOTE: could push + # that into _ECDSACurveSet.get_by_key_format_identifier(), but it + # feels more correct to do it here?) + suffix = "-cert-v01@openssh.com" + if key_type.endswith(suffix): + key_type = key_type[: -len(suffix)] + self.ecdsa_curve = self._ECDSA_CURVES.get_by_key_format_identifier( + key_type + ) + key_types = self._ECDSA_CURVES.get_key_format_identifier_list() + cert_types = [ + "{}-cert-v01@openssh.com".format(x) for x in key_types + ] + self._check_type_and_load_cert( + msg=msg, key_type=key_types, cert_type=cert_types + ) + curvename = msg.get_text() + if curvename != self.ecdsa_curve.nist_name: + raise SSHException( + "Can't handle curve of type {}".format(curvename) + ) + + pointinfo = msg.get_binary() + try: + key = ec.EllipticCurvePublicKey.from_encoded_point( + self.ecdsa_curve.curve_class(), pointinfo + ) + self.verifying_key = key + except ValueError: + raise SSHException("Invalid public key") + + @classmethod + def supported_key_format_identifiers(cls): + return cls._ECDSA_CURVES.get_key_format_identifier_list() + + def asbytes(self): + key = self.verifying_key + m = Message() + m.add_string(self.ecdsa_curve.key_format_identifier) + m.add_string(self.ecdsa_curve.nist_name) + + numbers = key.public_numbers() + + key_size_bytes = (key.curve.key_size + 7) // 8 + + x_bytes = deflate_long(numbers.x, add_sign_padding=False) + x_bytes = b"\x00" * (key_size_bytes - len(x_bytes)) + x_bytes + + y_bytes = deflate_long(numbers.y, add_sign_padding=False) + y_bytes = b"\x00" * (key_size_bytes - len(y_bytes)) + y_bytes + + point_str = four_byte + x_bytes + y_bytes + m.add_string(point_str) + return m.asbytes() + + def __str__(self): + return self.asbytes() + + @property + def _fields(self): + return ( + self.get_name(), + self.verifying_key.public_numbers().x, + self.verifying_key.public_numbers().y, + ) + + def get_name(self): + return self.ecdsa_curve.key_format_identifier + + def get_bits(self): + return self.ecdsa_curve.key_length + + def can_sign(self): + return self.signing_key is not None + + def sign_ssh_data(self, data, algorithm=None): + ecdsa = ec.ECDSA(self.ecdsa_curve.hash_object()) + sig = self.signing_key.sign(data, ecdsa) + r, s = decode_dss_signature(sig) + + m = Message() + m.add_string(self.ecdsa_curve.key_format_identifier) + m.add_string(self._sigencode(r, s)) + return m + + def verify_ssh_sig(self, data, msg): + if msg.get_text() != self.ecdsa_curve.key_format_identifier: + return False + sig = msg.get_binary() + sigR, sigS = self._sigdecode(sig) + signature = encode_dss_signature(sigR, sigS) + + try: + self.verifying_key.verify( + signature, data, ec.ECDSA(self.ecdsa_curve.hash_object()) + ) + except InvalidSignature: + return False + else: + return True + + def write_private_key_file(self, filename, password=None): + self._write_private_key_file( + filename, + self.signing_key, + serialization.PrivateFormat.TraditionalOpenSSL, + password=password, + ) + + def write_private_key(self, file_obj, password=None): + self._write_private_key( + file_obj, + self.signing_key, + serialization.PrivateFormat.TraditionalOpenSSL, + password=password, + ) + + @classmethod + def generate(cls, curve=ec.SECP256R1(), progress_func=None, bits=None): + """ + Generate a new private ECDSA key. This factory function can be used to + generate a new host key or authentication key. + + :param progress_func: Not used for this type of key. + :returns: A new private key (`.ECDSAKey`) object + """ + if bits is not None: + curve = cls._ECDSA_CURVES.get_by_key_length(bits) + if curve is None: + raise ValueError("Unsupported key length: {:d}".format(bits)) + curve = curve.curve_class() + + private_key = ec.generate_private_key(curve, backend=default_backend()) + return ECDSAKey(vals=(private_key, private_key.public_key())) + + # ...internals... + + def _from_private_key_file(self, filename, password): + data = self._read_private_key_file("EC", filename, password) + self._decode_key(data) + + def _from_private_key(self, file_obj, password): + data = self._read_private_key("EC", file_obj, password) + self._decode_key(data) + + def _decode_key(self, data): + pkformat, data = data + if pkformat == self._PRIVATE_KEY_FORMAT_ORIGINAL: + try: + key = serialization.load_der_private_key( + data, password=None, backend=default_backend() + ) + except ( + ValueError, + AssertionError, + TypeError, + UnsupportedAlgorithm, + ) as e: + raise SSHException(str(e)) + elif pkformat == self._PRIVATE_KEY_FORMAT_OPENSSH: + try: + msg = Message(data) + curve_name = msg.get_text() + verkey = msg.get_binary() # noqa: F841 + sigkey = msg.get_mpint() + name = "ecdsa-sha2-" + curve_name + curve = self._ECDSA_CURVES.get_by_key_format_identifier(name) + if not curve: + raise SSHException("Invalid key curve identifier") + key = ec.derive_private_key( + sigkey, curve.curve_class(), default_backend() + ) + except Exception as e: + # PKey._read_private_key_openssh() should check or return + # keytype - parsing could fail for any reason due to wrong type + raise SSHException(str(e)) + else: + self._got_bad_key_format_id(pkformat) + + self.signing_key = key + self.verifying_key = key.public_key() + curve_class = key.curve.__class__ + self.ecdsa_curve = self._ECDSA_CURVES.get_by_curve_class(curve_class) + + def _sigencode(self, r, s): + msg = Message() + msg.add_mpint(r) + msg.add_mpint(s) + return msg.asbytes() + + def _sigdecode(self, sig): + msg = Message(sig) + r = msg.get_mpint() + s = msg.get_mpint() + return r, s diff --git a/paramiko/ed25519key.py b/paramiko/ed25519key.py new file mode 100644 index 0000000..c8c4da6 --- /dev/null +++ b/paramiko/ed25519key.py @@ -0,0 +1,209 @@ +# This file is part of paramiko. +# +# Paramiko is free software; you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free +# Software Foundation; either version 2.1 of the License, or (at your option) +# any later version. +# +# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Paramiko; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +import bcrypt + +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives.ciphers import Cipher + +import nacl.signing + +from paramiko.message import Message +from paramiko.pkey import PKey, OPENSSH_AUTH_MAGIC, _unpad_openssh +from paramiko.util import b +from paramiko.ssh_exception import SSHException, PasswordRequiredException + + +class Ed25519Key(PKey): + """ + Representation of an `Ed25519 <https://ed25519.cr.yp.to/>`_ key. + + .. note:: + Ed25519 key support was added to OpenSSH in version 6.5. + + .. versionadded:: 2.2 + .. versionchanged:: 2.3 + Added a ``file_obj`` parameter to match other key classes. + """ + + def __init__( + self, msg=None, data=None, filename=None, password=None, file_obj=None + ): + self.public_blob = None + verifying_key = signing_key = None + if msg is None and data is not None: + msg = Message(data) + if msg is not None: + self._check_type_and_load_cert( + msg=msg, + key_type="ssh-ed25519", + cert_type="ssh-ed25519-cert-v01@openssh.com", + ) + verifying_key = nacl.signing.VerifyKey(msg.get_binary()) + elif filename is not None: + with open(filename, "r") as f: + pkformat, data = self._read_private_key("OPENSSH", f) + elif file_obj is not None: + pkformat, data = self._read_private_key("OPENSSH", file_obj) + + if filename or file_obj: + signing_key = self._parse_signing_key_data(data, password) + + if signing_key is None and verifying_key is None: + raise ValueError("need a key") + + self._signing_key = signing_key + self._verifying_key = verifying_key + + def _parse_signing_key_data(self, data, password): + from paramiko.transport import Transport + + # We may eventually want this to be usable for other key types, as + # OpenSSH moves to it, but for now this is just for Ed25519 keys. + # This format is described here: + # https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.key + # The description isn't totally complete, and I had to refer to the + # source for a full implementation. + message = Message(data) + if message.get_bytes(len(OPENSSH_AUTH_MAGIC)) != OPENSSH_AUTH_MAGIC: + raise SSHException("Invalid key") + + ciphername = message.get_text() + kdfname = message.get_text() + kdfoptions = message.get_binary() + num_keys = message.get_int() + + if kdfname == "none": + # kdfname of "none" must have an empty kdfoptions, the ciphername + # must be "none" + if kdfoptions or ciphername != "none": + raise SSHException("Invalid key") + elif kdfname == "bcrypt": + if not password: + raise PasswordRequiredException( + "Private key file is encrypted" + ) + kdf = Message(kdfoptions) + bcrypt_salt = kdf.get_binary() + bcrypt_rounds = kdf.get_int() + else: + raise SSHException("Invalid key") + + if ciphername != "none" and ciphername not in Transport._cipher_info: + raise SSHException("Invalid key") + + public_keys = [] + for _ in range(num_keys): + pubkey = Message(message.get_binary()) + if pubkey.get_text() != "ssh-ed25519": + raise SSHException("Invalid key") + public_keys.append(pubkey.get_binary()) + + private_ciphertext = message.get_binary() + if ciphername == "none": + private_data = private_ciphertext + else: + cipher = Transport._cipher_info[ciphername] + key = bcrypt.kdf( + password=b(password), + salt=bcrypt_salt, + desired_key_bytes=cipher["key-size"] + cipher["block-size"], + rounds=bcrypt_rounds, + # We can't control how many rounds are on disk, so no sense + # warning about it. + ignore_few_rounds=True, + ) + decryptor = Cipher( + cipher["class"](key[: cipher["key-size"]]), + cipher["mode"](key[cipher["key-size"] :]), + backend=default_backend(), + ).decryptor() + private_data = ( + decryptor.update(private_ciphertext) + decryptor.finalize() + ) + + message = Message(_unpad_openssh(private_data)) + if message.get_int() != message.get_int(): + raise SSHException("Invalid key") + + signing_keys = [] + for i in range(num_keys): + if message.get_text() != "ssh-ed25519": + raise SSHException("Invalid key") + # A copy of the public key, again, ignore. + public = message.get_binary() + key_data = message.get_binary() + # The second half of the key data is yet another copy of the public + # key... + signing_key = nacl.signing.SigningKey(key_data[:32]) + # Verify that all the public keys are the same... + assert ( + signing_key.verify_key.encode() + == public + == public_keys[i] + == key_data[32:] + ) + signing_keys.append(signing_key) + # Comment, ignore. + message.get_binary() + + if len(signing_keys) != 1: + raise SSHException("Invalid key") + return signing_keys[0] + + def asbytes(self): + if self.can_sign(): + v = self._signing_key.verify_key + else: + v = self._verifying_key + m = Message() + m.add_string("ssh-ed25519") + m.add_string(v.encode()) + return m.asbytes() + + @property + def _fields(self): + if self.can_sign(): + v = self._signing_key.verify_key + else: + v = self._verifying_key + return (self.get_name(), v) + + def get_name(self): + return "ssh-ed25519" + + def get_bits(self): + return 256 + + def can_sign(self): + return self._signing_key is not None + + def sign_ssh_data(self, data, algorithm=None): + m = Message() + m.add_string("ssh-ed25519") + m.add_string(self._signing_key.sign(data).signature) + return m + + def verify_ssh_sig(self, data, msg): + if msg.get_text() != "ssh-ed25519": + return False + + try: + self._verifying_key.verify(data, msg.get_binary()) + except nacl.exceptions.BadSignatureError: + return False + else: + return True diff --git a/paramiko/file.py b/paramiko/file.py new file mode 100644 index 0000000..a36abb9 --- /dev/null +++ b/paramiko/file.py @@ -0,0 +1,528 @@ +# Copyright (C) 2003-2007 Robey Pointer <robeypointer@gmail.com> +# +# This file is part of paramiko. +# +# Paramiko is free software; you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free +# Software Foundation; either version 2.1 of the License, or (at your option) +# any later version. +# +# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Paramiko; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +from io import BytesIO + +from paramiko.common import ( + linefeed_byte_value, + crlf, + cr_byte, + linefeed_byte, + cr_byte_value, +) + +from paramiko.util import ClosingContextManager, u + + +class BufferedFile(ClosingContextManager): + """ + Reusable base class to implement Python-style file buffering around a + simpler stream. + """ + + _DEFAULT_BUFSIZE = 8192 + + SEEK_SET = 0 + SEEK_CUR = 1 + SEEK_END = 2 + + FLAG_READ = 0x1 + FLAG_WRITE = 0x2 + FLAG_APPEND = 0x4 + FLAG_BINARY = 0x10 + FLAG_BUFFERED = 0x20 + FLAG_LINE_BUFFERED = 0x40 + FLAG_UNIVERSAL_NEWLINE = 0x80 + + def __init__(self): + self.newlines = None + self._flags = 0 + self._bufsize = self._DEFAULT_BUFSIZE + self._wbuffer = BytesIO() + self._rbuffer = bytes() + self._at_trailing_cr = False + self._closed = False + # pos - position within the file, according to the user + # realpos - position according the OS + # (these may be different because we buffer for line reading) + self._pos = self._realpos = 0 + # size only matters for seekable files + self._size = 0 + + def __del__(self): + self.close() + + def __iter__(self): + """ + Returns an iterator that can be used to iterate over the lines in this + file. This iterator happens to return the file itself, since a file is + its own iterator. + + :raises: ``ValueError`` -- if the file is closed. + """ + if self._closed: + raise ValueError("I/O operation on closed file") + return self + + def close(self): + """ + Close the file. Future read and write operations will fail. + """ + self.flush() + self._closed = True + + def flush(self): + """ + Write out any data in the write buffer. This may do nothing if write + buffering is not turned on. + """ + self._write_all(self._wbuffer.getvalue()) + self._wbuffer = BytesIO() + return + + def __next__(self): + """ + Returns the next line from the input, or raises ``StopIteration`` + when EOF is hit. Unlike python file objects, it's okay to mix + calls to `.next` and `.readline`. + + :raises: ``StopIteration`` -- when the end of the file is reached. + + :returns: + a line (`str`, or `bytes` if the file was opened in binary mode) + read from the file. + """ + line = self.readline() + if not line: + raise StopIteration + return line + + def readable(self): + """ + Check if the file can be read from. + + :returns: + `True` if the file can be read from. If `False`, `read` will raise + an exception. + """ + return (self._flags & self.FLAG_READ) == self.FLAG_READ + + def writable(self): + """ + Check if the file can be written to. + + :returns: + `True` if the file can be written to. If `False`, `write` will + raise an exception. + """ + return (self._flags & self.FLAG_WRITE) == self.FLAG_WRITE + + def seekable(self): + """ + Check if the file supports random access. + + :returns: + `True` if the file supports random access. If `False`, `seek` will + raise an exception. + """ + return False + + def readinto(self, buff): + """ + Read up to ``len(buff)`` bytes into ``bytearray`` *buff* and return the + number of bytes read. + + :returns: + The number of bytes read. + """ + data = self.read(len(buff)) + buff[: len(data)] = data + return len(data) + + def read(self, size=None): + """ + Read at most ``size`` bytes from the file (less if we hit the end of + the file first). If the ``size`` argument is negative or omitted, + read all the remaining data in the file. + + .. note:: + ``'b'`` mode flag is ignored (``self.FLAG_BINARY`` in + ``self._flags``), because SSH treats all files as binary, since we + have no idea what encoding the file is in, or even if the file is + text data. + + :param int size: maximum number of bytes to read + :returns: + data read from the file (as bytes), or an empty string if EOF was + encountered immediately + """ + if self._closed: + raise IOError("File is closed") + if not (self._flags & self.FLAG_READ): + raise IOError("File is not open for reading") + if (size is None) or (size < 0): + # go for broke + result = bytearray(self._rbuffer) + self._rbuffer = bytes() + self._pos += len(result) + while True: + try: + new_data = self._read(self._DEFAULT_BUFSIZE) + except EOFError: + new_data = None + if (new_data is None) or (len(new_data) == 0): + break + result.extend(new_data) + self._realpos += len(new_data) + self._pos += len(new_data) + return bytes(result) + if size <= len(self._rbuffer): + result = self._rbuffer[:size] + self._rbuffer = self._rbuffer[size:] + self._pos += len(result) + return result + while len(self._rbuffer) < size: + read_size = size - len(self._rbuffer) + if self._flags & self.FLAG_BUFFERED: + read_size = max(self._bufsize, read_size) + try: + new_data = self._read(read_size) + except EOFError: + new_data = None + if (new_data is None) or (len(new_data) == 0): + break + self._rbuffer += new_data + self._realpos += len(new_data) + result = self._rbuffer[:size] + self._rbuffer = self._rbuffer[size:] + self._pos += len(result) + return result + + def readline(self, size=None): + """ + Read one entire line from the file. A trailing newline character is + kept in the string (but may be absent when a file ends with an + incomplete line). If the size argument is present and non-negative, it + is a maximum byte count (including the trailing newline) and an + incomplete line may be returned. An empty string is returned only when + EOF is encountered immediately. + + .. note:: + Unlike stdio's ``fgets``, the returned string contains null + characters (``'\\0'``) if they occurred in the input. + + :param int size: maximum length of returned string. + :returns: + next line of the file, or an empty string if the end of the + file has been reached. + + If the file was opened in binary (``'b'``) mode: bytes are returned + Else: the encoding of the file is assumed to be UTF-8 and character + strings (`str`) are returned + """ + # it's almost silly how complex this function is. + if self._closed: + raise IOError("File is closed") + if not (self._flags & self.FLAG_READ): + raise IOError("File not open for reading") + line = self._rbuffer + truncated = False + while True: + if ( + self._at_trailing_cr + and self._flags & self.FLAG_UNIVERSAL_NEWLINE + and len(line) > 0 + ): + # edge case: the newline may be '\r\n' and we may have read + # only the first '\r' last time. + if line[0] == linefeed_byte_value: + line = line[1:] + self._record_newline(crlf) + else: + self._record_newline(cr_byte) + self._at_trailing_cr = False + # check size before looking for a linefeed, in case we already have + # enough. + if (size is not None) and (size >= 0): + if len(line) >= size: + # truncate line + self._rbuffer = line[size:] + line = line[:size] + truncated = True + break + n = size - len(line) + else: + n = self._bufsize + if linefeed_byte in line or ( + self._flags & self.FLAG_UNIVERSAL_NEWLINE and cr_byte in line + ): + break + try: + new_data = self._read(n) + except EOFError: + new_data = None + if (new_data is None) or (len(new_data) == 0): + self._rbuffer = bytes() + self._pos += len(line) + return line if self._flags & self.FLAG_BINARY else u(line) + line += new_data + self._realpos += len(new_data) + # find the newline + pos = line.find(linefeed_byte) + if self._flags & self.FLAG_UNIVERSAL_NEWLINE: + rpos = line.find(cr_byte) + if (rpos >= 0) and (rpos < pos or pos < 0): + pos = rpos + if pos == -1: + # we couldn't find a newline in the truncated string, return it + self._pos += len(line) + return line if self._flags & self.FLAG_BINARY else u(line) + xpos = pos + 1 + if ( + line[pos] == cr_byte_value + and xpos < len(line) + and line[xpos] == linefeed_byte_value + ): + xpos += 1 + # if the string was truncated, _rbuffer needs to have the string after + # the newline character plus the truncated part of the line we stored + # earlier in _rbuffer + if truncated: + self._rbuffer = line[xpos:] + self._rbuffer + else: + self._rbuffer = line[xpos:] + + lf = line[pos:xpos] + line = line[:pos] + linefeed_byte + if (len(self._rbuffer) == 0) and (lf == cr_byte): + # we could read the line up to a '\r' and there could still be a + # '\n' following that we read next time. note that and eat it. + self._at_trailing_cr = True + else: + self._record_newline(lf) + self._pos += len(line) + return line if self._flags & self.FLAG_BINARY else u(line) + + def readlines(self, sizehint=None): + """ + Read all remaining lines using `readline` and return them as a list. + If the optional ``sizehint`` argument is present, instead of reading up + to EOF, whole lines totalling approximately sizehint bytes (possibly + after rounding up to an internal buffer size) are read. + + :param int sizehint: desired maximum number of bytes to read. + :returns: list of lines read from the file. + """ + lines = [] + byte_count = 0 + while True: + line = self.readline() + if len(line) == 0: + break + lines.append(line) + byte_count += len(line) + if (sizehint is not None) and (byte_count >= sizehint): + break + return lines + + def seek(self, offset, whence=0): + """ + Set the file's current position, like stdio's ``fseek``. Not all file + objects support seeking. + + .. note:: + If a file is opened in append mode (``'a'`` or ``'a+'``), any seek + operations will be undone at the next write (as the file position + will move back to the end of the file). + + :param int offset: + position to move to within the file, relative to ``whence``. + :param int whence: + type of movement: 0 = absolute; 1 = relative to the current + position; 2 = relative to the end of the file. + + :raises: ``IOError`` -- if the file doesn't support random access. + """ + raise IOError("File does not support seeking.") + + def tell(self): + """ + Return the file's current position. This may not be accurate or + useful if the underlying file doesn't support random access, or was + opened in append mode. + + :returns: file position (`number <int>` of bytes). + """ + return self._pos + + def write(self, data): + """ + Write data to the file. If write buffering is on (``bufsize`` was + specified and non-zero), some or all of the data may not actually be + written yet. (Use `flush` or `close` to force buffered data to be + written out.) + + :param data: ``str``/``bytes`` data to write + """ + if isinstance(data, str): + # Accept text and encode as utf-8 for compatibility only. + data = data.encode("utf-8") + if self._closed: + raise IOError("File is closed") + if not (self._flags & self.FLAG_WRITE): + raise IOError("File not open for writing") + if not (self._flags & self.FLAG_BUFFERED): + self._write_all(data) + return + self._wbuffer.write(data) + if self._flags & self.FLAG_LINE_BUFFERED: + # only scan the new data for linefeed, to avoid wasting time. + last_newline_pos = data.rfind(linefeed_byte) + if last_newline_pos >= 0: + wbuf = self._wbuffer.getvalue() + last_newline_pos += len(wbuf) - len(data) + self._write_all(wbuf[: last_newline_pos + 1]) + self._wbuffer = BytesIO() + self._wbuffer.write(wbuf[last_newline_pos + 1 :]) + return + # even if we're line buffering, if the buffer has grown past the + # buffer size, force a flush. + if self._wbuffer.tell() >= self._bufsize: + self.flush() + return + + def writelines(self, sequence): + """ + Write a sequence of strings to the file. The sequence can be any + iterable object producing strings, typically a list of strings. (The + name is intended to match `readlines`; `writelines` does not add line + separators.) + + :param sequence: an iterable sequence of strings. + """ + for line in sequence: + self.write(line) + return + + def xreadlines(self): + """ + Identical to ``iter(f)``. This is a deprecated file interface that + predates Python iterator support. + """ + return self + + @property + def closed(self): + return self._closed + + # ...overrides... + + def _read(self, size): + """ + (subclass override) + Read data from the stream. Return ``None`` or raise ``EOFError`` to + indicate EOF. + """ + raise EOFError() + + def _write(self, data): + """ + (subclass override) + Write data into the stream. + """ + raise IOError("write not implemented") + + def _get_size(self): + """ + (subclass override) + Return the size of the file. This is called from within `_set_mode` + if the file is opened in append mode, so the file position can be + tracked and `seek` and `tell` will work correctly. If the file is + a stream that can't be randomly accessed, you don't need to override + this method, + """ + return 0 + + # ...internals... + + def _set_mode(self, mode="r", bufsize=-1): + """ + Subclasses call this method to initialize the BufferedFile. + """ + # set bufsize in any event, because it's used for readline(). + self._bufsize = self._DEFAULT_BUFSIZE + if bufsize < 0: + # do no buffering by default, because otherwise writes will get + # buffered in a way that will probably confuse people. + bufsize = 0 + if bufsize == 1: + # apparently, line buffering only affects writes. reads are only + # buffered if you call readline (directly or indirectly: iterating + # over a file will indirectly call readline). + self._flags |= self.FLAG_BUFFERED | self.FLAG_LINE_BUFFERED + elif bufsize > 1: + self._bufsize = bufsize + self._flags |= self.FLAG_BUFFERED + self._flags &= ~self.FLAG_LINE_BUFFERED + elif bufsize == 0: + # unbuffered + self._flags &= ~(self.FLAG_BUFFERED | self.FLAG_LINE_BUFFERED) + + if ("r" in mode) or ("+" in mode): + self._flags |= self.FLAG_READ + if ("w" in mode) or ("+" in mode): + self._flags |= self.FLAG_WRITE + if "a" in mode: + self._flags |= self.FLAG_WRITE | self.FLAG_APPEND + self._size = self._get_size() + self._pos = self._realpos = self._size + if "b" in mode: + self._flags |= self.FLAG_BINARY + if "U" in mode: + self._flags |= self.FLAG_UNIVERSAL_NEWLINE + # built-in file objects have this attribute to store which kinds of + # line terminations they've seen: + # <http://www.python.org/doc/current/lib/built-in-funcs.html> + self.newlines = None + + def _write_all(self, raw_data): + # the underlying stream may be something that does partial writes (like + # a socket). + data = memoryview(raw_data) + while len(data) > 0: + count = self._write(data) + data = data[count:] + if self._flags & self.FLAG_APPEND: + self._size += count + self._pos = self._realpos = self._size + else: + self._pos += count + self._realpos += count + return None + + def _record_newline(self, newline): + # silliness about tracking what kinds of newlines we've seen. + # i don't understand why it can be None, a string, or a tuple, instead + # of just always being a tuple, but we'll emulate that behavior anyway. + if not (self._flags & self.FLAG_UNIVERSAL_NEWLINE): + return + if self.newlines is None: + self.newlines = newline + elif self.newlines != newline and isinstance(self.newlines, bytes): + self.newlines = (self.newlines, newline) + elif newline not in self.newlines: + self.newlines += (newline,) diff --git a/paramiko/hostkeys.py b/paramiko/hostkeys.py new file mode 100644 index 0000000..bbfa575 --- /dev/null +++ b/paramiko/hostkeys.py @@ -0,0 +1,389 @@ +# Copyright (C) 2006-2007 Robey Pointer <robeypointer@gmail.com> +# +# This file is part of paramiko. +# +# Paramiko is free software; you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free +# Software Foundation; either version 2.1 of the License, or (at your option) +# any later version. +# +# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Paramiko; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + + +from base64 import encodebytes, decodebytes +import binascii +import os +import re + +from collections.abc import MutableMapping +from hashlib import sha1 +from hmac import HMAC + + +from paramiko.dsskey import DSSKey +from paramiko.rsakey import RSAKey +from paramiko.util import get_logger, constant_time_bytes_eq, b, u +from paramiko.ecdsakey import ECDSAKey +from paramiko.ed25519key import Ed25519Key +from paramiko.ssh_exception import SSHException + + +class HostKeys(MutableMapping): + """ + Representation of an OpenSSH-style "known hosts" file. Host keys can be + read from one or more files, and then individual hosts can be looked up to + verify server keys during SSH negotiation. + + A `.HostKeys` object can be treated like a dict; any dict lookup is + equivalent to calling `lookup`. + + .. versionadded:: 1.5.3 + """ + + def __init__(self, filename=None): + """ + Create a new HostKeys object, optionally loading keys from an OpenSSH + style host-key file. + + :param str filename: filename to load host keys from, or ``None`` + """ + # emulate a dict of { hostname: { keytype: PKey } } + self._entries = [] + if filename is not None: + self.load(filename) + + def add(self, hostname, keytype, key): + """ + Add a host key entry to the table. Any existing entry for a + ``(hostname, keytype)`` pair will be replaced. + + :param str hostname: the hostname (or IP) to add + :param str keytype: key type (``"ssh-rsa"`` or ``"ssh-dss"``) + :param .PKey key: the key to add + """ + for e in self._entries: + if (hostname in e.hostnames) and (e.key.get_name() == keytype): + e.key = key + return + self._entries.append(HostKeyEntry([hostname], key)) + + def load(self, filename): + """ + Read a file of known SSH host keys, in the format used by OpenSSH. + This type of file unfortunately doesn't exist on Windows, but on + posix, it will usually be stored in + ``os.path.expanduser("~/.ssh/known_hosts")``. + + If this method is called multiple times, the host keys are merged, + not cleared. So multiple calls to `load` will just call `add`, + replacing any existing entries and adding new ones. + + :param str filename: name of the file to read host keys from + + :raises: ``IOError`` -- if there was an error reading the file + """ + with open(filename, "r") as f: + for lineno, line in enumerate(f, 1): + line = line.strip() + if (len(line) == 0) or (line[0] == "#"): + continue + try: + e = HostKeyEntry.from_line(line, lineno) + except SSHException: + continue + if e is not None: + _hostnames = e.hostnames + for h in _hostnames: + if self.check(h, e.key): + e.hostnames.remove(h) + if len(e.hostnames): + self._entries.append(e) + + def save(self, filename): + """ + Save host keys into a file, in the format used by OpenSSH. The order + of keys in the file will be preserved when possible (if these keys were + loaded from a file originally). The single exception is that combined + lines will be split into individual key lines, which is arguably a bug. + + :param str filename: name of the file to write + + :raises: ``IOError`` -- if there was an error writing the file + + .. versionadded:: 1.6.1 + """ + with open(filename, "w") as f: + for e in self._entries: + line = e.to_line() + if line: + f.write(line) + + def lookup(self, hostname): + """ + Find a hostkey entry for a given hostname or IP. If no entry is found, + ``None`` is returned. Otherwise a dictionary of keytype to key is + returned. The keytype will be either ``"ssh-rsa"`` or ``"ssh-dss"``. + + :param str hostname: the hostname (or IP) to lookup + :return: dict of `str` -> `.PKey` keys associated with this host + (or ``None``) + """ + + class SubDict(MutableMapping): + def __init__(self, hostname, entries, hostkeys): + self._hostname = hostname + self._entries = entries + self._hostkeys = hostkeys + + def __iter__(self): + for k in self.keys(): + yield k + + def __len__(self): + return len(self.keys()) + + def __delitem__(self, key): + for e in list(self._entries): + if e.key.get_name() == key: + self._entries.remove(e) + break + else: + raise KeyError(key) + + def __getitem__(self, key): + for e in self._entries: + if e.key.get_name() == key: + return e.key + raise KeyError(key) + + def __setitem__(self, key, val): + for e in self._entries: + if e.key is None: + continue + if e.key.get_name() == key: + # replace + e.key = val + break + else: + # add a new one + e = HostKeyEntry([hostname], val) + self._entries.append(e) + self._hostkeys._entries.append(e) + + def keys(self): + return [ + e.key.get_name() + for e in self._entries + if e.key is not None + ] + + entries = [] + for e in self._entries: + if self._hostname_matches(hostname, e): + entries.append(e) + if len(entries) == 0: + return None + return SubDict(hostname, entries, self) + + def _hostname_matches(self, hostname, entry): + """ + Tests whether ``hostname`` string matches given SubDict ``entry``. + + :returns bool: + """ + for h in entry.hostnames: + if ( + h == hostname + or h.startswith("|1|") + and not hostname.startswith("|1|") + and constant_time_bytes_eq(self.hash_host(hostname, h), h) + ): + return True + return False + + def check(self, hostname, key): + """ + Return True if the given key is associated with the given hostname + in this dictionary. + + :param str hostname: hostname (or IP) of the SSH server + :param .PKey key: the key to check + :return: + ``True`` if the key is associated with the hostname; else ``False`` + """ + k = self.lookup(hostname) + if k is None: + return False + host_key = k.get(key.get_name(), None) + if host_key is None: + return False + return host_key.asbytes() == key.asbytes() + + def clear(self): + """ + Remove all host keys from the dictionary. + """ + self._entries = [] + + def __iter__(self): + for k in self.keys(): + yield k + + def __len__(self): + return len(self.keys()) + + def __getitem__(self, key): + ret = self.lookup(key) + if ret is None: + raise KeyError(key) + return ret + + def __delitem__(self, key): + index = None + for i, entry in enumerate(self._entries): + if self._hostname_matches(key, entry): + index = i + break + if index is None: + raise KeyError(key) + self._entries.pop(index) + + def __setitem__(self, hostname, entry): + # don't use this please. + if len(entry) == 0: + self._entries.append(HostKeyEntry([hostname], None)) + return + for key_type in entry.keys(): + found = False + for e in self._entries: + if (hostname in e.hostnames) and e.key.get_name() == key_type: + # replace + e.key = entry[key_type] + found = True + if not found: + self._entries.append(HostKeyEntry([hostname], entry[key_type])) + + def keys(self): + ret = [] + for e in self._entries: + for h in e.hostnames: + if h not in ret: + ret.append(h) + return ret + + def values(self): + ret = [] + for k in self.keys(): + ret.append(self.lookup(k)) + return ret + + @staticmethod + def hash_host(hostname, salt=None): + """ + Return a "hashed" form of the hostname, as used by OpenSSH when storing + hashed hostnames in the known_hosts file. + + :param str hostname: the hostname to hash + :param str salt: optional salt to use when hashing + (must be 20 bytes long) + :return: the hashed hostname as a `str` + """ + if salt is None: + salt = os.urandom(sha1().digest_size) + else: + if salt.startswith("|1|"): + salt = salt.split("|")[2] + salt = decodebytes(b(salt)) + assert len(salt) == sha1().digest_size + hmac = HMAC(salt, b(hostname), sha1).digest() + hostkey = "|1|{}|{}".format(u(encodebytes(salt)), u(encodebytes(hmac))) + return hostkey.replace("\n", "") + + +class InvalidHostKey(Exception): + def __init__(self, line, exc): + self.line = line + self.exc = exc + self.args = (line, exc) + + +class HostKeyEntry: + """ + Representation of a line in an OpenSSH-style "known hosts" file. + """ + + def __init__(self, hostnames=None, key=None): + self.valid = (hostnames is not None) and (key is not None) + self.hostnames = hostnames + self.key = key + + @classmethod + def from_line(cls, line, lineno=None): + """ + Parses the given line of text to find the names for the host, + the type of key, and the key data. The line is expected to be in the + format used by the OpenSSH known_hosts file. Fields are separated by a + single space or tab. + + Lines are expected to not have leading or trailing whitespace. + We don't bother to check for comments or empty lines. All of + that should be taken care of before sending the line to us. + + :param str line: a line from an OpenSSH known_hosts file + """ + log = get_logger("paramiko.hostkeys") + fields = re.split(" |\t", line) + if len(fields) < 3: + # Bad number of fields + msg = "Not enough fields found in known_hosts in line {} ({!r})" + log.info(msg.format(lineno, line)) + return None + fields = fields[:3] + + names, keytype, key = fields + names = names.split(",") + + # Decide what kind of key we're looking at and create an object + # to hold it accordingly. + try: + key = b(key) + if keytype == "ssh-rsa": + key = RSAKey(data=decodebytes(key)) + elif keytype == "ssh-dss": + key = DSSKey(data=decodebytes(key)) + elif keytype in ECDSAKey.supported_key_format_identifiers(): + key = ECDSAKey(data=decodebytes(key), validate_point=False) + elif keytype == "ssh-ed25519": + key = Ed25519Key(data=decodebytes(key)) + else: + log.info("Unable to handle key of type {}".format(keytype)) + return None + + except binascii.Error as e: + raise InvalidHostKey(line, e) + + return cls(names, key) + + def to_line(self): + """ + Returns a string in OpenSSH known_hosts file format, or None if + the object is not in a valid state. A trailing newline is + included. + """ + if self.valid: + return "{} {} {}\n".format( + ",".join(self.hostnames), + self.key.get_name(), + self.key.get_base64(), + ) + return None + + def __repr__(self): + return "<HostKeyEntry {!r}: {!r}>".format(self.hostnames, self.key) diff --git a/paramiko/kex_curve25519.py b/paramiko/kex_curve25519.py new file mode 100644 index 0000000..20c23e4 --- /dev/null +++ b/paramiko/kex_curve25519.py @@ -0,0 +1,131 @@ +import binascii +import hashlib + +from cryptography.exceptions import UnsupportedAlgorithm +from cryptography.hazmat.primitives import constant_time, serialization +from cryptography.hazmat.primitives.asymmetric.x25519 import ( + X25519PrivateKey, + X25519PublicKey, +) + +from paramiko.message import Message +from paramiko.common import byte_chr +from paramiko.ssh_exception import SSHException + + +_MSG_KEXECDH_INIT, _MSG_KEXECDH_REPLY = range(30, 32) +c_MSG_KEXECDH_INIT, c_MSG_KEXECDH_REPLY = [byte_chr(c) for c in range(30, 32)] + + +class KexCurve25519: + hash_algo = hashlib.sha256 + + def __init__(self, transport): + self.transport = transport + self.key = None + + @classmethod + def is_available(cls): + try: + X25519PrivateKey.generate() + except UnsupportedAlgorithm: + return False + else: + return True + + def _perform_exchange(self, peer_key): + secret = self.key.exchange(peer_key) + if constant_time.bytes_eq(secret, b"\x00" * 32): + raise SSHException( + "peer's curve25519 public value has wrong order" + ) + return secret + + def start_kex(self): + self.key = X25519PrivateKey.generate() + if self.transport.server_mode: + self.transport._expect_packet(_MSG_KEXECDH_INIT) + return + + m = Message() + m.add_byte(c_MSG_KEXECDH_INIT) + m.add_string( + self.key.public_key().public_bytes( + serialization.Encoding.Raw, serialization.PublicFormat.Raw + ) + ) + self.transport._send_message(m) + self.transport._expect_packet(_MSG_KEXECDH_REPLY) + + def parse_next(self, ptype, m): + if self.transport.server_mode and (ptype == _MSG_KEXECDH_INIT): + return self._parse_kexecdh_init(m) + elif not self.transport.server_mode and (ptype == _MSG_KEXECDH_REPLY): + return self._parse_kexecdh_reply(m) + raise SSHException( + "KexCurve25519 asked to handle packet type {:d}".format(ptype) + ) + + def _parse_kexecdh_init(self, m): + peer_key_bytes = m.get_string() + peer_key = X25519PublicKey.from_public_bytes(peer_key_bytes) + K = self._perform_exchange(peer_key) + K = int(binascii.hexlify(K), 16) + # compute exchange hash + hm = Message() + hm.add( + self.transport.remote_version, + self.transport.local_version, + self.transport.remote_kex_init, + self.transport.local_kex_init, + ) + server_key_bytes = self.transport.get_server_key().asbytes() + exchange_key_bytes = self.key.public_key().public_bytes( + serialization.Encoding.Raw, serialization.PublicFormat.Raw + ) + hm.add_string(server_key_bytes) + hm.add_string(peer_key_bytes) + hm.add_string(exchange_key_bytes) + hm.add_mpint(K) + H = self.hash_algo(hm.asbytes()).digest() + self.transport._set_K_H(K, H) + sig = self.transport.get_server_key().sign_ssh_data( + H, self.transport.host_key_type + ) + # construct reply + m = Message() + m.add_byte(c_MSG_KEXECDH_REPLY) + m.add_string(server_key_bytes) + m.add_string(exchange_key_bytes) + m.add_string(sig) + self.transport._send_message(m) + self.transport._activate_outbound() + + def _parse_kexecdh_reply(self, m): + peer_host_key_bytes = m.get_string() + peer_key_bytes = m.get_string() + sig = m.get_binary() + + peer_key = X25519PublicKey.from_public_bytes(peer_key_bytes) + + K = self._perform_exchange(peer_key) + K = int(binascii.hexlify(K), 16) + # compute exchange hash and verify signature + hm = Message() + hm.add( + self.transport.local_version, + self.transport.remote_version, + self.transport.local_kex_init, + self.transport.remote_kex_init, + ) + hm.add_string(peer_host_key_bytes) + hm.add_string( + self.key.public_key().public_bytes( + serialization.Encoding.Raw, serialization.PublicFormat.Raw + ) + ) + hm.add_string(peer_key_bytes) + hm.add_mpint(K) + self.transport._set_K_H(K, self.hash_algo(hm.asbytes()).digest()) + self.transport._verify_key(peer_host_key_bytes, sig) + self.transport._activate_outbound() diff --git a/paramiko/kex_ecdh_nist.py b/paramiko/kex_ecdh_nist.py new file mode 100644 index 0000000..41fab46 --- /dev/null +++ b/paramiko/kex_ecdh_nist.py @@ -0,0 +1,151 @@ +""" +Ephemeral Elliptic Curve Diffie-Hellman (ECDH) key exchange +RFC 5656, Section 4 +""" + +from hashlib import sha256, sha384, sha512 +from paramiko.common import byte_chr +from paramiko.message import Message +from paramiko.ssh_exception import SSHException +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives.asymmetric import ec +from cryptography.hazmat.primitives import serialization +from binascii import hexlify + +_MSG_KEXECDH_INIT, _MSG_KEXECDH_REPLY = range(30, 32) +c_MSG_KEXECDH_INIT, c_MSG_KEXECDH_REPLY = [byte_chr(c) for c in range(30, 32)] + + +class KexNistp256: + + name = "ecdh-sha2-nistp256" + hash_algo = sha256 + curve = ec.SECP256R1() + + def __init__(self, transport): + self.transport = transport + # private key, client public and server public keys + self.P = 0 + self.Q_C = None + self.Q_S = None + + def start_kex(self): + self._generate_key_pair() + if self.transport.server_mode: + self.transport._expect_packet(_MSG_KEXECDH_INIT) + return + m = Message() + m.add_byte(c_MSG_KEXECDH_INIT) + # SEC1: V2.0 2.3.3 Elliptic-Curve-Point-to-Octet-String Conversion + m.add_string( + self.Q_C.public_bytes( + serialization.Encoding.X962, + serialization.PublicFormat.UncompressedPoint, + ) + ) + self.transport._send_message(m) + self.transport._expect_packet(_MSG_KEXECDH_REPLY) + + def parse_next(self, ptype, m): + if self.transport.server_mode and (ptype == _MSG_KEXECDH_INIT): + return self._parse_kexecdh_init(m) + elif not self.transport.server_mode and (ptype == _MSG_KEXECDH_REPLY): + return self._parse_kexecdh_reply(m) + raise SSHException( + "KexECDH asked to handle packet type {:d}".format(ptype) + ) + + def _generate_key_pair(self): + self.P = ec.generate_private_key(self.curve, default_backend()) + if self.transport.server_mode: + self.Q_S = self.P.public_key() + return + self.Q_C = self.P.public_key() + + def _parse_kexecdh_init(self, m): + Q_C_bytes = m.get_string() + self.Q_C = ec.EllipticCurvePublicKey.from_encoded_point( + self.curve, Q_C_bytes + ) + K_S = self.transport.get_server_key().asbytes() + K = self.P.exchange(ec.ECDH(), self.Q_C) + K = int(hexlify(K), 16) + # compute exchange hash + hm = Message() + hm.add( + self.transport.remote_version, + self.transport.local_version, + self.transport.remote_kex_init, + self.transport.local_kex_init, + ) + hm.add_string(K_S) + hm.add_string(Q_C_bytes) + # SEC1: V2.0 2.3.3 Elliptic-Curve-Point-to-Octet-String Conversion + hm.add_string( + self.Q_S.public_bytes( + serialization.Encoding.X962, + serialization.PublicFormat.UncompressedPoint, + ) + ) + hm.add_mpint(int(K)) + H = self.hash_algo(hm.asbytes()).digest() + self.transport._set_K_H(K, H) + sig = self.transport.get_server_key().sign_ssh_data( + H, self.transport.host_key_type + ) + # construct reply + m = Message() + m.add_byte(c_MSG_KEXECDH_REPLY) + m.add_string(K_S) + m.add_string( + self.Q_S.public_bytes( + serialization.Encoding.X962, + serialization.PublicFormat.UncompressedPoint, + ) + ) + m.add_string(sig) + self.transport._send_message(m) + self.transport._activate_outbound() + + def _parse_kexecdh_reply(self, m): + K_S = m.get_string() + Q_S_bytes = m.get_string() + self.Q_S = ec.EllipticCurvePublicKey.from_encoded_point( + self.curve, Q_S_bytes + ) + sig = m.get_binary() + K = self.P.exchange(ec.ECDH(), self.Q_S) + K = int(hexlify(K), 16) + # compute exchange hash and verify signature + hm = Message() + hm.add( + self.transport.local_version, + self.transport.remote_version, + self.transport.local_kex_init, + self.transport.remote_kex_init, + ) + hm.add_string(K_S) + # SEC1: V2.0 2.3.3 Elliptic-Curve-Point-to-Octet-String Conversion + hm.add_string( + self.Q_C.public_bytes( + serialization.Encoding.X962, + serialization.PublicFormat.UncompressedPoint, + ) + ) + hm.add_string(Q_S_bytes) + hm.add_mpint(K) + self.transport._set_K_H(K, self.hash_algo(hm.asbytes()).digest()) + self.transport._verify_key(K_S, sig) + self.transport._activate_outbound() + + +class KexNistp384(KexNistp256): + name = "ecdh-sha2-nistp384" + hash_algo = sha384 + curve = ec.SECP384R1() + + +class KexNistp521(KexNistp256): + name = "ecdh-sha2-nistp521" + hash_algo = sha512 + curve = ec.SECP521R1() diff --git a/paramiko/kex_gex.py b/paramiko/kex_gex.py new file mode 100644 index 0000000..baa0803 --- /dev/null +++ b/paramiko/kex_gex.py @@ -0,0 +1,288 @@ +# Copyright (C) 2003-2007 Robey Pointer <robeypointer@gmail.com> +# +# This file is part of paramiko. +# +# Paramiko is free software; you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free +# Software Foundation; either version 2.1 of the License, or (at your option) +# any later version. +# +# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Paramiko; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +""" +Variant on `KexGroup1 <paramiko.kex_group1.KexGroup1>` where the prime "p" and +generator "g" are provided by the server. A bit more work is required on the +client side, and a **lot** more on the server side. +""" + +import os +from hashlib import sha1, sha256 + +from paramiko import util +from paramiko.common import DEBUG, byte_chr, byte_ord, byte_mask +from paramiko.message import Message +from paramiko.ssh_exception import SSHException + + +( + _MSG_KEXDH_GEX_REQUEST_OLD, + _MSG_KEXDH_GEX_GROUP, + _MSG_KEXDH_GEX_INIT, + _MSG_KEXDH_GEX_REPLY, + _MSG_KEXDH_GEX_REQUEST, +) = range(30, 35) + +( + c_MSG_KEXDH_GEX_REQUEST_OLD, + c_MSG_KEXDH_GEX_GROUP, + c_MSG_KEXDH_GEX_INIT, + c_MSG_KEXDH_GEX_REPLY, + c_MSG_KEXDH_GEX_REQUEST, +) = [byte_chr(c) for c in range(30, 35)] + + +class KexGex: + + name = "diffie-hellman-group-exchange-sha1" + min_bits = 1024 + max_bits = 8192 + preferred_bits = 2048 + hash_algo = sha1 + + def __init__(self, transport): + self.transport = transport + self.p = None + self.q = None + self.g = None + self.x = None + self.e = None + self.f = None + self.old_style = False + + def start_kex(self, _test_old_style=False): + if self.transport.server_mode: + self.transport._expect_packet( + _MSG_KEXDH_GEX_REQUEST, _MSG_KEXDH_GEX_REQUEST_OLD + ) + return + # request a bit range: we accept (min_bits) to (max_bits), but prefer + # (preferred_bits). according to the spec, we shouldn't pull the + # minimum up above 1024. + m = Message() + if _test_old_style: + # only used for unit tests: we shouldn't ever send this + m.add_byte(c_MSG_KEXDH_GEX_REQUEST_OLD) + m.add_int(self.preferred_bits) + self.old_style = True + else: + m.add_byte(c_MSG_KEXDH_GEX_REQUEST) + m.add_int(self.min_bits) + m.add_int(self.preferred_bits) + m.add_int(self.max_bits) + self.transport._send_message(m) + self.transport._expect_packet(_MSG_KEXDH_GEX_GROUP) + + def parse_next(self, ptype, m): + if ptype == _MSG_KEXDH_GEX_REQUEST: + return self._parse_kexdh_gex_request(m) + elif ptype == _MSG_KEXDH_GEX_GROUP: + return self._parse_kexdh_gex_group(m) + elif ptype == _MSG_KEXDH_GEX_INIT: + return self._parse_kexdh_gex_init(m) + elif ptype == _MSG_KEXDH_GEX_REPLY: + return self._parse_kexdh_gex_reply(m) + elif ptype == _MSG_KEXDH_GEX_REQUEST_OLD: + return self._parse_kexdh_gex_request_old(m) + msg = "KexGex {} asked to handle packet type {:d}" + raise SSHException(msg.format(self.name, ptype)) + + # ...internals... + + def _generate_x(self): + # generate an "x" (1 < x < (p-1)/2). + q = (self.p - 1) // 2 + qnorm = util.deflate_long(q, 0) + qhbyte = byte_ord(qnorm[0]) + byte_count = len(qnorm) + qmask = 0xFF + while not (qhbyte & 0x80): + qhbyte <<= 1 + qmask >>= 1 + while True: + x_bytes = os.urandom(byte_count) + x_bytes = byte_mask(x_bytes[0], qmask) + x_bytes[1:] + x = util.inflate_long(x_bytes, 1) + if (x > 1) and (x < q): + break + self.x = x + + def _parse_kexdh_gex_request(self, m): + minbits = m.get_int() + preferredbits = m.get_int() + maxbits = m.get_int() + # smoosh the user's preferred size into our own limits + if preferredbits > self.max_bits: + preferredbits = self.max_bits + if preferredbits < self.min_bits: + preferredbits = self.min_bits + # fix min/max if they're inconsistent. technically, we could just pout + # and hang up, but there's no harm in giving them the benefit of the + # doubt and just picking a bitsize for them. + if minbits > preferredbits: + minbits = preferredbits + if maxbits < preferredbits: + maxbits = preferredbits + # now save a copy + self.min_bits = minbits + self.preferred_bits = preferredbits + self.max_bits = maxbits + # generate prime + pack = self.transport._get_modulus_pack() + if pack is None: + raise SSHException("Can't do server-side gex with no modulus pack") + self.transport._log( + DEBUG, + "Picking p ({} <= {} <= {} bits)".format( + minbits, preferredbits, maxbits + ), + ) + self.g, self.p = pack.get_modulus(minbits, preferredbits, maxbits) + m = Message() + m.add_byte(c_MSG_KEXDH_GEX_GROUP) + m.add_mpint(self.p) + m.add_mpint(self.g) + self.transport._send_message(m) + self.transport._expect_packet(_MSG_KEXDH_GEX_INIT) + + def _parse_kexdh_gex_request_old(self, m): + # same as above, but without min_bits or max_bits (used by older + # clients like putty) + self.preferred_bits = m.get_int() + # smoosh the user's preferred size into our own limits + if self.preferred_bits > self.max_bits: + self.preferred_bits = self.max_bits + if self.preferred_bits < self.min_bits: + self.preferred_bits = self.min_bits + # generate prime + pack = self.transport._get_modulus_pack() + if pack is None: + raise SSHException("Can't do server-side gex with no modulus pack") + self.transport._log( + DEBUG, "Picking p (~ {} bits)".format(self.preferred_bits) + ) + self.g, self.p = pack.get_modulus( + self.min_bits, self.preferred_bits, self.max_bits + ) + m = Message() + m.add_byte(c_MSG_KEXDH_GEX_GROUP) + m.add_mpint(self.p) + m.add_mpint(self.g) + self.transport._send_message(m) + self.transport._expect_packet(_MSG_KEXDH_GEX_INIT) + self.old_style = True + + def _parse_kexdh_gex_group(self, m): + self.p = m.get_mpint() + self.g = m.get_mpint() + # reject if p's bit length < 1024 or > 8192 + bitlen = util.bit_length(self.p) + if (bitlen < 1024) or (bitlen > 8192): + raise SSHException( + "Server-generated gex p (don't ask) is out of range " + "({} bits)".format(bitlen) + ) + self.transport._log(DEBUG, "Got server p ({} bits)".format(bitlen)) + self._generate_x() + # now compute e = g^x mod p + self.e = pow(self.g, self.x, self.p) + m = Message() + m.add_byte(c_MSG_KEXDH_GEX_INIT) + m.add_mpint(self.e) + self.transport._send_message(m) + self.transport._expect_packet(_MSG_KEXDH_GEX_REPLY) + + def _parse_kexdh_gex_init(self, m): + self.e = m.get_mpint() + if (self.e < 1) or (self.e > self.p - 1): + raise SSHException('Client kex "e" is out of range') + self._generate_x() + self.f = pow(self.g, self.x, self.p) + K = pow(self.e, self.x, self.p) + key = self.transport.get_server_key().asbytes() + # okay, build up the hash H of + # (V_C || V_S || I_C || I_S || K_S || min || n || max || p || g || e || f || K) # noqa + hm = Message() + hm.add( + self.transport.remote_version, + self.transport.local_version, + self.transport.remote_kex_init, + self.transport.local_kex_init, + key, + ) + if not self.old_style: + hm.add_int(self.min_bits) + hm.add_int(self.preferred_bits) + if not self.old_style: + hm.add_int(self.max_bits) + hm.add_mpint(self.p) + hm.add_mpint(self.g) + hm.add_mpint(self.e) + hm.add_mpint(self.f) + hm.add_mpint(K) + H = self.hash_algo(hm.asbytes()).digest() + self.transport._set_K_H(K, H) + # sign it + sig = self.transport.get_server_key().sign_ssh_data( + H, self.transport.host_key_type + ) + # send reply + m = Message() + m.add_byte(c_MSG_KEXDH_GEX_REPLY) + m.add_string(key) + m.add_mpint(self.f) + m.add_string(sig) + self.transport._send_message(m) + self.transport._activate_outbound() + + def _parse_kexdh_gex_reply(self, m): + host_key = m.get_string() + self.f = m.get_mpint() + sig = m.get_string() + if (self.f < 1) or (self.f > self.p - 1): + raise SSHException('Server kex "f" is out of range') + K = pow(self.f, self.x, self.p) + # okay, build up the hash H of + # (V_C || V_S || I_C || I_S || K_S || min || n || max || p || g || e || f || K) # noqa + hm = Message() + hm.add( + self.transport.local_version, + self.transport.remote_version, + self.transport.local_kex_init, + self.transport.remote_kex_init, + host_key, + ) + if not self.old_style: + hm.add_int(self.min_bits) + hm.add_int(self.preferred_bits) + if not self.old_style: + hm.add_int(self.max_bits) + hm.add_mpint(self.p) + hm.add_mpint(self.g) + hm.add_mpint(self.e) + hm.add_mpint(self.f) + hm.add_mpint(K) + self.transport._set_K_H(K, self.hash_algo(hm.asbytes()).digest()) + self.transport._verify_key(host_key, sig) + self.transport._activate_outbound() + + +class KexGexSHA256(KexGex): + name = "diffie-hellman-group-exchange-sha256" + hash_algo = sha256 diff --git a/paramiko/kex_group1.py b/paramiko/kex_group1.py new file mode 100644 index 0000000..f074256 --- /dev/null +++ b/paramiko/kex_group1.py @@ -0,0 +1,155 @@ +# Copyright (C) 2003-2007 Robey Pointer <robeypointer@gmail.com> +# +# This file is part of paramiko. +# +# Paramiko is free software; you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free +# Software Foundation; either version 2.1 of the License, or (at your option) +# any later version. +# +# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Paramiko; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +""" +Standard SSH key exchange ("kex" if you wanna sound cool). Diffie-Hellman of +1024 bit key halves, using a known "p" prime and "g" generator. +""" + +import os +from hashlib import sha1 + +from paramiko import util +from paramiko.common import max_byte, zero_byte, byte_chr, byte_mask +from paramiko.message import Message +from paramiko.ssh_exception import SSHException + + +_MSG_KEXDH_INIT, _MSG_KEXDH_REPLY = range(30, 32) +c_MSG_KEXDH_INIT, c_MSG_KEXDH_REPLY = [byte_chr(c) for c in range(30, 32)] + +b7fffffffffffffff = byte_chr(0x7F) + max_byte * 7 +b0000000000000000 = zero_byte * 8 + + +class KexGroup1: + + # draft-ietf-secsh-transport-09.txt, page 17 + P = 0xFFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE65381FFFFFFFFFFFFFFFF # noqa + G = 2 + + name = "diffie-hellman-group1-sha1" + hash_algo = sha1 + + def __init__(self, transport): + self.transport = transport + self.x = 0 + self.e = 0 + self.f = 0 + + def start_kex(self): + self._generate_x() + if self.transport.server_mode: + # compute f = g^x mod p, but don't send it yet + self.f = pow(self.G, self.x, self.P) + self.transport._expect_packet(_MSG_KEXDH_INIT) + return + # compute e = g^x mod p (where g=2), and send it + self.e = pow(self.G, self.x, self.P) + m = Message() + m.add_byte(c_MSG_KEXDH_INIT) + m.add_mpint(self.e) + self.transport._send_message(m) + self.transport._expect_packet(_MSG_KEXDH_REPLY) + + def parse_next(self, ptype, m): + if self.transport.server_mode and (ptype == _MSG_KEXDH_INIT): + return self._parse_kexdh_init(m) + elif not self.transport.server_mode and (ptype == _MSG_KEXDH_REPLY): + return self._parse_kexdh_reply(m) + msg = "KexGroup1 asked to handle packet type {:d}" + raise SSHException(msg.format(ptype)) + + # ...internals... + + def _generate_x(self): + # generate an "x" (1 < x < q), where q is (p-1)/2. + # p is a 128-byte (1024-bit) number, where the first 64 bits are 1. + # therefore q can be approximated as a 2^1023. we drop the subset of + # potential x where the first 63 bits are 1, because some of those + # will be larger than q (but this is a tiny tiny subset of + # potential x). + while 1: + x_bytes = os.urandom(128) + x_bytes = byte_mask(x_bytes[0], 0x7F) + x_bytes[1:] + if ( + x_bytes[:8] != b7fffffffffffffff + and x_bytes[:8] != b0000000000000000 + ): + break + self.x = util.inflate_long(x_bytes) + + def _parse_kexdh_reply(self, m): + # client mode + host_key = m.get_string() + self.f = m.get_mpint() + if (self.f < 1) or (self.f > self.P - 1): + raise SSHException('Server kex "f" is out of range') + sig = m.get_binary() + K = pow(self.f, self.x, self.P) + # okay, build up the hash H of + # (V_C || V_S || I_C || I_S || K_S || e || f || K) + hm = Message() + hm.add( + self.transport.local_version, + self.transport.remote_version, + self.transport.local_kex_init, + self.transport.remote_kex_init, + ) + hm.add_string(host_key) + hm.add_mpint(self.e) + hm.add_mpint(self.f) + hm.add_mpint(K) + self.transport._set_K_H(K, self.hash_algo(hm.asbytes()).digest()) + self.transport._verify_key(host_key, sig) + self.transport._activate_outbound() + + def _parse_kexdh_init(self, m): + # server mode + self.e = m.get_mpint() + if (self.e < 1) or (self.e > self.P - 1): + raise SSHException('Client kex "e" is out of range') + K = pow(self.e, self.x, self.P) + key = self.transport.get_server_key().asbytes() + # okay, build up the hash H of + # (V_C || V_S || I_C || I_S || K_S || e || f || K) + hm = Message() + hm.add( + self.transport.remote_version, + self.transport.local_version, + self.transport.remote_kex_init, + self.transport.local_kex_init, + ) + hm.add_string(key) + hm.add_mpint(self.e) + hm.add_mpint(self.f) + hm.add_mpint(K) + H = self.hash_algo(hm.asbytes()).digest() + self.transport._set_K_H(K, H) + # sign it + sig = self.transport.get_server_key().sign_ssh_data( + H, self.transport.host_key_type + ) + # send reply + m = Message() + m.add_byte(c_MSG_KEXDH_REPLY) + m.add_string(key) + m.add_mpint(self.f) + m.add_string(sig) + self.transport._send_message(m) + self.transport._activate_outbound() diff --git a/paramiko/kex_group14.py b/paramiko/kex_group14.py new file mode 100644 index 0000000..8dee551 --- /dev/null +++ b/paramiko/kex_group14.py @@ -0,0 +1,40 @@ +# Copyright (C) 2013 Torsten Landschoff <torsten@debian.org> +# +# This file is part of paramiko. +# +# Paramiko is free software; you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free +# Software Foundation; either version 2.1 of the License, or (at your option) +# any later version. +# +# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Paramiko; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +""" +Standard SSH key exchange ("kex" if you wanna sound cool). Diffie-Hellman of +2048 bit key halves, using a known "p" prime and "g" generator. +""" + +from paramiko.kex_group1 import KexGroup1 +from hashlib import sha1, sha256 + + +class KexGroup14(KexGroup1): + + # http://tools.ietf.org/html/rfc3526#section-3 + P = 0xFFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D670C354E4ABC9804F1746C08CA18217C32905E462E36CE3BE39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF6955817183995497CEA956AE515D2261898FA051015728E5A8AACAA68FFFFFFFFFFFFFFFF # noqa + G = 2 + + name = "diffie-hellman-group14-sha1" + hash_algo = sha1 + + +class KexGroup14SHA256(KexGroup14): + name = "diffie-hellman-group14-sha256" + hash_algo = sha256 diff --git a/paramiko/kex_group16.py b/paramiko/kex_group16.py new file mode 100644 index 0000000..c675f87 --- /dev/null +++ b/paramiko/kex_group16.py @@ -0,0 +1,35 @@ +# Copyright (C) 2019 Edgar Sousa <https://github.com/edgsousa> +# +# This file is part of paramiko. +# +# Paramiko is free software; you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free +# Software Foundation; either version 2.1 of the License, or (at your option) +# any later version. +# +# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Paramiko; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +""" +Standard SSH key exchange ("kex" if you wanna sound cool). Diffie-Hellman of +4096 bit key halves, using a known "p" prime and "g" generator. +""" + +from paramiko.kex_group1 import KexGroup1 +from hashlib import sha512 + + +class KexGroup16SHA512(KexGroup1): + name = "diffie-hellman-group16-sha512" + # http://tools.ietf.org/html/rfc3526#section-5 + P = 0xFFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D670C354E4ABC9804F1746C08CA18217C32905E462E36CE3BE39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF6955817183995497CEA956AE515D2261898FA051015728E5A8AAAC42DAD33170D04507A33A85521ABDF1CBA64ECFB850458DBEF0A8AEA71575D060C7DB3970F85A6E1E4C7ABF5AE8CDB0933D71E8C94E04A25619DCEE3D2261AD2EE6BF12FFA06D98A0864D87602733EC86A64521F2B18177B200CBBE117577A615D6C770988C0BAD946E208E24FA074E5AB3143DB5BFCE0FD108E4B82D120A92108011A723C12A787E6D788719A10BDBA5B2699C327186AF4E23C1A946834B6150BDA2583E9CA2AD44CE8DBBBC2DB04DE8EF92E8EFC141FBECAA6287C59474E6BC05D99B2964FA090C3A2233BA186515BE7ED1F612970CEE2D7AFB81BDD762170481CD0069127D5B05AA993B4EA988D8FDDC186FFB7DC90A6C08F4DF435C934063199FFFFFFFFFFFFFFFF # noqa + G = 2 + + name = "diffie-hellman-group16-sha512" + hash_algo = sha512 diff --git a/paramiko/kex_gss.py b/paramiko/kex_gss.py new file mode 100644 index 0000000..2a5f29e --- /dev/null +++ b/paramiko/kex_gss.py @@ -0,0 +1,686 @@ +# Copyright (C) 2003-2007 Robey Pointer <robeypointer@gmail.com> +# Copyright (C) 2013-2014 science + computing ag +# Author: Sebastian Deiss <sebastian.deiss@t-online.de> +# +# +# This file is part of paramiko. +# +# Paramiko is free software; you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free +# Software Foundation; either version 2.1 of the License, or (at your option) +# any later version. +# +# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Paramiko; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + + +""" +This module provides GSS-API / SSPI Key Exchange as defined in :rfc:`4462`. + +.. note:: Credential delegation is not supported in server mode. + +.. note:: + `RFC 4462 Section 2.2 + <https://tools.ietf.org/html/rfc4462.html#section-2.2>`_ says we are not + required to implement GSS-API error messages. Thus, in many methods within + this module, if an error occurs an exception will be thrown and the + connection will be terminated. + +.. seealso:: :doc:`/api/ssh_gss` + +.. versionadded:: 1.15 +""" + +import os +from hashlib import sha1 + +from paramiko.common import ( + DEBUG, + max_byte, + zero_byte, + byte_chr, + byte_mask, + byte_ord, +) +from paramiko import util +from paramiko.message import Message +from paramiko.ssh_exception import SSHException + + +( + MSG_KEXGSS_INIT, + MSG_KEXGSS_CONTINUE, + MSG_KEXGSS_COMPLETE, + MSG_KEXGSS_HOSTKEY, + MSG_KEXGSS_ERROR, +) = range(30, 35) +(MSG_KEXGSS_GROUPREQ, MSG_KEXGSS_GROUP) = range(40, 42) +( + c_MSG_KEXGSS_INIT, + c_MSG_KEXGSS_CONTINUE, + c_MSG_KEXGSS_COMPLETE, + c_MSG_KEXGSS_HOSTKEY, + c_MSG_KEXGSS_ERROR, +) = [byte_chr(c) for c in range(30, 35)] +(c_MSG_KEXGSS_GROUPREQ, c_MSG_KEXGSS_GROUP) = [ + byte_chr(c) for c in range(40, 42) +] + + +class KexGSSGroup1: + """ + GSS-API / SSPI Authenticated Diffie-Hellman Key Exchange as defined in `RFC + 4462 Section 2 <https://tools.ietf.org/html/rfc4462.html#section-2>`_ + """ + + # draft-ietf-secsh-transport-09.txt, page 17 + P = 0xFFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE65381FFFFFFFFFFFFFFFF # noqa + G = 2 + b7fffffffffffffff = byte_chr(0x7F) + max_byte * 7 # noqa + b0000000000000000 = zero_byte * 8 # noqa + NAME = "gss-group1-sha1-toWM5Slw5Ew8Mqkay+al2g==" + + def __init__(self, transport): + self.transport = transport + self.kexgss = self.transport.kexgss_ctxt + self.gss_host = None + self.x = 0 + self.e = 0 + self.f = 0 + + def start_kex(self): + """ + Start the GSS-API / SSPI Authenticated Diffie-Hellman Key Exchange. + """ + self._generate_x() + if self.transport.server_mode: + # compute f = g^x mod p, but don't send it yet + self.f = pow(self.G, self.x, self.P) + self.transport._expect_packet(MSG_KEXGSS_INIT) + return + # compute e = g^x mod p (where g=2), and send it + self.e = pow(self.G, self.x, self.P) + # Initialize GSS-API Key Exchange + self.gss_host = self.transport.gss_host + m = Message() + m.add_byte(c_MSG_KEXGSS_INIT) + m.add_string(self.kexgss.ssh_init_sec_context(target=self.gss_host)) + m.add_mpint(self.e) + self.transport._send_message(m) + self.transport._expect_packet( + MSG_KEXGSS_HOSTKEY, + MSG_KEXGSS_CONTINUE, + MSG_KEXGSS_COMPLETE, + MSG_KEXGSS_ERROR, + ) + + def parse_next(self, ptype, m): + """ + Parse the next packet. + + :param ptype: The (string) type of the incoming packet + :param `.Message` m: The packet content + """ + if self.transport.server_mode and (ptype == MSG_KEXGSS_INIT): + return self._parse_kexgss_init(m) + elif not self.transport.server_mode and (ptype == MSG_KEXGSS_HOSTKEY): + return self._parse_kexgss_hostkey(m) + elif self.transport.server_mode and (ptype == MSG_KEXGSS_CONTINUE): + return self._parse_kexgss_continue(m) + elif not self.transport.server_mode and (ptype == MSG_KEXGSS_COMPLETE): + return self._parse_kexgss_complete(m) + elif ptype == MSG_KEXGSS_ERROR: + return self._parse_kexgss_error(m) + msg = "GSS KexGroup1 asked to handle packet type {:d}" + raise SSHException(msg.format(ptype)) + + # ## internals... + + def _generate_x(self): + """ + generate an "x" (1 < x < q), where q is (p-1)/2. + p is a 128-byte (1024-bit) number, where the first 64 bits are 1. + therefore q can be approximated as a 2^1023. we drop the subset of + potential x where the first 63 bits are 1, because some of those will + be larger than q (but this is a tiny tiny subset of potential x). + """ + while 1: + x_bytes = os.urandom(128) + x_bytes = byte_mask(x_bytes[0], 0x7F) + x_bytes[1:] + first = x_bytes[:8] + if first not in (self.b7fffffffffffffff, self.b0000000000000000): + break + self.x = util.inflate_long(x_bytes) + + def _parse_kexgss_hostkey(self, m): + """ + Parse the SSH2_MSG_KEXGSS_HOSTKEY message (client mode). + + :param `.Message` m: The content of the SSH2_MSG_KEXGSS_HOSTKEY message + """ + # client mode + host_key = m.get_string() + self.transport.host_key = host_key + sig = m.get_string() + self.transport._verify_key(host_key, sig) + self.transport._expect_packet(MSG_KEXGSS_CONTINUE, MSG_KEXGSS_COMPLETE) + + def _parse_kexgss_continue(self, m): + """ + Parse the SSH2_MSG_KEXGSS_CONTINUE message. + + :param `.Message` m: The content of the SSH2_MSG_KEXGSS_CONTINUE + message + """ + if not self.transport.server_mode: + srv_token = m.get_string() + m = Message() + m.add_byte(c_MSG_KEXGSS_CONTINUE) + m.add_string( + self.kexgss.ssh_init_sec_context( + target=self.gss_host, recv_token=srv_token + ) + ) + self.transport.send_message(m) + self.transport._expect_packet( + MSG_KEXGSS_CONTINUE, MSG_KEXGSS_COMPLETE, MSG_KEXGSS_ERROR + ) + else: + pass + + def _parse_kexgss_complete(self, m): + """ + Parse the SSH2_MSG_KEXGSS_COMPLETE message (client mode). + + :param `.Message` m: The content of the + SSH2_MSG_KEXGSS_COMPLETE message + """ + # client mode + if self.transport.host_key is None: + self.transport.host_key = NullHostKey() + self.f = m.get_mpint() + if (self.f < 1) or (self.f > self.P - 1): + raise SSHException('Server kex "f" is out of range') + mic_token = m.get_string() + # This must be TRUE, if there is a GSS-API token in this message. + bool = m.get_boolean() + srv_token = None + if bool: + srv_token = m.get_string() + K = pow(self.f, self.x, self.P) + # okay, build up the hash H of + # (V_C || V_S || I_C || I_S || K_S || e || f || K) + hm = Message() + hm.add( + self.transport.local_version, + self.transport.remote_version, + self.transport.local_kex_init, + self.transport.remote_kex_init, + ) + hm.add_string(self.transport.host_key.__str__()) + hm.add_mpint(self.e) + hm.add_mpint(self.f) + hm.add_mpint(K) + H = sha1(str(hm)).digest() + self.transport._set_K_H(K, H) + if srv_token is not None: + self.kexgss.ssh_init_sec_context( + target=self.gss_host, recv_token=srv_token + ) + self.kexgss.ssh_check_mic(mic_token, H) + else: + self.kexgss.ssh_check_mic(mic_token, H) + self.transport.gss_kex_used = True + self.transport._activate_outbound() + + def _parse_kexgss_init(self, m): + """ + Parse the SSH2_MSG_KEXGSS_INIT message (server mode). + + :param `.Message` m: The content of the SSH2_MSG_KEXGSS_INIT message + """ + # server mode + client_token = m.get_string() + self.e = m.get_mpint() + if (self.e < 1) or (self.e > self.P - 1): + raise SSHException('Client kex "e" is out of range') + K = pow(self.e, self.x, self.P) + self.transport.host_key = NullHostKey() + key = self.transport.host_key.__str__() + # okay, build up the hash H of + # (V_C || V_S || I_C || I_S || K_S || e || f || K) + hm = Message() + hm.add( + self.transport.remote_version, + self.transport.local_version, + self.transport.remote_kex_init, + self.transport.local_kex_init, + ) + hm.add_string(key) + hm.add_mpint(self.e) + hm.add_mpint(self.f) + hm.add_mpint(K) + H = sha1(hm.asbytes()).digest() + self.transport._set_K_H(K, H) + srv_token = self.kexgss.ssh_accept_sec_context( + self.gss_host, client_token + ) + m = Message() + if self.kexgss._gss_srv_ctxt_status: + mic_token = self.kexgss.ssh_get_mic( + self.transport.session_id, gss_kex=True + ) + m.add_byte(c_MSG_KEXGSS_COMPLETE) + m.add_mpint(self.f) + m.add_string(mic_token) + if srv_token is not None: + m.add_boolean(True) + m.add_string(srv_token) + else: + m.add_boolean(False) + self.transport._send_message(m) + self.transport.gss_kex_used = True + self.transport._activate_outbound() + else: + m.add_byte(c_MSG_KEXGSS_CONTINUE) + m.add_string(srv_token) + self.transport._send_message(m) + self.transport._expect_packet( + MSG_KEXGSS_CONTINUE, MSG_KEXGSS_COMPLETE, MSG_KEXGSS_ERROR + ) + + def _parse_kexgss_error(self, m): + """ + Parse the SSH2_MSG_KEXGSS_ERROR message (client mode). + The server may send a GSS-API error message. if it does, we display + the error by throwing an exception (client mode). + + :param `.Message` m: The content of the SSH2_MSG_KEXGSS_ERROR message + :raise SSHException: Contains GSS-API major and minor status as well as + the error message and the language tag of the + message + """ + maj_status = m.get_int() + min_status = m.get_int() + err_msg = m.get_string() + m.get_string() # we don't care about the language! + raise SSHException( + """GSS-API Error: +Major Status: {} +Minor Status: {} +Error Message: {} +""".format( + maj_status, min_status, err_msg + ) + ) + + +class KexGSSGroup14(KexGSSGroup1): + """ + GSS-API / SSPI Authenticated Diffie-Hellman Group14 Key Exchange as defined + in `RFC 4462 Section 2 + <https://tools.ietf.org/html/rfc4462.html#section-2>`_ + """ + + P = 0xFFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D670C354E4ABC9804F1746C08CA18217C32905E462E36CE3BE39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF6955817183995497CEA956AE515D2261898FA051015728E5A8AACAA68FFFFFFFFFFFFFFFF # noqa + G = 2 + NAME = "gss-group14-sha1-toWM5Slw5Ew8Mqkay+al2g==" + + +class KexGSSGex: + """ + GSS-API / SSPI Authenticated Diffie-Hellman Group Exchange as defined in + `RFC 4462 Section 2 <https://tools.ietf.org/html/rfc4462.html#section-2>`_ + """ + + NAME = "gss-gex-sha1-toWM5Slw5Ew8Mqkay+al2g==" + min_bits = 1024 + max_bits = 8192 + preferred_bits = 2048 + + def __init__(self, transport): + self.transport = transport + self.kexgss = self.transport.kexgss_ctxt + self.gss_host = None + self.p = None + self.q = None + self.g = None + self.x = None + self.e = None + self.f = None + self.old_style = False + + def start_kex(self): + """ + Start the GSS-API / SSPI Authenticated Diffie-Hellman Group Exchange + """ + if self.transport.server_mode: + self.transport._expect_packet(MSG_KEXGSS_GROUPREQ) + return + # request a bit range: we accept (min_bits) to (max_bits), but prefer + # (preferred_bits). according to the spec, we shouldn't pull the + # minimum up above 1024. + self.gss_host = self.transport.gss_host + m = Message() + m.add_byte(c_MSG_KEXGSS_GROUPREQ) + m.add_int(self.min_bits) + m.add_int(self.preferred_bits) + m.add_int(self.max_bits) + self.transport._send_message(m) + self.transport._expect_packet(MSG_KEXGSS_GROUP) + + def parse_next(self, ptype, m): + """ + Parse the next packet. + + :param ptype: The (string) type of the incoming packet + :param `.Message` m: The packet content + """ + if ptype == MSG_KEXGSS_GROUPREQ: + return self._parse_kexgss_groupreq(m) + elif ptype == MSG_KEXGSS_GROUP: + return self._parse_kexgss_group(m) + elif ptype == MSG_KEXGSS_INIT: + return self._parse_kexgss_gex_init(m) + elif ptype == MSG_KEXGSS_HOSTKEY: + return self._parse_kexgss_hostkey(m) + elif ptype == MSG_KEXGSS_CONTINUE: + return self._parse_kexgss_continue(m) + elif ptype == MSG_KEXGSS_COMPLETE: + return self._parse_kexgss_complete(m) + elif ptype == MSG_KEXGSS_ERROR: + return self._parse_kexgss_error(m) + msg = "KexGex asked to handle packet type {:d}" + raise SSHException(msg.format(ptype)) + + # ## internals... + + def _generate_x(self): + # generate an "x" (1 < x < (p-1)/2). + q = (self.p - 1) // 2 + qnorm = util.deflate_long(q, 0) + qhbyte = byte_ord(qnorm[0]) + byte_count = len(qnorm) + qmask = 0xFF + while not (qhbyte & 0x80): + qhbyte <<= 1 + qmask >>= 1 + while True: + x_bytes = os.urandom(byte_count) + x_bytes = byte_mask(x_bytes[0], qmask) + x_bytes[1:] + x = util.inflate_long(x_bytes, 1) + if (x > 1) and (x < q): + break + self.x = x + + def _parse_kexgss_groupreq(self, m): + """ + Parse the SSH2_MSG_KEXGSS_GROUPREQ message (server mode). + + :param `.Message` m: The content of the + SSH2_MSG_KEXGSS_GROUPREQ message + """ + minbits = m.get_int() + preferredbits = m.get_int() + maxbits = m.get_int() + # smoosh the user's preferred size into our own limits + if preferredbits > self.max_bits: + preferredbits = self.max_bits + if preferredbits < self.min_bits: + preferredbits = self.min_bits + # fix min/max if they're inconsistent. technically, we could just pout + # and hang up, but there's no harm in giving them the benefit of the + # doubt and just picking a bitsize for them. + if minbits > preferredbits: + minbits = preferredbits + if maxbits < preferredbits: + maxbits = preferredbits + # now save a copy + self.min_bits = minbits + self.preferred_bits = preferredbits + self.max_bits = maxbits + # generate prime + pack = self.transport._get_modulus_pack() + if pack is None: + raise SSHException("Can't do server-side gex with no modulus pack") + self.transport._log( + DEBUG, # noqa + "Picking p ({} <= {} <= {} bits)".format( + minbits, preferredbits, maxbits + ), + ) + self.g, self.p = pack.get_modulus(minbits, preferredbits, maxbits) + m = Message() + m.add_byte(c_MSG_KEXGSS_GROUP) + m.add_mpint(self.p) + m.add_mpint(self.g) + self.transport._send_message(m) + self.transport._expect_packet(MSG_KEXGSS_INIT) + + def _parse_kexgss_group(self, m): + """ + Parse the SSH2_MSG_KEXGSS_GROUP message (client mode). + + :param `Message` m: The content of the SSH2_MSG_KEXGSS_GROUP message + """ + self.p = m.get_mpint() + self.g = m.get_mpint() + # reject if p's bit length < 1024 or > 8192 + bitlen = util.bit_length(self.p) + if (bitlen < 1024) or (bitlen > 8192): + raise SSHException( + "Server-generated gex p (don't ask) is out of range " + "({} bits)".format(bitlen) + ) + self.transport._log( + DEBUG, "Got server p ({} bits)".format(bitlen) + ) # noqa + self._generate_x() + # now compute e = g^x mod p + self.e = pow(self.g, self.x, self.p) + m = Message() + m.add_byte(c_MSG_KEXGSS_INIT) + m.add_string(self.kexgss.ssh_init_sec_context(target=self.gss_host)) + m.add_mpint(self.e) + self.transport._send_message(m) + self.transport._expect_packet( + MSG_KEXGSS_HOSTKEY, + MSG_KEXGSS_CONTINUE, + MSG_KEXGSS_COMPLETE, + MSG_KEXGSS_ERROR, + ) + + def _parse_kexgss_gex_init(self, m): + """ + Parse the SSH2_MSG_KEXGSS_INIT message (server mode). + + :param `Message` m: The content of the SSH2_MSG_KEXGSS_INIT message + """ + client_token = m.get_string() + self.e = m.get_mpint() + if (self.e < 1) or (self.e > self.p - 1): + raise SSHException('Client kex "e" is out of range') + self._generate_x() + self.f = pow(self.g, self.x, self.p) + K = pow(self.e, self.x, self.p) + self.transport.host_key = NullHostKey() + key = self.transport.host_key.__str__() + # okay, build up the hash H of + # (V_C || V_S || I_C || I_S || K_S || min || n || max || p || g || e || f || K) # noqa + hm = Message() + hm.add( + self.transport.remote_version, + self.transport.local_version, + self.transport.remote_kex_init, + self.transport.local_kex_init, + key, + ) + hm.add_int(self.min_bits) + hm.add_int(self.preferred_bits) + hm.add_int(self.max_bits) + hm.add_mpint(self.p) + hm.add_mpint(self.g) + hm.add_mpint(self.e) + hm.add_mpint(self.f) + hm.add_mpint(K) + H = sha1(hm.asbytes()).digest() + self.transport._set_K_H(K, H) + srv_token = self.kexgss.ssh_accept_sec_context( + self.gss_host, client_token + ) + m = Message() + if self.kexgss._gss_srv_ctxt_status: + mic_token = self.kexgss.ssh_get_mic( + self.transport.session_id, gss_kex=True + ) + m.add_byte(c_MSG_KEXGSS_COMPLETE) + m.add_mpint(self.f) + m.add_string(mic_token) + if srv_token is not None: + m.add_boolean(True) + m.add_string(srv_token) + else: + m.add_boolean(False) + self.transport._send_message(m) + self.transport.gss_kex_used = True + self.transport._activate_outbound() + else: + m.add_byte(c_MSG_KEXGSS_CONTINUE) + m.add_string(srv_token) + self.transport._send_message(m) + self.transport._expect_packet( + MSG_KEXGSS_CONTINUE, MSG_KEXGSS_COMPLETE, MSG_KEXGSS_ERROR + ) + + def _parse_kexgss_hostkey(self, m): + """ + Parse the SSH2_MSG_KEXGSS_HOSTKEY message (client mode). + + :param `Message` m: The content of the SSH2_MSG_KEXGSS_HOSTKEY message + """ + # client mode + host_key = m.get_string() + self.transport.host_key = host_key + sig = m.get_string() + self.transport._verify_key(host_key, sig) + self.transport._expect_packet(MSG_KEXGSS_CONTINUE, MSG_KEXGSS_COMPLETE) + + def _parse_kexgss_continue(self, m): + """ + Parse the SSH2_MSG_KEXGSS_CONTINUE message. + + :param `Message` m: The content of the SSH2_MSG_KEXGSS_CONTINUE message + """ + if not self.transport.server_mode: + srv_token = m.get_string() + m = Message() + m.add_byte(c_MSG_KEXGSS_CONTINUE) + m.add_string( + self.kexgss.ssh_init_sec_context( + target=self.gss_host, recv_token=srv_token + ) + ) + self.transport.send_message(m) + self.transport._expect_packet( + MSG_KEXGSS_CONTINUE, MSG_KEXGSS_COMPLETE, MSG_KEXGSS_ERROR + ) + else: + pass + + def _parse_kexgss_complete(self, m): + """ + Parse the SSH2_MSG_KEXGSS_COMPLETE message (client mode). + + :param `Message` m: The content of the SSH2_MSG_KEXGSS_COMPLETE message + """ + if self.transport.host_key is None: + self.transport.host_key = NullHostKey() + self.f = m.get_mpint() + mic_token = m.get_string() + # This must be TRUE, if there is a GSS-API token in this message. + bool = m.get_boolean() + srv_token = None + if bool: + srv_token = m.get_string() + if (self.f < 1) or (self.f > self.p - 1): + raise SSHException('Server kex "f" is out of range') + K = pow(self.f, self.x, self.p) + # okay, build up the hash H of + # (V_C || V_S || I_C || I_S || K_S || min || n || max || p || g || e || f || K) # noqa + hm = Message() + hm.add( + self.transport.local_version, + self.transport.remote_version, + self.transport.local_kex_init, + self.transport.remote_kex_init, + self.transport.host_key.__str__(), + ) + if not self.old_style: + hm.add_int(self.min_bits) + hm.add_int(self.preferred_bits) + if not self.old_style: + hm.add_int(self.max_bits) + hm.add_mpint(self.p) + hm.add_mpint(self.g) + hm.add_mpint(self.e) + hm.add_mpint(self.f) + hm.add_mpint(K) + H = sha1(hm.asbytes()).digest() + self.transport._set_K_H(K, H) + if srv_token is not None: + self.kexgss.ssh_init_sec_context( + target=self.gss_host, recv_token=srv_token + ) + self.kexgss.ssh_check_mic(mic_token, H) + else: + self.kexgss.ssh_check_mic(mic_token, H) + self.transport.gss_kex_used = True + self.transport._activate_outbound() + + def _parse_kexgss_error(self, m): + """ + Parse the SSH2_MSG_KEXGSS_ERROR message (client mode). + The server may send a GSS-API error message. if it does, we display + the error by throwing an exception (client mode). + + :param `Message` m: The content of the SSH2_MSG_KEXGSS_ERROR message + :raise SSHException: Contains GSS-API major and minor status as well as + the error message and the language tag of the + message + """ + maj_status = m.get_int() + min_status = m.get_int() + err_msg = m.get_string() + m.get_string() # we don't care about the language (lang_tag)! + raise SSHException( + """GSS-API Error: +Major Status: {} +Minor Status: {} +Error Message: {} +""".format( + maj_status, min_status, err_msg + ) + ) + + +class NullHostKey: + """ + This class represents the Null Host Key for GSS-API Key Exchange as defined + in `RFC 4462 Section 5 + <https://tools.ietf.org/html/rfc4462.html#section-5>`_ + """ + + def __init__(self): + self.key = "" + + def __str__(self): + return self.key + + def get_name(self): + return self.key diff --git a/paramiko/message.py b/paramiko/message.py new file mode 100644 index 0000000..8c2b3bd --- /dev/null +++ b/paramiko/message.py @@ -0,0 +1,318 @@ +# Copyright (C) 2003-2007 Robey Pointer <robeypointer@gmail.com> +# +# This file is part of paramiko. +# +# Paramiko is free software; you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free +# Software Foundation; either version 2.1 of the License, or (at your option) +# any later version. +# +# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Paramiko; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +""" +Implementation of an SSH2 "message". +""" + +import struct +from io import BytesIO + +from paramiko import util +from paramiko.common import zero_byte, max_byte, one_byte +from paramiko.util import u + + +class Message: + """ + An SSH2 message is a stream of bytes that encodes some combination of + strings, integers, bools, and infinite-precision integers. This class + builds or breaks down such a byte stream. + + Normally you don't need to deal with anything this low-level, but it's + exposed for people implementing custom extensions, or features that + paramiko doesn't support yet. + """ + + big_int = 0xFF000000 + + def __init__(self, content=None): + """ + Create a new SSH2 message. + + :param bytes content: + the byte stream to use as the message content (passed in only when + decomposing a message). + """ + if content is not None: + self.packet = BytesIO(content) + else: + self.packet = BytesIO() + + def __bytes__(self): + return self.asbytes() + + def __repr__(self): + """ + Returns a string representation of this object, for debugging. + """ + return "paramiko.Message(" + repr(self.packet.getvalue()) + ")" + + # TODO 4.0: just merge into __bytes__ (everywhere) + def asbytes(self): + """ + Return the byte stream content of this Message, as a `bytes`. + """ + return self.packet.getvalue() + + def rewind(self): + """ + Rewind the message to the beginning as if no items had been parsed + out of it yet. + """ + self.packet.seek(0) + + def get_remainder(self): + """ + Return the `bytes` of this message that haven't already been parsed and + returned. + """ + position = self.packet.tell() + remainder = self.packet.read() + self.packet.seek(position) + return remainder + + def get_so_far(self): + """ + Returns the `bytes` of this message that have been parsed and + returned. The string passed into a message's constructor can be + regenerated by concatenating ``get_so_far`` and `get_remainder`. + """ + position = self.packet.tell() + self.rewind() + return self.packet.read(position) + + def get_bytes(self, n): + """ + Return the next ``n`` bytes of the message, without decomposing into an + int, decoded string, etc. Just the raw bytes are returned. Returns a + string of ``n`` zero bytes if there weren't ``n`` bytes remaining in + the message. + """ + b = self.packet.read(n) + max_pad_size = 1 << 20 # Limit padding to 1 MB + if len(b) < n < max_pad_size: + return b + zero_byte * (n - len(b)) + return b + + def get_byte(self): + """ + Return the next byte of the message, without decomposing it. This + is equivalent to `get_bytes(1) <get_bytes>`. + + :return: + the next (`bytes`) byte of the message, or ``b'\000'`` if there + aren't any bytes remaining. + """ + return self.get_bytes(1) + + def get_boolean(self): + """ + Fetch a boolean from the stream. + """ + b = self.get_bytes(1) + return b != zero_byte + + def get_adaptive_int(self): + """ + Fetch an int from the stream. + + :return: a 32-bit unsigned `int`. + """ + byte = self.get_bytes(1) + if byte == max_byte: + return util.inflate_long(self.get_binary()) + byte += self.get_bytes(3) + return struct.unpack(">I", byte)[0] + + def get_int(self): + """ + Fetch an int from the stream. + """ + return struct.unpack(">I", self.get_bytes(4))[0] + + def get_int64(self): + """ + Fetch a 64-bit int from the stream. + + :return: a 64-bit unsigned integer (`int`). + """ + return struct.unpack(">Q", self.get_bytes(8))[0] + + def get_mpint(self): + """ + Fetch a long int (mpint) from the stream. + + :return: an arbitrary-length integer (`int`). + """ + return util.inflate_long(self.get_binary()) + + # TODO 4.0: depending on where this is used internally or downstream, force + # users to specify get_binary instead and delete this. + def get_string(self): + """ + Fetch a "string" from the stream. This will actually be a `bytes` + object, and may contain unprintable characters. (It's not unheard of + for a string to contain another byte-stream message.) + """ + return self.get_bytes(self.get_int()) + + # TODO 4.0: also consider having this take over the get_string name, and + # remove this name instead. + def get_text(self): + """ + Fetch a Unicode string from the stream. + + This currently operates by attempting to encode the next "string" as + ``utf-8``. + """ + return u(self.get_string()) + + def get_binary(self): + """ + Alias for `get_string` (obtains a bytestring). + """ + return self.get_bytes(self.get_int()) + + def get_list(self): + """ + Fetch a list of `strings <str>` from the stream. + + These are trivially encoded as comma-separated values in a string. + """ + return self.get_text().split(",") + + def add_bytes(self, b): + """ + Write bytes to the stream, without any formatting. + + :param bytes b: bytes to add + """ + self.packet.write(b) + return self + + def add_byte(self, b): + """ + Write a single byte to the stream, without any formatting. + + :param bytes b: byte to add + """ + self.packet.write(b) + return self + + def add_boolean(self, b): + """ + Add a boolean value to the stream. + + :param bool b: boolean value to add + """ + if b: + self.packet.write(one_byte) + else: + self.packet.write(zero_byte) + return self + + def add_int(self, n): + """ + Add an integer to the stream. + + :param int n: integer to add + """ + self.packet.write(struct.pack(">I", n)) + return self + + def add_adaptive_int(self, n): + """ + Add an integer to the stream. + + :param int n: integer to add + """ + if n >= Message.big_int: + self.packet.write(max_byte) + self.add_string(util.deflate_long(n)) + else: + self.packet.write(struct.pack(">I", n)) + return self + + def add_int64(self, n): + """ + Add a 64-bit int to the stream. + + :param int n: long int to add + """ + self.packet.write(struct.pack(">Q", n)) + return self + + def add_mpint(self, z): + """ + Add a long int to the stream, encoded as an infinite-precision + integer. This method only works on positive numbers. + + :param int z: long int to add + """ + self.add_string(util.deflate_long(z)) + return self + + # TODO: see the TODO for get_string/get_text/et al, this should change + # to match. + def add_string(self, s): + """ + Add a bytestring to the stream. + + :param byte s: bytestring to add + """ + s = util.asbytes(s) + self.add_int(len(s)) + self.packet.write(s) + return self + + def add_list(self, l): # noqa: E741 + """ + Add a list of strings to the stream. They are encoded identically to + a single string of values separated by commas. (Yes, really, that's + how SSH2 does it.) + + :param l: list of strings to add + """ + self.add_string(",".join(l)) + return self + + def _add(self, i): + if type(i) is bool: + return self.add_boolean(i) + elif isinstance(i, int): + return self.add_adaptive_int(i) + elif type(i) is list: + return self.add_list(i) + else: + return self.add_string(i) + + # TODO: this would never have worked for unicode strings under Python 3, + # guessing nobody/nothing ever used it for that purpose? + def add(self, *seq): + """ + Add a sequence of items to the stream. The values are encoded based + on their type: bytes, str, int, bool, or list. + + .. warning:: + Longs are encoded non-deterministically. Don't use this method. + + :param seq: the sequence of items + """ + for item in seq: + self._add(item) diff --git a/paramiko/packet.py b/paramiko/packet.py new file mode 100644 index 0000000..e40355e --- /dev/null +++ b/paramiko/packet.py @@ -0,0 +1,634 @@ +# Copyright (C) 2003-2007 Robey Pointer <robeypointer@gmail.com> +# +# This file is part of paramiko. +# +# Paramiko is free software; you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free +# Software Foundation; either version 2.1 of the License, or (at your option) +# any later version. +# +# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Paramiko; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +""" +Packet handling +""" + +import errno +import os +import socket +import struct +import threading +import time +from hmac import HMAC + +from paramiko import util +from paramiko.common import ( + linefeed_byte, + cr_byte_value, + MSG_NAMES, + DEBUG, + xffffffff, + zero_byte, + byte_ord, +) +from paramiko.util import u +from paramiko.ssh_exception import SSHException, ProxyCommandFailure +from paramiko.message import Message + + +def compute_hmac(key, message, digest_class): + return HMAC(key, message, digest_class).digest() + + +class NeedRekeyException(Exception): + """ + Exception indicating a rekey is needed. + """ + + pass + + +def first_arg(e): + arg = None + if type(e.args) is tuple and len(e.args) > 0: + arg = e.args[0] + return arg + + +class Packetizer: + """ + Implementation of the base SSH packet protocol. + """ + + # READ the secsh RFC's before raising these values. if anything, + # they should probably be lower. + REKEY_PACKETS = pow(2, 29) + REKEY_BYTES = pow(2, 29) + + # Allow receiving this many packets after a re-key request before + # terminating + REKEY_PACKETS_OVERFLOW_MAX = pow(2, 29) + # Allow receiving this many bytes after a re-key request before terminating + REKEY_BYTES_OVERFLOW_MAX = pow(2, 29) + + def __init__(self, socket): + self.__socket = socket + self.__logger = None + self.__closed = False + self.__dump_packets = False + self.__need_rekey = False + self.__init_count = 0 + self.__remainder = bytes() + + # used for noticing when to re-key: + self.__sent_bytes = 0 + self.__sent_packets = 0 + self.__received_bytes = 0 + self.__received_packets = 0 + self.__received_bytes_overflow = 0 + self.__received_packets_overflow = 0 + + # current inbound/outbound ciphering: + self.__block_size_out = 8 + self.__block_size_in = 8 + self.__mac_size_out = 0 + self.__mac_size_in = 0 + self.__block_engine_out = None + self.__block_engine_in = None + self.__sdctr_out = False + self.__mac_engine_out = None + self.__mac_engine_in = None + self.__mac_key_out = bytes() + self.__mac_key_in = bytes() + self.__compress_engine_out = None + self.__compress_engine_in = None + self.__sequence_number_out = 0 + self.__sequence_number_in = 0 + self.__etm_out = False + self.__etm_in = False + + # lock around outbound writes (packet computation) + self.__write_lock = threading.RLock() + + # keepalives: + self.__keepalive_interval = 0 + self.__keepalive_last = time.time() + self.__keepalive_callback = None + + self.__timer = None + self.__handshake_complete = False + self.__timer_expired = False + + @property + def closed(self): + return self.__closed + + def set_log(self, log): + """ + Set the Python log object to use for logging. + """ + self.__logger = log + + def set_outbound_cipher( + self, + block_engine, + block_size, + mac_engine, + mac_size, + mac_key, + sdctr=False, + etm=False, + ): + """ + Switch outbound data cipher. + :param etm: Set encrypt-then-mac from OpenSSH + """ + self.__block_engine_out = block_engine + self.__sdctr_out = sdctr + self.__block_size_out = block_size + self.__mac_engine_out = mac_engine + self.__mac_size_out = mac_size + self.__mac_key_out = mac_key + self.__sent_bytes = 0 + self.__sent_packets = 0 + self.__etm_out = etm + # wait until the reset happens in both directions before clearing + # rekey flag + self.__init_count |= 1 + if self.__init_count == 3: + self.__init_count = 0 + self.__need_rekey = False + + def set_inbound_cipher( + self, + block_engine, + block_size, + mac_engine, + mac_size, + mac_key, + etm=False, + ): + """ + Switch inbound data cipher. + :param etm: Set encrypt-then-mac from OpenSSH + """ + self.__block_engine_in = block_engine + self.__block_size_in = block_size + self.__mac_engine_in = mac_engine + self.__mac_size_in = mac_size + self.__mac_key_in = mac_key + self.__received_bytes = 0 + self.__received_packets = 0 + self.__received_bytes_overflow = 0 + self.__received_packets_overflow = 0 + self.__etm_in = etm + # wait until the reset happens in both directions before clearing + # rekey flag + self.__init_count |= 2 + if self.__init_count == 3: + self.__init_count = 0 + self.__need_rekey = False + + def set_outbound_compressor(self, compressor): + self.__compress_engine_out = compressor + + def set_inbound_compressor(self, compressor): + self.__compress_engine_in = compressor + + def close(self): + self.__closed = True + self.__socket.close() + + def set_hexdump(self, hexdump): + self.__dump_packets = hexdump + + def get_hexdump(self): + return self.__dump_packets + + def get_mac_size_in(self): + return self.__mac_size_in + + def get_mac_size_out(self): + return self.__mac_size_out + + def need_rekey(self): + """ + Returns ``True`` if a new set of keys needs to be negotiated. This + will be triggered during a packet read or write, so it should be + checked after every read or write, or at least after every few. + """ + return self.__need_rekey + + def set_keepalive(self, interval, callback): + """ + Turn on/off the callback keepalive. If ``interval`` seconds pass with + no data read from or written to the socket, the callback will be + executed and the timer will be reset. + """ + self.__keepalive_interval = interval + self.__keepalive_callback = callback + self.__keepalive_last = time.time() + + def read_timer(self): + self.__timer_expired = True + + def start_handshake(self, timeout): + """ + Tells `Packetizer` that the handshake process started. + Starts a book keeping timer that can signal a timeout in the + handshake process. + + :param float timeout: amount of seconds to wait before timing out + """ + if not self.__timer: + self.__timer = threading.Timer(float(timeout), self.read_timer) + self.__timer.start() + + def handshake_timed_out(self): + """ + Checks if the handshake has timed out. + + If `start_handshake` wasn't called before the call to this function, + the return value will always be `False`. If the handshake completed + before a timeout was reached, the return value will be `False` + + :return: handshake time out status, as a `bool` + """ + if not self.__timer: + return False + if self.__handshake_complete: + return False + return self.__timer_expired + + def complete_handshake(self): + """ + Tells `Packetizer` that the handshake has completed. + """ + if self.__timer: + self.__timer.cancel() + self.__timer_expired = False + self.__handshake_complete = True + + def read_all(self, n, check_rekey=False): + """ + Read as close to N bytes as possible, blocking as long as necessary. + + :param int n: number of bytes to read + :return: the data read, as a `str` + + :raises: + ``EOFError`` -- if the socket was closed before all the bytes could + be read + """ + out = bytes() + # handle over-reading from reading the banner line + if len(self.__remainder) > 0: + out = self.__remainder[:n] + self.__remainder = self.__remainder[n:] + n -= len(out) + while n > 0: + got_timeout = False + if self.handshake_timed_out(): + raise EOFError() + try: + x = self.__socket.recv(n) + if len(x) == 0: + raise EOFError() + out += x + n -= len(x) + except socket.timeout: + got_timeout = True + except socket.error as e: + # on Linux, sometimes instead of socket.timeout, we get + # EAGAIN. this is a bug in recent (> 2.6.9) kernels but + # we need to work around it. + arg = first_arg(e) + if arg == errno.EAGAIN: + got_timeout = True + elif self.__closed: + raise EOFError() + else: + raise + if got_timeout: + if self.__closed: + raise EOFError() + if check_rekey and (len(out) == 0) and self.__need_rekey: + raise NeedRekeyException() + self._check_keepalive() + return out + + def write_all(self, out): + self.__keepalive_last = time.time() + iteration_with_zero_as_return_value = 0 + while len(out) > 0: + retry_write = False + try: + n = self.__socket.send(out) + except socket.timeout: + retry_write = True + except socket.error as e: + arg = first_arg(e) + if arg == errno.EAGAIN: + retry_write = True + else: + n = -1 + except ProxyCommandFailure: + raise # so it doesn't get swallowed by the below catchall + except Exception: + # could be: (32, 'Broken pipe') + n = -1 + if retry_write: + n = 0 + if self.__closed: + n = -1 + else: + if n == 0 and iteration_with_zero_as_return_value > 10: + # We shouldn't retry the write, but we didn't + # manage to send anything over the socket. This might be an + # indication that we have lost contact with the remote + # side, but are yet to receive an EOFError or other socket + # errors. Let's give it some iteration to try and catch up. + n = -1 + iteration_with_zero_as_return_value += 1 + if n < 0: + raise EOFError() + if n == len(out): + break + out = out[n:] + return + + def readline(self, timeout): + """ + Read a line from the socket. We assume no data is pending after the + line, so it's okay to attempt large reads. + """ + buf = self.__remainder + while linefeed_byte not in buf: + buf += self._read_timeout(timeout) + n = buf.index(linefeed_byte) + self.__remainder = buf[n + 1 :] + buf = buf[:n] + if (len(buf) > 0) and (buf[-1] == cr_byte_value): + buf = buf[:-1] + return u(buf) + + def send_message(self, data): + """ + Write a block of data using the current cipher, as an SSH block. + """ + # encrypt this sucka + data = data.asbytes() + cmd = byte_ord(data[0]) + if cmd in MSG_NAMES: + cmd_name = MSG_NAMES[cmd] + else: + cmd_name = "${:x}".format(cmd) + orig_len = len(data) + self.__write_lock.acquire() + try: + if self.__compress_engine_out is not None: + data = self.__compress_engine_out(data) + packet = self._build_packet(data) + if self.__dump_packets: + self._log( + DEBUG, + "Write packet <{}>, length {}".format(cmd_name, orig_len), + ) + self._log(DEBUG, util.format_binary(packet, "OUT: ")) + if self.__block_engine_out is not None: + if self.__etm_out: + # packet length is not encrypted in EtM + out = packet[0:4] + self.__block_engine_out.update( + packet[4:] + ) + else: + out = self.__block_engine_out.update(packet) + else: + out = packet + # + mac + if self.__block_engine_out is not None: + packed = struct.pack(">I", self.__sequence_number_out) + payload = packed + (out if self.__etm_out else packet) + out += compute_hmac( + self.__mac_key_out, payload, self.__mac_engine_out + )[: self.__mac_size_out] + self.__sequence_number_out = ( + self.__sequence_number_out + 1 + ) & xffffffff + self.write_all(out) + + self.__sent_bytes += len(out) + self.__sent_packets += 1 + sent_too_much = ( + self.__sent_packets >= self.REKEY_PACKETS + or self.__sent_bytes >= self.REKEY_BYTES + ) + if sent_too_much and not self.__need_rekey: + # only ask once for rekeying + msg = "Rekeying (hit {} packets, {} bytes sent)" + self._log( + DEBUG, msg.format(self.__sent_packets, self.__sent_bytes) + ) + self.__received_bytes_overflow = 0 + self.__received_packets_overflow = 0 + self._trigger_rekey() + finally: + self.__write_lock.release() + + def read_message(self): + """ + Only one thread should ever be in this function (no other locking is + done). + + :raises: `.SSHException` -- if the packet is mangled + :raises: `.NeedRekeyException` -- if the transport should rekey + """ + header = self.read_all(self.__block_size_in, check_rekey=True) + if self.__etm_in: + packet_size = struct.unpack(">I", header[:4])[0] + remaining = packet_size - self.__block_size_in + 4 + packet = header[4:] + self.read_all(remaining, check_rekey=False) + mac = self.read_all(self.__mac_size_in, check_rekey=False) + mac_payload = ( + struct.pack(">II", self.__sequence_number_in, packet_size) + + packet + ) + my_mac = compute_hmac( + self.__mac_key_in, mac_payload, self.__mac_engine_in + )[: self.__mac_size_in] + if not util.constant_time_bytes_eq(my_mac, mac): + raise SSHException("Mismatched MAC") + header = packet + + if self.__block_engine_in is not None: + header = self.__block_engine_in.update(header) + if self.__dump_packets: + self._log(DEBUG, util.format_binary(header, "IN: ")) + + # When ETM is in play, we've already read the packet size & decrypted + # everything, so just set the packet back to the header we obtained. + if self.__etm_in: + packet = header + # Otherwise, use the older non-ETM logic + else: + packet_size = struct.unpack(">I", header[:4])[0] + + # leftover contains decrypted bytes from the first block (after the + # length field) + leftover = header[4:] + if (packet_size - len(leftover)) % self.__block_size_in != 0: + raise SSHException("Invalid packet blocking") + buf = self.read_all( + packet_size + self.__mac_size_in - len(leftover) + ) + packet = buf[: packet_size - len(leftover)] + post_packet = buf[packet_size - len(leftover) :] + + if self.__block_engine_in is not None: + packet = self.__block_engine_in.update(packet) + packet = leftover + packet + + if self.__dump_packets: + self._log(DEBUG, util.format_binary(packet, "IN: ")) + + if self.__mac_size_in > 0 and not self.__etm_in: + mac = post_packet[: self.__mac_size_in] + mac_payload = ( + struct.pack(">II", self.__sequence_number_in, packet_size) + + packet + ) + my_mac = compute_hmac( + self.__mac_key_in, mac_payload, self.__mac_engine_in + )[: self.__mac_size_in] + if not util.constant_time_bytes_eq(my_mac, mac): + raise SSHException("Mismatched MAC") + padding = byte_ord(packet[0]) + payload = packet[1 : packet_size - padding] + + if self.__dump_packets: + self._log( + DEBUG, + "Got payload ({} bytes, {} padding)".format( + packet_size, padding + ), + ) + + if self.__compress_engine_in is not None: + payload = self.__compress_engine_in(payload) + + msg = Message(payload[1:]) + msg.seqno = self.__sequence_number_in + self.__sequence_number_in = (self.__sequence_number_in + 1) & xffffffff + + # check for rekey + raw_packet_size = packet_size + self.__mac_size_in + 4 + self.__received_bytes += raw_packet_size + self.__received_packets += 1 + if self.__need_rekey: + # we've asked to rekey -- give them some packets to comply before + # dropping the connection + self.__received_bytes_overflow += raw_packet_size + self.__received_packets_overflow += 1 + if ( + self.__received_packets_overflow + >= self.REKEY_PACKETS_OVERFLOW_MAX + ) or ( + self.__received_bytes_overflow >= self.REKEY_BYTES_OVERFLOW_MAX + ): + raise SSHException( + "Remote transport is ignoring rekey requests" + ) + elif (self.__received_packets >= self.REKEY_PACKETS) or ( + self.__received_bytes >= self.REKEY_BYTES + ): + # only ask once for rekeying + err = "Rekeying (hit {} packets, {} bytes received)" + self._log( + DEBUG, + err.format(self.__received_packets, self.__received_bytes), + ) + self.__received_bytes_overflow = 0 + self.__received_packets_overflow = 0 + self._trigger_rekey() + + cmd = byte_ord(payload[0]) + if cmd in MSG_NAMES: + cmd_name = MSG_NAMES[cmd] + else: + cmd_name = "${:x}".format(cmd) + if self.__dump_packets: + self._log( + DEBUG, + "Read packet <{}>, length {}".format(cmd_name, len(payload)), + ) + return cmd, msg + + # ...protected... + + def _log(self, level, msg): + if self.__logger is None: + return + if issubclass(type(msg), list): + for m in msg: + self.__logger.log(level, m) + else: + self.__logger.log(level, msg) + + def _check_keepalive(self): + if ( + not self.__keepalive_interval + or not self.__block_engine_out + or self.__need_rekey + ): + # wait till we're encrypting, and not in the middle of rekeying + return + now = time.time() + if now > self.__keepalive_last + self.__keepalive_interval: + self.__keepalive_callback() + self.__keepalive_last = now + + def _read_timeout(self, timeout): + start = time.time() + while True: + try: + x = self.__socket.recv(128) + if len(x) == 0: + raise EOFError() + break + except socket.timeout: + pass + if self.__closed: + raise EOFError() + now = time.time() + if now - start >= timeout: + raise socket.timeout() + return x + + def _build_packet(self, payload): + # pad up at least 4 bytes, to nearest block-size (usually 8) + bsize = self.__block_size_out + # do not include payload length in computations for padding in EtM mode + # (payload length won't be encrypted) + addlen = 4 if self.__etm_out else 8 + padding = 3 + bsize - ((len(payload) + addlen) % bsize) + packet = struct.pack(">IB", len(payload) + padding + 1, padding) + packet += payload + if self.__sdctr_out or self.__block_engine_out is None: + # cute trick i caught openssh doing: if we're not encrypting or + # SDCTR mode (RFC4344), + # don't waste random bytes for the padding + packet += zero_byte * padding + else: + packet += os.urandom(padding) + return packet + + def _trigger_rekey(self): + # outside code should check for this flag + self.__need_rekey = True diff --git a/paramiko/pipe.py b/paramiko/pipe.py new file mode 100644 index 0000000..65944fa --- /dev/null +++ b/paramiko/pipe.py @@ -0,0 +1,148 @@ +# Copyright (C) 2003-2007 Robey Pointer <robeypointer@gmail.com> +# +# This file is part of paramiko. +# +# Paramiko is free software; you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free +# Software Foundation; either version 2.1 of the License, or (at your option) +# any later version. +# +# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Paramiko; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +""" +Abstraction of a one-way pipe where the read end can be used in +`select.select`. Normally this is trivial, but Windows makes it nearly +impossible. + +The pipe acts like an Event, which can be set or cleared. When set, the pipe +will trigger as readable in `select <select.select>`. +""" + +import sys +import os +import socket + + +def make_pipe(): + if sys.platform[:3] != "win": + p = PosixPipe() + else: + p = WindowsPipe() + return p + + +class PosixPipe: + def __init__(self): + self._rfd, self._wfd = os.pipe() + self._set = False + self._forever = False + self._closed = False + + def close(self): + os.close(self._rfd) + os.close(self._wfd) + # used for unit tests: + self._closed = True + + def fileno(self): + return self._rfd + + def clear(self): + if not self._set or self._forever: + return + os.read(self._rfd, 1) + self._set = False + + def set(self): + if self._set or self._closed: + return + self._set = True + os.write(self._wfd, b"*") + + def set_forever(self): + self._forever = True + self.set() + + +class WindowsPipe: + """ + On Windows, only an OS-level "WinSock" may be used in select(), but reads + and writes must be to the actual socket object. + """ + + def __init__(self): + serv = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + serv.bind(("127.0.0.1", 0)) + serv.listen(1) + + # need to save sockets in _rsock/_wsock so they don't get closed + self._rsock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self._rsock.connect(("127.0.0.1", serv.getsockname()[1])) + + self._wsock, addr = serv.accept() + serv.close() + self._set = False + self._forever = False + self._closed = False + + def close(self): + self._rsock.close() + self._wsock.close() + # used for unit tests: + self._closed = True + + def fileno(self): + return self._rsock.fileno() + + def clear(self): + if not self._set or self._forever: + return + self._rsock.recv(1) + self._set = False + + def set(self): + if self._set or self._closed: + return + self._set = True + self._wsock.send(b"*") + + def set_forever(self): + self._forever = True + self.set() + + +class OrPipe: + def __init__(self, pipe): + self._set = False + self._partner = None + self._pipe = pipe + + def set(self): + self._set = True + if not self._partner._set: + self._pipe.set() + + def clear(self): + self._set = False + if not self._partner._set: + self._pipe.clear() + + +def make_or_pipe(pipe): + """ + wraps a pipe into two pipe-like objects which are "or"d together to + affect the real pipe. if either returned pipe is set, the wrapped pipe + is set. when both are cleared, the wrapped pipe is cleared. + """ + p1 = OrPipe(pipe) + p2 = OrPipe(pipe) + p1._partner = p2 + p2._partner = p1 + return p1, p2 diff --git a/paramiko/pkey.py b/paramiko/pkey.py new file mode 100644 index 0000000..ff6095a --- /dev/null +++ b/paramiko/pkey.py @@ -0,0 +1,747 @@ +# Copyright (C) 2003-2007 Robey Pointer <robeypointer@gmail.com> +# +# This file is part of paramiko. +# +# Paramiko is free software; you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free +# Software Foundation; either version 2.1 of the License, or (at your option) +# any later version. +# +# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Paramiko; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +""" +Common API for all public keys. +""" + +import base64 +from base64 import encodebytes, decodebytes +from binascii import unhexlify +import os +from hashlib import md5 +import re +import struct + +import bcrypt + +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.ciphers import algorithms, modes, Cipher + +from paramiko import util +from paramiko.util import u, b +from paramiko.common import o600 +from paramiko.ssh_exception import SSHException, PasswordRequiredException +from paramiko.message import Message + + +OPENSSH_AUTH_MAGIC = b"openssh-key-v1\x00" + + +def _unpad_openssh(data): + # At the moment, this is only used for unpadding private keys on disk. This + # really ought to be made constant time (possibly by upstreaming this logic + # into pyca/cryptography). + padding_length = data[-1] + if 0x20 <= padding_length < 0x7F: + return data # no padding, last byte part comment (printable ascii) + if padding_length > 15: + raise SSHException("Invalid key") + for i in range(padding_length): + if data[i - padding_length] != i + 1: + raise SSHException("Invalid key") + return data[:-padding_length] + + +class PKey: + """ + Base class for public keys. + """ + + # known encryption types for private key files: + _CIPHER_TABLE = { + "AES-128-CBC": { + "cipher": algorithms.AES, + "keysize": 16, + "blocksize": 16, + "mode": modes.CBC, + }, + "AES-256-CBC": { + "cipher": algorithms.AES, + "keysize": 32, + "blocksize": 16, + "mode": modes.CBC, + }, + "DES-EDE3-CBC": { + "cipher": algorithms.TripleDES, + "keysize": 24, + "blocksize": 8, + "mode": modes.CBC, + }, + } + _PRIVATE_KEY_FORMAT_ORIGINAL = 1 + _PRIVATE_KEY_FORMAT_OPENSSH = 2 + BEGIN_TAG = re.compile( + r"^-{5}BEGIN (RSA|DSA|EC|OPENSSH) PRIVATE KEY-{5}\s*$" + ) + END_TAG = re.compile(r"^-{5}END (RSA|DSA|EC|OPENSSH) PRIVATE KEY-{5}\s*$") + + def __init__(self, msg=None, data=None): + """ + Create a new instance of this public key type. If ``msg`` is given, + the key's public part(s) will be filled in from the message. If + ``data`` is given, the key's public part(s) will be filled in from + the string. + + :param .Message msg: + an optional SSH `.Message` containing a public key of this type. + :param str data: an optional string containing a public key + of this type + + :raises: `.SSHException` -- + if a key cannot be created from the ``data`` or ``msg`` given, or + no key was passed in. + """ + pass + + # TODO 4.0: just merge into __bytes__ (everywhere) + def asbytes(self): + """ + Return a string of an SSH `.Message` made up of the public part(s) of + this key. This string is suitable for passing to `__init__` to + re-create the key object later. + """ + return bytes() + + def __bytes__(self): + return self.asbytes() + + def __eq__(self, other): + return isinstance(other, PKey) and self._fields == other._fields + + def __hash__(self): + return hash(self._fields) + + @property + def _fields(self): + raise NotImplementedError + + def get_name(self): + """ + Return the name of this private key implementation. + + :return: + name of this private key type, in SSH terminology, as a `str` (for + example, ``"ssh-rsa"``). + """ + return "" + + def get_bits(self): + """ + Return the number of significant bits in this key. This is useful + for judging the relative security of a key. + + :return: bits in the key (as an `int`) + """ + return 0 + + def can_sign(self): + """ + Return ``True`` if this key has the private part necessary for signing + data. + """ + return False + + def get_fingerprint(self): + """ + Return an MD5 fingerprint of the public part of this key. Nothing + secret is revealed. + + :return: + a 16-byte `string <str>` (binary) of the MD5 fingerprint, in SSH + format. + """ + return md5(self.asbytes()).digest() + + def get_base64(self): + """ + Return a base64 string containing the public part of this key. Nothing + secret is revealed. This format is compatible with that used to store + public key files or recognized host keys. + + :return: a base64 `string <str>` containing the public part of the key. + """ + return u(encodebytes(self.asbytes())).replace("\n", "") + + def sign_ssh_data(self, data, algorithm=None): + """ + Sign a blob of data with this private key, and return a `.Message` + representing an SSH signature message. + + :param bytes data: + the data to sign. + :param str algorithm: + the signature algorithm to use, if different from the key's + internal name. Default: ``None``. + :return: an SSH signature `message <.Message>`. + + .. versionchanged:: 2.9 + Added the ``algorithm`` kwarg. + """ + return bytes() + + def verify_ssh_sig(self, data, msg): + """ + Given a blob of data, and an SSH message representing a signature of + that data, verify that it was signed with this key. + + :param bytes data: the data that was signed. + :param .Message msg: an SSH signature message + :return: + ``True`` if the signature verifies correctly; ``False`` otherwise. + """ + return False + + @classmethod + def from_private_key_file(cls, filename, password=None): + """ + Create a key object by reading a private key file. If the private + key is encrypted and ``password`` is not ``None``, the given password + will be used to decrypt the key (otherwise `.PasswordRequiredException` + is thrown). Through the magic of Python, this factory method will + exist in all subclasses of PKey (such as `.RSAKey` or `.DSSKey`), but + is useless on the abstract PKey class. + + :param str filename: name of the file to read + :param str password: + an optional password to use to decrypt the key file, if it's + encrypted + :return: a new `.PKey` based on the given private key + + :raises: ``IOError`` -- if there was an error reading the file + :raises: `.PasswordRequiredException` -- if the private key file is + encrypted, and ``password`` is ``None`` + :raises: `.SSHException` -- if the key file is invalid + """ + key = cls(filename=filename, password=password) + return key + + @classmethod + def from_private_key(cls, file_obj, password=None): + """ + Create a key object by reading a private key from a file (or file-like) + object. If the private key is encrypted and ``password`` is not + ``None``, the given password will be used to decrypt the key (otherwise + `.PasswordRequiredException` is thrown). + + :param file_obj: the file-like object to read from + :param str password: + an optional password to use to decrypt the key, if it's encrypted + :return: a new `.PKey` based on the given private key + + :raises: ``IOError`` -- if there was an error reading the key + :raises: `.PasswordRequiredException` -- + if the private key file is encrypted, and ``password`` is ``None`` + :raises: `.SSHException` -- if the key file is invalid + """ + key = cls(file_obj=file_obj, password=password) + return key + + def write_private_key_file(self, filename, password=None): + """ + Write private key contents into a file. If the password is not + ``None``, the key is encrypted before writing. + + :param str filename: name of the file to write + :param str password: + an optional password to use to encrypt the key file + + :raises: ``IOError`` -- if there was an error writing the file + :raises: `.SSHException` -- if the key is invalid + """ + raise Exception("Not implemented in PKey") + + def write_private_key(self, file_obj, password=None): + """ + Write private key contents into a file (or file-like) object. If the + password is not ``None``, the key is encrypted before writing. + + :param file_obj: the file-like object to write into + :param str password: an optional password to use to encrypt the key + + :raises: ``IOError`` -- if there was an error writing to the file + :raises: `.SSHException` -- if the key is invalid + """ + raise Exception("Not implemented in PKey") + + def _read_private_key_file(self, tag, filename, password=None): + """ + Read an SSH2-format private key file, looking for a string of the type + ``"BEGIN xxx PRIVATE KEY"`` for some ``xxx``, base64-decode the text we + find, and return it as a string. If the private key is encrypted and + ``password`` is not ``None``, the given password will be used to + decrypt the key (otherwise `.PasswordRequiredException` is thrown). + + :param str tag: ``"RSA"`` or ``"DSA"``, the tag used to mark the + data block. + :param str filename: name of the file to read. + :param str password: + an optional password to use to decrypt the key file, if it's + encrypted. + :return: the `bytes` that make up the private key. + + :raises: ``IOError`` -- if there was an error reading the file. + :raises: `.PasswordRequiredException` -- if the private key file is + encrypted, and ``password`` is ``None``. + :raises: `.SSHException` -- if the key file is invalid. + """ + with open(filename, "r") as f: + data = self._read_private_key(tag, f, password) + return data + + def _read_private_key(self, tag, f, password=None): + lines = f.readlines() + if not lines: + raise SSHException("no lines in {} private key file".format(tag)) + + # find the BEGIN tag + start = 0 + m = self.BEGIN_TAG.match(lines[start]) + line_range = len(lines) - 1 + while start < line_range and not m: + start += 1 + m = self.BEGIN_TAG.match(lines[start]) + start += 1 + keytype = m.group(1) if m else None + if start >= len(lines) or keytype is None: + raise SSHException("not a valid {} private key file".format(tag)) + + # find the END tag + end = start + m = self.END_TAG.match(lines[end]) + while end < line_range and not m: + end += 1 + m = self.END_TAG.match(lines[end]) + + if keytype == tag: + data = self._read_private_key_pem(lines, end, password) + pkformat = self._PRIVATE_KEY_FORMAT_ORIGINAL + elif keytype == "OPENSSH": + data = self._read_private_key_openssh(lines[start:end], password) + pkformat = self._PRIVATE_KEY_FORMAT_OPENSSH + else: + raise SSHException( + "encountered {} key, expected {} key".format(keytype, tag) + ) + + return pkformat, data + + def _got_bad_key_format_id(self, id_): + err = "{}._read_private_key() spat out an unknown key format id '{}'" + raise SSHException(err.format(self.__class__.__name__, id_)) + + def _read_private_key_pem(self, lines, end, password): + start = 0 + # parse any headers first + headers = {} + start += 1 + while start < len(lines): + line = lines[start].split(": ") + if len(line) == 1: + break + headers[line[0].lower()] = line[1].strip() + start += 1 + # if we trudged to the end of the file, just try to cope. + try: + data = decodebytes(b("".join(lines[start:end]))) + except base64.binascii.Error as e: + raise SSHException("base64 decoding error: {}".format(e)) + if "proc-type" not in headers: + # unencryped: done + return data + # encrypted keyfile: will need a password + proc_type = headers["proc-type"] + if proc_type != "4,ENCRYPTED": + raise SSHException( + 'Unknown private key structure "{}"'.format(proc_type) + ) + try: + encryption_type, saltstr = headers["dek-info"].split(",") + except: + raise SSHException("Can't parse DEK-info in private key file") + if encryption_type not in self._CIPHER_TABLE: + raise SSHException( + 'Unknown private key cipher "{}"'.format(encryption_type) + ) + # if no password was passed in, + # raise an exception pointing out that we need one + if password is None: + raise PasswordRequiredException("Private key file is encrypted") + cipher = self._CIPHER_TABLE[encryption_type]["cipher"] + keysize = self._CIPHER_TABLE[encryption_type]["keysize"] + mode = self._CIPHER_TABLE[encryption_type]["mode"] + salt = unhexlify(b(saltstr)) + key = util.generate_key_bytes(md5, salt, password, keysize) + decryptor = Cipher( + cipher(key), mode(salt), backend=default_backend() + ).decryptor() + return decryptor.update(data) + decryptor.finalize() + + def _read_private_key_openssh(self, lines, password): + """ + Read the new OpenSSH SSH2 private key format available + since OpenSSH version 6.5 + Reference: + https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.key + """ + try: + data = decodebytes(b("".join(lines))) + except base64.binascii.Error as e: + raise SSHException("base64 decoding error: {}".format(e)) + + # read data struct + auth_magic = data[:15] + if auth_magic != OPENSSH_AUTH_MAGIC: + raise SSHException("unexpected OpenSSH key header encountered") + + cstruct = self._uint32_cstruct_unpack(data[15:], "sssur") + cipher, kdfname, kdf_options, num_pubkeys, remainder = cstruct + # For now, just support 1 key. + if num_pubkeys > 1: + raise SSHException( + "unsupported: private keyfile has multiple keys" + ) + pubkey, privkey_blob = self._uint32_cstruct_unpack(remainder, "ss") + + if kdfname == b("bcrypt"): + if cipher == b("aes256-cbc"): + mode = modes.CBC + elif cipher == b("aes256-ctr"): + mode = modes.CTR + else: + raise SSHException( + "unknown cipher `{}` used in private key file".format( + cipher.decode("utf-8") + ) + ) + # Encrypted private key. + # If no password was passed in, raise an exception pointing + # out that we need one + if password is None: + raise PasswordRequiredException( + "private key file is encrypted" + ) + + # Unpack salt and rounds from kdfoptions + salt, rounds = self._uint32_cstruct_unpack(kdf_options, "su") + + # run bcrypt kdf to derive key and iv/nonce (32 + 16 bytes) + key_iv = bcrypt.kdf( + b(password), + b(salt), + 48, + rounds, + # We can't control how many rounds are on disk, so no sense + # warning about it. + ignore_few_rounds=True, + ) + key = key_iv[:32] + iv = key_iv[32:] + + # decrypt private key blob + decryptor = Cipher( + algorithms.AES(key), mode(iv), default_backend() + ).decryptor() + decrypted_privkey = decryptor.update(privkey_blob) + decrypted_privkey += decryptor.finalize() + elif cipher == b("none") and kdfname == b("none"): + # Unencrypted private key + decrypted_privkey = privkey_blob + else: + raise SSHException( + "unknown cipher or kdf used in private key file" + ) + + # Unpack private key and verify checkints + cstruct = self._uint32_cstruct_unpack(decrypted_privkey, "uusr") + checkint1, checkint2, keytype, keydata = cstruct + + if checkint1 != checkint2: + raise SSHException( + "OpenSSH private key file checkints do not match" + ) + + return _unpad_openssh(keydata) + + def _uint32_cstruct_unpack(self, data, strformat): + """ + Used to read new OpenSSH private key format. + Unpacks a c data structure containing a mix of 32-bit uints and + variable length strings prefixed by 32-bit uint size field, + according to the specified format. Returns the unpacked vars + in a tuple. + Format strings: + s - denotes a string + i - denotes a long integer, encoded as a byte string + u - denotes a 32-bit unsigned integer + r - the remainder of the input string, returned as a string + """ + arr = [] + idx = 0 + try: + for f in strformat: + if f == "s": + # string + s_size = struct.unpack(">L", data[idx : idx + 4])[0] + idx += 4 + s = data[idx : idx + s_size] + idx += s_size + arr.append(s) + if f == "i": + # long integer + s_size = struct.unpack(">L", data[idx : idx + 4])[0] + idx += 4 + s = data[idx : idx + s_size] + idx += s_size + i = util.inflate_long(s, True) + arr.append(i) + elif f == "u": + # 32-bit unsigned int + u = struct.unpack(">L", data[idx : idx + 4])[0] + idx += 4 + arr.append(u) + elif f == "r": + # remainder as string + s = data[idx:] + arr.append(s) + break + except Exception as e: + # PKey-consuming code frequently wants to save-and-skip-over issues + # with loading keys, and uses SSHException as the (really friggin + # awful) signal for this. So for now...we do this. + raise SSHException(str(e)) + return tuple(arr) + + def _write_private_key_file(self, filename, key, format, password=None): + """ + Write an SSH2-format private key file in a form that can be read by + paramiko or openssh. If no password is given, the key is written in + a trivially-encoded format (base64) which is completely insecure. If + a password is given, DES-EDE3-CBC is used. + + :param str tag: + ``"RSA"`` or ``"DSA"``, the tag used to mark the data block. + :param filename: name of the file to write. + :param bytes data: data blob that makes up the private key. + :param str password: an optional password to use to encrypt the file. + + :raises: ``IOError`` -- if there was an error writing the file. + """ + # Ensure that we create new key files directly with a user-only mode, + # instead of opening, writing, then chmodding, which leaves us open to + # CVE-2022-24302. + with os.fdopen( + os.open( + filename, + # NOTE: O_TRUNC is a noop on new files, and O_CREAT is a noop + # on existing files, so using all 3 in both cases is fine. + flags=os.O_WRONLY | os.O_TRUNC | os.O_CREAT, + # Ditto the use of the 'mode' argument; it should be safe to + # give even for existing files (though it will not act like a + # chmod in that case). + mode=o600, + ), + # Yea, you still gotta inform the FLO that it is in "write" mode. + "w", + ) as f: + self._write_private_key(f, key, format, password=password) + + def _write_private_key(self, f, key, format, password=None): + if password is None: + encryption = serialization.NoEncryption() + else: + encryption = serialization.BestAvailableEncryption(b(password)) + + f.write( + key.private_bytes( + serialization.Encoding.PEM, format, encryption + ).decode() + ) + + def _check_type_and_load_cert(self, msg, key_type, cert_type): + """ + Perform message type-checking & optional certificate loading. + + This includes fast-forwarding cert ``msg`` objects past the nonce, so + that the subsequent fields are the key numbers; thus the caller may + expect to treat the message as key material afterwards either way. + + The obtained key type is returned for classes which need to know what + it was (e.g. ECDSA.) + """ + # Normalization; most classes have a single key type and give a string, + # but eg ECDSA is a 1:N mapping. + key_types = key_type + cert_types = cert_type + if isinstance(key_type, str): + key_types = [key_types] + if isinstance(cert_types, str): + cert_types = [cert_types] + # Can't do much with no message, that should've been handled elsewhere + if msg is None: + raise SSHException("Key object may not be empty") + # First field is always key type, in either kind of object. (make sure + # we rewind before grabbing it - sometimes caller had to do their own + # introspection first!) + msg.rewind() + type_ = msg.get_text() + # Regular public key - nothing special to do besides the implicit + # type check. + if type_ in key_types: + pass + # OpenSSH-compatible certificate - store full copy as .public_blob + # (so signing works correctly) and then fast-forward past the + # nonce. + + elif type_ in cert_types: + # This seems the cleanest way to 'clone' an already-being-read + # message; they're *IO objects at heart and their .getvalue() + # always returns the full value regardless of pointer position. + self.load_certificate(Message(msg.asbytes())) + # Read out nonce as it comes before the public numbers. + # TODO: usefully interpret it & other non-public-number fields + # (requires going back into per-type subclasses.) + msg.get_string() + else: + err = "Invalid key (class: {}, data type: {}" + raise SSHException(err.format(self.__class__.__name__, type_)) + + def load_certificate(self, value): + """ + Supplement the private key contents with data loaded from an OpenSSH + public key (``.pub``) or certificate (``-cert.pub``) file, a string + containing such a file, or a `.Message` object. + + The .pub contents adds no real value, since the private key + file includes sufficient information to derive the public + key info. For certificates, however, this can be used on + the client side to offer authentication requests to the server + based on certificate instead of raw public key. + + See: + https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.certkeys + + Note: very little effort is made to validate the certificate contents, + that is for the server to decide if it is good enough to authenticate + successfully. + """ + if isinstance(value, Message): + constructor = "from_message" + elif os.path.isfile(value): + constructor = "from_file" + else: + constructor = "from_string" + blob = getattr(PublicBlob, constructor)(value) + if not blob.key_type.startswith(self.get_name()): + err = "PublicBlob type {} incompatible with key type {}" + raise ValueError(err.format(blob.key_type, self.get_name())) + self.public_blob = blob + + +# General construct for an OpenSSH style Public Key blob +# readable from a one-line file of the format: +# <key-name> <base64-blob> [<comment>] +# Of little value in the case of standard public keys +# {ssh-rsa, ssh-dss, ssh-ecdsa, ssh-ed25519}, but should +# provide rudimentary support for {*-cert.v01} +class PublicBlob: + """ + OpenSSH plain public key or OpenSSH signed public key (certificate). + + Tries to be as dumb as possible and barely cares about specific + per-key-type data. + + .. note:: + + Most of the time you'll want to call `from_file`, `from_string` or + `from_message` for useful instantiation, the main constructor is + basically "I should be using ``attrs`` for this." + """ + + def __init__(self, type_, blob, comment=None): + """ + Create a new public blob of given type and contents. + + :param str type_: Type indicator, eg ``ssh-rsa``. + :param bytes blob: The blob bytes themselves. + :param str comment: A comment, if one was given (e.g. file-based.) + """ + self.key_type = type_ + self.key_blob = blob + self.comment = comment + + @classmethod + def from_file(cls, filename): + """ + Create a public blob from a ``-cert.pub``-style file on disk. + """ + with open(filename) as f: + string = f.read() + return cls.from_string(string) + + @classmethod + def from_string(cls, string): + """ + Create a public blob from a ``-cert.pub``-style string. + """ + fields = string.split(None, 2) + if len(fields) < 2: + msg = "Not enough fields for public blob: {}" + raise ValueError(msg.format(fields)) + key_type = fields[0] + key_blob = decodebytes(b(fields[1])) + try: + comment = fields[2].strip() + except IndexError: + comment = None + # Verify that the blob message first (string) field matches the + # key_type + m = Message(key_blob) + blob_type = m.get_text() + if blob_type != key_type: + deets = "key type={!r}, but blob type={!r}".format( + key_type, blob_type + ) + raise ValueError("Invalid PublicBlob contents: {}".format(deets)) + # All good? All good. + return cls(type_=key_type, blob=key_blob, comment=comment) + + @classmethod + def from_message(cls, message): + """ + Create a public blob from a network `.Message`. + + Specifically, a cert-bearing pubkey auth packet, because by definition + OpenSSH-style certificates 'are' their own network representation." + """ + type_ = message.get_text() + return cls(type_=type_, blob=message.asbytes()) + + def __str__(self): + ret = "{} public key/certificate".format(self.key_type) + if self.comment: + ret += "- {}".format(self.comment) + return ret + + def __eq__(self, other): + # Just piggyback on Message/BytesIO, since both of these should be one. + return self and other and self.key_blob == other.key_blob + + def __ne__(self, other): + return not self == other diff --git a/paramiko/primes.py b/paramiko/primes.py new file mode 100644 index 0000000..663c58e --- /dev/null +++ b/paramiko/primes.py @@ -0,0 +1,148 @@ +# Copyright (C) 2003-2007 Robey Pointer <robeypointer@gmail.com> +# +# This file is part of paramiko. +# +# Paramiko is free software; you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free +# Software Foundation; either version 2.1 of the License, or (at your option) +# any later version. +# +# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Paramiko; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +""" +Utility functions for dealing with primes. +""" + +import os + +from paramiko import util +from paramiko.common import byte_mask +from paramiko.ssh_exception import SSHException + + +def _roll_random(n): + """returns a random # from 0 to N-1""" + bits = util.bit_length(n - 1) + byte_count = (bits + 7) // 8 + hbyte_mask = pow(2, bits % 8) - 1 + + # so here's the plan: + # we fetch as many random bits as we'd need to fit N-1, and if the + # generated number is >= N, we try again. in the worst case (N-1 is a + # power of 2), we have slightly better than 50% odds of getting one that + # fits, so i can't guarantee that this loop will ever finish, but the odds + # of it looping forever should be infinitesimal. + while True: + x = os.urandom(byte_count) + if hbyte_mask > 0: + x = byte_mask(x[0], hbyte_mask) + x[1:] + num = util.inflate_long(x, 1) + if num < n: + break + return num + + +class ModulusPack: + """ + convenience object for holding the contents of the /etc/ssh/moduli file, + on systems that have such a file. + """ + + def __init__(self): + # pack is a hash of: bits -> [ (generator, modulus) ... ] + self.pack = {} + self.discarded = [] + + def _parse_modulus(self, line): + ( + timestamp, + mod_type, + tests, + tries, + size, + generator, + modulus, + ) = line.split() + mod_type = int(mod_type) + tests = int(tests) + tries = int(tries) + size = int(size) + generator = int(generator) + modulus = int(modulus, 16) + + # weed out primes that aren't at least: + # type 2 (meets basic structural requirements) + # test 4 (more than just a small-prime sieve) + # tries < 100 if test & 4 (at least 100 tries of miller-rabin) + if ( + mod_type < 2 + or tests < 4 + or (tests & 4 and tests < 8 and tries < 100) + ): + self.discarded.append( + (modulus, "does not meet basic requirements") + ) + return + if generator == 0: + generator = 2 + + # there's a bug in the ssh "moduli" file (yeah, i know: shock! dismay! + # call cnn!) where it understates the bit lengths of these primes by 1. + # this is okay. + bl = util.bit_length(modulus) + if (bl != size) and (bl != size + 1): + self.discarded.append( + (modulus, "incorrectly reported bit length {}".format(size)) + ) + return + if bl not in self.pack: + self.pack[bl] = [] + self.pack[bl].append((generator, modulus)) + + def read_file(self, filename): + """ + :raises IOError: passed from any file operations that fail. + """ + self.pack = {} + with open(filename, "r") as f: + for line in f: + line = line.strip() + if (len(line) == 0) or (line[0] == "#"): + continue + try: + self._parse_modulus(line) + except: + continue + + def get_modulus(self, min, prefer, max): + bitsizes = sorted(self.pack.keys()) + if len(bitsizes) == 0: + raise SSHException("no moduli available") + good = -1 + # find nearest bitsize >= preferred + for b in bitsizes: + if (b >= prefer) and (b <= max) and (b < good or good == -1): + good = b + # if that failed, find greatest bitsize >= min + if good == -1: + for b in bitsizes: + if (b >= min) and (b <= max) and (b > good): + good = b + if good == -1: + # their entire (min, max) range has no intersection with our range. + # if their range is below ours, pick the smallest. otherwise pick + # the largest. it'll be out of their range requirement either way, + # but we'll be sending them the closest one we have. + good = bitsizes[0] + if min > good: + good = bitsizes[-1] + # now pick a random modulus of this bitsize + n = _roll_random(len(self.pack[good])) + return self.pack[good][n] diff --git a/paramiko/proxy.py b/paramiko/proxy.py new file mode 100644 index 0000000..f7609c9 --- /dev/null +++ b/paramiko/proxy.py @@ -0,0 +1,134 @@ +# Copyright (C) 2012 Yipit, Inc <coders@yipit.com> +# +# This file is part of paramiko. +# +# Paramiko is free software; you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free +# Software Foundation; either version 2.1 of the License, or (at your option) +# any later version. +# +# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Paramiko; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + + +import os +import shlex +import signal +from select import select +import socket +import time + +# Try-and-ignore import so platforms w/o subprocess (eg Google App Engine) can +# still import paramiko. +subprocess, subprocess_import_error = None, None +try: + import subprocess +except ImportError as e: + subprocess_import_error = e + +from paramiko.ssh_exception import ProxyCommandFailure +from paramiko.util import ClosingContextManager + + +class ProxyCommand(ClosingContextManager): + """ + Wraps a subprocess running ProxyCommand-driven programs. + + This class implements a the socket-like interface needed by the + `.Transport` and `.Packetizer` classes. Using this class instead of a + regular socket makes it possible to talk with a Popen'd command that will + proxy traffic between the client and a server hosted in another machine. + + Instances of this class may be used as context managers. + """ + + def __init__(self, command_line): + """ + Create a new CommandProxy instance. The instance created by this + class can be passed as an argument to the `.Transport` class. + + :param str command_line: + the command that should be executed and used as the proxy. + """ + if subprocess is None: + raise subprocess_import_error + self.cmd = shlex.split(command_line) + self.process = subprocess.Popen( + self.cmd, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + bufsize=0, + ) + self.timeout = None + + def send(self, content): + """ + Write the content received from the SSH client to the standard + input of the forked command. + + :param str content: string to be sent to the forked command + """ + try: + self.process.stdin.write(content) + except IOError as e: + # There was a problem with the child process. It probably + # died and we can't proceed. The best option here is to + # raise an exception informing the user that the informed + # ProxyCommand is not working. + raise ProxyCommandFailure(" ".join(self.cmd), e.strerror) + return len(content) + + def recv(self, size): + """ + Read from the standard output of the forked program. + + :param int size: how many chars should be read + + :return: the string of bytes read, which may be shorter than requested + """ + try: + buffer = b"" + start = time.time() + while len(buffer) < size: + select_timeout = None + if self.timeout is not None: + elapsed = time.time() - start + if elapsed >= self.timeout: + raise socket.timeout() + select_timeout = self.timeout - elapsed + + r, w, x = select([self.process.stdout], [], [], select_timeout) + if r and r[0] == self.process.stdout: + buffer += os.read( + self.process.stdout.fileno(), size - len(buffer) + ) + return buffer + except socket.timeout: + if buffer: + # Don't raise socket.timeout, return partial result instead + return buffer + raise # socket.timeout is a subclass of IOError + except IOError as e: + raise ProxyCommandFailure(" ".join(self.cmd), e.strerror) + + def close(self): + os.kill(self.process.pid, signal.SIGTERM) + + @property + def closed(self): + return self.process.returncode is not None + + @property + def _closed(self): + # Concession to Python 3 socket-like API + return self.closed + + def settimeout(self, timeout): + self.timeout = timeout diff --git a/paramiko/rsakey.py b/paramiko/rsakey.py new file mode 100644 index 0000000..9ea00e9 --- /dev/null +++ b/paramiko/rsakey.py @@ -0,0 +1,217 @@ +# Copyright (C) 2003-2007 Robey Pointer <robeypointer@gmail.com> +# +# This file is part of paramiko. +# +# Paramiko is free software; you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free +# Software Foundation; either version 2.1 of the License, or (at your option) +# any later version. +# +# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Paramiko; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +""" +RSA keys. +""" + +from cryptography.exceptions import InvalidSignature, UnsupportedAlgorithm +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import hashes, serialization +from cryptography.hazmat.primitives.asymmetric import rsa, padding + +from paramiko.message import Message +from paramiko.pkey import PKey +from paramiko.ssh_exception import SSHException + + +class RSAKey(PKey): + """ + Representation of an RSA key which can be used to sign and verify SSH2 + data. + """ + + HASHES = { + "ssh-rsa": hashes.SHA1, + "ssh-rsa-cert-v01@openssh.com": hashes.SHA1, + "rsa-sha2-256": hashes.SHA256, + "rsa-sha2-256-cert-v01@openssh.com": hashes.SHA256, + "rsa-sha2-512": hashes.SHA512, + "rsa-sha2-512-cert-v01@openssh.com": hashes.SHA512, + } + + def __init__( + self, + msg=None, + data=None, + filename=None, + password=None, + key=None, + file_obj=None, + ): + self.key = None + self.public_blob = None + if file_obj is not None: + self._from_private_key(file_obj, password) + return + if filename is not None: + self._from_private_key_file(filename, password) + return + if (msg is None) and (data is not None): + msg = Message(data) + if key is not None: + self.key = key + else: + self._check_type_and_load_cert( + msg=msg, + # NOTE: this does NOT change when using rsa2 signatures; it's + # purely about key loading, not exchange or verification + key_type="ssh-rsa", + cert_type="ssh-rsa-cert-v01@openssh.com", + ) + self.key = rsa.RSAPublicNumbers( + e=msg.get_mpint(), n=msg.get_mpint() + ).public_key(default_backend()) + + @property + def size(self): + return self.key.key_size + + @property + def public_numbers(self): + if isinstance(self.key, rsa.RSAPrivateKey): + return self.key.private_numbers().public_numbers + else: + return self.key.public_numbers() + + def asbytes(self): + m = Message() + m.add_string("ssh-rsa") + m.add_mpint(self.public_numbers.e) + m.add_mpint(self.public_numbers.n) + return m.asbytes() + + def __str__(self): + # NOTE: see #853 to explain some legacy behavior. + # TODO 4.0: replace with a nice clean fingerprint display or something + return self.asbytes().decode("utf8", errors="ignore") + + @property + def _fields(self): + return (self.get_name(), self.public_numbers.e, self.public_numbers.n) + + def get_name(self): + return "ssh-rsa" + + def get_bits(self): + return self.size + + def can_sign(self): + return isinstance(self.key, rsa.RSAPrivateKey) + + def sign_ssh_data(self, data, algorithm="ssh-rsa"): + sig = self.key.sign( + data, + padding=padding.PKCS1v15(), + algorithm=self.HASHES[algorithm](), + ) + m = Message() + m.add_string(algorithm.replace("-cert-v01@openssh.com", "")) + m.add_string(sig) + return m + + def verify_ssh_sig(self, data, msg): + sig_algorithm = msg.get_text() + if sig_algorithm not in self.HASHES: + return False + key = self.key + if isinstance(key, rsa.RSAPrivateKey): + key = key.public_key() + + # NOTE: pad received signature with leading zeros, key.verify() + # expects a signature of key size (e.g. PuTTY doesn't pad) + sign = msg.get_binary() + diff = key.key_size - len(sign) * 8 + if diff > 0: + sign = b"\x00" * ((diff + 7) // 8) + sign + + try: + key.verify( + sign, data, padding.PKCS1v15(), self.HASHES[sig_algorithm]() + ) + except InvalidSignature: + return False + else: + return True + + def write_private_key_file(self, filename, password=None): + self._write_private_key_file( + filename, + self.key, + serialization.PrivateFormat.TraditionalOpenSSL, + password=password, + ) + + def write_private_key(self, file_obj, password=None): + self._write_private_key( + file_obj, + self.key, + serialization.PrivateFormat.TraditionalOpenSSL, + password=password, + ) + + @staticmethod + def generate(bits, progress_func=None): + """ + Generate a new private RSA key. This factory function can be used to + generate a new host key or authentication key. + + :param int bits: number of bits the generated key should be. + :param progress_func: Unused + :return: new `.RSAKey` private key + """ + key = rsa.generate_private_key( + public_exponent=65537, key_size=bits, backend=default_backend() + ) + return RSAKey(key=key) + + # ...internals... + + def _from_private_key_file(self, filename, password): + data = self._read_private_key_file("RSA", filename, password) + self._decode_key(data) + + def _from_private_key(self, file_obj, password): + data = self._read_private_key("RSA", file_obj, password) + self._decode_key(data) + + def _decode_key(self, data): + pkformat, data = data + if pkformat == self._PRIVATE_KEY_FORMAT_ORIGINAL: + try: + key = serialization.load_der_private_key( + data, password=None, backend=default_backend() + ) + except (ValueError, TypeError, UnsupportedAlgorithm) as e: + raise SSHException(str(e)) + elif pkformat == self._PRIVATE_KEY_FORMAT_OPENSSH: + n, e, d, iqmp, p, q = self._uint32_cstruct_unpack(data, "iiiiii") + public_numbers = rsa.RSAPublicNumbers(e=e, n=n) + key = rsa.RSAPrivateNumbers( + p=p, + q=q, + d=d, + dmp1=d % (p - 1), + dmq1=d % (q - 1), + iqmp=iqmp, + public_numbers=public_numbers, + ).private_key(default_backend()) + else: + self._got_bad_key_format_id(pkformat) + assert isinstance(key, rsa.RSAPrivateKey) + self.key = key diff --git a/paramiko/server.py b/paramiko/server.py new file mode 100644 index 0000000..6b0bb0f --- /dev/null +++ b/paramiko/server.py @@ -0,0 +1,732 @@ +# Copyright (C) 2003-2007 Robey Pointer <robeypointer@gmail.com> +# +# This file is part of paramiko. +# +# Paramiko is free software; you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free +# Software Foundation; either version 2.1 of the License, or (at your option) +# any later version. +# +# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Paramiko; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +""" +`.ServerInterface` is an interface to override for server support. +""" + +import threading +from paramiko import util +from paramiko.common import ( + DEBUG, + ERROR, + OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED, + AUTH_FAILED, + AUTH_SUCCESSFUL, +) + + +class ServerInterface: + """ + This class defines an interface for controlling the behavior of Paramiko + in server mode. + + Methods on this class are called from Paramiko's primary thread, so you + shouldn't do too much work in them. (Certainly nothing that blocks or + sleeps.) + """ + + def check_channel_request(self, kind, chanid): + """ + Determine if a channel request of a given type will be granted, and + return ``OPEN_SUCCEEDED`` or an error code. This method is + called in server mode when the client requests a channel, after + authentication is complete. + + If you allow channel requests (and an ssh server that didn't would be + useless), you should also override some of the channel request methods + below, which are used to determine which services will be allowed on + a given channel: + + - `check_channel_pty_request` + - `check_channel_shell_request` + - `check_channel_subsystem_request` + - `check_channel_window_change_request` + - `check_channel_x11_request` + - `check_channel_forward_agent_request` + + The ``chanid`` parameter is a small number that uniquely identifies the + channel within a `.Transport`. A `.Channel` object is not created + unless this method returns ``OPEN_SUCCEEDED`` -- once a + `.Channel` object is created, you can call `.Channel.get_id` to + retrieve the channel ID. + + The return value should either be ``OPEN_SUCCEEDED`` (or + ``0``) to allow the channel request, or one of the following error + codes to reject it: + + - ``OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED`` + - ``OPEN_FAILED_CONNECT_FAILED`` + - ``OPEN_FAILED_UNKNOWN_CHANNEL_TYPE`` + - ``OPEN_FAILED_RESOURCE_SHORTAGE`` + + The default implementation always returns + ``OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED``. + + :param str kind: + the kind of channel the client would like to open (usually + ``"session"``). + :param int chanid: ID of the channel + :return: an `int` success or failure code (listed above) + """ + return OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED + + def get_allowed_auths(self, username): + """ + Return a list of authentication methods supported by the server. + This list is sent to clients attempting to authenticate, to inform them + of authentication methods that might be successful. + + The "list" is actually a string of comma-separated names of types of + authentication. Possible values are ``"password"``, ``"publickey"``, + and ``"none"``. + + The default implementation always returns ``"password"``. + + :param str username: the username requesting authentication. + :return: a comma-separated `str` of authentication types + """ + return "password" + + def check_auth_none(self, username): + """ + Determine if a client may open channels with no (further) + authentication. + + Return ``AUTH_FAILED`` if the client must authenticate, or + ``AUTH_SUCCESSFUL`` if it's okay for the client to not + authenticate. + + The default implementation always returns ``AUTH_FAILED``. + + :param str username: the username of the client. + :return: + ``AUTH_FAILED`` if the authentication fails; ``AUTH_SUCCESSFUL`` if + it succeeds. + :rtype: int + """ + return AUTH_FAILED + + def check_auth_password(self, username, password): + """ + Determine if a given username and password supplied by the client is + acceptable for use in authentication. + + Return ``AUTH_FAILED`` if the password is not accepted, + ``AUTH_SUCCESSFUL`` if the password is accepted and completes + the authentication, or ``AUTH_PARTIALLY_SUCCESSFUL`` if your + authentication is stateful, and this key is accepted for + authentication, but more authentication is required. (In this latter + case, `get_allowed_auths` will be called to report to the client what + options it has for continuing the authentication.) + + The default implementation always returns ``AUTH_FAILED``. + + :param str username: the username of the authenticating client. + :param str password: the password given by the client. + :return: + ``AUTH_FAILED`` if the authentication fails; ``AUTH_SUCCESSFUL`` if + it succeeds; ``AUTH_PARTIALLY_SUCCESSFUL`` if the password auth is + successful, but authentication must continue. + :rtype: int + """ + return AUTH_FAILED + + def check_auth_publickey(self, username, key): + """ + Determine if a given key supplied by the client is acceptable for use + in authentication. You should override this method in server mode to + check the username and key and decide if you would accept a signature + made using this key. + + Return ``AUTH_FAILED`` if the key is not accepted, + ``AUTH_SUCCESSFUL`` if the key is accepted and completes the + authentication, or ``AUTH_PARTIALLY_SUCCESSFUL`` if your + authentication is stateful, and this password is accepted for + authentication, but more authentication is required. (In this latter + case, `get_allowed_auths` will be called to report to the client what + options it has for continuing the authentication.) + + Note that you don't have to actually verify any key signtature here. + If you're willing to accept the key, Paramiko will do the work of + verifying the client's signature. + + The default implementation always returns ``AUTH_FAILED``. + + :param str username: the username of the authenticating client + :param .PKey key: the key object provided by the client + :return: + ``AUTH_FAILED`` if the client can't authenticate with this key; + ``AUTH_SUCCESSFUL`` if it can; ``AUTH_PARTIALLY_SUCCESSFUL`` if it + can authenticate with this key but must continue with + authentication + :rtype: int + """ + return AUTH_FAILED + + def check_auth_interactive(self, username, submethods): + """ + Begin an interactive authentication challenge, if supported. You + should override this method in server mode if you want to support the + ``"keyboard-interactive"`` auth type, which requires you to send a + series of questions for the client to answer. + + Return ``AUTH_FAILED`` if this auth method isn't supported. Otherwise, + you should return an `.InteractiveQuery` object containing the prompts + and instructions for the user. The response will be sent via a call + to `check_auth_interactive_response`. + + The default implementation always returns ``AUTH_FAILED``. + + :param str username: the username of the authenticating client + :param str submethods: + a comma-separated list of methods preferred by the client (usually + empty) + :return: + ``AUTH_FAILED`` if this auth method isn't supported; otherwise an + object containing queries for the user + :rtype: int or `.InteractiveQuery` + """ + return AUTH_FAILED + + def check_auth_interactive_response(self, responses): + """ + Continue or finish an interactive authentication challenge, if + supported. You should override this method in server mode if you want + to support the ``"keyboard-interactive"`` auth type. + + Return ``AUTH_FAILED`` if the responses are not accepted, + ``AUTH_SUCCESSFUL`` if the responses are accepted and complete + the authentication, or ``AUTH_PARTIALLY_SUCCESSFUL`` if your + authentication is stateful, and this set of responses is accepted for + authentication, but more authentication is required. (In this latter + case, `get_allowed_auths` will be called to report to the client what + options it has for continuing the authentication.) + + If you wish to continue interactive authentication with more questions, + you may return an `.InteractiveQuery` object, which should cause the + client to respond with more answers, calling this method again. This + cycle can continue indefinitely. + + The default implementation always returns ``AUTH_FAILED``. + + :param responses: list of `str` responses from the client + :return: + ``AUTH_FAILED`` if the authentication fails; ``AUTH_SUCCESSFUL`` if + it succeeds; ``AUTH_PARTIALLY_SUCCESSFUL`` if the interactive auth + is successful, but authentication must continue; otherwise an + object containing queries for the user + :rtype: int or `.InteractiveQuery` + """ + return AUTH_FAILED + + def check_auth_gssapi_with_mic( + self, username, gss_authenticated=AUTH_FAILED, cc_file=None + ): + """ + Authenticate the given user to the server if he is a valid krb5 + principal. + + :param str username: The username of the authenticating client + :param int gss_authenticated: The result of the krb5 authentication + :param str cc_filename: The krb5 client credentials cache filename + :return: ``AUTH_FAILED`` if the user is not authenticated otherwise + ``AUTH_SUCCESSFUL`` + :rtype: int + :note: Kerberos credential delegation is not supported. + :see: `.ssh_gss` + :note: : We are just checking in L{AuthHandler} that the given user is + a valid krb5 principal! + We don't check if the krb5 principal is allowed to log in on + the server, because there is no way to do that in python. So + if you develop your own SSH server with paramiko for a cetain + platform like Linux, you should call C{krb5_kuserok()} in + your local kerberos library to make sure that the + krb5_principal has an account on the server and is allowed to + log in as a user. + :see: http://www.unix.com/man-page/all/3/krb5_kuserok/ + """ + if gss_authenticated == AUTH_SUCCESSFUL: + return AUTH_SUCCESSFUL + return AUTH_FAILED + + def check_auth_gssapi_keyex( + self, username, gss_authenticated=AUTH_FAILED, cc_file=None + ): + """ + Authenticate the given user to the server if he is a valid krb5 + principal and GSS-API Key Exchange was performed. + If GSS-API Key Exchange was not performed, this authentication method + won't be available. + + :param str username: The username of the authenticating client + :param int gss_authenticated: The result of the krb5 authentication + :param str cc_filename: The krb5 client credentials cache filename + :return: ``AUTH_FAILED`` if the user is not authenticated otherwise + ``AUTH_SUCCESSFUL`` + :rtype: int + :note: Kerberos credential delegation is not supported. + :see: `.ssh_gss` `.kex_gss` + :note: : We are just checking in L{AuthHandler} that the given user is + a valid krb5 principal! + We don't check if the krb5 principal is allowed to log in on + the server, because there is no way to do that in python. So + if you develop your own SSH server with paramiko for a cetain + platform like Linux, you should call C{krb5_kuserok()} in + your local kerberos library to make sure that the + krb5_principal has an account on the server and is allowed + to log in as a user. + :see: http://www.unix.com/man-page/all/3/krb5_kuserok/ + """ + if gss_authenticated == AUTH_SUCCESSFUL: + return AUTH_SUCCESSFUL + return AUTH_FAILED + + def enable_auth_gssapi(self): + """ + Overwrite this function in your SSH server to enable GSSAPI + authentication. + The default implementation always returns false. + + :returns bool: Whether GSSAPI authentication is enabled. + :see: `.ssh_gss` + """ + UseGSSAPI = False + return UseGSSAPI + + def check_port_forward_request(self, address, port): + """ + Handle a request for port forwarding. The client is asking that + connections to the given address and port be forwarded back across + this ssh connection. An address of ``"0.0.0.0"`` indicates a global + address (any address associated with this server) and a port of ``0`` + indicates that no specific port is requested (usually the OS will pick + a port). + + The default implementation always returns ``False``, rejecting the + port forwarding request. If the request is accepted, you should return + the port opened for listening. + + :param str address: the requested address + :param int port: the requested port + :return: + the port number (`int`) that was opened for listening, or ``False`` + to reject + """ + return False + + def cancel_port_forward_request(self, address, port): + """ + The client would like to cancel a previous port-forwarding request. + If the given address and port is being forwarded across this ssh + connection, the port should be closed. + + :param str address: the forwarded address + :param int port: the forwarded port + """ + pass + + def check_global_request(self, kind, msg): + """ + Handle a global request of the given ``kind``. This method is called + in server mode and client mode, whenever the remote host makes a global + request. If there are any arguments to the request, they will be in + ``msg``. + + There aren't any useful global requests defined, aside from port + forwarding, so usually this type of request is an extension to the + protocol. + + If the request was successful and you would like to return contextual + data to the remote host, return a tuple. Items in the tuple will be + sent back with the successful result. (Note that the items in the + tuple can only be strings, ints, or bools.) + + The default implementation always returns ``False``, indicating that it + does not support any global requests. + + .. note:: Port forwarding requests are handled separately, in + `check_port_forward_request`. + + :param str kind: the kind of global request being made. + :param .Message msg: any extra arguments to the request. + :return: + ``True`` or a `tuple` of data if the request was granted; ``False`` + otherwise. + """ + return False + + # ...Channel requests... + + def check_channel_pty_request( + self, channel, term, width, height, pixelwidth, pixelheight, modes + ): + """ + Determine if a pseudo-terminal of the given dimensions (usually + requested for shell access) can be provided on the given channel. + + The default implementation always returns ``False``. + + :param .Channel channel: the `.Channel` the pty request arrived on. + :param str term: type of terminal requested (for example, ``"vt100"``). + :param int width: width of screen in characters. + :param int height: height of screen in characters. + :param int pixelwidth: + width of screen in pixels, if known (may be ``0`` if unknown). + :param int pixelheight: + height of screen in pixels, if known (may be ``0`` if unknown). + :return: + ``True`` if the pseudo-terminal has been allocated; ``False`` + otherwise. + """ + return False + + def check_channel_shell_request(self, channel): + """ + Determine if a shell will be provided to the client on the given + channel. If this method returns ``True``, the channel should be + connected to the stdin/stdout of a shell (or something that acts like + a shell). + + The default implementation always returns ``False``. + + :param .Channel channel: the `.Channel` the request arrived on. + :return: + ``True`` if this channel is now hooked up to a shell; ``False`` if + a shell can't or won't be provided. + """ + return False + + def check_channel_exec_request(self, channel, command): + """ + Determine if a shell command will be executed for the client. If this + method returns ``True``, the channel should be connected to the stdin, + stdout, and stderr of the shell command. + + The default implementation always returns ``False``. + + :param .Channel channel: the `.Channel` the request arrived on. + :param str command: the command to execute. + :return: + ``True`` if this channel is now hooked up to the stdin, stdout, and + stderr of the executing command; ``False`` if the command will not + be executed. + + .. versionadded:: 1.1 + """ + return False + + def check_channel_subsystem_request(self, channel, name): + """ + Determine if a requested subsystem will be provided to the client on + the given channel. If this method returns ``True``, all future I/O + through this channel will be assumed to be connected to the requested + subsystem. An example of a subsystem is ``sftp``. + + The default implementation checks for a subsystem handler assigned via + `.Transport.set_subsystem_handler`. + If one has been set, the handler is invoked and this method returns + ``True``. Otherwise it returns ``False``. + + .. note:: Because the default implementation uses the `.Transport` to + identify valid subsystems, you probably won't need to override this + method. + + :param .Channel channel: the `.Channel` the pty request arrived on. + :param str name: name of the requested subsystem. + :return: + ``True`` if this channel is now hooked up to the requested + subsystem; ``False`` if that subsystem can't or won't be provided. + """ + transport = channel.get_transport() + handler_class, args, kwargs = transport._get_subsystem_handler(name) + if handler_class is None: + return False + handler = handler_class(channel, name, self, *args, **kwargs) + handler.start() + return True + + def check_channel_window_change_request( + self, channel, width, height, pixelwidth, pixelheight + ): + """ + Determine if the pseudo-terminal on the given channel can be resized. + This only makes sense if a pty was previously allocated on it. + + The default implementation always returns ``False``. + + :param .Channel channel: the `.Channel` the pty request arrived on. + :param int width: width of screen in characters. + :param int height: height of screen in characters. + :param int pixelwidth: + width of screen in pixels, if known (may be ``0`` if unknown). + :param int pixelheight: + height of screen in pixels, if known (may be ``0`` if unknown). + :return: ``True`` if the terminal was resized; ``False`` if not. + """ + return False + + def check_channel_x11_request( + self, + channel, + single_connection, + auth_protocol, + auth_cookie, + screen_number, + ): + """ + Determine if the client will be provided with an X11 session. If this + method returns ``True``, X11 applications should be routed through new + SSH channels, using `.Transport.open_x11_channel`. + + The default implementation always returns ``False``. + + :param .Channel channel: the `.Channel` the X11 request arrived on + :param bool single_connection: + ``True`` if only a single X11 channel should be opened, else + ``False``. + :param str auth_protocol: the protocol used for X11 authentication + :param str auth_cookie: the cookie used to authenticate to X11 + :param int screen_number: the number of the X11 screen to connect to + :return: ``True`` if the X11 session was opened; ``False`` if not + """ + return False + + def check_channel_forward_agent_request(self, channel): + """ + Determine if the client will be provided with an forward agent session. + If this method returns ``True``, the server will allow SSH Agent + forwarding. + + The default implementation always returns ``False``. + + :param .Channel channel: the `.Channel` the request arrived on + :return: ``True`` if the AgentForward was loaded; ``False`` if not + + If ``True`` is returned, the server should create an + :class:`AgentServerProxy` to access the agent. + """ + return False + + def check_channel_direct_tcpip_request(self, chanid, origin, destination): + """ + Determine if a local port forwarding channel will be granted, and + return ``OPEN_SUCCEEDED`` or an error code. This method is + called in server mode when the client requests a channel, after + authentication is complete. + + The ``chanid`` parameter is a small number that uniquely identifies the + channel within a `.Transport`. A `.Channel` object is not created + unless this method returns ``OPEN_SUCCEEDED`` -- once a + `.Channel` object is created, you can call `.Channel.get_id` to + retrieve the channel ID. + + The origin and destination parameters are (ip_address, port) tuples + that correspond to both ends of the TCP connection in the forwarding + tunnel. + + The return value should either be ``OPEN_SUCCEEDED`` (or + ``0``) to allow the channel request, or one of the following error + codes to reject it: + + - ``OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED`` + - ``OPEN_FAILED_CONNECT_FAILED`` + - ``OPEN_FAILED_UNKNOWN_CHANNEL_TYPE`` + - ``OPEN_FAILED_RESOURCE_SHORTAGE`` + + The default implementation always returns + ``OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED``. + + :param int chanid: ID of the channel + :param tuple origin: + 2-tuple containing the IP address and port of the originator + (client side) + :param tuple destination: + 2-tuple containing the IP address and port of the destination + (server side) + :return: an `int` success or failure code (listed above) + """ + return OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED + + def check_channel_env_request(self, channel, name, value): + """ + Check whether a given environment variable can be specified for the + given channel. This method should return ``True`` if the server + is willing to set the specified environment variable. Note that + some environment variables (e.g., PATH) can be exceedingly + dangerous, so blindly allowing the client to set the environment + is almost certainly not a good idea. + + The default implementation always returns ``False``. + + :param channel: the `.Channel` the env request arrived on + :param str name: name + :param str value: Channel value + :returns: A boolean + """ + return False + + def get_banner(self): + """ + A pre-login banner to display to the user. The message may span + multiple lines separated by crlf pairs. The language should be in + rfc3066 style, for example: en-US + + The default implementation always returns ``(None, None)``. + + :returns: A tuple containing the banner and language code. + + .. versionadded:: 2.3 + """ + return (None, None) + + +class InteractiveQuery: + """ + A query (set of prompts) for a user during interactive authentication. + """ + + def __init__(self, name="", instructions="", *prompts): + """ + Create a new interactive query to send to the client. The name and + instructions are optional, but are generally displayed to the end + user. A list of prompts may be included, or they may be added via + the `add_prompt` method. + + :param str name: name of this query + :param str instructions: + user instructions (usually short) about this query + :param str prompts: one or more authentication prompts + """ + self.name = name + self.instructions = instructions + self.prompts = [] + for x in prompts: + if isinstance(x, str): + self.add_prompt(x) + else: + self.add_prompt(x[0], x[1]) + + def add_prompt(self, prompt, echo=True): + """ + Add a prompt to this query. The prompt should be a (reasonably short) + string. Multiple prompts can be added to the same query. + + :param str prompt: the user prompt + :param bool echo: + ``True`` (default) if the user's response should be echoed; + ``False`` if not (for a password or similar) + """ + self.prompts.append((prompt, echo)) + + +class SubsystemHandler(threading.Thread): + """ + Handler for a subsystem in server mode. If you create a subclass of this + class and pass it to `.Transport.set_subsystem_handler`, an object of this + class will be created for each request for this subsystem. Each new object + will be executed within its own new thread by calling `start_subsystem`. + When that method completes, the channel is closed. + + For example, if you made a subclass ``MP3Handler`` and registered it as the + handler for subsystem ``"mp3"``, then whenever a client has successfully + authenticated and requests subsystem ``"mp3"``, an object of class + ``MP3Handler`` will be created, and `start_subsystem` will be called on + it from a new thread. + """ + + def __init__(self, channel, name, server): + """ + Create a new handler for a channel. This is used by `.ServerInterface` + to start up a new handler when a channel requests this subsystem. You + don't need to override this method, but if you do, be sure to pass the + ``channel`` and ``name`` parameters through to the original + ``__init__`` method here. + + :param .Channel channel: the channel associated with this + subsystem request. + :param str name: name of the requested subsystem. + :param .ServerInterface server: + the server object for the session that started this subsystem + """ + threading.Thread.__init__(self, target=self._run) + self.__channel = channel + self.__transport = channel.get_transport() + self.__name = name + self.__server = server + + def get_server(self): + """ + Return the `.ServerInterface` object associated with this channel and + subsystem. + """ + return self.__server + + def _run(self): + try: + self.__transport._log( + DEBUG, "Starting handler for subsystem {}".format(self.__name) + ) + self.start_subsystem(self.__name, self.__transport, self.__channel) + except Exception as e: + self.__transport._log( + ERROR, + 'Exception in subsystem handler for "{}": {}'.format( + self.__name, e + ), + ) + self.__transport._log(ERROR, util.tb_strings()) + try: + self.finish_subsystem() + except: + pass + + def start_subsystem(self, name, transport, channel): + """ + Process an ssh subsystem in server mode. This method is called on a + new object (and in a new thread) for each subsystem request. It is + assumed that all subsystem logic will take place here, and when the + subsystem is finished, this method will return. After this method + returns, the channel is closed. + + The combination of ``transport`` and ``channel`` are unique; this + handler corresponds to exactly one `.Channel` on one `.Transport`. + + .. note:: + It is the responsibility of this method to exit if the underlying + `.Transport` is closed. This can be done by checking + `.Transport.is_active` or noticing an EOF on the `.Channel`. If + this method loops forever without checking for this case, your + Python interpreter may refuse to exit because this thread will + still be running. + + :param str name: name of the requested subsystem. + :param .Transport transport: the server-mode `.Transport`. + :param .Channel channel: the channel associated with this subsystem + request. + """ + pass + + def finish_subsystem(self): + """ + Perform any cleanup at the end of a subsystem. The default + implementation just closes the channel. + + .. versionadded:: 1.1 + """ + self.__channel.close() diff --git a/paramiko/sftp.py b/paramiko/sftp.py new file mode 100644 index 0000000..b3528d4 --- /dev/null +++ b/paramiko/sftp.py @@ -0,0 +1,224 @@ +# Copyright (C) 2003-2007 Robey Pointer <robeypointer@gmail.com> +# +# This file is part of paramiko. +# +# Paramiko is free software; you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free +# Software Foundation; either version 2.1 of the License, or (at your option) +# any later version. +# +# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Paramiko; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +import select +import socket +import struct + +from paramiko import util +from paramiko.common import DEBUG, byte_chr, byte_ord +from paramiko.message import Message + + +( + CMD_INIT, + CMD_VERSION, + CMD_OPEN, + CMD_CLOSE, + CMD_READ, + CMD_WRITE, + CMD_LSTAT, + CMD_FSTAT, + CMD_SETSTAT, + CMD_FSETSTAT, + CMD_OPENDIR, + CMD_READDIR, + CMD_REMOVE, + CMD_MKDIR, + CMD_RMDIR, + CMD_REALPATH, + CMD_STAT, + CMD_RENAME, + CMD_READLINK, + CMD_SYMLINK, +) = range(1, 21) +(CMD_STATUS, CMD_HANDLE, CMD_DATA, CMD_NAME, CMD_ATTRS) = range(101, 106) +(CMD_EXTENDED, CMD_EXTENDED_REPLY) = range(200, 202) + +SFTP_OK = 0 +( + SFTP_EOF, + SFTP_NO_SUCH_FILE, + SFTP_PERMISSION_DENIED, + SFTP_FAILURE, + SFTP_BAD_MESSAGE, + SFTP_NO_CONNECTION, + SFTP_CONNECTION_LOST, + SFTP_OP_UNSUPPORTED, +) = range(1, 9) + +SFTP_DESC = [ + "Success", + "End of file", + "No such file", + "Permission denied", + "Failure", + "Bad message", + "No connection", + "Connection lost", + "Operation unsupported", +] + +SFTP_FLAG_READ = 0x1 +SFTP_FLAG_WRITE = 0x2 +SFTP_FLAG_APPEND = 0x4 +SFTP_FLAG_CREATE = 0x8 +SFTP_FLAG_TRUNC = 0x10 +SFTP_FLAG_EXCL = 0x20 + +_VERSION = 3 + + +# for debugging +CMD_NAMES = { + CMD_INIT: "init", + CMD_VERSION: "version", + CMD_OPEN: "open", + CMD_CLOSE: "close", + CMD_READ: "read", + CMD_WRITE: "write", + CMD_LSTAT: "lstat", + CMD_FSTAT: "fstat", + CMD_SETSTAT: "setstat", + CMD_FSETSTAT: "fsetstat", + CMD_OPENDIR: "opendir", + CMD_READDIR: "readdir", + CMD_REMOVE: "remove", + CMD_MKDIR: "mkdir", + CMD_RMDIR: "rmdir", + CMD_REALPATH: "realpath", + CMD_STAT: "stat", + CMD_RENAME: "rename", + CMD_READLINK: "readlink", + CMD_SYMLINK: "symlink", + CMD_STATUS: "status", + CMD_HANDLE: "handle", + CMD_DATA: "data", + CMD_NAME: "name", + CMD_ATTRS: "attrs", + CMD_EXTENDED: "extended", + CMD_EXTENDED_REPLY: "extended_reply", +} + + +# TODO: rewrite SFTP file/server modules' overly-flexible "make a request with +# xyz components" so we don't need this very silly method of signaling whether +# a given Python integer should be 32- or 64-bit. +# NOTE: this only became an issue when dropping Python 2 support; prior to +# doing so, we had to support actual-longs, which served as that signal. This +# is simply recreating that structure in a more tightly scoped fashion. +class int64(int): + pass + + +class SFTPError(Exception): + pass + + +class BaseSFTP: + def __init__(self): + self.logger = util.get_logger("paramiko.sftp") + self.sock = None + self.ultra_debug = False + + # ...internals... + + def _send_version(self): + m = Message() + m.add_int(_VERSION) + self._send_packet(CMD_INIT, m) + t, data = self._read_packet() + if t != CMD_VERSION: + raise SFTPError("Incompatible sftp protocol") + version = struct.unpack(">I", data[:4])[0] + # if version != _VERSION: + # raise SFTPError('Incompatible sftp protocol') + return version + + def _send_server_version(self): + # winscp will freak out if the server sends version info before the + # client finishes sending INIT. + t, data = self._read_packet() + if t != CMD_INIT: + raise SFTPError("Incompatible sftp protocol") + version = struct.unpack(">I", data[:4])[0] + # advertise that we support "check-file" + extension_pairs = ["check-file", "md5,sha1"] + msg = Message() + msg.add_int(_VERSION) + msg.add(*extension_pairs) + self._send_packet(CMD_VERSION, msg) + return version + + def _log(self, level, msg, *args): + self.logger.log(level, msg, *args) + + def _write_all(self, out): + while len(out) > 0: + n = self.sock.send(out) + if n <= 0: + raise EOFError() + if n == len(out): + return + out = out[n:] + return + + def _read_all(self, n): + out = bytes() + while n > 0: + if isinstance(self.sock, socket.socket): + # sometimes sftp is used directly over a socket instead of + # through a paramiko channel. in this case, check periodically + # if the socket is closed. (for some reason, recv() won't ever + # return or raise an exception, but calling select on a closed + # socket will.) + while True: + read, write, err = select.select([self.sock], [], [], 0.1) + if len(read) > 0: + x = self.sock.recv(n) + break + else: + x = self.sock.recv(n) + + if len(x) == 0: + raise EOFError() + out += x + n -= len(x) + return out + + def _send_packet(self, t, packet): + packet = packet.asbytes() + out = struct.pack(">I", len(packet) + 1) + byte_chr(t) + packet + if self.ultra_debug: + self._log(DEBUG, util.format_binary(out, "OUT: ")) + self._write_all(out) + + def _read_packet(self): + x = self._read_all(4) + # most sftp servers won't accept packets larger than about 32k, so + # anything with the high byte set (> 16MB) is just garbage. + if byte_ord(x[0]): + raise SFTPError("Garbage packet received") + size = struct.unpack(">I", x)[0] + data = self._read_all(size) + if self.ultra_debug: + self._log(DEBUG, util.format_binary(data, "IN: ")) + if size > 0: + t = byte_ord(data[0]) + return t, data[1:] + return 0, bytes() diff --git a/paramiko/sftp_attr.py b/paramiko/sftp_attr.py new file mode 100644 index 0000000..18ffbf8 --- /dev/null +++ b/paramiko/sftp_attr.py @@ -0,0 +1,239 @@ +# Copyright (C) 2003-2006 Robey Pointer <robeypointer@gmail.com> +# +# This file is part of paramiko. +# +# Paramiko is free software; you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free +# Software Foundation; either version 2.1 of the License, or (at your option) +# any later version. +# +# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Paramiko; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +import stat +import time +from paramiko.common import x80000000, o700, o70, xffffffff + + +class SFTPAttributes: + """ + Representation of the attributes of a file (or proxied file) for SFTP in + client or server mode. It attempts to mirror the object returned by + `os.stat` as closely as possible, so it may have the following fields, + with the same meanings as those returned by an `os.stat` object: + + - ``st_size`` + - ``st_uid`` + - ``st_gid`` + - ``st_mode`` + - ``st_atime`` + - ``st_mtime`` + + Because SFTP allows flags to have other arbitrary named attributes, these + are stored in a dict named ``attr``. Occasionally, the filename is also + stored, in ``filename``. + """ + + FLAG_SIZE = 1 + FLAG_UIDGID = 2 + FLAG_PERMISSIONS = 4 + FLAG_AMTIME = 8 + FLAG_EXTENDED = x80000000 + + def __init__(self): + """ + Create a new (empty) SFTPAttributes object. All fields will be empty. + """ + self._flags = 0 + self.st_size = None + self.st_uid = None + self.st_gid = None + self.st_mode = None + self.st_atime = None + self.st_mtime = None + self.attr = {} + + @classmethod + def from_stat(cls, obj, filename=None): + """ + Create an `.SFTPAttributes` object from an existing ``stat`` object (an + object returned by `os.stat`). + + :param object obj: an object returned by `os.stat` (or equivalent). + :param str filename: the filename associated with this file. + :return: new `.SFTPAttributes` object with the same attribute fields. + """ + attr = cls() + attr.st_size = obj.st_size + attr.st_uid = obj.st_uid + attr.st_gid = obj.st_gid + attr.st_mode = obj.st_mode + attr.st_atime = obj.st_atime + attr.st_mtime = obj.st_mtime + if filename is not None: + attr.filename = filename + return attr + + def __repr__(self): + return "<SFTPAttributes: {}>".format(self._debug_str()) + + # ...internals... + @classmethod + def _from_msg(cls, msg, filename=None, longname=None): + attr = cls() + attr._unpack(msg) + if filename is not None: + attr.filename = filename + if longname is not None: + attr.longname = longname + return attr + + def _unpack(self, msg): + self._flags = msg.get_int() + if self._flags & self.FLAG_SIZE: + self.st_size = msg.get_int64() + if self._flags & self.FLAG_UIDGID: + self.st_uid = msg.get_int() + self.st_gid = msg.get_int() + if self._flags & self.FLAG_PERMISSIONS: + self.st_mode = msg.get_int() + if self._flags & self.FLAG_AMTIME: + self.st_atime = msg.get_int() + self.st_mtime = msg.get_int() + if self._flags & self.FLAG_EXTENDED: + count = msg.get_int() + for i in range(count): + self.attr[msg.get_string()] = msg.get_string() + + def _pack(self, msg): + self._flags = 0 + if self.st_size is not None: + self._flags |= self.FLAG_SIZE + if (self.st_uid is not None) and (self.st_gid is not None): + self._flags |= self.FLAG_UIDGID + if self.st_mode is not None: + self._flags |= self.FLAG_PERMISSIONS + if (self.st_atime is not None) and (self.st_mtime is not None): + self._flags |= self.FLAG_AMTIME + if len(self.attr) > 0: + self._flags |= self.FLAG_EXTENDED + msg.add_int(self._flags) + if self._flags & self.FLAG_SIZE: + msg.add_int64(self.st_size) + if self._flags & self.FLAG_UIDGID: + msg.add_int(self.st_uid) + msg.add_int(self.st_gid) + if self._flags & self.FLAG_PERMISSIONS: + msg.add_int(self.st_mode) + if self._flags & self.FLAG_AMTIME: + # throw away any fractional seconds + msg.add_int(int(self.st_atime)) + msg.add_int(int(self.st_mtime)) + if self._flags & self.FLAG_EXTENDED: + msg.add_int(len(self.attr)) + for key, val in self.attr.items(): + msg.add_string(key) + msg.add_string(val) + return + + def _debug_str(self): + out = "[ " + if self.st_size is not None: + out += "size={} ".format(self.st_size) + if (self.st_uid is not None) and (self.st_gid is not None): + out += "uid={} gid={} ".format(self.st_uid, self.st_gid) + if self.st_mode is not None: + out += "mode=" + oct(self.st_mode) + " " + if (self.st_atime is not None) and (self.st_mtime is not None): + out += "atime={} mtime={} ".format(self.st_atime, self.st_mtime) + for k, v in self.attr.items(): + out += '"{}"={!r} '.format(str(k), v) + out += "]" + return out + + @staticmethod + def _rwx(n, suid, sticky=False): + if suid: + suid = 2 + out = "-r"[n >> 2] + "-w"[(n >> 1) & 1] + if sticky: + out += "-xTt"[suid + (n & 1)] + else: + out += "-xSs"[suid + (n & 1)] + return out + + def __str__(self): + """create a unix-style long description of the file (like ls -l)""" + if self.st_mode is not None: + kind = stat.S_IFMT(self.st_mode) + if kind == stat.S_IFIFO: + ks = "p" + elif kind == stat.S_IFCHR: + ks = "c" + elif kind == stat.S_IFDIR: + ks = "d" + elif kind == stat.S_IFBLK: + ks = "b" + elif kind == stat.S_IFREG: + ks = "-" + elif kind == stat.S_IFLNK: + ks = "l" + elif kind == stat.S_IFSOCK: + ks = "s" + else: + ks = "?" + ks += self._rwx( + (self.st_mode & o700) >> 6, self.st_mode & stat.S_ISUID + ) + ks += self._rwx( + (self.st_mode & o70) >> 3, self.st_mode & stat.S_ISGID + ) + ks += self._rwx( + self.st_mode & 7, self.st_mode & stat.S_ISVTX, True + ) + else: + ks = "?---------" + # compute display date + if (self.st_mtime is None) or (self.st_mtime == xffffffff): + # shouldn't really happen + datestr = "(unknown date)" + else: + time_tuple = time.localtime(self.st_mtime) + if abs(time.time() - self.st_mtime) > 15_552_000: + # (15,552,000s = 6 months) + datestr = time.strftime("%d %b %Y", time_tuple) + else: + datestr = time.strftime("%d %b %H:%M", time_tuple) + filename = getattr(self, "filename", "?") + + # not all servers support uid/gid + uid = self.st_uid + gid = self.st_gid + size = self.st_size + if uid is None: + uid = 0 + if gid is None: + gid = 0 + if size is None: + size = 0 + + # TODO: not sure this actually worked as expected beforehand, leaving + # it untouched for the time being, re: .format() upgrade, until someone + # has time to doublecheck + return "%s 1 %-8d %-8d %8d %-12s %s" % ( + ks, + uid, + gid, + size, + datestr, + filename, + ) + + def asbytes(self): + return str(self).encode() diff --git a/paramiko/sftp_client.py b/paramiko/sftp_client.py new file mode 100644 index 0000000..31ac129 --- /dev/null +++ b/paramiko/sftp_client.py @@ -0,0 +1,930 @@ +# Copyright (C) 2003-2007 Robey Pointer <robeypointer@gmail.com> +# +# This file is part of Paramiko. +# +# Paramiko is free software; you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free +# Software Foundation; either version 2.1 of the License, or (at your option) +# any later version. +# +# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Paramiko; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + + +from binascii import hexlify +import errno +import os +import stat +import threading +import time +import weakref +from paramiko import util +from paramiko.channel import Channel +from paramiko.message import Message +from paramiko.common import INFO, DEBUG, o777 +from paramiko.sftp import ( + BaseSFTP, + CMD_OPENDIR, + CMD_HANDLE, + SFTPError, + CMD_READDIR, + CMD_NAME, + CMD_CLOSE, + SFTP_FLAG_READ, + SFTP_FLAG_WRITE, + SFTP_FLAG_CREATE, + SFTP_FLAG_TRUNC, + SFTP_FLAG_APPEND, + SFTP_FLAG_EXCL, + CMD_OPEN, + CMD_REMOVE, + CMD_RENAME, + CMD_MKDIR, + CMD_RMDIR, + CMD_STAT, + CMD_ATTRS, + CMD_LSTAT, + CMD_SYMLINK, + CMD_SETSTAT, + CMD_READLINK, + CMD_REALPATH, + CMD_STATUS, + CMD_EXTENDED, + SFTP_OK, + SFTP_EOF, + SFTP_NO_SUCH_FILE, + SFTP_PERMISSION_DENIED, + int64, +) + +from paramiko.sftp_attr import SFTPAttributes +from paramiko.ssh_exception import SSHException +from paramiko.sftp_file import SFTPFile +from paramiko.util import ClosingContextManager, b, u + + +def _to_unicode(s): + """ + decode a string as ascii or utf8 if possible (as required by the sftp + protocol). if neither works, just return a byte string because the server + probably doesn't know the filename's encoding. + """ + try: + return s.encode("ascii") + except (UnicodeError, AttributeError): + try: + return s.decode("utf-8") + except UnicodeError: + return s + + +b_slash = b"/" + + +class SFTPClient(BaseSFTP, ClosingContextManager): + """ + SFTP client object. + + Used to open an SFTP session across an open SSH `.Transport` and perform + remote file operations. + + Instances of this class may be used as context managers. + """ + + def __init__(self, sock): + """ + Create an SFTP client from an existing `.Channel`. The channel + should already have requested the ``"sftp"`` subsystem. + + An alternate way to create an SFTP client context is by using + `from_transport`. + + :param .Channel sock: an open `.Channel` using the ``"sftp"`` subsystem + + :raises: + `.SSHException` -- if there's an exception while negotiating sftp + """ + BaseSFTP.__init__(self) + self.sock = sock + self.ultra_debug = False + self.request_number = 1 + # lock for request_number + self._lock = threading.Lock() + self._cwd = None + # request # -> SFTPFile + self._expecting = weakref.WeakValueDictionary() + if type(sock) is Channel: + # override default logger + transport = self.sock.get_transport() + self.logger = util.get_logger( + transport.get_log_channel() + ".sftp" + ) + self.ultra_debug = transport.get_hexdump() + try: + server_version = self._send_version() + except EOFError: + raise SSHException("EOF during negotiation") + self._log( + INFO, + "Opened sftp connection (server version {})".format( + server_version + ), + ) + + @classmethod + def from_transport(cls, t, window_size=None, max_packet_size=None): + """ + Create an SFTP client channel from an open `.Transport`. + + Setting the window and packet sizes might affect the transfer speed. + The default settings in the `.Transport` class are the same as in + OpenSSH and should work adequately for both files transfers and + interactive sessions. + + :param .Transport t: an open `.Transport` which is already + authenticated + :param int window_size: + optional window size for the `.SFTPClient` session. + :param int max_packet_size: + optional max packet size for the `.SFTPClient` session.. + + :return: + a new `.SFTPClient` object, referring to an sftp session (channel) + across the transport + + .. versionchanged:: 1.15 + Added the ``window_size`` and ``max_packet_size`` arguments. + """ + chan = t.open_session( + window_size=window_size, max_packet_size=max_packet_size + ) + if chan is None: + return None + chan.invoke_subsystem("sftp") + return cls(chan) + + def _log(self, level, msg, *args): + if isinstance(msg, list): + for m in msg: + self._log(level, m, *args) + else: + # NOTE: these bits MUST continue using %-style format junk because + # logging.Logger.log() explicitly requires it. Grump. + # escape '%' in msg (they could come from file or directory names) + # before logging + msg = msg.replace("%", "%%") + super()._log( + level, + "[chan %s] " + msg, + *([self.sock.get_name()] + list(args)) + ) + + def close(self): + """ + Close the SFTP session and its underlying channel. + + .. versionadded:: 1.4 + """ + self._log(INFO, "sftp session closed.") + self.sock.close() + + def get_channel(self): + """ + Return the underlying `.Channel` object for this SFTP session. This + might be useful for doing things like setting a timeout on the channel. + + .. versionadded:: 1.7.1 + """ + return self.sock + + def listdir(self, path="."): + """ + Return a list containing the names of the entries in the given + ``path``. + + The list is in arbitrary order. It does not include the special + entries ``'.'`` and ``'..'`` even if they are present in the folder. + This method is meant to mirror ``os.listdir`` as closely as possible. + For a list of full `.SFTPAttributes` objects, see `listdir_attr`. + + :param str path: path to list (defaults to ``'.'``) + """ + return [f.filename for f in self.listdir_attr(path)] + + def listdir_attr(self, path="."): + """ + Return a list containing `.SFTPAttributes` objects corresponding to + files in the given ``path``. The list is in arbitrary order. It does + not include the special entries ``'.'`` and ``'..'`` even if they are + present in the folder. + + The returned `.SFTPAttributes` objects will each have an additional + field: ``longname``, which may contain a formatted string of the file's + attributes, in unix format. The content of this string will probably + depend on the SFTP server implementation. + + :param str path: path to list (defaults to ``'.'``) + :return: list of `.SFTPAttributes` objects + + .. versionadded:: 1.2 + """ + path = self._adjust_cwd(path) + self._log(DEBUG, "listdir({!r})".format(path)) + t, msg = self._request(CMD_OPENDIR, path) + if t != CMD_HANDLE: + raise SFTPError("Expected handle") + handle = msg.get_binary() + filelist = [] + while True: + try: + t, msg = self._request(CMD_READDIR, handle) + except EOFError: + # done with handle + break + if t != CMD_NAME: + raise SFTPError("Expected name response") + count = msg.get_int() + for i in range(count): + filename = msg.get_text() + longname = msg.get_text() + attr = SFTPAttributes._from_msg(msg, filename, longname) + if (filename != ".") and (filename != ".."): + filelist.append(attr) + self._request(CMD_CLOSE, handle) + return filelist + + def listdir_iter(self, path=".", read_aheads=50): + """ + Generator version of `.listdir_attr`. + + See the API docs for `.listdir_attr` for overall details. + + This function adds one more kwarg on top of `.listdir_attr`: + ``read_aheads``, an integer controlling how many + ``SSH_FXP_READDIR`` requests are made to the server. The default of 50 + should suffice for most file listings as each request/response cycle + may contain multiple files (dependent on server implementation.) + + .. versionadded:: 1.15 + """ + path = self._adjust_cwd(path) + self._log(DEBUG, "listdir({!r})".format(path)) + t, msg = self._request(CMD_OPENDIR, path) + + if t != CMD_HANDLE: + raise SFTPError("Expected handle") + + handle = msg.get_string() + + nums = list() + while True: + try: + # Send out a bunch of readdir requests so that we can read the + # responses later on Section 6.7 of the SSH file transfer RFC + # explains this + # http://filezilla-project.org/specs/draft-ietf-secsh-filexfer-02.txt + for i in range(read_aheads): + num = self._async_request(type(None), CMD_READDIR, handle) + nums.append(num) + + # For each of our sent requests + # Read and parse the corresponding packets + # If we're at the end of our queued requests, then fire off + # some more requests + # Exit the loop when we've reached the end of the directory + # handle + for num in nums: + t, pkt_data = self._read_packet() + msg = Message(pkt_data) + new_num = msg.get_int() + if num == new_num: + if t == CMD_STATUS: + self._convert_status(msg) + count = msg.get_int() + for i in range(count): + filename = msg.get_text() + longname = msg.get_text() + attr = SFTPAttributes._from_msg( + msg, filename, longname + ) + if (filename != ".") and (filename != ".."): + yield attr + + # If we've hit the end of our queued requests, reset nums. + nums = list() + + except EOFError: + self._request(CMD_CLOSE, handle) + return + + def open(self, filename, mode="r", bufsize=-1): + """ + Open a file on the remote server. The arguments are the same as for + Python's built-in `python:file` (aka `python:open`). A file-like + object is returned, which closely mimics the behavior of a normal + Python file object, including the ability to be used as a context + manager. + + The mode indicates how the file is to be opened: ``'r'`` for reading, + ``'w'`` for writing (truncating an existing file), ``'a'`` for + appending, ``'r+'`` for reading/writing, ``'w+'`` for reading/writing + (truncating an existing file), ``'a+'`` for reading/appending. The + Python ``'b'`` flag is ignored, since SSH treats all files as binary. + The ``'U'`` flag is supported in a compatible way. + + Since 1.5.2, an ``'x'`` flag indicates that the operation should only + succeed if the file was created and did not previously exist. This has + no direct mapping to Python's file flags, but is commonly known as the + ``O_EXCL`` flag in posix. + + The file will be buffered in standard Python style by default, but + can be altered with the ``bufsize`` parameter. ``<=0`` turns off + buffering, ``1`` uses line buffering, and any number greater than 1 + (``>1``) uses that specific buffer size. + + :param str filename: name of the file to open + :param str mode: mode (Python-style) to open in + :param int bufsize: desired buffering (default: ``-1``) + :return: an `.SFTPFile` object representing the open file + + :raises: ``IOError`` -- if the file could not be opened. + """ + filename = self._adjust_cwd(filename) + self._log(DEBUG, "open({!r}, {!r})".format(filename, mode)) + imode = 0 + if ("r" in mode) or ("+" in mode): + imode |= SFTP_FLAG_READ + if ("w" in mode) or ("+" in mode) or ("a" in mode): + imode |= SFTP_FLAG_WRITE + if "w" in mode: + imode |= SFTP_FLAG_CREATE | SFTP_FLAG_TRUNC + if "a" in mode: + imode |= SFTP_FLAG_CREATE | SFTP_FLAG_APPEND + if "x" in mode: + imode |= SFTP_FLAG_CREATE | SFTP_FLAG_EXCL + attrblock = SFTPAttributes() + t, msg = self._request(CMD_OPEN, filename, imode, attrblock) + if t != CMD_HANDLE: + raise SFTPError("Expected handle") + handle = msg.get_binary() + self._log( + DEBUG, + "open({!r}, {!r}) -> {}".format( + filename, mode, u(hexlify(handle)) + ), + ) + return SFTPFile(self, handle, mode, bufsize) + + # Python continues to vacillate about "open" vs "file"... + file = open + + def remove(self, path): + """ + Remove the file at the given path. This only works on files; for + removing folders (directories), use `rmdir`. + + :param str path: path (absolute or relative) of the file to remove + + :raises: ``IOError`` -- if the path refers to a folder (directory) + """ + path = self._adjust_cwd(path) + self._log(DEBUG, "remove({!r})".format(path)) + self._request(CMD_REMOVE, path) + + unlink = remove + + def rename(self, oldpath, newpath): + """ + Rename a file or folder from ``oldpath`` to ``newpath``. + + .. note:: + This method implements 'standard' SFTP ``RENAME`` behavior; those + seeking the OpenSSH "POSIX rename" extension behavior should use + `posix_rename`. + + :param str oldpath: + existing name of the file or folder + :param str newpath: + new name for the file or folder, must not exist already + + :raises: + ``IOError`` -- if ``newpath`` is a folder, or something else goes + wrong + """ + oldpath = self._adjust_cwd(oldpath) + newpath = self._adjust_cwd(newpath) + self._log(DEBUG, "rename({!r}, {!r})".format(oldpath, newpath)) + self._request(CMD_RENAME, oldpath, newpath) + + def posix_rename(self, oldpath, newpath): + """ + Rename a file or folder from ``oldpath`` to ``newpath``, following + posix conventions. + + :param str oldpath: existing name of the file or folder + :param str newpath: new name for the file or folder, will be + overwritten if it already exists + + :raises: + ``IOError`` -- if ``newpath`` is a folder, posix-rename is not + supported by the server or something else goes wrong + + :versionadded: 2.2 + """ + oldpath = self._adjust_cwd(oldpath) + newpath = self._adjust_cwd(newpath) + self._log(DEBUG, "posix_rename({!r}, {!r})".format(oldpath, newpath)) + self._request( + CMD_EXTENDED, "posix-rename@openssh.com", oldpath, newpath + ) + + def mkdir(self, path, mode=o777): + """ + Create a folder (directory) named ``path`` with numeric mode ``mode``. + The default mode is 0777 (octal). On some systems, mode is ignored. + Where it is used, the current umask value is first masked out. + + :param str path: name of the folder to create + :param int mode: permissions (posix-style) for the newly-created folder + """ + path = self._adjust_cwd(path) + self._log(DEBUG, "mkdir({!r}, {!r})".format(path, mode)) + attr = SFTPAttributes() + attr.st_mode = mode + self._request(CMD_MKDIR, path, attr) + + def rmdir(self, path): + """ + Remove the folder named ``path``. + + :param str path: name of the folder to remove + """ + path = self._adjust_cwd(path) + self._log(DEBUG, "rmdir({!r})".format(path)) + self._request(CMD_RMDIR, path) + + def stat(self, path): + """ + Retrieve information about a file on the remote system. The return + value is an object whose attributes correspond to the attributes of + Python's ``stat`` structure as returned by ``os.stat``, except that it + contains fewer fields. An SFTP server may return as much or as little + info as it wants, so the results may vary from server to server. + + Unlike a Python `python:stat` object, the result may not be accessed as + a tuple. This is mostly due to the author's slack factor. + + The fields supported are: ``st_mode``, ``st_size``, ``st_uid``, + ``st_gid``, ``st_atime``, and ``st_mtime``. + + :param str path: the filename to stat + :return: + an `.SFTPAttributes` object containing attributes about the given + file + """ + path = self._adjust_cwd(path) + self._log(DEBUG, "stat({!r})".format(path)) + t, msg = self._request(CMD_STAT, path) + if t != CMD_ATTRS: + raise SFTPError("Expected attributes") + return SFTPAttributes._from_msg(msg) + + def lstat(self, path): + """ + Retrieve information about a file on the remote system, without + following symbolic links (shortcuts). This otherwise behaves exactly + the same as `stat`. + + :param str path: the filename to stat + :return: + an `.SFTPAttributes` object containing attributes about the given + file + """ + path = self._adjust_cwd(path) + self._log(DEBUG, "lstat({!r})".format(path)) + t, msg = self._request(CMD_LSTAT, path) + if t != CMD_ATTRS: + raise SFTPError("Expected attributes") + return SFTPAttributes._from_msg(msg) + + def symlink(self, source, dest): + """ + Create a symbolic link to the ``source`` path at ``destination``. + + :param str source: path of the original file + :param str dest: path of the newly created symlink + """ + dest = self._adjust_cwd(dest) + self._log(DEBUG, "symlink({!r}, {!r})".format(source, dest)) + source = b(source) + self._request(CMD_SYMLINK, source, dest) + + def chmod(self, path, mode): + """ + Change the mode (permissions) of a file. The permissions are + unix-style and identical to those used by Python's `os.chmod` + function. + + :param str path: path of the file to change the permissions of + :param int mode: new permissions + """ + path = self._adjust_cwd(path) + self._log(DEBUG, "chmod({!r}, {!r})".format(path, mode)) + attr = SFTPAttributes() + attr.st_mode = mode + self._request(CMD_SETSTAT, path, attr) + + def chown(self, path, uid, gid): + """ + Change the owner (``uid``) and group (``gid``) of a file. As with + Python's `os.chown` function, you must pass both arguments, so if you + only want to change one, use `stat` first to retrieve the current + owner and group. + + :param str path: path of the file to change the owner and group of + :param int uid: new owner's uid + :param int gid: new group id + """ + path = self._adjust_cwd(path) + self._log(DEBUG, "chown({!r}, {!r}, {!r})".format(path, uid, gid)) + attr = SFTPAttributes() + attr.st_uid, attr.st_gid = uid, gid + self._request(CMD_SETSTAT, path, attr) + + def utime(self, path, times): + """ + Set the access and modified times of the file specified by ``path``. + If ``times`` is ``None``, then the file's access and modified times + are set to the current time. Otherwise, ``times`` must be a 2-tuple + of numbers, of the form ``(atime, mtime)``, which is used to set the + access and modified times, respectively. This bizarre API is mimicked + from Python for the sake of consistency -- I apologize. + + :param str path: path of the file to modify + :param tuple times: + ``None`` or a tuple of (access time, modified time) in standard + internet epoch time (seconds since 01 January 1970 GMT) + """ + path = self._adjust_cwd(path) + if times is None: + times = (time.time(), time.time()) + self._log(DEBUG, "utime({!r}, {!r})".format(path, times)) + attr = SFTPAttributes() + attr.st_atime, attr.st_mtime = times + self._request(CMD_SETSTAT, path, attr) + + def truncate(self, path, size): + """ + Change the size of the file specified by ``path``. This usually + extends or shrinks the size of the file, just like the `~file.truncate` + method on Python file objects. + + :param str path: path of the file to modify + :param int size: the new size of the file + """ + path = self._adjust_cwd(path) + self._log(DEBUG, "truncate({!r}, {!r})".format(path, size)) + attr = SFTPAttributes() + attr.st_size = size + self._request(CMD_SETSTAT, path, attr) + + def readlink(self, path): + """ + Return the target of a symbolic link (shortcut). You can use + `symlink` to create these. The result may be either an absolute or + relative pathname. + + :param str path: path of the symbolic link file + :return: target path, as a `str` + """ + path = self._adjust_cwd(path) + self._log(DEBUG, "readlink({!r})".format(path)) + t, msg = self._request(CMD_READLINK, path) + if t != CMD_NAME: + raise SFTPError("Expected name response") + count = msg.get_int() + if count == 0: + return None + if count != 1: + raise SFTPError("Readlink returned {} results".format(count)) + return _to_unicode(msg.get_string()) + + def normalize(self, path): + """ + Return the normalized path (on the server) of a given path. This + can be used to quickly resolve symbolic links or determine what the + server is considering to be the "current folder" (by passing ``'.'`` + as ``path``). + + :param str path: path to be normalized + :return: normalized form of the given path (as a `str`) + + :raises: ``IOError`` -- if the path can't be resolved on the server + """ + path = self._adjust_cwd(path) + self._log(DEBUG, "normalize({!r})".format(path)) + t, msg = self._request(CMD_REALPATH, path) + if t != CMD_NAME: + raise SFTPError("Expected name response") + count = msg.get_int() + if count != 1: + raise SFTPError("Realpath returned {} results".format(count)) + return msg.get_text() + + def chdir(self, path=None): + """ + Change the "current directory" of this SFTP session. Since SFTP + doesn't really have the concept of a current working directory, this is + emulated by Paramiko. Once you use this method to set a working + directory, all operations on this `.SFTPClient` object will be relative + to that path. You can pass in ``None`` to stop using a current working + directory. + + :param str path: new current working directory + + :raises: + ``IOError`` -- if the requested path doesn't exist on the server + + .. versionadded:: 1.4 + """ + if path is None: + self._cwd = None + return + if not stat.S_ISDIR(self.stat(path).st_mode): + code = errno.ENOTDIR + raise SFTPError(code, "{}: {}".format(os.strerror(code), path)) + self._cwd = b(self.normalize(path)) + + def getcwd(self): + """ + Return the "current working directory" for this SFTP session, as + emulated by Paramiko. If no directory has been set with `chdir`, + this method will return ``None``. + + .. versionadded:: 1.4 + """ + # TODO: make class initialize with self._cwd set to self.normalize('.') + return self._cwd and u(self._cwd) + + def _transfer_with_callback(self, reader, writer, file_size, callback): + size = 0 + while True: + data = reader.read(32768) + writer.write(data) + size += len(data) + if len(data) == 0: + break + if callback is not None: + callback(size, file_size) + return size + + def putfo(self, fl, remotepath, file_size=0, callback=None, confirm=True): + """ + Copy the contents of an open file object (``fl``) to the SFTP server as + ``remotepath``. Any exception raised by operations will be passed + through. + + The SFTP operations use pipelining for speed. + + :param fl: opened file or file-like object to copy + :param str remotepath: the destination path on the SFTP server + :param int file_size: + optional size parameter passed to callback. If none is specified, + size defaults to 0 + :param callable callback: + optional callback function (form: ``func(int, int)``) that accepts + the bytes transferred so far and the total bytes to be transferred + (since 1.7.4) + :param bool confirm: + whether to do a stat() on the file afterwards to confirm the file + size (since 1.7.7) + + :return: + an `.SFTPAttributes` object containing attributes about the given + file. + + .. versionadded:: 1.10 + """ + with self.file(remotepath, "wb") as fr: + fr.set_pipelined(True) + size = self._transfer_with_callback( + reader=fl, writer=fr, file_size=file_size, callback=callback + ) + if confirm: + s = self.stat(remotepath) + if s.st_size != size: + raise IOError( + "size mismatch in put! {} != {}".format(s.st_size, size) + ) + else: + s = SFTPAttributes() + return s + + def put(self, localpath, remotepath, callback=None, confirm=True): + """ + Copy a local file (``localpath``) to the SFTP server as ``remotepath``. + Any exception raised by operations will be passed through. This + method is primarily provided as a convenience. + + The SFTP operations use pipelining for speed. + + :param str localpath: the local file to copy + :param str remotepath: the destination path on the SFTP server. Note + that the filename should be included. Only specifying a directory + may result in an error. + :param callable callback: + optional callback function (form: ``func(int, int)``) that accepts + the bytes transferred so far and the total bytes to be transferred + :param bool confirm: + whether to do a stat() on the file afterwards to confirm the file + size + + :return: an `.SFTPAttributes` object containing attributes about the + given file + + .. versionadded:: 1.4 + .. versionchanged:: 1.7.4 + ``callback`` and rich attribute return value added. + .. versionchanged:: 1.7.7 + ``confirm`` param added. + """ + file_size = os.stat(localpath).st_size + with open(localpath, "rb") as fl: + return self.putfo(fl, remotepath, file_size, callback, confirm) + + def getfo(self, remotepath, fl, callback=None, prefetch=True): + """ + Copy a remote file (``remotepath``) from the SFTP server and write to + an open file or file-like object, ``fl``. Any exception raised by + operations will be passed through. This method is primarily provided + as a convenience. + + :param object remotepath: opened file or file-like object to copy to + :param str fl: + the destination path on the local host or open file object + :param callable callback: + optional callback function (form: ``func(int, int)``) that accepts + the bytes transferred so far and the total bytes to be transferred + :param bool prefetch: + controls whether prefetching is performed (default: True) + :return: the `number <int>` of bytes written to the opened file object + + .. versionadded:: 1.10 + .. versionchanged:: 2.8 + Added the ``prefetch`` keyword argument. + """ + file_size = self.stat(remotepath).st_size + with self.open(remotepath, "rb") as fr: + if prefetch: + fr.prefetch(file_size) + return self._transfer_with_callback( + reader=fr, writer=fl, file_size=file_size, callback=callback + ) + + def get(self, remotepath, localpath, callback=None, prefetch=True): + """ + Copy a remote file (``remotepath``) from the SFTP server to the local + host as ``localpath``. Any exception raised by operations will be + passed through. This method is primarily provided as a convenience. + + :param str remotepath: the remote file to copy + :param str localpath: the destination path on the local host + :param callable callback: + optional callback function (form: ``func(int, int)``) that accepts + the bytes transferred so far and the total bytes to be transferred + :param bool prefetch: + controls whether prefetching is performed (default: True) + + .. versionadded:: 1.4 + .. versionchanged:: 1.7.4 + Added the ``callback`` param + .. versionchanged:: 2.8 + Added the ``prefetch`` keyword argument. + """ + with open(localpath, "wb") as fl: + size = self.getfo(remotepath, fl, callback, prefetch) + s = os.stat(localpath) + if s.st_size != size: + raise IOError( + "size mismatch in get! {} != {}".format(s.st_size, size) + ) + + # ...internals... + + def _request(self, t, *args): + num = self._async_request(type(None), t, *args) + return self._read_response(num) + + def _async_request(self, fileobj, t, *args): + # this method may be called from other threads (prefetch) + self._lock.acquire() + try: + msg = Message() + msg.add_int(self.request_number) + for item in args: + if isinstance(item, int64): + msg.add_int64(item) + elif isinstance(item, int): + msg.add_int(item) + elif isinstance(item, SFTPAttributes): + item._pack(msg) + else: + # For all other types, rely on as_string() to either coerce + # to bytes before writing or raise a suitable exception. + msg.add_string(item) + num = self.request_number + self._expecting[num] = fileobj + self.request_number += 1 + finally: + self._lock.release() + self._send_packet(t, msg) + return num + + def _read_response(self, waitfor=None): + while True: + try: + t, data = self._read_packet() + except EOFError as e: + raise SSHException("Server connection dropped: {}".format(e)) + msg = Message(data) + num = msg.get_int() + self._lock.acquire() + try: + if num not in self._expecting: + # might be response for a file that was closed before + # responses came back + self._log(DEBUG, "Unexpected response #{}".format(num)) + if waitfor is None: + # just doing a single check + break + continue + fileobj = self._expecting[num] + del self._expecting[num] + finally: + self._lock.release() + if num == waitfor: + # synchronous + if t == CMD_STATUS: + self._convert_status(msg) + return t, msg + + # can not rewrite this to deal with E721, either as a None check + # nor as not an instance of None or NoneType + if fileobj is not type(None): # noqa + fileobj._async_response(t, msg, num) + if waitfor is None: + # just doing a single check + break + return None, None + + def _finish_responses(self, fileobj): + while fileobj in self._expecting.values(): + self._read_response() + fileobj._check_exception() + + def _convert_status(self, msg): + """ + Raises EOFError or IOError on error status; otherwise does nothing. + """ + code = msg.get_int() + text = msg.get_text() + if code == SFTP_OK: + return + elif code == SFTP_EOF: + raise EOFError(text) + elif code == SFTP_NO_SUCH_FILE: + # clever idea from john a. meinel: map the error codes to errno + raise IOError(errno.ENOENT, text) + elif code == SFTP_PERMISSION_DENIED: + raise IOError(errno.EACCES, text) + else: + raise IOError(text) + + def _adjust_cwd(self, path): + """ + Return an adjusted path if we're emulating a "current working + directory" for the server. + """ + path = b(path) + if self._cwd is None: + return path + if len(path) and path[0:1] == b_slash: + # absolute path + return path + if self._cwd == b_slash: + return self._cwd + path + return self._cwd + b_slash + path + + +class SFTP(SFTPClient): + """ + An alias for `.SFTPClient` for backwards compatibility. + """ + + pass diff --git a/paramiko/sftp_file.py b/paramiko/sftp_file.py new file mode 100644 index 0000000..9a0a6b3 --- /dev/null +++ b/paramiko/sftp_file.py @@ -0,0 +1,570 @@ +# Copyright (C) 2003-2007 Robey Pointer <robeypointer@gmail.com> +# +# This file is part of paramiko. +# +# Paramiko is free software; you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free +# Software Foundation; either version 2.1 of the License, or (at your option) +# any later version. +# +# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Paramiko; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +""" +SFTP file object +""" + + +from binascii import hexlify +from collections import deque +import socket +import threading +import time +from paramiko.common import DEBUG + +from paramiko.file import BufferedFile +from paramiko.util import u +from paramiko.sftp import ( + CMD_CLOSE, + CMD_READ, + CMD_DATA, + SFTPError, + CMD_WRITE, + CMD_STATUS, + CMD_FSTAT, + CMD_ATTRS, + CMD_FSETSTAT, + CMD_EXTENDED, + int64, +) +from paramiko.sftp_attr import SFTPAttributes + + +class SFTPFile(BufferedFile): + """ + Proxy object for a file on the remote server, in client mode SFTP. + + Instances of this class may be used as context managers in the same way + that built-in Python file objects are. + """ + + # Some sftp servers will choke if you send read/write requests larger than + # this size. + MAX_REQUEST_SIZE = 32768 + + def __init__(self, sftp, handle, mode="r", bufsize=-1): + BufferedFile.__init__(self) + self.sftp = sftp + self.handle = handle + BufferedFile._set_mode(self, mode, bufsize) + self.pipelined = False + self._prefetching = False + self._prefetch_done = False + self._prefetch_data = {} + self._prefetch_extents = {} + self._prefetch_lock = threading.Lock() + self._saved_exception = None + self._reqs = deque() + + def __del__(self): + self._close(async_=True) + + def close(self): + """ + Close the file. + """ + self._close(async_=False) + + def _close(self, async_=False): + # We allow double-close without signaling an error, because real + # Python file objects do. However, we must protect against actually + # sending multiple CMD_CLOSE packets, because after we close our + # handle, the same handle may be re-allocated by the server, and we + # may end up mysteriously closing some random other file. (This is + # especially important because we unconditionally call close() from + # __del__.) + if self._closed: + return + self.sftp._log(DEBUG, "close({})".format(u(hexlify(self.handle)))) + if self.pipelined: + self.sftp._finish_responses(self) + BufferedFile.close(self) + try: + if async_: + # GC'd file handle could be called from an arbitrary thread + # -- don't wait for a response + self.sftp._async_request(type(None), CMD_CLOSE, self.handle) + else: + self.sftp._request(CMD_CLOSE, self.handle) + except EOFError: + # may have outlived the Transport connection + pass + except (IOError, socket.error): + # may have outlived the Transport connection + pass + + def _data_in_prefetch_requests(self, offset, size): + k = [ + x for x in list(self._prefetch_extents.values()) if x[0] <= offset + ] + if len(k) == 0: + return False + k.sort(key=lambda x: x[0]) + buf_offset, buf_size = k[-1] + if buf_offset + buf_size <= offset: + # prefetch request ends before this one begins + return False + if buf_offset + buf_size >= offset + size: + # inclusive + return True + # well, we have part of the request. see if another chunk has + # the rest. + return self._data_in_prefetch_requests( + buf_offset + buf_size, offset + size - buf_offset - buf_size + ) + + def _data_in_prefetch_buffers(self, offset): + """ + if a block of data is present in the prefetch buffers, at the given + offset, return the offset of the relevant prefetch buffer. otherwise, + return None. this guarantees nothing about the number of bytes + collected in the prefetch buffer so far. + """ + k = [i for i in self._prefetch_data.keys() if i <= offset] + if len(k) == 0: + return None + index = max(k) + buf_offset = offset - index + if buf_offset >= len(self._prefetch_data[index]): + # it's not here + return None + return index + + def _read_prefetch(self, size): + """ + read data out of the prefetch buffer, if possible. if the data isn't + in the buffer, return None. otherwise, behaves like a normal read. + """ + # while not closed, and haven't fetched past the current position, + # and haven't reached EOF... + while True: + offset = self._data_in_prefetch_buffers(self._realpos) + if offset is not None: + break + if self._prefetch_done or self._closed: + break + self.sftp._read_response() + self._check_exception() + if offset is None: + self._prefetching = False + return None + prefetch = self._prefetch_data[offset] + del self._prefetch_data[offset] + + buf_offset = self._realpos - offset + if buf_offset > 0: + self._prefetch_data[offset] = prefetch[:buf_offset] + prefetch = prefetch[buf_offset:] + if size < len(prefetch): + self._prefetch_data[self._realpos + size] = prefetch[size:] + prefetch = prefetch[:size] + return prefetch + + def _read(self, size): + size = min(size, self.MAX_REQUEST_SIZE) + if self._prefetching: + data = self._read_prefetch(size) + if data is not None: + return data + t, msg = self.sftp._request( + CMD_READ, self.handle, int64(self._realpos), int(size) + ) + if t != CMD_DATA: + raise SFTPError("Expected data") + return msg.get_string() + + def _write(self, data): + # may write less than requested if it would exceed max packet size + chunk = min(len(data), self.MAX_REQUEST_SIZE) + sftp_async_request = self.sftp._async_request( + type(None), + CMD_WRITE, + self.handle, + int64(self._realpos), + data[:chunk], + ) + self._reqs.append(sftp_async_request) + if not self.pipelined or ( + len(self._reqs) > 100 and self.sftp.sock.recv_ready() + ): + while len(self._reqs): + req = self._reqs.popleft() + t, msg = self.sftp._read_response(req) + if t != CMD_STATUS: + raise SFTPError("Expected status") + # convert_status already called + return chunk + + def settimeout(self, timeout): + """ + Set a timeout on read/write operations on the underlying socket or + ssh `.Channel`. + + :param float timeout: + seconds to wait for a pending read/write operation before raising + ``socket.timeout``, or ``None`` for no timeout + + .. seealso:: `.Channel.settimeout` + """ + self.sftp.sock.settimeout(timeout) + + def gettimeout(self): + """ + Returns the timeout in seconds (as a `float`) associated with the + socket or ssh `.Channel` used for this file. + + .. seealso:: `.Channel.gettimeout` + """ + return self.sftp.sock.gettimeout() + + def setblocking(self, blocking): + """ + Set blocking or non-blocking mode on the underiying socket or ssh + `.Channel`. + + :param int blocking: + 0 to set non-blocking mode; non-0 to set blocking mode. + + .. seealso:: `.Channel.setblocking` + """ + self.sftp.sock.setblocking(blocking) + + def seekable(self): + """ + Check if the file supports random access. + + :return: + `True` if the file supports random access. If `False`, + :meth:`seek` will raise an exception + """ + return True + + def seek(self, offset, whence=0): + """ + Set the file's current position. + + See `file.seek` for details. + """ + self.flush() + if whence == self.SEEK_SET: + self._realpos = self._pos = offset + elif whence == self.SEEK_CUR: + self._pos += offset + self._realpos = self._pos + else: + self._realpos = self._pos = self._get_size() + offset + self._rbuffer = bytes() + + def stat(self): + """ + Retrieve information about this file from the remote system. This is + exactly like `.SFTPClient.stat`, except that it operates on an + already-open file. + + :returns: + an `.SFTPAttributes` object containing attributes about this file. + """ + t, msg = self.sftp._request(CMD_FSTAT, self.handle) + if t != CMD_ATTRS: + raise SFTPError("Expected attributes") + return SFTPAttributes._from_msg(msg) + + def chmod(self, mode): + """ + Change the mode (permissions) of this file. The permissions are + unix-style and identical to those used by Python's `os.chmod` + function. + + :param int mode: new permissions + """ + self.sftp._log( + DEBUG, "chmod({}, {!r})".format(hexlify(self.handle), mode) + ) + attr = SFTPAttributes() + attr.st_mode = mode + self.sftp._request(CMD_FSETSTAT, self.handle, attr) + + def chown(self, uid, gid): + """ + Change the owner (``uid``) and group (``gid``) of this file. As with + Python's `os.chown` function, you must pass both arguments, so if you + only want to change one, use `stat` first to retrieve the current + owner and group. + + :param int uid: new owner's uid + :param int gid: new group id + """ + self.sftp._log( + DEBUG, + "chown({}, {!r}, {!r})".format(hexlify(self.handle), uid, gid), + ) + attr = SFTPAttributes() + attr.st_uid, attr.st_gid = uid, gid + self.sftp._request(CMD_FSETSTAT, self.handle, attr) + + def utime(self, times): + """ + Set the access and modified times of this file. If + ``times`` is ``None``, then the file's access and modified times are + set to the current time. Otherwise, ``times`` must be a 2-tuple of + numbers, of the form ``(atime, mtime)``, which is used to set the + access and modified times, respectively. This bizarre API is mimicked + from Python for the sake of consistency -- I apologize. + + :param tuple times: + ``None`` or a tuple of (access time, modified time) in standard + internet epoch time (seconds since 01 January 1970 GMT) + """ + if times is None: + times = (time.time(), time.time()) + self.sftp._log( + DEBUG, "utime({}, {!r})".format(hexlify(self.handle), times) + ) + attr = SFTPAttributes() + attr.st_atime, attr.st_mtime = times + self.sftp._request(CMD_FSETSTAT, self.handle, attr) + + def truncate(self, size): + """ + Change the size of this file. This usually extends + or shrinks the size of the file, just like the ``truncate()`` method on + Python file objects. + + :param size: the new size of the file + """ + self.sftp._log( + DEBUG, "truncate({}, {!r})".format(hexlify(self.handle), size) + ) + attr = SFTPAttributes() + attr.st_size = size + self.sftp._request(CMD_FSETSTAT, self.handle, attr) + + def check(self, hash_algorithm, offset=0, length=0, block_size=0): + """ + Ask the server for a hash of a section of this file. This can be used + to verify a successful upload or download, or for various rsync-like + operations. + + The file is hashed from ``offset``, for ``length`` bytes. + If ``length`` is 0, the remainder of the file is hashed. Thus, if both + ``offset`` and ``length`` are zero, the entire file is hashed. + + Normally, ``block_size`` will be 0 (the default), and this method will + return a byte string representing the requested hash (for example, a + string of length 16 for MD5, or 20 for SHA-1). If a non-zero + ``block_size`` is given, each chunk of the file (from ``offset`` to + ``offset + length``) of ``block_size`` bytes is computed as a separate + hash. The hash results are all concatenated and returned as a single + string. + + For example, ``check('sha1', 0, 1024, 512)`` will return a string of + length 40. The first 20 bytes will be the SHA-1 of the first 512 bytes + of the file, and the last 20 bytes will be the SHA-1 of the next 512 + bytes. + + :param str hash_algorithm: + the name of the hash algorithm to use (normally ``"sha1"`` or + ``"md5"``) + :param offset: + offset into the file to begin hashing (0 means to start from the + beginning) + :param length: + number of bytes to hash (0 means continue to the end of the file) + :param int block_size: + number of bytes to hash per result (must not be less than 256; 0 + means to compute only one hash of the entire segment) + :return: + `str` of bytes representing the hash of each block, concatenated + together + + :raises: + ``IOError`` -- if the server doesn't support the "check-file" + extension, or possibly doesn't support the hash algorithm requested + + .. note:: Many (most?) servers don't support this extension yet. + + .. versionadded:: 1.4 + """ + t, msg = self.sftp._request( + CMD_EXTENDED, + "check-file", + self.handle, + hash_algorithm, + int64(offset), + int64(length), + block_size, + ) + msg.get_text() # ext + msg.get_text() # alg + data = msg.get_remainder() + return data + + def set_pipelined(self, pipelined=True): + """ + Turn on/off the pipelining of write operations to this file. When + pipelining is on, paramiko won't wait for the server response after + each write operation. Instead, they're collected as they come in. At + the first non-write operation (including `.close`), all remaining + server responses are collected. This means that if there was an error + with one of your later writes, an exception might be thrown from within + `.close` instead of `.write`. + + By default, files are not pipelined. + + :param bool pipelined: + ``True`` if pipelining should be turned on for this file; ``False`` + otherwise + + .. versionadded:: 1.5 + """ + self.pipelined = pipelined + + def prefetch(self, file_size=None): + """ + Pre-fetch the remaining contents of this file in anticipation of future + `.read` calls. If reading the entire file, pre-fetching can + dramatically improve the download speed by avoiding roundtrip latency. + The file's contents are incrementally buffered in a background thread. + + The prefetched data is stored in a buffer until read via the `.read` + method. Once data has been read, it's removed from the buffer. The + data may be read in a random order (using `.seek`); chunks of the + buffer that haven't been read will continue to be buffered. + + :param int file_size: + When this is ``None`` (the default), this method calls `stat` to + determine the remote file size. In some situations, doing so can + cause exceptions or hangs (see `#562 + <https://github.com/paramiko/paramiko/pull/562>`_); as a + workaround, one may call `stat` explicitly and pass its value in + via this parameter. + + .. versionadded:: 1.5.1 + .. versionchanged:: 1.16.0 + The ``file_size`` parameter was added (with no default value). + .. versionchanged:: 1.16.1 + The ``file_size`` parameter was made optional for backwards + compatibility. + """ + if file_size is None: + file_size = self.stat().st_size + + # queue up async reads for the rest of the file + chunks = [] + n = self._realpos + while n < file_size: + chunk = min(self.MAX_REQUEST_SIZE, file_size - n) + chunks.append((n, chunk)) + n += chunk + if len(chunks) > 0: + self._start_prefetch(chunks) + + def readv(self, chunks): + """ + Read a set of blocks from the file by (offset, length). This is more + efficient than doing a series of `.seek` and `.read` calls, since the + prefetch machinery is used to retrieve all the requested blocks at + once. + + :param chunks: + a list of ``(offset, length)`` tuples indicating which sections of + the file to read + :return: a list of blocks read, in the same order as in ``chunks`` + + .. versionadded:: 1.5.4 + """ + self.sftp._log( + DEBUG, "readv({}, {!r})".format(hexlify(self.handle), chunks) + ) + + read_chunks = [] + for offset, size in chunks: + # don't fetch data that's already in the prefetch buffer + if self._data_in_prefetch_buffers( + offset + ) or self._data_in_prefetch_requests(offset, size): + continue + + # break up anything larger than the max read size + while size > 0: + chunk_size = min(size, self.MAX_REQUEST_SIZE) + read_chunks.append((offset, chunk_size)) + offset += chunk_size + size -= chunk_size + + self._start_prefetch(read_chunks) + # now we can just devolve to a bunch of read()s :) + for x in chunks: + self.seek(x[0]) + yield self.read(x[1]) + + # ...internals... + + def _get_size(self): + try: + return self.stat().st_size + except: + return 0 + + def _start_prefetch(self, chunks): + self._prefetching = True + self._prefetch_done = False + + t = threading.Thread(target=self._prefetch_thread, args=(chunks,)) + t.daemon = True + t.start() + + def _prefetch_thread(self, chunks): + # do these read requests in a temporary thread because there may be + # a lot of them, so it may block. + for offset, length in chunks: + num = self.sftp._async_request( + self, CMD_READ, self.handle, int64(offset), int(length) + ) + with self._prefetch_lock: + self._prefetch_extents[num] = (offset, length) + + def _async_response(self, t, msg, num): + if t == CMD_STATUS: + # save exception and re-raise it on next file operation + try: + self.sftp._convert_status(msg) + except Exception as e: + self._saved_exception = e + return + if t != CMD_DATA: + raise SFTPError("Expected data") + data = msg.get_string() + while True: + with self._prefetch_lock: + # spin if in race with _prefetch_thread + if num in self._prefetch_extents: + offset, length = self._prefetch_extents[num] + self._prefetch_data[offset] = data + del self._prefetch_extents[num] + if len(self._prefetch_extents) == 0: + self._prefetch_done = True + break + + def _check_exception(self): + """if there's a saved exception, raise & clear it""" + if self._saved_exception is not None: + x = self._saved_exception + self._saved_exception = None + raise x diff --git a/paramiko/sftp_handle.py b/paramiko/sftp_handle.py new file mode 100644 index 0000000..b204652 --- /dev/null +++ b/paramiko/sftp_handle.py @@ -0,0 +1,196 @@ +# Copyright (C) 2003-2007 Robey Pointer <robeypointer@gmail.com> +# +# This file is part of paramiko. +# +# Paramiko is free software; you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free +# Software Foundation; either version 2.1 of the License, or (at your option) +# any later version. +# +# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Paramiko; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +""" +Abstraction of an SFTP file handle (for server mode). +""" + +import os +from paramiko.sftp import SFTP_OP_UNSUPPORTED, SFTP_OK +from paramiko.util import ClosingContextManager + + +class SFTPHandle(ClosingContextManager): + """ + Abstract object representing a handle to an open file (or folder) in an + SFTP server implementation. Each handle has a string representation used + by the client to refer to the underlying file. + + Server implementations can (and should) subclass SFTPHandle to implement + features of a file handle, like `stat` or `chattr`. + + Instances of this class may be used as context managers. + """ + + def __init__(self, flags=0): + """ + Create a new file handle representing a local file being served over + SFTP. If ``flags`` is passed in, it's used to determine if the file + is open in append mode. + + :param int flags: optional flags as passed to + `.SFTPServerInterface.open` + """ + self.__flags = flags + self.__name = None + # only for handles to folders: + self.__files = {} + self.__tell = None + + def close(self): + """ + When a client closes a file, this method is called on the handle. + Normally you would use this method to close the underlying OS level + file object(s). + + The default implementation checks for attributes on ``self`` named + ``readfile`` and/or ``writefile``, and if either or both are present, + their ``close()`` methods are called. This means that if you are + using the default implementations of `read` and `write`, this + method's default implementation should be fine also. + """ + readfile = getattr(self, "readfile", None) + if readfile is not None: + readfile.close() + writefile = getattr(self, "writefile", None) + if writefile is not None: + writefile.close() + + def read(self, offset, length): + """ + Read up to ``length`` bytes from this file, starting at position + ``offset``. The offset may be a Python long, since SFTP allows it + to be 64 bits. + + If the end of the file has been reached, this method may return an + empty string to signify EOF, or it may also return ``SFTP_EOF``. + + The default implementation checks for an attribute on ``self`` named + ``readfile``, and if present, performs the read operation on the Python + file-like object found there. (This is meant as a time saver for the + common case where you are wrapping a Python file object.) + + :param offset: position in the file to start reading from. + :param int length: number of bytes to attempt to read. + :return: the `bytes` read, or an error code `int`. + """ + readfile = getattr(self, "readfile", None) + if readfile is None: + return SFTP_OP_UNSUPPORTED + try: + if self.__tell is None: + self.__tell = readfile.tell() + if offset != self.__tell: + readfile.seek(offset) + self.__tell = offset + data = readfile.read(length) + except IOError as e: + self.__tell = None + return SFTPServer.convert_errno(e.errno) + self.__tell += len(data) + return data + + def write(self, offset, data): + """ + Write ``data`` into this file at position ``offset``. Extending the + file past its original end is expected. Unlike Python's normal + ``write()`` methods, this method cannot do a partial write: it must + write all of ``data`` or else return an error. + + The default implementation checks for an attribute on ``self`` named + ``writefile``, and if present, performs the write operation on the + Python file-like object found there. The attribute is named + differently from ``readfile`` to make it easy to implement read-only + (or write-only) files, but if both attributes are present, they should + refer to the same file. + + :param offset: position in the file to start reading from. + :param bytes data: data to write into the file. + :return: an SFTP error code like ``SFTP_OK``. + """ + writefile = getattr(self, "writefile", None) + if writefile is None: + return SFTP_OP_UNSUPPORTED + try: + # in append mode, don't care about seeking + if (self.__flags & os.O_APPEND) == 0: + if self.__tell is None: + self.__tell = writefile.tell() + if offset != self.__tell: + writefile.seek(offset) + self.__tell = offset + writefile.write(data) + writefile.flush() + except IOError as e: + self.__tell = None + return SFTPServer.convert_errno(e.errno) + if self.__tell is not None: + self.__tell += len(data) + return SFTP_OK + + def stat(self): + """ + Return an `.SFTPAttributes` object referring to this open file, or an + error code. This is equivalent to `.SFTPServerInterface.stat`, except + it's called on an open file instead of a path. + + :return: + an attributes object for the given file, or an SFTP error code + (like ``SFTP_PERMISSION_DENIED``). + :rtype: `.SFTPAttributes` or error code + """ + return SFTP_OP_UNSUPPORTED + + def chattr(self, attr): + """ + Change the attributes of this file. The ``attr`` object will contain + only those fields provided by the client in its request, so you should + check for the presence of fields before using them. + + :param .SFTPAttributes attr: the attributes to change on this file. + :return: an `int` error code like ``SFTP_OK``. + """ + return SFTP_OP_UNSUPPORTED + + # ...internals... + + def _set_files(self, files): + """ + Used by the SFTP server code to cache a directory listing. (In + the SFTP protocol, listing a directory is a multi-stage process + requiring a temporary handle.) + """ + self.__files = files + + def _get_next_files(self): + """ + Used by the SFTP server code to retrieve a cached directory + listing. + """ + fnlist = self.__files[:16] + self.__files = self.__files[16:] + return fnlist + + def _get_name(self): + return self.__name + + def _set_name(self, name): + self.__name = name + + +from paramiko.sftp_server import SFTPServer diff --git a/paramiko/sftp_server.py b/paramiko/sftp_server.py new file mode 100644 index 0000000..cd3910d --- /dev/null +++ b/paramiko/sftp_server.py @@ -0,0 +1,537 @@ +# Copyright (C) 2003-2007 Robey Pointer <robeypointer@gmail.com> +# +# This file is part of paramiko. +# +# Paramiko is free software; you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free +# Software Foundation; either version 2.1 of the License, or (at your option) +# any later version. +# +# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Paramiko; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +""" +Server-mode SFTP support. +""" + +import os +import errno +import sys +from hashlib import md5, sha1 + +from paramiko import util +from paramiko.sftp import ( + BaseSFTP, + Message, + SFTP_FAILURE, + SFTP_PERMISSION_DENIED, + SFTP_NO_SUCH_FILE, + int64, +) +from paramiko.sftp_si import SFTPServerInterface +from paramiko.sftp_attr import SFTPAttributes +from paramiko.common import DEBUG +from paramiko.server import SubsystemHandler +from paramiko.util import b + + +# known hash algorithms for the "check-file" extension +from paramiko.sftp import ( + CMD_HANDLE, + SFTP_DESC, + CMD_STATUS, + SFTP_EOF, + CMD_NAME, + SFTP_BAD_MESSAGE, + CMD_EXTENDED_REPLY, + SFTP_FLAG_READ, + SFTP_FLAG_WRITE, + SFTP_FLAG_APPEND, + SFTP_FLAG_CREATE, + SFTP_FLAG_TRUNC, + SFTP_FLAG_EXCL, + CMD_NAMES, + CMD_OPEN, + CMD_CLOSE, + SFTP_OK, + CMD_READ, + CMD_DATA, + CMD_WRITE, + CMD_REMOVE, + CMD_RENAME, + CMD_MKDIR, + CMD_RMDIR, + CMD_OPENDIR, + CMD_READDIR, + CMD_STAT, + CMD_ATTRS, + CMD_LSTAT, + CMD_FSTAT, + CMD_SETSTAT, + CMD_FSETSTAT, + CMD_READLINK, + CMD_SYMLINK, + CMD_REALPATH, + CMD_EXTENDED, + SFTP_OP_UNSUPPORTED, +) + +_hash_class = {"sha1": sha1, "md5": md5} + + +class SFTPServer(BaseSFTP, SubsystemHandler): + """ + Server-side SFTP subsystem support. Since this is a `.SubsystemHandler`, + it can be (and is meant to be) set as the handler for ``"sftp"`` requests. + Use `.Transport.set_subsystem_handler` to activate this class. + """ + + def __init__( + self, + channel, + name, + server, + sftp_si=SFTPServerInterface, + *args, + **kwargs + ): + """ + The constructor for SFTPServer is meant to be called from within the + `.Transport` as a subsystem handler. ``server`` and any additional + parameters or keyword parameters are passed from the original call to + `.Transport.set_subsystem_handler`. + + :param .Channel channel: channel passed from the `.Transport`. + :param str name: name of the requested subsystem. + :param .ServerInterface server: + the server object associated with this channel and subsystem + :param sftp_si: + a subclass of `.SFTPServerInterface` to use for handling individual + requests. + """ + BaseSFTP.__init__(self) + SubsystemHandler.__init__(self, channel, name, server) + transport = channel.get_transport() + self.logger = util.get_logger(transport.get_log_channel() + ".sftp") + self.ultra_debug = transport.get_hexdump() + self.next_handle = 1 + # map of handle-string to SFTPHandle for files & folders: + self.file_table = {} + self.folder_table = {} + self.server = sftp_si(server, *args, **kwargs) + + def _log(self, level, msg): + if issubclass(type(msg), list): + for m in msg: + super()._log(level, "[chan " + self.sock.get_name() + "] " + m) + else: + super()._log(level, "[chan " + self.sock.get_name() + "] " + msg) + + def start_subsystem(self, name, transport, channel): + self.sock = channel + self._log(DEBUG, "Started sftp server on channel {!r}".format(channel)) + self._send_server_version() + self.server.session_started() + while True: + try: + t, data = self._read_packet() + except EOFError: + self._log(DEBUG, "EOF -- end of session") + return + except Exception as e: + self._log(DEBUG, "Exception on channel: " + str(e)) + self._log(DEBUG, util.tb_strings()) + return + msg = Message(data) + request_number = msg.get_int() + try: + self._process(t, request_number, msg) + except Exception as e: + self._log(DEBUG, "Exception in server processing: " + str(e)) + self._log(DEBUG, util.tb_strings()) + # send some kind of failure message, at least + try: + self._send_status(request_number, SFTP_FAILURE) + except: + pass + + def finish_subsystem(self): + self.server.session_ended() + super().finish_subsystem() + # close any file handles that were left open + # (so we can return them to the OS quickly) + for f in self.file_table.values(): + f.close() + for f in self.folder_table.values(): + f.close() + self.file_table = {} + self.folder_table = {} + + @staticmethod + def convert_errno(e): + """ + Convert an errno value (as from an ``OSError`` or ``IOError``) into a + standard SFTP result code. This is a convenience function for trapping + exceptions in server code and returning an appropriate result. + + :param int e: an errno code, as from ``OSError.errno``. + :return: an `int` SFTP error code like ``SFTP_NO_SUCH_FILE``. + """ + if e == errno.EACCES: + # permission denied + return SFTP_PERMISSION_DENIED + elif (e == errno.ENOENT) or (e == errno.ENOTDIR): + # no such file + return SFTP_NO_SUCH_FILE + else: + return SFTP_FAILURE + + @staticmethod + def set_file_attr(filename, attr): + """ + Change a file's attributes on the local filesystem. The contents of + ``attr`` are used to change the permissions, owner, group ownership, + and/or modification & access time of the file, depending on which + attributes are present in ``attr``. + + This is meant to be a handy helper function for translating SFTP file + requests into local file operations. + + :param str filename: + name of the file to alter (should usually be an absolute path). + :param .SFTPAttributes attr: attributes to change. + """ + if sys.platform != "win32": + # mode operations are meaningless on win32 + if attr._flags & attr.FLAG_PERMISSIONS: + os.chmod(filename, attr.st_mode) + if attr._flags & attr.FLAG_UIDGID: + os.chown(filename, attr.st_uid, attr.st_gid) + if attr._flags & attr.FLAG_AMTIME: + os.utime(filename, (attr.st_atime, attr.st_mtime)) + if attr._flags & attr.FLAG_SIZE: + with open(filename, "w+") as f: + f.truncate(attr.st_size) + + # ...internals... + + def _response(self, request_number, t, *args): + msg = Message() + msg.add_int(request_number) + for item in args: + # NOTE: this is a very silly tiny class used for SFTPFile mostly + if isinstance(item, int64): + msg.add_int64(item) + elif isinstance(item, int): + msg.add_int(item) + elif isinstance(item, (str, bytes)): + msg.add_string(item) + elif type(item) is SFTPAttributes: + item._pack(msg) + else: + raise Exception( + "unknown type for {!r} type {!r}".format(item, type(item)) + ) + self._send_packet(t, msg) + + def _send_handle_response(self, request_number, handle, folder=False): + if not issubclass(type(handle), SFTPHandle): + # must be error code + self._send_status(request_number, handle) + return + handle._set_name(b("hx{:d}".format(self.next_handle))) + self.next_handle += 1 + if folder: + self.folder_table[handle._get_name()] = handle + else: + self.file_table[handle._get_name()] = handle + self._response(request_number, CMD_HANDLE, handle._get_name()) + + def _send_status(self, request_number, code, desc=None): + if desc is None: + try: + desc = SFTP_DESC[code] + except IndexError: + desc = "Unknown" + # some clients expect a "language" tag at the end + # (but don't mind it being blank) + self._response(request_number, CMD_STATUS, code, desc, "") + + def _open_folder(self, request_number, path): + resp = self.server.list_folder(path) + if issubclass(type(resp), list): + # got an actual list of filenames in the folder + folder = SFTPHandle() + folder._set_files(resp) + self._send_handle_response(request_number, folder, True) + return + # must be an error code + self._send_status(request_number, resp) + + def _read_folder(self, request_number, folder): + flist = folder._get_next_files() + if len(flist) == 0: + self._send_status(request_number, SFTP_EOF) + return + msg = Message() + msg.add_int(request_number) + msg.add_int(len(flist)) + for attr in flist: + msg.add_string(attr.filename) + msg.add_string(attr) + attr._pack(msg) + self._send_packet(CMD_NAME, msg) + + def _check_file(self, request_number, msg): + # this extension actually comes from v6 protocol, but since it's an + # extension, i feel like we can reasonably support it backported. + # it's very useful for verifying uploaded files or checking for + # rsync-like differences between local and remote files. + handle = msg.get_binary() + alg_list = msg.get_list() + start = msg.get_int64() + length = msg.get_int64() + block_size = msg.get_int() + if handle not in self.file_table: + self._send_status( + request_number, SFTP_BAD_MESSAGE, "Invalid handle" + ) + return + f = self.file_table[handle] + for x in alg_list: + if x in _hash_class: + algname = x + alg = _hash_class[x] + break + else: + self._send_status( + request_number, SFTP_FAILURE, "No supported hash types found" + ) + return + if length == 0: + st = f.stat() + if not issubclass(type(st), SFTPAttributes): + self._send_status(request_number, st, "Unable to stat file") + return + length = st.st_size - start + if block_size == 0: + block_size = length + if block_size < 256: + self._send_status( + request_number, SFTP_FAILURE, "Block size too small" + ) + return + + sum_out = bytes() + offset = start + while offset < start + length: + blocklen = min(block_size, start + length - offset) + # don't try to read more than about 64KB at a time + chunklen = min(blocklen, 65536) + count = 0 + hash_obj = alg() + while count < blocklen: + data = f.read(offset, chunklen) + if not isinstance(data, bytes): + self._send_status( + request_number, data, "Unable to hash file" + ) + return + hash_obj.update(data) + count += len(data) + offset += count + sum_out += hash_obj.digest() + + msg = Message() + msg.add_int(request_number) + msg.add_string("check-file") + msg.add_string(algname) + msg.add_bytes(sum_out) + self._send_packet(CMD_EXTENDED_REPLY, msg) + + def _convert_pflags(self, pflags): + """convert SFTP-style open() flags to Python's os.open() flags""" + if (pflags & SFTP_FLAG_READ) and (pflags & SFTP_FLAG_WRITE): + flags = os.O_RDWR + elif pflags & SFTP_FLAG_WRITE: + flags = os.O_WRONLY + else: + flags = os.O_RDONLY + if pflags & SFTP_FLAG_APPEND: + flags |= os.O_APPEND + if pflags & SFTP_FLAG_CREATE: + flags |= os.O_CREAT + if pflags & SFTP_FLAG_TRUNC: + flags |= os.O_TRUNC + if pflags & SFTP_FLAG_EXCL: + flags |= os.O_EXCL + return flags + + def _process(self, t, request_number, msg): + self._log(DEBUG, "Request: {}".format(CMD_NAMES[t])) + if t == CMD_OPEN: + path = msg.get_text() + flags = self._convert_pflags(msg.get_int()) + attr = SFTPAttributes._from_msg(msg) + self._send_handle_response( + request_number, self.server.open(path, flags, attr) + ) + elif t == CMD_CLOSE: + handle = msg.get_binary() + if handle in self.folder_table: + del self.folder_table[handle] + self._send_status(request_number, SFTP_OK) + return + if handle in self.file_table: + self.file_table[handle].close() + del self.file_table[handle] + self._send_status(request_number, SFTP_OK) + return + self._send_status( + request_number, SFTP_BAD_MESSAGE, "Invalid handle" + ) + elif t == CMD_READ: + handle = msg.get_binary() + offset = msg.get_int64() + length = msg.get_int() + if handle not in self.file_table: + self._send_status( + request_number, SFTP_BAD_MESSAGE, "Invalid handle" + ) + return + data = self.file_table[handle].read(offset, length) + if isinstance(data, (bytes, str)): + if len(data) == 0: + self._send_status(request_number, SFTP_EOF) + else: + self._response(request_number, CMD_DATA, data) + else: + self._send_status(request_number, data) + elif t == CMD_WRITE: + handle = msg.get_binary() + offset = msg.get_int64() + data = msg.get_binary() + if handle not in self.file_table: + self._send_status( + request_number, SFTP_BAD_MESSAGE, "Invalid handle" + ) + return + self._send_status( + request_number, self.file_table[handle].write(offset, data) + ) + elif t == CMD_REMOVE: + path = msg.get_text() + self._send_status(request_number, self.server.remove(path)) + elif t == CMD_RENAME: + oldpath = msg.get_text() + newpath = msg.get_text() + self._send_status( + request_number, self.server.rename(oldpath, newpath) + ) + elif t == CMD_MKDIR: + path = msg.get_text() + attr = SFTPAttributes._from_msg(msg) + self._send_status(request_number, self.server.mkdir(path, attr)) + elif t == CMD_RMDIR: + path = msg.get_text() + self._send_status(request_number, self.server.rmdir(path)) + elif t == CMD_OPENDIR: + path = msg.get_text() + self._open_folder(request_number, path) + return + elif t == CMD_READDIR: + handle = msg.get_binary() + if handle not in self.folder_table: + self._send_status( + request_number, SFTP_BAD_MESSAGE, "Invalid handle" + ) + return + folder = self.folder_table[handle] + self._read_folder(request_number, folder) + elif t == CMD_STAT: + path = msg.get_text() + resp = self.server.stat(path) + if issubclass(type(resp), SFTPAttributes): + self._response(request_number, CMD_ATTRS, resp) + else: + self._send_status(request_number, resp) + elif t == CMD_LSTAT: + path = msg.get_text() + resp = self.server.lstat(path) + if issubclass(type(resp), SFTPAttributes): + self._response(request_number, CMD_ATTRS, resp) + else: + self._send_status(request_number, resp) + elif t == CMD_FSTAT: + handle = msg.get_binary() + if handle not in self.file_table: + self._send_status( + request_number, SFTP_BAD_MESSAGE, "Invalid handle" + ) + return + resp = self.file_table[handle].stat() + if issubclass(type(resp), SFTPAttributes): + self._response(request_number, CMD_ATTRS, resp) + else: + self._send_status(request_number, resp) + elif t == CMD_SETSTAT: + path = msg.get_text() + attr = SFTPAttributes._from_msg(msg) + self._send_status(request_number, self.server.chattr(path, attr)) + elif t == CMD_FSETSTAT: + handle = msg.get_binary() + attr = SFTPAttributes._from_msg(msg) + if handle not in self.file_table: + self._response( + request_number, SFTP_BAD_MESSAGE, "Invalid handle" + ) + return + self._send_status( + request_number, self.file_table[handle].chattr(attr) + ) + elif t == CMD_READLINK: + path = msg.get_text() + resp = self.server.readlink(path) + if isinstance(resp, (bytes, str)): + self._response( + request_number, CMD_NAME, 1, resp, "", SFTPAttributes() + ) + else: + self._send_status(request_number, resp) + elif t == CMD_SYMLINK: + # the sftp 2 draft is incorrect here! + # path always follows target_path + target_path = msg.get_text() + path = msg.get_text() + self._send_status( + request_number, self.server.symlink(target_path, path) + ) + elif t == CMD_REALPATH: + path = msg.get_text() + rpath = self.server.canonicalize(path) + self._response( + request_number, CMD_NAME, 1, rpath, "", SFTPAttributes() + ) + elif t == CMD_EXTENDED: + tag = msg.get_text() + if tag == "check-file": + self._check_file(request_number, msg) + elif tag == "posix-rename@openssh.com": + oldpath = msg.get_text() + newpath = msg.get_text() + self._send_status( + request_number, self.server.posix_rename(oldpath, newpath) + ) + else: + self._send_status(request_number, SFTP_OP_UNSUPPORTED) + else: + self._send_status(request_number, SFTP_OP_UNSUPPORTED) + + +from paramiko.sftp_handle import SFTPHandle diff --git a/paramiko/sftp_si.py b/paramiko/sftp_si.py new file mode 100644 index 0000000..72b5db9 --- /dev/null +++ b/paramiko/sftp_si.py @@ -0,0 +1,316 @@ +# Copyright (C) 2003-2007 Robey Pointer <robeypointer@gmail.com> +# +# This file is part of paramiko. +# +# Paramiko is free software; you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free +# Software Foundation; either version 2.1 of the License, or (at your option) +# any later version. +# +# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Paramiko; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +""" +An interface to override for SFTP server support. +""" + +import os +import sys +from paramiko.sftp import SFTP_OP_UNSUPPORTED + + +class SFTPServerInterface: + """ + This class defines an interface for controlling the behavior of paramiko + when using the `.SFTPServer` subsystem to provide an SFTP server. + + Methods on this class are called from the SFTP session's thread, so you can + block as long as necessary without affecting other sessions (even other + SFTP sessions). However, raising an exception will usually cause the SFTP + session to abruptly end, so you will usually want to catch exceptions and + return an appropriate error code. + + All paths are in string form instead of unicode because not all SFTP + clients & servers obey the requirement that paths be encoded in UTF-8. + """ + + def __init__(self, server, *args, **kwargs): + """ + Create a new SFTPServerInterface object. This method does nothing by + default and is meant to be overridden by subclasses. + + :param .ServerInterface server: + the server object associated with this channel and SFTP subsystem + """ + super().__init__(*args, **kwargs) + + def session_started(self): + """ + The SFTP server session has just started. This method is meant to be + overridden to perform any necessary setup before handling callbacks + from SFTP operations. + """ + pass + + def session_ended(self): + """ + The SFTP server session has just ended, either cleanly or via an + exception. This method is meant to be overridden to perform any + necessary cleanup before this `.SFTPServerInterface` object is + destroyed. + """ + pass + + def open(self, path, flags, attr): + """ + Open a file on the server and create a handle for future operations + on that file. On success, a new object subclassed from `.SFTPHandle` + should be returned. This handle will be used for future operations + on the file (read, write, etc). On failure, an error code such as + ``SFTP_PERMISSION_DENIED`` should be returned. + + ``flags`` contains the requested mode for opening (read-only, + write-append, etc) as a bitset of flags from the ``os`` module: + + - ``os.O_RDONLY`` + - ``os.O_WRONLY`` + - ``os.O_RDWR`` + - ``os.O_APPEND`` + - ``os.O_CREAT`` + - ``os.O_TRUNC`` + - ``os.O_EXCL`` + + (One of ``os.O_RDONLY``, ``os.O_WRONLY``, or ``os.O_RDWR`` will always + be set.) + + The ``attr`` object contains requested attributes of the file if it + has to be created. Some or all attribute fields may be missing if + the client didn't specify them. + + .. note:: The SFTP protocol defines all files to be in "binary" mode. + There is no equivalent to Python's "text" mode. + + :param str path: + the requested path (relative or absolute) of the file to be opened. + :param int flags: + flags or'd together from the ``os`` module indicating the requested + mode for opening the file. + :param .SFTPAttributes attr: + requested attributes of the file if it is newly created. + :return: a new `.SFTPHandle` or error code. + """ + return SFTP_OP_UNSUPPORTED + + def list_folder(self, path): + """ + Return a list of files within a given folder. The ``path`` will use + posix notation (``"/"`` separates folder names) and may be an absolute + or relative path. + + The list of files is expected to be a list of `.SFTPAttributes` + objects, which are similar in structure to the objects returned by + ``os.stat``. In addition, each object should have its ``filename`` + field filled in, since this is important to a directory listing and + not normally present in ``os.stat`` results. The method + `.SFTPAttributes.from_stat` will usually do what you want. + + In case of an error, you should return one of the ``SFTP_*`` error + codes, such as ``SFTP_PERMISSION_DENIED``. + + :param str path: the requested path (relative or absolute) to be + listed. + :return: + a list of the files in the given folder, using `.SFTPAttributes` + objects. + + .. note:: + You should normalize the given ``path`` first (see the `os.path` + module) and check appropriate permissions before returning the list + of files. Be careful of malicious clients attempting to use + relative paths to escape restricted folders, if you're doing a + direct translation from the SFTP server path to your local + filesystem. + """ + return SFTP_OP_UNSUPPORTED + + def stat(self, path): + """ + Return an `.SFTPAttributes` object for a path on the server, or an + error code. If your server supports symbolic links (also known as + "aliases"), you should follow them. (`lstat` is the corresponding + call that doesn't follow symlinks/aliases.) + + :param str path: + the requested path (relative or absolute) to fetch file statistics + for. + :return: + an `.SFTPAttributes` object for the given file, or an SFTP error + code (like ``SFTP_PERMISSION_DENIED``). + """ + return SFTP_OP_UNSUPPORTED + + def lstat(self, path): + """ + Return an `.SFTPAttributes` object for a path on the server, or an + error code. If your server supports symbolic links (also known as + "aliases"), you should not follow them -- instead, you should + return data on the symlink or alias itself. (`stat` is the + corresponding call that follows symlinks/aliases.) + + :param str path: + the requested path (relative or absolute) to fetch file statistics + for. + :type path: str + :return: + an `.SFTPAttributes` object for the given file, or an SFTP error + code (like ``SFTP_PERMISSION_DENIED``). + """ + return SFTP_OP_UNSUPPORTED + + def remove(self, path): + """ + Delete a file, if possible. + + :param str path: + the requested path (relative or absolute) of the file to delete. + :return: an SFTP error code `int` like ``SFTP_OK``. + """ + return SFTP_OP_UNSUPPORTED + + def rename(self, oldpath, newpath): + """ + Rename (or move) a file. The SFTP specification implies that this + method can be used to move an existing file into a different folder, + and since there's no other (easy) way to move files via SFTP, it's + probably a good idea to implement "move" in this method too, even for + files that cross disk partition boundaries, if at all possible. + + .. note:: You should return an error if a file with the same name as + ``newpath`` already exists. (The rename operation should be + non-desctructive.) + + .. note:: + This method implements 'standard' SFTP ``RENAME`` behavior; those + seeking the OpenSSH "POSIX rename" extension behavior should use + `posix_rename`. + + :param str oldpath: + the requested path (relative or absolute) of the existing file. + :param str newpath: the requested new path of the file. + :return: an SFTP error code `int` like ``SFTP_OK``. + """ + return SFTP_OP_UNSUPPORTED + + def posix_rename(self, oldpath, newpath): + """ + Rename (or move) a file, following posix conventions. If newpath + already exists, it will be overwritten. + + :param str oldpath: + the requested path (relative or absolute) of the existing file. + :param str newpath: the requested new path of the file. + :return: an SFTP error code `int` like ``SFTP_OK``. + + :versionadded: 2.2 + """ + return SFTP_OP_UNSUPPORTED + + def mkdir(self, path, attr): + """ + Create a new directory with the given attributes. The ``attr`` + object may be considered a "hint" and ignored. + + The ``attr`` object will contain only those fields provided by the + client in its request, so you should use ``hasattr`` to check for + the presence of fields before using them. In some cases, the ``attr`` + object may be completely empty. + + :param str path: + requested path (relative or absolute) of the new folder. + :param .SFTPAttributes attr: requested attributes of the new folder. + :return: an SFTP error code `int` like ``SFTP_OK``. + """ + return SFTP_OP_UNSUPPORTED + + def rmdir(self, path): + """ + Remove a directory if it exists. The ``path`` should refer to an + existing, empty folder -- otherwise this method should return an + error. + + :param str path: + requested path (relative or absolute) of the folder to remove. + :return: an SFTP error code `int` like ``SFTP_OK``. + """ + return SFTP_OP_UNSUPPORTED + + def chattr(self, path, attr): + """ + Change the attributes of a file. The ``attr`` object will contain + only those fields provided by the client in its request, so you + should check for the presence of fields before using them. + + :param str path: + requested path (relative or absolute) of the file to change. + :param attr: + requested attributes to change on the file (an `.SFTPAttributes` + object) + :return: an error code `int` like ``SFTP_OK``. + """ + return SFTP_OP_UNSUPPORTED + + def canonicalize(self, path): + """ + Return the canonical form of a path on the server. For example, + if the server's home folder is ``/home/foo``, the path + ``"../betty"`` would be canonicalized to ``"/home/betty"``. Note + the obvious security issues: if you're serving files only from a + specific folder, you probably don't want this method to reveal path + names outside that folder. + + You may find the Python methods in ``os.path`` useful, especially + ``os.path.normpath`` and ``os.path.realpath``. + + The default implementation returns ``os.path.normpath('/' + path)``. + """ + if os.path.isabs(path): + out = os.path.normpath(path) + else: + out = os.path.normpath("/" + path) + if sys.platform == "win32": + # on windows, normalize backslashes to sftp/posix format + out = out.replace("\\", "/") + return out + + def readlink(self, path): + """ + Return the target of a symbolic link (or shortcut) on the server. + If the specified path doesn't refer to a symbolic link, an error + should be returned. + + :param str path: path (relative or absolute) of the symbolic link. + :return: + the target `str` path of the symbolic link, or an error code like + ``SFTP_NO_SUCH_FILE``. + """ + return SFTP_OP_UNSUPPORTED + + def symlink(self, target_path, path): + """ + Create a symbolic link on the server, as new pathname ``path``, + with ``target_path`` as the target of the link. + + :param str target_path: + path (relative or absolute) of the target for this new symbolic + link. + :param str path: + path (relative or absolute) of the symbolic link to create. + :return: an error code `int` like ``SFTP_OK``. + """ + return SFTP_OP_UNSUPPORTED diff --git a/paramiko/ssh_exception.py b/paramiko/ssh_exception.py new file mode 100644 index 0000000..9b1b44c --- /dev/null +++ b/paramiko/ssh_exception.py @@ -0,0 +1,235 @@ +# Copyright (C) 2003-2007 Robey Pointer <robeypointer@gmail.com> +# +# This file is part of paramiko. +# +# Paramiko is free software; you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free +# Software Foundation; either version 2.1 of the License, or (at your option) +# any later version. +# +# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Paramiko; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +import socket + + +class SSHException(Exception): + """ + Exception raised by failures in SSH2 protocol negotiation or logic errors. + """ + + pass + + +class AuthenticationException(SSHException): + """ + Exception raised when authentication failed for some reason. It may be + possible to retry with different credentials. (Other classes specify more + specific reasons.) + + .. versionadded:: 1.6 + """ + + pass + + +class PasswordRequiredException(AuthenticationException): + """ + Exception raised when a password is needed to unlock a private key file. + """ + + pass + + +class BadAuthenticationType(AuthenticationException): + """ + Exception raised when an authentication type (like password) is used, but + the server isn't allowing that type. (It may only allow public-key, for + example.) + + .. versionadded:: 1.1 + """ + + allowed_types = [] + + # TODO 4.0: remove explanation kwarg + def __init__(self, explanation, types): + # TODO 4.0: remove this supercall unless it's actually required for + # pickling (after fixing pickling) + AuthenticationException.__init__(self, explanation, types) + self.explanation = explanation + self.allowed_types = types + + def __str__(self): + return "{}; allowed types: {!r}".format( + self.explanation, self.allowed_types + ) + + +class PartialAuthentication(AuthenticationException): + """ + An internal exception thrown in the case of partial authentication. + """ + + allowed_types = [] + + def __init__(self, types): + AuthenticationException.__init__(self, types) + self.allowed_types = types + + def __str__(self): + return "Partial authentication; allowed types: {!r}".format( + self.allowed_types + ) + + +class ChannelException(SSHException): + """ + Exception raised when an attempt to open a new `.Channel` fails. + + :param int code: the error code returned by the server + + .. versionadded:: 1.6 + """ + + def __init__(self, code, text): + SSHException.__init__(self, code, text) + self.code = code + self.text = text + + def __str__(self): + return "ChannelException({!r}, {!r})".format(self.code, self.text) + + +class BadHostKeyException(SSHException): + """ + The host key given by the SSH server did not match what we were expecting. + + :param str hostname: the hostname of the SSH server + :param PKey got_key: the host key presented by the server + :param PKey expected_key: the host key expected + + .. versionadded:: 1.6 + """ + + def __init__(self, hostname, got_key, expected_key): + SSHException.__init__(self, hostname, got_key, expected_key) + self.hostname = hostname + self.key = got_key + self.expected_key = expected_key + + def __str__(self): + msg = "Host key for server '{}' does not match: got '{}', expected '{}'" # noqa + return msg.format( + self.hostname, + self.key.get_base64(), + self.expected_key.get_base64(), + ) + + +class IncompatiblePeer(SSHException): + """ + A disagreement arose regarding an algorithm required for key exchange. + + .. versionadded:: 2.9 + """ + + # TODO 4.0: consider making this annotate w/ 1..N 'missing' algorithms, + # either just the first one that would halt kex, or even updating the + # Transport logic so we record /all/ that /could/ halt kex. + # TODO: update docstrings where this may end up raised so they are more + # specific. + pass + + +class ProxyCommandFailure(SSHException): + """ + The "ProxyCommand" found in the .ssh/config file returned an error. + + :param str command: The command line that is generating this exception. + :param str error: The error captured from the proxy command output. + """ + + def __init__(self, command, error): + SSHException.__init__(self, command, error) + self.command = command + self.error = error + + def __str__(self): + return 'ProxyCommand("{}") returned nonzero exit status: {}'.format( + self.command, self.error + ) + + +class NoValidConnectionsError(socket.error): + """ + Multiple connection attempts were made and no families succeeded. + + This exception class wraps multiple "real" underlying connection errors, + all of which represent failed connection attempts. Because these errors are + not guaranteed to all be of the same error type (i.e. different errno, + `socket.error` subclass, message, etc) we expose a single unified error + message and a ``None`` errno so that instances of this class match most + normal handling of `socket.error` objects. + + To see the wrapped exception objects, access the ``errors`` attribute. + ``errors`` is a dict whose keys are address tuples (e.g. ``('127.0.0.1', + 22)``) and whose values are the exception encountered trying to connect to + that address. + + It is implied/assumed that all the errors given to a single instance of + this class are from connecting to the same hostname + port (and thus that + the differences are in the resolution of the hostname - e.g. IPv4 vs v6). + + .. versionadded:: 1.16 + """ + + def __init__(self, errors): + """ + :param dict errors: + The errors dict to store, as described by class docstring. + """ + addrs = sorted(errors.keys()) + body = ", ".join([x[0] for x in addrs[:-1]]) + tail = addrs[-1][0] + if body: + msg = "Unable to connect to port {0} on {1} or {2}" + else: + msg = "Unable to connect to port {0} on {2}" + super().__init__( + None, msg.format(addrs[0][1], body, tail) # stand-in for errno + ) + self.errors = errors + + def __reduce__(self): + return (self.__class__, (self.errors,)) + + +class CouldNotCanonicalize(SSHException): + """ + Raised when hostname canonicalization fails & fallback is disabled. + + .. versionadded:: 2.7 + """ + + pass + + +class ConfigParseError(SSHException): + """ + A fatal error was encountered trying to parse SSH config data. + + Typically this means a config file violated the ``ssh_config`` + specification in a manner that requires exiting immediately, such as not + matching ``key = value`` syntax or misusing certain ``Match`` keywords. + + .. versionadded:: 2.7 + """ + + pass diff --git a/paramiko/ssh_gss.py b/paramiko/ssh_gss.py new file mode 100644 index 0000000..ee49c34 --- /dev/null +++ b/paramiko/ssh_gss.py @@ -0,0 +1,778 @@ +# Copyright (C) 2013-2014 science + computing ag +# Author: Sebastian Deiss <sebastian.deiss@t-online.de> +# +# +# This file is part of paramiko. +# +# Paramiko is free software; you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free +# Software Foundation; either version 2.1 of the License, or (at your option) +# any later version. +# +# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Paramiko; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + + +""" +This module provides GSS-API / SSPI authentication as defined in :rfc:`4462`. + +.. note:: Credential delegation is not supported in server mode. + +.. seealso:: :doc:`/api/kex_gss` + +.. versionadded:: 1.15 +""" + +import struct +import os +import sys + + +#: A boolean constraint that indicates if GSS-API / SSPI is available. +GSS_AUTH_AVAILABLE = True + + +#: A tuple of the exception types used by the underlying GSSAPI implementation. +GSS_EXCEPTIONS = () + + +#: :var str _API: Constraint for the used API +_API = None + +try: + import gssapi + + if hasattr(gssapi, "__title__") and gssapi.__title__ == "python-gssapi": + # old, unmaintained python-gssapi package + _API = "MIT" # keep this for compatibility + GSS_EXCEPTIONS = (gssapi.GSSException,) + else: + _API = "PYTHON-GSSAPI-NEW" + GSS_EXCEPTIONS = ( + gssapi.exceptions.GeneralError, + gssapi.raw.misc.GSSError, + ) +except (ImportError, OSError): + try: + import pywintypes + import sspicon + import sspi + + _API = "SSPI" + GSS_EXCEPTIONS = (pywintypes.error,) + except ImportError: + GSS_AUTH_AVAILABLE = False + _API = None + +from paramiko.common import MSG_USERAUTH_REQUEST +from paramiko.ssh_exception import SSHException +from paramiko._version import __version_info__ + + +def GSSAuth(auth_method, gss_deleg_creds=True): + """ + Provide SSH2 GSS-API / SSPI authentication. + + :param str auth_method: The name of the SSH authentication mechanism + (gssapi-with-mic or gss-keyex) + :param bool gss_deleg_creds: Delegate client credentials or not. + We delegate credentials by default. + :return: Either an `._SSH_GSSAPI_OLD` or `._SSH_GSSAPI_NEW` (Unix) + object or an `_SSH_SSPI` (Windows) object + :rtype: object + + :raises: ``ImportError`` -- If no GSS-API / SSPI module could be imported. + + :see: `RFC 4462 <http://www.ietf.org/rfc/rfc4462.txt>`_ + :note: Check for the available API and return either an `._SSH_GSSAPI_OLD` + (MIT GSSAPI using python-gssapi package) object, an + `._SSH_GSSAPI_NEW` (MIT GSSAPI using gssapi package) object + or an `._SSH_SSPI` (MS SSPI) object. + If there is no supported API available, + ``None`` will be returned. + """ + if _API == "MIT": + return _SSH_GSSAPI_OLD(auth_method, gss_deleg_creds) + elif _API == "PYTHON-GSSAPI-NEW": + return _SSH_GSSAPI_NEW(auth_method, gss_deleg_creds) + elif _API == "SSPI" and os.name == "nt": + return _SSH_SSPI(auth_method, gss_deleg_creds) + else: + raise ImportError("Unable to import a GSS-API / SSPI module!") + + +class _SSH_GSSAuth: + """ + Contains the shared variables and methods of `._SSH_GSSAPI_OLD`, + `._SSH_GSSAPI_NEW` and `._SSH_SSPI`. + """ + + def __init__(self, auth_method, gss_deleg_creds): + """ + :param str auth_method: The name of the SSH authentication mechanism + (gssapi-with-mic or gss-keyex) + :param bool gss_deleg_creds: Delegate client credentials or not + """ + self._auth_method = auth_method + self._gss_deleg_creds = gss_deleg_creds + self._gss_host = None + self._username = None + self._session_id = None + self._service = "ssh-connection" + """ + OpenSSH supports Kerberos V5 mechanism only for GSS-API authentication, + so we also support the krb5 mechanism only. + """ + self._krb5_mech = "1.2.840.113554.1.2.2" + + # client mode + self._gss_ctxt = None + self._gss_ctxt_status = False + + # server mode + self._gss_srv_ctxt = None + self._gss_srv_ctxt_status = False + self.cc_file = None + + def set_service(self, service): + """ + This is just a setter to use a non default service. + I added this method, because RFC 4462 doesn't specify "ssh-connection" + as the only service value. + + :param str service: The desired SSH service + """ + if service.find("ssh-"): + self._service = service + + def set_username(self, username): + """ + Setter for C{username}. If GSS-API Key Exchange is performed, the + username is not set by C{ssh_init_sec_context}. + + :param str username: The name of the user who attempts to login + """ + self._username = username + + def ssh_gss_oids(self, mode="client"): + """ + This method returns a single OID, because we only support the + Kerberos V5 mechanism. + + :param str mode: Client for client mode and server for server mode + :return: A byte sequence containing the number of supported + OIDs, the length of the OID and the actual OID encoded with + DER + :note: In server mode we just return the OID length and the DER encoded + OID. + """ + from pyasn1.type.univ import ObjectIdentifier + from pyasn1.codec.der import encoder + + OIDs = self._make_uint32(1) + krb5_OID = encoder.encode(ObjectIdentifier(self._krb5_mech)) + OID_len = self._make_uint32(len(krb5_OID)) + if mode == "server": + return OID_len + krb5_OID + return OIDs + OID_len + krb5_OID + + def ssh_check_mech(self, desired_mech): + """ + Check if the given OID is the Kerberos V5 OID (server mode). + + :param str desired_mech: The desired GSS-API mechanism of the client + :return: ``True`` if the given OID is supported, otherwise C{False} + """ + from pyasn1.codec.der import decoder + + mech, __ = decoder.decode(desired_mech) + if mech.__str__() != self._krb5_mech: + return False + return True + + # Internals + # ------------------------------------------------------------------------- + def _make_uint32(self, integer): + """ + Create a 32 bit unsigned integer (The byte sequence of an integer). + + :param int integer: The integer value to convert + :return: The byte sequence of an 32 bit integer + """ + return struct.pack("!I", integer) + + def _ssh_build_mic(self, session_id, username, service, auth_method): + """ + Create the SSH2 MIC filed for gssapi-with-mic. + + :param str session_id: The SSH session ID + :param str username: The name of the user who attempts to login + :param str service: The requested SSH service + :param str auth_method: The requested SSH authentication mechanism + :return: The MIC as defined in RFC 4462. The contents of the + MIC field are: + string session_identifier, + byte SSH_MSG_USERAUTH_REQUEST, + string user-name, + string service (ssh-connection), + string authentication-method + (gssapi-with-mic or gssapi-keyex) + """ + mic = self._make_uint32(len(session_id)) + mic += session_id + mic += struct.pack("B", MSG_USERAUTH_REQUEST) + mic += self._make_uint32(len(username)) + mic += username.encode() + mic += self._make_uint32(len(service)) + mic += service.encode() + mic += self._make_uint32(len(auth_method)) + mic += auth_method.encode() + return mic + + +class _SSH_GSSAPI_OLD(_SSH_GSSAuth): + """ + Implementation of the GSS-API MIT Kerberos Authentication for SSH2, + using the older (unmaintained) python-gssapi package. + + :see: `.GSSAuth` + """ + + def __init__(self, auth_method, gss_deleg_creds): + """ + :param str auth_method: The name of the SSH authentication mechanism + (gssapi-with-mic or gss-keyex) + :param bool gss_deleg_creds: Delegate client credentials or not + """ + _SSH_GSSAuth.__init__(self, auth_method, gss_deleg_creds) + + if self._gss_deleg_creds: + self._gss_flags = ( + gssapi.C_PROT_READY_FLAG, + gssapi.C_INTEG_FLAG, + gssapi.C_MUTUAL_FLAG, + gssapi.C_DELEG_FLAG, + ) + else: + self._gss_flags = ( + gssapi.C_PROT_READY_FLAG, + gssapi.C_INTEG_FLAG, + gssapi.C_MUTUAL_FLAG, + ) + + def ssh_init_sec_context( + self, target, desired_mech=None, username=None, recv_token=None + ): + """ + Initialize a GSS-API context. + + :param str username: The name of the user who attempts to login + :param str target: The hostname of the target to connect to + :param str desired_mech: The negotiated GSS-API mechanism + ("pseudo negotiated" mechanism, because we + support just the krb5 mechanism :-)) + :param str recv_token: The GSS-API token received from the Server + :raises: + `.SSHException` -- Is raised if the desired mechanism of the client + is not supported + :return: A ``String`` if the GSS-API has returned a token or + ``None`` if no token was returned + """ + from pyasn1.codec.der import decoder + + self._username = username + self._gss_host = target + targ_name = gssapi.Name( + "host@" + self._gss_host, gssapi.C_NT_HOSTBASED_SERVICE + ) + ctx = gssapi.Context() + ctx.flags = self._gss_flags + if desired_mech is None: + krb5_mech = gssapi.OID.mech_from_string(self._krb5_mech) + else: + mech, __ = decoder.decode(desired_mech) + if mech.__str__() != self._krb5_mech: + raise SSHException("Unsupported mechanism OID.") + else: + krb5_mech = gssapi.OID.mech_from_string(self._krb5_mech) + token = None + try: + if recv_token is None: + self._gss_ctxt = gssapi.InitContext( + peer_name=targ_name, + mech_type=krb5_mech, + req_flags=ctx.flags, + ) + token = self._gss_ctxt.step(token) + else: + token = self._gss_ctxt.step(recv_token) + except gssapi.GSSException: + message = "{} Target: {}".format(sys.exc_info()[1], self._gss_host) + raise gssapi.GSSException(message) + self._gss_ctxt_status = self._gss_ctxt.established + return token + + def ssh_get_mic(self, session_id, gss_kex=False): + """ + Create the MIC token for a SSH2 message. + + :param str session_id: The SSH session ID + :param bool gss_kex: Generate the MIC for GSS-API Key Exchange or not + :return: gssapi-with-mic: + Returns the MIC token from GSS-API for the message we created + with ``_ssh_build_mic``. + gssapi-keyex: + Returns the MIC token from GSS-API with the SSH session ID as + message. + """ + self._session_id = session_id + if not gss_kex: + mic_field = self._ssh_build_mic( + self._session_id, + self._username, + self._service, + self._auth_method, + ) + mic_token = self._gss_ctxt.get_mic(mic_field) + else: + # for key exchange with gssapi-keyex + mic_token = self._gss_srv_ctxt.get_mic(self._session_id) + return mic_token + + def ssh_accept_sec_context(self, hostname, recv_token, username=None): + """ + Accept a GSS-API context (server mode). + + :param str hostname: The servers hostname + :param str username: The name of the user who attempts to login + :param str recv_token: The GSS-API Token received from the server, + if it's not the initial call. + :return: A ``String`` if the GSS-API has returned a token or ``None`` + if no token was returned + """ + # hostname and username are not required for GSSAPI, but for SSPI + self._gss_host = hostname + self._username = username + if self._gss_srv_ctxt is None: + self._gss_srv_ctxt = gssapi.AcceptContext() + token = self._gss_srv_ctxt.step(recv_token) + self._gss_srv_ctxt_status = self._gss_srv_ctxt.established + return token + + def ssh_check_mic(self, mic_token, session_id, username=None): + """ + Verify the MIC token for a SSH2 message. + + :param str mic_token: The MIC token received from the client + :param str session_id: The SSH session ID + :param str username: The name of the user who attempts to login + :return: None if the MIC check was successful + :raises: ``gssapi.GSSException`` -- if the MIC check failed + """ + self._session_id = session_id + self._username = username + if self._username is not None: + # server mode + mic_field = self._ssh_build_mic( + self._session_id, + self._username, + self._service, + self._auth_method, + ) + self._gss_srv_ctxt.verify_mic(mic_field, mic_token) + else: + # for key exchange with gssapi-keyex + # client mode + self._gss_ctxt.verify_mic(self._session_id, mic_token) + + @property + def credentials_delegated(self): + """ + Checks if credentials are delegated (server mode). + + :return: ``True`` if credentials are delegated, otherwise ``False`` + """ + if self._gss_srv_ctxt.delegated_cred is not None: + return True + return False + + def save_client_creds(self, client_token): + """ + Save the Client token in a file. This is used by the SSH server + to store the client credentials if credentials are delegated + (server mode). + + :param str client_token: The GSS-API token received form the client + :raises: + ``NotImplementedError`` -- Credential delegation is currently not + supported in server mode + """ + raise NotImplementedError + + +if __version_info__ < (2, 5): + # provide the old name for strict backward compatibility + _SSH_GSSAPI = _SSH_GSSAPI_OLD + + +class _SSH_GSSAPI_NEW(_SSH_GSSAuth): + """ + Implementation of the GSS-API MIT Kerberos Authentication for SSH2, + using the newer, currently maintained gssapi package. + + :see: `.GSSAuth` + """ + + def __init__(self, auth_method, gss_deleg_creds): + """ + :param str auth_method: The name of the SSH authentication mechanism + (gssapi-with-mic or gss-keyex) + :param bool gss_deleg_creds: Delegate client credentials or not + """ + _SSH_GSSAuth.__init__(self, auth_method, gss_deleg_creds) + + if self._gss_deleg_creds: + self._gss_flags = ( + gssapi.RequirementFlag.protection_ready, + gssapi.RequirementFlag.integrity, + gssapi.RequirementFlag.mutual_authentication, + gssapi.RequirementFlag.delegate_to_peer, + ) + else: + self._gss_flags = ( + gssapi.RequirementFlag.protection_ready, + gssapi.RequirementFlag.integrity, + gssapi.RequirementFlag.mutual_authentication, + ) + + def ssh_init_sec_context( + self, target, desired_mech=None, username=None, recv_token=None + ): + """ + Initialize a GSS-API context. + + :param str username: The name of the user who attempts to login + :param str target: The hostname of the target to connect to + :param str desired_mech: The negotiated GSS-API mechanism + ("pseudo negotiated" mechanism, because we + support just the krb5 mechanism :-)) + :param str recv_token: The GSS-API token received from the Server + :raises: `.SSHException` -- Is raised if the desired mechanism of the + client is not supported + :raises: ``gssapi.exceptions.GSSError`` if there is an error signaled + by the GSS-API implementation + :return: A ``String`` if the GSS-API has returned a token or ``None`` + if no token was returned + """ + from pyasn1.codec.der import decoder + + self._username = username + self._gss_host = target + targ_name = gssapi.Name( + "host@" + self._gss_host, + name_type=gssapi.NameType.hostbased_service, + ) + if desired_mech is not None: + mech, __ = decoder.decode(desired_mech) + if mech.__str__() != self._krb5_mech: + raise SSHException("Unsupported mechanism OID.") + krb5_mech = gssapi.MechType.kerberos + token = None + if recv_token is None: + self._gss_ctxt = gssapi.SecurityContext( + name=targ_name, + flags=self._gss_flags, + mech=krb5_mech, + usage="initiate", + ) + token = self._gss_ctxt.step(token) + else: + token = self._gss_ctxt.step(recv_token) + self._gss_ctxt_status = self._gss_ctxt.complete + return token + + def ssh_get_mic(self, session_id, gss_kex=False): + """ + Create the MIC token for a SSH2 message. + + :param str session_id: The SSH session ID + :param bool gss_kex: Generate the MIC for GSS-API Key Exchange or not + :return: gssapi-with-mic: + Returns the MIC token from GSS-API for the message we created + with ``_ssh_build_mic``. + gssapi-keyex: + Returns the MIC token from GSS-API with the SSH session ID as + message. + :rtype: str + """ + self._session_id = session_id + if not gss_kex: + mic_field = self._ssh_build_mic( + self._session_id, + self._username, + self._service, + self._auth_method, + ) + mic_token = self._gss_ctxt.get_signature(mic_field) + else: + # for key exchange with gssapi-keyex + mic_token = self._gss_srv_ctxt.get_signature(self._session_id) + return mic_token + + def ssh_accept_sec_context(self, hostname, recv_token, username=None): + """ + Accept a GSS-API context (server mode). + + :param str hostname: The servers hostname + :param str username: The name of the user who attempts to login + :param str recv_token: The GSS-API Token received from the server, + if it's not the initial call. + :return: A ``String`` if the GSS-API has returned a token or ``None`` + if no token was returned + """ + # hostname and username are not required for GSSAPI, but for SSPI + self._gss_host = hostname + self._username = username + if self._gss_srv_ctxt is None: + self._gss_srv_ctxt = gssapi.SecurityContext(usage="accept") + token = self._gss_srv_ctxt.step(recv_token) + self._gss_srv_ctxt_status = self._gss_srv_ctxt.complete + return token + + def ssh_check_mic(self, mic_token, session_id, username=None): + """ + Verify the MIC token for a SSH2 message. + + :param str mic_token: The MIC token received from the client + :param str session_id: The SSH session ID + :param str username: The name of the user who attempts to login + :return: None if the MIC check was successful + :raises: ``gssapi.exceptions.GSSError`` -- if the MIC check failed + """ + self._session_id = session_id + self._username = username + if self._username is not None: + # server mode + mic_field = self._ssh_build_mic( + self._session_id, + self._username, + self._service, + self._auth_method, + ) + self._gss_srv_ctxt.verify_signature(mic_field, mic_token) + else: + # for key exchange with gssapi-keyex + # client mode + self._gss_ctxt.verify_signature(self._session_id, mic_token) + + @property + def credentials_delegated(self): + """ + Checks if credentials are delegated (server mode). + + :return: ``True`` if credentials are delegated, otherwise ``False`` + :rtype: bool + """ + if self._gss_srv_ctxt.delegated_creds is not None: + return True + return False + + def save_client_creds(self, client_token): + """ + Save the Client token in a file. This is used by the SSH server + to store the client credentials if credentials are delegated + (server mode). + + :param str client_token: The GSS-API token received form the client + :raises: ``NotImplementedError`` -- Credential delegation is currently + not supported in server mode + """ + raise NotImplementedError + + +class _SSH_SSPI(_SSH_GSSAuth): + """ + Implementation of the Microsoft SSPI Kerberos Authentication for SSH2. + + :see: `.GSSAuth` + """ + + def __init__(self, auth_method, gss_deleg_creds): + """ + :param str auth_method: The name of the SSH authentication mechanism + (gssapi-with-mic or gss-keyex) + :param bool gss_deleg_creds: Delegate client credentials or not + """ + _SSH_GSSAuth.__init__(self, auth_method, gss_deleg_creds) + + if self._gss_deleg_creds: + self._gss_flags = ( + sspicon.ISC_REQ_INTEGRITY + | sspicon.ISC_REQ_MUTUAL_AUTH + | sspicon.ISC_REQ_DELEGATE + ) + else: + self._gss_flags = ( + sspicon.ISC_REQ_INTEGRITY | sspicon.ISC_REQ_MUTUAL_AUTH + ) + + def ssh_init_sec_context( + self, target, desired_mech=None, username=None, recv_token=None + ): + """ + Initialize a SSPI context. + + :param str username: The name of the user who attempts to login + :param str target: The FQDN of the target to connect to + :param str desired_mech: The negotiated SSPI mechanism + ("pseudo negotiated" mechanism, because we + support just the krb5 mechanism :-)) + :param recv_token: The SSPI token received from the Server + :raises: + `.SSHException` -- Is raised if the desired mechanism of the client + is not supported + :return: A ``String`` if the SSPI has returned a token or ``None`` if + no token was returned + """ + from pyasn1.codec.der import decoder + + self._username = username + self._gss_host = target + error = 0 + targ_name = "host/" + self._gss_host + if desired_mech is not None: + mech, __ = decoder.decode(desired_mech) + if mech.__str__() != self._krb5_mech: + raise SSHException("Unsupported mechanism OID.") + try: + if recv_token is None: + self._gss_ctxt = sspi.ClientAuth( + "Kerberos", scflags=self._gss_flags, targetspn=targ_name + ) + error, token = self._gss_ctxt.authorize(recv_token) + token = token[0].Buffer + except pywintypes.error as e: + e.strerror += ", Target: {}".format(self._gss_host) + raise + + if error == 0: + """ + if the status is GSS_COMPLETE (error = 0) the context is fully + established an we can set _gss_ctxt_status to True. + """ + self._gss_ctxt_status = True + token = None + """ + You won't get another token if the context is fully established, + so i set token to None instead of "" + """ + return token + + def ssh_get_mic(self, session_id, gss_kex=False): + """ + Create the MIC token for a SSH2 message. + + :param str session_id: The SSH session ID + :param bool gss_kex: Generate the MIC for Key Exchange with SSPI or not + :return: gssapi-with-mic: + Returns the MIC token from SSPI for the message we created + with ``_ssh_build_mic``. + gssapi-keyex: + Returns the MIC token from SSPI with the SSH session ID as + message. + """ + self._session_id = session_id + if not gss_kex: + mic_field = self._ssh_build_mic( + self._session_id, + self._username, + self._service, + self._auth_method, + ) + mic_token = self._gss_ctxt.sign(mic_field) + else: + # for key exchange with gssapi-keyex + mic_token = self._gss_srv_ctxt.sign(self._session_id) + return mic_token + + def ssh_accept_sec_context(self, hostname, username, recv_token): + """ + Accept a SSPI context (server mode). + + :param str hostname: The servers FQDN + :param str username: The name of the user who attempts to login + :param str recv_token: The SSPI Token received from the server, + if it's not the initial call. + :return: A ``String`` if the SSPI has returned a token or ``None`` if + no token was returned + """ + self._gss_host = hostname + self._username = username + targ_name = "host/" + self._gss_host + self._gss_srv_ctxt = sspi.ServerAuth("Kerberos", spn=targ_name) + error, token = self._gss_srv_ctxt.authorize(recv_token) + token = token[0].Buffer + if error == 0: + self._gss_srv_ctxt_status = True + token = None + return token + + def ssh_check_mic(self, mic_token, session_id, username=None): + """ + Verify the MIC token for a SSH2 message. + + :param str mic_token: The MIC token received from the client + :param str session_id: The SSH session ID + :param str username: The name of the user who attempts to login + :return: None if the MIC check was successful + :raises: ``sspi.error`` -- if the MIC check failed + """ + self._session_id = session_id + self._username = username + if username is not None: + # server mode + mic_field = self._ssh_build_mic( + self._session_id, + self._username, + self._service, + self._auth_method, + ) + # Verifies data and its signature. If verification fails, an + # sspi.error will be raised. + self._gss_srv_ctxt.verify(mic_field, mic_token) + else: + # for key exchange with gssapi-keyex + # client mode + # Verifies data and its signature. If verification fails, an + # sspi.error will be raised. + self._gss_ctxt.verify(self._session_id, mic_token) + + @property + def credentials_delegated(self): + """ + Checks if credentials are delegated (server mode). + + :return: ``True`` if credentials are delegated, otherwise ``False`` + """ + return self._gss_flags & sspicon.ISC_REQ_DELEGATE and ( + self._gss_srv_ctxt_status or self._gss_flags + ) + + def save_client_creds(self, client_token): + """ + Save the Client token in a file. This is used by the SSH server + to store the client credentails if credentials are delegated + (server mode). + + :param str client_token: The SSPI token received form the client + :raises: + ``NotImplementedError`` -- Credential delegation is currently not + supported in server mode + """ + raise NotImplementedError diff --git a/paramiko/transport.py b/paramiko/transport.py new file mode 100644 index 0000000..a83a901 --- /dev/null +++ b/paramiko/transport.py @@ -0,0 +1,3217 @@ +# Copyright (C) 2003-2007 Robey Pointer <robeypointer@gmail.com> +# Copyright (C) 2003-2007 Robey Pointer <robeypointer@gmail.com> +# +# This file is part of paramiko. +# +# Paramiko is free software; you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free +# Software Foundation; either version 2.1 of the License, or (at your option) +# any later version. +# +# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Paramiko; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +""" +Core protocol implementation +""" + +import os +import socket +import sys +import threading +import time +import weakref +from hashlib import md5, sha1, sha256, sha512 + +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives.ciphers import algorithms, Cipher, modes + +import paramiko +from paramiko import util +from paramiko.auth_handler import AuthHandler +from paramiko.ssh_gss import GSSAuth +from paramiko.channel import Channel +from paramiko.common import ( + xffffffff, + cMSG_CHANNEL_OPEN, + cMSG_IGNORE, + cMSG_GLOBAL_REQUEST, + DEBUG, + MSG_KEXINIT, + MSG_IGNORE, + MSG_DISCONNECT, + MSG_DEBUG, + ERROR, + WARNING, + cMSG_UNIMPLEMENTED, + INFO, + cMSG_KEXINIT, + cMSG_NEWKEYS, + MSG_NEWKEYS, + cMSG_REQUEST_SUCCESS, + cMSG_REQUEST_FAILURE, + CONNECTION_FAILED_CODE, + OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED, + OPEN_SUCCEEDED, + cMSG_CHANNEL_OPEN_FAILURE, + cMSG_CHANNEL_OPEN_SUCCESS, + MSG_GLOBAL_REQUEST, + MSG_REQUEST_SUCCESS, + MSG_REQUEST_FAILURE, + MSG_CHANNEL_OPEN_SUCCESS, + MSG_CHANNEL_OPEN_FAILURE, + MSG_CHANNEL_OPEN, + MSG_CHANNEL_SUCCESS, + MSG_CHANNEL_FAILURE, + MSG_CHANNEL_DATA, + MSG_CHANNEL_EXTENDED_DATA, + MSG_CHANNEL_WINDOW_ADJUST, + MSG_CHANNEL_REQUEST, + MSG_CHANNEL_EOF, + MSG_CHANNEL_CLOSE, + MIN_WINDOW_SIZE, + MIN_PACKET_SIZE, + MAX_WINDOW_SIZE, + DEFAULT_WINDOW_SIZE, + DEFAULT_MAX_PACKET_SIZE, + HIGHEST_USERAUTH_MESSAGE_ID, + MSG_UNIMPLEMENTED, + MSG_NAMES, + MSG_EXT_INFO, + cMSG_EXT_INFO, + byte_ord, +) +from paramiko.compress import ZlibCompressor, ZlibDecompressor +from paramiko.dsskey import DSSKey +from paramiko.ed25519key import Ed25519Key +from paramiko.kex_curve25519 import KexCurve25519 +from paramiko.kex_gex import KexGex, KexGexSHA256 +from paramiko.kex_group1 import KexGroup1 +from paramiko.kex_group14 import KexGroup14, KexGroup14SHA256 +from paramiko.kex_group16 import KexGroup16SHA512 +from paramiko.kex_ecdh_nist import KexNistp256, KexNistp384, KexNistp521 +from paramiko.kex_gss import KexGSSGex, KexGSSGroup1, KexGSSGroup14 +from paramiko.message import Message +from paramiko.packet import Packetizer, NeedRekeyException +from paramiko.primes import ModulusPack +from paramiko.rsakey import RSAKey +from paramiko.ecdsakey import ECDSAKey +from paramiko.server import ServerInterface +from paramiko.sftp_client import SFTPClient +from paramiko.ssh_exception import ( + SSHException, + BadAuthenticationType, + ChannelException, + IncompatiblePeer, + ProxyCommandFailure, +) +from paramiko.util import ( + ClosingContextManager, + clamp_value, + b, +) + + +# for thread cleanup +_active_threads = [] + + +def _join_lingering_threads(): + for thr in _active_threads: + thr.stop_thread() + + +import atexit + +atexit.register(_join_lingering_threads) + + +class Transport(threading.Thread, ClosingContextManager): + """ + An SSH Transport attaches to a stream (usually a socket), negotiates an + encrypted session, authenticates, and then creates stream tunnels, called + `channels <.Channel>`, across the session. Multiple channels can be + multiplexed across a single session (and often are, in the case of port + forwardings). + + Instances of this class may be used as context managers. + """ + + _ENCRYPT = object() + _DECRYPT = object() + + _PROTO_ID = "2.0" + _CLIENT_ID = "paramiko_{}".format(paramiko.__version__) + + # These tuples of algorithm identifiers are in preference order; do not + # reorder without reason! + # NOTE: if you need to modify these, we suggest leveraging the + # `disabled_algorithms` constructor argument (also available in SSHClient) + # instead of monkeypatching or subclassing. + _preferred_ciphers = ( + "aes128-ctr", + "aes192-ctr", + "aes256-ctr", + "aes128-cbc", + "aes192-cbc", + "aes256-cbc", + "3des-cbc", + ) + _preferred_macs = ( + "hmac-sha2-256", + "hmac-sha2-512", + "hmac-sha2-256-etm@openssh.com", + "hmac-sha2-512-etm@openssh.com", + "hmac-sha1", + "hmac-md5", + "hmac-sha1-96", + "hmac-md5-96", + ) + # ~= HostKeyAlgorithms in OpenSSH land + _preferred_keys = ( + "ssh-ed25519", + "ecdsa-sha2-nistp256", + "ecdsa-sha2-nistp384", + "ecdsa-sha2-nistp521", + "rsa-sha2-512", + "rsa-sha2-256", + "ssh-rsa", + "ssh-dss", + ) + # ~= PubKeyAcceptedAlgorithms + _preferred_pubkeys = ( + "ssh-ed25519", + "ecdsa-sha2-nistp256", + "ecdsa-sha2-nistp384", + "ecdsa-sha2-nistp521", + "rsa-sha2-512", + "rsa-sha2-256", + "ssh-rsa", + "ssh-dss", + ) + _preferred_kex = ( + "ecdh-sha2-nistp256", + "ecdh-sha2-nistp384", + "ecdh-sha2-nistp521", + "diffie-hellman-group16-sha512", + "diffie-hellman-group-exchange-sha256", + "diffie-hellman-group14-sha256", + "diffie-hellman-group-exchange-sha1", + "diffie-hellman-group14-sha1", + "diffie-hellman-group1-sha1", + ) + if KexCurve25519.is_available(): + _preferred_kex = ("curve25519-sha256@libssh.org",) + _preferred_kex + _preferred_gsskex = ( + "gss-gex-sha1-toWM5Slw5Ew8Mqkay+al2g==", + "gss-group14-sha1-toWM5Slw5Ew8Mqkay+al2g==", + "gss-group1-sha1-toWM5Slw5Ew8Mqkay+al2g==", + ) + _preferred_compression = ("none",) + + _cipher_info = { + "aes128-ctr": { + "class": algorithms.AES, + "mode": modes.CTR, + "block-size": 16, + "key-size": 16, + }, + "aes192-ctr": { + "class": algorithms.AES, + "mode": modes.CTR, + "block-size": 16, + "key-size": 24, + }, + "aes256-ctr": { + "class": algorithms.AES, + "mode": modes.CTR, + "block-size": 16, + "key-size": 32, + }, + "aes128-cbc": { + "class": algorithms.AES, + "mode": modes.CBC, + "block-size": 16, + "key-size": 16, + }, + "aes192-cbc": { + "class": algorithms.AES, + "mode": modes.CBC, + "block-size": 16, + "key-size": 24, + }, + "aes256-cbc": { + "class": algorithms.AES, + "mode": modes.CBC, + "block-size": 16, + "key-size": 32, + }, + "3des-cbc": { + "class": algorithms.TripleDES, + "mode": modes.CBC, + "block-size": 8, + "key-size": 24, + }, + } + + _mac_info = { + "hmac-sha1": {"class": sha1, "size": 20}, + "hmac-sha1-96": {"class": sha1, "size": 12}, + "hmac-sha2-256": {"class": sha256, "size": 32}, + "hmac-sha2-256-etm@openssh.com": {"class": sha256, "size": 32}, + "hmac-sha2-512": {"class": sha512, "size": 64}, + "hmac-sha2-512-etm@openssh.com": {"class": sha512, "size": 64}, + "hmac-md5": {"class": md5, "size": 16}, + "hmac-md5-96": {"class": md5, "size": 12}, + } + + _key_info = { + # TODO: at some point we will want to drop this as it's no longer + # considered secure due to using SHA-1 for signatures. OpenSSH 8.8 no + # longer supports it. Question becomes at what point do we want to + # prevent users with older setups from using this? + "ssh-rsa": RSAKey, + "ssh-rsa-cert-v01@openssh.com": RSAKey, + "rsa-sha2-256": RSAKey, + "rsa-sha2-256-cert-v01@openssh.com": RSAKey, + "rsa-sha2-512": RSAKey, + "rsa-sha2-512-cert-v01@openssh.com": RSAKey, + "ssh-dss": DSSKey, + "ssh-dss-cert-v01@openssh.com": DSSKey, + "ecdsa-sha2-nistp256": ECDSAKey, + "ecdsa-sha2-nistp256-cert-v01@openssh.com": ECDSAKey, + "ecdsa-sha2-nistp384": ECDSAKey, + "ecdsa-sha2-nistp384-cert-v01@openssh.com": ECDSAKey, + "ecdsa-sha2-nistp521": ECDSAKey, + "ecdsa-sha2-nistp521-cert-v01@openssh.com": ECDSAKey, + "ssh-ed25519": Ed25519Key, + "ssh-ed25519-cert-v01@openssh.com": Ed25519Key, + } + + _kex_info = { + "diffie-hellman-group1-sha1": KexGroup1, + "diffie-hellman-group14-sha1": KexGroup14, + "diffie-hellman-group-exchange-sha1": KexGex, + "diffie-hellman-group-exchange-sha256": KexGexSHA256, + "diffie-hellman-group14-sha256": KexGroup14SHA256, + "diffie-hellman-group16-sha512": KexGroup16SHA512, + "gss-group1-sha1-toWM5Slw5Ew8Mqkay+al2g==": KexGSSGroup1, + "gss-group14-sha1-toWM5Slw5Ew8Mqkay+al2g==": KexGSSGroup14, + "gss-gex-sha1-toWM5Slw5Ew8Mqkay+al2g==": KexGSSGex, + "ecdh-sha2-nistp256": KexNistp256, + "ecdh-sha2-nistp384": KexNistp384, + "ecdh-sha2-nistp521": KexNistp521, + } + if KexCurve25519.is_available(): + _kex_info["curve25519-sha256@libssh.org"] = KexCurve25519 + + _compression_info = { + # zlib@openssh.com is just zlib, but only turned on after a successful + # authentication. openssh servers may only offer this type because + # they've had troubles with security holes in zlib in the past. + "zlib@openssh.com": (ZlibCompressor, ZlibDecompressor), + "zlib": (ZlibCompressor, ZlibDecompressor), + "none": (None, None), + } + + _modulus_pack = None + _active_check_timeout = 0.1 + + def __init__( + self, + sock, + default_window_size=DEFAULT_WINDOW_SIZE, + default_max_packet_size=DEFAULT_MAX_PACKET_SIZE, + gss_kex=False, + gss_deleg_creds=True, + disabled_algorithms=None, + server_sig_algs=True, + ): + """ + Create a new SSH session over an existing socket, or socket-like + object. This only creates the `.Transport` object; it doesn't begin + the SSH session yet. Use `connect` or `start_client` to begin a client + session, or `start_server` to begin a server session. + + If the object is not actually a socket, it must have the following + methods: + + - ``send(bytes)``: Writes from 1 to ``len(bytes)`` bytes, and returns + an int representing the number of bytes written. Returns + 0 or raises ``EOFError`` if the stream has been closed. + - ``recv(int)``: Reads from 1 to ``int`` bytes and returns them as a + string. Returns 0 or raises ``EOFError`` if the stream has been + closed. + - ``close()``: Closes the socket. + - ``settimeout(n)``: Sets a (float) timeout on I/O operations. + + For ease of use, you may also pass in an address (as a tuple) or a host + string as the ``sock`` argument. (A host string is a hostname with an + optional port (separated by ``":"``) which will be converted into a + tuple of ``(hostname, port)``.) A socket will be connected to this + address and used for communication. Exceptions from the ``socket`` + call may be thrown in this case. + + .. note:: + Modifying the the window and packet sizes might have adverse + effects on your channels created from this transport. The default + values are the same as in the OpenSSH code base and have been + battle tested. + + :param socket sock: + a socket or socket-like object to create the session over. + :param int default_window_size: + sets the default window size on the transport. (defaults to + 2097152) + :param int default_max_packet_size: + sets the default max packet size on the transport. (defaults to + 32768) + :param bool gss_kex: + Whether to enable GSSAPI key exchange when GSSAPI is in play. + Default: ``False``. + :param bool gss_deleg_creds: + Whether to enable GSSAPI credential delegation when GSSAPI is in + play. Default: ``True``. + :param dict disabled_algorithms: + If given, must be a dictionary mapping algorithm type to an + iterable of algorithm identifiers, which will be disabled for the + lifetime of the transport. + + Keys should match the last word in the class' builtin algorithm + tuple attributes, such as ``"ciphers"`` to disable names within + ``_preferred_ciphers``; or ``"kex"`` to disable something defined + inside ``_preferred_kex``. Values should exactly match members of + the matching attribute. + + For example, if you need to disable + ``diffie-hellman-group16-sha512`` key exchange (perhaps because + your code talks to a server which implements it differently from + Paramiko), specify ``disabled_algorithms={"kex": + ["diffie-hellman-group16-sha512"]}``. + :param bool server_sig_algs: + Whether to send an extra message to compatible clients, in server + mode, with a list of supported pubkey algorithms. Default: + ``True``. + + .. versionchanged:: 1.15 + Added the ``default_window_size`` and ``default_max_packet_size`` + arguments. + .. versionchanged:: 1.15 + Added the ``gss_kex`` and ``gss_deleg_creds`` kwargs. + .. versionchanged:: 2.6 + Added the ``disabled_algorithms`` kwarg. + .. versionchanged:: 2.9 + Added the ``server_sig_algs`` kwarg. + """ + self.active = False + self.hostname = None + self.server_extensions = {} + + if isinstance(sock, str): + + # convert "host:port" into (host, port) + hl = sock.split(":", 1) + self.hostname = hl[0] + if len(hl) == 1: + sock = (hl[0], 22) + else: + sock = (hl[0], int(hl[1])) + if type(sock) is tuple: + # connect to the given (host, port) + hostname, port = sock + self.hostname = hostname + reason = "No suitable address family" + addrinfos = socket.getaddrinfo( + hostname, port, socket.AF_UNSPEC, socket.SOCK_STREAM + ) + for family, socktype, proto, canonname, sockaddr in addrinfos: + if socktype == socket.SOCK_STREAM: + af = family + # addr = sockaddr + sock = socket.socket(af, socket.SOCK_STREAM) + try: + sock.connect((hostname, port)) + except socket.error as e: + reason = str(e) + else: + break + else: + raise SSHException( + "Unable to connect to {}: {}".format(hostname, reason) + ) + # okay, normal socket-ish flow here... + threading.Thread.__init__(self) + self.daemon = True + self.sock = sock + # we set the timeout so we can check self.active periodically to + # see if we should bail. socket.timeout exception is never propagated. + self.sock.settimeout(self._active_check_timeout) + + # negotiated crypto parameters + self.packetizer = Packetizer(sock) + self.local_version = "SSH-" + self._PROTO_ID + "-" + self._CLIENT_ID + self.remote_version = "" + self.local_cipher = self.remote_cipher = "" + self.local_kex_init = self.remote_kex_init = None + self.local_mac = self.remote_mac = None + self.local_compression = self.remote_compression = None + self.session_id = None + self.host_key_type = None + self.host_key = None + + # GSS-API / SSPI Key Exchange + self.use_gss_kex = gss_kex + # This will be set to True if GSS-API Key Exchange was performed + self.gss_kex_used = False + self.kexgss_ctxt = None + self.gss_host = None + if self.use_gss_kex: + self.kexgss_ctxt = GSSAuth("gssapi-keyex", gss_deleg_creds) + self._preferred_kex = self._preferred_gsskex + self._preferred_kex + + # state used during negotiation + self.kex_engine = None + self.H = None + self.K = None + + self.initial_kex_done = False + self.in_kex = False + self.authenticated = False + self._expected_packet = tuple() + # synchronization (always higher level than write_lock) + self.lock = threading.Lock() + + # tracking open channels + self._channels = ChannelMap() + self.channel_events = {} # (id -> Event) + self.channels_seen = {} # (id -> True) + self._channel_counter = 0 + self.default_max_packet_size = default_max_packet_size + self.default_window_size = default_window_size + self._forward_agent_handler = None + self._x11_handler = None + self._tcp_handler = None + + self.saved_exception = None + self.clear_to_send = threading.Event() + self.clear_to_send_lock = threading.Lock() + self.clear_to_send_timeout = 30.0 + self.log_name = "paramiko.transport" + self.logger = util.get_logger(self.log_name) + self.packetizer.set_log(self.logger) + self.auth_handler = None + # response Message from an arbitrary global request + self.global_response = None + # user-defined event callbacks + self.completion_event = None + # how long (seconds) to wait for the SSH banner + self.banner_timeout = 15 + # how long (seconds) to wait for the handshake to finish after SSH + # banner sent. + self.handshake_timeout = 15 + # how long (seconds) to wait for the auth response. + self.auth_timeout = 30 + # how long (seconds) to wait for opening a channel + self.channel_timeout = 60 * 60 + self.disabled_algorithms = disabled_algorithms or {} + self.server_sig_algs = server_sig_algs + + # server mode: + self.server_mode = False + self.server_object = None + self.server_key_dict = {} + self.server_accepts = [] + self.server_accept_cv = threading.Condition(self.lock) + self.subsystem_table = {} + + def _filter_algorithm(self, type_): + default = getattr(self, "_preferred_{}".format(type_)) + return tuple( + x + for x in default + if x not in self.disabled_algorithms.get(type_, []) + ) + + @property + def preferred_ciphers(self): + return self._filter_algorithm("ciphers") + + @property + def preferred_macs(self): + return self._filter_algorithm("macs") + + @property + def preferred_keys(self): + # Interleave cert variants here; resistant to various background + # overwriting of _preferred_keys, and necessary as hostkeys can't use + # the logic pubkey auth does re: injecting/checking for certs at + # runtime + filtered = self._filter_algorithm("keys") + return tuple( + filtered + + tuple("{}-cert-v01@openssh.com".format(x) for x in filtered) + ) + + @property + def preferred_pubkeys(self): + return self._filter_algorithm("pubkeys") + + @property + def preferred_kex(self): + return self._filter_algorithm("kex") + + @property + def preferred_compression(self): + return self._filter_algorithm("compression") + + def __repr__(self): + """ + Returns a string representation of this object, for debugging. + """ + id_ = hex(id(self) & xffffffff) + out = "<paramiko.Transport at {}".format(id_) + if not self.active: + out += " (unconnected)" + else: + if self.local_cipher != "": + out += " (cipher {}, {:d} bits)".format( + self.local_cipher, + self._cipher_info[self.local_cipher]["key-size"] * 8, + ) + if self.is_authenticated(): + out += " (active; {} open channel(s))".format( + len(self._channels) + ) + elif self.initial_kex_done: + out += " (connected; awaiting auth)" + else: + out += " (connecting)" + out += ">" + return out + + def atfork(self): + """ + Terminate this Transport without closing the session. On posix + systems, if a Transport is open during process forking, both parent + and child will share the underlying socket, but only one process can + use the connection (without corrupting the session). Use this method + to clean up a Transport object without disrupting the other process. + + .. versionadded:: 1.5.3 + """ + self.sock.close() + self.close() + + def get_security_options(self): + print("get_security_options") + """ + Return a `.SecurityOptions` object which can be used to tweak the + encryption algorithms this transport will permit (for encryption, + digest/hash operations, public keys, and key exchanges) and the order + of preference for them. + """ + return SecurityOptions(self) + + def set_gss_host(self, gss_host, trust_dns=True, gssapi_requested=True): + print("set_gss_host") + """ + Normalize/canonicalize ``self.gss_host`` depending on various factors. + + :param str gss_host: + The explicitly requested GSS-oriented hostname to connect to (i.e. + what the host's name is in the Kerberos database.) Defaults to + ``self.hostname`` (which will be the 'real' target hostname and/or + host portion of given socket object.) + :param bool trust_dns: + Indicates whether or not DNS is trusted; if true, DNS will be used + to canonicalize the GSS hostname (which again will either be + ``gss_host`` or the transport's default hostname.) + (Defaults to True due to backwards compatibility.) + :param bool gssapi_requested: + Whether GSSAPI key exchange or authentication was even requested. + If not, this is a no-op and nothing happens + (and ``self.gss_host`` is not set.) + (Defaults to True due to backwards compatibility.) + :returns: ``None``. + """ + # No GSSAPI in play == nothing to do + if not gssapi_requested: + return + # Obtain the correct host first - did user request a GSS-specific name + # to use that is distinct from the actual SSH target hostname? + if gss_host is None: + gss_host = self.hostname + # Finally, canonicalize via DNS if DNS is trusted. + if trust_dns and gss_host is not None: + gss_host = socket.getfqdn(gss_host) + # And set attribute for reference later. + self.gss_host = gss_host + + def start_client(self, event=None, timeout=None): + print("start_client") + """ + Negotiate a new SSH2 session as a client. This is the first step after + creating a new `.Transport`. A separate thread is created for protocol + negotiation. + + If an event is passed in, this method returns immediately. When + negotiation is done (successful or not), the given ``Event`` will + be triggered. On failure, `is_active` will return ``False``. + + (Since 1.4) If ``event`` is ``None``, this method will not return until + negotiation is done. On success, the method returns normally. + Otherwise an SSHException is raised. + + After a successful negotiation, you will usually want to authenticate, + calling `auth_password <Transport.auth_password>` or + `auth_publickey <Transport.auth_publickey>`. + + .. note:: `connect` is a simpler method for connecting as a client. + + .. note:: + After calling this method (or `start_server` or `connect`), you + should no longer directly read from or write to the original socket + object. + + :param .threading.Event event: + an event to trigger when negotiation is complete (optional) + + :param float timeout: + a timeout, in seconds, for SSH2 session negotiation (optional) + + :raises: + `.SSHException` -- if negotiation fails (and no ``event`` was + passed in) + """ + self.active = True + if event is not None: + # async, return immediately and let the app poll for completion + self.completion_event = event + self.start() + return + + # synchronous, wait for a result + self.completion_event = event = threading.Event() + self.start() + max_time = time.time() + timeout if timeout is not None else None + while True: + event.wait(0.1) + if not self.active: + e = self.get_exception() + if e is not None: + raise e + raise SSHException("Negotiation failed.") + if event.is_set() or ( + timeout is not None and time.time() >= max_time + ): + break + + def start_server(self, event=None, server=None): + print("start_server") + """ + Negotiate a new SSH2 session as a server. This is the first step after + creating a new `.Transport` and setting up your server host key(s). A + separate thread is created for protocol negotiation. + + If an event is passed in, this method returns immediately. When + negotiation is done (successful or not), the given ``Event`` will + be triggered. On failure, `is_active` will return ``False``. + + (Since 1.4) If ``event`` is ``None``, this method will not return until + negotiation is done. On success, the method returns normally. + Otherwise an SSHException is raised. + + After a successful negotiation, the client will need to authenticate. + Override the methods `get_allowed_auths + <.ServerInterface.get_allowed_auths>`, `check_auth_none + <.ServerInterface.check_auth_none>`, `check_auth_password + <.ServerInterface.check_auth_password>`, and `check_auth_publickey + <.ServerInterface.check_auth_publickey>` in the given ``server`` object + to control the authentication process. + + After a successful authentication, the client should request to open a + channel. Override `check_channel_request + <.ServerInterface.check_channel_request>` in the given ``server`` + object to allow channels to be opened. + + .. note:: + After calling this method (or `start_client` or `connect`), you + should no longer directly read from or write to the original socket + object. + + :param .threading.Event event: + an event to trigger when negotiation is complete. + :param .ServerInterface server: + an object used to perform authentication and create `channels + <.Channel>` + + :raises: + `.SSHException` -- if negotiation fails (and no ``event`` was + passed in) + """ + if server is None: + server = ServerInterface() + self.server_mode = True + self.server_object = server + self.active = True + if event is not None: + # async, return immediately and let the app poll for completion + self.completion_event = event + self.start() + return + + # synchronous, wait for a result + self.completion_event = event = threading.Event() + self.start() + while True: + event.wait(0.1) + if not self.active: + e = self.get_exception() + if e is not None: + raise e + raise SSHException("Negotiation failed.") + if event.is_set(): + break + + def add_server_key(self, key): + print("add_server_key") + """ + Add a host key to the list of keys used for server mode. When behaving + as a server, the host key is used to sign certain packets during the + SSH2 negotiation, so that the client can trust that we are who we say + we are. Because this is used for signing, the key must contain private + key info, not just the public half. Only one key of each type (RSA or + DSS) is kept. + + :param .PKey key: + the host key to add, usually an `.RSAKey` or `.DSSKey`. + """ + self.server_key_dict[key.get_name()] = key + # Handle SHA-2 extensions for RSA by ensuring that lookups into + # self.server_key_dict will yield this key for any of the algorithm + # names. + if isinstance(key, RSAKey): + self.server_key_dict["rsa-sha2-256"] = key + self.server_key_dict["rsa-sha2-512"] = key + + def get_server_key(self): + print("get_server_key") + """ + Return the active host key, in server mode. After negotiating with the + client, this method will return the negotiated host key. If only one + type of host key was set with `add_server_key`, that's the only key + that will ever be returned. But in cases where you have set more than + one type of host key (for example, an RSA key and a DSS key), the key + type will be negotiated by the client, and this method will return the + key of the type agreed on. If the host key has not been negotiated + yet, ``None`` is returned. In client mode, the behavior is undefined. + + :return: + host key (`.PKey`) of the type negotiated by the client, or + ``None``. + """ + try: + return self.server_key_dict[self.host_key_type] + except KeyError: + pass + return None + + @staticmethod + def load_server_moduli(filename=None): + print("load_server_moduli") + """ + (optional) + Load a file of prime moduli for use in doing group-exchange key + negotiation in server mode. It's a rather obscure option and can be + safely ignored. + + In server mode, the remote client may request "group-exchange" key + negotiation, which asks the server to send a random prime number that + fits certain criteria. These primes are pretty difficult to compute, + so they can't be generated on demand. But many systems contain a file + of suitable primes (usually named something like ``/etc/ssh/moduli``). + If you call `load_server_moduli` and it returns ``True``, then this + file of primes has been loaded and we will support "group-exchange" in + server mode. Otherwise server mode will just claim that it doesn't + support that method of key negotiation. + + :param str filename: + optional path to the moduli file, if you happen to know that it's + not in a standard location. + :return: + True if a moduli file was successfully loaded; False otherwise. + + .. note:: This has no effect when used in client mode. + """ + Transport._modulus_pack = ModulusPack() + # places to look for the openssh "moduli" file + file_list = ["/etc/ssh/moduli", "/usr/local/etc/moduli"] + if filename is not None: + file_list.insert(0, filename) + for fn in file_list: + try: + Transport._modulus_pack.read_file(fn) + return True + except IOError: + pass + # none succeeded + Transport._modulus_pack = None + return False + + def close(self): + print("close") + """ + Close this session, and any open channels that are tied to it. + """ + if not self.active: + return + self.stop_thread() + for chan in list(self._channels.values()): + chan._unlink() + self.sock.close() + + def get_remote_server_key(self): + print("get_remote_server_key") + """ + Return the host key of the server (in client mode). + + .. note:: + Previously this call returned a tuple of ``(key type, key + string)``. You can get the same effect by calling `.PKey.get_name` + for the key type, and ``str(key)`` for the key string. + + :raises: `.SSHException` -- if no session is currently active. + + :return: public key (`.PKey`) of the remote server + """ + if (not self.active) or (not self.initial_kex_done): + raise SSHException("No existing session") + return self.host_key + + def is_active(self): + print("is_active") + """ + Return true if this session is active (open). + + :return: + True if the session is still active (open); False if the session is + closed + """ + return self.active + + def open_session( + self, window_size=None, max_packet_size=None, timeout=None + ): + """ + Request a new channel to the server, of type ``"session"``. This is + just an alias for calling `open_channel` with an argument of + ``"session"``. + + .. note:: Modifying the the window and packet sizes might have adverse + effects on the session created. The default values are the same + as in the OpenSSH code base and have been battle tested. + + :param int window_size: + optional window size for this session. + :param int max_packet_size: + optional max packet size for this session. + + :return: a new `.Channel` + + :raises: + `.SSHException` -- if the request is rejected or the session ends + prematurely + + .. versionchanged:: 1.13.4/1.14.3/1.15.3 + Added the ``timeout`` argument. + .. versionchanged:: 1.15 + Added the ``window_size`` and ``max_packet_size`` arguments. + """ + return self.open_channel( + "session", + window_size=window_size, + max_packet_size=max_packet_size, + timeout=timeout, + ) + + def open_x11_channel(self, src_addr=None): + print("open_x11_channel") + """ + Request a new channel to the client, of type ``"x11"``. This + is just an alias for ``open_channel('x11', src_addr=src_addr)``. + + :param tuple src_addr: + the source address (``(str, int)``) of the x11 server (port is the + x11 port, ie. 6010) + :return: a new `.Channel` + + :raises: + `.SSHException` -- if the request is rejected or the session ends + prematurely + """ + return self.open_channel("x11", src_addr=src_addr) + + def open_forward_agent_channel(self): + print("open_forward_agent_channel") + """ + Request a new channel to the client, of type + ``"auth-agent@openssh.com"``. + + This is just an alias for ``open_channel('auth-agent@openssh.com')``. + + :return: a new `.Channel` + + :raises: `.SSHException` -- + if the request is rejected or the session ends prematurely + """ + return self.open_channel("auth-agent@openssh.com") + + def open_forwarded_tcpip_channel(self, src_addr, dest_addr): + print("open_forwarded_tcpip_channel") + """ + Request a new channel back to the client, of type ``forwarded-tcpip``. + + This is used after a client has requested port forwarding, for sending + incoming connections back to the client. + + :param src_addr: originator's address + :param dest_addr: local (server) connected address + """ + return self.open_channel("forwarded-tcpip", dest_addr, src_addr) + + def open_channel( + self, + kind, + dest_addr=None, + src_addr=None, + window_size=None, + max_packet_size=None, + timeout=None, + ): + """ + Request a new channel to the server. `Channels <.Channel>` are + socket-like objects used for the actual transfer of data across the + session. You may only request a channel after negotiating encryption + (using `connect` or `start_client`) and authenticating. + + .. note:: Modifying the the window and packet sizes might have adverse + effects on the channel created. The default values are the same + as in the OpenSSH code base and have been battle tested. + + :param str kind: + the kind of channel requested (usually ``"session"``, + ``"forwarded-tcpip"``, ``"direct-tcpip"``, or ``"x11"``) + :param tuple dest_addr: + the destination address (address + port tuple) of this port + forwarding, if ``kind`` is ``"forwarded-tcpip"`` or + ``"direct-tcpip"`` (ignored for other channel types) + :param src_addr: the source address of this port forwarding, if + ``kind`` is ``"forwarded-tcpip"``, ``"direct-tcpip"``, or ``"x11"`` + :param int window_size: + optional window size for this session. + :param int max_packet_size: + optional max packet size for this session. + :param float timeout: + optional timeout opening a channel, default 3600s (1h) + + :return: a new `.Channel` on success + + :raises: + `.SSHException` -- if the request is rejected, the session ends + prematurely or there is a timeout opening a channel + + .. versionchanged:: 1.15 + Added the ``window_size`` and ``max_packet_size`` arguments. + """ + if not self.active: + raise SSHException("SSH session not active") + timeout = self.channel_timeout if timeout is None else timeout + self.lock.acquire() + try: + window_size = self._sanitize_window_size(window_size) + max_packet_size = self._sanitize_packet_size(max_packet_size) + chanid = self._next_channel() + m = Message() + m.add_byte(cMSG_CHANNEL_OPEN) + m.add_string(kind) + m.add_int(chanid) + m.add_int(window_size) + m.add_int(max_packet_size) + if (kind == "forwarded-tcpip") or (kind == "direct-tcpip"): + m.add_string(dest_addr[0]) + m.add_int(dest_addr[1]) + m.add_string(src_addr[0]) + m.add_int(src_addr[1]) + elif kind == "x11": + m.add_string(src_addr[0]) + m.add_int(src_addr[1]) + chan = Channel(chanid) + self._channels.put(chanid, chan) + self.channel_events[chanid] = event = threading.Event() + self.channels_seen[chanid] = True + chan._set_transport(self) + chan._set_window(window_size, max_packet_size) + finally: + self.lock.release() + self._send_user_message(m) + start_ts = time.time() + while True: + event.wait(0.1) + if not self.active: + e = self.get_exception() + if e is None: + e = SSHException("Unable to open channel.") + raise e + if event.is_set(): + break + elif start_ts + timeout < time.time(): + raise SSHException("Timeout opening channel.") + chan = self._channels.get(chanid) + if chan is not None: + return chan + e = self.get_exception() + if e is None: + e = SSHException("Unable to open channel.") + raise e + + def request_port_forward(self, address, port, handler=None): + print("request_port_forward") + """ + Ask the server to forward TCP connections from a listening port on + the server, across this SSH session. + + If a handler is given, that handler is called from a different thread + whenever a forwarded connection arrives. The handler parameters are:: + + handler( + channel, + (origin_addr, origin_port), + (server_addr, server_port), + ) + + where ``server_addr`` and ``server_port`` are the address and port that + the server was listening on. + + If no handler is set, the default behavior is to send new incoming + forwarded connections into the accept queue, to be picked up via + `accept`. + + :param str address: the address to bind when forwarding + :param int port: + the port to forward, or 0 to ask the server to allocate any port + :param callable handler: + optional handler for incoming forwarded connections, of the form + ``func(Channel, (str, int), (str, int))``. + + :return: the port number (`int`) allocated by the server + + :raises: + `.SSHException` -- if the server refused the TCP forward request + """ + if not self.active: + raise SSHException("SSH session not active") + port = int(port) + response = self.global_request( + "tcpip-forward", (address, port), wait=True + ) + if response is None: + raise SSHException("TCP forwarding request denied") + if port == 0: + port = response.get_int() + if handler is None: + + def default_handler(channel, src_addr, dest_addr_port): + # src_addr, src_port = src_addr_port + # dest_addr, dest_port = dest_addr_port + self._queue_incoming_channel(channel) + + handler = default_handler + self._tcp_handler = handler + return port + + def cancel_port_forward(self, address, port): + print("cancel_port_forward") + """ + Ask the server to cancel a previous port-forwarding request. No more + connections to the given address & port will be forwarded across this + ssh connection. + + :param str address: the address to stop forwarding + :param int port: the port to stop forwarding + """ + if not self.active: + return + self._tcp_handler = None + self.global_request("cancel-tcpip-forward", (address, port), wait=True) + + def open_sftp_client(self): + print("open_sftp_client") + """ + Create an SFTP client channel from an open transport. On success, an + SFTP session will be opened with the remote host, and a new + `.SFTPClient` object will be returned. + + :return: + a new `.SFTPClient` referring to an sftp session (channel) across + this transport + """ + return SFTPClient.from_transport(self) + + def send_ignore(self, byte_count=None): + print("send_ignore") + """ + Send a junk packet across the encrypted link. This is sometimes used + to add "noise" to a connection to confuse would-be attackers. It can + also be used as a keep-alive for long lived connections traversing + firewalls. + + :param int byte_count: + the number of random bytes to send in the payload of the ignored + packet -- defaults to a random number from 10 to 41. + """ + m = Message() + m.add_byte(cMSG_IGNORE) + if byte_count is None: + byte_count = (byte_ord(os.urandom(1)) % 32) + 10 + m.add_bytes(os.urandom(byte_count)) + self._send_user_message(m) + + def renegotiate_keys(self): + print("renegotiate_keys") + """ + Force this session to switch to new keys. Normally this is done + automatically after the session hits a certain number of packets or + bytes sent or received, but this method gives you the option of forcing + new keys whenever you want. Negotiating new keys causes a pause in + traffic both ways as the two sides swap keys and do computations. This + method returns when the session has switched to new keys. + + :raises: + `.SSHException` -- if the key renegotiation failed (which causes + the session to end) + """ + self.completion_event = threading.Event() + self._send_kex_init() + while True: + self.completion_event.wait(0.1) + if not self.active: + e = self.get_exception() + if e is not None: + raise e + raise SSHException("Negotiation failed.") + if self.completion_event.is_set(): + break + return + + def set_keepalive(self, interval): + print("set_keepalive") + """ + Turn on/off keepalive packets (default is off). If this is set, after + ``interval`` seconds without sending any data over the connection, a + "keepalive" packet will be sent (and ignored by the remote host). This + can be useful to keep connections alive over a NAT, for example. + + :param int interval: + seconds to wait before sending a keepalive packet (or + 0 to disable keepalives). + """ + + def _request(x=weakref.proxy(self)): + return x.global_request("keepalive@lag.net", wait=False) + + self.packetizer.set_keepalive(interval, _request) + + def global_request(self, kind, data=None, wait=True): + + print("global_request") + """ + Make a global request to the remote host. These are normally + extensions to the SSH2 protocol. + + :param str kind: name of the request. + :param tuple data: + an optional tuple containing additional data to attach to the + request. + :param bool wait: + ``True`` if this method should not return until a response is + received; ``False`` otherwise. + :return: + a `.Message` containing possible additional data if the request was + successful (or an empty `.Message` if ``wait`` was ``False``); + ``None`` if the request was denied. + """ + if wait: + self.completion_event = threading.Event() + m = Message() + m.add_byte(cMSG_GLOBAL_REQUEST) + m.add_string(kind) + m.add_boolean(wait) + if data is not None: + m.add(*data) + self._log(DEBUG, 'Sending global request "{}"'.format(kind)) + self._send_user_message(m) + if not wait: + return None + while True: + self.completion_event.wait(0.1) + if not self.active: + return None + if self.completion_event.is_set(): + break + return self.global_response + + def accept(self, timeout=None): + print("accept") + """ + Return the next channel opened by the client over this transport, in + server mode. If no channel is opened before the given timeout, + ``None`` is returned. + + :param int timeout: + seconds to wait for a channel, or ``None`` to wait forever + :return: a new `.Channel` opened by the client + """ + + + self.lock.acquire() + try: + if len(self.server_accepts) > 0: + chan = self.server_accepts.pop(0) + else: + self.server_accept_cv.wait(timeout) + if len(self.server_accepts) > 0: + chan = self.server_accepts.pop(0) + else: + # timeout + chan = None + finally: + self.lock.release() + return chan + + def connect( + self, + hostkey=None, + username="", + password=None, + pkey=None, + gss_host=None, + gss_auth=False, + gss_kex=False, + gss_deleg_creds=True, + gss_trust_dns=True, + ): + print("Connect") + """ + Negotiate an SSH2 session, and optionally verify the server's host key + and authenticate using a password or private key. This is a shortcut + for `start_client`, `get_remote_server_key`, and + `Transport.auth_password` or `Transport.auth_publickey`. Use those + methods if you want more control. + + You can use this method immediately after creating a Transport to + negotiate encryption with a server. If it fails, an exception will be + thrown. On success, the method will return cleanly, and an encrypted + session exists. You may immediately call `open_channel` or + `open_session` to get a `.Channel` object, which is used for data + transfer. + + .. note:: + If you fail to supply a password or private key, this method may + succeed, but a subsequent `open_channel` or `open_session` call may + fail because you haven't authenticated yet. + + :param .PKey hostkey: + the host key expected from the server, or ``None`` if you don't + want to do host key verification. + :param str username: the username to authenticate as. + :param str password: + a password to use for authentication, if you want to use password + authentication; otherwise ``None``. + :param .PKey pkey: + a private key to use for authentication, if you want to use private + key authentication; otherwise ``None``. + :param str gss_host: + The target's name in the kerberos database. Default: hostname + :param bool gss_auth: + ``True`` if you want to use GSS-API authentication. + :param bool gss_kex: + Perform GSS-API Key Exchange and user authentication. + :param bool gss_deleg_creds: + Whether to delegate GSS-API client credentials. + :param gss_trust_dns: + Indicates whether or not the DNS is trusted to securely + canonicalize the name of the host being connected to (default + ``True``). + + :raises: `.SSHException` -- if the SSH2 negotiation fails, the host key + supplied by the server is incorrect, or authentication fails. + + .. versionchanged:: 2.3 + Added the ``gss_trust_dns`` argument. + """ + if hostkey is not None: + # TODO: a more robust implementation would be to ask each key class + # for its nameS plural, and just use that. + # TODO: that could be used in a bunch of other spots too + if isinstance(hostkey, RSAKey): + self._preferred_keys = [ + "rsa-sha2-512", + "rsa-sha2-256", + "ssh-rsa", + ] + else: + self._preferred_keys = [hostkey.get_name()] + + self.set_gss_host( + gss_host=gss_host, + trust_dns=gss_trust_dns, + gssapi_requested=gss_kex or gss_auth, + ) + + self.start_client() + + # check host key if we were given one + # If GSS-API Key Exchange was performed, we are not required to check + # the host key. + if (hostkey is not None) and not gss_kex: + key = self.get_remote_server_key() + if ( + key.get_name() != hostkey.get_name() + or key.asbytes() != hostkey.asbytes() + ): + self._log(DEBUG, "Bad host key from server") + self._log( + DEBUG, + "Expected: {}: {}".format( + hostkey.get_name(), repr(hostkey.asbytes()) + ), + ) + self._log( + DEBUG, + "Got : {}: {}".format( + key.get_name(), repr(key.asbytes()) + ), + ) + raise SSHException("Bad host key from server") + self._log( + DEBUG, "Host key verified ({})".format(hostkey.get_name()) + ) + + if (pkey is not None) or (password is not None) or gss_auth or gss_kex: + if gss_auth: + self._log( + DEBUG, "Attempting GSS-API auth... (gssapi-with-mic)" + ) # noqa + self.auth_gssapi_with_mic( + username, self.gss_host, gss_deleg_creds + ) + elif gss_kex: + self._log(DEBUG, "Attempting GSS-API auth... (gssapi-keyex)") + self.auth_gssapi_keyex(username) + elif pkey is not None: + self._log(DEBUG, "Attempting public-key auth...") + self.auth_publickey(username, pkey) + else: + self._log(DEBUG, "Attempting password auth...") + self.auth_password(username, password) + + return + + def get_exception(self): + print("Get Exception") + """ + Return any exception that happened during the last server request. + This can be used to fetch more specific error information after using + calls like `start_client`. The exception (if any) is cleared after + this call. + + :return: + an exception, or ``None`` if there is no stored exception. + + .. versionadded:: 1.1 + """ + self.lock.acquire() + try: + e = self.saved_exception + self.saved_exception = None + return e + finally: + self.lock.release() + + def set_subsystem_handler(self, name, handler, *args, **kwargs): + print("Set Subsystem Handler") + """ + Set the handler class for a subsystem in server mode. If a request + for this subsystem is made on an open ssh channel later, this handler + will be constructed and called -- see `.SubsystemHandler` for more + detailed documentation. + + Any extra parameters (including keyword arguments) are saved and + passed to the `.SubsystemHandler` constructor later. + + :param str name: name of the subsystem. + :param handler: + subclass of `.SubsystemHandler` that handles this subsystem. + """ + try: + self.lock.acquire() + self.subsystem_table[name] = (handler, args, kwargs) + finally: + self.lock.release() + + def is_authenticated(self): + print("Is Authenticated") + """ + Return true if this session is active and authenticated. + + :return: + True if the session is still open and has been authenticated + successfully; False if authentication failed and/or the session is + closed. + """ + return ( + self.active + and self.auth_handler is not None + and self.auth_handler.is_authenticated() + ) + + def get_username(self): + print("Get Username") + """ + Return the username this connection is authenticated for. If the + session is not authenticated (or authentication failed), this method + returns ``None``. + + :return: username that was authenticated (a `str`), or ``None``. + """ + if not self.active or (self.auth_handler is None): + return None + return self.auth_handler.get_username() + + def get_banner(self): + print("Get Banner") + """ + Return the banner supplied by the server upon connect. If no banner is + supplied, this method returns ``None``. + + :returns: server supplied banner (`str`), or ``None``. + + .. versionadded:: 1.13 + """ + if not self.active or (self.auth_handler is None): + return None + return self.auth_handler.banner + + def auth_none(self, username): + print("Auth None") + """ + Try to authenticate to the server using no authentication at all. + This will almost always fail. It may be useful for determining the + list of authentication types supported by the server, by catching the + `.BadAuthenticationType` exception raised. + + :param str username: the username to authenticate as + :return: + list of auth types permissible for the next stage of + authentication (normally empty) + + :raises: + `.BadAuthenticationType` -- if "none" authentication isn't allowed + by the server for this user + :raises: + `.SSHException` -- if the authentication failed due to a network + error + + .. versionadded:: 1.5 + """ + if (not self.active) or (not self.initial_kex_done): + raise SSHException("No existing session") + my_event = threading.Event() + self.auth_handler = AuthHandler(self) + self.auth_handler.auth_none(username, my_event) + return self.auth_handler.wait_for_response(my_event) + + def auth_password(self, username, password, event=None, fallback=True): + print("Auth Password") + """ + Authenticate to the server using a password. The username and password + are sent over an encrypted link. + + If an ``event`` is passed in, this method will return immediately, and + the event will be triggered once authentication succeeds or fails. On + success, `is_authenticated` will return ``True``. On failure, you may + use `get_exception` to get more detailed error information. + + Since 1.1, if no event is passed, this method will block until the + authentication succeeds or fails. On failure, an exception is raised. + Otherwise, the method simply returns. + + Since 1.5, if no event is passed and ``fallback`` is ``True`` (the + default), if the server doesn't support plain password authentication + but does support so-called "keyboard-interactive" mode, an attempt + will be made to authenticate using this interactive mode. If it fails, + the normal exception will be thrown as if the attempt had never been + made. This is useful for some recent Gentoo and Debian distributions, + which turn off plain password authentication in a misguided belief + that interactive authentication is "more secure". (It's not.) + + If the server requires multi-step authentication (which is very rare), + this method will return a list of auth types permissible for the next + step. Otherwise, in the normal case, an empty list is returned. + + :param str username: the username to authenticate as + :param basestring password: the password to authenticate with + :param .threading.Event event: + an event to trigger when the authentication attempt is complete + (whether it was successful or not) + :param bool fallback: + ``True`` if an attempt at an automated "interactive" password auth + should be made if the server doesn't support normal password auth + :return: + list of auth types permissible for the next stage of + authentication (normally empty) + + :raises: + `.BadAuthenticationType` -- if password authentication isn't + allowed by the server for this user (and no event was passed in) + :raises: + `.AuthenticationException` -- if the authentication failed (and no + event was passed in) + :raises: `.SSHException` -- if there was a network error + """ + if (not self.active) or (not self.initial_kex_done): + # we should never try to send the password unless we're on a secure + # link + raise SSHException("No existing session") + if event is None: + my_event = threading.Event() + else: + my_event = event + self.auth_handler = AuthHandler(self) + self.auth_handler.auth_password(username, password, my_event) + if event is not None: + # caller wants to wait for event themselves + return [] + try: + return self.auth_handler.wait_for_response(my_event) + except BadAuthenticationType as e: + # if password auth isn't allowed, but keyboard-interactive *is*, + # try to fudge it + if not fallback or ("keyboard-interactive" not in e.allowed_types): + raise + try: + + def handler(title, instructions, fields): + if len(fields) > 1: + raise SSHException("Fallback authentication failed.") + if len(fields) == 0: + # for some reason, at least on os x, a 2nd request will + # be made with zero fields requested. maybe it's just + # to try to fake out automated scripting of the exact + # type we're doing here. *shrug* :) + return [] + return [password] + + return self.auth_interactive(username, handler) + except SSHException: + # attempt failed; just raise the original exception + raise e + + def auth_publickey(self, username, key, event=None): + print("Auth Public Key") + """ + Authenticate to the server using a private key. The key is used to + sign data from the server, so it must include the private part. + + If an ``event`` is passed in, this method will return immediately, and + the event will be triggered once authentication succeeds or fails. On + success, `is_authenticated` will return ``True``. On failure, you may + use `get_exception` to get more detailed error information. + + Since 1.1, if no event is passed, this method will block until the + authentication succeeds or fails. On failure, an exception is raised. + Otherwise, the method simply returns. + + If the server requires multi-step authentication (which is very rare), + this method will return a list of auth types permissible for the next + step. Otherwise, in the normal case, an empty list is returned. + + :param str username: the username to authenticate as + :param .PKey key: the private key to authenticate with + :param .threading.Event event: + an event to trigger when the authentication attempt is complete + (whether it was successful or not) + :return: + list of auth types permissible for the next stage of + authentication (normally empty) + + :raises: + `.BadAuthenticationType` -- if public-key authentication isn't + allowed by the server for this user (and no event was passed in) + :raises: + `.AuthenticationException` -- if the authentication failed (and no + event was passed in) + :raises: `.SSHException` -- if there was a network error + """ + + print("auth_publickeyyyyyy") + + if (not self.active) or (not self.initial_kex_done): + # we should never try to authenticate unless we're on a secure link + raise SSHException("No existing session") + if event is None: + my_event = threading.Event() + else: + my_event = event + self.auth_handler = AuthHandler(self) + self.auth_handler.auth_publickey(username, key, my_event) + if event is not None: + # caller wants to wait for event themselves + return [] + return self.auth_handler.wait_for_response(my_event) + + def auth_interactive(self, username, handler, submethods=""): + print("Auth Interactive") + """ + Authenticate to the server interactively. A handler is used to answer + arbitrary questions from the server. On many servers, this is just a + dumb wrapper around PAM. + + This method will block until the authentication succeeds or fails, + peroidically calling the handler asynchronously to get answers to + authentication questions. The handler may be called more than once + if the server continues to ask questions. + + The handler is expected to be a callable that will handle calls of the + form: ``handler(title, instructions, prompt_list)``. The ``title`` is + meant to be a dialog-window title, and the ``instructions`` are user + instructions (both are strings). ``prompt_list`` will be a list of + prompts, each prompt being a tuple of ``(str, bool)``. The string is + the prompt and the boolean indicates whether the user text should be + echoed. + + A sample call would thus be: + ``handler('title', 'instructions', [('Password:', False)])``. + + The handler should return a list or tuple of answers to the server's + questions. + + If the server requires multi-step authentication (which is very rare), + this method will return a list of auth types permissible for the next + step. Otherwise, in the normal case, an empty list is returned. + + :param str username: the username to authenticate as + :param callable handler: a handler for responding to server questions + :param str submethods: a string list of desired submethods (optional) + :return: + list of auth types permissible for the next stage of + authentication (normally empty). + + :raises: `.BadAuthenticationType` -- if public-key authentication isn't + allowed by the server for this user + :raises: `.AuthenticationException` -- if the authentication failed + :raises: `.SSHException` -- if there was a network error + + .. versionadded:: 1.5 + """ + if (not self.active) or (not self.initial_kex_done): + # we should never try to authenticate unless we're on a secure link + raise SSHException("No existing session") + my_event = threading.Event() + self.auth_handler = AuthHandler(self) + self.auth_handler.auth_interactive( + username, handler, my_event, submethods + ) + return self.auth_handler.wait_for_response(my_event) + + def auth_interactive_dumb(self, username, handler=None, submethods=""): + print("Auth Interactive Dumb") + """ + Authenticate to the server interactively but dumber. + Just print the prompt and / or instructions to stdout and send back + the response. This is good for situations where partial auth is + achieved by key and then the user has to enter a 2fac token. + """ + + if not handler: + + def handler(title, instructions, prompt_list): + answers = [] + if title: + print(title.strip()) + if instructions: + print(instructions.strip()) + for prompt, show_input in prompt_list: + print(prompt.strip(), end=" ") + answers.append(input()) + return answers + + return self.auth_interactive(username, handler, submethods) + + def auth_gssapi_with_mic(self, username, gss_host, gss_deleg_creds): + print("Auth GSSAPI with mic") + """ + Authenticate to the Server using GSS-API / SSPI. + + :param str username: The username to authenticate as + :param str gss_host: The target host + :param bool gss_deleg_creds: Delegate credentials or not + :return: list of auth types permissible for the next stage of + authentication (normally empty) + :raises: `.BadAuthenticationType` -- if gssapi-with-mic isn't + allowed by the server (and no event was passed in) + :raises: + `.AuthenticationException` -- if the authentication failed (and no + event was passed in) + :raises: `.SSHException` -- if there was a network error + """ + if (not self.active) or (not self.initial_kex_done): + # we should never try to authenticate unless we're on a secure link + raise SSHException("No existing session") + my_event = threading.Event() + self.auth_handler = AuthHandler(self) + self.auth_handler.auth_gssapi_with_mic( + username, gss_host, gss_deleg_creds, my_event + ) + return self.auth_handler.wait_for_response(my_event) + + def auth_gssapi_keyex(self, username): + print("Auth GSSAPI keyex") + """ + Authenticate to the server with GSS-API/SSPI if GSS-API kex is in use. + + :param str username: The username to authenticate as. + :returns: + a list of auth types permissible for the next stage of + authentication (normally empty) + :raises: `.BadAuthenticationType` -- + if GSS-API Key Exchange was not performed (and no event was passed + in) + :raises: `.AuthenticationException` -- + if the authentication failed (and no event was passed in) + :raises: `.SSHException` -- if there was a network error + """ + if (not self.active) or (not self.initial_kex_done): + # we should never try to authenticate unless we're on a secure link + raise SSHException("No existing session") + my_event = threading.Event() + self.auth_handler = AuthHandler(self) + self.auth_handler.auth_gssapi_keyex(username, my_event) + return self.auth_handler.wait_for_response(my_event) + + def set_log_channel(self, name): + print("Set log channel") + """ + Set the channel for this transport's logging. The default is + ``"paramiko.transport"`` but it can be set to anything you want. (See + the `.logging` module for more info.) SSH Channels will log to a + sub-channel of the one specified. + + :param str name: new channel name for logging + + .. versionadded:: 1.1 + """ + self.log_name = name + self.logger = util.get_logger(name) + self.packetizer.set_log(self.logger) + + def get_log_channel(self): + print("Get log channel") + """ + Return the channel name used for this transport's logging. + + :return: channel name as a `str` + + .. versionadded:: 1.2 + """ + return self.log_name + + def set_hexdump(self, hexdump): + print("Set hexdump") + """ + Turn on/off logging a hex dump of protocol traffic at DEBUG level in + the logs. Normally you would want this off (which is the default), + but if you are debugging something, it may be useful. + + :param bool hexdump: + ``True`` to log protocol traffix (in hex) to the log; ``False`` + otherwise. + """ + self.packetizer.set_hexdump(hexdump) + + def get_hexdump(self): + print("Get hexdump") + """ + Return ``True`` if the transport is currently logging hex dumps of + protocol traffic. + + :return: ``True`` if hex dumps are being logged, else ``False``. + + .. versionadded:: 1.4 + """ + return self.packetizer.get_hexdump() + + def use_compression(self, compress=True): + print("Use compression") + """ + Turn on/off compression. This will only have an affect before starting + the transport (ie before calling `connect`, etc). By default, + compression is off since it negatively affects interactive sessions. + + :param bool compress: + ``True`` to ask the remote client/server to compress traffic; + ``False`` to refuse compression + + .. versionadded:: 1.5.2 + """ + if compress: + self._preferred_compression = ("zlib@openssh.com", "zlib", "none") + else: + self._preferred_compression = ("none",) + + def getpeername(self): + print("Get peer name") + """ + Return the address of the remote side of this Transport, if possible. + + This is effectively a wrapper around ``getpeername`` on the underlying + socket. If the socket-like object has no ``getpeername`` method, then + ``("unknown", 0)`` is returned. + + :return: + the address of the remote host, if known, as a ``(str, int)`` + tuple. + """ + gp = getattr(self.sock, "getpeername", None) + if gp is None: + return "unknown", 0 + return gp() + + def stop_thread(self): + print("Stop thread") + self.active = False + self.packetizer.close() + # Keep trying to join() our main thread, quickly, until: + # * We join()ed successfully (self.is_alive() == False) + # * Or it looks like we've hit issue #520 (socket.recv hitting some + # race condition preventing it from timing out correctly), wherein + # our socket and packetizer are both closed (but where we'd + # otherwise be sitting forever on that recv()). + while ( + self.is_alive() + and self is not threading.current_thread() + and not self.sock._closed + and not self.packetizer.closed + ): + self.join(0.1) + + # internals... + + def _log(self, level, msg, *args): + if issubclass(type(msg), list): + for m in msg: + self.logger.log(level, m) + else: + self.logger.log(level, msg, *args) + + def _get_modulus_pack(self): + """used by KexGex to find primes for group exchange""" + return self._modulus_pack + + def _next_channel(self): + """you are holding the lock""" + chanid = self._channel_counter + while self._channels.get(chanid) is not None: + self._channel_counter = (self._channel_counter + 1) & 0xFFFFFF + chanid = self._channel_counter + self._channel_counter = (self._channel_counter + 1) & 0xFFFFFF + return chanid + + def _unlink_channel(self, chanid): + """used by a Channel to remove itself from the active channel list""" + self._channels.delete(chanid) + + def _send_message(self, data): + print("send mesage") + self.packetizer.send_message(data) + + def _send_user_message(self, data): + print("send user message") + """ + send a message, but block if we're in key negotiation. this is used + for user-initiated requests. + """ + start = time.time() + while True: + self.clear_to_send.wait(0.1) + if not self.active: + self._log( + DEBUG, "Dropping user packet because connection is dead." + ) # noqa + return + self.clear_to_send_lock.acquire() + if self.clear_to_send.is_set(): + break + self.clear_to_send_lock.release() + if time.time() > start + self.clear_to_send_timeout: + raise SSHException( + "Key-exchange timed out waiting for key negotiation" + ) # noqa + try: + self._send_message(data) + finally: + self.clear_to_send_lock.release() + + def _set_K_H(self, k, h): + print("set K H") + """ + Used by a kex obj to set the K (root key) and H (exchange hash). + """ + self.K = k + self.H = h + if self.session_id is None: + self.session_id = h + + def _expect_packet(self, *ptypes): + print("expect packet") + """ + Used by a kex obj to register the next packet type it expects to see. + """ + self._expected_packet = tuple(ptypes) + + def _verify_key(self, host_key, sig): + print("verify key") + key = self._key_info[self.host_key_type](Message(host_key)) + if key is None: + raise SSHException("Unknown host key type") + if not key.verify_ssh_sig(self.H, Message(sig)): + raise SSHException( + "Signature verification ({}) failed.".format( + self.host_key_type + ) + ) # noqa + self.host_key = key + + def _compute_key(self, id, nbytes): + print("compute key") + """id is 'A' - 'F' for the various keys used by ssh""" + m = Message() + m.add_mpint(self.K) + m.add_bytes(self.H) + m.add_byte(b(id)) + m.add_bytes(self.session_id) + # Fallback to SHA1 for kex engines that fail to specify a hex + # algorithm, or for e.g. transport tests that don't run kexinit. + hash_algo = getattr(self.kex_engine, "hash_algo", None) + hash_select_msg = "kex engine {} specified hash_algo {!r}".format( + self.kex_engine.__class__.__name__, hash_algo + ) + if hash_algo is None: + hash_algo = sha1 + hash_select_msg += ", falling back to sha1" + if not hasattr(self, "_logged_hash_selection"): + self._log(DEBUG, hash_select_msg) + setattr(self, "_logged_hash_selection", True) + out = sofar = hash_algo(m.asbytes()).digest() + while len(out) < nbytes: + m = Message() + m.add_mpint(self.K) + m.add_bytes(self.H) + m.add_bytes(sofar) + digest = hash_algo(m.asbytes()).digest() + out += digest + sofar += digest + return out[:nbytes] + + def _get_cipher(self, name, key, iv, operation): + print("get cipher") + if name not in self._cipher_info: + raise SSHException("Unknown client cipher " + name) + else: + cipher = Cipher( + self._cipher_info[name]["class"](key), + self._cipher_info[name]["mode"](iv), + backend=default_backend(), + ) + if operation is self._ENCRYPT: + return cipher.encryptor() + else: + return cipher.decryptor() + + def _set_forward_agent_handler(self, handler): + print("set forward agent handler") + if handler is None: + + def default_handler(channel): + self._queue_incoming_channel(channel) + + self._forward_agent_handler = default_handler + else: + self._forward_agent_handler = handler + + def _set_x11_handler(self, handler): + print("set x11 handler") + # only called if a channel has turned on x11 forwarding + if handler is None: + # by default, use the same mechanism as accept() + def default_handler(channel, src_addr_port): + self._queue_incoming_channel(channel) + + self._x11_handler = default_handler + else: + self._x11_handler = handler + + def _queue_incoming_channel(self, channel): + print("queue incoming channel") + self.lock.acquire() + try: + self.server_accepts.append(channel) + self.server_accept_cv.notify() + finally: + self.lock.release() + + def _sanitize_window_size(self, window_size): + print("sanitize window size") + if window_size is None: + window_size = self.default_window_size + return clamp_value(MIN_WINDOW_SIZE, window_size, MAX_WINDOW_SIZE) + + def _sanitize_packet_size(self, max_packet_size): + print("sanitize packet size") + if max_packet_size is None: + max_packet_size = self.default_max_packet_size + return clamp_value(MIN_PACKET_SIZE, max_packet_size, MAX_WINDOW_SIZE) + + def _ensure_authed(self, ptype, message): + print("ensure authed") + """ + Checks message type against current auth state. + + If server mode, and auth has not succeeded, and the message is of a + post-auth type (channel open or global request) an appropriate error + response Message is crafted and returned to caller for sending. + + Otherwise (client mode, authed, or pre-auth message) returns None. + """ + if ( + not self.server_mode + or ptype <= HIGHEST_USERAUTH_MESSAGE_ID + or self.is_authenticated() + ): + return None + # WELP. We must be dealing with someone trying to do non-auth things + # without being authed. Tell them off, based on message class. + reply = Message() + # Global requests have no details, just failure. + if ptype == MSG_GLOBAL_REQUEST: + reply.add_byte(cMSG_REQUEST_FAILURE) + # Channel opens let us reject w/ a specific type + message. + elif ptype == MSG_CHANNEL_OPEN: + kind = message.get_text() # noqa + chanid = message.get_int() + reply.add_byte(cMSG_CHANNEL_OPEN_FAILURE) + reply.add_int(chanid) + reply.add_int(OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED) + reply.add_string("") + reply.add_string("en") + # NOTE: Post-open channel messages do not need checking; the above will + # reject attempts to open channels, meaning that even if a malicious + # user tries to send a MSG_CHANNEL_REQUEST, it will simply fall under + # the logic that handles unknown channel IDs (as the channel list will + # be empty.) + return reply + + def run(self): + print("run") + # (use the exposed "run" method, because if we specify a thread target + # of a private method, threading.Thread will keep a reference to it + # indefinitely, creating a GC cycle and not letting Transport ever be + # GC'd. it's a bug in Thread.) + + # Hold reference to 'sys' so we can test sys.modules to detect + # interpreter shutdown. + self.sys = sys + + # active=True occurs before the thread is launched, to avoid a race + _active_threads.append(self) + tid = hex(id(self) & xffffffff) + if self.server_mode: + self._log(DEBUG, "starting thread (server mode): {}".format(tid)) + else: + self._log(DEBUG, "starting thread (client mode): {}".format(tid)) + try: + try: + self.packetizer.write_all(b(self.local_version + "\r\n")) + self._log( + DEBUG, + "Local version/idstring: {}".format(self.local_version), + ) # noqa + self._check_banner() + # The above is actually very much part of the handshake, but + # sometimes the banner can be read but the machine is not + # responding, for example when the remote ssh daemon is loaded + # in to memory but we can not read from the disk/spawn a new + # shell. + # Make sure we can specify a timeout for the initial handshake. + # Re-use the banner timeout for now. + self.packetizer.start_handshake(self.handshake_timeout) + self._send_kex_init() + self._expect_packet(MSG_KEXINIT) + + while self.active: + if self.packetizer.need_rekey() and not self.in_kex: + self._send_kex_init() + try: + ptype, m = self.packetizer.read_message() + except NeedRekeyException: + continue + if ptype == MSG_IGNORE: + continue + elif ptype == MSG_DISCONNECT: + self._parse_disconnect(m) + break + elif ptype == MSG_DEBUG: + self._parse_debug(m) + continue + if len(self._expected_packet) > 0: + if ptype not in self._expected_packet: + raise SSHException( + "Expecting packet from {!r}, got {:d}".format( + self._expected_packet, ptype + ) + ) # noqa + self._expected_packet = tuple() + if (ptype >= 30) and (ptype <= 41): + self.kex_engine.parse_next(ptype, m) + continue + + if ptype in self._handler_table: + error_msg = self._ensure_authed(ptype, m) + if error_msg: + self._send_message(error_msg) + else: + self._handler_table[ptype](self, m) + elif ptype in self._channel_handler_table: + chanid = m.get_int() + chan = self._channels.get(chanid) + if chan is not None: + self._channel_handler_table[ptype](chan, m) + elif chanid in self.channels_seen: + self._log( + DEBUG, + "Ignoring message for dead channel {:d}".format( # noqa + chanid + ), + ) + else: + self._log( + ERROR, + "Channel request for unknown channel {:d}".format( # noqa + chanid + ), + ) + break + elif ( + self.auth_handler is not None + and ptype in self.auth_handler._handler_table + ): + handler = self.auth_handler._handler_table[ptype] + handler(self.auth_handler, m) + if len(self._expected_packet) > 0: + continue + else: + # Respond with "I don't implement this particular + # message type" message (unless the message type was + # itself literally MSG_UNIMPLEMENTED, in which case, we + # just shut up to avoid causing a useless loop). + name = MSG_NAMES[ptype] + warning = "Oops, unhandled type {} ({!r})".format( + ptype, name + ) + self._log(WARNING, warning) + if ptype != MSG_UNIMPLEMENTED: + msg = Message() + msg.add_byte(cMSG_UNIMPLEMENTED) + msg.add_int(m.seqno) + self._send_message(msg) + self.packetizer.complete_handshake() + except SSHException as e: + self._log( + ERROR, + "Exception ({}): {}".format( + "server" if self.server_mode else "client", e + ), + ) + self._log(ERROR, util.tb_strings()) + self.saved_exception = e + except EOFError as e: + self._log(DEBUG, "EOF in transport thread") + self.saved_exception = e + except socket.error as e: + if type(e.args) is tuple: + if e.args: + emsg = "{} ({:d})".format(e.args[1], e.args[0]) + else: # empty tuple, e.g. socket.timeout + emsg = str(e) or repr(e) + else: + emsg = e.args + self._log(ERROR, "Socket exception: " + emsg) + self.saved_exception = e + except Exception as e: + self._log(ERROR, "Unknown exception: " + str(e)) + self._log(ERROR, util.tb_strings()) + self.saved_exception = e + _active_threads.remove(self) + for chan in list(self._channels.values()): + chan._unlink() + if self.active: + self.active = False + self.packetizer.close() + if self.completion_event is not None: + self.completion_event.set() + if self.auth_handler is not None: + self.auth_handler.abort() + for event in self.channel_events.values(): + event.set() + try: + self.lock.acquire() + self.server_accept_cv.notify() + finally: + self.lock.release() + self.sock.close() + except: + # Don't raise spurious 'NoneType has no attribute X' errors when we + # wake up during interpreter shutdown. Or rather -- raise + # everything *if* sys.modules (used as a convenient sentinel) + # appears to still exist. + if self.sys.modules is not None: + raise + + def _log_agreement(self, which, local, remote): + print("local") + # Log useful, non-duplicative line re: an agreed-upon algorithm. + # Old code implied algorithms could be asymmetrical (different for + # inbound vs outbound) so we preserve that possibility. + msg = "{}: ".format(which) + if local == remote: + msg += local + else: + msg += "local={}, remote={}".format(local, remote) + self._log(DEBUG, msg) + + # protocol stages + + def _negotiate_keys(self, m): + print("negotiate keys") + # throws SSHException on anything unusual + self.clear_to_send_lock.acquire() + try: + self.clear_to_send.clear() + finally: + self.clear_to_send_lock.release() + if self.local_kex_init is None: + # remote side wants to renegotiate + self._send_kex_init() + self._parse_kex_init(m) + self.kex_engine.start_kex() + + def _check_banner(self): + print("check banner") + # this is slow, but we only have to do it once + for i in range(100): + # give them 15 seconds for the first line, then just 2 seconds + # each additional line. (some sites have very high latency.) + if i == 0: + timeout = self.banner_timeout + else: + timeout = 2 + try: + buf = self.packetizer.readline(timeout) + except ProxyCommandFailure: + raise + except Exception as e: + raise SSHException( + "Error reading SSH protocol banner" + str(e) + ) + if buf[:4] == "SSH-": + break + self._log(DEBUG, "Banner: " + buf) + if buf[:4] != "SSH-": + raise SSHException('Indecipherable protocol version "' + buf + '"') + # save this server version string for later + self.remote_version = buf + self._log(DEBUG, "Remote version/idstring: {}".format(buf)) + # pull off any attached comment + # NOTE: comment used to be stored in a variable and then...never used. + # since 2003. ca 877cd974b8182d26fa76d566072917ea67b64e67 + i = buf.find(" ") + if i >= 0: + buf = buf[:i] + # parse out version string and make sure it matches + segs = buf.split("-", 2) + if len(segs) < 3: + raise SSHException("Invalid SSH banner") + version = segs[1] + client = segs[2] + if version != "1.99" and version != "2.0": + msg = "Incompatible version ({} instead of 2.0)" + raise IncompatiblePeer(msg.format(version)) + msg = "Connected (version {}, client {})".format(version, client) + self._log(INFO, msg) + + def _send_kex_init(self): + print("send kex init") + """ + announce to the other side that we'd like to negotiate keys, and what + kind of key negotiation we support. + """ + self.clear_to_send_lock.acquire() + try: + self.clear_to_send.clear() + finally: + self.clear_to_send_lock.release() + self.gss_kex_used = False + self.in_kex = True + kex_algos = list(self.preferred_kex) + if self.server_mode: + mp_required_prefix = "diffie-hellman-group-exchange-sha" + kex_mp = [k for k in kex_algos if k.startswith(mp_required_prefix)] + if (self._modulus_pack is None) and (len(kex_mp) > 0): + # can't do group-exchange if we don't have a pack of potential + # primes + pkex = [ + k + for k in self.get_security_options().kex + if not k.startswith(mp_required_prefix) + ] + self.get_security_options().kex = pkex + available_server_keys = list( + filter( + list(self.server_key_dict.keys()).__contains__, + # TODO: ensure tests will catch if somebody streamlines + # this by mistake - case is the admittedly silly one where + # the only calls to add_server_key() contain keys which + # were filtered out of the below via disabled_algorithms. + # If this is streamlined, we would then be allowing the + # disabled algorithm(s) for hostkey use + # TODO: honestly this prob just wants to get thrown out + # when we make kex configuration more straightforward + self.preferred_keys, + ) + ) + else: + available_server_keys = self.preferred_keys + # Signal support for MSG_EXT_INFO. + # NOTE: doing this here handily means we don't even consider this + # value when agreeing on real kex algo to use (which is a common + # pitfall when adding this apparently). + kex_algos.append("ext-info-c") + + m = Message() + m.add_byte(cMSG_KEXINIT) + m.add_bytes(os.urandom(16)) + m.add_list(kex_algos) + m.add_list(available_server_keys) + m.add_list(self.preferred_ciphers) + m.add_list(self.preferred_ciphers) + m.add_list(self.preferred_macs) + m.add_list(self.preferred_macs) + m.add_list(self.preferred_compression) + m.add_list(self.preferred_compression) + m.add_string(bytes()) + m.add_string(bytes()) + m.add_boolean(False) + m.add_int(0) + # save a copy for later (needed to compute a hash) + self.local_kex_init = self._latest_kex_init = m.asbytes() + self._send_message(m) + + def _really_parse_kex_init(self, m, ignore_first_byte=False): + print("really parse kex init") + parsed = {} + if ignore_first_byte: + m.get_byte() + m.get_bytes(16) # cookie, discarded + parsed["kex_algo_list"] = m.get_list() + parsed["server_key_algo_list"] = m.get_list() + parsed["client_encrypt_algo_list"] = m.get_list() + parsed["server_encrypt_algo_list"] = m.get_list() + parsed["client_mac_algo_list"] = m.get_list() + parsed["server_mac_algo_list"] = m.get_list() + parsed["client_compress_algo_list"] = m.get_list() + parsed["server_compress_algo_list"] = m.get_list() + parsed["client_lang_list"] = m.get_list() + parsed["server_lang_list"] = m.get_list() + parsed["kex_follows"] = m.get_boolean() + m.get_int() # unused + return parsed + + def _get_latest_kex_init(self): + print("get latest kex init") + return self._really_parse_kex_init( + Message(self._latest_kex_init), ignore_first_byte=True + ) + + def _parse_kex_init(self, m): + print("parse kex init") + parsed = self._really_parse_kex_init(m) + kex_algo_list = parsed["kex_algo_list"] + server_key_algo_list = parsed["server_key_algo_list"] + client_encrypt_algo_list = parsed["client_encrypt_algo_list"] + server_encrypt_algo_list = parsed["server_encrypt_algo_list"] + client_mac_algo_list = parsed["client_mac_algo_list"] + server_mac_algo_list = parsed["server_mac_algo_list"] + client_compress_algo_list = parsed["client_compress_algo_list"] + server_compress_algo_list = parsed["server_compress_algo_list"] + client_lang_list = parsed["client_lang_list"] + server_lang_list = parsed["server_lang_list"] + kex_follows = parsed["kex_follows"] + + self._log(DEBUG, "=== Key exchange possibilities ===") + for prefix, value in ( + ("kex algos", kex_algo_list), + ("server key", server_key_algo_list), + # TODO: shouldn't these two lines say "cipher" to match usual + # terminology (including elsewhere in paramiko!)? + ("client encrypt", client_encrypt_algo_list), + ("server encrypt", server_encrypt_algo_list), + ("client mac", client_mac_algo_list), + ("server mac", server_mac_algo_list), + ("client compress", client_compress_algo_list), + ("server compress", server_compress_algo_list), + ("client lang", client_lang_list), + ("server lang", server_lang_list), + ): + if value == [""]: + value = ["<none>"] + value = ", ".join(value) + self._log(DEBUG, "{}: {}".format(prefix, value)) + self._log(DEBUG, "kex follows: {}".format(kex_follows)) + self._log(DEBUG, "=== Key exchange agreements ===") + + # Strip out ext-info "kex algo" + self._remote_ext_info = None + if kex_algo_list[-1].startswith("ext-info-"): + self._remote_ext_info = kex_algo_list.pop() + + # as a server, we pick the first item in the client's list that we + # support. + # as a client, we pick the first item in our list that the server + # supports. + if self.server_mode: + agreed_kex = list( + filter(self.preferred_kex.__contains__, kex_algo_list) + ) + else: + agreed_kex = list( + filter(kex_algo_list.__contains__, self.preferred_kex) + ) + if len(agreed_kex) == 0: + # TODO: do an auth-overhaul style aggregate exception here? + # TODO: would let us streamline log output & show all failures up + # front + raise IncompatiblePeer( + "Incompatible ssh peer (no acceptable kex algorithm)" + ) # noqa + self.kex_engine = self._kex_info[agreed_kex[0]](self) + self._log(DEBUG, "Kex: {}".format(agreed_kex[0])) + + if self.server_mode: + available_server_keys = list( + filter( + list(self.server_key_dict.keys()).__contains__, + self.preferred_keys, + ) + ) + agreed_keys = list( + filter( + available_server_keys.__contains__, server_key_algo_list + ) + ) + else: + agreed_keys = list( + filter(server_key_algo_list.__contains__, self.preferred_keys) + ) + if len(agreed_keys) == 0: + raise IncompatiblePeer( + "Incompatible ssh peer (no acceptable host key)" + ) # noqa + self.host_key_type = agreed_keys[0] + if self.server_mode and (self.get_server_key() is None): + raise IncompatiblePeer( + "Incompatible ssh peer (can't match requested host key type)" + ) # noqa + self._log_agreement("HostKey", agreed_keys[0], agreed_keys[0]) + + if self.server_mode: + agreed_local_ciphers = list( + filter( + self.preferred_ciphers.__contains__, + server_encrypt_algo_list, + ) + ) + agreed_remote_ciphers = list( + filter( + self.preferred_ciphers.__contains__, + client_encrypt_algo_list, + ) + ) + else: + agreed_local_ciphers = list( + filter( + client_encrypt_algo_list.__contains__, + self.preferred_ciphers, + ) + ) + agreed_remote_ciphers = list( + filter( + server_encrypt_algo_list.__contains__, + self.preferred_ciphers, + ) + ) + if len(agreed_local_ciphers) == 0 or len(agreed_remote_ciphers) == 0: + raise IncompatiblePeer( + "Incompatible ssh server (no acceptable ciphers)" + ) # noqa + self.local_cipher = agreed_local_ciphers[0] + self.remote_cipher = agreed_remote_ciphers[0] + self._log_agreement( + "Cipher", local=self.local_cipher, remote=self.remote_cipher + ) + + if self.server_mode: + agreed_remote_macs = list( + filter(self.preferred_macs.__contains__, client_mac_algo_list) + ) + agreed_local_macs = list( + filter(self.preferred_macs.__contains__, server_mac_algo_list) + ) + else: + agreed_local_macs = list( + filter(client_mac_algo_list.__contains__, self.preferred_macs) + ) + agreed_remote_macs = list( + filter(server_mac_algo_list.__contains__, self.preferred_macs) + ) + if (len(agreed_local_macs) == 0) or (len(agreed_remote_macs) == 0): + raise IncompatiblePeer( + "Incompatible ssh server (no acceptable macs)" + ) + self.local_mac = agreed_local_macs[0] + self.remote_mac = agreed_remote_macs[0] + self._log_agreement( + "MAC", local=self.local_mac, remote=self.remote_mac + ) + + if self.server_mode: + agreed_remote_compression = list( + filter( + self.preferred_compression.__contains__, + client_compress_algo_list, + ) + ) + agreed_local_compression = list( + filter( + self.preferred_compression.__contains__, + server_compress_algo_list, + ) + ) + else: + agreed_local_compression = list( + filter( + client_compress_algo_list.__contains__, + self.preferred_compression, + ) + ) + agreed_remote_compression = list( + filter( + server_compress_algo_list.__contains__, + self.preferred_compression, + ) + ) + if ( + len(agreed_local_compression) == 0 + or len(agreed_remote_compression) == 0 + ): + msg = "Incompatible ssh server (no acceptable compression)" + msg += " {!r} {!r} {!r}" + raise IncompatiblePeer( + msg.format( + agreed_local_compression, + agreed_remote_compression, + self.preferred_compression, + ) + ) + self.local_compression = agreed_local_compression[0] + self.remote_compression = agreed_remote_compression[0] + self._log_agreement( + "Compression", + local=self.local_compression, + remote=self.remote_compression, + ) + self._log(DEBUG, "=== End of kex handshake ===") + + # save for computing hash later... + # now wait! openssh has a bug (and others might too) where there are + # actually some extra bytes (one NUL byte in openssh's case) added to + # the end of the packet but not parsed. turns out we need to throw + # away those bytes because they aren't part of the hash. + self.remote_kex_init = cMSG_KEXINIT + m.get_so_far() + + def _activate_inbound(self): + print("_activate_inbound") + """switch on newly negotiated encryption parameters for + inbound traffic""" + block_size = self._cipher_info[self.remote_cipher]["block-size"] + if self.server_mode: + IV_in = self._compute_key("A", block_size) + key_in = self._compute_key( + "C", self._cipher_info[self.remote_cipher]["key-size"] + ) + else: + IV_in = self._compute_key("B", block_size) + key_in = self._compute_key( + "D", self._cipher_info[self.remote_cipher]["key-size"] + ) + engine = self._get_cipher( + self.remote_cipher, key_in, IV_in, self._DECRYPT + ) + etm = "etm@openssh.com" in self.remote_mac + mac_size = self._mac_info[self.remote_mac]["size"] + mac_engine = self._mac_info[self.remote_mac]["class"] + # initial mac keys are done in the hash's natural size (not the + # potentially truncated transmission size) + if self.server_mode: + mac_key = self._compute_key("E", mac_engine().digest_size) + else: + mac_key = self._compute_key("F", mac_engine().digest_size) + self.packetizer.set_inbound_cipher( + engine, block_size, mac_engine, mac_size, mac_key, etm=etm + ) + compress_in = self._compression_info[self.remote_compression][1] + if compress_in is not None and ( + self.remote_compression != "zlib@openssh.com" or self.authenticated + ): + self._log(DEBUG, "Switching on inbound compression ...") + self.packetizer.set_inbound_compressor(compress_in()) + + def _activate_outbound(self): + print("_activate_outbound") + """switch on newly negotiated encryption parameters for + outbound traffic""" + m = Message() + m.add_byte(cMSG_NEWKEYS) + self._send_message(m) + block_size = self._cipher_info[self.local_cipher]["block-size"] + if self.server_mode: + IV_out = self._compute_key("B", block_size) + key_out = self._compute_key( + "D", self._cipher_info[self.local_cipher]["key-size"] + ) + else: + IV_out = self._compute_key("A", block_size) + key_out = self._compute_key( + "C", self._cipher_info[self.local_cipher]["key-size"] + ) + engine = self._get_cipher( + self.local_cipher, key_out, IV_out, self._ENCRYPT + ) + etm = "etm@openssh.com" in self.local_mac + mac_size = self._mac_info[self.local_mac]["size"] + mac_engine = self._mac_info[self.local_mac]["class"] + # initial mac keys are done in the hash's natural size (not the + # potentially truncated transmission size) + if self.server_mode: + mac_key = self._compute_key("F", mac_engine().digest_size) + else: + mac_key = self._compute_key("E", mac_engine().digest_size) + sdctr = self.local_cipher.endswith("-ctr") + self.packetizer.set_outbound_cipher( + engine, block_size, mac_engine, mac_size, mac_key, sdctr, etm=etm + ) + compress_out = self._compression_info[self.local_compression][0] + if compress_out is not None and ( + self.local_compression != "zlib@openssh.com" or self.authenticated + ): + self._log(DEBUG, "Switching on outbound compression ...") + self.packetizer.set_outbound_compressor(compress_out()) + if not self.packetizer.need_rekey(): + self.in_kex = False + # If client indicated extension support, send that packet immediately + if ( + self.server_mode + and self.server_sig_algs + and self._remote_ext_info == "ext-info-c" + ): + extensions = {"server-sig-algs": ",".join(self.preferred_pubkeys)} + m = Message() + m.add_byte(cMSG_EXT_INFO) + m.add_int(len(extensions)) + for name, value in sorted(extensions.items()): + m.add_string(name) + m.add_string(value) + self._send_message(m) + # we always expect to receive NEWKEYS now + self._expect_packet(MSG_NEWKEYS) + + def _auth_trigger(self): + print("_auth_trigger") + self.authenticated = True + # delayed initiation of compression + if self.local_compression == "zlib@openssh.com": + compress_out = self._compression_info[self.local_compression][0] + self._log(DEBUG, "Switching on outbound compression ...") + self.packetizer.set_outbound_compressor(compress_out()) + if self.remote_compression == "zlib@openssh.com": + compress_in = self._compression_info[self.remote_compression][1] + self._log(DEBUG, "Switching on inbound compression ...") + self.packetizer.set_inbound_compressor(compress_in()) + + def _parse_ext_info(self, msg): + print("_parse_ext_info") + # Packet is a count followed by that many key-string to possibly-bytes + # pairs. + extensions = {} + for _ in range(msg.get_int()): + name = msg.get_text() + value = msg.get_string() + extensions[name] = value + self._log(DEBUG, "Got EXT_INFO: {}".format(extensions)) + # NOTE: this should work ok in cases where a server sends /two/ such + # messages; the RFC explicitly states a 2nd one should overwrite the + # 1st. + self.server_extensions = extensions + + def _parse_newkeys(self, m): + print("_parse_newkeys") + self._log(DEBUG, "Switch to new keys ...") + self._activate_inbound() + # can also free a bunch of stuff here + self.local_kex_init = self.remote_kex_init = None + self.K = None + self.kex_engine = None + if self.server_mode and (self.auth_handler is None): + # create auth handler for server mode + self.auth_handler = AuthHandler(self) + if not self.initial_kex_done: + # this was the first key exchange + self.initial_kex_done = True + # send an event? + if self.completion_event is not None: + self.completion_event.set() + # it's now okay to send data again (if this was a re-key) + if not self.packetizer.need_rekey(): + self.in_kex = False + self.clear_to_send_lock.acquire() + try: + self.clear_to_send.set() + finally: + self.clear_to_send_lock.release() + return + + def _parse_disconnect(self, m): + print("_parse_disconnect") + code = m.get_int() + desc = m.get_text() + self._log(INFO, "Disconnect (code {:d}): {}".format(code, desc)) + + def _parse_global_request(self, m): + print("_parse_global_request") + kind = m.get_text() + self._log(DEBUG, 'Received global request "{}"'.format(kind)) + want_reply = m.get_boolean() + if not self.server_mode: + self._log( + DEBUG, + 'Rejecting "{}" global request from server.'.format(kind), + ) + ok = False + elif kind == "tcpip-forward": + address = m.get_text() + port = m.get_int() + ok = self.server_object.check_port_forward_request(address, port) + if ok: + ok = (ok,) + elif kind == "cancel-tcpip-forward": + address = m.get_text() + port = m.get_int() + self.server_object.cancel_port_forward_request(address, port) + ok = True + else: + ok = self.server_object.check_global_request(kind, m) + extra = () + if type(ok) is tuple: + extra = ok + ok = True + if want_reply: + msg = Message() + if ok: + msg.add_byte(cMSG_REQUEST_SUCCESS) + msg.add(*extra) + else: + msg.add_byte(cMSG_REQUEST_FAILURE) + self._send_message(msg) + + def _parse_request_success(self, m): + print("_parse_request_success") + self._log(DEBUG, "Global request successful.") + self.global_response = m + if self.completion_event is not None: + self.completion_event.set() + + def _parse_request_failure(self, m): + print("_parse_request_failure") + self._log(DEBUG, "Global request denied.") + self.global_response = None + if self.completion_event is not None: + self.completion_event.set() + + def _parse_channel_open_success(self, m): + print("_parse_channel_open_success") + chanid = m.get_int() + server_chanid = m.get_int() + server_window_size = m.get_int() + server_max_packet_size = m.get_int() + chan = self._channels.get(chanid) + if chan is None: + self._log(WARNING, "Success for unrequested channel! [??]") + return + self.lock.acquire() + try: + chan._set_remote_channel( + server_chanid, server_window_size, server_max_packet_size + ) + self._log(DEBUG, "Secsh channel {:d} opened.".format(chanid)) + if chanid in self.channel_events: + self.channel_events[chanid].set() + del self.channel_events[chanid] + finally: + self.lock.release() + return + + def _parse_channel_open_failure(self, m): + print("_parse_channel_open_failure") + chanid = m.get_int() + reason = m.get_int() + reason_str = m.get_text() + m.get_text() # ignored language + reason_text = CONNECTION_FAILED_CODE.get(reason, "(unknown code)") + self._log( + ERROR, + "Secsh channel {:d} open FAILED: {}: {}".format( + chanid, reason_str, reason_text + ), + ) + self.lock.acquire() + try: + self.saved_exception = ChannelException(reason, reason_text) + if chanid in self.channel_events: + self._channels.delete(chanid) + if chanid in self.channel_events: + self.channel_events[chanid].set() + del self.channel_events[chanid] + finally: + self.lock.release() + return + + def _parse_channel_open(self, m): + print("_parse_channel_open") + kind = m.get_text() + chanid = m.get_int() + initial_window_size = m.get_int() + max_packet_size = m.get_int() + reject = False + if ( + kind == "auth-agent@openssh.com" + and self._forward_agent_handler is not None + ): + self._log(DEBUG, "Incoming forward agent connection") + self.lock.acquire() + try: + my_chanid = self._next_channel() + finally: + self.lock.release() + elif (kind == "x11") and (self._x11_handler is not None): + origin_addr = m.get_text() + origin_port = m.get_int() + self._log( + DEBUG, + "Incoming x11 connection from {}:{:d}".format( + origin_addr, origin_port + ), + ) + self.lock.acquire() + try: + my_chanid = self._next_channel() + finally: + self.lock.release() + elif (kind == "forwarded-tcpip") and (self._tcp_handler is not None): + server_addr = m.get_text() + server_port = m.get_int() + origin_addr = m.get_text() + origin_port = m.get_int() + self._log( + DEBUG, + "Incoming tcp forwarded connection from {}:{:d}".format( + origin_addr, origin_port + ), + ) + self.lock.acquire() + try: + my_chanid = self._next_channel() + finally: + self.lock.release() + elif not self.server_mode: + self._log( + DEBUG, + 'Rejecting "{}" channel request from server.'.format(kind), + ) + reject = True + reason = OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED + else: + self.lock.acquire() + try: + my_chanid = self._next_channel() + finally: + self.lock.release() + if kind == "direct-tcpip": + # handle direct-tcpip requests coming from the client + dest_addr = m.get_text() + dest_port = m.get_int() + origin_addr = m.get_text() + origin_port = m.get_int() + reason = self.server_object.check_channel_direct_tcpip_request( + my_chanid, + (origin_addr, origin_port), + (dest_addr, dest_port), + ) + else: + reason = self.server_object.check_channel_request( + kind, my_chanid + ) + if reason != OPEN_SUCCEEDED: + self._log( + DEBUG, + 'Rejecting "{}" channel request from client.'.format(kind), + ) + reject = True + if reject: + msg = Message() + msg.add_byte(cMSG_CHANNEL_OPEN_FAILURE) + msg.add_int(chanid) + msg.add_int(reason) + msg.add_string("") + msg.add_string("en") + self._send_message(msg) + return + + chan = Channel(my_chanid) + self.lock.acquire() + try: + self._channels.put(my_chanid, chan) + self.channels_seen[my_chanid] = True + chan._set_transport(self) + chan._set_window( + self.default_window_size, self.default_max_packet_size + ) + chan._set_remote_channel( + chanid, initial_window_size, max_packet_size + ) + finally: + self.lock.release() + m = Message() + m.add_byte(cMSG_CHANNEL_OPEN_SUCCESS) + m.add_int(chanid) + m.add_int(my_chanid) + m.add_int(self.default_window_size) + m.add_int(self.default_max_packet_size) + self._send_message(m) + self._log( + DEBUG, "Secsh channel {:d} ({}) opened.".format(my_chanid, kind) + ) + if kind == "auth-agent@openssh.com": + self._forward_agent_handler(chan) + elif kind == "x11": + self._x11_handler(chan, (origin_addr, origin_port)) + elif kind == "forwarded-tcpip": + chan.origin_addr = (origin_addr, origin_port) + self._tcp_handler( + chan, (origin_addr, origin_port), (server_addr, server_port) + ) + else: + self._queue_incoming_channel(chan) + + def _parse_debug(self, m): + print("_parse_debug") + m.get_boolean() # always_display + msg = m.get_string() + m.get_string() # language + self._log(DEBUG, "Debug msg: {}".format(util.safe_string(msg))) + + def _get_subsystem_handler(self, name): + print("_get_subsystem_handler") + try: + self.lock.acquire() + if name not in self.subsystem_table: + return None, [], {} + return self.subsystem_table[name] + finally: + self.lock.release() + + _handler_table = { + MSG_EXT_INFO: _parse_ext_info, + MSG_NEWKEYS: _parse_newkeys, + MSG_GLOBAL_REQUEST: _parse_global_request, + MSG_REQUEST_SUCCESS: _parse_request_success, + MSG_REQUEST_FAILURE: _parse_request_failure, + MSG_CHANNEL_OPEN_SUCCESS: _parse_channel_open_success, + MSG_CHANNEL_OPEN_FAILURE: _parse_channel_open_failure, + MSG_CHANNEL_OPEN: _parse_channel_open, + MSG_KEXINIT: _negotiate_keys, + } + + _channel_handler_table = { + MSG_CHANNEL_SUCCESS: Channel._request_success, + MSG_CHANNEL_FAILURE: Channel._request_failed, + MSG_CHANNEL_DATA: Channel._feed, + MSG_CHANNEL_EXTENDED_DATA: Channel._feed_extended, + MSG_CHANNEL_WINDOW_ADJUST: Channel._window_adjust, + MSG_CHANNEL_REQUEST: Channel._handle_request, + MSG_CHANNEL_EOF: Channel._handle_eof, + MSG_CHANNEL_CLOSE: Channel._handle_close, + } + + +# TODO 4.0: drop this, we barely use it ourselves, it badly replicates the +# Transport-internal algorithm management, AND does so in a way which doesn't +# honor newer things like disabled_algorithms! +class SecurityOptions: + """ + Simple object containing the security preferences of an ssh transport. + These are tuples of acceptable ciphers, digests, key types, and key + exchange algorithms, listed in order of preference. + + Changing the contents and/or order of these fields affects the underlying + `.Transport` (but only if you change them before starting the session). + If you try to add an algorithm that paramiko doesn't recognize, + ``ValueError`` will be raised. If you try to assign something besides a + tuple to one of the fields, ``TypeError`` will be raised. + """ + + __slots__ = "_transport" + + def __init__(self, transport): + self._transport = transport + + def __repr__(self): + """ + Returns a string representation of this object, for debugging. + """ + return "<paramiko.SecurityOptions for {!r}>".format(self._transport) + + def _set(self, name, orig, x): + if type(x) is list: + x = tuple(x) + if type(x) is not tuple: + raise TypeError("expected tuple or list") + possible = list(getattr(self._transport, orig).keys()) + forbidden = [n for n in x if n not in possible] + if len(forbidden) > 0: + raise ValueError("unknown cipher") + setattr(self._transport, name, x) + + @property + def ciphers(self): + """Symmetric encryption ciphers""" + return self._transport._preferred_ciphers + + @ciphers.setter + def ciphers(self, x): + self._set("_preferred_ciphers", "_cipher_info", x) + + @property + def digests(self): + """Digest (one-way hash) algorithms""" + return self._transport._preferred_macs + + @digests.setter + def digests(self, x): + self._set("_preferred_macs", "_mac_info", x) + + @property + def key_types(self): + """Public-key algorithms""" + return self._transport._preferred_keys + + @key_types.setter + def key_types(self, x): + self._set("_preferred_keys", "_key_info", x) + + @property + def kex(self): + """Key exchange algorithms""" + return self._transport._preferred_kex + + @kex.setter + def kex(self, x): + self._set("_preferred_kex", "_kex_info", x) + + @property + def compression(self): + """Compression algorithms""" + return self._transport._preferred_compression + + @compression.setter + def compression(self, x): + self._set("_preferred_compression", "_compression_info", x) + + +class ChannelMap: + def __init__(self): + # (id -> Channel) + self._map = weakref.WeakValueDictionary() + self._lock = threading.Lock() + + def put(self, chanid, chan): + self._lock.acquire() + try: + self._map[chanid] = chan + finally: + self._lock.release() + + def get(self, chanid): + self._lock.acquire() + try: + return self._map.get(chanid, None) + finally: + self._lock.release() + + def delete(self, chanid): + self._lock.acquire() + try: + try: + del self._map[chanid] + except KeyError: + pass + finally: + self._lock.release() + + def values(self): + self._lock.acquire() + try: + return list(self._map.values()) + finally: + self._lock.release() + + def __len__(self): + self._lock.acquire() + try: + return len(self._map) + finally: + self._lock.release() diff --git a/paramiko/util.py b/paramiko/util.py new file mode 100644 index 0000000..f1e33a5 --- /dev/null +++ b/paramiko/util.py @@ -0,0 +1,337 @@ +# Copyright (C) 2003-2007 Robey Pointer <robeypointer@gmail.com> +# +# This file is part of paramiko. +# +# Paramiko is free software; you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free +# Software Foundation; either version 2.1 of the License, or (at your option) +# any later version. +# +# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Paramiko; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +""" +Useful functions used by the rest of paramiko. +""" + + +import sys +import struct +import traceback +import threading +import logging + +from paramiko.common import ( + DEBUG, + zero_byte, + xffffffff, + max_byte, + byte_ord, + byte_chr, +) +from paramiko.config import SSHConfig + + +def inflate_long(s, always_positive=False): + """turns a normalized byte string into a long-int + (adapted from Crypto.Util.number)""" + out = 0 + negative = 0 + if not always_positive and (len(s) > 0) and (byte_ord(s[0]) >= 0x80): + negative = 1 + if len(s) % 4: + filler = zero_byte + if negative: + filler = max_byte + # never convert this to ``s +=`` because this is a string, not a number + # noinspection PyAugmentAssignment + s = filler * (4 - len(s) % 4) + s + for i in range(0, len(s), 4): + out = (out << 32) + struct.unpack(">I", s[i : i + 4])[0] + if negative: + out -= 1 << (8 * len(s)) + return out + + +def deflate_long(n, add_sign_padding=True): + """turns a long-int into a normalized byte string + (adapted from Crypto.Util.number)""" + # after much testing, this algorithm was deemed to be the fastest + s = bytes() + n = int(n) + while (n != 0) and (n != -1): + s = struct.pack(">I", n & xffffffff) + s + n >>= 32 + # strip off leading zeros, FFs + for i in enumerate(s): + if (n == 0) and (i[1] != 0): + break + if (n == -1) and (i[1] != 0xFF): + break + else: + # degenerate case, n was either 0 or -1 + i = (0,) + if n == 0: + s = zero_byte + else: + s = max_byte + s = s[i[0] :] + if add_sign_padding: + if (n == 0) and (byte_ord(s[0]) >= 0x80): + s = zero_byte + s + if (n == -1) and (byte_ord(s[0]) < 0x80): + s = max_byte + s + return s + + +def format_binary(data, prefix=""): + x = 0 + out = [] + while len(data) > x + 16: + out.append(format_binary_line(data[x : x + 16])) + x += 16 + if x < len(data): + out.append(format_binary_line(data[x:])) + return [prefix + line for line in out] + + +def format_binary_line(data): + left = " ".join(["{:02X}".format(byte_ord(c)) for c in data]) + right = "".join( + [".{:c}..".format(byte_ord(c))[(byte_ord(c) + 63) // 95] for c in data] + ) + return "{:50s} {}".format(left, right) + + +def safe_string(s): + out = b"" + for c in s: + i = byte_ord(c) + if 32 <= i <= 127: + out += byte_chr(i) + else: + out += b("%{:02X}".format(i)) + return out + + +def bit_length(n): + try: + return n.bit_length() + except AttributeError: + norm = deflate_long(n, False) + hbyte = byte_ord(norm[0]) + if hbyte == 0: + return 1 + bitlen = len(norm) * 8 + while not (hbyte & 0x80): + hbyte <<= 1 + bitlen -= 1 + return bitlen + + +def tb_strings(): + return "".join(traceback.format_exception(*sys.exc_info())).split("\n") + + +def generate_key_bytes(hash_alg, salt, key, nbytes): + """ + Given a password, passphrase, or other human-source key, scramble it + through a secure hash into some keyworthy bytes. This specific algorithm + is used for encrypting/decrypting private key files. + + :param function hash_alg: A function which creates a new hash object, such + as ``hashlib.sha256``. + :param salt: data to salt the hash with. + :type bytes salt: Hash salt bytes. + :param str key: human-entered password or passphrase. + :param int nbytes: number of bytes to generate. + :return: Key data, as `bytes`. + """ + keydata = bytes() + digest = bytes() + if len(salt) > 8: + salt = salt[:8] + while nbytes > 0: + hash_obj = hash_alg() + if len(digest) > 0: + hash_obj.update(digest) + hash_obj.update(b(key)) + hash_obj.update(salt) + digest = hash_obj.digest() + size = min(nbytes, len(digest)) + keydata += digest[:size] + nbytes -= size + return keydata + + +def load_host_keys(filename): + """ + Read a file of known SSH host keys, in the format used by openssh, and + return a compound dict of ``hostname -> keytype ->`` `PKey + <paramiko.pkey.PKey>`. The hostname may be an IP address or DNS name. The + keytype will be either ``"ssh-rsa"`` or ``"ssh-dss"``. + + This type of file unfortunately doesn't exist on Windows, but on posix, + it will usually be stored in ``os.path.expanduser("~/.ssh/known_hosts")``. + + Since 1.5.3, this is just a wrapper around `.HostKeys`. + + :param str filename: name of the file to read host keys from + :return: + nested dict of `.PKey` objects, indexed by hostname and then keytype + """ + from paramiko.hostkeys import HostKeys + + return HostKeys(filename) + + +def parse_ssh_config(file_obj): + """ + Provided only as a backward-compatible wrapper around `.SSHConfig`. + + .. deprecated:: 2.7 + Use `SSHConfig.from_file` instead. + """ + config = SSHConfig() + config.parse(file_obj) + return config + + +def lookup_ssh_host_config(hostname, config): + """ + Provided only as a backward-compatible wrapper around `.SSHConfig`. + """ + return config.lookup(hostname) + + +def mod_inverse(x, m): + # it's crazy how small Python can make this function. + u1, u2, u3 = 1, 0, m + v1, v2, v3 = 0, 1, x + + while v3 > 0: + q = u3 // v3 + u1, v1 = v1, u1 - v1 * q + u2, v2 = v2, u2 - v2 * q + u3, v3 = v3, u3 - v3 * q + if u2 < 0: + u2 += m + return u2 + + +_g_thread_data = threading.local() +_g_thread_counter = 0 +_g_thread_lock = threading.Lock() + + +def get_thread_id(): + global _g_thread_data, _g_thread_counter, _g_thread_lock + try: + return _g_thread_data.id + except AttributeError: + with _g_thread_lock: + _g_thread_counter += 1 + _g_thread_data.id = _g_thread_counter + return _g_thread_data.id + + +def log_to_file(filename, level=DEBUG): + """send paramiko logs to a logfile, + if they're not already going somewhere""" + logger = logging.getLogger("paramiko") + if len(logger.handlers) > 0: + return + logger.setLevel(level) + f = open(filename, "a") + handler = logging.StreamHandler(f) + frm = "%(levelname)-.3s [%(asctime)s.%(msecs)03d] thr=%(_threadid)-3d" + frm += " %(name)s: %(message)s" + handler.setFormatter(logging.Formatter(frm, "%Y%m%d-%H:%M:%S")) + logger.addHandler(handler) + + +# make only one filter object, so it doesn't get applied more than once +class PFilter: + def filter(self, record): + record._threadid = get_thread_id() + return True + + +_pfilter = PFilter() + + +def get_logger(name): + logger = logging.getLogger(name) + logger.addFilter(_pfilter) + return logger + + +def constant_time_bytes_eq(a, b): + if len(a) != len(b): + return False + res = 0 + # noinspection PyUnresolvedReferences + for i in range(len(a)): # noqa: F821 + res |= byte_ord(a[i]) ^ byte_ord(b[i]) + return res == 0 + + +class ClosingContextManager: + def __enter__(self): + return self + + def __exit__(self, type, value, traceback): + self.close() + + +def clamp_value(minimum, val, maximum): + return max(minimum, min(val, maximum)) + + +def asbytes(s): + """ + Coerce to bytes if possible or return unchanged. + """ + try: + # Attempt to run through our version of b(), which does the Right Thing + # for unicode strings vs bytestrings, and raises TypeError if it's not + # one of those types. + return b(s) + except TypeError: + try: + # If it wasn't a string/byte/buffer-ish object, try calling an + # asbytes() method, which many of our internal classes implement. + return s.asbytes() + except AttributeError: + # Finally, just do nothing & assume this object is sufficiently + # byte-y or buffer-y that everything will work out (or that callers + # are capable of handling whatever it is.) + return s + + +# TODO: clean this up / force callers to assume bytes OR unicode +def b(s, encoding="utf8"): + """cast unicode or bytes to bytes""" + if isinstance(s, bytes): + return s + elif isinstance(s, str): + return s.encode(encoding) + else: + raise TypeError(f"Expected unicode or bytes, got {type(s)}") + + +# TODO: clean this up / force callers to assume bytes OR unicode +def u(s, encoding="utf8"): + """cast bytes or unicode to unicode""" + if isinstance(s, bytes): + return s.decode(encoding) + elif isinstance(s, str): + return s + else: + raise TypeError(f"Expected unicode or bytes, got {type(s)}") diff --git a/paramiko/win_openssh.py b/paramiko/win_openssh.py new file mode 100644 index 0000000..614b589 --- /dev/null +++ b/paramiko/win_openssh.py @@ -0,0 +1,56 @@ +# Copyright (C) 2021 Lew Gordon <lew.gordon@genesys.com> +# Copyright (C) 2022 Patrick Spendrin <ps_ml@gmx.de> +# +# This file is part of paramiko. +# +# Paramiko is free software; you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free +# Software Foundation; either version 2.1 of the License, or (at your option) +# any later version. +# +# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Paramiko; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +import os.path +import time + +PIPE_NAME = r"\\.\pipe\openssh-ssh-agent" + + +def can_talk_to_agent(): + # use os.listdir() instead of os.path.exists(), because os.path.exists() + # uses CreateFileW() API and the pipe cannot be reopen unless the server + # calls DisconnectNamedPipe(). + dir_, name = os.path.split(PIPE_NAME) + name = name.lower() + return any(name == n.lower() for n in os.listdir(dir_)) + + +class OpenSSHAgentConnection: + def __init__(self): + while True: + try: + self._pipe = os.open(PIPE_NAME, os.O_RDWR | os.O_BINARY) + except OSError as e: + # retry when errno 22 which means that the server has not + # called DisconnectNamedPipe() yet. + if e.errno != 22: + raise + else: + break + time.sleep(0.1) + + def send(self, data): + return os.write(self._pipe, data) + + def recv(self, n): + return os.read(self._pipe, n) + + def close(self): + return os.close(self._pipe) diff --git a/paramiko/win_pageant.py b/paramiko/win_pageant.py new file mode 100644 index 0000000..c927de6 --- /dev/null +++ b/paramiko/win_pageant.py @@ -0,0 +1,138 @@ +# Copyright (C) 2005 John Arbash-Meinel <john@arbash-meinel.com> +# Modified up by: Todd Whiteman <ToddW@ActiveState.com> +# +# This file is part of paramiko. +# +# Paramiko is free software; you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free +# Software Foundation; either version 2.1 of the License, or (at your option) +# any later version. +# +# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Paramiko; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +""" +Functions for communicating with Pageant, the basic windows ssh agent program. +""" + +import array +import ctypes.wintypes +import platform +import struct +from paramiko.common import zero_byte +from paramiko.util import b + +import _thread as thread + +from . import _winapi + + +_AGENT_COPYDATA_ID = 0x804E50BA +_AGENT_MAX_MSGLEN = 8192 +# Note: The WM_COPYDATA value is pulled from win32con, as a workaround +# so we do not have to import this huge library just for this one variable. +win32con_WM_COPYDATA = 74 + + +def _get_pageant_window_object(): + return ctypes.windll.user32.FindWindowA(b"Pageant", b"Pageant") + + +def can_talk_to_agent(): + """ + Check to see if there is a "Pageant" agent we can talk to. + + This checks both if we have the required libraries (win32all or ctypes) + and if there is a Pageant currently running. + """ + return bool(_get_pageant_window_object()) + + +if platform.architecture()[0] == "64bit": + ULONG_PTR = ctypes.c_uint64 +else: + ULONG_PTR = ctypes.c_uint32 + + +class COPYDATASTRUCT(ctypes.Structure): + """ + ctypes implementation of + http://msdn.microsoft.com/en-us/library/windows/desktop/ms649010%28v=vs.85%29.aspx + """ + + _fields_ = [ + ("num_data", ULONG_PTR), + ("data_size", ctypes.wintypes.DWORD), + ("data_loc", ctypes.c_void_p), + ] + + +def _query_pageant(msg): + """ + Communication with the Pageant process is done through a shared + memory-mapped file. + """ + hwnd = _get_pageant_window_object() + if not hwnd: + # Raise a failure to connect exception, pageant isn't running anymore! + return None + + # create a name for the mmap + map_name = f"PageantRequest{thread.get_ident():08x}" + + pymap = _winapi.MemoryMap( + map_name, _AGENT_MAX_MSGLEN, _winapi.get_security_attributes_for_user() + ) + with pymap: + pymap.write(msg) + # Create an array buffer containing the mapped filename + char_buffer = array.array("b", b(map_name) + zero_byte) # noqa + char_buffer_address, char_buffer_size = char_buffer.buffer_info() + # Create a string to use for the SendMessage function call + cds = COPYDATASTRUCT( + _AGENT_COPYDATA_ID, char_buffer_size, char_buffer_address + ) + + response = ctypes.windll.user32.SendMessageA( + hwnd, win32con_WM_COPYDATA, ctypes.sizeof(cds), ctypes.byref(cds) + ) + + if response > 0: + pymap.seek(0) + datalen = pymap.read(4) + retlen = struct.unpack(">I", datalen)[0] + return datalen + pymap.read(retlen) + return None + + +class PageantConnection: + """ + Mock "connection" to an agent which roughly approximates the behavior of + a unix local-domain socket (as used by Agent). Requests are sent to the + pageant daemon via special Windows magick, and responses are buffered back + for subsequent reads. + """ + + def __init__(self): + self._response = None + + def send(self, data): + self._response = _query_pageant(data) + + def recv(self, n): + if self._response is None: + return "" + ret = self._response[:n] + self._response = self._response[n:] + if self._response == "": + self._response = None + return ret + + def close(self): + pass diff --git a/ssh-keys/hello.txt b/ssh-keys/hello.txt new file mode 100644 index 0000000..6751cc9 --- /dev/null +++ b/ssh-keys/hello.txt @@ -0,0 +1,2 @@ +mes1: paramiko.Message(b'2\x00\x00\x00\tparallels\x00\x00\x00\x0essh-connection\x00\x00\x00\tpublickey\x01\x00\x00\x00\x0crsa-sha2-512\x00\x00\x01\x97\x00\x00\x00\x07ssh-rsa\x00\x00\x00\x03\x01\x00\x01\x00\x00\x01\x81\x00\xdb~\x08\xebLX\n\x93.jw\x07u\xb4<=\x83U\xe1V\x14\xf2w\xcb\x1e\xa7OWf\xac\x88\xb6\xa7\xd4\xf5\xd1\xe2}t\xcfT\xa9\xe4\xec\x99\xe8\x97i\xe1\xb8\x95\xc9B\xfb;\x86\xb3\xebPL\x11\x18n\xefN\xfd\x1a;\xc7\xdeM\x1b\xf3y>\x02l\x90\x18\xc7\xbe4\xf7`\xcd\'\xc8\x88\xa9V\xdd\x0b3X\t\xe7\xcd2O\xfaN\x88\x03\xa1\xc9dY\x89xojF\x93\xf1\xad\x85\x03#\xebV\x97\x19\x05\xeaw*\xcdD\x81\xa0\x84\x1a\xfd\x99\x0b\xb9\xd2.\xbf\x94\xf1\x10A`\x14Ssvr6\x0c\x03\xbb\x98q\xc4&=\xd4S\xd3\xb9\xa4\x05\xfe\xddAY\x0eQ\xf8g\xd0\xd1|V\x13%\x91\xe2\xb9\xdd\xf2\xd6\xf6\x11\xb6\x9a\x11\x04\xfa\xa4+$\x94O\x03\x89\x9e\xedJw \x87\xc6-s\xe2\xde\x87\x80\x01LH\xd0Hm\x051Y\x19\xcaPX\x161\x88\xc65\x9c\xe8\xf5w\xe0\xd2C,\x9d\x9c{\x91\xc6]\x94\xc8\xd7\x93\x16Y9u^I\xbe\x8c\xda\xf5m:0\xcb\xae\xf8\x1a\xa1\xb7\xbe\xdf\r\xc3\x82V\xb0\xf3\xd6\xe5\xbf\xf6\xbc\x1d\xed\x17P\xe3M\x84G\xb2\xf6\xea~\x02f\xcd\x18\xf1\xa84E\xd3KW\xe8\x17\x98Q\x14y\xf6\xb2!\x08\x95\x06K\xb3\xa0\xc2.\xe6\xb87D\xe0A\xb6\xdb\x19\x10>T0\x91W\xfc\xf8\xa5\xa9\xfa\xd8g\xb1\x1d\xe6UKs\x7fhX^\x9b\x92\x06>\x0c(\x12\x19ZN\x8e\x14BZ\x99\x1b\xed\x141\x8f*\x9d$\x19\xa5X\xc1\x914z\xbe<\xd1\x00\x00\x01\x94\x00\x00\x00\x0crsa-sha2-512\x00\x00\x01\x80\xca\x7f593\x85\xecz\x0b\xe2I\x99\xbe#w6\x87\xba~t\x92\x9c`7\xf1E\x1a\xf6\x83\xb6\x0b\xfb\xa8\x00]+\xfc\xd5\xad\xc8<H\x90@\xb5\xb7Z{\xeas\x9d\x01\x13G\x0f_D\x88\xd4\xb9+v\x91\xd0w\xf1\xdcq\x7f\x8a\x1c\x99\x16\xa3SG)\xfd\xd3\xbd\x057\xf3\\\x89\xd2\xcfJe\xbcn\x93\xae\x06Q\x80l\x85\xac1\xe4[\xab\xe2\xe7.\xb9)\x15\x9d\xed\x82|`\x8b\xd1\x14N\x18\xaadKB\xec\xc7\x0c2\xb7\x87\x83\xb7\x95\x1a4\x1e\xa15\x19\xa6\xc2b\n\x0e`\x11\x9e\x89q\xeaV\x97\xf8;\x94K"\xb3\x0f\xa7@\xb6\xd5\x80\x17\xcf_6\xc5\xae\x19\x06vf\xa0\\\xf7\r6\xfc\x80\xb7$j\xa6\x07k\xe3\xbc\x12&\xe4Qg\x06.\xab\xb3\xac\x93\xe1K\xfc3\xcd\xd4\xcf\xf5vt\xd0\x95\xfd\xef\x02%\xcc\xc6\xce\xdc#6Nn\ta\x95\xa0\xae\x1a\xa2\xa4\xda\xd3\xd5\xf2\xb5\xddI\xbb\xa24\xb4\xf4\xae!:\x9f\xb7\xbc\xedJfL\xedZI\xd1\xa0\xc4\xadfG\xff\xbb\xf6c\x10\x0b\x9aoi\xb0\xddS\x898\x02\xa2\x7f\x83j{0<\xa2O\xb9Ar^\x93f{\x16\xbby\xa0\xc5\xcf\xcaU\xad\x11\xf5\r\xec\xd9\xd0\xbb$\xf4G\xb5\xcaq\x00\x89c\x0b\xa4\x84\xbf|[\x001\xd9\xfd\xdb^[\x8a@\xcc\xba\xb3\xe4\xd1:\x05\xf3\x8b}\xe5\xb9Y\x9a\xf2\x8b\x0f\xa2\x8d\x15\x94Mr\x0f\x01<2\x95`\xf9\x80\x88:4\xdf\xa6}Q\xdd\x0bS\x9aEU6\xdb\x14i\xc6\xc4\x04') +mes2: paramiko.Message(b'2\x00\x00\x00\tparallels\x00\x00\x00\x0essh-connection\x00\x00\x00\tpublickey\x01\x00\x00\x00\x0crsa-sha2-512\x00\x00\x01\x97\x00\x00\x00\x07ssh-rsa\x00\x00\x00\x03\x01\x00\x01\x00\x00\x01\x81\x00\xdb~\x08\xebLX\n\x93.jw\x07u\xb4<=\x83U\xe1V\x14\xf2w\xcb\x1e\xa7OWf\xac\x88\xb6\xa7\xd4\xf5\xd1\xe2}t\xcfT\xa9\xe4\xec\x99\xe8\x97i\xe1\xb8\x95\xc9B\xfb;\x86\xb3\xebPL\x11\x18n\xefN\xfd\x1a;\xc7\xdeM\x1b\xf3y>\x02l\x90\x18\xc7\xbe4\xf7`\xcd\'\xc8\x88\xa9V\xdd\x0b3X\t\xe7\xcd2O\xfaN\x88\x03\xa1\xc9dY\x89xojF\x93\xf1\xad\x85\x03#\xebV\x97\x19\x05\xeaw*\xcdD\x81\xa0\x84\x1a\xfd\x99\x0b\xb9\xd2.\xbf\x94\xf1\x10A`\x14Ssvr6\x0c\x03\xbb\x98q\xc4&=\xd4S\xd3\xb9\xa4\x05\xfe\xddAY\x0eQ\xf8g\xd0\xd1|V\x13%\x91\xe2\xb9\xdd\xf2\xd6\xf6\x11\xb6\x9a\x11\x04\xfa\xa4+$\x94O\x03\x89\x9e\xedJw \x87\xc6-s\xe2\xde\x87\x80\x01LH\xd0Hm\x051Y\x19\xcaPX\x161\x88\xc65\x9c\xe8\xf5w\xe0\xd2C,\x9d\x9c{\x91\xc6]\x94\xc8\xd7\x93\x16Y9u^I\xbe\x8c\xda\xf5m:0\xcb\xae\xf8\x1a\xa1\xb7\xbe\xdf\r\xc3\x82V\xb0\xf3\xd6\xe5\xbf\xf6\xbc\x1d\xed\x17P\xe3M\x84G\xb2\xf6\xea~\x02f\xcd\x18\xf1\xa84E\xd3KW\xe8\x17\x98Q\x14y\xf6\xb2!\x08\x95\x06K\xb3\xa0\xc2.\xe6\xb87D\xe0A\xb6\xdb\x19\x10>T0\x91W\xfc\xf8\xa5\xa9\xfa\xd8g\xb1\x1d\xe6UKs\x7fhX^\x9b\x92\x06>\x0c(\x12\x19ZN\x8e\x14BZ\x99\x1b\xed\x141\x8f*\x9d$\x19\xa5X\xc1\x914z\xbe<\xd1\x00\x00\x01\x94\x00\x00\x00\x0crsa-sha2-512\x00\x00\x01\x80\xca\x7f593\x85\xecz\x0b\xe2I\x99\xbe#w6\x87\xba~t\x92\x9c`7\xf1E\x1a\xf6\x83\xb6\x0b\xfb\xa8\x00]+\xfc\xd5\xad\xc8<H\x90@\xb5\xb7Z{\xeas\x9d\x01\x13G\x0f_D\x88\xd4\xb9+v\x91\xd0w\xf1\xdcq\x7f\x8a\x1c\x99\x16\xa3SG)\xfd\xd3\xbd\x057\xf3\\\x89\xd2\xcfJe\xbcn\x93\xae\x06Q\x80l\x85\xac1\xe4[\xab\xe2\xe7.\xb9)\x15\x9d\xed\x82|`\x8b\xd1\x14N\x18\xaadKB\xec\xc7\x0c2\xb7\x87\x83\xb7\x95\x1a4\x1e\xa15\x19\xa6\xc2b\n\x0e`\x11\x9e\x89q\xeaV\x97\xf8;\x94K"\xb3\x0f\xa7@\xb6\xd5\x80\x17\xcf_6\xc5\xae\x19\x06vf\xa0\\\xf7\r6\xfc\x80\xb7$j\xa6\x07k\xe3\xbc\x12&\xe4Qg\x06.\xab\xb3\xac\x93\xe1K\xfc3\xcd\xd4\xcf\xf5vt\xd0\x95\xfd\xef\x02%\xcc\xc6\xce\xdc#6Nn\ta\x95\xa0\xae\x1a\xa2\xa4\xda\xd3\xd5\xf2\xb5\xddI\xbb\xa24\xb4\xf4\xae!:\x9f\xb7\xbc\xedJfL\xedZI\xd1\xa0\xc4\xadfG\xff\xbb\xf6c\x10\x0b\x9aoi\xb0\xddS\x898\x02\xa2\x7f\x83j{0<\xa2O\xb9Ar^\x93f{\x16\xbby\xa0\xc5\xcf\xcaU\xad\x11\xf5\r\xec\xd9\xd0\xbb$\xf4G\xb5\xcaq\x00\x89c\x0b\xa4\x84\xbf|[\x001\xd9\xfd\xdb^[\x8a@\xcc\xba\xb3\xe4\xd1:\x05\xf3\x8b}\xe5\xb9Y\x9a\xf2\x8b\x0f\xa2\x8d\x15\x94Mr\x0f\x01<2\x95`\xf9\x80\x88:4\xdf\xa6}Q\xdd\x0bS\x9aEU6\xdb\x14i\xc6\xc4\x04') \ No newline at end of file diff --git a/ssh-keys/hello2.txt b/ssh-keys/hello2.txt new file mode 100644 index 0000000..e69de29 diff --git a/ssh-keys/test b/ssh-keys/test new file mode 100644 index 0000000..381b68f --- /dev/null +++ b/ssh-keys/test @@ -0,0 +1,39 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +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 +bAECAw== +-----END OPENSSH PRIVATE KEY----- diff --git a/ssh-keys/test.pub b/ssh-keys/test.pub new file mode 100644 index 0000000..3426f41 --- /dev/null +++ b/ssh-keys/test.pub @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDbfgjrTFgKky5qdwd1tDw9g1XhVhTyd8sep09XZqyItqfU9dHifXTPVKnk7Jnol2nhuJXJQvs7hrPrUEwRGG7vTv0aO8feTRvzeT4CbJAYx74092DNJ8iIqVbdCzNYCefNMk/6TogDoclkWYl4b2pGk/GthQMj61aXGQXqdyrNRIGghBr9mQu50i6/lPEQQWAUU3N2cjYMA7uYccQmPdRT07mkBf7dQVkOUfhn0NF8VhMlkeK53fLW9hG2mhEE+qQrJJRPA4me7Up3IIfGLXPi3oeAAUxI0EhtBTFZGcpQWBYxiMY1nOj1d+DSQyydnHuRxl2UyNeTFlk5dV5Jvoza9W06MMuu+Bqht77fDcOCVrDz1uW/9rwd7RdQ402ER7L26n4CZs0Y8ag0RdNLV+gXmFEUefayIQiVBkuzoMIu5rg3ROBBttsZED5UMJFX/PilqfrYZ7Ed5lVLc39oWF6bkgY+DCgSGVpOjhRCWpkb7RQxjyqdJBmlWMGRNHq+PNE= jorgemachado@MacBook-Pro-de-Jorge.local diff --git a/ssh-keys/yeeee b/ssh-keys/yeeee new file mode 100644 index 0000000..ab87036 --- /dev/null +++ b/ssh-keys/yeeee @@ -0,0 +1,39 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +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 +bAECAw== +-----END OPENSSH PRIVATE KEY----- diff --git a/ssh-keys/yeeee.pub b/ssh-keys/yeeee.pub new file mode 100644 index 0000000..a21eee8 --- /dev/null +++ b/ssh-keys/yeeee.pub @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDvDZoQ4jLcJVJFQ+1bU4yYkNaOFF8HxiJHN8fAKpwiQnk1bKUa/ucfVbjm8MAzli1ieK11Hvp4p39fUYfWJOXR+OB0Ci5Yd5qs1nwq9NRUKt9c1PgwCcBF1iMywSDLZNDecoQDaycZ9+8BODbj/Wx9vGzmPO61NxrTFSmqWBbUASBu9D6w9rZGuVMRKz+V1syjjHggj8D/NMoDoQqTq0UpZ9kxR4oZ78R/Ka79xXoHd0ZbkzmiGD3kVGFNK6r1orFa/O4HE0k9zU2LAuoykPZs9x2s5XvqHRrkbxp+tqSRO//Ejn/hYaijZoXbcso02VMEmIDLaMuHBs4a9TEUVQJ8vfnYcB+O7fuXZo0+UffX/rFftsMJoww/JFPYKAvjgNDhpE8GpvC1QnmS9m5yC4e55SfsE4FceY+GDuV9EmBtLTpgEIL7FJDlF3JxQfXfegBdqHee2b12arvalM3rkr+ZkNEljNASsydOkns6CIZ2lYBGh6dHS99qyQYDojN9Ih0= jorgemachado@MacBook-Pro-de-Jorge.local -- GitLab