View | Details | Raw Unified | Return to bug 47330
Collapse All | Expand All

(-)java/org/apache/catalina/connector/RemoteIpValve.java (+528 lines)
Line 0 Link Here
1
/*
2
 * Licensed to the Apache Software Foundation (ASF) under one or more
3
 * contributor license agreements.  See the NOTICE file distributed with
4
 * this work for additional information regarding copyright ownership.
5
 * The ASF licenses this file to You under the Apache License, Version 2.0
6
 * (the "License"); you may not use this file except in compliance with
7
 * the License.  You may obtain a copy of the License at
8
 * 
9
 *      http://www.apache.org/licenses/LICENSE-2.0
10
 * 
11
 * Unless required by applicable law or agreed to in writing, software
12
 * distributed under the License is distributed on an "AS IS" BASIS,
13
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
 * See the License for the specific language governing permissions and
15
 * limitations under the License.
16
 */
17
18
package org.apache.catalina.connector;
19
20
import java.io.IOException;
21
import java.util.ArrayList;
22
import java.util.Iterator;
23
import java.util.LinkedList;
24
import java.util.List;
25
import java.util.regex.Pattern;
26
import java.util.regex.PatternSyntaxException;
27
28
import javax.servlet.ServletException;
29
30
import org.apache.catalina.util.StringManager;
31
import org.apache.catalina.valves.Constants;
32
import org.apache.catalina.valves.RequestFilterValve;
33
import org.apache.catalina.valves.ValveBase;
34
import org.apache.juli.logging.Log;
35
import org.apache.juli.logging.LogFactory;
36
37
/**
38
 * <p>
39
 * Tomcat port of <a href="http://httpd.apache.org/docs/trunk/mod/mod_remoteip.html">mod_remoteip</a>, this valve replaces the apparent
40
 * client remote IP address and hostname for the request with the IP address list presented by a proxy or a load balancer via a request
41
 * headers.
42
 * </p>
43
 * <p>
44
 * This valve proceeds as follows:
45
 * <ul>
46
 * <li>Check if the incoming <code>request.getRemoteAddr()</code> matches the valve's list of internal proxies.</li>
47
 * <li>If so, loop on the comma delimited list of IPs and hostnames passed by the preceding load balancer or proxy in the given request's
48
 * Http header named <code>remoteIPHeader</code> (default value <code>x-forwarded-for</code>). Values are processed in Right-to-Left order.</li>
49
 * <li>For each ip/host of the list:
50
 * <ul>
51
 * <li>if it matches the internal proxies list, the ip/host is swallowed</li>
52
 * <li>if it matches the trusted proxies list, the ip/host is added to the created proxies header</li>
53
 * <li>otherwise, the ip/host is declared to be the remote ip and looping is stopped.</li>
54
 * </ul>
55
 * </li>
56
 * </ul>
57
 * </p>
58
 * <p>
59
 * <strong>Configuration parameters:</strong>
60
 * <table border="1">
61
 * <tr>
62
 * <th>RemoteIpValve property</th>
63
 * <th>Equivalent mod_remoteip directive</th>
64
 * <th>Format</th>
65
 * <th>Default Value</th>
66
 * </tr>
67
 * <tr>
68
 * <td>remoteIPHeader</td>
69
 * <td>RemoteIPHeader</td>
70
 * <td>Compliant http header string</td>
71
 * <td>x-forwarded-for</td>
72
 * </tr>
73
 * <tr>
74
 * <td>internalProxies</td>
75
 * <td>RemoteIPInternalProxy</td>
76
 * <td>Comma delimited list of regular expressions (in the syntax supported by the {@link java.util.regex.Pattern} library)</td>
77
 * <td>10\.\d{1,3}\.\d{1,3}\.\d{1,3}, 192\.168\.\d{1,3}\.\d{1,3}, 169\.254\.\d{1,3}\.\d{1,3}, 127\.\d{1,3}\.\d{1,3}\.\d{1,3} <br/>
78
 * By default, 10/8, 192.168/16, 169.254/16 and 127/8 are allowed ; 172.16/12 has not been enabled by default because it is complex to
79
 * describe with regular expressions</td>
80
 * </tr>
81
 * </tr>
82
 * <tr>
83
 * <td>proxiesHeader</td>
84
 * <td>RemoteIPProxiesHeader</td>
85
 * <td>Compliant http header String</td>
86
 * <td>x-forwarded-by</td>
87
 * </tr>
88
 * <tr>
89
 * <td>trustedProxies</td>
90
 * <td>RemoteIPTrustedProxy</td>
91
 * <td>Comma delimited list of regular expressions (in the syntax supported by the {@link java.util.regex.Pattern} library)</td>
92
 * <td>&nbsp;</td>
93
 * </tr>
94
 * </table>
95
 * </p>
96
 * <p>
97
 * <p>
98
 * This Valve may be attached to any Container, depending on the granularity of the filtering you wish to perform.
99
 * </p>
100
 * <p>
101
 * <strong>Regular expression vs. IP address blocks:</strong> <code>mod_remoteip</code> allows to use address blocks (e.g.
102
 * <code>192.168/16</code>) to configure <code>RemoteIPInternalProxy</code> and <code>RemoteIPTrustedProxy</code> ; as Tomcat doesn't have a
103
 * library similar to <a
104
 * href="http://apr.apache.org/docs/apr/1.3/group__apr__network__io.html#gb74d21b8898b7c40bf7fd07ad3eb993d">apr_ipsubnet_test</a>,
105
 * <code>RemoteIpValve</code> uses regular expression to configure <code>internalProxies</code> and <code>trustedProxies</code> in the same
106
 * fashion as {@link RequestFilterValve} does.
107
 * </p>
108
 * <p>
109
 * <strong>Package org.apache.catalina.connector vs. org.apache.catalina.valves</strong>: This valve is
110
 * temporarily located in <code>org.apache.catalina.connector</code> package instead of <code>org.apache.catalina.valves</code> because it
111
 * uses <code>protected</code> visibility of {@link Request#remoteAddr} and {@link Request#remoteHost}. This valve could move to
112
 * <code>org.apache.catalina.valves</code> if {@link Request#setRemoteAddr(String)} and {@link Request#setRemoteHost(String)} were modified
113
 * to no longer be no-op but actually set the underlying property.
114
 * </p>
115
 * <hr/>
116
 * <p>
117
 * <strong>Sample with trusted proxies</strong>
118
 * </p>
119
 * <p>
120
 * RemoteIpValve configuration:
121
 * </p>
122
 * <code><pre>
123
 * &lt;Valve 
124
 *   className="org.apache.catalina.connector.RemoteIpValve"
125
 *   allowedInternalProxies="192\.168\.0\.10, 192\.168\.0\.11"
126
 *   remoteIPHeader="x-forwarded-for"
127
 *   remoteIPProxiesHeader="x-forwarded-by"
128
 *   trustedProxies="proxy1, proxy2"
129
 *   /&gt;</pre></code>
130
 * <p>
131
 * Request values:
132
 * <table border="1">
133
 * <tr>
134
 * <th>property</th>
135
 * <th>Value Before RemoteIpValve</th>
136
 * <th>Value After RemoteIpValve</th>
137
 * </tr>
138
 * <tr>
139
 * <td>request.remoteAddr</td>
140
 * <td>192.168.0.10</td>
141
 * <td>140.211.11.130</td>
142
 * </tr>
143
 * <tr>
144
 * <td>request.header['x-forwarded-for']</td>
145
 * <td>140.211.11.130, proxy1, proxy2</td>
146
 * <td>null</td>
147
 * </tr>
148
 * <tr>
149
 * <td>request.header['x-forwarded-by']</td>
150
 * <td>null</td>
151
 * <td>proxy1, proxy2</td>
152
 * </tr>
153
 * </table>
154
 * Note : <code>proxy1</code> and <code>proxy2</code> are both trusted proxies that come in <code>x-forwarded-for</code> header, they both
155
 * are migrated in <code>x-forwarded-by</code> header. <code>x-forwarded-by</code> is null because all the proxies are trusted or internal.
156
 * </p>
157
 * <hr/>
158
 * <p>
159
 * <strong>Sample with internal and trusted proxies</strong>
160
 * </p>
161
 * <p>
162
 * RemoteIpValve configuration:
163
 * </p>
164
 * <code><pre>
165
 * &lt;Valve 
166
 *   className="org.apache.catalina.connector.RemoteIpValve"
167
 *   allowedInternalProxies="192\.168\.0\.10, 192\.168\.0\.11"
168
 *   remoteIPHeader="x-forwarded-for"
169
 *   remoteIPProxiesHeader="x-forwarded-by"
170
 *   trustedProxies="proxy1, proxy2"
171
 *   /&gt;</pre></code>
172
 * <p>
173
 * Request values:
174
 * <table border="1">
175
 * <tr>
176
 * <th>property</th>
177
 * <th>Value Before RemoteIpValve</th>
178
 * <th>Value After RemoteIpValve</th>
179
 * </tr>
180
 * <tr>
181
 * <td>request.remoteAddr</td>
182
 * <td>192.168.0.10</td>
183
 * <td>140.211.11.130</td>
184
 * </tr>
185
 * <tr>
186
 * <td>request.header['x-forwarded-for']</td>
187
 * <td>140.211.11.130, proxy1, proxy2, 192.168.0.10</td>
188
 * <td>null</td>
189
 * </tr>
190
 * <tr>
191
 * <td>request.header['x-forwarded-by']</td>
192
 * <td>null</td>
193
 * <td>proxy1, proxy2</td>
194
 * </tr>
195
 * </table>
196
 * Note : <code>proxy1</code> and <code>proxy2</code> are both trusted proxies that come in <code>x-forwarded-for</code> header, they both
197
 * are migrated in <code>x-forwarded-by</code> header. As <code>192.168.0.10</code> is an internal proxy, it does not appear in
198
 * <code>x-forwarded-by</code>. <code>x-forwarded-by</code> is null because all the proxies are trusted or internal.
199
 * </p>
200
 * <hr/>
201
 * <p>
202
 * <strong>Sample with internal proxies</strong>
203
 * </p>
204
 * <p>
205
 * RemoteIpValve configuration:
206
 * </p>
207
 * <code><pre>
208
 * &lt;Valve 
209
 *   className="org.apache.catalina.connector.RemoteIpValve"
210
 *   allowedInternalProxies="192\.168\.0\.10, 192\.168\.0\.11"
211
 *   remoteIPHeader="x-forwarded-for"
212
 *   remoteIPProxiesHeader="x-forwarded-by"
213
 *   /&gt;</pre></code>
214
 *
215
 * <p>
216
 * Request values:
217
 * <table border="1">
218
 * <tr>
219
 * <th>property</th>
220
 * <th>Value Before RemoteIpValve</th>
221
 * <th>Value After RemoteIpValve</th>
222
 * </tr>
223
 * <tr>
224
 * <td>request.remoteAddr</td>
225
 * <td>192.168.0.10</td>
226
 * <td>140.211.11.130</td>
227
 * </tr>
228
 * <tr>
229
 * <td>request.header['x-forwarded-for']</td>
230
 * <td>140.211.11.130, 192.168.0.10</td>
231
 * <td>null</td>
232
 * </tr>
233
 * <tr>
234
 * <td>request.header['x-forwarded-by']</td>
235
 * <td>null</td>
236
 * <td>null</td>
237
 * </tr>
238
 * </table>
239
 * Note : <code>x-forwarded-by</code> header is null because only internal proxies as been traversed by the request.
240
 * <code>x-forwarded-by</code> is null because all the proxies are trusted or internal.
241
 * </p>
242
 * <hr/>
243
 * <p>
244
 * <strong>Sample with an untrusted proxy</strong>
245
 * </p>
246
 * <p>
247
 * RemoteIpValve configuration:
248
 * </p>
249
 * <code><pre>
250
 * &lt;Valve 
251
 *   className="org.apache.catalina.connector.RemoteIpValve"
252
 *   allowedInternalProxies="192\.168\.0\.10, 192\.168\.0\.11"
253
 *   remoteIPHeader="x-forwarded-for"
254
 *   remoteIPProxiesHeader="x-forwarded-by"
255
 *   trustedProxies="proxy1, proxy2"
256
 *   /&gt;</pre></code>
257
 * <p>
258
 * Request values:
259
 * <table border="1">
260
 * <tr>
261
 * <th>property</th>
262
 * <th>Value Before RemoteIpValve</th>
263
 * <th>Value After RemoteIpValve</th>
264
 * </tr>
265
 * <tr>
266
 * <td>request.remoteAddr</td>
267
 * <td>192.168.0.10</td>
268
 * <td>untrusted-proxy</td>
269
 * </tr>
270
 * <tr>
271
 * <td>request.header['x-forwarded-for']</td>
272
 * <td>140.211.11.130, untrusted-proxy, proxy1</td>
273
 * <td>140.211.11.130</td>
274
 * </tr>
275
 * <tr>
276
 * <td>request.header['x-forwarded-by']</td>
277
 * <td>null</td>
278
 * <td>proxy1</td>
279
 * </tr>
280
 * </table>
281
 * Note : <code>x-forwarded-by</code> holds the trusted proxy <code>proxy1</code>. <code>x-forwarded-by</code> holds
282
 * <code>140.211.11.130</code> because <code>untrusted-proxy</code> is not trusted and thus, we can not trust that
283
 * <code>untrusted-proxy</code> is the actual remote ip. <code>request.remoteAddr</code> is <code>untrusted-proxy</code> that is an IP
284
 * verified by <code>proxy1</code>.
285
 * </p>
286
 * <hr/>
287
 * <p>
288
 * TODO : add "remoteIpValve.syntax" NLSString.
289
 * </p>
290
 */
291
public class RemoteIpValve extends ValveBase {
292
    
293
    /**
294
     * Logger
295
     */
296
    private static Log log = LogFactory.getLog(RemoteIpValve.class);
297
    
298
    /**
299
     * {@link Pattern} for a comma delimited string that support whitespace characters
300
     */
301
    private static final Pattern commaSeparatedValuesPattern = Pattern.compile("\\s*,\\s*");
302
    
303
    /**
304
     * The descriptive information related to this implementation.
305
     */
306
    private static final String info = "org.apache.catalina.connector.RemoteIpValve/1.0";
307
    
308
    /**
309
     * The StringManager for this package.
310
     */
311
    protected static StringManager sm = StringManager.getManager(Constants.Package);
312
    
313
    /**
314
     * Convert a given comma delimited list of regular expressions into an array of compiled {@link Pattern}
315
     */
316
    protected static Pattern[] commaDelimitedListToPatternArray(String commaDelimitedPatterns) {
317
        String[] patterns = commaDelimitedListToStringArray(commaDelimitedPatterns);
318
        List<Pattern> patternsList = new ArrayList<Pattern>();
319
        for (String pattern : patterns) {
320
            try {
321
                patternsList.add(Pattern.compile(pattern));
322
            } catch (PatternSyntaxException e) {
323
                throw new IllegalArgumentException(sm.getString("remoteIpValve.syntax", pattern), e);
324
            }
325
        }
326
        return patternsList.toArray(new Pattern[0]);
327
    }
328
    
329
    /**
330
     * Convert a given comma delimited list of regular expressions into an array of String
331
     */
332
    protected static String[] commaDelimitedListToStringArray(String commaDelimitedStrings) {
333
        return (commaDelimitedStrings == null || commaDelimitedStrings.length() == 0) ? new String[0] : commaSeparatedValuesPattern
334
            .split(commaDelimitedStrings);
335
    }
336
    
337
    /**
338
     * Convert an array of strings in a comma delimited string
339
     */
340
    protected static String listToCommaDelimitedString(List<String> stringList) {
341
        if (stringList == null) {
342
            return "";
343
        }
344
        StringBuilder result = new StringBuilder();
345
        for (Iterator<String> it = stringList.iterator(); it.hasNext();) {
346
            String element = it.next();
347
            if (element != null) {
348
                result.append(element);
349
                if (it.hasNext()) {
350
                    result.append(", ");
351
                }
352
            }
353
        }
354
        return result.toString();
355
    }
356
    
357
    /**
358
     * Return <code>true</code> if the given <code>str</code> matches at least one of the given <code>patterns</code>.
359
     */
360
    protected static boolean matchesOne(String str, Pattern... patterns) {
361
        for (Pattern pattern : patterns) {
362
            if (pattern.matcher(str).matches()) {
363
                return true;
364
            }
365
        }
366
        return false;
367
    }
368
    
369
    /**
370
     * @see #setInternalProxies(String)
371
     */
372
    private Pattern[] internalProxies = new Pattern[] {
373
        Pattern.compile("10\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}"), Pattern.compile("192\\.168\\.\\d{1,3}\\.\\d{1,3}"),
374
        Pattern.compile("169\\.254\\.\\d{1,3}\\.\\d{1,3}"), Pattern.compile("127\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}")
375
    };
376
    
377
    /**
378
     * @see #setRemoteIPHeader(String)
379
     */
380
    private String remoteIPHeader = "X-Forwarded-For";
381
    
382
    /**
383
     * @see #setProxiesHeader(String)
384
     */
385
    private String proxiesHeader = "X-Forwarded-By";
386
    
387
    /**
388
     * @see RemoteIpValve#setTrustedProxies(String)
389
     */
390
    private Pattern[] trustedProxies = new Pattern[0];
391
    
392
    /**
393
     * Return descriptive information about this Valve implementation.
394
     */
395
    public String getInfo() {
396
        return info;
397
    }
398
    
399
    /**
400
     * {@inheritDoc}
401
     */
402
    @Override
403
    public void invoke(Request request, Response response) throws IOException, ServletException {
404
        final String originalRemoteAddr = request.getRemoteAddr();
405
        final String originalRemoteHost = request.getRemoteHost();
406
        
407
        if (matchesOne(originalRemoteAddr, internalProxies)) {
408
            String remoteIp = null;
409
            // In java 6, proxiesHeaderValue should be declared as a java.util.Deque
410
            LinkedList<String> proxiesHeaderValue = new LinkedList<String>();
411
            
412
            String[] remoteIPHeaderValue = commaDelimitedListToStringArray(request.getHeader(remoteIPHeader));
413
            int idx;
414
            // loop on remoteIPHeaderValue to find the first trusted remote ip and to build the proxies chain
415
            for (idx = remoteIPHeaderValue.length - 1; idx >= 0; idx--) {
416
                String currentRemoteIp = remoteIPHeaderValue[idx];
417
                remoteIp = currentRemoteIp;
418
                if (matchesOne(currentRemoteIp, internalProxies)) {
419
                    // do nothing, internalProxies IPs are not appended to the
420
                } else if (matchesOne(currentRemoteIp, trustedProxies)) {
421
                    proxiesHeaderValue.addFirst(currentRemoteIp);
422
                } else {
423
                    idx--; // decrement idx because break statement doesn't do it
424
                    break;
425
                }
426
            }
427
            // continue to loop on remoteIPHeaderValue to build the new value of the remoteIPHeader
428
            LinkedList<String> newRemoteIpHeaderValue = new LinkedList<String>();
429
            for (; idx >= 0; idx--) {
430
                String currentRemoteIp = remoteIPHeaderValue[idx];
431
                newRemoteIpHeaderValue.addFirst(currentRemoteIp);
432
            }
433
            if (remoteIp != null) {
434
                if (log.isInfoEnabled()) {
435
                    log.debug("Overwrite remoteAddr '" + request.remoteAddr + "' and remoteHost '" + request.remoteHost + "' by remoteIp '"
436
                              + remoteIp + "' for incoming '" + remoteIPHeader + "' : '" + request.getHeader(remoteIPHeader) + "'");
437
                }
438
                
439
                // use field access instead of setters because request.setRemoteAddr(str) and request.setRemoteHost() are no-op in Tomcat 6.0
440
                request.remoteAddr = remoteIp;
441
                request.remoteHost = remoteIp;
442
                
443
                // use request.coyoteRequest.mimeHeaders.setValue(str).setString(str) because request.addHeader(str, str) is no-op in Tomcat 6.0
444
                if (proxiesHeaderValue.size() == 0) {
445
                    request.getCoyoteRequest().getMimeHeaders().removeHeader(proxiesHeader);
446
                } else {
447
                    String commaDelimitedListOfProxies = listToCommaDelimitedString(proxiesHeaderValue);
448
                    request.getCoyoteRequest().getMimeHeaders().setValue(proxiesHeader).setString(commaDelimitedListOfProxies);
449
                }
450
                if (newRemoteIpHeaderValue.size() == 0) {
451
                    request.getCoyoteRequest().getMimeHeaders().removeHeader(remoteIPHeader);
452
                } else {
453
                    String commaDelimitedRemoteIpHeaderValue = listToCommaDelimitedString(newRemoteIpHeaderValue);
454
                    request.getCoyoteRequest().getMimeHeaders().setValue(remoteIPHeader).setString(commaDelimitedRemoteIpHeaderValue);
455
                }
456
            }
457
        }
458
        try {
459
            getNext().invoke(request, response);
460
        } finally {
461
            // use field access instead of setters because setters are no-op in Tomcat 6.0
462
            request.remoteAddr = originalRemoteAddr;
463
            request.remoteHost = originalRemoteHost;
464
        }
465
    }
466
    
467
    /**
468
     * <p>
469
     * Comma delimited list of internal proxies. Can be expressed with regular expressions.
470
     * </p>
471
     * <p>
472
     * Default value : 10\.\d{1,3}\.\d{1,3}\.\d{1,3}, 192\.168\.\d{1,3}\.\d{1,3}, 127\.\d{1,3}\.\d{1,3}\.\d{1,3}
473
     * </p>
474
     */
475
    public void setInternalProxies(String commaAllowedInternalProxies) {
476
        this.internalProxies = commaDelimitedListToPatternArray(commaAllowedInternalProxies);
477
    }
478
    
479
    /**
480
     * <p>
481
     * Name of the http header from which the remote ip is extracted.
482
     * </p>
483
     * <p>
484
     * The value of this header can be comma delimited.
485
     * </p>
486
     * <p>
487
     * Default value : <code>X-Forwarded-For</code>
488
     * </p>
489
     * 
490
     * @param remoteIPHeader
491
     */
492
    public void setRemoteIPHeader(String remoteIPHeader) {
493
        this.remoteIPHeader = remoteIPHeader;
494
    }
495
    
496
    /**
497
     * <p>
498
     * The proxiesHeader directive specifies a header into which mod_remoteip will collect a list of all of the intermediate client IP
499
     * addresses trusted to resolve the actual remote IP. Note that intermediate RemoteIPTrustedProxy addresses are recorded in this header,
500
     * while any intermediate RemoteIPInternalProxy addresses are discarded.
501
     * </p>
502
     * <p>
503
     * Name of the http header that holds the list of trusted proxies that has been traversed by the http request.
504
     * </p>
505
     * <p>
506
     * The value of this header can be comma delimited.
507
     * </p>
508
     * <p>
509
     * Default value : <code>X-Forwarded-By</code>
510
     * </p>
511
     */
512
    public void setProxiesHeader(String proxiesHeader) {
513
        this.proxiesHeader = proxiesHeader;
514
    }
515
    
516
    /**
517
     * <p>
518
     * Comma delimited list of proxies that are trusted when they appear in the {@link #remoteIPHeader} header. Can be expressed as a
519
     * regular expression.
520
     * </p>
521
     * <p>
522
     * Default value : empty list, no external proxy is trusted.
523
     * </p>
524
     */
525
    public void setTrustedProxies(String commaDelimitedTrustedProxies) {
526
        this.trustedProxies = commaDelimitedListToPatternArray(commaDelimitedTrustedProxies);
527
    }
528
}
(-)test/org/apache/catalina/connector/RemoteIpValveTest.java (+379 lines)
Line 0 Link Here
1
/*
2
 * Licensed to the Apache Software Foundation (ASF) under one or more
3
 * contributor license agreements.  See the NOTICE file distributed with
4
 * this work for additional information regarding copyright ownership.
5
 * The ASF licenses this file to You under the Apache License, Version 2.0
6
 * (the "License"); you may not use this file except in compliance with
7
 * the License.  You may obtain a copy of the License at
8
 * 
9
 *      http://www.apache.org/licenses/LICENSE-2.0
10
 * 
11
 * Unless required by applicable law or agreed to in writing, software
12
 * distributed under the License is distributed on an "AS IS" BASIS,
13
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
 * See the License for the specific language governing permissions and
15
 * limitations under the License.
16
 */
17
18
package org.apache.catalina.connector;
19
20
import static org.junit.Assert.*;
21
22
import java.io.IOException;
23
import java.util.ArrayList;
24
import java.util.Arrays;
25
import java.util.List;
26
27
import javax.servlet.ServletException;
28
29
import org.apache.catalina.valves.ValveBase;
30
import org.junit.Test;
31
32
/**
33
 * {@link RemoteIpValve} Tests
34
 */
35
public class RemoteIpValveTest {
36
    
37
    static class RemoteAddrAndHostTrackerValve extends ValveBase {
38
        private String remoteAddr;
39
        private String remoteHost;
40
        
41
        public String getRemoteAddr() {
42
            return remoteAddr;
43
        }
44
        
45
        public String getRemoteHost() {
46
            return remoteHost;
47
        }
48
        
49
        @Override
50
        public void invoke(Request request, Response response) throws IOException, ServletException {
51
            this.remoteHost = request.getRemoteHost();
52
            this.remoteAddr = request.getRemoteAddr();
53
        }
54
    }
55
    
56
    @Test
57
    public void testCommaDelimitedListToStringArray() {
58
        List<String> elements = Arrays.asList("element1", "element2", "element3");
59
        String actual = RemoteIpValve.listToCommaDelimitedString(elements);
60
        assertEquals("element1, element2, element3", actual);
61
    }
62
    
63
    @Test
64
    public void testCommaDelimitedListToStringArrayEmptyList() {
65
        List<String> elements = new ArrayList<String>();
66
        String actual = RemoteIpValve.listToCommaDelimitedString(elements);
67
        assertEquals("", actual);
68
    }
69
    
70
    @Test
71
    public void testCommaDelimitedListToStringArrayNullList() {
72
        String actual = RemoteIpValve.listToCommaDelimitedString(null);
73
        assertEquals("", actual);
74
    }
75
    
76
    @Test
77
    public void testInvokeAllowedRemoteAddrWithNullRemoteIpHeader() throws Exception {
78
        // PREPARE
79
        RemoteIpValve remoteIpValve = new RemoteIpValve();
80
        remoteIpValve.setInternalProxies("192\\.168\\.0\\.10, 192\\.168\\.0\\.11");
81
        remoteIpValve.setTrustedProxies("proxy1, proxy2, proxy3");
82
        remoteIpValve.setRemoteIPHeader("x-forwarded-for");
83
        remoteIpValve.setProxiesHeader("x-forwarded-by");
84
        RemoteAddrAndHostTrackerValve remoteAddrAndHostTrackerValve = new RemoteAddrAndHostTrackerValve();
85
        remoteIpValve.setNext(remoteAddrAndHostTrackerValve);
86
        
87
        Request request = new Request();
88
        request.setCoyoteRequest(new org.apache.coyote.Request());
89
        request.remoteAddr = "192.168.0.10";
90
        request.remoteHost = "remote-host-original-value";
91
        
92
        // TEST
93
        remoteIpValve.invoke(request, null);
94
        
95
        // VERIFY
96
        String actualXForwardedFor = request.getHeader("x-forwarded-for");
97
        assertNull("x-forwarded-for must be null", actualXForwardedFor);
98
        
99
        String actualXForwardedBy = request.getHeader("x-forwarded-by");
100
        assertNull("x-forwarded-by must be null", actualXForwardedBy);
101
        
102
        String actualRemoteAddr = remoteAddrAndHostTrackerValve.getRemoteAddr();
103
        assertEquals("remoteAddr", "192.168.0.10", actualRemoteAddr);
104
        
105
        String actualRemoteHost = remoteAddrAndHostTrackerValve.getRemoteHost();
106
        assertEquals("remoteHost", "remote-host-original-value", actualRemoteHost);
107
        
108
        String actualPostInvokeRemoteAddr = request.getRemoteAddr();
109
        assertEquals("postInvoke remoteAddr", "192.168.0.10", actualPostInvokeRemoteAddr);
110
        
111
        String actualPostInvokeRemoteHost = request.getRemoteHost();
112
        assertEquals("postInvoke remoteAddr", "remote-host-original-value", actualPostInvokeRemoteHost);
113
        
114
    }
115
    
116
    @Test
117
    public void testInvokeAllProxiesAreTrusted() throws Exception {
118
        
119
        // PREPARE
120
        RemoteIpValve remoteIpValve = new RemoteIpValve();
121
        remoteIpValve.setInternalProxies("192\\.168\\.0\\.10, 192\\.168\\.0\\.11");
122
        remoteIpValve.setTrustedProxies("proxy1, proxy2, proxy3");
123
        remoteIpValve.setRemoteIPHeader("x-forwarded-for");
124
        remoteIpValve.setProxiesHeader("x-forwarded-by");
125
        RemoteAddrAndHostTrackerValve remoteAddrAndHostTrackerValve = new RemoteAddrAndHostTrackerValve();
126
        remoteIpValve.setNext(remoteAddrAndHostTrackerValve);
127
        
128
        Request request = new Request();
129
        request.setCoyoteRequest(new org.apache.coyote.Request());
130
        request.remoteAddr = "192.168.0.10";
131
        request.remoteHost = "remote-host-original-value";
132
        request.getCoyoteRequest().getMimeHeaders().addValue("x-forwarded-for").setString("140.211.11.130, proxy1, proxy2");
133
        
134
        // TEST
135
        remoteIpValve.invoke(request, null);
136
        
137
        // VERIFY
138
        String actualXForwardedFor = request.getHeader("x-forwarded-for");
139
        assertNull("all proxies are trusted, x-forwarded-for must be null", actualXForwardedFor);
140
        
141
        String actualXForwardedBy = request.getHeader("x-forwarded-by");
142
        assertEquals("all proxies are trusted, they must appear in x-forwarded-by", "proxy1, proxy2", actualXForwardedBy);
143
        
144
        String actualRemoteAddr = remoteAddrAndHostTrackerValve.getRemoteAddr();
145
        assertEquals("remoteAddr", "140.211.11.130", actualRemoteAddr);
146
        
147
        String actualRemoteHost = remoteAddrAndHostTrackerValve.getRemoteHost();
148
        assertEquals("remoteHost", "140.211.11.130", actualRemoteHost);
149
        
150
        String actualPostInvokeRemoteAddr = request.getRemoteAddr();
151
        assertEquals("postInvoke remoteAddr", "192.168.0.10", actualPostInvokeRemoteAddr);
152
        
153
        String actualPostInvokeRemoteHost = request.getRemoteHost();
154
        assertEquals("postInvoke remoteAddr", "remote-host-original-value", actualPostInvokeRemoteHost);
155
    }
156
    
157
    @Test
158
    public void testInvokeAllProxiesAreTrustedOrInternal() throws Exception {
159
        
160
        // PREPARE
161
        RemoteIpValve remoteIpValve = new RemoteIpValve();
162
        remoteIpValve.setInternalProxies("192\\.168\\.0\\.10, 192\\.168\\.0\\.11");
163
        remoteIpValve.setTrustedProxies("proxy1, proxy2, proxy3");
164
        remoteIpValve.setRemoteIPHeader("x-forwarded-for");
165
        remoteIpValve.setProxiesHeader("x-forwarded-by");
166
        RemoteAddrAndHostTrackerValve remoteAddrAndHostTrackerValve = new RemoteAddrAndHostTrackerValve();
167
        remoteIpValve.setNext(remoteAddrAndHostTrackerValve);
168
        
169
        Request request = new Request();
170
        request.setCoyoteRequest(new org.apache.coyote.Request());
171
        request.remoteAddr = "192.168.0.10";
172
        request.remoteHost = "remote-host-original-value";
173
        request.getCoyoteRequest().getMimeHeaders().addValue("x-forwarded-for")
174
            .setString("140.211.11.130, proxy1, proxy2, 192.168.0.10, 192.168.0.11");
175
        
176
        // TEST
177
        remoteIpValve.invoke(request, null);
178
        
179
        // VERIFY
180
        String actualXForwardedFor = request.getHeader("x-forwarded-for");
181
        assertNull("all proxies are trusted, x-forwarded-for must be null", actualXForwardedFor);
182
        
183
        String actualXForwardedBy = request.getHeader("x-forwarded-by");
184
        assertEquals("all proxies are trusted, they must appear in x-forwarded-by", "proxy1, proxy2", actualXForwardedBy);
185
        
186
        String actualRemoteAddr = remoteAddrAndHostTrackerValve.getRemoteAddr();
187
        assertEquals("remoteAddr", "140.211.11.130", actualRemoteAddr);
188
        
189
        String actualRemoteHost = remoteAddrAndHostTrackerValve.getRemoteHost();
190
        assertEquals("remoteHost", "140.211.11.130", actualRemoteHost);
191
        
192
        String actualPostInvokeRemoteAddr = request.getRemoteAddr();
193
        assertEquals("postInvoke remoteAddr", "192.168.0.10", actualPostInvokeRemoteAddr);
194
        
195
        String actualPostInvokeRemoteHost = request.getRemoteHost();
196
        assertEquals("postInvoke remoteAddr", "remote-host-original-value", actualPostInvokeRemoteHost);
197
    }
198
    
199
    @Test
200
    public void testInvokeAllProxiesAreInternal() throws Exception {
201
        
202
        // PREPARE
203
        RemoteIpValve remoteIpValve = new RemoteIpValve();
204
        remoteIpValve.setInternalProxies("192\\.168\\.0\\.10, 192\\.168\\.0\\.11");
205
        remoteIpValve.setTrustedProxies("proxy1, proxy2, proxy3");
206
        remoteIpValve.setRemoteIPHeader("x-forwarded-for");
207
        remoteIpValve.setProxiesHeader("x-forwarded-by");
208
        RemoteAddrAndHostTrackerValve remoteAddrAndHostTrackerValve = new RemoteAddrAndHostTrackerValve();
209
        remoteIpValve.setNext(remoteAddrAndHostTrackerValve);
210
        
211
        Request request = new Request();
212
        request.setCoyoteRequest(new org.apache.coyote.Request());
213
        request.remoteAddr = "192.168.0.10";
214
        request.remoteHost = "remote-host-original-value";
215
        request.getCoyoteRequest().getMimeHeaders().addValue("x-forwarded-for").setString("140.211.11.130, 192.168.0.10, 192.168.0.11");
216
        
217
        // TEST
218
        remoteIpValve.invoke(request, null);
219
        
220
        // VERIFY
221
        String actualXForwardedFor = request.getHeader("x-forwarded-for");
222
        assertNull("all proxies are internal, x-forwarded-for must be null", actualXForwardedFor);
223
        
224
        String actualXForwardedBy = request.getHeader("x-forwarded-by");
225
        assertNull("all proxies are internal, x-forwarded-by must be null", actualXForwardedBy);
226
        
227
        String actualRemoteAddr = remoteAddrAndHostTrackerValve.getRemoteAddr();
228
        assertEquals("remoteAddr", "140.211.11.130", actualRemoteAddr);
229
        
230
        String actualRemoteHost = remoteAddrAndHostTrackerValve.getRemoteHost();
231
        assertEquals("remoteHost", "140.211.11.130", actualRemoteHost);
232
        
233
        String actualPostInvokeRemoteAddr = request.getRemoteAddr();
234
        assertEquals("postInvoke remoteAddr", "192.168.0.10", actualPostInvokeRemoteAddr);
235
        
236
        String actualPostInvokeRemoteHost = request.getRemoteHost();
237
        assertEquals("postInvoke remoteAddr", "remote-host-original-value", actualPostInvokeRemoteHost);
238
    }
239
    
240
    @Test
241
    public void testInvokeAllProxiesAreTrustedAndRemoteAddrMatchRegexp() throws Exception {
242
        
243
        // PREPARE
244
        RemoteIpValve remoteIpValve = new RemoteIpValve();
245
        remoteIpValve.setInternalProxies("127\\.0\\.0\\.1, 192\\.168\\..*, another-internal-proxy");
246
        remoteIpValve.setTrustedProxies("proxy1, proxy2, proxy3");
247
        remoteIpValve.setRemoteIPHeader("x-forwarded-for");
248
        remoteIpValve.setProxiesHeader("x-forwarded-by");
249
        RemoteAddrAndHostTrackerValve remoteAddrAndHostTrackerValve = new RemoteAddrAndHostTrackerValve();
250
        remoteIpValve.setNext(remoteAddrAndHostTrackerValve);
251
        
252
        Request request = new Request();
253
        request.setCoyoteRequest(new org.apache.coyote.Request());
254
        request.remoteAddr = "192.168.0.10";
255
        request.remoteHost = "remote-host-original-value";
256
        request.getCoyoteRequest().getMimeHeaders().addValue("x-forwarded-for").setString("140.211.11.130, proxy1, proxy2");
257
        
258
        // TEST
259
        remoteIpValve.invoke(request, null);
260
        
261
        // VERIFY
262
        String actualXForwardedFor = request.getHeader("x-forwarded-for");
263
        assertNull("all proxies are trusted, x-forwarded-for must be null", actualXForwardedFor);
264
        
265
        String actualXForwardedBy = request.getHeader("x-forwarded-by");
266
        assertEquals("all proxies are trusted, they must appear in x-forwarded-by", "proxy1, proxy2", actualXForwardedBy);
267
        
268
        String actualRemoteAddr = remoteAddrAndHostTrackerValve.getRemoteAddr();
269
        assertEquals("remoteAddr", "140.211.11.130", actualRemoteAddr);
270
        
271
        String actualRemoteHost = remoteAddrAndHostTrackerValve.getRemoteHost();
272
        assertEquals("remoteHost", "140.211.11.130", actualRemoteHost);
273
        
274
        String actualPostInvokeRemoteAddr = request.getRemoteAddr();
275
        assertEquals("postInvoke remoteAddr", "192.168.0.10", actualPostInvokeRemoteAddr);
276
        
277
        String actualPostInvokeRemoteHost = request.getRemoteHost();
278
        assertEquals("postInvoke remoteAddr", "remote-host-original-value", actualPostInvokeRemoteHost);
279
    }
280
    
281
    @Test
282
    public void testInvokeNotAllowedRemoteAddr() throws Exception {
283
        // PREPARE
284
        RemoteIpValve remoteIpValve = new RemoteIpValve();
285
        remoteIpValve.setInternalProxies("192\\.168\\.0\\.10, 192\\.168\\.0\\.11");
286
        remoteIpValve.setTrustedProxies("proxy1, proxy2, proxy3");
287
        remoteIpValve.setRemoteIPHeader("x-forwarded-for");
288
        remoteIpValve.setProxiesHeader("x-forwarded-by");
289
        RemoteAddrAndHostTrackerValve remoteAddrAndHostTrackerValve = new RemoteAddrAndHostTrackerValve();
290
        remoteIpValve.setNext(remoteAddrAndHostTrackerValve);
291
        
292
        Request request = new Request();
293
        request.setCoyoteRequest(new org.apache.coyote.Request());
294
        request.remoteAddr = "not-allowed-internal-proxy";
295
        request.remoteHost = "not-allowed-internal-proxy-host";
296
        request.getCoyoteRequest().getMimeHeaders().addValue("x-forwarded-for").setString("140.211.11.130, proxy1, proxy2");
297
        
298
        // TEST
299
        remoteIpValve.invoke(request, null);
300
        
301
        // VERIFY
302
        String actualXForwardedFor = request.getHeader("x-forwarded-for");
303
        assertEquals("x-forwarded-for must be unchanged", "140.211.11.130, proxy1, proxy2", actualXForwardedFor);
304
        
305
        String actualXForwardedBy = request.getHeader("x-forwarded-by");
306
        assertNull("x-forwarded-by must be null", actualXForwardedBy);
307
        
308
        String actualRemoteAddr = remoteAddrAndHostTrackerValve.getRemoteAddr();
309
        assertEquals("remoteAddr", "not-allowed-internal-proxy", actualRemoteAddr);
310
        
311
        String actualRemoteHost = remoteAddrAndHostTrackerValve.getRemoteHost();
312
        assertEquals("remoteHost", "not-allowed-internal-proxy-host", actualRemoteHost);
313
        
314
        String actualPostInvokeRemoteAddr = request.getRemoteAddr();
315
        assertEquals("postInvoke remoteAddr", "not-allowed-internal-proxy", actualPostInvokeRemoteAddr);
316
        
317
        String actualPostInvokeRemoteHost = request.getRemoteHost();
318
        assertEquals("postInvoke remoteAddr", "not-allowed-internal-proxy-host", actualPostInvokeRemoteHost);
319
    }
320
    
321
    @Test
322
    public void testInvokeUntrustedProxyInTheChain() throws Exception {
323
        // PREPARE
324
        RemoteIpValve remoteIpValve = new RemoteIpValve();
325
        remoteIpValve.setInternalProxies("192\\.168\\.0\\.10, 192\\.168\\.0\\.11");
326
        remoteIpValve.setTrustedProxies("proxy1, proxy2, proxy3");
327
        remoteIpValve.setRemoteIPHeader("x-forwarded-for");
328
        remoteIpValve.setProxiesHeader("x-forwarded-by");
329
        RemoteAddrAndHostTrackerValve remoteAddrAndHostTrackerValve = new RemoteAddrAndHostTrackerValve();
330
        remoteIpValve.setNext(remoteAddrAndHostTrackerValve);
331
        
332
        Request request = new Request();
333
        request.setCoyoteRequest(new org.apache.coyote.Request());
334
        request.remoteAddr = "192.168.0.10";
335
        request.remoteHost = "remote-host-original-value";
336
        request.getCoyoteRequest().getMimeHeaders().addValue("x-forwarded-for")
337
            .setString("140.211.11.130, proxy1, untrusted-proxy, proxy2");
338
        
339
        // TEST
340
        remoteIpValve.invoke(request, null);
341
        
342
        // VERIFY
343
        String actualXForwardedFor = request.getHeader("x-forwarded-for");
344
        assertEquals("ip/host before untrusted-proxy must appear in x-forwarded-for", "140.211.11.130, proxy1", actualXForwardedFor);
345
        
346
        String actualXForwardedBy = request.getHeader("x-forwarded-by");
347
        assertEquals("ip/host after untrusted-proxy must appear in  x-forwarded-by", "proxy2", actualXForwardedBy);
348
        
349
        String actualRemoteAddr = remoteAddrAndHostTrackerValve.getRemoteAddr();
350
        assertEquals("remoteAddr", "untrusted-proxy", actualRemoteAddr);
351
        
352
        String actualRemoteHost = remoteAddrAndHostTrackerValve.getRemoteHost();
353
        assertEquals("remoteHost", "untrusted-proxy", actualRemoteHost);
354
        
355
        String actualPostInvokeRemoteAddr = request.getRemoteAddr();
356
        assertEquals("postInvoke remoteAddr", "192.168.0.10", actualPostInvokeRemoteAddr);
357
        
358
        String actualPostInvokeRemoteHost = request.getRemoteHost();
359
        assertEquals("postInvoke remoteAddr", "remote-host-original-value", actualPostInvokeRemoteHost);
360
    }
361
    
362
    @Test
363
    public void testListToCommaDelimitedString() {
364
        String[] actual = RemoteIpValve.commaDelimitedListToStringArray("element1, element2, element3");
365
        String[] expected = new String[] {
366
            "element1", "element2", "element3"
367
        };
368
        assertArrayEquals(expected, actual);
369
    }
370
    
371
    @Test
372
    public void testListToCommaDelimitedStringMixedSpaceChars() {
373
        String[] actual = RemoteIpValve.commaDelimitedListToStringArray("element1  , element2,\t element3");
374
        String[] expected = new String[] {
375
            "element1", "element2", "element3"
376
        };
377
        assertArrayEquals(expected, actual);
378
    }
379
}

Return to bug 47330