ConditionalHandler (#10492)

Added a Conditional Handler

Co-authored-by: Jan Bartel <janb@webtide.com>
This commit is contained in:
Greg Wilkins 2023-09-22 01:25:24 +02:00 committed by GitHub
parent 9bfa5cc65e
commit f9ca02c393
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 1523 additions and 320 deletions

View File

@ -41,7 +41,7 @@ import org.slf4j.LoggerFactory;
* @param <E> the type of mapping endpoint
*/
@ManagedObject("Path Mappings")
public class PathMappings<E> extends AbstractMap<PathSpec, E> implements Iterable<MappedResource<E>>, Dumpable
public class PathMappings<E> extends AbstractMap<PathSpec, E> implements Iterable<MappedResource<E>>, Dumpable, Predicate<String>
{
private static final Logger LOG = LoggerFactory.getLogger(PathMappings.class);
// In prefix matches, this is the length ("/*".length() + 1) - used for the best prefix match loop
@ -191,6 +191,77 @@ public class PathMappings<E> extends AbstractMap<PathSpec, E> implements Iterabl
return matches == null ? Collections.emptyList() : matches;
}
/**
* Test if the mappings contains a specified path.
* @param path the path to return matches on
* @return true if the path matches
*/
@Override
public boolean test(String path)
{
if (_mappings.isEmpty())
return false;
// Try for default
if (_servletDefault != null)
return true;
// Try a root match
if (_servletRoot != null && "/".equals(path))
return true;
// try an exact match
MappedResource<E> exact = _exactMap.get(path);
if (exact != null)
return true;
// Try a prefix match
MappedResource<E> prefix = _prefixMap.getBest(path);
while (prefix != null)
{
PathSpec pathSpec = prefix.getPathSpec();
if (pathSpec.matches(path))
return true;
int specLength = pathSpec.getSpecLength();
prefix = specLength > PREFIX_TAIL_LEN ? _prefixMap.getBest(path, 0, specLength - PREFIX_TAIL_LEN) : null;
}
// Try a suffix match
if (!_suffixMap.isEmpty())
{
int i = Math.max(0, path.lastIndexOf("/"));
// Loop through each suffix mark
// Input is "/a.b.c.foo"
// Loop 1: "b.c.foo"
// Loop 2: "c.foo"
// Loop 3: "foo"
while ((i = path.indexOf('.', i + 1)) > 0)
{
MappedResource<E> suffix = _suffixMap.get(path, i + 1, path.length() - i - 1);
if (suffix == null)
continue;
MatchedPath matchedPath = suffix.getPathSpec().matched(path);
if (matchedPath != null)
return true;
}
}
// If order is significant, then we need to match by iterating over all mappings.
if (_orderIsSignificant)
{
for (MappedResource<E> mr : _mappings)
{
if (mr.getPathSpec() instanceof ServletPathSpec)
continue;
if (mr.getPathSpec().matches(path))
return true;
}
}
return false;
}
/**
* <p>Find the best single match for a path.</p>
* <p>The match may be found by optimized direct lookups when possible, otherwise all mappings

View File

@ -97,20 +97,24 @@ public interface PathSpec extends Comparable<PathSpec>
String getSuffix();
/**
* Test to see if the provided path matches this path spec
* Test to see if the provided path matches this path spec.
* This can be more efficient that {@link #matched(String)} if the details of the match are not required.
*
* @param path the path to test
* @return true if the path matches this path spec, false otherwise
* @deprecated use {@link #matched(String)} instead
* @see #matched(String)
*/
@Deprecated
boolean matches(String path);
default boolean matches(String path)
{
return matched(path) != null;
}
/**
* Get the complete matched details of the provided path.
*
* @param path the path to test
* @return the matched details, if a match was possible, or null if not able to be matched.
* @see #matches(String)
*/
MatchedPath matched(String path);
}

View File

@ -30,7 +30,7 @@ public class PathSpecSet extends AbstractSet<String> implements Predicate<String
@Override
public boolean test(String s)
{
return specs.getMatched(s) != null;
return specs.test(s);
}
@Override
@ -53,16 +53,26 @@ public class PathSpecSet extends AbstractSet<String> implements Predicate<String
return PathSpec.from(Objects.toString(o));
}
public boolean add(PathSpec pathSpec)
{
return specs.put(pathSpec, Boolean.TRUE) == null;
}
@Override
public boolean add(String s)
{
return specs.put(PathSpec.from(s), Boolean.TRUE) == null;
return add(PathSpec.from(s));
}
public boolean remove(PathSpec pathSpec)
{
return specs.remove(pathSpec) != null;
}
@Override
public boolean remove(Object o)
{
return specs.remove(asPathSpec(o)) != null;
return remove(asPathSpec(o));
}
@Override

View File

@ -30,7 +30,9 @@ import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.nullValue;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
// @checkstyle-disable-check : AvoidEscapedUnicodeCharactersCheck
public class PathMappingsTest
@ -42,6 +44,7 @@ public class PathMappingsTest
assertThat(msg, matched, notNullValue());
String actualMatch = matched.getResource();
assertEquals(expectedValue, actualMatch, msg);
assertTrue(pathmap.test(path));
}
/**
@ -411,9 +414,13 @@ public class PathMappingsTest
MatchedResource<String> match = p.getMatched(path);
if (matched == null)
{
assertFalse(p.test(path));
assertThat(match, nullValue());
}
else
{
assertTrue(p.test(path));
assertThat(match, notNullValue());
assertThat(p.getMatched(path).getResource(), equalTo(matched));
}

View File

@ -19,7 +19,6 @@ import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.MimeTypes;
import org.eclipse.jetty.http.pathmap.PathSpecSet;
import org.eclipse.jetty.io.ByteBufferAccumulator;
import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.io.RetainableByteBuffer;
@ -53,14 +52,12 @@ import org.slf4j.LoggerFactory;
* generated can also be unbounded.
* </p>
*/
public class BufferedResponseHandler extends Handler.Wrapper
public class BufferedResponseHandler extends ConditionalHandler.Abstract
{
public static final String BUFFER_SIZE_ATTRIBUTE_NAME = BufferedResponseHandler.class.getName() + ".buffer-size";
private static final Logger LOG = LoggerFactory.getLogger(BufferedResponseHandler.class);
private final IncludeExclude<String> _methods = new IncludeExclude<>();
private final IncludeExclude<String> _paths = new IncludeExclude<>(PathSpecSet.class);
private final IncludeExclude<String> _mimeTypes = new IncludeExclude<>();
public BufferedResponseHandler()
@ -71,7 +68,11 @@ public class BufferedResponseHandler extends Handler.Wrapper
public BufferedResponseHandler(Handler handler)
{
super(handler);
_methods.include(HttpMethod.GET.asString());
includeMethod(HttpMethod.GET.asString());
// Mimetypes are not a condition on the ConditionalHandler as they
// are also check during response generation, once the type is known.
for (String type : MimeTypes.DEFAULTS.getMimeMap().values())
{
if (type.startsWith("image/") ||
@ -84,19 +85,18 @@ public class BufferedResponseHandler extends Handler.Wrapper
LOG.debug("{} mime types {}", this, _mimeTypes);
}
public IncludeExclude<String> getMethodIncludeExclude()
public void includeMimeType(String... mimeTypes)
{
return _methods;
if (isStarted())
throw new IllegalStateException(getState());
_mimeTypes.include(mimeTypes);
}
public IncludeExclude<String> getPathIncludeExclude()
public void excludeMimeType(String... mimeTypes)
{
return _paths;
}
public IncludeExclude<String> getMimeIncludeExclude()
{
return _mimeTypes;
if (isStarted())
throw new IllegalStateException(getState());
_mimeTypes.exclude(mimeTypes);
}
protected boolean isMimeTypeBufferable(String mimetype)
@ -104,14 +104,6 @@ public class BufferedResponseHandler extends Handler.Wrapper
return _mimeTypes.test(mimetype);
}
protected boolean isPathBufferable(String requestURI)
{
if (requestURI == null)
return true;
return _paths.test(requestURI);
}
protected boolean shouldBuffer(Response response, boolean last)
{
if (last)
@ -130,34 +122,17 @@ public class BufferedResponseHandler extends Handler.Wrapper
}
@Override
public boolean handle(Request request, Response response, Callback callback) throws Exception
public boolean onConditionsMet(Request request, Response response, Callback callback) throws Exception
{
Handler next = getHandler();
if (next == null)
return false;
if (LOG.isDebugEnabled())
LOG.debug("{} handle {} in {}", this, request, request.getContext());
// If not a supported method this URI is always excluded.
if (!_methods.test(request.getMethod()))
{
if (LOG.isDebugEnabled())
LOG.debug("{} excluded by method {}", this, request);
return super.handle(request, response, callback);
}
// If not a supported path this URI is always excluded.
String path = Request.getPathInContext(request);
if (!isPathBufferable(path))
{
if (LOG.isDebugEnabled())
LOG.debug("{} excluded by path {}", this, request);
return super.handle(request, response, callback);
}
LOG.debug("{} doHandle {} in {}", this, request, request.getContext());
// If the mime type is known from the path then apply mime type filtering.
String mimeType = request.getContext().getMimeTypes().getMimeByExtension(path);
String mimeType = request.getContext().getMimeTypes().getMimeByExtension(request.getHttpURI().getCanonicalPath());
if (mimeType != null)
{
mimeType = MimeTypes.getContentTypeWithoutCharset(mimeType);
@ -175,6 +150,12 @@ public class BufferedResponseHandler extends Handler.Wrapper
return next.handle(request, bufferedResponse, bufferedResponse);
}
@Override
protected boolean onConditionsNotMet(Request request, Response response, Callback callback) throws Exception
{
return nextHandler(request, response, callback);
}
private class BufferedResponse extends Response.Wrapper implements Callback
{
private final Callback _callback;

View File

@ -0,0 +1,854 @@
//
// ========================================================================
// 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.server.handler;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.net.UnknownHostException;
import java.util.AbstractSet;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.Objects;
import java.util.Set;
import java.util.function.Predicate;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.pathmap.PathSpec;
import org.eclipse.jetty.http.pathmap.PathSpecSet;
import org.eclipse.jetty.server.ConnectionMetaData;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Response;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.IncludeExclude;
import org.eclipse.jetty.util.IncludeExcludeSet;
import org.eclipse.jetty.util.InetAddressPattern;
import org.eclipse.jetty.util.TypeUtil;
import org.eclipse.jetty.util.component.DumpableCollection;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A {@link Handler.Wrapper} that conditionally handles a {@link Request}.
* The conditions are implemented by {@link IncludeExclude}s of:
* <ul>
* <li>A HTTP method name, which can be efficiently matched</li>
* <li>A {@link PathSpec} or string representation, which can be efficiently matched.</li>
* <li>An arbitrary {@link Predicate} taking the {@link Request}, which is matched in a linear test of all predicates.</li>
* </ul>
*
* <p>If the conditions are met, the abstract {@link #onConditionsMet(Request, Response, Callback)} method will be invoked,
* otherwise the {@link #onConditionsNotMet(Request, Response, Callback)} method will be invoked. Implementations may call
* the {@link #nextHandler(Request, Response, Callback)} method to call the wrapped handler.</p>
*
* <p>A typical usage is to extend the {@link Abstract} sub class and provide an implementation of
* {@link #onConditionsMet(Request, Response, Callback)} and {@link #onConditionsNotMet(Request, Response, Callback)}:</p>
* <pre>{@code
* public class MyOptionalHandler extends ConditionalHandler.Abstract
* {
* @Override
* public boolean onConditionsMet(Request request, Response response, Callback callback)
* {
* response.getHeaders().add("Test", "My Optional Handling");
* return nextHandle(request, response, callback);
* }
*
* @Override
* public boolean onConditionsNoMet(Request request, Response response, Callback callback)
* {
* return false;
* }
* }
* }</pre>
*
* <p>If the conditions added to {@code MyOptionalHandler} are met, then the {@link #onConditionsMet(Request, Response, Callback)}
* method is called and a response header added before invoking {@link #nextHandler(Request, Response, Callback)}, otherwise
* the {@link #onConditionsNotMet(Request, Response, Callback)} is called, which returns false to indicate no more handling.</p>
*
* <p>Alternatively, one of the concrete subclasses may be used. These implementations conditionally provide a specific
* action in their {@link #onConditionsMet(Request, Response, Callback)} methods:
* <ul>
* <li>{@link DontHandle} - If the conditions are met, terminate further handling by returning {@code false}</li>
* <li>{@link Reject} - If the conditions are met, reject the request with a {@link HttpStatus#FORBIDDEN_403} (or other status code) response.</li>
* <li>{@link SkipNext} - If the conditions are met, then the {@link #getHandler() next handler} is skipped and the
* {@link Singleton#getHandler() following hander} invoked instead.</li>
* </ul>
* <p>Otherwise, if their conditions are not met, these subclasses are all extension of the abstract {@link ElseNext} subclass,
* that implements {@link #onConditionsNotMet(Request, Response, Callback)} to call {@link #nextHandler(Request, Response, Callback)}.
* Thus their specific behaviour is not applied and the handling continues with the next handler.</p>
*
* <p>These concrete handlers are ideal for retrofitting conditional behavior. For example, if an application handler was
* found to not correctly handle the {@code OPTIONS} method for the path "/secret/*", it could be protected as follows:</p>
* <pre>{@code
* Server server = new Server();
* ApplicationHandler application = new ApplicationHandler();
* server.setHandler(application);
*
* ConditionalHandler reject = new ConditionalHandler.Reject(403); // or DontHandle
* reject.includeMethod("OPTIONS");
* reject.includePath("/secret/*");
* server.insertHandler(reject);
* }</pre>
*
* <p>Another example, in an application comprised of several handlers, one of which is a wrapping handler whose behavior
* needs to be skipped for "POST" requests, then it could be achieved as follows:</p>
* <pre>{@code
* Server server = new Server();
* ApplicationWrappingHandler wrappingHandler = new ApplicationWrappingHandler();
* ApplicationHandler applicationHandler = new ApplicationHandler();
* server.setHandler(wrappingHandler);
* filter.setHandler(applicationHandler);
*
* ConditionalHandler skipNext = new ConditionalHandler.SkipNext();
* skipNext.includeMethod("POST");
* skipNext.setHandler(wrappingHandler);
* server.setHandler(skipNext);
* }</pre>
* <p>Note that a better solution, if possible, would be for the {@code ApplicationFilterHandler} and/or
* {@code ApplicationHandler} handlers to extend {@code ConditionalHandler}.</p>
*/
public abstract class ConditionalHandler extends Handler.Wrapper
{
private static final Logger LOG = LoggerFactory.getLogger(ConditionalHandler.class);
private final IncludeExclude<String> _methods = new IncludeExclude<>();
private final IncludeExclude<String> _pathSpecs = new IncludeExclude<>(PathSpecSet.class);
private final IncludeExcludeSet<Predicate<Request>, Request> _predicates = new IncludeExcludeSet<>(PredicateSet.class);
private Predicate<Request> _handlePredicate;
private ConditionalHandler()
{
this(false, null);
}
private ConditionalHandler(Handler nextHandler)
{
this(false, nextHandler);
}
private ConditionalHandler(boolean dynamic, Handler nextHandler)
{
super(dynamic, nextHandler);
}
/**
* Clear all inclusions and exclusions.
*/
public void clear()
{
if (isStarted())
throw new IllegalStateException(getState());
_methods.clear();
_pathSpecs.clear();
_predicates.clear();
}
IncludeExclude<String> getMethods()
{
// Used only for testing
return _methods;
}
IncludeExclude<String> getPathSpecs()
{
// Used only for testing
return _pathSpecs;
}
IncludeExcludeSet<Predicate<Request>, Request> getPredicates()
{
// Used only for testing
return _predicates;
}
/**
* Include {@link Request#getMethod() method}s in the conditions to be met
* @param methods The exact case-sensitive method name
*/
public void includeMethod(String... methods)
{
if (isStarted())
throw new IllegalStateException(getState());
_methods.include(methods);
}
/**
* Exclude {@link Request#getMethod() method}s in the conditions to be met
* @param methods The exact case-sensitive method name
*/
public void excludeMethod(String... methods)
{
if (isStarted())
throw new IllegalStateException(getState());
_methods.exclude(methods);
}
/**
* Include {@link PathSpec}s in the conditions to be met
* @param paths The {@link PathSpec}s that are tested against the {@link Request#getPathInContext(Request) pathInContext}.
*/
public void include(PathSpec... paths)
{
if (isStarted())
throw new IllegalStateException(getState());
for (PathSpec p : paths)
((PathSpecSet)_pathSpecs.getIncluded()).add(p);
}
/**
* Exclude {@link PathSpec}s in the conditions to be met
* @param paths The {@link PathSpec}s that are tested against the {@link Request#getPathInContext(Request) pathInContext}.
*/
public void exclude(PathSpec... paths)
{
if (isStarted())
throw new IllegalStateException(getState());
for (PathSpec p : paths)
((PathSpecSet)_pathSpecs.getExcluded()).add(p);
}
/**
* Include {@link PathSpec}s in the conditions to be met
* @param paths String representations of {@link PathSpec}s that are
* tested against the {@link Request#getPathInContext(Request) pathInContext}.
*/
public void includePath(String... paths)
{
if (isStarted())
throw new IllegalStateException(getState());
_pathSpecs.include(paths);
}
/**
* Exclude {@link PathSpec} in the conditions to be met
* @param paths String representations of {@link PathSpec}s that are
* tested against the {@link Request#getPathInContext(Request) pathInContext}.
*/
public void excludePath(String... paths)
{
if (isStarted())
throw new IllegalStateException(getState());
_pathSpecs.exclude(paths);
}
/**
* Include {@link InetAddressPattern}s in the conditions to be met
* @param patterns {@link InetAddressPattern}s that are
* tested against the {@link ConnectionMetaData#getRemoteSocketAddress() getRemoteSocketAddress()} of
* {@link Request#getConnectionMetaData()}.
*/
public void include(InetAddressPattern... patterns)
{
if (isStarted())
throw new IllegalStateException(getState());
for (InetAddressPattern p : patterns)
_predicates.include(new InetAddressPatternPredicate(p));
}
/**
* Include {@link InetAddressPattern}s in the conditions to be met
* @param patterns String representations of {@link InetAddressPattern}s that are
* tested against the {@link ConnectionMetaData#getRemoteSocketAddress() getRemoteSocketAddress()} of
* {@link Request#getConnectionMetaData()}.
*/
public void includeInetAddressPattern(String... patterns)
{
for (String p : patterns)
include(InetAddressPattern.from(p));
}
/**
* Exclude {@link InetAddressPattern}s in the conditions to be met
* @param patterns {@link InetAddressPattern}s that are
* tested against the {@link ConnectionMetaData#getRemoteSocketAddress() getRemoteSocketAddress()} of
* {@link Request#getConnectionMetaData()}.
*/
public void exclude(InetAddressPattern... patterns)
{
if (isStarted())
throw new IllegalStateException(getState());
for (InetAddressPattern p : patterns)
_predicates.exclude(new InetAddressPatternPredicate(p));
}
/**
* Exclude {@link InetAddressPattern} in the conditions to be met
* @param patterns String representations of {@link InetAddressPattern}s that are
* tested against the {@link ConnectionMetaData#getRemoteSocketAddress() getRemoteSocketAddress()} of
* {@link Request#getConnectionMetaData()}.
*/
public void excludeInetAddressPattern(String... patterns)
{
for (String p : patterns)
exclude(InetAddressPattern.from(p));
}
/**
* {@link IncludeExclude#include(Object) Include} arbitrary {@link Predicate}s in the conditions.
* @param predicates {@link Predicate}s that are tested against the {@link Request}.
* This method is optimized so that a passed {@link MethodPredicate} or {@link PathSpecPredicate} is
* converted to a more efficient {@link #includeMethod(String...)} or {@link #include(PathSpec...)} respectively.
*/
@SafeVarargs
public final void include(Predicate<Request>... predicates)
{
if (isStarted())
throw new IllegalStateException(getState());
for (Predicate<Request> p : predicates)
{
if (p instanceof MethodPredicate methodPredicate)
includeMethod(methodPredicate._method);
else if (p instanceof PathSpecPredicate pathSpecPredicate)
include(pathSpecPredicate._pathSpec);
else
_predicates.include(p);
}
}
/**
* {@link IncludeExclude#exclude(Object) Exclude} arbitrary {@link Predicate}s in the conditions.
* @param predicates {@link Predicate}s that are tested against the {@link Request}.
* This method is optimized so that a passed {@link MethodPredicate} or {@link PathSpecPredicate} is
* converted to a more efficient {@link #excludeMethod(String...)} or {@link #exclude(PathSpec...)} respectively.
*/
@SafeVarargs
public final void exclude(Predicate<Request>... predicates)
{
if (isStarted())
throw new IllegalStateException(getState());
for (Predicate<Request> p : predicates)
{
if (p instanceof MethodPredicate methodPredicate)
excludeMethod(methodPredicate._method);
else if (p instanceof PathSpecPredicate pathSpecPredicate)
exclude(pathSpecPredicate._pathSpec);
else
_predicates.exclude(p);
}
}
private boolean testMethods(Request request)
{
return _methods.test(request.getMethod());
}
private boolean testPathSpecs(Request request)
{
return _pathSpecs.test(Request.getPathInContext(request));
}
private boolean testPredicates(Request request)
{
return _predicates.test(request);
}
@Override
protected void doStart() throws Exception
{
_handlePredicate = TypeUtil.truePredicate();
if (!_methods.isEmpty())
_handlePredicate = _handlePredicate.and(this::testMethods);
if (!_pathSpecs.isEmpty())
_handlePredicate = _handlePredicate.and(this::testPathSpecs);
if (!_predicates.isEmpty())
_handlePredicate = _handlePredicate.and(this::testPredicates);
super.doStart();
}
public final boolean handle(Request request, Response response, Callback callback) throws Exception
{
if (_handlePredicate.test(request))
return onConditionsMet(request, response, callback);
return onConditionsNotMet(request, response, callback);
}
/**
* Handle a request that has met the conditions.
* Typically, the implementation will provide optional handling and then call the
* {@link #nextHandler(Request, Response, Callback)} method to continue handling.
* @param request The request to handle
* @param response The response to generate
* @param callback The callback for completion
* @return True if this handler will complete the callback
* @throws Exception If there is a problem handling
* @see Handler#handle(Request, Response, Callback)
*/
protected abstract boolean onConditionsMet(Request request, Response response, Callback callback) throws Exception;
/**
* This method is called when the request has not met the conditions and is not to
* be handled by this handler.
* Implementations may return false; send an error response; or handle the request differently.
* @param request The request to handle
* @param response The response to generate
* @param callback The callback for completion
* @return True if this handler will complete the callback
* @throws Exception If there is a problem handling
* @see Handler#handle(Request, Response, Callback)
*/
protected abstract boolean onConditionsNotMet(Request request, Response response, Callback callback) throws Exception;
/**
* Handle a request by invoking the {@link #handle(Request, Response, Callback)} method of the
* {@link #getHandler() next Handler}.
* @param request The request to handle
* @param response The response to generate
* @param callback The callback for completion
* @return True if this handler will complete the callback
* @throws Exception If there is a problem handling
* @see Handler#handle(Request, Response, Callback)
*/
protected boolean nextHandler(Request request, Response response, Callback callback) throws Exception
{
return super.handle(request, response, callback);
}
@Override
public void dump(Appendable out, String indent) throws IOException
{
dumpObjects(out, indent,
new DumpableCollection("included methods", _methods.getIncluded()),
new DumpableCollection("included paths", _pathSpecs.getIncluded()),
new DumpableCollection("included predicates", _predicates.getIncluded()),
new DumpableCollection("excluded methods", _methods.getExcluded()),
new DumpableCollection("excluded paths", _pathSpecs.getExcluded()),
new DumpableCollection("excluded predicates", _predicates.getExcluded())
);
}
/**
* Create a {@link Predicate} over {@link Request} built from the {@link Predicate#and(Predicate) and} of one or more of: <ul>
* <li>{@link ConnectorPredicate}</li>
* <li>{@link InetAddressPatternPredicate}</li>
* <li>{@link MethodPredicate}</li>
* <li>{@link PathSpecPredicate}</li>
* </ul>
* @param connectorName The connector name or {@code null}
* @param inetAddressPattern An {@link InetAddressPattern} string or {@code null}
* @param method A {@link org.eclipse.jetty.http.HttpMethod} name or {@code null}
* @param pathSpec A {@link PathSpec} string or {@code null}
* @return the combined {@link Predicate} over {@link Request}
*/
public static Predicate<Request> from(String connectorName, String inetAddressPattern, String method, String pathSpec)
{
return from(connectorName, InetAddressPattern.from(inetAddressPattern), method, pathSpec == null ? null : PathSpec.from(pathSpec));
}
/**
* Create a {@link Predicate} over {@link Request} built from the {@link Predicate#and(Predicate) and} of one or more of: <ul>
* <li>{@link TypeUtil#truePredicate()}</li>
* <li>{@link ConnectorPredicate}</li>
* <li>{@link InetAddressPatternPredicate}</li>
* <li>{@link MethodPredicate}</li>
* <li>{@link PathSpecPredicate}</li>
* </ul>
* @param connectorName The connector name or {@code null}
* @param inetAddressPattern An {@link InetAddressPattern} or {@code null}
* @param method A {@link org.eclipse.jetty.http.HttpMethod} name or {@code null}
* @param pathSpec A {@link PathSpec} or {@code null}
* @return the combined {@link Predicate} over {@link Request}
*/
public static Predicate<Request> from(String connectorName, InetAddressPattern inetAddressPattern, String method, PathSpec pathSpec)
{
Predicate<Request> predicate = TypeUtil.truePredicate();
if (connectorName != null)
predicate = predicate.and(new ConnectorPredicate(connectorName));
if (inetAddressPattern != null)
predicate = predicate.and(new InetAddressPatternPredicate(inetAddressPattern));
if (method != null)
predicate = predicate.and(new MethodPredicate(method));
if (pathSpec != null)
predicate = predicate.and(new PathSpecPredicate(pathSpec));
return predicate;
}
/**
* A Set of {@link Predicate} over {@link Request} optimized for use by {@link IncludeExclude}.
*/
public static class PredicateSet extends AbstractSet<Predicate<Request>> implements Set<Predicate<Request>>, Predicate<Request>
{
private final ArrayList<Predicate<Request>> _predicates = new ArrayList<>();
@Override
public boolean add(Predicate<Request> predicate)
{
if (_predicates.contains(predicate))
return false;
return _predicates.add(predicate);
}
@Override
public boolean remove(Object o)
{
return _predicates.remove(o);
}
@Override
public Iterator<Predicate<Request>> iterator()
{
return _predicates.iterator();
}
@Override
public int size()
{
return _predicates.size();
}
@Override
public boolean test(Request request)
{
if (request == null)
return false;
for (Predicate<Request> predicate : _predicates)
{
if (predicate.test(request))
return true;
}
return false;
}
}
/**
* A {@link Predicate} over {@link Request} that tests the {@link Connector#getName() name} of the
* {@link ConnectionMetaData#getConnector() connector} obtained from {@link Request#getConnectionMetaData()}
*/
public static class ConnectorPredicate implements Predicate<Request>
{
private final String _connector;
public ConnectorPredicate(String connector)
{
this._connector = Objects.requireNonNull(connector);
}
@Override
public boolean test(Request request)
{
return _connector.equals(request.getConnectionMetaData().getConnector().getName());
}
@Override
public int hashCode()
{
return _connector.hashCode();
}
@Override
public boolean equals(Object obj)
{
return obj instanceof ConnectorPredicate other && _connector.equals(other._connector);
}
@Override
public String toString()
{
return String.format("%s@%x{%s}", getClass().getSimpleName(), hashCode(), _connector);
}
}
/**
* A {@link Predicate} over {@link Request} that tests an {@link InetAddressPattern}
* against the {@link ConnectionMetaData#getRemoteSocketAddress() getRemoteSocketAddress()} of
* {@link Request#getConnectionMetaData()}.
*/
public static class InetAddressPatternPredicate implements Predicate<Request>
{
public static InetAddress getInetAddress(SocketAddress socketAddress)
{
if (socketAddress instanceof InetSocketAddress inetSocketAddress)
{
if (inetSocketAddress.isUnresolved())
{
try
{
return InetAddress.getByName(inetSocketAddress.getHostString());
}
catch (UnknownHostException e)
{
if (LOG.isTraceEnabled())
LOG.trace("ignored", e);
return null;
}
}
return inetSocketAddress.getAddress();
}
return null;
}
private final InetAddressPattern _pattern;
public InetAddressPatternPredicate(InetAddressPattern pattern)
{
_pattern = pattern;
}
@Override
public boolean test(Request request)
{
return _pattern.test(getInetAddress(request.getConnectionMetaData().getRemoteSocketAddress()));
}
@Override
public int hashCode()
{
return _pattern.hashCode();
}
@Override
public boolean equals(Object other)
{
return other instanceof InetAddressPatternPredicate inetAddressPatternPredicate && _pattern.equals(inetAddressPatternPredicate._pattern);
}
@Override
public String toString()
{
return "%s@%x{%s}".formatted(getClass().getSimpleName(), hashCode(), _pattern);
}
}
/**
* A {@link Predicate} over {@link Request} that tests {@link Request#getMethod() method} name.
* Using predicates in less efficient than using {@link ConditionalHandler#includeMethod(String...)}
* and {@link ConditionalHandler#excludeMethod(String...)}, so this predicate should only be used
* if necessary to combine with other predicates.
*/
public static class MethodPredicate implements Predicate<Request>
{
private final String _method;
public MethodPredicate(String method)
{
_method = Objects.requireNonNull(method);
}
@Override
public boolean test(Request request)
{
return _method.equals(request.getMethod());
}
@Override
public int hashCode()
{
return _method.hashCode();
}
@Override
public boolean equals(Object obj)
{
return obj instanceof MethodPredicate other && _method.equals(other._method);
}
@Override
public String toString()
{
return String.format("%s@%x{%s}", getClass().getSimpleName(), hashCode(), _method);
}
}
/**
* A {@link Predicate} over {@link Request} that tests a {@link PathSpec} against
* the {@link Request#getPathInContext(Request) pathInContext}.
* Using predicates in less efficient than using {@link ConditionalHandler#include(PathSpec...)}
* and {@link ConditionalHandler#exclude(PathSpec...)}, so this predicate should only be used
* if necessary to combine with other predicates.
*/
public static class PathSpecPredicate implements Predicate<Request>
{
private final PathSpec _pathSpec;
public PathSpecPredicate(PathSpec pathSpec)
{
_pathSpec = Objects.requireNonNull(pathSpec);
}
@Override
public boolean test(Request request)
{
return _pathSpec.matches(Request.getPathInContext(request));
}
@Override
public int hashCode()
{
return _pathSpec.hashCode();
}
@Override
public boolean equals(Object obj)
{
return obj instanceof PathSpecPredicate other && _pathSpec.equals(other._pathSpec);
}
@Override
public String toString()
{
return String.format("%s@%x{%s}", getClass().getSimpleName(), hashCode(), _pathSpec);
}
}
/**
* An Abstract {@link ConditionalHandler}. Implementations must provide
* both {@link #onConditionsMet(Request, Response, Callback)} and
* {@link #onConditionsNotMet(Request, Response, Callback)} implementations.
*/
public abstract static class Abstract extends ConditionalHandler
{
protected Abstract()
{
}
protected Abstract(Handler nextHandler)
{
super(nextHandler);
}
protected Abstract(boolean dynamic, Handler nextHandler)
{
super(dynamic, nextHandler);
}
}
/**
* An abstract implementation of {@link ConditionalHandler} that, if conditions are not met, will call
* the {@link #nextHandler(Request, Response, Callback)} from {@link #onConditionsNotMet(Request, Response, Callback)}.
* Implementations must provide an {@link #onConditionsMet(Request, Response, Callback)} to provide the
* handling for when conditions are met.
*/
public abstract static class ElseNext extends ConditionalHandler
{
public ElseNext()
{
this(null);
}
public ElseNext(Handler handler)
{
super(handler);
}
@Override
protected boolean onConditionsNotMet(Request request, Response response, Callback callback) throws Exception
{
return nextHandler(request, response, callback);
}
}
/**
* An implementation of {@link ConditionalHandler} that, if conditions are met, will not do any further
* handling by returning {@code false} from {@link #onConditionsMet(Request, Response, Callback)}.
* Otherwise, the {@link #nextHandler(Request, Response, Callback) next handler} will be invoked.
*/
public static class DontHandle extends ConditionalHandler.ElseNext
{
public DontHandle()
{
super();
}
public DontHandle(Handler handler)
{
super(handler);
}
@Override
protected boolean onConditionsMet(Request request, Response response, Callback callback) throws Exception
{
return false;
}
}
/**
* An implementation of {@link ConditionalHandler} that, if conditions are met, will reject
* the request by sending a response (by default a {@link HttpStatus#FORBIDDEN_403}).
* Otherwise, the {@link #nextHandler(Request, Response, Callback) next handler} will be invoked.
*/
public static class Reject extends ConditionalHandler.ElseNext
{
private final int _status;
public Reject()
{
this(null, HttpStatus.FORBIDDEN_403);
}
public Reject(int status)
{
this(null, status);
}
public Reject(Handler handler)
{
this(handler, HttpStatus.FORBIDDEN_403);
}
public Reject(Handler handler, int status)
{
super(handler);
if (status < 200 || status > 999)
throw new IllegalArgumentException("bad status");
_status = status;
}
@Override
protected boolean onConditionsMet(Request request, Response response, Callback callback) throws Exception
{
Response.writeError(request, response, callback, _status);
return true;
}
}
/**
* An implementation of {@link ConditionalHandler} that, if conditions are met, will skip the next {@link Handler} by
* invoking its {@link Singleton#getHandler() next Handler}.
* Otherwise, the {@link #nextHandler(Request, Response, Callback) next handler} will be invoked.
*/
public static class SkipNext extends ConditionalHandler.ElseNext
{
public SkipNext()
{
super();
}
public SkipNext(Handler handler)
{
super(handler);
}
@Override
protected boolean onConditionsMet(Request request, Response response, Callback callback) throws Exception
{
if (!(getHandler() instanceof Singleton nextHandler))
return false;
Handler nextNext = nextHandler.getHandler();
return nextNext != null && nextNext.handle(request, response, callback);
}
}
}

View File

@ -13,11 +13,6 @@
package org.eclipse.jetty.server.handler;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.pathmap.PathSpec;
import org.eclipse.jetty.server.Handler;
@ -27,10 +22,6 @@ import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.IncludeExcludeSet;
import org.eclipse.jetty.util.InetAddressPattern;
import org.eclipse.jetty.util.InetAddressSet;
import org.eclipse.jetty.util.component.DumpableCollection;
import static org.eclipse.jetty.server.handler.InetAccessSet.AccessTuple;
import static org.eclipse.jetty.server.handler.InetAccessSet.PatternTuple;
/**
* InetAddress Access Handler
@ -41,12 +32,8 @@ import static org.eclipse.jetty.server.handler.InetAccessSet.PatternTuple;
* the forwarded for headers, as this cannot be as easily forged.
* </p>
*/
public class InetAccessHandler extends Handler.Wrapper
public class InetAccessHandler extends ConditionalHandler.Abstract
{
// TODO replace this handler with a general conditional handler wrapper.
private final IncludeExcludeSet<PatternTuple, AccessTuple> _set = new IncludeExcludeSet<>(InetAccessSet.class);
public InetAccessHandler()
{
this(null);
@ -57,13 +44,17 @@ public class InetAccessHandler extends Handler.Wrapper
super(handler);
}
/**
* Clears all the includes, excludes, included connector names and excluded
* connector names.
*/
public void clear()
@Override
protected boolean onConditionsMet(Request request, Response response, Callback callback) throws Exception
{
_set.clear();
return nextHandler(request, response, callback);
}
@Override
protected boolean onConditionsNotMet(Request request, Response response, Callback callback) throws Exception
{
Response.writeError(request, response, callback, HttpStatus.FORBIDDEN_403);
return true;
}
/**
@ -87,7 +78,7 @@ public class InetAccessHandler extends Handler.Wrapper
*/
public void include(String pattern)
{
_set.include(PatternTuple.from(pattern));
includeExclude(true, pattern);
}
/**
@ -113,7 +104,7 @@ public class InetAccessHandler extends Handler.Wrapper
*/
public void include(String connectorName, String addressPattern, PathSpec pathSpec)
{
_set.include(new PatternTuple(connectorName, InetAddressPattern.from(addressPattern), pathSpec));
include(from(connectorName, InetAddressPattern.from(addressPattern), null, pathSpec));
}
/**
@ -121,13 +112,16 @@ public class InetAccessHandler extends Handler.Wrapper
*
* <p>The connector name is separated from the InetAddress pattern with an '@' character,
* and the InetAddress pattern is separated from the URI pattern using the "|" (pipe)
* character. URI patterns follow the servlet specification for simple * prefix and
* character. A method name is separated from the URI pattern using the ">" character.
* URI patterns follow the servlet specification for simple * prefix and
* suffix wild cards (e.g. /, /foo, /foo/bar, /foo/bar/*, *.baz).</p>
*
* <br>Examples:
* <ul>
* <li>"connector1@127.0.0.1|/foo"</li>
* <li>"127.0.0.1|/foo"</li>
* <li>"127.0.0.1>GET|/foo"</li>
* <li>"127.0.0.1>GET"</li>
* <li>"connector1@127.0.0.1"</li>
* <li>"127.0.0.1"</li>
* </ul>
@ -137,7 +131,7 @@ public class InetAccessHandler extends Handler.Wrapper
*/
public void exclude(String pattern)
{
_set.exclude(PatternTuple.from(pattern));
includeExclude(false, pattern);
}
/**
@ -163,41 +157,41 @@ public class InetAccessHandler extends Handler.Wrapper
*/
public void exclude(String connectorName, String addressPattern, PathSpec pathSpec)
{
_set.exclude(new PatternTuple(connectorName, InetAddressPattern.from(addressPattern), pathSpec));
exclude(from(connectorName, InetAddressPattern.from(addressPattern), null, pathSpec));
}
@Override
public boolean handle(Request request, Response response, Callback callback) throws Exception
private void includeExclude(boolean include, String pattern)
{
SocketAddress socketAddress = request.getConnectionMetaData().getRemoteSocketAddress();
if (socketAddress instanceof InetSocketAddress inetSocketAddress && !isAllowed(inetSocketAddress.getAddress(), request))
String path = null;
int pathIndex = pattern.indexOf('|');
if (pathIndex >= 0)
{
// TODO a false return may be better here.
Response.writeError(request, response, callback, HttpStatus.FORBIDDEN_403);
return true;
}
return super.handle(request, response, callback);
path = pattern.substring(pathIndex + 1);
pattern = pattern.substring(0, pathIndex);
}
/**
* Checks if specified address and request are allowed by current InetAddress rules.
*
* @param addr the inetAddress to check
* @param request the HttpServletRequest request to check
* @return true if inetAddress and request are allowed
*/
protected boolean isAllowed(InetAddress addr, Request request)
String method = null;
int methodIndex = pattern.indexOf('>');
if (methodIndex >= 0)
{
String connectorName = request.getConnectionMetaData().getConnector().getName();
String path = Request.getPathInContext(request);
return _set.test(new AccessTuple(connectorName, addr, path));
method = pattern.substring(methodIndex + 1);
pattern = pattern.substring(0, methodIndex);
}
@Override
public void dump(Appendable out, String indent) throws IOException
{
dumpObjects(out, indent,
new DumpableCollection("included", _set.getIncluded()),
new DumpableCollection("excluded", _set.getExcluded()));
String connector = null;
int connectorIndex = pattern.indexOf('@');
if (connectorIndex >= 0)
connector = pattern.substring(0, connectorIndex);
String addr = null;
int addrStart = (connectorIndex < 0) ? 0 : connectorIndex + 1;
int addrEnd = (pathIndex < 0) ? pattern.length() : pathIndex;
if (addrStart != addrEnd)
addr = pattern.substring(addrStart, addrEnd);
if (include)
include(from(connector, addr, method, path));
else
exclude(from(connector, addr, method, path));
}
}

View File

@ -1,156 +0,0 @@
//
// ========================================================================
// 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.server.handler;
import java.net.InetAddress;
import java.util.AbstractSet;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.Set;
import java.util.function.Predicate;
import org.eclipse.jetty.http.pathmap.PathSpec;
import org.eclipse.jetty.http.pathmap.ServletPathSpec;
import org.eclipse.jetty.util.InetAddressPattern;
import org.eclipse.jetty.util.StringUtil;
public class InetAccessSet extends AbstractSet<InetAccessSet.PatternTuple> implements Set<InetAccessSet.PatternTuple>, Predicate<InetAccessSet.AccessTuple>
{
private final ArrayList<PatternTuple> tuples = new ArrayList<>();
@Override
public boolean add(PatternTuple storageTuple)
{
return tuples.add(storageTuple);
}
@Override
public boolean remove(Object o)
{
return tuples.remove(o);
}
@Override
public Iterator<PatternTuple> iterator()
{
return tuples.iterator();
}
@Override
public int size()
{
return tuples.size();
}
@Override
public boolean test(AccessTuple entry)
{
if (entry == null)
return false;
for (PatternTuple tuple : tuples)
{
if (tuple.test(entry))
return true;
}
return false;
}
public static class PatternTuple implements Predicate<AccessTuple>
{
private final String connector;
private final InetAddressPattern address;
private final PathSpec pathSpec;
public static PatternTuple from(String pattern)
{
String path = null;
int pathIndex = pattern.indexOf('|');
if (pathIndex >= 0)
path = pattern.substring(pathIndex + 1);
String connector = null;
int connectorIndex = pattern.indexOf('@');
if (connectorIndex >= 0)
connector = pattern.substring(0, connectorIndex);
String addr = null;
int addrStart = (connectorIndex < 0) ? 0 : connectorIndex + 1;
int addrEnd = (pathIndex < 0) ? pattern.length() : pathIndex;
if (addrStart != addrEnd)
addr = pattern.substring(addrStart, addrEnd);
return new PatternTuple(connector, InetAddressPattern.from(addr),
StringUtil.isEmpty(path) ? null : new ServletPathSpec(path));
}
public PatternTuple(String connector, InetAddressPattern address, PathSpec pathSpec)
{
this.connector = connector;
this.address = address;
this.pathSpec = pathSpec;
}
@Override
public boolean test(AccessTuple entry)
{
// Match for connector.
if ((connector != null) && !connector.equals(entry.getConnector()))
return false;
// If we have a path we must be at this path to match for an address.
if ((pathSpec != null) && !pathSpec.matches(entry.getPath()))
return false;
// Match for InetAddress.
return (address == null) || address.test(entry.getAddress());
}
@Override
public String toString()
{
return String.format("%s@%x{connector=%s, addressPattern=%s, pathSpec=%s}", getClass().getSimpleName(), hashCode(), connector, address, pathSpec);
}
}
public static class AccessTuple
{
private final String connector;
private final InetAddress address;
private final String path;
public AccessTuple(String connector, InetAddress address, String path)
{
this.connector = connector;
this.address = address;
this.path = path;
}
public String getConnector()
{
return connector;
}
public InetAddress getAddress()
{
return address;
}
public String getPath()
{
return path;
}
}
}

View File

@ -42,9 +42,9 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* <p>A quality of service {@link Handler} that limits the number
* of concurrent requests, to provide more predictable end-user
* experience in case descendant {@link Handler}s have limited
* <p>A quality of service {@link Handler} that {@link ConditionalHandler conditionally}
* limits the number of concurrent requests, to provide more predictable
* end-user experience in case descendant {@link Handler}s have limited
* capacity.</p>
* <p>This {@code Handler} limits the number of concurrent requests
* to the number configured via {@link #setMaxRequestCount(int)}.
@ -70,7 +70,7 @@ import org.slf4j.LoggerFactory;
* always be able to access the web application.</p>
*/
@ManagedObject
public class QoSHandler extends Handler.Wrapper
public class QoSHandler extends ConditionalHandler.Abstract
{
private static final Logger LOG = LoggerFactory.getLogger(QoSHandler.class);
private static final String EXPIRED_ATTRIBUTE_NAME = QoSHandler.class.getName() + ".expired";
@ -181,7 +181,7 @@ public class QoSHandler extends Handler.Wrapper
}
@Override
public boolean handle(Request request, Response response, Callback callback) throws Exception
public boolean onConditionsMet(Request request, Response response, Callback callback) throws Exception
{
if (LOG.isDebugEnabled())
LOG.debug("{} handling {}", this, request);
@ -213,6 +213,12 @@ public class QoSHandler extends Handler.Wrapper
}
}
@Override
protected boolean onConditionsNotMet(Request request, Response response, Callback callback) throws Exception
{
return nextHandler(request, response, callback);
}
private static void notAvailable(Response response, Callback callback)
{
response.setStatus(HttpStatus.SERVICE_UNAVAILABLE_503);
@ -258,7 +264,7 @@ public class QoSHandler extends Handler.Wrapper
if (LOG.isDebugEnabled())
LOG.debug("{} forwarding {}", this, request);
request.addHttpStreamWrapper(stream -> new Resumer(stream, request));
return super.handle(request, response, callback);
return nextHandler(request, response, callback);
}
private void suspend(Request request, Response response, Callback callback)

View File

@ -13,7 +13,6 @@
package org.eclipse.jetty.server.handler;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.WritePendingException;
@ -37,8 +36,6 @@ import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Response;
import org.eclipse.jetty.util.Callback;
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;
@ -50,30 +47,27 @@ import org.slf4j.LoggerFactory;
/**
* <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>
* 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:
* any that may be made by the {@link ForwardedRequestCustomizer} or similar:</p>
* <ul>
* <li>This handler will use either only a single style
* of forwarded header. This is on the assumption that a trusted local proxy
* <li>This handler will use 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>
* <p>Requests in excess of the limit will be asynchronously suspended until
* a thread is available.</p>
*/
public class ThreadLimitHandler extends Handler.Wrapper
public class ThreadLimitHandler extends ConditionalHandler.Abstract
{
private static final Logger LOG = LoggerFactory.getLogger(ThreadLimitHandler.class);
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;
@ -105,7 +99,7 @@ public class ThreadLimitHandler extends Handler.Wrapper
protected void doStart() throws Exception
{
super.doStart();
LOG.info(String.format("ThreadLimitHandler enable=%b limit=%d include=%s", _enabled, _threadLimit, _includeExcludeSet));
LOG.info(String.format("ThreadLimitHandler enable=%b limit=%d", _enabled, _threadLimit));
}
@ManagedAttribute("true if this handler is enabled")
@ -117,7 +111,7 @@ public class ThreadLimitHandler extends Handler.Wrapper
public void setEnabled(boolean enabled)
{
_enabled = enabled;
LOG.info(String.format("ThreadLimitHandler enable=%b limit=%d include=%s", _enabled, _threadLimit, _includeExcludeSet));
LOG.info(String.format("ThreadLimitHandler enable=%b limit=%d", _enabled, _threadLimit));
}
@ManagedAttribute("The maximum threads that can be dispatched per remote IP")
@ -128,21 +122,6 @@ public class ThreadLimitHandler extends Handler.Wrapper
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;
}
@ -156,17 +135,17 @@ public class ThreadLimitHandler extends Handler.Wrapper
@ManagedOperation("Include IP in thread limits")
public void include(String inetAddressPattern)
{
_includeExcludeSet.include(inetAddressPattern);
includeInetAddressPattern(inetAddressPattern);
}
@ManagedOperation("Exclude IP from thread limits")
public void exclude(String inetAddressPattern)
{
_includeExcludeSet.exclude(inetAddressPattern);
excludeInetAddressPattern(inetAddressPattern);
}
@Override
public boolean handle(Request request, Response response, Callback callback) throws Exception
public boolean onConditionsMet(Request request, Response response, Callback callback) throws Exception
{
Handler next = getHandler();
if (next == null)
@ -189,6 +168,12 @@ public class ThreadLimitHandler extends Handler.Wrapper
return true;
}
@Override
protected boolean onConditionsNotMet(Request request, Response response, Callback callback) throws Exception
{
return nextHandler(request, response, callback);
}
private Remote getRemote(Request baseRequest)
{
String ip = getRemoteIP(baseRequest);

View File

@ -51,9 +51,9 @@ public class BufferedResponseHandlerTest
_server.addConnector(_local);
BufferedResponseHandler bufferedHandler = new BufferedResponseHandler();
bufferedHandler.getPathIncludeExclude().include("/include/*");
bufferedHandler.getPathIncludeExclude().exclude("*.exclude");
bufferedHandler.getMimeIncludeExclude().exclude("text/excluded");
bufferedHandler.includePath("/include/*");
bufferedHandler.excludePath("*.exclude");
bufferedHandler.excludeMimeType("text/excluded");
bufferedHandler.setHandler(_test = new TestHandler());
@ -221,7 +221,8 @@ public class BufferedResponseHandlerTest
response.getHeaders().put(HttpHeader.CONTENT_TYPE, _mimeType);
// Do not close the stream before adding the header: Written: true.
OutputStream outputStream = Content.Sink.asOutputStream(response);
try (OutputStream outputStream = Content.Sink.asOutputStream(response))
{
for (int i = 0; i < _writes; i++)
{
response.getHeaders().add("Write", Integer.toString(i));
@ -229,9 +230,10 @@ public class BufferedResponseHandlerTest
if (_flush)
outputStream.flush();
}
response.getHeaders().add("Written", "true");
callback.succeeded();
}
return true;
}
}

View File

@ -0,0 +1,282 @@
//
// ========================================================================
// 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.server.handler;
import java.util.function.Predicate;
import java.util.stream.Stream;
import org.eclipse.jetty.http.pathmap.PathSpec;
import org.eclipse.jetty.server.ForwardedRequestCustomizer;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.HttpConnectionFactory;
import org.eclipse.jetty.server.LocalConnector;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Response;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.util.Callback;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.not;
public class ConditionalHandlerTest
{
private Server _server;
private LocalConnector _connector;
private HelloHandler _helloHandler;
private Expected _expected;
@BeforeEach
public void beforeEach() throws Exception
{
_server = new Server();
_connector = new LocalConnector(_server);
_connector.getConnectionFactory(HttpConnectionFactory.class).getHttpConfiguration().addCustomizer(new ForwardedRequestCustomizer());
_server.addConnector(_connector);
_helloHandler = new HelloHandler();
}
@AfterEach
public void afterEach() throws Exception
{
_server.stop();
}
private void startServer(Handler.Singleton testHandler) throws Exception
{
_expected = (Expected)testHandler;
_server.setHandler(testHandler);
testHandler.getTail().setHandler(_helloHandler);
_server.start();
}
public static Stream<ConditionalHandler> conditionalHandlers()
{
return Stream.of(
new TestConditionalHandler(),
new TestConditionalHandlerSkipNext(new TestHandler()),
new TestConditionalHandlerDontHandle(new TestHandler()),
new TestConditionalHandlerReject(new TestHandler())
);
}
@ParameterizedTest
@MethodSource("conditionalHandlers")
public void testNoConditions(ConditionalHandler testHandler) throws Exception
{
startServer(testHandler);
Expected expected = (Expected)testHandler;
String response = _connector.getResponse("GET / HTTP/1.0\n\n");
expected.testDoHandle(response);
response = _connector.getResponse("POST /foo HTTP/1.0\n\n");
expected.testDoHandle(response);
}
@ParameterizedTest
@MethodSource("conditionalHandlers")
public void testMethod(ConditionalHandler testHandler) throws Exception
{
testHandler.includeMethod("GET");
testHandler.excludeMethod("POST");
startServer(testHandler);
String response = _connector.getResponse("GET / HTTP/1.0\n\n");
_expected.testDoHandle(response);
response = _connector.getResponse("POST /foo HTTP/1.0\n\n");
_expected.testDoNotHandle(response);
}
@ParameterizedTest
@MethodSource("conditionalHandlers")
public void testPath(ConditionalHandler testHandler) throws Exception
{
testHandler.includePath("/foo/*");
testHandler.excludePath("/foo/bar");
startServer(testHandler);
String response = _connector.getResponse("GET /foo HTTP/1.0\n\n");
_expected.testDoHandle(response);
response = _connector.getResponse("POST /foo/bar HTTP/1.0\n\n");
_expected.testDoNotHandle(response);
}
@ParameterizedTest
@MethodSource("conditionalHandlers")
public void testInet(ConditionalHandler testHandler) throws Exception
{
testHandler.includeInetAddressPattern("192.168.128.0-192.168.128.128");
testHandler.excludeInetAddressPattern("192.168.128.30-192.168.128.39");
startServer(testHandler);
String response = _connector.getResponse("""
GET /foo HTTP/1.0
Forwarded: for=192.168.128.1
""");
_expected.testDoHandle(response);
response = _connector.getResponse("""
GET /foo HTTP/1.0
Forwarded: for=192.168.128.31
""");
_expected.testDoNotHandle(response);
}
@ParameterizedTest
@MethodSource("conditionalHandlers")
public void testMethodPath(ConditionalHandler testHandler) throws Exception
{
testHandler.includeMethod("GET");
testHandler.excludeMethod("POST");
testHandler.includePath("/foo/*");
testHandler.excludePath("/foo/bar");
startServer(testHandler);
String response = _connector.getResponse("GET /foo HTTP/1.0\n\n");
_expected.testDoHandle(response);
response = _connector.getResponse("GET /foo/bar HTTP/1.0\n\n");
_expected.testDoNotHandle(response);
response = _connector.getResponse("POST /foo HTTP/1.0\n\n");
_expected.testDoNotHandle(response);
response = _connector.getResponse("POST /foo/bar HTTP/1.0\n\n");
_expected.testDoNotHandle(response);
}
@Test
public void testMethodPredicateOptimization()
{
Predicate<Request> predicate = ConditionalHandler.from(null, null, "GET", (String)null);
assertThat(predicate, instanceOf(ConditionalHandler.MethodPredicate.class));
ConditionalHandler conditionalHandler = new ConditionalHandler.DontHandle();
conditionalHandler.include(predicate);
assertThat(conditionalHandler.getMethods().getIncluded(), hasSize(1));
assertThat(conditionalHandler.getPredicates().getIncluded(), hasSize(0));
}
@Test
public void testPathSpecPredicateOptimization()
{
Predicate<Request> predicate = ConditionalHandler.from(null, null, null, PathSpec.from("/*"));
assertThat(predicate, instanceOf(ConditionalHandler.PathSpecPredicate.class));
ConditionalHandler conditionalHandler = new ConditionalHandler.DontHandle();
conditionalHandler.include(predicate);
assertThat(conditionalHandler.getPathSpecs().getIncluded(), hasSize(1));
assertThat(conditionalHandler.getPredicates().getIncluded(), hasSize(0));
}
interface Expected
{
void testDoHandle(String response);
default void testDoNotHandle(String response)
{
assertThat(response, containsString("200 OK"));
assertThat(response, containsString("Test: applied"));
}
}
public static class TestConditionalHandler extends ConditionalHandler.Abstract implements Expected
{
@Override
public boolean onConditionsMet(Request request, Response response, Callback callback) throws Exception
{
response.getHeaders().put("Test", "applied");
return nextHandler(request, response, callback);
}
@Override
protected boolean onConditionsNotMet(Request request, Response response, Callback callback) throws Exception
{
return nextHandler(request, response, callback);
}
public void testDoHandle(String response)
{
assertThat(response, containsString("200 OK"));
assertThat(response, containsString("Test: applied"));
}
public void testDoNotHandle(String response)
{
assertThat(response, containsString("200 OK"));
assertThat(response, not(containsString("Test: applied")));
}
}
public static class TestConditionalHandlerSkipNext extends ConditionalHandler.SkipNext implements Expected
{
TestConditionalHandlerSkipNext(Handler handler)
{
super(handler);
}
public void testDoHandle(String response)
{
assertThat(response, containsString("200 OK"));
assertThat(response, not(containsString("Test: applied")));
}
}
public static class TestConditionalHandlerDontHandle extends ConditionalHandler.DontHandle implements Expected
{
TestConditionalHandlerDontHandle(Handler handler)
{
super(handler);
}
public void testDoHandle(String response)
{
assertThat(response, containsString("404 Not Found"));
assertThat(response, not(containsString("Test: applied")));
}
}
public static class TestConditionalHandlerReject extends ConditionalHandler.Reject implements Expected
{
TestConditionalHandlerReject(Handler handler)
{
super(handler);
}
public void testDoHandle(String response)
{
assertThat(response, containsString("403 Forbidden"));
assertThat(response, not(containsString("Test: applied")));
}
}
public static class TestHandler extends Handler.Wrapper
{
@Override
public boolean handle(Request request, Response response, Callback callback) throws Exception
{
response.getHeaders().put("Test", "applied");
return super.handle(request, response, callback);
}
}
}

View File

@ -31,8 +31,8 @@ import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.StringUtil;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
@ -41,13 +41,13 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
public class InetAccessHandlerTest
{
private static Server _server;
private static ServerConnector _connector1;
private static ServerConnector _connector2;
private static InetAccessHandler _handler;
private Server _server;
private ServerConnector _connector1;
private ServerConnector _connector2;
private InetAccessHandler _handler;
@BeforeAll
public static void setUp() throws Exception
@BeforeEach
public void setUp() throws Exception
{
_server = new Server();
_connector1 = new ServerConnector(_server);
@ -70,11 +70,10 @@ public class InetAccessHandlerTest
});
_server.setHandler(_handler);
_server.start();
}
@AfterAll
public static void tearDown() throws Exception
@AfterEach
public void tearDown() throws Exception
{
_server.stop();
}
@ -123,6 +122,7 @@ public class InetAccessHandlerTest
}
}
_server.start();
testConnector(_connector1.getLocalPort(), path, include, exclude, includeConnectors, excludeConnectors, codePerConnector.get(0));
testConnector(_connector2.getLocalPort(), path, include, exclude, includeConnectors, excludeConnectors, codePerConnector.get(1));
}

View File

@ -334,4 +334,77 @@ public class QoSHandlerTest
})
);
}
@Test
public void testConditional() throws Exception
{
int maxRequests = 1;
QoSHandler qosHandler = new QoSHandler();
qosHandler.excludePath("/special/*");
qosHandler.setMaxRequestCount(maxRequests);
List<Callback> callbacks = new ArrayList<>();
qosHandler.setHandler(new Handler.Abstract()
{
@Override
public boolean handle(Request request, Response response, Callback callback)
{
// Save the callback but do not succeed it yet.
callbacks.add(callback);
return true;
}
});
start(qosHandler);
// Wait until a normal request arrives at the handler.
LocalConnector.LocalEndPoint normalEndPoint = connector.executeRequest("""
GET /normal/request HTTP/1.1
Host: localhost
""");
await().atMost(5, TimeUnit.SECONDS).until(callbacks::size, is(1));
// Check that another normal request does not arrive at the handler
LocalConnector.LocalEndPoint anotherEndPoint = connector.executeRequest("""
GET /another/normal/request HTTP/1.1
Host: localhost
""");
await().atLeast(100, TimeUnit.MILLISECONDS).until(callbacks::size, is(1));
// Wait until special request arrives at the handler
LocalConnector.LocalEndPoint specialEndPoint = connector.executeRequest("""
GET /special/info HTTP/1.1
Host: localhost
""");
// Wait that the request arrives at the server.
await().atMost(5, TimeUnit.SECONDS).until(callbacks::size, is(2));
// Finish the special request
callbacks.get(1).succeeded();
String text = specialEndPoint.getResponse(false, 5, TimeUnit.SECONDS);
HttpTester.Response response = HttpTester.parseResponse(text);
assertEquals(HttpStatus.OK_200, response.getStatus());
// Check that other normal request is still waiting
await().atLeast(100, TimeUnit.MILLISECONDS).until(callbacks::size, is(2));
// Finish the first normal request
callbacks.get(0).succeeded();
text = normalEndPoint.getResponse(false, 5, TimeUnit.SECONDS);
response = HttpTester.parseResponse(text);
assertEquals(HttpStatus.OK_200, response.getStatus());
// wait for the second normal request to arrive at the handler
await().atMost(5, TimeUnit.SECONDS).until(callbacks::size, is(3));
// Finish the second normal request
callbacks.get(2).succeeded();
text = anotherEndPoint.getResponse(false, 5, TimeUnit.SECONDS);
response = HttpTester.parseResponse(text);
assertEquals(HttpStatus.OK_200, response.getStatus());
}
}

View File

@ -79,6 +79,18 @@ public abstract class InetAddressPattern implements Predicate<InetAddress>
_pattern = pattern;
}
@Override
public int hashCode()
{
return _pattern.hashCode();
}
@Override
public boolean equals(Object obj)
{
return obj instanceof InetAddressPattern inetAddressPattern && _pattern.equals(inetAddressPattern._pattern);
}
@Override
public String toString()
{
@ -150,6 +162,8 @@ public abstract class InetAddressPattern implements Predicate<InetAddress>
@Override
public boolean test(InetAddress address)
{
if (address == null)
return false;
byte[] raw = address.getAddress();
if (raw.length != _min.length)
return false;
@ -214,6 +228,8 @@ public abstract class InetAddressPattern implements Predicate<InetAddress>
@Override
public boolean test(InetAddress address)
{
if (address == null)
return false;
byte[] raw = address.getAddress();
if (raw.length != _raw.length)
return false;

View File

@ -34,10 +34,12 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.ServiceConfigurationError;
import java.util.ServiceLoader;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;
@ -725,6 +727,78 @@ public class TypeUtil
return StreamSupport.stream(new ServiceLoaderSpliterator<>(serviceLoader), false);
}
/**
* A Predicate that is always true, with optimized {@code and}/{@code or}/{@code not} methods.
* @param <T> The type of the predicate test
* @return true
*/
public static <T> Predicate<T> truePredicate()
{
return new Predicate<T>()
{
@Override
public boolean test(T t)
{
return true;
}
@Override
@SuppressWarnings("unchecked")
public Predicate<T> and(Predicate<? super T> other)
{
return (Predicate<T>)Objects.requireNonNull(other);
}
@Override
public Predicate<T> negate()
{
return falsePredicate();
}
@Override
public Predicate<T> or(Predicate<? super T> other)
{
return this;
}
};
}
/**
* A {@link Predicate} that is always false, with optimized {@code and}/{@code or}/{@code not} methods.
* @param <T> The type of the predicate test
* @return true
*/
public static <T> Predicate<T> falsePredicate()
{
return new Predicate<T>()
{
@Override
public boolean test(T t)
{
return false;
}
@Override
public Predicate<T> and(Predicate<? super T> other)
{
return this;
}
@Override
public Predicate<T> negate()
{
return truePredicate();
}
@Override
@SuppressWarnings("unchecked")
public Predicate<T> or(Predicate<? super T> other)
{
return (Predicate<T>)Objects.requireNonNull(other);
}
};
}
private TypeUtil()
{
// prevents instantiation