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

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