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

Factor out a JSch-independent ssh config parser Move the bulk of the basic parsing and host entry handling into a new class OpenSshConfigFile that has no dependencies on any concrete ssh implementation. Make the existing OpenSshConfig use the new parser. Introduce a new class SshConstants collecting all the various ssh- related string literals. Also use TreeMaps with a case-insensitive key comparator instead of converting keys to uppercase. Add a test to verify that keys are matched case-insensitively. Most of the parsing code was simply moved, except that the new parser supports looking up entries given host name, port, and user name, and can thus handle more %-substitutions correctly. This feature is not yet used and cannot be used with JSch since JSch only has a ConfigRepository.getConfig(String) interface. The split is still worth the trouble as it opens the way to using another ssh client altogether. Apache MINA sshd, for instance, resolves host entries giving host name, port, and user name. (Apache MINA has a built-in ssh config handling, but that has problems, too: its pattern matching is case-insensitive, and its merging of host entries if several match is not the same as in OpenSsh. But with this refactoring, it will be possible to plug in OpenSshConfigFile into an Apache MINA sshd client without dragging along JSch.) One test case that doesn't make sense anymore has been removed. It tested that repeatedly querying for a host entry returned the same object. That is no longer true since the caching has been moved to a deeper level. Bug: 520927 Change-Id: I6381d52b29099595e6eaf8b05c786aeeaefbf9cc Signed-off-by: Thomas Wolf <thomas.wolf@paranor.ch>
5 anni fa
Merge branch 'stable-5.1' into stable-5.2 * stable-5.1: Fix OpenSshConfigTest#config FileSnapshot: fix bug with timestamp thresholding In LockFile#waitForStatChange wait in units of file time resolution Cache FileStoreAttributeCache per directory Fix FileSnapshot#save(long) and FileSnapshot#save(Instant) Persist minimal racy threshold and allow manual configuration Measure minimum racy interval to auto-configure FileSnapshot Reuse FileUtils to recursively delete files created by tests Fix FileAttributeCache.toString() Add test for racy git detection in FileSnapshot Repeat RefDirectoryTest.testGetRef_DiscoversModifiedLoose 100 times Fix org.eclipse.jdt.core.prefs of org.eclipse.jgit.junit Add missing javadoc in org.eclipse.jgit.junit Enhance RepeatRule to report number of failures at the end Fix FileSnapshotTests for filesystem with high timestamp resolution Retry deleting test files in FileBasedConfigTest Measure filesystem timestamp resolution already in test setup Refactor FileSnapshotTest to use NIO APIs Measure stored timestamp resolution instead of time to touch file Handle CancellationException in FileStoreAttributeCache Fix FileSnapshot#saveNoConfig Use Instant for smudge time in DirCache and DirCacheEntry Use Instant instead of milliseconds for filesystem timestamp handling Workaround SecurityException in FS#getFsTimestampResolution Fix NPE in FS$FileStoreAttributeCache.getFsTimestampResolution FS: ignore AccessDeniedException when measuring timestamp resolution Add debug trace for FileSnapshot Use FileChannel.open to touch file and set mtime to now Persist filesystem timestamp resolution and allow manual configuration Increase bazel timeout for long running tests Bazel: Fix lint warning flagged by buildifier Update bazlets to latest version Bazel: Add missing dependencies for ArchiveCommandTest Bazel: Remove FileTreeIteratorWithTimeControl from BUILD file Add support for nanoseconds and microseconds for Config#getTimeUnit Optionally measure filesystem timestamp resolution asynchronously Delete unused FileTreeIteratorWithTimeControl FileSnapshot#equals: consider UNKNOWN_SIZE Timeout measuring file timestamp resolution after 2 seconds Fix RacyGitTests#testRacyGitDetection Change RacyGitTests to create a racy git situation in a stable way Deprecate Constants.CHARACTER_ENCODING in favor of StandardCharsets.UTF_8 Fix non-deterministic hash of archives created by ArchiveCommand Update Maven plugins ecj, plexus, error-prone Update Maven plugins and cleanup Maven warnings Make inner classes static where possible Fix API problem filters Change-Id: Ia57385b2a60f48a5317c8d723721c235d7043a84 Signed-off-by: Matthias Sohn <matthias.sohn@sap.com>
4 anni fa
Factor out a JSch-independent ssh config parser Move the bulk of the basic parsing and host entry handling into a new class OpenSshConfigFile that has no dependencies on any concrete ssh implementation. Make the existing OpenSshConfig use the new parser. Introduce a new class SshConstants collecting all the various ssh- related string literals. Also use TreeMaps with a case-insensitive key comparator instead of converting keys to uppercase. Add a test to verify that keys are matched case-insensitively. Most of the parsing code was simply moved, except that the new parser supports looking up entries given host name, port, and user name, and can thus handle more %-substitutions correctly. This feature is not yet used and cannot be used with JSch since JSch only has a ConfigRepository.getConfig(String) interface. The split is still worth the trouble as it opens the way to using another ssh client altogether. Apache MINA sshd, for instance, resolves host entries giving host name, port, and user name. (Apache MINA has a built-in ssh config handling, but that has problems, too: its pattern matching is case-insensitive, and its merging of host entries if several match is not the same as in OpenSsh. But with this refactoring, it will be possible to plug in OpenSshConfigFile into an Apache MINA sshd client without dragging along JSch.) One test case that doesn't make sense anymore has been removed. It tested that repeatedly querying for a host entry returned the same object. That is no longer true since the caching has been moved to a deeper level. Bug: 520927 Change-Id: I6381d52b29099595e6eaf8b05c786aeeaefbf9cc Signed-off-by: Thomas Wolf <thomas.wolf@paranor.ch>
5 anni fa
Merge branch 'stable-5.1' into stable-5.2 * stable-5.1: Fix OpenSshConfigTest#config FileSnapshot: fix bug with timestamp thresholding In LockFile#waitForStatChange wait in units of file time resolution Cache FileStoreAttributeCache per directory Fix FileSnapshot#save(long) and FileSnapshot#save(Instant) Persist minimal racy threshold and allow manual configuration Measure minimum racy interval to auto-configure FileSnapshot Reuse FileUtils to recursively delete files created by tests Fix FileAttributeCache.toString() Add test for racy git detection in FileSnapshot Repeat RefDirectoryTest.testGetRef_DiscoversModifiedLoose 100 times Fix org.eclipse.jdt.core.prefs of org.eclipse.jgit.junit Add missing javadoc in org.eclipse.jgit.junit Enhance RepeatRule to report number of failures at the end Fix FileSnapshotTests for filesystem with high timestamp resolution Retry deleting test files in FileBasedConfigTest Measure filesystem timestamp resolution already in test setup Refactor FileSnapshotTest to use NIO APIs Measure stored timestamp resolution instead of time to touch file Handle CancellationException in FileStoreAttributeCache Fix FileSnapshot#saveNoConfig Use Instant for smudge time in DirCache and DirCacheEntry Use Instant instead of milliseconds for filesystem timestamp handling Workaround SecurityException in FS#getFsTimestampResolution Fix NPE in FS$FileStoreAttributeCache.getFsTimestampResolution FS: ignore AccessDeniedException when measuring timestamp resolution Add debug trace for FileSnapshot Use FileChannel.open to touch file and set mtime to now Persist filesystem timestamp resolution and allow manual configuration Increase bazel timeout for long running tests Bazel: Fix lint warning flagged by buildifier Update bazlets to latest version Bazel: Add missing dependencies for ArchiveCommandTest Bazel: Remove FileTreeIteratorWithTimeControl from BUILD file Add support for nanoseconds and microseconds for Config#getTimeUnit Optionally measure filesystem timestamp resolution asynchronously Delete unused FileTreeIteratorWithTimeControl FileSnapshot#equals: consider UNKNOWN_SIZE Timeout measuring file timestamp resolution after 2 seconds Fix RacyGitTests#testRacyGitDetection Change RacyGitTests to create a racy git situation in a stable way Deprecate Constants.CHARACTER_ENCODING in favor of StandardCharsets.UTF_8 Fix non-deterministic hash of archives created by ArchiveCommand Update Maven plugins ecj, plexus, error-prone Update Maven plugins and cleanup Maven warnings Make inner classes static where possible Fix API problem filters Change-Id: Ia57385b2a60f48a5317c8d723721c235d7043a84 Signed-off-by: Matthias Sohn <matthias.sohn@sap.com>
4 anni fa
Factor out a JSch-independent ssh config parser Move the bulk of the basic parsing and host entry handling into a new class OpenSshConfigFile that has no dependencies on any concrete ssh implementation. Make the existing OpenSshConfig use the new parser. Introduce a new class SshConstants collecting all the various ssh- related string literals. Also use TreeMaps with a case-insensitive key comparator instead of converting keys to uppercase. Add a test to verify that keys are matched case-insensitively. Most of the parsing code was simply moved, except that the new parser supports looking up entries given host name, port, and user name, and can thus handle more %-substitutions correctly. This feature is not yet used and cannot be used with JSch since JSch only has a ConfigRepository.getConfig(String) interface. The split is still worth the trouble as it opens the way to using another ssh client altogether. Apache MINA sshd, for instance, resolves host entries giving host name, port, and user name. (Apache MINA has a built-in ssh config handling, but that has problems, too: its pattern matching is case-insensitive, and its merging of host entries if several match is not the same as in OpenSsh. But with this refactoring, it will be possible to plug in OpenSshConfigFile into an Apache MINA sshd client without dragging along JSch.) One test case that doesn't make sense anymore has been removed. It tested that repeatedly querying for a host entry returned the same object. That is no longer true since the caching has been moved to a deeper level. Bug: 520927 Change-Id: I6381d52b29099595e6eaf8b05c786aeeaefbf9cc Signed-off-by: Thomas Wolf <thomas.wolf@paranor.ch>
5 anni fa
Merge branch 'stable-5.1' into stable-5.2 * stable-5.1: Fix OpenSshConfigTest#config FileSnapshot: fix bug with timestamp thresholding In LockFile#waitForStatChange wait in units of file time resolution Cache FileStoreAttributeCache per directory Fix FileSnapshot#save(long) and FileSnapshot#save(Instant) Persist minimal racy threshold and allow manual configuration Measure minimum racy interval to auto-configure FileSnapshot Reuse FileUtils to recursively delete files created by tests Fix FileAttributeCache.toString() Add test for racy git detection in FileSnapshot Repeat RefDirectoryTest.testGetRef_DiscoversModifiedLoose 100 times Fix org.eclipse.jdt.core.prefs of org.eclipse.jgit.junit Add missing javadoc in org.eclipse.jgit.junit Enhance RepeatRule to report number of failures at the end Fix FileSnapshotTests for filesystem with high timestamp resolution Retry deleting test files in FileBasedConfigTest Measure filesystem timestamp resolution already in test setup Refactor FileSnapshotTest to use NIO APIs Measure stored timestamp resolution instead of time to touch file Handle CancellationException in FileStoreAttributeCache Fix FileSnapshot#saveNoConfig Use Instant for smudge time in DirCache and DirCacheEntry Use Instant instead of milliseconds for filesystem timestamp handling Workaround SecurityException in FS#getFsTimestampResolution Fix NPE in FS$FileStoreAttributeCache.getFsTimestampResolution FS: ignore AccessDeniedException when measuring timestamp resolution Add debug trace for FileSnapshot Use FileChannel.open to touch file and set mtime to now Persist filesystem timestamp resolution and allow manual configuration Increase bazel timeout for long running tests Bazel: Fix lint warning flagged by buildifier Update bazlets to latest version Bazel: Add missing dependencies for ArchiveCommandTest Bazel: Remove FileTreeIteratorWithTimeControl from BUILD file Add support for nanoseconds and microseconds for Config#getTimeUnit Optionally measure filesystem timestamp resolution asynchronously Delete unused FileTreeIteratorWithTimeControl FileSnapshot#equals: consider UNKNOWN_SIZE Timeout measuring file timestamp resolution after 2 seconds Fix RacyGitTests#testRacyGitDetection Change RacyGitTests to create a racy git situation in a stable way Deprecate Constants.CHARACTER_ENCODING in favor of StandardCharsets.UTF_8 Fix non-deterministic hash of archives created by ArchiveCommand Update Maven plugins ecj, plexus, error-prone Update Maven plugins and cleanup Maven warnings Make inner classes static where possible Fix API problem filters Change-Id: Ia57385b2a60f48a5317c8d723721c235d7043a84 Signed-off-by: Matthias Sohn <matthias.sohn@sap.com>
4 anni fa
Factor out a JSch-independent ssh config parser Move the bulk of the basic parsing and host entry handling into a new class OpenSshConfigFile that has no dependencies on any concrete ssh implementation. Make the existing OpenSshConfig use the new parser. Introduce a new class SshConstants collecting all the various ssh- related string literals. Also use TreeMaps with a case-insensitive key comparator instead of converting keys to uppercase. Add a test to verify that keys are matched case-insensitively. Most of the parsing code was simply moved, except that the new parser supports looking up entries given host name, port, and user name, and can thus handle more %-substitutions correctly. This feature is not yet used and cannot be used with JSch since JSch only has a ConfigRepository.getConfig(String) interface. The split is still worth the trouble as it opens the way to using another ssh client altogether. Apache MINA sshd, for instance, resolves host entries giving host name, port, and user name. (Apache MINA has a built-in ssh config handling, but that has problems, too: its pattern matching is case-insensitive, and its merging of host entries if several match is not the same as in OpenSsh. But with this refactoring, it will be possible to plug in OpenSshConfigFile into an Apache MINA sshd client without dragging along JSch.) One test case that doesn't make sense anymore has been removed. It tested that repeatedly querying for a host entry returned the same object. That is no longer true since the caching has been moved to a deeper level. Bug: 520927 Change-Id: I6381d52b29099595e6eaf8b05c786aeeaefbf9cc Signed-off-by: Thomas Wolf <thomas.wolf@paranor.ch>
5 anni fa
Merge branch 'stable-5.1' into stable-5.2 * stable-5.1: Fix OpenSshConfigTest#config FileSnapshot: fix bug with timestamp thresholding In LockFile#waitForStatChange wait in units of file time resolution Cache FileStoreAttributeCache per directory Fix FileSnapshot#save(long) and FileSnapshot#save(Instant) Persist minimal racy threshold and allow manual configuration Measure minimum racy interval to auto-configure FileSnapshot Reuse FileUtils to recursively delete files created by tests Fix FileAttributeCache.toString() Add test for racy git detection in FileSnapshot Repeat RefDirectoryTest.testGetRef_DiscoversModifiedLoose 100 times Fix org.eclipse.jdt.core.prefs of org.eclipse.jgit.junit Add missing javadoc in org.eclipse.jgit.junit Enhance RepeatRule to report number of failures at the end Fix FileSnapshotTests for filesystem with high timestamp resolution Retry deleting test files in FileBasedConfigTest Measure filesystem timestamp resolution already in test setup Refactor FileSnapshotTest to use NIO APIs Measure stored timestamp resolution instead of time to touch file Handle CancellationException in FileStoreAttributeCache Fix FileSnapshot#saveNoConfig Use Instant for smudge time in DirCache and DirCacheEntry Use Instant instead of milliseconds for filesystem timestamp handling Workaround SecurityException in FS#getFsTimestampResolution Fix NPE in FS$FileStoreAttributeCache.getFsTimestampResolution FS: ignore AccessDeniedException when measuring timestamp resolution Add debug trace for FileSnapshot Use FileChannel.open to touch file and set mtime to now Persist filesystem timestamp resolution and allow manual configuration Increase bazel timeout for long running tests Bazel: Fix lint warning flagged by buildifier Update bazlets to latest version Bazel: Add missing dependencies for ArchiveCommandTest Bazel: Remove FileTreeIteratorWithTimeControl from BUILD file Add support for nanoseconds and microseconds for Config#getTimeUnit Optionally measure filesystem timestamp resolution asynchronously Delete unused FileTreeIteratorWithTimeControl FileSnapshot#equals: consider UNKNOWN_SIZE Timeout measuring file timestamp resolution after 2 seconds Fix RacyGitTests#testRacyGitDetection Change RacyGitTests to create a racy git situation in a stable way Deprecate Constants.CHARACTER_ENCODING in favor of StandardCharsets.UTF_8 Fix non-deterministic hash of archives created by ArchiveCommand Update Maven plugins ecj, plexus, error-prone Update Maven plugins and cleanup Maven warnings Make inner classes static where possible Fix API problem filters Change-Id: Ia57385b2a60f48a5317c8d723721c235d7043a84 Signed-off-by: Matthias Sohn <matthias.sohn@sap.com>
4 anni fa
Factor out a JSch-independent ssh config parser Move the bulk of the basic parsing and host entry handling into a new class OpenSshConfigFile that has no dependencies on any concrete ssh implementation. Make the existing OpenSshConfig use the new parser. Introduce a new class SshConstants collecting all the various ssh- related string literals. Also use TreeMaps with a case-insensitive key comparator instead of converting keys to uppercase. Add a test to verify that keys are matched case-insensitively. Most of the parsing code was simply moved, except that the new parser supports looking up entries given host name, port, and user name, and can thus handle more %-substitutions correctly. This feature is not yet used and cannot be used with JSch since JSch only has a ConfigRepository.getConfig(String) interface. The split is still worth the trouble as it opens the way to using another ssh client altogether. Apache MINA sshd, for instance, resolves host entries giving host name, port, and user name. (Apache MINA has a built-in ssh config handling, but that has problems, too: its pattern matching is case-insensitive, and its merging of host entries if several match is not the same as in OpenSsh. But with this refactoring, it will be possible to plug in OpenSshConfigFile into an Apache MINA sshd client without dragging along JSch.) One test case that doesn't make sense anymore has been removed. It tested that repeatedly querying for a host entry returned the same object. That is no longer true since the caching has been moved to a deeper level. Bug: 520927 Change-Id: I6381d52b29099595e6eaf8b05c786aeeaefbf9cc Signed-off-by: Thomas Wolf <thomas.wolf@paranor.ch>
5 anni fa
Factor out a JSch-independent ssh config parser Move the bulk of the basic parsing and host entry handling into a new class OpenSshConfigFile that has no dependencies on any concrete ssh implementation. Make the existing OpenSshConfig use the new parser. Introduce a new class SshConstants collecting all the various ssh- related string literals. Also use TreeMaps with a case-insensitive key comparator instead of converting keys to uppercase. Add a test to verify that keys are matched case-insensitively. Most of the parsing code was simply moved, except that the new parser supports looking up entries given host name, port, and user name, and can thus handle more %-substitutions correctly. This feature is not yet used and cannot be used with JSch since JSch only has a ConfigRepository.getConfig(String) interface. The split is still worth the trouble as it opens the way to using another ssh client altogether. Apache MINA sshd, for instance, resolves host entries giving host name, port, and user name. (Apache MINA has a built-in ssh config handling, but that has problems, too: its pattern matching is case-insensitive, and its merging of host entries if several match is not the same as in OpenSsh. But with this refactoring, it will be possible to plug in OpenSshConfigFile into an Apache MINA sshd client without dragging along JSch.) One test case that doesn't make sense anymore has been removed. It tested that repeatedly querying for a host entry returned the same object. That is no longer true since the caching has been moved to a deeper level. Bug: 520927 Change-Id: I6381d52b29099595e6eaf8b05c786aeeaefbf9cc Signed-off-by: Thomas Wolf <thomas.wolf@paranor.ch>
5 anni fa
Factor out a JSch-independent ssh config parser Move the bulk of the basic parsing and host entry handling into a new class OpenSshConfigFile that has no dependencies on any concrete ssh implementation. Make the existing OpenSshConfig use the new parser. Introduce a new class SshConstants collecting all the various ssh- related string literals. Also use TreeMaps with a case-insensitive key comparator instead of converting keys to uppercase. Add a test to verify that keys are matched case-insensitively. Most of the parsing code was simply moved, except that the new parser supports looking up entries given host name, port, and user name, and can thus handle more %-substitutions correctly. This feature is not yet used and cannot be used with JSch since JSch only has a ConfigRepository.getConfig(String) interface. The split is still worth the trouble as it opens the way to using another ssh client altogether. Apache MINA sshd, for instance, resolves host entries giving host name, port, and user name. (Apache MINA has a built-in ssh config handling, but that has problems, too: its pattern matching is case-insensitive, and its merging of host entries if several match is not the same as in OpenSsh. But with this refactoring, it will be possible to plug in OpenSshConfigFile into an Apache MINA sshd client without dragging along JSch.) One test case that doesn't make sense anymore has been removed. It tested that repeatedly querying for a host entry returned the same object. That is no longer true since the caching has been moved to a deeper level. Bug: 520927 Change-Id: I6381d52b29099595e6eaf8b05c786aeeaefbf9cc Signed-off-by: Thomas Wolf <thomas.wolf@paranor.ch>
5 anni fa
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. }