// // ======================================================================== // Copyright (c) 1995-2017 Mort Bay Consulting Pty. Ltd. // ------------------------------------------------------------------------ // All rights reserved. This program and the accompanying materials // are made available under the terms of the Eclipse Public License v1.0 // and Apache License v2.0 which accompanies this distribution. // // The Eclipse Public License is available at // http://www.eclipse.org/legal/epl-v10.html // // The Apache License v2.0 is available at // http://www.opensource.org/licenses/apache2.0.php // // You may elect to redistribute this code under either of these licenses. // ======================================================================== // package org.eclipse.jetty.server.handler; import java.io.Closeable; import java.io.IOException; import java.net.InetAddress; import java.net.InetSocketAddress; import java.util.ArrayDeque; import java.util.Deque; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ExecutionException; import javax.servlet.AsyncContext; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.eclipse.jetty.http.HostPortHttpField; import org.eclipse.jetty.http.HttpField; import org.eclipse.jetty.http.HttpFields; import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.QuotedCSV; import org.eclipse.jetty.server.ForwardedRequestCustomizer; import org.eclipse.jetty.server.Request; import org.eclipse.jetty.util.IncludeExcludeSet; import org.eclipse.jetty.util.InetAddressSet; import org.eclipse.jetty.util.StringUtil; import org.eclipse.jetty.util.annotation.ManagedAttribute; import org.eclipse.jetty.util.annotation.ManagedOperation; import org.eclipse.jetty.util.annotation.Name; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; import org.eclipse.jetty.util.thread.Locker; /** * <p>Handler to limit the threads per IP address for DOS protection</p> * <p>The ThreadLimitHandler applies a limit to the number of Threads * that can be used simultaneously per remote IP address. * </p> * <p>The handler makes a determination of the remote IP separately to * any that may be made by the {@link ForwardedRequestCustomizer} or similar: * <ul> * <li>This handler will use either only a single style * of forwarded header. This is on the assumption that a trusted local proxy * will produce only a single forwarded header and that any additional * headers are likely from untrusted client side proxies.</li> * <li>If multiple instances of a forwarded header are provided, this * handler will use the right-most instance, which will have been set from * the trusted local proxy</li> * </ul> * Requests in excess of the limit will be asynchronously suspended until * a thread is available. * <p>This is a simpler alternative to DosFilter</p> */ public class ThreadLimitHandler extends HandlerWrapper { private static final Logger LOG = Log.getLogger(ThreadLimitHandler.class); private final static String REMOTE = "o.e.j.s.h.TLH.REMOTE"; private final static String PERMIT = "o.e.j.s.h.TLH.PASS"; private final boolean _rfc7239; private final String _forwardedHeader; private final IncludeExcludeSet<String, InetAddress> _includeExcludeSet = new IncludeExcludeSet<>(InetAddressSet.class); private final ConcurrentMap<String, Remote> _remotes = new ConcurrentHashMap<>(); private volatile boolean _enabled; private int _threadLimit=10; public ThreadLimitHandler() { this(null,false); } public ThreadLimitHandler(@Name("forwardedHeader") String forwardedHeader) { this(forwardedHeader,HttpHeader.FORWARDED.is(forwardedHeader)); } public ThreadLimitHandler(@Name("forwardedHeader") String forwardedHeader, @Name("rfc7239") boolean rfc7239) { super(); _rfc7239 = rfc7239; _forwardedHeader = forwardedHeader; _enabled = true; } @Override protected void doStart() throws Exception { super.doStart(); LOG.info(String.format("ThreadLimitHandler enable=%b limit=%d include=%s",_enabled,_threadLimit,_includeExcludeSet)); } @ManagedAttribute("true if this handler is enabled") public boolean isEnabled() { return _enabled; } public void setEnabled(boolean enabled) { _enabled = enabled; LOG.info(String.format("ThreadLimitHandler enable=%b limit=%d include=%s",_enabled,_threadLimit,_includeExcludeSet)); } @ManagedAttribute("The maximum threads that can be dispatched per remote IP") public int getThreadLimit() { return _threadLimit; } public void setThreadLimit(int threadLimit) { if (threadLimit<=0) throw new IllegalArgumentException("limit must be >0"); _threadLimit = threadLimit; } @ManagedOperation("Include IP in thread limits") public void include(String inetAddressPattern) { _includeExcludeSet.include(inetAddressPattern); } @ManagedOperation("Exclude IP from thread limits") public void exclude(String inetAddressPattern) { _includeExcludeSet.exclude(inetAddressPattern); } @Override public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { // Allow ThreadLimit to be enabled dynamically without restarting server if (!_enabled) { // if disabled, handle normally super.handle(target,baseRequest,request,response); } else { // Get the remote address of the request Remote remote = getRemote(baseRequest); if (remote==null) { // if remote is not known, handle normally super.handle(target,baseRequest,request,response); } else { // Do we already have a future permit from a previous invocation? Closeable permit = (Closeable)baseRequest.getAttribute(PERMIT); try { if (permit!=null) { // Yes, remove it from any future async cycles. baseRequest.removeAttribute(PERMIT); } else { // No, then lets try to acquire one CompletableFuture<Closeable> future_permit=remote.acquire(); // Did we get a permit? if (future_permit.isDone()) { // yes permit=future_permit.get(); } else { if (LOG.isDebugEnabled()) LOG.debug("Threadlimited {} {}",remote,target); // No, lets asynchronously suspend the request AsyncContext async = baseRequest.startAsync(); // let's never timeout the async. If this is a DOS, then good to make them wait, if this is not // then give them maximum time to get a thread. async.setTimeout(0); // dispatch the request when we do eventually get a pass future_permit.thenAccept(c-> { baseRequest.setAttribute(PERMIT,c); async.dispatch(); }); return; } } // Use the permit super.handle(target,baseRequest,request,response); } catch (InterruptedException | ExecutionException e) { throw new ServletException(e); } finally { if (permit!=null) permit.close(); } } } } protected int getThreadLimit(String ip) { if (!_includeExcludeSet.isEmpty()) { try { if (!_includeExcludeSet.test(InetAddress.getByName(ip))) { LOG.debug("excluded {}",ip); return 0; } } catch(Exception e) { LOG.ignore(e); } } return _threadLimit; } protected Remote getRemote(Request baseRequest) { Remote remote = (Remote)baseRequest.getAttribute(REMOTE); if (remote!=null) return remote; String ip=getRemoteIP(baseRequest); LOG.debug("ip={}",ip); if (ip==null) return null; int limit = getThreadLimit(ip); if (limit<=0) return null; remote = _remotes.get(ip); if (remote==null) { Remote r = new Remote(ip,limit); remote = _remotes.putIfAbsent(ip,r); if (remote==null) remote = r; } baseRequest.setAttribute(REMOTE,remote); return remote; } protected String getRemoteIP(Request baseRequest) { // Do we have a forwarded header set? if (_forwardedHeader!=null && !_forwardedHeader.isEmpty()) { // Yes, then try to get the remote IP from the header String remote = _rfc7239?getForwarded(baseRequest):getXForwardedFor(baseRequest); if (remote!=null && !remote.isEmpty()) return remote; } // If no remote IP from a header, determine it directly from the channel // Do not use the request methods, as they may have been lied to by the // RequestCustomizer! InetSocketAddress inet_addr = baseRequest.getHttpChannel().getRemoteAddress(); if (inet_addr!=null && inet_addr.getAddress()!=null) return inet_addr.getAddress().getHostAddress(); return null; } private String getForwarded(Request request) { // Get the right most Forwarded for value. // This is the value from the closest proxy and the only one that // can be trusted. RFC7239 rfc7239 = new RFC7239(); HttpFields httpFields = request.getHttpFields(); for (HttpField field : httpFields) if (_forwardedHeader.equalsIgnoreCase(field.getName())) rfc7239.addValue(field.getValue()); if (rfc7239.getFor()!=null) return new HostPortHttpField(rfc7239.getFor()).getHost(); return null; } private String getXForwardedFor(Request request) { // Get the right most XForwarded-For for value. // This is the value from the closest proxy and the only one that // can be trusted. String forwarded_for = null; HttpFields httpFields = request.getHttpFields(); for (HttpField field : httpFields) if (_forwardedHeader.equalsIgnoreCase(field.getName())) forwarded_for = field.getValue(); if (forwarded_for==null || forwarded_for.isEmpty()) return null; int comma = forwarded_for.lastIndexOf(','); return (comma>=0)?forwarded_for.substring(comma+1).trim():forwarded_for; } private final class Remote implements Closeable { private final String _ip; private final int _limit; private final Locker _locker = new Locker(); private int _permits; private Deque<CompletableFuture<Closeable>> _queue = new ArrayDeque<>(); private final CompletableFuture<Closeable> _permitted = CompletableFuture.completedFuture(this); public Remote(String ip, int limit) { _ip=ip; _limit=limit; } public CompletableFuture<Closeable> acquire() { try(Locker.Lock lock = _locker.lock()) { // Do we have available passes? if (_permits<_limit) { // Yes - increment the allocated passes _permits++; // return the already completed future return _permitted; // TODO is it OK to share/reuse this? } // No pass available, so queue a new future CompletableFuture<Closeable> pass = new CompletableFuture<Closeable>(); _queue.addLast(pass); return pass; } } @Override public void close() throws IOException { try(Locker.Lock lock = _locker.lock()) { // reduce the allocated passes _permits--; while(true) { // Are there any future passes waiting? CompletableFuture<Closeable> permit = _queue.pollFirst(); // No - we are done if (permit==null) break; // Yes - if we can complete them, we are done if (permit.complete(this)) { _permits++; break; } // Somebody else must have completed/failed that future pass, // so let's try for another. } } } @Override public String toString() { try(Locker.Lock lock = _locker.lock()) { return String.format("R[ip=%s,p=%d,l=%d,q=%d]",_ip,_permits,_limit,_queue.size()); } } } private final class RFC7239 extends QuotedCSV { String _for; private RFC7239() { super(false); } String getFor() { return _for; } @Override protected void parsedParam(StringBuffer buffer, int valueLength, int paramName, int paramValue) { if (valueLength==0 && paramValue>paramName) { String name=StringUtil.asciiToLowerCase(buffer.substring(paramName,paramValue-1)); if ("for".equalsIgnoreCase(name)) { String value=buffer.substring(paramValue); // if unknown, clear any leftward values if ("unknown".equalsIgnoreCase(value)) _for = null; // Otherwise accept IP or token(starting with '_') as remote keys else _for=value; } } } } }