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