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.http.conn.util;
28  
29  import java.net.IDN;
30  import java.util.Collection;
31  import java.util.List;
32  import java.util.Locale;
33  import java.util.Map;
34  import java.util.concurrent.ConcurrentHashMap;
35  
36  import org.apache.http.annotation.Contract;
37  import org.apache.http.annotation.ThreadingBehavior;
38  import org.apache.http.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   *
46   * @see org.apache.http.conn.util.PublicSuffixList
47   *
48   * @since 4.4
49   */
50  @Contract(threading = ThreadingBehavior.SAFE)
51  public final class PublicSuffixMatcher {
52  
53      private final Map<String, DomainType> rules;
54      private final Map<String, DomainType> exceptions;
55  
56      public PublicSuffixMatcher(final Collection<String> rules, final Collection<String> exceptions) {
57          this(DomainType.UNKNOWN, rules, exceptions);
58      }
59  
60      /**
61       * @since 4.5
62       */
63      public PublicSuffixMatcher(
64              final DomainType domainType, final Collection<String> rules, final Collection<String> exceptions) {
65          Args.notNull(domainType,  "Domain type");
66          Args.notNull(rules,  "Domain suffix rules");
67          this.rules = new ConcurrentHashMap<String, DomainType>(rules.size());
68          for (final String rule: rules) {
69              this.rules.put(rule, domainType);
70          }
71          this.exceptions = new ConcurrentHashMap<String, DomainType>();
72          if (exceptions != null) {
73              for (final String exception: exceptions) {
74                  this.exceptions.put(exception, domainType);
75              }
76          }
77      }
78  
79      /**
80       * @since 4.5
81       */
82      public PublicSuffixMatcher(final Collection<PublicSuffixList> lists) {
83          Args.notNull(lists,  "Domain suffix lists");
84          this.rules = new ConcurrentHashMap<String, DomainType>();
85          this.exceptions = new ConcurrentHashMap<String, DomainType>();
86          for (final PublicSuffixList list: lists) {
87              final DomainType domainType = list.getType();
88              final List<String> rules = list.getRules();
89              for (final String rule: rules) {
90                  this.rules.put(rule, domainType);
91              }
92              final List<String> exceptions = list.getExceptions();
93              if (exceptions != null) {
94                  for (final String exception: exceptions) {
95                      this.exceptions.put(exception, domainType);
96                  }
97              }
98          }
99      }
100 
101     private static boolean hasEntry(final Map<String, DomainType> map, final String rule, final DomainType expectedType) {
102         if (map == null) {
103             return false;
104         }
105         final DomainType domainType = map.get(rule);
106         return domainType == null ? false : expectedType == null || domainType.equals(expectedType);
107     }
108 
109     private boolean hasRule(final String rule, final DomainType expectedType) {
110         return hasEntry(this.rules, rule, expectedType);
111     }
112 
113     private boolean hasException(final String exception, final DomainType expectedType) {
114         return hasEntry(this.exceptions, exception, expectedType);
115     }
116 
117     /**
118      * Returns registrable part of the domain for the given domain name or {@code null}
119      * if given domain represents a public suffix.
120      *
121      * @param domain
122      * @return domain root
123      */
124     public String getDomainRoot(final String domain) {
125         return getDomainRoot(domain, null);
126     }
127 
128     /**
129      * Returns registrable part of the domain for the given domain name or {@code null}
130      * if given domain represents a public suffix.
131      *
132      * @param domain
133      * @param expectedType expected domain type or {@code null} if any.
134      * @return domain root
135      *
136      * @since 4.5
137      */
138     public String getDomainRoot(final String domain, final DomainType expectedType) {
139         if (domain == null) {
140             return null;
141         }
142         if (domain.startsWith(".")) {
143             return null;
144         }
145         final String normalized = domain.toLowerCase(Locale.ROOT);
146         String segment = normalized;
147         String result = null;
148         while (segment != null) {
149             // An exception rule takes priority over any other matching rule.
150             final String key = IDN.toUnicode(segment);
151             if (hasException(key, expectedType)) {
152                 return segment;
153             }
154             if (hasRule(key, expectedType)) {
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                 if (hasRule("*." + IDN.toUnicode(nextSegment), expectedType)) {
163                     return result;
164                 }
165             }
166             result = segment;
167             segment = nextSegment;
168         }
169         return normalized;
170     }
171 
172     /**
173      * Tests whether the given domain matches any of entry from the public suffix list.
174      */
175     public boolean matches(final String domain) {
176         return matches(domain, null);
177     }
178 
179     /**
180      * Tests whether the given domain matches any of entry from the public suffix list.
181      *
182      * @param domain
183      * @param expectedType expected domain type or {@code null} if any.
184      * @return {@code true} if the given domain matches any of the public suffixes.
185      *
186      * @since 4.5
187      */
188     public boolean matches(final String domain, final DomainType expectedType) {
189         if (domain == null) {
190             return false;
191         }
192         final String domainRoot = getDomainRoot(
193                 domain.startsWith(".") ? domain.substring(1) : domain, expectedType);
194         return domainRoot == null;
195     }
196 
197 }