1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 """
20 L{SSHConfig}.
21 """
22
23 import fnmatch
24 import os
25 import re
26 import socket
27
28 SSH_PORT=22
29 proxy_re = re.compile(r"^(proxycommand)\s*=*\s*(.*)", re.I)
30
31
33 """
34 Representation of config information as stored in the format used by
35 OpenSSH. Queries can be made via L{lookup}. The format is described in
36 OpenSSH's C{ssh_config} man page. This class is provided primarily as a
37 convenience to posix users (since the OpenSSH format is a de-facto
38 standard on posix) but should work fine on Windows too.
39
40 @since: 1.6
41 """
42
44 """
45 Create a new OpenSSH config object.
46 """
47 self._config = [ { 'host': '*' } ]
48
49 - def parse(self, file_obj):
50 """
51 Read an OpenSSH config from the given file object.
52
53 @param file_obj: a file-like object to read the config file from
54 @type file_obj: file
55 """
56 configs = [self._config[0]]
57 for line in file_obj:
58 line = line.rstrip('\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 del configs[:]
81
82 for host in value.split():
83
84 matches = [c for c in self._config if c['host'] == host]
85 if len(matches) > 0:
86 configs.append(matches[0])
87 else:
88 config = { 'host': host }
89 self._config.append(config)
90 configs.append(config)
91 else:
92 for config in configs:
93 config[key] = value
94
96 """
97 Return a dict of config options for a given hostname.
98
99 The host-matching rules of OpenSSH's C{ssh_config} man page are used,
100 which means that all configuration options from matching host
101 specifications are merged, with more specific hostmasks taking
102 precedence. In other words, if C{"Port"} is set under C{"Host *"}
103 and also C{"Host *.example.com"}, and the lookup is for
104 C{"ssh.example.com"}, then the port entry for C{"Host *.example.com"}
105 will win out.
106
107 The keys in the returned dict are all normalized to lowercase (look for
108 C{"port"}, not C{"Port"}. No other processing is done to the keys or
109 values.
110
111 @param hostname: the hostname to lookup
112 @type hostname: str
113 """
114 matches = [x for x in self._config if fnmatch.fnmatch(hostname, x['host'])]
115
116 _star = matches.pop(0)
117 matches.append(_star)
118 ret = {}
119 for m in matches:
120 for k,v in m.iteritems():
121 if not k in ret:
122 ret[k] = v
123 ret = self._expand_variables(ret, hostname)
124 del ret['host']
125 return ret
126
128 """
129 Return a dict of config options with expanded substitutions
130 for a given hostname.
131
132 Please refer to man ssh_config(5) for the parameters that
133 are replaced.
134
135 @param config: the config for the hostname
136 @type hostname: dict
137 @param hostname: the hostname that the config belongs to
138 @type hostname: str
139 """
140
141 if 'hostname' in config:
142 config['hostname'] = config['hostname'].replace('%h',hostname)
143 else:
144 config['hostname'] = hostname
145
146 if 'port' in config:
147 port = config['port']
148 else:
149 port = SSH_PORT
150
151 user = os.getenv('USER')
152 if 'user' in config:
153 remoteuser = config['user']
154 else:
155 remoteuser = user
156
157 host = socket.gethostname().split('.')[0]
158 fqdn = socket.getfqdn()
159 homedir = os.path.expanduser('~')
160 replacements = {
161 'controlpath': [
162 ('%h', config['hostname']),
163 ('%l', fqdn),
164 ('%L', host),
165 ('%n', hostname),
166 ('%p', port),
167 ('%r', remoteuser),
168 ('%u', user)
169 ],
170 'identityfile': [
171 ('~', homedir),
172 ('%d', homedir),
173 ('%h', config['hostname']),
174 ('%l', fqdn),
175 ('%u', user),
176 ('%r', remoteuser)
177 ],
178 'proxycommand': [
179 ('%h', config['hostname']),
180 ('%p', port),
181 ('%r', remoteuser),
182 ],
183 }
184 for k in config:
185 if k in replacements:
186 for find, replace in replacements[k]:
187 config[k] = config[k].replace(find, str(replace))
188 return config
189