diff options
Diffstat (limited to 'paramiko/transport.py')
-rw-r--r-- | paramiko/transport.py | 1824 |
1 files changed, 1824 insertions, 0 deletions
diff --git a/paramiko/transport.py b/paramiko/transport.py new file mode 100644 index 0000000..8714a96 --- /dev/null +++ b/paramiko/transport.py @@ -0,0 +1,1824 @@ +# Copyright (C) 2003-2005 Robey Pointer <robey@lag.net> +# +# 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 distrubuted 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., +# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. + +""" +L{Transport} handles the core SSH2 protocol. +""" + +import os +import socket +import string +import struct +import sys +import threading +import time +import weakref + +from paramiko import util +from paramiko.common import * +from paramiko.compress import ZlibCompressor, ZlibDecompressor +from paramiko.ssh_exception import SSHException, BadAuthenticationType +from paramiko.message import Message +from paramiko.channel import Channel +from paramiko.sftp_client import SFTPClient +from paramiko.packet import Packetizer, NeedRekeyException +from paramiko.rsakey import RSAKey +from paramiko.dsskey import DSSKey +from paramiko.kex_group1 import KexGroup1 +from paramiko.kex_gex import KexGex +from paramiko.primes import ModulusPack +from paramiko.auth_handler import AuthHandler + +# these come from PyCrypt +# http://www.amk.ca/python/writing/pycrypt/ +# i believe this on the standards track. +# PyCrypt compiled for Win32 can be downloaded from the HashTar homepage: +# http://nitace.bsd.uchicago.edu:8080/hashtar +from Crypto.Cipher import Blowfish, AES, DES3 +from Crypto.Hash import SHA, MD5, HMAC + + +# 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 SecurityOptions (object): + """ + 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 + L{Transport} (but only if you change them before starting the session). + If you try to add an algorithm that paramiko doesn't recognize, + C{ValueError} will be raised. If you try to assign something besides a + tuple to one of the fields, C{TypeError} will be raised. + + @since: ivysaur + """ + __slots__ = [ 'ciphers', 'digests', 'key_types', 'kex', 'compression', '_transport' ] + + def __init__(self, transport): + self._transport = transport + + def __repr__(self): + """ + Returns a string representation of this object, for debugging. + + @rtype: str + """ + return '<paramiko.SecurityOptions for %s>' % repr(self._transport) + + def _get_ciphers(self): + return self._transport._preferred_ciphers + + def _get_digests(self): + return self._transport._preferred_macs + + def _get_key_types(self): + return self._transport._preferred_keys + + def _get_kex(self): + return self._transport._preferred_kex + + def _get_compression(self): + return self._transport._preferred_compression + + 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 = getattr(self._transport, orig).keys() + if len(filter(lambda n: n not in possible, x)) > 0: + raise ValueError('unknown cipher') + setattr(self._transport, name, x) + + def _set_ciphers(self, x): + self._set('_preferred_ciphers', '_cipher_info', x) + + def _set_digests(self, x): + self._set('_preferred_macs', '_mac_info', x) + + def _set_key_types(self, x): + self._set('_preferred_keys', '_key_info', x) + + def _set_kex(self, x): + self._set('_preferred_kex', '_kex_info', x) + + def _set_compression(self, x): + self._set('_preferred_compression', '_compression_info', x) + + ciphers = property(_get_ciphers, _set_ciphers, None, + "Symmetric encryption ciphers") + digests = property(_get_digests, _set_digests, None, + "Digest (one-way hash) algorithms") + key_types = property(_get_key_types, _set_key_types, None, + "Public-key algorithms") + kex = property(_get_kex, _set_kex, None, "Key exchange algorithms") + compression = property(_get_compression, _set_compression, None, + "Compression algorithms") + + +class Transport (threading.Thread): + """ + An SSH Transport attaches to a stream (usually a socket), negotiates an + encrypted session, authenticates, and then creates stream tunnels, called + L{Channel}s, across the session. Multiple channels can be multiplexed + across a single session (and often are, in the case of port forwardings). + """ + + _PROTO_ID = '2.0' + _CLIENT_ID = 'paramiko_1.5.2' + + _preferred_ciphers = ( 'aes128-cbc', 'blowfish-cbc', 'aes256-cbc', '3des-cbc' ) + _preferred_macs = ( 'hmac-sha1', 'hmac-md5', 'hmac-sha1-96', 'hmac-md5-96' ) + _preferred_keys = ( 'ssh-rsa', 'ssh-dss' ) + _preferred_kex = ( 'diffie-hellman-group1-sha1', 'diffie-hellman-group-exchange-sha1' ) + _preferred_compression = ( 'none', ) + + _cipher_info = { + 'blowfish-cbc': { 'class': Blowfish, 'mode': Blowfish.MODE_CBC, 'block-size': 8, 'key-size': 16 }, + 'aes128-cbc': { 'class': AES, 'mode': AES.MODE_CBC, 'block-size': 16, 'key-size': 16 }, + 'aes256-cbc': { 'class': AES, 'mode': AES.MODE_CBC, 'block-size': 16, 'key-size': 32 }, + '3des-cbc': { 'class': DES3, 'mode': DES3.MODE_CBC, 'block-size': 8, 'key-size': 24 }, + } + + _mac_info = { + 'hmac-sha1': { 'class': SHA, 'size': 20 }, + 'hmac-sha1-96': { 'class': SHA, 'size': 12 }, + 'hmac-md5': { 'class': MD5, 'size': 16 }, + 'hmac-md5-96': { 'class': MD5, 'size': 12 }, + } + + _key_info = { + 'ssh-rsa': RSAKey, + 'ssh-dss': DSSKey, + } + + _kex_info = { + 'diffie-hellman-group1-sha1': KexGroup1, + 'diffie-hellman-group-exchange-sha1': KexGex, + } + + _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 + + def __init__(self, sock): + """ + 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 L{connect} or L{start_client} to begin a client + session, or L{start_server} to begin a server session. + + If the object is not actually a socket, it must have the following + methods: + - C{send(str)}: Writes from 1 to C{len(str)} bytes, and + returns an int representing the number of bytes written. Returns + 0 or raises C{EOFError} if the stream has been closed. + - C{recv(int)}: Reads from 1 to C{int} bytes and returns them as a + string. Returns 0 or raises C{EOFError} if the stream has been + closed. + - C{close()}: Closes the socket. + - C{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 C{sock} argument. (A host string is a hostname with an + optional port (separated by C{":"}) which will be converted into a + tuple of C{(hostname, port)}.) A socket will be connected to this + address and used for communication. Exceptions from the C{socket} call + may be thrown in this case. + + @param sock: a socket or socket-like object to create the session over. + @type sock: socket + """ + if type(sock) is str: + # convert "host:port" into (host, port) + hl = sock.split(':', 1) + 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 + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.connect((hostname, port)) + # okay, normal socket-ish flow here... + threading.Thread.__init__(self) + self.randpool = randpool + self.sock = sock + # Python < 2.3 doesn't have the settimeout method - RogerB + try: + # 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(0.1) + except AttributeError: + pass + # 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.session_id = None + # /negotiated crypto parameters + self.expected_packet = 0 + self.active = False + self.initial_kex_done = False + self.in_kex = False + self.lock = threading.Lock() # synchronization (always higher level than write_lock) + self.channels = weakref.WeakValueDictionary() # (id -> Channel) + self.channel_events = { } # (id -> Event) + self.channels_seen = { } # (id -> True) + self.channel_counter = 1 + self.window_size = 65536 + self.max_packet_size = 34816 + self.saved_exception = None + self.clear_to_send = threading.Event() + self.clear_to_send_lock = threading.Lock() + self.log_name = 'paramiko.transport' + self.logger = util.get_logger(self.log_name) + self.packetizer.set_log(self.logger) + self.auth_handler = None + self.authenticated = False + # user-defined event callbacks: + self.completion_event = None + # 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 __del__(self): + self.close() + + def __repr__(self): + """ + Returns a string representation of this object, for debugging. + + @rtype: str + """ + out = '<paramiko.Transport at %s' % hex(long(id(self)) & 0xffffffffL) + if not self.active: + out += ' (unconnected)' + else: + if self.local_cipher != '': + out += ' (cipher %s, %d bits)' % (self.local_cipher, + self._cipher_info[self.local_cipher]['key-size'] * 8) + if self.is_authenticated(): + if len(self.channels) == 1: + out += ' (active; 1 open channel)' + else: + out += ' (active; %d open channels)' % len(self.channels) + elif self.initial_kex_done: + out += ' (connected; awaiting auth)' + else: + out += ' (connecting)' + out += '>' + return out + + def get_security_options(self): + """ + Return a L{SecurityOptions} object which can be used to tweak the + encryption algorithms this transport will permit, and the order of + preference for them. + + @return: an object that can be used to change the preferred algorithms + for encryption, digest (hash), public key, and key exchange. + @rtype: L{SecurityOptions} + + @since: ivysaur + """ + return SecurityOptions(self) + + def start_client(self, event=None): + """ + Negotiate a new SSH2 session as a client. This is the first step after + creating a new L{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 C{Event} will + be triggered. On failure, L{is_active} will return C{False}. + + (Since 1.4) If C{event} is C{None}, this method will not return until + negotation is done. On success, the method returns normally. + Otherwise an SSHException is raised. + + After a successful negotiation, you will usually want to authenticate, + calling L{auth_password <Transport.auth_password>} or + L{auth_publickey <Transport.auth_publickey>}. + + @note: L{connect} is a simpler method for connecting as a client. + + @note: After calling this method (or L{start_server} or L{connect}), + you should no longer directly read from or write to the original + socket object. + + @param event: an event to trigger when negotiation is complete + (optional) + @type event: threading.Event + + @raise SSHException: if negotiation fails (and no C{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() + 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.isSet(): + break + + def start_server(self, event=None, server=None): + """ + Negotiate a new SSH2 session as a server. This is the first step after + creating a new L{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 C{Event} will + be triggered. On failure, L{is_active} will return C{False}. + + (Since 1.4) If C{event} is C{None}, this method will not return until + negotation 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 + L{get_allowed_auths <ServerInterface.get_allowed_auths>}, + L{check_auth_none <ServerInterface.check_auth_none>}, + L{check_auth_password <ServerInterface.check_auth_password>}, and + L{check_auth_publickey <ServerInterface.check_auth_publickey>} in the + given C{server} object to control the authentication process. + + After a successful authentication, the client should request to open + a channel. Override + L{check_channel_request <ServerInterface.check_channel_request>} in the + given C{server} object to allow channels to be opened. + + @note: After calling this method (or L{start_client} or L{connect}), + you should no longer directly read from or write to the original + socket object. + + @param event: an event to trigger when negotiation is complete. + @type event: threading.Event + @param server: an object used to perform authentication and create + L{Channel}s. + @type server: L{server.ServerInterface} + + @raise SSHException: if negotiation fails (and no C{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.isSet(): + break + + def add_server_key(self, 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 key: the host key to add, usually an L{RSAKey <rsakey.RSAKey>} or + L{DSSKey <dsskey.DSSKey>}. + @type key: L{PKey <pkey.PKey>} + """ + self.server_key_dict[key.get_name()] = key + + def get_server_key(self): + """ + 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 L{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, C{None} is returned. In client mode, the behavior is undefined. + + @return: host key of the type negotiated by the client, or C{None}. + @rtype: L{PKey <pkey.PKey>} + """ + try: + return self.server_key_dict[self.host_key_type] + except KeyError: + return None + + def load_server_moduli(filename=None): + """ + I{(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 C{/etc/ssh/moduli}). + If you call C{load_server_moduli} and it returns C{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 filename: optional path to the moduli file, if you happen to + know that it's not in a standard location. + @type filename: str + @return: True if a moduli file was successfully loaded; False + otherwise. + @rtype: bool + + @since: doduo + + @note: This has no effect when used in client mode. + """ + Transport._modulus_pack = ModulusPack(randpool) + # 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 + load_server_moduli = staticmethod(load_server_moduli) + + def close(self): + """ + Close this session, and any open channels that are tied to it. + """ + self.active = False + # since this may be called from __del__, can't assume any attributes exist + try: + self.packetizer.close() + for chan in self.channels.values(): + chan._unlink() + except AttributeError: + pass + + def get_remote_server_key(self): + """ + 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 + L{PKey.get_name <pkey.PKey.get_name>} for the key type, and + C{str(key)} for the key string. + + @raise SSHException: if no session is currently active. + + @return: public key of the remote server. + @rtype: L{PKey <pkey.PKey>} + """ + if (not self.active) or (not self.initial_kex_done): + raise SSHException('No existing session') + return self.host_key + + def is_active(self): + """ + Return true if this session is active (open). + + @return: True if the session is still active (open); False if the + session is closed. + @rtype: bool + """ + return self.active + + def open_session(self): + """ + Request a new channel to the server, of type C{"session"}. This + is just an alias for C{open_channel('session')}. + + @return: a new L{Channel} on success, or C{None} if the request is + rejected or the session ends prematurely. + @rtype: L{Channel} + """ + return self.open_channel('session') + + def open_channel(self, kind, dest_addr=None, src_addr=None): + """ + Request a new channel to the server. L{Channel}s are socket-like + objects used for the actual transfer of data across the session. + You may only request a channel after negotiating encryption (using + L{connect} or L{start_client}) and authenticating. + + @param kind: the kind of channel requested (usually C{"session"}, + C{"forwarded-tcpip"} or C{"direct-tcpip"}). + @type kind: str + @param dest_addr: the destination address of this port forwarding, + if C{kind} is C{"forwarded-tcpip"} or C{"direct-tcpip"} (ignored + for other channel types). + @type dest_addr: (str, int) + @param src_addr: the source address of this port forwarding, if + C{kind} is C{"forwarded-tcpip"} or C{"direct-tcpip"}. + @type src_addr: (str, int) + @return: a new L{Channel} on success, or C{None} if the request is + rejected or the session ends prematurely. + @rtype: L{Channel} + """ + chan = None + if not self.active: + # don't bother trying to allocate a channel + return None + self.lock.acquire() + try: + chanid = self.channel_counter + while self.channels.has_key(chanid): + self.channel_counter = (self.channel_counter + 1) & 0xffffff + chanid = self.channel_counter + self.channel_counter = (self.channel_counter + 1) & 0xffffff + m = Message() + m.add_byte(chr(MSG_CHANNEL_OPEN)) + m.add_string(kind) + m.add_int(chanid) + m.add_int(self.window_size) + m.add_int(self.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]) + self.channels[chanid] = chan = Channel(chanid) + self.channel_events[chanid] = event = threading.Event() + self.channels_seen[chanid] = True + chan._set_transport(self) + chan._set_window(self.window_size, self.max_packet_size) + finally: + self.lock.release() + self._send_user_message(m) + while 1: + event.wait(0.1); + if not self.active: + return None + if event.isSet(): + break + try: + self.lock.acquire() + if not self.channels.has_key(chanid): + chan = None + finally: + self.lock.release() + return chan + + def open_sftp_client(self): + """ + 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 L{SFTPClient} object, referring to an sftp session + (channel) across this transport + @rtype: L{SFTPClient} + """ + return SFTPClient.from_transport(self) + + def send_ignore(self, bytes=None): + """ + 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 bytes: the number of random bytes to send in the payload of the + ignored packet -- defaults to a random number from 10 to 41. + @type bytes: int + + @since: fearow + """ + m = Message() + m.add_byte(chr(MSG_IGNORE)) + randpool.stir() + if bytes is None: + bytes = (ord(randpool.get_bytes(1)) % 32) + 10 + m.add_bytes(randpool.get_bytes(bytes)) + self._send_user_message(m) + + def renegotiate_keys(self): + """ + 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, or the + session has died mid-negotiation. + + @return: True if the renegotiation was successful, and the link is + using new keys; False if the session dropped during renegotiation. + @rtype: bool + """ + self.completion_event = threading.Event() + self._send_kex_init() + while 1: + self.completion_event.wait(0.1); + if not self.active: + return False + if self.completion_event.isSet(): + break + return True + + def set_keepalive(self, interval): + """ + Turn on/off keepalive packets (default is off). If this is set, after + C{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 interval: seconds to wait before sending a keepalive packet (or + 0 to disable keepalives). + @type interval: int + + @since: fearow + """ + self.packetizer.set_keepalive(interval, + lambda x=self: x.global_request('keepalive@lag.net', wait=False)) + + def global_request(self, kind, data=None, wait=True): + """ + Make a global request to the remote host. These are normally + extensions to the SSH2 protocol. + + @param kind: name of the request. + @type kind: str + @param data: an optional tuple containing additional data to attach + to the request. + @type data: tuple + @param wait: C{True} if this method should not return until a response + is received; C{False} otherwise. + @type wait: bool + @return: a L{Message} containing possible additional data if the + request was successful (or an empty L{Message} if C{wait} was + C{False}); C{None} if the request was denied. + @rtype: L{Message} + + @since: fearow + """ + if wait: + self.completion_event = threading.Event() + m = Message() + m.add_byte(chr(MSG_GLOBAL_REQUEST)) + m.add_string(kind) + m.add_boolean(wait) + if data is not None: + m.add(*data) + self._log(DEBUG, 'Sending global request "%s"' % 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.isSet(): + break + return self.global_response + + def accept(self, timeout=None): + """ + Return the next channel opened by the client over this transport, in + server mode. If no channel is opened before the given timeout, C{None} + is returned. + + @param timeout: seconds to wait for a channel, or C{None} to wait + forever + @type timeout: int + @return: a new Channel opened by the client + @rtype: L{Channel} + """ + 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): + """ + 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 L{start_client}, L{get_remote_server_key}, and + L{Transport.auth_password} or L{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 L{open_channel} or + L{open_session} to get a L{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 L{open_channel} or L{open_session} call may + fail because you haven't authenticated yet. + + @param hostkey: the host key expected from the server, or C{None} if + you don't want to do host key verification. + @type hostkey: L{PKey<pkey.PKey>} + @param username: the username to authenticate as. + @type username: str + @param password: a password to use for authentication, if you want to + use password authentication; otherwise C{None}. + @type password: str + @param pkey: a private key to use for authentication, if you want to + use private key authentication; otherwise C{None}. + @type pkey: L{PKey<pkey.PKey>} + + @raise SSHException: if the SSH2 negotiation fails, the host key + supplied by the server is incorrect, or authentication fails. + + @since: doduo + """ + if hostkey is not None: + self._preferred_keys = [ hostkey.get_name() ] + + self.start_client() + + # check host key if we were given one + if (hostkey is not None): + key = self.get_remote_server_key() + if (key.get_name() != hostkey.get_name()) or (str(key) != str(hostkey)): + self._log(DEBUG, 'Bad host key from server') + self._log(DEBUG, 'Expected: %s: %s' % (hostkey.get_name(), repr(str(hostkey)))) + self._log(DEBUG, 'Got : %s: %s' % (key.get_name(), repr(str(key)))) + raise SSHException('Bad host key from server') + self._log(DEBUG, 'Host key verified (%s)' % hostkey.get_name()) + + if (pkey is not None) or (password is not None): + if password is not None: + self._log(DEBUG, 'Attempting password auth...') + self.auth_password(username, password) + else: + self._log(DEBUG, 'Attempting public-key auth...') + self.auth_publickey(username, pkey) + + return + + def get_exception(self): + """ + Return any exception that happened during the last server request. + This can be used to fetch more specific error information after using + calls like L{start_client}. The exception (if any) is cleared after + this call. + + @return: an exception, or C{None} if there is no stored exception. + @rtype: Exception + + @since: 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, *larg, **kwarg): + """ + 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 L{SubsystemHandler} for more + detailed documentation. + + Any extra parameters (including keyword arguments) are saved and + passed to the L{SubsystemHandler} constructor later. + + @param name: name of the subsystem. + @type name: str + @param handler: subclass of L{SubsystemHandler} that handles this + subsystem. + @type handler: class + """ + try: + self.lock.acquire() + self.subsystem_table[name] = (handler, larg, kwarg) + finally: + self.lock.release() + + def is_authenticated(self): + """ + 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. + @rtype: bool + """ + return self.active and (self.auth_handler is not None) and self.auth_handler.is_authenticated() + + def get_username(self): + """ + Return the username this connection is authenticated for. If the + session is not authenticated (or authentication failed), this method + returns C{None}. + + @return: username that was authenticated, or C{None}. + @rtype: string + + @since: fearow + """ + if not self.active or (self.auth_handler is None): + return None + return self.auth_handler.get_username() + + def auth_none(self, username): + """ + 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 + L{BadAuthenticationType} exception raised. + + @param username: the username to authenticate as + @type username: string + @return: list of auth types permissible for the next stage of + authentication (normally empty) + @rtype: list + + @raise BadAuthenticationType: if "none" authentication isn't allowed + by the server for this user + @raise SSHException: if the authentication failed due to a network + error + + @since: 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): + """ + Authenticate to the server using a password. The username and password + are sent over an encrypted link. + + If an C{event} is passed in, this method will return immediately, and + the event will be triggered once authentication succeeds or fails. On + success, L{is_authenticated} will return C{True}. On failure, you may + use L{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 C{fallback} is C{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 username: the username to authenticate as + @type username: string + @param password: the password to authenticate with + @type password: string + @param event: an event to trigger when the authentication attempt is + complete (whether it was successful or not) + @type event: threading.Event + @param fallback: C{True} if an attempt at an automated "interactive" + password auth should be made if the server doesn't support normal + password auth + @type fallback: bool + @return: list of auth types permissible for the next stage of + authentication (normally empty) + @rtype: list + + @raise BadAuthenticationType: if password authentication isn't + allowed by the server for this user (and no event was passed in) + @raise SSHException: if the authentication failed (and no event was + passed in) + """ + 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, x: + # if password auth isn't allowed, but keyboard-interactive *is*, try to fudge it + if not fallback or not 'keyboard-interactive' in x.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, ignored: + # attempt failed; just raise the original exception + raise x + + def auth_publickey(self, username, key, event=None): + """ + 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 C{event} is passed in, this method will return immediately, and + the event will be triggered once authentication succeeds or fails. On + success, L{is_authenticated} will return C{True}. On failure, you may + use L{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 username: the username to authenticate as + @type username: string + @param key: the private key to authenticate with + @type key: L{PKey <pkey.PKey>} + @param event: an event to trigger when the authentication attempt is + complete (whether it was successful or not) + @type event: threading.Event + @return: list of auth types permissible for the next stage of + authentication (normally empty). + @rtype: list + + @raise BadAuthenticationType: if public-key authentication isn't + allowed by the server for this user (and no event was passed in). + @raise SSHException: if the authentication failed (and no event was + passed in). + """ + 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=''): + """ + 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: C{handler(title, instructions, prompt_list)}. The C{title} is + meant to be a dialog-window title, and the C{instructions} are user + instructions (both are strings). C{prompt_list} will be a list of + prompts, each prompt being a tuple of C{(str, bool)}. The string is + the prompt and the boolean indicates whether the user text should be + echoed. + + A sample call would thus be: + C{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 username: the username to authenticate as + @type username: string + @param handler: a handler for responding to server questions + @type handler: callable + @param submethods: a string list of desired submethods (optional) + @type submethods: str + @return: list of auth types permissible for the next stage of + authentication (normally empty). + @rtype: list + + @raise BadAuthenticationType: if public-key authentication isn't + allowed by the server for this user + @raise SSHException: if the authentication failed + + @since: 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 set_log_channel(self, name): + """ + Set the channel for this transport's logging. The default is + C{"paramiko.transport"} but it can be set to anything you want. + (See the C{logging} module for more info.) SSH Channels will log + to a sub-channel of the one specified. + + @param name: new channel name for logging. + @type name: str + + @since: 1.1 + """ + self.log_name = name + self.logger = util.get_logger(name) + + def get_log_channel(self): + """ + Return the channel name used for this transport's logging. + + @return: channel name. + @rtype: str + + @since: 1.2 + """ + return self.log_name + + def set_hexdump(self, 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 hexdump: C{True} to log protocol traffix (in hex) to the log; + C{False} otherwise. + @type hexdump: bool + """ + self.packetizer.set_hexdump(hexdump) + + def get_hexdump(self): + """ + Return C{True} if the transport is currently logging hex dumps of + protocol traffic. + + @return: C{True} if hex dumps are being logged + @rtype: bool + + @since: 1.4 + """ + return self.packetizer.get_hexdump() + + def use_compression(self, compress=True): + """ + Turn on/off compression. This will only have an affect before starting + the transport (ie before calling L{connect}, etc). By default, + compression is off since it negatively affects interactive sessions + and is not fully tested. + + @param compress: C{True} to ask the remote client/server to compress + traffic; C{False} to refuse compression + @type compress: bool + + @since: 1.5.2 + """ + if compress: + self._preferred_compression = ( 'zlib@openssh.com', 'zlib', 'none' ) + else: + self._preferred_compression = ( 'none', ) + + def stop_thread(self): + self.active = False + self.packetizer.close() + + + ### internals... + + + def _log(self, level, msg): + if issubclass(type(msg), list): + for m in msg: + self.logger.log(level, m) + else: + self.logger.log(level, msg) + + def _get_modulus_pack(self): + "used by KexGex to find primes for group exchange" + return self._modulus_pack + + def _unlink_channel(self, chanid): + "used by a Channel to remove itself from the active channel list" + try: + self.lock.acquire() + if self.channels.has_key(chanid): + del self.channels[chanid] + finally: + self.lock.release() + + def _send_message(self, data): + self.packetizer.send_message(data) + + def _send_user_message(self, data): + """ + send a message, but block if we're in key negotiation. this is used + for user-initiated requests. + """ + while True: + self.clear_to_send.wait(0.1) + if not self.active: + self._log(DEBUG, 'Dropping user packet because connection is dead.') + return + self.clear_to_send_lock.acquire() + if self.clear_to_send.isSet(): + break + self.clear_to_send_lock.release() + try: + self._send_message(data) + finally: + self.clear_to_send_lock.release() + + def _set_K_H(self, k, h): + "used by a kex object to set the K (root key) and H (exchange hash)" + self.K = k + self.H = h + if self.session_id == None: + self.session_id = h + + def _expect_packet(self, type): + "used by a kex object to register the next packet type it expects to see" + self.expected_packet = type + + def _verify_key(self, host_key, sig): + 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 (%s) failed. Boo. Robey should debug this.' % self.host_key_type) + self.host_key = key + + def _compute_key(self, id, nbytes): + "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(id) + m.add_bytes(self.session_id) + out = sofar = SHA.new(str(m)).digest() + while len(out) < nbytes: + m = Message() + m.add_mpint(self.K) + m.add_bytes(self.H) + m.add_bytes(sofar) + hash = SHA.new(str(m)).digest() + out += hash + sofar += hash + return out[:nbytes] + + def _get_cipher(self, name, key, iv): + if not self._cipher_info.has_key(name): + raise SSHException('Unknown client cipher ' + name) + return self._cipher_info[name]['class'].new(key, self._cipher_info[name]['mode'], iv) + + def run(self): + # (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.) + + # active=True occurs before the thread is launched, to avoid a race + _active_threads.append(self) + if self.server_mode: + self._log(DEBUG, 'starting thread (server mode): %s' % hex(long(id(self)) & 0xffffffffL)) + else: + self._log(DEBUG, 'starting thread (client mode): %s' % hex(long(id(self)) & 0xffffffffL)) + try: + self.packetizer.write_all(self.local_version + '\r\n') + self._check_banner() + self._send_kex_init() + self.expected_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) + self.active = False + self.packetizer.close() + break + elif ptype == MSG_DEBUG: + self._parse_debug(m) + continue + if self.expected_packet != 0: + if ptype != self.expected_packet: + raise SSHException('Expecting packet %d, got %d' % (self.expected_packet, ptype)) + self.expected_packet = 0 + if (ptype >= 30) and (ptype <= 39): + self.kex_engine.parse_next(ptype, m) + continue + + if self._handler_table.has_key(ptype): + self._handler_table[ptype](self, m) + elif self._channel_handler_table.has_key(ptype): + chanid = m.get_int() + if self.channels.has_key(chanid): + self._channel_handler_table[ptype](self.channels[chanid], m) + elif self.channels_seen.has_key(chanid): + self._log(DEBUG, 'Ignoring message for dead channel %d' % chanid) + else: + self._log(ERROR, 'Channel request for unknown channel %d' % chanid) + self.active = False + self.packetizer.close() + elif (self.auth_handler is not None) and self.auth_handler._handler_table.has_key(ptype): + self.auth_handler._handler_table[ptype](self.auth_handler, m) + else: + self._log(WARNING, 'Oops, unhandled type %d' % ptype) + msg = Message() + msg.add_byte(chr(MSG_UNIMPLEMENTED)) + msg.add_int(m.seqno) + self._send_message(msg) + except SSHException, e: + self._log(ERROR, 'Exception: ' + str(e)) + self._log(ERROR, util.tb_strings()) + self.saved_exception = e + except EOFError, e: + self._log(DEBUG, 'EOF in transport thread') + #self._log(DEBUG, util.tb_strings()) + self.saved_exception = e + except socket.error, e: + if type(e.args) is tuple: + emsg = '%s (%d)' % (e.args[1], e.args[0]) + else: + emsg = e.args + self._log(ERROR, 'Socket exception: ' + emsg) + self.saved_exception = e + except Exception, 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 self.channels.values(): + chan._unlink() + if self.active: + self.active = False + self.packetizer.close() + if self.completion_event != 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() + self.sock.close() + + + ### protocol stages + + + def _negotiate_keys(self, m): + # 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 == None: + # remote side wants to renegotiate + self._send_kex_init() + self._parse_kex_init(m) + self.kex_engine.start_kex() + + def _check_banner(self): + # this is slow, but we only have to do it once + for i in range(5): + # give them 5 seconds for the first line, then just 2 seconds each additional line + if i == 0: + timeout = 5 + else: + timeout = 2 + try: + buffer = self.packetizer.readline(timeout) + except Exception, x: + raise SSHException('Error reading SSH protocol banner' + str(x)) + if buffer[:4] == 'SSH-': + break + self._log(DEBUG, 'Banner: ' + buffer) + if buffer[:4] != 'SSH-': + raise SSHException('Indecipherable protocol version "' + buffer + '"') + # save this server version string for later + self.remote_version = buffer + # pull off any attached comment + comment = '' + i = string.find(buffer, ' ') + if i >= 0: + comment = buffer[i+1:] + buffer = buffer[:i] + # parse out version string and make sure it matches + segs = buffer.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': + raise SSHException('Incompatible version (%s instead of 2.0)' % (version,)) + self._log(INFO, 'Connected (version %s, client %s)' % (version, client)) + + def _send_kex_init(self): + """ + 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.in_kex = True + if self.server_mode: + if (self._modulus_pack is None) and ('diffie-hellman-group-exchange-sha1' in self._preferred_kex): + # can't do group-exchange if we don't have a pack of potential primes + pkex = list(self.get_security_options().kex) + pkex.remove('diffie-hellman-group-exchange-sha1') + self.get_security_options().kex = pkex + available_server_keys = filter(self.server_key_dict.keys().__contains__, + self._preferred_keys) + else: + available_server_keys = self._preferred_keys + + randpool.stir() + m = Message() + m.add_byte(chr(MSG_KEXINIT)) + m.add_bytes(randpool.get_bytes(16)) + m.add_list(self._preferred_kex) + 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('') + m.add_string('') + m.add_boolean(False) + m.add_int(0) + # save a copy for later (needed to compute a hash) + self.local_kex_init = str(m) + self._send_message(m) + + def _parse_kex_init(self, m): + cookie = m.get_bytes(16) + kex_algo_list = m.get_list() + server_key_algo_list = m.get_list() + client_encrypt_algo_list = m.get_list() + server_encrypt_algo_list = m.get_list() + client_mac_algo_list = m.get_list() + server_mac_algo_list = m.get_list() + client_compress_algo_list = m.get_list() + server_compress_algo_list = m.get_list() + client_lang_list = m.get_list() + server_lang_list = m.get_list() + kex_follows = m.get_boolean() + unused = m.get_int() + + self._log(DEBUG, 'kex algos:' + str(kex_algo_list) + ' server key:' + str(server_key_algo_list) + \ + ' client encrypt:' + str(client_encrypt_algo_list) + \ + ' server encrypt:' + str(server_encrypt_algo_list) + \ + ' client mac:' + str(client_mac_algo_list) + \ + ' server mac:' + str(server_mac_algo_list) + \ + ' client compress:' + str(client_compress_algo_list) + \ + ' server compress:' + str(server_compress_algo_list) + \ + ' client lang:' + str(client_lang_list) + \ + ' server lang:' + str(server_lang_list) + \ + ' kex follows?' + str(kex_follows)) + + # 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 = filter(self._preferred_kex.__contains__, kex_algo_list) + else: + agreed_kex = filter(kex_algo_list.__contains__, self._preferred_kex) + if len(agreed_kex) == 0: + raise SSHException('Incompatible ssh peer (no acceptable kex algorithm)') + self.kex_engine = self._kex_info[agreed_kex[0]](self) + + if self.server_mode: + available_server_keys = filter(self.server_key_dict.keys().__contains__, + self._preferred_keys) + agreed_keys = filter(available_server_keys.__contains__, server_key_algo_list) + else: + agreed_keys = filter(server_key_algo_list.__contains__, self._preferred_keys) + if len(agreed_keys) == 0: + raise SSHException('Incompatible ssh peer (no acceptable host key)') + self.host_key_type = agreed_keys[0] + if self.server_mode and (self.get_server_key() is None): + raise SSHException('Incompatible ssh peer (can\'t match requested host key type)') + + if self.server_mode: + agreed_local_ciphers = filter(self._preferred_ciphers.__contains__, + server_encrypt_algo_list) + agreed_remote_ciphers = filter(self._preferred_ciphers.__contains__, + client_encrypt_algo_list) + else: + agreed_local_ciphers = filter(client_encrypt_algo_list.__contains__, + self._preferred_ciphers) + agreed_remote_ciphers = filter(server_encrypt_algo_list.__contains__, + self._preferred_ciphers) + if (len(agreed_local_ciphers) == 0) or (len(agreed_remote_ciphers) == 0): + raise SSHException('Incompatible ssh server (no acceptable ciphers)') + self.local_cipher = agreed_local_ciphers[0] + self.remote_cipher = agreed_remote_ciphers[0] + self._log(DEBUG, 'Ciphers agreed: local=%s, remote=%s' % (self.local_cipher, self.remote_cipher)) + + if self.server_mode: + agreed_remote_macs = filter(self._preferred_macs.__contains__, client_mac_algo_list) + agreed_local_macs = filter(self._preferred_macs.__contains__, server_mac_algo_list) + else: + agreed_local_macs = filter(client_mac_algo_list.__contains__, self._preferred_macs) + agreed_remote_macs = filter(server_mac_algo_list.__contains__, self._preferred_macs) + if (len(agreed_local_macs) == 0) or (len(agreed_remote_macs) == 0): + raise SSHException('Incompatible ssh server (no acceptable macs)') + self.local_mac = agreed_local_macs[0] + self.remote_mac = agreed_remote_macs[0] + + if self.server_mode: + agreed_remote_compression = filter(self._preferred_compression.__contains__, client_compress_algo_list) + agreed_local_compression = filter(self._preferred_compression.__contains__, server_compress_algo_list) + else: + agreed_local_compression = filter(client_compress_algo_list.__contains__, self._preferred_compression) + agreed_remote_compression = filter(server_compress_algo_list.__contains__, self._preferred_compression) + if (len(agreed_local_compression) == 0) or (len(agreed_remote_compression) == 0): + raise SSHException('Incompatible ssh server (no acceptable compression) %r %r %r' % (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(DEBUG, 'using kex %s; server key type %s; cipher: local %s, remote %s; mac: local %s, remote %s; compression: local %s, remote %s' % + (agreed_kex[0], self.host_key_type, self.local_cipher, self.remote_cipher, self.local_mac, + self.remote_mac, self.local_compression, self.remote_compression)) + + # 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 = chr(MSG_KEXINIT) + m.get_so_far() + + def _activate_inbound(self): + "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) + 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) + 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): + "switch on newly negotiated encryption parameters for outbound traffic" + m = Message() + m.add_byte(chr(MSG_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) + 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) + self.packetizer.set_outbound_cipher(engine, block_size, mac_engine, mac_size, mac_key) + 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 + # we always expect to receive NEWKEYS now + self.expected_packet = MSG_NEWKEYS + + def _auth_trigger(self): + 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_newkeys(self, m): + 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 != 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): + code = m.get_int() + desc = m.get_string() + self._log(INFO, 'Disconnect (code %d): %s' % (code, desc)) + + def _parse_global_request(self, m): + kind = m.get_string() + self._log(DEBUG, 'Received global request "%s"' % kind) + want_reply = m.get_boolean() + 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(chr(MSG_REQUEST_SUCCESS)) + msg.add(*extra) + else: + msg.add_byte(chr(MSG_REQUEST_FAILURE)) + self._send_message(msg) + + def _parse_request_success(self, m): + 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): + 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): + chanid = m.get_int() + server_chanid = m.get_int() + server_window_size = m.get_int() + server_max_packet_size = m.get_int() + if not self.channels.has_key(chanid): + self._log(WARNING, 'Success for unrequested channel! [??]') + return + self.lock.acquire() + try: + chan = self.channels[chanid] + chan._set_remote_channel(server_chanid, server_window_size, server_max_packet_size) + self._log(INFO, 'Secsh channel %d opened.' % chanid) + if self.channel_events.has_key(chanid): + self.channel_events[chanid].set() + del self.channel_events[chanid] + finally: + self.lock.release() + return + + def _parse_channel_open_failure(self, m): + chanid = m.get_int() + reason = m.get_int() + reason_str = m.get_string() + lang = m.get_string() + if CONNECTION_FAILED_CODE.has_key(reason): + reason_text = CONNECTION_FAILED_CODE[reason] + else: + reason_text = '(unknown code)' + self._log(INFO, 'Secsh channel %d open FAILED: %s: %s' % (chanid, reason_str, reason_text)) + try: + self.lock.aquire() + if self.channels.has_key(chanid): + del self.channels[chanid] + if self.channel_events.has_key(chanid): + self.channel_events[chanid].set() + del self.channel_events[chanid] + finally: + self.lock.release() + return + + def _parse_channel_open(self, m): + kind = m.get_string() + chanid = m.get_int() + initial_window_size = m.get_int() + max_packet_size = m.get_int() + reject = False + if not self.server_mode: + self._log(DEBUG, 'Rejecting "%s" channel request from server.' % kind) + reject = True + reason = OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED + else: + self.lock.acquire() + try: + my_chanid = self.channel_counter + while self.channels.has_key(my_chanid): + self.channel_counter = (self.channel_counter + 1) & 0xffffff + my_chanid = self.channel_counter + self.channel_counter = (self.channel_counter + 1) & 0xffffff + finally: + self.lock.release() + reason = self.server_object.check_channel_request(kind, my_chanid) + if reason != OPEN_SUCCEEDED: + self._log(DEBUG, 'Rejecting "%s" channel request from client.' % kind) + reject = True + if reject: + msg = Message() + msg.add_byte(chr(MSG_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) + try: + self.lock.acquire() + self.channels[my_chanid] = chan + self.channels_seen[my_chanid] = True + chan._set_transport(self) + chan._set_window(self.window_size, self.max_packet_size) + chan._set_remote_channel(chanid, initial_window_size, max_packet_size) + finally: + self.lock.release() + m = Message() + m.add_byte(chr(MSG_CHANNEL_OPEN_SUCCESS)) + m.add_int(chanid) + m.add_int(my_chanid) + m.add_int(self.window_size) + m.add_int(self.max_packet_size) + self._send_message(m) + self._log(INFO, 'Secsh channel %d opened.' % my_chanid) + try: + self.lock.acquire() + self.server_accepts.append(chan) + self.server_accept_cv.notify() + finally: + self.lock.release() + + def _parse_debug(self, m): + always_display = m.get_boolean() + msg = m.get_string() + lang = m.get_string() + self._log(DEBUG, 'Debug msg: ' + util.safe_string(msg)) + + def _get_subsystem_handler(self, name): + try: + self.lock.acquire() + if not self.subsystem_table.has_key(name): + return (None, [], {}) + return self.subsystem_table[name] + finally: + self.lock.release() + + _handler_table = { + 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, + } |