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:52 UTC
[1/3] mina-sshd git commit: [SSHD-266] Support for known hosts
Repository: mina-sshd
Updated Branches:
refs/heads/master 305850cfb -> 509c871ea
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/509c871e/sshd-core/src/main/java/org/apache/sshd/server/config/keys/AuthorizedKeysAuthenticator.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/server/config/keys/AuthorizedKeysAuthenticator.java b/sshd-core/src/main/java/org/apache/sshd/server/config/keys/AuthorizedKeysAuthenticator.java
index e4f844a..a978b06 100644
--- a/sshd-core/src/main/java/org/apache/sshd/server/config/keys/AuthorizedKeysAuthenticator.java
+++ b/sshd-core/src/main/java/org/apache/sshd/server/config/keys/AuthorizedKeysAuthenticator.java
@@ -21,13 +21,19 @@ package org.apache.sshd.server.config.keys;
import java.io.File;
import java.io.IOException;
+import java.nio.file.Files;
import java.nio.file.LinkOption;
+import java.nio.file.OpenOption;
import java.nio.file.Path;
import java.security.GeneralSecurityException;
import java.security.PublicKey;
import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
+import org.apache.sshd.common.config.keys.AuthorizedKeyEntry;
+import org.apache.sshd.common.config.keys.PublicKeyEntry;
import org.apache.sshd.common.config.keys.PublicKeyEntryResolver;
import org.apache.sshd.common.util.GenericUtils;
import org.apache.sshd.common.util.ValidateUtils;
@@ -47,6 +53,16 @@ import org.apache.sshd.server.session.ServerSession;
* @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
*/
public class AuthorizedKeysAuthenticator extends ModifiableFileWatcher implements PublickeyAuthenticator {
+
+ /**
+ * Standard OpenSSH authorized keys file name
+ */
+ public static final String STD_AUTHORIZED_KEYS_FILENAME = "authorized_keys";
+
+ private static final class LazyDefaultAuthorizedKeysFileHolder {
+ private static final Path KEYS_FILE = PublicKeyEntry.getDefaultKeysFolderPath().resolve(STD_AUTHORIZED_KEYS_FILENAME);
+ }
+
private final AtomicReference<PublickeyAuthenticator> delegateHolder = // assumes initially reject-all
new AtomicReference<PublickeyAuthenticator>(RejectAllPublickeyAuthenticator.INSTANCE);
@@ -130,4 +146,29 @@ public class AuthorizedKeysAuthenticator extends ModifiableFileWatcher implement
updateReloadAttributes();
return entries;
}
+
+ /**
+ * @return The default {@link Path} location of the OpenSSH authorized keys file
+ */
+ @SuppressWarnings("synthetic-access")
+ public static Path getDefaultAuthorizedKeysFile() {
+ return LazyDefaultAuthorizedKeysFileHolder.KEYS_FILE;
+ }
+
+ /**
+ * Reads read the contents of the default OpenSSH <code>authorized_keys</code> file
+ *
+ * @param options The {@link OpenOption}s to use when reading the file
+ * @return A {@link List} of all the {@link AuthorizedKeyEntry}-ies found there -
+ * or empty if file does not exist
+ * @throws IOException If failed to read keys from file
+ */
+ public static List<AuthorizedKeyEntry> readDefaultAuthorizedKeys(OpenOption ... options) throws IOException {
+ Path keysFile = getDefaultAuthorizedKeysFile();
+ if (Files.exists(keysFile, IoUtils.EMPTY_LINK_OPTIONS)) {
+ return AuthorizedKeyEntry.readAuthorizedKeys(keysFile);
+ } else {
+ return Collections.emptyList();
+ }
+ }
}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/509c871e/sshd-core/src/main/java/org/apache/sshd/server/config/keys/DefaultAuthorizedKeysAuthenticator.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/server/config/keys/DefaultAuthorizedKeysAuthenticator.java b/sshd-core/src/main/java/org/apache/sshd/server/config/keys/DefaultAuthorizedKeysAuthenticator.java
index 6cdc7ea..ea9579f 100644
--- a/sshd-core/src/main/java/org/apache/sshd/server/config/keys/DefaultAuthorizedKeysAuthenticator.java
+++ b/sshd-core/src/main/java/org/apache/sshd/server/config/keys/DefaultAuthorizedKeysAuthenticator.java
@@ -29,6 +29,7 @@ import java.util.Collection;
import java.util.Collections;
import org.apache.sshd.common.auth.UsernameHolder;
+import org.apache.sshd.common.config.keys.AuthorizedKeyEntry;
import org.apache.sshd.common.config.keys.KeyUtils;
import org.apache.sshd.common.util.OsUtils;
import org.apache.sshd.common.util.Pair;
@@ -64,7 +65,7 @@ public class DefaultAuthorizedKeysAuthenticator extends AuthorizedKeysAuthentica
}
public DefaultAuthorizedKeysAuthenticator(String user, boolean strict) {
- this(user, AuthorizedKeyEntry.getDefaultAuthorizedKeysFile(), strict);
+ this(user, getDefaultAuthorizedKeysFile(), strict);
}
public DefaultAuthorizedKeysAuthenticator(File file, boolean strict) {
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/509c871e/sshd-core/src/test/java/org/apache/sshd/client/ClientTest.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/test/java/org/apache/sshd/client/ClientTest.java b/sshd-core/src/test/java/org/apache/sshd/client/ClientTest.java
index 24b47cb..666c9cd 100644
--- a/sshd-core/src/test/java/org/apache/sshd/client/ClientTest.java
+++ b/sshd-core/src/test/java/org/apache/sshd/client/ClientTest.java
@@ -26,6 +26,7 @@ import java.io.InputStream;
import java.io.OutputStream;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
+import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.charset.StandardCharsets;
import java.security.KeyPair;
@@ -97,6 +98,7 @@ import org.apache.sshd.common.util.ValidateUtils;
import org.apache.sshd.common.util.buffer.Buffer;
import org.apache.sshd.common.util.buffer.ByteArrayBuffer;
import org.apache.sshd.common.util.io.NoCloseOutputStream;
+import org.apache.sshd.common.util.net.SshdSocketAddress;
import org.apache.sshd.server.Command;
import org.apache.sshd.server.SshServer;
import org.apache.sshd.server.auth.keyboard.DefaultKeyboardInteractiveAuthenticator;
@@ -1463,6 +1465,11 @@ public class ClientTest extends BaseTestSupport {
session.addPasswordIdentity(getCurrentTestName());
session.auth().verify(5L, TimeUnit.SECONDS);
+ InetSocketAddress addr = SshdSocketAddress.toInetSocketAddress(session.getConnectAddress());
+ assertNotNull("No reported connect address", addr);
+ assertEquals("Mismatched connect host", TEST_LOCALHOST, addr.getHostString());
+ assertEquals("Mismatched connect port", port, addr.getPort());
+
ClientSession returnValue = session;
session = null; // avoid 'finally' close
return returnValue;
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/509c871e/sshd-core/src/test/java/org/apache/sshd/client/config/hosts/ConfigFileHostEntryResolverTest.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/test/java/org/apache/sshd/client/config/hosts/ConfigFileHostEntryResolverTest.java b/sshd-core/src/test/java/org/apache/sshd/client/config/hosts/ConfigFileHostEntryResolverTest.java
index da8bfa0..a75e18f 100644
--- a/sshd-core/src/test/java/org/apache/sshd/client/config/hosts/ConfigFileHostEntryResolverTest.java
+++ b/sshd-core/src/test/java/org/apache/sshd/client/config/hosts/ConfigFileHostEntryResolverTest.java
@@ -61,17 +61,17 @@ public class ConfigFileHostEntryResolverTest extends BaseTestSupport {
testConfigFileReload("Non-existing", path, reloadCount, null, resolver, expected, null);
testConfigFileReload("Empty", path, reloadCount, Collections.<HostConfigEntry>emptyList(), resolver, expected, null);
testConfigFileReload("Global", path, reloadCount,
- Collections.singletonList(new HostConfigEntry(HostConfigEntry.ALL_HOSTS_PATTERN, expected.getHost(), expected.getPort(), expected.getUsername())),
+ Collections.singletonList(new HostConfigEntry(HostPatternsHolder.ALL_HOSTS_PATTERN, expected.getHost(), expected.getPort(), expected.getUsername())),
resolver, expected, expected);
testConfigFileReload("Wildcard", path, reloadCount,
Arrays.asList(
- new HostConfigEntry(HostConfigEntry.ALL_HOSTS_PATTERN, getClass().getSimpleName(), 1234, getClass().getSimpleName()),
- new HostConfigEntry(expected.getHost() + String.valueOf(HostConfigEntry.WILDCARD_PATTERN), expected.getHost(), expected.getPort(), expected.getUsername())),
+ new HostConfigEntry(HostPatternsHolder.ALL_HOSTS_PATTERN, getClass().getSimpleName(), 1234, getClass().getSimpleName()),
+ new HostConfigEntry(expected.getHost() + String.valueOf(HostPatternsHolder.WILDCARD_PATTERN), expected.getHost(), expected.getPort(), expected.getUsername())),
resolver, expected, expected);
testConfigFileReload("Specific", path, reloadCount,
Arrays.asList(
- new HostConfigEntry(HostConfigEntry.ALL_HOSTS_PATTERN, getClass().getSimpleName(), 1234, getClass().getSimpleName()),
- new HostConfigEntry(getClass().getSimpleName() + String.valueOf(HostConfigEntry.WILDCARD_PATTERN), getClass().getSimpleName(), 1234, getClass().getSimpleName()),
+ new HostConfigEntry(HostPatternsHolder.ALL_HOSTS_PATTERN, getClass().getSimpleName(), 1234, getClass().getSimpleName()),
+ new HostConfigEntry(getClass().getSimpleName() + String.valueOf(HostPatternsHolder.WILDCARD_PATTERN), getClass().getSimpleName(), 1234, getClass().getSimpleName()),
expected),
resolver, expected, expected);
}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/509c871e/sshd-core/src/test/java/org/apache/sshd/client/config/hosts/HostConfigEntryResolverTest.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/test/java/org/apache/sshd/client/config/hosts/HostConfigEntryResolverTest.java b/sshd-core/src/test/java/org/apache/sshd/client/config/hosts/HostConfigEntryResolverTest.java
index 9ff1dc1..4296b72 100644
--- a/sshd-core/src/test/java/org/apache/sshd/client/config/hosts/HostConfigEntryResolverTest.java
+++ b/sshd-core/src/test/java/org/apache/sshd/client/config/hosts/HostConfigEntryResolverTest.java
@@ -114,7 +114,7 @@ public class HostConfigEntryResolverTest extends BaseTestSupport {
public void testNegatedHostEntriesResolution() throws Exception {
HostConfigEntry positiveEntry = new HostConfigEntry(TEST_LOCALHOST, TEST_LOCALHOST, port, getCurrentTestName());
HostConfigEntry negativeEntry = new HostConfigEntry(
- String.valueOf(HostConfigEntry.NEGATION_CHAR_PATTERN) + positiveEntry.getHost(),
+ String.valueOf(HostPatternsHolder.NEGATION_CHAR_PATTERN) + positiveEntry.getHost(),
positiveEntry.getHostName(),
getMovedPortNumber(positiveEntry.getPort()),
getClass().getPackage().getName());
@@ -264,15 +264,7 @@ public class HostConfigEntryResolverTest extends BaseTestSupport {
private static <S extends Session> S assertEffectiveRemoteAddress(S session, HostConfigEntry entry) {
IoSession ioSession = session.getIoSession();
SocketAddress remoteAddress = ioSession.getRemoteAddress();
- InetSocketAddress inetAddress;
- if (remoteAddress instanceof InetSocketAddress) {
- inetAddress = (InetSocketAddress) remoteAddress;
- } else if (remoteAddress instanceof SshdSocketAddress) {
- inetAddress = ((SshdSocketAddress) remoteAddress).toInetSocketAddress();
- } else {
- throw new ClassCastException("Unknown remote address type: " + remoteAddress);
- }
-
+ InetSocketAddress inetAddress = SshdSocketAddress.toInetSocketAddress(remoteAddress);
assertEquals("Mismatched effective port", entry.getPort(), inetAddress.getPort());
assertEquals("Mismatched effective user", entry.getUsername(), session.getUsername());
return session;
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/509c871e/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 d86a565..635c6cc 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
@@ -50,7 +50,7 @@ public class HostConfigEntryTest extends BaseTestSupport {
StringBuilder sb = new StringBuilder(testHost.length() + Byte.SIZE);
List<Pair<Pattern, Boolean>> patterns = new ArrayList<>(elements.length + 1);
// all wildcard patterns are not negated - only the actual host
- patterns.add(HostConfigEntry.toPattern(String.valueOf(HostConfigEntry.NEGATION_CHAR_PATTERN) + testHost));
+ patterns.add(HostPatternsHolder.toPattern(String.valueOf(HostPatternsHolder.NEGATION_CHAR_PATTERN) + testHost));
for (int i = 0; i < elements.length; i++) {
sb.setLength(0);
@@ -60,17 +60,17 @@ public class HostConfigEntryTest extends BaseTestSupport {
sb.append('.');
}
if (i == j) {
- sb.append(HostConfigEntry.WILDCARD_PATTERN);
+ sb.append(HostPatternsHolder.WILDCARD_PATTERN);
} else {
sb.append(elements[j]);
}
}
- patterns.add(HostConfigEntry.toPattern(sb));
+ patterns.add(HostPatternsHolder.toPattern(sb));
}
for (int index = 0; index < patterns.size(); index++) {
- assertFalse("Unexpected match for " + patterns, HostConfigEntry.isHostMatch(testHost, patterns));
+ assertFalse("Unexpected match for " + patterns, HostPatternsHolder.isHostMatch(testHost, patterns));
Collections.shuffle(patterns);
}
}
@@ -79,13 +79,13 @@ public class HostConfigEntryTest extends BaseTestSupport {
public void testHostWildcardPatternMatching() {
String pkgName = getClass().getPackage().getName();
String[] elements = GenericUtils.split(pkgName, '.');
- StringBuilder sb = new StringBuilder(pkgName.length() + Long.SIZE + 1).append(HostConfigEntry.WILDCARD_PATTERN);
+ StringBuilder sb = new StringBuilder(pkgName.length() + Long.SIZE + 1).append(HostPatternsHolder.WILDCARD_PATTERN);
for (int index = elements.length - 1; index >= 0; index--) {
sb.append('.').append(elements[index]);
}
String value = sb.toString();
- Pair<Pattern, Boolean> pp = HostConfigEntry.toPattern(value);
+ Pair<Pattern, Boolean> pp = HostPatternsHolder.toPattern(value);
Pattern pattern = pp.getFirst();
String domain = value.substring(1); // chomp the wildcard prefix
for (String host : new String[] {
@@ -106,13 +106,13 @@ public class HostConfigEntryTest extends BaseTestSupport {
StringBuilder sb = new StringBuilder().append("10.0.0.");
int sbLen = sb.length();
- Pattern pattern = HostConfigEntry.toPattern(sb.append(HostConfigEntry.WILDCARD_PATTERN)).getFirst();
+ Pattern pattern = HostPatternsHolder.toPattern(sb.append(HostPatternsHolder.WILDCARD_PATTERN)).getFirst();
for (int v = 0; v <= 255; v++) {
sb.setLength(sbLen); // start from scratch
sb.append(v);
String address = sb.toString();
- assertTrue("No match for " + address, HostConfigEntry.isHostMatch(address, pattern));
+ assertTrue("No match for " + address, HostPatternsHolder.isHostMatch(address, pattern));
}
}
@@ -122,8 +122,8 @@ public class HostConfigEntryTest extends BaseTestSupport {
StringBuilder sb = new StringBuilder(value);
for (boolean restoreOriginal : new boolean[] { true, false }) {
for (int index = 0; index < value.length(); index++) {
- sb.setCharAt(index, HostConfigEntry.SINGLE_CHAR_PATTERN);
- testCaseInsensitivePatternMatching(value, HostConfigEntry.toPattern(sb.toString()).getFirst(), true);
+ sb.setCharAt(index, HostPatternsHolder.SINGLE_CHAR_PATTERN);
+ testCaseInsensitivePatternMatching(value, HostPatternsHolder.toPattern(sb.toString()).getFirst(), true);
if (restoreOriginal) {
sb.setCharAt(index, value.charAt(index));
}
@@ -143,46 +143,46 @@ public class HostConfigEntryTest extends BaseTestSupport {
String address = sb.toString();
// replace the added digits with single char pattern
for (int index = sbLen; index < sb.length(); index++) {
- sb.setCharAt(index, HostConfigEntry.SINGLE_CHAR_PATTERN);
+ sb.setCharAt(index, HostPatternsHolder.SINGLE_CHAR_PATTERN);
}
String pattern = sb.toString();
- Pair<Pattern, Boolean> pp = HostConfigEntry.toPattern(pattern);
- assertTrue("No match for " + address + " on pattern=" + pattern, HostConfigEntry.isHostMatch(address, Collections.singletonList(pp)));
+ Pair<Pattern, Boolean> pp = HostPatternsHolder.toPattern(pattern);
+ assertTrue("No match for " + address + " on pattern=" + pattern, HostPatternsHolder.isHostMatch(address, Collections.singletonList(pp)));
}
}
@Test
public void testIsValidPatternChar() {
for (char ch = '\0'; ch <= ' '; ch++) {
- assertFalse("Unexpected valid character (0x" + Integer.toHexString(ch & 0xFF) + ")", HostConfigEntry.isValidPatternChar(ch));
+ assertFalse("Unexpected valid character (0x" + Integer.toHexString(ch & 0xFF) + ")", HostPatternsHolder.isValidPatternChar(ch));
}
for (char ch = 'a'; ch <= 'z'; ch++) {
- assertTrue("Valid character not recognized: " + String.valueOf(ch), HostConfigEntry.isValidPatternChar(ch));
+ assertTrue("Valid character not recognized: " + String.valueOf(ch), HostPatternsHolder.isValidPatternChar(ch));
}
for (char ch = 'A'; ch <= 'Z'; ch++) {
- assertTrue("Valid character not recognized: " + String.valueOf(ch), HostConfigEntry.isValidPatternChar(ch));
+ assertTrue("Valid character not recognized: " + String.valueOf(ch), HostPatternsHolder.isValidPatternChar(ch));
}
for (char ch = '0'; ch <= '9'; ch++) {
- assertTrue("Valid character not recognized: " + String.valueOf(ch), HostConfigEntry.isValidPatternChar(ch));
+ assertTrue("Valid character not recognized: " + String.valueOf(ch), HostPatternsHolder.isValidPatternChar(ch));
}
- for (char ch : new char[] { '-', '_', '.', HostConfigEntry.SINGLE_CHAR_PATTERN, HostConfigEntry.WILDCARD_PATTERN }) {
- assertTrue("Valid character not recognized: " + String.valueOf(ch), HostConfigEntry.isValidPatternChar(ch));
+ for (char ch : new char[] { '-', '_', '.', HostPatternsHolder.SINGLE_CHAR_PATTERN, HostPatternsHolder.WILDCARD_PATTERN }) {
+ assertTrue("Valid character not recognized: " + String.valueOf(ch), HostPatternsHolder.isValidPatternChar(ch));
}
for (char ch : new char[] {
'(', ')', '{', '}', '[', ']', '@',
'#', '$', '^', '&', '%', '~', '<', '>',
',', '/', '\\', '\'', '"', ':', ';' }) {
- assertFalse("Unexpected valid character: " + String.valueOf(ch), HostConfigEntry.isValidPatternChar(ch));
+ assertFalse("Unexpected valid character: " + String.valueOf(ch), HostPatternsHolder.isValidPatternChar(ch));
}
for (char ch = 0x7E; ch <= 0xFF; ch++) {
- assertFalse("Unexpected valid character (0x" + Integer.toHexString(ch & 0xFF) + ")", HostConfigEntry.isValidPatternChar(ch));
+ assertFalse("Unexpected valid character (0x" + Integer.toHexString(ch & 0xFF) + ")", HostPatternsHolder.isValidPatternChar(ch));
}
}
@@ -220,7 +220,7 @@ public class HostConfigEntryTest extends BaseTestSupport {
// global entry MUST be 1st one
HostConfigEntry globalEntry = entries.get(0);
- assertEquals("Mismatched global entry pattern", HostConfigEntry.ALL_HOSTS_PATTERN, globalEntry.getHost());
+ assertEquals("Mismatched global entry pattern", HostPatternsHolder.ALL_HOSTS_PATTERN, globalEntry.getHost());
for (int index = 1; index < entries.size(); index++) {
HostConfigEntry entry = entries.get(index);
@@ -274,8 +274,8 @@ public class HostConfigEntryTest extends BaseTestSupport {
final String HOST = getCurrentTestName();
HostConfigEntry expected = new HostConfigEntry(HOST, HOST, 7365, HOST);
List<HostConfigEntry> matches = new ArrayList<>();
- matches.add(new HostConfigEntry(HostConfigEntry.ALL_HOSTS_PATTERN, getClass().getSimpleName(), Short.MAX_VALUE, getClass().getSimpleName()));
- matches.add(new HostConfigEntry(HOST + String.valueOf(HostConfigEntry.WILDCARD_PATTERN), getClass().getSimpleName(), Byte.MAX_VALUE, getClass().getSimpleName()));
+ matches.add(new HostConfigEntry(HostPatternsHolder.ALL_HOSTS_PATTERN, getClass().getSimpleName(), Short.MAX_VALUE, getClass().getSimpleName()));
+ matches.add(new HostConfigEntry(HOST + String.valueOf(HostPatternsHolder.WILDCARD_PATTERN), getClass().getSimpleName(), Byte.MAX_VALUE, getClass().getSimpleName()));
matches.add(expected);
for (int index = 0; index < matches.size(); index++) {
@@ -307,7 +307,7 @@ public class HostConfigEntryTest extends BaseTestSupport {
}
private static void testCaseInsensitivePatternMatching(String value, Pattern pattern, boolean expected) {
for (int index = 0; index < value.length(); index++) {
- boolean actual = HostConfigEntry.isHostMatch(value, pattern);
+ boolean actual = HostPatternsHolder.isHostMatch(value, pattern);
assertEquals("Mismatched match result for " + value + " on pattern=" + pattern.pattern(), expected, actual);
value = shuffleCase(value);
}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/509c871e/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
new file mode 100644
index 0000000..2dcdbff
--- /dev/null
+++ b/sshd-core/src/test/java/org/apache/sshd/client/config/hosts/KnownHostHashValueTest.java
@@ -0,0 +1,73 @@
+/*
+ * 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.Arrays;
+import java.util.Collection;
+
+import org.apache.sshd.util.test.BaseTestSupport;
+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;
+
+/**
+ * @author <a href="mailto:dev@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
+public class KnownHostHashValueTest extends BaseTestSupport {
+ private final String hostName;
+ private final String hashValue;
+ private final KnownHostHashValue hash;
+
+ public KnownHostHashValueTest(String hostName, String hashValue) {
+ this.hostName = hostName;
+ this.hashValue = hashValue;
+ this.hash = KnownHostHashValue.parse(hashValue);
+ }
+
+ @Parameters(name = "host={0}, hash={1}")
+ public static Collection<Object[]> parameters() {
+ return Arrays.<Object[]>asList(
+ (Object[]) new String[]{ "192.168.1.61", "|1|F1E1KeoE/eEWhi10WpGv4OdiO6Y=|3988QV0VE8wmZL7suNrYQLITLCg=" });
+ }
+
+ @Test
+ public void testDecodeEncode() {
+ assertSame("Mismatched digester", KnownHostHashValue.DEFAULT_DIGEST, hash.getDigester());
+ assertEquals("Mismatched encoded form", hashValue, hash.toString());
+ }
+
+ @Test
+ public void testHostMatch() {
+ assertTrue("Specified host does not match", hash.isHostMatch(hostName));
+ assertFalse("Unexpected host match", hash.isHostMatch(getCurrentTestName()));
+ }
+
+ @Test
+ public void testCalculateHashValue() throws Exception {
+ byte[] expected = hash.getDigestValue();
+ byte[] actual = KnownHostHashValue.calculateHashValue(hostName, hash.getDigester(), hash.getSaltValue());
+ assertArrayEquals("Mismatched hash value", expected, actual);
+ }
+}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/509c871e/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
new file mode 100644
index 0000000..ec073f8
--- /dev/null
+++ b/sshd-core/src/test/java/org/apache/sshd/client/keyverifier/KnownHostsServerKeyVerifierTest.java
@@ -0,0 +1,358 @@
+/*
+ * 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.net.SocketAddress;
+import java.net.URL;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+import java.security.KeyPair;
+import java.security.PublicKey;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Map;
+import java.util.TreeMap;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.apache.sshd.client.ClientFactoryManager;
+import org.apache.sshd.client.config.hosts.KnownHostEntry;
+import org.apache.sshd.client.config.hosts.KnownHostHashValue;
+import org.apache.sshd.client.keyverifier.KnownHostsServerKeyVerifier.HostEntryPair;
+import org.apache.sshd.client.session.ClientSession;
+import org.apache.sshd.common.NamedFactory;
+import org.apache.sshd.common.config.keys.AuthorizedKeyEntry;
+import org.apache.sshd.common.config.keys.PublicKeyEntry;
+import org.apache.sshd.common.config.keys.PublicKeyEntryResolver;
+import org.apache.sshd.common.mac.Mac;
+import org.apache.sshd.common.random.JceRandomFactory;
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.ValidateUtils;
+import org.apache.sshd.common.util.net.SshdSocketAddress;
+import org.apache.sshd.util.test.BaseTestSupport;
+import org.apache.sshd.util.test.Utils;
+import org.junit.BeforeClass;
+import org.junit.FixMethodOrder;
+import org.junit.Test;
+import org.junit.runners.MethodSorters;
+import org.mockito.Mockito;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+@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> hostsKeys = new TreeMap<String, PublicKey>(String.CASE_INSENSITIVE_ORDER);
+ private static Map<String, KnownHostEntry> hostsEntries;
+ private static Path entriesFile;
+
+ public KnownHostsServerKeyVerifierTest() {
+ super();
+ }
+
+ @BeforeClass
+ public static void loadHostsEntries() throws Exception {
+ URL url = KnownHostsServerKeyVerifierTest.class.getResource(KnownHostEntry.STD_HOSTS_FILENAME);
+ assertNotNull("Missing test file resource", url);
+ entriesFile = new File(url.toURI()).toPath();
+ outputDebugMessage("loadHostsEntries(%s)", entriesFile);
+ hostsEntries = loadEntries(entriesFile);
+
+ for (Map.Entry<String, KnownHostEntry> ke : hostsEntries.entrySet()) {
+ String host = 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, hostsKeys.put(host, key));
+ }
+ }
+
+ @Test
+ public void testNoUpdatesNoNewHostsAuthentication() throws Exception {
+ final AtomicInteger delegateCount = new AtomicInteger(0);
+ ServerKeyVerifier delegate = new ServerKeyVerifier() {
+ @Override
+ public boolean verifyServerKey(ClientSession clientSession, SocketAddress remoteAddress, PublicKey serverKey) {
+ delegateCount.incrementAndGet();
+ fail("verifyServerKey(" + clientSession + ")[" + remoteAddress + "] unexpected invocation");
+ return false;
+ }
+ };
+
+ final AtomicInteger updateCount = new AtomicInteger(0);
+ ServerKeyVerifier verifier = new KnownHostsServerKeyVerifier(delegate, createKnownHostsCopy()) {
+ @Override
+ protected KnownHostEntry updateKnownHostsFile(
+ ClientSession clientSession, SocketAddress remoteAddress, PublicKey serverKey,
+ Path file, Collection<HostEntryPair> knownHosts)
+ throws Exception {
+ updateCount.incrementAndGet();
+ fail("updateKnownHostsFile(" + clientSession + ")[" + remoteAddress + "] unexpected invocation: " + file);
+ return super.updateKnownHostsFile(clientSession, remoteAddress, serverKey, file, knownHosts);
+ }
+
+ };
+
+ for (Map.Entry<String, PublicKey> ke : hostsKeys.entrySet()) {
+ String host = ke.getKey();
+ PublicKey serverKey = ke.getValue();
+ KnownHostEntry entry = hostsEntries.get(host);
+ outputDebugMessage("Verify host=%s", entry);
+ assertTrue("Failed to verify server=" + entry, invokeVerifier(verifier, host, serverKey));
+ assertEquals("Unexpected delegate invocation for host=" + entry, 0, delegateCount.get());
+ assertEquals("Unexpected update invocation for host=" + entry, 0, updateCount.get());
+ }
+ }
+
+ @Test
+ public void testFileUpdatedOnEveryNewHost() throws Exception {
+ final AtomicInteger delegateCount = new AtomicInteger(0);
+ ServerKeyVerifier delegate = new ServerKeyVerifier() {
+ @Override
+ public boolean verifyServerKey(ClientSession clientSession, SocketAddress remoteAddress, PublicKey serverKey) {
+ delegateCount.incrementAndGet();
+ return true;
+ }
+ };
+
+ Path path = getKnownHostCopyPath();
+ Files.deleteIfExists(path);
+
+ final AtomicInteger updateCount = new AtomicInteger(0);
+ ServerKeyVerifier verifier = new KnownHostsServerKeyVerifier(delegate, path) {
+ @Override
+ protected KnownHostEntry updateKnownHostsFile(
+ ClientSession clientSession, SocketAddress remoteAddress, PublicKey serverKey,
+ Path file, Collection<HostEntryPair> knownHosts)
+ throws Exception {
+ updateCount.incrementAndGet();
+ return super.updateKnownHostsFile(clientSession, remoteAddress, serverKey, file, knownHosts);
+ }
+ };
+
+ int verificationCount = 0;
+ for (Map.Entry<String, PublicKey> ke : hostsKeys.entrySet()) {
+ String host = ke.getKey();
+ PublicKey serverKey = ke.getValue();
+ KnownHostEntry entry = hostsEntries.get(host);
+ outputDebugMessage("Verify host=%s", entry);
+ assertTrue("Failed to verify server=" + entry, invokeVerifier(verifier, host, 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);
+ for (Map.Entry<String, KnownHostEntry> ke : hostsEntries.entrySet()) {
+ String host = ke.getKey();
+ KnownHostEntry expected = ke.getValue();
+ KnownHostEntry actual = updatedEntries.remove(host);
+ assertNotNull("No updated entry for host=" + host, 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);
+ }
+
+ int pos = expLine.indexOf("comment-");
+ if (pos > 0) {
+ expLine = expLine.substring(0, pos).trim();
+ }
+
+ assertEquals("Mismatched entry data for host=" + host, expLine, actual.getConfigLine());
+ }
+
+ assertTrue("Unexpected extra updated hosts: " + updatedEntries, updatedEntries.isEmpty());
+ }
+
+ @Test
+ public void testWriteHashedHostValues() throws Exception {
+ Path path = getKnownHostCopyPath();
+ Files.deleteIfExists(path);
+
+ KnownHostsServerKeyVerifier verifier = new KnownHostsServerKeyVerifier(AcceptAllServerKeyVerifier.INSTANCE, path) {
+ @Override
+ protected NamedFactory<Mac> getHostValueDigester(ClientSession clientSession, SocketAddress remoteAddress, String hostIdentity) {
+ return KnownHostHashValue.DEFAULT_DIGEST;
+ }
+ };
+
+ ClientFactoryManager manager = Mockito.mock(ClientFactoryManager.class);
+ Mockito.when(manager.getRandomFactory()).thenReturn(JceRandomFactory.INSTANCE);
+
+ ClientSession session = Mockito.mock(ClientSession.class);
+ Mockito.when(session.getFactoryManager()).thenReturn(manager);
+ for (Map.Entry<String, PublicKey> ke : hostsKeys.entrySet()) {
+ String host = ke.getKey();
+ PublicKey serverKey = ke.getValue();
+ KnownHostEntry entry = hostsEntries.get(host);
+ 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));
+ }
+
+ // force re-read to ensure all values are hashed
+ Collection<HostEntryPair> keys = verifier.reloadKnownHosts(path);
+ for (HostEntryPair ke : keys) {
+ KnownHostEntry entry = ke.getHostEntry();
+ assertNotNull("No hashing for entry=" + entry, entry.getHashedEntry());
+ }
+ verifier.setLoadedHostsEntries(keys);
+
+ // make sure can still validate the original hosts
+ for (Map.Entry<String, PublicKey> ke : hostsKeys.entrySet()) {
+ String host = ke.getKey();
+ PublicKey serverKey = ke.getValue();
+ KnownHostEntry entry = hostsEntries.get(host);
+ 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));
+ }
+ }
+
+ @Test
+ public void testRejectModifiedServerKey() throws Exception {
+ KeyPair kp = Utils.generateKeyPair("RSA", 1024);
+ final PublicKey modifiedKey = kp.getPublic();
+ final AtomicInteger acceptCount = new AtomicInteger(0);
+ ServerKeyVerifier verifier = new KnownHostsServerKeyVerifier(AcceptAllServerKeyVerifier.INSTANCE, createKnownHostsCopy()) {
+ @Override
+ protected boolean acceptModifiedServerKey(
+ ClientSession clientSession, SocketAddress remoteAddress, HostEntryPair match, PublicKey actual) {
+ acceptCount.incrementAndGet();
+ assertSame("Mismatched actual key for " + remoteAddress, modifiedKey, actual);
+ return super.acceptModifiedServerKey(clientSession, remoteAddress, match, actual);
+ }
+ };
+
+ int validationCount = 0;
+ for (Map.Entry<String, KnownHostEntry> ke : hostsEntries.entrySet()) {
+ String host = ke.getKey();
+ KnownHostEntry entry = ke.getValue();
+ outputDebugMessage("Verify host=%s", entry);
+ assertFalse("Unexpected to verification success for " + entry, invokeVerifier(verifier, host, modifiedKey));
+ validationCount++;
+ assertEquals("Mismatched invocation count for host=" + entry, validationCount, acceptCount.get());
+ }
+ }
+
+ @Test
+ public void testAcceptModifiedServerKeyUpdatesFile() throws Exception {
+ KeyPair kp = Utils.generateKeyPair("RSA", 1024);
+ final PublicKey modifiedKey = kp.getPublic();
+ Path path = createKnownHostsCopy();
+ ServerKeyVerifier verifier = new KnownHostsServerKeyVerifier(AcceptAllServerKeyVerifier.INSTANCE, path) {
+ @Override
+ protected boolean acceptModifiedServerKey(
+ ClientSession clientSession, SocketAddress remoteAddress, HostEntryPair match, PublicKey actual) {
+ assertSame("Mismatched actual key for " + remoteAddress, modifiedKey, actual);
+ return true;
+ }
+ };
+
+ for (Map.Entry<String, KnownHostEntry> ke : hostsEntries.entrySet()) {
+ String host = ke.getKey();
+ KnownHostEntry entry = ke.getValue();
+ outputDebugMessage("Verify host=%s", entry);
+ assertTrue("Failed to verify " + entry, invokeVerifier(verifier, host, modifiedKey));
+ }
+
+ String expected = PublicKeyEntry.toString(modifiedKey);
+ Map<String, KnownHostEntry> updatedKeys = loadEntries(path);
+ for (Map.Entry<String, KnownHostEntry> ke : hostsEntries.entrySet()) {
+ String host = ke.getKey();
+ KnownHostEntry original = ke.getValue();
+ KnownHostEntry updated = updatedKeys.remove(host);
+ assertNotNull("No updated entry for " + original, updated);
+
+ String actual = updated.getConfigLine();
+ int pos = actual.indexOf(' ');
+ if (actual.charAt(0) == KnownHostEntry.MARKER_INDICATOR) {
+ for (pos++; pos < actual.length(); pos++) {
+ if (actual.charAt(pos) != ' ') {
+ break;
+ }
+ }
+ pos = actual.indexOf(' ', pos);
+ }
+
+ actual = GenericUtils.trimToEmpty(actual.substring(pos + 1));
+ assertEquals("Mismatched updated value for host=" + host, expected, actual);
+ }
+
+ assertTrue("Unexpected extra updated entries: " + updatedKeys, updatedKeys.isEmpty());
+ }
+
+ private Path createKnownHostsCopy() throws IOException {
+ Path file = getKnownHostCopyPath();
+ Files.copy(entriesFile, file, StandardCopyOption.REPLACE_EXISTING);
+ return file;
+ }
+
+ private Path getKnownHostCopyPath() throws IOException {
+ Path file = getTempTargetRelativeFile(getClass().getSimpleName(), getCurrentTestName());
+ assertHierarchyTargetFolderExists(file.getParent());
+ return file;
+ }
+
+ private boolean invokeVerifier(ServerKeyVerifier verifier, String host, PublicKey serverKey) {
+ SocketAddress address = new SshdSocketAddress(host, 7365);
+ 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);
+ }
+
+ private static Map<String, 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, KnownHostEntry>(String.CASE_INSENSITIVE_ORDER);
+ for (KnownHostEntry entry : entries) {
+ String line = entry.getConfigLine();
+ outputDebugMessage("loadTestLines(%s) processing %s", file, line);
+ // extract hosts
+ 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));
+ } else {
+ String[] addrs = GenericUtils.split(patterns, ',');
+ for (String a : addrs) {
+ assertNull("Multiple entries for address=" + a, hostsMap.put(a, entry));
+ }
+ }
+ }
+
+ return hostsMap;
+ }
+}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/509c871e/sshd-core/src/test/java/org/apache/sshd/client/scp/ScpTest.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/test/java/org/apache/sshd/client/scp/ScpTest.java b/sshd-core/src/test/java/org/apache/sshd/client/scp/ScpTest.java
index 57d49ba..7d5550d 100644
--- a/sshd-core/src/test/java/org/apache/sshd/client/scp/ScpTest.java
+++ b/sshd-core/src/test/java/org/apache/sshd/client/scp/ScpTest.java
@@ -758,7 +758,7 @@ public class ScpTest extends BaseTestSupport {
try {
scp.upload(data, remotePath, EnumSet.allOf(PosixFilePermission.class), null);
outputDebugMessage("Upload success to %s", remotePath);
- } catch(ScpException e) {
+ } catch (ScpException e) {
Integer exitCode = e.getExitStatus();
assertNotNull("No upload exit status", exitCode);
assertEquals("Mismatched upload exit status", TEST_EXIT_VALUE, exitCode.intValue());
@@ -775,7 +775,7 @@ public class ScpTest extends BaseTestSupport {
try {
byte[] downloaded = scp.downloadBytes(remotePath);
outputDebugMessage("Download success to %s: %s", remotePath, new String(downloaded, StandardCharsets.UTF_8));
- } catch(ScpException e) {
+ } catch (ScpException e) {
Integer exitCode = e.getExitStatus();
assertNotNull("No download exit status", exitCode);
assertEquals("Mismatched download exit status", TEST_EXIT_VALUE, exitCode.intValue());
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/509c871e/sshd-core/src/test/java/org/apache/sshd/common/config/keys/AuthorizedKeyEntryTest.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/test/java/org/apache/sshd/common/config/keys/AuthorizedKeyEntryTest.java b/sshd-core/src/test/java/org/apache/sshd/common/config/keys/AuthorizedKeyEntryTest.java
new file mode 100644
index 0000000..a40eb1e
--- /dev/null
+++ b/sshd-core/src/test/java/org/apache/sshd/common/config/keys/AuthorizedKeyEntryTest.java
@@ -0,0 +1,137 @@
+/*
+ * 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.common.config.keys;
+
+import java.nio.file.Files;
+import java.nio.file.LinkOption;
+import java.nio.file.Path;
+import java.security.PublicKey;
+import java.util.Collection;
+import java.util.List;
+
+import org.apache.sshd.common.config.keys.AuthorizedKeyEntry;
+import org.apache.sshd.common.config.keys.KeyUtils;
+import org.apache.sshd.common.config.keys.PublicKeyEntryResolver;
+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.server.auth.pubkey.PublickeyAuthenticator;
+import org.apache.sshd.server.config.keys.AuthorizedKeysAuthenticator;
+import org.junit.FixMethodOrder;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runners.MethodSorters;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+public class AuthorizedKeyEntryTest extends AuthorizedKeysTestSupport {
+ public AuthorizedKeyEntryTest() {
+ super();
+ }
+
+ @Test
+ public void testReadAuthorizedKeysFile() throws Exception {
+ Path file = getTempTargetRelativeFile(getCurrentTestName());
+ writeDefaultSupportedKeys(file);
+ runAuthorizedKeysTests(AuthorizedKeyEntry.readAuthorizedKeys(file));
+ }
+
+ @Test
+ public void testEncodePublicKeyEntry() throws Exception {
+ List<String> keyLines = loadDefaultSupportedKeys();
+ StringBuilder sb = new StringBuilder(Byte.MAX_VALUE);
+ for (String line : keyLines) {
+ int pos = line.indexOf(' ');
+ String data = line;
+ String keyType = line.substring(0, pos);
+ // assume this happens if starts with login options
+ if (KeyUtils.getPublicKeyEntryDecoder(keyType) == null) {
+ data = line.substring(pos + 1).trim();
+ }
+
+ AuthorizedKeyEntry entry = AuthorizedKeyEntry.parseAuthorizedKeyEntry(data);
+ if (sb.length() > 0) {
+ sb.setLength(0);
+ }
+
+ PublicKey key = entry.appendPublicKey(sb, PublicKeyEntryResolver.FAILING);
+ assertNotNull("No key for line=" + line, key);
+
+ String encoded = sb.toString();
+ assertEquals("Mismatched encoded form for line=" + line, data, encoded);
+ }
+ }
+
+ @Test
+ @Ignore("It might cause some exceptions if user's file contains unsupported keys")
+ public void testReadDefaultAuthorizedKeysFile() throws Exception {
+ Path path = AuthorizedKeysAuthenticator.getDefaultAuthorizedKeysFile();
+ assertNotNull("No default location", path);
+
+ LinkOption[] options = IoUtils.getLinkOptions(false);
+ if (!Files.exists(path, options)) {
+ outputDebugMessage("Verify non-existing %s", path);
+ Collection<AuthorizedKeyEntry> entries = AuthorizedKeysAuthenticator.readDefaultAuthorizedKeys();
+ assertTrue("Non-empty keys even though file not found: " + entries, GenericUtils.isEmpty(entries));
+ } else {
+ assertFalse("Not a file: " + path, Files.isDirectory(path, options));
+ runAuthorizedKeysTests(AuthorizedKeysAuthenticator.readDefaultAuthorizedKeys());
+ }
+ }
+
+ private void runAuthorizedKeysTests(Collection<AuthorizedKeyEntry> entries) throws Exception {
+ testReadAuthorizedKeys(entries);
+ testAuthorizedKeysAuth(entries);
+ }
+
+ private static Collection<AuthorizedKeyEntry> testReadAuthorizedKeys(Collection<AuthorizedKeyEntry> entries) throws Exception {
+ assertFalse("No entries read", GenericUtils.isEmpty(entries));
+
+ Exception err = null;
+ for (AuthorizedKeyEntry entry : entries) {
+ try {
+ ValidateUtils.checkNotNull(entry.resolvePublicKey(PublicKeyEntryResolver.FAILING), "No public key resolved from %s", entry);
+ } catch (Exception e) {
+ System.err.append("Failed (").append(e.getClass().getSimpleName()).append(')')
+ .append(" to resolve key of entry=").append(entry.toString())
+ .append(": ").println(e.getMessage());
+ err = e;
+ }
+ }
+
+ if (err != null) {
+ throw err;
+ }
+
+ return entries;
+ }
+
+ private PublickeyAuthenticator testAuthorizedKeysAuth(Collection<AuthorizedKeyEntry> entries) throws Exception {
+ Collection<PublicKey> keySet = AuthorizedKeyEntry.resolveAuthorizedKeys(PublicKeyEntryResolver.FAILING, entries);
+ PublickeyAuthenticator auth = AuthorizedKeyEntry.fromAuthorizedEntries(PublicKeyEntryResolver.FAILING, entries);
+ for (PublicKey key : keySet) {
+ assertTrue("Failed to authenticate with key=" + key.getAlgorithm(), auth.authenticate(getCurrentTestName(), key, null));
+ }
+
+ return auth;
+ }
+}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/509c871e/sshd-core/src/test/java/org/apache/sshd/common/config/keys/AuthorizedKeysTestSupport.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/test/java/org/apache/sshd/common/config/keys/AuthorizedKeysTestSupport.java b/sshd-core/src/test/java/org/apache/sshd/common/config/keys/AuthorizedKeysTestSupport.java
new file mode 100644
index 0000000..54f9022
--- /dev/null
+++ b/sshd-core/src/test/java/org/apache/sshd/common/config/keys/AuthorizedKeysTestSupport.java
@@ -0,0 +1,121 @@
+/*
+ * 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.common.config.keys;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.io.Writer;
+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.List;
+
+import org.apache.sshd.common.cipher.ECCurves;
+import org.apache.sshd.common.config.keys.PublicKeyEntry;
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.SecurityUtils;
+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;
+import org.apache.sshd.server.config.keys.AuthorizedKeysAuthenticator;
+import org.apache.sshd.util.test.BaseTestSupport;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public abstract class AuthorizedKeysTestSupport extends BaseTestSupport {
+ protected AuthorizedKeysTestSupport() {
+ super();
+ }
+
+ protected List<String> writeDefaultSupportedKeys(Path file, OpenOption ... options) throws IOException {
+ List<String> keyLines = loadDefaultSupportedKeys();
+ if (Files.exists(file)) {
+ Files.delete(file);
+ }
+
+ assertHierarchyTargetFolderExists(file.getParent());
+
+ try (Writer w = Files.newBufferedWriter(file, StandardCharsets.UTF_8, options)) {
+ w.append(PublicKeyEntry.COMMENT_CHAR)
+ .append(' ').append(getCurrentTestName())
+ .append(' ').append(String.valueOf(keyLines.size())).append(" remaining keys")
+ .append(IoUtils.EOL)
+ ;
+ for (String l : keyLines) {
+ w.append(l).append(IoUtils.EOL);
+ }
+ }
+
+ return keyLines;
+ }
+
+ protected List<String> loadDefaultSupportedKeys() throws IOException {
+ return loadSupportedKeys(
+ ValidateUtils.checkNotNull(
+ getClass().getResource(AuthorizedKeysAuthenticator.STD_AUTHORIZED_KEYS_FILENAME),
+ "Missing resource=" + AuthorizedKeysAuthenticator.STD_AUTHORIZED_KEYS_FILENAME));
+ }
+
+ public static List<String> loadSupportedKeys(URL url) throws IOException {
+ return loadSupportedKeys(url.openStream(), true);
+ }
+
+ public static List<String> loadSupportedKeys(InputStream input, boolean okToClose) throws IOException {
+ try (Reader r = new InputStreamReader(NoCloseInputStream.resolveInputStream(input, okToClose), StandardCharsets.UTF_8)) {
+ return loadSupportedKeys(r, true);
+ }
+ }
+
+ public static List<String> loadSupportedKeys(Reader rdr, boolean okToClose) throws IOException {
+ try (BufferedReader buf = new BufferedReader(NoCloseReader.resolveReader(rdr, okToClose))) {
+ return loadSupportedKeys(buf);
+ }
+ }
+
+ public static List<String> loadSupportedKeys(BufferedReader rdr) throws IOException {
+ List<String> keyLines = new ArrayList<String>();
+ boolean eccSupported = SecurityUtils.hasEcc();
+ for (String l = rdr.readLine(); l != null; l = rdr.readLine()) {
+ l = GenericUtils.trimToEmpty(l);
+ // filter out empty and comment lines
+ if (GenericUtils.isEmpty(l) || (l.charAt(0) == PublicKeyEntry.COMMENT_CHAR)) {
+ continue;
+ }
+
+ // skip EC keys if ECC not supported
+ if (l.contains(ECCurves.Constants.ECDSA_SHA2_PREFIX) && (!eccSupported)) {
+ System.out.println("Skip (ECC not supported) " + l);
+ continue;
+ }
+
+ keyLines.add(l);
+ }
+
+ return keyLines;
+ }
+}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/509c871e/sshd-core/src/test/java/org/apache/sshd/common/util/Base64Test.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/test/java/org/apache/sshd/common/util/Base64Test.java b/sshd-core/src/test/java/org/apache/sshd/common/util/Base64Test.java
new file mode 100644
index 0000000..9c2ec8a
--- /dev/null
+++ b/sshd-core/src/test/java/org/apache/sshd/common/util/Base64Test.java
@@ -0,0 +1,73 @@
+/*
+ * 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.common.util;
+
+import java.nio.charset.StandardCharsets;
+
+import org.apache.sshd.util.test.BaseTestSupport;
+import org.junit.FixMethodOrder;
+import org.junit.Test;
+import org.junit.runners.MethodSorters;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+public class Base64Test extends BaseTestSupport {
+ public Base64Test() {
+ super();
+ }
+
+ @Test
+ public void testDiscardWhitespaceNullOrEmptyData() {
+ assertSame("Mismatched result for null data", GenericUtils.EMPTY_BYTE_ARRAY, Base64.discardWhitespace(null));
+ assertSame("Mismatched result for empty data", GenericUtils.EMPTY_BYTE_ARRAY, Base64.discardWhitespace(new byte[0]));
+ }
+
+ @Test
+ public void testDiscardWhitespaceOnNonWhitespace() {
+ byte[] expected = getCurrentTestName().getBytes(StandardCharsets.UTF_8);
+ byte[] actual = Base64.discardWhitespace(expected);
+ assertSame("Mismatched result", expected, actual);
+ }
+
+ @Test
+ public void testDiscardWhitespaceOnExistingWhitespace() {
+ String expected = getCurrentTestName();
+ StringBuilder sb = new StringBuilder(expected.length() * 2);
+ for (int index = 0; index < expected.length(); index++) {
+ sb.append(" \t\r\n".charAt(index % 4)).append(expected.charAt(index));
+ }
+
+ byte[] data = sb.toString().getBytes(StandardCharsets.UTF_8);
+ byte[] result = Base64.discardWhitespace(data);
+ assertEquals("Mismatched cleaned result", expected, new String(result, StandardCharsets.UTF_8));
+ }
+
+ @Test
+ public void testBasicEncodeDecode() {
+ String expected = getCurrentTestName();
+ String b64 = Base64.encodeToString(expected.getBytes(StandardCharsets.UTF_8));
+ byte[] decoded = Base64.decodeBase64(b64.getBytes(StandardCharsets.UTF_8));
+ String actual = new String(decoded, StandardCharsets.UTF_8);
+ assertEquals("Mismatched decoded result for " + b64, expected, actual);
+
+ }
+}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/509c871e/sshd-core/src/test/java/org/apache/sshd/server/config/keys/AuthorizedKeyEntryTest.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/test/java/org/apache/sshd/server/config/keys/AuthorizedKeyEntryTest.java b/sshd-core/src/test/java/org/apache/sshd/server/config/keys/AuthorizedKeyEntryTest.java
deleted file mode 100644
index 2420f8c..0000000
--- a/sshd-core/src/test/java/org/apache/sshd/server/config/keys/AuthorizedKeyEntryTest.java
+++ /dev/null
@@ -1,135 +0,0 @@
-/*
- * 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.server.config.keys;
-
-import java.nio.file.Files;
-import java.nio.file.LinkOption;
-import java.nio.file.Path;
-import java.security.PublicKey;
-import java.util.Collection;
-import java.util.List;
-
-import org.apache.sshd.common.config.keys.KeyUtils;
-import org.apache.sshd.common.config.keys.PublicKeyEntryResolver;
-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.server.auth.pubkey.PublickeyAuthenticator;
-import org.junit.FixMethodOrder;
-import org.junit.Ignore;
-import org.junit.Test;
-import org.junit.runners.MethodSorters;
-
-/**
- * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
- */
-@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-public class AuthorizedKeyEntryTest extends AuthorizedKeysTestSupport {
- public AuthorizedKeyEntryTest() {
- super();
- }
-
- @Test
- public void testReadAuthorizedKeysFile() throws Exception {
- Path file = getTempTargetRelativeFile(getCurrentTestName());
- writeDefaultSupportedKeys(file);
- runAuthorizedKeysTests(AuthorizedKeyEntry.readAuthorizedKeys(file));
- }
-
- @Test
- public void testEncodePublicKeyEntry() throws Exception {
- List<String> keyLines = loadDefaultSupportedKeys();
- StringBuilder sb = new StringBuilder(Byte.MAX_VALUE);
- for (String line : keyLines) {
- int pos = line.indexOf(' ');
- String data = line;
- String keyType = line.substring(0, pos);
- // assume this happens if starts with login options
- if (KeyUtils.getPublicKeyEntryDecoder(keyType) == null) {
- data = line.substring(pos + 1).trim();
- }
-
- AuthorizedKeyEntry entry = AuthorizedKeyEntry.parseAuthorizedKeyEntry(data);
- if (sb.length() > 0) {
- sb.setLength(0);
- }
-
- PublicKey key = entry.appendPublicKey(sb, PublicKeyEntryResolver.FAILING);
- assertNotNull("No key for line=" + line, key);
-
- String encoded = sb.toString();
- assertEquals("Mismatched encoded form for line=" + line, data, encoded);
- }
- }
-
- @Test
- @Ignore("It might cause some exceptions if user's file contains unsupported keys")
- public void testReadDefaultAuthorizedKeysFile() throws Exception {
- Path path = AuthorizedKeyEntry.getDefaultAuthorizedKeysFile();
- assertNotNull("No default location", path);
-
- LinkOption[] options = IoUtils.getLinkOptions(false);
- if (!Files.exists(path, options)) {
- outputDebugMessage("Verify non-existing %s", path);
- Collection<AuthorizedKeyEntry> entries = AuthorizedKeyEntry.readDefaultAuthorizedKeys();
- assertTrue("Non-empty keys even though file not found: " + entries, GenericUtils.isEmpty(entries));
- } else {
- assertFalse("Not a file: " + path, Files.isDirectory(path, options));
- runAuthorizedKeysTests(AuthorizedKeyEntry.readDefaultAuthorizedKeys());
- }
- }
-
- private void runAuthorizedKeysTests(Collection<AuthorizedKeyEntry> entries) throws Exception {
- testReadAuthorizedKeys(entries);
- testAuthorizedKeysAuth(entries);
- }
-
- private static Collection<AuthorizedKeyEntry> testReadAuthorizedKeys(Collection<AuthorizedKeyEntry> entries) throws Exception {
- assertFalse("No entries read", GenericUtils.isEmpty(entries));
-
- Exception err = null;
- for (AuthorizedKeyEntry entry : entries) {
- try {
- ValidateUtils.checkNotNull(entry.resolvePublicKey(PublicKeyEntryResolver.FAILING), "No public key resolved from %s", entry);
- } catch (Exception e) {
- System.err.append("Failed (").append(e.getClass().getSimpleName()).append(')')
- .append(" to resolve key of entry=").append(entry.toString())
- .append(": ").println(e.getMessage());
- err = e;
- }
- }
-
- if (err != null) {
- throw err;
- }
-
- return entries;
- }
-
- private PublickeyAuthenticator testAuthorizedKeysAuth(Collection<AuthorizedKeyEntry> entries) throws Exception {
- Collection<PublicKey> keySet = AuthorizedKeyEntry.resolveAuthorizedKeys(PublicKeyEntryResolver.FAILING, entries);
- PublickeyAuthenticator auth = AuthorizedKeyEntry.fromAuthorizedEntries(PublicKeyEntryResolver.FAILING, entries);
- for (PublicKey key : keySet) {
- assertTrue("Failed to authenticate with key=" + key.getAlgorithm(), auth.authenticate(getCurrentTestName(), key, null));
- }
-
- return auth;
- }
-}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/509c871e/sshd-core/src/test/java/org/apache/sshd/server/config/keys/AuthorizedKeysAuthenticatorTest.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/test/java/org/apache/sshd/server/config/keys/AuthorizedKeysAuthenticatorTest.java b/sshd-core/src/test/java/org/apache/sshd/server/config/keys/AuthorizedKeysAuthenticatorTest.java
index 3ec0e35..33d895e 100644
--- a/sshd-core/src/test/java/org/apache/sshd/server/config/keys/AuthorizedKeysAuthenticatorTest.java
+++ b/sshd-core/src/test/java/org/apache/sshd/server/config/keys/AuthorizedKeysAuthenticatorTest.java
@@ -29,6 +29,8 @@ import java.util.Collection;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
+import org.apache.sshd.common.config.keys.AuthorizedKeyEntry;
+import org.apache.sshd.common.config.keys.AuthorizedKeysTestSupport;
import org.apache.sshd.common.config.keys.PublicKeyEntry;
import org.apache.sshd.common.config.keys.PublicKeyEntryResolver;
import org.apache.sshd.common.util.io.IoUtils;
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/509c871e/sshd-core/src/test/java/org/apache/sshd/server/config/keys/AuthorizedKeysTestSupport.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/test/java/org/apache/sshd/server/config/keys/AuthorizedKeysTestSupport.java b/sshd-core/src/test/java/org/apache/sshd/server/config/keys/AuthorizedKeysTestSupport.java
deleted file mode 100644
index 3b6d0d5..0000000
--- a/sshd-core/src/test/java/org/apache/sshd/server/config/keys/AuthorizedKeysTestSupport.java
+++ /dev/null
@@ -1,120 +0,0 @@
-/*
- * 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.server.config.keys;
-
-import java.io.BufferedReader;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.io.Reader;
-import java.io.Writer;
-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.List;
-
-import org.apache.sshd.common.cipher.ECCurves;
-import org.apache.sshd.common.config.keys.PublicKeyEntry;
-import org.apache.sshd.common.util.GenericUtils;
-import org.apache.sshd.common.util.SecurityUtils;
-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;
-import org.apache.sshd.util.test.BaseTestSupport;
-
-/**
- * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
- */
-public abstract class AuthorizedKeysTestSupport extends BaseTestSupport {
- protected AuthorizedKeysTestSupport() {
- super();
- }
-
- protected List<String> writeDefaultSupportedKeys(Path file, OpenOption ... options) throws IOException {
- List<String> keyLines = loadDefaultSupportedKeys();
- if (Files.exists(file)) {
- Files.delete(file);
- }
-
- assertHierarchyTargetFolderExists(file.getParent());
-
- try (Writer w = Files.newBufferedWriter(file, StandardCharsets.UTF_8, options)) {
- w.append(PublicKeyEntry.COMMENT_CHAR)
- .append(' ').append(getCurrentTestName())
- .append(' ').append(String.valueOf(keyLines.size())).append(" remaining keys")
- .append(IoUtils.EOL)
- ;
- for (String l : keyLines) {
- w.append(l).append(IoUtils.EOL);
- }
- }
-
- return keyLines;
- }
-
- protected List<String> loadDefaultSupportedKeys() throws IOException {
- return loadSupportedKeys(
- ValidateUtils.checkNotNull(
- getClass().getResource(AuthorizedKeyEntry.STD_AUTHORIZED_KEYS_FILENAME),
- "Missing resource=" + AuthorizedKeyEntry.STD_AUTHORIZED_KEYS_FILENAME));
- }
-
- public static List<String> loadSupportedKeys(URL url) throws IOException {
- return loadSupportedKeys(url.openStream(), true);
- }
-
- public static List<String> loadSupportedKeys(InputStream input, boolean okToClose) throws IOException {
- try (Reader r = new InputStreamReader(NoCloseInputStream.resolveInputStream(input, okToClose), StandardCharsets.UTF_8)) {
- return loadSupportedKeys(r, true);
- }
- }
-
- public static List<String> loadSupportedKeys(Reader rdr, boolean okToClose) throws IOException {
- try (BufferedReader buf = new BufferedReader(NoCloseReader.resolveReader(rdr, okToClose))) {
- return loadSupportedKeys(buf);
- }
- }
-
- public static List<String> loadSupportedKeys(BufferedReader rdr) throws IOException {
- List<String> keyLines = new ArrayList<String>();
- boolean eccSupported = SecurityUtils.hasEcc();
- for (String l = rdr.readLine(); l != null; l = rdr.readLine()) {
- l = GenericUtils.trimToEmpty(l);
- // filter out empty and comment lines
- if (GenericUtils.isEmpty(l) || (l.charAt(0) == PublicKeyEntry.COMMENT_CHAR)) {
- continue;
- }
-
- // skip EC keys if ECC not supported
- if (l.contains(ECCurves.Constants.ECDSA_SHA2_PREFIX) && (!eccSupported)) {
- System.out.println("Skip (ECC not supported) " + l);
- continue;
- }
-
- keyLines.add(l);
- }
-
- return keyLines;
- }
-}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/509c871e/sshd-core/src/test/java/org/apache/sshd/server/config/keys/DefaultAuthorizedKeysAuthenticatorTest.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/test/java/org/apache/sshd/server/config/keys/DefaultAuthorizedKeysAuthenticatorTest.java b/sshd-core/src/test/java/org/apache/sshd/server/config/keys/DefaultAuthorizedKeysAuthenticatorTest.java
index cd6809c..5d6b22a 100644
--- a/sshd-core/src/test/java/org/apache/sshd/server/config/keys/DefaultAuthorizedKeysAuthenticatorTest.java
+++ b/sshd-core/src/test/java/org/apache/sshd/server/config/keys/DefaultAuthorizedKeysAuthenticatorTest.java
@@ -23,6 +23,8 @@ import java.nio.file.Path;
import java.security.PublicKey;
import java.util.Collection;
+import org.apache.sshd.common.config.keys.AuthorizedKeyEntry;
+import org.apache.sshd.common.config.keys.AuthorizedKeysTestSupport;
import org.apache.sshd.common.config.keys.PublicKeyEntryResolver;
import org.apache.sshd.common.util.OsUtils;
import org.apache.sshd.server.auth.pubkey.PublickeyAuthenticator;
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/509c871e/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
new file mode 100644
index 0000000..2f8034b
--- /dev/null
+++ b/sshd-core/src/test/resources/org/apache/sshd/client/keyverifier/known_hosts
@@ -0,0 +1,12 @@
+# multiple names for same host
+server.sshd.apache.org,10.23.222.240 ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBCMOm8SqJPJqvSa5Q6r/pGhkp3aBc4Evf9wF8DjhSy13m+wwQwQCENQ8V+5bpI58Z0jjB8O3lmuOLils+Nx9AFc= comment-host-list
+
+# comment
+
+10.23.222.127 ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBCbZVVpqEHGLNWMqMeyU1VbWb91XteoamVcgpy4yxNVbZffb5IDdbo1ons/y9KAhcub6LZeLrvXzVUZbXCZiUkg=
+10.163.4.61 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAvQK9DhBSStbi2CaZNOo5vHy9Nga/hLCnO19tL6i5U/5Nhh5Y8W+tL+AA8hqD/doLBnyaEv2xjfzECwKlStc9HWx6EcJ+9B1rA4+5HztuUEWxuozNnkvcScjTBBqEd7fPPt0INI+pSZRYa2InEBBUUHTt1YaDEXamM/j4RVKovEH7Efgq9VUti148ZG90/w9V5ZT1o6yhuOw9UbME/eHIbS2E9P/Gy33OhkAgTLyOfCZAdJiYvcvFXqNWSKVFx3H5hSolh9ppxVFVFj2hW6QtvgYTphLU0ccHOWTBd/UToG4Xd9GjgSoD1pAI9e0NwBOsUfiqSzO99wqpzs6erfwelQ==
+
+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
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/509c871e/sshd-core/src/test/resources/org/apache/sshd/common/config/keys/authorized_keys
----------------------------------------------------------------------
diff --git a/sshd-core/src/test/resources/org/apache/sshd/common/config/keys/authorized_keys b/sshd-core/src/test/resources/org/apache/sshd/common/config/keys/authorized_keys
new file mode 100644
index 0000000..3a12f32
--- /dev/null
+++ b/sshd-core/src/test/resources/org/apache/sshd/common/config/keys/authorized_keys
@@ -0,0 +1,16 @@
+ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA2KFr3GqL/3yXY2bAwRGGDxl/qLuE9qdx20+DMh5oAZPpwprlUnlxLm+ikimwn65Z0KeUyfofYKt+vc3rl1k2mDqyG8DqHeH0C+uFBbom0fthX7PRiQr2T9SOzSodjowZuBHlWIfgtcZI0bygX+GlKaAq00l4yCoe1xUTCRd2ZVyNuB1nozcFI+sUzdeKfaxvuyvbccG4tOx06HDryNdxW2e99bsAhLAg7d8xciOeb4PCAI1USg83dt0wVZE9VJbnRnoZ2y/DaQCJtBJ8t8uNLVdggakydDzQuglyd4dYRxeU7t4TEw6wsfXPB0kqdecd0Llspjx0ciEY/BbycdiApw== lgoldstein@LGOLDSTEIN-WIN7
+ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAoiPvv8awFmA1iIwNlWD/29gfNyiTnkpjCVtFOqPGM8YTGF2G5FRrAwJyehdJu8qFJSUaNMzrjz4qlP4OyP4qcd16TE3FkIwd22+sXo0K9oEbOF307FVtBGrqnp+m5aFPQIZNhX86Rd9m0zrE4eSkZQ0qOjhp0Q60+G2lleaHSYvdc0IOcOJsWI43+ytlzJ2gKoVwPtttsXVtycjt2ZmD99V/lk3G7sdXQGL5S+lxn7rMtxamOSy+VR1eVu2ZagOCp2XZM1eFNWIRCH0KbRJh2mDrk08pIN9yCh2q/5BF+oh/CQyS8W8754MJuQ+0U0qHBH/wNtogIomedxpW8hG1jQ== lgoldstein@tlv-svn.eng.vmware.com
+
+# some empty lines
+
+
+# no comment for key
+ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIBVdEJBWozLd41Huv2LZOglgE3HpRfcxvKBUB/poMAfRY6nMoStxaVCc/dPzpzE6nOAbX+tuRLL611H3Ooby9quSlKsSfx4/Dk76tPokY7T6eLTmoJP6S3c9OMdNFLQ31UKbW4RlBRFYFFMMQGr3PMzXRKDpQhl0Bvts+N5qQ2eaQ==
+
+# dummy login options
+command="/usr/bin/tinyfugue",environment="PATH=/bin:/usr/bin/:/opt/gtm/bin" ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIBVdEJBWozLd41Huv2LZOglgE3HpRfcxvKBUB/poMAfRY6nMoStxaVCc/dPzpzE6nOAbX+tuRLL611H3Ooby9quSlKsSfx4/Dk76tPokY7T6eLTmoJP6S3c9OMdNFLQ31UKbW4RlBRFYFFMMQGr3PMzXRKDpQhl0Bvts+N5qQ2eaQ==
+hosts="git.eng.vmware.com" ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAvQK9DhBSStbi2CaZNOo5vHy9Nga/hLCnO19tL6i5U/5Nhh5Y8W+tL+AA8hqD/doLBnyaEv2xjfzECwKlStc9HWx6EcJ+9B1rA4+5HztuUEWxuozNnkvcScjTBBqEd7fPPt0INI+pSZRYa2InEBBUUHTt1YaDEXamM/j4RVKovEH7Efgq9VUti148ZG90/w9V5ZT1o6yhuOw9UbME/eHIbS2E9P/Gy33OhkAgTLyOfCZAdJiYvcvFXqNWSKVFx3H5hSolh9ppxVFVFj2hW6QtvgYTphLU0ccHOWTBd/UToG4Xd9GjgSoD1pAI9e0NwBOsUfiqSzO99wqpzs6erfwelQ==
+
+# ECDSA keys
+ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBCbZVVpqEHGLNWMqMeyU1VbWb91XteoamVcgpy4yxNVbZffb5IDdbo1ons/y9KAhcub6LZeLrvXzVUZbXCZiUkg=
+host=10.23.222.240 ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBCMOm8SqJPJqvSa5Q6r/pGhkp3aBc4Evf9wF8DjhSy13m+wwQwQCENQ8V+5bpI58Z0jjB8O3lmuOLils+Nx9AFc=
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/509c871e/sshd-ldap/src/main/java/org/apache/sshd/server/auth/pubkey/LdapPublickeyAuthenticator.java
----------------------------------------------------------------------
diff --git a/sshd-ldap/src/main/java/org/apache/sshd/server/auth/pubkey/LdapPublickeyAuthenticator.java b/sshd-ldap/src/main/java/org/apache/sshd/server/auth/pubkey/LdapPublickeyAuthenticator.java
index ff8f5ce..2584ba3 100644
--- a/sshd-ldap/src/main/java/org/apache/sshd/server/auth/pubkey/LdapPublickeyAuthenticator.java
+++ b/sshd-ldap/src/main/java/org/apache/sshd/server/auth/pubkey/LdapPublickeyAuthenticator.java
@@ -31,12 +31,12 @@ import java.util.Objects;
import javax.naming.NamingException;
+import org.apache.sshd.common.config.keys.AuthorizedKeyEntry;
import org.apache.sshd.common.config.keys.KeyUtils;
import org.apache.sshd.common.config.keys.PublicKeyEntryResolver;
import org.apache.sshd.common.util.GenericUtils;
import org.apache.sshd.common.util.ValidateUtils;
import org.apache.sshd.server.auth.LdapAuthenticator;
-import org.apache.sshd.server.config.keys.AuthorizedKeyEntry;
import org.apache.sshd.server.session.ServerSession;
/**
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/509c871e/sshd-ldap/src/test/java/org/apache/sshd/server/auth/pubkey/LdapPublickeyAuthenticatorTest.java
----------------------------------------------------------------------
diff --git a/sshd-ldap/src/test/java/org/apache/sshd/server/auth/pubkey/LdapPublickeyAuthenticatorTest.java b/sshd-ldap/src/test/java/org/apache/sshd/server/auth/pubkey/LdapPublickeyAuthenticatorTest.java
index 7bf273f..c42e8f9 100644
--- a/sshd-ldap/src/test/java/org/apache/sshd/server/auth/pubkey/LdapPublickeyAuthenticatorTest.java
+++ b/sshd-ldap/src/test/java/org/apache/sshd/server/auth/pubkey/LdapPublickeyAuthenticatorTest.java
@@ -26,13 +26,13 @@ import java.util.concurrent.atomic.AtomicReference;
import org.apache.directory.server.core.DirectoryService;
import org.apache.directory.server.ldap.LdapServer;
+import org.apache.sshd.common.config.keys.AuthorizedKeyEntry;
import org.apache.sshd.common.config.keys.KeyUtils;
import org.apache.sshd.common.config.keys.PublicKeyEntryResolver;
import org.apache.sshd.common.util.GenericUtils;
import org.apache.sshd.common.util.Pair;
import org.apache.sshd.common.util.ValidateUtils;
import org.apache.sshd.server.auth.BaseAuthenticatorTest;
-import org.apache.sshd.server.config.keys.AuthorizedKeyEntry;
import org.apache.sshd.server.session.ServerSession;
import org.junit.AfterClass;
import org.junit.BeforeClass;
[3/3] mina-sshd git commit: [SSHD-266] Support for known hosts
Posted by lg...@apache.org.
[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);
+ }
+}
[2/3] mina-sshd git commit: [SSHD-266] Support for known hosts
Posted by lg...@apache.org.
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/509c871e/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
new file mode 100644
index 0000000..6961f901
--- /dev/null
+++ b/sshd-core/src/main/java/org/apache/sshd/client/keyverifier/KnownHostsServerKeyVerifier.java
@@ -0,0 +1,720 @@
+/*
+ * 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.BufferedReader;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.Writer;
+import java.net.SocketAddress;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.LinkOption;
+import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
+import java.security.GeneralSecurityException;
+import java.security.PublicKey;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.TreeSet;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.apache.sshd.client.config.hosts.KnownHostEntry;
+import org.apache.sshd.client.config.hosts.KnownHostHashValue;
+import org.apache.sshd.client.session.ClientSession;
+import org.apache.sshd.common.Factory;
+import org.apache.sshd.common.FactoryManager;
+import org.apache.sshd.common.NamedFactory;
+import org.apache.sshd.common.config.SshConfigFileReader;
+import org.apache.sshd.common.config.keys.AuthorizedKeyEntry;
+import org.apache.sshd.common.config.keys.KeyUtils;
+import org.apache.sshd.common.config.keys.PublicKeyEntry;
+import org.apache.sshd.common.config.keys.PublicKeyEntryResolver;
+import org.apache.sshd.common.mac.Mac;
+import org.apache.sshd.common.random.Random;
+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.ModifiableFileWatcher;
+import org.apache.sshd.common.util.net.SshdSocketAddress;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public class KnownHostsServerKeyVerifier extends ModifiableFileWatcher implements ServerKeyVerifier {
+ /**
+ * Represents an entry in the internal verifier's cach
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+ public static class HostEntryPair {
+ private KnownHostEntry hostEntry;
+ private PublicKey serverKey;
+
+ public HostEntryPair() {
+ super();
+ }
+
+ public HostEntryPair(KnownHostEntry entry, PublicKey key) {
+ this.hostEntry = ValidateUtils.checkNotNull(entry, "No entry");
+ this.serverKey = ValidateUtils.checkNotNull(key, "No key");
+ }
+
+ public KnownHostEntry getHostEntry() {
+ return hostEntry;
+ }
+
+ public void setHostEntry(KnownHostEntry hostEntry) {
+ this.hostEntry = hostEntry;
+ }
+
+ public PublicKey getServerKey() {
+ return serverKey;
+ }
+
+ public void setServerKey(PublicKey serverKey) {
+ this.serverKey = serverKey;
+ }
+
+ @Override
+ public String toString() {
+ return String.valueOf(getHostEntry());
+ }
+ }
+
+ protected final Object updateLock = new Object();
+ private final ServerKeyVerifier delegate;
+ private final AtomicReference<Collection<HostEntryPair>> keysHolder =
+ new AtomicReference<Collection<HostEntryPair>>(Collections.<HostEntryPair>emptyList());
+
+ public KnownHostsServerKeyVerifier(ServerKeyVerifier delegate, Path file) {
+ this(delegate, file, IoUtils.EMPTY_LINK_OPTIONS);
+ }
+
+ public KnownHostsServerKeyVerifier(ServerKeyVerifier delegate, Path file, LinkOption... options) {
+ super(file, options);
+ this.delegate = ValidateUtils.checkNotNull(delegate, "No delegate");
+ }
+
+ public ServerKeyVerifier getDelegateVerifier() {
+ return delegate;
+ }
+
+ @Override
+ public boolean verifyServerKey(ClientSession clientSession, SocketAddress remoteAddress, PublicKey serverKey) {
+ Collection<HostEntryPair> knownHosts = getLoadedHostsEntries();
+ try {
+ if (checkReloadRequired()) {
+ Path file = getPath();
+ if (exists()) {
+ knownHosts = reloadKnownHosts(file);
+ } else {
+ if (log.isDebugEnabled()) {
+ log.debug("verifyServerKey({})[{}] missing known hosts file {}",
+ clientSession, remoteAddress, file);
+ }
+ knownHosts = Collections.<HostEntryPair>emptyList();
+ }
+
+ setLoadedHostsEntries(knownHosts);
+ }
+ } catch (Throwable t) {
+ return acceptIncompleteHostKeys(clientSession, remoteAddress, serverKey, t);
+ }
+
+ return acceptKnownHostEntries(clientSession, remoteAddress, serverKey, knownHosts);
+ }
+
+ protected Collection<HostEntryPair> getLoadedHostsEntries() {
+ return keysHolder.get();
+ }
+
+ protected void setLoadedHostsEntries(Collection<HostEntryPair> keys) {
+ keysHolder.set(keys);
+ }
+
+ /**
+ * @param file The {@link Path} to reload from
+ * @return A {@link List} of the loaded {@link HostEntryPair}s - may be {@code null}/empty
+ * @throws IOException If failed to parse the file
+ * @throws GeneralSecurityException If failed to resolve the encoded public keys
+ */
+ protected List<HostEntryPair> reloadKnownHosts(Path file) throws IOException, GeneralSecurityException {
+ Collection<KnownHostEntry> entries = KnownHostEntry.readKnownHostEntries(file);
+ if (log.isDebugEnabled()) {
+ log.debug("reloadKnownHosts({}) loaded {} entries", file, entries.size());
+ }
+ updateReloadAttributes();
+
+ if (GenericUtils.isEmpty(entries)) {
+ return Collections.emptyList();
+ }
+
+ List<HostEntryPair> keys = new ArrayList<>(entries.size());
+ PublicKeyEntryResolver resolver = getFallbackPublicKeyEntryResolver();
+ for (KnownHostEntry entry : entries) {
+ try {
+ PublicKey key = resolveHostKey(entry, resolver);
+ if (key != null) {
+ keys.add(new HostEntryPair(entry, key));
+ }
+ } catch (Throwable t) {
+ log.warn("reloadKnownHosts({}) failed ({}) to load key of {}: {}",
+ file, t.getClass().getSimpleName(), entry, t.getMessage());
+ if (log.isDebugEnabled()) {
+ log.debug("reloadKnownHosts(" + file + ") key=" + entry + " load failure details", t);
+ }
+ }
+ }
+
+ return keys;
+ }
+
+ /**
+ * Recover the associated public key from a known host entry
+ *
+ * @param entry The {@link KnownHostEntry} - ignored if {@code null}
+ * @param resolver The {@link PublicKeyEntryResolver} to use if immediate
+ * - decoding does not work - ignored if {@code null}
+ * @return The extracted {@link PublicKey} - {@code null} if none
+ * @throws IOException If failed to decode the key
+ * @throws GeneralSecurityException If failed to generate the key
+ * @see #getFallbackPublicKeyEntryResolver()
+ * @see AuthorizedKeyEntry#resolvePublicKey(PublicKeyEntryResolver)
+ */
+ protected PublicKey resolveHostKey(KnownHostEntry entry, PublicKeyEntryResolver resolver)
+ throws IOException, GeneralSecurityException {
+ if (entry == null) {
+ return null;
+ }
+
+ AuthorizedKeyEntry authEntry = ValidateUtils.checkNotNull(entry.getKeyEntry(), "No key extracted from %s", entry);
+ PublicKey key = authEntry.resolvePublicKey(resolver);
+ if (log.isDebugEnabled()) {
+ log.debug("resolveHostKey({}) loaded {}-{}", entry, KeyUtils.getKeyType(key), KeyUtils.getFingerPrint(key));
+ }
+
+ return key;
+ }
+
+ protected PublicKeyEntryResolver getFallbackPublicKeyEntryResolver() {
+ return PublicKeyEntryResolver.IGNORING;
+ }
+
+ protected boolean acceptKnownHostEntries(
+ ClientSession clientSession, SocketAddress remoteAddress, PublicKey serverKey, Collection<HostEntryPair> knownHosts) {
+ // TODO allow for several candidates and check if ANY of them matches the key and has 'revoked' marker
+ HostEntryPair match = findKnownHostEntry(clientSession, remoteAddress, knownHosts);
+ if (match == null) {
+ return acceptUnknownHostKey(clientSession, remoteAddress, serverKey);
+ }
+
+ KnownHostEntry entry = match.getHostEntry();
+ PublicKey expected = match.getServerKey();
+ if (KeyUtils.compareKeys(expected, serverKey)) {
+ return acceptKnownHostEntry(clientSession, remoteAddress, serverKey, entry);
+ }
+
+ if (!acceptModifiedServerKey(clientSession, remoteAddress, match, serverKey)) {
+ return false;
+ }
+
+ Path file = getPath();
+ try {
+ updateModifiedServerKey(clientSession, remoteAddress, match, serverKey, file, knownHosts);
+ } catch (Throwable t) {
+ handleModifiedServerKeyUpdateFailure(clientSession, remoteAddress, match, serverKey, file, knownHosts, t);
+ }
+
+ return true;
+ }
+
+ /**
+ * Invoked if a matching host entry was found, but the key did not match and
+ * {@link #acceptModifiedServerKey(ClientSession, SocketAddress, HostEntryPair, PublicKey)}
+ * returned {@code true}. By default it locates the line to be updated and updates only
+ * its key data, marking the file for reload on next verification just to be
+ * on the safe side.
+ *
+ * @param clientSession The {@link ClientSession}
+ * @param remoteAddress The remote host address
+ * @param match The {@link HostEntryPair} whose key does not match
+ * @param actual The presented server {@link PublicKey} to be updated
+ * @param file The file {@link Path} to be updated
+ * @param knownHosts The currently loaded entries
+ * @throws Exception If failed to update the file - <B>Note:</B> this may mean the
+ * file is now corrupted
+ * @see #handleModifiedServerKeyUpdateFailure(ClientSession, SocketAddress, HostEntryPair, PublicKey, Path, Collection, Throwable)
+ * @see #prepareModifiedServerKeyLine(ClientSession, SocketAddress, KnownHostEntry, String, PublicKey, PublicKey)
+ */
+ protected void updateModifiedServerKey(
+ ClientSession clientSession, SocketAddress remoteAddress, HostEntryPair match, PublicKey actual,
+ Path file, Collection<HostEntryPair> knownHosts)
+ throws Exception {
+ KnownHostEntry entry = match.getHostEntry();
+ String matchLine = ValidateUtils.checkNotNullAndNotEmpty(entry.getConfigLine(), "No entry config line");
+ String newLine = prepareModifiedServerKeyLine(clientSession, remoteAddress, entry, matchLine, match.getServerKey(), actual);
+ if (GenericUtils.isEmpty(newLine)) {
+ if (log.isDebugEnabled()) {
+ log.debug("updateModifiedServerKey({})[{}] no replacement generated for {}",
+ clientSession, remoteAddress, matchLine);
+ }
+ return;
+ }
+
+ if (matchLine.equals(newLine)) {
+ if (log.isDebugEnabled()) {
+ log.debug("updateModifiedServerKey({})[{}] unmodified upodated lline for {}",
+ clientSession, remoteAddress, matchLine);
+ }
+ return;
+ }
+
+ List<String> lines = new ArrayList<>();
+ synchronized (updateLock) {
+ int matchingIndex = -1; // read all lines but replace the
+ try (BufferedReader rdr = Files.newBufferedReader(file, StandardCharsets.UTF_8)) {
+ for (String line = rdr.readLine(); line != null; line = rdr.readLine()) {
+ // skip if already replaced the original line
+ if (matchingIndex >= 0) {
+ lines.add(line);
+ continue;
+ }
+ line = GenericUtils.trimToEmpty(line);
+ if (GenericUtils.isEmpty(line)) {
+ lines.add(line);
+ continue;
+ }
+
+ int pos = line.indexOf(SshConfigFileReader.COMMENT_CHAR);
+ if (pos == 0) {
+ lines.add(line);
+ continue;
+ }
+
+ if (pos > 0) {
+ line = line.substring(0, pos);
+ line = line.trim();
+ }
+
+ if (!matchLine.equals(line)) {
+ lines.add(line);
+ continue;
+ }
+
+ lines.add(newLine);
+ matchingIndex = lines.size();
+ }
+ }
+
+ ValidateUtils.checkTrue(matchingIndex >= 0, "No match found for line=%s", matchLine);
+
+ try (Writer w = Files.newBufferedWriter(file, StandardCharsets.UTF_8)) {
+ for (String l : lines) {
+ w.append(l).append(IoUtils.EOL);
+ }
+ }
+
+ synchronized (match) {
+ match.setServerKey(actual);
+ entry.setConfigLine(newLine);
+ }
+ }
+
+ if (log.isDebugEnabled()) {
+ log.debug("updateModifiedServerKey({}) replaced '{}' with '{}'", file, matchLine, newLine);
+ }
+ resetReloadAttributes(); // force reload on next verification
+ }
+
+ /**
+ * Invoked by {@link #updateModifiedServerKey(ClientSession, SocketAddress, HostEntryPair, PublicKey, Path, Collection)}
+ * in order to prepare the replacement - by default it replaces the key part with the new one
+ *
+ * @param clientSession The {@link ClientSession}
+ * @param remoteAddress The remote host address
+ * @param entry The {@link KnownHostEntry}
+ * @param curLine The current entry line data
+ * @param expected The expected {@link PublicKey}
+ * @param actual The present key to be update
+ * @return The updated line - ignored if {@code null}/empty or same as original one
+ * @throws Exception if failed to prepare the line
+ */
+ protected String prepareModifiedServerKeyLine(
+ ClientSession clientSession, SocketAddress remoteAddress, KnownHostEntry entry,
+ String curLine, PublicKey expected, PublicKey actual)
+ throws Exception {
+ if ((entry == null) || GenericUtils.isEmpty(curLine)) {
+ return curLine; // just to be on the safe side
+ }
+
+ int pos = curLine.indexOf(' ');
+ if (curLine.charAt(0) == KnownHostEntry.MARKER_INDICATOR) {
+ // skip marker till next token
+ for (pos++; pos < curLine.length(); pos++) {
+ if (curLine.charAt(pos) != ' ') {
+ break;
+ }
+ }
+
+ pos = (pos < curLine.length()) ? curLine.indexOf(' ', pos) : -1;
+ }
+
+ ValidateUtils.checkTrue((pos > 0) && (pos < (curLine.length() - 1)), "Missing encoded key in line=%s", curLine);
+ StringBuilder sb = new StringBuilder(curLine.length());
+ sb.append(curLine.substring(0, pos)); // copy the marker/patterns as-is
+ PublicKeyEntry.appendPublicKeyEntry(sb.append(' '), actual);
+ return sb.toString();
+ }
+
+ /**
+ * Invoked if {@code #updateModifiedServerKey(ClientSession, SocketAddress, HostEntryPair, PublicKey, Path)}
+ * throws an exception. This may mean the file is corrupted, but it can be recovered from the known hosts
+ * that are being provided. By default, it only logs a warning and does not attempt to recover the file
+ *
+ * @param clientSession The {@link ClientSession}
+ * @param remoteAddress The remote host address
+ * @param match The {@link HostEntryPair} whose key does not match
+ * @param serverKey The presented server {@link PublicKey} to be updated
+ * @param file The file {@link Path} to be updated
+ * @param knownHosts The currently cached entries (may be {@code null}/empty)
+ * @param reason The failure reason
+ */
+ protected void handleModifiedServerKeyUpdateFailure(
+ ClientSession clientSession, SocketAddress remoteAddress, HostEntryPair match,
+ PublicKey serverKey, Path file, Collection<HostEntryPair> knownHosts, Throwable reason) {
+ // NOTE !!! this may mean the file is corrupted, but it can be recovered from the known hosts
+ log.warn("acceptKnownHostEntries({})[{}] failed ({}) to update modified server key of {}: {}",
+ clientSession, remoteAddress, reason.getClass().getSimpleName(), match, reason.getMessage());
+ if (log.isDebugEnabled()) {
+ log.debug("acceptKnownHostEntries(" + clientSession + ")[" + remoteAddress + "]"
+ + " modified key update failure details", reason);
+ }
+ }
+
+ /**
+ * Invoked <U>after</U> known host entry located and keys match - by default
+ * checks that entry has not been revoked
+ *
+ * @param clientSession The {@link ClientSession}
+ * @param remoteAddress The remote host address
+ * @param serverKey The presented server {@link PublicKey}
+ * @param entry The {@link KnownHostEntry} value - if {@code null} then no
+ * known matching host entry was found - default will call
+ * {@link #acceptUnknownHostKey(ClientSession, SocketAddress, PublicKey)}
+ * @return {@code true} if OK to accept the server
+ */
+ protected boolean acceptKnownHostEntry(ClientSession clientSession, SocketAddress remoteAddress, PublicKey serverKey, KnownHostEntry entry) {
+ if (entry == null) { // not really expected, but manage it
+ return acceptUnknownHostKey(clientSession, remoteAddress, serverKey);
+ }
+
+ if ("revoked".equals(entry.getMarker())) {
+ log.debug("acceptKnownHostEntry({})[{}] key={}-{} marked as {}",
+ clientSession, remoteAddress, KeyUtils.getKeyType(serverKey), KeyUtils.getFingerPrint(serverKey), entry.getMarker());
+ return false;
+ }
+
+ if (log.isDebugEnabled()) {
+ log.debug("acceptKnownHostEntry({})[{}] matched key={}-{}",
+ clientSession, remoteAddress, KeyUtils.getKeyType(serverKey), KeyUtils.getFingerPrint(serverKey));
+ }
+ return true;
+ }
+
+ protected HostEntryPair findKnownHostEntry(
+ ClientSession clientSession, SocketAddress remoteAddress, Collection<HostEntryPair> knownHosts) {
+ if (GenericUtils.isEmpty(knownHosts)) {
+ return null;
+ }
+
+ Collection<String> candidates = resolveHostNetworkIdentities(clientSession, remoteAddress);
+ if (log.isDebugEnabled()) {
+ log.debug("findKnownHostEntry({})[{}] host network identities: {}",
+ clientSession, remoteAddress, candidates);
+ }
+
+ if (GenericUtils.isEmpty(candidates)) {
+ return null;
+ }
+
+ for (HostEntryPair match : knownHosts) {
+ KnownHostEntry entry = match.getHostEntry();
+ for (String host : candidates) {
+ try {
+ if (entry.isHostMatch(host)) {
+ if (log.isDebugEnabled()) {
+ log.debug("findKnownHostEntry({})[{}] matched host={} for entry={}",
+ clientSession, remoteAddress, host, entry);
+ }
+ return match;
+ }
+ } catch (RuntimeException | Error e) {
+ log.warn("findKnownHostEntry({})[{}] failed ({}) to check host={} for entry={}: {}",
+ clientSession, remoteAddress, e.getClass().getSimpleName(),
+ host, entry.getConfigLine(), e.getMessage());
+ if (log.isDebugEnabled()) {
+ log.debug("findKnownHostEntry(" + clientSession + ") host=" + host + ", entry=" + entry + " match failure details", e);
+ }
+ }
+ }
+ }
+
+ return null; // no match found
+ }
+
+ /**
+ * Called if failed to reload known hosts - by default invokes
+ * {@link #acceptUnknownHostKey(ClientSession, SocketAddress, PublicKey)}
+ *
+ * @param clientSession The {@link ClientSession}
+ * @param remoteAddress The remote host address
+ * @param serverKey The presented server {@link PublicKey}
+ * @param reason The {@link Throwable} that indicates the reload failure
+ * @return {@code true} if accept the server key anyway
+ * @see #acceptUnknownHostKey(ClientSession, SocketAddress, PublicKey)
+ */
+ protected boolean acceptIncompleteHostKeys(ClientSession clientSession, SocketAddress remoteAddress, PublicKey serverKey, Throwable reason) {
+ log.warn("Failed ({}) to reload server keys from {}: {}",
+ reason.getClass().getSimpleName(), getPath(), reason.getMessage());
+ if (log.isDebugEnabled()) {
+ log.debug(getPath() + " reload failure details", reason);
+ }
+ return acceptUnknownHostKey(clientSession, remoteAddress, serverKey);
+ }
+
+ /**
+ * Invoked if none of the known hosts matches the current one - by default invokes the delegate.
+ * If the delegate accepts the key, then it is <U>appended</U> to the currently monitored entries
+ * and the file is updated
+ *
+ * @param clientSession The {@link ClientSession}
+ * @param remoteAddress The remote host address
+ * @param serverKey The presented server {@link PublicKey}
+ * @return {@code true} if accept the server key
+ * @see #updateKnownHostsFile(ClientSession, SocketAddress, PublicKey, Path, Collection)
+ * @see #handleKnownHostsFileUpdateFailure(ClientSession, SocketAddress, PublicKey, Path, Collection, Throwable)
+ */
+ protected boolean acceptUnknownHostKey(ClientSession clientSession, SocketAddress remoteAddress, PublicKey serverKey) {
+ if (log.isDebugEnabled()) {
+ log.debug("acceptUnknownHostKey({}) host={}, key={}",
+ clientSession, remoteAddress, KeyUtils.getFingerPrint(serverKey));
+ }
+
+ if (delegate.verifyServerKey(clientSession, remoteAddress, serverKey)) {
+ Path file = getPath();
+ Collection<HostEntryPair> keys = getLoadedHostsEntries();
+ try {
+ updateKnownHostsFile(clientSession, remoteAddress, serverKey, file, keys);
+ } catch (Throwable t) {
+ handleKnownHostsFileUpdateFailure(clientSession, remoteAddress, serverKey, file, keys, t);
+ }
+
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Invoked when {@link #updateKnownHostsFile(ClientSession, SocketAddress, PublicKey, Path, Collection)} fails - by
+ * default just issues a warning. <B>Note:</B> there is a chance that the file is now corrupted and
+ * cannot be re-used, so we provide a way to recover it via overriding this method and using the cached
+ * entries to re-created it.
+ *
+ * @param clientSession The {@link ClientSession}
+ * @param remoteAddress The remote host address
+ * @param serverKey The server {@link PublicKey} that was attempted to update
+ * @param file The file {@link Path} to be updated
+ * @param knownHosts The currently known entries (may be {@code null}/empty
+ * @param reason The failure reason
+ */
+ protected void handleKnownHostsFileUpdateFailure(ClientSession clientSession, SocketAddress remoteAddress, PublicKey serverKey,
+ Path file, Collection<HostEntryPair> knownHosts, Throwable reason) {
+ log.warn("handleKnownHostsFileUpdateFailure({})[{}] failed ({}) to update key={}-{} in {}: {}",
+ clientSession, remoteAddress, reason.getClass().getSimpleName(),
+ KeyUtils.getKeyType(serverKey), KeyUtils.getFingerPrint(serverKey),
+ file, reason.getMessage());
+ if (log.isDebugEnabled()) {
+ log.debug("handleKnownHostsFileUpdateFailure(" + clientSession + ")[" + remoteAddress + "]"
+ + " file update failure details", reason);
+ }
+ }
+
+ /**
+ * Invoked if a new previously unknown host key has been accepted - by default
+ * appends a new entry at the end of the currently monitored known hosts file
+ *
+ * @param clientSession The {@link ClientSession}
+ * @param remoteAddress The remote host address
+ * @param serverKey The server {@link PublicKey} that to update
+ * @param file The file {@link Path} to be updated
+ * @param knownHosts The currently cached entries (may be {@code null}/empty)
+ * @return The generated {@link KnownHostEntry} or {@code null} if nothing updated.
+ * If anything updated then the file will be re-loaded on next verification
+ * regardless of which server is verified
+ * @throws Exception If failed to update the file - <B>Note:</B> in this case
+ * the file may be corrupted so {@link #handleKnownHostsFileUpdateFailure(ClientSession, SocketAddress, PublicKey, Path, Collection, Throwable)}
+ * 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 {
+ KnownHostEntry entry = prepareKnownHostEntry(clientSession, remoteAddress, serverKey);
+ if (entry == null) {
+ if (log.isDebugEnabled()) {
+ log.debug("updateKnownHostsFile({})[{}] no entry generated for key={}",
+ clientSession, remoteAddress, KeyUtils.getFingerPrint(serverKey));
+ }
+
+ return null;
+ }
+
+ String line = entry.getConfigLine();
+ byte[] lineData = line.getBytes(StandardCharsets.UTF_8);
+ boolean reuseExisting = Files.exists(file) && (Files.size(file) > 0);
+ synchronized (updateLock) {
+ try (OutputStream output = reuseExisting ? Files.newOutputStream(file, StandardOpenOption.APPEND) : Files.newOutputStream(file)) {
+ if (reuseExisting) {
+ output.write(IoUtils.getEOLBytes()); // separate from previous lines
+ }
+ output.write(lineData);
+ }
+ }
+
+ if (log.isDebugEnabled()) {
+ log.debug("updateKnownHostsFile({}) updated: {}", file, entry);
+ }
+ resetReloadAttributes(); // force reload on next verification
+ return entry;
+ }
+
+ /**
+ * Invoked by {@link #updateKnownHostsFile(ClientSession, SocketAddress, PublicKey, Path, Collection)}
+ * in order to generate the host entry to be written
+ *
+ * @param clientSession The {@link ClientSession}
+ * @param remoteAddress The remote host address
+ * @param serverKey The server {@link PublicKey} that was attempted to update
+ * @return The {@link KnownHostEntry} to use - if {@code null} then entry is
+ * not updated in the file
+ * @throws Exception If failed to generate the entry - e.g. failed to hash
+ * @see #resolveHostNetworkIdentities(ClientSession, SocketAddress)
+ * @see KnownHostEntry#getConfigLine()
+ */
+ protected KnownHostEntry prepareKnownHostEntry(ClientSession clientSession, SocketAddress remoteAddress, PublicKey serverKey) throws Exception {
+ Collection<String> patterns = resolveHostNetworkIdentities(clientSession, remoteAddress);
+ if (GenericUtils.isEmpty(patterns)) {
+ return null;
+ }
+
+ StringBuilder sb = new StringBuilder(Byte.MAX_VALUE);
+ Random rnd = null;
+ for (String hostIdentity : patterns) {
+ if (sb.length() > 0) {
+ sb.append(',');
+ }
+
+ NamedFactory<Mac> digester = getHostValueDigester(clientSession, remoteAddress, hostIdentity);
+ if (digester != null) {
+ if (rnd == null) {
+ FactoryManager manager =
+ ValidateUtils.checkNotNull(clientSession.getFactoryManager(), "No factory manager");
+ Factory<? extends Random> factory =
+ ValidateUtils.checkNotNull(manager.getRandomFactory(), "No random factory");
+ rnd = ValidateUtils.checkNotNull(factory.create(), "No randomizer created");
+ }
+
+ Mac mac = digester.create();
+ int blockSize = mac.getDefaultBlockSize();
+ byte[] salt = new byte[blockSize];
+ rnd.fill(salt);
+
+ byte[] digestValue = KnownHostHashValue.calculateHashValue(hostIdentity, mac, salt);
+ KnownHostHashValue.append(sb, digester, salt, digestValue);
+ } else {
+ sb.append(hostIdentity);
+ }
+ }
+
+ PublicKeyEntry.appendPublicKeyEntry(sb.append(' '), serverKey);
+ return KnownHostEntry.parseKnownHostEntry(sb.toString());
+ }
+
+ /**
+ * Invoked by {@link #prepareKnownHostEntry(ClientSession, SocketAddress, PublicKey)}
+ * in order to query whether to use a hashed value instead of a plain one for the
+ * written host name/address - default returns {@code null} - i.e., no hashing
+ *
+ * @param clientSession The {@link ClientSession}
+ * @param remoteAddress The remote host address
+ * @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) {
+ return null;
+ }
+
+ /**
+ * Retrieves the host identities to be used when matching or updating an entry
+ * for it - by default returns the reported remote address and the original
+ * connection target host name/address (if same, then only one value is returned)
+ *
+ * @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)
+ * @see ClientSession#getConnectAddress()
+ * @see SshdSocketAddress#toAddressString(SocketAddress)
+ */
+ protected Collection<String> 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>(String.CASE_INSENSITIVE_ORDER);
+ candidates.add(SshdSocketAddress.toAddressString(remoteAddress));
+ SocketAddress connectAddress = clientSession.getConnectAddress();
+ candidates.add(SshdSocketAddress.toAddressString(connectAddress));
+ return candidates;
+ }
+
+ /**
+ * Invoked when a matching known host key was found but it does not match
+ * the presented one.
+ *
+ * @param clientSession The {@link ClientSession}
+ * @param remoteAddress The remote host address
+ * @param match The original {@link HostEntryPair} whose key did not match
+ * @param actual The presented server {@link PublicKey}
+ * @return {@code true} if accept the server key anyway - default={@code false}
+ */
+ protected boolean acceptModifiedServerKey(
+ ClientSession clientSession, SocketAddress remoteAddress, HostEntryPair match, PublicKey actual) {
+ KnownHostEntry entry = match.getHostEntry();
+ PublicKey expected = match.getServerKey();
+ log.warn("acceptModifiedServerKey({}) mismatched keys presented by {} for entry={}: expected={}-{}, actual={}-{}",
+ clientSession, remoteAddress, entry,
+ KeyUtils.getKeyType(expected), KeyUtils.getFingerPrint(expected),
+ KeyUtils.getKeyType(actual), KeyUtils.getFingerPrint(actual));
+ return false;
+ }
+}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/509c871e/sshd-core/src/main/java/org/apache/sshd/client/keyverifier/ServerKeyVerifier.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/keyverifier/ServerKeyVerifier.java b/sshd-core/src/main/java/org/apache/sshd/client/keyverifier/ServerKeyVerifier.java
index d0eb3da..5305dab 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/keyverifier/ServerKeyVerifier.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/keyverifier/ServerKeyVerifier.java
@@ -30,15 +30,13 @@ import org.apache.sshd.client.session.ClientSession;
* @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
*/
public interface ServerKeyVerifier {
-
/**
* Verify that the server key provided is really the one of the host.
*
- * @param sshClientSession the current session
- * @param remoteAddress the host
- * @param serverKey the presented key
+ * @param clientSession the current {@link ClientSession}
+ * @param remoteAddress the host's {@link SocketAddress}
+ * @param serverKey the presented server {@link PublicKey}
* @return <code>true</code> if the key is accepted for the host
*/
- boolean verifyServerKey(ClientSession sshClientSession, SocketAddress remoteAddress, PublicKey serverKey);
-
+ boolean verifyServerKey(ClientSession clientSession, SocketAddress remoteAddress, PublicKey serverKey);
}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/509c871e/sshd-core/src/main/java/org/apache/sshd/client/session/AbstractClientSession.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/session/AbstractClientSession.java b/sshd-core/src/main/java/org/apache/sshd/client/session/AbstractClientSession.java
index 936866f..195d5ef 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/session/AbstractClientSession.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/session/AbstractClientSession.java
@@ -20,6 +20,7 @@
package org.apache.sshd.client.session;
import java.io.IOException;
+import java.net.SocketAddress;
import java.nio.file.FileSystem;
import java.security.KeyPair;
import java.util.List;
@@ -67,6 +68,7 @@ public abstract class AbstractClientSession extends AbstractSession implements C
private PasswordIdentityProvider passwordIdentityProvider;
private List<NamedFactory<UserAuth>> userAuthFactories;
private ScpTransferEventListener scpListener;
+ private SocketAddress connectAddress;
protected AbstractClientSession(ClientFactoryManager factoryManager, IoSession ioSession) {
super(false, factoryManager, ioSession);
@@ -79,6 +81,15 @@ public abstract class AbstractClientSession extends AbstractSession implements C
}
@Override
+ public SocketAddress getConnectAddress() {
+ return connectAddress;
+ }
+
+ public void setConnectAddress(SocketAddress connectAddress) {
+ this.connectAddress = connectAddress;
+ }
+
+ @Override
public ServerKeyVerifier getServerKeyVerifier() {
return resolveEffectiveProvider(ServerKeyVerifier.class, serverKeyVerifier, getFactoryManager().getServerKeyVerifier());
}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/509c871e/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSession.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSession.java b/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSession.java
index 9d7acbd..db3f10f 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSession.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSession.java
@@ -19,6 +19,7 @@
package org.apache.sshd.client.session;
import java.io.IOException;
+import java.net.SocketAddress;
import java.nio.file.FileSystem;
import java.util.Collection;
import java.util.Map;
@@ -77,6 +78,17 @@ public interface ClientSession extends Session, ClientAuthenticationManager {
}
/**
+ * Returns the original address (after having been translated through host
+ * configuration entries if any) that was request to connect. It contains the
+ * original host or address string that was used. <B>Note:</B> this may be
+ * different than the result of the {@link #getIoSession()} report of the
+ * remote peer
+ *
+ * @return The original requested address
+ */
+ SocketAddress getConnectAddress();
+
+ /**
* Starts the authentication process.
* User identities will be tried until the server successfully authenticate the user.
* User identities must be provided before calling this method using
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/509c871e/sshd-core/src/main/java/org/apache/sshd/common/config/keys/AuthorizedKeyEntry.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/config/keys/AuthorizedKeyEntry.java b/sshd-core/src/main/java/org/apache/sshd/common/config/keys/AuthorizedKeyEntry.java
new file mode 100644
index 0000000..acf7e80
--- /dev/null
+++ b/sshd-core/src/main/java/org/apache/sshd/common/config/keys/AuthorizedKeyEntry.java
@@ -0,0 +1,361 @@
+/*
+ * 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.common.config.keys;
+
+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.security.GeneralSecurityException;
+import java.security.PublicKey;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.io.NoCloseInputStream;
+import org.apache.sshd.common.util.io.NoCloseReader;
+import org.apache.sshd.server.auth.pubkey.KeySetPublickeyAuthenticator;
+import org.apache.sshd.server.auth.pubkey.PublickeyAuthenticator;
+import org.apache.sshd.server.auth.pubkey.RejectAllPublickeyAuthenticator;
+
+/**
+ * Represents an entry in the user's {@code authorized_keys} file according
+ * to the <A HREF="http://en.wikibooks.org/wiki/OpenSSH/Client_Configuration_Files#.7E.2F.ssh.2Fauthorized_keys">OpenSSH format</A>.
+ * <B>Note:</B> {@code equals/hashCode} check only the key type and data - the
+ * comment and/or login options are not considered part of equality
+ *
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public class AuthorizedKeyEntry extends PublicKeyEntry {
+
+ private static final long serialVersionUID = -9007505285002809156L;
+
+ private String comment;
+ // for options that have no value, "true" is used
+ private Map<String, String> loginOptions = Collections.emptyMap();
+
+ public AuthorizedKeyEntry() {
+ super();
+ }
+
+ public String getComment() {
+ return comment;
+ }
+
+ public void setComment(String value) {
+ this.comment = value;
+ }
+
+ public Map<String, String> getLoginOptions() {
+ return loginOptions;
+ }
+
+ public void setLoginOptions(Map<String, String> value) {
+ if (value == null) {
+ this.loginOptions = Collections.emptyMap();
+ } else {
+ this.loginOptions = value;
+ }
+ }
+
+ @Override
+ public PublicKey appendPublicKey(Appendable sb, PublicKeyEntryResolver fallbackResolver) throws IOException, GeneralSecurityException {
+ Map<String, String> options = getLoginOptions();
+ if (!GenericUtils.isEmpty(options)) {
+ int index = 0;
+ for (Map.Entry<String, String> oe : options.entrySet()) {
+ String key = oe.getKey();
+ String value = oe.getValue();
+ if (index > 0) {
+ sb.append(',');
+ }
+ sb.append(key);
+ // TODO figure out a way to remember which options where quoted
+ // TODO figure out a way to remember which options had no value
+ if (!Boolean.TRUE.toString().equals(value)) {
+ sb.append('=').append(value);
+ }
+ index++;
+ }
+
+ if (index > 0) {
+ sb.append(' ');
+ }
+ }
+
+ PublicKey key = super.appendPublicKey(sb, fallbackResolver);
+ String kc = getComment();
+ if (!GenericUtils.isEmpty(kc)) {
+ sb.append(' ').append(kc);
+ }
+
+ return key;
+ }
+
+ @Override
+ public String toString() {
+ String entry = super.toString();
+ String kc = getComment();
+ Map<?, ?> ko = getLoginOptions();
+ return (GenericUtils.isEmpty(ko) ? "" : ko.toString() + " ")
+ + entry
+ + (GenericUtils.isEmpty(kc) ? "" : " " + kc);
+ }
+
+ public static PublickeyAuthenticator fromAuthorizedEntries(PublicKeyEntryResolver fallbackResolver, Collection<? extends AuthorizedKeyEntry> entries)
+ throws IOException, GeneralSecurityException {
+ Collection<PublicKey> keys = resolveAuthorizedKeys(fallbackResolver, entries);
+ if (GenericUtils.isEmpty(keys)) {
+ return RejectAllPublickeyAuthenticator.INSTANCE;
+ } else {
+ return new KeySetPublickeyAuthenticator(keys);
+ }
+ }
+
+ public static List<PublicKey> resolveAuthorizedKeys(PublicKeyEntryResolver fallbackResolver, Collection<? extends AuthorizedKeyEntry> entries)
+ throws IOException, GeneralSecurityException {
+ if (GenericUtils.isEmpty(entries)) {
+ return Collections.emptyList();
+ }
+
+ List<PublicKey> keys = new ArrayList<PublicKey>(entries.size());
+ for (AuthorizedKeyEntry e : entries) {
+ PublicKey k = e.resolvePublicKey(fallbackResolver);
+ if (k != null) {
+ keys.add(k);
+ }
+ }
+
+ return keys;
+ }
+
+ /**
+ * Reads read the contents of an <code>authorized_keys</code> file
+ *
+ * @param url The {@link URL} to read from
+ * @return A {@link List} of all the {@link AuthorizedKeyEntry}-ies found there
+ * @throws IOException If failed to read or parse the entries
+ * @see #readAuthorizedKeys(InputStream, boolean)
+ */
+ public static List<AuthorizedKeyEntry> readAuthorizedKeys(URL url) throws IOException {
+ try (InputStream in = url.openStream()) {
+ return readAuthorizedKeys(in, true);
+ }
+ }
+
+ /**
+ * Reads read the contents of an <code>authorized_keys</code> file
+ *
+ * @param file The {@link File} to read from
+ * @return A {@link List} of all the {@link AuthorizedKeyEntry}-ies found there
+ * @throws IOException If failed to read or parse the entries
+ * @see #readAuthorizedKeys(InputStream, boolean)
+ */
+ public static List<AuthorizedKeyEntry> readAuthorizedKeys(File file) throws IOException {
+ try (InputStream in = new FileInputStream(file)) {
+ return readAuthorizedKeys(in, true);
+ }
+ }
+
+ /**
+ * Reads read the contents of an <code>authorized_keys</code> file
+ *
+ * @param path {@link Path} to read from
+ * @param options The {@link OpenOption}s to use - if unspecified then appropriate
+ * defaults assumed
+ * @return A {@link List} of all the {@link AuthorizedKeyEntry}-ies found there
+ * @throws IOException If failed to read or parse the entries
+ * @see #readAuthorizedKeys(InputStream, boolean)
+ * @see Files#newInputStream(Path, OpenOption...)
+ */
+ public static List<AuthorizedKeyEntry> readAuthorizedKeys(Path path, OpenOption... options) throws IOException {
+ try (InputStream in = Files.newInputStream(path, options)) {
+ return readAuthorizedKeys(in, true);
+ }
+ }
+
+ /**
+ * Reads read the contents of an <code>authorized_keys</code> file
+ *
+ * @param filePath The file path to read from
+ * @return A {@link List} of all the {@link AuthorizedKeyEntry}-ies found there
+ * @throws IOException If failed to read or parse the entries
+ * @see #readAuthorizedKeys(InputStream, boolean)
+ */
+ public static List<AuthorizedKeyEntry> readAuthorizedKeys(String filePath) throws IOException {
+ try (InputStream in = new FileInputStream(filePath)) {
+ return readAuthorizedKeys(in, true);
+ }
+ }
+
+ /**
+ * Reads read the contents of an <code>authorized_keys</code> file
+ *
+ * @param in The {@link InputStream}
+ * @param okToClose <code>true</code> if method may close the input stream
+ * regardless of whether successful or failed
+ * @return A {@link List} of all the {@link AuthorizedKeyEntry}-ies found there
+ * @throws IOException If failed to read or parse the entries
+ * @see #readAuthorizedKeys(Reader, boolean)
+ */
+ public static List<AuthorizedKeyEntry> readAuthorizedKeys(InputStream in, boolean okToClose) throws IOException {
+ try (Reader rdr = new InputStreamReader(NoCloseInputStream.resolveInputStream(in, okToClose), StandardCharsets.UTF_8)) {
+ return readAuthorizedKeys(rdr, true);
+ }
+ }
+
+ /**
+ * Reads read the contents of an <code>authorized_keys</code> file
+ *
+ * @param rdr The {@link Reader}
+ * @param okToClose <code>true</code> if method may close the input stream
+ * regardless of whether successful or failed
+ * @return A {@link List} of all the {@link AuthorizedKeyEntry}-ies found there
+ * @throws IOException If failed to read or parse the entries
+ * @see #readAuthorizedKeys(BufferedReader)
+ */
+ public static List<AuthorizedKeyEntry> readAuthorizedKeys(Reader rdr, boolean okToClose) throws IOException {
+ try (BufferedReader buf = new BufferedReader(NoCloseReader.resolveReader(rdr, okToClose))) {
+ return readAuthorizedKeys(buf);
+ }
+ }
+
+ /**
+ * @param rdr The {@link BufferedReader} to use to read the contents of
+ * an <code>authorized_keys</code> file
+ * @return A {@link List} of all the {@link AuthorizedKeyEntry}-ies found there
+ * @throws IOException If failed to read or parse the entries
+ * @see #parseAuthorizedKeyEntry(String)
+ */
+ public static List<AuthorizedKeyEntry> readAuthorizedKeys(BufferedReader rdr) throws IOException {
+ List<AuthorizedKeyEntry> entries = null;
+
+ for (String line = rdr.readLine(); line != null; line = rdr.readLine()) {
+ AuthorizedKeyEntry entry;
+ try {
+ entry = parseAuthorizedKeyEntry(line.trim());
+ if (entry == null) { // null, empty or comment line
+ continue;
+ }
+ } catch (RuntimeException | Error e) {
+ throw new StreamCorruptedException("Failed (" + e.getClass().getSimpleName() + ")"
+ + " to parse key entry=" + line + ": " + e.getMessage());
+ }
+
+ if (entries == null) {
+ entries = new ArrayList<>();
+ }
+
+ entries.add(entry);
+ }
+
+ if (entries == null) {
+ return Collections.emptyList();
+ } else {
+ return entries;
+ }
+ }
+
+ /**
+ * @param line Original line from an <code>authorized_keys</code> file
+ * @return {@link AuthorizedKeyEntry} or {@code null} if the line is
+ * {@code null}/empty or a comment line
+ * @throws IllegalArgumentException If failed to parse/decode the line
+ * @see #COMMENT_CHAR
+ */
+ public static AuthorizedKeyEntry parseAuthorizedKeyEntry(String line) throws IllegalArgumentException {
+ if (GenericUtils.isEmpty(line) || (line.charAt(0) == COMMENT_CHAR) /* comment ? */) {
+ return null;
+ }
+
+ int startPos = line.indexOf(' ');
+ if (startPos <= 0) {
+ throw new IllegalArgumentException("Bad format (no key data delimiter): " + line);
+ }
+
+ int endPos = line.indexOf(' ', startPos + 1);
+ if (endPos <= startPos) {
+ endPos = line.length();
+ }
+
+ String keyType = line.substring(0, startPos);
+ PublicKeyEntryDecoder<?, ?> decoder = KeyUtils.getPublicKeyEntryDecoder(keyType);
+ final AuthorizedKeyEntry entry;
+ if (decoder == null) { // assume this is due to the fact that it starts with login options
+ entry = parseAuthorizedKeyEntry(line.substring(startPos + 1).trim());
+ if (entry == null) {
+ throw new IllegalArgumentException("Bad format (no key data after login options): " + line);
+ }
+
+ entry.setLoginOptions(parseLoginOptions(keyType));
+ } else {
+ String encData = (endPos < (line.length() - 1)) ? line.substring(0, endPos).trim() : line;
+ String comment = (endPos < (line.length() - 1)) ? line.substring(endPos + 1).trim() : null;
+ entry = parsePublicKeyEntry(new AuthorizedKeyEntry(), encData);
+ entry.setComment(comment);
+ }
+
+ return entry;
+ }
+
+ public static Map<String, String> parseLoginOptions(String options) {
+ // TODO add support if quoted values contain ','
+ String[] pairs = GenericUtils.split(options, ',');
+ if (GenericUtils.isEmpty(pairs)) {
+ return Collections.emptyMap();
+ }
+
+ Map<String, String> optsMap = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
+ for (String p : pairs) {
+ p = GenericUtils.trimToEmpty(p);
+ if (GenericUtils.isEmpty(p)) {
+ continue;
+ }
+
+ int pos = p.indexOf('=');
+ String name = (pos < 0) ? p : GenericUtils.trimToEmpty(p.substring(0, pos));
+ CharSequence value = (pos < 0) ? null : GenericUtils.trimToEmpty(p.substring(pos + 1));
+ value = GenericUtils.stripQuotes(value);
+ if (value == null) {
+ value = Boolean.TRUE.toString();
+ }
+
+ String prev = optsMap.put(name, value.toString());
+ if (prev != null) {
+ throw new IllegalArgumentException("Multiple values for key=" + name + ": old=" + prev + ", new=" + value);
+ }
+ }
+
+ return optsMap;
+ }
+}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/509c871e/sshd-core/src/main/java/org/apache/sshd/common/mac/BaseMac.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/mac/BaseMac.java b/sshd-core/src/main/java/org/apache/sshd/common/mac/BaseMac.java
index f5e89c7..72adddd 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/mac/BaseMac.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/mac/BaseMac.java
@@ -92,15 +92,25 @@ public class BaseMac implements Mac {
}
@Override // TODO make this a default method in Java 8
+ public byte[] doFinal() throws Exception {
+ int blockSize = getBlockSize();
+ byte[] buf = new byte[blockSize];
+ doFinal(buf);
+ return buf;
+ }
+
+ @Override // TODO make this a default method in Java 8
public void doFinal(byte[] buf) throws Exception {
doFinal(buf, 0);
}
@Override
public void doFinal(byte[] buf, int offset) throws Exception {
- if (bsize != defbsize) {
+ int blockSize = getBlockSize();
+ int defaultSize = getDefaultBlockSize();
+ if (blockSize != defaultSize) {
mac.doFinal(tmp, 0);
- System.arraycopy(tmp, 0, buf, offset, bsize);
+ System.arraycopy(tmp, 0, buf, offset, blockSize);
} else {
mac.doFinal(buf, offset);
}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/509c871e/sshd-core/src/main/java/org/apache/sshd/common/mac/Mac.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/mac/Mac.java b/sshd-core/src/main/java/org/apache/sshd/common/mac/Mac.java
index 0da9472..b69ad34 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/mac/Mac.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/mac/Mac.java
@@ -33,6 +33,8 @@ public interface Mac extends MacInformation {
void updateUInt(long foo);
+ byte[] doFinal() throws Exception;
+
void doFinal(byte[] buf) throws Exception;
void doFinal(byte[] buf, int offset) throws Exception;
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/509c871e/sshd-core/src/main/java/org/apache/sshd/common/util/Base64.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/util/Base64.java b/sshd-core/src/main/java/org/apache/sshd/common/util/Base64.java
index c392420..fe7f04d 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/util/Base64.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/util/Base64.java
@@ -33,7 +33,7 @@ import java.security.InvalidParameterException;
* TODO replace this class with {@code java.util.Base64} when upgrading to JDK 1.8
* @see <a href="http://www.ietf.org/rfc/rfc2045.txt">RFC 2045</a>
*/
-public class Base64 {
+public final class Base64 {
/**
* <P>Chunk size per RFC 2045 section 6.8.</P>
@@ -43,54 +43,54 @@ public class Base64 {
*
* @see <a href="http://www.ietf.org/rfc/rfc2045.txt">RFC 2045 section 6.8</a>
*/
- static final int CHUNK_SIZE = 76;
-
- /**
- * Chunk separator per RFC 2045 section 2.1.
- *
- * @see <a href="http://www.ietf.org/rfc/rfc2045.txt">RFC 2045 section 2.1</a>
- */
- static final byte[] CHUNK_SEPARATOR = "\r\n".getBytes(StandardCharsets.UTF_8);
+ public static final int CHUNK_SIZE = 76;
/**
* The base length.
*/
- static final int BASELENGTH = 255;
+ public static final int BASELENGTH = 255;
/**
* Lookup length.
*/
- static final int LOOKUPLENGTH = 64;
+ public static final int LOOKUPLENGTH = 64;
/**
* Used to calculate the number of bits in a byte.
*/
- static final int EIGHTBIT = 8;
+ public static final int EIGHTBIT = Byte.SIZE;
/**
* Used when encoding something which has fewer than 24 bits.
*/
- static final int SIXTEENBIT = 16;
+ public static final int SIXTEENBIT = 2 * EIGHTBIT;
/**
* Used to determine how many bits data contains.
*/
- static final int TWENTYFOURBITGROUP = 24;
+ public static final int TWENTYFOURBITGROUP = 3 * EIGHTBIT;
/**
* Used to get the number of Quadruples.
*/
- static final int FOURBYTE = 4;
+ public static final int FOURBYTE = 4;
/**
* Used to test the sign of a byte.
*/
- static final int SIGN = -128;
+ public static final int SIGN = -128;
/**
* Byte used to pad output.
*/
- static final byte PAD = (byte) '=';
+ public static final byte PAD = (byte) '=';
+
+ /**
+ * Chunk separator per RFC 2045 section 2.1.
+ *
+ * @see <a href="http://www.ietf.org/rfc/rfc2045.txt">RFC 2045 section 2.1</a>
+ */
+ static final byte[] CHUNK_SEPARATOR = "\r\n".getBytes(StandardCharsets.UTF_8);
// Create arrays to hold the base64 characters and a
// lookup for base64 chars
@@ -132,7 +132,11 @@ public class Base64 {
lookUpBase64Alphabet[63] = (byte) '/';
}
- private static boolean isBase64(byte octect) {
+ private Base64() {
+ throw new UnsupportedOperationException("No instance");
+ }
+
+ public static boolean isBase64(byte octect) {
return octect == PAD || base64Alphabet[octect] != -1;
}
@@ -145,10 +149,9 @@ public class Base64 {
* alphabet or if the byte array is empty; false, otherwise
*/
public static boolean isArrayByteBase64(byte[] arrayOctect) {
-
arrayOctect = discardWhitespace(arrayOctect);
- int length = arrayOctect.length;
+ int length = NumberUtils.length(arrayOctect);
if (length == 0) {
// shouldn't a 0 length array be valid base64 data?
return true;
@@ -227,7 +230,8 @@ public class Base64 {
* @return Base64-encoded data.
*/
public static byte[] encodeBase64(byte[] binaryData, boolean isChunked) {
- int lengthDataBits = binaryData.length * EIGHTBIT;
+ int lengthDataBytes = NumberUtils.length(binaryData);
+ int lengthDataBits = lengthDataBytes * EIGHTBIT;
int fewerThan24bits = lengthDataBits % TWENTYFOURBITGROUP;
int numberTriplets = lengthDataBits / TWENTYFOURBITGROUP;
byte encodedData[];
@@ -246,7 +250,6 @@ public class Base64 {
// for compliance with RFC 2045 MIME, then it is important to
// allow for extra length to account for the separator(s)
if (isChunked) {
-
nbrChunks = CHUNK_SEPARATOR.length == 0 ? 0 : (int) Math.ceil((float) encodedDataLength / CHUNK_SIZE);
encodedDataLength += nbrChunks * CHUNK_SEPARATOR.length;
}
@@ -309,7 +312,6 @@ public class Base64 {
encodedData[encodedIndex + 2] = PAD;
encodedData[encodedIndex + 3] = PAD;
} else if (fewerThan24bits == SIXTEENBIT) {
-
b1 = binaryData[dataIndex];
b2 = binaryData[dataIndex + 1];
l = (byte) (b2 & 0x0f);
@@ -418,28 +420,50 @@ public class Base64 {
*
* @param data The base-64 encoded data to discard the whitespace
* from.
- * @return The data, less whitespace (see RFC 2045).
+ * @return The data, less whitespace (see RFC 2045) - may be same
+ * as input if no whitespace found
*/
- static byte[] discardWhitespace(byte[] data) {
- byte groomedData[] = new byte[data.length];
+ public static byte[] discardWhitespace(byte[] data) {
+ if (NumberUtils.isEmpty(data)) {
+ return GenericUtils.EMPTY_BYTE_ARRAY;
+ }
+
+ byte groomedData[] = null;
int bytesCopied = 0;
- for (int i = 0; i < data.length; i++) {
- switch (data[i]) {
- case (byte) ' ':
- case (byte) '\n':
- case (byte) '\r':
- case (byte) '\t':
- break;
- default:
- groomedData[bytesCopied++] = data[i];
+ for (int index = 0; index < data.length; index++) {
+ byte v = data[index];
+ boolean isWhiteSpace = (v == (byte) ' ') || (v == (byte) '\t') || (v == (byte) '\r') || (v == (byte) '\n');
+ if (groomedData == null) {
+ if (isWhiteSpace) { // all values up to this index were NOT white space
+ groomedData = new byte[data.length - 1];
+ if (index > 0) {
+ System.arraycopy(data, 0, groomedData, 0, index);
+ }
+ bytesCopied = index;
+ }
+ } else {
+ if (isWhiteSpace) {
+ continue;
+ }
+ groomedData[bytesCopied++] = v;
}
}
- byte packedData[] = new byte[bytesCopied];
+ if (groomedData == null) {
+ return data; // all characters where non-whitespace
+ }
- System.arraycopy(groomedData, 0, packedData, 0, bytesCopied);
+ if (bytesCopied <= 0) {
+ return GenericUtils.EMPTY_BYTE_ARRAY;
+ }
+ if (bytesCopied == groomedData.length) {
+ return groomedData;
+ }
+
+ byte[] packedData = new byte[bytesCopied];
+ System.arraycopy(groomedData, 0, packedData, 0, bytesCopied);
return packedData;
}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/509c871e/sshd-core/src/main/java/org/apache/sshd/common/util/Pair.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/util/Pair.java b/sshd-core/src/main/java/org/apache/sshd/common/util/Pair.java
index 4621862..0d34fd2 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/util/Pair.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/util/Pair.java
@@ -21,7 +21,7 @@ package org.apache.sshd.common.util;
import java.util.Objects;
/**
- * Represents a pair of values
+ * Represents an un-modifiable pair of values
*
* @param <F> First value type
* @param <S> Second value type
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/509c871e/sshd-core/src/main/java/org/apache/sshd/common/util/buffer/BufferUtils.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/util/buffer/BufferUtils.java b/sshd-core/src/main/java/org/apache/sshd/common/util/buffer/BufferUtils.java
index caa9a35..9181b90 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/util/buffer/BufferUtils.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/util/buffer/BufferUtils.java
@@ -314,8 +314,9 @@ public final class BufferUtils {
// TODO use Integer.BYTES for JDK-8
IoUtils.readFully(input, buf, offset, Integer.SIZE / Byte.SIZE);
return getUInt(buf, offset, len);
- } catch (IllegalArgumentException e) {
- throw new StreamCorruptedException(e.getMessage());
+ } catch (RuntimeException | Error e) {
+ throw new StreamCorruptedException("Failed (" + e.getClass().getSimpleName() + ")"
+ + " to read UINT value: " + e.getMessage());
}
}
@@ -408,8 +409,9 @@ public final class BufferUtils {
try {
int writeLen = putUInt(value, buf, off, len);
output.write(buf, off, writeLen);
- } catch (IllegalArgumentException e) {
- throw new StreamCorruptedException(e.getMessage());
+ } catch (RuntimeException | Error e) {
+ throw new StreamCorruptedException("Failed (" + e.getClass().getSimpleName() + ")"
+ + " to write UINT value=" + value + ": " + e.getMessage());
}
}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/509c871e/sshd-core/src/main/java/org/apache/sshd/common/util/io/ModifiableFileWatcher.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/util/io/ModifiableFileWatcher.java b/sshd-core/src/main/java/org/apache/sshd/common/util/io/ModifiableFileWatcher.java
index 94b8f28..d51b7f6 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/util/io/ModifiableFileWatcher.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/util/io/ModifiableFileWatcher.java
@@ -213,9 +213,10 @@ public class ModifiableFileWatcher extends AbstractLoggingBean {
* @param path The {@link Path} to be checked - ignored if {@code null}
* or does not exist
* @param options The {@link LinkOption}s to use to query the file's permissions
- * @return The violated permission as {@link Pair} first is a message second is
- * offending object {@link PosixFilePermission} or {@link String} for owner - {@code null} if
- * no violations detected
+ * @return The violated permission as {@link Pair} where {@link Pair#getClass()}
+ * is a loggable message and {@link Pair#getSecond()} is the offending object
+ * - e.g., {@link PosixFilePermission} or {@link String} for owner. Return
+ * value is {@code null} if no violations detected
* @throws IOException If failed to retrieve the permissions
* @see #STRICTLY_PROHIBITED_FILE_PERMISSION
*/
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/509c871e/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 7ea3806..9a2778d 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
@@ -289,6 +289,47 @@ public class SshdSocketAddress extends SocketAddress {
return true;
}
+ public static String toAddressString(SocketAddress addr) {
+ if (addr == null) {
+ return null;
+ }
+
+ if (addr instanceof InetSocketAddress) {
+ return ((InetSocketAddress) addr).getHostString();
+ }
+
+ if (addr instanceof SshdSocketAddress) {
+ return ((SshdSocketAddress) addr).getHostName();
+ }
+
+ return addr.toString();
+ }
+
+ /**
+ * <P>Converts a {@code SocketAddress} into an {@link InetSocketAddress} if possible:</P></BR>
+ * <UL>
+ * <LI>If already an {@link InetSocketAddress} then cast it as such</LI>
+ * <LI>If an {@code SshdSocketAddress} then invoke {@link #toInetSocketAddress()}</LI>
+ * <LI>Otherwise, throw an exception</LI>
+ * </UL>
+ *
+ * @param remoteAddress The {@link SocketAddress} - ignored if {@code null}
+ * @return The {@link InetSocketAddress} instance
+ * @throws ClassCastException if argument is not already an {@code InetSocketAddress}
+ * or a {@code SshdSocketAddress}
+ */
+ public static InetSocketAddress toInetSocketAddress(SocketAddress remoteAddress) {
+ if (remoteAddress == null) {
+ return null;
+ } else if (remoteAddress instanceof InetSocketAddress) {
+ return (InetSocketAddress) remoteAddress;
+ } else if (remoteAddress instanceof SshdSocketAddress) {
+ return ((SshdSocketAddress) remoteAddress).toInetSocketAddress();
+ } else {
+ throw new ClassCastException("Unknown remote address type: " + remoteAddress);
+ }
+ }
+
public static String toAddressString(InetAddress addr) {
String ip = (addr == null) ? null : addr.toString();
if (GenericUtils.isEmpty(ip)) {
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/509c871e/sshd-core/src/main/java/org/apache/sshd/server/config/keys/AuthorizedKeyEntry.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/server/config/keys/AuthorizedKeyEntry.java b/sshd-core/src/main/java/org/apache/sshd/server/config/keys/AuthorizedKeyEntry.java
deleted file mode 100644
index c57cde5..0000000
--- a/sshd-core/src/main/java/org/apache/sshd/server/config/keys/AuthorizedKeyEntry.java
+++ /dev/null
@@ -1,399 +0,0 @@
-/*
- * 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.server.config.keys;
-
-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.security.GeneralSecurityException;
-import java.security.PublicKey;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
-import java.util.TreeMap;
-
-import org.apache.sshd.common.config.keys.KeyUtils;
-import org.apache.sshd.common.config.keys.PublicKeyEntry;
-import org.apache.sshd.common.config.keys.PublicKeyEntryDecoder;
-import org.apache.sshd.common.config.keys.PublicKeyEntryResolver;
-import org.apache.sshd.common.util.GenericUtils;
-import org.apache.sshd.common.util.io.IoUtils;
-import org.apache.sshd.common.util.io.NoCloseInputStream;
-import org.apache.sshd.common.util.io.NoCloseReader;
-import org.apache.sshd.server.auth.pubkey.KeySetPublickeyAuthenticator;
-import org.apache.sshd.server.auth.pubkey.PublickeyAuthenticator;
-import org.apache.sshd.server.auth.pubkey.RejectAllPublickeyAuthenticator;
-
-/**
- * Represents an entry in the user's {@code authorized_keys} file according
- * to the <A HREF="http://en.wikibooks.org/wiki/OpenSSH/Client_Configuration_Files#.7E.2F.ssh.2Fauthorized_keys">OpenSSH format</A>.
- * <B>Note:</B> {@code equals/hashCode} check only the key type and data - the
- * comment and/or login options are not considered part of equality
- *
- * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
- */
-public class AuthorizedKeyEntry extends PublicKeyEntry {
-
- /**
- * Standard OpenSSH authorized keys file name
- */
- public static final String STD_AUTHORIZED_KEYS_FILENAME = "authorized_keys";
-
- private static final long serialVersionUID = -9007505285002809156L;
-
- private static final class LazyDefaultAuthorizedKeysFileHolder {
- private static final Path KEYS_FILE = PublicKeyEntry.getDefaultKeysFolderPath().resolve(STD_AUTHORIZED_KEYS_FILENAME);
- }
-
- private String comment;
- // for options that have no value, "true" is used
- private Map<String, String> loginOptions = Collections.emptyMap();
-
- public AuthorizedKeyEntry() {
- super();
- }
-
- public String getComment() {
- return comment;
- }
-
- public void setComment(String value) {
- this.comment = value;
- }
-
- public Map<String, String> getLoginOptions() {
- return loginOptions;
- }
-
- public void setLoginOptions(Map<String, String> value) {
- if (value == null) {
- this.loginOptions = Collections.emptyMap();
- } else {
- this.loginOptions = value;
- }
- }
-
- @Override
- public PublicKey appendPublicKey(Appendable sb, PublicKeyEntryResolver fallbackResolver) throws IOException, GeneralSecurityException {
- Map<String, String> options = getLoginOptions();
- if (!GenericUtils.isEmpty(options)) {
- int index = 0;
- for (Map.Entry<String, String> oe : options.entrySet()) {
- String key = oe.getKey();
- String value = oe.getValue();
- if (index > 0) {
- sb.append(',');
- }
- sb.append(key);
- // TODO figure out a way to remember which options where quoted
- // TODO figure out a way to remember which options had no value
- if (!Boolean.TRUE.toString().equals(value)) {
- sb.append('=').append(value);
- }
- index++;
- }
-
- if (index > 0) {
- sb.append(' ');
- }
- }
-
- PublicKey key = super.appendPublicKey(sb, fallbackResolver);
- String kc = getComment();
- if (!GenericUtils.isEmpty(kc)) {
- sb.append(' ').append(kc);
- }
-
- return key;
- }
-
- @Override
- public String toString() {
- String entry = super.toString();
- String kc = getComment();
- Map<?, ?> ko = getLoginOptions();
- return (GenericUtils.isEmpty(ko) ? "" : ko.toString() + " ")
- + entry
- + (GenericUtils.isEmpty(kc) ? "" : " " + kc);
- }
-
- public static PublickeyAuthenticator fromAuthorizedEntries(PublicKeyEntryResolver fallbackResolver, Collection<? extends AuthorizedKeyEntry> entries)
- throws IOException, GeneralSecurityException {
- Collection<PublicKey> keys = resolveAuthorizedKeys(fallbackResolver, entries);
- if (GenericUtils.isEmpty(keys)) {
- return RejectAllPublickeyAuthenticator.INSTANCE;
- } else {
- return new KeySetPublickeyAuthenticator(keys);
- }
- }
-
- public static List<PublicKey> resolveAuthorizedKeys(PublicKeyEntryResolver fallbackResolver, Collection<? extends AuthorizedKeyEntry> entries)
- throws IOException, GeneralSecurityException {
- if (GenericUtils.isEmpty(entries)) {
- return Collections.emptyList();
- }
-
- List<PublicKey> keys = new ArrayList<PublicKey>(entries.size());
- for (AuthorizedKeyEntry e : entries) {
- PublicKey k = e.resolvePublicKey(fallbackResolver);
- if (k != null) {
- keys.add(k);
- }
- }
-
- return keys;
- }
-
- /**
- * @return The default {@link Path} location of the OpenSSH authorized keys file
- */
- @SuppressWarnings("synthetic-access")
- public static Path getDefaultAuthorizedKeysFile() {
- return LazyDefaultAuthorizedKeysFileHolder.KEYS_FILE;
- }
-
- /**
- * Reads read the contents of the default OpenSSH <code>authorized_keys</code> file
- *
- * @param options The {@link OpenOption}s to use when reading the file
- * @return A {@link List} of all the {@link AuthorizedKeyEntry}-ies found there -
- * or empty if file does not exist
- * @throws IOException If failed to read keys from file
- */
- public static List<AuthorizedKeyEntry> readDefaultAuthorizedKeys(OpenOption ... options) throws IOException {
- Path keysFile = getDefaultAuthorizedKeysFile();
- if (Files.exists(keysFile, IoUtils.EMPTY_LINK_OPTIONS)) {
- return readAuthorizedKeys(keysFile);
- } else {
- return Collections.emptyList();
- }
- }
-
- /**
- * Reads read the contents of an <code>authorized_keys</code> file
- *
- * @param url The {@link URL} to read from
- * @return A {@link List} of all the {@link AuthorizedKeyEntry}-ies found there
- * @throws IOException If failed to read or parse the entries
- * @see #readAuthorizedKeys(InputStream, boolean)
- */
- public static List<AuthorizedKeyEntry> readAuthorizedKeys(URL url) throws IOException {
- try (InputStream in = url.openStream()) {
- return readAuthorizedKeys(in, true);
- }
- }
-
- /**
- * Reads read the contents of an <code>authorized_keys</code> file
- *
- * @param file The {@link File} to read from
- * @return A {@link List} of all the {@link AuthorizedKeyEntry}-ies found there
- * @throws IOException If failed to read or parse the entries
- * @see #readAuthorizedKeys(InputStream, boolean)
- */
- public static List<AuthorizedKeyEntry> readAuthorizedKeys(File file) throws IOException {
- try (InputStream in = new FileInputStream(file)) {
- return readAuthorizedKeys(in, true);
- }
- }
-
- /**
- * Reads read the contents of an <code>authorized_keys</code> file
- *
- * @param path {@link Path} to read from
- * @param options The {@link OpenOption}s to use - if unspecified then appropriate
- * defaults assumed
- * @return A {@link List} of all the {@link AuthorizedKeyEntry}-ies found there
- * @throws IOException If failed to read or parse the entries
- * @see #readAuthorizedKeys(InputStream, boolean)
- * @see Files#newInputStream(Path, OpenOption...)
- */
- public static List<AuthorizedKeyEntry> readAuthorizedKeys(Path path, OpenOption... options) throws IOException {
- try (InputStream in = Files.newInputStream(path, options)) {
- return readAuthorizedKeys(in, true);
- }
- }
-
- /**
- * Reads read the contents of an <code>authorized_keys</code> file
- *
- * @param filePath The file path to read from
- * @return A {@link List} of all the {@link AuthorizedKeyEntry}-ies found there
- * @throws IOException If failed to read or parse the entries
- * @see #readAuthorizedKeys(InputStream, boolean)
- */
- public static List<AuthorizedKeyEntry> readAuthorizedKeys(String filePath) throws IOException {
- try (InputStream in = new FileInputStream(filePath)) {
- return readAuthorizedKeys(in, true);
- }
- }
-
- /**
- * Reads read the contents of an <code>authorized_keys</code> file
- *
- * @param in The {@link InputStream}
- * @param okToClose <code>true</code> if method may close the input stream
- * regardless of whether successful or failed
- * @return A {@link List} of all the {@link AuthorizedKeyEntry}-ies found there
- * @throws IOException If failed to read or parse the entries
- * @see #readAuthorizedKeys(Reader, boolean)
- */
- public static List<AuthorizedKeyEntry> readAuthorizedKeys(InputStream in, boolean okToClose) throws IOException {
- try (Reader rdr = new InputStreamReader(NoCloseInputStream.resolveInputStream(in, okToClose), StandardCharsets.UTF_8)) {
- return readAuthorizedKeys(rdr, true);
- }
- }
-
- /**
- * Reads read the contents of an <code>authorized_keys</code> file
- *
- * @param rdr The {@link Reader}
- * @param okToClose <code>true</code> if method may close the input stream
- * regardless of whether successful or failed
- * @return A {@link List} of all the {@link AuthorizedKeyEntry}-ies found there
- * @throws IOException If failed to read or parse the entries
- * @see #readAuthorizedKeys(BufferedReader)
- */
- public static List<AuthorizedKeyEntry> readAuthorizedKeys(Reader rdr, boolean okToClose) throws IOException {
- try (BufferedReader buf = new BufferedReader(NoCloseReader.resolveReader(rdr, okToClose))) {
- return readAuthorizedKeys(buf);
- }
- }
-
- /**
- * @param rdr The {@link BufferedReader} to use to read the contents of
- * an <code>authorized_keys</code> file
- * @return A {@link List} of all the {@link AuthorizedKeyEntry}-ies found there
- * @throws IOException If failed to read or parse the entries
- * @see #parseAuthorizedKeyEntry(String)
- */
- public static List<AuthorizedKeyEntry> readAuthorizedKeys(BufferedReader rdr) throws IOException {
- List<AuthorizedKeyEntry> entries = null;
-
- for (String line = rdr.readLine(); line != null; line = rdr.readLine()) {
- AuthorizedKeyEntry entry;
- try {
- entry = parseAuthorizedKeyEntry(line.trim());
- if (entry == null) { // null, empty or comment line
- continue;
- }
- } catch (IllegalArgumentException e) {
- throw new StreamCorruptedException(e.getMessage());
- }
-
- if (entries == null) {
- entries = new ArrayList<>();
- }
-
- entries.add(entry);
- }
-
- if (entries == null) {
- return Collections.emptyList();
- } else {
- return entries;
- }
- }
-
- /**
- * @param line Original line from an <code>authorized_keys</code> file
- * @return {@link AuthorizedKeyEntry} or {@code null} if the line is
- * {@code null}/empty or a comment line
- * @throws IllegalArgumentException If failed to parse/decode the line
- * @see #COMMENT_CHAR
- */
- public static AuthorizedKeyEntry parseAuthorizedKeyEntry(String line) throws IllegalArgumentException {
- if (GenericUtils.isEmpty(line) || (line.charAt(0) == COMMENT_CHAR) /* comment ? */) {
- return null;
- }
-
- int startPos = line.indexOf(' ');
- if (startPos <= 0) {
- throw new IllegalArgumentException("Bad format (no key data delimiter): " + line);
- }
-
- int endPos = line.indexOf(' ', startPos + 1);
- if (endPos <= startPos) {
- endPos = line.length();
- }
-
- String keyType = line.substring(0, startPos);
- PublicKeyEntryDecoder<?, ?> decoder = KeyUtils.getPublicKeyEntryDecoder(keyType);
- final AuthorizedKeyEntry entry;
- if (decoder == null) { // assume this is due to the fact that it starts with login options
- entry = parseAuthorizedKeyEntry(line.substring(startPos + 1).trim());
- if (entry == null) {
- throw new IllegalArgumentException("Bad format (no key data after login options): " + line);
- }
-
- entry.setLoginOptions(parseLoginOptions(keyType));
- } else {
- String encData = (endPos < (line.length() - 1)) ? line.substring(0, endPos).trim() : line;
- String comment = (endPos < (line.length() - 1)) ? line.substring(endPos + 1).trim() : null;
- entry = parsePublicKeyEntry(new AuthorizedKeyEntry(), encData);
- entry.setComment(comment);
- }
-
- return entry;
- }
-
- public static Map<String, String> parseLoginOptions(String options) {
- // TODO add support if quoted values contain ','
- String[] pairs = GenericUtils.split(options, ',');
- if (GenericUtils.isEmpty(pairs)) {
- return Collections.emptyMap();
- }
-
- Map<String, String> optsMap = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
- for (String p : pairs) {
- p = GenericUtils.trimToEmpty(p);
- if (GenericUtils.isEmpty(p)) {
- continue;
- }
-
- int pos = p.indexOf('=');
- String name = (pos < 0) ? p : GenericUtils.trimToEmpty(p.substring(0, pos));
- CharSequence value = (pos < 0) ? null : GenericUtils.trimToEmpty(p.substring(pos + 1));
- value = GenericUtils.stripQuotes(value);
- if (value == null) {
- value = Boolean.TRUE.toString();
- }
-
- String prev = optsMap.put(name, value.toString());
- if (prev != null) {
- throw new IllegalArgumentException("Multiple values for key=" + name + ": old=" + prev + ", new=" + value);
- }
- }
-
- return optsMap;
- }
-}