diff options
-rw-r--r-- | dev-requirements.txt | 2 | ||||
-rw-r--r-- | paramiko/_version.py | 2 | ||||
-rw-r--r-- | paramiko/agent.py | 14 | ||||
-rw-r--r-- | paramiko/client.py | 5 | ||||
-rw-r--r-- | paramiko/hostkeys.py | 6 | ||||
-rw-r--r-- | paramiko/server.py | 3 | ||||
-rw-r--r-- | paramiko/transport.py | 4 | ||||
-rw-r--r-- | sites/www/changelog.rst | 9 | ||||
-rw-r--r-- | sites/www/conf.py | 14 | ||||
-rw-r--r-- | tests/test_client.py | 24 | ||||
-rw-r--r-- | tests/test_hostkeys.py | 18 |
11 files changed, 91 insertions, 10 deletions
diff --git a/dev-requirements.txt b/dev-requirements.txt index e402f20f..26d0efa4 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -15,7 +15,7 @@ codespell>=2.2,<2.3 coverage>=6.2,<7 # Documentation tools alabaster==0.7.13 -releases>=2.0 +releases>=2.1 # Debuggery icecream>=2.1 # Self (sans GSS which is a pain to bother with most of the time) diff --git a/paramiko/_version.py b/paramiko/_version.py index 06833102..ba3f1b3c 100644 --- a/paramiko/_version.py +++ b/paramiko/_version.py @@ -1,2 +1,2 @@ -__version_info__ = (3, 0, 0) +__version_info__ = (3, 1, 0) __version__ = ".".join(map(str, __version_info__)) diff --git a/paramiko/agent.py b/paramiko/agent.py index 73fa1f82..30ec1590 100644 --- a/paramiko/agent.py +++ b/paramiko/agent.py @@ -65,6 +65,9 @@ class AgentSSH: no SSH agent was running (or it couldn't be contacted), an empty list will be returned. + This method performs no IO, just returns the list of keys retrieved + when the connection was made. + :return: a tuple of `.AgentKey` objects representing keys available on the SSH agent @@ -277,6 +280,17 @@ class AgentClientProxy: class AgentServerProxy(AgentSSH): """ + Allows an SSH server to access a forwarded agent. + + This also creates a unix domain socket on the system to allow external + programs to also access the agent. For this reason, you probably only want + to create one of these. + + :meth:`connect` must be called before it is usable. This will also load the + list of keys the agent contains. You must also call :meth:`close` in + order to clean up the unix socket and the thread that maintains it. + (:class:`contextlib.closing` might be helpful to you.) + :param .Transport t: Transport used for SSH Agent communication forwarding :raises: `.SSHException` -- mostly if we lost the agent diff --git a/paramiko/client.py b/paramiko/client.py index e9bcfb56..1fe14b07 100644 --- a/paramiko/client.py +++ b/paramiko/client.py @@ -233,6 +233,7 @@ class SSHClient(ClosingContextManager): gss_host=None, banner_timeout=None, auth_timeout=None, + channel_timeout=None, gss_trust_dns=True, passphrase=None, disabled_algorithms=None, @@ -311,6 +312,8 @@ class SSHClient(ClosingContextManager): for the SSH banner to be presented. :param float auth_timeout: an optional timeout (in seconds) to wait for an authentication response. + :param float channel_timeout: an optional timeout (in seconds) to wait + for a channel open response. :param dict disabled_algorithms: an optional dict passed directly to `.Transport` and its keyword argument of the same name. @@ -406,6 +409,8 @@ class SSHClient(ClosingContextManager): t.banner_timeout = banner_timeout if auth_timeout is not None: t.auth_timeout = auth_timeout + if channel_timeout is not None: + t.channel_timeout = channel_timeout if port == SSH_PORT: server_hostkey_name = hostname diff --git a/paramiko/hostkeys.py b/paramiko/hostkeys.py index b189aac6..bbfa5755 100644 --- a/paramiko/hostkeys.py +++ b/paramiko/hostkeys.py @@ -20,6 +20,7 @@ from base64 import encodebytes, decodebytes import binascii import os +import re from collections.abc import MutableMapping from hashlib import sha1 @@ -328,7 +329,8 @@ class HostKeyEntry: """ Parses the given line of text to find the names for the host, the type of key, and the key data. The line is expected to be in the - format used by the OpenSSH known_hosts file. + format used by the OpenSSH known_hosts file. Fields are separated by a + single space or tab. Lines are expected to not have leading or trailing whitespace. We don't bother to check for comments or empty lines. All of @@ -337,7 +339,7 @@ class HostKeyEntry: :param str line: a line from an OpenSSH known_hosts file """ log = get_logger("paramiko.hostkeys") - fields = line.split(" ") + fields = re.split(" |\t", line) if len(fields) < 3: # Bad number of fields msg = "Not enough fields found in known_hosts in line {} ({!r})" diff --git a/paramiko/server.py b/paramiko/server.py index 3875b8a2..6b0bb0f6 100644 --- a/paramiko/server.py +++ b/paramiko/server.py @@ -517,6 +517,9 @@ class ServerInterface: :param .Channel channel: the `.Channel` the request arrived on :return: ``True`` if the AgentForward was loaded; ``False`` if not + + If ``True`` is returned, the server should create an + :class:`AgentServerProxy` to access the agent. """ return False diff --git a/paramiko/transport.py b/paramiko/transport.py index 569c5cdd..98cdae03 100644 --- a/paramiko/transport.py +++ b/paramiko/transport.py @@ -516,6 +516,8 @@ class Transport(threading.Thread, ClosingContextManager): self.handshake_timeout = 15 # how long (seconds) to wait for the auth response. self.auth_timeout = 30 + # how long (seconds) to wait for opening a channel + self.channel_timeout = 60 * 60 self.disabled_algorithms = disabled_algorithms or {} self.server_sig_algs = server_sig_algs @@ -1015,7 +1017,7 @@ class Transport(threading.Thread, ClosingContextManager): """ if not self.active: raise SSHException("SSH session not active") - timeout = 3600 if timeout is None else timeout + timeout = self.channel_timeout if timeout is None else timeout self.lock.acquire() try: window_size = self._sanitize_window_size(window_size) diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst index 9d8ab3a1..0b2022ca 100644 --- a/sites/www/changelog.rst +++ b/sites/www/changelog.rst @@ -2,6 +2,15 @@ Changelog ========= +- :release:`3.1.0 <2023-03-10>` +- :feature:`2013` (solving :issue:`2009`, plus others) Add an explicit + ``channel_timeout`` keyword argument to `paramiko.client.SSHClient.connect`, + allowing users to configure the previously-hardcoded default value of 3600 + seconds. Thanks to ``@VakarisZ`` and ``@ilija-lazoroski`` for the report and + patch, with credit to Mike Salvatore for patch review. +- :feature:`2173` Accept single tabs as field separators (in addition to + single spaces) in `<paramiko.hostkeys.HostKeyEntry.from_line>` for parity + with OpenSSH's KnownHosts parser. Patched by Alex Chavkin. - :support:`2178 backported` Apply ``codespell`` to the codebase, which found a lot of very old minor spelling mistakes in docstrings. Also modernize many instances of ``*largs`` vs ``*args`` and ``**kwarg`` vs ``**kwargs``. Patch diff --git a/sites/www/conf.py b/sites/www/conf.py index 00944871..179f0b7f 100644 --- a/sites/www/conf.py +++ b/sites/www/conf.py @@ -1,22 +1,26 @@ # Obtain shared config values -import sys +from pathlib import Path import os -from os.path import abspath, join, dirname +import sys -sys.path.append(abspath(join(dirname(__file__), ".."))) +updir = Path(__file__).parent.parent.resolve() +sys.path.append(str(updir)) from shared_conf import * # Releases changelog extension extensions.append("releases") releases_release_uri = "https://github.com/paramiko/paramiko/tree/%s" releases_issue_uri = "https://github.com/paramiko/paramiko/issues/%s" +releases_development_branch = "main" +# Don't show unreleased_X.x sections up top for 1.x or 2.x anymore +releases_supported_versions = [3] # Default is 'local' building, but reference the public docs site when building # under RTD. -target = join(dirname(__file__), "..", "docs", "_build") +target = updir / "docs" / "_build" if os.environ.get("READTHEDOCS") == "True": target = "http://docs.paramiko.org/en/latest/" -intersphinx_mapping["docs"] = (target, None) +intersphinx_mapping["docs"] = (str(target), None) # Sister-site links to API docs html_theme_options["extra_nav_links"] = { diff --git a/tests/test_client.py b/tests/test_client.py index dae5b13a..21ecd72a 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -564,6 +564,30 @@ class SSHClientTest(ClientTest): auth_timeout=0.5, ) + @patch.object( + paramiko.Channel, + "_set_remote_channel", + lambda *args, **kwargs: time.sleep(100), + ) + def test_channel_timeout(self): + """ + verify that the SSHClient has a configurable channel timeout + """ + threading.Thread(target=self._run).start() + # Client setup + self.tc = SSHClient() + self.tc.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + + # Actual connection + self.tc.connect( + **dict( + self.connect_kwargs, password="pygmalion", channel_timeout=0.5 + ) + ) + self.event.wait(1.0) + + self.assertRaises(paramiko.SSHException, self.tc.open_sftp) + @requires_gss_auth def test_auth_trickledown_gsskex(self): """ diff --git a/tests/test_hostkeys.py b/tests/test_hostkeys.py index ebcc40f5..bdda295a 100644 --- a/tests/test_hostkeys.py +++ b/tests/test_hostkeys.py @@ -38,6 +38,18 @@ BGQ3GQ/Fc7SX6gkpXkwcZryoi4kNFhHu5LvHcZPdxXV1D+uTMfGS1eyd2Yz/DoNWXNAl8TI0cAsW\ 5ymME3bQ4J/k1IKxCtz/bAlAqFgKoc+EolMziDYqWIATtW0rYTJvzGAzTmMj80/QpsFH+Pc2M= """ +test_hosts_file_tabs = """\ +secure.example.com\tssh-rsa\tAAAAB3NzaC1yc2EAAAABIwAAAIEA1PD6U2/TVxET6lkpKhOk5r\ +9q/kAYG6sP9f5zuUYP8i7FOFp/6ncCEbbtg/lB+A3iidyxoSWl+9jtoyyDOOVX4UIDV9G11Ml8om3\ +D+jrpI9cycZHqilK0HmxDeCuxbwyMuaCygU9gS2qoRvNLWZk70OpIKSSpBo0Wl3/XUmz9uhc= +happy.example.com\tssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEA8bP1ZA7DCZDB9J0s50l31M\ +BGQ3GQ/Fc7SX6gkpXkwcZryoi4kNFhHu5LvHcZPdxXV1D+uTMfGS1eyd2Yz/DoNWXNAl8TI0cAsW\ +5ymME3bQ4J/k1IKxCtz/bAlAqFgKoc+EolMziDYqWIATtW0rYTJvzGAzTmMj80/QpsFH+Pc2M= +doublespace.example.com ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEA1PD6U2/TVxET6lkp\ +KhOk5r9q/kAYG6sP9f5zuUYP8i7FOFp/6ncCEbbtg/lB+A3iidyxoSWl+9jtoyyDOOVX4UIDV9G11M\ +l8om3D+jrpI9cycZHqilK0HmxDeCuxbwyMuaCygU9gS2qoRvNLWZk70OpIKSSpBo0Wl3/XUmz8BtZ= +""" + keyblob = b"""\ AAAAB3NzaC1yc2EAAAABIwAAAIEA8bP1ZA7DCZDB9J0s50l31MBGQ3GQ/Fc7SX6gkpXkwcZryoi4k\ NFhHu5LvHcZPdxXV1D+uTMfGS1eyd2Yz/DoNWXNAl8TI0cAsW5ymME3bQ4J/k1IKxCtz/bAlAqFgK\ @@ -147,3 +159,9 @@ class HostKeysTest(unittest.TestCase): pass # Good else: assert False, "Key was not deleted from Entry on delitem!" + + +class HostKeysTabsTest(HostKeysTest): + def setUp(self): + with open("hostfile.temp", "w") as f: + f.write(test_hosts_file_tabs) |