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:53 UTC
[2/3] mina-sshd git commit: [SSHD-266] Support for known hosts
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;
- }
-}