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.

OpenSshConfigFile.java 28KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951
  1. /*
  2. * Copyright (C) 2008, 2017, Google Inc.
  3. * Copyright (C) 2017, 2018, Thomas Wolf <thomas.wolf@paranor.ch> and others
  4. *
  5. * This program and the accompanying materials are made available under the
  6. * terms of the Eclipse Distribution License v. 1.0 which is available at
  7. * https://www.eclipse.org/org/documents/edl-v10.php.
  8. *
  9. * SPDX-License-Identifier: BSD-3-Clause
  10. */
  11. package org.eclipse.jgit.internal.transport.ssh;
  12. import static java.nio.charset.StandardCharsets.UTF_8;
  13. import java.io.BufferedReader;
  14. import java.io.File;
  15. import java.io.IOException;
  16. import java.nio.file.Files;
  17. import java.time.Instant;
  18. import java.util.ArrayList;
  19. import java.util.Collections;
  20. import java.util.HashMap;
  21. import java.util.LinkedHashMap;
  22. import java.util.List;
  23. import java.util.Map;
  24. import java.util.Set;
  25. import java.util.TreeMap;
  26. import java.util.TreeSet;
  27. import org.eclipse.jgit.annotations.NonNull;
  28. import org.eclipse.jgit.errors.InvalidPatternException;
  29. import org.eclipse.jgit.fnmatch.FileNameMatcher;
  30. import org.eclipse.jgit.transport.SshConfigStore;
  31. import org.eclipse.jgit.transport.SshConstants;
  32. import org.eclipse.jgit.util.FS;
  33. import org.eclipse.jgit.util.StringUtils;
  34. import org.eclipse.jgit.util.SystemReader;
  35. /**
  36. * Fairly complete configuration parser for the openssh ~/.ssh/config file.
  37. * <p>
  38. * Both JSch 0.1.54 and Apache MINA sshd 2.1.0 have parsers for this, but both
  39. * are buggy. Therefore we implement our own parser to read an openssh
  40. * configuration file.
  41. * </p>
  42. * <p>
  43. * Limitations compared to the full openssh 7.5 parser:
  44. * </p>
  45. * <ul>
  46. * <li>This parser does not handle Match or Include keywords.
  47. * <li>This parser does not do host name canonicalization.
  48. * </ul>
  49. * <p>
  50. * Note that openssh's readconf.c is a validating parser; this parser does not
  51. * validate entries.
  52. * </p>
  53. * <p>
  54. * This config does %-substitutions for the following tokens:
  55. * </p>
  56. * <ul>
  57. * <li>%% - single %
  58. * <li>%C - short-hand for %l%h%p%r.
  59. * <li>%d - home directory path
  60. * <li>%h - remote host name
  61. * <li>%L - local host name without domain
  62. * <li>%l - FQDN of the local host
  63. * <li>%n - host name as specified in {@link #lookup(String, int, String)}
  64. * <li>%p - port number; if not given in {@link #lookup(String, int, String)}
  65. * replaced only if set in the config
  66. * <li>%r - remote user name; if not given in
  67. * {@link #lookup(String, int, String)} replaced only if set in the config
  68. * <li>%u - local user name
  69. * </ul>
  70. * <p>
  71. * %i is not handled; Java has no concept of a "user ID". %T is always replaced
  72. * by NONE.
  73. * </p>
  74. *
  75. * @see <a href="http://man.openbsd.org/OpenBSD-current/man5/ssh_config.5">man
  76. * ssh-config</a>
  77. */
  78. public class OpenSshConfigFile implements SshConfigStore {
  79. /**
  80. * "Host" name of the HostEntry for the default options before the first
  81. * host block in a config file.
  82. */
  83. private static final String DEFAULT_NAME = ""; //$NON-NLS-1$
  84. /** The user's home directory, as key files may be relative to here. */
  85. private final File home;
  86. /** The .ssh/config file we read and monitor for updates. */
  87. private final File configFile;
  88. /** User name of the user on the host OS. */
  89. private final String localUserName;
  90. /** Modification time of {@link #configFile} when it was last loaded. */
  91. private Instant lastModified;
  92. /**
  93. * Encapsulates entries read out of the configuration file, and a cache of
  94. * fully resolved entries created from that.
  95. */
  96. private static class State {
  97. // Keyed by pattern; if a "Host" line has multiple patterns, we generate
  98. // duplicate HostEntry objects
  99. Map<String, HostEntry> entries = new LinkedHashMap<>();
  100. // Keyed by user@hostname:port
  101. Map<String, HostEntry> hosts = new HashMap<>();
  102. @Override
  103. @SuppressWarnings("nls")
  104. public String toString() {
  105. return "State [entries=" + entries + ", hosts=" + hosts + "]";
  106. }
  107. }
  108. /** State read from the config file, plus the cache. */
  109. private State state;
  110. /**
  111. * Creates a new {@link OpenSshConfigFile} that will read the config from
  112. * file {@code config} use the given file {@code home} as "home" directory.
  113. *
  114. * @param home
  115. * user's home directory for the purpose of ~ replacement
  116. * @param config
  117. * file to load.
  118. * @param localUserName
  119. * user name of the current user on the local host OS
  120. */
  121. public OpenSshConfigFile(@NonNull File home, @NonNull File config,
  122. @NonNull String localUserName) {
  123. this.home = home;
  124. this.configFile = config;
  125. this.localUserName = localUserName;
  126. state = new State();
  127. }
  128. /**
  129. * Locate the configuration for a specific host request.
  130. *
  131. * @param hostName
  132. * the name the user has supplied to the SSH tool. This may be a
  133. * real host name, or it may just be a "Host" block in the
  134. * configuration file.
  135. * @param port
  136. * the user supplied; <= 0 if none
  137. * @param userName
  138. * the user supplied, may be {@code null} or empty if none given
  139. * @return the configuration for the requested name.
  140. */
  141. @Override
  142. @NonNull
  143. public HostEntry lookup(@NonNull String hostName, int port,
  144. String userName) {
  145. final State cache = refresh();
  146. String cacheKey = toCacheKey(hostName, port, userName);
  147. HostEntry h = cache.hosts.get(cacheKey);
  148. if (h != null) {
  149. return h;
  150. }
  151. HostEntry fullConfig = new HostEntry();
  152. // Initialize with default entries at the top of the file, before the
  153. // first Host block.
  154. fullConfig.merge(cache.entries.get(DEFAULT_NAME));
  155. for (Map.Entry<String, HostEntry> e : cache.entries.entrySet()) {
  156. String pattern = e.getKey();
  157. if (isHostMatch(pattern, hostName)) {
  158. fullConfig.merge(e.getValue());
  159. }
  160. }
  161. fullConfig.substitute(hostName, port, userName, localUserName, home);
  162. cache.hosts.put(cacheKey, fullConfig);
  163. return fullConfig;
  164. }
  165. @NonNull
  166. private String toCacheKey(@NonNull String hostName, int port,
  167. String userName) {
  168. String key = hostName;
  169. if (port > 0) {
  170. key = key + ':' + Integer.toString(port);
  171. }
  172. if (userName != null && !userName.isEmpty()) {
  173. key = userName + '@' + key;
  174. }
  175. return key;
  176. }
  177. private synchronized State refresh() {
  178. final Instant mtime = FS.DETECTED.lastModifiedInstant(configFile);
  179. if (!mtime.equals(lastModified)) {
  180. State newState = new State();
  181. try (BufferedReader br = Files
  182. .newBufferedReader(configFile.toPath(), UTF_8)) {
  183. newState.entries = parse(br);
  184. } catch (IOException | RuntimeException none) {
  185. // Ignore -- we'll set and return an empty state
  186. }
  187. lastModified = mtime;
  188. state = newState;
  189. }
  190. return state;
  191. }
  192. private Map<String, HostEntry> parse(BufferedReader reader)
  193. throws IOException {
  194. final Map<String, HostEntry> entries = new LinkedHashMap<>();
  195. final List<HostEntry> current = new ArrayList<>(4);
  196. String line;
  197. // The man page doesn't say so, but the openssh parser (readconf.c)
  198. // starts out in active mode and thus always applies any lines that
  199. // occur before the first host block. We gather those options in a
  200. // HostEntry for DEFAULT_NAME.
  201. HostEntry defaults = new HostEntry();
  202. current.add(defaults);
  203. entries.put(DEFAULT_NAME, defaults);
  204. while ((line = reader.readLine()) != null) {
  205. // OpenSsh ignores trailing comments on a line. Anything after the
  206. // first # on a line is trimmed away (yes, even if the hash is
  207. // inside quotes).
  208. //
  209. // See https://github.com/openssh/openssh-portable/commit/2bcbf679
  210. int i = line.indexOf('#');
  211. if (i >= 0) {
  212. line = line.substring(0, i);
  213. }
  214. line = line.trim();
  215. if (line.isEmpty()) {
  216. continue;
  217. }
  218. String[] parts = line.split("[ \t]*[= \t]", 2); //$NON-NLS-1$
  219. // Although the ssh-config man page doesn't say so, the openssh
  220. // parser does allow quoted keywords.
  221. String keyword = dequote(parts[0].trim());
  222. // man 5 ssh-config says lines had the format "keyword arguments",
  223. // with no indication that arguments were optional. However, let's
  224. // not crap out on missing arguments. See bug 444319.
  225. String argValue = parts.length > 1 ? parts[1].trim() : ""; //$NON-NLS-1$
  226. if (StringUtils.equalsIgnoreCase(SshConstants.HOST, keyword)) {
  227. current.clear();
  228. for (String name : parseList(argValue)) {
  229. if (name == null || name.isEmpty()) {
  230. // null should not occur, but better be safe than sorry.
  231. continue;
  232. }
  233. HostEntry c = entries.get(name);
  234. if (c == null) {
  235. c = new HostEntry();
  236. entries.put(name, c);
  237. }
  238. current.add(c);
  239. }
  240. continue;
  241. }
  242. if (current.isEmpty()) {
  243. // We received an option outside of a Host block. We
  244. // don't know who this should match against, so skip.
  245. continue;
  246. }
  247. if (HostEntry.isListKey(keyword)) {
  248. List<String> args = validate(keyword, parseList(argValue));
  249. for (HostEntry entry : current) {
  250. entry.setValue(keyword, args);
  251. }
  252. } else if (!argValue.isEmpty()) {
  253. argValue = validate(keyword, dequote(argValue));
  254. for (HostEntry entry : current) {
  255. entry.setValue(keyword, argValue);
  256. }
  257. }
  258. }
  259. return entries;
  260. }
  261. /**
  262. * Splits the argument into a list of whitespace-separated elements.
  263. * Elements containing whitespace must be quoted and will be de-quoted.
  264. *
  265. * @param argument
  266. * argument part of the configuration line as read from the
  267. * config file
  268. * @return a {@link List} of elements, possibly empty and possibly
  269. * containing empty elements, but not containing {@code null}
  270. */
  271. private List<String> parseList(String argument) {
  272. List<String> result = new ArrayList<>(4);
  273. int start = 0;
  274. int length = argument.length();
  275. while (start < length) {
  276. // Skip whitespace
  277. if (Character.isSpaceChar(argument.charAt(start))) {
  278. start++;
  279. continue;
  280. }
  281. if (argument.charAt(start) == '"') {
  282. int stop = argument.indexOf('"', ++start);
  283. if (stop < start) {
  284. // No closing double quote: skip
  285. break;
  286. }
  287. result.add(argument.substring(start, stop));
  288. start = stop + 1;
  289. } else {
  290. int stop = start + 1;
  291. while (stop < length
  292. && !Character.isSpaceChar(argument.charAt(stop))) {
  293. stop++;
  294. }
  295. result.add(argument.substring(start, stop));
  296. start = stop + 1;
  297. }
  298. }
  299. return result;
  300. }
  301. /**
  302. * Hook to perform validation on a single value, or to sanitize it. If this
  303. * throws an (unchecked) exception, parsing of the file is abandoned.
  304. *
  305. * @param key
  306. * of the entry
  307. * @param value
  308. * as read from the config file
  309. * @return the validated and possibly sanitized value
  310. */
  311. protected String validate(String key, String value) {
  312. if (String.CASE_INSENSITIVE_ORDER.compare(key,
  313. SshConstants.PREFERRED_AUTHENTICATIONS) == 0) {
  314. return stripWhitespace(value);
  315. }
  316. return value;
  317. }
  318. /**
  319. * Hook to perform validation on values, or to sanitize them. If this throws
  320. * an (unchecked) exception, parsing of the file is abandoned.
  321. *
  322. * @param key
  323. * of the entry
  324. * @param value
  325. * list of arguments as read from the config file
  326. * @return a {@link List} of values, possibly empty and possibly containing
  327. * empty elements, but not containing {@code null}
  328. */
  329. protected List<String> validate(String key, List<String> value) {
  330. return value;
  331. }
  332. private static boolean isHostMatch(String pattern, String name) {
  333. if (pattern.startsWith("!")) { //$NON-NLS-1$
  334. return !patternMatchesHost(pattern.substring(1), name);
  335. }
  336. return patternMatchesHost(pattern, name);
  337. }
  338. private static boolean patternMatchesHost(String pattern, String name) {
  339. if (pattern.indexOf('*') >= 0 || pattern.indexOf('?') >= 0) {
  340. final FileNameMatcher fn;
  341. try {
  342. fn = new FileNameMatcher(pattern, null);
  343. } catch (InvalidPatternException e) {
  344. return false;
  345. }
  346. fn.append(name);
  347. return fn.isMatch();
  348. }
  349. // Not a pattern but a full host name
  350. return pattern.equals(name);
  351. }
  352. private static String dequote(String value) {
  353. if (value.startsWith("\"") && value.endsWith("\"") //$NON-NLS-1$ //$NON-NLS-2$
  354. && value.length() > 1)
  355. return value.substring(1, value.length() - 1);
  356. return value;
  357. }
  358. private static String stripWhitespace(String value) {
  359. final StringBuilder b = new StringBuilder();
  360. for (int i = 0; i < value.length(); i++) {
  361. if (!Character.isSpaceChar(value.charAt(i)))
  362. b.append(value.charAt(i));
  363. }
  364. return b.toString();
  365. }
  366. private static File toFile(String path, File home) {
  367. if (path.startsWith("~/") || path.startsWith("~" + File.separator)) { //$NON-NLS-1$ //$NON-NLS-2$
  368. return new File(home, path.substring(2));
  369. }
  370. File ret = new File(path);
  371. if (ret.isAbsolute()) {
  372. return ret;
  373. }
  374. return new File(home, path);
  375. }
  376. /**
  377. * Converts a positive value into an {@code int}.
  378. *
  379. * @param value
  380. * to convert
  381. * @return the value, or -1 if it wasn't a positive integral value
  382. */
  383. public static int positive(String value) {
  384. if (value != null) {
  385. try {
  386. return Integer.parseUnsignedInt(value);
  387. } catch (NumberFormatException e) {
  388. // Ignore
  389. }
  390. }
  391. return -1;
  392. }
  393. /**
  394. * Converts a ssh config flag value (yes/true/on - no/false/off) into an
  395. * {@code boolean}.
  396. *
  397. * @param value
  398. * to convert
  399. * @return {@code true} if {@code value} is "yes", "on", or "true";
  400. * {@code false} otherwise
  401. */
  402. public static boolean flag(String value) {
  403. if (value == null) {
  404. return false;
  405. }
  406. return SshConstants.YES.equals(value) || SshConstants.ON.equals(value)
  407. || SshConstants.TRUE.equals(value);
  408. }
  409. /**
  410. * Retrieves the local user name as given in the constructor.
  411. *
  412. * @return the user name
  413. */
  414. public String getLocalUserName() {
  415. return localUserName;
  416. }
  417. /**
  418. * A host entry from the ssh config file. Any merging of global values and
  419. * of several matching host entries, %-substitutions, and ~ replacement have
  420. * all been done.
  421. */
  422. public static class HostEntry implements SshConfigStore.HostConfig {
  423. /**
  424. * Keys that can be specified multiple times, building up a list. (I.e.,
  425. * those are the keys that do not follow the general rule of "first
  426. * occurrence wins".)
  427. */
  428. private static final Set<String> MULTI_KEYS = new TreeSet<>(
  429. String.CASE_INSENSITIVE_ORDER);
  430. static {
  431. MULTI_KEYS.add(SshConstants.CERTIFICATE_FILE);
  432. MULTI_KEYS.add(SshConstants.IDENTITY_FILE);
  433. MULTI_KEYS.add(SshConstants.LOCAL_FORWARD);
  434. MULTI_KEYS.add(SshConstants.REMOTE_FORWARD);
  435. MULTI_KEYS.add(SshConstants.SEND_ENV);
  436. }
  437. /**
  438. * Keys that take a whitespace-separated list of elements as argument.
  439. * Because the dequote-handling is different, we must handle those in
  440. * the parser. There are a few other keys that take comma-separated
  441. * lists as arguments, but for the parser those are single arguments
  442. * that must be quoted if they contain whitespace, and taking them apart
  443. * is the responsibility of the user of those keys.
  444. */
  445. private static final Set<String> LIST_KEYS = new TreeSet<>(
  446. String.CASE_INSENSITIVE_ORDER);
  447. static {
  448. LIST_KEYS.add(SshConstants.CANONICAL_DOMAINS);
  449. LIST_KEYS.add(SshConstants.GLOBAL_KNOWN_HOSTS_FILE);
  450. LIST_KEYS.add(SshConstants.SEND_ENV);
  451. LIST_KEYS.add(SshConstants.USER_KNOWN_HOSTS_FILE);
  452. }
  453. /**
  454. * OpenSSH has renamed some config keys. This maps old names to new
  455. * names.
  456. */
  457. private static final Map<String, String> ALIASES = new TreeMap<>(
  458. String.CASE_INSENSITIVE_ORDER);
  459. static {
  460. // See https://github.com/openssh/openssh-portable/commit/ee9c0da80
  461. ALIASES.put("PubkeyAcceptedKeyTypes", //$NON-NLS-1$
  462. SshConstants.PUBKEY_ACCEPTED_ALGORITHMS);
  463. }
  464. private Map<String, String> options;
  465. private Map<String, List<String>> multiOptions;
  466. private Map<String, List<String>> listOptions;
  467. private static String toKey(String key) {
  468. String k = ALIASES.get(key);
  469. return k != null ? k : key;
  470. }
  471. /**
  472. * Retrieves the value of a single-valued key, or the first if the key
  473. * has multiple values. Keys are case-insensitive, so
  474. * {@code getValue("HostName") == getValue("HOSTNAME")}.
  475. *
  476. * @param key
  477. * to get the value of
  478. * @return the value, or {@code null} if none
  479. */
  480. @Override
  481. public String getValue(String key) {
  482. String k = toKey(key);
  483. String result = options != null ? options.get(k) : null;
  484. if (result == null) {
  485. // Let's be lenient and return at least the first value from
  486. // a list-valued or multi-valued key.
  487. List<String> values = listOptions != null ? listOptions.get(k)
  488. : null;
  489. if (values == null) {
  490. values = multiOptions != null ? multiOptions.get(k) : null;
  491. }
  492. if (values != null && !values.isEmpty()) {
  493. result = values.get(0);
  494. }
  495. }
  496. return result;
  497. }
  498. /**
  499. * Retrieves the values of a multi or list-valued key. Keys are
  500. * case-insensitive, so
  501. * {@code getValue("HostName") == getValue("HOSTNAME")}.
  502. *
  503. * @param key
  504. * to get the values of
  505. * @return a possibly empty list of values
  506. */
  507. @Override
  508. public List<String> getValues(String key) {
  509. String k = toKey(key);
  510. List<String> values = listOptions != null ? listOptions.get(k)
  511. : null;
  512. if (values == null) {
  513. values = multiOptions != null ? multiOptions.get(k) : null;
  514. }
  515. if (values == null || values.isEmpty()) {
  516. return new ArrayList<>();
  517. }
  518. return new ArrayList<>(values);
  519. }
  520. /**
  521. * Sets the value of a single-valued key if it not set yet, or adds a
  522. * value to a multi-valued key. If the value is {@code null}, the key is
  523. * removed altogether, whether it is single-, list-, or multi-valued.
  524. *
  525. * @param key
  526. * to modify
  527. * @param value
  528. * to set or add
  529. */
  530. public void setValue(String key, String value) {
  531. String k = toKey(key);
  532. if (value == null) {
  533. if (multiOptions != null) {
  534. multiOptions.remove(k);
  535. }
  536. if (listOptions != null) {
  537. listOptions.remove(k);
  538. }
  539. if (options != null) {
  540. options.remove(k);
  541. }
  542. return;
  543. }
  544. if (MULTI_KEYS.contains(k)) {
  545. if (multiOptions == null) {
  546. multiOptions = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
  547. }
  548. List<String> values = multiOptions.get(k);
  549. if (values == null) {
  550. values = new ArrayList<>(4);
  551. multiOptions.put(k, values);
  552. }
  553. values.add(value);
  554. } else {
  555. if (options == null) {
  556. options = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
  557. }
  558. if (!options.containsKey(k)) {
  559. options.put(k, value);
  560. }
  561. }
  562. }
  563. /**
  564. * Sets the values of a multi- or list-valued key.
  565. *
  566. * @param key
  567. * to set
  568. * @param values
  569. * a non-empty list of values
  570. */
  571. public void setValue(String key, List<String> values) {
  572. if (values.isEmpty()) {
  573. return;
  574. }
  575. String k = toKey(key);
  576. // Check multi-valued keys first; because of the replacement
  577. // strategy, they must take precedence over list-valued keys
  578. // which always follow the "first occurrence wins" strategy.
  579. //
  580. // Note that SendEnv is a multi-valued list-valued key. (It's
  581. // rather immaterial for JGit, though.)
  582. if (MULTI_KEYS.contains(k)) {
  583. if (multiOptions == null) {
  584. multiOptions = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
  585. }
  586. List<String> items = multiOptions.get(k);
  587. if (items == null) {
  588. items = new ArrayList<>(values);
  589. multiOptions.put(k, items);
  590. } else {
  591. items.addAll(values);
  592. }
  593. } else {
  594. if (listOptions == null) {
  595. listOptions = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
  596. }
  597. if (!listOptions.containsKey(k)) {
  598. listOptions.put(k, values);
  599. }
  600. }
  601. }
  602. /**
  603. * Does the key take a whitespace-separated list of values?
  604. *
  605. * @param key
  606. * to check
  607. * @return {@code true} if the key is a list-valued key.
  608. */
  609. public static boolean isListKey(String key) {
  610. return LIST_KEYS.contains(toKey(key));
  611. }
  612. void merge(HostEntry entry) {
  613. if (entry == null) {
  614. // Can occur if we could not read the config file
  615. return;
  616. }
  617. if (entry.options != null) {
  618. if (options == null) {
  619. options = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
  620. }
  621. for (Map.Entry<String, String> item : entry.options
  622. .entrySet()) {
  623. if (!options.containsKey(item.getKey())) {
  624. options.put(item.getKey(), item.getValue());
  625. }
  626. }
  627. }
  628. if (entry.listOptions != null) {
  629. if (listOptions == null) {
  630. listOptions = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
  631. }
  632. for (Map.Entry<String, List<String>> item : entry.listOptions
  633. .entrySet()) {
  634. if (!listOptions.containsKey(item.getKey())) {
  635. listOptions.put(item.getKey(), item.getValue());
  636. }
  637. }
  638. }
  639. if (entry.multiOptions != null) {
  640. if (multiOptions == null) {
  641. multiOptions = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
  642. }
  643. for (Map.Entry<String, List<String>> item : entry.multiOptions
  644. .entrySet()) {
  645. List<String> values = multiOptions.get(item.getKey());
  646. if (values == null) {
  647. values = new ArrayList<>(item.getValue());
  648. multiOptions.put(item.getKey(), values);
  649. } else {
  650. values.addAll(item.getValue());
  651. }
  652. }
  653. }
  654. }
  655. private List<String> substitute(List<String> values, String allowed,
  656. Replacer r, boolean withEnv) {
  657. List<String> result = new ArrayList<>(values.size());
  658. for (String value : values) {
  659. result.add(r.substitute(value, allowed, withEnv));
  660. }
  661. return result;
  662. }
  663. private List<String> replaceTilde(List<String> values, File home) {
  664. List<String> result = new ArrayList<>(values.size());
  665. for (String value : values) {
  666. result.add(toFile(value, home).getPath());
  667. }
  668. return result;
  669. }
  670. void substitute(String originalHostName, int port, String userName,
  671. String localUserName, File home) {
  672. int p = port >= 0 ? port : positive(getValue(SshConstants.PORT));
  673. if (p < 0) {
  674. p = SshConstants.SSH_DEFAULT_PORT;
  675. }
  676. String u = userName != null && !userName.isEmpty() ? userName
  677. : getValue(SshConstants.USER);
  678. if (u == null || u.isEmpty()) {
  679. u = localUserName;
  680. }
  681. Replacer r = new Replacer(originalHostName, p, u, localUserName,
  682. home);
  683. if (options != null) {
  684. // HOSTNAME first
  685. String hostName = options.get(SshConstants.HOST_NAME);
  686. if (hostName == null || hostName.isEmpty()) {
  687. options.put(SshConstants.HOST_NAME, originalHostName);
  688. } else {
  689. hostName = r.substitute(hostName, "h", false); //$NON-NLS-1$
  690. options.put(SshConstants.HOST_NAME, hostName);
  691. r.update('h', hostName);
  692. }
  693. }
  694. if (multiOptions != null) {
  695. List<String> values = multiOptions
  696. .get(SshConstants.IDENTITY_FILE);
  697. if (values != null) {
  698. values = substitute(values, "dhlru", r, true); //$NON-NLS-1$
  699. values = replaceTilde(values, home);
  700. multiOptions.put(SshConstants.IDENTITY_FILE, values);
  701. }
  702. values = multiOptions.get(SshConstants.CERTIFICATE_FILE);
  703. if (values != null) {
  704. values = substitute(values, "dhlru", r, true); //$NON-NLS-1$
  705. values = replaceTilde(values, home);
  706. multiOptions.put(SshConstants.CERTIFICATE_FILE, values);
  707. }
  708. }
  709. if (listOptions != null) {
  710. List<String> values = listOptions
  711. .get(SshConstants.USER_KNOWN_HOSTS_FILE);
  712. if (values != null) {
  713. values = replaceTilde(values, home);
  714. listOptions.put(SshConstants.USER_KNOWN_HOSTS_FILE, values);
  715. }
  716. }
  717. if (options != null) {
  718. // HOSTNAME already done above
  719. String value = options.get(SshConstants.IDENTITY_AGENT);
  720. if (value != null) {
  721. value = r.substitute(value, "dhlru", true); //$NON-NLS-1$
  722. value = toFile(value, home).getPath();
  723. options.put(SshConstants.IDENTITY_AGENT, value);
  724. }
  725. value = options.get(SshConstants.CONTROL_PATH);
  726. if (value != null) {
  727. value = r.substitute(value, "ChLlnpru", true); //$NON-NLS-1$
  728. value = toFile(value, home).getPath();
  729. options.put(SshConstants.CONTROL_PATH, value);
  730. }
  731. value = options.get(SshConstants.LOCAL_COMMAND);
  732. if (value != null) {
  733. value = r.substitute(value, "CdhlnprTu", false); //$NON-NLS-1$
  734. options.put(SshConstants.LOCAL_COMMAND, value);
  735. }
  736. value = options.get(SshConstants.REMOTE_COMMAND);
  737. if (value != null) {
  738. value = r.substitute(value, "Cdhlnpru", false); //$NON-NLS-1$
  739. options.put(SshConstants.REMOTE_COMMAND, value);
  740. }
  741. value = options.get(SshConstants.PROXY_COMMAND);
  742. if (value != null) {
  743. value = r.substitute(value, "hpr", false); //$NON-NLS-1$
  744. options.put(SshConstants.PROXY_COMMAND, value);
  745. }
  746. }
  747. // Match is not implemented and would need to be done elsewhere
  748. // anyway.
  749. }
  750. /**
  751. * Retrieves an unmodifiable map of all single-valued options, with
  752. * case-insensitive lookup by keys.
  753. *
  754. * @return all single-valued options
  755. */
  756. @Override
  757. @NonNull
  758. public Map<String, String> getOptions() {
  759. if (options == null) {
  760. return Collections.emptyMap();
  761. }
  762. return Collections.unmodifiableMap(options);
  763. }
  764. /**
  765. * Retrieves an unmodifiable map of all multi-valued options, with
  766. * case-insensitive lookup by keys.
  767. *
  768. * @return all multi-valued options
  769. */
  770. @Override
  771. @NonNull
  772. public Map<String, List<String>> getMultiValuedOptions() {
  773. if (listOptions == null && multiOptions == null) {
  774. return Collections.emptyMap();
  775. }
  776. Map<String, List<String>> allValues = new TreeMap<>(
  777. String.CASE_INSENSITIVE_ORDER);
  778. if (multiOptions != null) {
  779. allValues.putAll(multiOptions);
  780. }
  781. if (listOptions != null) {
  782. allValues.putAll(listOptions);
  783. }
  784. return Collections.unmodifiableMap(allValues);
  785. }
  786. @Override
  787. @SuppressWarnings("nls")
  788. public String toString() {
  789. return "HostEntry [options=" + options + ", multiOptions="
  790. + multiOptions + ", listOptions=" + listOptions + "]";
  791. }
  792. }
  793. private static class Replacer {
  794. private final Map<Character, String> replacements = new HashMap<>();
  795. public Replacer(String host, int port, String user,
  796. String localUserName, File home) {
  797. replacements.put(Character.valueOf('%'), "%"); //$NON-NLS-1$
  798. replacements.put(Character.valueOf('d'), home.getPath());
  799. replacements.put(Character.valueOf('h'), host);
  800. String localhost = SystemReader.getInstance().getHostname();
  801. replacements.put(Character.valueOf('l'), localhost);
  802. int period = localhost.indexOf('.');
  803. if (period > 0) {
  804. localhost = localhost.substring(0, period);
  805. }
  806. replacements.put(Character.valueOf('L'), localhost);
  807. replacements.put(Character.valueOf('n'), host);
  808. replacements.put(Character.valueOf('p'), Integer.toString(port));
  809. replacements.put(Character.valueOf('r'), user == null ? "" : user); //$NON-NLS-1$
  810. replacements.put(Character.valueOf('u'), localUserName);
  811. replacements.put(Character.valueOf('C'),
  812. substitute("%l%h%p%r", "hlpr", false)); //$NON-NLS-1$ //$NON-NLS-2$
  813. replacements.put(Character.valueOf('T'), "NONE"); //$NON-NLS-1$
  814. }
  815. public void update(char key, String value) {
  816. replacements.put(Character.valueOf(key), value);
  817. if ("lhpr".indexOf(key) >= 0) { //$NON-NLS-1$
  818. replacements.put(Character.valueOf('C'),
  819. substitute("%l%h%p%r", "hlpr", false)); //$NON-NLS-1$ //$NON-NLS-2$
  820. }
  821. }
  822. public String substitute(String input, String allowed,
  823. boolean withEnv) {
  824. if (input == null || input.length() <= 1
  825. || input.indexOf('%') < 0
  826. && (!withEnv || input.indexOf("${") < 0)) { //$NON-NLS-1$
  827. return input;
  828. }
  829. StringBuilder builder = new StringBuilder();
  830. int start = 0;
  831. int length = input.length();
  832. while (start < length) {
  833. char ch = input.charAt(start);
  834. switch (ch) {
  835. case '%':
  836. if (start + 1 >= length) {
  837. break;
  838. }
  839. String replacement = null;
  840. ch = input.charAt(start + 1);
  841. if (ch == '%' || allowed.indexOf(ch) >= 0) {
  842. replacement = replacements.get(Character.valueOf(ch));
  843. }
  844. if (replacement == null) {
  845. builder.append('%').append(ch);
  846. } else {
  847. builder.append(replacement);
  848. }
  849. start += 2;
  850. continue;
  851. case '$':
  852. if (!withEnv || start + 2 >= length) {
  853. break;
  854. }
  855. ch = input.charAt(start + 1);
  856. if (ch == '{') {
  857. int close = input.indexOf('}', start + 2);
  858. if (close > start + 2) {
  859. String variable = SystemReader.getInstance()
  860. .getenv(input.substring(start + 2, close));
  861. if (!StringUtils.isEmptyOrNull(variable)) {
  862. builder.append(variable);
  863. }
  864. start = close + 1;
  865. continue;
  866. }
  867. }
  868. ch = '$';
  869. break;
  870. default:
  871. break;
  872. }
  873. builder.append(ch);
  874. start++;
  875. }
  876. return builder.toString();
  877. }
  878. }
  879. /** {@inheritDoc} */
  880. @Override
  881. @SuppressWarnings("nls")
  882. public String toString() {
  883. return "OpenSshConfig [home=" + home + ", configFile=" + configFile
  884. + ", lastModified=" + lastModified + ", state=" + state + "]";
  885. }
  886. }