pulled out openssh config parsing into its own class
This commit is contained in:
Robey Pointer 2006-05-07 17:52:24 -07:00
parent 887354c088
commit 02e8178510
5 changed files with 121 additions and 85 deletions

1
README
View File

@ -269,7 +269,6 @@ v1.0 JIGGLYPUFF
* [sigh] release a fork of pycrypto with the speed improvements * [sigh] release a fork of pycrypto with the speed improvements
--- BEFORE 1.6: --- --- BEFORE 1.6: ---
* pull out util.parse_* (2 funcs) into a separate class
* try making bzr use SSHClient * try making bzr use SSHClient
* host-based auth (yuck!) * host-based auth (yuck!)

View File

@ -86,6 +86,7 @@ from file import BufferedFile
from agent import Agent, AgentKey from agent import Agent, AgentKey
from pkey import PKey from pkey import PKey
from hostkeys import HostKeys from hostkeys import HostKeys
from config import SSHConfig
# fix module names for epydoc # fix module names for epydoc
for x in (Transport, SecurityOptions, Channel, SFTPServer, SSHException, 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, SFTP, SFTPClient, SFTPServer, Message, Packetizer, SFTPAttributes,
SFTPHandle, SFTPServerInterface, BufferedFile, Agent, AgentKey, SFTPHandle, SFTPServerInterface, BufferedFile, Agent, AgentKey,
PKey, BaseSFTP, SFTPFile, ServerInterface, HostKeys, SSHClient, PKey, BaseSFTP, SFTPFile, ServerInterface, HostKeys, SSHClient,
MissingHostKeyPolicy, AutoAddPolicy, RejectPolicy, ChannelException): MissingHostKeyPolicy, AutoAddPolicy, RejectPolicy, ChannelException,
SSHConfig):
x.__module__ = 'paramiko' x.__module__ = 'paramiko'
from common import AUTH_SUCCESSFUL, AUTH_PARTIALLY_SUCCESSFUL, AUTH_FAILED, \ from common import AUTH_SUCCESSFUL, AUTH_PARTIALLY_SUCCESSFUL, AUTH_FAILED, \
@ -133,4 +135,5 @@ __all__ = [ 'Transport',
'Agent', 'Agent',
'AgentKey', 'AgentKey',
'HostKeys', 'HostKeys',
'SSHConfig',
'util' ] 'util' ]

105
paramiko/config.py Normal file
View File

@ -0,0 +1,105 @@
# Copyright (C) 2006 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.
"""
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

View File

@ -22,13 +22,13 @@ Useful functions used by the rest of paramiko.
from __future__ import generators from __future__ import generators
import fnmatch
import sys import sys
import struct import struct
import traceback import traceback
import threading import threading
from paramiko.common import * from paramiko.common import *
from paramiko.config import SSHConfig
# Change by RogerB - python < 2.3 doesn't have enumerate so we implement it # 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): def parse_ssh_config(file_obj):
""" """
Parse a config file of the format used by OpenSSH, and return an object Provided only as a backward-compatible wrapper around L{SSHConfig}.
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
""" """
ret = [] config = SSHConfig()
config = { 'host': '*' } config.parse(file_obj)
ret.append(config) return 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): def lookup_ssh_host_config(hostname, config):
""" """
Return a dict of config options for a given hostname. The C{config} object Provided only as a backward-compatible wrapper around L{SSHConfig}.
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
""" """
matches = [x for x in config if fnmatch.fnmatch(hostname, x['host'])] return config.lookup(hostname)
# 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
def mod_inverse(x, m): def mod_inverse(x, m):
# it's crazy how small python can make this function. # it's crazy how small python can make this function.

View File

@ -66,10 +66,11 @@ class UtilTest (unittest.TestCase):
global test_config_file global test_config_file
f = cStringIO.StringIO(test_config_file) f = cStringIO.StringIO(test_config_file)
config = paramiko.util.parse_ssh_config(f) config = paramiko.util.parse_ssh_config(f)
self.assertEquals(config, [ {'identityfile': '~/.ssh/id_rsa', 'host': '*', 'user': 'robey', self.assertEquals(config._config,
'crazy': 'something dumb '}, [ {'identityfile': '~/.ssh/id_rsa', 'host': '*', 'user': 'robey',
{'host': '*.example.com', 'user': 'bjork', 'port': '3333'}, 'crazy': 'something dumb '},
{'host': 'spoo.example.com', 'crazy': 'something else'}]) {'host': '*.example.com', 'user': 'bjork', 'port': '3333'},
{'host': 'spoo.example.com', 'crazy': 'something else'}])
def test_2_host_config(self): def test_2_host_config(self):
global test_config_file global test_config_file