Skip to content

Commit 2a2fcf6

Browse files
committed
Merging feature branches into master
2 parents 78461d8 + 2d1dd6c commit 2a2fcf6

File tree

4 files changed

+75
-1
lines changed

4 files changed

+75
-1
lines changed

fake_server/fake_agent.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
"""
2+
Fake SSH Agent for testing ssh agent forwarding and agent based key
3+
authentication
4+
"""
5+
6+
import paramiko.agent
7+
8+
class FakeAgent(paramiko.agent.AgentSSH):
9+
10+
def __init__(self):
11+
self._conn = None
12+
self.keys = []
13+
14+
def add_key(self, key):
15+
"""Add key to agent.
16+
:param key: Key to add
17+
:type key: :mod:`paramiko.pkey.PKey`
18+
"""
19+
self.keys.append(key)
20+
21+
def _connect(self, conn):
22+
pass
23+
24+
def _close(self):
25+
self._keys = []
26+
27+
def get_keys(self):
28+
return tuple(self.keys)

fake_server/fake_server.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,10 @@ def check_channel_pty_request(self, channel, term, width, height, pixelwidth,
5353
pixelheight, modes):
5454
return True
5555

56+
def check_channel_forward_agent_request(self, channel):
57+
logger.debug("Forward agent key request for channel %s" % (channel,))
58+
return True
59+
5660
def check_channel_exec_request(self, channel, cmd):
5761
logger.debug("Got exec request on channel %s for cmd %s" % (channel, cmd,))
5862
# Remove any 'bash -c' and/or quotes from command

pssh.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,8 @@ class SSHClient(object):
7272

7373
def __init__(self, host,
7474
user=None, password=None, port=None,
75-
pkey=None):
75+
pkey=None, forward_ssh_agent=True,
76+
_agent=None):
7677
"""Connect to host honouring any user set configuration in ~/.ssh/config \
7778
or /etc/ssh/ssh_config
7879
@@ -89,6 +90,15 @@ def __init__(self, host,
8990
:type port: int
9091
:param pkey: (Optional) Client's private key to be used to connect with
9192
:type pkey: :mod:`paramiko.PKey`
93+
:param forward_ssh_agent: (Optional) Turn on SSH agent forwarding - \
94+
equivalent to `ssh -A` from the `ssh` command line utility.
95+
Defaults to True if not set.
96+
:type forward_ssh_agent: bool
97+
:param _agent: (Optional) Override SSH agent object with the provided. \
98+
This allows for overriding of the default paramiko behaviour of \
99+
connecting to local SSH agent to lookup keys with our own SSH agent.
100+
Only really useful for testing, hence the internal variable prefix.
101+
:type _agent: :mod:`paramiko.agent.Agent`
92102
:raises: :mod:`pssh.AuthenticationException` on authentication error
93103
:raises: :mod:`pssh.UnknownHostException` on DNS resolution error
94104
:raises: :mod:`pssh.ConnectionErrorException` on error connecting
@@ -113,6 +123,7 @@ def __init__(self, host,
113123
user = _user
114124
client = paramiko.SSHClient()
115125
client.set_missing_host_key_policy(paramiko.MissingHostKeyPolicy())
126+
self.forward_ssh_agent = forward_ssh_agent
116127
self.client = client
117128
self.channel = None
118129
self.user = user
@@ -125,6 +136,8 @@ def __init__(self, host,
125136
if self.proxy_command:
126137
logger.debug("Proxy configured for destination host %s - ProxyCommand: '%s'" % (
127138
self.host, " ".join(self.proxy_command.cmd),))
139+
if _agent:
140+
self.client._agent = _agent
128141
self._connect()
129142

130143
def _connect(self, retries=1):
@@ -177,6 +190,8 @@ def exec_command(self, command, sudo=False, **kwargs):
177190
stderr are buffers containing command output.
178191
"""
179192
channel = self.client.get_transport().open_session()
193+
if self.forward_ssh_agent:
194+
agent_handler = paramiko.agent.AgentRequestHandler(channel)
180195
channel.get_pty()
181196
# stdin (unused), stdout, stderr
182197
(_, stdout, stderr) = (channel.makefile('wb'), channel.makefile('rb'),

tests/test_ssh_client.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,18 @@
2222
from pssh import SSHClient, ParallelSSHClient, UnknownHostException, AuthenticationException, _setup_logger, logger
2323
from fake_server.fake_server import start_server, make_socket, logger as server_logger, \
2424
paramiko_logger
25+
from fake_server.fake_agent import FakeAgent
26+
import paramiko
2527
import os
2628
from test_pssh_client import USER_KEY
2729

2830
# _setup_logger(server_logger)
2931
# _setup_logger(logger)
3032
# _setup_logger(paramiko_logger)
3133

34+
USER_KEY = paramiko.RSAKey.from_private_key_file(
35+
os.path.sep.join([os.path.dirname(__file__), 'test_client_private_key']))
36+
3237
class SSHClientTest(unittest.TestCase):
3338

3439
def setUp(self):
@@ -70,5 +75,27 @@ def test_ssh_client_sftp(self):
7075
del client
7176
server.join()
7277

78+
def test_ssh_agent_authentication(self):
79+
"""Test authentication via SSH agent.
80+
Do not provide public key to use when creating SSHClient,
81+
instead override the client's agent with our own fake SSH agent,
82+
add our to key to agent and try to login to server.
83+
Key should be automatically picked up from the overriden agent"""
84+
agent = FakeAgent()
85+
agent.add_key(USER_KEY)
86+
server = start_server({ self.fake_cmd : self.fake_resp },
87+
self.listen_socket)
88+
client = SSHClient('127.0.0.1', port=self.listen_port,
89+
_agent=agent)
90+
channel, host, _stdout, _stderr = client.exec_command(self.fake_cmd)
91+
output = (line.strip() for line in _stdout)
92+
channel.close()
93+
output = list(output)
94+
expected = [self.fake_resp]
95+
self.assertEqual(expected, output,
96+
msg = "Got unexpected command output - %s" % (output,))
97+
del client
98+
server.join()
99+
73100
if __name__ == '__main__':
74101
unittest.main()

0 commit comments

Comments
 (0)