Path: blob/master/src/java.base/share/classes/sun/security/util/DomainName.java
41159 views
/*1* Copyright (c) 2017, 2021, Oracle and/or its affiliates. All rights reserved.2* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.3*4* This code is free software; you can redistribute it and/or modify it5* under the terms of the GNU General Public License version 2 only, as6* published by the Free Software Foundation. Oracle designates this7* particular file as subject to the "Classpath" exception as provided8* by Oracle in the LICENSE file that accompanied this code.9*10* This code is distributed in the hope that it will be useful, but WITHOUT11* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or12* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License13* version 2 for more details (a copy is included in the LICENSE file that14* accompanied this code).15*16* You should have received a copy of the GNU General Public License version17* 2 along with this work; if not, write to the Free Software Foundation,18* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.19*20* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA21* or visit www.oracle.com if you need additional information or have any22* questions.23*/2425package sun.security.util;2627import java.io.BufferedReader;28import java.io.File;29import java.io.FileInputStream;30import java.io.FileNotFoundException;31import java.io.InputStream;32import java.io.InputStreamReader;33import java.io.IOException;34import java.security.AccessController;35import java.security.PrivilegedAction;36import java.util.Arrays;37import java.util.HashSet;38import java.util.Iterator;39import java.util.LinkedList;40import java.util.List;41import java.util.Map;42import java.util.Set;43import java.util.concurrent.ConcurrentHashMap;44import java.util.zip.ZipEntry;45import java.util.zip.ZipInputStream;4647import static java.nio.charset.StandardCharsets.UTF_8;4849import sun.security.ssl.SSLLogger;5051/**52* Allows public suffixes and registered domains to be determined from a53* string that represents a target domain name. A database of known54* registered suffixes is used to perform the determination.55*56* A public suffix is defined as the rightmost part of a domain name57* that is not owned by an individual registrant. Examples of58* public suffixes are:59* com60* edu61* co.uk62* k12.ak.us63* com.tw64* \u7db2\u8def.tw65*66* Public suffixes effectively denote registration authorities.67*68* A registered domain is a public suffix preceded by one domain label69* and a ".". Examples are:70* oracle.com71* mit.edu72*73* The internal database is derived from the information maintained at74* http://publicsuffix.org. The information is fixed for a particular75* JDK installation, but may be updated in future releases or updates.76*77* Because of the large number of top-level domains (TLDs) and public78* suffix rules, we only load the rules on demand -- from a Zip file79* containing an entry for each TLD.80*81* As each entry is loaded, its data is stored permanently in a cache.82*83* The containment hierarchy for the data is shown below:84*85* Rules --> contains all the rules for a particular TLD86* RuleSet --> contains all the rules that match 1 label87* RuleSet --> contains all the rules that match 2 labels88* RuleSet --> contains all the rules that match 3 labels89* :90* RuleSet --> contains all the rules that match N labels91* HashSet of rules, where each rule is an exception rule, a "normal"92* rule, a wildcard rule (rules that contain a wildcard prefix only),93* or a LinkedList of "other" rules94*95* The general matching algorithm tries to find a longest match. So, the96* search begins at the RuleSet with the most labels, and works backwards.97*98* Exceptions take priority over all other rules, and if a Rule contains99* any exceptions, then even if we find a "normal" match, we search all100* other RuleSets for exceptions. It is assumed that all other rules don't101* intersect/overlap. If this happens, a match will be returned, but not102* necessarily the expected one. For a further explanation of the rules,103* see http://publicsuffix.org/list/.104*105* The "other" rules are for the (possible future) case where wildcards106* are located in a rule any place other than the beginning.107*/108109class DomainName {110/**111* For efficiency, the set of rules for each TLD is kept112* in text files and only loaded if needed.113*/114private static final Map<String, Rules> cache = new ConcurrentHashMap<>();115116private DomainName() {}117118/**119* Returns the registered domain of the specified domain.120*121* @param domain the domain name122* @return the registered domain, or null if not known or not registerable123* @throws NullPointerException if domain is null124*/125public static RegisteredDomain registeredDomain(String domain) {126Match match = getMatch(domain);127return (match != null) ? match.registeredDomain() : null;128}129130private static Match getMatch(String domain) {131if (domain == null) {132throw new NullPointerException();133}134Rules rules = Rules.getRules(domain);135return rules == null ? null : rules.match(domain);136}137138/**139* A Rules object contains a list of rules for a particular TLD.140*141* Rules are stored in a linked list of RuleSet objects. The list is142* indexed according to the number of labels in the name (minus one)143* such that all rules with the same number of labels are stored144* in the same RuleSet.145*146* Doing this means we can find the longest match first, and also we147* can stop comparing as soon as we find a match.148*/149private static class Rules {150151private final LinkedList<RuleSet> ruleSets = new LinkedList<>();152private final boolean hasExceptions;153154private Rules(InputStream is) throws IOException {155InputStreamReader isr = new InputStreamReader(is, UTF_8);156BufferedReader reader = new BufferedReader(isr);157boolean hasExceptions = false;158159String line;160int type = reader.read();161while (type != -1 && (line = reader.readLine()) != null) {162int numLabels = RuleSet.numLabels(line);163if (numLabels != 0) {164RuleSet ruleset = getRuleSet(numLabels - 1);165ruleset.addRule(type, line);166hasExceptions |= ruleset.hasExceptions;167}168type = reader.read();169}170this.hasExceptions = hasExceptions;171}172173static Rules getRules(String domain) {174String tld = getTopLevelDomain(domain);175if (tld.isEmpty()) {176return null;177}178return cache.computeIfAbsent(tld, k -> createRules(tld));179}180181private static String getTopLevelDomain(String domain) {182int n = domain.lastIndexOf('.');183if (n == -1) {184return domain;185}186return domain.substring(n + 1);187}188189private static Rules createRules(String tld) {190try (InputStream pubSuffixStream = getPubSuffixStream()) {191if (pubSuffixStream == null) {192return null;193}194return getRules(tld, new ZipInputStream(pubSuffixStream));195} catch (IOException e) {196if (SSLLogger.isOn && SSLLogger.isOn("ssl")) {197SSLLogger.fine(198"cannot parse public suffix data for " + tld +199": " + e.getMessage());200}201return null;202}203}204205private static InputStream getPubSuffixStream() {206@SuppressWarnings("removal")207InputStream is = AccessController.doPrivileged(208new PrivilegedAction<>() {209@Override210public InputStream run() {211File f = new File(System.getProperty("java.home"),212"lib/security/public_suffix_list.dat");213try {214return new FileInputStream(f);215} catch (FileNotFoundException e) {216return null;217}218}219}220);221if (is == null) {222if (SSLLogger.isOn && SSLLogger.isOn("ssl") &&223SSLLogger.isOn("trustmanager")) {224SSLLogger.fine(225"lib/security/public_suffix_list.dat not found");226}227}228return is;229}230231private static Rules getRules(String tld,232ZipInputStream zis) throws IOException {233boolean found = false;234ZipEntry ze = zis.getNextEntry();235while (ze != null && !found) {236if (ze.getName().equals(tld)) {237found = true;238} else {239ze = zis.getNextEntry();240}241}242if (!found) {243if (SSLLogger.isOn && SSLLogger.isOn("ssl")) {244SSLLogger.fine("Domain " + tld + " not found");245}246return null;247}248return new Rules(zis);249}250251/**252* Return the requested RuleSet. If it hasn't been created yet,253* create it and any RuleSets leading up to it.254*/255private RuleSet getRuleSet(int index) {256if (index < ruleSets.size()) {257return ruleSets.get(index);258}259RuleSet r = null;260for (int i = ruleSets.size(); i <= index; i++) {261r = new RuleSet(i + 1);262ruleSets.add(r);263}264return r;265}266267/**268* Find a match for the target string.269*/270Match match(String domain) {271// Start at the end of the rules list, looking for longest match.272// After we find a normal match, we only look for exceptions.273Match possibleMatch = null;274275Iterator<RuleSet> it = ruleSets.descendingIterator();276while (it.hasNext()) {277RuleSet ruleSet = it.next();278Match match = ruleSet.match(domain);279if (match != null) {280if (match.type() == Rule.Type.EXCEPTION || !hasExceptions) {281return match;282}283if (possibleMatch == null) {284possibleMatch = match;285}286}287}288return possibleMatch;289}290291/**292* Represents a set of rules with the same number of labels293* and for a particular TLD.294*295* Examples:296* numLabels = 2297* names: co.uk, ac.uk298* wildcards *.de (only "de" stored in HashSet)299* exceptions: !foo.de (stored as "foo.de")300*/301private static class RuleSet {302// the number of labels in this ruleset303private final int numLabels;304private final Set<Rule> rules = new HashSet<>();305boolean hasExceptions = false;306private static final RegisteredDomain.Type[] AUTHS =307RegisteredDomain.Type.values();308309RuleSet(int n) {310numLabels = n;311}312313void addRule(int auth, String rule) {314if (rule.startsWith("!")) {315rules.add(new Rule(rule.substring(1), Rule.Type.EXCEPTION,316AUTHS[auth]));317hasExceptions = true;318} else if (rule.startsWith("*.") &&319rule.lastIndexOf('*') == 0) {320rules.add(new Rule(rule.substring(2), Rule.Type.WILDCARD,321AUTHS[auth]));322} else if (rule.indexOf('*') == -1) {323// a "normal" label324rules.add(new Rule(rule, Rule.Type.NORMAL, AUTHS[auth]));325} else {326// There is a wildcard in a non-leading label. This case327// doesn't currently exist, but we need to handle it anyway.328rules.add(new OtherRule(rule, AUTHS[auth], split(rule)));329}330}331332Match match(String domain) {333Match match = null;334for (Rule rule : rules) {335switch (rule.type) {336case NORMAL:337if (match == null) {338match = matchNormal(domain, rule);339}340break;341case WILDCARD:342if (match == null) {343match = matchWildcard(domain, rule);344}345break;346case OTHER:347if (match == null) {348match = matchOther(domain, rule);349}350break;351case EXCEPTION:352Match excMatch = matchException(domain, rule);353if (excMatch != null) {354return excMatch;355}356break;357}358}359return match;360}361362private static LinkedList<String> split(String rule) {363String[] labels = rule.split("\\.");364return new LinkedList<>(Arrays.asList(labels));365}366367private static int numLabels(String rule) {368if (rule.isEmpty()) {369return 0;370}371int len = rule.length();372int count = 0;373int index = 0;374while (index < len) {375int pos;376if ((pos = rule.indexOf('.', index)) == -1) {377return count + 1;378}379index = pos + 1;380count++;381}382return count;383}384385/**386* Check for a match with an explicit name rule or a wildcard rule387* (i.e., a non-exception rule).388*/389private Match matchNormal(String domain, Rule rule) {390int index = labels(domain, numLabels);391if (index == -1) {392return null;393}394395// Check for explicit names.396String substring = domain.substring(index);397if (rule.domain.equals(substring)) {398return new CommonMatch(domain, rule, index);399}400401return null;402}403404private Match matchWildcard(String domain, Rule rule) {405// Now check for wildcards. In this case, there is one fewer406// label than numLabels.407int index = labels(domain, numLabels - 1);408if (index > 0) {409String substring = domain.substring(index);410411if (rule.domain.equals(substring)) {412return new CommonMatch(domain, rule,413labels(domain, numLabels));414}415}416417return null;418}419420/**421* Check for a match with an exception rule.422*/423private Match matchException(String domain, Rule rule) {424int index = labels(domain, numLabels);425if (index == -1) {426return null;427}428String substring = domain.substring(index);429430if (rule.domain.equals(substring)) {431return new CommonMatch(domain, rule,432labels(domain, numLabels - 1));433}434435return null;436}437438/**439* A left-to-right comparison of labels.440* The simplest approach to doing match() would be to441* use a descending iterator giving a right-to-left comparison.442* But, it's more efficient to do left-to-right compares443* because the left most labels are the ones most likely to be444* different. We just have to figure out which label to start at.445*/446private Match matchOther(String domain, Rule rule) {447OtherRule otherRule = (OtherRule)rule;448LinkedList<String> target = split(domain);449450int diff = target.size() - numLabels;451if (diff < 0) {452return null;453}454455boolean found = true;456for (int i = 0; i < numLabels; i++) {457String ruleLabel = otherRule.labels.get(i);458String targetLabel = target.get(i + diff);459460if (ruleLabel.charAt(0) != '*' &&461!ruleLabel.equalsIgnoreCase(targetLabel)) {462found = false;463break;464}465}466if (found) {467return new OtherMatch(rule, numLabels, target);468}469return null;470}471472/**473* Returns a substring (index) with the n right-most labels from s.474* Returns -1 if s does not have at least n labels, 0, if the475* substring is s.476*/477private static int labels(String s, int n) {478if (n < 1) {479return -1;480}481int index = s.length();482for (int i = 0; i < n; i++) {483int next = s.lastIndexOf('.', index);484if (next == -1) {485if (i == n - 1) {486return 0;487} else {488return -1;489}490}491index = next - 1;492}493return index + 2;494}495}496}497498private static class Rule {499enum Type { EXCEPTION, NORMAL, OTHER, WILDCARD }500501String domain;502Type type;503RegisteredDomain.Type auth;504Rule(String domain, Type type, RegisteredDomain.Type auth) {505this.domain = domain;506this.type = type;507this.auth = auth;508}509}510511private static class OtherRule extends Rule {512List<String> labels;513OtherRule(String domain, RegisteredDomain.Type auth,514List<String> labels) {515super(domain, Type.OTHER, auth);516this.labels = labels;517}518}519520/**521* Represents a string's match with a rule in the public suffix list.522*/523private interface Match {524RegisteredDomain registeredDomain();525Rule.Type type();526}527528private static class RegisteredDomainImpl implements RegisteredDomain {529private final String name;530private final Type type;531private final String publicSuffix;532RegisteredDomainImpl(String name, Type type, String publicSuffix) {533this.name = name;534this.type = type;535this.publicSuffix = publicSuffix;536}537@Override538public String name() {539return name;540}541@Override542public Type type() {543return type;544}545@Override546public String publicSuffix() {547return publicSuffix;548}549}550551/**552* Represents a match against a standard rule in the public suffix list.553* A standard rule is an explicit name, a wildcard rule with a wildcard554* only in the leading label, or an exception rule.555*/556private static class CommonMatch implements Match {557private String domain;558private int publicSuffix; // index to559private int registeredDomain; // index to560private final Rule rule;561562CommonMatch(String domain, Rule rule, int publicSuffix) {563this.domain = domain;564this.publicSuffix = publicSuffix;565this.rule = rule;566// now locate the previous label567registeredDomain = domain.lastIndexOf('.', publicSuffix - 2);568if (registeredDomain == -1) {569registeredDomain = 0;570} else {571registeredDomain++;572}573}574575@Override576public RegisteredDomain registeredDomain() {577if (publicSuffix == 0) {578return null;579}580return new RegisteredDomainImpl(domain.substring(registeredDomain),581rule.auth,582domain.substring(publicSuffix));583}584585@Override586public Rule.Type type() {587return rule.type;588}589}590591/**592* Represents a non-match with {@code NO_MATCH} or a match against593* a non-standard rule in the public suffix list. A non-standard rule594* is a wildcard rule that includes wildcards in a label other than595* the leading label. The public suffix list doesn't currently have596* such rules.597*/598private static class OtherMatch implements Match {599private final Rule rule;600private final int numLabels;601private final LinkedList<String> target;602603OtherMatch(Rule rule, int numLabels, LinkedList<String> target) {604this.rule = rule;605this.numLabels = numLabels;606this.target = target;607}608609@Override610public RegisteredDomain registeredDomain() {611int nlabels = numLabels + 1;612if (nlabels > target.size()) {613// special case when registered domain is same as pub suff614return null;615}616return new RegisteredDomainImpl(getSuffixes(nlabels),617rule.auth, getSuffixes(numLabels));618}619620@Override621public Rule.Type type() {622return rule.type;623}624625private String getSuffixes(int n) {626Iterator<String> targetIter = target.descendingIterator();627StringBuilder sb = new StringBuilder();628while (n > 0 && targetIter.hasNext()) {629String s = targetIter.next();630sb.insert(0, s);631if (n > 1) {632sb.insert(0, '.');633}634n--;635}636return sb.toString();637}638}639}640641642