ConditionalHandler (#10492)
Added a Conditional Handler Co-authored-by: Jan Bartel <janb@webtide.com>
This commit is contained in:
parent
9bfa5cc65e
commit
f9ca02c393
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
path = pattern.substring(pathIndex + 1);
|
||||
pattern = pattern.substring(0, pathIndex);
|
||||
}
|
||||
return super.handle(request, response, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 connectorName = request.getConnectionMetaData().getConnector().getName();
|
||||
String path = Request.getPathInContext(request);
|
||||
return _set.test(new AccessTuple(connectorName, addr, path));
|
||||
}
|
||||
String method = null;
|
||||
int methodIndex = pattern.indexOf('>');
|
||||
if (methodIndex >= 0)
|
||||
{
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,17 +221,19 @@ 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);
|
||||
for (int i = 0; i < _writes; i++)
|
||||
try (OutputStream outputStream = Content.Sink.asOutputStream(response))
|
||||
{
|
||||
response.getHeaders().add("Write", Integer.toString(i));
|
||||
outputStream.write(_content);
|
||||
if (_flush)
|
||||
outputStream.flush();
|
||||
for (int i = 0; i < _writes; i++)
|
||||
{
|
||||
response.getHeaders().add("Write", Integer.toString(i));
|
||||
outputStream.write(_content);
|
||||
if (_flush)
|
||||
outputStream.flush();
|
||||
}
|
||||
response.getHeaders().add("Written", "true");
|
||||
callback.succeeded();
|
||||
}
|
||||
|
||||
response.getHeaders().add("Written", "true");
|
||||
callback.succeeded();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue