1
# Copyright (C) 2006-2007 Robey Pointer <robeypointer@gmail.com>
2
# Copyright (C) 2012 Olle Lundberg <geek@nerd.sh>
4
# This file is part of paramiko.
6
# Paramiko is free software; you can redistribute it and/or modify it under the
7
# terms of the GNU Lesser General Public License as published by the Free
8
# Software Foundation; either version 2.1 of the License, or (at your option)
11
# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY
12
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
13
# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
16
# You should have received a copy of the GNU Lesser General Public License
17
# along with Paramiko; if not, write to the Free Software Foundation, Inc.,
18
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
21
Configuration file (aka ``ssh_config``) support.
33
class SSHConfig (object):
35
Representation of config information as stored in the format used by
36
OpenSSH. Queries can be made via `lookup`. The format is described in
37
OpenSSH's ``ssh_config`` man page. This class is provided primarily as a
38
convenience to posix users (since the OpenSSH format is a de-facto
39
standard on posix) but should work fine on Windows too.
44
SETTINGS_REGEX = re.compile(r'(\w+)(?:\s*=\s*|\s+)(.+)')
48
Create a new OpenSSH config object.
52
def parse(self, file_obj):
54
Read an OpenSSH config from the given file object.
56
:param file_obj: a file-like object to read the config file from
58
host = {"host": ['*'], "config": {}}
60
# Strip any leading or trailing whitespace from the line.
61
# Refer to https://github.com/paramiko/paramiko/issues/499
63
if not line or line.startswith('#'):
66
match = re.match(self.SETTINGS_REGEX, line)
68
raise Exception("Unparsable line {}".format(line))
69
key = match.group(1).lower()
70
value = match.group(2)
73
self._config.append(host)
75
'host': self._get_hosts(value),
78
elif key == 'proxycommand' and value.lower() == 'none':
79
# Store 'none' as None; prior to 3.x, it will get stripped out
80
# at the end (for compatibility with issue #415). After 3.x, it
81
# will simply not get stripped, leaving a nice explicit marker.
82
host['config'][key] = None
84
if value.startswith('"') and value.endswith('"'):
87
# identityfile, localforward, remoteforward keys are special
88
# cases, since they are allowed to be specified multiple times
89
# and they should be tried in order of specification.
90
if key in ['identityfile', 'localforward', 'remoteforward']:
91
if key in host['config']:
92
host['config'][key].append(value)
94
host['config'][key] = [value]
95
elif key not in host['config']:
96
host['config'][key] = value
97
self._config.append(host)
99
def lookup(self, hostname):
101
Return a dict of config options for a given hostname.
103
The host-matching rules of OpenSSH's ``ssh_config`` man page are used:
104
For each parameter, the first obtained value will be used. The
105
configuration files contain sections separated by ``Host``
106
specifications, and that section is only applied for hosts that match
107
one of the patterns given in the specification.
109
Since the first obtained value for each parameter is used, more host-
110
specific declarations should be given near the beginning of the file,
111
and general defaults at the end.
113
The keys in the returned dict are all normalized to lowercase (look for
114
``"port"``, not ``"Port"``. The values are processed according to the
115
rules for substitution variable expansion in ``ssh_config``.
117
:param str hostname: the hostname to lookup
120
config for config in self._config
121
if self._allowed(config['host'], hostname)
125
for match in matches:
126
for key, value in match['config'].items():
128
# Create a copy of the original value,
129
# else it will reference the original list
130
# in self._config and update that value too
131
# when the extend() is being called.
132
ret[key] = value[:] if value is not None else value
133
elif key == 'identityfile':
134
ret[key].extend(value)
135
ret = self._expand_variables(ret, hostname)
136
# TODO: remove in 3.x re #670
137
if 'proxycommand' in ret and ret['proxycommand'] is None:
138
del ret['proxycommand']
141
def get_hostnames(self):
143
Return the set of literal hostnames defined in the SSH config (both
144
explicit hostnames and wildcard entries).
147
for entry in self._config:
148
hosts.update(entry['host'])
151
def _allowed(self, hosts, hostname):
154
if host.startswith('!') and fnmatch.fnmatch(hostname, host[1:]):
156
elif fnmatch.fnmatch(hostname, host):
160
def _expand_variables(self, config, hostname):
162
Return a dict of config options with expanded substitutions
163
for a given hostname.
165
Please refer to man ``ssh_config`` for the parameters that
168
:param dict config: the config for the hostname
169
:param str hostname: the hostname that the config belongs to
172
if 'hostname' in config:
173
config['hostname'] = config['hostname'].replace('%h', hostname)
175
config['hostname'] = hostname
178
port = config['port']
182
user = os.getenv('USER')
184
remoteuser = config['user']
188
host = socket.gethostname().split('.')[0]
189
fqdn = LazyFqdn(config, host)
190
homedir = os.path.expanduser('~')
191
replacements = {'controlpath':
193
('%h', config['hostname']),
205
('%h', config['hostname']),
213
('%h', config['hostname']),
220
if config[k] is None:
222
if k in replacements:
223
for find, replace in replacements[k]:
224
if isinstance(config[k], list):
225
for item in range(len(config[k])):
226
if find in config[k][item]:
227
config[k][item] = config[k][item].replace(
231
if find in config[k]:
232
config[k] = config[k].replace(find, str(replace))
235
def _get_hosts(self, host):
237
Return a list of host_names from host value.
240
return shlex.split(host)
242
raise Exception("Unparsable host {}".format(host))
245
class LazyFqdn(object):
247
Returns the host's fqdn on request as string.
250
def __init__(self, config, host=None):
256
if self.fqdn is None:
258
# If the SSH config contains AddressFamily, use that when
259
# determining the local host's FQDN. Using socket.getfqdn() from
260
# the standard library is the most general solution, but can
261
# result in noticeable delays on some platforms when IPv6 is
262
# misconfigured or not available, as it calls getaddrinfo with no
263
# address family specified, so both IPv4 and IPv6 are checked.
266
# Handle specific option
268
address_family = self.config.get('addressfamily', 'any').lower()
269
if address_family != 'any':
271
family = socket.AF_INET6
272
if address_family == 'inet':
274
results = socket.getaddrinfo(
283
af, socktype, proto, canonname, sa = res
284
if canonname and '.' in canonname:
287
# giaerror -> socket.getaddrinfo() can't resolve self.host
288
# (which is from socket.gethostname()). Fall back to the
289
# getfqdn() call below.
290
except socket.gaierror:
292
# Handle 'any' / unspecified
294
fqdn = socket.getfqdn()