| /* |
| * 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(); |
| } |
| } |
| } |