summaryrefslogtreecommitdiffhomepage
path: root/paramiko/transport.py
diff options
context:
space:
mode:
Diffstat (limited to 'paramiko/transport.py')
-rw-r--r--paramiko/transport.py247
1 files changed, 209 insertions, 38 deletions
diff --git a/paramiko/transport.py b/paramiko/transport.py
index 35c20341..9009f9f5 100644
--- a/paramiko/transport.py
+++ b/paramiko/transport.py
@@ -31,6 +31,7 @@ from hashlib import md5, sha1, sha256
import paramiko
from paramiko import util
from paramiko.auth_handler import AuthHandler
+from paramiko.ssh_gss import GSSAuth
from paramiko.channel import Channel
from paramiko.common import xffffffff, cMSG_CHANNEL_OPEN, cMSG_IGNORE, \
cMSG_GLOBAL_REQUEST, DEBUG, MSG_KEXINIT, MSG_IGNORE, MSG_DISCONNECT, \
@@ -42,11 +43,14 @@ from paramiko.common import xffffffff, cMSG_CHANNEL_OPEN, cMSG_IGNORE, \
MSG_CHANNEL_OPEN_SUCCESS, MSG_CHANNEL_OPEN_FAILURE, MSG_CHANNEL_OPEN, \
MSG_CHANNEL_SUCCESS, MSG_CHANNEL_FAILURE, MSG_CHANNEL_DATA, \
MSG_CHANNEL_EXTENDED_DATA, MSG_CHANNEL_WINDOW_ADJUST, MSG_CHANNEL_REQUEST, \
- MSG_CHANNEL_EOF, MSG_CHANNEL_CLOSE
+ MSG_CHANNEL_EOF, MSG_CHANNEL_CLOSE, MIN_PACKET_SIZE, 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, KexGexSHA256
from paramiko.kex_group1 import KexGroup1
+from paramiko.kex_group14 import KexGroup14
+from paramiko.kex_gss import KexGSSGex, KexGSSGroup1, KexGSSGroup14, NullHostKey
from paramiko.message import Message
from paramiko.packet import Packetizer, NeedRekeyException
from paramiko.primes import ModulusPack
@@ -57,7 +61,7 @@ from paramiko.server import ServerInterface
from paramiko.sftp_client import SFTPClient
from paramiko.ssh_exception import (SSHException, BadAuthenticationType,
ChannelException, ProxyCommandFailure)
-from paramiko.util import retry_on_signal
+from paramiko.util import retry_on_signal, ClosingContextManager, clamp_value
from Crypto.Cipher import Blowfish, AES, DES3, ARC4
try:
@@ -77,13 +81,15 @@ import atexit
atexit.register(_join_lingering_threads)
-class Transport (threading.Thread):
+class Transport (threading.Thread, ClosingContextManager):
"""
An SSH Transport attaches to a stream (usually a socket), negotiates an
encrypted session, authenticates, and then creates stream tunnels, called
`channels <.Channel>`, across the session. Multiple channels can be
multiplexed across a single session (and often are, in the case of port
forwardings).
+
+ Instances of this class may be used as context managers.
"""
_PROTO_ID = '2.0'
_CLIENT_ID = 'paramiko_%s' % paramiko.__version__
@@ -93,9 +99,10 @@ class Transport (threading.Thread):
_preferred_macs = ('hmac-sha1', 'hmac-md5', 'hmac-sha1-96', 'hmac-md5-96',
'hmac-sha2-256')
_preferred_keys = ('ssh-rsa', 'ssh-dss', 'ecdsa-sha2-nistp256')
- _preferred_kex = ('diffie-hellman-group1-sha1',
- 'diffie-hellman-group-exchange-sha1',
- 'diffie-hellman-group-exchange-sha256')
+ _preferred_kex = ('diffie-hellman-group-exchange-sha256',
+ 'diffie-hellman-group14-sha1',
+ 'diffie-hellman-group-exchange-sha1' ,
+ 'diffie-hellman-group1-sha1')
_preferred_compression = ('none',)
_cipher_info = {
@@ -125,8 +132,12 @@ class Transport (threading.Thread):
_kex_info = {
'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
}
_compression_info = {
@@ -140,7 +151,12 @@ class Transport (threading.Thread):
_modulus_pack = None
- def __init__(self, sock):
+ def __init__(self,
+ sock,
+ default_window_size=DEFAULT_WINDOW_SIZE,
+ default_max_packet_size=DEFAULT_MAX_PACKET_SIZE,
+ gss_kex=False,
+ gss_deleg_creds=True):
"""
Create a new SSH session over an existing socket, or socket-like
object. This only creates the `.Transport` object; it doesn't begin the
@@ -166,9 +182,27 @@ class Transport (threading.Thread):
address and used for communication. Exceptions from the ``socket``
call may be thrown in this case.
+ .. note::
+ Modifying the the window and packet sizes might have adverse
+ effects on your channels created from this transport. The default
+ values are the same as in the OpenSSH code base and have been
+ battle tested.
+
:param socket sock:
a socket or socket-like object to create the session over.
+ :param int default_window_size:
+ sets the default window size on the transport. (defaults to
+ 2097152)
+ :param int default_max_packet_size:
+ sets the default max packet size on the transport. (defaults to
+ 32768)
+
+ .. versionchanged:: 1.15
+ Added the ``default_window_size`` and ``default_max_packet_size``
+ arguments.
"""
+ self.active = False
+
if isinstance(sock, string_types):
# convert "host:port" into (host, port)
hl = sock.split(':', 1)
@@ -219,12 +253,26 @@ class Transport (threading.Thread):
self.host_key_type = None
self.host_key = None
+ # GSS-API / SSPI Key Exchange
+ self.use_gss_kex = gss_kex
+ # This will be set to True if GSS-API Key Exchange was performed
+ self.gss_kex_used = False
+ self.kexgss_ctxt = None
+ 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')
+
# state used during negotiation
self.kex_engine = None
self.H = None
self.K = None
- self.active = False
self.initial_kex_done = False
self.in_kex = False
self.authenticated = False
@@ -236,8 +284,8 @@ class Transport (threading.Thread):
self.channel_events = {} # (id -> Event)
self.channels_seen = {} # (id -> True)
self._channel_counter = 1
- self.window_size = 65536
- self.max_packet_size = 34816
+ self.default_max_packet_size = default_max_packet_size
+ self.default_window_size = default_window_size
self._forward_agent_handler = None
self._x11_handler = None
self._tcp_handler = None
@@ -292,6 +340,7 @@ class Transport (threading.Thread):
.. versionadded:: 1.5.3
"""
+ self.sock.close()
self.close()
def get_security_options(self):
@@ -303,6 +352,17 @@ class Transport (threading.Thread):
"""
return SecurityOptions(self)
+ def set_gss_host(self, gss_host):
+ """
+ Setter for C{gss_host} if GSS-API Key Exchange is performed.
+
+ :param str gss_host: The targets name in the kerberos database
+ Default: The name of the host to connect to
+ :rtype: Void
+ """
+ # We need the FQDN to get this working with SSPI
+ self.gss_host = socket.getfqdn(gss_host)
+
def start_client(self, event=None):
"""
Negotiate a new SSH2 session as a client. This is the first step after
@@ -323,9 +383,10 @@ class Transport (threading.Thread):
.. note:: `connect` is a simpler method for connecting as a client.
- .. note:: After calling this method (or `start_server` or `connect`),
- you should no longer directly read from or write to the original
- socket object.
+ .. note::
+ After calling this method (or `start_server` or `connect`), you
+ should no longer directly read from or write to the original socket
+ object.
:param .threading.Event event:
an event to trigger when negotiation is complete (optional)
@@ -532,18 +593,32 @@ class Transport (threading.Thread):
"""
return self.active
- def open_session(self):
+ def open_session(self, window_size=None, max_packet_size=None):
"""
Request a new channel to the server, of type ``"session"``. This is
just an alias for calling `open_channel` with an argument of
``"session"``.
+ .. note:: Modifying the the window and packet sizes might have adverse
+ effects on the session created. The default values are the same
+ as in the OpenSSH code base and have been battle tested.
+
+ :param int window_size:
+ optional window size for this session.
+ :param int max_packet_size:
+ optional max packet size for this session.
+
:return: a new `.Channel`
:raises SSHException: if the request is rejected or the session ends
prematurely
+
+ .. versionchanged:: 1.15
+ Added the ``window_size`` and ``max_packet_size`` arguments.
"""
- return self.open_channel('session')
+ return self.open_channel('session',
+ window_size=window_size,
+ max_packet_size=max_packet_size)
def open_x11_channel(self, src_addr=None):
"""
@@ -585,13 +660,22 @@ class Transport (threading.Thread):
"""
return self.open_channel('forwarded-tcpip', dest_addr, src_addr)
- def open_channel(self, kind, dest_addr=None, src_addr=None):
+ def open_channel(self,
+ kind,
+ dest_addr=None,
+ src_addr=None,
+ window_size=None,
+ max_packet_size=None):
"""
Request a new channel to the server. `Channels <.Channel>` are
socket-like objects used for the actual transfer of data across the
session. You may only request a channel after negotiating encryption
(using `connect` or `start_client`) and authenticating.
+ .. note:: Modifying the the window and packet sizes might have adverse
+ effects on the channel created. The default values are the same
+ as in the OpenSSH code base and have been battle tested.
+
:param str kind:
the kind of channel requested (usually ``"session"``,
``"forwarded-tcpip"``, ``"direct-tcpip"``, or ``"x11"``)
@@ -601,22 +685,32 @@ class Transport (threading.Thread):
``"direct-tcpip"`` (ignored for other channel types)
:param src_addr: the source address of this port forwarding, if
``kind`` is ``"forwarded-tcpip"``, ``"direct-tcpip"``, or ``"x11"``
+ :param int window_size:
+ optional window size for this session.
+ :param int max_packet_size:
+ optional max packet size for this session.
+
:return: a new `.Channel` on success
:raises SSHException: if the request is rejected or the session ends
prematurely
+
+ .. versionchanged:: 1.15
+ Added the ``window_size`` and ``max_packet_size`` arguments.
"""
if not self.active:
raise SSHException('SSH session not active')
self.lock.acquire()
try:
+ window_size = self._sanitize_window_size(window_size)
+ max_packet_size = self._sanitize_packet_size(max_packet_size)
chanid = self._next_channel()
m = Message()
m.add_byte(cMSG_CHANNEL_OPEN)
m.add_string(kind)
m.add_int(chanid)
- m.add_int(self.window_size)
- m.add_int(self.max_packet_size)
+ m.add_int(window_size)
+ m.add_int(max_packet_size)
if (kind == 'forwarded-tcpip') or (kind == 'direct-tcpip'):
m.add_string(dest_addr[0])
m.add_int(dest_addr[1])
@@ -630,7 +724,7 @@ class Transport (threading.Thread):
self.channel_events[chanid] = event = threading.Event()
self.channels_seen[chanid] = True
chan._set_transport(self)
- chan._set_window(self.window_size, self.max_packet_size)
+ chan._set_window(window_size, max_packet_size)
finally:
self.lock.release()
self._send_user_message(m)
@@ -674,6 +768,7 @@ class Transport (threading.Thread):
:param callable handler:
optional handler for incoming forwarded connections, of the form
``func(Channel, (str, int), (str, int))``.
+
:return: the port number (`int`) allocated by the server
:raises SSHException: if the server refused the TCP forward request
@@ -840,7 +935,8 @@ class Transport (threading.Thread):
self.lock.release()
return chan
- def connect(self, hostkey=None, username='', password=None, pkey=None):
+ def connect(self, hostkey=None, username='', password=None, pkey=None,
+ gss_host=None, gss_auth=False, gss_kex=False, gss_deleg_creds=True):
"""
Negotiate an SSH2 session, and optionally verify the server's host key
and authenticate using a password or private key. This is a shortcut
@@ -870,6 +966,14 @@ class Transport (threading.Thread):
:param .PKey pkey:
a private key to use for authentication, if you want to use private
key authentication; otherwise ``None``.
+ :param str gss_host:
+ The target's name in the kerberos database. Default: hostname
+ :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:
+ Whether to delegate GSS-API client credentials.
:raises SSHException: if the SSH2 negotiation fails, the host key
supplied by the server is incorrect, or authentication fails.
@@ -880,7 +984,9 @@ class Transport (threading.Thread):
self.start_client()
# check host key if we were given one
- if hostkey is not None:
+ # If GSS-API Key Exchange was performed, we are not required to check
+ # the host key.
+ if (hostkey is not None) and not gss_kex:
key = self.get_remote_server_key()
if (key.get_name() != hostkey.get_name()) or (key.asbytes() != hostkey.asbytes()):
self._log(DEBUG, 'Bad host key from server')
@@ -889,13 +995,19 @@ class Transport (threading.Thread):
raise SSHException('Bad host key from server')
self._log(DEBUG, 'Host key verified (%s)' % hostkey.get_name())
- if (pkey is not None) or (password is not None):
- if password is not None:
- self._log(DEBUG, 'Attempting password auth...')
- self.auth_password(username, password)
- else:
+ if (pkey is not None) or (password is not None) or gss_auth or gss_kex:
+ if gss_auth:
+ self._log(DEBUG, 'Attempting GSS-API auth... (gssapi-with-mic)')
+ self.auth_gssapi_with_mic(username, gss_host, gss_deleg_creds)
+ elif gss_kex:
+ self._log(DEBUG, 'Attempting GSS-API auth... (gssapi-keyex)')
+ self.auth_gssapi_keyex(username)
+ elif pkey is not None:
self._log(DEBUG, 'Attempting public-key auth...')
self.auth_publickey(username, pkey)
+ else:
+ self._log(DEBUG, 'Attempting password auth...')
+ self.auth_password(username, password)
return
@@ -965,10 +1077,9 @@ class Transport (threading.Thread):
def get_banner(self):
"""
Return the banner supplied by the server upon connect. If no banner is
- supplied, this method returns C{None}.
+ supplied, this method returns ``None``.
- @return: server supplied banner, or C{None}.
- @rtype: string
+ :returns: server supplied banner (`str`), or ``None``.
"""
if not self.active or (self.auth_handler is None):
return None
@@ -1177,6 +1288,55 @@ class Transport (threading.Thread):
self.auth_handler.auth_interactive(username, handler, my_event, submethods)
return self.auth_handler.wait_for_response(my_event)
+ def auth_gssapi_with_mic(self, username, gss_host, gss_deleg_creds):
+ """
+ Authenticate to the Server using GSS-API / SSPI.
+
+ :param str username: The username to authenticate as
+ :param str gss_host: The target host
+ :param bool gss_deleg_creds: Delegate credentials or not
+ :return: list of auth types permissible for the next stage of
+ authentication (normally empty)
+ :rtype: list
+ :raise BadAuthenticationType: if gssapi-with-mic isn't
+ allowed by the server (and no event was passed in)
+ :raise AuthenticationException: if the authentication failed (and no
+ event was passed in)
+ :raise SSHException: if there was a network error
+ """
+ if (not self.active) or (not self.initial_kex_done):
+ # we should never try to authenticate unless we're on a secure link
+ raise SSHException('No existing session')
+ my_event = threading.Event()
+ self.auth_handler = AuthHandler(self)
+ self.auth_handler.auth_gssapi_with_mic(username, gss_host, gss_deleg_creds, my_event)
+ return self.auth_handler.wait_for_response(my_event)
+
+ def auth_gssapi_keyex(self, username):
+ """
+ Authenticate to the Server with GSS-API / SSPI if GSS-API Key Exchange
+ was the used key exchange method.
+
+ :param str username: The username to authenticate as
+ :param str gss_host: The target host
+ :param bool gss_deleg_creds: Delegate credentials or not
+ :return: list of auth types permissible for the next stage of
+ authentication (normally empty)
+ :rtype: list
+ :raise BadAuthenticationType: if GSS-API Key Exchange was not performed
+ (and no event was passed in)
+ :raise AuthenticationException: if the authentication failed (and no
+ event was passed in)
+ :raise SSHException: if there was a network error
+ """
+ if (not self.active) or (not self.initial_kex_done):
+ # we should never try to authenticate unless we're on a secure link
+ raise SSHException('No existing session')
+ my_event = threading.Event()
+ self.auth_handler = AuthHandler(self)
+ self.auth_handler.auth_gssapi_keyex(username, my_event)
+ return self.auth_handler.wait_for_response(my_event)
+
def set_log_channel(self, name):
"""
Set the channel for this transport's logging. The default is
@@ -1261,7 +1421,7 @@ class Transport (threading.Thread):
def stop_thread(self):
self.active = False
self.packetizer.close()
- while self.isAlive():
+ while self.is_alive() and (self is not threading.current_thread()):
self.join(10)
### internals...
@@ -1396,6 +1556,17 @@ class Transport (threading.Thread):
finally:
self.lock.release()
+ def _sanitize_window_size(self, window_size):
+ if window_size is None:
+ window_size = self.default_window_size
+ return clamp_value(MIN_PACKET_SIZE, window_size, MAX_WINDOW_SIZE)
+
+ def _sanitize_packet_size(self, max_packet_size):
+ if max_packet_size is None:
+ max_packet_size = self.default_max_packet_size
+ return clamp_value(MIN_PACKET_SIZE, max_packet_size, MAX_WINDOW_SIZE)
+
+
def run(self):
# (use the exposed "run" method, because if we specify a thread target
# of a private method, threading.Thread will keep a reference to it
@@ -1440,7 +1611,7 @@ class Transport (threading.Thread):
if ptype not in self._expected_packet:
raise SSHException('Expecting packet from %r, got %d' % (self._expected_packet, ptype))
self._expected_packet = tuple()
- if (ptype >= 30) and (ptype <= 39):
+ if (ptype >= 30) and (ptype <= 41):
self.kex_engine.parse_next(ptype, m)
continue
@@ -1862,7 +2033,7 @@ class Transport (threading.Thread):
self.lock.acquire()
try:
chan._set_remote_channel(server_chanid, server_window_size, server_max_packet_size)
- self._log(INFO, 'Secsh channel %d opened.' % chanid)
+ self._log(DEBUG, 'Secsh channel %d opened.' % chanid)
if chanid in self.channel_events:
self.channel_events[chanid].set()
del self.channel_events[chanid]
@@ -1876,7 +2047,7 @@ class Transport (threading.Thread):
reason_str = m.get_text()
lang = m.get_text()
reason_text = CONNECTION_FAILED_CODE.get(reason, '(unknown code)')
- self._log(INFO, 'Secsh channel %d open FAILED: %s: %s' % (chanid, reason_str, reason_text))
+ self._log(ERROR, 'Secsh channel %d open FAILED: %s: %s' % (chanid, reason_str, reason_text))
self.lock.acquire()
try:
self.saved_exception = ChannelException(reason, reason_text)
@@ -1961,7 +2132,7 @@ class Transport (threading.Thread):
self._channels.put(my_chanid, chan)
self.channels_seen[my_chanid] = True
chan._set_transport(self)
- chan._set_window(self.window_size, self.max_packet_size)
+ chan._set_window(self.default_window_size, self.default_max_packet_size)
chan._set_remote_channel(chanid, initial_window_size, max_packet_size)
finally:
self.lock.release()
@@ -1969,10 +2140,10 @@ class Transport (threading.Thread):
m.add_byte(cMSG_CHANNEL_OPEN_SUCCESS)
m.add_int(chanid)
m.add_int(my_chanid)
- m.add_int(self.window_size)
- m.add_int(self.max_packet_size)
+ m.add_int(self.default_window_size)
+ m.add_int(self.default_max_packet_size)
self._send_message(m)
- self._log(INFO, 'Secsh channel %d (%s) opened.', my_chanid, kind)
+ self._log(DEBUG, 'Secsh channel %d (%s) opened.', my_chanid, kind)
if kind == 'auth-agent@openssh.com':
self._forward_agent_handler(chan)
elif kind == 'x11':
@@ -1987,7 +2158,7 @@ class Transport (threading.Thread):
always_display = m.get_boolean()
msg = m.get_string()
lang = m.get_string()
- self._log(DEBUG, 'Debug msg: ' + util.safe_string(msg))
+ self._log(DEBUG, 'Debug msg: {0}'.format(util.safe_string(msg)))
def _get_subsystem_handler(self, name):
try: