summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--dev-requirements.txt2
-rw-r--r--paramiko/_version.py2
-rw-r--r--paramiko/agent.py14
-rw-r--r--paramiko/client.py5
-rw-r--r--paramiko/hostkeys.py6
-rw-r--r--paramiko/server.py3
-rw-r--r--paramiko/transport.py4
-rw-r--r--sites/www/changelog.rst9
-rw-r--r--sites/www/conf.py14
-rw-r--r--tests/test_client.py24
-rw-r--r--tests/test_hostkeys.py18
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)