Repository: cxf Updated Branches: refs/heads/3.0.x-fixes 5c8e6c86e -> 6cdfe4bab
Updating HostnameVerifier as per recent changes in httpclient Project: http://git-wip-us.apache.org/repos/asf/cxf/repo Commit: http://git-wip-us.apache.org/repos/asf/cxf/commit/99276baf Tree: http://git-wip-us.apache.org/repos/asf/cxf/tree/99276baf Diff: http://git-wip-us.apache.org/repos/asf/cxf/diff/99276baf Branch: refs/heads/3.0.x-fixes Commit: 99276baf0a2e6f8aaa08586d21ed905c5cce574e Parents: 5c8e6c8 Author: Colm O hEigeartaigh <cohei...@apache.org> Authored: Fri Jan 8 16:48:43 2016 +0000 Committer: Colm O hEigeartaigh <cohei...@apache.org> Committed: Fri Jan 8 16:50:15 2016 +0000 ---------------------------------------------------------------------- .../httpclient/DefaultHostnameVerifier.java | 71 ++++++------- .../transport/https/httpclient/DomainType.java | 37 +++++++ .../https/httpclient/PublicSuffixList.java | 11 +- .../httpclient/PublicSuffixListParser.java | 105 ++++++++++++++----- .../https/httpclient/PublicSuffixMatcher.java | 99 ++++++++++++++--- .../httpclient/DefaultHostnameVerifierTest.java | 14 ++- 6 files changed, 254 insertions(+), 83 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/cxf/blob/99276baf/rt/transports/http/src/main/java/org/apache/cxf/transport/https/httpclient/DefaultHostnameVerifier.java ---------------------------------------------------------------------- diff --git a/rt/transports/http/src/main/java/org/apache/cxf/transport/https/httpclient/DefaultHostnameVerifier.java b/rt/transports/http/src/main/java/org/apache/cxf/transport/https/httpclient/DefaultHostnameVerifier.java index 8fb067f..5d3287c 100644 --- a/rt/transports/http/src/main/java/org/apache/cxf/transport/https/httpclient/DefaultHostnameVerifier.java +++ b/rt/transports/http/src/main/java/org/apache/cxf/transport/https/httpclient/DefaultHostnameVerifier.java @@ -58,6 +58,8 @@ import org.apache.cxf.common.logging.LogUtils; */ public final class DefaultHostnameVerifier implements HostnameVerifier { + enum TYPE { IPv4, IPv6, DNS }; + static final int DNS_NAME_TYPE = 2; static final int IP_ADDRESS_TYPE = 7; @@ -90,16 +92,29 @@ public final class DefaultHostnameVerifier implements HostnameVerifier { public void verify( final String host, final X509Certificate cert) throws SSLException { - final boolean ipv4 = InetAddressUtils.isIPv4Address(host); - final boolean ipv6 = InetAddressUtils.isIPv6Address(host); - final int subjectType = ipv4 || ipv6 ? IP_ADDRESS_TYPE : DNS_NAME_TYPE; + TYPE hostFormat = TYPE.DNS; + if (InetAddressUtils.isIPv4Address(host)) { + hostFormat = TYPE.IPv4; + } else { + String s = host; + if (s.startsWith("[") && s.endsWith("]")) { + s = host.substring(1, host.length() - 1); + } + if (InetAddressUtils.isIPv6Address(s)) { + hostFormat = TYPE.IPv6; + } + } + final int subjectType = hostFormat == TYPE.IPv4 || hostFormat == TYPE.IPv6 ? IP_ADDRESS_TYPE : DNS_NAME_TYPE; final List<String> subjectAlts = extractSubjectAlts(cert, subjectType); if (subjectAlts != null && !subjectAlts.isEmpty()) { - if (ipv4) { + switch (hostFormat) { + case IPv4: matchIPAddress(host, subjectAlts); - } else if (ipv6) { + break; + case IPv6: matchIPv6Address(host, subjectAlts); - } else { + break; + default: matchDNSName(host, subjectAlts, this.publicSuffixMatcher); } } else { @@ -108,7 +123,7 @@ public final class DefaultHostnameVerifier implements HostnameVerifier { final X500Principal subjectPrincipal = cert.getSubjectX500Principal(); final String cn = extractCN(subjectPrincipal.getName(X500Principal.RFC2253)); if (cn == null) { - throw new SSLException("Certificate subject for <" + host + "> doesn't contain " + throw new SSLException("Certificate subject for <" + host + "> doesn't contain " + "a common name and does not have alternative names"); } matchCN(host, cn, this.publicSuffixMatcher); @@ -160,35 +175,23 @@ public final class DefaultHostnameVerifier implements HostnameVerifier { + "common name of the certificate subject: " + cn); } } + + static boolean matchDomainRoot(final String host, final String domainRoot) { + if (domainRoot == null) { + return false; + } + return host.endsWith(domainRoot) && (host.length() == domainRoot.length() + || host.charAt(host.length() - domainRoot.length() - 1) == '.'); + } private static boolean matchIdentity(final String host, final String identity, final PublicSuffixMatcher publicSuffixMatcher, final boolean strict) { - if (host == null) { + if (publicSuffixMatcher != null && host.contains(".") + && !matchDomainRoot(host, publicSuffixMatcher.getDomainRoot(identity, DomainType.ICANN))) { return false; } - if (publicSuffixMatcher != null && host.contains(".")) { - String domainRoot = publicSuffixMatcher.getDomainRoot(identity); - if (domainRoot == null) { - // Public domain - return false; - } - domainRoot = "." + domainRoot; - if (!host.endsWith(domainRoot)) { - // Domain root mismatch - return false; - } - if (strict && countDots(identity) != countDots(domainRoot)) { - return false; - } - } - - return matchServerIdentity(host, identity, strict); - } - - private static boolean matchServerIdentity(final String host, final String identity, - boolean strict) { // RFC 2818, 3.1. Server Identity // "...Names may contain the wildcard // character * which is considered to match any single domain name @@ -217,16 +220,6 @@ public final class DefaultHostnameVerifier implements HostnameVerifier { return host.equalsIgnoreCase(identity); } - static int countDots(final String s) { - int count = 0; - for (int i = 0; i < s.length(); i++) { - if (s.charAt(i) == '.') { - count++; - } - } - return count; - } - static boolean matchIdentity(final String host, final String identity, final PublicSuffixMatcher publicSuffixMatcher) { return matchIdentity(host, identity, publicSuffixMatcher, false); http://git-wip-us.apache.org/repos/asf/cxf/blob/99276baf/rt/transports/http/src/main/java/org/apache/cxf/transport/https/httpclient/DomainType.java ---------------------------------------------------------------------- diff --git a/rt/transports/http/src/main/java/org/apache/cxf/transport/https/httpclient/DomainType.java b/rt/transports/http/src/main/java/org/apache/cxf/transport/https/httpclient/DomainType.java new file mode 100644 index 0000000..41ec355 --- /dev/null +++ b/rt/transports/http/src/main/java/org/apache/cxf/transport/https/httpclient/DomainType.java @@ -0,0 +1,37 @@ +/** + * 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. + */ +/* + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * <http://www.apache.org/>. + * + */ +package org.apache.cxf.transport.https.httpclient; + +/** + * Domain types differentiated by Mozilla Public Suffix List. + * + * Copied from httpclient + */ +public enum DomainType { + + UNKNOWN, ICANN, PRIVATE + +} http://git-wip-us.apache.org/repos/asf/cxf/blob/99276baf/rt/transports/http/src/main/java/org/apache/cxf/transport/https/httpclient/PublicSuffixList.java ---------------------------------------------------------------------- diff --git a/rt/transports/http/src/main/java/org/apache/cxf/transport/https/httpclient/PublicSuffixList.java b/rt/transports/http/src/main/java/org/apache/cxf/transport/https/httpclient/PublicSuffixList.java index b714a46..9bb9536 100644 --- a/rt/transports/http/src/main/java/org/apache/cxf/transport/https/httpclient/PublicSuffixList.java +++ b/rt/transports/http/src/main/java/org/apache/cxf/transport/https/httpclient/PublicSuffixList.java @@ -39,19 +39,28 @@ import java.util.List; */ public final class PublicSuffixList { + private final DomainType type; private final List<String> rules; private final List<String> exceptions; - public PublicSuffixList(final List<String> rules, final List<String> exceptions) { + public PublicSuffixList(final DomainType type, final List<String> rules, final List<String> exceptions) { + if (type == null) { + throw new IllegalArgumentException("Domain type is null"); + } if (rules == null) { throw new IllegalArgumentException("Domain suffix rules are null"); } if (exceptions == null) { throw new IllegalArgumentException("Domain suffix exceptions are null"); } + this.type = type; this.rules = Collections.unmodifiableList(rules); this.exceptions = Collections.unmodifiableList(exceptions); } + + public DomainType getType() { + return type; + } public List<String> getRules() { return rules; http://git-wip-us.apache.org/repos/asf/cxf/blob/99276baf/rt/transports/http/src/main/java/org/apache/cxf/transport/https/httpclient/PublicSuffixListParser.java ---------------------------------------------------------------------- diff --git a/rt/transports/http/src/main/java/org/apache/cxf/transport/https/httpclient/PublicSuffixListParser.java b/rt/transports/http/src/main/java/org/apache/cxf/transport/https/httpclient/PublicSuffixListParser.java index f4b61d7..5c4df13 100644 --- a/rt/transports/http/src/main/java/org/apache/cxf/transport/https/httpclient/PublicSuffixListParser.java +++ b/rt/transports/http/src/main/java/org/apache/cxf/transport/https/httpclient/PublicSuffixListParser.java @@ -39,27 +39,25 @@ import java.util.List; */ public final class PublicSuffixListParser { - private static final int MAX_LINE_LEN = 256; - public PublicSuffixListParser() { } /** - * Parses the public suffix list format. When creating the reader from the file, make sure to - * use the correct encoding (the original list is in UTF-8). + * Parses the public suffix list format. + * <p> + * When creating the reader from the file, make sure to use the correct encoding + * (the original list is in UTF-8). * * @param reader the data reader. The caller is responsible for closing the reader. * @throws java.io.IOException on error while reading from list */ public PublicSuffixList parse(final Reader reader) throws IOException { - final List<String> rules = new ArrayList<String>(); - final List<String> exceptions = new ArrayList<String>(); + final List<String> rules = new ArrayList<>(); + final List<String> exceptions = new ArrayList<>(); final BufferedReader r = new BufferedReader(reader); - final StringBuilder sb = new StringBuilder(256); - boolean more = true; - while (more) { - more = readLine(r, sb); - String line = sb.toString(); + + String line; + while ((line = r.readLine()) != null) { if (line.isEmpty()) { continue; } @@ -81,30 +79,79 @@ public final class PublicSuffixListParser { rules.add(line); } } - return new PublicSuffixList(rules, exceptions); + return new PublicSuffixList(DomainType.UNKNOWN, rules, exceptions); } - private boolean readLine(final Reader r, final StringBuilder sb) throws IOException { - sb.setLength(0); - int b; - boolean hitWhitespace = false; - while ((b = r.read()) != -1) { - final char c = (char) b; - if (c == '\n') { - break; + /** + * Parses the public suffix list format by domain type (currently supported ICANN and PRIVATE). + * <p> + * When creating the reader from the file, make sure to use the correct encoding + * (the original list is in UTF-8). + * + * @param reader the data reader. The caller is responsible for closing the reader. + * @throws java.io.IOException on error while reading from list + * + * @since 4.5 + */ + public List<PublicSuffixList> parseByType(final Reader reader) throws IOException { + final List<PublicSuffixList> result = new ArrayList<>(2); + + final BufferedReader r = new BufferedReader(reader); + + DomainType domainType = null; + List<String> rules = null; + List<String> exceptions = null; + String line; + while ((line = r.readLine()) != null) { + if (line.isEmpty()) { + continue; } - // Each line is only read up to the first whitespace - if (Character.isWhitespace(c)) { - hitWhitespace = true; + if (line.startsWith("//")) { + + if (domainType == null) { + if (line.contains("===BEGIN ICANN DOMAINS===")) { + domainType = DomainType.ICANN; + } else if (line.contains("===BEGIN PRIVATE DOMAINS===")) { + domainType = DomainType.PRIVATE; + } + } else { + if (line.contains("===END ICANN DOMAINS===") || line.contains("===END PRIVATE DOMAINS===")) { + if (rules != null) { + result.add(new PublicSuffixList(domainType, rules, exceptions)); + } + domainType = null; + rules = null; + exceptions = null; + } + } + + continue; //entire lines can also be commented using // } - if (!hitWhitespace) { - sb.append(c); + if (domainType == null) { + continue; + } + + if (line.startsWith(".")) { + line = line.substring(1); // A leading dot is optional + } + // An exclamation mark (!) at the start of a rule marks an exception to a previous wildcard rule + final boolean isException = line.startsWith("!"); + if (isException) { + line = line.substring(1); } - if (sb.length() > MAX_LINE_LEN) { - return false; // prevent excess memory usage + + if (isException) { + if (exceptions == null) { + exceptions = new ArrayList<>(); + } + exceptions.add(line); + } else { + if (rules == null) { + rules = new ArrayList<>(); + } + rules.add(line); } } - return b != -1; + return result; } - } http://git-wip-us.apache.org/repos/asf/cxf/blob/99276baf/rt/transports/http/src/main/java/org/apache/cxf/transport/https/httpclient/PublicSuffixMatcher.java ---------------------------------------------------------------------- diff --git a/rt/transports/http/src/main/java/org/apache/cxf/transport/https/httpclient/PublicSuffixMatcher.java b/rt/transports/http/src/main/java/org/apache/cxf/transport/https/httpclient/PublicSuffixMatcher.java index fa2318f..b50b83f 100644 --- a/rt/transports/http/src/main/java/org/apache/cxf/transport/https/httpclient/PublicSuffixMatcher.java +++ b/rt/transports/http/src/main/java/org/apache/cxf/transport/https/httpclient/PublicSuffixMatcher.java @@ -27,6 +27,7 @@ package org.apache.cxf.transport.https.httpclient; import java.net.IDN; import java.util.Collection; +import java.util.List; import java.util.Locale; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -41,35 +42,94 @@ import java.util.concurrent.ConcurrentHashMap; */ public final class PublicSuffixMatcher { - private final Map<String, String> rules; - private final Map<String, String> exceptions; + private final Map<String, DomainType> rules; + private final Map<String, DomainType> exceptions; public PublicSuffixMatcher(final Collection<String> rules, final Collection<String> exceptions) { + this(DomainType.UNKNOWN, rules, exceptions); + } + + public PublicSuffixMatcher(final DomainType domainType, + final Collection<String> rules, final Collection<String> exceptions) { + if (domainType == null) { + throw new IllegalArgumentException("Domain type is null"); + } if (rules == null) { throw new IllegalArgumentException("Domain suffix rules are null"); } - this.rules = new ConcurrentHashMap<String, String>(rules.size()); + this.rules = new ConcurrentHashMap<String, DomainType>(rules.size()); for (String rule: rules) { - this.rules.put(rule, rule); + this.rules.put(rule, domainType); } + this.exceptions = new ConcurrentHashMap<String, DomainType>(); if (exceptions != null) { - this.exceptions = new ConcurrentHashMap<String, String>(exceptions.size()); for (String exception: exceptions) { - this.exceptions.put(exception, exception); + this.exceptions.put(exception, domainType); } + } + } + + public PublicSuffixMatcher(final Collection<PublicSuffixList> lists) { + if (lists == null) { + throw new IllegalArgumentException("Domain suffix lists are null"); + } + this.rules = new ConcurrentHashMap<String, DomainType>(); + this.exceptions = new ConcurrentHashMap<String, DomainType>(); + for (PublicSuffixList list: lists) { + final DomainType domainType = list.getType(); + for (String rule: list.getRules()) { + this.rules.put(rule, domainType); + } + final List<String> listExceptions = list.getExceptions(); + if (listExceptions != null) { + for (String exception: listExceptions) { + this.exceptions.put(exception, domainType); + } + } + } + } + + private static boolean hasEntry(final Map<String, DomainType> map, final String rule, + final DomainType expectedType) { + if (map == null) { + return false; + } + final DomainType domainType = map.get(rule); + if (domainType == null) { + return false; } else { - this.exceptions = null; + return expectedType == null || domainType.equals(expectedType); } } + private boolean hasRule(final String rule, final DomainType expectedType) { + return hasEntry(this.rules, rule, expectedType); + } + + private boolean hasException(final String exception, final DomainType expectedType) { + return hasEntry(this.exceptions, exception, expectedType); + } + /** - * Returns registrable part of the domain for the given domain name of {@code null} + * Returns registrable part of the domain for the given domain name or {@code null} * if given domain represents a public suffix. * * @param domain * @return domain root */ public String getDomainRoot(final String domain) { + return getDomainRoot(domain, null); + } + + /** + * Returns registrable part of the domain for the given domain name or {@code null} + * if given domain represents a public suffix. + * + * @param domain + * @param expectedType expected domain type or {@code null} if any. + * @return domain root + */ + public String getDomainRoot(final String domain, final DomainType expectedType) { if (domain == null) { return null; } @@ -81,11 +141,11 @@ public final class PublicSuffixMatcher { while (segment != null) { // An exception rule takes priority over any other matching rule. - if (this.exceptions != null && this.exceptions.containsKey(IDN.toUnicode(segment))) { + if (hasException(IDN.toUnicode(segment), expectedType)) { return segment; } - if (this.rules.containsKey(IDN.toUnicode(segment))) { + if (hasRule(IDN.toUnicode(segment), expectedType)) { break; } @@ -93,7 +153,7 @@ public final class PublicSuffixMatcher { final String nextSegment = nextdot != -1 ? segment.substring(nextdot + 1) : null; if (nextSegment != null - && this.rules.containsKey("*." + IDN.toUnicode(nextSegment))) { + && hasRule("*." + IDN.toUnicode(nextSegment), expectedType)) { break; } if (nextdot != -1) { @@ -104,11 +164,26 @@ public final class PublicSuffixMatcher { return domainName; } + /** + * Tests whether the given domain matches any of entry from the public suffix list. + */ public boolean matches(final String domain) { + return matches(domain, null); + } + + /** + * Tests whether the given domain matches any of entry from the public suffix list. + * + * @param domain + * @param expectedType expected domain type or {@code null} if any. + * @return {@code true} if the given domain matches any of the public suffixes. + */ + public boolean matches(final String domain, final DomainType expectedType) { if (domain == null) { return false; } - final String domainRoot = getDomainRoot(domain.startsWith(".") ? domain.substring(1) : domain); + final String domainRoot = getDomainRoot( + domain.startsWith(".") ? domain.substring(1) : domain, expectedType); return domainRoot == null; } http://git-wip-us.apache.org/repos/asf/cxf/blob/99276baf/rt/transports/http/src/test/java/org/apache/cxf/transport/https/httpclient/DefaultHostnameVerifierTest.java ---------------------------------------------------------------------- diff --git a/rt/transports/http/src/test/java/org/apache/cxf/transport/https/httpclient/DefaultHostnameVerifierTest.java b/rt/transports/http/src/test/java/org/apache/cxf/transport/https/httpclient/DefaultHostnameVerifierTest.java index da284e8..3ec14d1 100644 --- a/rt/transports/http/src/test/java/org/apache/cxf/transport/https/httpclient/DefaultHostnameVerifierTest.java +++ b/rt/transports/http/src/test/java/org/apache/cxf/transport/https/httpclient/DefaultHostnameVerifierTest.java @@ -50,9 +50,9 @@ public class DefaultHostnameVerifierTest { private DefaultHostnameVerifier implWithPublicSuffixCheck; @Before - public void setUp() { + public void setup() { impl = new DefaultHostnameVerifier(); - publicSuffixMatcher = new PublicSuffixMatcher(Arrays.asList("com", "co.jp", "gov.uk"), null); + publicSuffixMatcher = new PublicSuffixMatcher(DomainType.ICANN, Arrays.asList("com", "co.jp", "gov.uk"), null); implWithPublicSuffixCheck = new DefaultHostnameVerifier(publicSuffixMatcher); } @@ -191,6 +191,16 @@ public class DefaultHostnameVerifierTest { // whew! we're okay! } } + + @Test + public void testDomainRootMatching() { + + Assert.assertFalse(DefaultHostnameVerifier.matchDomainRoot("a.b.c", null)); + Assert.assertTrue(DefaultHostnameVerifier.matchDomainRoot("a.b.c", "a.b.c")); + Assert.assertFalse(DefaultHostnameVerifier.matchDomainRoot("aa.b.c", "a.b.c")); + Assert.assertFalse(DefaultHostnameVerifier.matchDomainRoot("a.b.c", "aa.b.c")); + Assert.assertTrue(DefaultHostnameVerifier.matchDomainRoot("a.a.b.c", "a.b.c")); + } @Test public void testIdentityMatching() {