config.py 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692
  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. # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
  19. """
  20. Configuration file (aka ``ssh_config``) support.
  21. """
  22. import fnmatch
  23. import getpass
  24. import os
  25. import re
  26. import shlex
  27. import socket
  28. from hashlib import sha1
  29. from functools import partial
  30. from .py3compat import StringIO
  31. invoke, invoke_import_error = None, None
  32. try:
  33. import invoke
  34. except ImportError as e:
  35. invoke_import_error = e
  36. from .ssh_exception import CouldNotCanonicalize, ConfigParseError
  37. SSH_PORT = 22
  38. class SSHConfig(object):
  39. """
  40. Representation of config information as stored in the format used by
  41. OpenSSH. Queries can be made via `lookup`. The format is described in
  42. OpenSSH's ``ssh_config`` man page. This class is provided primarily as a
  43. convenience to posix users (since the OpenSSH format is a de-facto
  44. standard on posix) but should work fine on Windows too.
  45. .. versionadded:: 1.6
  46. """
  47. SETTINGS_REGEX = re.compile(r"(\w+)(?:\s*=\s*|\s+)(.+)")
  48. # TODO: do a full scan of ssh.c & friends to make sure we're fully
  49. # compatible across the board, e.g. OpenSSH 8.1 added %n to ProxyCommand.
  50. TOKENS_BY_CONFIG_KEY = {
  51. "controlpath": ["%C", "%h", "%l", "%L", "%n", "%p", "%r", "%u"],
  52. "hostname": ["%h"],
  53. "identityfile": ["%C", "~", "%d", "%h", "%l", "%u", "%r"],
  54. "proxycommand": ["~", "%h", "%p", "%r"],
  55. "proxyjump": ["%h", "%p", "%r"],
  56. # Doesn't seem worth making this 'special' for now, it will fit well
  57. # enough (no actual match-exec config key to be confused with).
  58. "match-exec": ["%C", "%d", "%h", "%L", "%l", "%n", "%p", "%r", "%u"],
  59. }
  60. def __init__(self):
  61. """
  62. Create a new OpenSSH config object.
  63. Note: the newer alternate constructors `from_path`, `from_file` and
  64. `from_text` are simpler to use, as they parse on instantiation. For
  65. example, instead of::
  66. config = SSHConfig()
  67. config.parse(open("some-path.config")
  68. you could::
  69. config = SSHConfig.from_file(open("some-path.config"))
  70. # Or more directly:
  71. config = SSHConfig.from_path("some-path.config")
  72. # Or if you have arbitrary ssh_config text from some other source:
  73. config = SSHConfig.from_text("Host foo\\n\\tUser bar")
  74. """
  75. self._config = []
  76. @classmethod
  77. def from_text(cls, text):
  78. """
  79. Create a new, parsed `SSHConfig` from ``text`` string.
  80. .. versionadded:: 2.7
  81. """
  82. return cls.from_file(StringIO(text))
  83. @classmethod
  84. def from_path(cls, path):
  85. """
  86. Create a new, parsed `SSHConfig` from the file found at ``path``.
  87. .. versionadded:: 2.7
  88. """
  89. with open(path) as flo:
  90. return cls.from_file(flo)
  91. @classmethod
  92. def from_file(cls, flo):
  93. """
  94. Create a new, parsed `SSHConfig` from file-like object ``flo``.
  95. .. versionadded:: 2.7
  96. """
  97. obj = cls()
  98. obj.parse(flo)
  99. return obj
  100. def parse(self, file_obj):
  101. """
  102. Read an OpenSSH config from the given file object.
  103. :param file_obj: a file-like object to read the config file from
  104. """
  105. # Start out w/ implicit/anonymous global host-like block to hold
  106. # anything not contained by an explicit one.
  107. context = {"host": ["*"], "config": {}}
  108. for line in file_obj:
  109. # Strip any leading or trailing whitespace from the line.
  110. # Refer to https://github.com/paramiko/paramiko/issues/499
  111. line = line.strip()
  112. # Skip blanks, comments
  113. if not line or line.startswith("#"):
  114. continue
  115. # Parse line into key, value
  116. match = re.match(self.SETTINGS_REGEX, line)
  117. if not match:
  118. raise ConfigParseError("Unparsable line {}".format(line))
  119. key = match.group(1).lower()
  120. value = match.group(2)
  121. # Host keyword triggers switch to new block/context
  122. if key in ("host", "match"):
  123. self._config.append(context)
  124. context = {"config": {}}
  125. if key == "host":
  126. # TODO 3.0: make these real objects or at least name this
  127. # "hosts" to acknowledge it's an iterable. (Doing so prior
  128. # to 3.0, despite it being a private API, feels bad -
  129. # surely such an old codebase has folks actually relying on
  130. # these keys.)
  131. context["host"] = self._get_hosts(value)
  132. else:
  133. context["matches"] = self._get_matches(value)
  134. # Special-case for noop ProxyCommands
  135. elif key == "proxycommand" and value.lower() == "none":
  136. # Store 'none' as None; prior to 3.x, it will get stripped out
  137. # at the end (for compatibility with issue #415). After 3.x, it
  138. # will simply not get stripped, leaving a nice explicit marker.
  139. context["config"][key] = None
  140. # All other keywords get stored, directly or via append
  141. else:
  142. if value.startswith('"') and value.endswith('"'):
  143. value = value[1:-1]
  144. # identityfile, localforward, remoteforward keys are special
  145. # cases, since they are allowed to be specified multiple times
  146. # and they should be tried in order of specification.
  147. if key in ["identityfile", "localforward", "remoteforward"]:
  148. if key in context["config"]:
  149. context["config"][key].append(value)
  150. else:
  151. context["config"][key] = [value]
  152. elif key not in context["config"]:
  153. context["config"][key] = value
  154. # Store last 'open' block and we're done
  155. self._config.append(context)
  156. def lookup(self, hostname):
  157. """
  158. Return a dict (`SSHConfigDict`) of config options for a given hostname.
  159. The host-matching rules of OpenSSH's ``ssh_config`` man page are used:
  160. For each parameter, the first obtained value will be used. The
  161. configuration files contain sections separated by ``Host`` and/or
  162. ``Match`` specifications, and that section is only applied for hosts
  163. which match the given patterns or keywords
  164. Since the first obtained value for each parameter is used, more host-
  165. specific declarations should be given near the beginning of the file,
  166. and general defaults at the end.
  167. The keys in the returned dict are all normalized to lowercase (look for
  168. ``"port"``, not ``"Port"``. The values are processed according to the
  169. rules for substitution variable expansion in ``ssh_config``.
  170. Finally, please see the docs for `SSHConfigDict` for deeper info on
  171. features such as optional type conversion methods, e.g.::
  172. conf = my_config.lookup('myhost')
  173. assert conf['passwordauthentication'] == 'yes'
  174. assert conf.as_bool('passwordauthentication') is True
  175. .. note::
  176. If there is no explicitly configured ``HostName`` value, it will be
  177. set to the being-looked-up hostname, which is as close as we can
  178. get to OpenSSH's behavior around that particular option.
  179. :param str hostname: the hostname to lookup
  180. .. versionchanged:: 2.5
  181. Returns `SSHConfigDict` objects instead of dict literals.
  182. .. versionchanged:: 2.7
  183. Added canonicalization support.
  184. .. versionchanged:: 2.7
  185. Added ``Match`` support.
  186. """
  187. # First pass
  188. options = self._lookup(hostname=hostname)
  189. # Inject HostName if it was not set (this used to be done incidentally
  190. # during tokenization, for some reason).
  191. if "hostname" not in options:
  192. options["hostname"] = hostname
  193. # Handle canonicalization
  194. canon = options.get("canonicalizehostname", None) in ("yes", "always")
  195. maxdots = int(options.get("canonicalizemaxdots", 1))
  196. if canon and hostname.count(".") <= maxdots:
  197. # NOTE: OpenSSH manpage does not explicitly state this, but its
  198. # implementation for CanonicalDomains is 'split on any whitespace'.
  199. domains = options["canonicaldomains"].split()
  200. hostname = self.canonicalize(hostname, options, domains)
  201. # Overwrite HostName again here (this is also what OpenSSH does)
  202. options["hostname"] = hostname
  203. options = self._lookup(hostname, options, canonical=True)
  204. return options
  205. def _lookup(self, hostname, options=None, canonical=False):
  206. # Init
  207. if options is None:
  208. options = SSHConfigDict()
  209. # Iterate all stanzas, applying any that match, in turn (so that things
  210. # like Match can reference currently understood state)
  211. for context in self._config:
  212. if not (
  213. self._pattern_matches(context.get("host", []), hostname)
  214. or self._does_match(
  215. context.get("matches", []), hostname, canonical, options
  216. )
  217. ):
  218. continue
  219. for key, value in context["config"].items():
  220. if key not in options:
  221. # Create a copy of the original value,
  222. # else it will reference the original list
  223. # in self._config and update that value too
  224. # when the extend() is being called.
  225. options[key] = value[:] if value is not None else value
  226. elif key == "identityfile":
  227. options[key].extend(
  228. x for x in value if x not in options[key]
  229. )
  230. # Expand variables in resulting values (besides 'Match exec' which was
  231. # already handled above)
  232. options = self._expand_variables(options, hostname)
  233. # TODO: remove in 3.x re #670
  234. if "proxycommand" in options and options["proxycommand"] is None:
  235. del options["proxycommand"]
  236. return options
  237. def canonicalize(self, hostname, options, domains):
  238. """
  239. Return canonicalized version of ``hostname``.
  240. :param str hostname: Target hostname.
  241. :param options: An `SSHConfigDict` from a previous lookup pass.
  242. :param domains: List of domains (e.g. ``["paramiko.org"]``).
  243. :returns: A canonicalized hostname if one was found, else ``None``.
  244. .. versionadded:: 2.7
  245. """
  246. found = False
  247. for domain in domains:
  248. candidate = "{}.{}".format(hostname, domain)
  249. family_specific = _addressfamily_host_lookup(candidate, options)
  250. if family_specific is not None:
  251. # TODO: would we want to dig deeper into other results? e.g. to
  252. # find something that satisfies PermittedCNAMEs when that is
  253. # implemented?
  254. found = family_specific[0]
  255. else:
  256. # TODO: what does ssh use here and is there a reason to use
  257. # that instead of gethostbyname?
  258. try:
  259. found = socket.gethostbyname(candidate)
  260. except socket.gaierror:
  261. pass
  262. if found:
  263. # TODO: follow CNAME (implied by found != candidate?) if
  264. # CanonicalizePermittedCNAMEs allows it
  265. return candidate
  266. # If we got here, it means canonicalization failed.
  267. # When CanonicalizeFallbackLocal is undefined or 'yes', we just spit
  268. # back the original hostname.
  269. if options.get("canonicalizefallbacklocal", "yes") == "yes":
  270. return hostname
  271. # And here, we failed AND fallback was set to a non-yes value, so we
  272. # need to get mad.
  273. raise CouldNotCanonicalize(hostname)
  274. def get_hostnames(self):
  275. """
  276. Return the set of literal hostnames defined in the SSH config (both
  277. explicit hostnames and wildcard entries).
  278. """
  279. hosts = set()
  280. for entry in self._config:
  281. hosts.update(entry["host"])
  282. return hosts
  283. def _pattern_matches(self, patterns, target):
  284. # Convenience auto-splitter if not already a list
  285. if hasattr(patterns, "split"):
  286. patterns = patterns.split(",")
  287. match = False
  288. for pattern in patterns:
  289. # Short-circuit if target matches a negated pattern
  290. if pattern.startswith("!") and fnmatch.fnmatch(
  291. target, pattern[1:]
  292. ):
  293. return False
  294. # Flag a match, but continue (in case of later negation) if regular
  295. # match occurs
  296. elif fnmatch.fnmatch(target, pattern):
  297. match = True
  298. return match
  299. # TODO 3.0: remove entirely (is now unused internally)
  300. def _allowed(self, hosts, hostname):
  301. return self._pattern_matches(hosts, hostname)
  302. def _does_match(self, match_list, target_hostname, canonical, options):
  303. matched = []
  304. candidates = match_list[:]
  305. local_username = getpass.getuser()
  306. while candidates:
  307. candidate = candidates.pop(0)
  308. passed = None
  309. # Obtain latest host/user value every loop, so later Match may
  310. # reference values assigned within a prior Match.
  311. configured_host = options.get("hostname", None)
  312. configured_user = options.get("user", None)
  313. type_, param = candidate["type"], candidate["param"]
  314. # Canonical is a hard pass/fail based on whether this is a
  315. # canonicalized re-lookup.
  316. if type_ == "canonical":
  317. if self._should_fail(canonical, candidate):
  318. return False
  319. # The parse step ensures we only see this by itself or after
  320. # canonical, so it's also an easy hard pass. (No negation here as
  321. # that would be uh, pretty weird?)
  322. elif type_ == "all":
  323. return True
  324. # From here, we are testing various non-hard criteria,
  325. # short-circuiting only on fail
  326. elif type_ == "host":
  327. hostval = configured_host or target_hostname
  328. passed = self._pattern_matches(param, hostval)
  329. elif type_ == "originalhost":
  330. passed = self._pattern_matches(param, target_hostname)
  331. elif type_ == "user":
  332. user = configured_user or local_username
  333. passed = self._pattern_matches(param, user)
  334. elif type_ == "localuser":
  335. passed = self._pattern_matches(param, local_username)
  336. elif type_ == "exec":
  337. exec_cmd = self._tokenize(
  338. options, target_hostname, "match-exec", param
  339. )
  340. # This is the laziest spot in which we can get mad about an
  341. # inability to import Invoke.
  342. if invoke is None:
  343. raise invoke_import_error
  344. # Like OpenSSH, we 'redirect' stdout but let stderr bubble up
  345. passed = invoke.run(exec_cmd, hide="stdout", warn=True).ok
  346. # Tackle any 'passed, but was negated' results from above
  347. if passed is not None and self._should_fail(passed, candidate):
  348. return False
  349. # Made it all the way here? Everything matched!
  350. matched.append(candidate)
  351. # Did anything match? (To be treated as bool, usually.)
  352. return matched
  353. def _should_fail(self, would_pass, candidate):
  354. return would_pass if candidate["negate"] else not would_pass
  355. def _tokenize(self, config, target_hostname, key, value):
  356. """
  357. Tokenize a string based on current config/hostname data.
  358. :param config: Current config data.
  359. :param target_hostname: Original target connection hostname.
  360. :param key: Config key being tokenized (used to filter token list).
  361. :param value: Config value being tokenized.
  362. :returns: The tokenized version of the input ``value`` string.
  363. """
  364. allowed_tokens = self._allowed_tokens(key)
  365. # Short-circuit if no tokenization possible
  366. if not allowed_tokens:
  367. return value
  368. # Obtain potentially configured hostname, for use with %h.
  369. # Special-case where we are tokenizing the hostname itself, to avoid
  370. # replacing %h with a %h-bearing value, etc.
  371. configured_hostname = target_hostname
  372. if key != "hostname":
  373. configured_hostname = config.get("hostname", configured_hostname)
  374. # Ditto the rest of the source values
  375. if "port" in config:
  376. port = config["port"]
  377. else:
  378. port = SSH_PORT
  379. user = getpass.getuser()
  380. if "user" in config:
  381. remoteuser = config["user"]
  382. else:
  383. remoteuser = user
  384. local_hostname = socket.gethostname().split(".")[0]
  385. local_fqdn = LazyFqdn(config, local_hostname)
  386. homedir = os.path.expanduser("~")
  387. tohash = local_hostname + target_hostname + repr(port) + remoteuser
  388. # The actual tokens!
  389. replacements = {
  390. # TODO: %%???
  391. "%C": sha1(tohash.encode()).hexdigest(),
  392. "%d": homedir,
  393. "%h": configured_hostname,
  394. # TODO: %i?
  395. "%L": local_hostname,
  396. "%l": local_fqdn,
  397. # also this is pseudo buggy when not in Match exec mode so document
  398. # that. also WHY is that the case?? don't we do all of this late?
  399. "%n": target_hostname,
  400. "%p": port,
  401. "%r": remoteuser,
  402. # TODO: %T? don't believe this is possible however
  403. "%u": user,
  404. "~": homedir,
  405. }
  406. # Do the thing with the stuff
  407. tokenized = value
  408. for find, replace in replacements.items():
  409. if find not in allowed_tokens:
  410. continue
  411. tokenized = tokenized.replace(find, str(replace))
  412. # TODO: log? eg that value -> tokenized
  413. return tokenized
  414. def _allowed_tokens(self, key):
  415. """
  416. Given config ``key``, return list of token strings to tokenize.
  417. .. note::
  418. This feels like it wants to eventually go away, but is used to
  419. preserve as-strict-as-possible compatibility with OpenSSH, which
  420. for whatever reason only applies some tokens to some config keys.
  421. """
  422. return self.TOKENS_BY_CONFIG_KEY.get(key, [])
  423. def _expand_variables(self, config, target_hostname):
  424. """
  425. Return a dict of config options with expanded substitutions
  426. for a given original & current target hostname.
  427. Please refer to :doc:`/api/config` for details.
  428. :param dict config: the currently parsed config
  429. :param str hostname: the hostname whose config is being looked up
  430. """
  431. for k in config:
  432. if config[k] is None:
  433. continue
  434. tokenizer = partial(self._tokenize, config, target_hostname, k)
  435. if isinstance(config[k], list):
  436. for i, value in enumerate(config[k]):
  437. config[k][i] = tokenizer(value)
  438. else:
  439. config[k] = tokenizer(config[k])
  440. return config
  441. def _get_hosts(self, host):
  442. """
  443. Return a list of host_names from host value.
  444. """
  445. try:
  446. return shlex.split(host)
  447. except ValueError:
  448. raise ConfigParseError("Unparsable host {}".format(host))
  449. def _get_matches(self, match):
  450. """
  451. Parse a specific Match config line into a list-of-dicts for its values.
  452. Performs some parse-time validation as well.
  453. """
  454. matches = []
  455. tokens = shlex.split(match)
  456. while tokens:
  457. match = {"type": None, "param": None, "negate": False}
  458. type_ = tokens.pop(0)
  459. # Handle per-keyword negation
  460. if type_.startswith("!"):
  461. match["negate"] = True
  462. type_ = type_[1:]
  463. match["type"] = type_
  464. # all/canonical have no params (everything else does)
  465. if type_ in ("all", "canonical"):
  466. matches.append(match)
  467. continue
  468. if not tokens:
  469. raise ConfigParseError(
  470. "Missing parameter to Match '{}' keyword".format(type_)
  471. )
  472. match["param"] = tokens.pop(0)
  473. matches.append(match)
  474. # Perform some (easier to do now than in the middle) validation that is
  475. # better handled here than at lookup time.
  476. keywords = [x["type"] for x in matches]
  477. if "all" in keywords:
  478. allowable = ("all", "canonical")
  479. ok, bad = (
  480. list(filter(lambda x: x in allowable, keywords)),
  481. list(filter(lambda x: x not in allowable, keywords)),
  482. )
  483. err = None
  484. if any(bad):
  485. err = "Match does not allow 'all' mixed with anything but 'canonical'" # noqa
  486. elif "canonical" in ok and ok.index("canonical") > ok.index("all"):
  487. err = "Match does not allow 'all' before 'canonical'"
  488. if err is not None:
  489. raise ConfigParseError(err)
  490. return matches
  491. def _addressfamily_host_lookup(hostname, options):
  492. """
  493. Try looking up ``hostname`` in an IPv4 or IPv6 specific manner.
  494. This is an odd duck due to needing use in two divergent use cases. It looks
  495. up ``AddressFamily`` in ``options`` and if it is ``inet`` or ``inet6``,
  496. this function uses `socket.getaddrinfo` to perform a family-specific
  497. lookup, returning the result if successful.
  498. In any other situation -- lookup failure, or ``AddressFamily`` being
  499. unspecified or ``any`` -- ``None`` is returned instead and the caller is
  500. expected to do something situation-appropriate like calling
  501. `socket.gethostbyname`.
  502. :param str hostname: Hostname to look up.
  503. :param options: `SSHConfigDict` instance w/ parsed options.
  504. :returns: ``getaddrinfo``-style tuples, or ``None``, depending.
  505. """
  506. address_family = options.get("addressfamily", "any").lower()
  507. if address_family == "any":
  508. return
  509. try:
  510. family = socket.AF_INET6
  511. if address_family == "inet":
  512. family = socket.AF_INET
  513. return socket.getaddrinfo(
  514. hostname,
  515. None,
  516. family,
  517. socket.SOCK_DGRAM,
  518. socket.IPPROTO_IP,
  519. socket.AI_CANONNAME,
  520. )
  521. except socket.gaierror:
  522. pass
  523. class LazyFqdn(object):
  524. """
  525. Returns the host's fqdn on request as string.
  526. """
  527. def __init__(self, config, host=None):
  528. self.fqdn = None
  529. self.config = config
  530. self.host = host
  531. def __str__(self):
  532. if self.fqdn is None:
  533. #
  534. # If the SSH config contains AddressFamily, use that when
  535. # determining the local host's FQDN. Using socket.getfqdn() from
  536. # the standard library is the most general solution, but can
  537. # result in noticeable delays on some platforms when IPv6 is
  538. # misconfigured or not available, as it calls getaddrinfo with no
  539. # address family specified, so both IPv4 and IPv6 are checked.
  540. #
  541. # Handle specific option
  542. fqdn = None
  543. results = _addressfamily_host_lookup(self.host, self.config)
  544. if results is not None:
  545. for res in results:
  546. af, socktype, proto, canonname, sa = res
  547. if canonname and "." in canonname:
  548. fqdn = canonname
  549. break
  550. # Handle 'any' / unspecified / lookup failure
  551. if fqdn is None:
  552. fqdn = socket.getfqdn()
  553. # Cache
  554. self.fqdn = fqdn
  555. return self.fqdn
  556. class SSHConfigDict(dict):
  557. """
  558. A dictionary wrapper/subclass for per-host configuration structures.
  559. This class introduces some usage niceties for consumers of `SSHConfig`,
  560. specifically around the issue of variable type conversions: normal value
  561. access yields strings, but there are now methods such as `as_bool` and
  562. `as_int` that yield casted values instead.
  563. For example, given the following ``ssh_config`` file snippet::
  564. Host foo.example.com
  565. PasswordAuthentication no
  566. Compression yes
  567. ServerAliveInterval 60
  568. the following code highlights how you can access the raw strings as well as
  569. usefully Python type-casted versions (recalling that keys are all
  570. normalized to lowercase first)::
  571. my_config = SSHConfig()
  572. my_config.parse(open('~/.ssh/config'))
  573. conf = my_config.lookup('foo.example.com')
  574. assert conf['passwordauthentication'] == 'no'
  575. assert conf.as_bool('passwordauthentication') is False
  576. assert conf['compression'] == 'yes'
  577. assert conf.as_bool('compression') is True
  578. assert conf['serveraliveinterval'] == '60'
  579. assert conf.as_int('serveraliveinterval') == 60
  580. .. versionadded:: 2.5
  581. """
  582. def __init__(self, *args, **kwargs):
  583. # Hey, guess what? Python 2's userdict is an old-style class!
  584. super(SSHConfigDict, self).__init__(*args, **kwargs)
  585. def as_bool(self, key):
  586. """
  587. Express given key's value as a boolean type.
  588. Typically, this is used for ``ssh_config``'s pseudo-boolean values
  589. which are either ``"yes"`` or ``"no"``. In such cases, ``"yes"`` yields
  590. ``True`` and any other value becomes ``False``.
  591. .. note::
  592. If (for whatever reason) the stored value is already boolean in
  593. nature, it's simply returned.
  594. .. versionadded:: 2.5
  595. """
  596. val = self[key]
  597. if isinstance(val, bool):
  598. return val
  599. return val.lower() == "yes"
  600. def as_int(self, key):
  601. """
  602. Express given key's value as an integer, if possible.
  603. This method will raise ``ValueError`` or similar if the value is not
  604. int-appropriate, same as the builtin `int` type.
  605. .. versionadded:: 2.5
  606. """
  607. return int(self[key])