diff --git a/httpclient/src/main/java/org/apache/http/conn/ssl/AbstractVerifier.java b/httpclient/src/main/java/org/apache/http/conn/ssl/AbstractVerifier.java index 59526d6b2..1044e887f 100644 --- a/httpclient/src/main/java/org/apache/http/conn/ssl/AbstractVerifier.java +++ b/httpclient/src/main/java/org/apache/http/conn/ssl/AbstractVerifier.java @@ -59,6 +59,15 @@ public abstract class AbstractVerifier implements X509HostnameVerifier { private final Log log = LogFactory.getLog(getClass()); + final static String[] BAD_COUNTRY_2LDS = + { "ac", "co", "com", "ed", "edu", "go", "gouv", "gov", "info", + "lg", "ne", "net", "or", "org" }; + + static { + // Just in case developer forgot to manually sort the array. :-) + Arrays.sort(BAD_COUNTRY_2LDS); + } + @Override public final void verify(final String host, final SSLSocket ssl) throws IOException { @@ -179,7 +188,7 @@ public abstract class AbstractVerifier implements X509HostnameVerifier { if (parts.length != 3 || parts[2].length() != 2) { return true; // it's not an attempt to wildcard a 2TLD within a country code } - return Arrays.binarySearch(DefaultHostnameVerifier.BAD_COUNTRY_2LDS, parts[1]) < 0; + return Arrays.binarySearch(BAD_COUNTRY_2LDS, parts[1]) < 0; } public static String[] getCNs(final X509Certificate cert) { @@ -219,7 +228,13 @@ public abstract class AbstractVerifier implements X509HostnameVerifier { * @return number of dots */ public static int countDots(final String s) { - return DefaultHostnameVerifier.countDots(s); + int count = 0; + for(int i = 0; i < s.length(); i++) { + if(s.charAt(i) == '.') { + count++; + } + } + return count; } } diff --git a/httpclient/src/main/java/org/apache/http/conn/ssl/DefaultHostnameVerifier.java b/httpclient/src/main/java/org/apache/http/conn/ssl/DefaultHostnameVerifier.java index 4f9768372..7b11f87a9 100644 --- a/httpclient/src/main/java/org/apache/http/conn/ssl/DefaultHostnameVerifier.java +++ b/httpclient/src/main/java/org/apache/http/conn/ssl/DefaultHostnameVerifier.java @@ -33,11 +33,11 @@ import java.security.cert.Certificate; import java.security.cert.CertificateParsingException; import java.security.cert.X509Certificate; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collection; import java.util.List; -import java.util.Locale; import java.util.NoSuchElementException; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; import javax.naming.InvalidNameException; import javax.naming.NamingException; @@ -65,6 +65,9 @@ public final class DefaultHostnameVerifier implements HostnameVerifier { public static final DefaultHostnameVerifier INSTANCE = new DefaultHostnameVerifier(); + private final static Pattern WILDCARD_PATTERN = Pattern.compile( + "^[a-z0-9\\-\\*]+(\\.[a-z0-9\\-]+){2,}$", + Pattern.CASE_INSENSITIVE); /** * This contains a list of 2nd-level domains that aren't allowed to * have wildcards when combined with country-codes. @@ -75,14 +78,9 @@ public final class DefaultHostnameVerifier implements HostnameVerifier { * Looks like we're the only implementation guarding against this. * Firefox, Curl, Sun Java 1.4, 5, 6 don't bother with this check. */ - final static String[] BAD_COUNTRY_2LDS = - { "ac", "co", "com", "ed", "edu", "go", "gouv", "gov", "info", - "lg", "ne", "net", "or", "org" }; - - static { - // Just in case developer forgot to manually sort the array. :-) - Arrays.sort(BAD_COUNTRY_2LDS); - } + private final static Pattern BAD_COUNTRY_WILDCARD_PATTERN = Pattern.compile( + "^[a-z0-9\\-\\*]+\\.(ac|co|com|ed|edu|go|gouv|gov|info|lg|ne|net|or|org)\\.[a-z0-9\\-]{2}$", + Pattern.CASE_INSENSITIVE); final static int DNS_NAME_TYPE = 2; final static int IP_ADDRESS_TYPE = 7; @@ -177,29 +175,37 @@ public final class DefaultHostnameVerifier implements HostnameVerifier { if (host == null) { return false; } - final String normalizedHost = host.toLowerCase(Locale.ROOT); - final String normalizedIdentity = identity.toLowerCase(Locale.ROOT); // The CN better have at least two dots if it wants wildcard // action. It also can't be [*.co.uk] or [*.co.jp] or // [*.org.uk], etc... - final String parts[] = normalizedIdentity.split("\\."); - final boolean doWildcard = parts.length >= 3 && parts[0].endsWith("*") && - (!strict || validCountryWildcard(parts)); - if (doWildcard) { - boolean match; - final String firstpart = parts[0]; - if (firstpart.length() > 1) { // e.g. server* - final String prefix = firstpart.substring(0, firstpart.length() - 1); // e.g. server - final String suffix = normalizedIdentity.substring(firstpart.length()); // skip wildcard part from cn - final String hostSuffix = normalizedHost.substring(prefix.length()); // skip wildcard part from normalizedHost - match = normalizedHost.startsWith(prefix) && hostSuffix.endsWith(suffix); - } else { - match = normalizedHost.endsWith(normalizedIdentity.substring(1)); + if (identity.contains("*") && WILDCARD_PATTERN.matcher(identity).matches()) { + if (!strict || !BAD_COUNTRY_WILDCARD_PATTERN.matcher(identity).matches()) { + final StringBuilder buf = new StringBuilder(); + buf.append("^"); + for (int i = 0; i < identity.length(); i++) { + final char ch = identity.charAt(i); + if (ch == '.') { + buf.append("\\."); + } else if (ch == '*') { + if (strict) { + buf.append("[a-z0-9\\-]*"); + } else { + buf.append(".*"); + } + } else { + buf.append(ch); + } + } + buf.append("$"); + try { + final Pattern identityPattern = Pattern.compile(buf.toString(), Pattern.CASE_INSENSITIVE); + return identityPattern.matcher(host).matches(); + } catch (PatternSyntaxException ignore) { + // do simple match + } } - return match && (!strict || countDots(normalizedHost) == countDots(normalizedIdentity)); - } else { - return normalizedHost.equals(normalizedIdentity); } + return host.equalsIgnoreCase(identity); } static boolean matchIdentity(final String host, final String identity) { @@ -210,13 +216,6 @@ public final class DefaultHostnameVerifier implements HostnameVerifier { return matchIdentity(host, identity, true); } - static boolean validCountryWildcard(final String[] parts) { - if (parts.length != 3 || parts[2].length() != 2) { - return true; // it's not an attempt to wildcard a 2TLD within a country code - } - return Arrays.binarySearch(BAD_COUNTRY_2LDS, parts[1]) < 0; - } - static String extractCN(final String subjectPrincipal) throws SSLException { if (subjectPrincipal == null) { return null; @@ -268,21 +267,6 @@ public final class DefaultHostnameVerifier implements HostnameVerifier { return subjectAltList; } - /** - * Counts the number of dots "." in a string. - * @param s string to count dots from - * @return number of dots - */ - static int countDots(final String s) { - int count = 0; - for(int i = 0; i < s.length(); i++) { - if(s.charAt(i) == '.') { - count++; - } - } - return count; - } - /* * Normalize IPv6 or DNS name. */ diff --git a/httpclient/src/test/java/org/apache/http/conn/ssl/TestDefaultHostnameVerifier.java b/httpclient/src/test/java/org/apache/http/conn/ssl/TestDefaultHostnameVerifier.java index 1358fa5aa..848280f02 100644 --- a/httpclient/src/test/java/org/apache/http/conn/ssl/TestDefaultHostnameVerifier.java +++ b/httpclient/src/test/java/org/apache/http/conn/ssl/TestDefaultHostnameVerifier.java @@ -113,7 +113,7 @@ public class TestDefaultHostnameVerifier { x509 = (X509Certificate) cf.generateCertificate(in); exceptionPlease(impl, "foo.com", x509); impl.verify("www.foo.com", x509); - impl.verify("\u82b1\u5b50.foo.com", x509); + exceptionPlease(impl, "\u82b1\u5b50.foo.com", x509); exceptionPlease(impl, "a.b.foo.com", x509); in = new ByteArrayInputStream(CertificatesToPlayWith.X509_WILD_CO_JP); @@ -134,7 +134,7 @@ public class TestDefaultHostnameVerifier { // try the bar.com variations exceptionPlease(impl, "bar.com", x509); impl.verify("www.bar.com", x509); - impl.verify("\u82b1\u5b50.bar.com", x509); + exceptionPlease(impl, "\u82b1\u5b50.bar.com", x509); exceptionPlease(impl, "a.b.bar.com", x509); in = new ByteArrayInputStream(CertificatesToPlayWith.X509_MULTIPLE_VALUE_AVA); @@ -191,6 +191,13 @@ public class TestDefaultHostnameVerifier { Assert.assertTrue(DefaultHostnameVerifier.matchIdentity("a.b.c", "*.b.c")); Assert.assertTrue(DefaultHostnameVerifier.matchIdentityStrict("a.b.c", "*.b.c")); + Assert.assertTrue(DefaultHostnameVerifier.matchIdentity("a.B.c", "*.b.c")); + Assert.assertTrue(DefaultHostnameVerifier.matchIdentityStrict("a.B.c", "*.b.c")); + + Assert.assertTrue(DefaultHostnameVerifier.matchIdentity("a.b.C", "*.B.c")); + Assert.assertTrue(DefaultHostnameVerifier.matchIdentityStrict("a.b.C", "*.B.c")); + + Assert.assertTrue(DefaultHostnameVerifier.matchIdentity("s.a.b.c", "*.b.c")); Assert.assertFalse(DefaultHostnameVerifier.matchIdentityStrict("s.a.b.c", "*.b.c")); // subdomain not OK @@ -211,6 +218,12 @@ public class TestDefaultHostnameVerifier { Assert.assertFalse(DefaultHostnameVerifier.matchIdentity("s.a.gov.uk", "a*.gov.uk")); // Bad 2TLD Assert.assertFalse(DefaultHostnameVerifier.matchIdentityStrict("s.a.gov.uk", "a*.gov.uk")); // Bad 2TLD/no subdomain allowed + + Assert.assertFalse(DefaultHostnameVerifier.matchIdentity("a.b.c", "*.b.*")); + Assert.assertFalse(DefaultHostnameVerifier.matchIdentityStrict("a.b.c", "*.b.*")); + + Assert.assertFalse(DefaultHostnameVerifier.matchIdentity("a.b.c", "*.*.c")); + Assert.assertFalse(DefaultHostnameVerifier.matchIdentityStrict("a.b.c", "*.*.c")); } @Test diff --git a/httpclient/src/test/java/org/apache/http/conn/ssl/TestHostnameVerifier.java b/httpclient/src/test/java/org/apache/http/conn/ssl/TestHostnameVerifier.java index 657f8df93..9604a75f7 100644 --- a/httpclient/src/test/java/org/apache/http/conn/ssl/TestHostnameVerifier.java +++ b/httpclient/src/test/java/org/apache/http/conn/ssl/TestHostnameVerifier.java @@ -139,7 +139,7 @@ public class TestHostnameVerifier { DEFAULT.verify("www.foo.com", x509); STRICT.verify("www.foo.com", x509); DEFAULT.verify("\u82b1\u5b50.foo.com", x509); - STRICT.verify("\u82b1\u5b50.foo.com", x509); + exceptionPlease(STRICT, "\u82b1\u5b50.foo.com", x509); DEFAULT.verify("a.b.foo.com", x509); exceptionPlease(STRICT, "a.b.foo.com", x509); @@ -171,7 +171,7 @@ public class TestHostnameVerifier { DEFAULT.verify("www.bar.com", x509); STRICT.verify("www.bar.com", x509); DEFAULT.verify("\u82b1\u5b50.bar.com", x509); - STRICT.verify("\u82b1\u5b50.bar.com", x509); + exceptionPlease(STRICT, "\u82b1\u5b50.bar.com", x509); DEFAULT.verify("a.b.bar.com", x509); exceptionPlease(STRICT, "a.b.bar.com", x509);