| /* | |
| * Copyright (c) 2017, 2024, 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.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 static java.nio.charset.StandardCharsets.UTF_8; | |
| import jdk.internal.util.StaticProperty; | |
| 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 | |
| * 網路.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 the 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 = null; | |
| File f = new File(System.getProperty("java.home"), | |
| "lib/security/public_suffix_list.dat"); | |
| try { | |
| is = new FileInputStream(f); | |
| } catch (FileNotFoundException e) { } | |
| 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.isEmpty()) { | |
| 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 final String domain; | |
| private final 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 suffix | |
| 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(); | |
| } | |
| } | |
| } |