summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorJeff Forcier <jeff@bitprophet.org>2015-09-30 14:53:09 -0700
committerJeff Forcier <jeff@bitprophet.org>2015-09-30 14:53:09 -0700
commitc0f1da60bd7da55bf7a2051a329192c1c018f099 (patch)
tree86927ac72ee1adef71db6f68fae2d028d1d59953
parente520165eeec1b7c2d165ab611f7947b6c8d4da0f (diff)
parent57106d04def84ca1d9dd23c4d85b2ba9242556ff (diff)
Merge branch '1.15'
-rw-r--r--.travis.yml4
-rw-r--r--paramiko/client.py2
-rw-r--r--paramiko/packet.py45
-rw-r--r--paramiko/transport.py27
-rw-r--r--sites/www/changelog.rst4
-rw-r--r--tests/test_transport.py17
6 files changed, 91 insertions, 8 deletions
diff --git a/.travis.yml b/.travis.yml
index 9a55dbb6..45fb1722 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -13,8 +13,8 @@ install:
- pip install coveralls # For coveralls.io specifically
- pip install -r dev-requirements.txt
script:
- # Main tests, with coverage!
- - inv test --coverage
+ # Main tests, w/ coverage! (but skip coverage on 3.2, coverage.py dropped it)
+ - "[[ $TRAVIS_PYTHON_VERSION != 3.2 ]] && inv test --coverage || inv test"
# Ensure documentation & invoke pipeline run OK.
# Run 'docs' first since its objects.inv is referred to by 'www'.
# Also force warnings to be errors since most of them tend to be actual
diff --git a/paramiko/client.py b/paramiko/client.py
index e10e9da7..5a215a81 100644
--- a/paramiko/client.py
+++ b/paramiko/client.py
@@ -397,7 +397,7 @@ class SSHClient (ClosingContextManager):
:raises SSHException: if the server fails to execute the command
"""
- chan = self._transport.open_session()
+ chan = self._transport.open_session(timeout=timeout)
if get_pty:
chan.get_pty()
chan.settimeout(timeout)
diff --git a/paramiko/packet.py b/paramiko/packet.py
index f516ff9b..b922000c 100644
--- a/paramiko/packet.py
+++ b/paramiko/packet.py
@@ -99,6 +99,10 @@ class Packetizer (object):
self.__keepalive_last = time.time()
self.__keepalive_callback = None
+ self.__timer = None
+ self.__handshake_complete = False
+ self.__timer_expired = False
+
def set_log(self, log):
"""
Set the Python log object to use for logging.
@@ -182,6 +186,45 @@ class Packetizer (object):
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 time out 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.
@@ -200,6 +243,8 @@ class Packetizer (object):
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:
diff --git a/paramiko/transport.py b/paramiko/transport.py
index 36da3043..31c27a2f 100644
--- a/paramiko/transport.py
+++ b/paramiko/transport.py
@@ -295,6 +295,8 @@ class Transport (threading.Thread, ClosingContextManager):
self.global_response = None # response Message from an arbitrary global request
self.completion_event = None # user-defined event callbacks
self.banner_timeout = 15 # how long (seconds) to wait for the SSH banner
+ self.handshake_timeout = 15 # how long (seconds) to wait for the handshake to finish after SSH banner sent.
+
# server mode:
self.server_mode = False
@@ -587,7 +589,7 @@ class Transport (threading.Thread, ClosingContextManager):
"""
return self.active
- def open_session(self, window_size=None, max_packet_size=None):
+ 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
@@ -612,7 +614,8 @@ class Transport (threading.Thread, ClosingContextManager):
"""
return self.open_channel('session',
window_size=window_size,
- max_packet_size=max_packet_size)
+ max_packet_size=max_packet_size,
+ timeout=timeout)
def open_x11_channel(self, src_addr=None):
"""
@@ -659,7 +662,8 @@ class Transport (threading.Thread, ClosingContextManager):
dest_addr=None,
src_addr=None,
window_size=None,
- max_packet_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
@@ -683,17 +687,20 @@ class Transport (threading.Thread, ClosingContextManager):
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 or the session ends
- prematurely
+ :raises SSHException: if the request is rejected, the session ends
+ prematurely or there is a timeout openning 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 = 3600 if timeout is None else timeout
self.lock.acquire()
try:
window_size = self._sanitize_window_size(window_size)
@@ -722,6 +729,7 @@ class Transport (threading.Thread, ClosingContextManager):
finally:
self.lock.release()
self._send_user_message(m)
+ start_ts = time.time()
while True:
event.wait(0.1)
if not self.active:
@@ -731,6 +739,8 @@ class Transport (threading.Thread, ClosingContextManager):
raise e
if event.is_set():
break
+ elif start_ts + timeout < time.time():
+ raise SSHException('Timeout openning channel.')
chan = self._channels.get(chanid)
if chan is not None:
return chan
@@ -1582,6 +1592,12 @@ class Transport (threading.Thread, ClosingContextManager):
try:
self.packetizer.write_all(b(self.local_version + '\r\n'))
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)
@@ -1631,6 +1647,7 @@ class Transport (threading.Thread, ClosingContextManager):
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: ' + str(e))
self._log(ERROR, util.tb_strings())
diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst
index fb09b7a1..bbc89b75 100644
--- a/sites/www/changelog.rst
+++ b/sites/www/changelog.rst
@@ -2,6 +2,10 @@
Changelog
=========
+* :bug:`491` (combines :issue:`62` and :issue:`439`) Implement timeout
+ functionality to address hangs from dropped network connections and/or failed
+ handshakes. Credit to ``@vazir`` and ``@dacut`` for the original patches and
+ to Olle Lundberg for reimplementation.
* :bug:`490` Skip invalid/unparseable lines in ``known_hosts`` files, instead
of raising `SSHException`. This brings Paramiko's behavior more in line with
OpenSSH, which silently ignores such input. Catch & patch courtesy of Martin
diff --git a/tests/test_transport.py b/tests/test_transport.py
index 5cf9a867..3c8ad81e 100644
--- a/tests/test_transport.py
+++ b/tests/test_transport.py
@@ -792,3 +792,20 @@ class TransportTest(unittest.TestCase):
(None, DEFAULT_WINDOW_SIZE),
(2**32, MAX_WINDOW_SIZE)]:
self.assertEqual(self.tc._sanitize_window_size(val), correct)
+
+ def test_L_handshake_timeout(self):
+ """
+ verify that we can get a hanshake timeout.
+ """
+ host_key = RSAKey.from_private_key_file(test_path('test_rsa.key'))
+ public_host_key = RSAKey(data=host_key.asbytes())
+ self.ts.add_server_key(host_key)
+ event = threading.Event()
+ server = NullServer()
+ self.assertTrue(not event.is_set())
+ self.tc.handshake_timeout = 0.000000000001
+ self.ts.start_server(event, server)
+ self.assertRaises(EOFError, self.tc.connect,
+ hostkey=public_host_key,
+ username='slowdive',
+ password='pygmalion')