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         if (domainType == null) {
107             return false;
108         } else {
109             return expectedType == null || domainType.equals(expectedType);
110         }
111     }
112 
113     private boolean hasRule(final String rule, final DomainType expectedType) {
114         return hasEntry(this.rules, rule, expectedType);
115     }
116 
117     private boolean hasException(final String exception, final DomainType expectedType) {
118         return hasEntry(this.exceptions, exception, expectedType);
119     }
120 
121     /**
122      * Returns registrable part of the domain for the given domain name or {@code null}
123      * if given domain represents a public suffix.
124      *
125      * @param domain
126      * @return domain root
127      */
128     public String getDomainRoot(final String domain) {
129         return getDomainRoot(domain, null);
130     }
131 
132     /**
133      * Returns registrable part of the domain for the given domain name or {@code null}
134      * if given domain represents a public suffix.
135      *
136      * @param domain
137      * @param expectedType expected domain type or {@code null} if any.
138      * @return domain root
139      *
140      * @since 4.5
141      */
142     public String getDomainRoot(final String domain, final DomainType expectedType) {
143         if (domain == null) {
144             return null;
145         }
146         if (domain.startsWith(".")) {
147             return null;
148         }
149         String domainName = null;
150         String segment = domain.toLowerCase(Locale.ROOT);
151         while (segment != null) {
152 
153             // An exception rule takes priority over any other matching rule.
154             if (hasException(IDN.toUnicode(segment), expectedType)) {
155                 return segment;
156             }
157 
158             if (hasRule(IDN.toUnicode(segment), expectedType)) {
159                 break;
160             }
161 
162             final int nextdot = segment.indexOf('.');
163             final String nextSegment = nextdot != -1 ? segment.substring(nextdot + 1) : null;
164 
165             if (nextSegment != null) {
166                 if (hasRule("*." + IDN.toUnicode(nextSegment), expectedType)) {
167                     break;
168                 }
169             }
170             if (nextdot != -1) {
171                 domainName = segment;
172             }
173             segment = nextSegment;
174         }
175         return domainName;
176     }
177 
178     /**
179      * Tests whether the given domain matches any of entry from the public suffix list.
180      */
181     public boolean matches(final String domain) {
182         return matches(domain, null);
183     }
184 
185     /**
186      * Tests whether the given domain matches any of entry from the public suffix list.
187      *
188      * @param domain
189      * @param expectedType expected domain type or {@code null} if any.
190      * @return {@code true} if the given domain matches any of the public suffixes.
191      *
192      * @since 4.5
193      */
194     public boolean matches(final String domain, final DomainType expectedType) {
195         if (domain == null) {
196             return false;
197         }
198         final String domainRoot = getDomainRoot(
199                 domain.startsWith(".") ? domain.substring(1) : domain, expectedType);
200         return domainRoot == null;
201     }
202 
203 }