From 02e81785104d97e0634e5a41e87f3b083ea0e952 Mon Sep 17 00:00:00 2001 From: Robey Pointer Date: Sun, 7 May 2006 17:52:24 -0700 Subject: [PATCH] [project @ robey@lag.net-20060508005224-a8a04cd59f9fa016] pulled out openssh config parsing into its own class --- README | 1 - paramiko/__init__.py | 5 ++- paramiko/config.py | 105 +++++++++++++++++++++++++++++++++++++++++++ paramiko/util.py | 86 +++-------------------------------- tests/test_util.py | 9 ++-- 5 files changed, 121 insertions(+), 85 deletions(-) create mode 100644 paramiko/config.py diff --git a/README b/README index 84384d5..63ec6f0 100644 --- a/README +++ b/README @@ -269,7 +269,6 @@ v1.0 JIGGLYPUFF * [sigh] release a fork of pycrypto with the speed improvements --- BEFORE 1.6: --- -* pull out util.parse_* (2 funcs) into a separate class * try making bzr use SSHClient * host-based auth (yuck!) diff --git a/paramiko/__init__.py b/paramiko/__init__.py index e9d504a..a376985 100644 --- a/paramiko/__init__.py +++ b/paramiko/__init__.py @@ -86,6 +86,7 @@ from file import BufferedFile from agent import Agent, AgentKey from pkey import PKey from hostkeys import HostKeys +from config import SSHConfig # fix module names for epydoc for x in (Transport, SecurityOptions, Channel, SFTPServer, SSHException, @@ -94,7 +95,8 @@ for x in (Transport, SecurityOptions, Channel, SFTPServer, SSHException, SFTP, SFTPClient, SFTPServer, Message, Packetizer, SFTPAttributes, SFTPHandle, SFTPServerInterface, BufferedFile, Agent, AgentKey, PKey, BaseSFTP, SFTPFile, ServerInterface, HostKeys, SSHClient, - MissingHostKeyPolicy, AutoAddPolicy, RejectPolicy, ChannelException): + MissingHostKeyPolicy, AutoAddPolicy, RejectPolicy, ChannelException, + SSHConfig): x.__module__ = 'paramiko' from common import AUTH_SUCCESSFUL, AUTH_PARTIALLY_SUCCESSFUL, AUTH_FAILED, \ @@ -133,4 +135,5 @@ __all__ = [ 'Transport', 'Agent', 'AgentKey', 'HostKeys', + 'SSHConfig', 'util' ] diff --git a/paramiko/config.py b/paramiko/config.py new file mode 100644 index 0000000..01a6eb5 --- /dev/null +++ b/paramiko/config.py @@ -0,0 +1,105 @@ +# Copyright (C) 2006 Robey Pointer +# +# 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. + +""" +L{SSHConfig}. +""" + +import fnmatch + + +class SSHConfig (object): + """ + Representation of config information as stored in the format used by + OpenSSH. Queries can be made via L{lookup}. The format is described in + OpenSSH's C{ssh_config} man page. This class 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. + + @since: 1.6 + """ + + def __init__(self): + """ + Create a new OpenSSH config object. + """ + self._config = [ { 'host': '*' } ] + + def parse(self, file_obj): + """ + Read an OpenSSH config from the given file object. + + @param file_obj: a file-like object to read the config file from + @type file_obj: file + """ + config = self._config[0] + 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 self._config if c['host'] == value] + if len(matches) > 0: + config = matches[0] + else: + config = { 'host': value } + self._config.append(config) + else: + config[key] = value + + def lookup(self, hostname): + """ + Return a dict of config options for a given hostname. + + 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 + """ + matches = [x for x in self._config if fnmatch.fnmatch(hostname, x['host'])] + # sort in order of shortest match (usually '*') to longest + matches.sort(lambda x,y: cmp(len(x['host']), len(y['host']))) + ret = {} + for m in matches: + ret.update(m) + del ret['host'] + return ret diff --git a/paramiko/util.py b/paramiko/util.py index f39ff29..a8fcf6e 100644 --- a/paramiko/util.py +++ b/paramiko/util.py @@ -22,13 +22,13 @@ Useful functions used by the rest of paramiko. from __future__ import generators -import fnmatch import sys import struct import traceback import threading from paramiko.common import * +from paramiko.config import SSHConfig # Change by RogerB - python < 2.3 doesn't have enumerate so we implement it @@ -201,89 +201,17 @@ def load_host_keys(filename): 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 - - @since: 1.5.2 + Provided only as a backward-compatible wrapper around L{SSHConfig}. """ - 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 + config = SSHConfig() + config.parse(file_obj) + return config 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 - - @since: 1.5.2 + Provided only as a backward-compatible wrapper around L{SSHConfig}. """ - matches = [x for x in config if fnmatch.fnmatch(hostname, x['host'])] - # sort in order of shortest match (usually '*') to longest - matches.sort(lambda x,y: cmp(len(x['host']), len(y['host']))) - ret = {} - for m in matches: - ret.update(m) - del ret['host'] - return ret + return config.lookup(hostname) def mod_inverse(x, m): # it's crazy how small python can make this function. diff --git a/tests/test_util.py b/tests/test_util.py index 83852b0..c99c1c9 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -66,10 +66,11 @@ class UtilTest (unittest.TestCase): 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'}]) + self.assertEquals(config._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