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.

OpenSshConfig.java 32KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041
  1. /*
  2. * Copyright (C) 2008, 2017, Google Inc.
  3. * and other copyright owners as documented in the project's IP log.
  4. *
  5. * This program and the accompanying materials are made available
  6. * under the terms of the Eclipse Distribution License v1.0 which
  7. * accompanies this distribution, is reproduced below, and is
  8. * available at http://www.eclipse.org/org/documents/edl-v10.php
  9. *
  10. * All rights reserved.
  11. *
  12. * Redistribution and use in source and binary forms, with or
  13. * without modification, are permitted provided that the following
  14. * conditions are met:
  15. *
  16. * - Redistributions of source code must retain the above copyright
  17. * notice, this list of conditions and the following disclaimer.
  18. *
  19. * - Redistributions in binary form must reproduce the above
  20. * copyright notice, this list of conditions and the following
  21. * disclaimer in the documentation and/or other materials provided
  22. * with the distribution.
  23. *
  24. * - Neither the name of the Eclipse Foundation, Inc. nor the
  25. * names of its contributors may be used to endorse or promote
  26. * products derived from this software without specific prior
  27. * written permission.
  28. *
  29. * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
  30. * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
  31. * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
  32. * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
  33. * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
  34. * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
  35. * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
  36. * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
  37. * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
  38. * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
  39. * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
  40. * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
  41. * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  42. */
  43. package org.eclipse.jgit.transport;
  44. import java.io.BufferedReader;
  45. import java.io.File;
  46. import java.io.FileInputStream;
  47. import java.io.IOException;
  48. import java.io.InputStream;
  49. import java.io.InputStreamReader;
  50. import java.security.AccessController;
  51. import java.security.PrivilegedAction;
  52. import java.util.ArrayList;
  53. import java.util.HashMap;
  54. import java.util.HashSet;
  55. import java.util.LinkedHashMap;
  56. import java.util.List;
  57. import java.util.Locale;
  58. import java.util.Map;
  59. import java.util.Set;
  60. import java.util.concurrent.TimeUnit;
  61. import org.eclipse.jgit.errors.InvalidPatternException;
  62. import org.eclipse.jgit.fnmatch.FileNameMatcher;
  63. import org.eclipse.jgit.lib.Constants;
  64. import org.eclipse.jgit.util.FS;
  65. import org.eclipse.jgit.util.StringUtils;
  66. import org.eclipse.jgit.util.SystemReader;
  67. import com.jcraft.jsch.ConfigRepository;
  68. /**
  69. * Fairly complete configuration parser for the OpenSSH ~/.ssh/config file.
  70. * <p>
  71. * JSch does have its own config file parser
  72. * {@link com.jcraft.jsch.OpenSSHConfig} since version 0.1.50, but it has a
  73. * number of problems:
  74. * <ul>
  75. * <li>it splits lines of the format "keyword = value" wrongly: you'd end up
  76. * with the value "= value".
  77. * <li>its "Host" keyword is not case insensitive.
  78. * <li>it doesn't handle quoted values.
  79. * <li>JSch's OpenSSHConfig doesn't monitor for config file changes.
  80. * </ul>
  81. * <p>
  82. * Therefore implement our own parser to read an OpenSSH configuration file. It
  83. * makes the critical options available to {@link SshSessionFactory} via
  84. * {@link Host} objects returned by {@link #lookup(String)}, and implements a
  85. * fully conforming {@link ConfigRepository} providing
  86. * {@link com.jcraft.jsch.ConfigRepository.Config}s via
  87. * {@link #getConfig(String)}.
  88. * </p>
  89. * <p>
  90. * Limitations compared to the full OpenSSH 7.5 parser:
  91. * </p>
  92. * <ul>
  93. * <li>This parser does not handle Match or Include keywords.
  94. * <li>This parser does not do host name canonicalization (Jsch ignores it
  95. * anyway).
  96. * </ul>
  97. * <p>
  98. * Note that OpenSSH's readconf.c is a validating parser; Jsch's
  99. * ConfigRepository OTOH treats all option values as plain strings, so any
  100. * validation must happen in Jsch outside of the parser. Thus this parser does
  101. * not validate option values, except for a few options when constructing a
  102. * {@link Host} object.
  103. * </p>
  104. * <p>
  105. * This config does %-substitutions for the following tokens:
  106. * </p>
  107. * <ul>
  108. * <li>%% - single %
  109. * <li>%C - short-hand for %l%h%p%r. See %p and %r below; the replacement may be
  110. * done partially only and may leave %p or %r or both unreplaced.
  111. * <li>%d - home directory path
  112. * <li>%h - remote host name
  113. * <li>%L - local host name without domain
  114. * <li>%l - FQDN of the local host
  115. * <li>%n - host name as specified in {@link #lookup(String)}
  116. * <li>%p - port number; replaced only if set in the config
  117. * <li>%r - remote user name; replaced only if set in the config
  118. * <li>%u - local user name
  119. * </ul>
  120. * <p>
  121. * If the config doesn't set the port or the remote user name, %p and %r remain
  122. * un-substituted. It's the caller's responsibility to replace them with values
  123. * obtained from the connection URI. %i is not handled; Java has no concept of a
  124. * "user ID".
  125. * </p>
  126. */
  127. public class OpenSshConfig implements ConfigRepository {
  128. /** IANA assigned port number for SSH. */
  129. static final int SSH_PORT = 22;
  130. /**
  131. * Obtain the user's configuration data.
  132. * <p>
  133. * The configuration file is always returned to the caller, even if no file
  134. * exists in the user's home directory at the time the call was made. Lookup
  135. * requests are cached and are automatically updated if the user modifies
  136. * the configuration file since the last time it was cached.
  137. *
  138. * @param fs
  139. * the file system abstraction which will be necessary to
  140. * perform certain file system operations.
  141. * @return a caching reader of the user's configuration file.
  142. */
  143. public static OpenSshConfig get(FS fs) {
  144. File home = fs.userHome();
  145. if (home == null)
  146. home = new File(".").getAbsoluteFile(); //$NON-NLS-1$
  147. final File config = new File(new File(home, ".ssh"), Constants.CONFIG); //$NON-NLS-1$
  148. final OpenSshConfig osc = new OpenSshConfig(home, config);
  149. osc.refresh();
  150. return osc;
  151. }
  152. /** The user's home directory, as key files may be relative to here. */
  153. private final File home;
  154. /** The .ssh/config file we read and monitor for updates. */
  155. private final File configFile;
  156. /** Modification time of {@link #configFile} when it was last loaded. */
  157. private long lastModified;
  158. /**
  159. * Encapsulates entries read out of the configuration file, and
  160. * {@link Host}s created from that.
  161. */
  162. private static class State {
  163. Map<String, HostEntry> entries = new LinkedHashMap<>();
  164. Map<String, Host> hosts = new HashMap<>();
  165. @Override
  166. @SuppressWarnings("nls")
  167. public String toString() {
  168. return "State [entries=" + entries + ", hosts=" + hosts + "]";
  169. }
  170. }
  171. /** State read from the config file, plus {@link Host}s created from it. */
  172. private State state;
  173. OpenSshConfig(final File h, final File cfg) {
  174. home = h;
  175. configFile = cfg;
  176. state = new State();
  177. }
  178. /**
  179. * Locate the configuration for a specific host request.
  180. *
  181. * @param hostName
  182. * the name the user has supplied to the SSH tool. This may be a
  183. * real host name, or it may just be a "Host" block in the
  184. * configuration file.
  185. * @return r configuration for the requested name. Never null.
  186. */
  187. public Host lookup(final String hostName) {
  188. final State cache = refresh();
  189. Host h = cache.hosts.get(hostName);
  190. if (h != null) {
  191. return h;
  192. }
  193. HostEntry fullConfig = new HostEntry();
  194. // Initialize with default entries at the top of the file, before the
  195. // first Host block.
  196. fullConfig.merge(cache.entries.get(HostEntry.DEFAULT_NAME));
  197. for (final Map.Entry<String, HostEntry> e : cache.entries.entrySet()) {
  198. String key = e.getKey();
  199. if (isHostMatch(key, hostName)) {
  200. fullConfig.merge(e.getValue());
  201. }
  202. }
  203. fullConfig.substitute(hostName, home);
  204. h = new Host(fullConfig, hostName, home);
  205. cache.hosts.put(hostName, h);
  206. return h;
  207. }
  208. private synchronized State refresh() {
  209. final long mtime = configFile.lastModified();
  210. if (mtime != lastModified) {
  211. State newState = new State();
  212. try (FileInputStream in = new FileInputStream(configFile)) {
  213. newState.entries = parse(in);
  214. } catch (IOException none) {
  215. // Ignore -- we'll set and return an empty state
  216. }
  217. lastModified = mtime;
  218. state = newState;
  219. }
  220. return state;
  221. }
  222. private Map<String, HostEntry> parse(final InputStream in)
  223. throws IOException {
  224. final Map<String, HostEntry> m = new LinkedHashMap<>();
  225. final BufferedReader br = new BufferedReader(new InputStreamReader(in));
  226. final List<HostEntry> current = new ArrayList<>(4);
  227. String line;
  228. // The man page doesn't say so, but the OpenSSH parser (readconf.c)
  229. // starts out in active mode and thus always applies any lines that
  230. // occur before the first host block. We gather those options in a
  231. // HostEntry for DEFAULT_NAME.
  232. HostEntry defaults = new HostEntry();
  233. current.add(defaults);
  234. m.put(HostEntry.DEFAULT_NAME, defaults);
  235. while ((line = br.readLine()) != null) {
  236. line = line.trim();
  237. if (line.isEmpty() || line.startsWith("#")) { //$NON-NLS-1$
  238. continue;
  239. }
  240. String[] parts = line.split("[ \t]*[= \t]", 2); //$NON-NLS-1$
  241. // Although the ssh-config man page doesn't say so, the OpenSSH
  242. // parser does allow quoted keywords.
  243. String keyword = dequote(parts[0].trim());
  244. // man 5 ssh-config says lines had the format "keyword arguments",
  245. // with no indication that arguments were optional. However, let's
  246. // not crap out on missing arguments. See bug 444319.
  247. String argValue = parts.length > 1 ? parts[1].trim() : ""; //$NON-NLS-1$
  248. if (StringUtils.equalsIgnoreCase("Host", keyword)) { //$NON-NLS-1$
  249. current.clear();
  250. for (String name : HostEntry.parseList(argValue)) {
  251. if (name == null || name.isEmpty()) {
  252. // null should not occur, but better be safe than sorry.
  253. continue;
  254. }
  255. HostEntry c = m.get(name);
  256. if (c == null) {
  257. c = new HostEntry();
  258. m.put(name, c);
  259. }
  260. current.add(c);
  261. }
  262. continue;
  263. }
  264. if (current.isEmpty()) {
  265. // We received an option outside of a Host block. We
  266. // don't know who this should match against, so skip.
  267. continue;
  268. }
  269. if (HostEntry.isListKey(keyword)) {
  270. List<String> args = HostEntry.parseList(argValue);
  271. for (HostEntry entry : current) {
  272. entry.setValue(keyword, args);
  273. }
  274. } else if (!argValue.isEmpty()) {
  275. argValue = dequote(argValue);
  276. for (HostEntry entry : current) {
  277. entry.setValue(keyword, argValue);
  278. }
  279. }
  280. }
  281. return m;
  282. }
  283. private static boolean isHostMatch(final String pattern,
  284. final String name) {
  285. if (pattern.startsWith("!")) { //$NON-NLS-1$
  286. return !patternMatchesHost(pattern.substring(1), name);
  287. } else {
  288. return patternMatchesHost(pattern, name);
  289. }
  290. }
  291. private static boolean patternMatchesHost(final String pattern,
  292. final String name) {
  293. if (pattern.indexOf('*') >= 0 || pattern.indexOf('?') >= 0) {
  294. final FileNameMatcher fn;
  295. try {
  296. fn = new FileNameMatcher(pattern, null);
  297. } catch (InvalidPatternException e) {
  298. return false;
  299. }
  300. fn.append(name);
  301. return fn.isMatch();
  302. } else {
  303. // Not a pattern but a full host name
  304. return pattern.equals(name);
  305. }
  306. }
  307. private static String dequote(final String value) {
  308. if (value.startsWith("\"") && value.endsWith("\"") //$NON-NLS-1$ //$NON-NLS-2$
  309. && value.length() > 1)
  310. return value.substring(1, value.length() - 1);
  311. return value;
  312. }
  313. private static String nows(final String value) {
  314. final StringBuilder b = new StringBuilder();
  315. for (int i = 0; i < value.length(); i++) {
  316. if (!Character.isSpaceChar(value.charAt(i)))
  317. b.append(value.charAt(i));
  318. }
  319. return b.toString();
  320. }
  321. private static Boolean yesno(final String value) {
  322. if (StringUtils.equalsIgnoreCase("yes", value)) //$NON-NLS-1$
  323. return Boolean.TRUE;
  324. return Boolean.FALSE;
  325. }
  326. private static File toFile(String path, File home) {
  327. if (path.startsWith("~/")) { //$NON-NLS-1$
  328. return new File(home, path.substring(2));
  329. }
  330. File ret = new File(path);
  331. if (ret.isAbsolute()) {
  332. return ret;
  333. }
  334. return new File(home, path);
  335. }
  336. private static int positive(final String value) {
  337. if (value != null) {
  338. try {
  339. return Integer.parseUnsignedInt(value);
  340. } catch (NumberFormatException e) {
  341. // Ignore
  342. }
  343. }
  344. return -1;
  345. }
  346. static String userName() {
  347. return AccessController.doPrivileged(new PrivilegedAction<String>() {
  348. @Override
  349. public String run() {
  350. return SystemReader.getInstance()
  351. .getProperty(Constants.OS_USER_NAME_KEY);
  352. }
  353. });
  354. }
  355. private static class HostEntry implements ConfigRepository.Config {
  356. /**
  357. * "Host name" of the HostEntry for the default options before the first
  358. * host block in a config file.
  359. */
  360. public static final String DEFAULT_NAME = ""; //$NON-NLS-1$
  361. // See com.jcraft.jsch.OpenSSHConfig. Translates some command-line keys
  362. // to ssh-config keys.
  363. private static final Map<String, String> KEY_MAP = new HashMap<>();
  364. static {
  365. KEY_MAP.put("kex", "KexAlgorithms"); //$NON-NLS-1$//$NON-NLS-2$
  366. KEY_MAP.put("server_host_key", "HostKeyAlgorithms"); //$NON-NLS-1$ //$NON-NLS-2$
  367. KEY_MAP.put("cipher.c2s", "Ciphers"); //$NON-NLS-1$ //$NON-NLS-2$
  368. KEY_MAP.put("cipher.s2c", "Ciphers"); //$NON-NLS-1$ //$NON-NLS-2$
  369. KEY_MAP.put("mac.c2s", "Macs"); //$NON-NLS-1$ //$NON-NLS-2$
  370. KEY_MAP.put("mac.s2c", "Macs"); //$NON-NLS-1$ //$NON-NLS-2$
  371. KEY_MAP.put("compression.s2c", "Compression"); //$NON-NLS-1$ //$NON-NLS-2$
  372. KEY_MAP.put("compression.c2s", "Compression"); //$NON-NLS-1$ //$NON-NLS-2$
  373. KEY_MAP.put("compression_level", "CompressionLevel"); //$NON-NLS-1$ //$NON-NLS-2$
  374. KEY_MAP.put("MaxAuthTries", "NumberOfPasswordPrompts"); //$NON-NLS-1$ //$NON-NLS-2$
  375. }
  376. /**
  377. * Keys that can be specified multiple times, building up a list. (I.e.,
  378. * those are the keys that do not follow the general rule of "first
  379. * occurrence wins".)
  380. */
  381. private static final Set<String> MULTI_KEYS = new HashSet<>();
  382. static {
  383. MULTI_KEYS.add("CERTIFICATEFILE"); //$NON-NLS-1$
  384. MULTI_KEYS.add("IDENTITYFILE"); //$NON-NLS-1$
  385. MULTI_KEYS.add("LOCALFORWARD"); //$NON-NLS-1$
  386. MULTI_KEYS.add("REMOTEFORWARD"); //$NON-NLS-1$
  387. MULTI_KEYS.add("SENDENV"); //$NON-NLS-1$
  388. }
  389. /**
  390. * Keys that take a whitespace-separated list of elements as argument.
  391. * Because the dequote-handling is different, we must handle those in
  392. * the parser. There are a few other keys that take comma-separated
  393. * lists as arguments, but for the parser those are single arguments
  394. * that must be quoted if they contain whitespace, and taking them apart
  395. * is the responsibility of the user of those keys.
  396. */
  397. private static final Set<String> LIST_KEYS = new HashSet<>();
  398. static {
  399. LIST_KEYS.add("CANONICALDOMAINS"); //$NON-NLS-1$
  400. LIST_KEYS.add("GLOBALKNOWNHOSTSFILE"); //$NON-NLS-1$
  401. LIST_KEYS.add("SENDENV"); //$NON-NLS-1$
  402. LIST_KEYS.add("USERKNOWNHOSTSFILE"); //$NON-NLS-1$
  403. }
  404. private Map<String, String> options;
  405. private Map<String, List<String>> multiOptions;
  406. private Map<String, List<String>> listOptions;
  407. @Override
  408. public String getHostname() {
  409. return getValue("HOSTNAME"); //$NON-NLS-1$
  410. }
  411. @Override
  412. public String getUser() {
  413. return getValue("USER"); //$NON-NLS-1$
  414. }
  415. @Override
  416. public int getPort() {
  417. return positive(getValue("PORT")); //$NON-NLS-1$
  418. }
  419. private static String mapKey(String key) {
  420. String k = KEY_MAP.get(key);
  421. if (k == null) {
  422. k = key;
  423. }
  424. return k.toUpperCase(Locale.ROOT);
  425. }
  426. private String findValue(String key) {
  427. String k = mapKey(key);
  428. String result = options != null ? options.get(k) : null;
  429. if (result == null) {
  430. // Also check the list and multi options. Modern OpenSSH treats
  431. // UserKnownHostsFile and GlobalKnownHostsFile as list-valued,
  432. // and so does this parser. Jsch 0.1.54 in general doesn't know
  433. // about list-valued options (it _does_ know multi-valued
  434. // options, though), and will ask for a single value for such
  435. // options.
  436. //
  437. // Let's be lenient and return at least the first value from
  438. // a list-valued or multi-valued key for which Jsch asks for a
  439. // single value.
  440. List<String> values = listOptions != null ? listOptions.get(k)
  441. : null;
  442. if (values == null) {
  443. values = multiOptions != null ? multiOptions.get(k) : null;
  444. }
  445. if (values != null && !values.isEmpty()) {
  446. result = values.get(0);
  447. }
  448. }
  449. return result;
  450. }
  451. @Override
  452. public String getValue(String key) {
  453. // See com.jcraft.jsch.OpenSSHConfig.MyConfig.getValue() for this
  454. // special case.
  455. if (key.equals("compression.s2c") //$NON-NLS-1$
  456. || key.equals("compression.c2s")) { //$NON-NLS-1$
  457. String foo = findValue(key);
  458. if (foo == null || foo.equals("no")) { //$NON-NLS-1$
  459. return "none,zlib@openssh.com,zlib"; //$NON-NLS-1$
  460. }
  461. return "zlib@openssh.com,zlib,none"; //$NON-NLS-1$
  462. }
  463. return findValue(key);
  464. }
  465. @Override
  466. public String[] getValues(String key) {
  467. String k = mapKey(key);
  468. List<String> values = listOptions != null ? listOptions.get(k)
  469. : null;
  470. if (values == null) {
  471. values = multiOptions != null ? multiOptions.get(k) : null;
  472. }
  473. if (values == null || values.isEmpty()) {
  474. return new String[0];
  475. }
  476. return values.toArray(new String[values.size()]);
  477. }
  478. public void setValue(String key, String value) {
  479. String k = key.toUpperCase(Locale.ROOT);
  480. if (MULTI_KEYS.contains(k)) {
  481. if (multiOptions == null) {
  482. multiOptions = new HashMap<>();
  483. }
  484. List<String> values = multiOptions.get(k);
  485. if (values == null) {
  486. values = new ArrayList<>(4);
  487. multiOptions.put(k, values);
  488. }
  489. values.add(value);
  490. } else {
  491. if (options == null) {
  492. options = new HashMap<>();
  493. }
  494. if (!options.containsKey(k)) {
  495. options.put(k, value);
  496. }
  497. }
  498. }
  499. public void setValue(String key, List<String> values) {
  500. if (values.isEmpty()) {
  501. // Can occur only on a missing argument: ignore.
  502. return;
  503. }
  504. String k = key.toUpperCase(Locale.ROOT);
  505. // Check multi-valued keys first; because of the replacement
  506. // strategy, they must take precedence over list-valued keys
  507. // which always follow the "first occurrence wins" strategy.
  508. //
  509. // Note that SendEnv is a multi-valued list-valued key. (It's
  510. // rather immaterial for JGit, though.)
  511. if (MULTI_KEYS.contains(k)) {
  512. if (multiOptions == null) {
  513. multiOptions = new HashMap<>(2 * MULTI_KEYS.size());
  514. }
  515. List<String> items = multiOptions.get(k);
  516. if (items == null) {
  517. items = new ArrayList<>(values);
  518. multiOptions.put(k, items);
  519. } else {
  520. items.addAll(values);
  521. }
  522. } else {
  523. if (listOptions == null) {
  524. listOptions = new HashMap<>(2 * LIST_KEYS.size());
  525. }
  526. if (!listOptions.containsKey(k)) {
  527. listOptions.put(k, values);
  528. }
  529. }
  530. }
  531. public static boolean isListKey(String key) {
  532. return LIST_KEYS.contains(key.toUpperCase(Locale.ROOT));
  533. }
  534. /**
  535. * Splits the argument into a list of whitespace-separated elements.
  536. * Elements containing whitespace must be quoted and will be de-quoted.
  537. *
  538. * @param argument
  539. * argument part of the configuration line as read from the
  540. * config file
  541. * @return a {@link List} of elements, possibly empty and possibly
  542. * containing empty elements
  543. */
  544. public static List<String> parseList(String argument) {
  545. List<String> result = new ArrayList<>(4);
  546. int start = 0;
  547. int length = argument.length();
  548. while (start < length) {
  549. // Skip whitespace
  550. if (Character.isSpaceChar(argument.charAt(start))) {
  551. start++;
  552. continue;
  553. }
  554. if (argument.charAt(start) == '"') {
  555. int stop = argument.indexOf('"', ++start);
  556. if (stop < start) {
  557. // No closing double quote: skip
  558. break;
  559. }
  560. result.add(argument.substring(start, stop));
  561. start = stop + 1;
  562. } else {
  563. int stop = start + 1;
  564. while (stop < length
  565. && !Character.isSpaceChar(argument.charAt(stop))) {
  566. stop++;
  567. }
  568. result.add(argument.substring(start, stop));
  569. start = stop + 1;
  570. }
  571. }
  572. return result;
  573. }
  574. protected void merge(HostEntry entry) {
  575. if (entry == null) {
  576. // Can occur if we could not read the config file
  577. return;
  578. }
  579. if (entry.options != null) {
  580. if (options == null) {
  581. options = new HashMap<>();
  582. }
  583. for (Map.Entry<String, String> item : entry.options
  584. .entrySet()) {
  585. if (!options.containsKey(item.getKey())) {
  586. options.put(item.getKey(), item.getValue());
  587. }
  588. }
  589. }
  590. if (entry.listOptions != null) {
  591. if (listOptions == null) {
  592. listOptions = new HashMap<>(2 * LIST_KEYS.size());
  593. }
  594. for (Map.Entry<String, List<String>> item : entry.listOptions
  595. .entrySet()) {
  596. if (!listOptions.containsKey(item.getKey())) {
  597. listOptions.put(item.getKey(), item.getValue());
  598. }
  599. }
  600. }
  601. if (entry.multiOptions != null) {
  602. if (multiOptions == null) {
  603. multiOptions = new HashMap<>(2 * MULTI_KEYS.size());
  604. }
  605. for (Map.Entry<String, List<String>> item : entry.multiOptions
  606. .entrySet()) {
  607. List<String> values = multiOptions.get(item.getKey());
  608. if (values == null) {
  609. values = new ArrayList<>(item.getValue());
  610. multiOptions.put(item.getKey(), values);
  611. } else {
  612. values.addAll(item.getValue());
  613. }
  614. }
  615. }
  616. }
  617. private class Replacer {
  618. private final Map<Character, String> replacements = new HashMap<>();
  619. public Replacer(String originalHostName, File home) {
  620. replacements.put(Character.valueOf('%'), "%"); //$NON-NLS-1$
  621. replacements.put(Character.valueOf('d'), home.getPath());
  622. // Needs special treatment...
  623. String host = getValue("HOSTNAME"); //$NON-NLS-1$
  624. replacements.put(Character.valueOf('h'), originalHostName);
  625. if (host != null && host.indexOf('%') >= 0) {
  626. host = substitute(host, "h"); //$NON-NLS-1$
  627. options.put("HOSTNAME", host); //$NON-NLS-1$
  628. }
  629. if (host != null) {
  630. replacements.put(Character.valueOf('h'), host);
  631. }
  632. String localhost = SystemReader.getInstance().getHostname();
  633. replacements.put(Character.valueOf('l'), localhost);
  634. int period = localhost.indexOf('.');
  635. if (period > 0) {
  636. localhost = localhost.substring(0, period);
  637. }
  638. replacements.put(Character.valueOf('L'), localhost);
  639. replacements.put(Character.valueOf('n'), originalHostName);
  640. replacements.put(Character.valueOf('p'), getValue("PORT")); //$NON-NLS-1$
  641. replacements.put(Character.valueOf('r'), getValue("USER")); //$NON-NLS-1$
  642. replacements.put(Character.valueOf('u'), userName());
  643. replacements.put(Character.valueOf('C'),
  644. substitute("%l%h%p%r", "hlpr")); //$NON-NLS-1$ //$NON-NLS-2$
  645. }
  646. public String substitute(String input, String allowed) {
  647. if (input == null || input.length() <= 1
  648. || input.indexOf('%') < 0) {
  649. return input;
  650. }
  651. StringBuilder builder = new StringBuilder();
  652. int start = 0;
  653. int length = input.length();
  654. while (start < length) {
  655. int percent = input.indexOf('%', start);
  656. if (percent < 0 || percent + 1 >= length) {
  657. builder.append(input.substring(start));
  658. break;
  659. }
  660. String replacement = null;
  661. char ch = input.charAt(percent + 1);
  662. if (ch == '%' || allowed.indexOf(ch) >= 0) {
  663. replacement = replacements.get(Character.valueOf(ch));
  664. }
  665. if (replacement == null) {
  666. builder.append(input.substring(start, percent + 2));
  667. } else {
  668. builder.append(input.substring(start, percent))
  669. .append(replacement);
  670. }
  671. start = percent + 2;
  672. }
  673. return builder.toString();
  674. }
  675. }
  676. private List<String> substitute(List<String> values, String allowed,
  677. Replacer r) {
  678. List<String> result = new ArrayList<>(values.size());
  679. for (String value : values) {
  680. result.add(r.substitute(value, allowed));
  681. }
  682. return result;
  683. }
  684. private List<String> replaceTilde(List<String> values, File home) {
  685. List<String> result = new ArrayList<>(values.size());
  686. for (String value : values) {
  687. result.add(toFile(value, home).getPath());
  688. }
  689. return result;
  690. }
  691. protected void substitute(String originalHostName, File home) {
  692. Replacer r = new Replacer(originalHostName, home);
  693. if (multiOptions != null) {
  694. List<String> values = multiOptions.get("IDENTITYFILE"); //$NON-NLS-1$
  695. if (values != null) {
  696. values = substitute(values, "dhlru", r); //$NON-NLS-1$
  697. values = replaceTilde(values, home);
  698. multiOptions.put("IDENTITYFILE", values); //$NON-NLS-1$
  699. }
  700. values = multiOptions.get("CERTIFICATEFILE"); //$NON-NLS-1$
  701. if (values != null) {
  702. values = substitute(values, "dhlru", r); //$NON-NLS-1$
  703. values = replaceTilde(values, home);
  704. multiOptions.put("CERTIFICATEFILE", values); //$NON-NLS-1$
  705. }
  706. }
  707. if (listOptions != null) {
  708. List<String> values = listOptions.get("GLOBALKNOWNHOSTSFILE"); //$NON-NLS-1$
  709. if (values != null) {
  710. values = replaceTilde(values, home);
  711. listOptions.put("GLOBALKNOWNHOSTSFILE", values); //$NON-NLS-1$
  712. }
  713. values = listOptions.get("USERKNOWNHOSTSFILE"); //$NON-NLS-1$
  714. if (values != null) {
  715. values = replaceTilde(values, home);
  716. listOptions.put("USERKNOWNHOSTSFILE", values); //$NON-NLS-1$
  717. }
  718. }
  719. if (options != null) {
  720. // HOSTNAME already done in Replacer constructor
  721. String value = options.get("IDENTITYAGENT"); //$NON-NLS-1$
  722. if (value != null) {
  723. value = r.substitute(value, "dhlru"); //$NON-NLS-1$
  724. value = toFile(value, home).getPath();
  725. options.put("IDENTITYAGENT", value); //$NON-NLS-1$
  726. }
  727. }
  728. // Match is not implemented and would need to be done elsewhere
  729. // anyway. ControlPath, LocalCommand, ProxyCommand, and
  730. // RemoteCommand are not used by Jsch.
  731. }
  732. @Override
  733. @SuppressWarnings("nls")
  734. public String toString() {
  735. return "HostEntry [options=" + options + ", multiOptions="
  736. + multiOptions + ", listOptions=" + listOptions + "]";
  737. }
  738. }
  739. /**
  740. * Configuration of one "Host" block in the configuration file.
  741. * <p>
  742. * If returned from {@link OpenSshConfig#lookup(String)} some or all of the
  743. * properties may not be populated. The properties which are not populated
  744. * should be defaulted by the caller.
  745. * <p>
  746. * When returned from {@link OpenSshConfig#lookup(String)} any wildcard
  747. * entries which appear later in the configuration file will have been
  748. * already merged into this block.
  749. */
  750. public static class Host {
  751. String hostName;
  752. int port;
  753. File identityFile;
  754. String user;
  755. String preferredAuthentications;
  756. Boolean batchMode;
  757. String strictHostKeyChecking;
  758. int connectionAttempts;
  759. private Config config;
  760. /**
  761. * Creates a new uninitialized {@link Host}.
  762. */
  763. public Host() {
  764. // For API backwards compatibility with pre-4.9 JGit
  765. }
  766. Host(Config config, String hostName, File homeDir) {
  767. this.config = config;
  768. complete(hostName, homeDir);
  769. }
  770. /**
  771. * @return the value StrictHostKeyChecking property, the valid values
  772. * are "yes" (unknown hosts are not accepted), "no" (unknown
  773. * hosts are always accepted), and "ask" (user should be asked
  774. * before accepting the host)
  775. */
  776. public String getStrictHostKeyChecking() {
  777. return strictHostKeyChecking;
  778. }
  779. /**
  780. * @return the real IP address or host name to connect to; never null.
  781. */
  782. public String getHostName() {
  783. return hostName;
  784. }
  785. /**
  786. * @return the real port number to connect to; never 0.
  787. */
  788. public int getPort() {
  789. return port;
  790. }
  791. /**
  792. * @return path of the private key file to use for authentication; null
  793. * if the caller should use default authentication strategies.
  794. */
  795. public File getIdentityFile() {
  796. return identityFile;
  797. }
  798. /**
  799. * @return the real user name to connect as; never null.
  800. */
  801. public String getUser() {
  802. return user;
  803. }
  804. /**
  805. * @return the preferred authentication methods, separated by commas if
  806. * more than one authentication method is preferred.
  807. */
  808. public String getPreferredAuthentications() {
  809. return preferredAuthentications;
  810. }
  811. /**
  812. * @return true if batch (non-interactive) mode is preferred for this
  813. * host connection.
  814. */
  815. public boolean isBatchMode() {
  816. return batchMode != null && batchMode.booleanValue();
  817. }
  818. /**
  819. * @return the number of tries (one per second) to connect before
  820. * exiting. The argument must be an integer. This may be useful
  821. * in scripts if the connection sometimes fails. The default is
  822. * 1.
  823. * @since 3.4
  824. */
  825. public int getConnectionAttempts() {
  826. return connectionAttempts;
  827. }
  828. private void complete(String initialHostName, File homeDir) {
  829. // Try to set values from the options.
  830. hostName = config.getHostname();
  831. user = config.getUser();
  832. port = config.getPort();
  833. connectionAttempts = positive(
  834. config.getValue("ConnectionAttempts")); //$NON-NLS-1$
  835. strictHostKeyChecking = config.getValue("StrictHostKeyChecking"); //$NON-NLS-1$
  836. String value = config.getValue("BatchMode"); //$NON-NLS-1$
  837. if (value != null) {
  838. batchMode = yesno(value);
  839. }
  840. value = config.getValue("PreferredAuthentications"); //$NON-NLS-1$
  841. if (value != null) {
  842. preferredAuthentications = nows(value);
  843. }
  844. // Fill in defaults if still not set
  845. if (hostName == null) {
  846. hostName = initialHostName;
  847. }
  848. if (user == null) {
  849. user = OpenSshConfig.userName();
  850. }
  851. if (port <= 0) {
  852. port = OpenSshConfig.SSH_PORT;
  853. }
  854. if (connectionAttempts <= 0) {
  855. connectionAttempts = 1;
  856. }
  857. String[] identityFiles = config.getValues("IdentityFile"); //$NON-NLS-1$
  858. if (identityFiles != null && identityFiles.length > 0) {
  859. identityFile = toFile(identityFiles[0], homeDir);
  860. }
  861. }
  862. Config getConfig() {
  863. return config;
  864. }
  865. @Override
  866. @SuppressWarnings("nls")
  867. public String toString() {
  868. return "Host [hostName=" + hostName + ", port=" + port
  869. + ", identityFile=" + identityFile + ", user=" + user
  870. + ", preferredAuthentications=" + preferredAuthentications
  871. + ", batchMode=" + batchMode + ", strictHostKeyChecking="
  872. + strictHostKeyChecking + ", connectionAttempts="
  873. + connectionAttempts + ", config=" + config + "]";
  874. }
  875. }
  876. /**
  877. * Retrieves the full {@link com.jcraft.jsch.ConfigRepository.Config Config}
  878. * for the given host name. Should be called only by Jsch and tests.
  879. *
  880. * @param hostName
  881. * to get the config for
  882. * @return the configuration for the host
  883. * @since 4.9
  884. */
  885. @Override
  886. public Config getConfig(String hostName) {
  887. Host host = lookup(hostName);
  888. return new JschBugFixingConfig(host.getConfig());
  889. }
  890. @Override
  891. @SuppressWarnings("nls")
  892. public String toString() {
  893. return "OpenSshConfig [home=" + home + ", configFile=" + configFile
  894. + ", lastModified=" + lastModified + ", state=" + state + "]";
  895. }
  896. /**
  897. * A {@link com.jcraft.jsch.ConfigRepository.Config} that transforms some
  898. * values from the config file into the format Jsch 0.1.54 expects. This is
  899. * a work-around for bugs in Jsch.
  900. */
  901. private static class JschBugFixingConfig implements Config {
  902. private final Config real;
  903. public JschBugFixingConfig(Config delegate) {
  904. real = delegate;
  905. }
  906. @Override
  907. public String getHostname() {
  908. return real.getHostname();
  909. }
  910. @Override
  911. public String getUser() {
  912. return real.getUser();
  913. }
  914. @Override
  915. public int getPort() {
  916. return real.getPort();
  917. }
  918. @Override
  919. public String getValue(String key) {
  920. String result = real.getValue(key);
  921. if (result != null) {
  922. String k = key.toUpperCase(Locale.ROOT);
  923. if ("SERVERALIVEINTERVAL".equals(k) //$NON-NLS-1$
  924. || "CONNECTTIMEOUT".equals(k)) { //$NON-NLS-1$
  925. // These values are in seconds. Jsch 0.1.54 passes them on
  926. // as is to java.net.Socket.setSoTimeout(), which expects
  927. // milliseconds. So convert here to milliseconds...
  928. try {
  929. int timeout = Integer.parseInt(result);
  930. result = Long
  931. .toString(TimeUnit.SECONDS.toMillis(timeout));
  932. } catch (NumberFormatException e) {
  933. // Ignore
  934. }
  935. }
  936. }
  937. return result;
  938. }
  939. @Override
  940. public String[] getValues(String key) {
  941. return real.getValues(key);
  942. }
  943. }
  944. }