summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJeff Forcier <jeff@bitprophet.org>2017-09-18 12:05:57 -0700
committerJeff Forcier <jeff@bitprophet.org>2017-09-18 12:05:57 -0700
commitd20f8b9820294006ee078944ca9eb01a41b65dd8 (patch)
tree2f6b3ac3d6529c8ac2b4704f318d0b1820a09928
parent20b69e7735e8c7e8bf63ee4df2c6a6b34743e646 (diff)
parent89a9b583e46f634792d814c5cff8e0cecdb5fa50 (diff)
downloadparamiko-d20f8b9820294006ee078944ca9eb01a41b65dd8.tar.gz
Merge branch '2.0' of github.com:paramiko/paramiko into 2.0
-rw-r--r--.travis.yml2
-rw-r--r--[-rwxr-xr-x]demos/demo_simple.py11
-rw-r--r--paramiko/__init__.py2
-rw-r--r--paramiko/_version.py2
-rw-r--r--paramiko/auth_handler.py195
-rw-r--r--paramiko/client.py2
-rw-r--r--paramiko/common.py22
-rw-r--r--paramiko/file.py6
-rw-r--r--paramiko/hostkeys.py7
-rw-r--r--paramiko/kex_gss.py2
-rw-r--r--paramiko/primes.py1
-rw-r--r--paramiko/py3compat.py11
-rw-r--r--paramiko/sftp_client.py11
-rw-r--r--paramiko/ssh_gss.py36
-rw-r--r--paramiko/transport.py27
-rw-r--r--paramiko/win_pageant.py2
-rw-r--r--setup.py1
-rw-r--r--sites/www/changelog.rst24
-rw-r--r--tests/__init__.py36
-rw-r--r--tests/test_client.py63
-rwxr-xr-xtests/test_file.py57
-rwxr-xr-xtests/test_sftp.py30
-rw-r--r--tests/test_ssh_gss.py45
-rw-r--r--tests/test_transport.py69
24 files changed, 513 insertions, 151 deletions
diff --git a/.travis.yml b/.travis.yml
index 7ca8db09..0e46ec84 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -10,7 +10,7 @@ python:
- "3.4"
- "3.5"
- "3.6"
- - "pypy-5.4.1"
+ - "pypy-5.6.0"
install:
# Self-install for setup.py-driven deps
- pip install -e .
diff --git a/demos/demo_simple.py b/demos/demo_simple.py
index 3a17988c..7ae3d8c8 100755..100644
--- a/demos/demo_simple.py
+++ b/demos/demo_simple.py
@@ -37,8 +37,10 @@ except ImportError:
# setup logging
paramiko.util.log_to_file('demo_simple.log')
# Paramiko client configuration
-UseGSSAPI = True # enable GSS-API / SSPI authentication
-DoGSSAPIKeyExchange = True
+UseGSSAPI = paramiko.GSS_AUTH_AVAILABLE # enable "gssapi-with-mic" authentication, if supported by your python installation
+DoGSSAPIKeyExchange = paramiko.GSS_AUTH_AVAILABLE # enable "gssapi-kex" key exchange, if supported by your python installation
+# UseGSSAPI = False
+# DoGSSAPIKeyExchange = False
port = 22
# get hostname
@@ -64,7 +66,7 @@ if username == '':
username = input('Username [%s]: ' % default_username)
if len(username) == 0:
username = default_username
-if not UseGSSAPI or (not UseGSSAPI and not DoGSSAPIKeyExchange):
+if not UseGSSAPI and not DoGSSAPIKeyExchange:
password = getpass.getpass('Password for %s@%s: ' % (username, hostname))
@@ -74,7 +76,7 @@ try:
client.load_system_host_keys()
client.set_missing_host_key_policy(paramiko.WarningPolicy())
print('*** Connecting...')
- if not UseGSSAPI or (not UseGSSAPI and not DoGSSAPIKeyExchange):
+ if not UseGSSAPI and not DoGSSAPIKeyExchange:
client.connect(hostname, port, username, password)
else:
# SSPI works only with the FQDN of the target host
@@ -83,6 +85,7 @@ try:
client.connect(hostname, port, username, gss_auth=UseGSSAPI,
gss_kex=DoGSSAPIKeyExchange)
except Exception:
+ # traceback.print_exc()
password = getpass.getpass('Password for %s@%s: ' % (username, hostname))
client.connect(hostname, port, username, password)
diff --git a/paramiko/__init__.py b/paramiko/__init__.py
index 197f519a..01dc973c 100644
--- a/paramiko/__init__.py
+++ b/paramiko/__init__.py
@@ -34,7 +34,7 @@ from paramiko.client import (
WarningPolicy,
)
from paramiko.auth_handler import AuthHandler
-from paramiko.ssh_gss import GSSAuth, GSS_AUTH_AVAILABLE
+from paramiko.ssh_gss import GSSAuth, GSS_AUTH_AVAILABLE, GSS_EXCEPTIONS
from paramiko.channel import Channel, ChannelFile
from paramiko.ssh_exception import (
SSHException, PasswordRequiredException, BadAuthenticationType,
diff --git a/paramiko/_version.py b/paramiko/_version.py
index 4e7cf19d..98c3cc2d 100644
--- a/paramiko/_version.py
+++ b/paramiko/_version.py
@@ -1,2 +1,2 @@
-__version_info__ = (2, 0, 5)
+__version_info__ = (2, 0, 6)
__version__ = '.'.join(map(str, __version_info__))
diff --git a/paramiko/auth_handler.py b/paramiko/auth_handler.py
index 33f01da6..24ada232 100644
--- a/paramiko/auth_handler.py
+++ b/paramiko/auth_handler.py
@@ -43,7 +43,7 @@ from paramiko.ssh_exception import (
PartialAuthentication,
)
from paramiko.server import InteractiveQuery
-from paramiko.ssh_gss import GSSAuth
+from paramiko.ssh_gss import GSSAuth, GSS_EXCEPTIONS
class AuthHandler (object):
@@ -262,19 +262,26 @@ class AuthHandler (object):
mech = m.get_string()
m = Message()
m.add_byte(cMSG_USERAUTH_GSSAPI_TOKEN)
- m.add_string(sshgss.ssh_init_sec_context(self.gss_host,
- mech,
- self.username,))
+ try:
+ m.add_string(sshgss.ssh_init_sec_context(
+ self.gss_host,
+ mech,
+ self.username,))
+ except GSS_EXCEPTIONS as e:
+ return self._handle_local_gss_failure(e)
self.transport._send_message(m)
while True:
ptype, m = self.transport.packetizer.read_message()
if ptype == MSG_USERAUTH_GSSAPI_TOKEN:
srv_token = m.get_string()
- next_token = sshgss.ssh_init_sec_context(
- self.gss_host,
- mech,
- self.username,
- srv_token)
+ try:
+ next_token = sshgss.ssh_init_sec_context(
+ self.gss_host,
+ mech,
+ self.username,
+ srv_token)
+ except GSS_EXCEPTIONS as e:
+ return self._handle_local_gss_failure(e)
# After this step the GSSAPI should not return any
# token. If it does, we keep sending the token to
# the server until no more token is returned.
@@ -302,7 +309,7 @@ class AuthHandler (object):
maj_status = m.get_int()
min_status = m.get_int()
err_msg = m.get_string()
- m.get_string() # Lang tag - discarded
+ m.get_string() # Lang tag - discarded
raise SSHException("GSS-API Error:\nMajor Status: %s\n\
Minor Status: %s\ \nError Message:\
%s\n") % (str(maj_status),
@@ -395,7 +402,7 @@ class AuthHandler (object):
(self.auth_username != username)):
self.transport._log(
WARNING,
- 'Auth rejected because the client attempted to change username in mid-flight' # noqa
+ 'Auth rejected because the client attempted to change username in mid-flight' # noqa
)
self._disconnect_no_more_auth()
return
@@ -503,52 +510,16 @@ class AuthHandler (object):
supported_mech = sshgss.ssh_gss_oids("server")
# RFC 4462 says we are not required to implement GSS-API error
# messages. See section 3.8 in http://www.ietf.org/rfc/rfc4462.txt
- while True:
- m = Message()
- m.add_byte(cMSG_USERAUTH_GSSAPI_RESPONSE)
- m.add_bytes(supported_mech)
- self.transport._send_message(m)
- ptype, m = self.transport.packetizer.read_message()
- if ptype == MSG_USERAUTH_GSSAPI_TOKEN:
- client_token = m.get_string()
- # use the client token as input to establish a secure
- # context.
- try:
- token = sshgss.ssh_accept_sec_context(self.gss_host,
- client_token,
- username)
- except Exception:
- result = AUTH_FAILED
- self._send_auth_result(username, method, result)
- raise
- if token is not None:
- m = Message()
- m.add_byte(cMSG_USERAUTH_GSSAPI_TOKEN)
- m.add_string(token)
- self.transport._send_message(m)
- else:
- result = AUTH_FAILED
- self._send_auth_result(username, method, result)
- return
- # check MIC
- ptype, m = self.transport.packetizer.read_message()
- if ptype == MSG_USERAUTH_GSSAPI_MIC:
- break
- mic_token = m.get_string()
- try:
- sshgss.ssh_check_mic(mic_token,
- self.transport.session_id,
- username)
- except Exception:
- result = AUTH_FAILED
- self._send_auth_result(username, method, result)
- raise
- # 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)
+ m = Message()
+ m.add_byte(cMSG_USERAUTH_GSSAPI_RESPONSE)
+ m.add_bytes(supported_mech)
+ self.transport.auth_handler = GssapiWithMicAuthHandler(self,
+ sshgss)
+ self.transport._expected_packet = (MSG_USERAUTH_GSSAPI_TOKEN,
+ MSG_USERAUTH_REQUEST,
+ MSG_SERVICE_REQUEST)
+ self.transport._send_message(m)
+ return
elif method == "gssapi-keyex" and gss_auth:
mic_token = m.get_string()
sshgss = self.transport.kexgss_ctxt
@@ -648,6 +619,17 @@ class AuthHandler (object):
self._send_auth_result(
self.auth_username, 'keyboard-interactive', result)
+ def _handle_local_gss_failure(self, e):
+ self.transport.saved_exception = e
+ self.transport._log(DEBUG, "GSSAPI failure: %s" % str(e))
+ self.transport._log(INFO, 'Authentication (%s) failed.' %
+ self.auth_method)
+ self.authenticated = False
+ self.username = None
+ if self.auth_event is not None:
+ self.auth_event.set()
+ return
+
_handler_table = {
MSG_SERVICE_REQUEST: _parse_service_request,
MSG_SERVICE_ACCEPT: _parse_service_accept,
@@ -658,3 +640,102 @@ class AuthHandler (object):
MSG_USERAUTH_INFO_REQUEST: _parse_userauth_info_request,
MSG_USERAUTH_INFO_RESPONSE: _parse_userauth_info_response,
}
+
+
+class GssapiWithMicAuthHandler(object):
+ """A specialized Auth handler for gssapi-with-mic
+
+ During the GSSAPI token exchange we need a modified dispatch table,
+ because the packet type numbers are not unique.
+ """
+
+ method = "gssapi-with-mic"
+
+ def __init__(self, delegate, sshgss):
+ self._delegate = delegate
+ self.sshgss = sshgss
+
+ def abort(self):
+ self._restore_delegate_auth_handler()
+ return self._delegate.abort()
+
+ @property
+ def transport(self):
+ return self._delegate.transport
+
+ @property
+ def _send_auth_result(self):
+ return self._delegate._send_auth_result
+
+ @property
+ def auth_username(self):
+ return self._delegate.auth_username
+
+ @property
+ def gss_host(self):
+ return self._delegate.gss_host
+
+ def _restore_delegate_auth_handler(self):
+ self.transport.auth_handler = self._delegate
+
+ def _parse_userauth_gssapi_token(self, m):
+ client_token = m.get_string()
+ # use the client token as input to establish a secure
+ # context.
+ sshgss = self.sshgss
+ try:
+ token = sshgss.ssh_accept_sec_context(self.gss_host,
+ client_token,
+ self.auth_username)
+ except Exception as e:
+ self.transport.saved_exception = e
+ result = AUTH_FAILED
+ self._restore_delegate_auth_handler()
+ self._send_auth_result(self.auth_username, self.method, result)
+ raise
+ if token is not None:
+ m = Message()
+ m.add_byte(cMSG_USERAUTH_GSSAPI_TOKEN)
+ m.add_string(token)
+ self.transport._expected_packet = (MSG_USERAUTH_GSSAPI_TOKEN,
+ MSG_USERAUTH_GSSAPI_MIC,
+ MSG_USERAUTH_REQUEST)
+ self.transport._send_message(m)
+
+ def _parse_userauth_gssapi_mic(self, m):
+ mic_token = m.get_string()
+ sshgss = self.sshgss
+ username = self.auth_username
+ self._restore_delegate_auth_handler()
+ try:
+ sshgss.ssh_check_mic(mic_token,
+ self.transport.session_id,
+ username)
+ except Exception as e:
+ self.transport.saved_exception = e
+ result = AUTH_FAILED
+ self._send_auth_result(username, self.method, result)
+ raise
+ # 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)
+ # okay, send result
+ self._send_auth_result(username, self.method, result)
+
+ def _parse_service_request(self, m):
+ self._restore_delegate_auth_handler()
+ return self._delegate._parse_service_request(m)
+
+ def _parse_userauth_request(self, m):
+ self._restore_delegate_auth_handler()
+ return self._delegate._parse_userauth_request(m)
+
+ _handler_table = {
+ MSG_SERVICE_REQUEST: _parse_service_request,
+ MSG_USERAUTH_REQUEST: _parse_userauth_request,
+ MSG_USERAUTH_GSSAPI_TOKEN: _parse_userauth_gssapi_token,
+ MSG_USERAUTH_GSSAPI_MIC: _parse_userauth_gssapi_mic,
+ }
diff --git a/paramiko/client.py b/paramiko/client.py
index 224109bf..39617d5b 100644
--- a/paramiko/client.py
+++ b/paramiko/client.py
@@ -353,7 +353,7 @@ class SSHClient (ClosingContextManager):
# If GSS-API Key Exchange is performed we are not required to check the
# host key, because the host is authenticated via GSS-API / SSPI as
# well as our client.
- if not self._transport.use_gss_kex:
+ if not self._transport.gss_kex_used:
our_server_key = self._system_host_keys.get(
server_hostkey_name, {}).get(keytype)
if our_server_key is None:
diff --git a/paramiko/common.py b/paramiko/common.py
index 556f046a..0012372a 100644
--- a/paramiko/common.py
+++ b/paramiko/common.py
@@ -20,9 +20,7 @@
Common constants and global variables.
"""
import logging
-from paramiko.py3compat import (
- byte_chr, PY2, bytes_types, string_types, b, long,
-)
+from paramiko.py3compat import byte_chr, PY2, bytes_types, text_type, long
MSG_DISCONNECT, MSG_IGNORE, MSG_UNIMPLEMENTED, MSG_DEBUG, \
MSG_SERVICE_REQUEST, MSG_SERVICE_ACCEPT = range(1, 7)
@@ -163,14 +161,16 @@ else:
def asbytes(s):
- if not isinstance(s, bytes_types):
- if isinstance(s, string_types):
- s = b(s)
- else:
- try:
- s = s.asbytes()
- except Exception:
- raise Exception('Unknown type')
+ """Coerce to bytes if possible or return unchanged."""
+ if isinstance(s, bytes_types):
+ return s
+ if isinstance(s, text_type):
+ # Accept text and encode as utf-8 for compatibility only.
+ return s.encode("utf-8")
+ asbytes = getattr(s, "asbytes", None)
+ if asbytes is not None:
+ return asbytes()
+ # May be an object that implements the buffer api, let callers handle.
return s
diff --git a/paramiko/file.py b/paramiko/file.py
index 5212091a..a1bdafbe 100644
--- a/paramiko/file.py
+++ b/paramiko/file.py
@@ -18,7 +18,7 @@
from paramiko.common import (
linefeed_byte_value, crlf, cr_byte, linefeed_byte, cr_byte_value,
)
-from paramiko.py3compat import BytesIO, PY2, u, b, bytes_types
+from paramiko.py3compat import BytesIO, PY2, u, bytes_types, text_type
from paramiko.util import ClosingContextManager
@@ -391,7 +391,9 @@ class BufferedFile (ClosingContextManager):
:param data: ``str``/``bytes`` data to write
"""
- data = b(data)
+ if isinstance(data, text_type):
+ # Accept text and encode as utf-8 for compatibility only.
+ data = data.encode('utf-8')
if self._closed:
raise IOError('File is closed')
if not (self._flags & self.FLAG_WRITE):
diff --git a/paramiko/hostkeys.py b/paramiko/hostkeys.py
index 008ba592..c873f58b 100644
--- a/paramiko/hostkeys.py
+++ b/paramiko/hostkeys.py
@@ -20,17 +20,12 @@
import binascii
import os
+from collections import MutableMapping
from hashlib import sha1
from hmac import HMAC
from paramiko.py3compat import b, u, encodebytes, decodebytes
-try:
- from collections import MutableMapping
-except ImportError:
- # noinspection PyUnresolvedReferences
- from UserDict import DictMixin as MutableMapping
-
from paramiko.dsskey import DSSKey
from paramiko.rsakey import RSAKey
from paramiko.util import get_logger, constant_time_bytes_eq
diff --git a/paramiko/kex_gss.py b/paramiko/kex_gss.py
index ba24c0a0..3406babb 100644
--- a/paramiko/kex_gss.py
+++ b/paramiko/kex_gss.py
@@ -40,7 +40,7 @@ This module provides GSS-API / SSPI Key Exchange as defined in :rfc:`4462`.
import os
from hashlib import sha1
-from paramiko.common import * # noqa
+from paramiko.common import DEBUG, max_byte, zero_byte
from paramiko import util
from paramiko.message import Message
from paramiko.py3compat import byte_chr, byte_mask, byte_ord
diff --git a/paramiko/primes.py b/paramiko/primes.py
index 48a34e53..65617914 100644
--- a/paramiko/primes.py
+++ b/paramiko/primes.py
@@ -25,7 +25,6 @@ import os
from paramiko import util
from paramiko.py3compat import byte_mask, long
from paramiko.ssh_exception import SSHException
-from paramiko.common import * # noqa
def _roll_random(n):
diff --git a/paramiko/py3compat.py b/paramiko/py3compat.py
index 095b0d09..6703ace8 100644
--- a/paramiko/py3compat.py
+++ b/paramiko/py3compat.py
@@ -65,15 +65,8 @@ if PY2:
return s
- try:
- import cStringIO
-
- StringIO = cStringIO.StringIO # NOQA
- except ImportError:
- import StringIO
-
- StringIO = StringIO.StringIO # NOQA
-
+ import cStringIO
+ StringIO = cStringIO.StringIO
BytesIO = StringIO
diff --git a/paramiko/sftp_client.py b/paramiko/sftp_client.py
index 12fccb2f..ee5ab073 100644
--- a/paramiko/sftp_client.py
+++ b/paramiko/sftp_client.py
@@ -28,9 +28,7 @@ from paramiko import util
from paramiko.channel import Channel
from paramiko.message import Message
from paramiko.common import INFO, DEBUG, o777
-from paramiko.py3compat import (
- bytestring, b, u, long, string_types, bytes_types,
-)
+from paramiko.py3compat import bytestring, b, u, long
from paramiko.sftp import (
BaseSFTP, CMD_OPENDIR, CMD_HANDLE, SFTPError, CMD_READDIR, CMD_NAME,
CMD_CLOSE, SFTP_FLAG_READ, SFTP_FLAG_WRITE, SFTP_FLAG_CREATE,
@@ -758,13 +756,12 @@ class SFTPClient(BaseSFTP, ClosingContextManager):
msg.add_int64(item)
elif isinstance(item, int):
msg.add_int(item)
- elif isinstance(item, (string_types, bytes_types)):
- msg.add_string(item)
elif isinstance(item, SFTPAttributes):
item._pack(msg)
else:
- raise Exception(
- 'unknown type for %r type %r' % (item, type(item)))
+ # For all other types, rely on as_string() to either coerce
+ # to bytes before writing or raise a suitable exception.
+ msg.add_string(item)
num = self.request_number
self._expecting[num] = fileobj
self.request_number += 1
diff --git a/paramiko/ssh_gss.py b/paramiko/ssh_gss.py
index 414485f9..b3c3f72b 100644
--- a/paramiko/ssh_gss.py
+++ b/paramiko/ssh_gss.py
@@ -33,34 +33,39 @@ import struct
import os
import sys
-"""
-:var bool GSS_AUTH_AVAILABLE:
- Constraint that indicates if GSS-API / SSPI is available.
-"""
+
+#: A boolean constraint that indicates if GSS-API / SSPI is available.
GSS_AUTH_AVAILABLE = True
+
+#: A tuple of the exception types used by the underlying GSSAPI implementation.
+GSS_EXCEPTIONS = ()
+
+
from pyasn1.type.univ import ObjectIdentifier
from pyasn1.codec.der import encoder, decoder
-from paramiko.common import MSG_USERAUTH_REQUEST
-from paramiko.ssh_exception import SSHException
-"""
-:var str _API: Constraint for the used API
-"""
+#: :var str _API: Constraint for the used API
_API = "MIT"
try:
import gssapi
+ GSS_EXCEPTIONS = (gssapi.GSSException,)
except (ImportError, OSError):
try:
+ import pywintypes
import sspicon
import sspi
_API = "SSPI"
+ GSS_EXCEPTIONS = (pywintypes.error,)
except ImportError:
GSS_AUTH_AVAILABLE = False
_API = None
+from paramiko.common import MSG_USERAUTH_REQUEST
+from paramiko.ssh_exception import SSHException
+
def GSSAuth(auth_method, gss_deleg_creds=True):
"""
@@ -345,9 +350,9 @@ class _SSH_GSSAPI(_SSH_GSSAuth):
if self._username is not None:
# server mode
mic_field = self._ssh_build_mic(self._session_id,
- self._username,
- self._service,
- self._auth_method)
+ self._username,
+ self._service,
+ self._auth_method)
self._gss_srv_ctxt.verify_mic(mic_field, mic_token)
else:
# for key exchange with gssapi-keyex
@@ -438,9 +443,10 @@ class _SSH_SSPI(_SSH_GSSAuth):
targetspn=targ_name)
error, token = self._gss_ctxt.authorize(recv_token)
token = token[0].Buffer
- except:
- raise Exception("{0}, Target: {1}".format(sys.exc_info()[1],
- self._gss_host))
+ except pywintypes.error as e:
+ e.strerror += ", Target: {1}".format(e, self._gss_host)
+ raise
+
if error == 0:
"""
if the status is GSS_COMPLETE (error = 0) the context is fully
diff --git a/paramiko/transport.py b/paramiko/transport.py
index d219550d..1ab60841 100644
--- a/paramiko/transport.py
+++ b/paramiko/transport.py
@@ -106,9 +106,9 @@ class Transport(threading.Thread, ClosingContextManager):
'aes192-ctr',
'aes256-ctr',
'aes128-cbc',
- 'blowfish-cbc',
'aes192-cbc',
'aes256-cbc',
+ 'blowfish-cbc',
'3des-cbc',
)
_preferred_macs = (
@@ -132,6 +132,11 @@ class Transport(threading.Thread, ClosingContextManager):
'diffie-hellman-group-exchange-sha1',
'diffie-hellman-group-exchange-sha256',
)
+ _preferred_gsskex = (
+ 'gss-gex-sha1-toWM5Slw5Ew8Mqkay+al2g==',
+ 'gss-group14-sha1-toWM5Slw5Ew8Mqkay+al2g==',
+ 'gss-group1-sha1-toWM5Slw5Ew8Mqkay+al2g==',
+ )
_preferred_compression = ('none',)
_cipher_info = {
@@ -309,14 +314,9 @@ class Transport(threading.Thread, ClosingContextManager):
threading.Thread.__init__(self)
self.setDaemon(True)
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(self._active_check_timeout)
- except AttributeError:
- pass
+ # 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(self._active_check_timeout)
# negotiated crypto parameters
self.packetizer = Packetizer(sock)
@@ -338,12 +338,7 @@ class Transport(threading.Thread, ClosingContextManager):
self.gss_host = None
if self.use_gss_kex:
self.kexgss_ctxt = GSSAuth("gssapi-keyex", gss_deleg_creds)
- self._preferred_kex = ('gss-gex-sha1-toWM5Slw5Ew8Mqkay+al2g==',
- 'gss-group14-sha1-toWM5Slw5Ew8Mqkay+al2g==',
- 'gss-group1-sha1-toWM5Slw5Ew8Mqkay+al2g==',
- 'diffie-hellman-group-exchange-sha1',
- 'diffie-hellman-group14-sha1',
- 'diffie-hellman-group1-sha1')
+ self._preferred_kex = self._preferred_gsskex + self._preferred_kex
# state used during negotiation
self.kex_engine = None
@@ -1843,6 +1838,8 @@ class Transport(threading.Thread, ClosingContextManager):
):
handler = self.auth_handler._handler_table[ptype]
handler(self.auth_handler, m)
+ if len(self._expected_packet) > 0:
+ continue
else:
self._log(WARNING, 'Oops, unhandled type %d' % ptype)
msg = Message()
diff --git a/paramiko/win_pageant.py b/paramiko/win_pageant.py
index c8c2c7bc..fda3b9c1 100644
--- a/paramiko/win_pageant.py
+++ b/paramiko/win_pageant.py
@@ -25,7 +25,7 @@ import array
import ctypes.wintypes
import platform
import struct
-from paramiko.util import * # noqa
+from paramiko.common import zero_byte
from paramiko.py3compat import b
try:
diff --git a/setup.py b/setup.py
index 80d5ea7f..d3ab1ea9 100644
--- a/setup.py
+++ b/setup.py
@@ -72,6 +72,7 @@ setup(
'Programming Language :: Python :: 3.3',
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5',
+ 'Programming Language :: Python :: 3.6',
],
install_requires=[
'cryptography>=1.1',
diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst
index 49854695..ac1ba9ae 100644
--- a/sites/www/changelog.rst
+++ b/sites/www/changelog.rst
@@ -3,6 +3,30 @@ Changelog
=========
* :release:`1.18.4 <2017-09-18>`
+* :bug:`1061` Clean up GSSAPI authentication procedures so they do not prevent
+ normal fallback to other authentication methods on failure. (In other words,
+ presence of GSSAPI functionality on a target server precluded use of _any_
+ other auth type if the user was unable to pass GSSAPI auth.) Patch via Anselm
+ Kruis.
+* :bug:`1060` Fix key exchange (kex) algorithm list for GSSAPI authentication;
+ previously, the list used solely out-of-date algorithms, and now contains
+ newer ones listed preferentially before the old. Credit: Anselm Kruis.
+* :bug:`1055` (also :issue:`1056`, :issue:`1057`, :issue:`1058`, :issue:`1059`)
+ Fix up host-key checking in our GSSAPI support, which was previously using an
+ incorrect API call. Thanks to Anselm Kruis for the patches.
+* :release:`2.0.6 <2017-06-09>`
+* :release:`1.18.3 <2017-06-09>`
+* :release:`1.17.5 <2017-06-09>`
+* :support:`906 (1.18+)` Clean up a handful of outdated imports and related
+ tweaks. Thanks to Pierce Lopez.
+* :bug:`984` Enhance default cipher preference order such that
+ ``aes(192|256)-cbc`` are preferred over ``blowfish-cbc``. Thanks to Alex
+ Gaynor.
+* :bug:`971 (1.17+)` Allow any type implementing the buffer API to be used with
+ `BufferedFile <paramiko.file.BufferedFile>`, `Channel
+ <paramiko.channel.Channel>`, and `SFTPFile <paramiko.sftp_file.SFTPFile>`.
+ This resolves a regression introduced in 1.13 with the Python 3 porting
+ changes, when using types such as ``memoryview``. Credit: Martin Packman.
* :bug:`741` (also :issue:`809`, :issue:`772`; all via :issue:`912`) Writing
encrypted/password-protected private key files was silently broken since 2.0
due to an incorrect API call; this has been fixed.
diff --git a/tests/__init__.py b/tests/__init__.py
index e69de29b..8878f14d 100644
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -0,0 +1,36 @@
+# Copyright (C) 2017 Martin Packman <gzlist@googlemail.com>
+#
+# 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 distributed 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.
+
+"""Base classes and helpers for testing paramiko."""
+
+import unittest
+
+from paramiko.py3compat import (
+ builtins,
+ )
+
+
+def skipUnlessBuiltin(name):
+ """Skip decorated test if builtin name does not exist."""
+ if getattr(builtins, name, None) is None:
+ skip = getattr(unittest, "skip", None)
+ if skip is None:
+ # Python 2.6 pseudo-skip
+ return lambda func: None
+ return skip("No builtin " + repr(name))
+ return lambda func: func
diff --git a/tests/test_client.py b/tests/test_client.py
index f2f2ea45..9da6eaca 100644
--- a/tests/test_client.py
+++ b/tests/test_client.py
@@ -35,7 +35,7 @@ import time
from tests.util import test_path
import paramiko
-from paramiko.common import PY2
+from paramiko.py3compat import PY2, b
from paramiko.ssh_exception import SSHException
@@ -141,6 +141,7 @@ class SSHClientTest (unittest.TestCase):
self.assertTrue(self.ts.is_active())
self.assertEqual('slowdive', self.ts.get_username())
self.assertEqual(True, self.ts.is_authenticated())
+ self.assertEqual(False, self.tc.get_transport().gss_kex_used)
# Command execution functions?
stdin, stdout, stderr = self.tc.exec_command('yes')
@@ -366,3 +367,63 @@ class SSHClientTest (unittest.TestCase):
password='pygmalion',
)
self._test_connection(**kwargs)
+
+ def test_9_auth_trickledown_gsskex(self):
+ """
+ Failed gssapi-keyex auth doesn't prevent subsequent key auth from succeeding
+ """
+ if not paramiko.GSS_AUTH_AVAILABLE:
+ return # for python 2.6 lacks skipTest
+ kwargs = dict(
+ gss_kex=True,
+ key_filename=[test_path('test_rsa.key')],
+ )
+ self._test_connection(**kwargs)
+
+ def test_10_auth_trickledown_gssauth(self):
+ """
+ Failed gssapi-with-mic auth doesn't prevent subsequent key auth from succeeding
+ """
+ if not paramiko.GSS_AUTH_AVAILABLE:
+ return # for python 2.6 lacks skipTest
+ kwargs = dict(
+ gss_auth=True,
+ key_filename=[test_path('test_rsa.key')],
+ )
+ self._test_connection(**kwargs)
+
+ def test_11_reject_policy(self):
+ """
+ verify that SSHClient's RejectPolicy works.
+ """
+ threading.Thread(target=self._run).start()
+
+ self.tc = paramiko.SSHClient()
+ self.tc.set_missing_host_key_policy(paramiko.RejectPolicy())
+ self.assertEqual(0, len(self.tc.get_host_keys()))
+ self.assertRaises(
+ paramiko.SSHException,
+ self.tc.connect,
+ password='pygmalion', **self.connect_kwargs
+ )
+
+ def test_12_reject_policy_gsskex(self):
+ """
+ verify that SSHClient's RejectPolicy works,
+ even if gssapi-keyex was enabled but not used.
+ """
+ # Test for a bug present in paramiko versions released before 2017-08-01
+ if not paramiko.GSS_AUTH_AVAILABLE:
+ return # for python 2.6 lacks skipTest
+ threading.Thread(target=self._run).start()
+
+ self.tc = paramiko.SSHClient()
+ self.tc.set_missing_host_key_policy(paramiko.RejectPolicy())
+ self.assertEqual(0, len(self.tc.get_host_keys()))
+ self.assertRaises(
+ paramiko.SSHException,
+ self.tc.connect,
+ password='pygmalion',
+ gss_kex=True,
+ **self.connect_kwargs
+ )
diff --git a/tests/test_file.py b/tests/test_file.py
index 7fab6985..b33ecd51 100755
--- a/tests/test_file.py
+++ b/tests/test_file.py
@@ -21,10 +21,14 @@ Some unit tests for the BufferedFile abstraction.
"""
import unittest
-from paramiko.file import BufferedFile
-from paramiko.common import linefeed_byte, crlf, cr_byte
import sys
+from paramiko.common import linefeed_byte, crlf, cr_byte
+from paramiko.file import BufferedFile
+from paramiko.py3compat import BytesIO
+
+from tests import skipUnlessBuiltin
+
class LoopbackFile (BufferedFile):
"""
@@ -33,19 +37,16 @@ class LoopbackFile (BufferedFile):
def __init__(self, mode='r', bufsize=-1):
BufferedFile.__init__(self)
self._set_mode(mode, bufsize)
- self.buffer = bytes()
+ self.buffer = BytesIO()
+ self.offset = 0
def _read(self, size):
- if len(self.buffer) == 0:
- return None
- if size > len(self.buffer):
- size = len(self.buffer)
- data = self.buffer[:size]
- self.buffer = self.buffer[size:]
+ data = self.buffer.getvalue()[self.offset:self.offset+size]
+ self.offset += len(data)
return data
def _write(self, data):
- self.buffer += data
+ self.buffer.write(data)
return len(data)
@@ -187,6 +188,42 @@ class BufferedFileTest (unittest.TestCase):
self.assertEqual(data, b'hello')
f.close()
+ def test_write_bad_type(self):
+ with LoopbackFile('wb') as f:
+ self.assertRaises(TypeError, f.write, object())
+
+ def test_write_unicode_as_binary(self):
+ text = u"\xa7 why is writing text to a binary file allowed?\n"
+ with LoopbackFile('rb+') as f:
+ f.write(text)
+ self.assertEqual(f.read(), text.encode("utf-8"))
+
+ @skipUnlessBuiltin('memoryview')
+ def test_write_bytearray(self):
+ with LoopbackFile('rb+') as f:
+ f.write(bytearray(12))
+ self.assertEqual(f.read(), 12 * b"\0")
+
+ @skipUnlessBuiltin('buffer')
+ def test_write_buffer(self):
+ data = 3 * b"pretend giant block of data\n"
+ offsets = range(0, len(data), 8)
+ with LoopbackFile('rb+') as f:
+ for offset in offsets:
+ f.write(buffer(data, offset, 8))
+ self.assertEqual(f.read(), data)
+
+ @skipUnlessBuiltin('memoryview')
+ def test_write_memoryview(self):
+ data = 3 * b"pretend giant block of data\n"
+ offsets = range(0, len(data), 8)
+ with LoopbackFile('rb+') as f:
+ view = memoryview(data)
+ for offset in offsets:
+ f.write(view[offset:offset+8])
+ self.assertEqual(f.read(), data)
+
+
if __name__ == '__main__':
from unittest import main
main()
diff --git a/tests/test_sftp.py b/tests/test_sftp.py
index d3064fff..98a9cebb 100755
--- a/tests/test_sftp.py
+++ b/tests/test_sftp.py
@@ -35,6 +35,7 @@ from tempfile import mkstemp
import paramiko
from paramiko.py3compat import PY2, b, u, StringIO
from paramiko.common import o777, o600, o666, o644
+from tests import skipUnlessBuiltin
from tests.stub_sftp import StubServer, StubSFTPServer
from tests.loop import LoopSocket
from tests.util import test_path
@@ -817,6 +818,35 @@ class SFTPTest (unittest.TestCase):
sftp_attributes = SFTPAttributes()
self.assertEqual(str(sftp_attributes), "?--------- 1 0 0 0 (unknown date) ?")
+ @skipUnlessBuiltin('buffer')
+ def test_write_buffer(self):
+ """Test write() using a buffer instance."""
+ data = 3 * b'A potentially large block of data to chunk up.\n'
+ try:
+ with sftp.open('%s/write_buffer' % FOLDER, 'wb') as f:
+ for offset in range(0, len(data), 8):
+ f.write(buffer(data, offset, 8))
+
+ with sftp.open('%s/write_buffer' % FOLDER, 'rb') as f:
+ self.assertEqual(f.read(), data)
+ finally:
+ sftp.remove('%s/write_buffer' % FOLDER)
+
+ @skipUnlessBuiltin('memoryview')
+ def test_write_memoryview(self):
+ """Test write() using a memoryview instance."""
+ data = 3 * b'A potentially large block of data to chunk up.\n'
+ try:
+ with sftp.open('%s/write_memoryview' % FOLDER, 'wb') as f:
+ view = memoryview(data)
+ for offset in range(0, len(data), 8):
+ f.write(view[offset:offset+8])
+
+ with sftp.open('%s/write_memoryview' % FOLDER, 'rb') as f:
+ self.assertEqual(f.read(), data)
+ finally:
+ sftp.remove('%s/write_memoryview' % FOLDER)
+
if __name__ == '__main__':
SFTPTest.init_loopback()
diff --git a/tests/test_ssh_gss.py b/tests/test_ssh_gss.py
index 967b3b81..d8d05d2b 100644
--- a/tests/test_ssh_gss.py
+++ b/tests/test_ssh_gss.py
@@ -29,11 +29,13 @@ import unittest
import paramiko
+from tests.util import test_path
+from tests.test_client import FINGERPRINTS
class NullServer (paramiko.ServerInterface):
def get_allowed_auths(self, username):
- return 'gssapi-with-mic'
+ return 'gssapi-with-mic,publickey'
def check_auth_gssapi_with_mic(self, username,
gss_authenticated=paramiko.AUTH_FAILED,
@@ -45,6 +47,16 @@ class NullServer (paramiko.ServerInterface):
def enable_auth_gssapi(self):
return True
+ def check_auth_publickey(self, username, key):
+ try:
+ expected = FINGERPRINTS[key.get_name()]
+ except KeyError:
+ return paramiko.AUTH_FAILED
+ else:
+ if key.get_fingerprint() == expected:
+ return paramiko.AUTH_SUCCESSFUL
+ return paramiko.AUTH_FAILED
+
def check_channel_request(self, kind, chanid):
return paramiko.OPEN_SUCCEEDED
@@ -85,19 +97,21 @@ class GSSAuthTest(unittest.TestCase):
server = NullServer()
self.ts.start_server(self.event, server)
- def test_1_gss_auth(self):
+ def _test_connection(self, **kwargs):
"""
- Verify that Paramiko can handle SSHv2 GSS-API / SSPI authentication
- (gssapi-with-mic) in client and server mode.
+ (Most) kwargs get passed directly into SSHClient.connect().
+
+ The exception is ... no exception yet
"""
host_key = paramiko.RSAKey.from_private_key_file('tests/test_rsa.key')
public_host_key = paramiko.RSAKey(data=host_key.asbytes())
self.tc = paramiko.SSHClient()
- self.tc.get_host_keys().add('[%s]:%d' % (self.hostname, self.port),
+ self.tc.set_missing_host_key_policy(paramiko.WarningPolicy())
+ self.tc.get_host_keys().add('[%s]:%d' % (self.addr, self.port),
'ssh-rsa', public_host_key)
- self.tc.connect(self.hostname, self.port, username=self.username,
- gss_auth=True)
+ self.tc.connect(hostname=self.addr, port=self.port, username=self.username, gss_host=self.hostname,
+ gss_auth=True, **kwargs)
self.event.wait(1.0)
self.assert_(self.event.is_set())
@@ -120,3 +134,20 @@ class GSSAuthTest(unittest.TestCase):
stdin.close()
stdout.close()
stderr.close()
+
+ def test_1_gss_auth(self):
+ """
+ Verify that Paramiko can handle SSHv2 GSS-API / SSPI authentication
+ (gssapi-with-mic) in client and server mode.
+ """
+ self._test_connection(allow_agent=False,
+ look_for_keys=False)
+
+ def test_2_auth_trickledown(self):
+ """
+ Failed gssapi-with-mic auth doesn't prevent subsequent key auth from succeeding
+ """
+ self.hostname = "this_host_does_not_exists_and_causes_a_GSSAPI-exception"
+ self._test_connection(key_filename=[test_path('test_rsa.key')],
+ allow_agent=False,
+ look_for_keys=False)
diff --git a/tests/test_transport.py b/tests/test_transport.py
index c426cef1..3e352919 100644
--- a/tests/test_transport.py
+++ b/tests/test_transport.py
@@ -43,6 +43,7 @@ from paramiko.common import (
)
from paramiko.py3compat import bytes
from paramiko.message import Message
+from tests import skipUnlessBuiltin
from tests.loop import LoopSocket
from tests.util import test_path
@@ -858,3 +859,71 @@ class TransportTest(unittest.TestCase):
self.assertEqual([chan], r)
self.assertEqual([], w)
self.assertEqual([], e)
+
+ def test_channel_send_misc(self):
+ """
+ verify behaviours sending various instances to a channel
+ """
+ self.setup_test_server()
+ text = u"\xa7 slice me nicely"
+ with self.tc.open_session() as chan:
+ schan = self.ts.accept(1.0)
+ if schan is None:
+ self.fail("Test server transport failed to accept")
+ sfile = schan.makefile()
+
+ # TypeError raised on non string or buffer type
+ self.assertRaises(TypeError, chan.send, object())
+ self.assertRaises(TypeError, chan.sendall, object())
+
+ # sendall() accepts a unicode instance
+ chan.sendall(text)
+ expected = text.encode("utf-8")
+ self.assertEqual(sfile.read(len(expected)), expected)
+
+ @skipUnlessBuiltin('buffer')
+ def test_channel_send_buffer(self):
+ """
+ verify sending buffer instances to a channel
+ """
+ self.setup_test_server()
+ data = 3 * b'some test data\n whole'
+ with self.tc.open_session() as chan:
+ schan = self.ts.accept(1.0)
+ if schan is None:
+ self.fail("Test server transport failed to accept")
+ sfile = schan.makefile()
+
+ # send() accepts buffer instances
+ sent = 0
+ while sent < len(data):
+ sent += chan.send(buffer(data, sent, 8))
+ self.assertEqual(sfile.read(len(data)), data)
+
+ # sendall() accepts a buffer instance
+ chan.sendall(buffer(data))
+ self.assertEqual(sfile.read(len(data)), data)
+
+ @skipUnlessBuiltin('memoryview')
+ def test_channel_send_memoryview(self):
+ """
+ verify sending memoryview instances to a channel
+ """
+ self.setup_test_server()
+ data = 3 * b'some test data\n whole'
+ with self.tc.open_session() as chan:
+ schan = self.ts.accept(1.0)
+ if schan is None:
+ self.fail("Test server transport failed to accept")
+ sfile = schan.makefile()
+
+ # send() accepts memoryview slices
+ sent = 0
+ view = memoryview(data)
+ while sent < len(view):
+ sent += chan.send(view[sent:sent+8])
+ self.assertEqual(sfile.read(len(data)), data)
+
+ # sendall() accepts a memoryview instance
+ chan.sendall(memoryview(data))
+ self.assertEqual(sfile.read(len(data)), data)