View Javadoc

1   package com.atlassian.ip;
2   
3   import java.net.InetAddress;
4   import java.net.UnknownHostException;
5   import java.util.Arrays;
6   import java.util.HashSet;
7   import java.util.Set;
8   
9   /**
10   * A Flexible, immutable, IP address matcher.
11   *
12   * Matches IP addresses against an internal database of IP addresses and IP
13   * subnets.
14   * <p>
15   * Performs DNS lookups only when adding hosts names using {@link Builder#addPatternOrHost(String)}.
16   * If using host names, instances of this class should be regularly discarded
17   * as any changes to DNS will not be picked up after the instance has been
18   * created.
19   * <p>
20   * Example usage:
21   * <pre>{@code
22   *   final IPMatcher ipMatcher = IPMatcher.builder()
23   *           .addPatternOrHost("example.org")
24   *           .addPattern("192.168.1.0/24")
25   *           .build();
26   *   ipMatcher.match("192.168.1.13");}</pre>
27   */
28  public class IPMatcher
29  {
30      private final Set<Subnet> subnets;
31  
32      private IPMatcher(Set<Subnet> subnets)
33      {
34          this.subnets = new HashSet<Subnet>(subnets);
35      }
36  
37      /**
38       * Returns true if the given ip address matches one of the stored patterns.
39       *
40       * @param ipAddress ip address to match
41       * @return true if the given ip address matches
42       * @throws IllegalArgumentException if the argument is not a valid IP string literal
43       */
44      public boolean matches(String ipAddress)
45      {
46          return matches(InetAddresses.forString(ipAddress));
47      }
48  
49      /**
50       * Returns true if the given ip address matches one of the stored patterns.
51       *
52       * @param ipAddress ip address to match
53       * @return true if the given ip address matches
54       */
55      public boolean matches(InetAddress ipAddress)
56      {
57          for (Subnet subnet : subnets)
58          {
59              if (matches(subnet, ipAddress))
60              {
61                  return true;
62              }
63          }
64  
65          return false;
66      }
67  
68      private boolean matches(Subnet subnet, InetAddress ipAddress)
69      {
70          // Code based on:
71          // https://fisheye.springsource.org/browse/~raw,r=4de8b84b0d685c8adab5b3fb93c08bcf518708a9/spring-security/web/src/main/java/org/springframework/security/web/util/IpAddressMatcher.java
72  
73          int nMaskBits = subnet.getMask();
74          int oddBits = nMaskBits % 8;
75          int nMaskBytes = nMaskBits / 8 + (oddBits == 0 ? 0 : 1);
76          byte[] mask = new byte[nMaskBytes];
77  
78          byte[] allowedIpAddress = subnet.getAddress();
79          byte[] requestIpAddress = ipAddress.getAddress();
80  
81          // If IPs are not both IPv4 or IPv6, we can't compare
82          if (allowedIpAddress.length != requestIpAddress.length)
83          {
84              return false;
85          }
86  
87          Arrays.fill(mask, 0, oddBits == 0 ? mask.length : mask.length - 1, (byte) 0xFF);
88  
89          if (oddBits != 0)
90          {
91              int finalByte = (1 << oddBits) - 1;
92              finalByte <<= 8 - oddBits;
93              mask[mask.length - 1] = (byte) finalByte;
94          }
95  
96          for (int i = 0; i < mask.length; i++)
97          {
98              if ((allowedIpAddress[i] & mask[i]) != (requestIpAddress[i] & mask[i]))
99              {
100                 return false;
101             }
102         }
103 
104         return true;
105     }
106 
107     public static boolean isValidPatternOrHost(String patternOrHost)
108     {
109         if (patternOrHost == null || patternOrHost.trim().isEmpty())
110         {
111             return false;
112         }
113         return !builder().addPatternOrHost(patternOrHost).subnets.isEmpty();
114     }
115 
116     /**
117      * Returns a new builder.
118      *
119      * @return new builder
120      */
121     public static Builder builder()
122     {
123         return new Builder();
124     }
125 
126     /**
127      * A builder for creating immutable IPMatcher instances.
128      *
129      * Builder instances can be reused; it is safe to call {@link #build()}
130      * multiple times.
131      */
132     public static class Builder
133     {
134         private final Set<Subnet> subnets = new HashSet<Subnet>();
135 
136         private Builder() {}
137 
138         /**
139          * Adds a new pattern or host to be matched against. DNS lookup
140          * is performed for hosts in order to get related IP addresses.
141          *
142          * Patterns can be IPv4/IPv6 addresses, subnets in both asterisk and
143          * CIDR notation, or hostnames. The following patterns are valid ip
144          * addresses or subnets:
145          * <pre>
146          * 192.168.1.10
147          * ::10
148          * 192.168.1.*
149          * 192.168.5.128/26
150          * 0:0:0:7b::/64
151          * </pre>
152          *
153          * If a pattern is not a valid ip address or subnet, it will be treated as a hostname.
154          *
155          * @param patternOrHost pattern to be matched against
156          * @throws IllegalArgumentException if the pattern is <tt>null</tt> or empty
157          */
158         public Builder addPatternOrHost(String patternOrHost)
159         {
160             if (patternOrHost == null || patternOrHost.length() == 0)
161             {
162                 throw new IllegalArgumentException("You cannot add an empty pattern or host");
163             }
164 
165             try
166             {
167                 subnets.add(Subnet.forPattern(patternOrHost));
168             }
169             catch (IllegalArgumentException e)
170             {
171 
172                 try
173                 {
174                     for (InetAddress address : getAllByName(patternOrHost))
175                     {
176                         subnets.add(Subnet.forAddress(address));
177                     }
178                 }
179                 catch (UnknownHostException e1)
180                 {
181                     // Could not find any ip addresses for the hostname.
182                 }
183             }
184 
185             return this;
186         }
187 
188         /**
189          * Adds a new pattern to be matched against.
190          *
191          * Patterns can be IPv4/IPv6 addresses or subnets in both asterisk and CIDR
192          * notation. Examples of valid patterns:
193          * <pre>
194          * 192.168.1.10
195          * ::10
196          * 192.168.1.*
197          * 192.168.5.128/26
198          * 0:0:0:7b::/64
199          * </pre>
200          *
201          * @param pattern pattern to be matched against
202          * @throws IllegalArgumentException if the pattern is <tt>null</tt> or empty
203          */
204         public Builder addPattern(String pattern)
205         {
206             if (pattern == null || pattern.length() == 0)
207             {
208                 throw new IllegalArgumentException("You cannot add an empty pattern");
209             }
210 
211             subnets.add(Subnet.forPattern(pattern));
212 
213             return this;
214         }
215 
216         /**
217          * Adds a new subnet to be matched against.
218          *
219          * @param subnet non-null subnet to be matched against
220          */
221         public Builder addSubnet(Subnet subnet)
222         {
223             if (subnet == null)
224             {
225                 throw new NullPointerException("Subnet is null");
226             }
227 
228             subnets.add(subnet);
229 
230             return this;
231         }
232 
233         /**
234          * Return a newly-created <tt>IPMatcher</tt> based on the contents of
235          * the <tt>Builder</tt>.
236          *
237          * @return
238          */
239         public IPMatcher build()
240         {
241             return new IPMatcher(subnets);
242         }
243 
244         // Abstracted out to allow mocking
245         InetAddress[] getAllByName(String host) throws UnknownHostException
246         {
247             return InetAddress.getAllByName(host);
248         }
249     }
250 }
251