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.ThreadingBehavior;
38  import org.apache.hc.core5.util.Args;
39  
40  /**
41   * Utility class that can test if DNS names match the content of the Public Suffix List.
42   * <p>
43   * An up-to-date list of suffixes can be obtained from
44   * <a href="http://publicsuffix.org/">publicsuffix.org</a>
45   * </p>
46   *
47   * @see PublicSuffixList
48   *
49   * @since 4.4
50   */
51  @Contract(threading = ThreadingBehavior.SAFE)
52  public final class PublicSuffixMatcher {
53  
54      private final Map<String, DomainType> rules;
55      private final Map<String, DomainType> exceptions;
56  
57      public PublicSuffixMatcher(final Collection<String> rules, final Collection<String> exceptions) {
58          this(DomainType.UNKNOWN, rules, exceptions);
59      }
60  
61      /**
62       * @since 4.5
63       */
64      public PublicSuffixMatcher(
65              final DomainType domainType, final Collection<String> rules, final Collection<String> exceptions) {
66          Args.notNull(domainType,  "Domain type");
67          Args.notNull(rules,  "Domain suffix rules");
68          this.rules = new ConcurrentHashMap<>(rules.size());
69          for (final String rule: rules) {
70              this.rules.put(rule, domainType);
71          }
72          this.exceptions = new ConcurrentHashMap<>();
73          if (exceptions != null) {
74              for (final String exception: exceptions) {
75                  this.exceptions.put(exception, domainType);
76              }
77          }
78      }
79  
80      /**
81       * @since 4.5
82       */
83      public PublicSuffixMatcher(final Collection<PublicSuffixList> lists) {
84          Args.notNull(lists,  "Domain suffix lists");
85          this.rules = new ConcurrentHashMap<>();
86          this.exceptions = new ConcurrentHashMap<>();
87          for (final PublicSuffixList list: lists) {
88              final DomainType domainType = list.getType();
89              final List<String> rules = list.getRules();
90              for (final String rule: rules) {
91                  this.rules.put(rule, domainType);
92              }
93              final List<String> exceptions = list.getExceptions();
94              if (exceptions != null) {
95                  for (final String exception: exceptions) {
96                      this.exceptions.put(exception, domainType);
97                  }
98              }
99          }
100     }
101 
102     private static DomainType findEntry(final Map<String, DomainType> map, final String rule) {
103         if (map == null) {
104             return null;
105         }
106         return map.get(rule);
107     }
108 
109     private static boolean match(final DomainType domainType, final DomainType expectedType) {
110         return domainType != null && (expectedType == null || domainType.equals(expectedType));
111     }
112 
113     /**
114      * Returns registrable part of the domain for the given domain name or {@code null}
115      * if given domain represents a public suffix.
116      *
117      * @param domain
118      * @return domain root
119      */
120     public String getDomainRoot(final String domain) {
121         return getDomainRoot(domain, null);
122     }
123 
124     /**
125      * Returns registrable part of the domain for the given domain name or {@code null}
126      * if given domain represents a public suffix.
127      *
128      * @param domain
129      * @param expectedType expected domain type or {@code null} if any.
130      * @return domain root
131      *
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 segment = DnsUtils.normalize(domain);
142         String result = null;
143         while (segment != null) {
144             // An exception rule takes priority over any other matching rule.
145             final String key = IDN.toUnicode(segment);
146             final DomainType exceptionRule = findEntry(exceptions, key);
147             if (match(exceptionRule, expectedType)) {
148                 return segment;
149             }
150             final DomainType domainRule = findEntry(rules, key);
151             if (match(domainRule, expectedType)) {
152                 if (domainRule == DomainType.PRIVATE) {
153                     return segment;
154                 }
155                 return result;
156             }
157 
158             final int nextdot = segment.indexOf('.');
159             final String nextSegment = nextdot != -1 ? segment.substring(nextdot + 1) : null;
160 
161             if (nextSegment != null) {
162                 final DomainType wildcardDomainRule = findEntry(rules, "*." + IDN.toUnicode(nextSegment));
163                 if (match(wildcardDomainRule, expectedType)) {
164                     if (wildcardDomainRule == DomainType.PRIVATE) {
165                         return segment;
166                     }
167                     return result;
168                 }
169             }
170             result = segment;
171             segment = nextSegment;
172         }
173 
174         // If no expectations then this result is good.
175         if (expectedType == null || expectedType == DomainType.UNKNOWN) {
176             return result;
177         }
178 
179         // If we did have expectations apparently there was no match
180         return null;
181     }
182 
183     /**
184      * Tests whether the given domain matches any of entry from the public suffix list.
185      */
186     public boolean matches(final String domain) {
187         return matches(domain, null);
188     }
189 
190     /**
191      * Tests whether the given domain matches any of entry from the public suffix list.
192      *
193      * @param domain
194      * @param expectedType expected domain type or {@code null} if any.
195      * @return {@code true} if the given domain matches any of the public suffixes.
196      *
197      * @since 4.5
198      */
199     public boolean matches(final String domain, final DomainType expectedType) {
200         if (domain == null) {
201             return false;
202         }
203         final String domainRoot = getDomainRoot(
204                 domain.startsWith(".") ? domain.substring(1) : domain, expectedType);
205         return domainRoot == null;
206     }
207 
208 }