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 2015/11/16 07:13:38 UTC

mina-sshd git commit: [SSHD-583] ssh_config Host entry should support '!' pattern

Repository: mina-sshd
Updated Branches:
  refs/heads/master ef317acff -> bc0ab58ba


[SSHD-583] ssh_config Host entry should support '!' pattern


Project: http://git-wip-us.apache.org/repos/asf/mina-sshd/repo
Commit: http://git-wip-us.apache.org/repos/asf/mina-sshd/commit/bc0ab58b
Tree: http://git-wip-us.apache.org/repos/asf/mina-sshd/tree/bc0ab58b
Diff: http://git-wip-us.apache.org/repos/asf/mina-sshd/diff/bc0ab58b

Branch: refs/heads/master
Commit: bc0ab58ba229c6733a5b94eac7d98a8f9521015e
Parents: ef317ac
Author: Lyor Goldstein <lg...@vmware.com>
Authored: Mon Nov 16 08:13:21 2015 +0200
Committer: Lyor Goldstein <lg...@vmware.com>
Committed: Mon Nov 16 08:13:21 2015 +0200

----------------------------------------------------------------------
 .../client/config/hosts/HostConfigEntry.java    | 237 +++++++++----------
 .../hosts/HostConfigEntryResolverTest.java      |  21 ++
 .../config/hosts/HostConfigEntryTest.java       |  72 +++---
 3 files changed, 172 insertions(+), 158 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/bc0ab58b/sshd-core/src/main/java/org/apache/sshd/client/config/hosts/HostConfigEntry.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/config/hosts/HostConfigEntry.java b/sshd-core/src/main/java/org/apache/sshd/client/config/hosts/HostConfigEntry.java
index 7485d4e..884aee5 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/config/hosts/HostConfigEntry.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/config/hosts/HostConfigEntry.java
@@ -54,6 +54,7 @@ import org.apache.sshd.common.config.SshConfigFileReader;
 import org.apache.sshd.common.config.keys.IdentityUtils;
 import org.apache.sshd.common.config.keys.PublicKeyEntry;
 import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.Pair;
 import org.apache.sshd.common.util.ValidateUtils;
 import org.apache.sshd.common.util.io.IoUtils;
 import org.apache.sshd.common.util.io.NoCloseInputStream;
@@ -66,7 +67,7 @@ import org.apache.sshd.common.util.io.NoCloseReader;
  * file format</A>
  * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
  */
-public class HostConfigEntry implements Cloneable, UsernameHolder {
+public class HostConfigEntry implements UsernameHolder {
 
     /**
      * Used in a host pattern to denote zero or more consecutive characters
@@ -85,9 +86,14 @@ public class HostConfigEntry implements Cloneable, UsernameHolder {
     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});
+    public static final String PATTERN_CHARS = new String(new char[]{WILDCARD_PATTERN, SINGLE_CHAR_PATTERN, NEGATION_CHAR_PATTERN});
 
     public static final String HOST_CONFIG_PROP = "Host";
     public static final String HOST_NAME_CONFIG_PROP = "HostName";
@@ -126,12 +132,12 @@ public class HostConfigEntry implements Cloneable, UsernameHolder {
         private static final Path KEYS_FILE = PublicKeyEntry.getDefaultKeysFolderPath().resolve(STD_CONFIG_FILENAME);
     }
 
-    private String hostValue;
-    private Pattern hostPattern;
+    private String host;
     private String hostName;
     private int port;
     private String username;
     private Boolean exclusiveIdentites;
+    private Collection<Pair<Pattern, Boolean>> patterns = new LinkedList<>();
     private Collection<String> identities = Collections.emptyList();
     private Map<String, String> properties = Collections.emptyMap();
 
@@ -147,35 +153,35 @@ public class HostConfigEntry implements Cloneable, UsernameHolder {
     }
 
     /**
-     * @return The host <U>pattern</U> represented by this entry
-     * @see <A HREF="http://www.gsp.com/cgi-bin/man.cgi?topic=ssh_config#3">Host Patterns</A>
+     * @return The <U>pattern(s)</U> represented by this entry
      */
     public String getHost() {
-        return hostValue;
+        return host;
     }
 
-    public Pattern getHostPattern() {
-        return hostPattern;
+    public void setHost(String host) {
+        this.host = host;
+        this.patterns = parsePatterns(parseConfigValue(host));
     }
 
-    /**
-     * @param host The host name/address/pattern represented by the entry
-     */
-    public void setHost(String host) {
-        hostValue = host;
-        hostPattern = toPattern(host);
+    public void setHost(Collection<String> patterns) {
+        this.host = GenericUtils.join(ValidateUtils.checkNotNullAndNotEmpty(patterns, "No patterns"), ',');
+        this.patterns = parsePatterns(patterns);
+    }
+
+    public Collection<Pair<Pattern, Boolean>> getPatterns() {
+        return patterns;
     }
 
     /**
-     * Checks if a given host name / address matches the entry's host pattern
+     * Checks if a given host name / address matches the entry's host pattern(s)
      *
      * @param host The host name / address - ignored if {@code null}/empty
-     * @return {@code true} if the name / address matches the pattern
-     * @see #getHostPattern()
+     * @return {@code true} if the name / address matches the pattern(s)
      * @see #isHostMatch(String, Pattern)
      */
     public boolean isHostMatch(String host) {
-        return isHostMatch(host, getHostPattern());
+        return isHostMatch(host, getPatterns());
     }
 
     /**
@@ -601,35 +607,6 @@ public class HostConfigEntry implements Cloneable, UsernameHolder {
     }
 
     @Override
-    public HostConfigEntry clone() {
-        try {
-            HostConfigEntry other = getClass().cast(super.clone());
-
-            // avoid shared instances
-            other.setIdentities(null);
-            Collection<String> ids = this.getIdentities();
-            if (GenericUtils.size(ids) > 0) {
-                for (String kp : ids) {
-                    other.addIdentity(kp);
-                }
-            }
-
-            // avoid shared instances
-            other.setProperties(null);
-            Map<String, String> props = this.getProperties();
-            if (GenericUtils.size(props) > 0) {
-                for (Map.Entry<String, String> pe : props.entrySet()) {
-                    other.setProperty(pe.getKey(), pe.getValue());
-                }
-            }
-
-            return other;
-        } catch (CloneNotSupportedException e) {
-            throw new RuntimeException("Failed (" + e.getClass().getSimpleName() + ") to clone " + toString() + ": " + e.getMessage(), e);
-        }
-    }
-
-    @Override
     public String toString() {
         return getHost() + ": " + getUsername() + "@" + getHostName() + ":" + getPort();
     }
@@ -769,14 +746,20 @@ public class HostConfigEntry implements Cloneable, UsernameHolder {
      */
     public static HostConfigEntry normalizeEntry(HostConfigEntry entry, String host, int port, String username) throws IOException {
         if (entry == null) {
-            return entry;
+            return null;
         }
 
-        HostConfigEntry normal = entry.clone();
+        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<String, String>(props));
+        }
+
         Collection<String> ids = entry.getIdentities();
         if (GenericUtils.isEmpty(ids)) {
             return normal;
@@ -882,6 +865,7 @@ public class HostConfigEntry implements Cloneable, UsernameHolder {
      * @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)) {
@@ -997,17 +981,39 @@ public class HostConfigEntry implements Cloneable, UsernameHolder {
         }
     }
 
-    /**
-     * 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 pattern string - ignored if {@code null}/empty
-     * @return {@code true} if the name / address matches the pattern
-     * @see #toPattern(String)
-     * @see #isHostMatch(String, Pattern)
-     */
-    public static boolean isHostMatch(String host, CharSequence pattern) {
-        return isHostMatch(host, toPattern(pattern));
+    public static boolean isHostMatch(String host, Collection<Pair<Pattern, Boolean>> patterns) {
+        if (GenericUtils.isEmpty(patterns)) {
+            return false;
+        }
+
+        boolean matchFound = false;
+        for (Pair<Pattern, Boolean> pp : patterns) {
+            Boolean negated = pp.getSecond();
+            /*
+             * If already found a match we are interested only in negations
+             */
+            if (matchFound && (!negated.booleanValue())) {
+                continue;
+            }
+
+            if (!isHostMatch(host, pp.getFirst())) {
+                continue;
+            }
+
+            /*
+             * According to https://www.freebsd.org/cgi/man.cgi?query=ssh_config&sektion=5:
+             *
+             *      If a negated entry is matched, then the Host entry is ignored,
+             *      regardless of whether any other patterns on the line match.
+             */
+            if (negated.booleanValue()) {
+                return false;
+            }
+
+            matchFound = true;
+        }
+
+        return matchFound;
     }
 
     /**
@@ -1026,22 +1032,37 @@ public class HostConfigEntry implements Cloneable, UsernameHolder {
         return m.matches();
     }
 
+    public static List<Pair<Pattern, Boolean>> parsePatterns(Collection<? extends CharSequence> patterns) {
+        if (GenericUtils.isEmpty(patterns)) {
+            return Collections.emptyList();
+        }
+
+        List<Pair<Pattern, Boolean>> result = new ArrayList<>(patterns.size());
+        for (CharSequence p : patterns) {
+            result.add(ValidateUtils.checkNotNull(toPattern(p), "No pattern for %s", p));
+        }
+
+        return result;
+    }
+
     /**
      * Converts a host pattern string to a regular expression matcher.
      * <B>Note:</B> pattern matching is <U>case insensitive</U>
      *
      * @param pattern The original pattern string - ignored if {@code null}/empty
-     * @return The regular expression matcher {@link Pattern} - {@code null}
-     * if empty original string
+     * @return The regular expression matcher {@link Pattern} and the indication
+     * whether it is a negating pattern or not - {@code null} if no original string
      * @see #WILDCARD_PATTERN
      * @see #SINGLE_CHAR_PATTERN
+     * @see #NEGATION_CHAR_PATTERN
      */
-    public static Pattern toPattern(CharSequence pattern) {
+    public static Pair<Pattern, Boolean> toPattern(CharSequence pattern) {
         if (GenericUtils.isEmpty(pattern)) {
             return null;
         }
 
         StringBuilder sb = new StringBuilder(pattern.length());
+        boolean negated = false;
         for (int curPos = 0; curPos < pattern.length(); curPos++) {
             char ch = pattern.charAt(curPos);
             ValidateUtils.checkTrue(isValidPatternChar(ch), "Invalid host pattern char in %s", pattern);
@@ -1056,12 +1077,17 @@ public class HostConfigEntry implements Cloneable, UsernameHolder {
                 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 Pattern.compile(sb.toString(), Pattern.CASE_INSENSITIVE);
+        return new Pair<Pattern, Boolean>(Pattern.compile(sb.toString(), Pattern.CASE_INSENSITIVE), Boolean.valueOf(negated));
     }
 
     /**
@@ -1097,7 +1123,7 @@ public class HostConfigEntry implements Cloneable, UsernameHolder {
         if ("-_.".indexOf(ch) >= 0) {
             return true;
         }
-        if ((ch == SINGLE_CHAR_PATTERN) || (ch == WILDCARD_PATTERN)) {
+        if (PATTERN_CHARS.indexOf(ch) >= 0) {
             return true;
         }
         return false;
@@ -1148,7 +1174,6 @@ public class HostConfigEntry implements Cloneable, UsernameHolder {
         HostConfigEntry curEntry = null;
         HostConfigEntry globalEntry = null;
         List<HostConfigEntry> entries = null;
-        List<String> multiHosts = Collections.emptyList();
 
         int lineNumber = 1;
         for (String line = rdr.readLine(); line != null; line = rdr.readLine(), lineNumber++) {
@@ -1200,21 +1225,14 @@ public class HostConfigEntry implements Cloneable, UsernameHolder {
                     curEntry.processGlobalValues(globalEntry);
                 }
 
-                entries = updateEntriesList(entries, curEntry, multiHosts);
+                entries = updateEntriesList(entries, curEntry);
 
                 curEntry = new HostConfigEntry();
-                curEntry.setHost(valsList.get(0));
-                // allow for multiple hosts patterns
-                if (valsList.size() > 1) {
-                    multiHosts = valsList.subList(1, valsList.size());
-                } else {
-                    multiHosts = Collections.emptyList();
-                }
+                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(ALL_HOSTS_PATTERN);
-                multiHosts = Collections.emptyList();
+                curEntry.setHost(Collections.singletonList(ALL_HOSTS_PATTERN));
                 globalEntry = curEntry;
             }
 
@@ -1231,7 +1249,7 @@ public class HostConfigEntry implements Cloneable, UsernameHolder {
             curEntry.processGlobalValues(globalEntry);
         }
 
-        entries = updateEntriesList(entries, curEntry, multiHosts);
+        entries = updateEntriesList(entries, curEntry);
         if (entries == null) {
             return Collections.emptyList();
         } else {
@@ -1239,6 +1257,18 @@ public class HostConfigEntry implements Cloneable, UsernameHolder {
         }
     }
 
+    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(ValidateUtils.checkNotNull(file, "No file").toPath(), entries, IoUtils.EMPTY_OPEN_OPTIONS);
     }
@@ -1272,57 +1302,6 @@ public class HostConfigEntry implements Cloneable, UsernameHolder {
     }
 
     /**
-     * @param entries The original list - if {@code null} one will be allocated if necessary
-     * @param curEntry The current entry - ignored if {@code null}
-     * @param hosts The extra host patterns to be attached to the entry - ignored
-     * if {@code null}/empty
-     * @return The updated list of entries - same as input if nothing added
-     * @see #duplicateConfiguration(HostConfigEntry, Collection)
-     */
-    public static List<HostConfigEntry> updateEntriesList(List<HostConfigEntry> entries, HostConfigEntry curEntry, Collection<String> hosts) {
-        if (curEntry == null) {
-            return entries;
-        }
-
-        // TODO consider allowing multiple patterns for same entry instead of duplicating the entries
-        Collection<HostConfigEntry> dups = duplicateConfiguration(curEntry, hosts);
-        int extraHosts = Math.max(0, GenericUtils.size(dups));
-        if (entries == null) {
-            entries = new ArrayList<>(Math.max(1 + extraHosts, Byte.SIZE));
-        }
-
-        entries.add(curEntry);
-
-        if (extraHosts > 0) {
-            entries.addAll(dups);
-        }
-
-        return entries;
-    }
-
-    /**
-     * @param entry The original entry to duplicate - ignored if {@code null}
-     * @param hosts The hosts patterns to attach to each duplicated entry -
-     * ignored if {@code null}/empty
-     * @return A {@link List} of duplicated entries where each new entry host
-     * pattern is updated from the given one
-     */
-    public static List<HostConfigEntry> duplicateConfiguration(HostConfigEntry entry, Collection<String> hosts) {
-        if ((entry == null) || GenericUtils.isEmpty(hosts)) {
-            return Collections.emptyList();
-        }
-
-        List<HostConfigEntry> dups = new ArrayList<>(hosts.size());
-        for (String pattern : hosts) {
-            HostConfigEntry cloned = entry.clone();
-            cloned.setHost(pattern);
-            dups.add(cloned);
-        }
-
-        return dups;
-    }
-
-    /**
      * Checks if this is a multi-value - allow space and comma
      *
      * @param value The value - ignored if {@code null}/empty (after trimming)

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/bc0ab58b/sshd-core/src/test/java/org/apache/sshd/client/config/hosts/HostConfigEntryResolverTest.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/test/java/org/apache/sshd/client/config/hosts/HostConfigEntryResolverTest.java b/sshd-core/src/test/java/org/apache/sshd/client/config/hosts/HostConfigEntryResolverTest.java
index 7680b46..b0c9029 100644
--- a/sshd-core/src/test/java/org/apache/sshd/client/config/hosts/HostConfigEntryResolverTest.java
+++ b/sshd-core/src/test/java/org/apache/sshd/client/config/hosts/HostConfigEntryResolverTest.java
@@ -27,6 +27,7 @@ import java.nio.file.Path;
 import java.security.GeneralSecurityException;
 import java.security.KeyPair;
 import java.security.PublicKey;
+import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.concurrent.TimeUnit;
@@ -110,6 +111,26 @@ public class HostConfigEntryResolverTest extends BaseTestSupport {
     }
 
     @Test
+    public void testNegatedHostEntriesResolution() throws Exception {
+        HostConfigEntry positiveEntry = new HostConfigEntry(TEST_LOCALHOST, TEST_LOCALHOST, port, getCurrentTestName());
+        HostConfigEntry negativeEntry = new HostConfigEntry(
+                String.valueOf(HostConfigEntry.NEGATION_CHAR_PATTERN) + positiveEntry.getHost(),
+                positiveEntry.getHostName(),
+                getMovedPortNumber(positiveEntry.getPort()),
+                getClass().getPackage().getName());
+        client.setHostConfigEntryResolver(HostConfigEntry.toHostConfigEntryResolver(Arrays.asList(negativeEntry, positiveEntry)));
+        client.start();
+
+        try(ClientSession session = client.connect(negativeEntry.getUsername(), negativeEntry.getHostName(), negativeEntry.getPort()).verify(7L, TimeUnit.SECONDS).getSession()) {
+            session.addPasswordIdentity(getCurrentTestName());
+            session.auth().verify(5L, TimeUnit.SECONDS);
+            assertEffectiveRemoteAddress(session, positiveEntry);
+        } finally {
+            client.stop();
+        }
+    }
+
+    @Test
     public void testPreloadedIdentities() throws Exception {
         final KeyPair identity = Utils.getFirstKeyPair(sshd);
         final String USER = getCurrentTestName();

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/bc0ab58b/sshd-core/src/test/java/org/apache/sshd/client/config/hosts/HostConfigEntryTest.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/test/java/org/apache/sshd/client/config/hosts/HostConfigEntryTest.java b/sshd-core/src/test/java/org/apache/sshd/client/config/hosts/HostConfigEntryTest.java
index 394871b..fe43b62 100644
--- a/sshd-core/src/test/java/org/apache/sshd/client/config/hosts/HostConfigEntryTest.java
+++ b/sshd-core/src/test/java/org/apache/sshd/client/config/hosts/HostConfigEntryTest.java
@@ -25,10 +25,10 @@ import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
-import java.util.Map;
 import java.util.regex.Pattern;
 
 import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.Pair;
 import org.apache.sshd.util.test.BaseTestSupport;
 import org.junit.FixMethodOrder;
 import org.junit.Test;
@@ -44,6 +44,38 @@ public class HostConfigEntryTest extends BaseTestSupport {
     }
 
     @Test
+    public void testNegatingPatternOverridesAll() {
+        String testHost = "37.77.34.7";
+        String[] elements = GenericUtils.split(testHost, '.');
+        StringBuilder sb = new StringBuilder(testHost.length() + Byte.SIZE);
+        List<Pair<Pattern, Boolean>> patterns = new ArrayList<>(elements.length + 1);
+        // all wildcard patterns are not negated - only the actual host
+        patterns.add(HostConfigEntry.toPattern(String.valueOf(HostConfigEntry.NEGATION_CHAR_PATTERN) + testHost));
+
+        for (int i = 0; i < elements.length; i++) {
+            sb.setLength(0);
+
+            for (int j = 0; j < elements.length; j++) {
+                if (j > 0) {
+                    sb.append('.');
+                }
+                if (i == j) {
+                    sb.append(HostConfigEntry.WILDCARD_PATTERN);
+                } else {
+                    sb.append(elements[j]);
+                }
+            }
+
+            patterns.add(HostConfigEntry.toPattern(sb));
+        }
+
+        for (int index = 0; index < patterns.size(); index++) {
+            assertFalse("Unexpected match for " + patterns, HostConfigEntry.isHostMatch(testHost, patterns));
+            Collections.shuffle(patterns);
+        }
+    }
+
+    @Test
     public void testHostWildcardPatternMatching() {
         String pkgName = getClass().getPackage().getName();
         String[] elements = GenericUtils.split(pkgName, '.');
@@ -53,7 +85,8 @@ public class HostConfigEntryTest extends BaseTestSupport {
         }
 
         String value = sb.toString();
-        Pattern pattern = HostConfigEntry.toPattern(value);
+        Pair<Pattern, Boolean> pp = HostConfigEntry.toPattern(value);
+        Pattern pattern = pp.getFirst();
         String domain = value.substring(1); // chomp the wildcard prefix
         for (String host : new String[] {
                 getClass().getSimpleName(),
@@ -73,7 +106,7 @@ public class HostConfigEntryTest extends BaseTestSupport {
         StringBuilder sb = new StringBuilder().append("10.0.0.");
         int sbLen = sb.length();
 
-        Pattern pattern = HostConfigEntry.toPattern(sb.append(HostConfigEntry.WILDCARD_PATTERN));
+        Pattern pattern = HostConfigEntry.toPattern(sb.append(HostConfigEntry.WILDCARD_PATTERN)).getFirst();
         for (int v = 0; v <= 255; v++) {
             sb.setLength(sbLen);    // start from scratch
             sb.append(v);
@@ -90,7 +123,7 @@ public class HostConfigEntryTest extends BaseTestSupport {
         for (boolean restoreOriginal : new boolean[] { true, false }) {
             for (int index = 0; index < value.length(); index++) {
                 sb.setCharAt(index, HostConfigEntry.SINGLE_CHAR_PATTERN);
-                testCaseInsensitivePatternMatching(value, HostConfigEntry.toPattern(sb.toString()), true);
+                testCaseInsensitivePatternMatching(value, HostConfigEntry.toPattern(sb.toString()).getFirst(), true);
                 if (restoreOriginal) {
                     sb.setCharAt(index, value.charAt(index));
                 }
@@ -112,9 +145,10 @@ public class HostConfigEntryTest extends BaseTestSupport {
             for (int index = sbLen; index < sb.length(); index++) {
                 sb.setCharAt(index, HostConfigEntry.SINGLE_CHAR_PATTERN);
             }
-            String pattern = sb.toString();
 
-            assertTrue("No match for " + address + " on pattern=" + pattern, HostConfigEntry.isHostMatch(address, pattern));
+            String pattern = sb.toString();
+            Pair<Pattern, Boolean> pp = HostConfigEntry.toPattern(pattern);
+            assertTrue("No match for " + address + " on pattern=" + pattern, HostConfigEntry.isHostMatch(address, Collections.singletonList(pp)));
         }
     }
 
@@ -141,7 +175,7 @@ public class HostConfigEntryTest extends BaseTestSupport {
         }
 
         for (char ch : new char[] {
-                    '(', ')', '{', '}', '[', ']', '!', '@',
+                    '(', ')', '{', '}', '[', ']', '@',
                     '#', '$', '^', '&', '%', '~', '<', '>',
                     ',', '/', '\\', '\'', '"', ':', ';' }) {
             assertFalse("Unexpected valid character: " + String.valueOf(ch), HostConfigEntry.isValidPatternChar(ch));
@@ -201,28 +235,8 @@ public class HostConfigEntryTest extends BaseTestSupport {
     @Test
     public void testReadMultipleHostPatterns() throws IOException {
         List<HostConfigEntry> entries = validateHostConfigEntries(readHostConfigEntries());
-        assertEquals("Mismatched number of duplicates", 3, GenericUtils.size(entries));
-
-        HostConfigEntry prototype = entries.get(0);
-        String protoIds = GenericUtils.join(prototype.getIdentities(), ',');
-        Collection<Map.Entry<String,String>> protoProps = prototype.getProperties().entrySet();
-        for (int index = 1; index < entries.size(); index++) {
-            HostConfigEntry entry = entries.get(index);
-            assertEquals("Mismatched host name for " + entry, prototype.getHostName(), entry.getHostName());
-            assertEquals("Mismatched port for " + entry, prototype.getPort(), entry.getPort());
-            assertSame("Mismatched user for " + entry, prototype.getUsername(), entry.getUsername());
-            assertEquals("Mismatched identities for " + entry, protoIds, GenericUtils.join(entry.getIdentities(), ','));
-
-            Map<String,String> entryProps = entry.getProperties();
-            assertEquals("Mismatched properties count for " + entry, GenericUtils.size(protoProps), GenericUtils.size(entryProps));
-
-            for (Map.Entry<String,String> ppe : protoProps) {
-                String key = ppe.getKey();
-                String expected = ppe.getValue();
-                String actual = entryProps.get(key);
-                assertEquals("Mismatched value for " + key + " property of " + entry, expected, actual);
-            }
-        }
+        assertEquals("Mismatched number of entries", 1, GenericUtils.size(entries));
+        assertEquals("Mismatched number of patterns", 3, GenericUtils.size(entries.get(0).getPatterns()));
     }
 
     @Test