View Javadoc

1   //
2   //  ========================================================================
3   //  Copyright (c) 1995-2016 Mort Bay Consulting Pty. Ltd.
4   //  ------------------------------------------------------------------------
5   //  All rights reserved. This program and the accompanying materials
6   //  are made available under the terms of the Eclipse Public License v1.0
7   //  and Apache License v2.0 which accompanies this distribution.
8   //
9   //      The Eclipse Public License is available at
10  //      http://www.eclipse.org/legal/epl-v10.html
11  //
12  //      The Apache License v2.0 is available at
13  //      http://www.opensource.org/licenses/apache2.0.php
14  //
15  //  You may elect to redistribute this code under either of these licenses.
16  //  ========================================================================
17  //
18  
19  package org.eclipse.jetty.server.handler;
20  
21  import java.io.Closeable;
22  import java.io.IOException;
23  import java.net.InetAddress;
24  import java.net.InetSocketAddress;
25  import java.util.ArrayDeque;
26  import java.util.Deque;
27  import java.util.concurrent.CompletableFuture;
28  import java.util.concurrent.ConcurrentHashMap;
29  import java.util.concurrent.ConcurrentMap;
30  import java.util.concurrent.ExecutionException;
31  
32  import javax.servlet.AsyncContext;
33  import javax.servlet.ServletException;
34  import javax.servlet.http.HttpServletRequest;
35  import javax.servlet.http.HttpServletResponse;
36  
37  import org.eclipse.jetty.http.HostPortHttpField;
38  import org.eclipse.jetty.http.HttpField;
39  import org.eclipse.jetty.http.HttpFields;
40  import org.eclipse.jetty.http.HttpHeader;
41  import org.eclipse.jetty.http.QuotedCSV;
42  import org.eclipse.jetty.server.ForwardedRequestCustomizer;
43  import org.eclipse.jetty.server.Request;
44  import org.eclipse.jetty.util.IncludeExcludeSet;
45  import org.eclipse.jetty.util.InetAddressSet;
46  import org.eclipse.jetty.util.StringUtil;
47  import org.eclipse.jetty.util.annotation.ManagedAttribute;
48  import org.eclipse.jetty.util.annotation.ManagedOperation;
49  import org.eclipse.jetty.util.annotation.Name;
50  import org.eclipse.jetty.util.log.Log;
51  import org.eclipse.jetty.util.log.Logger;
52  import org.eclipse.jetty.util.thread.Locker;
53  
54  
55  /**
56   * <p>Handler to limit the threads per IP address for DOS protection</p>
57   * <p>The ThreadLimitHandler applies a limit to the number of Threads 
58   * that can be used simultaneously per remote IP address.
59   * </p>
60   * <p>The handler makes a determination of the remote IP separately to
61   * any that may be made by the {@link ForwardedRequestCustomizer} or similar:
62   * <ul>
63   * <li>This handler will use either only a single style
64   * of forwarded header.   This is on the assumption that a trusted local proxy
65   * will produce only a single forwarded header and that any additional
66   * headers are likely from untrusted client side proxies.</li>
67   * <li>If multiple instances of a forwarded header are provided, this
68   * handler will use the right-most instance, which will have been set from
69   * the trusted local proxy</li>
70   * </ul>
71   * Requests in excess of the limit will be asynchronously suspended until
72   * a thread is available.  
73   * <p>This is a simpler alternative to DosFilter</p>
74   */
75  public class ThreadLimitHandler extends HandlerWrapper
76  {
77      private static final Logger LOG = Log.getLogger(ThreadLimitHandler.class);
78  
79      private final static String REMOTE = "o.e.j.s.h.TLH.REMOTE";
80      private final static String PERMIT = "o.e.j.s.h.TLH.PASS";
81      private final boolean _rfc7239;
82      private final String _forwardedHeader;
83      private final IncludeExcludeSet<String, InetAddress> _includeExcludeSet = new IncludeExcludeSet<>(InetAddressSet.class);
84      private final ConcurrentMap<String, Remote> _remotes = new ConcurrentHashMap<>();
85      private volatile boolean _enabled;
86      private int _threadLimit=10;
87  
88      public ThreadLimitHandler()
89      {
90          this(null,false);
91      }
92      
93      public ThreadLimitHandler(@Name("forwardedHeader") String forwardedHeader)
94      {
95          this(forwardedHeader,HttpHeader.FORWARDED.is(forwardedHeader));
96      }
97  
98      public ThreadLimitHandler(@Name("forwardedHeader") String forwardedHeader, @Name("rfc7239")  boolean rfc7239)
99      {
100         super();
101         _rfc7239 = rfc7239;
102         _forwardedHeader = forwardedHeader;
103         _enabled = true;
104     }
105     
106     @Override
107     protected void doStart() throws Exception
108     {
109         super.doStart();
110         LOG.info(String.format("ThreadLimitHandler enable=%b limit=%d include=%s",_enabled,_threadLimit,_includeExcludeSet));
111     }
112 
113     @ManagedAttribute("true if this handler is enabled")
114     public boolean isEnabled()
115     {
116         return _enabled;
117     }
118 
119     public void setEnabled(boolean enabled)
120     {
121         _enabled = enabled;
122         LOG.info(String.format("ThreadLimitHandler enable=%b limit=%d include=%s",_enabled,_threadLimit,_includeExcludeSet));
123     }
124 
125     @ManagedAttribute("The maximum threads that can be dispatched per remote IP")
126     public int getThreadLimit()
127     {
128         return _threadLimit;
129     }
130 
131     public void setThreadLimit(int threadLimit)
132     {
133         if (threadLimit<=0)
134             throw new IllegalArgumentException("limit must be >0");
135         _threadLimit = threadLimit;
136     }
137     
138     @ManagedOperation("Include IP in thread limits")
139     public void include(String inetAddressPattern)
140     {
141         _includeExcludeSet.include(inetAddressPattern);
142     }
143 
144     @ManagedOperation("Exclude IP from thread limits")
145     public void exclude(String inetAddressPattern)
146     {
147         _includeExcludeSet.exclude(inetAddressPattern);
148     }
149     
150     @Override
151     public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
152     {
153         // Allow ThreadLimit to be enabled dynamically without restarting server
154         if (!_enabled)
155         {
156             // if disabled, handle normally
157             super.handle(target,baseRequest,request,response);
158         }
159         else
160         {
161             // Get the remote address of the request
162             Remote remote = getRemote(baseRequest);
163             if (remote==null)
164             {
165                 // if remote is not known, handle normally
166                 super.handle(target,baseRequest,request,response);
167             }
168             else
169             {
170                 // Do we already have a future permit from a previous invocation?
171                 Closeable permit = (Closeable)baseRequest.getAttribute(PERMIT);
172                 try
173                 {
174                     if (permit!=null)
175                     {
176                         // Yes, remove it from any future async cycles.
177                         baseRequest.removeAttribute(PERMIT);
178                     }
179                     else
180                     {
181                         // No, then lets try to acquire one
182                         CompletableFuture<Closeable> future_permit=remote.acquire();
183 
184                         // Did we get a permit?
185                         if (future_permit.isDone())
186                         {
187                             // yes
188                             permit=future_permit.get();
189                         }
190                         else
191                         {
192                             if (LOG.isDebugEnabled())
193                                 LOG.debug("Threadlimited {} {}",remote,target);
194                             // No, lets asynchronously suspend the request
195                             AsyncContext async = baseRequest.startAsync();
196                             // let's never timeout the async.  If this is a DOS, then good to make them wait, if this is not
197                             // then give them maximum time to get a thread.
198                             async.setTimeout(0);
199 
200                             // dispatch the request when we do eventually get a pass
201                             future_permit.thenAccept(c->
202                             {
203                                 baseRequest.setAttribute(PERMIT,c);
204                                 async.dispatch();
205                             });
206                             return;
207                         }
208                     }
209 
210                     // Use the permit
211                     super.handle(target,baseRequest,request,response);
212                 }
213                 catch (InterruptedException | ExecutionException e)
214                 {
215                     throw new ServletException(e);
216                 }
217                 finally
218                 {
219                     if (permit!=null)
220                         permit.close();
221                 }
222             }
223         }
224     }
225     
226     protected int getThreadLimit(String ip)
227     {
228         if (!_includeExcludeSet.isEmpty())
229         {
230             try
231             {
232                 if (!_includeExcludeSet.test(InetAddress.getByName(ip)))
233                 {
234                     LOG.debug("excluded {}",ip);
235                     return 0;
236                 }
237             }
238             catch(Exception e)
239             {
240                 LOG.ignore(e);
241             }
242         }
243         return _threadLimit;
244     }
245 
246     protected Remote getRemote(Request baseRequest)
247     {
248         Remote remote = (Remote)baseRequest.getAttribute(REMOTE);
249         if (remote!=null)
250             return remote;
251         
252         String ip=getRemoteIP(baseRequest);
253         LOG.debug("ip={}",ip);
254         if (ip==null)
255             return null;
256         
257         int limit = getThreadLimit(ip);
258         if (limit<=0)
259             return null;
260 
261         remote = _remotes.get(ip);
262         if (remote==null)
263         {
264             Remote r = new Remote(ip,limit);
265             remote = _remotes.putIfAbsent(ip,r);
266             if (remote==null)
267                 remote = r;
268         }
269         
270         baseRequest.setAttribute(REMOTE,remote);
271 
272         return remote;
273     }
274     
275     
276     protected String getRemoteIP(Request baseRequest)
277     {
278         // Do we have a forwarded header set?
279         if (_forwardedHeader!=null && !_forwardedHeader.isEmpty())
280         {
281             // Yes, then try to get the remote IP from the header
282             String remote = _rfc7239?getForwarded(baseRequest):getXForwardedFor(baseRequest);
283             if (remote!=null && !remote.isEmpty())
284                 return remote;
285         }
286         
287         // If no remote IP from a header, determine it directly from the channel
288         // Do not use the request methods, as they may have been lied to by the 
289         // RequestCustomizer!
290         InetSocketAddress inet_addr = baseRequest.getHttpChannel().getRemoteAddress();
291         if (inet_addr!=null && inet_addr.getAddress()!=null)
292             return inet_addr.getAddress().getHostAddress();
293         return null;
294     }
295 
296     private String getForwarded(Request request)
297     {
298         // Get the right most Forwarded for value.
299         // This is the value from the closest proxy and the only one that
300         // can be trusted.
301         RFC7239 rfc7239 = new RFC7239();
302         HttpFields httpFields = request.getHttpFields();
303         for (HttpField field : httpFields)
304             if (_forwardedHeader.equalsIgnoreCase(field.getName()))
305                 rfc7239.addValue(field.getValue());
306         
307         if (rfc7239.getFor()!=null)
308             return new HostPortHttpField(rfc7239.getFor()).getHost();
309         
310         return null;
311     }
312     
313     private String getXForwardedFor(Request request)
314     {
315         // Get the right most XForwarded-For for value.
316         // This is the value from the closest proxy and the only one that
317         // can be trusted.
318         String forwarded_for = null;
319         HttpFields httpFields = request.getHttpFields();
320         for (HttpField field : httpFields)
321             if (_forwardedHeader.equalsIgnoreCase(field.getName()))
322                 forwarded_for = field.getValue();
323         
324         if (forwarded_for==null || forwarded_for.isEmpty())
325             return null;
326         
327         int comma = forwarded_for.lastIndexOf(',');
328         return (comma>=0)?forwarded_for.substring(comma+1).trim():forwarded_for;
329     }
330 
331 
332     private final class Remote implements Closeable
333     {
334         private final String _ip;
335         private final int _limit;
336         private final Locker _locker = new Locker();
337         private int _permits;
338         private Deque<CompletableFuture<Closeable>> _queue = new ArrayDeque<>();
339         private final CompletableFuture<Closeable> _permitted = CompletableFuture.completedFuture(this);
340         
341         public Remote(String ip, int limit)
342         {
343             _ip=ip;
344             _limit=limit;
345         }
346         
347         public CompletableFuture<Closeable> acquire()
348         {
349             try(Locker.Lock lock = _locker.lock())
350             {
351                 // Do we have available passes?
352                 if (_permits<_limit)
353                 {
354                     // Yes - increment the allocated passes
355                     _permits++;
356                     // return the already completed future
357                     return _permitted; // TODO is it OK to share/reuse this?
358                 }
359                 
360                 // No pass available, so queue a new future 
361                 CompletableFuture<Closeable> pass = new CompletableFuture<Closeable>();
362                 _queue.addLast(pass);
363                 return pass;
364             }
365         }
366         
367         @Override
368         public void close() throws IOException
369         {
370             try(Locker.Lock lock = _locker.lock())
371             {
372                 // reduce the allocated passes
373                 _permits--;
374                 while(true)
375                 {
376                     // Are there any future passes waiting?
377                     CompletableFuture<Closeable> permit = _queue.pollFirst();
378                     
379                     // No - we are done
380                     if (permit==null)
381                         break;
382                     
383                     // Yes - if we can complete them, we are done
384                     if (permit.complete(this))
385                     {
386                         _permits++;
387                         break;
388                     }
389                     
390                     // Somebody else must have completed/failed that future pass,
391                     // so let's try for another.
392                 }
393             }
394         }
395         
396         @Override
397         public String toString()
398         {
399             try(Locker.Lock lock = _locker.lock())
400             {
401                 return String.format("R[ip=%s,p=%d,l=%d,q=%d]",_ip,_permits,_limit,_queue.size());
402             }
403         }
404     }
405 
406     private final class RFC7239 extends QuotedCSV
407     {
408         String _for;
409         
410         private RFC7239()
411         {
412             super(false);
413         }
414         
415         String getFor()
416         {
417             return _for;
418         }
419 
420         @Override
421         protected void parsedParam(StringBuffer buffer, int valueLength, int paramName, int paramValue)
422         {
423             if (valueLength==0 && paramValue>paramName)
424             {
425                 String name=StringUtil.asciiToLowerCase(buffer.substring(paramName,paramValue-1));
426                 if ("for".equalsIgnoreCase(name))
427                 {
428                     String value=buffer.substring(paramValue);
429                     
430                     // if unknown, clear any leftward values
431                     if ("unknown".equalsIgnoreCase(value))
432                         _for = null;
433                     // Otherwise accept IP or token(starting with '_') as remote keys
434                     else
435                         _for=value;
436                 }
437             }
438         }
439     }
440 }