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 2018/09/06 16:04:00 UTC
[50/51] [abbrv] mina-sshd git commit: [SSHD-842] Split common
utilities code from sshd-core into sshd-common (new artifact)
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/10de190e/sshd-common/src/main/java/org/apache/sshd/client/config/hosts/ConfigFileHostEntryResolver.java
----------------------------------------------------------------------
diff --git a/sshd-common/src/main/java/org/apache/sshd/client/config/hosts/ConfigFileHostEntryResolver.java b/sshd-common/src/main/java/org/apache/sshd/client/config/hosts/ConfigFileHostEntryResolver.java
new file mode 100644
index 0000000..f1c9ea8
--- /dev/null
+++ b/sshd-common/src/main/java/org/apache/sshd/client/config/hosts/ConfigFileHostEntryResolver.java
@@ -0,0 +1,107 @@
+/*
+ * 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.File;
+import java.io.IOException;
+import java.nio.file.LinkOption;
+import java.nio.file.Path;
+import java.util.Collection;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.io.IoUtils;
+import org.apache.sshd.common.util.io.ModifiableFileWatcher;
+
+/**
+ * Watches for changes in a configuration file and automatically reloads any changes
+ *
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public class ConfigFileHostEntryResolver extends ModifiableFileWatcher implements HostConfigEntryResolver {
+ private final AtomicReference<HostConfigEntryResolver> delegateHolder = // assumes initially empty
+ new AtomicReference<>(HostConfigEntryResolver.EMPTY);
+
+ public ConfigFileHostEntryResolver(File file) {
+ this(Objects.requireNonNull(file, "No file to watch").toPath());
+ }
+
+ public ConfigFileHostEntryResolver(Path file) {
+ this(file, IoUtils.EMPTY_LINK_OPTIONS);
+ }
+
+ public ConfigFileHostEntryResolver(Path file, LinkOption... options) {
+ super(file, options);
+ }
+
+ @Override
+ public HostConfigEntry resolveEffectiveHost(String host, int port, String username) throws IOException {
+ try {
+ HostConfigEntryResolver delegate = Objects.requireNonNull(resolveEffectiveResolver(host, port, username), "No delegate");
+ HostConfigEntry entry = delegate.resolveEffectiveHost(host, port, username);
+ if (log.isDebugEnabled()) {
+ log.debug("resolveEffectiveHost({}@{}:{}) => {}", username, host, port, entry);
+ }
+
+ return entry;
+ } catch (Throwable e) {
+ if (log.isDebugEnabled()) {
+ log.debug("resolveEffectiveHost({}@{}:{}) failed ({}) to resolve: {}",
+ username, host, port, e.getClass().getSimpleName(), e.getMessage());
+ }
+
+ if (log.isTraceEnabled()) {
+ log.trace("resolveEffectiveHost(" + username + "@" + host + ":" + port + ") resolution failure details", e);
+ }
+ if (e instanceof IOException) {
+ throw (IOException) e;
+ } else {
+ throw new IOException(e);
+ }
+ }
+ }
+
+ protected HostConfigEntryResolver resolveEffectiveResolver(String host, int port, String username) throws IOException {
+ if (checkReloadRequired()) {
+ delegateHolder.set(HostConfigEntryResolver.EMPTY); // start fresh
+
+ Path path = getPath();
+ if (exists()) {
+ Collection<HostConfigEntry> entries = reloadHostConfigEntries(path, host, port, username);
+ if (GenericUtils.size(entries) > 0) {
+ delegateHolder.set(HostConfigEntry.toHostConfigEntryResolver(entries));
+ }
+ } else {
+ log.info("resolveEffectiveResolver({}@{}:{}) no configuration file at {}", username, host, port, path);
+ }
+ }
+
+ return delegateHolder.get();
+ }
+
+ protected List<HostConfigEntry> reloadHostConfigEntries(Path path, String host, int port, String username) throws IOException {
+ List<HostConfigEntry> entries = HostConfigEntry.readHostConfigEntries(path);
+ log.info("resolveEffectiveResolver({}@{}:{}) loaded {} entries from {}", username, host, port, GenericUtils.size(entries), path);
+ updateReloadAttributes();
+ return entries;
+ }
+}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/10de190e/sshd-common/src/main/java/org/apache/sshd/client/config/hosts/DefaultConfigFileHostEntryResolver.java
----------------------------------------------------------------------
diff --git a/sshd-common/src/main/java/org/apache/sshd/client/config/hosts/DefaultConfigFileHostEntryResolver.java b/sshd-common/src/main/java/org/apache/sshd/client/config/hosts/DefaultConfigFileHostEntryResolver.java
new file mode 100644
index 0000000..29bc355
--- /dev/null
+++ b/sshd-common/src/main/java/org/apache/sshd/client/config/hosts/DefaultConfigFileHostEntryResolver.java
@@ -0,0 +1,95 @@
+/*
+ * 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.File;
+import java.io.IOException;
+import java.nio.file.LinkOption;
+import java.nio.file.Path;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+import org.apache.sshd.common.util.io.IoUtils;
+
+/**
+ * Monitors the {@code ~/.ssh/config} file of the user currently running
+ * 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 {
+ /**
+ * The default instance that enforces the same permissions regime as {@code OpenSSH}
+ */
+ public static final DefaultConfigFileHostEntryResolver INSTANCE = new DefaultConfigFileHostEntryResolver(true);
+
+ private final boolean strict;
+
+ /**
+ * @param strict 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 DefaultConfigFileHostEntryResolver(boolean strict) {
+ this(HostConfigEntry.getDefaultHostConfigFile(), strict);
+ }
+
+ public DefaultConfigFileHostEntryResolver(File file, boolean strict) {
+ this(Objects.requireNonNull(file, "No file provided").toPath(), strict, IoUtils.getLinkOptions(true));
+ }
+
+ public DefaultConfigFileHostEntryResolver(Path path, boolean strict, LinkOption... options) {
+ super(path, 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<HostConfigEntry> reloadHostConfigEntries(Path path, String host, int port, String username) throws IOException {
+ if (isStrict()) {
+ if (log.isDebugEnabled()) {
+ log.debug("reloadHostConfigEntries({}@{}:{}) check permissions of {}", username, host, port, path);
+ }
+
+ Map.Entry<String, ?> violation = validateStrictConfigFilePermissions(path);
+ if (violation != null) {
+ log.warn("reloadHostConfigEntries({}@{}:{}) invalid file={} permissions: {}",
+ username, host, port, path, violation.getKey());
+ updateReloadAttributes();
+ return Collections.emptyList();
+ }
+ }
+
+ return super.reloadHostConfigEntries(path, host, port, username);
+ }
+}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/10de190e/sshd-common/src/main/java/org/apache/sshd/client/config/hosts/HostConfigEntry.java
----------------------------------------------------------------------
diff --git a/sshd-common/src/main/java/org/apache/sshd/client/config/hosts/HostConfigEntry.java b/sshd-common/src/main/java/org/apache/sshd/client/config/hosts/HostConfigEntry.java
new file mode 100644
index 0000000..8a05d75
--- /dev/null
+++ b/sshd-common/src/main/java/org/apache/sshd/client/config/hosts/HostConfigEntry.java
@@ -0,0 +1,1169 @@
+/*
+ * 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.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Reader;
+import java.io.StreamCorruptedException;
+import java.io.Writer;
+import java.net.InetAddress;
+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.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.TreeMap;
+
+import org.apache.sshd.common.auth.MutableUserHolder;
+import org.apache.sshd.common.config.ConfigFileReaderSupport;
+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.ValidateUtils;
+import org.apache.sshd.common.util.io.IoUtils;
+import org.apache.sshd.common.util.io.NoCloseInputStream;
+import org.apache.sshd.common.util.io.NoCloseOutputStream;
+import org.apache.sshd.common.util.io.NoCloseReader;
+
+/**
+ * Represents an entry in the client's configuration file as defined by
+ * the <A HREF="http://www.gsp.com/cgi-bin/man.cgi?topic=ssh_config">configuration
+ * file format</A>
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public class HostConfigEntry extends HostPatternsHolder implements MutableUserHolder {
+ /**
+ * Standard OpenSSH config file name
+ */
+ public static final String STD_CONFIG_FILENAME = "config";
+
+ public static final String HOST_CONFIG_PROP = "Host";
+ public static final String HOST_NAME_CONFIG_PROP = "HostName";
+ public static final String PORT_CONFIG_PROP = ConfigFileReaderSupport.PORT_CONFIG_PROP;
+ public static final String USER_CONFIG_PROP = "User";
+ public static final String IDENTITY_FILE_CONFIG_PROP = "IdentityFile";
+ /**
+ * Use only the identities specified in the host entry (if any)
+ */
+ public static final String EXCLUSIVE_IDENTITIES_CONFIG_PROP = "IdentitiesOnly";
+ public static final boolean DEFAULT_EXCLUSIVE_IDENTITIES = false;
+
+ /**
+ * A case <U>insensitive</U> {@link Set} of the properties that receive special handling
+ */
+ public static final Set<String> EXPLICIT_PROPERTIES =
+ Collections.unmodifiableSet(
+ GenericUtils.asSortedSet(String.CASE_INSENSITIVE_ORDER,
+ HOST_CONFIG_PROP, HOST_NAME_CONFIG_PROP, PORT_CONFIG_PROP,
+ USER_CONFIG_PROP, IDENTITY_FILE_CONFIG_PROP, EXCLUSIVE_IDENTITIES_CONFIG_PROP
+ ));
+
+ public static final String MULTI_VALUE_SEPARATORS = " ,";
+
+ public static final char HOME_TILDE_CHAR = '~';
+ public static final char PATH_MACRO_CHAR = '%';
+ public static final char LOCAL_HOME_MACRO = 'd';
+ public static final char LOCAL_USER_MACRO = 'u';
+ public static final char LOCAL_HOST_MACRO = 'l';
+ public static final char REMOTE_HOST_MACRO = 'h';
+ public static final char REMOTE_USER_MACRO = 'r';
+ // Extra - not part of the standard
+ public static final char REMOTE_PORT_MACRO = 'p';
+
+ private static final class LazyDefaultConfigFileHolder {
+ private static final Path CONFIG_FILE =
+ PublicKeyEntry.getDefaultKeysFolderPath().resolve(STD_CONFIG_FILENAME);
+
+ private LazyDefaultConfigFileHolder() {
+ throw new UnsupportedOperationException("No instance allowed");
+ }
+ }
+
+ private String host;
+ private String hostName;
+ private int port;
+ private String username;
+ private Boolean exclusiveIdentites;
+ private Collection<String> identities = Collections.emptyList();
+ private Map<String, String> properties = Collections.emptyMap();
+
+ public HostConfigEntry() {
+ super();
+ }
+
+ public HostConfigEntry(String pattern, String host, int port, String username) {
+ setHost(pattern);
+ setHostName(host);
+ setPort(port);
+ setUsername(username);
+ }
+
+ /**
+ * @return The <U>pattern(s)</U> represented by this entry
+ */
+ public String getHost() {
+ return host;
+ }
+
+ public void setHost(String host) {
+ this.host = host;
+ setPatterns(parsePatterns(parseConfigValue(host)));
+ }
+
+ public void setHost(Collection<String> patterns) {
+ this.host = GenericUtils.join(ValidateUtils.checkNotNullAndNotEmpty(patterns, "No patterns"), ',');
+ setPatterns(parsePatterns(patterns));
+ }
+
+ /**
+ * @return The effective host name to connect to if the pattern matches
+ */
+ public String getHostName() {
+ return hostName;
+ }
+
+ public void setHostName(String hostName) {
+ this.hostName = hostName;
+ }
+
+ public String resolveHostName(String originalHost) {
+ return resolveHostName(originalHost, getHostName());
+ }
+
+ /**
+ * @return A port override - if positive
+ */
+ public int getPort() {
+ return port;
+ }
+
+ public void setPort(int port) {
+ this.port = port;
+ }
+
+ /**
+ * Resolves the effective port to use
+ *
+ * @param originalPort The original requested port
+ * @return If the host entry port is positive, then it is used, otherwise
+ * the original requested port
+ * @see #resolvePort(int, int)
+ */
+ public int resolvePort(int originalPort) {
+ return resolvePort(originalPort, getPort());
+ }
+
+ /**
+ * @return A username override - if not {@code null}/empty
+ */
+ @Override
+ public String getUsername() {
+ return username;
+ }
+
+ @Override
+ public void setUsername(String username) {
+ this.username = username;
+ }
+
+ /**
+ * Resolves the effective username
+ *
+ * @param originalUser The original requested username
+ * @return If the configured host entry username is not {@code null}/empty
+ * then it is used, otherwise the original one.
+ * @see #resolveUsername(String)
+ */
+ public String resolveUsername(String originalUser) {
+ return resolveUsername(originalUser, getUsername());
+ }
+
+ /**
+ * @return The current identities file paths - may be {@code null}/empty
+ */
+ public Collection<String> getIdentities() {
+ return identities;
+ }
+
+ /**
+ * @param file A {@link File} that contains an identity key - never {@code null}
+ */
+ public void addIdentity(File file) {
+ addIdentity(Objects.requireNonNull(file, "No file").toPath());
+ }
+
+ /**
+ * @param path A {@link Path} to a file that contains an identity key
+ * - never {@code null}
+ */
+ public void addIdentity(Path path) {
+ addIdentity(Objects.requireNonNull(path, "No path").toAbsolutePath().normalize().toString());
+ }
+
+ /**
+ * Adds a path to an identity file
+ *
+ * @param id The identity path to add - never {@code null}
+ */
+ public void addIdentity(String id) {
+ String path = ValidateUtils.checkNotNullAndNotEmpty(id, "No identity provided");
+ if (GenericUtils.isEmpty(identities)) {
+ identities = new LinkedList<>();
+ }
+ identities.add(path);
+ }
+
+ public void setIdentities(Collection<String> identities) {
+ this.identities = (identities == null) ? Collections.emptyList() : identities;
+ }
+
+ /**
+ * @return {@code true} if must use only the identities in this entry
+ */
+ public boolean isIdentitiesOnly() {
+ return (exclusiveIdentites == null) ? DEFAULT_EXCLUSIVE_IDENTITIES : exclusiveIdentites;
+ }
+
+ public void setIdentitiesOnly(boolean identitiesOnly) {
+ exclusiveIdentites = identitiesOnly;
+ }
+
+ /**
+ * @return A {@link Map} of extra properties that have been read - may be
+ * {@code null}/empty, or even contain some values that have been parsed
+ * and set as members of the entry (e.g., host, port, etc.). <B>Note:</B>
+ * multi-valued keys use a comma-separated list of values
+ */
+ public Map<String, String> getProperties() {
+ return properties;
+ }
+
+ /**
+ * @param name Property name - never {@code null}/empty
+ * @return Property value or {@code null} if no such property
+ * @see #getProperty(String, String)
+ */
+ public String getProperty(String name) {
+ return getProperty(name, null);
+ }
+
+ /**
+ * @param name Property name - never {@code null}/empty
+ * @param defaultValue Default value to return if no such property
+ * @return The property value or the default one if no such property
+ */
+ public String getProperty(String name, String defaultValue) {
+ String key = ValidateUtils.checkNotNullAndNotEmpty(name, "No property name");
+ Map<String, String> props = getProperties();
+ if (GenericUtils.isEmpty(props)) {
+ return null;
+ }
+
+ String value = props.get(key);
+ if (GenericUtils.isEmpty(value)) {
+ return defaultValue;
+ } else {
+ return value;
+ }
+ }
+
+ /**
+ * Updates the values that are <U>not</U> already configured with those
+ * from the global entry
+ *
+ * @param globalEntry The global entry - ignored if {@code null} or
+ * same reference as this entry
+ * @return {@code true} if anything updated
+ */
+ public boolean processGlobalValues(HostConfigEntry globalEntry) {
+ if ((globalEntry == null) || (this == globalEntry)) {
+ return false;
+ }
+
+ boolean modified = false;
+ /*
+ * NOTE !!! DO NOT TRY TO CHANGE THE ORDER OF THE OR-ing AS IT
+ * WOULD CAUSE INVALID CODE EXECUTION
+ */
+ modified = updateGlobalPort(globalEntry.getPort()) || modified;
+ modified = updateGlobalHostName(globalEntry.getHostName()) || modified;
+ modified = updateGlobalUserName(globalEntry.getUsername()) || modified;
+ modified = updateGlobalIdentities(globalEntry.getIdentities()) || modified;
+ modified = updateGlobalIdentityOnly(globalEntry.isIdentitiesOnly()) || modified;
+
+ Map<String, String> updated = updateGlobalProperties(globalEntry.getProperties());
+ modified = (GenericUtils.size(updated) > 0) || modified;
+
+ return modified;
+ }
+
+ /**
+ * Sets all the properties for which no current value exists in the entry
+ *
+ * @param props The global properties - ignored if {@code null}/empty
+ * @return A {@link Map} of the <U>updated</U> properties
+ */
+ public Map<String, String> updateGlobalProperties(Map<String, String> props) {
+ if (GenericUtils.isEmpty(props)) {
+ return Collections.emptyMap();
+ }
+
+ Map<String, String> updated = null;
+ // Cannot use forEach because of the modification of the updated map value (non-final)
+ for (Map.Entry<String, String> pe : props.entrySet()) {
+ String key = pe.getKey();
+ String curValue = getProperty(key);
+ if (GenericUtils.length(curValue) > 0) {
+ continue;
+ }
+
+ String newValue = pe.getValue();
+ setProperty(key, newValue);
+
+ if (updated == null) {
+ updated = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
+ }
+
+ updated.put(key, newValue);
+ }
+
+ if (updated == null) {
+ return Collections.emptyMap();
+ } else {
+ return updated;
+ }
+ }
+
+ /**
+ * @param ids Global identities - ignored if {@code null}/empty or already
+ * have configured identities
+ * @return {@code true} if updated identities
+ */
+ public boolean updateGlobalIdentities(Collection<String> ids) {
+ if (GenericUtils.isEmpty(ids) || (GenericUtils.size(getIdentities()) > 0)) {
+ return false;
+ }
+
+ for (String id : ids) {
+ addIdentity(id);
+ }
+
+ return true;
+ }
+
+ /**
+ * @param user The global user name - ignored if {@code null}/empty or
+ * already have a configured user
+ * @return {@code true} if updated the username
+ */
+ public boolean updateGlobalUserName(String user) {
+ if (GenericUtils.isEmpty(user) || (GenericUtils.length(getUsername()) > 0)) {
+ return false;
+ }
+
+ setUsername(user);
+ return true;
+ }
+
+ /**
+ * @param name The global host name - ignored if {@code null}/empty or
+ * already have a configured target host
+ * @return {@code true} if updated the target host
+ */
+ public boolean updateGlobalHostName(String name) {
+ if (GenericUtils.isEmpty(name) || (GenericUtils.length(getHostName()) > 0)) {
+ return false;
+ }
+
+ setHostName(name);
+ return true;
+ }
+
+ /**
+ * @param portValue The global port value - ignored if not positive
+ * or already have a configured port
+ * @return {@code true} if updated the port value
+ */
+ public boolean updateGlobalPort(int portValue) {
+ if ((portValue <= 0) || (getPort() > 0)) {
+ return false;
+ }
+
+ setPort(portValue);
+ return true;
+ }
+
+ /**
+ * @param identitiesOnly Whether to use only the identities in this entry.
+ * Ignored if already set
+ * @return {@code true} if updated the option value
+ */
+ public boolean updateGlobalIdentityOnly(boolean identitiesOnly) {
+ if (exclusiveIdentites != null) {
+ return false;
+ }
+
+ setIdentitiesOnly(identitiesOnly);
+ return true;
+ }
+
+ /**
+ * @param name Property name - never {@code null}/empty
+ * @param valsList The available values for the property
+ * @param ignoreAlreadyInitialized If {@code false} and one of the "known"
+ * properties is encountered then throws an exception
+ * @throws IllegalArgumentException If an existing value is overwritten and
+ * <tt>ignoreAlreadyInitialized</tt> is {@code false} (except for {@link #IDENTITY_FILE_CONFIG_PROP}
+ * which is <U>cumulative</U>
+ * @see #HOST_NAME_CONFIG_PROP
+ * @see #PORT_CONFIG_PROP
+ * @see #USER_CONFIG_PROP
+ * @see #IDENTITY_FILE_CONFIG_PROP
+ */
+ public void processProperty(String name, Collection<String> valsList, boolean ignoreAlreadyInitialized) {
+ String key = ValidateUtils.checkNotNullAndNotEmpty(name, "No property name");
+ String joinedValue = GenericUtils.join(valsList, ',');
+ appendPropertyValue(key, joinedValue);
+
+ if (HOST_NAME_CONFIG_PROP.equalsIgnoreCase(key)) {
+ ValidateUtils.checkTrue(GenericUtils.size(valsList) == 1, "Multiple target hosts N/A: %s", joinedValue);
+
+ String curValue = getHostName();
+ ValidateUtils.checkTrue(GenericUtils.isEmpty(curValue) || ignoreAlreadyInitialized, "Already initialized %s: %s", key, curValue);
+ setHostName(joinedValue);
+ } else if (PORT_CONFIG_PROP.equalsIgnoreCase(key)) {
+ ValidateUtils.checkTrue(GenericUtils.size(valsList) == 1, "Multiple target ports N/A: %s", joinedValue);
+
+ int curValue = getPort();
+ ValidateUtils.checkTrue((curValue <= 0) || ignoreAlreadyInitialized, "Already initialized %s: %d", key, curValue);
+
+ int newValue = Integer.parseInt(joinedValue);
+ ValidateUtils.checkTrue(newValue > 0, "Bad new port value: %d", newValue);
+ setPort(newValue);
+ } else if (USER_CONFIG_PROP.equalsIgnoreCase(key)) {
+ ValidateUtils.checkTrue(GenericUtils.size(valsList) == 1, "Multiple target users N/A: %s", joinedValue);
+
+ String curValue = getUsername();
+ ValidateUtils.checkTrue(GenericUtils.isEmpty(curValue) || ignoreAlreadyInitialized, "Already initialized %s: %s", key, curValue);
+ setUsername(joinedValue);
+ } else if (IDENTITY_FILE_CONFIG_PROP.equalsIgnoreCase(key)) {
+ ValidateUtils.checkTrue(GenericUtils.size(valsList) > 0, "No identity files specified");
+ for (String id : valsList) {
+ addIdentity(id);
+ }
+ } else if (EXCLUSIVE_IDENTITIES_CONFIG_PROP.equalsIgnoreCase(key)) {
+ setIdentitiesOnly(
+ ConfigFileReaderSupport.parseBooleanValue(
+ ValidateUtils.checkNotNullAndNotEmpty(joinedValue, "No identities option value")));
+ }
+ }
+
+ /**
+ * Appends a value using a <U>comma</U> to an existing one. If no previous
+ * value then same as calling {@link #setProperty(String, String)}.
+ *
+ * @param name Property name - never {@code null}/empty
+ * @param value The value to be appended - ignored if {@code null}/empty
+ * @return The value <U>before</U> appending - {@code null} if no previous value
+ */
+ public String appendPropertyValue(String name, String value) {
+ String key = ValidateUtils.checkNotNullAndNotEmpty(name, "No property name");
+ String curVal = getProperty(key);
+ if (GenericUtils.isEmpty(value)) {
+ return curVal;
+ }
+
+ if (GenericUtils.isEmpty(curVal)) {
+ return setProperty(key, value);
+ }
+
+ return setProperty(key, curVal + ',' + value);
+ }
+
+ /**
+ * Sets / Replaces the property value
+ *
+ * @param name Property name - never {@code null}/empty
+ * @param value Property value - if {@code null}/empty then
+ * {@link #removeProperty(String)} is called
+ * @return The previous property value - {@code null} if no such name
+ */
+ public String setProperty(String name, String value) {
+ if (GenericUtils.isEmpty(value)) {
+ return removeProperty(name);
+ }
+
+ String key = ValidateUtils.checkNotNullAndNotEmpty(name, "No property name");
+ if (GenericUtils.isEmpty(properties)) {
+ properties = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
+ }
+
+ return properties.put(key, value);
+ }
+
+ /**
+ * @param name Property name - never {@code null}/empty
+ * @return The removed property value - {@code null} if no such property name
+ */
+ public String removeProperty(String name) {
+ String key = ValidateUtils.checkNotNullAndNotEmpty(name, "No property name");
+ Map<String, String> props = getProperties();
+ if (GenericUtils.isEmpty(props)) {
+ return null;
+ } else {
+ return props.remove(key);
+ }
+ }
+
+ /**
+ * @param properties The properties to set - if {@code null} then an empty
+ * map is effectively set. <B>Note:</B> it is highly recommended to use a
+ * <U>case insensitive</U> key mapper.
+ */
+ public void setProperties(Map<String, String> properties) {
+ this.properties = (properties == null) ? Collections.emptyMap() : properties;
+ }
+
+ public <A extends Appendable> A append(A sb) throws IOException {
+ sb.append(HOST_CONFIG_PROP).append(' ').append(ValidateUtils.checkNotNullAndNotEmpty(getHost(), "No host pattern")).append(IoUtils.EOL);
+ appendNonEmptyProperty(sb, HOST_NAME_CONFIG_PROP, getHostName());
+ appendNonEmptyPort(sb, PORT_CONFIG_PROP, getPort());
+ appendNonEmptyProperty(sb, USER_CONFIG_PROP, getUsername());
+ appendNonEmptyValues(sb, IDENTITY_FILE_CONFIG_PROP, getIdentities());
+ if (exclusiveIdentites != null) {
+ appendNonEmptyProperty(sb, EXCLUSIVE_IDENTITIES_CONFIG_PROP, ConfigFileReaderSupport.yesNoValueOf(exclusiveIdentites));
+ }
+ appendNonEmptyProperties(sb, getProperties());
+ return sb;
+ }
+
+ @Override
+ public String toString() {
+ return getHost() + ": " + getUsername() + "@" + getHostName() + ":" + getPort();
+ }
+
+ /**
+ * @param <A> The {@link Appendable} type
+ * @param sb The target appender
+ * @param name The property name - never {@code null}/empty
+ * @param port The port value - ignored if non-positive
+ * @return The target appender after having appended (or not) the value
+ * @throws IOException If failed to append the requested data
+ * @see #appendNonEmptyProperty(Appendable, String, Object)
+ */
+ public static <A extends Appendable> A appendNonEmptyPort(A sb, String name, int port) throws IOException {
+ return appendNonEmptyProperty(sb, name, (port > 0) ? Integer.toString(port) : null);
+ }
+
+ /**
+ * Appends the extra properties - while skipping the {@link #EXPLICIT_PROPERTIES} ones
+ *
+ * @param <A> The {@link Appendable} type
+ * @param sb The target appender
+ * @param props The {@link Map} of properties - ignored if {@code null}/empty
+ * @return The target appender after having appended (or not) the value
+ * @throws IOException If failed to append the requested data
+ * @see #appendNonEmptyProperty(Appendable, String, Object)
+ */
+ public static <A extends Appendable> A appendNonEmptyProperties(A sb, Map<String, ?> props) throws IOException {
+ if (GenericUtils.isEmpty(props)) {
+ return sb;
+ }
+
+ // Cannot use forEach because of the IOException being thrown by appendNonEmptyProperty
+ for (Map.Entry<String, ?> pe : props.entrySet()) {
+ String name = pe.getKey();
+ if (EXPLICIT_PROPERTIES.contains(name)) {
+ continue;
+ }
+
+ appendNonEmptyProperty(sb, name, pe.getValue());
+ }
+
+ return sb;
+ }
+
+ /**
+ * @param <A> The {@link Appendable} type
+ * @param sb The target appender
+ * @param name The property name - never {@code null}/empty
+ * @param value The property value - ignored if {@code null}. <B>Note:</B>
+ * if the string representation of the value contains any commas, they are
+ * assumed to indicate a multi-valued property which is broken down to
+ * <U>individual</U> lines - one per value.
+ * @return The target appender after having appended (or not) the value
+ * @throws IOException If failed to append the requested data
+ * @see #appendNonEmptyValues(Appendable, String, Object...)
+ */
+ public static <A extends Appendable> A appendNonEmptyProperty(A sb, String name, Object value) throws IOException {
+ String s = Objects.toString(value, null);
+ String[] vals = GenericUtils.split(s, ',');
+ return appendNonEmptyValues(sb, name, (Object[]) vals);
+ }
+
+ /**
+ * @param <A> The {@link Appendable} type
+ * @param sb The target appender
+ * @param name The property name - never {@code null}/empty
+ * @param values The values to be added - one per line - ignored if {@code null}/empty
+ * @return The target appender after having appended (or not) the value
+ * @throws IOException If failed to append the requested data
+ * @see #appendNonEmptyValues(Appendable, String, Collection)
+ */
+ public static <A extends Appendable> A appendNonEmptyValues(A sb, String name, Object... values) throws IOException {
+ return appendNonEmptyValues(sb, name, GenericUtils.isEmpty(values) ? Collections.emptyList() : Arrays.asList(values));
+ }
+
+ /**
+ * @param <A> The {@link Appendable} type
+ * @param sb The target appender
+ * @param name The property name - never {@code null}/empty
+ * @param values The values to be added - one per line - ignored if {@code null}/empty
+ * @return The target appender after having appended (or not) the value
+ * @throws IOException If failed to append the requested data
+ */
+ public static <A extends Appendable> A appendNonEmptyValues(A sb, String name, Collection<?> values) throws IOException {
+ String k = ValidateUtils.checkNotNullAndNotEmpty(name, "No property name");
+ if (GenericUtils.isEmpty(values)) {
+ return sb;
+ }
+
+ for (Object v : values) {
+ sb.append(" ").append(k).append(' ').append(Objects.toString(v)).append(IoUtils.EOL);
+ }
+
+ return sb;
+ }
+
+ /**
+ * @param entries The entries - ignored if {@code null}/empty
+ * @return A {@link HostConfigEntryResolver} wrapper using the entries
+ */
+ public static HostConfigEntryResolver toHostConfigEntryResolver(Collection<? extends HostConfigEntry> entries) {
+ if (GenericUtils.isEmpty(entries)) {
+ return HostConfigEntryResolver.EMPTY;
+ } else {
+ return (host1, port1, username1) -> {
+ List<HostConfigEntry> matches = findMatchingEntries(host1, entries);
+ int numMatches = GenericUtils.size(matches);
+ if (numMatches <= 0) {
+ return null;
+ }
+
+ HostConfigEntry match = (numMatches == 1) ? matches.get(0) : findBestMatch(matches);
+ if (match == null) {
+ ValidateUtils.throwIllegalArgumentException("No best match found for %s@%s:%d out of %d matches", username1, host1, port1, numMatches);
+ }
+
+ return normalizeEntry(match, host1, port1, username1);
+ };
+ }
+ }
+
+ /**
+ * @param entry The original entry - ignored if {@code null}
+ * @param host The original host name / address
+ * @param port The original port
+ * @param username The original user name
+ * @return A <U>cloned</U> entry whose values are resolved - including
+ * expanding macros in the identities files
+ * @throws IOException If failed to normalize the entry
+ * @see #resolveHostName(String)
+ * @see #resolvePort(int)
+ * @see #resolveUsername(String)
+ * @see #resolveIdentityFilePath(String, String, int, String)
+ */
+ public static HostConfigEntry normalizeEntry(HostConfigEntry entry, String host, int port, String username) throws IOException {
+ if (entry == null) {
+ return null;
+ }
+
+ HostConfigEntry normal = new HostConfigEntry();
+ normal.setHost(host);
+ normal.setHostName(entry.resolveHostName(host));
+ normal.setPort(entry.resolvePort(port));
+ normal.setUsername(entry.resolveUsername(username));
+
+ Map<String, String> props = entry.getProperties();
+ if (GenericUtils.size(props) > 0) {
+ normal.setProperties(new TreeMap<>(props));
+ }
+
+ Collection<String> ids = entry.getIdentities();
+ if (GenericUtils.isEmpty(ids)) {
+ return normal;
+ }
+
+ normal.setIdentities(Collections.emptyList()); // start fresh
+ for (String id : ids) {
+ String path = resolveIdentityFilePath(id, host, port, username);
+ normal.addIdentity(path);
+ }
+
+ return normal;
+ }
+
+ /**
+ * Resolves the effective target host
+ *
+ * @param originalName The original requested host
+ * @param entryName The configured host
+ * @return If the configured host entry is not {@code null}/empty
+ * then it is used, otherwise the original one.
+ */
+ public static String resolveHostName(String originalName, String entryName) {
+ if (GenericUtils.isEmpty(entryName)) {
+ return originalName;
+ } else {
+ return entryName;
+ }
+ }
+
+ /**
+ * Resolves the effective username
+ *
+ * @param originalUser The original requested username
+ * @param entryUser The configured host entry username
+ * @return If the configured host entry username is not {@code null}/empty
+ * then it is used, otherwise the original one.
+ */
+ public static String resolveUsername(String originalUser, String entryUser) {
+ if (GenericUtils.isEmpty(entryUser)) {
+ return originalUser;
+ } else {
+ return entryUser;
+ }
+ }
+
+ /**
+ * Resolves the effective port to use
+ *
+ * @param originalPort The original requested port
+ * @param entryPort The configured host entry port
+ * @return If the host entry port is positive, then it is used, otherwise
+ * the original requested port
+ */
+ public static int resolvePort(int originalPort, int entryPort) {
+ if (entryPort <= 0) {
+ return originalPort;
+ } else {
+ return entryPort;
+ }
+ }
+
+ public static List<HostConfigEntry> readHostConfigEntries(File file) throws IOException {
+ return readHostConfigEntries(file.toPath(), IoUtils.EMPTY_OPEN_OPTIONS);
+ }
+
+ public static List<HostConfigEntry> readHostConfigEntries(Path path, OpenOption... options) throws IOException {
+ try (InputStream input = Files.newInputStream(path, options)) {
+ return readHostConfigEntries(input, true);
+ }
+ }
+
+ public static List<HostConfigEntry> readHostConfigEntries(URL url) throws IOException {
+ try (InputStream input = url.openStream()) {
+ return readHostConfigEntries(input, true);
+ }
+ }
+
+ public static List<HostConfigEntry> readHostConfigEntries(String filePath) throws IOException {
+ try (InputStream inStream = new FileInputStream(filePath)) {
+ return readHostConfigEntries(inStream, true);
+ }
+ }
+
+ public static List<HostConfigEntry> readHostConfigEntries(InputStream inStream, boolean okToClose) throws IOException {
+ try (Reader reader = new InputStreamReader(NoCloseInputStream.resolveInputStream(inStream, okToClose), StandardCharsets.UTF_8)) {
+ return readHostConfigEntries(reader, true);
+ }
+ }
+
+ public static List<HostConfigEntry> readHostConfigEntries(Reader rdr, boolean okToClose) throws IOException {
+ try (BufferedReader buf = new BufferedReader(NoCloseReader.resolveReader(rdr, okToClose))) {
+ return readHostConfigEntries(buf);
+ }
+ }
+
+ /**
+ * Reads configuration entries
+ *
+ * @param rdr The {@link BufferedReader} to use
+ * @return The {@link List} of read {@link HostConfigEntry}-ies
+ * @throws IOException If failed to parse the read configuration
+ */
+ public static List<HostConfigEntry> readHostConfigEntries(BufferedReader rdr) throws IOException {
+ HostConfigEntry curEntry = null;
+ HostConfigEntry globalEntry = null;
+ List<HostConfigEntry> entries = null;
+
+ int lineNumber = 1;
+ for (String line = rdr.readLine(); line != null; line = rdr.readLine(), lineNumber++) {
+ line = GenericUtils.replaceWhitespaceAndTrim(line);
+ if (GenericUtils.isEmpty(line)) {
+ continue;
+ }
+
+ int pos = line.indexOf(ConfigFileReaderSupport.COMMENT_CHAR);
+ if (pos == 0) {
+ continue;
+ }
+
+ if (pos > 0) {
+ line = line.substring(0, pos);
+ line = line.trim();
+ }
+
+ /*
+ * Some options use '=', others use ' ' - try both
+ * NOTE: we do not validate the format for each option separately
+ */
+ pos = line.indexOf(' ');
+ if (pos < 0) {
+ pos = line.indexOf('=');
+ }
+
+ if (pos < 0) {
+ throw new StreamCorruptedException("No configuration value delimiter at line " + lineNumber + ": " + line);
+ }
+
+ String key = line.substring(0, pos);
+ String value = line.substring(pos + 1);
+ List<String> valsList = parseConfigValue(value);
+
+ if (HOST_CONFIG_PROP.equalsIgnoreCase(key)) {
+ if (GenericUtils.isEmpty(valsList)) {
+ throw new StreamCorruptedException("Missing host pattern(s) at line " + lineNumber + ": " + line);
+ }
+
+ // If the all-hosts pattern is used, make sure no global section already active
+ for (String name : valsList) {
+ if (ALL_HOSTS_PATTERN.equalsIgnoreCase(name) && (globalEntry != null)) {
+ throw new StreamCorruptedException("Overriding the global section with a specific one at line " + lineNumber + ": " + line);
+ }
+ }
+
+ if (curEntry != null) {
+ curEntry.processGlobalValues(globalEntry);
+ }
+
+ entries = updateEntriesList(entries, curEntry);
+
+ curEntry = new HostConfigEntry();
+ curEntry.setHost(valsList);
+ } else if (curEntry == null) {
+ // if 1st encountered property is NOT for a specific host, then configuration applies to ALL
+ curEntry = new HostConfigEntry();
+ curEntry.setHost(Collections.singletonList(ALL_HOSTS_PATTERN));
+ globalEntry = curEntry;
+ }
+
+ try {
+ curEntry.processProperty(key, valsList, false);
+ } catch (RuntimeException e) {
+ throw new StreamCorruptedException("Failed (" + e.getClass().getSimpleName() + ")"
+ + " to process line #" + lineNumber + " (" + line + ")"
+ + ": " + e.getMessage());
+ }
+ }
+
+ if (curEntry != null) {
+ curEntry.processGlobalValues(globalEntry);
+ }
+
+ entries = updateEntriesList(entries, curEntry);
+ if (entries == null) {
+ return Collections.emptyList();
+ } else {
+ return entries;
+ }
+ }
+
+ /**
+ * 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;
+ }
+
+ if (entries == null) {
+ entries = new ArrayList<>();
+ }
+
+ entries.add(curEntry);
+ return entries;
+ }
+
+ public static void writeHostConfigEntries(File file, Collection<? extends HostConfigEntry> entries) throws IOException {
+ writeHostConfigEntries(Objects.requireNonNull(file, "No file").toPath(), entries, IoUtils.EMPTY_OPEN_OPTIONS);
+ }
+
+ public static void writeHostConfigEntries(Path path, Collection<? extends HostConfigEntry> entries, OpenOption... options) throws IOException {
+ try (OutputStream outputStream = Files.newOutputStream(path, options)) {
+ writeHostConfigEntries(outputStream, true, entries);
+ }
+ }
+
+ public static void writeHostConfigEntries(OutputStream outputStream, boolean okToClose, Collection<? extends HostConfigEntry> entries) throws IOException {
+ if (GenericUtils.isEmpty(entries)) {
+ return;
+ }
+
+ try (Writer w = new OutputStreamWriter(NoCloseOutputStream.resolveOutputStream(outputStream, okToClose), StandardCharsets.UTF_8)) {
+ appendHostConfigEntries(w, entries);
+ }
+ }
+
+ public static <A extends Appendable> A appendHostConfigEntries(A sb, Collection<? extends HostConfigEntry> entries) throws IOException {
+ if (GenericUtils.isEmpty(entries)) {
+ return sb;
+ }
+
+ for (HostConfigEntry entry : entries) {
+ entry.append(sb);
+ }
+
+ return sb;
+ }
+
+ /**
+ * Checks if this is a multi-value - allow space and comma
+ *
+ * @param value The value - ignored if {@code null}/empty (after trimming)
+ * @return A {@link List} of the encountered values
+ */
+ public static List<String> parseConfigValue(String value) {
+ String s = GenericUtils.replaceWhitespaceAndTrim(value);
+ if (GenericUtils.isEmpty(s)) {
+ return Collections.emptyList();
+ }
+
+ for (int index = 0; index < MULTI_VALUE_SEPARATORS.length(); index++) {
+ char sep = MULTI_VALUE_SEPARATORS.charAt(index);
+ int pos = s.indexOf(sep);
+ if (pos >= 0) {
+ String[] vals = GenericUtils.split(s, sep);
+ if (GenericUtils.isEmpty(vals)) {
+ return Collections.emptyList();
+ } else {
+ return Arrays.asList(vals);
+ }
+ }
+ }
+
+ // this point is reached if no separators found
+ return Collections.singletonList(s);
+ }
+
+ // The file name may use the tilde syntax to refer to a user’s home directory or one of the following escape characters:
+ // '%d' (local user's home directory), '%u' (local user name), '%l' (local host name), '%h' (remote host name) or '%r' (remote user name).
+ public static String resolveIdentityFilePath(String id, String host, int port, String username) throws IOException {
+ if (GenericUtils.isEmpty(id)) {
+ return id;
+ }
+
+ String path = id.replace('/', File.separatorChar); // make sure all separators are local
+ String[] elements = GenericUtils.split(path, File.separatorChar);
+ StringBuilder sb = new StringBuilder(path.length() + Long.SIZE);
+ for (int index = 0; index < elements.length; index++) {
+ String elem = elements[index];
+ if (index > 0) {
+ sb.append(File.separatorChar);
+ }
+
+ for (int curPos = 0; curPos < elem.length(); curPos++) {
+ char ch = elem.charAt(curPos);
+ if (ch == HOME_TILDE_CHAR) {
+ ValidateUtils.checkTrue((curPos == 0) && (index == 0), "Home tilde must be first: %s", id);
+ appendUserHome(sb);
+ } else if (ch == PATH_MACRO_CHAR) {
+ curPos++;
+ ValidateUtils.checkTrue(curPos < elem.length(), "Missing macro modifier in %s", id);
+ ch = elem.charAt(curPos);
+ switch(ch) {
+ case PATH_MACRO_CHAR:
+ sb.append(ch);
+ break;
+ case LOCAL_HOME_MACRO:
+ ValidateUtils.checkTrue((curPos == 1) && (index == 0), "Home macro must be first: %s", id);
+ appendUserHome(sb);
+ break;
+ case LOCAL_USER_MACRO:
+ sb.append(ValidateUtils.checkNotNullAndNotEmpty(OsUtils.getCurrentUser(), "No local user name value"));
+ break;
+ case LOCAL_HOST_MACRO: {
+ InetAddress address = Objects.requireNonNull(InetAddress.getLocalHost(), "No local address");
+ sb.append(ValidateUtils.checkNotNullAndNotEmpty(address.getHostName(), "No local name"));
+ break;
+ }
+ case REMOTE_HOST_MACRO:
+ sb.append(ValidateUtils.checkNotNullAndNotEmpty(host, "No remote host provided"));
+ break;
+ case REMOTE_USER_MACRO:
+ sb.append(ValidateUtils.checkNotNullAndNotEmpty(username, "No remote user provided"));
+ break;
+ case REMOTE_PORT_MACRO:
+ ValidateUtils.checkTrue(port > 0, "Bad remote port value: %d", port);
+ sb.append(port);
+ break;
+ default:
+ ValidateUtils.throwIllegalArgumentException("Bad modifier '%s' in %s", String.valueOf(ch), id);
+ }
+ } else {
+ sb.append(ch);
+ }
+ }
+ }
+
+ return sb.toString();
+ }
+
+ public static StringBuilder appendUserHome(StringBuilder sb) {
+ return appendUserHome(sb, IdentityUtils.getUserHomeFolder());
+ }
+
+ public static StringBuilder appendUserHome(StringBuilder sb, Path userHome) {
+ return appendUserHome(sb, Objects.requireNonNull(userHome, "No user home folder").toString());
+ }
+
+ public static StringBuilder appendUserHome(StringBuilder sb, String userHome) {
+ if (GenericUtils.isEmpty(userHome)) {
+ return sb;
+ }
+
+ sb.append(userHome);
+ // strip any ending separator since we add our own
+ int len = sb.length();
+ if (sb.charAt(len - 1) == File.separatorChar) {
+ sb.setLength(len - 1);
+ }
+
+ return sb;
+ }
+
+ /**
+ * @return The default {@link Path} location of the OpenSSH hosts entries configuration file
+ */
+ @SuppressWarnings("synthetic-access")
+ public static Path getDefaultHostConfigFile() {
+ return LazyDefaultConfigFileHolder.CONFIG_FILE;
+ }
+}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/10de190e/sshd-common/src/main/java/org/apache/sshd/client/config/hosts/HostConfigEntryResolver.java
----------------------------------------------------------------------
diff --git a/sshd-common/src/main/java/org/apache/sshd/client/config/hosts/HostConfigEntryResolver.java b/sshd-common/src/main/java/org/apache/sshd/client/config/hosts/HostConfigEntryResolver.java
new file mode 100644
index 0000000..a07cfcf
--- /dev/null
+++ b/sshd-common/src/main/java/org/apache/sshd/client/config/hosts/HostConfigEntryResolver.java
@@ -0,0 +1,60 @@
+/*
+ * 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;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+@FunctionalInterface
+public interface HostConfigEntryResolver {
+
+ /**
+ * An "empty" implementation that does not resolve any entry
+ */
+ HostConfigEntryResolver EMPTY = new HostConfigEntryResolver() {
+ @Override
+ public HostConfigEntry resolveEffectiveHost(String host, int port, String username) throws IOException {
+ return null;
+ }
+
+ @Override
+ public String toString() {
+ return "EMPTY";
+ }
+ };
+
+ /**
+ * Invoked when creating a new client session in order to allow for overriding
+ * of the original parameters
+ *
+ * @param host The requested host - never {@code null}/empty
+ * @param port The requested port
+ * @param username The requested username
+ * @return A {@link HostConfigEntry} for the actual target - {@code null} if use
+ * original parameters. <B>Note:</B> if any identity files are attached to the
+ * configuration then they must point to <U>existing</U> locations. This means
+ * that any macros such as <code>~, %d, %h</code>, etc. must be resolved <U>prior</U>
+ * to returning the value
+ * @throws IOException If failed to resolve the configuration
+ */
+ HostConfigEntry resolveEffectiveHost(String host, int port, String username) throws IOException;
+}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/10de190e/sshd-common/src/main/java/org/apache/sshd/client/config/hosts/HostPatternValue.java
----------------------------------------------------------------------
diff --git a/sshd-common/src/main/java/org/apache/sshd/client/config/hosts/HostPatternValue.java b/sshd-common/src/main/java/org/apache/sshd/client/config/hosts/HostPatternValue.java
new file mode 100644
index 0000000..20d682f
--- /dev/null
+++ b/sshd-common/src/main/java/org/apache/sshd/client/config/hosts/HostPatternValue.java
@@ -0,0 +1,97 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.sshd.client.config.hosts;
+
+import java.util.regex.Pattern;
+
+import org.apache.sshd.common.util.GenericUtils;
+
+/**
+ * Represents a pattern definition in the <U>known_hosts</U> file
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ * @see <A HREF="https://en.wikibooks.org/wiki/OpenSSH/Client_Configuration_Files#About_the_Contents_of_the_known_hosts_Files">
+ * OpenSSH cookbook - About the Contents of the known hosts Files</A>
+ */
+public class HostPatternValue {
+ private Pattern pattern;
+ private int port;
+ private boolean negated;
+
+ public HostPatternValue() {
+ super();
+ }
+
+ public HostPatternValue(Pattern pattern, boolean negated) {
+ this(pattern, 0, negated);
+ }
+
+ public HostPatternValue(Pattern pattern, int port, boolean negated) {
+ this.pattern = pattern;
+ this.port = port;
+ this.negated = negated;
+ }
+
+ public Pattern getPattern() {
+ return pattern;
+ }
+
+ public void setPattern(Pattern pattern) {
+ this.pattern = pattern;
+ }
+
+ public int getPort() {
+ return port;
+ }
+
+ public void setPort(int port) {
+ this.port = port;
+ }
+
+ public boolean isNegated() {
+ return negated;
+ }
+
+ public void setNegated(boolean negated) {
+ this.negated = negated;
+ }
+
+ @Override
+ public String toString() {
+ Pattern p = getPattern();
+ String purePattern = (p == null) ? null : p.pattern();
+ StringBuilder sb = new StringBuilder(GenericUtils.length(purePattern) + Short.SIZE);
+ if (isNegated()) {
+ sb.append(HostPatternsHolder.NEGATION_CHAR_PATTERN);
+ }
+
+ int portValue = getPort();
+ if (portValue > 0) {
+ sb.append(HostPatternsHolder.NON_STANDARD_PORT_PATTERN_ENCLOSURE_START_DELIM);
+ }
+ sb.append(purePattern);
+ if (portValue > 0) {
+ sb.append(HostPatternsHolder.NON_STANDARD_PORT_PATTERN_ENCLOSURE_END_DELIM);
+ sb.append(HostPatternsHolder.PORT_VALUE_DELIMITER);
+ sb.append(portValue);
+ }
+
+ return sb.toString();
+ }
+}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/10de190e/sshd-common/src/main/java/org/apache/sshd/client/config/hosts/HostPatternsHolder.java
----------------------------------------------------------------------
diff --git a/sshd-common/src/main/java/org/apache/sshd/client/config/hosts/HostPatternsHolder.java b/sshd-common/src/main/java/org/apache/sshd/client/config/hosts/HostPatternsHolder.java
new file mode 100644
index 0000000..9d90dac
--- /dev/null
+++ b/sshd-common/src/main/java/org/apache/sshd/client/config/hosts/HostPatternsHolder.java
@@ -0,0 +1,343 @@
+/*
+ * 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.Objects;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.apache.sshd.common.util.GenericUtils;
+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});
+
+ /** Port value separator if non-standard port pattern used */
+ public static final char PORT_VALUE_DELIMITER = ':';
+
+ /** Non-standard port specification host pattern enclosure start delimiter */
+ public static final char NON_STANDARD_PORT_PATTERN_ENCLOSURE_START_DELIM = '[';
+
+ /** Non-standard port specification host pattern enclosure end delimiter */
+ public static final char NON_STANDARD_PORT_PATTERN_ENCLOSURE_END_DELIM = ']';
+
+ private Collection<HostPatternValue> patterns = new LinkedList<>();
+
+ protected HostPatternsHolder() {
+ super();
+ }
+
+ public Collection<HostPatternValue> getPatterns() {
+ return patterns;
+ }
+
+ public void setPatterns(Collection<HostPatternValue> 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
+ * @param port The connection port
+ * @return {@code true} if the name / address matches the pattern(s)
+ * @see #isHostMatch(String, Pattern)
+ */
+ public boolean isHostMatch(String host, int port) {
+ return isHostMatch(host, port, 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, int)
+ */
+ 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, int)
+ */
+ 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, 0 /* any port */)) {
+ 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, int port, Collection<HostPatternValue> patterns) {
+ if (GenericUtils.isEmpty(patterns)) {
+ return false;
+ }
+
+ boolean matchFound = false;
+ for (HostPatternValue pv : patterns) {
+ boolean negated = pv.isNegated();
+ /*
+ * If already found a match we are interested only in negations
+ */
+ if (matchFound && (!negated)) {
+ continue;
+ }
+
+ if (!isHostMatch(host, pv.getPattern())) {
+ 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) {
+ return false;
+ }
+
+ int pvPort = pv.getPort();
+ if ((pvPort != 0) && (port != 0) && (pvPort != port)) {
+ continue;
+ }
+
+ 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<HostPatternValue> parsePatterns(CharSequence... patterns) {
+ return parsePatterns(GenericUtils.isEmpty(patterns) ? Collections.emptyList() : Arrays.asList(patterns));
+ }
+
+ public static List<HostPatternValue> parsePatterns(Collection<? extends CharSequence> patterns) {
+ if (GenericUtils.isEmpty(patterns)) {
+ return Collections.emptyList();
+ }
+
+ List<HostPatternValue> 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 patternString The original pattern string - ignored if {@code null}/empty
+ * @return The regular expression matcher {@link Pattern} and the indication
+ * whether it is a negating pattern or not - {@code null} if no original string
+ * @see #NON_STANDARD_PORT_PATTERN_ENCLOSURE_START_DELIM
+ * @see #NON_STANDARD_PORT_PATTERN_ENCLOSURE_END_DELIM
+ * @see #WILDCARD_PATTERN
+ * @see #SINGLE_CHAR_PATTERN
+ * @see #NEGATION_CHAR_PATTERN
+ */
+ public static HostPatternValue toPattern(CharSequence patternString) {
+ String pattern = GenericUtils.replaceWhitespaceAndTrim(Objects.toString(patternString, null));
+ if (GenericUtils.isEmpty(pattern)) {
+ return null;
+ }
+
+ int patternLen = pattern.length();
+ int port = 0;
+ // Check if non-standard port value used
+ StringBuilder sb = new StringBuilder(patternLen);
+ if (pattern.charAt(0) == HostPatternsHolder.NON_STANDARD_PORT_PATTERN_ENCLOSURE_START_DELIM) {
+ int pos = GenericUtils.lastIndexOf(pattern, HostPatternsHolder.PORT_VALUE_DELIMITER);
+ ValidateUtils.checkTrue(pos > 0, "Missing non-standard port value delimiter in %s", pattern);
+ ValidateUtils.checkTrue(pos < (patternLen - 1), "Missing non-standard port value number in %s", pattern);
+ ValidateUtils.checkTrue(pattern.charAt(pos - 1) == HostPatternsHolder.NON_STANDARD_PORT_PATTERN_ENCLOSURE_END_DELIM,
+ "Invalid non-standard port value host pattern enclosure delimiters in %s", pattern);
+
+ String csPort = pattern.substring(pos + 1, patternLen);
+ port = Integer.parseInt(csPort);
+ ValidateUtils.checkTrue((port > 0) && (port <= 0xFFFF), "Invalid non-start port value (%d) in %s", port, pattern);
+
+ pattern = pattern.substring(1, pos - 1);
+ patternLen = pattern.length();
+ }
+
+ boolean negated = false;
+ for (int curPos = 0; curPos < patternLen; 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 HostPatternValue(Pattern.compile(sb.toString(), Pattern.CASE_INSENSITIVE), port, 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;
+ }
+ return PATTERN_CHARS.indexOf(ch) >= 0;
+ }
+}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/10de190e/sshd-common/src/main/java/org/apache/sshd/client/config/hosts/KnownHostDigest.java
----------------------------------------------------------------------
diff --git a/sshd-common/src/main/java/org/apache/sshd/client/config/hosts/KnownHostDigest.java b/sshd-common/src/main/java/org/apache/sshd/client/config/hosts/KnownHostDigest.java
new file mode 100644
index 0000000..2d9a322
--- /dev/null
+++ b/sshd-common/src/main/java/org/apache/sshd/client/config/hosts/KnownHostDigest.java
@@ -0,0 +1,66 @@
+/*
+ * 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.Objects;
+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 = Objects.requireNonNull(factory, "No factory");
+ }
+
+ @Override
+ public String getName() {
+ return name;
+ }
+
+ @Override
+ public Mac create() {
+ return factory.create();
+ }
+
+ public static KnownHostDigest fromName(String name) {
+ return NamedResource.findByName(name, String.CASE_INSENSITIVE_ORDER, VALUES);
+ }
+}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/10de190e/sshd-common/src/main/java/org/apache/sshd/client/config/hosts/KnownHostEntry.java
----------------------------------------------------------------------
diff --git a/sshd-common/src/main/java/org/apache/sshd/client/config/hosts/KnownHostEntry.java b/sshd-common/src/main/java/org/apache/sshd/client/config/hosts/KnownHostEntry.java
new file mode 100644
index 0000000..bcaf965
--- /dev/null
+++ b/sshd-common/src/main/java/org/apache/sshd/client/config/hosts/KnownHostEntry.java
@@ -0,0 +1,276 @@
+/*
+ * 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.ConfigFileReaderSupport;
+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 LazyDefaultConfigFileHolder() {
+ throw new UnsupportedOperationException("No instance allowed");
+ }
+ }
+
+ 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, int port) {
+ if (super.isHostMatch(host, port)) {
+ 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(ConfigFileReaderSupport.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 = GenericUtils.replaceWhitespaceAndTrim(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;
+ }
+}