1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 """
21 Configuration file (aka ``ssh_config``) support.
22 """
23
24 import fnmatch
25 import os
26 import re
27 import socket
28
29 SSH_PORT = 22
30 proxy_re = re.compile(r"^(proxycommand)\s*=*\s*(.*)", re.I)
31
32
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
45 """
46 Create a new OpenSSH config object.
47 """
48 self._config = []
49
50 - def parse(self, file_obj):
51 """
52 Read an OpenSSH config from the given file object.
53
54 :param file file_obj: a file-like object to read the config file from
55 """
56 host = {"host": ['*'], "config": {}}
57 for line in file_obj:
58 line = line.rstrip('\r\n').lstrip()
59 if (line == '') or (line[0] == '#'):
60 continue
61 if '=' in line:
62
63 if line.lower().strip().startswith('proxycommand'):
64 match = proxy_re.match(line)
65 key, value = match.group(1).lower(), match.group(2)
66 else:
67 key, value = line.split('=', 1)
68 key = key.strip().lower()
69 else:
70
71 i = 0
72 while (i < len(line)) and not line[i].isspace():
73 i += 1
74 if i == len(line):
75 raise Exception('Unparsable line: %r' % line)
76 key = line[:i].lower()
77 value = line[i:].lstrip()
78
79 if key == 'host':
80 self._config.append(host)
81 value = value.split()
82 host = {key: value, 'config': {}}
83
84
85
86
87 elif key in ['identityfile', 'localforward', 'remoteforward']:
88 if key in host['config']:
89 host['config'][key].append(value)
90 else:
91 host['config'][key] = [value]
92 elif key not in host['config']:
93 host['config'].update({key: value})
94 self._config.append(host)
95
97 """
98 Return a dict of config options for a given hostname.
99
100 The host-matching rules of OpenSSH's ``ssh_config`` man page are used,
101 which means that all configuration options from matching host
102 specifications are merged, with more specific hostmasks taking
103 precedence. In other words, if ``"Port"`` is set under ``"Host *"``
104 and also ``"Host *.example.com"``, and the lookup is for
105 ``"ssh.example.com"``, then the port entry for ``"Host *.example.com"``
106 will win out.
107
108 The keys in the returned dict are all normalized to lowercase (look for
109 ``"port"``, not ``"Port"``. The values are processed according to the
110 rules for substitution variable expansion in ``ssh_config``.
111
112 :param str hostname: the hostname to lookup
113 """
114 matches = [config for config in self._config if
115 self._allowed(hostname, config['host'])]
116
117 ret = {}
118 for match in matches:
119 for key, value in match['config'].items():
120 if key not in ret:
121
122
123
124
125 ret[key] = value[:]
126 elif key == 'identityfile':
127 ret[key].extend(value)
128 ret = self._expand_variables(ret, hostname)
129 return ret
130
132 match = False
133 for host in hosts:
134 if host.startswith('!') and fnmatch.fnmatch(hostname, host[1:]):
135 return False
136 elif fnmatch.fnmatch(hostname, host):
137 match = True
138 return match
139
141 """
142 Return a dict of config options with expanded substitutions
143 for a given hostname.
144
145 Please refer to man ``ssh_config`` for the parameters that
146 are replaced.
147
148 :param dict config: the config for the hostname
149 :param str hostname: the hostname that the config belongs to
150 """
151
152 if 'hostname' in config:
153 config['hostname'] = config['hostname'].replace('%h', hostname)
154 else:
155 config['hostname'] = hostname
156
157 if 'port' in config:
158 port = config['port']
159 else:
160 port = SSH_PORT
161
162 user = os.getenv('USER')
163 if 'user' in config:
164 remoteuser = config['user']
165 else:
166 remoteuser = user
167
168 host = socket.gethostname().split('.')[0]
169 fqdn = LazyFqdn(config, host)
170 homedir = os.path.expanduser('~')
171 replacements = {'controlpath':
172 [
173 ('%h', config['hostname']),
174 ('%l', fqdn),
175 ('%L', host),
176 ('%n', hostname),
177 ('%p', port),
178 ('%r', remoteuser),
179 ('%u', user)
180 ],
181 'identityfile':
182 [
183 ('~', homedir),
184 ('%d', homedir),
185 ('%h', config['hostname']),
186 ('%l', fqdn),
187 ('%u', user),
188 ('%r', remoteuser)
189 ],
190 'proxycommand':
191 [
192 ('%h', config['hostname']),
193 ('%p', port),
194 ('%r', remoteuser)
195 ]
196 }
197
198 for k in config:
199 if k in replacements:
200 for find, replace in replacements[k]:
201 if isinstance(config[k], list):
202 for item in range(len(config[k])):
203 if find in config[k][item]:
204 config[k][item] = config[k][item].\
205 replace(find, str(replace))
206 else:
207 if find in config[k]:
208 config[k] = config[k].replace(find, str(replace))
209 return config
210
211
213 """
214 Returns the host's fqdn on request as string.
215 """
216
218 self.fqdn = None
219 self.config = config
220 self.host = host
221
223 if self.fqdn is None:
224
225
226
227
228
229
230
231
232
233
234 fqdn = None
235 address_family = self.config.get('addressfamily', 'any').lower()
236 if address_family != 'any':
237 try:
238 family = socket.AF_INET if address_family == 'inet' \
239 else socket.AF_INET6
240 results = socket.getaddrinfo(
241 self.host,
242 None,
243 family,
244 socket.SOCK_DGRAM,
245 socket.IPPROTO_IP,
246 socket.AI_CANONNAME
247 )
248 for res in results:
249 af, socktype, proto, canonname, sa = res
250 if canonname and '.' in canonname:
251 fqdn = canonname
252 break
253
254
255
256 except socket.gaierror:
257 pass
258
259 if fqdn is None:
260 fqdn = socket.getfqdn()
261
262 self.fqdn = fqdn
263 return self.fqdn
264