mirror of
https://github.com/openjdk/jdk.git
synced 2025-08-28 15:24:43 +02:00
640 lines
23 KiB
Java
640 lines
23 KiB
Java
/*
|
|
* Copyright (c) 2017, 2022, 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 static java.nio.charset.StandardCharsets.UTF_8;
|
|
|
|
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 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() {
|
|
@SuppressWarnings("removal")
|
|
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.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();
|
|
}
|
|
}
|
|
}
|