PageRenderTime 26ms CodeModel.GetById 22ms RepoModel.GetById 0ms app.codeStats 0ms

/paramiko/config.py

https://gitlab.com/gupta.d.gaurav/paramiko
Python | 287 lines | 215 code | 14 blank | 58 comment | 26 complexity | 5f08ef40eef43c14e7c15030d5cd7f74 MD5 | raw file
  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. # See https://github.com/paramiko/paramiko/issues/499 for more info.
  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 %s" % 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. # Proxycommands of none should not be added as an actual value. (Issue #415)
  68. continue
  69. else:
  70. if value.startswith('"') and value.endswith('"'):
  71. value = value[1:-1]
  72. #identityfile, localforward, remoteforward keys are special cases, since they are allowed to be
  73. # specified multiple times and they should be tried in order
  74. # of specification.
  75. if key in ['identityfile', 'localforward', 'remoteforward']:
  76. if key in host['config']:
  77. host['config'][key].append(value)
  78. else:
  79. host['config'][key] = [value]
  80. elif key not in host['config']:
  81. host['config'][key] = value
  82. self._config.append(host)
  83. def lookup(self, hostname):
  84. """
  85. Return a dict of config options for a given hostname.
  86. The host-matching rules of OpenSSH's ``ssh_config`` man page are used:
  87. For each parameter, the first obtained value will be used. The
  88. configuration files contain sections separated by ``Host``
  89. specifications, and that section is only applied for hosts that match
  90. one of the patterns given in the specification.
  91. Since the first obtained value for each parameter is used, more host-
  92. specific declarations should be given near the beginning of the file,
  93. and general defaults at the end.
  94. The keys in the returned dict are all normalized to lowercase (look for
  95. ``"port"``, not ``"Port"``. The values are processed according to the
  96. rules for substitution variable expansion in ``ssh_config``.
  97. :param str hostname: the hostname to lookup
  98. """
  99. matches = [
  100. config for config in self._config
  101. if self._allowed(config['host'], hostname)
  102. ]
  103. ret = {}
  104. for match in matches:
  105. for key, value in match['config'].items():
  106. if key not in ret:
  107. # Create a copy of the original value,
  108. # else it will reference the original list
  109. # in self._config and update that value too
  110. # when the extend() is being called.
  111. ret[key] = value[:]
  112. elif key == 'identityfile':
  113. ret[key].extend(value)
  114. ret = self._expand_variables(ret, hostname)
  115. return ret
  116. def get_hostnames(self):
  117. """
  118. Return the set of literal hostnames defined in the SSH config (both
  119. explicit hostnames and wildcard entries).
  120. """
  121. hosts = set()
  122. for entry in self._config:
  123. hosts.update(entry['host'])
  124. return hosts
  125. def _allowed(self, hosts, hostname):
  126. match = False
  127. for host in hosts:
  128. if host.startswith('!') and fnmatch.fnmatch(hostname, host[1:]):
  129. return False
  130. elif fnmatch.fnmatch(hostname, host):
  131. match = True
  132. return match
  133. def _expand_variables(self, config, hostname):
  134. """
  135. Return a dict of config options with expanded substitutions
  136. for a given hostname.
  137. Please refer to man ``ssh_config`` for the parameters that
  138. are replaced.
  139. :param dict config: the config for the hostname
  140. :param str hostname: the hostname that the config belongs to
  141. """
  142. if 'hostname' in config:
  143. config['hostname'] = config['hostname'].replace('%h', hostname)
  144. else:
  145. config['hostname'] = hostname
  146. if 'port' in config:
  147. port = config['port']
  148. else:
  149. port = SSH_PORT
  150. user = os.getenv('USER')
  151. if 'user' in config:
  152. remoteuser = config['user']
  153. else:
  154. remoteuser = user
  155. host = socket.gethostname().split('.')[0]
  156. fqdn = LazyFqdn(config, host)
  157. homedir = os.path.expanduser('~')
  158. replacements = {'controlpath':
  159. [
  160. ('%h', config['hostname']),
  161. ('%l', fqdn),
  162. ('%L', host),
  163. ('%n', hostname),
  164. ('%p', port),
  165. ('%r', remoteuser),
  166. ('%u', user)
  167. ],
  168. 'identityfile':
  169. [
  170. ('~', homedir),
  171. ('%d', homedir),
  172. ('%h', config['hostname']),
  173. ('%l', fqdn),
  174. ('%u', user),
  175. ('%r', remoteuser)
  176. ],
  177. 'proxycommand':
  178. [
  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. if isinstance(config[k], list):
  188. for item in range(len(config[k])):
  189. if find in config[k][item]:
  190. config[k][item] = config[k][item].\
  191. replace(find, str(replace))
  192. else:
  193. if find in config[k]:
  194. config[k] = config[k].replace(find, str(replace))
  195. return config
  196. def _get_hosts(self, host):
  197. """
  198. Return a list of host_names from host value.
  199. """
  200. try:
  201. return shlex.split(host)
  202. except ValueError:
  203. raise Exception("Unparsable host %s" % host)
  204. class LazyFqdn(object):
  205. """
  206. Returns the host's fqdn on request as string.
  207. """
  208. def __init__(self, config, host=None):
  209. self.fqdn = None
  210. self.config = config
  211. self.host = host
  212. def __str__(self):
  213. if self.fqdn is None:
  214. #
  215. # If the SSH config contains AddressFamily, use that when
  216. # determining the local host's FQDN. Using socket.getfqdn() from
  217. # the standard library is the most general solution, but can
  218. # result in noticeable delays on some platforms when IPv6 is
  219. # misconfigured or not available, as it calls getaddrinfo with no
  220. # address family specified, so both IPv4 and IPv6 are checked.
  221. #
  222. # Handle specific option
  223. fqdn = None
  224. address_family = self.config.get('addressfamily', 'any').lower()
  225. if address_family != 'any':
  226. try:
  227. family = socket.AF_INET if address_family == 'inet' \
  228. else socket.AF_INET6
  229. results = socket.getaddrinfo(
  230. self.host,
  231. None,
  232. family,
  233. socket.SOCK_DGRAM,
  234. socket.IPPROTO_IP,
  235. socket.AI_CANONNAME
  236. )
  237. for res in results:
  238. af, socktype, proto, canonname, sa = res
  239. if canonname and '.' in canonname:
  240. fqdn = canonname
  241. break
  242. # giaerror -> socket.getaddrinfo() can't resolve self.host
  243. # (which is from socket.gethostname()). Fall back to the
  244. # getfqdn() call below.
  245. except socket.gaierror:
  246. pass
  247. # Handle 'any' / unspecified
  248. if fqdn is None:
  249. fqdn = socket.getfqdn()
  250. # Cache
  251. self.fqdn = fqdn
  252. return self.fqdn