summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--paramiko/util.py83
-rwxr-xr-xtest.py2
-rw-r--r--tests/test_util.py80
3 files changed, 165 insertions, 0 deletions
diff --git a/paramiko/util.py b/paramiko/util.py
index a60d3b94..abab825d 100644
--- a/paramiko/util.py
+++ b/paramiko/util.py
@@ -22,6 +22,7 @@ Useful functions used by the rest of paramiko.
from __future__ import generators
+import fnmatch
import sys
import struct
import traceback
@@ -218,6 +219,88 @@ def load_host_keys(filename):
f.close()
return keys
+def parse_ssh_config(file_obj):
+ """
+ Parse a config file of the format used by OpenSSH, and return an object
+ that can be used to make queries to L{lookup_ssh_host_config}. The
+ format is described in OpenSSH's C{ssh_config} man page. This method is
+ provided primarily as a convenience to posix users (since the OpenSSH
+ format is a de-facto standard on posix) but should work fine on Windows
+ too.
+
+ The return value is currently a list of dictionaries, each containing
+ host-specific configuration, but this is considered an implementation
+ detail and may be subject to change in later versions.
+
+ @param file_obj: a file-like object to read the config file from
+ @type file_obj: file
+ @return: opaque configuration object
+ @rtype: object
+ """
+ ret = []
+ config = { 'host': '*' }
+ ret.append(config)
+
+ for line in file_obj:
+ line = line.rstrip('\n').lstrip()
+ if (line == '') or (line[0] == '#'):
+ continue
+ if '=' in line:
+ key, value = line.split('=', 1)
+ key = key.strip().lower()
+ else:
+ # find first whitespace, and split there
+ i = 0
+ while (i < len(line)) and not line[i].isspace():
+ i += 1
+ if i == len(line):
+ raise Exception('Unparsable line: %r' % line)
+ key = line[:i].lower()
+ value = line[i:].lstrip()
+
+ if key == 'host':
+ # do we have a pre-existing host config to append to?
+ matches = [c for c in ret if c['host'] == value]
+ if len(matches) > 0:
+ config = matches[0]
+ else:
+ config = { 'host': value }
+ ret.append(config)
+ else:
+ config[key] = value
+
+ return ret
+
+def lookup_ssh_host_config(hostname, config):
+ """
+ Return a dict of config options for a given hostname. The C{config} object
+ must come from L{parse_ssh_config}.
+
+ The host-matching rules of OpenSSH's C{ssh_config} man page are used, which
+ means that all configuration options from matching host specifications are
+ merged, with more specific hostmasks taking precedence. In other words, if
+ C{"Port"} is set under C{"Host *"} and also C{"Host *.example.com"}, and
+ the lookup is for C{"ssh.example.com"}, then the port entry for
+ C{"Host *.example.com"} will win out.
+
+ The keys in the returned dict are all normalized to lowercase (look for
+ C{"port"}, not C{"Port"}. No other processing is done to the keys or
+ values.
+
+ @param hostname: the hostname to lookup
+ @type hostname: str
+ @param config: the config object to search
+ @type config: object
+ """
+ matches = [x for x in config if fnmatch.fnmatch(hostname, x['host'])]
+ # sort in order of shortest match (usually '*') to longest
+ matches.sort(key=lambda x: len(x['host']))
+ ret = {}
+ for m in matches:
+ ret.update(m)
+ del ret['host']
+ return ret
+
def mod_inverse(x, m):
# it's crazy how small python can make this function.
u1, u2, u3 = 1, 0, m
diff --git a/test.py b/test.py
index e75e8efd..17b62949 100755
--- a/test.py
+++ b/test.py
@@ -30,6 +30,7 @@ sys.path.append('tests/')
from test_message import MessageTest
from test_file import BufferedFileTest
+from test_util import UtilTest
from test_pkey import KeyTest
from test_kex import KexTest
from test_packetizer import PacketizerTest
@@ -87,6 +88,7 @@ if options.use_sftp:
suite = unittest.TestSuite()
suite.addTest(unittest.makeSuite(MessageTest))
suite.addTest(unittest.makeSuite(BufferedFileTest))
+suite.addTest(unittest.makeSuite(UtilTest))
if options.use_pkey:
suite.addTest(unittest.makeSuite(KeyTest))
suite.addTest(unittest.makeSuite(KexTest))
diff --git a/tests/test_util.py b/tests/test_util.py
new file mode 100644
index 00000000..fa8c029b
--- /dev/null
+++ b/tests/test_util.py
@@ -0,0 +1,80 @@
+#!/usr/bin/python
+
+# Copyright (C) 2003-2005 Robey Pointer <robey@lag.net>
+#
+# This file is part of paramiko.
+#
+# Paramiko is free software; you can redistribute it and/or modify it under the
+# terms of the GNU Lesser General Public License as published by the Free
+# Software Foundation; either version 2.1 of the License, or (at your option)
+# any later version.
+#
+# Paramiko is distrubuted in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with Paramiko; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
+
+"""
+Some unit tests for utility functions.
+"""
+
+import cStringIO
+import unittest
+from Crypto.Hash import SHA
+import paramiko.util
+
+
+test_config_file = """\
+Host *
+ User robey
+ IdentityFile =~/.ssh/id_rsa
+
+# comment
+Host *.example.com
+ \tUser bjork
+Port=3333
+Host *
+ \t \t Crazy something dumb
+Host spoo.example.com
+Crazy something else
+"""
+
+
+class UtilTest (unittest.TestCase):
+
+ K = 14730343317708716439807310032871972459448364195094179797249681733965528989482751523943515690110179031004049109375612685505881911274101441415545039654102474376472240501616988799699744135291070488314748284283496055223852115360852283821334858541043710301057312858051901453919067023103730011648890038847384890504L
+
+ def setUp(self):
+ pass
+
+ def tearDown(self):
+ pass
+
+ def test_1_parse_config(self):
+ global test_config_file
+ f = cStringIO.StringIO(test_config_file)
+ config = paramiko.util.parse_ssh_config(f)
+ self.assertEquals(config, [ {'identityfile': '~/.ssh/id_rsa', 'host': '*', 'user': 'robey',
+ 'crazy': 'something dumb '},
+ {'host': '*.example.com', 'user': 'bjork', 'port': '3333'},
+ {'host': 'spoo.example.com', 'crazy': 'something else'}])
+
+ def test_2_host_config(self):
+ global test_config_file
+ f = cStringIO.StringIO(test_config_file)
+ config = paramiko.util.parse_ssh_config(f)
+ c = paramiko.util.lookup_ssh_host_config('irc.danger.com', config)
+ self.assertEquals(c, {'identityfile': '~/.ssh/id_rsa', 'user': 'robey', 'crazy': 'something dumb '})
+ c = paramiko.util.lookup_ssh_host_config('irc.example.com', config)
+ self.assertEquals(c, {'identityfile': '~/.ssh/id_rsa', 'user': 'bjork', 'crazy': 'something dumb ', 'port': '3333'})
+ c = paramiko.util.lookup_ssh_host_config('spoo.example.com', config)
+ self.assertEquals(c, {'identityfile': '~/.ssh/id_rsa', 'user': 'bjork', 'crazy': 'something else', 'port': '3333'})
+
+ def test_3_generate_key_bytes(self):
+ x = paramiko.util.generate_key_bytes(SHA, 'ABCDEFGH', 'This is my secret passphrase.', 64)
+ hex = ''.join(['%02x' % ord(c) for c in x])
+ self.assertEquals(hex, '9110e2f6793b69363e58173e9436b13a5a4b339005741d5c680e505f57d871347b4239f14fb5c46e857d5e100424873ba849ac699cea98d729e57b3e84378e8b')