summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--paramiko/channel.py8
-rw-r--r--paramiko/common.py11
-rw-r--r--paramiko/sftp_client.py19
-rw-r--r--paramiko/transport.py82
-rw-r--r--paramiko/util.py3
-rw-r--r--tests/test_transport.py27
-rw-r--r--tests/test_util.py5
7 files changed, 129 insertions, 26 deletions
diff --git a/paramiko/channel.py b/paramiko/channel.py
index 27202a7e..78a14795 100644
--- a/paramiko/channel.py
+++ b/paramiko/channel.py
@@ -40,10 +40,6 @@ from paramiko.buffered_pipe import BufferedPipe, PipeTimeout
from paramiko import pipe
-# lower bound on the max packet size we'll accept from the remote host
-MIN_PACKET_SIZE = 1024
-
-
def open_only(func):
"""
Decorator for `.Channel` methods which performs an openness check.
@@ -355,6 +351,7 @@ class Channel (object):
`.Transport.accept`. The handler's calling signature is::
handler(channel: Channel, (address: str, port: int))
+
:param int screen_number: the x11 screen number (0, 10, etc.)
:param str auth_protocol:
the name of the X11 authentication method used; if none is given,
@@ -887,7 +884,8 @@ class Channel (object):
def _set_remote_channel(self, chanid, window_size, max_packet_size):
self.remote_chanid = chanid
self.out_window_size = window_size
- self.out_max_packet_size = max(max_packet_size, MIN_PACKET_SIZE)
+ self.out_max_packet_size = self.transport. \
+ _sanitize_packet_size(max_packet_size)
self.active = 1
self._log(DEBUG, 'Max packet out: %d bytes' % max_packet_size)
diff --git a/paramiko/common.py b/paramiko/common.py
index 18298922..93f71df0 100644
--- a/paramiko/common.py
+++ b/paramiko/common.py
@@ -171,3 +171,14 @@ CRITICAL = logging.CRITICAL
# Common IO/select/etc sleep period, in seconds
io_sleep = 0.01
+
+DEFAULT_WINDOW_SIZE = 64 * 2 ** 15
+DEFAULT_MAX_PACKET_SIZE = 2 ** 15
+
+# lower bound on the max packet size we'll accept from the remote host
+# Minimum packet size is 32768 bytes according to
+# http://www.ietf.org/rfc/rfc4254.txt
+MIN_PACKET_SIZE = 2 ** 15
+
+# Max windows size according to http://www.ietf.org/rfc/rfc4254.txt
+MAX_WINDOW_SIZE = 2**32 -1
diff --git a/paramiko/sftp_client.py b/paramiko/sftp_client.py
index 3e85a8c9..84b70961 100644
--- a/paramiko/sftp_client.py
+++ b/paramiko/sftp_client.py
@@ -61,7 +61,7 @@ b_slash = b'/'
class SFTPClient(BaseSFTP):
"""
SFTP client object.
-
+
Used to open an SFTP session across an open SSH `.Transport` and perform
remote file operations.
"""
@@ -98,16 +98,27 @@ class SFTPClient(BaseSFTP):
raise SSHException('EOF during negotiation')
self._log(INFO, 'Opened sftp connection (server version %d)' % server_version)
- def from_transport(cls, t):
+ def from_transport(cls, t, window_size=None, max_packet_size=None):
"""
Create an SFTP client channel from an open `.Transport`.
+ Setting the window and packet sizes might affect the transfer speed.
+ The default settings in the `.Transport` class are the same as in
+ OpenSSH and should work adequately for both files transfers and
+ interactive sessions.
+
:param .Transport t: an open `.Transport` which is already authenticated
+ :param int window_size:
+ optional window size for the `.SFTPClient` session.
+ :param int max_packet_size:
+ optional max packet size for the `.SFTPClient` session..
+
:return:
a new `.SFTPClient` object, referring to an sftp session (channel)
across the transport
"""
- chan = t.open_session()
+ chan = t.open_session(window_size=window_size,
+ max_packet_size=max_packet_size)
if chan is None:
return None
chan.invoke_subsystem('sftp')
@@ -643,7 +654,7 @@ class SFTPClient(BaseSFTP):
.. versionadded:: 1.4
.. versionchanged:: 1.7.4
- ``callback`` and rich attribute return value added.
+ ``callback`` and rich attribute return value added.
.. versionchanged:: 1.7.7
``confirm`` param added.
"""
diff --git a/paramiko/transport.py b/paramiko/transport.py
index fff11a1d..c43db6e2 100644
--- a/paramiko/transport.py
+++ b/paramiko/transport.py
@@ -42,7 +42,8 @@ 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
@@ -57,7 +58,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, clamp_value
from Crypto.Cipher import Blowfish, AES, DES3, ARC4
try:
@@ -135,7 +136,10 @@ 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):
"""
Create a new SSH session over an existing socket, or socket-like
object. This only creates the `.Transport` object; it doesn't begin the
@@ -161,8 +165,19 @@ 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)
"""
self.active = False
@@ -232,8 +247,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
@@ -528,18 +543,29 @@ 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
"""
- 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):
"""
@@ -581,13 +607,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"``)
@@ -597,6 +632,11 @@ 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
@@ -606,13 +646,15 @@ class Transport (threading.Thread):
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])
@@ -626,7 +668,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)
@@ -670,6 +712,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
@@ -1391,6 +1434,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
@@ -1954,7 +2008,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()
@@ -1962,8 +2016,8 @@ 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(DEBUG, 'Secsh channel %d (%s) opened.', my_chanid, kind)
if kind == 'auth-agent@openssh.com':
diff --git a/paramiko/util.py b/paramiko/util.py
index f4ee3adc..d029f52e 100644
--- a/paramiko/util.py
+++ b/paramiko/util.py
@@ -320,3 +320,6 @@ def constant_time_bytes_eq(a, b):
for i in (xrange if PY2 else range)(len(a)):
res |= byte_ord(a[i]) ^ byte_ord(b[i])
return res == 0
+
+def clamp_value(minimum, val, maximum):
+ return max(minimum, min(val, maximum))
diff --git a/tests/test_transport.py b/tests/test_transport.py
index 5c77a321..344d64b8 100644
--- a/tests/test_transport.py
+++ b/tests/test_transport.py
@@ -32,7 +32,9 @@ from paramiko import Transport, SecurityOptions, ServerInterface, RSAKey, DSSKey
SSHException, ChannelException
from paramiko import AUTH_FAILED, AUTH_SUCCESSFUL
from paramiko import OPEN_SUCCEEDED, OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED
-from paramiko.common import MSG_KEXINIT, cMSG_CHANNEL_WINDOW_ADJUST
+from paramiko.common import MSG_KEXINIT, cMSG_CHANNEL_WINDOW_ADJUST, \
+ MIN_PACKET_SIZE, MAX_WINDOW_SIZE, \
+ DEFAULT_WINDOW_SIZE, DEFAULT_MAX_PACKET_SIZE
from paramiko.py3compat import bytes
from paramiko.message import Message
from tests.loop import LoopSocket
@@ -586,12 +588,13 @@ class TransportTest(unittest.TestCase):
self.assertEqual(chan.send_ready(), True)
total = 0
K = '*' * 1024
- while total < 1024 * 1024:
+ limit = 1+(64 * 2 ** 15)
+ while total < limit:
chan.send(K)
total += len(K)
if not chan.send_ready():
break
- self.assertTrue(total < 1024 * 1024)
+ self.assertTrue(total < limit)
schan.close()
chan.close()
@@ -753,3 +756,21 @@ class TransportTest(unittest.TestCase):
# Close the channels
schan.close()
chan.close()
+
+ def test_J_sanitze_packet_size(self):
+ """
+ verify that we conform to the rfc of packet and window sizes.
+ """
+ for val, correct in [(32767, MIN_PACKET_SIZE),
+ (None, DEFAULT_MAX_PACKET_SIZE),
+ (2**32, MAX_WINDOW_SIZE)]:
+ self.assertEqual(self.tc._sanitize_packet_size(val), correct)
+
+ def test_K_sanitze_window_size(self):
+ """
+ verify that we conform to the rfc of packet and window sizes.
+ """
+ for val, correct in [(32767, MIN_PACKET_SIZE),
+ (None, DEFAULT_WINDOW_SIZE),
+ (2**32, MAX_WINDOW_SIZE)]:
+ self.assertEqual(self.tc._sanitize_window_size(val), correct)
diff --git a/tests/test_util.py b/tests/test_util.py
index 2827ded6..35e15765 100644
--- a/tests/test_util.py
+++ b/tests/test_util.py
@@ -338,6 +338,11 @@ IdentityFile something_%l_using_fqdn
config = paramiko.util.parse_ssh_config(StringIO(test_config))
assert config.lookup('meh') # will die during lookup() if bug regresses
+ def test_clamp_value(self):
+ self.assertEqual(32768, paramiko.util.clamp_value(32767, 32768, 32769))
+ self.assertEqual(32767, paramiko.util.clamp_value(32767, 32765, 32769))
+ self.assertEqual(32769, paramiko.util.clamp_value(32767, 32770, 32769))
+
def test_13_config_dos_crlf_succeeds(self):
config_file = StringIO("host abcqwerty\r\nHostName 127.0.0.1\r\n")
config = paramiko.SSHConfig()