You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@mina.apache.org by lg...@apache.org on 2016/02/14 15:16:54 UTC
[3/3] mina-sshd git commit: [SSHD-266] Support for known hosts
[SSHD-266] Support for known hosts
Project: http://git-wip-us.apache.org/repos/asf/mina-sshd/repo
Commit: http://git-wip-us.apache.org/repos/asf/mina-sshd/commit/509c871e
Tree: http://git-wip-us.apache.org/repos/asf/mina-sshd/tree/509c871e
Diff: http://git-wip-us.apache.org/repos/asf/mina-sshd/diff/509c871e
Branch: refs/heads/master
Commit: 509c871eac5f9dff521891971fb24673135b52c3
Parents: 305850c
Author: Lyor Goldstein <lg...@vmware.com>
Authored: Sun Feb 14 16:16:35 2016 +0200
Committer: Lyor Goldstein <lg...@vmware.com>
Committed: Sun Feb 14 16:16:35 2016 +0200
----------------------------------------------------------------------
.../java/org/apache/sshd/client/SshClient.java | 4 +-
.../DefaultConfigFileHostEntryResolver.java | 15 +-
.../client/config/hosts/HostConfigEntry.java | 448 +++---------
.../client/config/hosts/HostPatternsHolder.java | 310 ++++++++
.../client/config/hosts/KnownHostDigest.java | 65 ++
.../client/config/hosts/KnownHostEntry.java | 271 +++++++
.../client/config/hosts/KnownHostHashValue.java | 166 +++++
.../DefaultKnownHostsServerKeyVerifier.java | 89 +++
.../KnownHostsServerKeyVerifier.java | 720 +++++++++++++++++++
.../client/keyverifier/ServerKeyVerifier.java | 10 +-
.../client/session/AbstractClientSession.java | 11 +
.../sshd/client/session/ClientSession.java | 12 +
.../common/config/keys/AuthorizedKeyEntry.java | 361 ++++++++++
.../org/apache/sshd/common/mac/BaseMac.java | 14 +-
.../java/org/apache/sshd/common/mac/Mac.java | 2 +
.../org/apache/sshd/common/util/Base64.java | 98 ++-
.../java/org/apache/sshd/common/util/Pair.java | 2 +-
.../sshd/common/util/buffer/BufferUtils.java | 10 +-
.../common/util/io/ModifiableFileWatcher.java | 7 +-
.../sshd/common/util/net/SshdSocketAddress.java | 41 ++
.../server/config/keys/AuthorizedKeyEntry.java | 399 ----------
.../keys/AuthorizedKeysAuthenticator.java | 41 ++
.../DefaultAuthorizedKeysAuthenticator.java | 3 +-
.../java/org/apache/sshd/client/ClientTest.java | 7 +
.../hosts/ConfigFileHostEntryResolverTest.java | 10 +-
.../hosts/HostConfigEntryResolverTest.java | 12 +-
.../config/hosts/HostConfigEntryTest.java | 50 +-
.../config/hosts/KnownHostHashValueTest.java | 73 ++
.../KnownHostsServerKeyVerifierTest.java | 358 +++++++++
.../org/apache/sshd/client/scp/ScpTest.java | 4 +-
.../config/keys/AuthorizedKeyEntryTest.java | 137 ++++
.../config/keys/AuthorizedKeysTestSupport.java | 121 ++++
.../org/apache/sshd/common/util/Base64Test.java | 73 ++
.../config/keys/AuthorizedKeyEntryTest.java | 135 ----
.../keys/AuthorizedKeysAuthenticatorTest.java | 2 +
.../config/keys/AuthorizedKeysTestSupport.java | 120 ----
.../DefaultAuthorizedKeysAuthenticatorTest.java | 2 +
.../apache/sshd/client/keyverifier/known_hosts | 12 +
.../sshd/common/config/keys/authorized_keys | 16 +
.../auth/pubkey/LdapPublickeyAuthenticator.java | 2 +-
.../pubkey/LdapPublickeyAuthenticatorTest.java | 2 +-
41 files changed, 3123 insertions(+), 1112 deletions(-)
----------------------------------------------------------------------
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/509c871e/sshd-core/src/main/java/org/apache/sshd/client/SshClient.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/SshClient.java b/sshd-core/src/main/java/org/apache/sshd/client/SshClient.java
index 5d567b1..d159fe1 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/SshClient.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/SshClient.java
@@ -73,6 +73,7 @@ import org.apache.sshd.client.config.keys.DefaultClientIdentitiesWatcher;
import org.apache.sshd.client.future.ConnectFuture;
import org.apache.sshd.client.future.DefaultConnectFuture;
import org.apache.sshd.client.keyverifier.ServerKeyVerifier;
+import org.apache.sshd.client.session.AbstractClientSession;
import org.apache.sshd.client.session.ClientConnectionServiceFactory;
import org.apache.sshd.client.session.ClientSession;
import org.apache.sshd.client.session.ClientSessionCreator;
@@ -590,8 +591,9 @@ public class SshClient extends AbstractFactoryManager implements ClientFactoryMa
protected void onConnectOperationComplete(IoSession ioSession, ConnectFuture connectFuture,
String username, SocketAddress address, Collection<? extends KeyPair> identities, boolean useDefaultIdentities) {
- ClientSession session = (ClientSession) AbstractSession.getSession(ioSession);
+ AbstractClientSession session = (AbstractClientSession) AbstractSession.getSession(ioSession);
session.setUsername(username);
+ session.setConnectAddress(address);
if (useDefaultIdentities) {
setupDefaultSessionIdentities(session);
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/509c871e/sshd-core/src/main/java/org/apache/sshd/client/config/hosts/DefaultConfigFileHostEntryResolver.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/config/hosts/DefaultConfigFileHostEntryResolver.java b/sshd-core/src/main/java/org/apache/sshd/client/config/hosts/DefaultConfigFileHostEntryResolver.java
index 6b1e86b..3d39b6d 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/config/hosts/DefaultConfigFileHostEntryResolver.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/config/hosts/DefaultConfigFileHostEntryResolver.java
@@ -32,9 +32,9 @@ import org.apache.sshd.common.util.io.IoUtils;
/**
* Monitors the {@code ~/.ssh/config} file of the user currently running
- * the server, re-loading it if necessary. It also (optionally) enforces the same
- * permissions regime as {@code OpenSSH} does for the file permissions.
-
+ * the client, re-loading it if necessary. It also (optionally) enforces
+ * the same permissions regime as {@code OpenSSH}
+ *
* @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
*/
public class DefaultConfigFileHostEntryResolver extends ConfigFileHostEntryResolver {
@@ -48,8 +48,9 @@ public class DefaultConfigFileHostEntryResolver extends ConfigFileHostEntryResol
/**
* @param strict If {@code true} then makes sure that the containing folder
- * has 0700 access and the file 0600. <B>Note:</B> for <I>Windows</I> it
+ * has 0700 access and the file 0644. <B>Note:</B> for <I>Windows</I> it
* does not check these permissions
+ * @see #validateStrictConfigFilePermissions(Path, LinkOption...)
*/
public DefaultConfigFileHostEntryResolver(boolean strict) {
this(HostConfigEntry.getDefaultHostConfigFile(), strict);
@@ -64,6 +65,12 @@ public class DefaultConfigFileHostEntryResolver extends ConfigFileHostEntryResol
this.strict = strict;
}
+ /**
+ * @return If {@code true} then makes sure that the containing folder
+ * has 0700 access and the file 0644. <B>Note:</B> for <I>Windows</I> it
+ * does not check these permissions
+ * @see #validateStrictConfigFilePermissions(Path, LinkOption...)
+ */
public final boolean isStrict() {
return strict;
}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/509c871e/sshd-core/src/main/java/org/apache/sshd/client/config/hosts/HostConfigEntry.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/config/hosts/HostConfigEntry.java b/sshd-core/src/main/java/org/apache/sshd/client/config/hosts/HostConfigEntry.java
index 52a8a83..1d3fc09 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/config/hosts/HostConfigEntry.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/config/hosts/HostConfigEntry.java
@@ -46,16 +46,12 @@ import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.TreeMap;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
import org.apache.sshd.common.auth.MutableUserHolder;
import org.apache.sshd.common.config.SshConfigFileReader;
import org.apache.sshd.common.config.keys.IdentityUtils;
import org.apache.sshd.common.config.keys.PublicKeyEntry;
import org.apache.sshd.common.util.GenericUtils;
import org.apache.sshd.common.util.OsUtils;
-import org.apache.sshd.common.util.Pair;
import org.apache.sshd.common.util.ValidateUtils;
import org.apache.sshd.common.util.io.IoUtils;
import org.apache.sshd.common.util.io.NoCloseInputStream;
@@ -68,34 +64,12 @@ import org.apache.sshd.common.util.io.NoCloseReader;
* file format</A>
* @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
*/
-public class HostConfigEntry implements MutableUserHolder {
-
- /**
- * Used in a host pattern to denote zero or more consecutive characters
- */
- public static final char WILDCARD_PATTERN = '*';
- public static final String ALL_HOSTS_PATTERN = String.valueOf(HostConfigEntry.WILDCARD_PATTERN);
-
+public class HostConfigEntry extends HostPatternsHolder implements MutableUserHolder {
/**
* Standard OpenSSH config file name
*/
public static final String STD_CONFIG_FILENAME = "config";
- /**
- * Used in a host pattern to denote any <U>one</U> character
- */
- public static final char SINGLE_CHAR_PATTERN = '?';
-
- /**
- * Used to negate a host pattern
- */
- public static final char NEGATION_CHAR_PATTERN = '!';
-
- /**
- * The available pattern characters
- */
- public static final String PATTERN_CHARS = new String(new char[]{WILDCARD_PATTERN, SINGLE_CHAR_PATTERN, NEGATION_CHAR_PATTERN});
-
public static final String HOST_CONFIG_PROP = "Host";
public static final String HOST_NAME_CONFIG_PROP = "HostName";
public static final String PORT_CONFIG_PROP = SshConfigFileReader.PORT_CONFIG_PROP;
@@ -130,7 +104,7 @@ public class HostConfigEntry implements MutableUserHolder {
public static final char REMOTE_PORT_MACRO = 'p';
private static final class LazyDefaultConfigFileHolder {
- private static final Path KEYS_FILE = PublicKeyEntry.getDefaultKeysFolderPath().resolve(STD_CONFIG_FILENAME);
+ private static final Path CONFIG_FILE = PublicKeyEntry.getDefaultKeysFolderPath().resolve(STD_CONFIG_FILENAME);
}
private String host;
@@ -138,7 +112,6 @@ public class HostConfigEntry implements MutableUserHolder {
private int port;
private String username;
private Boolean exclusiveIdentites;
- private Collection<Pair<Pattern, Boolean>> patterns = new LinkedList<>();
private Collection<String> identities = Collections.emptyList();
private Map<String, String> properties = Collections.emptyMap();
@@ -162,27 +135,12 @@ public class HostConfigEntry implements MutableUserHolder {
public void setHost(String host) {
this.host = host;
- this.patterns = parsePatterns(parseConfigValue(host));
+ setPatterns(parsePatterns(parseConfigValue(host)));
}
public void setHost(Collection<String> patterns) {
this.host = GenericUtils.join(ValidateUtils.checkNotNullAndNotEmpty(patterns, "No patterns"), ',');
- this.patterns = parsePatterns(patterns);
- }
-
- public Collection<Pair<Pattern, Boolean>> getPatterns() {
- return patterns;
- }
-
- /**
- * Checks if a given host name / address matches the entry's host pattern(s)
- *
- * @param host The host name / address - ignored if {@code null}/empty
- * @return {@code true} if the name / address matches the pattern(s)
- * @see #isHostMatch(String, Pattern)
- */
- public boolean isHostMatch(String host) {
- return isHostMatch(host, getPatterns());
+ setPatterns(parsePatterns(patterns));
}
/**
@@ -777,165 +735,6 @@ public class HostConfigEntry implements MutableUserHolder {
}
/**
- * Finds the best match out of the given ones.
- *
- * @param matches The available matches - ignored if {@code null}/empty
- * @return The best match or {@code null} if no matches or no best match found
- * @see #findBestMatch(Iterator)
- */
- public static HostConfigEntry findBestMatch(Collection<? extends HostConfigEntry> matches) {
- if (GenericUtils.isEmpty(matches)) {
- return null;
- } else {
- return findBestMatch(matches.iterator());
- }
- }
-
- /**
- * Finds the best match out of the given ones.
- *
- * @param matches The available matches - ignored if {@code null}/empty
- * @return The best match or {@code null} if no matches or no best match found
- * @see #findBestMatch(Iterator)
- */
- public static HostConfigEntry findBestMatch(Iterable<? extends HostConfigEntry> matches) {
- if (matches == null) {
- return null;
- } else {
- return findBestMatch(matches.iterator());
- }
- }
-
- /**
- * Finds the best match out of the given ones. The best match is defined as one whose
- * pattern is as <U>specific</U> as possible (if more than one match is available).
- * I.e., a non-global match is preferred over global one, and a match with no wildcards
- * is preferred over one with such a pattern.
- *
- * @param matches The available matches - ignored if {@code null}/empty
- * @return The best match or {@code null} if no matches or no best match found
- * @see #isSpecificHostPattern(String)
- */
- public static HostConfigEntry findBestMatch(Iterator<? extends HostConfigEntry> matches) {
- if ((matches == null) || (!matches.hasNext())) {
- return null;
- }
-
- HostConfigEntry candidate = matches.next();
- int wildcardMatches = 0;
- while (matches.hasNext()) {
- HostConfigEntry entry = matches.next();
- String entryPattern = entry.getHost();
- String candidatePattern = candidate.getHost();
- // prefer non-global entry over global entry
- if (ALL_HOSTS_PATTERN.equalsIgnoreCase(candidatePattern)) {
- // unlikely, but handle it
- if (ALL_HOSTS_PATTERN.equalsIgnoreCase(entryPattern)) {
- wildcardMatches++;
- } else {
- candidate = entry;
- wildcardMatches = 0;
- }
- continue;
- }
-
- if (isSpecificHostPattern(entryPattern)) {
- // if both are specific then no best match
- if (isSpecificHostPattern(candidatePattern)) {
- return null;
- }
-
- candidate = entry;
- wildcardMatches = 0;
- continue;
- }
-
- wildcardMatches++;
- }
-
- String candidatePattern = candidate.getHost();
- // best match either has specific host or no wildcard matches
- if ((wildcardMatches <= 0) || (isSpecificHostPattern(candidatePattern))) {
- return candidate;
- }
-
- return null;
- }
-
- /**
- * @param pattern The pattern to check - ignored if {@code null}/empty
- * @return {@code true} if the pattern is not empty and contains no wildcard characters
- * @see #WILDCARD_PATTERN
- * @see #SINGLE_CHAR_PATTERN
- * @see #SINGLE_CHAR_PATTERN
- */
- public static boolean isSpecificHostPattern(String pattern) {
- if (GenericUtils.isEmpty(pattern)) {
- return false;
- }
-
- for (int index = 0; index < PATTERN_CHARS.length(); index++) {
- char ch = PATTERN_CHARS.charAt(index);
- if (pattern.indexOf(ch) >= 0) {
- return false;
- }
- }
-
- return true;
- }
-
- /**
- * Locates all the matching entries for a give host name / address
- *
- * @param host The host name / address - ignored if {@code null}/empty
- * @param entries The {@link HostConfigEntry}-ies to scan - ignored if {@code null}/empty
- * @return A {@link List} of all the matching entries
- * @see #isHostMatch(String)
- */
- public static List<HostConfigEntry> findMatchingEntries(String host, HostConfigEntry ... entries) {
- // TODO in Java-8 use Stream(s) + predicate
- if (GenericUtils.isEmpty(host) || GenericUtils.isEmpty(entries)) {
- return Collections.emptyList();
- } else {
- return findMatchingEntries(host, Arrays.asList(entries));
- }
- }
-
- /**
- * Locates all the matching entries for a give host name / address
- *
- * @param host The host name / address - ignored if {@code null}/empty
- * @param entries The {@link HostConfigEntry}-ies to scan - ignored if {@code null}/empty
- * @return A {@link List} of all the matching entries
- * @see #isHostMatch(String)
- */
- public static List<HostConfigEntry> findMatchingEntries(String host, Collection<? extends HostConfigEntry> entries) {
- // TODO in Java-8 use Stream(s) + predicate
- if (GenericUtils.isEmpty(host) || GenericUtils.isEmpty(entries)) {
- return Collections.emptyList();
- }
-
- List<HostConfigEntry> matches = null;
- for (HostConfigEntry entry : entries) {
- if (!entry.isHostMatch(host)) {
- continue; // debug breakpoint
- }
-
- if (matches == null) {
- matches = new ArrayList<>(entries.size()); // in case ALL of them match
- }
-
- matches.add(entry);
- }
-
- if (matches == null) {
- return Collections.emptyList();
- } else {
- return matches;
- }
- }
-
- /**
* Resolves the effective target host
*
* @param originalName The original requested host
@@ -983,154 +782,6 @@ public class HostConfigEntry implements MutableUserHolder {
}
}
- public static boolean isHostMatch(String host, Collection<Pair<Pattern, Boolean>> patterns) {
- if (GenericUtils.isEmpty(patterns)) {
- return false;
- }
-
- boolean matchFound = false;
- for (Pair<Pattern, Boolean> pp : patterns) {
- Boolean negated = pp.getSecond();
- /*
- * If already found a match we are interested only in negations
- */
- if (matchFound && (!negated.booleanValue())) {
- continue;
- }
-
- if (!isHostMatch(host, pp.getFirst())) {
- continue;
- }
-
- /*
- * According to https://www.freebsd.org/cgi/man.cgi?query=ssh_config&sektion=5:
- *
- * If a negated entry is matched, then the Host entry is ignored,
- * regardless of whether any other patterns on the line match.
- */
- if (negated.booleanValue()) {
- return false;
- }
-
- matchFound = true;
- }
-
- return matchFound;
- }
-
- /**
- * Checks if a given host name / address matches a host pattern
- *
- * @param host The host name / address - ignored if {@code null}/empty
- * @param pattern The host {@link Pattern} - ignored if {@code null}
- * @return {@code true} if the name / address matches the pattern
- */
- public static boolean isHostMatch(String host, Pattern pattern) {
- if (GenericUtils.isEmpty(host) || (pattern == null)) {
- return false;
- }
-
- Matcher m = pattern.matcher(host);
- return m.matches();
- }
-
- public static List<Pair<Pattern, Boolean>> parsePatterns(Collection<? extends CharSequence> patterns) {
- if (GenericUtils.isEmpty(patterns)) {
- return Collections.emptyList();
- }
-
- List<Pair<Pattern, Boolean>> result = new ArrayList<>(patterns.size());
- for (CharSequence p : patterns) {
- result.add(ValidateUtils.checkNotNull(toPattern(p), "No pattern for %s", p));
- }
-
- return result;
- }
-
- /**
- * Converts a host pattern string to a regular expression matcher.
- * <B>Note:</B> pattern matching is <U>case insensitive</U>
- *
- * @param pattern The original pattern string - ignored if {@code null}/empty
- * @return The regular expression matcher {@link Pattern} and the indication
- * whether it is a negating pattern or not - {@code null} if no original string
- * @see #WILDCARD_PATTERN
- * @see #SINGLE_CHAR_PATTERN
- * @see #NEGATION_CHAR_PATTERN
- */
- public static Pair<Pattern, Boolean> toPattern(CharSequence pattern) {
- if (GenericUtils.isEmpty(pattern)) {
- return null;
- }
-
- StringBuilder sb = new StringBuilder(pattern.length());
- boolean negated = false;
- for (int curPos = 0; curPos < pattern.length(); curPos++) {
- char ch = pattern.charAt(curPos);
- ValidateUtils.checkTrue(isValidPatternChar(ch), "Invalid host pattern char in %s", pattern);
-
- switch(ch) {
- case '.': // need to escape it
- sb.append('\\').append(ch);
- break;
- case SINGLE_CHAR_PATTERN:
- sb.append('.');
- break;
- case WILDCARD_PATTERN:
- sb.append(".*");
- break;
- case NEGATION_CHAR_PATTERN:
- ValidateUtils.checkTrue(!negated, "Double negation in %s", pattern);
- ValidateUtils.checkTrue(curPos == 0, "Negation must be 1st char: %s", pattern);
- negated = true;
- break;
- default:
- sb.append(ch);
- }
- }
-
- return new Pair<Pattern, Boolean>(Pattern.compile(sb.toString(), Pattern.CASE_INSENSITIVE), Boolean.valueOf(negated));
- }
-
- /**
- * Checks if the given character is valid for a host pattern. Valid
- * characters are:
- * <UL>
- * <LI>A-Z</LI>
- * <LI>a-z</LI>
- * <LI>0-9</LI>
- * <LI>Underscore (_)</LI>
- * <LI>Hyphen (-)</LI>
- * <LI>Dot (.)</LI>
- * <LI>The {@link #WILDCARD_PATTERN}</LI>
- * <LI>The {@link #SINGLE_CHAR_PATTERN}</LI>
- * </UL>
- *
- * @param ch The character to validate
- * @return {@code true} if valid pattern character
- */
- public static boolean isValidPatternChar(char ch) {
- if ((ch <= ' ') || (ch >= 0x7E)) {
- return false;
- }
- if ((ch >= 'a') && (ch <= 'z')) {
- return true;
- }
- if ((ch >= 'A') && (ch <= 'Z')) {
- return true;
- }
- if ((ch >= '0') && (ch <= '9')) {
- return true;
- }
- if ("-_.".indexOf(ch) >= 0) {
- return true;
- }
- if (PATTERN_CHARS.indexOf(ch) >= 0) {
- return true;
- }
- return false;
- }
-
public static List<HostConfigEntry> readHostConfigEntries(File file) throws IOException {
return readHostConfigEntries(file.toPath(), IoUtils.EMPTY_OPEN_OPTIONS);
}
@@ -1259,6 +910,92 @@ public class HostConfigEntry implements MutableUserHolder {
}
}
+ /**
+ * Finds the best match out of the given ones.
+ *
+ * @param matches The available matches - ignored if {@code null}/empty
+ * @return The best match or {@code null} if no matches or no best match found
+ * @see #findBestMatch(Iterator)
+ */
+ public static HostConfigEntry findBestMatch(Collection<? extends HostConfigEntry> matches) {
+ if (GenericUtils.isEmpty(matches)) {
+ return null;
+ } else {
+ return findBestMatch(matches.iterator());
+ }
+ }
+
+ /**
+ * Finds the best match out of the given ones.
+ *
+ * @param matches The available matches - ignored if {@code null}/empty
+ * @return The best match or {@code null} if no matches or no best match found
+ * @see #findBestMatch(Iterator)
+ */
+ public static HostConfigEntry findBestMatch(Iterable<? extends HostConfigEntry> matches) {
+ if (matches == null) {
+ return null;
+ } else {
+ return findBestMatch(matches.iterator());
+ }
+ }
+
+ /**
+ * Finds the best match out of the given ones. The best match is defined as one whose
+ * pattern is as <U>specific</U> as possible (if more than one match is available).
+ * I.e., a non-global match is preferred over global one, and a match with no wildcards
+ * is preferred over one with such a pattern.
+ *
+ * @param matches The available matches - ignored if {@code null}/empty
+ * @return The best match or {@code null} if no matches or no best match found
+ * @see #isSpecificHostPattern(String)
+ */
+ public static HostConfigEntry findBestMatch(Iterator<? extends HostConfigEntry> matches) {
+ if ((matches == null) || (!matches.hasNext())) {
+ return null;
+ }
+
+ HostConfigEntry candidate = matches.next();
+ int wildcardMatches = 0;
+ while (matches.hasNext()) {
+ HostConfigEntry entry = matches.next();
+ String entryPattern = entry.getHost();
+ String candidatePattern = candidate.getHost();
+ // prefer non-global entry over global entry
+ if (ALL_HOSTS_PATTERN.equalsIgnoreCase(candidatePattern)) {
+ // unlikely, but handle it
+ if (ALL_HOSTS_PATTERN.equalsIgnoreCase(entryPattern)) {
+ wildcardMatches++;
+ } else {
+ candidate = entry;
+ wildcardMatches = 0;
+ }
+ continue;
+ }
+
+ if (isSpecificHostPattern(entryPattern)) {
+ // if both are specific then no best match
+ if (isSpecificHostPattern(candidatePattern)) {
+ return null;
+ }
+
+ candidate = entry;
+ wildcardMatches = 0;
+ continue;
+ }
+
+ wildcardMatches++;
+ }
+
+ String candidatePattern = candidate.getHost();
+ // best match either has specific host or no wildcard matches
+ if ((wildcardMatches <= 0) || (isSpecificHostPattern(candidatePattern))) {
+ return candidate;
+ }
+
+ return null;
+ }
+
public static List<HostConfigEntry> updateEntriesList(List<HostConfigEntry> entries, HostConfigEntry curEntry) {
if (curEntry == null) {
return entries;
@@ -1271,6 +1008,7 @@ public class HostConfigEntry implements MutableUserHolder {
entries.add(curEntry);
return entries;
}
+
public static void writeHostConfigEntries(File file, Collection<? extends HostConfigEntry> entries) throws IOException {
writeHostConfigEntries(ValidateUtils.checkNotNull(file, "No file").toPath(), entries, IoUtils.EMPTY_OPEN_OPTIONS);
}
@@ -1420,10 +1158,10 @@ public class HostConfigEntry implements MutableUserHolder {
}
/**
- * @return The default {@link Path} location of the OpenSSH authorized keys file
+ * @return The default {@link Path} location of the OpenSSH hosts entries configuration file
*/
@SuppressWarnings("synthetic-access")
public static Path getDefaultHostConfigFile() {
- return LazyDefaultConfigFileHolder.KEYS_FILE;
+ return LazyDefaultConfigFileHolder.CONFIG_FILE;
}
}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/509c871e/sshd-core/src/main/java/org/apache/sshd/client/config/hosts/HostPatternsHolder.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/config/hosts/HostPatternsHolder.java b/sshd-core/src/main/java/org/apache/sshd/client/config/hosts/HostPatternsHolder.java
new file mode 100644
index 0000000..4878458
--- /dev/null
+++ b/sshd-core/src/main/java/org/apache/sshd/client/config/hosts/HostPatternsHolder.java
@@ -0,0 +1,310 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.sshd.client.config.hosts;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.Pair;
+import org.apache.sshd.common.util.ValidateUtils;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public abstract class HostPatternsHolder {
+
+ /**
+ * Used in a host pattern to denote zero or more consecutive characters
+ */
+ public static final char WILDCARD_PATTERN = '*';
+ public static final String ALL_HOSTS_PATTERN = String.valueOf(WILDCARD_PATTERN);
+
+ /**
+ * Used in a host pattern to denote any <U>one</U> character
+ */
+ public static final char SINGLE_CHAR_PATTERN = '?';
+
+ /**
+ * Used to negate a host pattern
+ */
+ public static final char NEGATION_CHAR_PATTERN = '!';
+
+ /**
+ * The available pattern characters
+ */
+ public static final String PATTERN_CHARS = new String(new char[]{WILDCARD_PATTERN, SINGLE_CHAR_PATTERN, NEGATION_CHAR_PATTERN});
+
+ private Collection<Pair<Pattern, Boolean>> patterns = new LinkedList<>();
+
+ protected HostPatternsHolder() {
+ super();
+ }
+
+ public Collection<Pair<Pattern, Boolean>> getPatterns() {
+ return patterns;
+ }
+
+ public void setPatterns(Collection<Pair<Pattern, Boolean>> patterns) {
+ this.patterns = patterns;
+ }
+
+ /**
+ * Checks if a given host name / address matches the entry's host pattern(s)
+ *
+ * @param host The host name / address - ignored if {@code null}/empty
+ * @return {@code true} if the name / address matches the pattern(s)
+ * @see #isHostMatch(String, Pattern)
+ */
+ public boolean isHostMatch(String host) {
+ return isHostMatch(host, getPatterns());
+ }
+
+ /**
+ * @param pattern The pattern to check - ignored if {@code null}/empty
+ * @return {@code true} if the pattern is not empty and contains no wildcard characters
+ * @see #WILDCARD_PATTERN
+ * @see #SINGLE_CHAR_PATTERN
+ * @see #SINGLE_CHAR_PATTERN
+ */
+ public static boolean isSpecificHostPattern(String pattern) {
+ if (GenericUtils.isEmpty(pattern)) {
+ return false;
+ }
+
+ for (int index = 0; index < PATTERN_CHARS.length(); index++) {
+ char ch = PATTERN_CHARS.charAt(index);
+ if (pattern.indexOf(ch) >= 0) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Locates all the matching entries for a give host name / address
+ *
+ * @param host The host name / address - ignored if {@code null}/empty
+ * @param entries The {@link HostConfigEntry}-ies to scan - ignored if {@code null}/empty
+ * @return A {@link List} of all the matching entries
+ * @see #isHostMatch(String)
+ */
+ public static List<HostConfigEntry> findMatchingEntries(String host, HostConfigEntry ... entries) {
+ // TODO in Java-8 use Stream(s) + predicate
+ if (GenericUtils.isEmpty(host) || GenericUtils.isEmpty(entries)) {
+ return Collections.emptyList();
+ } else {
+ return findMatchingEntries(host, Arrays.asList(entries));
+ }
+ }
+
+ /**
+ * Locates all the matching entries for a give host name / address
+ *
+ * @param host The host name / address - ignored if {@code null}/empty
+ * @param entries The {@link HostConfigEntry}-ies to scan - ignored if {@code null}/empty
+ * @return A {@link List} of all the matching entries
+ * @see #isHostMatch(String)
+ */
+ public static List<HostConfigEntry> findMatchingEntries(String host, Collection<? extends HostConfigEntry> entries) {
+ // TODO in Java-8 use Stream(s) + predicate
+ if (GenericUtils.isEmpty(host) || GenericUtils.isEmpty(entries)) {
+ return Collections.emptyList();
+ }
+
+ List<HostConfigEntry> matches = null;
+ for (HostConfigEntry entry : entries) {
+ if (!entry.isHostMatch(host)) {
+ continue; // debug breakpoint
+ }
+
+ if (matches == null) {
+ matches = new ArrayList<>(entries.size()); // in case ALL of them match
+ }
+
+ matches.add(entry);
+ }
+
+ if (matches == null) {
+ return Collections.emptyList();
+ } else {
+ return matches;
+ }
+ }
+
+ public static boolean isHostMatch(String host, Collection<Pair<Pattern, Boolean>> patterns) {
+ if (GenericUtils.isEmpty(patterns)) {
+ return false;
+ }
+
+ boolean matchFound = false;
+ for (Pair<Pattern, Boolean> pp : patterns) {
+ Boolean negated = pp.getSecond();
+ /*
+ * If already found a match we are interested only in negations
+ */
+ if (matchFound && (!negated.booleanValue())) {
+ continue;
+ }
+
+ if (!isHostMatch(host, pp.getFirst())) {
+ continue;
+ }
+
+ /*
+ * According to https://www.freebsd.org/cgi/man.cgi?query=ssh_config&sektion=5:
+ *
+ * If a negated entry is matched, then the Host entry is ignored,
+ * regardless of whether any other patterns on the line match.
+ */
+ if (negated.booleanValue()) {
+ return false;
+ }
+
+ matchFound = true;
+ }
+
+ return matchFound;
+ }
+
+ /**
+ * Checks if a given host name / address matches a host pattern
+ *
+ * @param host The host name / address - ignored if {@code null}/empty
+ * @param pattern The host {@link Pattern} - ignored if {@code null}
+ * @return {@code true} if the name / address matches the pattern
+ */
+ public static boolean isHostMatch(String host, Pattern pattern) {
+ if (GenericUtils.isEmpty(host) || (pattern == null)) {
+ return false;
+ }
+
+ Matcher m = pattern.matcher(host);
+ return m.matches();
+ }
+
+ public static List<Pair<Pattern, Boolean>> parsePatterns(CharSequence ... patterns) {
+ return parsePatterns(GenericUtils.isEmpty(patterns) ? Collections.<CharSequence>emptyList() : Arrays.asList(patterns));
+ }
+
+ public static List<Pair<Pattern, Boolean>> parsePatterns(Collection<? extends CharSequence> patterns) {
+ if (GenericUtils.isEmpty(patterns)) {
+ return Collections.emptyList();
+ }
+
+ List<Pair<Pattern, Boolean>> result = new ArrayList<>(patterns.size());
+ for (CharSequence p : patterns) {
+ result.add(ValidateUtils.checkNotNull(toPattern(p), "No pattern for %s", p));
+ }
+
+ return result;
+ }
+
+ /**
+ * Converts a host pattern string to a regular expression matcher.
+ * <B>Note:</B> pattern matching is <U>case insensitive</U>
+ *
+ * @param pattern The original pattern string - ignored if {@code null}/empty
+ * @return The regular expression matcher {@link Pattern} and the indication
+ * whether it is a negating pattern or not - {@code null} if no original string
+ * @see #WILDCARD_PATTERN
+ * @see #SINGLE_CHAR_PATTERN
+ * @see #NEGATION_CHAR_PATTERN
+ */
+ public static Pair<Pattern, Boolean> toPattern(CharSequence pattern) {
+ if (GenericUtils.isEmpty(pattern)) {
+ return null;
+ }
+
+ StringBuilder sb = new StringBuilder(pattern.length());
+ boolean negated = false;
+ for (int curPos = 0; curPos < pattern.length(); curPos++) {
+ char ch = pattern.charAt(curPos);
+ ValidateUtils.checkTrue(isValidPatternChar(ch), "Invalid host pattern char in %s", pattern);
+
+ switch(ch) {
+ case '.': // need to escape it
+ sb.append('\\').append(ch);
+ break;
+ case SINGLE_CHAR_PATTERN:
+ sb.append('.');
+ break;
+ case WILDCARD_PATTERN:
+ sb.append(".*");
+ break;
+ case NEGATION_CHAR_PATTERN:
+ ValidateUtils.checkTrue(!negated, "Double negation in %s", pattern);
+ ValidateUtils.checkTrue(curPos == 0, "Negation must be 1st char: %s", pattern);
+ negated = true;
+ break;
+ default:
+ sb.append(ch);
+ }
+ }
+
+ return new Pair<Pattern, Boolean>(Pattern.compile(sb.toString(), Pattern.CASE_INSENSITIVE), Boolean.valueOf(negated));
+ }
+
+ /**
+ * Checks if the given character is valid for a host pattern. Valid
+ * characters are:
+ * <UL>
+ * <LI>A-Z</LI>
+ * <LI>a-z</LI>
+ * <LI>0-9</LI>
+ * <LI>Underscore (_)</LI>
+ * <LI>Hyphen (-)</LI>
+ * <LI>Dot (.)</LI>
+ * <LI>The {@link #WILDCARD_PATTERN}</LI>
+ * <LI>The {@link #SINGLE_CHAR_PATTERN}</LI>
+ * </UL>
+ *
+ * @param ch The character to validate
+ * @return {@code true} if valid pattern character
+ */
+ public static boolean isValidPatternChar(char ch) {
+ if ((ch <= ' ') || (ch >= 0x7E)) {
+ return false;
+ }
+ if ((ch >= 'a') && (ch <= 'z')) {
+ return true;
+ }
+ if ((ch >= 'A') && (ch <= 'Z')) {
+ return true;
+ }
+ if ((ch >= '0') && (ch <= '9')) {
+ return true;
+ }
+ if ("-_.".indexOf(ch) >= 0) {
+ return true;
+ }
+ if (PATTERN_CHARS.indexOf(ch) >= 0) {
+ return true;
+ }
+ return false;
+ }
+}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/509c871e/sshd-core/src/main/java/org/apache/sshd/client/config/hosts/KnownHostDigest.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/config/hosts/KnownHostDigest.java b/sshd-core/src/main/java/org/apache/sshd/client/config/hosts/KnownHostDigest.java
new file mode 100644
index 0000000..3005f47
--- /dev/null
+++ b/sshd-core/src/main/java/org/apache/sshd/client/config/hosts/KnownHostDigest.java
@@ -0,0 +1,65 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.sshd.client.config.hosts;
+
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.Set;
+
+import org.apache.sshd.common.Factory;
+import org.apache.sshd.common.NamedFactory;
+import org.apache.sshd.common.NamedResource;
+import org.apache.sshd.common.mac.BuiltinMacs;
+import org.apache.sshd.common.mac.Mac;
+import org.apache.sshd.common.util.ValidateUtils;
+
+/**
+ * Available digesters for known hosts entries
+ *
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public enum KnownHostDigest implements NamedFactory<Mac> {
+ SHA1("1", BuiltinMacs.hmacsha1);
+
+ public static final Set<KnownHostDigest> VALUES =
+ Collections.unmodifiableSet(EnumSet.allOf(KnownHostDigest.class));
+
+ private final String name;
+ private final Factory<Mac> factory;
+
+ KnownHostDigest(String name, Factory<Mac> factory) {
+ this.name = ValidateUtils.checkNotNullAndNotEmpty(name, "No name");
+ this.factory = ValidateUtils.checkNotNull(factory, "No factory");
+ }
+
+ @Override
+ public String getName() {
+ return name;
+ }
+
+ @Override
+ public Mac create() {
+ return factory.create();
+ }
+
+ public static KnownHostDigest fromName(String name) {
+ return NamedResource.Utils.findByName(name, String.CASE_INSENSITIVE_ORDER, VALUES);
+ }
+}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/509c871e/sshd-core/src/main/java/org/apache/sshd/client/config/hosts/KnownHostEntry.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/config/hosts/KnownHostEntry.java b/sshd-core/src/main/java/org/apache/sshd/client/config/hosts/KnownHostEntry.java
new file mode 100644
index 0000000..da77bac
--- /dev/null
+++ b/sshd-core/src/main/java/org/apache/sshd/client/config/hosts/KnownHostEntry.java
@@ -0,0 +1,271 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.sshd.client.config.hosts;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.io.StreamCorruptedException;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.OpenOption;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.sshd.common.config.SshConfigFileReader;
+import org.apache.sshd.common.config.keys.AuthorizedKeyEntry;
+import org.apache.sshd.common.config.keys.PublicKeyEntry;
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.ValidateUtils;
+import org.apache.sshd.common.util.io.IoUtils;
+import org.apache.sshd.common.util.io.NoCloseInputStream;
+import org.apache.sshd.common.util.io.NoCloseReader;
+
+/**
+ * Contains a representation of an entry in the <code>known_hosts</code> file
+ *
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ * @see <A HREF="http://www.manpagez.com/man/8/sshd/">sshd(8) man page</A>
+ */
+public class KnownHostEntry extends HostPatternsHolder {
+ /**
+ * Character that denotes that start of a marker
+ */
+ public static final char MARKER_INDICATOR = '@';
+
+ /**
+ * Standard OpenSSH config file name
+ */
+ public static final String STD_HOSTS_FILENAME = "known_hosts";
+
+ private static final class LazyDefaultConfigFileHolder {
+ private static final Path HOSTS_FILE = PublicKeyEntry.getDefaultKeysFolderPath().resolve(STD_HOSTS_FILENAME);
+ }
+
+ private String line;
+ private String marker;
+ private AuthorizedKeyEntry keyEntry;
+ private KnownHostHashValue hashedEntry;
+
+ public KnownHostEntry() {
+ super();
+ }
+
+ /**
+ * @param line The original line from which this entry was created
+ */
+ public KnownHostEntry(String line) {
+ this.line = line;
+ }
+
+ /**
+ * @return The original line from which this entry was created
+ */
+ public String getConfigLine() {
+ return line;
+ }
+
+ public void setConfigLine(String line) {
+ this.line = line;
+ }
+
+ public String getMarker() {
+ return marker;
+ }
+
+ public void setMarker(String marker) {
+ this.marker = marker;
+ }
+
+ public AuthorizedKeyEntry getKeyEntry() {
+ return keyEntry;
+ }
+
+ public void setKeyEntry(AuthorizedKeyEntry keyEntry) {
+ this.keyEntry = keyEntry;
+ }
+
+ public KnownHostHashValue getHashedEntry() {
+ return hashedEntry;
+ }
+
+ public void setHashedEntry(KnownHostHashValue hashedEntry) {
+ this.hashedEntry = hashedEntry;
+ }
+
+ @Override
+ public boolean isHostMatch(String host) {
+ if (super.isHostMatch(host)) {
+ return true;
+ }
+
+ KnownHostHashValue hash = getHashedEntry();
+ return (hash != null) && hash.isHostMatch(host);
+ }
+
+ @Override
+ public String toString() {
+ return getConfigLine();
+ }
+
+ /**
+ * @return The default {@link Path} location of the OpenSSH known hosts file
+ */
+ @SuppressWarnings("synthetic-access")
+ public static Path getDefaultKnownHostsFile() {
+ return LazyDefaultConfigFileHolder.HOSTS_FILE;
+ }
+
+ public static List<KnownHostEntry> readKnownHostEntries(File file) throws IOException {
+ return readKnownHostEntries(file.toPath(), IoUtils.EMPTY_OPEN_OPTIONS);
+ }
+
+ public static List<KnownHostEntry> readKnownHostEntries(Path path, OpenOption... options) throws IOException {
+ try (InputStream input = Files.newInputStream(path, options)) {
+ return readKnownHostEntries(input, true);
+ }
+ }
+
+ public static List<KnownHostEntry> readKnownHostEntries(URL url) throws IOException {
+ try (InputStream input = url.openStream()) {
+ return readKnownHostEntries(input, true);
+ }
+ }
+
+ public static List<KnownHostEntry> readKnownHostEntries(String filePath) throws IOException {
+ try (InputStream inStream = new FileInputStream(filePath)) {
+ return readKnownHostEntries(inStream, true);
+ }
+ }
+
+ public static List<KnownHostEntry> readKnownHostEntries(InputStream inStream, boolean okToClose) throws IOException {
+ try (Reader reader = new InputStreamReader(NoCloseInputStream.resolveInputStream(inStream, okToClose), StandardCharsets.UTF_8)) {
+ return readKnownHostEntries(reader, true);
+ }
+ }
+
+ public static List<KnownHostEntry> readKnownHostEntries(Reader rdr, boolean okToClose) throws IOException {
+ try (BufferedReader buf = new BufferedReader(NoCloseReader.resolveReader(rdr, okToClose))) {
+ return readKnownHostEntries(buf);
+ }
+ }
+
+ /**
+ * Reads configuration entries
+ *
+ * @param rdr The {@link BufferedReader} to use
+ * @return The {@link List} of read {@link KnownHostEntry}-ies
+ * @throws IOException If failed to parse the read configuration
+ */
+ public static List<KnownHostEntry> readKnownHostEntries(BufferedReader rdr) throws IOException {
+ List<KnownHostEntry> entries = null;
+
+ int lineNumber = 1;
+ for (String line = rdr.readLine(); line != null; line = rdr.readLine(), lineNumber++) {
+ line = GenericUtils.trimToEmpty(line);
+ if (GenericUtils.isEmpty(line)) {
+ continue;
+ }
+
+ int pos = line.indexOf(SshConfigFileReader.COMMENT_CHAR);
+ if (pos == 0) {
+ continue;
+ }
+
+ if (pos > 0) {
+ line = line.substring(0, pos);
+ line = line.trim();
+ }
+
+ try {
+ KnownHostEntry entry = parseKnownHostEntry(line);
+ if (entry == null) {
+ continue;
+ }
+
+ if (entries == null) {
+ entries = new ArrayList<>();
+ }
+ entries.add(entry);
+ } catch (RuntimeException | Error e) { // TODO consider consulting a user callback
+ throw new StreamCorruptedException("Failed (" + e.getClass().getSimpleName() + ")"
+ + " to parse line #" + lineNumber + " '" + line + "': " + e.getMessage());
+ }
+ }
+
+ if (entries == null) {
+ return Collections.emptyList();
+ } else {
+ return entries;
+ }
+ }
+
+ public static KnownHostEntry parseKnownHostEntry(String line) {
+ return parseKnownHostEntry(GenericUtils.isEmpty(line) ? null : new KnownHostEntry(), line);
+ }
+
+ public static <E extends KnownHostEntry> E parseKnownHostEntry(E entry, String data) {
+ String line = data;
+ if (GenericUtils.isEmpty(line) || (line.charAt(0) == PublicKeyEntry.COMMENT_CHAR)) {
+ return entry;
+ }
+
+ entry.setConfigLine(line);
+
+ if (line.charAt(0) == MARKER_INDICATOR) {
+ int pos = line.indexOf(' ');
+ ValidateUtils.checkTrue(pos > 0, "Missing marker name end delimiter in line=%s", data);
+ ValidateUtils.checkTrue(pos > 1, "No marker name after indicator in line=%s", data);
+ entry.setMarker(line.substring(1, pos));
+ line = line.substring(pos + 1).trim();
+ } else {
+ entry.setMarker(null);
+ }
+
+ int pos = line.indexOf(' ');
+ ValidateUtils.checkTrue(pos > 0, "Missing host patterns end delimiter in line=%s", data);
+ String hostPattern = line.substring(0, pos);
+ line = line.substring(pos + 1).trim();
+
+ if (hostPattern.charAt(0) == KnownHostHashValue.HASHED_HOST_DELIMITER) {
+ KnownHostHashValue hash =
+ ValidateUtils.checkNotNull(KnownHostHashValue.parse(hostPattern),
+ "Failed to extract host hash value from line=%s", data);
+ entry.setHashedEntry(hash);
+ entry.setPatterns(null);
+ } else {
+ entry.setHashedEntry(null);
+ entry.setPatterns(parsePatterns(GenericUtils.split(hostPattern, ',')));
+ }
+
+ AuthorizedKeyEntry key =
+ ValidateUtils.checkNotNull(AuthorizedKeyEntry.parseAuthorizedKeyEntry(line),
+ "No valid key entry recovered from line=%s", data);
+ entry.setKeyEntry(key);
+ return entry;
+ }
+}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/509c871e/sshd-core/src/main/java/org/apache/sshd/client/config/hosts/KnownHostHashValue.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/config/hosts/KnownHostHashValue.java b/sshd-core/src/main/java/org/apache/sshd/client/config/hosts/KnownHostHashValue.java
new file mode 100644
index 0000000..f5fa825
--- /dev/null
+++ b/sshd-core/src/main/java/org/apache/sshd/client/config/hosts/KnownHostHashValue.java
@@ -0,0 +1,166 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.sshd.client.config.hosts;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.Objects;
+
+import org.apache.sshd.common.Factory;
+import org.apache.sshd.common.NamedFactory;
+import org.apache.sshd.common.NamedResource;
+import org.apache.sshd.common.RuntimeSshException;
+import org.apache.sshd.common.mac.Mac;
+import org.apache.sshd.common.util.Base64;
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.NumberUtils;
+import org.apache.sshd.common.util.ValidateUtils;
+import org.apache.sshd.common.util.buffer.BufferUtils;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public class KnownHostHashValue {
+ /**
+ * Character used to indicate a hashed host pattern
+ */
+ public static final char HASHED_HOST_DELIMITER = '|';
+
+ public static final NamedFactory<Mac> DEFAULT_DIGEST = KnownHostDigest.SHA1;
+
+ private NamedFactory<Mac> digester = DEFAULT_DIGEST;
+ private byte[] saltValue;
+ private byte[] digestValue;
+
+ public KnownHostHashValue() {
+ super();
+ }
+
+ public NamedFactory<Mac> getDigester() {
+ return digester;
+ }
+
+ public void setDigester(NamedFactory<Mac> digester) {
+ this.digester = digester;
+ }
+
+ public byte[] getSaltValue() {
+ return saltValue;
+ }
+
+ public void setSaltValue(byte[] saltValue) {
+ this.saltValue = saltValue;
+ }
+
+ public byte[] getDigestValue() {
+ return digestValue;
+ }
+
+ public void setDigestValue(byte[] digestValue) {
+ this.digestValue = digestValue;
+ }
+
+ /**
+ * Checks if the host matches the hash
+ *
+ * @param host The host name/address - ignored if {@code null}/empty
+ * @return {@code true} if host matches the hash
+ * @throws RuntimeException If entry not properly initialized
+ */
+ public boolean isHostMatch(String host) {
+ if (GenericUtils.isEmpty(host)) {
+ return false;
+ }
+
+ try {
+ byte[] expected = getDigestValue();
+ byte[] actual = calculateHashValue(host, getDigester(), getSaltValue());
+ return Arrays.equals(expected, actual);
+ } catch (Throwable t) {
+ if (t instanceof RuntimeException) {
+ throw (RuntimeException) t;
+ }
+ throw new RuntimeSshException("Failed (" + t.getClass().getSimpleName() + ")"
+ + " to calculate hash value: " + t.getMessage(), t);
+ }
+ }
+
+ @Override
+ public String toString() {
+ if ((getDigester() == null) || NumberUtils.isEmpty(getSaltValue()) || NumberUtils.isEmpty(getDigestValue())) {
+ return Objects.toString(getDigester(), null)
+ + "-" + BufferUtils.toHex(':', getSaltValue())
+ + "-" + BufferUtils.toHex(':', getDigestValue());
+ }
+
+ try {
+ return append(new StringBuilder(Byte.MAX_VALUE), this).toString();
+ } catch (IOException | RuntimeException e) { // unexpected
+ return e.getClass().getSimpleName() + ": " + e.getMessage();
+ }
+ }
+
+ // see http://nms.lcs.mit.edu/projects/ssh/README.hashed-hosts
+ public static byte[] calculateHashValue(String host, Factory<? extends Mac> factory, byte[] salt) throws Exception {
+ return calculateHashValue(host, factory.create(), salt);
+ }
+
+ public static byte[] calculateHashValue(String host, Mac mac, byte[] salt) throws Exception {
+ mac.init(salt);
+
+ byte[] hostBytes = host.getBytes(StandardCharsets.UTF_8);
+ mac.update(hostBytes);
+ return mac.doFinal();
+ }
+
+ public static <A extends Appendable> A append(A sb, KnownHostHashValue hashValue) throws IOException {
+ return (hashValue == null) ? sb : append(sb, hashValue.getDigester(), hashValue.getSaltValue(), hashValue.getDigestValue());
+ }
+
+ public static <A extends Appendable> A append(A sb, NamedResource factory, byte[] salt, byte[] digest) throws IOException {
+ sb.append(HASHED_HOST_DELIMITER).append(factory.getName());
+ sb.append(HASHED_HOST_DELIMITER).append(Base64.encodeToString(salt));
+ sb.append(HASHED_HOST_DELIMITER).append(Base64.encodeToString(digest));
+ return sb;
+ }
+
+ public static KnownHostHashValue parse(String pattern) {
+ return parse(pattern, GenericUtils.isEmpty(pattern) ? null : new KnownHostHashValue());
+ }
+
+ public static <V extends KnownHostHashValue> V parse(String pattern, V value) {
+ if (GenericUtils.isEmpty(pattern)) {
+ return value;
+ }
+
+ String[] components = GenericUtils.split(pattern, HASHED_HOST_DELIMITER);
+ ValidateUtils.checkTrue(components.length == 4 /* 1st one is empty */, "Invalid hash pattern (insufficient data): %s", pattern);
+ ValidateUtils.checkTrue(GenericUtils.isEmpty(components[0]), "Invalid hash pattern (unexpected extra data): %s", pattern);
+
+ NamedFactory<Mac> factory =
+ ValidateUtils.checkNotNull(KnownHostDigest.fromName(components[1]),
+ "Invalid hash pattern (unknwon digest): %s", pattern);
+ value.setDigester(factory);
+ value.setSaltValue(Base64.decodeString(components[2]));
+ value.setDigestValue(Base64.decodeString(components[3]));
+ return value;
+ }
+}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/509c871e/sshd-core/src/main/java/org/apache/sshd/client/keyverifier/DefaultKnownHostsServerKeyVerifier.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/keyverifier/DefaultKnownHostsServerKeyVerifier.java b/sshd-core/src/main/java/org/apache/sshd/client/keyverifier/DefaultKnownHostsServerKeyVerifier.java
new file mode 100644
index 0000000..f10645b
--- /dev/null
+++ b/sshd-core/src/main/java/org/apache/sshd/client/keyverifier/DefaultKnownHostsServerKeyVerifier.java
@@ -0,0 +1,89 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.sshd.client.keyverifier;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.LinkOption;
+import java.nio.file.Path;
+import java.security.GeneralSecurityException;
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.sshd.client.config.hosts.KnownHostEntry;
+import org.apache.sshd.common.util.Pair;
+import org.apache.sshd.common.util.ValidateUtils;
+import org.apache.sshd.common.util.io.IoUtils;
+
+/**
+ * Monitors the {@code ~/.ssh/known_hosts} file of the user currently running
+ * the client, updating and re-loading it if necessary. It also (optionally)
+ * enforces the same permissions regime as {@code OpenSSH}.
+ *
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public class DefaultKnownHostsServerKeyVerifier extends KnownHostsServerKeyVerifier {
+ private final boolean strict;
+
+ public DefaultKnownHostsServerKeyVerifier(ServerKeyVerifier delegate) {
+ this(delegate, true);
+ }
+
+ public DefaultKnownHostsServerKeyVerifier(ServerKeyVerifier delegate, boolean strict) {
+ this(delegate, strict, KnownHostEntry.getDefaultKnownHostsFile(), IoUtils.getLinkOptions(false));
+ }
+
+ public DefaultKnownHostsServerKeyVerifier(ServerKeyVerifier delegate, boolean strict, File file) {
+ this(delegate, strict, ValidateUtils.checkNotNull(file, "No file provided").toPath(), IoUtils.getLinkOptions(false));
+ }
+
+ public DefaultKnownHostsServerKeyVerifier(ServerKeyVerifier delegate, boolean strict, Path file, LinkOption... options) {
+ super(delegate, file, options);
+ this.strict = strict;
+ }
+
+ /**
+ * @return If {@code true} then makes sure that the containing folder
+ * has 0700 access and the file 0644. <B>Note:</B> for <I>Windows</I> it
+ * does not check these permissions
+ * @see #validateStrictConfigFilePermissions(Path, LinkOption...)
+ */
+ public final boolean isStrict() {
+ return strict;
+ }
+
+ @Override
+ protected List<HostEntryPair> reloadKnownHosts(Path file) throws IOException, GeneralSecurityException {
+ if (isStrict()) {
+ if (log.isDebugEnabled()) {
+ log.debug("reloadKnownHosts({}) check permissions", file);
+ }
+
+ Pair<String, Object> violation = validateStrictConfigFilePermissions(file);
+ if (violation != null) {
+ log.warn("reloadKnownHosts({}) invalid file permissions: {}", file, violation.getFirst());
+ updateReloadAttributes();
+ return Collections.emptyList();
+ }
+ }
+
+ return super.reloadKnownHosts(file);
+ }
+}