aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJeremy T. Bouse <Jeremy.Bouse@UnderGrid.net>2015-10-25 22:29:44 -0400
committerJeremy T. Bouse <Jeremy.Bouse@UnderGrid.net>2015-10-25 22:29:44 -0400
commitad30777fe9b9b34797ba7487056966d27d890974 (patch)
treefd14ac1ad24a701986483860e1218e7cdc09c732
parent767153f789ed210fe00509f591b013320fc0f3fa (diff)
parentbf855e6da326dba0c46f005eedc9f390c6c3b206 (diff)
downloadpython-paramiko-ad30777fe9b9b34797ba7487056966d27d890974.tar
python-paramiko-ad30777fe9b9b34797ba7487056966d27d890974.tar.gz
Merge tag 'upstream/1.15.3'
Upstream version 1.15.3
-rw-r--r--.travis.yml4
-rw-r--r--README2
-rw-r--r--dev-requirements.txt8
-rw-r--r--paramiko/_version.py2
-rw-r--r--paramiko/_winapi.py6
-rw-r--r--paramiko/agent.py27
-rw-r--r--paramiko/auth_handler.py24
-rw-r--r--paramiko/channel.py2
-rw-r--r--paramiko/client.py2
-rw-r--r--paramiko/ecdsakey.py8
-rw-r--r--paramiko/hostkeys.py6
-rw-r--r--paramiko/kex_gss.py32
-rw-r--r--paramiko/message.py45
-rw-r--r--paramiko/packet.py45
-rw-r--r--paramiko/server.py2
-rw-r--r--paramiko/ssh_exception.py6
-rw-r--r--paramiko/ssh_gss.py33
-rw-r--r--paramiko/transport.py27
-rw-r--r--sites/www/changelog.rst32
-rw-r--r--sites/www/index.rst8
-rw-r--r--tasks.py29
-rw-r--r--tests/test_hostkeys.py1
-rw-r--r--tests/test_message.py8
-rw-r--r--tests/test_transport.py17
24 files changed, 231 insertions, 145 deletions
diff --git a/.travis.yml b/.travis.yml
index 9a55dbb..45fb172 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/README b/README
index b5ccb69..5751260 100644
--- a/README
+++ b/README
@@ -5,7 +5,7 @@ paramiko
:Paramiko: Python SSH module
:Copyright: Copyright (c) 2003-2009 Robey Pointer <robeypointer@gmail.com>
-:Copyright: Copyright (c) 2013-2014 Jeff Forcier <jeff@bitprophet.org>
+:Copyright: Copyright (c) 2013-2015 Jeff Forcier <jeff@bitprophet.org>
:License: LGPL
:Homepage: https://github.com/paramiko/paramiko/
:API docs: http://docs.paramiko.org
diff --git a/dev-requirements.txt b/dev-requirements.txt
index 7a0ccbc..059572c 100644
--- a/dev-requirements.txt
+++ b/dev-requirements.txt
@@ -1,9 +1,11 @@
# Older junk
tox>=1.4,<1.5
# For newer tasks like building Sphinx docs.
-invoke>=0.7.0,<0.8
-invocations>=0.5.0
+invoke>=0.10
+invocations>=0.9.2
sphinx>=1.1.3
alabaster>=0.6.1
releases>=0.5.2
-wheel==0.23.0
+semantic_version>=2.4,<2.5
+wheel==0.24
+twine==1.5
diff --git a/paramiko/_version.py b/paramiko/_version.py
index 3bf9dac..25aac14 100644
--- a/paramiko/_version.py
+++ b/paramiko/_version.py
@@ -1,2 +1,2 @@
-__version_info__ = (1, 15, 2)
+__version_info__ = (1, 15, 3)
__version__ = '.'.join(map(str, __version_info__))
diff --git a/paramiko/_winapi.py b/paramiko/_winapi.py
index f48e189..cf4d68e 100644
--- a/paramiko/_winapi.py
+++ b/paramiko/_winapi.py
@@ -106,7 +106,7 @@ MapViewOfFile.restype = ctypes.wintypes.HANDLE
class MemoryMap(object):
"""
- A memory map object which can have security attributes overrideden.
+ A memory map object which can have security attributes overridden.
"""
def __init__(self, name, length, security_attributes=None):
self.name = name
@@ -219,8 +219,8 @@ class SECURITY_ATTRIBUTES(ctypes.Structure):
@descriptor.setter
def descriptor(self, value):
- self._descriptor = descriptor
- self.lpSecurityDescriptor = ctypes.addressof(descriptor)
+ self._descriptor = value
+ self.lpSecurityDescriptor = ctypes.addressof(value)
def GetTokenInformation(token, information_class):
"""
diff --git a/paramiko/agent.py b/paramiko/agent.py
index a75ac59..6a8e7fb 100644
--- a/paramiko/agent.py
+++ b/paramiko/agent.py
@@ -32,7 +32,7 @@ from select import select
from paramiko.common import asbytes, io_sleep
from paramiko.py3compat import byte_chr
-from paramiko.ssh_exception import SSHException
+from paramiko.ssh_exception import SSHException, AuthenticationException
from paramiko.message import Message
from paramiko.pkey import PKey
from paramiko.util import retry_on_signal
@@ -109,9 +109,12 @@ class AgentProxyThread(threading.Thread):
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
- self.__addr = addr
+ self.__addr = addr # This should be an IP address as a string? or None
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 ?
@@ -287,6 +290,26 @@ class AgentServerProxy(AgentSSH):
class AgentRequestHandler(object):
+ """
+ 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
diff --git a/paramiko/auth_handler.py b/paramiko/auth_handler.py
index c001aee..ef4a8c7 100644
--- a/paramiko/auth_handler.py
+++ b/paramiko/auth_handler.py
@@ -34,7 +34,7 @@ from paramiko.common import cMSG_SERVICE_REQUEST, cMSG_DISCONNECT, \
cMSG_USERAUTH_GSSAPI_ERRTOK, cMSG_USERAUTH_GSSAPI_MIC,\
MSG_USERAUTH_GSSAPI_RESPONSE, MSG_USERAUTH_GSSAPI_TOKEN, \
MSG_USERAUTH_GSSAPI_EXCHANGE_COMPLETE, MSG_USERAUTH_GSSAPI_ERROR, \
- MSG_USERAUTH_GSSAPI_ERRTOK, MSG_USERAUTH_GSSAPI_MIC
+ MSG_USERAUTH_GSSAPI_ERRTOK, MSG_USERAUTH_GSSAPI_MIC, MSG_NAMES
from paramiko.message import Message
from paramiko.py3compat import bytestring
@@ -510,15 +510,11 @@ class AuthHandler (object):
result = AUTH_FAILED
self._send_auth_result(username, method, result)
raise
- if retval == 0:
- # 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)
- else:
- result = AUTH_FAILED
+ # 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)
elif method == "gssapi-keyex" and gss_auth:
mic_token = m.get_string()
sshgss = self.transport.kexgss_ctxt
@@ -534,12 +530,8 @@ class AuthHandler (object):
result = AUTH_FAILED
self._send_auth_result(username, method, result)
raise
- if retval == 0:
- result = AUTH_SUCCESSFUL
- self.transport.server_object.check_auth_gssapi_keyex(username,
- result)
- else:
- result = AUTH_FAILED
+ 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
diff --git a/paramiko/channel.py b/paramiko/channel.py
index 8a97c97..7e39a15 100644
--- a/paramiko/channel.py
+++ b/paramiko/channel.py
@@ -337,7 +337,7 @@ class Channel (ClosingContextManager):
further x11 requests can be made from the server to the client,
when an x11 application is run in a shell session.
- From RFC4254::
+ 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
diff --git a/paramiko/client.py b/paramiko/client.py
index 393e3e0..9ee3028 100644
--- a/paramiko/client.py
+++ b/paramiko/client.py
@@ -338,7 +338,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/ecdsakey.py b/paramiko/ecdsakey.py
index 6b04795..8827a1d 100644
--- a/paramiko/ecdsakey.py
+++ b/paramiko/ecdsakey.py
@@ -129,13 +129,11 @@ class ECDSAKey (PKey):
@staticmethod
def generate(curve=curves.NIST256p, progress_func=None):
"""
- Generate a new private RSA key. This factory function can be used to
+ Generate a new private ECDSA key. This factory function can be used to
generate a new host key or authentication key.
- :param function progress_func:
- an optional function to call at key points in key generation (used
- by ``pyCrypto.PublicKey``).
- :returns: A new private key (`.RSAKey`) object
+ :param function progress_func: Not used for this type of key.
+ :returns: A new private key (`.ECDSAKey`) object
"""
signing_key = SigningKey.generate(curve)
key = ECDSAKey(vals=(signing_key, signing_key.get_verifying_key()))
diff --git a/paramiko/hostkeys.py b/paramiko/hostkeys.py
index 8486887..c7e1f72 100644
--- a/paramiko/hostkeys.py
+++ b/paramiko/hostkeys.py
@@ -35,6 +35,7 @@ from paramiko.dsskey import DSSKey
from paramiko.rsakey import RSAKey
from paramiko.util import get_logger, constant_time_bytes_eq
from paramiko.ecdsakey import ECDSAKey
+from paramiko.ssh_exception import SSHException
class HostKeys (MutableMapping):
@@ -96,7 +97,10 @@ class HostKeys (MutableMapping):
line = line.strip()
if (len(line) == 0) or (line[0] == '#'):
continue
- e = HostKeyEntry.from_line(line, lineno)
+ try:
+ e = HostKeyEntry.from_line(line, lineno)
+ except SSHException:
+ continue
if e is not None:
_hostnames = e.hostnames
for h in _hostnames:
diff --git a/paramiko/kex_gss.py b/paramiko/kex_gss.py
index 4e8380e..69969f8 100644
--- a/paramiko/kex_gss.py
+++ b/paramiko/kex_gss.py
@@ -21,14 +21,15 @@
"""
-This module provides GSS-API / SSPI Key Exchange as defined in RFC 4462.
+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 <http://www.ietf.org/rfc/rfc4462.txt>`_ 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
+ `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`
@@ -36,6 +37,7 @@ This module provides GSS-API / SSPI Key Exchange as defined in RFC 4462.
.. versionadded:: 1.15
"""
+import os
from hashlib import sha1
from paramiko.common import *
@@ -55,8 +57,8 @@ c_MSG_KEXGSS_GROUPREQ, c_MSG_KEXGSS_GROUP = [byte_chr(c) for c in range(40, 42)]
class KexGSSGroup1(object):
"""
- GSS-API / SSPI Authenticated Diffie-Hellman Key Exchange
- as defined in `RFC 4462 Section 2 <http://www.ietf.org/rfc/rfc4462.txt>`_
+ 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
@@ -129,7 +131,7 @@ class KexGSSGroup1(object):
larger than q (but this is a tiny tiny subset of potential x).
"""
while 1:
- x_bytes = self.transport.rng.read(128)
+ x_bytes = os.urandom(128)
x_bytes = byte_mask(x_bytes[0], 0x7f) + x_bytes[1:]
if (x_bytes[:8] != self.b7fffffffffffffff) and \
(x_bytes[:8] != self.b0000000000000000):
@@ -278,8 +280,9 @@ class KexGSSGroup1(object):
class KexGSSGroup14(KexGSSGroup1):
"""
- GSS-API / SSPI Authenticated Diffie-Hellman Group14 Key Exchange
- as defined in `RFC 4462 Section 2 <http://www.ietf.org/rfc/rfc4462.txt>`_
+ 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
G = 2
@@ -288,8 +291,8 @@ class KexGSSGroup14(KexGSSGroup1):
class KexGSSGex(object):
"""
- GSS-API / SSPI Authenticated Diffie-Hellman Group Exchange
- as defined in `RFC 4462 Section 2 <http://www.ietf.org/rfc/rfc4462.txt>`_
+ 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
@@ -364,7 +367,7 @@ class KexGSSGex(object):
qhbyte <<= 1
qmask >>= 1
while True:
- x_bytes = self.transport.rng.read(byte_count)
+ 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):
@@ -590,8 +593,9 @@ class KexGSSGex(object):
class NullHostKey(object):
"""
- This class represents the Null Host Key for GSS-API Key Exchange
- as defined in `RFC 4462 Section 5 <http://www.ietf.org/rfc/rfc4462.txt>`_
+ 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 = ""
diff --git a/paramiko/message.py b/paramiko/message.py
index b893e76..bf4c6b9 100644
--- a/paramiko/message.py
+++ b/paramiko/message.py
@@ -129,7 +129,7 @@ class Message (object):
b = self.get_bytes(1)
return b != zero_byte
- def get_int(self):
+ def get_adaptive_int(self):
"""
Fetch an int from the stream.
@@ -141,20 +141,7 @@ class Message (object):
byte += self.get_bytes(3)
return struct.unpack('>I', byte)[0]
- def get_size(self):
- """
- Fetch an int from the stream.
-
- @return: a 32-bit unsigned integer.
- @rtype: 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_size(self):
+ def get_int(self):
"""
Fetch an int from the stream.
@@ -185,7 +172,7 @@ class Message (object):
contain unprintable characters. (It's not unheard of for a string to
contain another byte-stream message.)
"""
- return self.get_bytes(self.get_size())
+ return self.get_bytes(self.get_int())
def get_text(self):
"""
@@ -196,7 +183,7 @@ class Message (object):
@return: a string.
@rtype: string
"""
- return u(self.get_bytes(self.get_size()))
+ return u(self.get_bytes(self.get_int()))
#return self.get_bytes(self.get_size())
def get_binary(self):
@@ -208,7 +195,7 @@ class Message (object):
@return: a string.
@rtype: string
"""
- return self.get_bytes(self.get_size())
+ return self.get_bytes(self.get_int())
def get_list(self):
"""
@@ -248,7 +235,7 @@ class Message (object):
self.packet.write(zero_byte)
return self
- def add_size(self, n):
+ def add_int(self, n):
"""
Add an integer to the stream.
@@ -257,7 +244,7 @@ class Message (object):
self.packet.write(struct.pack('>I', n))
return self
- def add_int(self, n):
+ def add_adaptive_int(self, n):
"""
Add an integer to the stream.
@@ -270,20 +257,6 @@ class Message (object):
self.packet.write(struct.pack('>I', n))
return self
- def add_int(self, n):
- """
- Add an integer to the stream.
-
- @param n: integer to add
- @type n: int
- """
- 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.
@@ -310,7 +283,7 @@ class Message (object):
:param str s: string to add
"""
s = asbytes(s)
- self.add_size(len(s))
+ self.add_int(len(s))
self.packet.write(s)
return self
@@ -329,7 +302,7 @@ class Message (object):
if type(i) is bool:
return self.add_boolean(i)
elif isinstance(i, integer_types):
- return self.add_int(i)
+ return self.add_adaptive_int(i)
elif type(i) is list:
return self.add_list(i)
else:
diff --git a/paramiko/packet.py b/paramiko/packet.py
index f516ff9..b922000 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/server.py b/paramiko/server.py
index bf5039a..f79a174 100644
--- a/paramiko/server.py
+++ b/paramiko/server.py
@@ -22,7 +22,7 @@
import threading
from paramiko import util
-from paramiko.common import DEBUG, ERROR, OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED, AUTH_FAILED
+from paramiko.common import DEBUG, ERROR, OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED, AUTH_FAILED, AUTH_SUCCESSFUL
from paramiko.py3compat import string_types
diff --git a/paramiko/ssh_exception.py b/paramiko/ssh_exception.py
index b99e42b..e120a45 100644
--- a/paramiko/ssh_exception.py
+++ b/paramiko/ssh_exception.py
@@ -105,7 +105,11 @@ class BadHostKeyException (SSHException):
.. versionadded:: 1.6
"""
def __init__(self, hostname, got_key, expected_key):
- SSHException.__init__(self, 'Host key for server %s does not match!' % hostname)
+ SSHException.__init__(self,
+ 'Host key for server %s does not match : got %s expected %s' % (
+ hostname,
+ got_key.get_base64(),
+ expected_key.get_base64()))
self.hostname = hostname
self.key = got_key
self.expected_key = expected_key
diff --git a/paramiko/ssh_gss.py b/paramiko/ssh_gss.py
index ebf2cc8..e9b13a6 100644
--- a/paramiko/ssh_gss.py
+++ b/paramiko/ssh_gss.py
@@ -20,7 +20,7 @@
"""
-This module provides GSS-API / SSPI authentication as defined in RFC 4462.
+This module provides GSS-API / SSPI authentication as defined in :rfc:`4462`.
.. note:: Credential delegation is not supported in server mode.
@@ -360,8 +360,8 @@ class _SSH_GSSAPI(_SSH_GSSAuth):
: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: 0 if the MIC check was successful and 1 if it fails
- :rtype: int
+ :return: None if the MIC check was successful
+ :raises gssapi.GSSException: if the MIC check failed
"""
self._session_id = session_id
self._username = username
@@ -371,11 +371,7 @@ class _SSH_GSSAPI(_SSH_GSSAuth):
self._username,
self._service,
self._auth_method)
- try:
- self._gss_srv_ctxt.verify_mic(mic_field,
- mic_token)
- except gssapi.BadSignature:
- raise Exception("GSS-API MIC check failed.")
+ self._gss_srv_ctxt.verify_mic(mic_field, mic_token)
else:
# for key exchange with gssapi-keyex
# client mode
@@ -534,31 +530,26 @@ class _SSH_SSPI(_SSH_GSSAuth):
: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: 0 if the MIC check was successful
- :rtype: int
+ :return: None if the MIC check was successful
+ :raises sspi.error: if the MIC check failed
"""
self._session_id = session_id
self._username = username
- mic_status = 1
if username is not None:
# server mode
mic_field = self._ssh_build_mic(self._session_id,
self._username,
self._service,
self._auth_method)
- mic_status = self._gss_srv_ctxt.verify(mic_field,
- mic_token)
+ # 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
- mic_status = self._gss_ctxt.verify(self._session_id,
- mic_token)
- """
- The SSPI method C{verify} has no return value, so if no SSPI error
- is returned, set C{mic_status} to 0.
- """
- mic_status = 0
- return mic_status
+ # 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):
diff --git a/paramiko/transport.py b/paramiko/transport.py
index 36da304..31c27a2 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 bb93f88..d94d5bc 100644
--- a/sites/www/changelog.rst
+++ b/sites/www/changelog.rst
@@ -2,6 +2,38 @@
Changelog
=========
+* :release:`1.15.3 <2015-10-02>`
+* :support:`554 backported` Fix inaccuracies in the docstring for the ECDSA key
+ class. Thanks to Jared Hance for the patch.
+* :support:`516 backported` Document `~paramiko.agent.AgentRequestHandler`.
+ Thanks to ``@toejough`` for report & suggestions.
+* :bug:`496` Fix a handful of small but critical bugs in Paramiko's GSSAPI
+ support (note: this includes switching from PyCrypo's Random to
+ `os.urandom`). Thanks to Anselm Kruis for catch & patch.
+* :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 `~paramiko.ssh_exception.SSHException`. This brings Paramiko's
+ behavior more in line with OpenSSH, which silently ignores such input. Catch
+ & patch courtesy of Martin Topholm.
+* :bug:`404` Print details when displaying
+ `~paramiko.ssh_exception.BadHostKeyException` objects (expected vs received
+ data) instead of just "hey shit broke". Patch credit: Loic Dachary.
+* :bug:`469` (also :issue:`488`, :issue:`461` and like a dozen others) Fix a
+ typo introduced in the 1.15 release which broke WinPageant support. Thanks to
+ everyone who submitted patches, and to Steve Cohen who was the lucky winner
+ of the cherry-pick lottery.
+* :bug:`353` (via :issue:`482`) Fix a bug introduced in the Python 3 port
+ which caused ``OverFlowError`` (and other symptoms) in SFTP functionality.
+ Thanks to ``@dboreham`` for leading the troubleshooting charge, and to
+ Scott Maxwell for the final patch.
+* :bug:`402` Check to see if an SSH agent is actually present before trying to
+ forward it to the remote end. This replaces what was usually a useless
+ ``TypeError`` with a human-readable
+ `~paramiko.ssh_exception.AuthenticationException`. Credit to Ken Jordan for
+ the fix and Yvan Marques for original report.
* :release:`1.15.2 <2014-12-19>`
* :release:`1.14.2 <2014-12-19>`
* :release:`1.13.3 <2014-12-19>`
diff --git a/sites/www/index.rst b/sites/www/index.rst
index 1b60970..8e7562a 100644
--- a/sites/www/index.rst
+++ b/sites/www/index.rst
@@ -26,11 +26,7 @@ Please see the sidebar to the left to begin.
.. rubric:: Footnotes
.. [#]
- SSH is defined in RFCs
- `4251 <http://www.rfc-editor.org/rfc/rfc4251.txt>`_,
- `4252 <http://www.rfc-editor.org/rfc/rfc4252.txt>`_,
- `4253 <http://www.rfc-editor.org/rfc/rfc4253.txt>`_, and
- `4254 <http://www.rfc-editor.org/rfc/rfc4254.txt>`_;
- the primary working implementation of the protocol is the `OpenSSH project
+ SSH is defined in :rfc:`4251`, :rfc:`4252`, :rfc:`4253` and :rfc:`4254`. The
+ primary working implementation of the protocol is the `OpenSSH project
<http://openssh.org>`_. Paramiko implements a large portion of the SSH
feature set, but there are occasional gaps.
diff --git a/tasks.py b/tasks.py
index 3503d01..3d57567 100644
--- a/tasks.py
+++ b/tasks.py
@@ -3,28 +3,10 @@ from os.path import join
from shutil import rmtree, copytree
from invoke import Collection, ctask as task
-from invocations import docs as _docs
+from invocations.docs import docs, www
from invocations.packaging import publish
-d = 'sites'
-
-# Usage doc/API site (published as docs.paramiko.org)
-docs_path = join(d, 'docs')
-docs_build = join(docs_path, '_build')
-docs = Collection.from_module(_docs, name='docs', config={
- 'sphinx.source': docs_path,
- 'sphinx.target': docs_build,
-})
-
-# Main/about/changelog site ((www.)?paramiko.org)
-www_path = join(d, 'www')
-www = Collection.from_module(_docs, name='www', config={
- 'sphinx.source': www_path,
- 'sphinx.target': join(www_path, '_build'),
-})
-
-
# Until we move to spec-based testing
@task
def test(ctx, coverage=False):
@@ -35,6 +17,11 @@ def test(ctx, coverage=False):
ctx.run("{0} test.py {1}".format(runner, flags), pty=True)
+@task
+def coverage(ctx):
+ ctx.run("coverage run --source=paramiko test.py --verbose")
+
+
# Until we stop bundling docs w/ releases. Need to discover use cases first.
@task
def release(ctx):
@@ -45,9 +32,9 @@ def release(ctx):
rmtree(target, ignore_errors=True)
copytree(docs_build, target)
# Publish
- publish(ctx, wheel=True)
+ publish(ctx)
# Remind
print("\n\nDon't forget to update RTD's versions page for new minor releases!")
-ns = Collection(test, release, docs=docs, www=www)
+ns = Collection(test, coverage, release, docs, www)
diff --git a/tests/test_hostkeys.py b/tests/test_hostkeys.py
index 0ee1bbf..2bdcad9 100644
--- a/tests/test_hostkeys.py
+++ b/tests/test_hostkeys.py
@@ -31,6 +31,7 @@ test_hosts_file = """\
secure.example.com ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEA1PD6U2/TVxET6lkpKhOk5r\
9q/kAYG6sP9f5zuUYP8i7FOFp/6ncCEbbtg/lB+A3iidyxoSWl+9jtoyyDOOVX4UIDV9G11Ml8om3\
D+jrpI9cycZHqilK0HmxDeCuxbwyMuaCygU9gS2qoRvNLWZk70OpIKSSpBo0Wl3/XUmz9uhc=
+broken.example.com ssh-rsa AAAA
happy.example.com ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEA8bP1ZA7DCZDB9J0s50l31M\
BGQ3GQ/Fc7SX6gkpXkwcZryoi4kNFhHu5LvHcZPdxXV1D+uTMfGS1eyd2Yz/DoNWXNAl8TI0cAsW\
5ymME3bQ4J/k1IKxCtz/bAlAqFgKoc+EolMziDYqWIATtW0rYTJvzGAzTmMj80/QpsFH+Pc2M=
diff --git a/tests/test_message.py b/tests/test_message.py
index f308c03..f18cae9 100644
--- a/tests/test_message.py
+++ b/tests/test_message.py
@@ -92,12 +92,12 @@ class MessageTest (unittest.TestCase):
def test_4_misc(self):
msg = Message(self.__d)
- self.assertEqual(msg.get_int(), 5)
- self.assertEqual(msg.get_int(), 0x1122334455)
- self.assertEqual(msg.get_int(), 0xf00000000000000000)
+ self.assertEqual(msg.get_adaptive_int(), 5)
+ self.assertEqual(msg.get_adaptive_int(), 0x1122334455)
+ self.assertEqual(msg.get_adaptive_int(), 0xf00000000000000000)
self.assertEqual(msg.get_so_far(), self.__d[:29])
self.assertEqual(msg.get_remainder(), self.__d[29:])
msg.rewind()
- self.assertEqual(msg.get_int(), 5)
+ self.assertEqual(msg.get_adaptive_int(), 5)
self.assertEqual(msg.get_so_far(), self.__d[:4])
self.assertEqual(msg.get_remainder(), self.__d[4:])
diff --git a/tests/test_transport.py b/tests/test_transport.py
index 5cf9a86..3c8ad81 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')