View Javadoc
1   /*
2    * ====================================================================
3    * Licensed to the Apache Software Foundation (ASF) under one
4    * or more contributor license agreements.  See the NOTICE file
5    * distributed with this work for additional information
6    * regarding copyright ownership.  The ASF licenses this file
7    * to you under the Apache License, Version 2.0 (the
8    * "License"); you may not use this file except in compliance
9    * with the License.  You may obtain a copy of the License at
10   *
11   *   http://www.apache.org/licenses/LICENSE-2.0
12   *
13   * Unless required by applicable law or agreed to in writing,
14   * software distributed under the License is distributed on an
15   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16   * KIND, either express or implied.  See the License for the
17   * specific language governing permissions and limitations
18   * under the License.
19   * ====================================================================
20   *
21   * This software consists of voluntary contributions made by many
22   * individuals on behalf of the Apache Software Foundation.  For more
23   * information on the Apache Software Foundation, please see
24   * <http://www.apache.org/>.
25   *
26   */
27  package org.apache.hc.client5.http.psl;
28  
29  import java.net.IDN;
30  import java.util.Collection;
31  import java.util.List;
32  import java.util.Map;
33  import java.util.concurrent.ConcurrentHashMap;
34  
35  import org.apache.hc.client5.http.utils.DnsUtils;
36  import org.apache.hc.core5.annotation.Contract;
37  import org.apache.hc.core5.annotation.Internal;
38  import org.apache.hc.core5.annotation.ThreadingBehavior;
39  import org.apache.hc.core5.util.Args;
40  
41  /**
42   * Utility class that can test if DNS names match the content of the Public Suffix List.
43   * <p>
44   * An up-to-date list of suffixes can be obtained from
45   * <a href="http://publicsuffix.org/">publicsuffix.org</a>
46   * </p>
47   *
48   * @see PublicSuffixList
49   *
50   * @since 4.4
51   */
52  @Contract(threading = ThreadingBehavior.SAFE)
53  public final class PublicSuffixMatcher {
54  
55      private final Map<String, DomainType> rules;
56      private final Map<String, DomainType> exceptions;
57  
58      public PublicSuffixMatcher(final Collection<String> rules, final Collection<String> exceptions) {
59          this(DomainType.UNKNOWN, rules, exceptions);
60      }
61  
62      /**
63       * @since 4.5
64       */
65      public PublicSuffixMatcher(
66              final DomainType domainType, final Collection<String> rules, final Collection<String> exceptions) {
67          Args.notNull(domainType, "Domain type");
68          Args.notNull(rules, "Domain suffix rules");
69          this.rules = new ConcurrentHashMap<>(rules.size());
70          for (final String rule: rules) {
71              this.rules.put(rule, domainType);
72          }
73          this.exceptions = new ConcurrentHashMap<>();
74          if (exceptions != null) {
75              for (final String exception: exceptions) {
76                  this.exceptions.put(exception, domainType);
77              }
78          }
79      }
80  
81      /**
82       * @since 4.5
83       */
84      public PublicSuffixMatcher(final Collection<PublicSuffixList> lists) {
85          Args.notNull(lists, "Domain suffix lists");
86          this.rules = new ConcurrentHashMap<>();
87          this.exceptions = new ConcurrentHashMap<>();
88          for (final PublicSuffixList list: lists) {
89              final DomainType domainType = list.getType();
90              final List<String> rules = list.getRules();
91              for (final String rule: rules) {
92                  this.rules.put(rule, domainType);
93              }
94              final List<String> exceptions = list.getExceptions();
95              if (exceptions != null) {
96                  for (final String exception: exceptions) {
97                      this.exceptions.put(exception, domainType);
98                  }
99              }
100         }
101     }
102 
103     private static DomainType findEntry(final Map<String, DomainType> map, final String rule) {
104         if (map == null) {
105             return null;
106         }
107         return map.get(rule);
108     }
109 
110     private static boolean match(final DomainType domainType, final DomainType expectedType) {
111         return domainType != null && (expectedType == null || domainType.equals(expectedType));
112     }
113 
114     /**
115      * Returns registrable part of the domain for the given domain name or {@code null}
116      * if given domain represents a public suffix.
117      *
118      * @param domain
119      * @return domain root
120      */
121     public String getDomainRoot(final String domain) {
122         return getDomainRoot(domain, null);
123     }
124 
125     /**
126      * Returns registrable part of the domain for the given domain name or {@code null}
127      * if given domain represents a public suffix.
128      *
129      * @param domain
130      * @param expectedType expected domain type or {@code null} if any.
131      * @return domain root
132      * @since 4.5
133      */
134     public String getDomainRoot(final String domain, final DomainType expectedType) {
135         if (domain == null) {
136             return null;
137         }
138         if (domain.startsWith(".")) {
139             return null;
140         }
141         String normalized = DnsUtils.normalize(domain);
142         final boolean punyCoded = normalized.contains("xn-");
143         if (punyCoded) {
144             normalized = IDN.toUnicode(normalized);
145         }
146         final DomainRootInfo match = resolveDomainRoot(normalized, expectedType);
147         String domainRoot = match != null ? match.root : null;
148         if (domainRoot != null && punyCoded) {
149             domainRoot = IDN.toASCII(domainRoot);
150         }
151         return domainRoot;
152     }
153 
154     static final class DomainRootInfo {
155 
156         final String root;
157         final String matchingKey;
158         final DomainType domainType;
159 
160         DomainRootInfo(final String root, final String matchingKey, final DomainType domainType) {
161             this.root = root;
162             this.matchingKey = matchingKey;
163             this.domainType = domainType;
164         }
165     }
166 
167     DomainRootInfo resolveDomainRoot(final String domain, final DomainType expectedType) {
168         String segment = domain;
169         String result = null;
170         while (segment != null) {
171             // An exception rule takes priority over any other matching rule.
172             final String key = segment;
173             final DomainType exceptionRule = findEntry(exceptions, key);
174             if (match(exceptionRule, expectedType)) {
175                 return new DomainRootInfo(segment, key, exceptionRule);
176             }
177             final DomainType domainRule = findEntry(rules, key);
178             if (match(domainRule, expectedType)) {
179                 return new DomainRootInfo(result, key, domainRule);
180             }
181 
182             final int nextdot = segment.indexOf('.');
183             final String nextSegment = nextdot != -1 ? segment.substring(nextdot + 1) : null;
184 
185             // look for wildcard entries
186             final String wildcardKey = (nextSegment == null) ? "*" : "*." + nextSegment;
187             final DomainType wildcardDomainRule = findEntry(rules, wildcardKey);
188             if (match(wildcardDomainRule, expectedType)) {
189                 return new DomainRootInfo(result, wildcardKey, wildcardDomainRule);
190             }
191 
192             // If we're out of segments, and we're not looking for a specific type of entry,
193             // apply the default `*` rule.
194             // This wildcard rule means any final segment in a domain is a public suffix,
195             // so the current `result` is the desired public suffix plus 1
196             if (nextSegment == null && (expectedType == null || expectedType == DomainType.UNKNOWN)) {
197                 return new DomainRootInfo(result, null, null);
198             }
199 
200             result = segment;
201             segment = nextSegment;
202         }
203 
204         // If no expectations then this result is good.
205         if (expectedType == null || expectedType == DomainType.UNKNOWN) {
206             return new DomainRootInfo(result, null, null);
207         }
208 
209         // If we did have expectations apparently there was no match
210         return null;
211     }
212 
213     /**
214      * Tests whether the given domain matches any of the entries from the public suffix list.
215      */
216     public boolean matches(final String domain) {
217         return matches(domain, null);
218     }
219 
220     /**
221      * Tests whether the given domain matches any of entry from the public suffix list.
222      *
223      * @param domain
224      * @param expectedType expected domain type or {@code null} if any.
225      * @return {@code true} if the given domain matches any of the public suffixes.
226      *
227      * @since 4.5
228      */
229     public boolean matches(final String domain, final DomainType expectedType) {
230         if (domain == null) {
231             return false;
232         }
233         final String domainRoot = getDomainRoot(
234                 domain.startsWith(".") ? domain.substring(1) : domain, expectedType);
235         return domainRoot == null;
236     }
237 
238     /**
239      * Verifies if the given domain does not represent a public domain root and is
240      * allowed to set cookies, have an identity represented by a certificate, etc.
241      * <p>
242      * This method tolerates a leading dot in the domain name, for example '.example.com'
243      * which will be automatically stripped.
244      * </p>
245      *
246      * @since 5.5
247      */
248      public boolean verify(final String domain) {
249          if (domain == null) {
250              return false;
251          }
252          return verifyInternal(domain.startsWith(".") ? domain.substring(1) : domain);
253      }
254 
255     @Internal
256     public boolean verifyInternal(final String domain) {
257         final DomainRootInfo domainRootInfo = resolveDomainRoot(domain, null);
258         if (domainRootInfo == null) {
259             return false;
260         }
261         return domainRootInfo.root != null ||
262                 domainRootInfo.domainType == DomainType.PRIVATE && domainRootInfo.matchingKey != null;
263     }
264 
265 }