From 1a716ed46d1d556d4ba6798608ab498320acd886 Mon Sep 17 00:00:00 2001 From: "Jeremy T. Bouse" Date: Sat, 25 May 2013 00:04:32 -0400 Subject: Imported Upstream version 1.10.1 --- paramiko/config.py | 198 ++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 172 insertions(+), 26 deletions(-) (limited to 'paramiko/config.py') diff --git a/paramiko/config.py b/paramiko/config.py index 2a2cbff..e41bae4 100644 --- a/paramiko/config.py +++ b/paramiko/config.py @@ -1,4 +1,5 @@ # Copyright (C) 2006-2007 Robey Pointer +# Copyright (C) 2012 Olle Lundberg # # This file is part of paramiko. # @@ -21,6 +22,57 @@ L{SSHConfig}. """ import fnmatch +import os +import re +import socket + +SSH_PORT = 22 +proxy_re = re.compile(r"^(proxycommand)\s*=*\s*(.*)", re.I) + + +class LazyFqdn(object): + """ + Returns the host's fqdn on request as string. + """ + + def __init__(self, config): + self.fqdn = None + self.config = config + + def __str__(self): + if self.fqdn is None: + # + # If the SSH config contains AddressFamily, use that when + # determining the local host's FQDN. Using socket.getfqdn() from + # the standard library is the most general solution, but can + # result in noticeable delays on some platforms when IPv6 is + # misconfigured or not available, as it calls getaddrinfo with no + # address family specified, so both IPv4 and IPv6 are checked. + # + + # Handle specific option + fqdn = None + address_family = self.config.get('addressfamily', 'any').lower() + if address_family != 'any': + family = socket.AF_INET if address_family == 'inet' \ + else socket.AF_INET6 + results = socket.getaddrinfo(host, + None, + family, + socket.SOCK_DGRAM, + socket.IPPROTO_IP, + socket.AI_CANONNAME) + for res in results: + af, socktype, proto, canonname, sa = res + if canonname and '.' in canonname: + fqdn = canonname + break + # Handle 'any' / unspecified + if fqdn is None: + fqdn = socket.getfqdn() + # Cache + self.fqdn = fqdn + return self.fqdn class SSHConfig (object): @@ -38,7 +90,7 @@ class SSHConfig (object): """ Create a new OpenSSH config object. """ - self._config = [ { 'host': '*' } ] + self._config = [] def parse(self, file_obj): """ @@ -47,14 +99,19 @@ class SSHConfig (object): @param file_obj: a file-like object to read the config file from @type file_obj: file """ - configs = [self._config[0]] + host = {"host": ['*'], "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() + # Ensure ProxyCommand gets properly split + if line.lower().strip().startswith('proxycommand'): + match = proxy_re.match(line) + key, value = match.group(1).lower(), match.group(2) + else: + key, value = line.split('=', 1) + key = key.strip().lower() else: # find first whitespace, and split there i = 0 @@ -66,20 +123,20 @@ class SSHConfig (object): value = line[i:].lstrip() if key == 'host': - del configs[:] - # the value may be multiple hosts, space-delimited - for host in value.split(): - # do we have a pre-existing host config to append to? - matches = [c for c in self._config if c['host'] == host] - if len(matches) > 0: - configs.append(matches[0]) - else: - config = { 'host': host } - self._config.append(config) - configs.append(config) - else: - for config in configs: - config[key] = value + self._config.append(host) + value = value.split() + host = {key: value, 'config': {}} + #identityfile is a special case, since it is allowed to be + # specified multiple times and they should be tried in order + # of specification. + elif key == 'identityfile': + if key in host['config']: + host['config']['identityfile'].append(value) + else: + host['config']['identityfile'] = [value] + elif key not in host['config']: + host['config'].update({key: value}) + self._config.append(host) def lookup(self, hostname): """ @@ -94,17 +151,106 @@ class SSHConfig (object): 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. + C{"port"}, not C{"Port"}. The values are processed according to the + rules for substitution variable expansion in C{ssh_config}. @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']))) + + matches = [config for config in self._config if + self._allowed(hostname, config['host'])] + ret = {} - for m in matches: - ret.update(m) - del ret['host'] + for match in matches: + for key, value in match['config'].iteritems(): + if key not in ret: + # Create a copy of the original value, + # else it will reference the original list + # in self._config and update that value too + # when the extend() is being called. + ret[key] = value[:] + elif key == 'identityfile': + ret[key].extend(value) + ret = self._expand_variables(ret, hostname) return ret + + def _allowed(self, hostname, hosts): + match = False + for host in hosts: + if host.startswith('!') and fnmatch.fnmatch(hostname, host[1:]): + return False + elif fnmatch.fnmatch(hostname, host): + match = True + return match + + def _expand_variables(self, config, hostname): + """ + Return a dict of config options with expanded substitutions + for a given hostname. + + Please refer to man C{ssh_config} for the parameters that + are replaced. + + @param config: the config for the hostname + @type hostname: dict + @param hostname: the hostname that the config belongs to + @type hostname: str + """ + + if 'hostname' in config: + config['hostname'] = config['hostname'].replace('%h', hostname) + else: + config['hostname'] = hostname + + if 'port' in config: + port = config['port'] + else: + port = SSH_PORT + + user = os.getenv('USER') + if 'user' in config: + remoteuser = config['user'] + else: + remoteuser = user + + host = socket.gethostname().split('.')[0] + fqdn = LazyFqdn(config) + homedir = os.path.expanduser('~') + replacements = {'controlpath': + [ + ('%h', config['hostname']), + ('%l', fqdn), + ('%L', host), + ('%n', hostname), + ('%p', port), + ('%r', remoteuser), + ('%u', user) + ], + 'identityfile': + [ + ('~', homedir), + ('%d', homedir), + ('%h', config['hostname']), + ('%l', fqdn), + ('%u', user), + ('%r', remoteuser) + ], + 'proxycommand': + [ + ('%h', config['hostname']), + ('%p', port), + ('%r', remoteuser) + ] + } + + for k in config: + if k in replacements: + for find, replace in replacements[k]: + if isinstance(config[k], list): + for item in range(len(config[k])): + config[k][item] = config[k][item].\ + replace(find, str(replace)) + else: + config[k] = config[k].replace(find, str(replace)) + return config -- cgit v1.2.3