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

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