You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

297 lines
11 KiB

7 years ago
  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. Configuration file (aka ``ssh_config``) support.
  21. """
  22. import fnmatch
  23. import os
  24. import re
  25. import shlex
  26. import socket
  27. SSH_PORT = 22
  28. class SSHConfig (object):
  29. """
  30. Representation of config information as stored in the format used by
  31. OpenSSH. Queries can be made via `lookup`. The format is described in
  32. OpenSSH's ``ssh_config`` man page. This class is provided primarily as a
  33. convenience to posix users (since the OpenSSH format is a de-facto
  34. standard on posix) but should work fine on Windows too.
  35. .. versionadded:: 1.6
  36. """
  37. SETTINGS_REGEX = re.compile(r'(\w+)(?:\s*=\s*|\s+)(.+)')
  38. def __init__(self):
  39. """
  40. Create a new OpenSSH config object.
  41. """
  42. self._config = []
  43. def parse(self, file_obj):
  44. """
  45. Read an OpenSSH config from the given file object.
  46. :param file_obj: a file-like object to read the config file from
  47. """
  48. host = {"host": ['*'], "config": {}}
  49. for line in file_obj:
  50. # Strip any leading or trailing whitespace from the line.
  51. # Refer to https://github.com/paramiko/paramiko/issues/499
  52. line = line.strip()
  53. if not line or line.startswith('#'):
  54. continue
  55. match = re.match(self.SETTINGS_REGEX, line)
  56. if not match:
  57. raise Exception("Unparsable line {}".format(line))
  58. key = match.group(1).lower()
  59. value = match.group(2)
  60. if key == 'host':
  61. self._config.append(host)
  62. host = {
  63. 'host': self._get_hosts(value),
  64. 'config': {}
  65. }
  66. elif key == 'proxycommand' and value.lower() == 'none':
  67. # Store 'none' as None; prior to 3.x, it will get stripped out
  68. # at the end (for compatibility with issue #415). After 3.x, it
  69. # will simply not get stripped, leaving a nice explicit marker.
  70. host['config'][key] = None
  71. else:
  72. if value.startswith('"') and value.endswith('"'):
  73. value = value[1:-1]
  74. # identityfile, localforward, remoteforward keys are special
  75. # cases, since they are allowed to be specified multiple times
  76. # and they should be tried in order of specification.
  77. if key in ['identityfile', 'localforward', 'remoteforward']:
  78. if key in host['config']:
  79. host['config'][key].append(value)
  80. else:
  81. host['config'][key] = [value]
  82. elif key not in host['config']:
  83. host['config'][key] = value
  84. self._config.append(host)
  85. def lookup(self, hostname):
  86. """
  87. Return a dict of config options for a given hostname.
  88. The host-matching rules of OpenSSH's ``ssh_config`` man page are used:
  89. For each parameter, the first obtained value will be used. The
  90. configuration files contain sections separated by ``Host``
  91. specifications, and that section is only applied for hosts that match
  92. one of the patterns given in the specification.
  93. Since the first obtained value for each parameter is used, more host-
  94. specific declarations should be given near the beginning of the file,
  95. and general defaults at the end.
  96. The keys in the returned dict are all normalized to lowercase (look for
  97. ``"port"``, not ``"Port"``. The values are processed according to the
  98. rules for substitution variable expansion in ``ssh_config``.
  99. :param str hostname: the hostname to lookup
  100. """
  101. matches = [
  102. config for config in self._config
  103. if self._allowed(config['host'], hostname)
  104. ]
  105. ret = {}
  106. for match in matches:
  107. for key, value in match['config'].items():
  108. if key not in ret:
  109. # Create a copy of the original value,
  110. # else it will reference the original list
  111. # in self._config and update that value too
  112. # when the extend() is being called.
  113. ret[key] = value[:] if value is not None else value
  114. elif key == 'identityfile':
  115. ret[key].extend(value)
  116. ret = self._expand_variables(ret, hostname)
  117. # TODO: remove in 3.x re #670
  118. if 'proxycommand' in ret and ret['proxycommand'] is None:
  119. del ret['proxycommand']
  120. return ret
  121. def get_hostnames(self):
  122. """
  123. Return the set of literal hostnames defined in the SSH config (both
  124. explicit hostnames and wildcard entries).
  125. """
  126. hosts = set()
  127. for entry in self._config:
  128. hosts.update(entry['host'])
  129. return hosts
  130. def _allowed(self, hosts, hostname):
  131. match = False
  132. for host in hosts:
  133. if host.startswith('!') and fnmatch.fnmatch(hostname, host[1:]):
  134. return False
  135. elif fnmatch.fnmatch(hostname, host):
  136. match = True
  137. return match
  138. def _expand_variables(self, config, hostname):
  139. """
  140. Return a dict of config options with expanded substitutions
  141. for a given hostname.
  142. Please refer to man ``ssh_config`` for the parameters that
  143. are replaced.
  144. :param dict config: the config for the hostname
  145. :param str hostname: the hostname that the config belongs to
  146. """
  147. if 'hostname' in config:
  148. config['hostname'] = config['hostname'].replace('%h', hostname)
  149. else:
  150. config['hostname'] = hostname
  151. if 'port' in config:
  152. port = config['port']
  153. else:
  154. port = SSH_PORT
  155. user = os.getenv('USER')
  156. if 'user' in config:
  157. remoteuser = config['user']
  158. else:
  159. remoteuser = user
  160. host = socket.gethostname().split('.')[0]
  161. fqdn = LazyFqdn(config, host)
  162. homedir = os.path.expanduser('~')
  163. replacements = {'controlpath':
  164. [
  165. ('%h', config['hostname']),
  166. ('%l', fqdn),
  167. ('%L', host),
  168. ('%n', hostname),
  169. ('%p', port),
  170. ('%r', remoteuser),
  171. ('%u', user)
  172. ],
  173. 'identityfile':
  174. [
  175. ('~', homedir),
  176. ('%d', homedir),
  177. ('%h', config['hostname']),
  178. ('%l', fqdn),
  179. ('%u', user),
  180. ('%r', remoteuser)
  181. ],
  182. 'proxycommand':
  183. [
  184. ('~', homedir),
  185. ('%h', config['hostname']),
  186. ('%p', port),
  187. ('%r', remoteuser)
  188. ]
  189. }
  190. for k in config:
  191. if config[k] is None:
  192. continue
  193. if k in replacements:
  194. for find, replace in replacements[k]:
  195. if isinstance(config[k], list):
  196. for item in range(len(config[k])):
  197. if find in config[k][item]:
  198. config[k][item] = config[k][item].replace(
  199. find, str(replace)
  200. )
  201. else:
  202. if find in config[k]:
  203. config[k] = config[k].replace(find, str(replace))
  204. return config
  205. def _get_hosts(self, host):
  206. """
  207. Return a list of host_names from host value.
  208. """
  209. try:
  210. return shlex.split(host)
  211. except ValueError:
  212. raise Exception("Unparsable host {}".format(host))
  213. class LazyFqdn(object):
  214. """
  215. Returns the host's fqdn on request as string.
  216. """
  217. def __init__(self, config, host=None):
  218. self.fqdn = None
  219. self.config = config
  220. self.host = host
  221. def __str__(self):
  222. if self.fqdn is None:
  223. #
  224. # If the SSH config contains AddressFamily, use that when
  225. # determining the local host's FQDN. Using socket.getfqdn() from
  226. # the standard library is the most general solution, but can
  227. # result in noticeable delays on some platforms when IPv6 is
  228. # misconfigured or not available, as it calls getaddrinfo with no
  229. # address family specified, so both IPv4 and IPv6 are checked.
  230. #
  231. # Handle specific option
  232. fqdn = None
  233. address_family = self.config.get('addressfamily', 'any').lower()
  234. if address_family != 'any':
  235. try:
  236. family = socket.AF_INET6
  237. if address_family == 'inet':
  238. socket.AF_INET
  239. results = socket.getaddrinfo(
  240. self.host,
  241. None,
  242. family,
  243. socket.SOCK_DGRAM,
  244. socket.IPPROTO_IP,
  245. socket.AI_CANONNAME
  246. )
  247. for res in results:
  248. af, socktype, proto, canonname, sa = res
  249. if canonname and '.' in canonname:
  250. fqdn = canonname
  251. break
  252. # giaerror -> socket.getaddrinfo() can't resolve self.host
  253. # (which is from socket.gethostname()). Fall back to the
  254. # getfqdn() call below.
  255. except socket.gaierror:
  256. pass
  257. # Handle 'any' / unspecified
  258. if fqdn is None:
  259. fqdn = socket.getfqdn()
  260. # Cache
  261. self.fqdn = fqdn
  262. return self.fqdn

Powered by TurnKey Linux.