[SSHD-747] Add support for non-standard port specification in known_hosts file
Project: http://git-wip-us.apache.org/repos/asf/mina-sshd/repo Commit: http://git-wip-us.apache.org/repos/asf/mina-sshd/commit/2119f798 Tree: http://git-wip-us.apache.org/repos/asf/mina-sshd/tree/2119f798 Diff: http://git-wip-us.apache.org/repos/asf/mina-sshd/diff/2119f798 Branch: refs/heads/master Commit: 2119f7984ac0eceaca10c6ace8321ab506490657 Parents: f2798ec Author: Goldstein Lyor <l...@c-b4.com> Authored: Sun May 14 12:17:02 2017 +0300 Committer: Lyor Goldstein <lyor.goldst...@gmail.com> Committed: Mon May 15 19:38:22 2017 +0300 ---------------------------------------------------------------------- .../client/config/hosts/HostPatternValue.java | 97 ++++++++++++++++++++ .../client/config/hosts/HostPatternsHolder.java | 70 ++++++++++---- .../client/config/hosts/KnownHostEntry.java | 4 +- .../KnownHostsServerKeyVerifier.java | 46 ++++++---- .../apache/sshd/common/util/GenericUtils.java | 24 +++++ .../sshd/common/util/net/SshdSocketAddress.java | 65 +++++++++++-- .../config/hosts/HostConfigEntryTest.java | 17 ++-- .../config/hosts/KnownHostHashValueTest.java | 3 + .../KnownHostsServerKeyVerifierTest.java | 96 ++++++++++--------- .../apache/sshd/client/keyverifier/known_hosts | 5 +- 10 files changed, 329 insertions(+), 98 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/2119f798/sshd-core/src/main/java/org/apache/sshd/client/config/hosts/HostPatternValue.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/main/java/org/apache/sshd/client/config/hosts/HostPatternValue.java b/sshd-core/src/main/java/org/apache/sshd/client/config/hosts/HostPatternValue.java new file mode 100644 index 0000000..20d682f --- /dev/null +++ b/sshd-core/src/main/java/org/apache/sshd/client/config/hosts/HostPatternValue.java @@ -0,0 +1,97 @@ +/* + * 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.regex.Pattern; + +import org.apache.sshd.common.util.GenericUtils; + +/** + * Represents a pattern definition in the <U>known_hosts</U> file + * @author <a href="mailto:d...@mina.apache.org">Apache MINA SSHD Project</a> + * @see <A HREF="https://en.wikibooks.org/wiki/OpenSSH/Client_Configuration_Files#About_the_Contents_of_the_known_hosts_Files"> + * OpenSSH cookbook - About the Contents of the known hosts Files</A> + */ +public class HostPatternValue { + private Pattern pattern; + private int port; + private boolean negated; + + public HostPatternValue() { + super(); + } + + public HostPatternValue(Pattern pattern, boolean negated) { + this(pattern, 0, negated); + } + + public HostPatternValue(Pattern pattern, int port, boolean negated) { + this.pattern = pattern; + this.port = port; + this.negated = negated; + } + + public Pattern getPattern() { + return pattern; + } + + public void setPattern(Pattern pattern) { + this.pattern = pattern; + } + + public int getPort() { + return port; + } + + public void setPort(int port) { + this.port = port; + } + + public boolean isNegated() { + return negated; + } + + public void setNegated(boolean negated) { + this.negated = negated; + } + + @Override + public String toString() { + Pattern p = getPattern(); + String purePattern = (p == null) ? null : p.pattern(); + StringBuilder sb = new StringBuilder(GenericUtils.length(purePattern) + Short.SIZE); + if (isNegated()) { + sb.append(HostPatternsHolder.NEGATION_CHAR_PATTERN); + } + + int portValue = getPort(); + if (portValue > 0) { + sb.append(HostPatternsHolder.NON_STANDARD_PORT_PATTERN_ENCLOSURE_START_DELIM); + } + sb.append(purePattern); + if (portValue > 0) { + sb.append(HostPatternsHolder.NON_STANDARD_PORT_PATTERN_ENCLOSURE_END_DELIM); + sb.append(HostPatternsHolder.PORT_VALUE_DELIMITER); + sb.append(portValue); + } + + return sb.toString(); + } +} http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/2119f798/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 index a3e20ab..eff34d9 100644 --- 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 @@ -29,7 +29,6 @@ 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; /** @@ -58,17 +57,26 @@ public abstract class HostPatternsHolder { */ 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<>(); + /** Port value separator if non-standard port pattern used */ + public static final char PORT_VALUE_DELIMITER = ':'; + + /** Non-standard port specification host pattern enclosure start delimiter */ + public static final char NON_STANDARD_PORT_PATTERN_ENCLOSURE_START_DELIM = '['; + + /** Non-standard port specification host pattern enclosure end delimiter */ + public static final char NON_STANDARD_PORT_PATTERN_ENCLOSURE_END_DELIM = ']'; + + private Collection<HostPatternValue> patterns = new LinkedList<>(); protected HostPatternsHolder() { super(); } - public Collection<Pair<Pattern, Boolean>> getPatterns() { + public Collection<HostPatternValue> getPatterns() { return patterns; } - public void setPatterns(Collection<Pair<Pattern, Boolean>> patterns) { + public void setPatterns(Collection<HostPatternValue> patterns) { this.patterns = patterns; } @@ -76,11 +84,12 @@ public abstract class HostPatternsHolder { * 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 + * @param port The connection port * @return {@code true} if the name / address matches the pattern(s) * @see #isHostMatch(String, Pattern) */ - public boolean isHostMatch(String host) { - return isHostMatch(host, getPatterns()); + public boolean isHostMatch(String host, int port) { + return isHostMatch(host, port, getPatterns()); } /** @@ -138,7 +147,7 @@ public abstract class HostPatternsHolder { List<HostConfigEntry> matches = null; for (HostConfigEntry entry : entries) { - if (!entry.isHostMatch(host)) { + if (!entry.isHostMatch(host, 0 /* any port */)) { continue; // debug breakpoint } @@ -156,14 +165,14 @@ public abstract class HostPatternsHolder { } } - public static boolean isHostMatch(String host, Collection<Pair<Pattern, Boolean>> patterns) { + public static boolean isHostMatch(String host, int port, Collection<HostPatternValue> patterns) { if (GenericUtils.isEmpty(patterns)) { return false; } boolean matchFound = false; - for (Pair<Pattern, Boolean> pp : patterns) { - Boolean negated = pp.getSecond(); + for (HostPatternValue pv : patterns) { + boolean negated = pv.isNegated(); /* * If already found a match we are interested only in negations */ @@ -171,7 +180,7 @@ public abstract class HostPatternsHolder { continue; } - if (!isHostMatch(host, pp.getFirst())) { + if (!isHostMatch(host, pv.getPattern())) { continue; } @@ -185,6 +194,11 @@ public abstract class HostPatternsHolder { return false; } + int pvPort = pv.getPort(); + if ((pvPort != 0) && (port != 0) && (pvPort != port)) { + continue; + } + matchFound = true; } @@ -207,16 +221,16 @@ public abstract class HostPatternsHolder { return m.matches(); } - public static List<Pair<Pattern, Boolean>> parsePatterns(CharSequence... patterns) { + public static List<HostPatternValue> parsePatterns(CharSequence... patterns) { return parsePatterns(GenericUtils.isEmpty(patterns) ? Collections.emptyList() : Arrays.asList(patterns)); } - public static List<Pair<Pattern, Boolean>> parsePatterns(Collection<? extends CharSequence> patterns) { + public static List<HostPatternValue> parsePatterns(Collection<? extends CharSequence> patterns) { if (GenericUtils.isEmpty(patterns)) { return Collections.emptyList(); } - List<Pair<Pattern, Boolean>> result = new ArrayList<>(patterns.size()); + List<HostPatternValue> result = new ArrayList<>(patterns.size()); for (CharSequence p : patterns) { result.add(ValidateUtils.checkNotNull(toPattern(p), "No pattern for %s", p)); } @@ -231,18 +245,38 @@ public abstract class HostPatternsHolder { * @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 #NON_STANDARD_PORT_PATTERN_ENCLOSURE_START_DELIM + * @see #NON_STANDARD_PORT_PATTERN_ENCLOSURE_END_DELIM * @see #WILDCARD_PATTERN * @see #SINGLE_CHAR_PATTERN * @see #NEGATION_CHAR_PATTERN */ - public static Pair<Pattern, Boolean> toPattern(CharSequence pattern) { + public static HostPatternValue toPattern(CharSequence pattern) { if (GenericUtils.isEmpty(pattern)) { return null; } - StringBuilder sb = new StringBuilder(pattern.length()); + int patternLen = pattern.length(); + int port = 0; + // Check if non-standard port value used + StringBuilder sb = new StringBuilder(patternLen); + if (pattern.charAt(0) == HostPatternsHolder.NON_STANDARD_PORT_PATTERN_ENCLOSURE_START_DELIM) { + int pos = GenericUtils.lastIndexOf(pattern, HostPatternsHolder.PORT_VALUE_DELIMITER); + ValidateUtils.checkTrue(pos > 0, "Missing non-standard port value delimiter in %s", pattern); + ValidateUtils.checkTrue(pos < (patternLen - 1), "Missing non-standard port value number in %s", pattern); + ValidateUtils.checkTrue(pattern.charAt(pos - 1) == HostPatternsHolder.NON_STANDARD_PORT_PATTERN_ENCLOSURE_END_DELIM, + "Invalid non-standard port value host pattern enclosure delimiters in %s", pattern); + + CharSequence csPort = pattern.subSequence(pos + 1, patternLen); + port = Integer.parseInt(csPort.toString()); + ValidateUtils.checkTrue((port > 0) && (port <= 0xFFFF), "Invalid non-start port value (%d) in %s", port, pattern); + + pattern = pattern.subSequence(1, pos - 1); + patternLen = pattern.length(); + } + boolean negated = false; - for (int curPos = 0; curPos < pattern.length(); curPos++) { + for (int curPos = 0; curPos < patternLen; curPos++) { char ch = pattern.charAt(curPos); ValidateUtils.checkTrue(isValidPatternChar(ch), "Invalid host pattern char in %s", pattern); @@ -266,7 +300,7 @@ public abstract class HostPatternsHolder { } } - return new Pair<>(Pattern.compile(sb.toString(), Pattern.CASE_INSENSITIVE), negated); + return new HostPatternValue(Pattern.compile(sb.toString(), Pattern.CASE_INSENSITIVE), port, negated); } /** http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/2119f798/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 index da77bac..bf56ddf 100644 --- 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 @@ -118,8 +118,8 @@ public class KnownHostEntry extends HostPatternsHolder { } @Override - public boolean isHostMatch(String host) { - if (super.isHostMatch(host)) { + public boolean isHostMatch(String host, int port) { + if (super.isHostMatch(host, port)) { return true; } http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/2119f798/sshd-core/src/main/java/org/apache/sshd/client/keyverifier/KnownHostsServerKeyVerifier.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/main/java/org/apache/sshd/client/keyverifier/KnownHostsServerKeyVerifier.java b/sshd-core/src/main/java/org/apache/sshd/client/keyverifier/KnownHostsServerKeyVerifier.java index 48f01bf..8dd6ca6 100644 --- a/sshd-core/src/main/java/org/apache/sshd/client/keyverifier/KnownHostsServerKeyVerifier.java +++ b/sshd-core/src/main/java/org/apache/sshd/client/keyverifier/KnownHostsServerKeyVerifier.java @@ -39,6 +39,7 @@ import java.util.Objects; import java.util.TreeSet; import java.util.concurrent.atomic.AtomicReference; +import org.apache.sshd.client.config.hosts.HostPatternsHolder; import org.apache.sshd.client.config.hosts.KnownHostEntry; import org.apache.sshd.client.config.hosts.KnownHostHashValue; import org.apache.sshd.client.session.ClientSession; @@ -490,7 +491,7 @@ public class KnownHostsServerKeyVerifier return null; } - Collection<String> candidates = resolveHostNetworkIdentities(clientSession, remoteAddress); + Collection<SshdSocketAddress> candidates = resolveHostNetworkIdentities(clientSession, remoteAddress); if (log.isDebugEnabled()) { log.debug("findKnownHostEntry({})[{}] host network identities: {}", clientSession, remoteAddress, candidates); @@ -502,9 +503,9 @@ public class KnownHostsServerKeyVerifier for (HostEntryPair match : knownHosts) { KnownHostEntry entry = match.getHostEntry(); - for (String host : candidates) { + for (SshdSocketAddress host : candidates) { try { - if (entry.isHostMatch(host)) { + if (entry.isHostMatch(host.getHostName(), host.getPort())) { if (log.isDebugEnabled()) { log.debug("findKnownHostEntry({})[{}] matched host={} for entry={}", clientSession, remoteAddress, host, entry); @@ -620,8 +621,9 @@ public class KnownHostsServerKeyVerifier * will be called in order to enable recovery of its data * @see #resetReloadAttributes() */ - protected KnownHostEntry updateKnownHostsFile(ClientSession clientSession, SocketAddress remoteAddress, PublicKey serverKey, - Path file, Collection<HostEntryPair> knownHosts) throws Exception { + protected KnownHostEntry updateKnownHostsFile( + ClientSession clientSession, SocketAddress remoteAddress, PublicKey serverKey, Path file, Collection<HostEntryPair> knownHosts) + throws Exception { KnownHostEntry entry = prepareKnownHostEntry(clientSession, remoteAddress, serverKey); if (entry == null) { if (log.isDebugEnabled()) { @@ -665,14 +667,14 @@ public class KnownHostsServerKeyVerifier * @see KnownHostEntry#getConfigLine() */ protected KnownHostEntry prepareKnownHostEntry(ClientSession clientSession, SocketAddress remoteAddress, PublicKey serverKey) throws Exception { - Collection<String> patterns = resolveHostNetworkIdentities(clientSession, remoteAddress); + Collection<SshdSocketAddress> patterns = resolveHostNetworkIdentities(clientSession, remoteAddress); if (GenericUtils.isEmpty(patterns)) { return null; } StringBuilder sb = new StringBuilder(Byte.MAX_VALUE); Random rnd = null; - for (String hostIdentity : patterns) { + for (SshdSocketAddress hostIdentity : patterns) { if (sb.length() > 0) { sb.append(','); } @@ -692,10 +694,20 @@ public class KnownHostsServerKeyVerifier byte[] salt = new byte[blockSize]; rnd.fill(salt); - byte[] digestValue = KnownHostHashValue.calculateHashValue(hostIdentity, mac, salt); + byte[] digestValue = KnownHostHashValue.calculateHashValue(hostIdentity.getHostName(), mac, salt); KnownHostHashValue.append(sb, digester, salt, digestValue); } else { - sb.append(hostIdentity); + int portValue = hostIdentity.getPort(); + boolean nonDefaultPort = (portValue > 0) && (portValue != SshConfigFileReader.DEFAULT_PORT); + if (nonDefaultPort) { + sb.append(HostPatternsHolder.NON_STANDARD_PORT_PATTERN_ENCLOSURE_START_DELIM); + } + sb.append(hostIdentity.getHostName()); + if (nonDefaultPort) { + sb.append(HostPatternsHolder.NON_STANDARD_PORT_PATTERN_ENCLOSURE_END_DELIM); + sb.append(HostPatternsHolder.PORT_VALUE_DELIMITER); + sb.append(portValue); + } } } @@ -713,7 +725,7 @@ public class KnownHostsServerKeyVerifier * @param hostIdentity The entry's host name/address * @return The digester {@link NamedFactory} - {@code null} if no hashing is to be made */ - protected NamedFactory<Mac> getHostValueDigester(ClientSession clientSession, SocketAddress remoteAddress, String hostIdentity) { + protected NamedFactory<Mac> getHostValueDigester(ClientSession clientSession, SocketAddress remoteAddress, SshdSocketAddress hostIdentity) { return null; } @@ -724,20 +736,20 @@ public class KnownHostsServerKeyVerifier * * @param clientSession The {@link ClientSession} * @param remoteAddress The remote host address - * @return A {@link Collection} of the names/addresses to use - if {@code null}/empty - * then ignored (i.e., no matching is done or no entry is generated) + * @return A {@link Collection} of the {@code InetSocketAddress}-es to use - if + * {@code null}/empty then ignored (i.e., no matching is done or no entry is generated) * @see ClientSession#getConnectAddress() - * @see SshdSocketAddress#toAddressString(SocketAddress) + * @see SshdSocketAddress#toSshdSocketAddress(SocketAddress) */ - protected Collection<String> resolveHostNetworkIdentities(ClientSession clientSession, SocketAddress remoteAddress) { + protected Collection<SshdSocketAddress> resolveHostNetworkIdentities(ClientSession clientSession, SocketAddress remoteAddress) { /* * NOTE !!! we do not resolve the fully-qualified name to avoid long DNS timeouts. * Instead we use the reported peer address and the original connection target host */ - Collection<String> candidates = new TreeSet<>(String.CASE_INSENSITIVE_ORDER); - candidates.add(SshdSocketAddress.toAddressString(remoteAddress)); + Collection<SshdSocketAddress> candidates = new TreeSet<>(SshdSocketAddress.BY_HOST_AND_PORT); + candidates.add(SshdSocketAddress.toSshdSocketAddress(remoteAddress)); SocketAddress connectAddress = clientSession.getConnectAddress(); - candidates.add(SshdSocketAddress.toAddressString(connectAddress)); + candidates.add(SshdSocketAddress.toSshdSocketAddress(connectAddress)); return candidates; } http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/2119f798/sshd-core/src/main/java/org/apache/sshd/common/util/GenericUtils.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/main/java/org/apache/sshd/common/util/GenericUtils.java b/sshd-core/src/main/java/org/apache/sshd/common/util/GenericUtils.java index e2f8733..1f56fec 100644 --- a/sshd-core/src/main/java/org/apache/sshd/common/util/GenericUtils.java +++ b/sshd-core/src/main/java/org/apache/sshd/common/util/GenericUtils.java @@ -156,6 +156,30 @@ public final class GenericUtils { return !isEmpty(cs); } + public static int indexOf(CharSequence cs, char c) { + int len = length(cs); + for (int pos = 0; pos < len; pos++) { + char ch = cs.charAt(pos); + if (ch == c) { + return pos; + } + } + + return -1; + } + + public static int lastIndexOf(CharSequence cs, char c) { + int len = length(cs); + for (int pos = len - 1; pos >= 0; pos--) { + char ch = cs.charAt(pos); + if (ch == c) { + return pos; + } + } + + return -1; + } + // a List would be better, but we want to be compatible with String.split(...) public static String[] split(String s, char ch) { if (isEmpty(s)) { http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/2119f798/sshd-core/src/main/java/org/apache/sshd/common/util/net/SshdSocketAddress.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/main/java/org/apache/sshd/common/util/net/SshdSocketAddress.java b/sshd-core/src/main/java/org/apache/sshd/common/util/net/SshdSocketAddress.java index c853e88..4919966 100644 --- a/sshd-core/src/main/java/org/apache/sshd/common/util/net/SshdSocketAddress.java +++ b/sshd-core/src/main/java/org/apache/sshd/common/util/net/SshdSocketAddress.java @@ -78,6 +78,8 @@ public class SshdSocketAddress extends SocketAddress { /** * Compares {@link InetAddress}-es according to their {@link InetAddress#getHostAddress()} * value case <U>insensitive</U> + * + * @see #toAddressString(InetAddress) */ public static final Comparator<InetAddress> BY_HOST_ADDRESS = (a1, a2) -> { String n1 = GenericUtils.trimToEmpty(toAddressString(a1)); @@ -85,6 +87,31 @@ public class SshdSocketAddress extends SocketAddress { return String.CASE_INSENSITIVE_ORDER.compare(n1, n2); }; + /** + * Compares {@link SocketAddress}-es according to their host case <U>insensitive</U> + * and if equals, then according to their port value (if any) + * + * @see #toAddressString(SocketAddress) + * @see #toAddressPort(SocketAddress) + */ + public static final Comparator<SocketAddress> BY_HOST_AND_PORT = (a1, a2) -> { + String n1 = GenericUtils.trimToEmpty(toAddressString(a1)); + String n2 = GenericUtils.trimToEmpty(toAddressString(a2)); + int nRes = String.CASE_INSENSITIVE_ORDER.compare(n1, n2); + if (nRes != 0) { + return nRes; + } + + int p1 = toAddressPort(a1); + int p2 = toAddressPort(a2); + nRes = Integer.compare(p1, p2); + if (nRes != 0) { + return nRes; + } + + return 0; + }; + private static final long serialVersionUID = 6461645947151952729L; private final String hostName; @@ -283,20 +310,46 @@ public class SshdSocketAddress extends SocketAddress { return true; } - public static String toAddressString(SocketAddress addr) { + public static SshdSocketAddress toSshdSocketAddress(SocketAddress addr) { if (addr == null) { return null; + } else if (addr instanceof SshdSocketAddress) { + return (SshdSocketAddress) addr; + } else if (addr instanceof InetSocketAddress) { + InetSocketAddress isockAddress = (InetSocketAddress) addr; + return new SshdSocketAddress(isockAddress.getHostName(), isockAddress.getPort()); + } else { + throw new UnsupportedOperationException("Cannot convert " + addr.getClass().getSimpleName() + + "=" + addr + " to " + SshdSocketAddress.class.getSimpleName()); } + } - if (addr instanceof InetSocketAddress) { + public static String toAddressString(SocketAddress addr) { + if (addr == null) { + return null; + } else if (addr instanceof InetSocketAddress) { return ((InetSocketAddress) addr).getHostString(); - } - - if (addr instanceof SshdSocketAddress) { + } else if (addr instanceof SshdSocketAddress) { return ((SshdSocketAddress) addr).getHostName(); + } else { + return addr.toString(); } + } - return addr.toString(); + /** + * Attempts to resolve the port value + * + * @param addr The {@link SocketAddress} to examine + * @return The associated port value - negative if failed to resolve + */ + public static int toAddressPort(SocketAddress addr) { + if (addr instanceof InetSocketAddress) { + return ((InetSocketAddress) addr).getPort(); + } else if (addr instanceof SshdSocketAddress) { + return ((SshdSocketAddress) addr).getPort(); + } else { + return -1; + } } /** http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/2119f798/sshd-core/src/test/java/org/apache/sshd/client/config/hosts/HostConfigEntryTest.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/test/java/org/apache/sshd/client/config/hosts/HostConfigEntryTest.java b/sshd-core/src/test/java/org/apache/sshd/client/config/hosts/HostConfigEntryTest.java index 814deb6..3eeb6d4 100644 --- a/sshd-core/src/test/java/org/apache/sshd/client/config/hosts/HostConfigEntryTest.java +++ b/sshd-core/src/test/java/org/apache/sshd/client/config/hosts/HostConfigEntryTest.java @@ -28,7 +28,6 @@ import java.util.List; import java.util.regex.Pattern; import org.apache.sshd.common.util.GenericUtils; -import org.apache.sshd.common.util.Pair; import org.apache.sshd.util.test.BaseTestSupport; import org.junit.FixMethodOrder; import org.junit.Test; @@ -48,7 +47,7 @@ public class HostConfigEntryTest extends BaseTestSupport { String testHost = "37.77.34.7"; String[] elements = GenericUtils.split(testHost, '.'); StringBuilder sb = new StringBuilder(testHost.length() + Byte.SIZE); - List<Pair<Pattern, Boolean>> patterns = new ArrayList<>(elements.length + 1); + List<HostPatternValue> patterns = new ArrayList<>(elements.length + 1); // all wildcard patterns are not negated - only the actual host patterns.add(HostPatternsHolder.toPattern(String.valueOf(HostPatternsHolder.NEGATION_CHAR_PATTERN) + testHost)); @@ -70,7 +69,7 @@ public class HostConfigEntryTest extends BaseTestSupport { } for (int index = 0; index < patterns.size(); index++) { - assertFalse("Unexpected match for " + patterns, HostPatternsHolder.isHostMatch(testHost, patterns)); + assertFalse("Unexpected match for " + patterns, HostPatternsHolder.isHostMatch(testHost, 0, patterns)); Collections.shuffle(patterns); } } @@ -85,8 +84,8 @@ public class HostConfigEntryTest extends BaseTestSupport { } String value = sb.toString(); - Pair<Pattern, Boolean> pp = HostPatternsHolder.toPattern(value); - Pattern pattern = pp.getFirst(); + HostPatternValue pp = HostPatternsHolder.toPattern(value); + Pattern pattern = pp.getPattern(); String domain = value.substring(1); // chomp the wildcard prefix for (String host : new String[] { getClass().getSimpleName(), @@ -106,7 +105,7 @@ public class HostConfigEntryTest extends BaseTestSupport { StringBuilder sb = new StringBuilder().append("10.0.0."); int sbLen = sb.length(); - Pattern pattern = HostPatternsHolder.toPattern(sb.append(HostPatternsHolder.WILDCARD_PATTERN)).getFirst(); + Pattern pattern = HostPatternsHolder.toPattern(sb.append(HostPatternsHolder.WILDCARD_PATTERN)).getPattern(); for (int v = 0; v <= 255; v++) { sb.setLength(sbLen); // start from scratch sb.append(v); @@ -123,7 +122,7 @@ public class HostConfigEntryTest extends BaseTestSupport { for (boolean restoreOriginal : new boolean[] {true, false}) { for (int index = 0; index < value.length(); index++) { sb.setCharAt(index, HostPatternsHolder.SINGLE_CHAR_PATTERN); - testCaseInsensitivePatternMatching(value, HostPatternsHolder.toPattern(sb.toString()).getFirst(), true); + testCaseInsensitivePatternMatching(value, HostPatternsHolder.toPattern(sb.toString()).getPattern(), true); if (restoreOriginal) { sb.setCharAt(index, value.charAt(index)); } @@ -147,8 +146,8 @@ public class HostConfigEntryTest extends BaseTestSupport { } String pattern = sb.toString(); - Pair<Pattern, Boolean> pp = HostPatternsHolder.toPattern(pattern); - assertTrue("No match for " + address + " on pattern=" + pattern, HostPatternsHolder.isHostMatch(address, Collections.singletonList(pp))); + HostPatternValue pp = HostPatternsHolder.toPattern(pattern); + assertTrue("No match for " + address + " on pattern=" + pattern, HostPatternsHolder.isHostMatch(address, 0, Collections.singletonList(pp))); } } http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/2119f798/sshd-core/src/test/java/org/apache/sshd/client/config/hosts/KnownHostHashValueTest.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/test/java/org/apache/sshd/client/config/hosts/KnownHostHashValueTest.java b/sshd-core/src/test/java/org/apache/sshd/client/config/hosts/KnownHostHashValueTest.java index 4fa4943..9deed93 100644 --- a/sshd-core/src/test/java/org/apache/sshd/client/config/hosts/KnownHostHashValueTest.java +++ b/sshd-core/src/test/java/org/apache/sshd/client/config/hosts/KnownHostHashValueTest.java @@ -23,18 +23,21 @@ import java.util.Arrays; import java.util.Collection; import org.apache.sshd.util.test.BaseTestSupport; +import org.apache.sshd.util.test.JUnit4ClassRunnerWithParametersFactory; import org.junit.FixMethodOrder; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.MethodSorters; import org.junit.runners.Parameterized; import org.junit.runners.Parameterized.Parameters; +import org.junit.runners.Parameterized.UseParametersRunnerFactory; /** * @author <a href="mailto:d...@mina.apache.org">Apache MINA SSHD Project</a> */ @FixMethodOrder(MethodSorters.NAME_ASCENDING) @RunWith(Parameterized.class) // see https://github.com/junit-team/junit/wiki/Parameterized-tests +@UseParametersRunnerFactory(JUnit4ClassRunnerWithParametersFactory.class) public class KnownHostHashValueTest extends BaseTestSupport { private final String hostName; private final String hashValue; http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/2119f798/sshd-core/src/test/java/org/apache/sshd/client/keyverifier/KnownHostsServerKeyVerifierTest.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/test/java/org/apache/sshd/client/keyverifier/KnownHostsServerKeyVerifierTest.java b/sshd-core/src/test/java/org/apache/sshd/client/keyverifier/KnownHostsServerKeyVerifierTest.java index fff5c4b..8d11f93 100644 --- a/sshd-core/src/test/java/org/apache/sshd/client/keyverifier/KnownHostsServerKeyVerifierTest.java +++ b/sshd-core/src/test/java/org/apache/sshd/client/keyverifier/KnownHostsServerKeyVerifierTest.java @@ -35,6 +35,7 @@ import java.util.TreeMap; import java.util.concurrent.atomic.AtomicInteger; import org.apache.sshd.client.ClientFactoryManager; +import org.apache.sshd.client.config.hosts.HostPatternsHolder; import org.apache.sshd.client.config.hosts.KnownHostEntry; import org.apache.sshd.client.config.hosts.KnownHostHashValue; import org.apache.sshd.client.keyverifier.KnownHostsServerKeyVerifier.HostEntryPair; @@ -63,8 +64,8 @@ import org.mockito.Mockito; @FixMethodOrder(MethodSorters.NAME_ASCENDING) public class KnownHostsServerKeyVerifierTest extends BaseTestSupport { private static final String HASHED_HOST = "192.168.1.61"; - private static final Map<String, PublicKey> HOST_KEYS = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); - private static Map<String, KnownHostEntry> hostsEntries; + private static final Map<SshdSocketAddress, PublicKey> HOST_KEYS = new TreeMap<>(SshdSocketAddress.BY_HOST_AND_PORT); + private static Map<SshdSocketAddress, KnownHostEntry> hostsEntries; private static Path entriesFile; public KnownHostsServerKeyVerifierTest() { @@ -80,12 +81,12 @@ public class KnownHostsServerKeyVerifierTest extends BaseTestSupport { hostsEntries = loadEntries(entriesFile); // Cannot use forEach because of the potential IOException/GeneralSecurityException being thrown - for (Map.Entry<String, KnownHostEntry> ke : hostsEntries.entrySet()) { - String host = ke.getKey(); + for (Map.Entry<SshdSocketAddress, KnownHostEntry> ke : hostsEntries.entrySet()) { + SshdSocketAddress hostIdentity = ke.getKey(); KnownHostEntry entry = ke.getValue(); AuthorizedKeyEntry authEntry = ValidateUtils.checkNotNull(entry.getKeyEntry(), "No key extracted from %s", entry); PublicKey key = authEntry.resolvePublicKey(PublicKeyEntryResolver.FAILING); - assertNull("Multiple keys for host=" + host, HOST_KEYS.put(host, key)); + assertNull("Multiple keys for host=" + hostIdentity, HOST_KEYS.put(hostIdentity, key)); } } @@ -112,10 +113,10 @@ public class KnownHostsServerKeyVerifierTest extends BaseTestSupport { }; - HOST_KEYS.forEach((host, serverKey) -> { - KnownHostEntry entry = hostsEntries.get(host); + HOST_KEYS.forEach((hostIdentity, serverKey) -> { + KnownHostEntry entry = hostsEntries.get(hostIdentity); outputDebugMessage("Verify host=%s", entry); - assertTrue("Failed to verify server=" + entry, invokeVerifier(verifier, host, serverKey)); + assertTrue("Failed to verify server=" + entry, invokeVerifier(verifier, hostIdentity, serverKey)); assertEquals("Unexpected delegate invocation for host=" + entry, 0, delegateCount.get()); assertEquals("Unexpected update invocation for host=" + entry, 0, updateCount.get()); }); @@ -146,28 +147,28 @@ public class KnownHostsServerKeyVerifierTest extends BaseTestSupport { int verificationCount = 0; // Cannot use forEach because the verification count variable is not effectively final - for (Map.Entry<String, PublicKey> ke : HOST_KEYS.entrySet()) { - String host = ke.getKey(); + for (Map.Entry<SshdSocketAddress, PublicKey> ke : HOST_KEYS.entrySet()) { + SshdSocketAddress hostIdentity = ke.getKey(); PublicKey serverKey = ke.getValue(); - KnownHostEntry entry = hostsEntries.get(host); + KnownHostEntry entry = hostsEntries.get(hostIdentity); outputDebugMessage("Verify host=%s", entry); - assertTrue("Failed to verify server=" + entry, invokeVerifier(verifier, host, serverKey)); + assertTrue("Failed to verify server=" + entry, invokeVerifier(verifier, hostIdentity, serverKey)); verificationCount++; assertEquals("Mismatched number of delegate counts for server=" + entry, verificationCount, delegateCount.get()); assertEquals("Mismatched number of update counts for server=" + entry, verificationCount, updateCount.get()); } // make sure we have all the original entries and ONLY them - Map<String, KnownHostEntry> updatedEntries = loadEntries(path); - hostsEntries.forEach((host, expected) -> { - KnownHostEntry actual = updatedEntries.remove(host); - assertNotNull("No updated entry for host=" + host, actual); + Map<SshdSocketAddress, KnownHostEntry> updatedEntries = loadEntries(path); + hostsEntries.forEach((hostIdentity, expected) -> { + KnownHostEntry actual = updatedEntries.remove(hostIdentity); + assertNotNull("No updated entry for host=" + hostIdentity, actual); String expLine = expected.getConfigLine(); // if original is a list or hashed then replace them with the expected host if ((expLine.indexOf(',') > 0) || (expLine.indexOf(KnownHostHashValue.HASHED_HOST_DELIMITER) >= 0)) { int pos = expLine.indexOf(' '); - expLine = host + expLine.substring(pos); + expLine = hostIdentity.getHostName() + expLine.substring(pos); } int pos = expLine.indexOf("comment-"); @@ -175,7 +176,7 @@ public class KnownHostsServerKeyVerifierTest extends BaseTestSupport { expLine = expLine.substring(0, pos).trim(); } - assertEquals("Mismatched entry data for host=" + host, expLine, actual.getConfigLine()); + assertEquals("Mismatched entry data for host=" + hostIdentity, expLine, actual.getConfigLine()); }); assertTrue("Unexpected extra updated hosts: " + updatedEntries, updatedEntries.isEmpty()); @@ -188,7 +189,7 @@ public class KnownHostsServerKeyVerifierTest extends BaseTestSupport { KnownHostsServerKeyVerifier verifier = new KnownHostsServerKeyVerifier(AcceptAllServerKeyVerifier.INSTANCE, path) { @Override - protected NamedFactory<Mac> getHostValueDigester(ClientSession clientSession, SocketAddress remoteAddress, String hostIdentity) { + protected NamedFactory<Mac> getHostValueDigester(ClientSession clientSession, SocketAddress remoteAddress, SshdSocketAddress hostIdentity) { return KnownHostHashValue.DEFAULT_DIGEST; } }; @@ -198,13 +199,12 @@ public class KnownHostsServerKeyVerifierTest extends BaseTestSupport { ClientSession session = Mockito.mock(ClientSession.class); Mockito.when(session.getFactoryManager()).thenReturn(manager); - HOST_KEYS.forEach((host, serverKey) -> { - KnownHostEntry entry = hostsEntries.get(host); + HOST_KEYS.forEach((hostIdentity, serverKey) -> { + KnownHostEntry entry = hostsEntries.get(hostIdentity); outputDebugMessage("Write host=%s", entry); - SocketAddress address = new SshdSocketAddress(host, 7365); - Mockito.when(session.getConnectAddress()).thenReturn(address); - assertTrue("Failed to validate server=" + entry, verifier.verifyServerKey(session, address, serverKey)); + Mockito.when(session.getConnectAddress()).thenReturn(hostIdentity); + assertTrue("Failed to validate server=" + entry, verifier.verifyServerKey(session, hostIdentity, serverKey)); }); // force re-read to ensure all values are hashed @@ -216,13 +216,12 @@ public class KnownHostsServerKeyVerifierTest extends BaseTestSupport { verifier.setLoadedHostsEntries(keys); // make sure can still validate the original hosts - HOST_KEYS.forEach((host, serverKey) -> { - KnownHostEntry entry = hostsEntries.get(host); + HOST_KEYS.forEach((hostIdentity, serverKey) -> { + KnownHostEntry entry = hostsEntries.get(hostIdentity); outputDebugMessage("Re-validate host=%s", entry); - SocketAddress address = new SshdSocketAddress(host, 7365); - Mockito.when(session.getConnectAddress()).thenReturn(address); - assertTrue("Failed to re-validate server=" + entry, verifier.verifyServerKey(session, address, serverKey)); + Mockito.when(session.getConnectAddress()).thenReturn(hostIdentity); + assertTrue("Failed to re-validate server=" + entry, verifier.verifyServerKey(session, hostIdentity, serverKey)); }); } @@ -244,11 +243,11 @@ public class KnownHostsServerKeyVerifierTest extends BaseTestSupport { int validationCount = 0; // Cannot use forEach because the validation count variable is not effectively final - for (Map.Entry<String, KnownHostEntry> ke : hostsEntries.entrySet()) { - String host = ke.getKey(); + for (Map.Entry<SshdSocketAddress, KnownHostEntry> ke : hostsEntries.entrySet()) { + SshdSocketAddress hostIdentity = ke.getKey(); KnownHostEntry entry = ke.getValue(); outputDebugMessage("Verify host=%s", entry); - assertFalse("Unexpected to verification success for " + entry, invokeVerifier(verifier, host, modifiedKey)); + assertFalse("Unexpected to verification success for " + entry, invokeVerifier(verifier, hostIdentity, modifiedKey)); validationCount++; assertEquals("Mismatched invocation count for host=" + entry, validationCount, acceptCount.get()); } @@ -275,9 +274,9 @@ public class KnownHostsServerKeyVerifierTest extends BaseTestSupport { }); String expected = PublicKeyEntry.toString(modifiedKey); - Map<String, KnownHostEntry> updatedKeys = loadEntries(path); - hostsEntries.forEach((host, original) -> { - KnownHostEntry updated = updatedKeys.remove(host); + Map<SshdSocketAddress, KnownHostEntry> updatedKeys = loadEntries(path); + hostsEntries.forEach((hostIdentity, original) -> { + KnownHostEntry updated = updatedKeys.remove(hostIdentity); assertNotNull("No updated entry for " + original, updated); String actual = updated.getConfigLine(); @@ -292,7 +291,7 @@ public class KnownHostsServerKeyVerifierTest extends BaseTestSupport { } actual = GenericUtils.trimToEmpty(actual.substring(pos + 1)); - assertEquals("Mismatched updated value for host=" + host, expected, actual); + assertEquals("Mismatched updated value for host=" + hostIdentity, expected, actual); }); assertTrue("Unexpected extra updated entries: " + updatedKeys, updatedKeys.isEmpty()); @@ -310,21 +309,20 @@ public class KnownHostsServerKeyVerifierTest extends BaseTestSupport { return file; } - private boolean invokeVerifier(ServerKeyVerifier verifier, String host, PublicKey serverKey) { - SocketAddress address = new SshdSocketAddress(host, 7365); + private boolean invokeVerifier(ServerKeyVerifier verifier, SshdSocketAddress hostIdentity, PublicKey serverKey) { ClientSession session = Mockito.mock(ClientSession.class); - Mockito.when(session.getConnectAddress()).thenReturn(address); - Mockito.when(session.toString()).thenReturn(getCurrentTestName() + "[" + host + "]"); - return verifier.verifyServerKey(session, address, serverKey); + Mockito.when(session.getConnectAddress()).thenReturn(hostIdentity); + Mockito.when(session.toString()).thenReturn(getCurrentTestName() + "[" + hostIdentity + "]"); + return verifier.verifyServerKey(session, hostIdentity, serverKey); } - private static Map<String, KnownHostEntry> loadEntries(Path file) throws IOException { + private static Map<SshdSocketAddress, KnownHostEntry> loadEntries(Path file) throws IOException { Collection<KnownHostEntry> entries = KnownHostEntry.readKnownHostEntries(file); if (GenericUtils.isEmpty(entries)) { return Collections.emptyMap(); } - Map<String, KnownHostEntry> hostsMap = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + Map<SshdSocketAddress, KnownHostEntry> hostsMap = new TreeMap<>(SshdSocketAddress.BY_HOST_AND_PORT); for (KnownHostEntry entry : entries) { String line = entry.getConfigLine(); outputDebugMessage("loadTestLines(%s) processing %s", file, line); @@ -332,11 +330,19 @@ public class KnownHostsServerKeyVerifierTest extends BaseTestSupport { int pos = line.indexOf(' '); String patterns = line.substring(0, pos); if (entry.getHashedEntry() != null) { - assertNull("Multiple hashed entries in file", hostsMap.put(HASHED_HOST, entry)); + assertNull("Multiple hashed entries in file", hostsMap.put(new SshdSocketAddress(HASHED_HOST, 0), entry)); } else { String[] addrs = GenericUtils.split(patterns, ','); for (String a : addrs) { - assertNull("Multiple entries for address=" + a, hostsMap.put(a, entry)); + int port = 0; + if (a.charAt(0) == HostPatternsHolder.NON_STANDARD_PORT_PATTERN_ENCLOSURE_START_DELIM) { + pos = a.indexOf(HostPatternsHolder.NON_STANDARD_PORT_PATTERN_ENCLOSURE_END_DELIM, 1); + assertTrue("Missing non-standard port host pattern enclosure: " + a, pos > 0); + + port = Integer.parseInt(a.substring(pos + 2)); + a = a.substring(1, pos); + } + assertNull("Multiple entries for address=" + a, hostsMap.put(new SshdSocketAddress(a, port), entry)); } } } http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/2119f798/sshd-core/src/test/resources/org/apache/sshd/client/keyverifier/known_hosts ---------------------------------------------------------------------- diff --git a/sshd-core/src/test/resources/org/apache/sshd/client/keyverifier/known_hosts b/sshd-core/src/test/resources/org/apache/sshd/client/keyverifier/known_hosts index 2f8034b..f842391 100644 --- a/sshd-core/src/test/resources/org/apache/sshd/client/keyverifier/known_hosts +++ b/sshd-core/src/test/resources/org/apache/sshd/client/keyverifier/known_hosts @@ -9,4 +9,7 @@ server.sshd.apache.org,10.23.222.240 ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbml 192.168.174.129 ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBJvwXQ0Wc7TdhmjaTdkrHJvatWrw/6t6W1Hgh2nTauN+rhsMKYbmQeFTnzsP1NctWzQXwsqIOGcXIMNVhT92jgQ= # hash of 192.168.1.61 - DO NOT CHANGE IT !!! -|1|F1E1KeoE/eEWhi10WpGv4OdiO6Y=|3988QV0VE8wmZL7suNrYQLITLCg= ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA2KFr3GqL/3yXY2bAwRGGDxl/qLuE9qdx20+DMh5oAZPpwprlUnlxLm+ikimwn65Z0KeUyfofYKt+vc3rl1k2mDqyG8DqHeH0C+uFBbom0fthX7PRiQr2T9SOzSodjowZuBHlWIfgtcZI0bygX+GlKaAq00l4yCoe1xUTCRd2ZVyNuB1nozcFI+sUzdeKfaxvuyvbccG4tOx06HDryNdxW2e99bsAhLAg7d8xciOeb4PCAI1USg83dt0wVZE9VJbnRnoZ2y/DaQCJtBJ8t8uNLVdggakydDzQuglyd4dYRxeU7t4TEw6wsfXPB0kqdecd0Llspjx0ciEY/BbycdiApw== comment-hashed-host \ No newline at end of file +|1|F1E1KeoE/eEWhi10WpGv4OdiO6Y=|3988QV0VE8wmZL7suNrYQLITLCg= ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA2KFr3GqL/3yXY2bAwRGGDxl/qLuE9qdx20+DMh5oAZPpwprlUnlxLm+ikimwn65Z0KeUyfofYKt+vc3rl1k2mDqyG8DqHeH0C+uFBbom0fthX7PRiQr2T9SOzSodjowZuBHlWIfgtcZI0bygX+GlKaAq00l4yCoe1xUTCRd2ZVyNuB1nozcFI+sUzdeKfaxvuyvbccG4tOx06HDryNdxW2e99bsAhLAg7d8xciOeb4PCAI1USg83dt0wVZE9VJbnRnoZ2y/DaQCJtBJ8t8uNLVdggakydDzQuglyd4dYRxeU7t4TEw6wsfXPB0kqdecd0Llspjx0ciEY/BbycdiApw== comment-hashed-host + +# non-standard port overrides +[issues.apache.org]:5637 ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBCWDHD00Ltb5fmmL9cFLTqWqxgJHwsxbiZgL632CXqbDmf69wA+8GSP96rtIix2d5aGXyh/kXMbSMjPgIx+n7p0= \ No newline at end of file