summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--.travis.yml10
-rw-r--r--dev-requirements.txt4
-rw-r--r--paramiko/_version.py2
-rw-r--r--paramiko/_winapi.py145
-rw-r--r--paramiko/agent.py20
-rw-r--r--paramiko/auth_handler.py24
-rw-r--r--paramiko/buffered_pipe.py4
-rw-r--r--paramiko/channel.py4
-rw-r--r--paramiko/client.py137
-rw-r--r--paramiko/config.py4
-rw-r--r--paramiko/ecdsakey.py8
-rw-r--r--paramiko/hostkeys.py8
-rw-r--r--paramiko/kex_gex.py13
-rw-r--r--paramiko/kex_group1.py1
-rw-r--r--paramiko/kex_group14.py2
-rw-r--r--paramiko/kex_gss.py5
-rw-r--r--paramiko/message.py45
-rw-r--r--paramiko/packet.py48
-rw-r--r--paramiko/pkey.py24
-rw-r--r--paramiko/py3compat.py5
-rw-r--r--paramiko/server.py2
-rw-r--r--paramiko/sftp_attr.py5
-rw-r--r--paramiko/sftp_client.py3
-rw-r--r--paramiko/ssh_exception.py44
-rw-r--r--paramiko/ssh_gss.py31
-rw-r--r--paramiko/transport.py241
-rw-r--r--paramiko/util.py2
-rw-r--r--setup.py1
-rw-r--r--setup_helper.py62
-rw-r--r--sites/shared_conf.py1
-rw-r--r--sites/www/changelog.rst83
-rw-r--r--tasks.py12
-rw-r--r--tests/test_client.py2
-rw-r--r--tests/test_hostkeys.py1
-rw-r--r--tests/test_kex.py120
-rw-r--r--tests/test_message.py8
-rwxr-xr-xtests/test_sftp.py5
-rw-r--r--tests/test_transport.py26
38 files changed, 918 insertions, 244 deletions
diff --git a/.travis.yml b/.travis.yml
index 9a55dbb6..c2c20a60 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -3,9 +3,9 @@ sudo: false
python:
- "2.6"
- "2.7"
- - "3.2"
- "3.3"
- "3.4"
+ - "3.5"
install:
# Self-install for setup.py-driven deps
- pip install -e .
@@ -13,15 +13,13 @@ 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
# problems.
- # Finally, skip them under Python 3.2 due to sphinx shenanigans
- - "[[ $TRAVIS_PYTHON_VERSION != 3.2 ]] && invoke docs -o -W || true"
- - "[[ $TRAVIS_PYTHON_VERSION != 3.2 ]] && invoke www -o -W || true"
+ - invoke docs -o -W www -o -W
notifications:
irc:
channels: "irc.freenode.org#paramiko"
diff --git a/dev-requirements.txt b/dev-requirements.txt
index 059572cf..90cfd477 100644
--- a/dev-requirements.txt
+++ b/dev-requirements.txt
@@ -1,8 +1,8 @@
# Older junk
tox>=1.4,<1.5
# For newer tasks like building Sphinx docs.
-invoke>=0.10
-invocations>=0.9.2
+invoke>=0.11.1
+invocations>=0.11.0
sphinx>=1.1.3
alabaster>=0.6.1
releases>=0.5.2
diff --git a/paramiko/_version.py b/paramiko/_version.py
index 3bf9dac7..e82b8667 100644
--- a/paramiko/_version.py
+++ b/paramiko/_version.py
@@ -1,2 +1,2 @@
-__version_info__ = (1, 15, 2)
+__version_info__ = (1, 16, 0)
__version__ = '.'.join(map(str, __version_info__))
diff --git a/paramiko/_winapi.py b/paramiko/_winapi.py
index f48e1890..9a8bdedd 100644
--- a/paramiko/_winapi.py
+++ b/paramiko/_winapi.py
@@ -1,23 +1,16 @@
"""
Windows API functions implemented as ctypes functions and classes as found
-in jaraco.windows (2.10).
+in jaraco.windows (3.3).
If you encounter issues with this module, please consider reporting the issues
in jaraco.windows and asking the author to port the fixes back here.
"""
-import ctypes
+import sys
import ctypes.wintypes
-from paramiko.py3compat import u
-try:
- import builtins
-except ImportError:
- import __builtin__ as builtins
-try:
- USHORT = ctypes.wintypes.USHORT
-except AttributeError:
- USHORT = ctypes.c_ushort
+from paramiko.py3compat import u, builtins
+
######################
# jaraco.windows.error
@@ -29,11 +22,7 @@ def format_system_message(errno):
"""
# first some flags used by FormatMessageW
ALLOCATE_BUFFER = 0x100
- ARGUMENT_ARRAY = 0x2000
- FROM_HMODULE = 0x800
- FROM_STRING = 0x400
FROM_SYSTEM = 0x1000
- IGNORE_INSERTS = 0x200
# Let FormatMessageW allocate the buffer (we'll free it below)
# Also, let it know we want a system error message.
@@ -44,7 +33,7 @@ def format_system_message(errno):
result_buffer = ctypes.wintypes.LPWSTR()
buffer_size = 0
arguments = None
- format_bytes = ctypes.windll.kernel32.FormatMessageW(
+ bytes = ctypes.windll.kernel32.FormatMessageW(
flags,
source,
message_id,
@@ -52,11 +41,11 @@ def format_system_message(errno):
ctypes.byref(result_buffer),
buffer_size,
arguments,
- )
+ )
# note the following will cause an infinite loop if GetLastError
# repeatedly returns an error that cannot be formatted, although
# this should not happen.
- handle_nonzero_success(format_bytes)
+ handle_nonzero_success(bytes)
message = result_buffer.value
ctypes.windll.kernel32.LocalFree(result_buffer)
return message
@@ -69,7 +58,11 @@ class WindowsError(builtins.WindowsError):
if value is None:
value = ctypes.windll.kernel32.GetLastError()
strerror = format_system_message(value)
- super(WindowsError, self).__init__(value, strerror)
+ if sys.version_info > (3,3):
+ args = 0, strerror, None, value
+ else:
+ args = value, strerror
+ super(WindowsError, self).__init__(*args)
@property
def message(self):
@@ -90,6 +83,27 @@ def handle_nonzero_success(result):
raise WindowsError()
+###########################
+# jaraco.windows.api.memory
+
+GMEM_MOVEABLE = 0x2
+
+GlobalAlloc = ctypes.windll.kernel32.GlobalAlloc
+GlobalAlloc.argtypes = ctypes.wintypes.UINT, ctypes.c_ssize_t
+GlobalAlloc.restype = ctypes.wintypes.HANDLE
+
+GlobalLock = ctypes.windll.kernel32.GlobalLock
+GlobalLock.argtypes = ctypes.wintypes.HGLOBAL,
+GlobalLock.restype = ctypes.wintypes.LPVOID
+
+GlobalUnlock = ctypes.windll.kernel32.GlobalUnlock
+GlobalUnlock.argtypes = ctypes.wintypes.HGLOBAL,
+GlobalUnlock.restype = ctypes.wintypes.BOOL
+
+GlobalSize = ctypes.windll.kernel32.GlobalSize
+GlobalSize.argtypes = ctypes.wintypes.HGLOBAL,
+GlobalSize.restype = ctypes.c_size_t
+
CreateFileMapping = ctypes.windll.kernel32.CreateFileMappingW
CreateFileMapping.argtypes = [
ctypes.wintypes.HANDLE,
@@ -104,9 +118,12 @@ CreateFileMapping.restype = ctypes.wintypes.HANDLE
MapViewOfFile = ctypes.windll.kernel32.MapViewOfFile
MapViewOfFile.restype = ctypes.wintypes.HANDLE
+#####################
+# jaraco.windows.mmap
+
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
@@ -136,10 +153,13 @@ class MemoryMap(object):
self.pos = pos
def write(self, msg):
+ assert isinstance(msg, bytes)
n = len(msg)
if self.pos + n >= self.length: # A little safety.
raise ValueError("Refusing to write %d bytes" % n)
- ctypes.windll.kernel32.RtlMoveMemory(self.view + self.pos, msg, n)
+ dest = self.view + self.pos
+ length = ctypes.wintypes.SIZE(n)
+ ctypes.windll.kernel32.RtlMoveMemory(dest, msg, length)
self.pos += n
def read(self, n):
@@ -147,7 +167,9 @@ class MemoryMap(object):
Read n bytes from mapped view.
"""
out = ctypes.create_string_buffer(n)
- ctypes.windll.kernel32.RtlMoveMemory(out, self.view + self.pos, n)
+ source = self.view + self.pos
+ length = ctypes.wintypes.SIZE(n)
+ ctypes.windll.kernel32.RtlMoveMemory(out, source, length)
self.pos += n
return out.raw
@@ -155,8 +177,71 @@ class MemoryMap(object):
ctypes.windll.kernel32.UnmapViewOfFile(self.view)
ctypes.windll.kernel32.CloseHandle(self.filemap)
-#########################
-# jaraco.windows.security
+#############################
+# jaraco.windows.api.security
+
+# from WinNT.h
+READ_CONTROL = 0x00020000
+STANDARD_RIGHTS_REQUIRED = 0x000F0000
+STANDARD_RIGHTS_READ = READ_CONTROL
+STANDARD_RIGHTS_WRITE = READ_CONTROL
+STANDARD_RIGHTS_EXECUTE = READ_CONTROL
+STANDARD_RIGHTS_ALL = 0x001F0000
+
+# from NTSecAPI.h
+POLICY_VIEW_LOCAL_INFORMATION = 0x00000001
+POLICY_VIEW_AUDIT_INFORMATION = 0x00000002
+POLICY_GET_PRIVATE_INFORMATION = 0x00000004
+POLICY_TRUST_ADMIN = 0x00000008
+POLICY_CREATE_ACCOUNT = 0x00000010
+POLICY_CREATE_SECRET = 0x00000020
+POLICY_CREATE_PRIVILEGE = 0x00000040
+POLICY_SET_DEFAULT_QUOTA_LIMITS = 0x00000080
+POLICY_SET_AUDIT_REQUIREMENTS = 0x00000100
+POLICY_AUDIT_LOG_ADMIN = 0x00000200
+POLICY_SERVER_ADMIN = 0x00000400
+POLICY_LOOKUP_NAMES = 0x00000800
+POLICY_NOTIFICATION = 0x00001000
+
+POLICY_ALL_ACCESS = (
+ STANDARD_RIGHTS_REQUIRED |
+ POLICY_VIEW_LOCAL_INFORMATION |
+ POLICY_VIEW_AUDIT_INFORMATION |
+ POLICY_GET_PRIVATE_INFORMATION |
+ POLICY_TRUST_ADMIN |
+ POLICY_CREATE_ACCOUNT |
+ POLICY_CREATE_SECRET |
+ POLICY_CREATE_PRIVILEGE |
+ POLICY_SET_DEFAULT_QUOTA_LIMITS |
+ POLICY_SET_AUDIT_REQUIREMENTS |
+ POLICY_AUDIT_LOG_ADMIN |
+ POLICY_SERVER_ADMIN |
+ POLICY_LOOKUP_NAMES)
+
+
+POLICY_READ = (
+ STANDARD_RIGHTS_READ |
+ POLICY_VIEW_AUDIT_INFORMATION |
+ POLICY_GET_PRIVATE_INFORMATION)
+
+POLICY_WRITE = (
+ STANDARD_RIGHTS_WRITE |
+ POLICY_TRUST_ADMIN |
+ POLICY_CREATE_ACCOUNT |
+ POLICY_CREATE_SECRET |
+ POLICY_CREATE_PRIVILEGE |
+ POLICY_SET_DEFAULT_QUOTA_LIMITS |
+ POLICY_SET_AUDIT_REQUIREMENTS |
+ POLICY_AUDIT_LOG_ADMIN |
+ POLICY_SERVER_ADMIN)
+
+POLICY_EXECUTE = (
+ STANDARD_RIGHTS_EXECUTE |
+ POLICY_VIEW_LOCAL_INFORMATION |
+ POLICY_LOOKUP_NAMES)
+
+class TokenAccess:
+ TOKEN_QUERY = 0x8
class TokenInformationClass:
TokenUser = 1
@@ -182,7 +267,7 @@ class SECURITY_DESCRIPTOR(ctypes.Structure):
PACL Dacl;
} SECURITY_DESCRIPTOR;
"""
- SECURITY_DESCRIPTOR_CONTROL = USHORT
+ SECURITY_DESCRIPTOR_CONTROL = ctypes.wintypes.USHORT
REVISION = 1
_fields_ = [
@@ -219,8 +304,11 @@ 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)
+
+#########################
+# jaraco.windows.security
def GetTokenInformation(token, information_class):
"""
@@ -236,9 +324,6 @@ def GetTokenInformation(token, information_class):
ctypes.byref(data_size)))
return ctypes.cast(data, ctypes.POINTER(TOKEN_USER)).contents
-class TokenAccess:
- TOKEN_QUERY = 0x8
-
def OpenProcessToken(proc_handle, access):
result = ctypes.wintypes.HANDLE()
proc_handle = ctypes.wintypes.HANDLE(proc_handle)
diff --git a/paramiko/agent.py b/paramiko/agent.py
index f928881e..6a8e7fb4 100644
--- a/paramiko/agent.py
+++ b/paramiko/agent.py
@@ -290,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 c001aeee..ef4a8c7e 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/buffered_pipe.py b/paramiko/buffered_pipe.py
index ac35b3e1..d5fe164e 100644
--- a/paramiko/buffered_pipe.py
+++ b/paramiko/buffered_pipe.py
@@ -81,7 +81,7 @@ class BufferedPipe (object):
Feed new data into this pipe. This method is assumed to be called
from a separate thread, so synchronization is done.
- :param data: the data to add, as a `str`
+ :param data: the data to add, as a `str` or `bytes`
"""
self._lock.acquire()
try:
@@ -125,7 +125,7 @@ class BufferedPipe (object):
:param int nbytes: maximum number of bytes to read
:param float timeout:
maximum seconds to wait (or ``None``, the default, to wait forever)
- :return: the read data, as a `str`
+ :return: the read data, as a `bytes`
:raises PipeTimeout:
if a timeout was specified and no data was ready before that
diff --git a/paramiko/channel.py b/paramiko/channel.py
index 7e39a15b..057b417b 100644
--- a/paramiko/channel.py
+++ b/paramiko/channel.py
@@ -587,7 +587,7 @@ class Channel (ClosingContextManager):
is returned, the channel stream has closed.
:param int nbytes: maximum number of bytes to read.
- :return: received data, as a `str`
+ :return: received data, as a `bytes`
:raises socket.timeout:
if no data is ready before the timeout set by `settimeout`.
@@ -975,7 +975,7 @@ class Channel (ClosingContextManager):
else:
ok = server.check_channel_env_request(self, name, value)
elif key == 'exec':
- cmd = m.get_text()
+ cmd = m.get_string()
if server is None:
ok = False
else:
diff --git a/paramiko/client.py b/paramiko/client.py
index 393e3e09..8d899a15 100644
--- a/paramiko/client.py
+++ b/paramiko/client.py
@@ -25,6 +25,7 @@ import getpass
import os
import socket
import warnings
+from errno import ECONNREFUSED, EHOSTUNREACH
from paramiko.agent import Agent
from paramiko.common import DEBUG
@@ -35,7 +36,9 @@ from paramiko.hostkeys import HostKeys
from paramiko.py3compat import string_types
from paramiko.resource import ResourceManager
from paramiko.rsakey import RSAKey
-from paramiko.ssh_exception import SSHException, BadHostKeyException
+from paramiko.ssh_exception import (
+ SSHException, BadHostKeyException, NoValidConnectionsError
+)
from paramiko.transport import Transport
from paramiko.util import retry_on_signal, ClosingContextManager
@@ -172,10 +175,46 @@ class SSHClient (ClosingContextManager):
"""
self._policy = policy
- def connect(self, hostname, port=SSH_PORT, username=None, password=None, pkey=None,
- key_filename=None, timeout=None, allow_agent=True, look_for_keys=True,
- compress=False, sock=None, gss_auth=False, gss_kex=False,
- gss_deleg_creds=True, gss_host=None, banner_timeout=None):
+ def _families_and_addresses(self, hostname, port):
+ """
+ Yield pairs of address families and addresses to try for connecting.
+
+ :param str hostname: the server to connect to
+ :param int port: the server port to connect to
+ :returns: Yields an iterable of ``(family, address)`` tuples
+ """
+ guess = True
+ addrinfos = socket.getaddrinfo(hostname, port, socket.AF_UNSPEC, socket.SOCK_STREAM)
+ for (family, socktype, proto, canonname, sockaddr) in addrinfos:
+ if socktype == socket.SOCK_STREAM:
+ yield family, sockaddr
+ guess = False
+
+ # some OS like AIX don't indicate SOCK_STREAM support, so just guess. :(
+ # We only do this if we did not get a single result marked as socktype == SOCK_STREAM.
+ if guess:
+ for family, _, _, _, sockaddr in addrinfos:
+ yield family, sockaddr
+
+ def connect(
+ self,
+ hostname,
+ port=SSH_PORT,
+ username=None,
+ password=None,
+ pkey=None,
+ key_filename=None,
+ timeout=None,
+ allow_agent=True,
+ look_for_keys=True,
+ compress=False,
+ sock=None,
+ gss_auth=False,
+ gss_kex=False,
+ gss_deleg_creds=True,
+ gss_host=None,
+ banner_timeout=None
+ ):
"""
Connect to an SSH server and authenticate to it. The server's host key
is checked against the system host keys (see `load_system_host_keys`)
@@ -206,8 +245,10 @@ class SSHClient (ClosingContextManager):
:param str key_filename:
the filename, or list of filenames, of optional private key(s) to
try for authentication
- :param float timeout: an optional timeout (in seconds) for the TCP connect
- :param bool allow_agent: set to False to disable connecting to the SSH agent
+ :param float timeout:
+ an optional timeout (in seconds) for the TCP connect
+ :param bool allow_agent:
+ set to False to disable connecting to the SSH agent
:param bool look_for_keys:
set to False to disable searching for discoverable private key
files in ``~/.ssh/``
@@ -215,10 +256,13 @@ class SSHClient (ClosingContextManager):
:param socket sock:
an open socket or socket-like object (such as a `.Channel`) to use
for communication to the target host
- :param bool gss_auth: ``True`` if you want to use GSS-API authentication
- :param bool gss_kex: Perform GSS-API Key Exchange and user authentication
+ :param bool gss_auth:
+ ``True`` if you want to use GSS-API authentication
+ :param bool gss_kex:
+ Perform GSS-API Key Exchange and user authentication
:param bool gss_deleg_creds: Delegate GSS-API client credentials or not
- :param str gss_host: The targets name in the kerberos database. default: hostname
+ :param str gss_host:
+ The targets name in the kerberos database. default: hostname
:param float banner_timeout: an optional timeout (in seconds) to wait
for the SSH banner to be presented.
@@ -234,21 +278,37 @@ class SSHClient (ClosingContextManager):
``gss_deleg_creds`` and ``gss_host`` arguments.
"""
if not sock:
- for (family, socktype, proto, canonname, sockaddr) in socket.getaddrinfo(hostname, port, socket.AF_UNSPEC, socket.SOCK_STREAM):
- if socktype == socket.SOCK_STREAM:
- af = family
- addr = sockaddr
- break
- else:
- # some OS like AIX don't indicate SOCK_STREAM support, so just guess. :(
- af, _, _, _, addr = socket.getaddrinfo(hostname, port, socket.AF_UNSPEC, socket.SOCK_STREAM)
- sock = socket.socket(af, socket.SOCK_STREAM)
- if timeout is not None:
+ errors = {}
+ # Try multiple possible address families (e.g. IPv4 vs IPv6)
+ to_try = list(self._families_and_addresses(hostname, port))
+ for af, addr in to_try:
try:
- sock.settimeout(timeout)
- except:
- pass
- retry_on_signal(lambda: sock.connect(addr))
+ sock = socket.socket(af, socket.SOCK_STREAM)
+ if timeout is not None:
+ try:
+ sock.settimeout(timeout)
+ except:
+ pass
+ retry_on_signal(lambda: sock.connect(addr))
+ # Break out of the loop on success
+ break
+ except socket.error as e:
+ # Raise anything that isn't a straight up connection error
+ # (such as a resolution error)
+ if e.errno not in (ECONNREFUSED, EHOSTUNREACH):
+ raise
+ # Capture anything else so we know how the run looks once
+ # iteration is complete. Retain info about which attempt
+ # this was.
+ errors[addr] = e
+
+ # Make sure we explode usefully if no address family attempts
+ # succeeded. We've no way of knowing which error is the "right"
+ # one, so we construct a hybrid exception containing all the real
+ # ones, of a subclass that client code should still be watching for
+ # (socket.error)
+ if len(errors) == len(to_try):
+ raise NoValidConnectionsError(errors)
t = self._transport = Transport(sock, gss_kex=gss_kex, gss_deleg_creds=gss_deleg_creds)
t.use_compression(compress=compress)
@@ -338,7 +398,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)
@@ -404,7 +464,8 @@ class SSHClient (ClosingContextManager):
"""
saved_exception = None
two_factor = False
- allowed_types = []
+ allowed_types = set()
+ two_factor_types = set(['keyboard-interactive','password'])
# If GSS-API support and GSS-PI Key Exchange was performed, we attempt
# authentication with gssapi-keyex.
@@ -430,8 +491,8 @@ class SSHClient (ClosingContextManager):
if pkey is not None:
try:
self._log(DEBUG, 'Trying SSH key %s' % hexlify(pkey.get_fingerprint()))
- allowed_types = self._transport.auth_publickey(username, pkey)
- two_factor = (allowed_types == ['password'])
+ allowed_types = set(self._transport.auth_publickey(username, pkey))
+ two_factor = (allowed_types & two_factor_types)
if not two_factor:
return
except SSHException as e:
@@ -443,8 +504,8 @@ class SSHClient (ClosingContextManager):
try:
key = pkey_class.from_private_key_file(key_filename, password)
self._log(DEBUG, 'Trying key %s from %s' % (hexlify(key.get_fingerprint()), key_filename))
- self._transport.auth_publickey(username, key)
- two_factor = (allowed_types == ['password'])
+ allowed_types = set(self._transport.auth_publickey(username, key))
+ two_factor = (allowed_types & two_factor_types)
if not two_factor:
return
break
@@ -458,9 +519,9 @@ class SSHClient (ClosingContextManager):
for key in self._agent.get_keys():
try:
self._log(DEBUG, 'Trying SSH agent key %s' % hexlify(key.get_fingerprint()))
- # for 2-factor auth a successfully auth'd key will result in ['password']
- allowed_types = self._transport.auth_publickey(username, key)
- two_factor = (allowed_types == ['password'])
+ # for 2-factor auth a successfully auth'd key password will return an allowed 2fac auth method
+ allowed_types = set(self._transport.auth_publickey(username, key))
+ two_factor = (allowed_types & two_factor_types)
if not two_factor:
return
break
@@ -497,8 +558,8 @@ class SSHClient (ClosingContextManager):
key = pkey_class.from_private_key_file(filename, password)
self._log(DEBUG, 'Trying discovered key %s in %s' % (hexlify(key.get_fingerprint()), filename))
# for 2-factor auth a successfully auth'd key will result in ['password']
- allowed_types = self._transport.auth_publickey(username, key)
- two_factor = (allowed_types == ['password'])
+ allowed_types = set(self._transport.auth_publickey(username, key))
+ two_factor = (allowed_types & two_factor_types)
if not two_factor:
return
break
@@ -512,7 +573,11 @@ class SSHClient (ClosingContextManager):
except SSHException as e:
saved_exception = e
elif two_factor:
- raise SSHException('Two-factor authentication requires a password')
+ try:
+ self._transport.auth_interactive_dumb(username)
+ return
+ except SSHException as e:
+ saved_exception = e
# if we got an auth-failed exception earlier, re-raise it
if saved_exception is not None:
diff --git a/paramiko/config.py b/paramiko/config.py
index 233a87d9..0b1345fd 100644
--- a/paramiko/config.py
+++ b/paramiko/config.py
@@ -53,7 +53,7 @@ class SSHConfig (object):
"""
Read an OpenSSH config from the given file object.
- :param file file_obj: a file-like object to read the config file from
+ :param file_obj: a file-like object to read the config file from
"""
host = {"host": ['*'], "config": {}}
for line in file_obj:
@@ -98,7 +98,7 @@ class SSHConfig (object):
The host-matching rules of OpenSSH's ``ssh_config`` man page are used:
For each parameter, the first obtained value will be used. The
- configuration files contain sections separated by ``Host''
+ configuration files contain sections separated by ``Host``
specifications, and that section is only applied for hosts that match
one of the patterns given in the specification.
diff --git a/paramiko/ecdsakey.py b/paramiko/ecdsakey.py
index 6b047959..8827a1db 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 84868875..38ac866b 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):
@@ -92,11 +93,14 @@ class HostKeys (MutableMapping):
:raises IOError: if there was an error reading the file
"""
with open(filename, 'r') as f:
- for lineno, line in enumerate(f):
+ for lineno, line in enumerate(f, 1):
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_gex.py b/paramiko/kex_gex.py
index cb548f33..c980b690 100644
--- a/paramiko/kex_gex.py
+++ b/paramiko/kex_gex.py
@@ -23,7 +23,7 @@ client side, and a **lot** more on the server side.
"""
import os
-from hashlib import sha1
+from hashlib import sha1, sha256
from paramiko import util
from paramiko.common import DEBUG
@@ -44,6 +44,7 @@ class KexGex (object):
min_bits = 1024
max_bits = 8192
preferred_bits = 2048
+ hash_algo = sha1
def __init__(self, transport):
self.transport = transport
@@ -87,7 +88,7 @@ class KexGex (object):
return self._parse_kexdh_gex_reply(m)
elif ptype == _MSG_KEXDH_GEX_REQUEST_OLD:
return self._parse_kexdh_gex_request_old(m)
- raise SSHException('KexGex asked to handle packet type %d' % ptype)
+ raise SSHException('KexGex %s asked to handle packet type %d' % self.name, ptype)
### internals...
@@ -204,7 +205,7 @@ class KexGex (object):
hm.add_mpint(self.e)
hm.add_mpint(self.f)
hm.add_mpint(K)
- H = sha1(hm.asbytes()).digest()
+ H = self.hash_algo(hm.asbytes()).digest()
self.transport._set_K_H(K, H)
# sign it
sig = self.transport.get_server_key().sign_ssh_data(H)
@@ -239,6 +240,10 @@ class KexGex (object):
hm.add_mpint(self.e)
hm.add_mpint(self.f)
hm.add_mpint(K)
- self.transport._set_K_H(K, sha1(hm.asbytes()).digest())
+ self.transport._set_K_H(K, self.hash_algo(hm.asbytes()).digest())
self.transport._verify_key(host_key, sig)
self.transport._activate_outbound()
+
+class KexGexSHA256(KexGex):
+ name = 'diffie-hellman-group-exchange-sha256'
+ hash_algo = sha256
diff --git a/paramiko/kex_group1.py b/paramiko/kex_group1.py
index a88f00d2..9eee066c 100644
--- a/paramiko/kex_group1.py
+++ b/paramiko/kex_group1.py
@@ -45,6 +45,7 @@ class KexGroup1(object):
G = 2
name = 'diffie-hellman-group1-sha1'
+ hash_algo = sha1
def __init__(self, transport):
self.transport = transport
diff --git a/paramiko/kex_group14.py b/paramiko/kex_group14.py
index a914aeaf..9f7dd216 100644
--- a/paramiko/kex_group14.py
+++ b/paramiko/kex_group14.py
@@ -22,6 +22,7 @@ Standard SSH key exchange ("kex" if you wanna sound cool). Diffie-Hellman of
"""
from paramiko.kex_group1 import KexGroup1
+from hashlib import sha1
class KexGroup14(KexGroup1):
@@ -31,3 +32,4 @@ class KexGroup14(KexGroup1):
G = 2
name = 'diffie-hellman-group14-sha1'
+ hash_algo = sha1
diff --git a/paramiko/kex_gss.py b/paramiko/kex_gss.py
index d026807c..69969f8a 100644
--- a/paramiko/kex_gss.py
+++ b/paramiko/kex_gss.py
@@ -37,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 *
@@ -130,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):
@@ -366,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):
diff --git a/paramiko/message.py b/paramiko/message.py
index b893e76d..bf4c6b95 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 f516ff9b..2be2bb2b 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:
@@ -344,7 +389,8 @@ class Packetizer (object):
if self.__dump_packets:
self._log(DEBUG, util.format_binary(header, 'IN: '))
packet_size = struct.unpack('>I', header[:4])[0]
- # leftover contains decrypted bytes from the first block (after the length field)
+ # leftover contains decrypted bytes from the first block (after the
+ # length field)
leftover = header[4:]
if (packet_size - len(leftover)) % self.__block_size_in != 0:
raise SSHException('Invalid packet blocking')
diff --git a/paramiko/pkey.py b/paramiko/pkey.py
index 1b4af010..e95d60ba 100644
--- a/paramiko/pkey.py
+++ b/paramiko/pkey.py
@@ -171,8 +171,9 @@ class PKey (object):
is useless on the abstract PKey class.
:param str filename: name of the file to read
- :param str password: an optional password to use to decrypt the key file,
- if it's encrypted
+ :param str password:
+ an optional password to use to decrypt the key file, if it's
+ encrypted
:return: a new `.PKey` based on the given private key
:raises IOError: if there was an error reading the file
@@ -187,18 +188,18 @@ class PKey (object):
def from_private_key(cls, file_obj, password=None):
"""
Create a key object by reading a private key from a file (or file-like)
- object. If the private key is encrypted and ``password`` is not ``None``,
- the given password will be used to decrypt the key (otherwise
+ object. If the private key is encrypted and ``password`` is not
+ ``None``, the given password will be used to decrypt the key (otherwise
`.PasswordRequiredException` is thrown).
- :param file file_obj: the file to read from
+ :param file_obj: the file-like object to read from
:param str password:
an optional password to use to decrypt the key, if it's encrypted
:return: a new `.PKey` based on the given private key
:raises IOError: if there was an error reading the key
- :raises PasswordRequiredException: if the private key file is encrypted,
- and ``password`` is ``None``
+ :raises PasswordRequiredException:
+ if the private key file is encrypted, and ``password`` is ``None``
:raises SSHException: if the key file is invalid
"""
key = cls(file_obj=file_obj, password=password)
@@ -223,7 +224,7 @@ class PKey (object):
Write private key contents into a file (or file-like) object. If the
password is not ``None``, the key is encrypted before writing.
- :param file file_obj: the file object to write into
+ :param file_obj: the file-like object to write into
:param str password: an optional password to use to encrypt the key
:raises IOError: if there was an error writing to the file
@@ -273,7 +274,7 @@ class PKey (object):
start += 1
# find end
end = start
- while (lines[end].strip() != '-----END ' + tag + ' PRIVATE KEY-----') and (end < len(lines)):
+ while end < len(lines) and lines[end].strip() != '-----END ' + tag + ' PRIVATE KEY-----':
end += 1
# if we trudged to the end of the file, just try to cope.
try:
@@ -309,8 +310,9 @@ class PKey (object):
a trivially-encoded format (base64) which is completely insecure. If
a password is given, DES-EDE3-CBC is used.
- :param str tag: ``"RSA"`` or ``"DSA"``, the tag used to mark the data block.
- :param file filename: name of the file to write.
+ :param str tag:
+ ``"RSA"`` or ``"DSA"``, the tag used to mark the data block.
+ :param filename: name of the file to write.
:param str data: data blob that makes up the private key.
:param str password: an optional password to use to encrypt the file.
diff --git a/paramiko/py3compat.py b/paramiko/py3compat.py
index 57c096b2..6fafc31d 100644
--- a/paramiko/py3compat.py
+++ b/paramiko/py3compat.py
@@ -3,7 +3,7 @@ import base64
__all__ = ['PY2', 'string_types', 'integer_types', 'text_type', 'bytes_types', 'bytes', 'long', 'input',
'decodebytes', 'encodebytes', 'bytestring', 'byte_ord', 'byte_chr', 'byte_mask',
- 'b', 'u', 'b2s', 'StringIO', 'BytesIO', 'is_callable', 'MAXSIZE', 'next']
+ 'b', 'u', 'b2s', 'StringIO', 'BytesIO', 'is_callable', 'MAXSIZE', 'next', 'builtins']
PY2 = sys.version_info[0] < 3
@@ -18,6 +18,8 @@ if PY2:
decodebytes = base64.decodestring
encodebytes = base64.encodestring
+ import __builtin__ as builtins
+
def bytestring(s): # NOQA
if isinstance(s, unicode):
@@ -102,6 +104,7 @@ if PY2:
else:
import collections
import struct
+ import builtins
string_types = str
text_type = str
bytes = bytes
diff --git a/paramiko/server.py b/paramiko/server.py
index bf5039a2..f79a1748 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/sftp_attr.py b/paramiko/sftp_attr.py
index cf48f654..0eaca30b 100644
--- a/paramiko/sftp_attr.py
+++ b/paramiko/sftp_attr.py
@@ -209,12 +209,15 @@ class SFTPAttributes (object):
# not all servers support uid/gid
uid = self.st_uid
gid = self.st_gid
+ size = self.st_size
if uid is None:
uid = 0
if gid is None:
gid = 0
+ if size is None:
+ size = 0
- return '%s 1 %-8d %-8d %8d %-12s %s' % (ks, uid, gid, self.st_size, datestr, filename)
+ return '%s 1 %-8d %-8d %8d %-12s %s' % (ks, uid, gid, size, datestr, filename)
def asbytes(self):
return b(str(self))
diff --git a/paramiko/sftp_client.py b/paramiko/sftp_client.py
index e4c3a37d..da6f6e87 100644
--- a/paramiko/sftp_client.py
+++ b/paramiko/sftp_client.py
@@ -589,6 +589,7 @@ class SFTPClient(BaseSFTP, ClosingContextManager):
.. versionadded:: 1.4
"""
+ # TODO: make class initialize with self._cwd set to self.normalize('.')
return self._cwd and u(self._cwd)
def putfo(self, fl, remotepath, file_size=0, callback=None, confirm=True):
@@ -599,7 +600,7 @@ class SFTPClient(BaseSFTP, ClosingContextManager):
The SFTP operations use pipelining for speed.
- :param file fl: opened file or file-like object to copy
+ :param fl: opened file or file-like object to copy
:param str remotepath: the destination path on the SFTP server
:param int file_size:
optional size parameter passed to callback. If none is specified,
diff --git a/paramiko/ssh_exception.py b/paramiko/ssh_exception.py
index b99e42b3..02f3e52e 100644
--- a/paramiko/ssh_exception.py
+++ b/paramiko/ssh_exception.py
@@ -16,6 +16,8 @@
# along with Paramiko; if not, write to the Free Software Foundation, Inc.,
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
+import socket
+
class SSHException (Exception):
"""
@@ -105,7 +107,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
@@ -129,3 +135,39 @@ class ProxyCommandFailure (SSHException):
self.error = error
# for unpickling
self.args = (command, error, )
+
+
+class NoValidConnectionsError(socket.error):
+ """
+ Multiple connection attempts were made and no families succeeded.
+
+ This exception class wraps multiple "real" underlying connection errors,
+ all of which represent failed connection attempts. Because these errors are
+ not guaranteed to all be of the same error type (i.e. different errno,
+ `socket.error` subclass, message, etc) we expose a single unified error
+ message and a ``None`` errno so that instances of this class match most
+ normal handling of `socket.error` objects.
+
+ To see the wrapped exception objects, access the ``errors`` attribute.
+ ``errors`` is a dict whose keys are address tuples (e.g. ``('127.0.0.1',
+ 22)``) and whose values are the exception encountered trying to connect to
+ that address.
+
+ It is implied/assumed that all the errors given to a single instance of
+ this class are from connecting to the same hostname + port (and thus that
+ the differences are in the resolution of the hostname - e.g. IPv4 vs v6).
+ """
+ def __init__(self, errors):
+ """
+ :param dict errors:
+ The errors dict to store, as described by class docstring.
+ """
+ addrs = errors.keys()
+ body = ', '.join([x[0] for x in addrs[:-1]])
+ tail = addrs[-1][0]
+ msg = "Unable to connect to port {0} on {1} or {2}"
+ super(NoValidConnectionsError, self).__init__(
+ None, # stand-in for errno
+ msg.format(addrs[0][1], body, tail)
+ )
+ self.errors = errors
diff --git a/paramiko/ssh_gss.py b/paramiko/ssh_gss.py
index aa28e2ec..e9b13a66 100644
--- a/paramiko/ssh_gss.py
+++ b/paramiko/ssh_gss.py
@@ -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 36da3043..18fb103b 100644
--- a/paramiko/transport.py
+++ b/paramiko/transport.py
@@ -20,13 +20,14 @@
Core protocol implementation
"""
+from __future__ import print_function
import os
import socket
import sys
import threading
import time
import weakref
-from hashlib import md5, sha1
+from hashlib import md5, sha1, sha256, sha512
import paramiko
from paramiko import util
@@ -47,7 +48,7 @@ from paramiko.common import xffffffff, cMSG_CHANNEL_OPEN, cMSG_IGNORE, \
MAX_WINDOW_SIZE, DEFAULT_WINDOW_SIZE, DEFAULT_MAX_PACKET_SIZE
from paramiko.compress import ZlibCompressor, ZlibDecompressor
from paramiko.dsskey import DSSKey
-from paramiko.kex_gex import KexGex
+from paramiko.kex_gex import KexGex, KexGexSHA256
from paramiko.kex_group1 import KexGroup1
from paramiko.kex_group14 import KexGroup14
from paramiko.kex_gss import KexGSSGex, KexGSSGroup1, KexGSSGroup14, NullHostKey
@@ -94,27 +95,109 @@ class Transport (threading.Thread, ClosingContextManager):
_PROTO_ID = '2.0'
_CLIENT_ID = 'paramiko_%s' % paramiko.__version__
- _preferred_ciphers = ('aes128-ctr', 'aes256-ctr', 'aes128-cbc', 'blowfish-cbc',
- 'aes256-cbc', '3des-cbc', 'arcfour128', 'arcfour256')
- _preferred_macs = ('hmac-sha1', 'hmac-md5', 'hmac-sha1-96', 'hmac-md5-96')
- _preferred_keys = ('ssh-rsa', 'ssh-dss', 'ecdsa-sha2-nistp256')
- _preferred_kex = ( 'diffie-hellman-group14-sha1', 'diffie-hellman-group-exchange-sha1' , 'diffie-hellman-group1-sha1')
+ # These tuples of algorithm identifiers are in preference order; do not
+ # reorder without reason!
+ _preferred_ciphers = (
+ 'aes128-ctr',
+ 'aes192-ctr',
+ 'aes256-ctr',
+ 'aes128-cbc',
+ 'blowfish-cbc',
+ 'aes192-cbc',
+ 'aes256-cbc',
+ '3des-cbc',
+ 'arcfour128',
+ 'arcfour256',
+ )
+ _preferred_macs = (
+ 'hmac-sha2-256',
+ 'hmac-sha2-512',
+ 'hmac-md5',
+ 'hmac-sha1-96',
+ 'hmac-md5-96',
+ 'hmac-sha1',
+ )
+ _preferred_keys = (
+ 'ssh-rsa',
+ 'ssh-dss',
+ 'ecdsa-sha2-nistp256',
+ )
+ _preferred_kex = (
+ 'diffie-hellman-group1-sha1',
+ 'diffie-hellman-group14-sha1',
+ 'diffie-hellman-group-exchange-sha1',
+ 'diffie-hellman-group-exchange-sha256',
+ )
_preferred_compression = ('none',)
_cipher_info = {
- 'aes128-ctr': {'class': AES, 'mode': AES.MODE_CTR, 'block-size': 16, 'key-size': 16},
- 'aes256-ctr': {'class': AES, 'mode': AES.MODE_CTR, 'block-size': 16, 'key-size': 32},
- 'blowfish-cbc': {'class': Blowfish, 'mode': Blowfish.MODE_CBC, 'block-size': 8, 'key-size': 16},
- 'aes128-cbc': {'class': AES, 'mode': AES.MODE_CBC, 'block-size': 16, 'key-size': 16},
- 'aes256-cbc': {'class': AES, 'mode': AES.MODE_CBC, 'block-size': 16, 'key-size': 32},
- '3des-cbc': {'class': DES3, 'mode': DES3.MODE_CBC, 'block-size': 8, 'key-size': 24},
- 'arcfour128': {'class': ARC4, 'mode': None, 'block-size': 8, 'key-size': 16},
- 'arcfour256': {'class': ARC4, 'mode': None, 'block-size': 8, 'key-size': 32},
+ 'aes128-ctr': {
+ 'class': AES,
+ 'mode': AES.MODE_CTR,
+ 'block-size': 16,
+ 'key-size': 16
+ },
+ 'aes192-ctr': {
+ 'class': AES,
+ 'mode': AES.MODE_CTR,
+ 'block-size': 16,
+ 'key-size': 24
+ },
+ 'aes256-ctr': {
+ 'class': AES,
+ 'mode': AES.MODE_CTR,
+ 'block-size': 16,
+ 'key-size': 32
+ },
+ 'blowfish-cbc': {
+ 'class': Blowfish,
+ 'mode': Blowfish.MODE_CBC,
+ 'block-size': 8,
+ 'key-size': 16
+ },
+ 'aes128-cbc': {
+ 'class': AES,
+ 'mode': AES.MODE_CBC,
+ 'block-size': 16,
+ 'key-size': 16
+ },
+ 'aes192-cbc': {
+ 'class': AES,
+ 'mode': AES.MODE_CBC,
+ 'block-size': 16,
+ 'key-size': 24
+ },
+ 'aes256-cbc': {
+ 'class': AES,
+ 'mode': AES.MODE_CBC,
+ 'block-size': 16,
+ 'key-size': 32
+ },
+ '3des-cbc': {
+ 'class': DES3,
+ 'mode': DES3.MODE_CBC,
+ 'block-size': 8,
+ 'key-size': 24
+ },
+ 'arcfour128': {
+ 'class': ARC4,
+ 'mode': None,
+ 'block-size': 8,
+ 'key-size': 16
+ },
+ 'arcfour256': {
+ 'class': ARC4,
+ 'mode': None,
+ 'block-size': 8,
+ 'key-size': 32
+ },
}
_mac_info = {
'hmac-sha1': {'class': sha1, 'size': 20},
'hmac-sha1-96': {'class': sha1, 'size': 12},
+ 'hmac-sha2-256': {'class': sha256, 'size': 32},
+ 'hmac-sha2-512': {'class': sha512, 'size': 64},
'hmac-md5': {'class': md5, 'size': 16},
'hmac-md5-96': {'class': md5, 'size': 12},
}
@@ -129,6 +212,7 @@ class Transport (threading.Thread, ClosingContextManager):
'diffie-hellman-group1-sha1': KexGroup1,
'diffie-hellman-group14-sha1': KexGroup14,
'diffie-hellman-group-exchange-sha1': KexGex,
+ 'diffie-hellman-group-exchange-sha256': KexGexSHA256,
'gss-group1-sha1-toWM5Slw5Ew8Mqkay+al2g==': KexGSSGroup1,
'gss-group14-sha1-toWM5Slw5Ew8Mqkay+al2g==': KexGSSGroup14,
'gss-gex-sha1-toWM5Slw5Ew8Mqkay+al2g==': KexGSSGex
@@ -295,6 +379,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 +673,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 +698,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 +746,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 +771,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 +813,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 +823,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
@@ -1284,6 +1378,27 @@ class Transport (threading.Thread, ClosingContextManager):
self.auth_handler.auth_interactive(username, handler, my_event, submethods)
return self.auth_handler.wait_for_response(my_event)
+ def auth_interactive_dumb(self, username, handler=None, submethods=''):
+ """
+ Autenticate to the server interactively but dumber.
+ Just print the prompt and / or instructions to stdout and send back
+ the response. This is good for situations where partial auth is
+ achieved by key and then the user has to enter a 2fac token.
+ """
+
+ if not handler:
+ def handler(title, instructions, prompt_list):
+ answers = []
+ if title:
+ print(title.strip())
+ if instructions:
+ print(instructions.strip())
+ for prompt,show_input in prompt_list:
+ print(prompt.strip(),end=' ')
+ answers.append(raw_input())
+ return answers
+ return self.auth_interactive(username, handler, submethods)
+
def auth_gssapi_with_mic(self, username, gss_host, gss_deleg_creds):
"""
Authenticate to the Server using GSS-API / SSPI.
@@ -1497,13 +1612,23 @@ class Transport (threading.Thread, ClosingContextManager):
m.add_bytes(self.H)
m.add_byte(b(id))
m.add_bytes(self.session_id)
- out = sofar = sha1(m.asbytes()).digest()
+ # Fallback to SHA1 for kex engines that fail to specify a hex
+ # algorithm, or for e.g. transport tests that don't run kexinit.
+ hash_algo = getattr(self.kex_engine, 'hash_algo', None)
+ hash_select_msg = "kex engine %s specified hash_algo %r" % (self.kex_engine.__class__.__name__, hash_algo)
+ if hash_algo is None:
+ hash_algo = sha1
+ hash_select_msg += ", falling back to sha1"
+ if not hasattr(self, '_logged_hash_selection'):
+ self._log(DEBUG, hash_select_msg)
+ setattr(self, '_logged_hash_selection', True)
+ out = sofar = hash_algo(m.asbytes()).digest()
while len(out) < nbytes:
m = Message()
m.add_mpint(self.K)
m.add_bytes(self.H)
m.add_bytes(sofar)
- digest = sha1(m.asbytes()).digest()
+ digest = hash_algo(m.asbytes()).digest()
out += digest
sofar += digest
return out[:nbytes]
@@ -1581,7 +1706,16 @@ class Transport (threading.Thread, ClosingContextManager):
try:
try:
self.packetizer.write_all(b(self.local_version + '\r\n'))
+ self._log(DEBUG, 'Local version/idstring: %s' % self.local_version)
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 +1765,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())
@@ -1679,6 +1814,18 @@ class Transport (threading.Thread, ClosingContextManager):
if self.sys.modules is not None:
raise
+
+ def _log_agreement(self, which, local, remote):
+ # Log useful, non-duplicative line re: an agreed-upon algorithm.
+ # Old code implied algorithms could be asymmetrical (different for
+ # inbound vs outbound) so we preserve that possibility.
+ msg = "{0} agreed: ".format(which)
+ if local == remote:
+ msg += local
+ else:
+ msg += "local={0}, remote={1}".format(local, remote)
+ self._log(DEBUG, msg)
+
### protocol stages
def _negotiate_keys(self, m):
@@ -1716,6 +1863,7 @@ class Transport (threading.Thread, ClosingContextManager):
raise SSHException('Indecipherable protocol version "' + buf + '"')
# save this server version string for later
self.remote_version = buf
+ self._log(DEBUG, 'Remote version/idstring: %s' % buf)
# pull off any attached comment
comment = ''
i = buf.find(' ')
@@ -1744,10 +1892,12 @@ class Transport (threading.Thread, ClosingContextManager):
self.clear_to_send_lock.release()
self.in_kex = True
if self.server_mode:
- if (self._modulus_pack is None) and ('diffie-hellman-group-exchange-sha1' in self._preferred_kex):
+ mp_required_prefix = 'diffie-hellman-group-exchange-sha'
+ kex_mp = [k for k in self._preferred_kex if k.startswith(mp_required_prefix)]
+ if (self._modulus_pack is None) and (len(kex_mp) > 0):
# can't do group-exchange if we don't have a pack of potential primes
- pkex = list(self.get_security_options().kex)
- pkex.remove('diffie-hellman-group-exchange-sha1')
+ pkex = [k for k in self.get_security_options().kex
+ if not k.startswith(mp_required_prefix)]
self.get_security_options().kex = pkex
available_server_keys = list(filter(list(self.server_key_dict.keys()).__contains__,
self._preferred_keys))
@@ -1799,15 +1949,24 @@ class Transport (threading.Thread, ClosingContextManager):
' server lang:' + str(server_lang_list) +
' kex follows?' + str(kex_follows))
- # as a server, we pick the first item in the client's list that we support.
- # as a client, we pick the first item in our list that the server supports.
+ # as a server, we pick the first item in the client's list that we
+ # support.
+ # as a client, we pick the first item in our list that the server
+ # supports.
if self.server_mode:
- agreed_kex = list(filter(self._preferred_kex.__contains__, kex_algo_list))
+ agreed_kex = list(filter(
+ self._preferred_kex.__contains__,
+ kex_algo_list
+ ))
else:
- agreed_kex = list(filter(kex_algo_list.__contains__, self._preferred_kex))
+ agreed_kex = list(filter(
+ kex_algo_list.__contains__,
+ self._preferred_kex
+ ))
if len(agreed_kex) == 0:
raise SSHException('Incompatible ssh peer (no acceptable kex algorithm)')
self.kex_engine = self._kex_info[agreed_kex[0]](self)
+ self._log(DEBUG, "Kex agreed: %s" % agreed_kex[0])
if self.server_mode:
available_server_keys = list(filter(list(self.server_key_dict.keys()).__contains__,
@@ -1835,7 +1994,9 @@ class Transport (threading.Thread, ClosingContextManager):
raise SSHException('Incompatible ssh server (no acceptable ciphers)')
self.local_cipher = agreed_local_ciphers[0]
self.remote_cipher = agreed_remote_ciphers[0]
- self._log(DEBUG, 'Ciphers agreed: local=%s, remote=%s' % (self.local_cipher, self.remote_cipher))
+ self._log_agreement(
+ 'Cipher', local=self.local_cipher, remote=self.remote_cipher
+ )
if self.server_mode:
agreed_remote_macs = list(filter(self._preferred_macs.__contains__, client_mac_algo_list))
@@ -1847,6 +2008,9 @@ class Transport (threading.Thread, ClosingContextManager):
raise SSHException('Incompatible ssh server (no acceptable macs)')
self.local_mac = agreed_local_macs[0]
self.remote_mac = agreed_remote_macs[0]
+ self._log_agreement(
+ 'MAC', local=self.local_mac, remote=self.remote_mac
+ )
if self.server_mode:
agreed_remote_compression = list(filter(self._preferred_compression.__contains__, client_compress_algo_list))
@@ -1858,10 +2022,11 @@ class Transport (threading.Thread, ClosingContextManager):
raise SSHException('Incompatible ssh server (no acceptable compression) %r %r %r' % (agreed_local_compression, agreed_remote_compression, self._preferred_compression))
self.local_compression = agreed_local_compression[0]
self.remote_compression = agreed_remote_compression[0]
-
- self._log(DEBUG, 'using kex %s; server key type %s; cipher: local %s, remote %s; mac: local %s, remote %s; compression: local %s, remote %s' %
- (agreed_kex[0], self.host_key_type, self.local_cipher, self.remote_cipher, self.local_mac,
- self.remote_mac, self.local_compression, self.remote_compression))
+ self._log_agreement(
+ 'Compression',
+ local=self.local_compression,
+ remote=self.remote_compression
+ )
# save for computing hash later...
# now wait! openssh has a bug (and others might too) where there are
@@ -1882,8 +2047,8 @@ class Transport (threading.Thread, ClosingContextManager):
engine = self._get_cipher(self.remote_cipher, key_in, IV_in)
mac_size = self._mac_info[self.remote_mac]['size']
mac_engine = self._mac_info[self.remote_mac]['class']
- # initial mac keys are done in the hash's natural size (not the potentially truncated
- # transmission size)
+ # initial mac keys are done in the hash's natural size (not the
+ # potentially truncated transmission size)
if self.server_mode:
mac_key = self._compute_key('E', mac_engine().digest_size)
else:
@@ -1909,8 +2074,8 @@ class Transport (threading.Thread, ClosingContextManager):
engine = self._get_cipher(self.local_cipher, key_out, IV_out)
mac_size = self._mac_info[self.local_mac]['size']
mac_engine = self._mac_info[self.local_mac]['class']
- # initial mac keys are done in the hash's natural size (not the potentially truncated
- # transmission size)
+ # initial mac keys are done in the hash's natural size (not the
+ # potentially truncated transmission size)
if self.server_mode:
mac_key = self._compute_key('F', mac_engine().digest_size)
else:
diff --git a/paramiko/util.py b/paramiko/util.py
index d9a29d74..855e5757 100644
--- a/paramiko/util.py
+++ b/paramiko/util.py
@@ -118,7 +118,7 @@ def safe_string(s):
def bit_length(n):
try:
- return n.bitlength()
+ return n.bit_length()
except AttributeError:
norm = deflate_long(n, False)
hbyte = byte_ord(norm[0])
diff --git a/setup.py b/setup.py
index 003a0617..629c28fd 100644
--- a/setup.py
+++ b/setup.py
@@ -87,6 +87,7 @@ setup(
'Programming Language :: Python :: 3.2',
'Programming Language :: Python :: 3.3',
'Programming Language :: Python :: 3.4',
+ 'Programming Language :: Python :: 3.5',
],
**kw
)
diff --git a/setup_helper.py b/setup_helper.py
index ff6b0e16..9e3834b3 100644
--- a/setup_helper.py
+++ b/setup_helper.py
@@ -30,9 +30,42 @@ import distutils.archive_util
from distutils.dir_util import mkpath
from distutils.spawn import spawn
-
-def make_tarball(base_name, base_dir, compress='gzip',
- verbose=False, dry_run=False):
+try:
+ from pwd import getpwnam
+except ImportError:
+ getpwnam = None
+
+try:
+ from grp import getgrnam
+except ImportError:
+ getgrnam = None
+
+def _get_gid(name):
+ """Returns a gid, given a group name."""
+ if getgrnam is None or name is None:
+ return None
+ try:
+ result = getgrnam(name)
+ except KeyError:
+ result = None
+ if result is not None:
+ return result[2]
+ return None
+
+def _get_uid(name):
+ """Returns an uid, given a user name."""
+ if getpwnam is None or name is None:
+ return None
+ try:
+ result = getpwnam(name)
+ except KeyError:
+ result = None
+ if result is not None:
+ return result[2]
+ return None
+
+def make_tarball(base_name, base_dir, compress='gzip', verbose=0, dry_run=0,
+ owner=None, group=None):
"""Create a tar file from all the files under 'base_dir'.
This file may be compressed.
@@ -75,11 +108,30 @@ def make_tarball(base_name, base_dir, compress='gzip',
mkpath(os.path.dirname(archive_name), dry_run=dry_run)
log.info('Creating tar file %s with mode %s' % (archive_name, mode))
+ uid = _get_uid(owner)
+ gid = _get_gid(group)
+
+ def _set_uid_gid(tarinfo):
+ if gid is not None:
+ tarinfo.gid = gid
+ tarinfo.gname = group
+ if uid is not None:
+ tarinfo.uid = uid
+ tarinfo.uname = owner
+ return tarinfo
+
if not dry_run:
tar = tarfile.open(archive_name, mode=mode)
# This recursively adds everything underneath base_dir
- tar.add(base_dir)
- tar.close()
+ try:
+ try:
+ # Support for the `filter' parameter was added in Python 2.7,
+ # earlier versions will raise TypeError.
+ tar.add(base_dir, filter=_set_uid_gid)
+ except TypeError:
+ tar.add(base_dir)
+ finally:
+ tar.close()
if compress and compress not in tarfile_compress_flag:
spawn([compress] + compress_flags[compress] + [archive_name],
diff --git a/sites/shared_conf.py b/sites/shared_conf.py
index 4a6a5c4e..99fab315 100644
--- a/sites/shared_conf.py
+++ b/sites/shared_conf.py
@@ -12,7 +12,6 @@ html_theme_options = {
'description': "A Python implementation of SSHv2.",
'github_user': 'paramiko',
'github_repo': 'paramiko',
- 'gratipay_user': 'bitprophet',
'analytics_id': 'UA-18486793-2',
'travis_button': True,
}
diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst
index 6520dde4..6ea85c45 100644
--- a/sites/www/changelog.rst
+++ b/sites/www/changelog.rst
@@ -2,10 +2,89 @@
Changelog
=========
+* :feature:`467` (also :issue:`139`, :issue:`412`) Fully enable two-factor
+ authentication (e.g. when a server requires ``AuthenticationMethods
+ pubkey,keyboard-interactive``). Thanks to ``@perryjrandall`` for the patch
+ and to ``@nevins-b`` and Matt Robenolt for additional support.
+* :bug:`502 major` Fix 'exec' requests in server mode to use ``get_string``
+ instead of ``get_text`` to avoid ``UnicodeDecodeError`` on non-UTF-8 input.
+ Thanks to Anselm Kruis for the patch & discussion.
+* :bug:`401` Fix line number reporting in log output regarding invalid
+ ``known_hosts`` line entries. Thanks to Dylan Thacker-Smith for catch &
+ patch.
+* :support:`525 backported` Update the vendored Windows API addon to a more
+ recent edition. Also fixes :issue:`193`, :issue:`488`, :issue:`498`. Thanks
+ to Jason Coombs.
+* :release:`1.15.4 <2015-11-02>`
+* :release:`1.14.3 <2015-11-02>`
+* :release:`1.13.4 <2015-11-02>`
+* :bug:`366` Fix `~paramiko.sftp_attributes.SFTPAttributes` so its string
+ representation doesn't raise exceptions on empty/initialized instances. Patch
+ by Ulrich Petri.
+* :bug:`359` Use correct attribute name when trying to use Python 3's
+ ``int.bit_length`` method; prior to fix, the Python 2 custom fallback
+ implementation was always used, even on Python 3. Thanks to Alex Gaynor.
+* :support:`594 backported` Correct some post-Python3-port docstrings to
+ specify ``bytes`` type instead of ``str``. Credit to ``@redixin``.
+* :bug:`565` Don't explode with ``IndexError`` when reading private key files
+ lacking an ``-----END <type> PRIVATE KEY-----`` footer. Patch courtesy of
+ Prasanna Santhanam.
+* :feature:`604` Add support for the ``aes192-ctr`` and ``aes192-cbc`` ciphers.
+ Thanks to Michiel Tiller for noticing it was as easy as tweaking some key
+ sizes :D
+* :feature:`356` (also :issue:`596`, :issue:`365`, :issue:`341`, :issue:`164`,
+ :issue:`581`, and a bunch of other duplicates besides) Add support for SHA-2
+ based key exchange (kex) algorithm ``diffie-hellman-group-exchange-sha256``
+ and (H)MAC algorithms ``hmac-sha2-256`` and ``hmac-sha2-512``.
+
+ This change includes tweaks to debug-level logging regarding
+ algorithm-selection handshakes; the old all-in-one log line is now multiple
+ easier-to-read, printed-at-handshake-time log lines.
+
+ Thanks to the many people who submitted patches for this functionality and/or
+ assisted in testing those patches. That list includes but is not limited to,
+ and in no particular order: Matthias Witte, Dag Wieers, Ash Berlin, Etienne
+ Perot, Gert van Dijk, ``@GuyShaanan``, Aaron Bieber, ``@cyphase``, and Eric
+ Brown.
+* :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.
+* :support:`582` Fix some old ``setup.py`` related helper code which was
+ breaking ``bdist_dumb`` on Mac OS X. Thanks to Peter Odding for the patch.
+* :bug:`22 major` Try harder to connect to multiple network families (e.g. IPv4
+ vs IPv6) in case of connection issues; this helps with problems such as hosts
+ which resolve both IPv4 and IPv6 addresses but are only listening on IPv4.
+ Thanks to Dries Desmet for original report and Torsten Landschoff for the
+ foundational patchset.
* :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 ``AuthenticationError``. Credit to Ken
- Jordan for the fix and Yvan Marques for original report.
+ ``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/tasks.py b/tasks.py
index 3d575670..3d55a778 100644
--- a/tasks.py
+++ b/tasks.py
@@ -3,17 +3,18 @@ from os.path import join
from shutil import rmtree, copytree
from invoke import Collection, ctask as task
-from invocations.docs import docs, www
+from invocations.docs import docs, www, sites
from invocations.packaging import publish
# Until we move to spec-based testing
@task
-def test(ctx, coverage=False):
+def test(ctx, coverage=False, flags=""):
+ if "--verbose" not in flags.split():
+ flags += " --verbose"
runner = "python"
if coverage:
runner = "coverage run --source=paramiko"
- flags = "--verbose"
ctx.run("{0} test.py {1}".format(runner, flags), pty=True)
@@ -30,11 +31,12 @@ def release(ctx):
# Move the built docs into where Epydocs used to live
target = 'docs'
rmtree(target, ignore_errors=True)
- copytree(docs_build, target)
+ # TODO: make it easier to yank out this config val from the docs coll
+ copytree('sites/docs/_build', target)
# Publish
publish(ctx)
# Remind
print("\n\nDon't forget to update RTD's versions page for new minor releases!")
-ns = Collection(test, coverage, release, docs, www)
+ns = Collection(test, coverage, release, docs, www, sites)
diff --git a/tests/test_client.py b/tests/test_client.py
index 3d2e75c9..04cab439 100644
--- a/tests/test_client.py
+++ b/tests/test_client.py
@@ -75,7 +75,7 @@ class NullServer (paramiko.ServerInterface):
return paramiko.OPEN_SUCCEEDED
def check_channel_exec_request(self, channel, command):
- if command != 'yes':
+ if command != b'yes':
return False
return True
diff --git a/tests/test_hostkeys.py b/tests/test_hostkeys.py
index 0ee1bbf0..2bdcad9c 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_kex.py b/tests/test_kex.py
index 56f1b7c7..19804fbf 100644
--- a/tests/test_kex.py
+++ b/tests/test_kex.py
@@ -26,7 +26,7 @@ import unittest
import paramiko.util
from paramiko.kex_group1 import KexGroup1
-from paramiko.kex_gex import KexGex
+from paramiko.kex_gex import KexGex, KexGexSHA256
from paramiko import Message
from paramiko.common import byte_chr
@@ -252,3 +252,121 @@ class KexTest (unittest.TestCase):
self.assertEqual(H, hexlify(transport._H).upper())
self.assertEqual(x, hexlify(transport._message.asbytes()).upper())
self.assertTrue(transport._activated)
+
+ def test_7_gex_sha256_client(self):
+ transport = FakeTransport()
+ transport.server_mode = False
+ kex = KexGexSHA256(transport)
+ kex.start_kex()
+ x = b'22000004000000080000002000'
+ self.assertEqual(x, hexlify(transport._message.asbytes()).upper())
+ self.assertEqual((paramiko.kex_gex._MSG_KEXDH_GEX_GROUP,), transport._expect)
+
+ msg = Message()
+ msg.add_mpint(FakeModulusPack.P)
+ msg.add_mpint(FakeModulusPack.G)
+ msg.rewind()
+ kex.parse_next(paramiko.kex_gex._MSG_KEXDH_GEX_GROUP, msg)
+ x = b'20000000807E2DDB1743F3487D6545F04F1C8476092FB912B013626AB5BCEB764257D88BBA64243B9F348DF7B41B8C814A995E00299913503456983FFB9178D3CD79EB6D55522418A8ABF65375872E55938AB99A84A0B5FC8A1ECC66A7C3766E7E0F80B7CE2C9225FC2DD683F4764244B72963BBB383F529DCF0C5D17740B8A2ADBE9208D4'
+ self.assertEqual(x, hexlify(transport._message.asbytes()).upper())
+ self.assertEqual((paramiko.kex_gex._MSG_KEXDH_GEX_REPLY,), transport._expect)
+
+ msg = Message()
+ msg.add_string('fake-host-key')
+ msg.add_mpint(69)
+ msg.add_string('fake-sig')
+ msg.rewind()
+ kex.parse_next(paramiko.kex_gex._MSG_KEXDH_GEX_REPLY, msg)
+ H = b'AD1A9365A67B4496F05594AD1BF656E3CDA0851289A4C1AFF549FEAE50896DF4'
+ self.assertEqual(self.K, transport._K)
+ self.assertEqual(H, hexlify(transport._H).upper())
+ self.assertEqual((b'fake-host-key', b'fake-sig'), transport._verify)
+ self.assertTrue(transport._activated)
+
+ def test_8_gex_sha256_old_client(self):
+ transport = FakeTransport()
+ transport.server_mode = False
+ kex = KexGexSHA256(transport)
+ kex.start_kex(_test_old_style=True)
+ x = b'1E00000800'
+ self.assertEqual(x, hexlify(transport._message.asbytes()).upper())
+ self.assertEqual((paramiko.kex_gex._MSG_KEXDH_GEX_GROUP,), transport._expect)
+
+ msg = Message()
+ msg.add_mpint(FakeModulusPack.P)
+ msg.add_mpint(FakeModulusPack.G)
+ msg.rewind()
+ kex.parse_next(paramiko.kex_gex._MSG_KEXDH_GEX_GROUP, msg)
+ x = b'20000000807E2DDB1743F3487D6545F04F1C8476092FB912B013626AB5BCEB764257D88BBA64243B9F348DF7B41B8C814A995E00299913503456983FFB9178D3CD79EB6D55522418A8ABF65375872E55938AB99A84A0B5FC8A1ECC66A7C3766E7E0F80B7CE2C9225FC2DD683F4764244B72963BBB383F529DCF0C5D17740B8A2ADBE9208D4'
+ self.assertEqual(x, hexlify(transport._message.asbytes()).upper())
+ self.assertEqual((paramiko.kex_gex._MSG_KEXDH_GEX_REPLY,), transport._expect)
+
+ msg = Message()
+ msg.add_string('fake-host-key')
+ msg.add_mpint(69)
+ msg.add_string('fake-sig')
+ msg.rewind()
+ kex.parse_next(paramiko.kex_gex._MSG_KEXDH_GEX_REPLY, msg)
+ H = b'518386608B15891AE5237DEE08DCADDE76A0BCEFCE7F6DB3AD66BC41D256DFE5'
+ self.assertEqual(self.K, transport._K)
+ self.assertEqual(H, hexlify(transport._H).upper())
+ self.assertEqual((b'fake-host-key', b'fake-sig'), transport._verify)
+ self.assertTrue(transport._activated)
+
+ def test_9_gex_sha256_server(self):
+ transport = FakeTransport()
+ transport.server_mode = True
+ kex = KexGexSHA256(transport)
+ kex.start_kex()
+ self.assertEqual((paramiko.kex_gex._MSG_KEXDH_GEX_REQUEST, paramiko.kex_gex._MSG_KEXDH_GEX_REQUEST_OLD), transport._expect)
+
+ msg = Message()
+ msg.add_int(1024)
+ msg.add_int(2048)
+ msg.add_int(4096)
+ msg.rewind()
+ kex.parse_next(paramiko.kex_gex._MSG_KEXDH_GEX_REQUEST, msg)
+ x = b'1F0000008100FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE65381FFFFFFFFFFFFFFFF0000000102'
+ self.assertEqual(x, hexlify(transport._message.asbytes()).upper())
+ self.assertEqual((paramiko.kex_gex._MSG_KEXDH_GEX_INIT,), transport._expect)
+
+ msg = Message()
+ msg.add_mpint(12345)
+ msg.rewind()
+ kex.parse_next(paramiko.kex_gex._MSG_KEXDH_GEX_INIT, msg)
+ K = 67592995013596137876033460028393339951879041140378510871612128162185209509220726296697886624612526735888348020498716482757677848959420073720160491114319163078862905400020959196386947926388406687288901564192071077389283980347784184487280885335302632305026248574716290537036069329724382811853044654824945750581
+ H = b'CCAC0497CF0ABA1DBF55E1A3995D17F4CC31824B0E8D95CDF8A06F169D050D80'
+ x = b'210000000866616B652D6B6579000000807E2DDB1743F3487D6545F04F1C8476092FB912B013626AB5BCEB764257D88BBA64243B9F348DF7B41B8C814A995E00299913503456983FFB9178D3CD79EB6D55522418A8ABF65375872E55938AB99A84A0B5FC8A1ECC66A7C3766E7E0F80B7CE2C9225FC2DD683F4764244B72963BBB383F529DCF0C5D17740B8A2ADBE9208D40000000866616B652D736967'
+ self.assertEqual(K, transport._K)
+ self.assertEqual(H, hexlify(transport._H).upper())
+ self.assertEqual(x, hexlify(transport._message.asbytes()).upper())
+ self.assertTrue(transport._activated)
+
+ def test_10_gex_sha256_server_with_old_client(self):
+ transport = FakeTransport()
+ transport.server_mode = True
+ kex = KexGexSHA256(transport)
+ kex.start_kex()
+ self.assertEqual((paramiko.kex_gex._MSG_KEXDH_GEX_REQUEST, paramiko.kex_gex._MSG_KEXDH_GEX_REQUEST_OLD), transport._expect)
+
+ msg = Message()
+ msg.add_int(2048)
+ msg.rewind()
+ kex.parse_next(paramiko.kex_gex._MSG_KEXDH_GEX_REQUEST_OLD, msg)
+ x = b'1F0000008100FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE65381FFFFFFFFFFFFFFFF0000000102'
+ self.assertEqual(x, hexlify(transport._message.asbytes()).upper())
+ self.assertEqual((paramiko.kex_gex._MSG_KEXDH_GEX_INIT,), transport._expect)
+
+ msg = Message()
+ msg.add_mpint(12345)
+ msg.rewind()
+ kex.parse_next(paramiko.kex_gex._MSG_KEXDH_GEX_INIT, msg)
+ K = 67592995013596137876033460028393339951879041140378510871612128162185209509220726296697886624612526735888348020498716482757677848959420073720160491114319163078862905400020959196386947926388406687288901564192071077389283980347784184487280885335302632305026248574716290537036069329724382811853044654824945750581
+ H = b'3DDD2AD840AD095E397BA4D0573972DC60F6461FD38A187CACA6615A5BC8ADBB'
+ x = b'210000000866616B652D6B6579000000807E2DDB1743F3487D6545F04F1C8476092FB912B013626AB5BCEB764257D88BBA64243B9F348DF7B41B8C814A995E00299913503456983FFB9178D3CD79EB6D55522418A8ABF65375872E55938AB99A84A0B5FC8A1ECC66A7C3766E7E0F80B7CE2C9225FC2DD683F4764244B72963BBB383F529DCF0C5D17740B8A2ADBE9208D40000000866616B652D736967'
+ self.assertEqual(K, transport._K)
+ self.assertEqual(H, hexlify(transport._H).upper())
+ self.assertEqual(x, hexlify(transport._message.asbytes()).upper())
+ self.assertTrue(transport._activated)
+
+
diff --git a/tests/test_message.py b/tests/test_message.py
index f308c037..f18cae90 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_sftp.py b/tests/test_sftp.py
index 55762c21..131b8abf 100755
--- a/tests/test_sftp.py
+++ b/tests/test_sftp.py
@@ -812,6 +812,11 @@ class SFTPTest (unittest.TestCase):
sftp.remove('%s/nonutf8data' % FOLDER)
+ def test_sftp_attributes_empty_str(self):
+ sftp_attributes = SFTPAttributes()
+ self.assertEqual(str(sftp_attributes), "?--------- 1 0 0 0 (unknown date) ?")
+
+
if __name__ == '__main__':
SFTPTest.init_loopback()
# logging is required by test_N_file_with_percent
diff --git a/tests/test_transport.py b/tests/test_transport.py
index 5cf9a867..a93d8b63 100644
--- a/tests/test_transport.py
+++ b/tests/test_transport.py
@@ -28,6 +28,7 @@ import socket
import time
import threading
import random
+from hashlib import sha1
import unittest
from paramiko import Transport, SecurityOptions, ServerInterface, RSAKey, DSSKey, \
@@ -77,7 +78,7 @@ class NullServer (ServerInterface):
return OPEN_SUCCEEDED
def check_channel_exec_request(self, channel, command):
- if command != 'yes':
+ if command != b'yes':
return False
return True
@@ -251,7 +252,7 @@ class TransportTest(unittest.TestCase):
chan = self.tc.open_session()
schan = self.ts.accept(1.0)
try:
- chan.exec_command('no')
+ chan.exec_command(b'command contains \xfc and is not a valid UTF-8 string')
self.assertTrue(False)
except SSHException:
pass
@@ -447,9 +448,11 @@ class TransportTest(unittest.TestCase):
bytes = self.tc.packetizer._Packetizer__sent_bytes
chan.send('x' * 1024)
bytes2 = self.tc.packetizer._Packetizer__sent_bytes
+ block_size = self.tc._cipher_info[self.tc.local_cipher]['block-size']
+ mac_size = self.tc._mac_info[self.tc.local_mac]['size']
# tests show this is actually compressed to *52 bytes*! including packet overhead! nice!! :)
self.assertTrue(bytes2 - bytes < 1024)
- self.assertEqual(52, bytes2 - bytes)
+ self.assertEqual(16 + block_size + mac_size, bytes2 - bytes)
chan.close()
schan.close()
@@ -792,3 +795,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')