summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--paramiko/_version.py2
-rw-r--r--paramiko/channel.py53
-rw-r--r--paramiko/client.py61
-rw-r--r--paramiko/message.py15
-rw-r--r--paramiko/transport.py14
-rw-r--r--sites/www/changelog.rst32
-rw-r--r--tests/test_client.py120
7 files changed, 243 insertions, 54 deletions
diff --git a/paramiko/_version.py b/paramiko/_version.py
index 93214f6d..4350dbd0 100644
--- a/paramiko/_version.py
+++ b/paramiko/_version.py
@@ -1,2 +1,2 @@
-__version_info__ = (2, 0, 8)
+__version_info__ = (2, 1, 5)
__version__ = ".".join(map(str, __version_info__))
diff --git a/paramiko/channel.py b/paramiko/channel.py
index 13ab7dcf..b2e8edd1 100644
--- a/paramiko/channel.py
+++ b/paramiko/channel.py
@@ -306,6 +306,59 @@ class Channel(ClosingContextManager):
m.add_int(height_pixels)
self.transport._send_user_message(m)
+ @open_only
+ def update_environment(self, environment):
+ """
+ Updates this channel's remote shell environment.
+
+ .. note::
+ This operation is additive - i.e. the current environment is not
+ reset before the given environment variables are set.
+
+ .. warning::
+ Servers may silently reject some environment variables; see the
+ warning in `set_environment_variable` for details.
+
+ :param dict environment:
+ a dictionary containing the name and respective values to set
+ :raises:
+ `.SSHException` -- if any of the environment variables was rejected
+ by the server or the channel was closed
+ """
+ for name, value in environment.items():
+ try:
+ self.set_environment_variable(name, value)
+ except SSHException as e:
+ err = 'Failed to set environment variable "{0}".'
+ raise SSHException(err.format(name), e)
+
+ @open_only
+ def set_environment_variable(self, name, value):
+ """
+ Set the value of an environment variable.
+
+ .. warning::
+ The server may reject this request depending on its ``AcceptEnv``
+ setting; such rejections will fail silently (which is common client
+ practice for this particular request type). Make sure you
+ understand your server's configuration before using!
+
+ :param str name: name of the environment variable
+ :param str value: value of the environment variable
+
+ :raises:
+ `.SSHException` -- if the request was rejected or the channel was
+ closed
+ """
+ m = Message()
+ m.add_byte(cMSG_CHANNEL_REQUEST)
+ m.add_int(self.remote_chanid)
+ m.add_string("env")
+ m.add_boolean(False)
+ m.add_string(name)
+ m.add_string(value)
+ self.transport._send_user_message(m)
+
def exit_status_ready(self):
"""
Return true if the remote process has exited and returned an exit
diff --git a/paramiko/client.py b/paramiko/client.py
index 15e2de09..bb6c7ff4 100644
--- a/paramiko/client.py
+++ b/paramiko/client.py
@@ -345,38 +345,40 @@ class SSHClient(ClosingContextManager):
t.set_log_channel(self._log_channel)
if banner_timeout is not None:
t.banner_timeout = banner_timeout
- t.start_client()
-
- server_key = t.get_remote_server_key()
- keytype = server_key.get_name()
if port == SSH_PORT:
server_hostkey_name = hostname
else:
server_hostkey_name = "[%s]:%d" % (hostname, port)
+ our_server_keys = None
+
+ our_server_keys = self._system_host_keys.get(server_hostkey_name)
+ if our_server_keys is None:
+ our_server_keys = self._host_keys.get(server_hostkey_name)
+ if our_server_keys is not None:
+ keytype = our_server_keys.keys()[0]
+ sec_opts = t.get_security_options()
+ other_types = [x for x in sec_opts.key_types if x != keytype]
+ sec_opts.key_types = [keytype] + other_types
+
+ t.start_client(timeout=timeout)
# If GSS-API Key Exchange is performed we are not required to check the
# host key, because the host is authenticated via GSS-API / SSPI as
# well as our client.
if not self._transport.gss_kex_used:
- our_server_key = self._system_host_keys.get(
- server_hostkey_name, {}
- ).get(keytype)
- if our_server_key is None:
- our_server_key = self._host_keys.get(
- server_hostkey_name, {}
- ).get(keytype, None)
- if our_server_key is None:
- # will raise exception if the key is rejected;
- # let that fall out
+ server_key = t.get_remote_server_key()
+ if our_server_keys is None:
+ # will raise exception if the key is rejected
self._policy.missing_host_key(
self, server_hostkey_name, server_key
)
- # if the callback returns, assume the key is ok
- our_server_key = server_key
-
- if server_key != our_server_key:
- raise BadHostKeyException(hostname, server_key, our_server_key)
+ else:
+ our_key = our_server_keys.get(server_key.get_name())
+ if our_key != server_key:
+ if our_key is None:
+ our_key = list(our_server_keys.values())[0]
+ raise BadHostKeyException(hostname, server_key, our_key)
if username is None:
username = getpass.getuser()
@@ -421,7 +423,14 @@ class SSHClient(ClosingContextManager):
self._agent.close()
self._agent = None
- def exec_command(self, command, bufsize=-1, timeout=None, get_pty=False):
+ def exec_command(
+ self,
+ command,
+ bufsize=-1,
+ timeout=None,
+ get_pty=False,
+ environment=None,
+ ):
"""
Execute a command on the SSH server. A new `.Channel` is opened and
the requested command is executed. The command's input and output
@@ -434,6 +443,14 @@ class SSHClient(ClosingContextManager):
Python
:param int timeout:
set command's channel timeout. See `.Channel.settimeout`
+ :param dict environment:
+ a dict of shell environment variables, to be merged into the
+ default environment that the remote command executes within.
+
+ .. warning::
+ Servers may silently reject some environment variables; see the
+ warning in `.Channel.set_environment_variable` for details.
+
:return:
the stdin, stdout, and stderr of the executing command, as a
3-tuple
@@ -444,6 +461,8 @@ class SSHClient(ClosingContextManager):
if get_pty:
chan.get_pty()
chan.settimeout(timeout)
+ if environment:
+ chan.update_environment(environment)
chan.exec_command(command)
stdin = chan.makefile("wb", bufsize)
stdout = chan.makefile("r", bufsize)
@@ -457,6 +476,7 @@ class SSHClient(ClosingContextManager):
height=24,
width_pixels=0,
height_pixels=0,
+ environment=None,
):
"""
Start an interactive shell session on the SSH server. A new `.Channel`
@@ -469,6 +489,7 @@ class SSHClient(ClosingContextManager):
:param int height: the height (in characters) of the terminal window
:param int width_pixels: the width (in pixels) of the terminal window
:param int height_pixels: the height (in pixels) of the terminal window
+ :param dict environment: the command's environment
:return: a new `.Channel` connected to the remote shell
:raises: `.SSHException` -- if the server fails to invoke a shell
diff --git a/paramiko/message.py b/paramiko/message.py
index 8181b9ec..869ac6c6 100644
--- a/paramiko/message.py
+++ b/paramiko/message.py
@@ -144,9 +144,6 @@ class Message(object):
def get_int(self):
"""
Fetch an int from the stream.
-
- @return: a 32-bit unsigned integer.
- @rtype: int
"""
return struct.unpack(">I", self.get_bytes(4))[0]
@@ -176,23 +173,15 @@ class Message(object):
def get_text(self):
"""
- Fetch a string from the stream. This could be a byte string and may
- contain unprintable characters. (It's not unheard of for a string to
- contain another byte-stream Message.)
-
- @return: a string.
- @rtype: string
+ Fetch a Unicode string from the stream.
"""
- return u(self.get_bytes(self.get_int()))
+ return u(self.get_string())
def get_binary(self):
"""
Fetch a string from the stream. This could be a byte string and may
contain unprintable characters. (It's not unheard of for a string to
contain another byte-stream Message.)
-
- @return: a string.
- @rtype: string
"""
return self.get_bytes(self.get_int())
diff --git a/paramiko/transport.py b/paramiko/transport.py
index 82914559..6465a784 100644
--- a/paramiko/transport.py
+++ b/paramiko/transport.py
@@ -1,4 +1,5 @@
# Copyright (C) 2003-2007 Robey Pointer <robeypointer@gmail.com>
+# Copyright (C) 2003-2007 Robey Pointer <robeypointer@gmail.com>
#
# This file is part of paramiko.
#
@@ -484,7 +485,7 @@ class Transport(threading.Thread, ClosingContextManager):
# We need the FQDN to get this working with SSPI
self.gss_host = socket.getfqdn(gss_host)
- def start_client(self, event=None):
+ def start_client(self, event=None, timeout=None):
"""
Negotiate a new SSH2 session as a client. This is the first step after
creating a new `.Transport`. A separate thread is created for protocol
@@ -495,7 +496,7 @@ class Transport(threading.Thread, ClosingContextManager):
be triggered. On failure, `is_active` will return ``False``.
(Since 1.4) If ``event`` is ``None``, this method will not return until
- negotation is done. On success, the method returns normally.
+ negotiation is done. On success, the method returns normally.
Otherwise an SSHException is raised.
After a successful negotiation, you will usually want to authenticate,
@@ -512,6 +513,9 @@ class Transport(threading.Thread, ClosingContextManager):
:param .threading.Event event:
an event to trigger when negotiation is complete (optional)
+ :param float timeout:
+ a timeout, in seconds, for SSH2 session negotiation (optional)
+
:raises:
`.SSHException` -- if negotiation fails (and no ``event`` was
passed in)
@@ -526,6 +530,7 @@ class Transport(threading.Thread, ClosingContextManager):
# synchronous, wait for a result
self.completion_event = event = threading.Event()
self.start()
+ max_time = time.time() + timeout if timeout is not None else None
while True:
event.wait(0.1)
if not self.active:
@@ -533,7 +538,9 @@ class Transport(threading.Thread, ClosingContextManager):
if e is not None:
raise e
raise SSHException("Negotiation failed.")
- if event.is_set():
+ if event.is_set() or (
+ timeout is not None and time.time() >= max_time
+ ):
break
def start_server(self, event=None, server=None):
@@ -2231,6 +2238,7 @@ class Transport(threading.Thread, ClosingContextManager):
raise SSHException(
"Incompatible ssh peer (can't match requested host key type)"
) # noqa
+ self._log_agreement("HostKey", agreed_keys[0], agreed_keys[0])
if self.server_mode:
agreed_local_ciphers = list(
diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst
index d6845e8e..5a13957d 100644
--- a/sites/www/changelog.rst
+++ b/sites/www/changelog.rst
@@ -37,6 +37,7 @@ Changelog
``black`` code formatter (both of which previously only existed in the 2.4
branch and above) to everything 2.0 and newer. This makes back/forward
porting bugfixes significantly easier.
+- :release:`2.1.5 <2018-03-12>`
- :release:`2.0.8 <2018-03-12>`
- :release:`1.18.5 <2018-03-12>`
- :release:`1.17.6 <2018-03-12>`
@@ -49,6 +50,7 @@ Changelog
``async``) so that we're compatible with the upcoming Python 3.7 release
(where ``async`` is a new keyword.) Thanks to ``@vEpiphyte`` for the report.
- :support:`- backported` Include LICENSE file in wheel archives.
+- :release:`2.1.4 <2017-09-18>`
- :release:`2.0.7 <2017-09-18>`
- :release:`1.18.4 <2017-09-18>`
- :bug:`1061` Clean up GSSAPI authentication procedures so they do not prevent
@@ -59,18 +61,23 @@ Changelog
- :bug:`1060` Fix key exchange (kex) algorithm list for GSSAPI authentication;
previously, the list used solely out-of-date algorithms, and now contains
newer ones listed preferentially before the old. Credit: Anselm Kruis.
+- :bug:`1055 (1.17+)` (also :issue:`1056`, :issue:`1057`, :issue:`1058`,
+ :issue:`1059`) Fix up host-key checking in our GSSAPI support, which was
+ previously using an incorrect API call. Thanks to Anselm Kruis for the
+ patches.
- :bug:`945 (1.18+)` (backport of :issue:`910` and re: :issue:`865`) SSHClient
now requests the type of host key it has (e.g. from known_hosts) and does not
consider a different type to be a "Missing" host key. This fixes a common
case where an ECDSA key is in known_hosts and the server also has an RSA host
key. Thanks to Pierce Lopez.
-- :bug:`1055 (1.17+)` (also :issue:`1056`, :issue:`1057`, :issue:`1058`,
- :issue:`1059`) Fix up host-key checking in our GSSAPI support, which was
- previously using an incorrect API call. Thanks to Anselm Kruis for the
- patches.
+- :release:`2.1.3 <2017-06-09>`
- :release:`2.0.6 <2017-06-09>`
- :release:`1.18.3 <2017-06-09>`
- :release:`1.17.5 <2017-06-09>`
+- :bug:`865` SSHClient now requests the type of host key it has (e.g. from
+ known_hosts) and does not consider a different type to be a "Missing" host
+ key. This fixes a common case where an ECDSA key is in known_hosts and the
+ server also has an RSA host key. Thanks to Pierce Lopez.
- :support:`906 (1.18+)` Clean up a handful of outdated imports and related
tweaks. Thanks to Pierce Lopez.
- :bug:`984` Enhance default cipher preference order such that
@@ -124,6 +131,7 @@ Changelog
Pierce Lopez for assistance.
- :bug:`683 (1.17+)` Make ``util.log_to_file`` append instead of replace.
Thanks to ``@vlcinsky`` for the report.
+- :release:`2.1.2 <2017-02-20>`
- :release:`2.0.5 <2017-02-20>`
- :release:`1.18.2 <2017-02-20>`
- :release:`1.17.4 <2017-02-20>`
@@ -152,6 +160,7 @@ Changelog
test-related file we don't support, and add PyPy to Travis-CI config. Thanks
to Pierce Lopez for the final patch and Pedro Rodrigues for an earlier
edition.
+- :release:`2.1.1 <2016-12-12>`
- :release:`2.0.4 <2016-12-12>`
- :release:`1.18.1 <2016-12-12>`
- :bug:`859 (1.18+)` (via :issue:`860`) A tweak to the original patch
@@ -163,6 +172,11 @@ Changelog
features (breaking `~paramiko.client.SSHClient.invoke_shell` with an
``AttributeError``.) The offending code has been stripped out of the 2.0.x
line (but of course, remains in 2.1.x and above.)
+- :bug:`859` (via :issue:`860`) A tweak to the original patch implementing
+ :issue:`398` was not fully applied, causing calls to
+ `~paramiko.client.SSHClient.invoke_shell` to fail with ``AttributeError``.
+ This has been fixed. Patch credit: Kirk Byers.
+- :release:`2.1.0 <2016-12-09>`
- :release:`2.0.3 <2016-12-09>`
- :release:`1.18.0 <2016-12-09>`
- :release:`1.17.3 <2016-12-09>`
@@ -206,6 +220,16 @@ Changelog
signature. Caught by ``@Score_Under``.
- :bug:`681 (1.17+)` Fix a Python3-specific bug re: the handling of read
buffers when using ``ProxyCommand``. Thanks to Paul Kapp for catch & patch.
+- :feature:`398 (1.18+)` Add an ``environment`` dict argument to
+ `Client.exec_command <paramiko.client.SSHClient.exec_command>` (plus the
+ lower level `Channel.update_environment
+ <paramiko.channel.Channel.update_environment>` and
+ `Channel.set_environment_variable
+ <paramiko.channel.Channel.set_environment_variable>` methods) which
+ implements the ``env`` SSH message type. This means the remote shell
+ environment can be set without the use of ``VARNAME=value`` shell tricks,
+ provided the server's ``AcceptEnv`` lists the variables you need to set.
+ Thanks to Philip Lorenz for the pull request.
- :support:`819 backported (>=1.15,<2.0)` Document how lacking ``gmp`` headers
at install time can cause a significant performance hit if you build PyCrypto
from source. (Most system-distributed packages already have this enabled.)
diff --git a/tests/test_client.py b/tests/test_client.py
index 03b163fc..fed38791 100644
--- a/tests/test_client.py
+++ b/tests/test_client.py
@@ -83,6 +83,16 @@ class NullServer(paramiko.ServerInterface):
return False
return True
+ def check_channel_env_request(self, channel, name, value):
+ if name == "INVALID_ENV":
+ return False
+
+ if not hasattr(channel, "env"):
+ setattr(channel, "env", {})
+
+ channel.env[name] = value
+ return True
+
class SSHClientTest(unittest.TestCase):
def setUp(self):
@@ -108,9 +118,11 @@ class SSHClientTest(unittest.TestCase):
allowed_keys = FINGERPRINTS.keys()
self.socks, addr = self.sockl.accept()
self.ts = paramiko.Transport(self.socks)
- host_key = paramiko.RSAKey.from_private_key_file(
- _support("test_rsa.key")
- )
+ keypath = _support("test_rsa.key")
+ host_key = paramiko.RSAKey.from_private_key_file(keypath)
+ self.ts.add_server_key(host_key)
+ keypath = _support("test_ecdsa_256.key")
+ host_key = paramiko.ECDSAKey.from_private_key_file(keypath)
self.ts.add_server_key(host_key)
server = NullServer(allowed_keys=allowed_keys)
if delay:
@@ -240,10 +252,9 @@ class SSHClientTest(unittest.TestCase):
verify that SSHClient's AutoAddPolicy works.
"""
threading.Thread(target=self._run).start()
- host_key = paramiko.RSAKey.from_private_key_file(
- _support("test_rsa.key")
- )
- public_host_key = paramiko.RSAKey(data=host_key.asbytes())
+ hostname = "[%s]:%d" % (self.addr, self.port)
+ key_file = _support("test_ecdsa_256.key")
+ public_host_key = paramiko.ECDSAKey.from_private_key_file(key_file)
self.tc = paramiko.SSHClient()
self.tc.set_missing_host_key_policy(paramiko.AutoAddPolicy())
@@ -256,12 +267,8 @@ class SSHClientTest(unittest.TestCase):
self.assertEqual("slowdive", self.ts.get_username())
self.assertEqual(True, self.ts.is_authenticated())
self.assertEqual(1, len(self.tc.get_host_keys()))
- self.assertEqual(
- public_host_key,
- self.tc.get_host_keys()["[%s]:%d" % (self.addr, self.port)][
- "ssh-rsa"
- ],
- )
+ new_host_key = list(self.tc.get_host_keys()[hostname].values())[0]
+ self.assertEqual(public_host_key, new_host_key)
def test_5_save_host_keys(self):
"""
@@ -440,3 +447,90 @@ class SSHClientTest(unittest.TestCase):
gss_kex=True,
**self.connect_kwargs
)
+
+ def _client_host_key_bad(self, host_key):
+ threading.Thread(target=self._run).start()
+ hostname = "[%s]:%d" % (self.addr, self.port)
+
+ self.tc = paramiko.SSHClient()
+ self.tc.set_missing_host_key_policy(paramiko.WarningPolicy())
+ known_hosts = self.tc.get_host_keys()
+ known_hosts.add(hostname, host_key.get_name(), host_key)
+
+ self.assertRaises(
+ paramiko.BadHostKeyException,
+ self.tc.connect,
+ password="pygmalion",
+ **self.connect_kwargs
+ )
+
+ def _client_host_key_good(self, ktype, kfile):
+ threading.Thread(target=self._run).start()
+ hostname = "[%s]:%d" % (self.addr, self.port)
+
+ self.tc = paramiko.SSHClient()
+ self.tc.set_missing_host_key_policy(paramiko.RejectPolicy())
+ host_key = ktype.from_private_key_file(_support(kfile))
+ known_hosts = self.tc.get_host_keys()
+ known_hosts.add(hostname, host_key.get_name(), host_key)
+
+ self.tc.connect(password="pygmalion", **self.connect_kwargs)
+ self.event.wait(1.0)
+ self.assertTrue(self.event.is_set())
+ self.assertTrue(self.ts.is_active())
+ self.assertEqual(True, self.ts.is_authenticated())
+
+ def test_host_key_negotiation_1(self):
+ host_key = paramiko.ECDSAKey.generate()
+ self._client_host_key_bad(host_key)
+
+ def test_host_key_negotiation_2(self):
+ host_key = paramiko.RSAKey.generate(2048)
+ self._client_host_key_bad(host_key)
+
+ def test_host_key_negotiation_3(self):
+ self._client_host_key_good(paramiko.ECDSAKey, "test_ecdsa_256.key")
+
+ def test_host_key_negotiation_4(self):
+ self._client_host_key_good(paramiko.RSAKey, "test_rsa.key")
+
+ def test_update_environment(self):
+ """
+ Verify that environment variables can be set by the client.
+ """
+ threading.Thread(target=self._run).start()
+
+ self.tc = paramiko.SSHClient()
+ self.tc.set_missing_host_key_policy(paramiko.AutoAddPolicy())
+ self.assertEqual(0, len(self.tc.get_host_keys()))
+ self.tc.connect(
+ self.addr, self.port, username="slowdive", password="pygmalion"
+ )
+
+ self.event.wait(1.0)
+ self.assertTrue(self.event.isSet())
+ self.assertTrue(self.ts.is_active())
+
+ target_env = {b"A": b"B", b"C": b"d"}
+
+ self.tc.exec_command("yes", environment=target_env)
+ schan = self.ts.accept(1.0)
+ self.assertEqual(target_env, getattr(schan, "env", {}))
+ schan.close()
+
+ # Cannot use assertRaises in context manager mode as it is not supported
+ # in Python 2.6.
+ try:
+ # Verify that a rejection by the server can be detected
+ self.tc.exec_command("yes", environment={b"INVALID_ENV": b""})
+ except SSHException as e:
+ self.assertTrue(
+ "INVALID_ENV" in str(e),
+ "Expected variable name in error message",
+ )
+ self.assertTrue(
+ isinstance(e.args[1], SSHException),
+ "Expected original SSHException in exception",
+ )
+ else:
+ self.assertFalse(False, "SSHException was not thrown.")