summaryrefslogtreecommitdiffhomepage
path: root/paramiko/transport.py
diff options
context:
space:
mode:
Diffstat (limited to 'paramiko/transport.py')
-rw-r--r--paramiko/transport.py62
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(