summaryrefslogtreecommitdiff
path: root/paramiko/config.py
diff options
context:
space:
mode:
authorJeremy T. Bouse <jbouse@debian.org>2013-05-25 00:04:32 -0400
committerJeremy T. Bouse <jbouse@debian.org>2013-05-25 00:04:32 -0400
commit1a716ed46d1d556d4ba6798608ab498320acd886 (patch)
treedbcb23de26387e312f7ea09085330eca90e15853 /paramiko/config.py
parenta88b8c8c0f591a3bfa8d7984343a27815184f495 (diff)
downloadpython-paramiko-upstream/1.10.1.tar
python-paramiko-upstream/1.10.1.tar.gz
Imported Upstream version 1.10.1upstream/1.10.1
Diffstat (limited to 'paramiko/config.py')
-rw-r--r--paramiko/config.py198
1 files changed, 172 insertions, 26 deletions
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 <robeypointer@gmail.com>
+# Copyright (C) 2012 Olle Lundberg <geek@nerd.sh>
#
# 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