~davide-cst00/uftp/0.2

« back to all changes in this revision

Viewing changes to paramiko/config.py

  • Committer: Davide Costa
  • Date: 2018-09-14 12:21:37 UTC
  • Revision ID: davide.cst00@gmail.com-20180914122137-j0ycqb4tk9z2r1k5
Xenial release 0.2.1

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2006-2007  Robey Pointer <robeypointer@gmail.com>
 
2
# Copyright (C) 2012  Olle Lundberg <geek@nerd.sh>
 
3
#
 
4
# This file is part of paramiko.
 
5
#
 
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)
 
9
# any later version.
 
10
#
 
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
 
14
# details.
 
15
#
 
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.
 
19
 
 
20
"""
 
21
Configuration file (aka ``ssh_config``) support.
 
22
"""
 
23
 
 
24
import fnmatch
 
25
import os
 
26
import re
 
27
import shlex
 
28
import socket
 
29
 
 
30
SSH_PORT = 22
 
31
 
 
32
 
 
33
class SSHConfig (object):
 
34
    """
 
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.
 
40
 
 
41
    .. versionadded:: 1.6
 
42
    """
 
43
 
 
44
    SETTINGS_REGEX = re.compile(r'(\w+)(?:\s*=\s*|\s+)(.+)')
 
45
 
 
46
    def __init__(self):
 
47
        """
 
48
        Create a new OpenSSH config object.
 
49
        """
 
50
        self._config = []
 
51
 
 
52
    def parse(self, file_obj):
 
53
        """
 
54
        Read an OpenSSH config from the given file object.
 
55
 
 
56
        :param file_obj: a file-like object to read the config file from
 
57
        """
 
58
        host = {"host": ['*'], "config": {}}
 
59
        for line in file_obj:
 
60
            # Strip any leading or trailing whitespace from the line.
 
61
            # Refer to https://github.com/paramiko/paramiko/issues/499
 
62
            line = line.strip()
 
63
            if not line or line.startswith('#'):
 
64
                continue
 
65
 
 
66
            match = re.match(self.SETTINGS_REGEX, line)
 
67
            if not match:
 
68
                raise Exception("Unparsable line {}".format(line))
 
69
            key = match.group(1).lower()
 
70
            value = match.group(2)
 
71
 
 
72
            if key == 'host':
 
73
                self._config.append(host)
 
74
                host = {
 
75
                    'host': self._get_hosts(value),
 
76
                    'config': {}
 
77
                }
 
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
 
83
            else:
 
84
                if value.startswith('"') and value.endswith('"'):
 
85
                    value = value[1:-1]
 
86
 
 
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)
 
93
                    else:
 
94
                        host['config'][key] = [value]
 
95
                elif key not in host['config']:
 
96
                    host['config'][key] = value
 
97
        self._config.append(host)
 
98
 
 
99
    def lookup(self, hostname):
 
100
        """
 
101
        Return a dict of config options for a given hostname.
 
102
 
 
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.
 
108
 
 
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.
 
112
 
 
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``.
 
116
 
 
117
        :param str hostname: the hostname to lookup
 
118
        """
 
119
        matches = [
 
120
            config for config in self._config
 
121
            if self._allowed(config['host'], hostname)
 
122
        ]
 
123
 
 
124
        ret = {}
 
125
        for match in matches:
 
126
            for key, value in match['config'].items():
 
127
                if key not in ret:
 
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']
 
139
        return ret
 
140
 
 
141
    def get_hostnames(self):
 
142
        """
 
143
        Return the set of literal hostnames defined in the SSH config (both
 
144
        explicit hostnames and wildcard entries).
 
145
        """
 
146
        hosts = set()
 
147
        for entry in self._config:
 
148
            hosts.update(entry['host'])
 
149
        return hosts
 
150
 
 
151
    def _allowed(self, hosts, hostname):
 
152
        match = False
 
153
        for host in hosts:
 
154
            if host.startswith('!') and fnmatch.fnmatch(hostname, host[1:]):
 
155
                return False
 
156
            elif fnmatch.fnmatch(hostname, host):
 
157
                match = True
 
158
        return match
 
159
 
 
160
    def _expand_variables(self, config, hostname):
 
161
        """
 
162
        Return a dict of config options with expanded substitutions
 
163
        for a given hostname.
 
164
 
 
165
        Please refer to man ``ssh_config`` for the parameters that
 
166
        are replaced.
 
167
 
 
168
        :param dict config: the config for the hostname
 
169
        :param str hostname: the hostname that the config belongs to
 
170
        """
 
171
 
 
172
        if 'hostname' in config:
 
173
            config['hostname'] = config['hostname'].replace('%h', hostname)
 
174
        else:
 
175
            config['hostname'] = hostname
 
176
 
 
177
        if 'port' in config:
 
178
            port = config['port']
 
179
        else:
 
180
            port = SSH_PORT
 
181
 
 
182
        user = os.getenv('USER')
 
183
        if 'user' in config:
 
184
            remoteuser = config['user']
 
185
        else:
 
186
            remoteuser = user
 
187
 
 
188
        host = socket.gethostname().split('.')[0]
 
189
        fqdn = LazyFqdn(config, host)
 
190
        homedir = os.path.expanduser('~')
 
191
        replacements = {'controlpath':
 
192
                        [
 
193
                            ('%h', config['hostname']),
 
194
                            ('%l', fqdn),
 
195
                            ('%L', host),
 
196
                            ('%n', hostname),
 
197
                            ('%p', port),
 
198
                            ('%r', remoteuser),
 
199
                            ('%u', user)
 
200
                        ],
 
201
                        'identityfile':
 
202
                        [
 
203
                            ('~', homedir),
 
204
                            ('%d', homedir),
 
205
                            ('%h', config['hostname']),
 
206
                            ('%l', fqdn),
 
207
                            ('%u', user),
 
208
                            ('%r', remoteuser)
 
209
                        ],
 
210
                        'proxycommand':
 
211
                        [
 
212
                            ('~', homedir),
 
213
                            ('%h', config['hostname']),
 
214
                            ('%p', port),
 
215
                            ('%r', remoteuser)
 
216
                        ]
 
217
                        }
 
218
 
 
219
        for k in config:
 
220
            if config[k] is None:
 
221
                continue
 
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(
 
228
                                    find, str(replace)
 
229
                                )
 
230
                    else:
 
231
                        if find in config[k]:
 
232
                            config[k] = config[k].replace(find, str(replace))
 
233
        return config
 
234
 
 
235
    def _get_hosts(self, host):
 
236
        """
 
237
        Return a list of host_names from host value.
 
238
        """
 
239
        try:
 
240
            return shlex.split(host)
 
241
        except ValueError:
 
242
            raise Exception("Unparsable host {}".format(host))
 
243
 
 
244
 
 
245
class LazyFqdn(object):
 
246
    """
 
247
    Returns the host's fqdn on request as string.
 
248
    """
 
249
 
 
250
    def __init__(self, config, host=None):
 
251
        self.fqdn = None
 
252
        self.config = config
 
253
        self.host = host
 
254
 
 
255
    def __str__(self):
 
256
        if self.fqdn is None:
 
257
            #
 
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.
 
264
            #
 
265
 
 
266
            # Handle specific option
 
267
            fqdn = None
 
268
            address_family = self.config.get('addressfamily', 'any').lower()
 
269
            if address_family != 'any':
 
270
                try:
 
271
                    family = socket.AF_INET6
 
272
                    if address_family == 'inet':
 
273
                        socket.AF_INET
 
274
                    results = socket.getaddrinfo(
 
275
                        self.host,
 
276
                        None,
 
277
                        family,
 
278
                        socket.SOCK_DGRAM,
 
279
                        socket.IPPROTO_IP,
 
280
                        socket.AI_CANONNAME
 
281
                    )
 
282
                    for res in results:
 
283
                        af, socktype, proto, canonname, sa = res
 
284
                        if canonname and '.' in canonname:
 
285
                            fqdn = canonname
 
286
                            break
 
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:
 
291
                    pass
 
292
            # Handle 'any' / unspecified
 
293
            if fqdn is None:
 
294
                fqdn = socket.getfqdn()
 
295
            # Cache
 
296
            self.fqdn = fqdn
 
297
        return self.fqdn