org.eclipse.jetty.ee8.nested.ThreadLimitHandler Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of jetty-ee8-nested Show documentation
Show all versions of jetty-ee8-nested Show documentation
The jetty core handler adapted to legacy ee8 handler.
The newest version!
//
// ========================================================================
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.ee8.nested;
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.ExecutionException;
import javax.servlet.AsyncContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;
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.HttpHeader;
import org.eclipse.jetty.http.QuotedCSV;
import org.eclipse.jetty.io.Retainable;
import org.eclipse.jetty.server.ForwardedRequestCustomizer;
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.thread.AutoLock;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Handler to limit the threads per IP address for DOS protection
* The ThreadLimitHandler applies a limit to the number of Threads
* that can be used simultaneously per remote IP address.
*
* The handler makes a determination of the remote IP separately to
* any that may be made by the {@link ForwardedRequestCustomizer} or similar:
*
* - 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.
* - 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
*
* Requests in excess of the limit will be asynchronously suspended until
* a thread is available.
* This is a simpler alternative to DosFilter
*/
public class ThreadLimitHandler extends HandlerWrapper {
private static final Logger LOG = LoggerFactory.getLogger(ThreadLimitHandler.class);
private static final String REMOTE = "o.e.j.s.h.TLH.REMOTE";
private static final String PERMIT = "o.e.j.s.h.TLH.PASS";
private final boolean _rfc7239;
private final String _forwardedHeader;
private final IncludeExcludeSet _includeExcludeSet = new IncludeExcludeSet<>(InetAddressSet.class);
private final ConcurrentHashMap _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;
}
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.trace("IGNORED", e);
}
}
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 {
baseRequest.addEventListener(new ServletRequestListener() {
@Override
public void requestDestroyed(ServletRequestEvent sre) {
// Use a compute method to remove the Remote instance as it is necessary for
// the ref counter release and the removal to be atomic.
_remotes.computeIfPresent(remote._ip, (k, v) -> v._referenceCounter.release() ? null : v);
}
});
// 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 futurePermit = remote.acquire();
// Did we get a permit?
if (futurePermit.isDone()) {
// yes
permit = futurePermit.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
futurePermit.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();
}
}
}
}
private 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;
// Use a compute method to create or retain the Remote instance as it is necessary for
// the ref counter increment or the instance creation to be mutually exclusive.
// The map MUST be a CHM as it guarantees the remapping function is only called once.
remote = _remotes.compute(ip, (k, v) -> {
if (v != null) {
v._referenceCounter.retain();
return v;
}
return new Remote(k, limit);
});
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 inetAddr = baseRequest.getHttpChannel().getRemoteAddress();
if (inetAddr != null && inetAddr.getAddress() != null)
return inetAddr.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();
for (HttpField field : request.getHttpFields()) {
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 forwardedFor = null;
for (HttpField field : request.getHttpFields()) {
if (_forwardedHeader.equalsIgnoreCase(field.getName()))
forwardedFor = field.getValue();
}
if (forwardedFor == null || forwardedFor.isEmpty())
return null;
int comma = forwardedFor.lastIndexOf(',');
return (comma >= 0) ? forwardedFor.substring(comma + 1).trim() : forwardedFor;
}
private static final class Remote implements Closeable {
private final String _ip;
private final int _limit;
private final AutoLock _lock = new AutoLock();
private final Retainable.ReferenceCounter _referenceCounter = new Retainable.ReferenceCounter();
private int _permits;
private Deque> _queue = new ArrayDeque<>();
private final CompletableFuture _permitted = CompletableFuture.completedFuture(this);
public Remote(String ip, int limit) {
_ip = ip;
_limit = limit;
}
public CompletableFuture acquire() {
try (AutoLock lock = _lock.lock()) {
// Do we have available passes?
if (_permits < _limit) {
// Yes - increment the allocated passes
_permits++;
// return the already completed future
// TODO is it OK to share/reuse this?
return _permitted;
}
// No pass available, so queue a new future
CompletableFuture pass = new CompletableFuture<>();
_queue.addLast(pass);
return pass;
}
}
@Override
public void close() {
try (AutoLock lock = _lock.lock()) {
// reduce the allocated passes
_permits--;
while (true) {
// Are there any future passes waiting?
CompletableFuture 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 (AutoLock lock = _lock.lock()) {
return String.format("R[ip=%s,p=%d,l=%d,q=%d]", _ip, _permits, _limit, _queue.size());
}
}
}
private static final class RFC7239 extends QuotedCSV {
String _for;
private RFC7239() {
super(false);
}
String getFor() {
return _for;
}
@Override
protected void parsedParam(StringBuilder 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;
else
// Otherwise accept IP or token(starting with '_') as remote keys
_for = value;
}
}
}
}
}