diff --git a/core/java/src/org/apache/http/conn/ssl/DefaultHostnameVerifier.java b/core/java/src/org/apache/http/conn/ssl/DefaultHostnameVerifier.java
new file mode 100644
index 0000000000000000000000000000000000000000..03a1edb9185d104be39be79d37456e09105c6614
--- /dev/null
+++ b/core/java/src/org/apache/http/conn/ssl/DefaultHostnameVerifier.java
@@ -0,0 +1,297 @@
+/*
+ * ====================================================================
+ * 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.http.conn.ssl;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateParsingException;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Locale;
+import java.util.NoSuchElementException;
+
+import javax.naming.InvalidNameException;
+import javax.naming.NamingException;
+import javax.naming.directory.Attribute;
+import javax.naming.directory.Attributes;
+import javax.naming.ldap.LdapName;
+import javax.naming.ldap.Rdn;
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.SSLException;
+import javax.net.ssl.SSLSession;
+import javax.security.auth.x500.X500Principal;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.http.annotation.Immutable;
+import org.apache.http.conn.util.InetAddressUtils;
+import org.apache.http.conn.util.PublicSuffixMatcher;
+
+/**
+ * Default {@link javax.net.ssl.HostnameVerifier} implementation.
+ *
+ * @since 4.4
+ */
+@Immutable
+public final class DefaultHostnameVerifier implements HostnameVerifier {
+
+    final static int DNS_NAME_TYPE        = 2;
+    final static int IP_ADDRESS_TYPE      = 7;
+
+    private final Log log = LogFactory.getLog(getClass());
+
+    private final PublicSuffixMatcher publicSuffixMatcher;
+
+    public DefaultHostnameVerifier(final PublicSuffixMatcher publicSuffixMatcher) {
+        this.publicSuffixMatcher = publicSuffixMatcher;
+    }
+
+    public DefaultHostnameVerifier() {
+        this(null);
+    }
+
+    @Override
+    public final boolean verify(final String host, final SSLSession session) {
+        try {
+            final Certificate[] certs = session.getPeerCertificates();
+            final X509Certificate x509 = (X509Certificate) certs[0];
+            verify(host, x509);
+            return true;
+        } catch(final SSLException ex) {
+            if (log.isDebugEnabled()) {
+                log.debug(ex.getMessage(), ex);
+            }
+            return false;
+        }
+    }
+
+    public final 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;
+        final List<String> subjectAlts = extractSubjectAlts(cert, subjectType);
+        if (subjectAlts != null && !subjectAlts.isEmpty()) {
+            if (ipv4) {
+                matchIPAddress(host, subjectAlts);
+            } else if (ipv6) {
+                matchIPv6Address(host, subjectAlts);
+            } else {
+                matchDNSName(host, subjectAlts, this.publicSuffixMatcher);
+            }
+        } else {
+            // CN matching has been deprecated by rfc2818 and can be used
+            // as fallback only when no subjectAlts are available
+            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 " +
+                        "a common name and does not have alternative names");
+            }
+            matchCN(host, cn, this.publicSuffixMatcher);
+        }
+    }
+
+    static void matchIPAddress(final String host, final List<String> subjectAlts) throws SSLException {
+        for (int i = 0; i < subjectAlts.size(); i++) {
+            final String subjectAlt = subjectAlts.get(i);
+            if (host.equals(subjectAlt)) {
+                return;
+            }
+        }
+        throw new SSLException("Certificate for <" + host + "> doesn't match any " +
+                "of the subject alternative names: " + subjectAlts);
+    }
+
+    static void matchIPv6Address(final String host, final List<String> subjectAlts) throws SSLException {
+        final String normalisedHost = normaliseAddress(host);
+        for (int i = 0; i < subjectAlts.size(); i++) {
+            final String subjectAlt = subjectAlts.get(i);
+            final String normalizedSubjectAlt = normaliseAddress(subjectAlt);
+            if (normalisedHost.equals(normalizedSubjectAlt)) {
+                return;
+            }
+        }
+        throw new SSLException("Certificate for <" + host + "> doesn't match any " +
+                "of the subject alternative names: " + subjectAlts);
+    }
+
+    static void matchDNSName(final String host, final List<String> subjectAlts,
+                             final PublicSuffixMatcher publicSuffixMatcher) throws SSLException {
+        final String normalizedHost = host.toLowerCase(Locale.ROOT);
+        for (int i = 0; i < subjectAlts.size(); i++) {
+            final String subjectAlt = subjectAlts.get(i);
+            final String normalizedSubjectAlt = subjectAlt.toLowerCase(Locale.ROOT);
+            if (matchIdentityStrict(normalizedHost, normalizedSubjectAlt, publicSuffixMatcher)) {
+                return;
+            }
+        }
+        throw new SSLException("Certificate for <" + host + "> doesn't match any " +
+                "of the subject alternative names: " + subjectAlts);
+    }
+
+    static void matchCN(final String host, final String cn,
+                 final PublicSuffixMatcher publicSuffixMatcher) throws SSLException {
+        if (!matchIdentityStrict(host, cn, publicSuffixMatcher)) {
+            throw new SSLException("Certificate for <" + host + "> doesn't match " +
+                    "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 (publicSuffixMatcher != null && host.contains(".")) {
+            if (!matchDomainRoot(host, publicSuffixMatcher.getDomainRoot(identity))) {
+                return false;
+            }
+        }
+
+        // RFC 2818, 3.1. Server Identity
+        // "...Names may contain the wildcard
+        // character * which is considered to match any single domain name
+        // component or component fragment..."
+        // Based on this statement presuming only singular wildcard is legal
+        final int asteriskIdx = identity.indexOf('*');
+        if (asteriskIdx != -1) {
+            final String prefix = identity.substring(0, asteriskIdx);
+            final String suffix = identity.substring(asteriskIdx + 1);
+            if (!prefix.isEmpty() && !host.startsWith(prefix)) {
+                return false;
+            }
+            if (!suffix.isEmpty() && !host.endsWith(suffix)) {
+                return false;
+            }
+            // Additional sanity checks on content selected by wildcard can be done here
+            if (strict) {
+                final String remainder = host.substring(
+                        prefix.length(), host.length() - suffix.length());
+                if (remainder.contains(".")) {
+                    return false;
+                }
+            }
+            return true;
+        }
+        return host.equalsIgnoreCase(identity);
+    }
+
+    static boolean matchIdentity(final String host, final String identity,
+                                 final PublicSuffixMatcher publicSuffixMatcher) {
+        return matchIdentity(host, identity, publicSuffixMatcher, false);
+    }
+
+    static boolean matchIdentity(final String host, final String identity) {
+        return matchIdentity(host, identity, null, false);
+    }
+
+    static boolean matchIdentityStrict(final String host, final String identity,
+                                       final PublicSuffixMatcher publicSuffixMatcher) {
+        return matchIdentity(host, identity, publicSuffixMatcher, true);
+    }
+
+    static boolean matchIdentityStrict(final String host, final String identity) {
+        return matchIdentity(host, identity, null, true);
+    }
+
+    static String extractCN(final String subjectPrincipal) throws SSLException {
+        if (subjectPrincipal == null) {
+            return null;
+        }
+        try {
+            final LdapName subjectDN = new LdapName(subjectPrincipal);
+            final List<Rdn> rdns = subjectDN.getRdns();
+            for (int i = rdns.size() - 1; i >= 0; i--) {
+                final Rdn rds = rdns.get(i);
+                final Attributes attributes = rds.toAttributes();
+                final Attribute cn = attributes.get("cn");
+                if (cn != null) {
+                    try {
+                        final Object value = cn.get();
+                        if (value != null) {
+                            return value.toString();
+                        }
+                    } catch (NoSuchElementException ignore) {
+                    } catch (NamingException ignore) {
+                    }
+                }
+            }
+            return null;
+        } catch (InvalidNameException e) {
+            throw new SSLException(subjectPrincipal + " is not a valid X500 distinguished name");
+        }
+    }
+
+    static List<String> extractSubjectAlts(final X509Certificate cert, final int subjectType) {
+        Collection<List<?>> c = null;
+        try {
+            c = cert.getSubjectAlternativeNames();
+        } catch(final CertificateParsingException ignore) {
+        }
+        List<String> subjectAltList = null;
+        if (c != null) {
+            for (final List<?> aC : c) {
+                final List<?> list = aC;
+                final int type = ((Integer) list.get(0)).intValue();
+                if (type == subjectType) {
+                    final String s = (String) list.get(1);
+                    if (subjectAltList == null) {
+                        subjectAltList = new ArrayList<String>();
+                    }
+                    subjectAltList.add(s);
+                }
+            }
+        }
+        return subjectAltList;
+    }
+
+    /*
+     * Normalize IPv6 or DNS name.
+     */
+    static String normaliseAddress(final String hostname) {
+        if (hostname == null) {
+            return hostname;
+        }
+        try {
+            final InetAddress inetAddress = InetAddress.getByName(hostname);
+            return inetAddress.getHostAddress();
+        } catch (final UnknownHostException unexpected) { // Should not happen, because we check for IPv6 address above
+            return hostname;
+        }
+    }
+}
diff --git a/core/java/src/org/apache/http/conn/util/InetAddressUtils.java b/core/java/src/org/apache/http/conn/util/InetAddressUtils.java
new file mode 100644
index 0000000000000000000000000000000000000000..acee8afa2372dc810f22523ac617beafbcb27281
--- /dev/null
+++ b/core/java/src/org/apache/http/conn/util/InetAddressUtils.java
@@ -0,0 +1,124 @@
+/*
+ * ====================================================================
+ * 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.http.conn.util;
+
+import java.util.regex.Pattern;
+
+import org.apache.http.annotation.Immutable;
+
+/**
+ * A collection of utilities relating to InetAddresses.
+ *
+ * @since 4.0
+ */
+@Immutable
+public class InetAddressUtils {
+
+    private InetAddressUtils() {
+    }
+
+    private static final String IPV4_BASIC_PATTERN_STRING =
+            "(([1-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){1}" + // initial first field, 1-255
+            "(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){2}" + // following 2 fields, 0-255 followed by .
+             "([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])"; // final field, 0-255
+
+    private static final Pattern IPV4_PATTERN =
+        Pattern.compile("^" + IPV4_BASIC_PATTERN_STRING + "$");
+
+    private static final Pattern IPV4_MAPPED_IPV6_PATTERN = // TODO does not allow for redundant leading zeros
+            Pattern.compile("^::[fF]{4}:" + IPV4_BASIC_PATTERN_STRING + "$");
+
+    private static final Pattern IPV6_STD_PATTERN =
+        Pattern.compile(
+                "^[0-9a-fA-F]{1,4}(:[0-9a-fA-F]{1,4}){7}$");
+
+    private static final Pattern IPV6_HEX_COMPRESSED_PATTERN =
+        Pattern.compile(
+                "^(([0-9A-Fa-f]{1,4}(:[0-9A-Fa-f]{1,4}){0,5})?)" + // 0-6 hex fields
+                 "::" +
+                 "(([0-9A-Fa-f]{1,4}(:[0-9A-Fa-f]{1,4}){0,5})?)$"); // 0-6 hex fields
+
+    /*
+     *  The above pattern is not totally rigorous as it allows for more than 7 hex fields in total
+     */
+    private static final char COLON_CHAR = ':';
+
+    // Must not have more than 7 colons (i.e. 8 fields)
+    private static final int MAX_COLON_COUNT = 7;
+
+    /**
+     * Checks whether the parameter is a valid IPv4 address
+     *
+     * @param input the address string to check for validity
+     * @return true if the input parameter is a valid IPv4 address
+     */
+    public static boolean isIPv4Address(final String input) {
+        return IPV4_PATTERN.matcher(input).matches();
+    }
+
+    public static boolean isIPv4MappedIPv64Address(final String input) {
+        return IPV4_MAPPED_IPV6_PATTERN.matcher(input).matches();
+    }
+
+    /**
+     * Checks whether the parameter is a valid standard (non-compressed) IPv6 address
+     *
+     * @param input the address string to check for validity
+     * @return true if the input parameter is a valid standard (non-compressed) IPv6 address
+     */
+    public static boolean isIPv6StdAddress(final String input) {
+        return IPV6_STD_PATTERN.matcher(input).matches();
+    }
+
+    /**
+     * Checks whether the parameter is a valid compressed IPv6 address
+     *
+     * @param input the address string to check for validity
+     * @return true if the input parameter is a valid compressed IPv6 address
+     */
+    public static boolean isIPv6HexCompressedAddress(final String input) {
+        int colonCount = 0;
+        for(int i = 0; i < input.length(); i++) {
+            if (input.charAt(i) == COLON_CHAR) {
+                colonCount++;
+            }
+        }
+        return  colonCount <= MAX_COLON_COUNT && IPV6_HEX_COMPRESSED_PATTERN.matcher(input).matches();
+    }
+
+    /**
+     * Checks whether the parameter is a valid IPv6 address (including compressed).
+     *
+     * @param input the address string to check for validity
+     * @return true if the input parameter is a valid standard or compressed IPv6 address
+     */
+    public static boolean isIPv6Address(final String input) {
+        return isIPv6StdAddress(input) || isIPv6HexCompressedAddress(input);
+    }
+
+}
diff --git a/core/java/src/org/apache/http/conn/util/PublicSuffixList.java b/core/java/src/org/apache/http/conn/util/PublicSuffixList.java
new file mode 100644
index 0000000000000000000000000000000000000000..ec15c9d40b22a5797355aedacca2844e8b310bb1
--- /dev/null
+++ b/core/java/src/org/apache/http/conn/util/PublicSuffixList.java
@@ -0,0 +1,63 @@
+/*
+ * ====================================================================
+ * 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.http.conn.util;
+
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.http.annotation.Immutable;
+import org.apache.http.util.Args;
+
+/**
+ * Public suffix is a set of DNS names or wildcards concatenated with dots. It represents
+ * the part of a domain name which is not under the control of the individual registrant
+ * <p>
+ * An up-to-date list of suffixes can be obtained from
+ * <a href="http://publicsuffix.org/">publicsuffix.org</a>
+ *
+ * @since 4.4
+ */
+@Immutable
+public final class PublicSuffixList {
+
+    private final List<String> rules;
+    private final List<String> exceptions;
+
+    public PublicSuffixList(final List<String> rules, final List<String> exceptions) {
+        this.rules = Collections.unmodifiableList(Args.notNull(rules, "Domain suffix rules"));
+        this.exceptions = Collections.unmodifiableList(Args.notNull(exceptions, "Domain suffix exceptions"));
+    }
+
+    public List<String> getRules() {
+        return rules;
+    }
+
+    public List<String> getExceptions() {
+        return exceptions;
+    }
+
+}
diff --git a/core/java/src/org/apache/http/conn/util/PublicSuffixListParser.java b/core/java/src/org/apache/http/conn/util/PublicSuffixListParser.java
new file mode 100644
index 0000000000000000000000000000000000000000..84bbd182fc8fe5b28615e44610732184efbd4ea1
--- /dev/null
+++ b/core/java/src/org/apache/http/conn/util/PublicSuffixListParser.java
@@ -0,0 +1,114 @@
+/*
+ * ====================================================================
+ * 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.http.conn.util;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.Reader;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.http.annotation.Immutable;
+
+/**
+ * Parses the list from <a href="http://publicsuffix.org/">publicsuffix.org</a>
+ * and configures a PublicSuffixFilter.
+ *
+ * @since 4.4
+ */
+@Immutable
+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).
+     *
+     * @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 BufferedReader r = new BufferedReader(reader);
+        final StringBuilder sb = new StringBuilder(256);
+        boolean more = true;
+        while (more) {
+            more = readLine(r, sb);
+            String line = sb.toString();
+            if (line.isEmpty()) {
+                continue;
+            }
+            if (line.startsWith("//")) {
+                continue; //entire lines can also be commented using //
+            }
+            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 (isException) {
+                exceptions.add(line);
+            } else {
+                rules.add(line);
+            }
+        }
+        return new PublicSuffixList(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;
+            }
+            // Each line is only read up to the first whitespace
+            if (Character.isWhitespace(c)) {
+                hitWhitespace = true;
+            }
+            if (!hitWhitespace) {
+                sb.append(c);
+            }
+            if (sb.length() > MAX_LINE_LEN) {
+                return false; // prevent excess memory usage
+            }
+        }
+        return (b != -1);
+    }
+
+}
diff --git a/core/java/src/org/apache/http/conn/util/PublicSuffixMatcher.java b/core/java/src/org/apache/http/conn/util/PublicSuffixMatcher.java
new file mode 100644
index 0000000000000000000000000000000000000000..02393aca4d517bbd09986cd0b291ddcdef222909
--- /dev/null
+++ b/core/java/src/org/apache/http/conn/util/PublicSuffixMatcher.java
@@ -0,0 +1,121 @@
+/*
+ * ====================================================================
+ * 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.http.conn.util;
+
+import java.net.IDN;
+import java.util.Collection;
+import java.util.Locale;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.apache.http.annotation.ThreadSafe;
+import org.apache.http.util.Args;
+
+/**
+ * Utility class that can test if DNS names match the content of the Public Suffix List.
+ * <p>
+ * An up-to-date list of suffixes can be obtained from
+ * <a href="http://publicsuffix.org/">publicsuffix.org</a>
+ *
+ * @see org.apache.http.conn.util.PublicSuffixList
+ *
+ * @since 4.4
+ */
+@ThreadSafe
+public final class PublicSuffixMatcher {
+
+    private final Map<String, String> rules;
+    private final Map<String, String> exceptions;
+
+    public PublicSuffixMatcher(final Collection<String> rules, final Collection<String> exceptions) {
+        Args.notNull(rules,  "Domain suffix rules");
+        this.rules = new ConcurrentHashMap<String, String>(rules.size());
+        for (String rule: rules) {
+            this.rules.put(rule, rule);
+        }
+        if (exceptions != null) {
+            this.exceptions = new ConcurrentHashMap<String, String>(exceptions.size());
+            for (String exception: exceptions) {
+                this.exceptions.put(exception, exception);
+            }
+        } else {
+            this.exceptions = null;
+        }
+    }
+
+    /**
+     * Returns registrable part of the domain for the given domain name of {@code null}
+     * if given domain represents a public suffix.
+     *
+     * @param domain
+     * @return domain root
+     */
+    public String getDomainRoot(final String domain) {
+        if (domain == null) {
+            return null;
+        }
+        if (domain.startsWith(".")) {
+            return null;
+        }
+        String domainName = null;
+        String segment = domain.toLowerCase(Locale.ROOT);
+        while (segment != null) {
+
+            // An exception rule takes priority over any other matching rule.
+            if (this.exceptions != null && this.exceptions.containsKey(IDN.toUnicode(segment))) {
+                return segment;
+            }
+
+            if (this.rules.containsKey(IDN.toUnicode(segment))) {
+                break;
+            }
+
+            final int nextdot = segment.indexOf('.');
+            final String nextSegment = nextdot != -1 ? segment.substring(nextdot + 1) : null;
+
+            if (nextSegment != null) {
+                if (this.rules.containsKey("*." + IDN.toUnicode(nextSegment))) {
+                    break;
+                }
+            }
+            if (nextdot != -1) {
+                domainName = segment;
+            }
+            segment = nextSegment;
+        }
+        return domainName;
+    }
+
+    public boolean matches(final String domain) {
+        if (domain == null) {
+            return false;
+        }
+        final String domainRoot = getDomainRoot(domain.startsWith(".") ? domain.substring(1) : domain);
+        return domainRoot == null;
+    }
+
+}
diff --git a/core/java/src/org/apache/http/util/Args.java b/core/java/src/org/apache/http/util/Args.java
new file mode 100644
index 0000000000000000000000000000000000000000..9eb8a251ff3c64b1a85568184cab002fc45fb4b2
--- /dev/null
+++ b/core/java/src/org/apache/http/util/Args.java
@@ -0,0 +1,127 @@
+/*
+ * ====================================================================
+ * 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.http.util;
+
+import java.util.Collection;
+
+public class Args {
+
+    public static void check(final boolean expression, final String message) {
+        if (!expression) {
+            throw new IllegalArgumentException(message);
+        }
+    }
+
+    public static void check(final boolean expression, final String message, final Object... args) {
+        if (!expression) {
+            throw new IllegalArgumentException(String.format(message, args));
+        }
+    }
+
+    public static void check(final boolean expression, final String message, final Object arg) {
+        if (!expression) {
+            throw new IllegalArgumentException(String.format(message, arg));
+        }
+    }
+
+    public static <T> T notNull(final T argument, final String name) {
+        if (argument == null) {
+            throw new IllegalArgumentException(name + " may not be null");
+        }
+        return argument;
+    }
+
+    public static <T extends CharSequence> T notEmpty(final T argument, final String name) {
+        if (argument == null) {
+            throw new IllegalArgumentException(name + " may not be null");
+        }
+        if (TextUtils.isEmpty(argument)) {
+            throw new IllegalArgumentException(name + " may not be empty");
+        }
+        return argument;
+    }
+
+    public static <T extends CharSequence> T notBlank(final T argument, final String name) {
+        if (argument == null) {
+            throw new IllegalArgumentException(name + " may not be null");
+        }
+        if (TextUtils.isBlank(argument)) {
+            throw new IllegalArgumentException(name + " may not be blank");
+        }
+        return argument;
+    }
+
+    public static <T extends CharSequence> T containsNoBlanks(final T argument, final String name) {
+        if (argument == null) {
+            throw new IllegalArgumentException(name + " may not be null");
+        }
+        if (TextUtils.containsBlanks(argument)) {
+            throw new IllegalArgumentException(name + " may not contain blanks");
+        }
+        return argument;
+    }
+
+    public static <E, T extends Collection<E>> T notEmpty(final T argument, final String name) {
+        if (argument == null) {
+            throw new IllegalArgumentException(name + " may not be null");
+        }
+        if (argument.isEmpty()) {
+            throw new IllegalArgumentException(name + " may not be empty");
+        }
+        return argument;
+    }
+
+    public static int positive(final int n, final String name) {
+        if (n <= 0) {
+            throw new IllegalArgumentException(name + " may not be negative or zero");
+        }
+        return n;
+    }
+
+    public static long positive(final long n, final String name) {
+        if (n <= 0) {
+            throw new IllegalArgumentException(name + " may not be negative or zero");
+        }
+        return n;
+    }
+
+    public static int notNegative(final int n, final String name) {
+        if (n < 0) {
+            throw new IllegalArgumentException(name + " may not be negative");
+        }
+        return n;
+    }
+
+    public static long notNegative(final long n, final String name) {
+        if (n < 0) {
+            throw new IllegalArgumentException(name + " may not be negative");
+        }
+        return n;
+    }
+
+}