--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/java.base/share/classes/sun/security/util/DomainName.java Tue Jun 26 18:55:48 2018 +0800
@@ -0,0 +1,637 @@
+/*
+ * Copyright (c) 2017, 2018, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package sun.security.util;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.IOException;
+import java.security.AccessController;
+import java.security.PrivilegedAction;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+
+import sun.security.ssl.SSLLogger;
+
+/**
+ * Allows public suffixes and registered domains to be determined from a
+ * string that represents a target domain name. A database of known
+ * registered suffixes is used to perform the determination.
+ *
+ * A public suffix is defined as the rightmost part of a domain name
+ * that is not owned by an individual registrant. Examples of
+ * public suffixes are:
+ * com
+ * edu
+ * co.uk
+ * k12.ak.us
+ * com.tw
+ * \u7db2\u8def.tw
+ *
+ * Public suffixes effectively denote registration authorities.
+ *
+ * A registered domain is a public suffix preceded by one domain label
+ * and a ".". Examples are:
+ * oracle.com
+ * mit.edu
+ *
+ * The internal database is derived from the information maintained at
+ * http://publicsuffix.org. The information is fixed for a particular
+ * JDK installation, but may be updated in future releases or updates.
+ *
+ * Because of the large number of top-level domains (TLDs) and public
+ * suffix rules, we only load the rules on demand -- from a Zip file
+ * containing an entry for each TLD.
+ *
+ * As each entry is loaded, its data is stored permanently in a cache.
+ *
+ * The containment hierarchy for the data is shown below:
+ *
+ * Rules --> contains all the rules for a particular TLD
+ * RuleSet --> contains all the rules that match 1 label
+ * RuleSet --> contains all the rules that match 2 labels
+ * RuleSet --> contains all the rules that match 3 labels
+ * :
+ * RuleSet --> contains all the rules that match N labels
+ * HashSet of rules, where each rule is an exception rule, a "normal"
+ * rule, a wildcard rule (rules that contain a wildcard prefix only),
+ * or a LinkedList of "other" rules
+ *
+ * The general matching algorithm tries to find a longest match. So, the
+ * search begins at the RuleSet with the most labels, and works backwards.
+ *
+ * Exceptions take priority over all other rules, and if a Rule contains
+ * any exceptions, then even if we find a "normal" match, we search all
+ * other RuleSets for exceptions. It is assumed that all other rules don't
+ * intersect/overlap. If this happens, a match will be returned, but not
+ * necessarily the expected one. For a further explanation of the rules,
+ * see http://publicsuffix.org/list/.
+ *
+ * The "other" rules are for the (possible future) case where wildcards
+ * are located in a rule any place other than the beginning.
+ */
+
+class DomainName {
+ /**
+ * For efficiency, the set of rules for each TLD is kept
+ * in text files and only loaded if needed.
+ */
+ private static final Map<String, Rules> cache = new ConcurrentHashMap<>();
+
+ private DomainName() {}
+
+ /**
+ * Returns the registered domain of the specified domain.
+ *
+ * @param domain the domain name
+ * @return the registered domain, or null if not known or not registerable
+ * @throws NullPointerException if domain is null
+ */
+ public static RegisteredDomain registeredDomain(String domain) {
+ Match match = getMatch(domain);
+ return (match != null) ? match.registeredDomain() : null;
+ }
+
+ private static Match getMatch(String domain) {
+ if (domain == null) {
+ throw new NullPointerException();
+ }
+ Rules rules = Rules.getRules(domain);
+ return rules == null ? null : rules.match(domain);
+ }
+
+ /**
+ * A Rules object contains a list of rules for a particular TLD.
+ *
+ * Rules are stored in a linked list of RuleSet objects. The list is
+ * indexed according to the number of labels in the name (minus one)
+ * such that all rules with the same number of labels are stored
+ * in the same RuleSet.
+ *
+ * Doing this means we can find the longest match first, and also we
+ * can stop comparing as soon as we find a match.
+ */
+ private static class Rules {
+
+ private final LinkedList<RuleSet> ruleSets = new LinkedList<>();
+ private final boolean hasExceptions;
+
+ private Rules(InputStream is) throws IOException {
+ InputStreamReader isr = new InputStreamReader(is, "UTF-8");
+ BufferedReader reader = new BufferedReader(isr);
+ boolean hasExceptions = false;
+
+ String line;
+ int type = reader.read();
+ while (type != -1 && (line = reader.readLine()) != null) {
+ int numLabels = RuleSet.numLabels(line);
+ if (numLabels != 0) {
+ RuleSet ruleset = getRuleSet(numLabels - 1);
+ ruleset.addRule(type, line);
+ hasExceptions |= ruleset.hasExceptions;
+ }
+ type = reader.read();
+ }
+ this.hasExceptions = hasExceptions;
+ }
+
+ static Rules getRules(String domain) {
+ String tld = getTopLevelDomain(domain);
+ if (tld.isEmpty()) {
+ return null;
+ }
+ return cache.computeIfAbsent(tld, k -> createRules(tld));
+ }
+
+ private static String getTopLevelDomain(String domain) {
+ int n = domain.lastIndexOf('.');
+ if (n == -1) {
+ return domain;
+ }
+ return domain.substring(n + 1);
+ }
+
+ private static Rules createRules(String tld) {
+ try (InputStream pubSuffixStream = getPubSuffixStream()) {
+ if (pubSuffixStream == null) {
+ return null;
+ }
+ return getRules(tld, new ZipInputStream(pubSuffixStream));
+ } catch (IOException e) {
+ if (SSLLogger.isOn && SSLLogger.isOn("ssl")) {
+ SSLLogger.fine(
+ "cannot parse public suffix data for " + tld +
+ ": " + e.getMessage());
+ }
+ return null;
+ }
+ }
+
+ private static InputStream getPubSuffixStream() {
+ InputStream is = AccessController.doPrivileged(
+ new PrivilegedAction<>() {
+ @Override
+ public InputStream run() {
+ File f = new File(System.getProperty("java.home"),
+ "lib/security/public_suffix_list.dat");
+ try {
+ return new FileInputStream(f);
+ } catch (FileNotFoundException e) {
+ return null;
+ }
+ }
+ }
+ );
+ if (is == null) {
+ if (SSLLogger.isOn && SSLLogger.isOn("ssl") &&
+ SSLLogger.isOn("trustmanager")) {
+ SSLLogger.fine(
+ "lib/security/public_suffix_list.dat not found");
+ }
+ }
+ return is;
+ }
+
+ private static Rules getRules(String tld,
+ ZipInputStream zis) throws IOException {
+ boolean found = false;
+ ZipEntry ze = zis.getNextEntry();
+ while (ze != null && !found) {
+ if (ze.getName().equals(tld)) {
+ found = true;
+ } else {
+ ze = zis.getNextEntry();
+ }
+ }
+ if (!found) {
+ if (SSLLogger.isOn && SSLLogger.isOn("ssl")) {
+ SSLLogger.fine("Domain " + tld + " not found");
+ }
+ return null;
+ }
+ return new Rules(zis);
+ }
+
+ /**
+ * Return the requested RuleSet. If it hasn't been created yet,
+ * create it and any RuleSets leading up to it.
+ */
+ private RuleSet getRuleSet(int index) {
+ if (index < ruleSets.size()) {
+ return ruleSets.get(index);
+ }
+ RuleSet r = null;
+ for (int i = ruleSets.size(); i <= index; i++) {
+ r = new RuleSet(i + 1);
+ ruleSets.add(r);
+ }
+ return r;
+ }
+
+ /**
+ * Find a match for the target string.
+ */
+ Match match(String domain) {
+ // Start at the end of the rules list, looking for longest match.
+ // After we find a normal match, we only look for exceptions.
+ Match possibleMatch = null;
+
+ Iterator<RuleSet> it = ruleSets.descendingIterator();
+ while (it.hasNext()) {
+ RuleSet ruleSet = it.next();
+ Match match = ruleSet.match(domain);
+ if (match != null) {
+ if (match.type() == Rule.Type.EXCEPTION || !hasExceptions) {
+ return match;
+ }
+ if (possibleMatch == null) {
+ possibleMatch = match;
+ }
+ }
+ }
+ return possibleMatch;
+ }
+
+ /**
+ * Represents a set of rules with the same number of labels
+ * and for a particular TLD.
+ *
+ * Examples:
+ * numLabels = 2
+ * names: co.uk, ac.uk
+ * wildcards *.de (only "de" stored in HashSet)
+ * exceptions: !foo.de (stored as "foo.de")
+ */
+ private static class RuleSet {
+ // the number of labels in this ruleset
+ private final int numLabels;
+ private final Set<Rule> rules = new HashSet<>();
+ boolean hasExceptions = false;
+ private static final RegisteredDomain.Type[] AUTHS =
+ RegisteredDomain.Type.values();
+
+ RuleSet(int n) {
+ numLabels = n;
+ }
+
+ void addRule(int auth, String rule) {
+ if (rule.startsWith("!")) {
+ rules.add(new Rule(rule.substring(1), Rule.Type.EXCEPTION,
+ AUTHS[auth]));
+ hasExceptions = true;
+ } else if (rule.startsWith("*.") &&
+ rule.lastIndexOf('*') == 0) {
+ rules.add(new Rule(rule.substring(2), Rule.Type.WILDCARD,
+ AUTHS[auth]));
+ } else if (rule.indexOf('*') == -1) {
+ // a "normal" label
+ rules.add(new Rule(rule, Rule.Type.NORMAL, AUTHS[auth]));
+ } else {
+ // There is a wildcard in a non-leading label. This case
+ // doesn't currently exist, but we need to handle it anyway.
+ rules.add(new OtherRule(rule, AUTHS[auth], split(rule)));
+ }
+ }
+
+ Match match(String domain) {
+ Match match = null;
+ for (Rule rule : rules) {
+ switch (rule.type) {
+ case NORMAL:
+ if (match == null) {
+ match = matchNormal(domain, rule);
+ }
+ break;
+ case WILDCARD:
+ if (match == null) {
+ match = matchWildcard(domain, rule);
+ }
+ break;
+ case OTHER:
+ if (match == null) {
+ match = matchOther(domain, rule);
+ }
+ break;
+ case EXCEPTION:
+ Match excMatch = matchException(domain, rule);
+ if (excMatch != null) {
+ return excMatch;
+ }
+ break;
+ }
+ }
+ return match;
+ }
+
+ private static LinkedList<String> split(String rule) {
+ String[] labels = rule.split("\\.");
+ return new LinkedList<>(Arrays.asList(labels));
+ }
+
+ private static int numLabels(String rule) {
+ if (rule.equals("")) {
+ return 0;
+ }
+ int len = rule.length();
+ int count = 0;
+ int index = 0;
+ while (index < len) {
+ int pos;
+ if ((pos = rule.indexOf('.', index)) == -1) {
+ return count + 1;
+ }
+ index = pos + 1;
+ count++;
+ }
+ return count;
+ }
+
+ /**
+ * Check for a match with an explicit name rule or a wildcard rule
+ * (i.e., a non-exception rule).
+ */
+ private Match matchNormal(String domain, Rule rule) {
+ int index = labels(domain, numLabels);
+ if (index == -1) {
+ return null;
+ }
+
+ // Check for explicit names.
+ String substring = domain.substring(index);
+ if (rule.domain.equals(substring)) {
+ return new CommonMatch(domain, rule, index);
+ }
+
+ return null;
+ }
+
+ private Match matchWildcard(String domain, Rule rule) {
+ // Now check for wildcards. In this case, there is one fewer
+ // label than numLabels.
+ int index = labels(domain, numLabels - 1);
+ if (index > 0) {
+ String substring = domain.substring(index);
+
+ if (rule.domain.equals(substring)) {
+ return new CommonMatch(domain, rule,
+ labels(domain, numLabels));
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Check for a match with an exception rule.
+ */
+ private Match matchException(String domain, Rule rule) {
+ int index = labels(domain, numLabels);
+ if (index == -1) {
+ return null;
+ }
+ String substring = domain.substring(index);
+
+ if (rule.domain.equals(substring)) {
+ return new CommonMatch(domain, rule,
+ labels(domain, numLabels - 1));
+ }
+
+ return null;
+ }
+
+ /**
+ * A left-to-right comparison of labels.
+ * The simplest approach to doing match() would be to
+ * use a descending iterator giving a right-to-left comparison.
+ * But, it's more efficient to do left-to-right compares
+ * because the left most labels are the ones most likely to be
+ * different. We just have to figure out which label to start at.
+ */
+ private Match matchOther(String domain, Rule rule) {
+ OtherRule otherRule = (OtherRule)rule;
+ LinkedList<String> target = split(domain);
+
+ int diff = target.size() - numLabels;
+ if (diff < 0) {
+ return null;
+ }
+
+ boolean found = true;
+ for (int i = 0; i < numLabels; i++) {
+ String ruleLabel = otherRule.labels.get(i);
+ String targetLabel = target.get(i + diff);
+
+ if (ruleLabel.charAt(0) != '*' &&
+ !ruleLabel.equalsIgnoreCase(targetLabel)) {
+ found = false;
+ break;
+ }
+ }
+ if (found) {
+ return new OtherMatch(rule, numLabels, target);
+ }
+ return null;
+ }
+
+ /**
+ * Returns a substring (index) with the n right-most labels from s.
+ * Returns -1 if s does not have at least n labels, 0, if the
+ * substring is s.
+ */
+ private static int labels(String s, int n) {
+ if (n < 1) {
+ return -1;
+ }
+ int index = s.length();
+ for (int i = 0; i < n; i++) {
+ int next = s.lastIndexOf('.', index);
+ if (next == -1) {
+ if (i == n - 1) {
+ return 0;
+ } else {
+ return -1;
+ }
+ }
+ index = next - 1;
+ }
+ return index + 2;
+ }
+ }
+ }
+
+ private static class Rule {
+ enum Type { EXCEPTION, NORMAL, OTHER, WILDCARD }
+
+ String domain;
+ Type type;
+ RegisteredDomain.Type auth;
+ Rule(String domain, Type type, RegisteredDomain.Type auth) {
+ this.domain = domain;
+ this.type = type;
+ this.auth = auth;
+ }
+ }
+
+ private static class OtherRule extends Rule {
+ List<String> labels;
+ OtherRule(String domain, RegisteredDomain.Type auth,
+ List<String> labels) {
+ super(domain, Type.OTHER, auth);
+ this.labels = labels;
+ }
+ }
+
+ /**
+ * Represents a string's match with a rule in the public suffix list.
+ */
+ private interface Match {
+ RegisteredDomain registeredDomain();
+ Rule.Type type();
+ }
+
+ private static class RegisteredDomainImpl implements RegisteredDomain {
+ private final String name;
+ private final Type type;
+ private final String publicSuffix;
+ RegisteredDomainImpl(String name, Type type, String publicSuffix) {
+ this.name = name;
+ this.type = type;
+ this.publicSuffix = publicSuffix;
+ }
+ @Override
+ public String name() {
+ return name;
+ }
+ @Override
+ public Type type() {
+ return type;
+ }
+ @Override
+ public String publicSuffix() {
+ return publicSuffix;
+ }
+ }
+
+ /**
+ * Represents a match against a standard rule in the public suffix list.
+ * A standard rule is an explicit name, a wildcard rule with a wildcard
+ * only in the leading label, or an exception rule.
+ */
+ private static class CommonMatch implements Match {
+ private String domain;
+ private int publicSuffix; // index to
+ private int registeredDomain; // index to
+ private final Rule rule;
+
+ CommonMatch(String domain, Rule rule, int publicSuffix) {
+ this.domain = domain;
+ this.publicSuffix = publicSuffix;
+ this.rule = rule;
+ // now locate the previous label
+ registeredDomain = domain.lastIndexOf('.', publicSuffix - 2);
+ if (registeredDomain == -1) {
+ registeredDomain = 0;
+ } else {
+ registeredDomain++;
+ }
+ }
+
+ @Override
+ public RegisteredDomain registeredDomain() {
+ if (publicSuffix == 0) {
+ return null;
+ }
+ return new RegisteredDomainImpl(domain.substring(registeredDomain),
+ rule.auth,
+ domain.substring(publicSuffix));
+ }
+
+ @Override
+ public Rule.Type type() {
+ return rule.type;
+ }
+ }
+
+ /**
+ * Represents a non-match with {@code NO_MATCH} or a match against
+ * a non-standard rule in the public suffix list. A non-standard rule
+ * is a wildcard rule that includes wildcards in a label other than
+ * the leading label. The public suffix list doesn't currently have
+ * such rules.
+ */
+ private static class OtherMatch implements Match {
+ private final Rule rule;
+ private final int numLabels;
+ private final LinkedList<String> target;
+
+ OtherMatch(Rule rule, int numLabels, LinkedList<String> target) {
+ this.rule = rule;
+ this.numLabels = numLabels;
+ this.target = target;
+ }
+
+ @Override
+ public RegisteredDomain registeredDomain() {
+ int nlabels = numLabels + 1;
+ if (nlabels > target.size()) {
+ // special case when registered domain is same as pub suff
+ return null;
+ }
+ return new RegisteredDomainImpl(getSuffixes(nlabels),
+ rule.auth, getSuffixes(numLabels));
+ }
+
+ @Override
+ public Rule.Type type() {
+ return rule.type;
+ }
+
+ private String getSuffixes(int n) {
+ Iterator<String> targetIter = target.descendingIterator();
+ StringBuilder sb = new StringBuilder();
+ while (n > 0 && targetIter.hasNext()) {
+ String s = targetIter.next();
+ sb.insert(0, s);
+ if (n > 1) {
+ sb.insert(0, '.');
+ }
+ n--;
+ }
+ return sb.toString();
+ }
+ }
+}