diff options
Diffstat (limited to 'paramiko/transport.py')
-rw-r--r-- | paramiko/transport.py | 62 |
1 files changed, 57 insertions, 5 deletions
diff --git a/paramiko/transport.py b/paramiko/transport.py index 711f395e..39c6eb89 100644 --- a/paramiko/transport.py +++ b/paramiko/transport.py @@ -1,4 +1,5 @@ # Copyright (C) 2003-2007 Robey Pointer <robeypointer@gmail.com> +# Copyright (C) 2003-2007 Robey Pointer <robeypointer@gmail.com> # # This file is part of paramiko. # @@ -49,7 +50,7 @@ from paramiko.common import ( 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, + DEFAULT_WINDOW_SIZE, DEFAULT_MAX_PACKET_SIZE, HIGHEST_USERAUTH_MESSAGE_ID, ) from paramiko.compress import ZlibCompressor, ZlibDecompressor from paramiko.dsskey import DSSKey @@ -443,7 +444,7 @@ class Transport(threading.Thread, ClosingContextManager): # We need the FQDN to get this working with SSPI self.gss_host = socket.getfqdn(gss_host) - def start_client(self, event=None): + def start_client(self, event=None, timeout=None): """ 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 @@ -454,7 +455,7 @@ class Transport(threading.Thread, ClosingContextManager): be triggered. On failure, `is_active` will return ``False``. (Since 1.4) If ``event`` is ``None``, this method will not return until - negotation is done. On success, the method returns normally. + negotiation is done. On success, the method returns normally. Otherwise an SSHException is raised. After a successful negotiation, you will usually want to authenticate, @@ -471,6 +472,9 @@ class Transport(threading.Thread, ClosingContextManager): :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) @@ -485,6 +489,7 @@ class Transport(threading.Thread, ClosingContextManager): # 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: @@ -492,7 +497,10 @@ class Transport(threading.Thread, ClosingContextManager): if e is not None: raise e raise SSHException('Negotiation failed.') - if event.is_set(): + if ( + event.is_set() or + (timeout is not None and time.time() >= max_time) + ): break def start_server(self, event=None, server=None): @@ -1760,6 +1768,43 @@ class Transport(threading.Thread, ClosingContextManager): 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): + """ + 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() + 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 attemps 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): # (use the exposed "run" method, because if we specify a thread target @@ -1820,7 +1865,11 @@ class Transport(threading.Thread, ClosingContextManager): continue if ptype in self._handler_table: - self._handler_table[ptype](self, m) + 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) @@ -2082,6 +2131,9 @@ class Transport(threading.Thread, ClosingContextManager): 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)') # noqa + self._log_agreement( + 'HostKey', agreed_keys[0], agreed_keys[0] + ) if self.server_mode: agreed_local_ciphers = list(filter( |