Reintroduce Cross Context Dispatch in Jetty 12 (#11451)

Re-introduce some support for cross context dispatch
This commit is contained in:
Jan Bartel 2024-03-25 18:44:44 +01:00 committed by GitHub
parent 2aa93575e7
commit 2fc7ad87d8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
45 changed files with 6824 additions and 633 deletions

View File

@ -26,11 +26,9 @@ import org.slf4j.LoggerFactory;
/**
* Cookie parser
* <p>Optimized stateful cookie parser.
* If the added fields are identical to those last added (as strings), then the
* cookies are not re-parsed.
*
* @deprecated Use {@code org.eclipse.jetty.server.CookieCache}
*/
@Deprecated (forRemoval = true)
public class CookieCache implements CookieParser.Handler, ComplianceViolation.Listener
{
protected static final Logger LOG = LoggerFactory.getLogger(CookieCache.class);
@ -39,11 +37,13 @@ public class CookieCache implements CookieParser.Handler, ComplianceViolation.Li
private final CookieParser _parser;
private List<ComplianceViolation.Event> _violations;
@Deprecated
public CookieCache()
{
this(CookieCompliance.RFC6265);
}
@Deprecated
public CookieCache(CookieCompliance compliance)
{
_parser = CookieParser.newParser(this, compliance, this);

View File

@ -474,6 +474,10 @@ public abstract class SecurityHandler extends Handler.Wrapper implements Configu
if (next == null)
return false;
// Skip security check if this is a dispatch rather than a fresh request
if (request.getContext().isCrossContextDispatch(request))
return next.handle(request, response, callback);
String pathInContext = Request.getPathInContext(request);
Constraint constraint = getConstraint(pathInContext, request);
if (LOG.isDebugEnabled())

View File

@ -117,6 +117,29 @@ public interface Context extends Attributes, Decorator, Executor
*/
File getTempDirectory();
/**
* Check cross context dispatch status
* @param request The request to check
* @return {@code True} IFF this context {@link ContextHandler#isCrossContextDispatchSupported() supports cross context}
* and the passed request is a cross context request.
*/
default boolean isCrossContextDispatch(Request request)
{
return false;
}
/**
* Get any cross context dispatch type
* @param request The request to get the type for
* @return A String representation of a dispatcher type iff this context
* {@link ContextHandler#isCrossContextDispatchSupported() supports cross context}
* and the passed request is a cross context request, otherwise null.
*/
default String getCrossContextDispatchType(Request request)
{
return null;
}
/**
* <p>Returns the URI path scoped to the passed context path.</p>
* <p>For example, if the context path passed is {@code /ctx} then a

View File

@ -0,0 +1,301 @@
//
// ========================================================================
// 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;
import java.lang.reflect.Array;
import java.util.AbstractList;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.function.Function;
import org.eclipse.jetty.http.BadMessageException;
import org.eclipse.jetty.http.ComplianceViolation;
import org.eclipse.jetty.http.CookieCompliance;
import org.eclipse.jetty.http.CookieParser;
import org.eclipse.jetty.http.HttpCookie;
import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.util.StringUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Cookie parser
* <p>Optimized stateful cookie parser.
* If the added fields are identical to those last added (as strings), then the
* cookies are not re-parsed.
*
*/
public class CookieCache extends AbstractList<HttpCookie> implements CookieParser.Handler, ComplianceViolation.Listener
{
/**
* Get the core HttpCookies for a request.
* Cookies may be cached as a {@link Request#getAttribute(String) request attribute}, failing that they may be
* cached in the {@link Components#getCache() Component cache}, in which case they will be checked to see if they
* have changed since a previous request. Otherwise, they are parsed from the request headers and both caches updated.
* @param request The request to obtain cookies from
* @return A list of core {@link HttpCookie}s from the request.
* @see #getApiCookies(Request, Class, Function)
*/
public static List<HttpCookie> getCookies(Request request)
{
@SuppressWarnings("unchecked")
List<HttpCookie> cookies = (List<HttpCookie>)request.getAttribute(Request.COOKIE_ATTRIBUTE);
if (cookies != null)
return cookies;
CookieCache cookieCache = (CookieCache)request.getComponents().getCache().getAttribute(Request.COOKIE_ATTRIBUTE);
if (cookieCache == null)
{
cookieCache = new CookieCache(request.getConnectionMetaData().getHttpConfiguration().getRequestCookieCompliance());
request.getComponents().getCache().setAttribute(Request.COOKIE_ATTRIBUTE, cookieCache);
}
cookieCache.parseCookies(request.getHeaders(), HttpChannel.from(request).getComplianceViolationListener());
request.setAttribute(Request.COOKIE_ATTRIBUTE, cookieCache);
return cookieCache;
}
/**
* Get the API specific cookies for a request.
* Internally the same caching/parsing is done as by {@link #getCookies(Request)} and the core {@link HttpCookie}s are
* obtained. The passed {@code convertor} function is used to covert the core {@link HttpCookie}s to API specific cookies
* and the results cached along with the core {@link HttpCookie}s
* @param request The request to get the cookies from.
* @param cookieClass The class of the cookie API
* @param convertor A function to convert from a {@link HttpCookie} to an API cookie of type {@code cookieClass}. The
* function may return null if the cookie is not compliant with the API.
* @param <C> The class of the cookie API
* @return An array of API specific cookies.
*/
public static <C> C[] getApiCookies(Request request, Class<C> cookieClass, Function<HttpCookie, C> convertor)
{
if (request == null)
return null;
CookieCache cookieCache = (CookieCache)request.getAttribute(Request.COOKIE_ATTRIBUTE);
if (cookieCache == null)
{
cookieCache = (CookieCache)request.getComponents().getCache().getAttribute(Request.COOKIE_ATTRIBUTE);
if (cookieCache == null)
{
cookieCache = new CookieCache(request.getConnectionMetaData().getHttpConfiguration().getRequestCookieCompliance());
request.getComponents().getCache().setAttribute(Request.COOKIE_ATTRIBUTE, cookieCache);
}
cookieCache.parseCookies(request.getHeaders(), HttpChannel.from(request).getComplianceViolationListener());
request.setAttribute(Request.COOKIE_ATTRIBUTE, cookieCache);
}
return cookieCache.getApiCookies(cookieClass, convertor);
}
protected static final Logger LOG = LoggerFactory.getLogger(CookieCache.class);
protected final List<String> _rawFields = new ArrayList<>();
private final CookieParser _parser;
private List<HttpCookie> _httpCookies = Collections.emptyList();
private Map<Class<?>, Object[]> _apiCookies;
private List<ComplianceViolation.Event> _violations;
public CookieCache()
{
this(CookieCompliance.RFC6265);
}
public CookieCache(CookieCompliance compliance)
{
_parser = CookieParser.newParser(this, compliance, this);
}
@Override
public HttpCookie get(int index)
{
return _httpCookies.get(index);
}
@Override
public int size()
{
return _httpCookies.size();
}
@Override
public void onComplianceViolation(ComplianceViolation.Event event)
{
if (_violations == null)
_violations = new ArrayList<>();
_violations.add(event);
}
@Override
public void addCookie(String cookieName, String cookieValue, int cookieVersion, String cookieDomain, String cookiePath, String cookieComment)
{
if (StringUtil.isEmpty(cookieDomain) && StringUtil.isEmpty(cookiePath) && cookieVersion <= 0 && StringUtil.isEmpty(cookieComment))
_httpCookies.add(HttpCookie.from(cookieName, cookieValue));
else
{
Map<String, String> attributes = new HashMap<>();
if (!StringUtil.isEmpty(cookieDomain))
attributes.put(HttpCookie.DOMAIN_ATTRIBUTE, cookieDomain);
if (!StringUtil.isEmpty(cookiePath))
attributes.put(HttpCookie.PATH_ATTRIBUTE, cookiePath);
if (!StringUtil.isEmpty(cookieComment))
attributes.put(HttpCookie.COMMENT_ATTRIBUTE, cookieComment);
_httpCookies.add(HttpCookie.from(cookieName, cookieValue, cookieVersion, attributes));
}
}
List<HttpCookie> getCookies(HttpFields headers)
{
parseCookies(headers, ComplianceViolation.Listener.NOOP);
return _httpCookies;
}
public void parseCookies(HttpFields headers, ComplianceViolation.Listener complianceViolationListener)
{
boolean building = false;
ListIterator<String> raw = _rawFields.listIterator();
// For each of the headers
for (HttpField field : headers)
{
// skip non cookie headers
if (!HttpHeader.COOKIE.equals(field.getHeader()))
continue;
// skip blank cookie headers
String value = field.getValue();
if (StringUtil.isBlank(value))
continue;
// If we are building a new cookie list
if (building)
{
// just add the raw string to the list to be parsed later
_rawFields.add(value);
continue;
}
// otherwise we are checking against previous cookies.
// Is there a previous raw cookie to compare with?
if (!raw.hasNext())
{
// No, so we will flip to building state and add to the raw fields we already have.
building = true;
_rawFields.add(value);
continue;
}
// If there is a previous raw cookie and it is the same, then continue checking
if (value.equals(raw.next()))
continue;
// otherwise there is a difference in the previous raw cookie field
// so switch to building mode and remove all subsequent raw fields
// then add the current raw field to be built later.
building = true;
raw.remove();
while (raw.hasNext())
{
raw.next();
raw.remove();
}
_rawFields.add(value);
}
// If we are not building, but there are still more unmatched raw fields, then a field was deleted
if (!building && raw.hasNext())
{
// switch to building mode and delete the unmatched raw fields
building = true;
while (raw.hasNext())
{
raw.next();
raw.remove();
}
}
// If we ended up in building mode, reparse the cookie list from the raw fields.
if (building)
{
_httpCookies = new ArrayList<>();
_apiCookies = null;
try
{
if (_violations != null)
_violations.clear();
_parser.parseFields(_rawFields);
}
catch (CookieParser.InvalidCookieException invalidCookieException)
{
throw new BadMessageException(invalidCookieException.getMessage(), invalidCookieException);
}
}
if (_violations != null && !_violations.isEmpty())
_violations.forEach(complianceViolationListener::onComplianceViolation);
}
public <C> C[] getApiCookies(Class<C> apiClass, Function<HttpCookie, C> convertor)
{
// No APIs if no Cookies
if (_httpCookies.isEmpty())
return null;
// If only the core APIs have been used, then no apiCookie map has been created
if (_apiCookies == null)
{
// When a cookie API is ued, the most common case in only a single API, so used a cheap Map
C[] apiCookies = convert(apiClass, convertor);
_apiCookies = Map.of(apiClass, apiCookies);
return apiCookies;
}
@SuppressWarnings("unchecked")
C[] apiCookies = (C[])_apiCookies.get(apiClass);
if (apiCookies == null)
{
// Only in the case of cross environment dispatch will more than 1 API be needed, so only invest in a real
// map when we know it is required.
if (_apiCookies.size() == 1)
_apiCookies = new HashMap<>(_apiCookies);
apiCookies = convert(apiClass, convertor);
_apiCookies.put(apiClass, apiCookies);
}
return apiCookies;
}
private <C> C[] convert(Class<C> apiClass, Function<HttpCookie, C> convertor)
{
@SuppressWarnings("unchecked")
C[] apiCookies = (C[])Array.newInstance(apiClass, _httpCookies.size());
int i = 0;
for (HttpCookie httpCookie : _httpCookies)
{
C apiCookie = convertor.apply(httpCookie);
// Exclude any API cookies that are not convertable to that API
if (apiCookie == null)
apiCookies = Arrays.copyOf(apiCookies, apiCookies.length - 1);
else
apiCookies[i++] = apiCookie;
}
return apiCookies;
}
}

View File

@ -98,6 +98,8 @@ public class FormFields extends ContentSourceCompletableFuture<Fields>
Object attr = request.getAttribute(FormFields.class.getName());
if (attr instanceof FormFields futureFormFields)
return futureFormFields;
else if (attr instanceof Fields fields)
return CompletableFuture.completedFuture(fields);
return EMPTY;
}
@ -221,7 +223,7 @@ public class FormFields extends ContentSourceCompletableFuture<Fields>
_maxFields = maxFields;
_maxLength = maxSize;
_builder = CharsetStringBuilder.forCharset(charset);
_fields = new Fields();
_fields = new Fields(true);
}
@Override

View File

@ -34,7 +34,6 @@ import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import org.eclipse.jetty.http.CookieCache;
import org.eclipse.jetty.http.HttpCookie;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpHeader;
@ -130,7 +129,6 @@ public interface Request extends Attributes, Content.Source
{
Logger LOG = LoggerFactory.getLogger(Request.class);
String CACHE_ATTRIBUTE = Request.class.getCanonicalName() + ".CookieCache";
String COOKIE_ATTRIBUTE = Request.class.getCanonicalName() + ".Cookies";
List<Locale> DEFAULT_LOCALES = List.of(Locale.getDefault());
@ -580,22 +578,7 @@ public interface Request extends Attributes, Content.Source
@SuppressWarnings("unchecked")
static List<HttpCookie> getCookies(Request request)
{
// TODO modify Request and HttpChannel to be optimised for the known attributes
List<HttpCookie> cookies = (List<HttpCookie>)request.getAttribute(COOKIE_ATTRIBUTE);
if (cookies != null)
return cookies;
// TODO: review whether to store the cookie cache at the connection level, or whether to cache them at all.
CookieCache cookieCache = (CookieCache)request.getComponents().getCache().getAttribute(CACHE_ATTRIBUTE);
if (cookieCache == null)
{
cookieCache = new CookieCache(request.getConnectionMetaData().getHttpConfiguration().getRequestCookieCompliance());
request.getComponents().getCache().setAttribute(CACHE_ATTRIBUTE, cookieCache);
}
cookies = cookieCache.getCookies(request.getHeaders(), HttpChannel.from(request).getComplianceViolationListener());
request.setAttribute(COOKIE_ATTRIBUTE, cookies);
return cookies;
return CookieCache.getCookies(request);
}
/**

View File

@ -72,7 +72,7 @@ public class ResourceListing
// check for query
if (query != null)
{
Fields params = new Fields();
Fields params = new Fields(true);
UrlEncoded.decodeUtf8To(query, 0, query.length(), params);
String paramO = params.getValue("O");

View File

@ -23,6 +23,7 @@ import java.util.Arrays;
import java.util.Collections;
import java.util.EventListener;
import java.util.List;
import java.util.ListIterator;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
@ -74,6 +75,12 @@ public class ContextHandler extends Handler.Wrapper implements Attributes, Alias
public static final String MANAGED_ATTRIBUTES = "org.eclipse.jetty.server.context.ManagedAttributes";
/**
* The attribute name that is set as a {@link Request} attribute to indicate the request is a cross context
* dispatch. The value can be set to a ServletDispatcher type if the target is known to be a servlet context.
*/
public static final String CROSS_CONTEXT_ATTRIBUTE = "org.eclipse.jetty.CrossContextDispatch";
/**
* Get the current Context if any.
*
@ -136,6 +143,7 @@ public class ContextHandler extends Handler.Wrapper implements Attributes, Alias
private File _tempDirectory;
private boolean _tempDirectoryPersisted = false;
private boolean _tempDirectoryCreated = false;
private boolean _crossContextDispatchSupported = false;
public enum Availability
{
@ -469,6 +477,83 @@ public class ContextHandler extends Handler.Wrapper implements Attributes, Alias
return _contextPath;
}
/**
* Get if this context support cross context dispatch, either as originator or target.
* @return True if this context supports cross context dispatch.
*/
@ManagedAttribute(value = "Cross context dispatch is support by the context")
public boolean isCrossContextDispatchSupported()
{
return _crossContextDispatchSupported;
}
/**
* Set if this context support cross context dispatch, either as originator or target.
* @param crossContextDispatchSupported True if this context supports cross context dispatch.
*/
public void setCrossContextDispatchSupported(boolean crossContextDispatchSupported)
{
_crossContextDispatchSupported = crossContextDispatchSupported;
}
/**
* If {@link #isCrossContextDispatchSupported() cross context dispatch is supported} by this context
* then find a context by {@link #getContextPath() contextPath} that also supports cross context dispatch.
* If more than one context is found, then those with disjoint {@link #getVirtualHosts() virtual hosts} are
* excluded and the first remaining context returned.
*
* @param path The path that will be served by the context
* @return The found {@link ContextHandler} or null.
*/
public ContextHandler getCrossContextHandler(String path)
{
if (!isCrossContextDispatchSupported())
return null;
List<ContextHandler> contexts = new ArrayList<>();
for (ContextHandler contextHandler : getServer().getDescendants(ContextHandler.class))
{
if (contextHandler == null || !contextHandler.isCrossContextDispatchSupported())
continue;
String contextPath = contextHandler.getContextPath();
if (path.equals(contextPath) ||
(path.startsWith(contextPath) && path.charAt(contextPath.length()) == '/') ||
"/".equals(contextPath))
contexts.add(contextHandler);
}
if (contexts.isEmpty())
return null;
if (contexts.size() == 1)
return contexts.get(0);
// Remove non-matching virtual hosts
List<String> vhosts = getVirtualHosts();
if (vhosts != null && !vhosts.isEmpty())
{
for (ListIterator<ContextHandler> i = contexts.listIterator(); i.hasNext(); )
{
ContextHandler ch = i.next();
List<String> targetVhosts = ch.getVirtualHosts();
if (targetVhosts == null || targetVhosts.isEmpty() || Collections.disjoint(vhosts, targetVhosts))
i.remove();
}
}
if (contexts.isEmpty())
return null;
// return the first longest
ContextHandler contextHandler = null;
for (ContextHandler c : contexts)
{
if (contextHandler == null || c.getContextPath().length() > contextHandler.getContextPath().length())
contextHandler = c;
}
return contextHandler;
}
/**
* Add a context event listeners.
*
@ -1252,6 +1337,18 @@ public class ContextHandler extends Handler.Wrapper implements Attributes, Alias
{
return _rootContext ? canonicallyEncodedPath : Context.getPathInContext(_contextPath, canonicallyEncodedPath);
}
@Override
public boolean isCrossContextDispatch(Request request)
{
return isCrossContextDispatchSupported() && request.getAttribute(CROSS_CONTEXT_ATTRIBUTE) != null;
}
@Override
public String getCrossContextDispatchType(Request request)
{
return isCrossContextDispatchSupported() ? (String)request.getAttribute(CROSS_CONTEXT_ATTRIBUTE) : null;
}
}
/**

View File

@ -13,10 +13,10 @@
package org.eclipse.jetty.server;
import java.util.Arrays;
import java.util.List;
import java.util.ListIterator;
import org.eclipse.jetty.http.CookieCache;
import org.eclipse.jetty.http.HttpCookie;
import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpFields;
@ -25,6 +25,7 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.is;
@ -175,4 +176,25 @@ public class CookieCacheTest
assertThat(cookie.getValue(), is("different"));
}
@Test
public void testApiCookie()
{
_fields.put("Some", "Header");
_fields.put("Cookie", "name=value");
_fields.add("Cookie", "other=cookie");
_fields.put("Other", "header");
List<HttpCookie> cookies0 = _cache.getCookies(_fields.asImmutable());
assertThat(cookies0, hasSize(2));
HttpCookie cookie = cookies0.get(0);
assertThat(cookie.getName(), is("name"));
assertThat(cookie.getValue(), is("value"));
cookie = cookies0.get(1);
assertThat(cookie.getName(), is("other"));
assertThat(cookie.getValue(), is("cookie"));
String[] strings = _cache.getApiCookies(String.class, c -> c.getName() + "=" + c.getValue());
assertThat(Arrays.asList(strings), contains("name=value", "other=cookie"));
assertThat(_cache.getApiCookies(String.class, null), sameInstance(strings));
}
}

View File

@ -61,7 +61,6 @@ public interface Attributes
* Get the immutable set of attribute names.
* @return Set of attribute names
*/
// TODO: change to getAttributeNames() once jetty-core is cleaned of servlet-api usages
Set<String> getAttributeNameSet();
default Map<String, Object> asAttributeMap()
@ -566,14 +565,14 @@ public interface Attributes
* but is instead calculated as needed. Modifications to synthetic attributes are maintained
* in a separate layer and no modifications are made to the backing {@link Attributes}.
* <p>
* Non synthetic attributes are handled normally by the backing {@link Attributes}
* Non-synthetic attributes are handled normally by the backing {@link Attributes}
* <p>
* Uses of this class must provide implementations for
* {@link #getSyntheticNameSet()} amd {@link #getSyntheticAttribute(String)}.
*/
abstract class Synthetic extends Wrapper
{
private static final Object REMOVED = new Object()
protected static final Object REMOVED = new Object()
{
@Override
public String toString()
@ -622,6 +621,8 @@ public interface Attributes
// Is there a synthetic value for the attribute? We just as for the value rather than checking the name.
Object s = getSyntheticAttribute(name);
if (s == REMOVED)
return null;
if (s != null)
return s;
@ -696,9 +697,15 @@ public interface Attributes
if (l == REMOVED)
// it has been removed
names.remove(s);
else if (l != null || getSyntheticAttribute(s) != null)
// else it was modified or has an original value
else if (l != null)
// else it was modified
names.add(s);
else
{
Object v = getSyntheticAttribute(s);
if (v != null && v != REMOVED)
names.add(s);
}
}
}

View File

@ -17,7 +17,6 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@ -28,7 +27,7 @@ import java.util.stream.Stream;
/**
* <p>A container for name/value pairs, known as fields.</p>
* <p>A {@link Field} is composed of a name string that can be case-sensitive
* <p>A {@link Field} is immutable and is composed of a name string that can be case-sensitive
* or case-insensitive (by specifying the option at the constructor) and
* of a case-sensitive set of value strings.</p>
* <p>The implementation of this class is not thread safe.</p>
@ -57,14 +56,41 @@ public class Fields implements Iterable<Fields.Field>
this(caseSensitive ? new LinkedHashMap<>() : new TreeMap<>(String::compareToIgnoreCase));
}
public Fields(MultiMap<String> params)
{
this(multiMapToMapOfFields(params));
}
public Fields(Map<String, Field> fields)
{
this.fields = fields;
}
public Fields(Fields fields)
{
if (fields.fields instanceof TreeMap<String, Field>)
{
this.fields = new TreeMap<>(String::compareToIgnoreCase);
this.fields.putAll(fields.fields);
}
else if (fields.fields instanceof LinkedHashMap<String, Field>)
{
this.fields = new LinkedHashMap<>(fields.fields);
}
else if (Collections.unmodifiableMap(fields.fields) == fields.fields)
{
this.fields = fields.fields;
}
else
{
throw new IllegalStateException("unknown case sensitivity");
}
}
public Fields asImmutable()
{
return new Fields(Collections.unmodifiableMap(fields));
Map<String, Field> unmodifiable = Collections.unmodifiableMap(fields);
return unmodifiable == fields ? this : new Fields(unmodifiable);
}
@Override
@ -103,12 +129,7 @@ public class Fields implements Iterable<Fields.Field>
*/
public Set<String> getNames()
{
Set<String> result = new LinkedHashSet<>();
for (Field field : fields.values())
{
result.add(field.getName());
}
return result;
return fields.keySet();
}
public Stream<Field> stream()
@ -197,8 +218,7 @@ public class Fields implements Iterable<Fields.Field>
*/
public void add(String name, String value)
{
String key = name;
fields.compute(key, (k, f) ->
fields.compute(name, (k, f) ->
{
if (f == null)
// Preserve the case for the field name
@ -208,6 +228,31 @@ public class Fields implements Iterable<Fields.Field>
});
}
/**
* <p>Adds the given value to a field with the given name,
* creating a {@link Field} is none exists for the given name.</p>
*
* @param name the field name
* @param values the field values to add
*/
public void add(String name, String... values)
{
if (values == null || values.length == 0)
return;
if (values.length == 1)
add(name, values[0]);
else
{
fields.compute(name, (k, f) ->
{
if (f == null)
return new Field(name, List.of(values));
else
return new Field(f.getName(), f.getValues(), List.of(values));
});
}
}
/**
* <p>Adds the given field, storing it if none exists for the given name,
* or adding all the values to the existing field with the given name.</p>
@ -216,8 +261,7 @@ public class Fields implements Iterable<Fields.Field>
*/
public void add(Field field)
{
String s = field.getName();
String key = s;
String key = field.getName();
fields.compute(key, (k, f) ->
{
if (f == null)
@ -290,6 +334,16 @@ public class Fields implements Iterable<Fields.Field>
return result;
}
/**
* @return the fields (name and values) of this instance copied into a {@code MultiMap<String>}
*/
public MultiMap<String> toMultiMap()
{
MultiMap<String> multiMap = new MultiMap<>();
fields.forEach((k, f) -> multiMap.addValues(k, f.getValues()));
return multiMap;
}
@Override
public String toString()
{
@ -312,18 +366,75 @@ public class Fields implements Iterable<Fields.Field>
this(name, List.of(value));
}
private Field(String name, List<String> values, String... moreValues)
public Field(String name, List<String> values)
{
this(name, values, List.of(moreValues));
this.name = name;
this.values = List.copyOf(values);
}
private Field(String name, List<String> values, String extraValue)
{
this(name, append(values, extraValue));
}
private Field(String name, List<String> values, List<String> moreValues)
{
this.name = name;
List<String> list = new ArrayList<>(values.size() + moreValues.size());
list.addAll(values);
list.addAll(moreValues);
this.values = List.copyOf(list);
this(name, append(values, moreValues));
}
private static List<String> append(List<String> values, String extraValue)
{
return switch (values.size())
{
case 0 -> List.of(extraValue);
case 1 -> List.of(values.get(0), extraValue);
case 2 -> List.of(values.get(0), values.get(1), extraValue);
case 3 -> List.of(values.get(0), values.get(1), values.get(2), extraValue);
case 4 -> List.of(values.get(0), values.get(1), values.get(2), values.get(3), extraValue);
case 5 -> List.of(values.get(0), values.get(1), values.get(2), values.get(3), values.get(4), extraValue);
default ->
{
List<String> list = new ArrayList<>(values.size() + 1);
list.addAll(values);
list.add(extraValue);
yield list;
}
};
}
private static List<String> append(List<String> values, List<String> moreValues)
{
if (moreValues == null || moreValues.isEmpty())
return values;
if (moreValues.size() == 1)
return append(values, moreValues.get(0));
return switch (values.size())
{
case 0 -> moreValues;
case 1 -> switch (moreValues.size())
{
case 2 -> List.of(values.get(0), moreValues.get(0), moreValues.get(1));
case 3 -> List.of(values.get(0), moreValues.get(0), moreValues.get(1), moreValues.get(2));
case 4 -> List.of(values.get(0), moreValues.get(0), moreValues.get(1), moreValues.get(2), moreValues.get(3));
case 5 -> List.of(values.get(0), moreValues.get(0), moreValues.get(1), moreValues.get(2), moreValues.get(3), moreValues.get(4));
default ->
{
List<String> list = new ArrayList<>(moreValues.size() + 1);
list.add(values.get(0));
list.addAll(moreValues);
yield list;
}
};
default ->
{
List<String> list = new ArrayList<>(values.size() + moreValues.size());
list.addAll(values);
list.addAll(moreValues);
yield list;
}
};
}
@Override
@ -399,7 +510,7 @@ public class Fields implements Iterable<Fields.Field>
/**
* <p>Combine two Fields</p>
* @param a The base Fields or null
* @param b The overlayed Fields or null
* @param b The overlay Fields or null
* @return Fields, which may be empty, but never null.
*/
public static Fields combine(Fields a, Fields b)
@ -410,10 +521,20 @@ public class Fields implements Iterable<Fields.Field>
if (a == null || a.isEmpty())
return b;
Fields fields = new Fields();
Fields fields = new Fields(a.fields instanceof LinkedHashMap<String, Field>);
fields.addAll(a);
fields.addAll(b);
return fields;
}
private static Map<String, Field> multiMapToMapOfFields(MultiMap<String> params)
{
if (params.isEmpty())
return Collections.emptyMap();
Map<String, Field> fields = new LinkedHashMap<>();
for (Map.Entry<String, List<String>> entry : params.entrySet())
fields.put(entry.getKey(), new Field(entry.getKey(), entry.getValue()));
return fields;
}
}

View File

@ -22,6 +22,7 @@ import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BiConsumer;
import org.slf4j.Logger;
@ -191,6 +192,23 @@ public class UrlEncoded
}, charset);
}
/**
* Decoded parameters to Map.
*
* @param content the string containing the encoded parameters
* @param adder Function to add parameter
* @param charset the charset to use for decoding
*/
public static void decodeTo(String content, BiConsumer<String, String> adder, Charset charset, int maxKeys)
{
AtomicInteger keys = new AtomicInteger(0);
decodeTo(content, (key, val) ->
{
adder.accept(key, val);
checkMaxKeys(keys.incrementAndGet(), maxKeys);
}, charset);
}
/**
* Decoded parameters to Map.
*
@ -395,6 +413,21 @@ public class UrlEncoded
*/
public static void decode88591To(InputStream in, MultiMap<String> map, int maxLength, int maxKeys)
throws IOException
{
decode88591To(in, map::add, maxLength, maxKeys);
}
/**
* Decoded parameters to MultiMap, using ISO8859-1 encodings.
*
* @param in InputSteam to read
* @param adder Function to add parameter
* @param maxLength maximum length of form to read or -1 for no limit
* @param maxKeys maximum number of keys to read or -1 for no limit
* @throws IOException if unable to decode the InputStream as ISO8859-1
*/
public static void decode88591To(InputStream in, BiConsumer<String, String> adder, int maxLength, int maxKeys)
throws IOException
{
StringBuilder buffer = new StringBuilder();
String key = null;
@ -403,6 +436,7 @@ public class UrlEncoded
int b;
int totalLength = 0;
int keys = 0;
while ((b = in.read()) >= 0)
{
switch ((char)b)
@ -412,14 +446,16 @@ public class UrlEncoded
buffer.setLength(0);
if (key != null)
{
map.add(key, value);
adder.accept(key, value);
keys++;
}
else if (value.length() > 0)
{
map.add(value, "");
adder.accept(value, "");
keys++;
}
key = null;
checkMaxKeys(map, maxKeys);
checkMaxKeys(keys, maxKeys);
break;
case '=':
@ -453,13 +489,15 @@ public class UrlEncoded
{
value = buffer.length() == 0 ? "" : buffer.toString();
buffer.setLength(0);
map.add(key, value);
adder.accept(key, value);
keys++;
}
else if (buffer.length() > 0)
{
map.add(buffer.toString(), "");
adder.accept(buffer.toString(), "");
keys++;
}
checkMaxKeys(map, maxKeys);
checkMaxKeys(keys, maxKeys);
}
/**
@ -473,6 +511,21 @@ public class UrlEncoded
*/
public static void decodeUtf8To(InputStream in, MultiMap<String> map, int maxLength, int maxKeys)
throws IOException
{
decodeUtf8To(in, map::add, maxLength, maxKeys);
}
/**
* Decoded parameters to Map.
*
* @param in InputSteam to read
* @param adder Function to add parameters to
* @param maxLength maximum form length to decode or -1 for no limit
* @param maxKeys the maximum number of keys to read or -1 for no limit
* @throws IOException if unable to decode the input stream
*/
public static void decodeUtf8To(InputStream in, BiConsumer<String, String> adder, int maxLength, int maxKeys)
throws IOException
{
Utf8StringBuilder buffer = new Utf8StringBuilder();
String key = null;
@ -481,6 +534,7 @@ public class UrlEncoded
int b;
int totalLength = 0;
int keys = 0;
while ((b = in.read()) >= 0)
{
switch ((char)b)
@ -490,14 +544,16 @@ public class UrlEncoded
buffer.reset();
if (key != null)
{
map.add(key, value);
adder.accept(key, value);
keys++;
}
else if (value != null && value.length() > 0)
{
map.add(value, "");
adder.accept(value, "");
keys++;
}
key = null;
checkMaxKeys(map, maxKeys);
checkMaxKeys(keys, maxKeys);
break;
case '=':
@ -531,22 +587,29 @@ public class UrlEncoded
{
value = buffer.toCompleteString();
buffer.reset();
map.add(key, value);
adder.accept(key, value);
keys++;
}
else if (buffer.length() > 0)
{
map.add(buffer.toCompleteString(), "");
adder.accept(buffer.toCompleteString(), "");
keys++;
}
checkMaxKeys(map, maxKeys);
checkMaxKeys(keys, maxKeys);
}
public static void decodeUtf16To(InputStream in, MultiMap<String> map, int maxLength, int maxKeys) throws IOException
{
decodeUtf16To(in, map::add, maxLength, maxKeys);
}
public static void decodeUtf16To(InputStream in, BiConsumer<String, String> adder, int maxLength, int maxKeys) throws IOException
{
InputStreamReader input = new InputStreamReader(in, StandardCharsets.UTF_16);
StringWriter buf = new StringWriter(8192);
IO.copy(input, buf, maxLength);
decodeTo(buf.getBuffer().toString(), map, StandardCharsets.UTF_16, maxKeys);
decodeTo(buf.getBuffer().toString(), adder, StandardCharsets.UTF_16, maxKeys);
}
/**
@ -572,6 +635,22 @@ public class UrlEncoded
*/
public static void decodeTo(InputStream in, MultiMap<String> map, Charset charset, int maxLength, int maxKeys)
throws IOException
{
decodeTo(in, map::add, charset, maxLength, maxKeys);
}
/**
* Decoded parameters to Map.
*
* @param in the stream containing the encoded parameters
* @param adder Function to add a parameter
* @param charset the charset to use for decoding
* @param maxLength the maximum length of the form to decode
* @param maxKeys the maximum number of keys to decode
* @throws IOException if unable to decode input stream
*/
public static void decodeTo(InputStream in, BiConsumer<String, String> adder, Charset charset, int maxLength, int maxKeys)
throws IOException
{
//no charset present, use the configured default
if (charset == null)
@ -579,19 +658,19 @@ public class UrlEncoded
if (StandardCharsets.UTF_8.equals(charset))
{
decodeUtf8To(in, map, maxLength, maxKeys);
decodeUtf8To(in, adder, maxLength, maxKeys);
return;
}
if (StandardCharsets.ISO_8859_1.equals(charset))
{
decode88591To(in, map, maxLength, maxKeys);
decode88591To(in, adder, maxLength, maxKeys);
return;
}
if (StandardCharsets.UTF_16.equals(charset)) // Should be all 2 byte encodings
{
decodeUtf16To(in, map, maxLength, maxKeys);
decodeUtf16To(in, adder, maxLength, maxKeys);
return;
}
@ -604,6 +683,7 @@ public class UrlEncoded
try (ByteArrayOutputStream2 output = new ByteArrayOutputStream2())
{
int keys = 0;
int size;
while ((c = in.read()) > 0)
@ -616,14 +696,16 @@ public class UrlEncoded
output.setCount(0);
if (key != null)
{
map.add(key, value);
adder.accept(key, value);
keys++;
}
else if (value != null && value.length() > 0)
{
map.add(value, "");
adder.accept(value, "");
keys++;
}
key = null;
checkMaxKeys(map, maxKeys);
checkMaxKeys(keys, maxKeys);
break;
case '=':
if (key != null)
@ -655,16 +737,24 @@ public class UrlEncoded
{
value = size == 0 ? "" : output.toString(charset);
output.setCount(0);
map.add(key, value);
adder.accept(key, value);
keys++;
}
else if (size > 0)
{
map.add(output.toString(charset), "");
adder.accept(output.toString(charset), "");
keys++;
}
checkMaxKeys(map, maxKeys);
checkMaxKeys(keys, maxKeys);
}
}
private static void checkMaxKeys(int size, int maxKeys)
{
if (maxKeys >= 0 && size > maxKeys)
throw new IllegalStateException(String.format("Form with too many keys [%d > %d]", size, maxKeys));
}
private static void checkMaxKeys(MultiMap<String> map, int maxKeys)
{
int size = map.size();

View File

@ -54,7 +54,7 @@ public class Negotiated
else
{
map = new HashMap<>();
Fields fields = new Fields();
Fields fields = new Fields(true);
UrlEncoded.decodeUtf8To(rawQuery, fields);
for (Fields.Field field : fields)
{

View File

@ -0,0 +1,274 @@
//
// ========================================================================
// 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.ee10.servlet;
import java.io.Closeable;
import java.io.IOException;
import java.util.Set;
import jakarta.servlet.DispatcherType;
import jakarta.servlet.RequestDispatcher;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.eclipse.jetty.http.HttpURI;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.handler.ContextHandler;
import org.eclipse.jetty.util.Attributes;
import org.eclipse.jetty.util.Blocker;
import org.eclipse.jetty.util.IO;
class CrossContextDispatcher implements RequestDispatcher
{
public static final String ORIGINAL_URI = "org.eclipse.jetty.dispatch.originalURI";
public static final String ORIGINAL_QUERY_STRING = "org.eclipse.jetty.dispatch.originalQueryString";
public static final String ORIGINAL_SERVLET_MAPPING = "org.eclipse.jetty.dispatch.originalServletMapping";
public static final String ORIGINAL_CONTEXT_PATH = "org.eclipse.jetty.dispatch.originalContextPath";
public static final Set<String> ATTRIBUTES = Set.of(
RequestDispatcher.FORWARD_REQUEST_URI,
RequestDispatcher.FORWARD_MAPPING,
RequestDispatcher.FORWARD_CONTEXT_PATH,
RequestDispatcher.FORWARD_SERVLET_PATH,
RequestDispatcher.FORWARD_QUERY_STRING,
RequestDispatcher.FORWARD_PATH_INFO,
RequestDispatcher.INCLUDE_REQUEST_URI,
RequestDispatcher.INCLUDE_MAPPING,
RequestDispatcher.INCLUDE_CONTEXT_PATH,
RequestDispatcher.INCLUDE_SERVLET_PATH,
RequestDispatcher.INCLUDE_QUERY_STRING,
RequestDispatcher.INCLUDE_PATH_INFO,
"javax.servlet.include.request_uri",
"javax.servlet.include.mapping",
"javax.servlet.include.context_path",
"javax.servlet.include.servlet_path",
"javax.servlet.include.query_string",
"javax.servlet.include.path_info",
ServletContextRequest.MULTIPART_CONFIG_ELEMENT,
ContextHandler.CROSS_CONTEXT_ATTRIBUTE,
ORIGINAL_URI,
ORIGINAL_QUERY_STRING,
ORIGINAL_SERVLET_MAPPING,
ORIGINAL_CONTEXT_PATH
);
private final CrossContextServletContext _targetContext;
private final HttpURI _uri;
private final String _decodedPathInContext;
private class IncludeRequest extends ServletCoreRequest
{
public IncludeRequest(HttpServletRequest httpServletRequest)
{
super(httpServletRequest, new Attributes.Synthetic(new ServletAttributes(httpServletRequest))
{
@Override
public Object getAttribute(String name)
{
//handle cross-environment dispatch from ee8
if (name.startsWith("javax.servlet."))
name = "jakarta.servlet." + name.substring(14);
return super.getAttribute(name);
}
@Override
protected Object getSyntheticAttribute(String name)
{
if (name == null)
return null;
//Servlet Spec 9.3.1 no include attributes if a named dispatcher
/* if (_namedServlet != null && name.startsWith(Dispatcher.__INCLUDE_PREFIX))
return null;*/
//Special include attributes refer to the target context and path
return switch (name)
{
case RequestDispatcher.INCLUDE_MAPPING -> null;
case RequestDispatcher.INCLUDE_SERVLET_PATH -> null;
case RequestDispatcher.INCLUDE_PATH_INFO -> _decodedPathInContext;
case RequestDispatcher.INCLUDE_REQUEST_URI -> (_uri == null) ? null : _uri.getPath();
case RequestDispatcher.INCLUDE_CONTEXT_PATH -> _targetContext.getContextPath();
case RequestDispatcher.INCLUDE_QUERY_STRING -> (_uri == null) ? null : _uri.getQuery();
case ContextHandler.CROSS_CONTEXT_ATTRIBUTE -> DispatcherType.INCLUDE.toString();
case ORIGINAL_URI -> httpServletRequest.getRequestURI();
case ORIGINAL_QUERY_STRING -> httpServletRequest.getQueryString();
case ORIGINAL_SERVLET_MAPPING -> httpServletRequest.getHttpServletMapping();
case ORIGINAL_CONTEXT_PATH -> httpServletRequest.getContextPath();
default -> httpServletRequest.getAttribute(name);
};
}
@Override
protected Set<String> getSyntheticNameSet()
{
return ATTRIBUTES;
}
@Override
public Object setAttribute(String name, Object attribute)
{
if (name == null)
return null;
//handle cross-environment dispatch from ee8
if (name.startsWith("javax.servlet."))
name = "jakarta.servlet." + name.substring(14);
return super.setAttribute(name, attribute);
}
});
}
@Override
public HttpURI getHttpURI()
{
//return the uri of the dispatch target
return _uri;
}
}
private class IncludeResponse extends ServletCoreResponse
{
public IncludeResponse(Request coreRequest, HttpServletResponse httpServletResponse)
{
super(coreRequest, httpServletResponse, true);
}
}
private class ForwardRequest extends ServletCoreRequest
{
/**
* @param httpServletRequest the request to wrap
*/
public ForwardRequest(HttpServletRequest httpServletRequest)
{
super(httpServletRequest, new Attributes.Synthetic(new ServletAttributes(httpServletRequest))
{
@Override
protected Object getSyntheticAttribute(String name)
{
if (name == null)
return null;
//handle cross-environment dispatch from ee8
if (name.startsWith("javax.servlet."))
name = "jakarta.servlet." + name.substring(14);
return switch (name)
{
case RequestDispatcher.FORWARD_REQUEST_URI -> httpServletRequest.getRequestURI();
case RequestDispatcher.FORWARD_SERVLET_PATH -> httpServletRequest.getServletPath();
case RequestDispatcher.FORWARD_PATH_INFO -> httpServletRequest.getPathInfo();
case RequestDispatcher.FORWARD_CONTEXT_PATH -> httpServletRequest.getContextPath();
case RequestDispatcher.FORWARD_MAPPING -> httpServletRequest.getHttpServletMapping();
case RequestDispatcher.FORWARD_QUERY_STRING -> httpServletRequest.getQueryString();
case RequestDispatcher.INCLUDE_MAPPING -> REMOVED;
case RequestDispatcher.INCLUDE_REQUEST_URI -> REMOVED;
case RequestDispatcher.INCLUDE_CONTEXT_PATH -> REMOVED;
case RequestDispatcher.INCLUDE_QUERY_STRING -> REMOVED;
case RequestDispatcher.INCLUDE_SERVLET_PATH -> REMOVED;
case RequestDispatcher.INCLUDE_PATH_INFO -> REMOVED;
//TODO
//case ServletContextRequest.MULTIPART_CONFIG_ELEMENT -> httpServletRequest.getAttribute(ServletMultiPartFormData.class.getName());
case ContextHandler.CROSS_CONTEXT_ATTRIBUTE -> DispatcherType.FORWARD.toString();
default -> null;
};
}
@Override
protected Set<String> getSyntheticNameSet()
{
return ATTRIBUTES;
}
});
}
@Override
public HttpURI getHttpURI()
{
return _uri;
}
}
CrossContextDispatcher(CrossContextServletContext targetContext, HttpURI uri, String decodedPathInContext)
{
_targetContext = targetContext;
_uri = uri;
_decodedPathInContext = decodedPathInContext;
}
@Override
public void forward(ServletRequest servletRequest, ServletResponse response) throws ServletException, IOException
{
HttpServletRequest httpServletRequest = (servletRequest instanceof HttpServletRequest) ? ((HttpServletRequest)servletRequest) : new ServletRequestHttpWrapper(servletRequest);
HttpServletResponse httpResponse = (response instanceof HttpServletResponse) ? (HttpServletResponse)response : new ServletResponseHttpWrapper(response);
ServletContextRequest servletContextRequest = ServletContextRequest.getServletContextRequest(servletRequest);
servletContextRequest.getServletContextResponse().resetForForward();
ForwardRequest forwardRequest = new ForwardRequest(httpServletRequest);
try (Blocker.Callback callback = Blocker.callback())
{
_targetContext.getTargetContext().getContextHandler().handle(forwardRequest, new ServletCoreResponse(forwardRequest, httpResponse, false), callback);
callback.block();
}
catch (Exception e)
{
throw new ServletException(e);
}
// If we are not async and not closed already, then close via the possibly wrapped response.
if (!servletContextRequest.getState().isAsync() && !servletContextRequest.getServletContextResponse().hasLastWrite())
{
Closeable closeable;
try
{
closeable = response.getOutputStream();
}
catch (IllegalStateException e)
{
closeable = response.getWriter();
}
IO.close(closeable);
}
}
@Override
public void include(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException
{
HttpServletRequest httpServletRequest = (servletRequest instanceof HttpServletRequest) ? ((HttpServletRequest)servletRequest) : new ServletRequestHttpWrapper(servletRequest);
HttpServletResponse httpServletResponse = (servletResponse instanceof HttpServletResponse) ? (HttpServletResponse)servletResponse : new ServletResponseHttpWrapper(servletResponse);
ServletContextResponse servletContextResponse = ServletContextResponse.getServletContextResponse(servletResponse);
IncludeRequest includeRequest = new IncludeRequest(httpServletRequest);
IncludeResponse includeResponse = new IncludeResponse(includeRequest, httpServletResponse);
try (Blocker.Callback callback = Blocker.callback())
{
_targetContext.getTargetContext().getContextHandler().handle(includeRequest, includeResponse, callback);
callback.block();
}
catch (Exception e)
{
throw new ServletException(e);
}
}
}

View File

@ -0,0 +1,426 @@
//
// ========================================================================
// 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.ee10.servlet;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.file.Path;
import java.util.Collections;
import java.util.Enumeration;
import java.util.EventListener;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import jakarta.servlet.Filter;
import jakarta.servlet.FilterRegistration;
import jakarta.servlet.RequestDispatcher;
import jakarta.servlet.Servlet;
import jakarta.servlet.ServletContext;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRegistration;
import jakarta.servlet.SessionCookieConfig;
import jakarta.servlet.SessionTrackingMode;
import jakarta.servlet.descriptor.JspConfigDescriptor;
import org.eclipse.jetty.http.HttpURI;
import org.eclipse.jetty.server.handler.ContextHandler;
import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.util.URIUtil;
import org.eclipse.jetty.util.resource.Resource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A {@link ServletContext} used for cross context dispatch, which wraps an arbitrary {@link ContextHandler}
*/
class CrossContextServletContext implements ServletContext
{
private static final Logger LOG = LoggerFactory.getLogger(CrossContextServletContext.class);
private final ServletContextHandler _servletContextHandler;
private final ContextHandler.ScopedContext _targetContext;
protected CrossContextServletContext(ServletContextHandler servletContextHandler, ContextHandler.ScopedContext targetContext)
{
_servletContextHandler = servletContextHandler;
_targetContext = Objects.requireNonNull(targetContext);
}
@Override
public String getContextPath()
{
return _targetContext.getContextPath();
}
@Override
public ServletContext getContext(String uripath)
{
throw new UnsupportedOperationException();
}
@Override
public int getMajorVersion()
{
return _servletContextHandler.getServletContext().getMajorVersion();
}
@Override
public int getMinorVersion()
{
return _servletContextHandler.getServletContext().getMinorVersion();
}
@Override
public int getEffectiveMajorVersion()
{
return _servletContextHandler.getServletContext().getEffectiveMajorVersion();
}
@Override
public int getEffectiveMinorVersion()
{
return _servletContextHandler.getServletContext().getEffectiveMajorVersion();
}
@Override
public String getMimeType(String file)
{
return _targetContext.getMimeTypes().getMimeByExtension(file);
}
@Override
public Set<String> getResourcePaths(String path)
{
Resource resource = _targetContext.getBaseResource().resolve(path);
if (resource != null && resource.isDirectory())
return resource.list().stream().map(Resource::getPath).map(Path::getFileName).map(Path::toString).collect(Collectors.toSet());
return null;
}
@Override
public URL getResource(String path) throws MalformedURLException
{
return _targetContext.getBaseResource().resolve(path).getURI().toURL();
}
@Override
public InputStream getResourceAsStream(String path)
{
throw new UnsupportedOperationException();
}
@Override
public RequestDispatcher getRequestDispatcher(String uriInContext)
{
// uriInContext is encoded, potentially with query.
if (uriInContext == null)
return null;
if (!uriInContext.startsWith("/"))
return null;
try
{
String contextPath = getContextPath();
// uriInContext is canonical by HttpURI.
HttpURI.Mutable uri = HttpURI.build(uriInContext);
String encodedPathInContext = uri.getCanonicalPath();
if (StringUtil.isEmpty(encodedPathInContext))
return null;
if (!StringUtil.isEmpty(contextPath))
{
uri.path(URIUtil.addPaths(contextPath, uri.getPath()));
encodedPathInContext = uri.getCanonicalPath().substring(contextPath.length());
}
return new CrossContextDispatcher(this, uri, URIUtil.decodePath(encodedPathInContext));
}
catch (Exception e)
{
LOG.trace("IGNORED", e);
}
return null;
}
@Override
public RequestDispatcher getNamedDispatcher(String name)
{
throw new UnsupportedOperationException();
}
@Override
public void log(String msg)
{
LOG.info(msg);
}
@Override
public void log(String message, Throwable throwable)
{
LOG.warn(message, throwable);
}
@Override
public String getRealPath(String path)
{
throw new UnsupportedOperationException();
}
@Override
public String getServerInfo()
{
return _servletContextHandler.getServer().getServerInfo();
}
@Override
public String getInitParameter(String name)
{
throw new UnsupportedOperationException();
}
@Override
public Enumeration<String> getInitParameterNames()
{
throw new UnsupportedOperationException();
}
@Override
public boolean setInitParameter(String name, String value)
{
throw new UnsupportedOperationException();
}
@Override
public Object getAttribute(String name)
{
return _targetContext.getAttribute(name);
}
@Override
public Enumeration<String> getAttributeNames()
{
return Collections.enumeration(_targetContext.getAttributeNameSet());
}
@Override
public void setAttribute(String name, Object object)
{
_targetContext.setAttribute(name, object);
}
@Override
public void removeAttribute(String name)
{
_targetContext.removeAttribute(name);
}
@Override
public String getServletContextName()
{
return _targetContext.getContextHandler().getDisplayName();
}
@Override
public ServletRegistration.Dynamic addServlet(String servletName, String className)
{
throw new UnsupportedOperationException();
}
@Override
public ServletRegistration.Dynamic addServlet(String servletName, Servlet servlet)
{
throw new UnsupportedOperationException();
}
@Override
public ServletRegistration.Dynamic addServlet(String servletName, Class<? extends Servlet> servletClass)
{
throw new UnsupportedOperationException();
}
@Override
public ServletRegistration.Dynamic addJspFile(String servletName, String jspFile)
{
throw new UnsupportedOperationException();
}
@Override
public <T extends Servlet> T createServlet(Class<T> clazz) throws ServletException
{
throw new UnsupportedOperationException();
}
@Override
public ServletRegistration getServletRegistration(String servletName)
{
throw new UnsupportedOperationException();
}
@Override
public Map<String, ? extends ServletRegistration> getServletRegistrations()
{
throw new UnsupportedOperationException();
}
@Override
public FilterRegistration.Dynamic addFilter(String filterName, String className)
{
throw new UnsupportedOperationException();
}
@Override
public FilterRegistration.Dynamic addFilter(String filterName, Filter filter)
{
throw new UnsupportedOperationException();
}
@Override
public FilterRegistration.Dynamic addFilter(String filterName, Class<? extends Filter> filterClass)
{
throw new UnsupportedOperationException();
}
@Override
public <T extends Filter> T createFilter(Class<T> clazz) throws ServletException
{
throw new UnsupportedOperationException();
}
@Override
public FilterRegistration getFilterRegistration(String filterName)
{
throw new UnsupportedOperationException();
}
@Override
public Map<String, ? extends FilterRegistration> getFilterRegistrations()
{
throw new UnsupportedOperationException();
}
@Override
public SessionCookieConfig getSessionCookieConfig()
{
throw new UnsupportedOperationException();
}
@Override
public void setSessionTrackingModes(Set<SessionTrackingMode> sessionTrackingModes)
{
throw new UnsupportedOperationException();
}
@Override
public Set<SessionTrackingMode> getDefaultSessionTrackingModes()
{
throw new UnsupportedOperationException();
}
@Override
public Set<SessionTrackingMode> getEffectiveSessionTrackingModes()
{
throw new UnsupportedOperationException();
}
@Override
public void addListener(String className)
{
throw new UnsupportedOperationException();
}
@Override
public <T extends EventListener> void addListener(T t)
{
throw new UnsupportedOperationException();
}
@Override
public void addListener(Class<? extends EventListener> listenerClass)
{
throw new UnsupportedOperationException();
}
@Override
public <T extends EventListener> T createListener(Class<T> clazz) throws ServletException
{
throw new UnsupportedOperationException();
}
@Override
public JspConfigDescriptor getJspConfigDescriptor()
{
throw new UnsupportedOperationException();
}
@Override
public ClassLoader getClassLoader()
{
return _targetContext.getClassLoader();
}
@Override
public void declareRoles(String... roleNames)
{
throw new UnsupportedOperationException();
}
@Override
public String getVirtualServerName()
{
return null;
}
@Override
public int getSessionTimeout()
{
throw new UnsupportedOperationException();
}
@Override
public void setSessionTimeout(int sessionTimeout)
{
throw new UnsupportedOperationException();
}
@Override
public String getRequestCharacterEncoding()
{
throw new UnsupportedOperationException();
}
@Override
public void setRequestCharacterEncoding(String encoding)
{
throw new UnsupportedOperationException();
}
@Override
public String getResponseCharacterEncoding()
{
throw new UnsupportedOperationException();
}
@Override
public void setResponseCharacterEncoding(String encoding)
{
throw new UnsupportedOperationException();
}
ContextHandler.ScopedContext getTargetContext()
{
return _targetContext;
}
}

View File

@ -487,7 +487,7 @@ public class DefaultServlet extends HttpServlet
// otherwise wrap the servlet request as a core request
Request coreRequest = httpServletRequest instanceof ServletApiRequest
? servletChannel.getRequest()
: new ServletCoreRequest(httpServletRequest);
: ServletCoreRequest.wrap(httpServletRequest);
// If the servlet response has been wrapped and has been written to,
// then the servlet response must be wrapped as a core response

View File

@ -42,8 +42,8 @@ import org.eclipse.jetty.ee10.servlet.util.ServletOutputStreamWrapper;
import org.eclipse.jetty.http.HttpURI;
import org.eclipse.jetty.http.pathmap.MatchedResource;
import org.eclipse.jetty.io.WriterOutputStream;
import org.eclipse.jetty.util.Fields;
import org.eclipse.jetty.util.IO;
import org.eclipse.jetty.util.MultiMap;
import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.util.UrlEncoded;
@ -64,6 +64,8 @@ public class Dispatcher implements RequestDispatcher
*/
public static final String __ORIGINAL_REQUEST = "org.eclipse.jetty.originalRequest";
public static final String JETTY_INCLUDE_HEADER_PREFIX = "org.eclipse.jetty.server.include.";
private final ServletContextHandler _contextHandler;
private final HttpURI _uri;
private final String _decodedPathInContext;
@ -168,31 +170,48 @@ public class Dispatcher implements RequestDispatcher
public class ParameterRequestWrapper extends HttpServletRequestWrapper
{
private MultiMap<String> _params = null;
private Fields _parameters = null;
public ParameterRequestWrapper(HttpServletRequest request)
{
super(request);
}
private MultiMap<String> getParams()
private Fields getParameters()
{
if (_params == null)
if (_parameters == null)
{
_params = new MultiMap<>();
// Have to assume ENCODING because we can't know otherwise.
String targetQuery = (_uri == null) ? null : _uri.getQuery();
if (targetQuery != null)
UrlEncoded.decodeTo(targetQuery, _params, UrlEncoded.ENCODING);
for (Enumeration<String> names = getRequest().getParameterNames(); names.hasMoreElements(); )
if (getRequest() instanceof ServletApiRequest servletApiRequest)
{
String name = names.nextElement();
_params.addValues(name, getRequest().getParameterValues(name));
Fields parameters = servletApiRequest.getParameters();
if (targetQuery == null)
{
_parameters = parameters;
}
else
{
_parameters = new Fields(true);
UrlEncoded.decodeTo(targetQuery, _parameters::add, UrlEncoded.ENCODING);
_parameters.addAll(parameters);
}
}
else
{
_parameters = new Fields(true);
// Have to assume ENCODING because we can't know otherwise.
if (targetQuery != null)
UrlEncoded.decodeTo(targetQuery, _parameters::add, UrlEncoded.ENCODING);
for (Enumeration<String> names = getRequest().getParameterNames(); names.hasMoreElements(); )
{
String name = names.nextElement();
_parameters.add(name, getRequest().getParameterValues(name));
}
}
}
return _params;
return _parameters;
}
@Override
@ -216,25 +235,25 @@ public class Dispatcher implements RequestDispatcher
@Override
public String getParameter(String name)
{
return getParams().getValue(name);
return getParameters().getValue(name);
}
@Override
public Map<String, String[]> getParameterMap()
{
return Collections.unmodifiableMap(getParams().toStringArrayMap());
return Collections.unmodifiableMap(getParameters().toStringArrayMap());
}
@Override
public Enumeration<String> getParameterNames()
{
return Collections.enumeration(getParams().keySet());
return Collections.enumeration(getParameters().getNames());
}
@Override
public String[] getParameterValues(String name)
{
List<String> vals = getParams().getValues(name);
List<String> vals = getParameters().getValues(name);
if (vals == null)
return null;
return vals.toArray(new String[0]);
@ -434,7 +453,6 @@ public class Dispatcher implements RequestDispatcher
private static class IncludeResponse extends HttpServletResponseWrapper
{
public static final String JETTY_INCLUDE_HEADER_PREFIX = "org.eclipse.jetty.server.include.";
ServletOutputStream _servletOutputStream;
PrintWriter _printWriter;
PrintWriter _mustFlush;

View File

@ -23,7 +23,6 @@ import java.nio.charset.IllegalCharsetNameException;
import java.nio.charset.StandardCharsets;
import java.nio.charset.UnsupportedCharsetException;
import java.security.Principal;
import java.util.AbstractList;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
@ -58,7 +57,6 @@ import jakarta.servlet.http.Part;
import jakarta.servlet.http.PushBuilder;
import org.eclipse.jetty.ee10.servlet.ServletContextHandler.ServletRequestInfo;
import org.eclipse.jetty.http.BadMessageException;
import org.eclipse.jetty.http.CookieCache;
import org.eclipse.jetty.http.CookieCompliance;
import org.eclipse.jetty.http.HttpCookie;
import org.eclipse.jetty.http.HttpException;
@ -70,11 +68,13 @@ import org.eclipse.jetty.http.HttpURI;
import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.http.MimeTypes;
import org.eclipse.jetty.http.SetCookieParser;
import org.eclipse.jetty.http.pathmap.MatchedResource;
import org.eclipse.jetty.io.QuietException;
import org.eclipse.jetty.io.RuntimeIOException;
import org.eclipse.jetty.security.AuthenticationState;
import org.eclipse.jetty.security.UserIdentity;
import org.eclipse.jetty.server.ConnectionMetaData;
import org.eclipse.jetty.server.CookieCache;
import org.eclipse.jetty.server.FormFields;
import org.eclipse.jetty.server.HttpCookieUtils;
import org.eclipse.jetty.server.Request;
@ -89,6 +89,7 @@ import org.eclipse.jetty.util.HostPort;
import org.eclipse.jetty.util.IO;
import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.util.URIUtil;
import org.eclipse.jetty.util.UrlEncoded;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -99,6 +100,132 @@ import org.slf4j.LoggerFactory;
*/
public class ServletApiRequest implements HttpServletRequest
{
public static class CrossContextForwarded extends ServletApiRequest
{
protected CrossContextForwarded(ServletContextRequest servletContextRequest)
{
super(servletContextRequest);
}
@Override
protected void extractQueryParameters() throws BadMessageException
{
// Extract query string parameters; these may be replaced by a forward()
// and may have already been extracted by mergeQueryParameters().
if (_queryParameters == null)
{
String forwardQueryString = (String)getAttribute(RequestDispatcher.FORWARD_QUERY_STRING);
String originalQueryString = getQueryString();
if (StringUtil.isBlank(forwardQueryString))
{
if (StringUtil.isBlank(originalQueryString))
{
_queryParameters = ServletContextRequest.NO_PARAMS;
}
else
{
_queryParameters = new Fields(true);
UrlEncoded.decodeTo(forwardQueryString, _queryParameters::add, getServletRequestInfo().getQueryEncoding());
}
}
else
{
_queryParameters = new Fields(true);
if (!StringUtil.isBlank(originalQueryString))
UrlEncoded.decodeTo(originalQueryString, _queryParameters::add, getServletRequestInfo().getQueryEncoding());
UrlEncoded.decodeTo(forwardQueryString, _queryParameters::add, getServletRequestInfo().getQueryEncoding());
}
}
}
}
public static class CrossContextIncluded extends ServletApiRequest
{
private final ServletPathMapping _originalMapping;
protected CrossContextIncluded(ServletContextRequest servletContextRequest)
{
super(servletContextRequest);
Request dispatchedRequest = servletContextRequest.getWrapped();
_originalMapping = ServletPathMapping.from(dispatchedRequest.getAttribute(CrossContextDispatcher.ORIGINAL_SERVLET_MAPPING));
//ensure the request is set up with the correct INCLUDE attributes now we know the matchedResource
MatchedResource<ServletHandler.MappedServlet> matchedResource = servletContextRequest.getMatchedResource();
dispatchedRequest.setAttribute(RequestDispatcher.INCLUDE_MAPPING, matchedResource.getResource().getServletPathMapping(getServletRequestInfo().getDecodedPathInContext()));
dispatchedRequest.setAttribute(RequestDispatcher.INCLUDE_SERVLET_PATH, matchedResource.getMatchedPath().getPathMatch());
dispatchedRequest.setAttribute(RequestDispatcher.INCLUDE_PATH_INFO, matchedResource.getMatchedPath().getPathInfo());
dispatchedRequest.setAttribute(RequestDispatcher.INCLUDE_CONTEXT_PATH, servletContextRequest.getContext().getContextPath());
}
@Override
public String getPathInfo()
{
return _originalMapping == null ? null : _originalMapping.getPathInfo();
}
@Override
public String getContextPath()
{
return (String)getAttribute(CrossContextDispatcher.ORIGINAL_CONTEXT_PATH);
}
@Override
public String getQueryString()
{
return (String)getAttribute(CrossContextDispatcher.ORIGINAL_QUERY_STRING);
}
@Override
public String getServletPath()
{
return _originalMapping == null ? null : _originalMapping.getServletPath();
}
@Override
public HttpServletMapping getHttpServletMapping()
{
return _originalMapping;
}
@Override
public String getRequestURI()
{
return (String)getAttribute(CrossContextDispatcher.ORIGINAL_URI);
}
@Override
protected void extractQueryParameters() throws BadMessageException
{
// Extract query string parameters; these may be replaced by a forward()
// and may have already been extracted by mergeQueryParameters().
if (_queryParameters == null)
{
String includedQueryString = (String)getAttribute(RequestDispatcher.INCLUDE_QUERY_STRING);
String originalQueryString = getQueryString();
if (StringUtil.isBlank(includedQueryString))
{
if (StringUtil.isBlank(originalQueryString))
{
_queryParameters = ServletContextRequest.NO_PARAMS;
}
else
{
_queryParameters = new Fields(true);
UrlEncoded.decodeTo(includedQueryString, _queryParameters::add, getServletRequestInfo().getQueryEncoding());
}
}
else
{
_queryParameters = new Fields(true);
if (!StringUtil.isBlank(originalQueryString))
UrlEncoded.decodeTo(originalQueryString, _queryParameters::add, getServletRequestInfo().getQueryEncoding());
UrlEncoded.decodeTo(includedQueryString, _queryParameters::add, getServletRequestInfo().getQueryEncoding());
}
}
}
}
private static final Logger LOG = LoggerFactory.getLogger(ServletApiRequest.class);
private static final SetCookieParser SET_COOKIE_PARSER = SetCookieParser.newInstance();
@ -110,10 +237,9 @@ public class ServletApiRequest implements HttpServletRequest
private int _inputState = ServletContextRequest.INPUT_NONE;
private BufferedReader _reader;
private String _contentType;
private boolean _contentParamsExtracted;
private Fields _contentParameters;
protected Fields _contentParameters;
private Fields _parameters;
private Fields _queryParameters;
protected Fields _queryParameters;
private ServletMultiPartFormData.Parts _parts;
private boolean _asyncSupported = true;
@ -242,17 +368,20 @@ public class ServletApiRequest implements HttpServletRequest
@Override
public Cookie[] getCookies()
{
List<HttpCookie> httpCookies = Request.getCookies(getRequest());
if (httpCookies.isEmpty())
return null;
if (httpCookies instanceof ServletCookieList servletCookieList)
return servletCookieList.getServletCookies();
return CookieCache.getApiCookies(getRequest(), Cookie.class, this::convertCookie);
}
ServletCookieList servletCookieList = new ServletCookieList(httpCookies, getRequest().getConnectionMetaData().getHttpConfiguration().getRequestCookieCompliance());
getRequest().setAttribute(Request.COOKIE_ATTRIBUTE, servletCookieList);
if (getRequest().getComponents().getCache().getAttribute(Request.CACHE_ATTRIBUTE) instanceof CookieCache cookieCache)
cookieCache.replaceCookieList(servletCookieList);
return servletCookieList.getServletCookies();
private Cookie convertCookie(HttpCookie cookie)
{
CookieCompliance compliance = getRequest().getConnectionMetaData().getHttpConfiguration().getRequestCookieCompliance();
Cookie result = new Cookie(cookie.getName(), cookie.getValue());
//RFC2965 defines the cookie header as supporting path and domain but RFC6265 permits only name=value
if (CookieCompliance.RFC2965.equals(compliance))
{
result.setPath(cookie.getPath());
result.setDomain(cookie.getDomain());
}
return result;
}
@Override
@ -500,6 +629,7 @@ public class ServletApiRequest implements HttpServletRequest
@Override
public Collection<Part> getParts() throws IOException, ServletException
{
//TODO support parts read during a cross context dispatch to environment other than EE10
if (_parts == null)
{
try
@ -564,8 +694,8 @@ public class ServletApiRequest implements HttpServletRequest
try (InputStream is = p.getInputStream())
{
String content = IO.toString(is, charset == null ? defaultCharset : Charset.forName(charset));
if (_contentParameters == null)
_contentParameters = new Fields();
if (_contentParameters == null || _contentParameters.isEmpty())
_contentParameters = new Fields(true);
_contentParameters.add(p.getName(), content);
}
}
@ -821,119 +951,115 @@ public class ServletApiRequest implements HttpServletRequest
return Collections.unmodifiableMap(getParameters().toStringArrayMap());
}
private Fields getParameters()
public Fields getParameters()
{
extractContentParameters();
extractQueryParameters();
// Do parameters need to be combined?
if (ServletContextRequest.isNoParams(_queryParameters) || _queryParameters.getSize() == 0)
_parameters = _contentParameters;
else if (ServletContextRequest.isNoParams(_contentParameters) || _contentParameters.getSize() == 0)
_parameters = _queryParameters;
else if (_parameters == null)
{
_parameters = new Fields(true);
_parameters.addAll(_queryParameters);
_parameters.addAll(_contentParameters);
}
// protect against calls to recycled requests (which is illegal, but
// this gives better failures
Fields parameters = _parameters;
if (parameters == null)
{
extractContentParameters();
extractQueryParameters();
// Do parameters need to be combined?
if (ServletContextRequest.isNoParams(_queryParameters) || _queryParameters.getSize() == 0)
_parameters = _contentParameters;
else if (ServletContextRequest.isNoParams(_contentParameters) || _contentParameters.getSize() == 0)
_parameters = _queryParameters;
else if (_parameters == null)
{
_parameters = new Fields(true);
_parameters.addAll(_queryParameters);
_parameters.addAll(_contentParameters);
}
parameters = _parameters;
}
return parameters == null ? ServletContextRequest.NO_PARAMS : parameters;
}
private void extractContentParameters() throws BadMessageException
{
if (!_contentParamsExtracted)
// Extract content parameters; these cannot be replaced by a forward()
// once extracted and may have already been extracted by getParts() or
// by a processing happening after a form-based authentication.
if (_contentParameters == null)
{
// content parameters need boolean protection as they can only be read
// once, but may be reset to null by a reset
_contentParamsExtracted = true;
// Extract content parameters; these cannot be replaced by a forward()
// once extracted and may have already been extracted by getParts() or
// by a processing happening after a form-based authentication.
if (_contentParameters == null)
try
{
try
int contentLength = getContentLength();
if (contentLength != 0 && _inputState == ServletContextRequest.INPUT_NONE)
{
int contentLength = getContentLength();
if (contentLength != 0 && _inputState == ServletContextRequest.INPUT_NONE)
String baseType = HttpField.getValueParameters(getContentType(), null);
if (MimeTypes.Type.FORM_ENCODED.is(baseType) &&
getRequest().getConnectionMetaData().getHttpConfiguration().isFormEncodedMethod(getMethod()))
{
String baseType = HttpField.getValueParameters(getContentType(), null);
if (MimeTypes.Type.FORM_ENCODED.is(baseType) &&
getRequest().getConnectionMetaData().getHttpConfiguration().isFormEncodedMethod(getMethod()))
try
{
try
{
ServletContextHandler contextHandler = getServletRequestInfo().getServletContextHandler();
int maxKeys = contextHandler.getMaxFormKeys();
int maxContentSize = contextHandler.getMaxFormContentSize();
_contentParameters = FormFields.from(getRequest(), maxKeys, maxContentSize).get();
}
catch (IllegalStateException | IllegalArgumentException | ExecutionException |
InterruptedException e)
{
LOG.warn(e.toString());
throw new BadMessageException("Unable to parse form content", e);
}
ServletContextHandler contextHandler = getServletRequestInfo().getServletContextHandler();
int maxKeys = contextHandler.getMaxFormKeys();
int maxContentSize = contextHandler.getMaxFormContentSize();
_contentParameters = FormFields.from(getRequest(), maxKeys, maxContentSize).get();
}
else if (MimeTypes.Type.MULTIPART_FORM_DATA.is(baseType) &&
getAttribute(ServletContextRequest.MULTIPART_CONFIG_ELEMENT) != null)
catch (IllegalStateException | IllegalArgumentException | ExecutionException |
InterruptedException e)
{
try
{
getParts();
}
catch (IOException e)
{
String msg = "Unable to extract content parameters";
if (LOG.isDebugEnabled())
LOG.debug(msg, e);
throw new RuntimeIOException(msg, e);
}
catch (ServletException e)
{
Throwable cause = e.getCause();
if (cause instanceof BadMessageException badMessageException)
throw badMessageException;
String msg = "Unable to extract content parameters";
if (LOG.isDebugEnabled())
LOG.debug(msg, e);
throw new RuntimeIOException(msg, e);
}
}
else
{
try
{
_contentParameters = FormFields.get(getRequest()).get();
}
catch (IllegalStateException | IllegalArgumentException | ExecutionException |
InterruptedException e)
{
LOG.warn(e.toString());
throw new BadMessageException("Unable to parse form content", e);
}
LOG.warn(e.toString());
throw new BadMessageException("Unable to parse form content", e);
}
}
else if (MimeTypes.Type.MULTIPART_FORM_DATA.is(baseType) &&
getAttribute(ServletContextRequest.MULTIPART_CONFIG_ELEMENT) != null)
{
try
{
getParts();
}
catch (IOException e)
{
String msg = "Unable to extract content parameters";
if (LOG.isDebugEnabled())
LOG.debug(msg, e);
throw new RuntimeIOException(msg, e);
}
catch (ServletException e)
{
Throwable cause = e.getCause();
if (cause instanceof BadMessageException badMessageException)
throw badMessageException;
if (_contentParameters == null || _contentParameters.isEmpty())
_contentParameters = ServletContextRequest.NO_PARAMS;
}
catch (IllegalStateException | IllegalArgumentException e)
{
LOG.warn(e.toString());
throw new BadMessageException("Unable to parse form content", e);
String msg = "Unable to extract content parameters";
if (LOG.isDebugEnabled())
LOG.debug(msg, e);
throw new RuntimeIOException(msg, e);
}
}
else
{
try
{
_contentParameters = FormFields.get(getRequest()).get();
}
catch (IllegalStateException | IllegalArgumentException | ExecutionException |
InterruptedException e)
{
LOG.warn(e.toString());
throw new BadMessageException("Unable to parse form content", e);
}
}
}
if (_contentParameters == null || _contentParameters.isEmpty())
_contentParameters = ServletContextRequest.NO_PARAMS;
}
catch (IllegalStateException | IllegalArgumentException e)
{
LOG.warn(e.toString());
throw new BadMessageException("Unable to parse form content", e);
}
}
}
private void extractQueryParameters() throws BadMessageException
protected void extractQueryParameters() throws BadMessageException
{
// Extract query string parameters; these may be replaced by a forward()
// and may have already been extracted by mergeQueryParameters().
@ -1286,7 +1412,9 @@ public class ServletApiRequest implements HttpServletRequest
@Override
public DispatcherType getDispatcherType()
{
return DispatcherType.REQUEST;
Request request = getRequest();
String dispatchType = request.getContext().getCrossContextDispatchType(request);
return dispatchType == null ? DispatcherType.REQUEST : DispatcherType.valueOf(dispatchType);
}
@Override
@ -1327,53 +1455,4 @@ public class ServletApiRequest implements HttpServletRequest
throw new HttpException.IllegalArgumentException(HttpStatus.BAD_REQUEST_400, msg);
}
}
/**
* Extended list of HttpCookies that converts and caches a servlet Cookie array.
*/
private static class ServletCookieList extends AbstractList<HttpCookie>
{
private final List<HttpCookie> _httpCookies;
private final Cookie[] _cookies;
ServletCookieList(List<HttpCookie> httpCookies, CookieCompliance compliance)
{
_httpCookies = httpCookies;
_cookies = new Cookie[_httpCookies.size()];
int i = 0;
for (HttpCookie httpCookie : _httpCookies)
{
_cookies[i++] = convertCookie(httpCookie, compliance);
}
}
@Override
public HttpCookie get(int index)
{
return _httpCookies.get(index);
}
public Cookie[] getServletCookies()
{
return _cookies;
}
@Override
public int size()
{
return _cookies.length;
}
private static Cookie convertCookie(HttpCookie cookie, CookieCompliance compliance)
{
Cookie result = new Cookie(cookie.getName(), cookie.getValue());
//RFC2965 defines the cookie header as supporting path and domain but RFC6265 permits only name=value
if (CookieCompliance.RFC2965.equals(compliance))
{
result.setPath(cookie.getPath());
result.setDomain(cookie.getDomain());
}
return result;
}
}
}

View File

@ -1132,11 +1132,15 @@ public class ServletContextHandler extends ContextHandler
@Override
protected ContextRequest wrapRequest(Request request, Response response)
{
String decodedPathInContext;
MatchedResource<ServletHandler.MappedServlet> matchedResource;
// Need to ask directly to the Context for the pathInContext, rather than using
// Request.getPathInContext(), as the request is not yet wrapped in this Context.
String decodedPathInContext = URIUtil.decodePath(getContext().getPathInContext(request.getHttpURI().getCanonicalPath()));
decodedPathInContext = URIUtil.decodePath(getContext().getPathInContext(request.getHttpURI().getCanonicalPath()));
matchedResource = _servletHandler.getMatchedServlet(decodedPathInContext);
MatchedResource<ServletHandler.MappedServlet> matchedResource = _servletHandler.getMatchedServlet(decodedPathInContext);
if (matchedResource == null)
return wrapNoServlet(request, response);
ServletHandler.MappedServlet mappedServlet = matchedResource.getResource();
@ -2702,10 +2706,14 @@ public class ServletContextHandler extends ContextHandler
}
@Override
public jakarta.servlet.ServletContext getContext(String uripath)
public jakarta.servlet.ServletContext getContext(String path)
{
//TODO jetty-12 does not currently support cross context dispatch
return null;
ContextHandler context = getContextHandler().getCrossContextHandler(path);
if (context == null)
return null;
if (context == ServletContextHandler.this)
return this;
return new CrossContextServletContext(ServletContextHandler.this, context.getContext());
}
@Override

View File

@ -21,6 +21,7 @@ import java.util.List;
import java.util.Set;
import jakarta.servlet.AsyncListener;
import jakarta.servlet.DispatcherType;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletRequestAttributeListener;
import jakarta.servlet.ServletRequestWrapper;
@ -125,12 +126,12 @@ public class ServletContextRequest extends ContextRequest implements ServletCont
{
super(servletContextApi.getContext(), request);
_servletChannel = servletChannel;
_servletApiRequest = newServletApiRequest();
_matchedResource = matchedResource;
_httpInput = _servletChannel.getHttpInput();
_decodedPathInContext = decodedPathInContext;
_response = newServletContextResponse(response);
_sessionManager = sessionManager;
_servletApiRequest = newServletApiRequest();
_response = newServletContextResponse(response);
_attributes = new Attributes.Synthetic(request)
{
@Override
@ -155,6 +156,7 @@ public class ServletContextRequest extends ContextRequest implements ServletCont
return ATTRIBUTES;
}
};
addIdleTimeoutListener(_servletChannel.getServletRequestState()::onIdleTimeout);
}
@ -219,7 +221,17 @@ public class ServletContextRequest extends ContextRequest implements ServletCont
return new ServletApiRequest.AmbiguousURI(this, msg.toString());
}
return new ServletApiRequest(this);
if (getServletContextHandler().isCrossContextDispatchSupported())
{
if (DispatcherType.INCLUDE.toString().equals(getContext().getCrossContextDispatchType(getWrapped())))
return new ServletApiRequest.CrossContextIncluded(this);
else if (DispatcherType.FORWARD.toString().equals(getContext().getCrossContextDispatchType(getWrapped())))
return new ServletApiRequest.CrossContextForwarded(this);
else
return new ServletApiRequest(this);
}
else
return new ServletApiRequest(this);
}
protected ServletContextResponse newServletContextResponse(Response response)

View File

@ -13,6 +13,7 @@
package org.eclipse.jetty.ee10.servlet;
import java.io.IOException;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Set;
@ -28,6 +29,7 @@ import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpURI;
import org.eclipse.jetty.io.Content;
import org.eclipse.jetty.io.content.InputStreamContentSource;
import org.eclipse.jetty.server.Components;
import org.eclipse.jetty.server.ConnectionMetaData;
import org.eclipse.jetty.server.Context;
@ -35,6 +37,8 @@ import org.eclipse.jetty.server.HttpStream;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Session;
import org.eclipse.jetty.server.TunnelSupport;
import org.eclipse.jetty.util.Attributes;
import org.eclipse.jetty.util.ExceptionUtil;
import org.eclipse.jetty.util.URIUtil;
import static org.eclipse.jetty.util.URIUtil.addEncodedPaths;
@ -51,17 +55,27 @@ import static org.eclipse.jetty.util.URIUtil.encodePath;
* The current implementation does not support any read operations.
* </p>
*/
class ServletCoreRequest implements Request
public class ServletCoreRequest implements Request
{
public static Request wrap(HttpServletRequest httpServletRequest)
{
return new ServletCoreRequest(httpServletRequest, null);
}
private final HttpServletRequest _servletRequest;
private final ServletContextRequest _servletContextRequest;
private final HttpFields _httpFields;
private final HttpURI _uri;
private final Attributes _attributes;
private final boolean _wrapped;
private Content.Source _source;
ServletCoreRequest(HttpServletRequest request)
ServletCoreRequest(HttpServletRequest request, Attributes attributes)
{
_servletRequest = request;
_wrapped = !(request instanceof ServletApiRequest);
_servletContextRequest = ServletContextRequest.getServletContextRequest(_servletRequest);
_attributes = attributes == null ? _servletContextRequest : attributes;
HttpFields.Mutable fields = HttpFields.build();
@ -93,6 +107,15 @@ class ServletCoreRequest implements Request
builder.path(request.getRequestURI());
builder.query(request.getQueryString());
_uri = builder.asImmutable();
_source = _wrapped ? null : _servletContextRequest;
}
private Content.Source source() throws IOException
{
if (_source == null)
_source = _wrapped ? new InputStreamContentSource(getServletRequest().getInputStream()) : _servletContextRequest;
return _source;
}
@Override
@ -133,51 +156,44 @@ class ServletCoreRequest implements Request
@Override
public Object removeAttribute(String name)
{
Object value = _servletRequest.getAttribute(name);
_servletRequest.removeAttribute(name);
return value;
return _attributes.removeAttribute(name);
}
@Override
public Object setAttribute(String name, Object attribute)
{
Object value = _servletRequest.getAttribute(name);
_servletRequest.setAttribute(name, attribute);
return value;
return _attributes.setAttribute(name, attribute);
}
@Override
public Object getAttribute(String name)
{
return _servletRequest.getAttribute(name);
return _attributes.getAttribute(name);
}
@Override
public Set<String> getAttributeNameSet()
{
Set<String> set = new HashSet<>();
Enumeration<String> e = _servletRequest.getAttributeNames();
while (e.hasMoreElements())
{
set.add(e.nextElement());
}
return set;
return _attributes.getAttributeNameSet();
}
@Override
public void clearAttributes()
{
Enumeration<String> e = _servletRequest.getAttributeNames();
while (e.hasMoreElements())
{
_servletRequest.removeAttribute(e.nextElement());
}
_attributes.clearAttributes();
}
@Override
public void fail(Throwable failure)
{
throw new UnsupportedOperationException();
try
{
source().fail(failure);
}
catch (Throwable t)
{
ExceptionUtil.addSuppressedIfNotAssociated(failure, t);
}
}
@Override
@ -201,7 +217,14 @@ class ServletCoreRequest implements Request
@Override
public void demand(Runnable demandCallback)
{
throw new UnsupportedOperationException();
try
{
source().demand(demandCallback);
}
catch (Throwable t)
{
demandCallback.run();
}
}
@Override
@ -225,13 +248,35 @@ class ServletCoreRequest implements Request
@Override
public Content.Chunk read()
{
throw new UnsupportedOperationException();
try
{
return source().read();
}
catch (Throwable t)
{
return Content.Chunk.from(t, true);
}
}
@Override
public boolean consumeAvailable()
{
throw new UnsupportedOperationException();
if (_wrapped)
{
try
{
Content.Source.consumeAll(source());
return true;
}
catch (IOException e)
{
return false;
}
}
else
{
return _servletContextRequest.consumeAvailable();
}
}
@Override
@ -263,4 +308,67 @@ class ServletCoreRequest implements Request
{
return Session.getSession(_servletRequest.getSession(create));
}
public static class ServletAttributes implements Attributes
{
private final HttpServletRequest _servletRequest;
private Set<String> _attributeNames;
public ServletAttributes(HttpServletRequest httpServletRequest)
{
_servletRequest = httpServletRequest;
}
@Override
public Object removeAttribute(String name)
{
Object value = _servletRequest.getAttribute(name);
if (value != null)
_attributeNames = null;
_servletRequest.removeAttribute(name);
return value;
}
@Override
public Object setAttribute(String name, Object attribute)
{
Object value = _servletRequest.getAttribute(name);
if (value == null)
_attributeNames = null;
_servletRequest.setAttribute(name, attribute);
return value;
}
@Override
public Object getAttribute(String name)
{
return _servletRequest.getAttribute(name);
}
@Override
public Set<String> getAttributeNameSet()
{
Set<String> set = _attributeNames;
if (set == null)
{
set = new HashSet<>();
Enumeration<String> e = _servletRequest.getAttributeNames();
while (e.hasMoreElements())
set.add(e.nextElement());
_attributeNames = set;
}
return set;
}
@Override
public void clearAttributes()
{
Enumeration<String> e = _servletRequest.getAttributeNames();
_attributeNames = null;
while (e.hasMoreElements())
{
_servletRequest.removeAttribute(e.nextElement());
}
}
}
}

View File

@ -34,25 +34,33 @@ import org.eclipse.jetty.server.Response;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.IO;
import org.eclipse.jetty.util.StringUtil;
/**
* A {@link HttpServletResponse} wrapped as a core {@link Response}.
* All write operations are internally converted to blocking writes on the servlet API.
*/
class ServletCoreResponse implements Response
public class ServletCoreResponse implements Response
{
private final HttpServletResponse _response;
public Response wrap(Request coreRequest, HttpServletResponse httpServletResponse, boolean included)
{
return new ServletCoreResponse(coreRequest, httpServletResponse, included);
}
private final HttpServletResponse _httpServletResponse;
private final Request _coreRequest;
private final HttpFields.Mutable _httpFields;
private final boolean _included;
private final ServletContextResponse _servletContextResponse;
private final boolean _wrapped;
public ServletCoreResponse(Request coreRequest, HttpServletResponse response, boolean included)
ServletCoreResponse(Request coreRequest, HttpServletResponse httpServletResponse, boolean included)
{
_coreRequest = coreRequest;
_response = response;
_servletContextResponse = ServletContextResponse.getServletContextResponse(response);
HttpFields.Mutable fields = new HttpServletResponseHttpFields(response);
_httpServletResponse = httpServletResponse;
_servletContextResponse = ServletContextResponse.getServletContextResponse(httpServletResponse);
_wrapped = !(httpServletResponse instanceof ServletApiResponse);
HttpFields.Mutable fields = new HttpServletResponseHttpFields(httpServletResponse);
if (included)
{
// If included, accept but ignore mutations.
@ -61,6 +69,12 @@ class ServletCoreResponse implements Response
@Override
public HttpField onAddField(HttpField field)
{
String name = field == null ? null : field.getName();
if (!StringUtil.isBlank(name) && name.startsWith(Dispatcher.JETTY_INCLUDE_HEADER_PREFIX))
{
return new HttpField(name.substring(Dispatcher.JETTY_INCLUDE_HEADER_PREFIX.length()), field.getValue());
}
return null;
}
@ -69,6 +83,12 @@ class ServletCoreResponse implements Response
{
return false;
}
@Override
public HttpField onReplaceField(HttpField oldField, HttpField newField)
{
return oldField;
}
};
}
_httpFields = fields;
@ -83,7 +103,7 @@ class ServletCoreResponse implements Response
public HttpServletResponse getServletResponse()
{
return _response;
return _httpServletResponse;
}
@Override
@ -101,7 +121,7 @@ class ServletCoreResponse implements Response
@Override
public boolean isCommitted()
{
return _response.isCommitted();
return _httpServletResponse.isCommitted();
}
private boolean isWriting()
@ -116,29 +136,36 @@ class ServletCoreResponse implements Response
last = false;
try
{
if (BufferUtil.hasContent(byteBuffer))
if (!_wrapped && !_servletContextResponse.isWritingOrStreaming())
{
if (isWriting())
{
String characterEncoding = _response.getCharacterEncoding();
try (ByteBufferInputStream bbis = new ByteBufferInputStream(byteBuffer);
InputStreamReader reader = new InputStreamReader(bbis, characterEncoding))
{
IO.copy(reader, _response.getWriter());
}
if (last)
_response.getWriter().close();
}
else
{
BufferUtil.writeTo(byteBuffer, _response.getOutputStream());
if (last)
_response.getOutputStream().close();
}
_servletContextResponse.write(last, byteBuffer, callback);
}
else
{
if (BufferUtil.hasContent(byteBuffer))
{
if (isWriting())
{
String characterEncoding = _httpServletResponse.getCharacterEncoding();
try (ByteBufferInputStream bbis = new ByteBufferInputStream(byteBuffer);
InputStreamReader reader = new InputStreamReader(bbis, characterEncoding))
{
IO.copy(reader, _httpServletResponse.getWriter());
}
callback.succeeded();
if (last)
_httpServletResponse.getWriter().close();
}
else
{
BufferUtil.writeTo(byteBuffer, _httpServletResponse.getOutputStream());
if (last)
_httpServletResponse.getOutputStream().close();
}
}
callback.succeeded();
}
}
catch (Throwable t)
{
@ -155,7 +182,7 @@ class ServletCoreResponse implements Response
@Override
public int getStatus()
{
return _response.getStatus();
return _httpServletResponse.getStatus();
}
@Override
@ -163,7 +190,7 @@ class ServletCoreResponse implements Response
{
if (_included)
return;
_response.setStatus(code);
_httpServletResponse.setStatus(code);
}
@Override
@ -180,19 +207,19 @@ class ServletCoreResponse implements Response
@Override
public void reset()
{
_response.reset();
_httpServletResponse.reset();
}
@Override
public CompletableFuture<Void> writeInterim(int status, HttpFields headers)
{
throw new UnsupportedOperationException();
return _servletContextResponse.writeInterim(status, headers);
}
@Override
public String toString()
{
return "%s@%x{%s,%s}".formatted(this.getClass().getSimpleName(), hashCode(), this._coreRequest, _response);
return "%s@%x{%s,%s}".formatted(this.getClass().getSimpleName(), hashCode(), this._coreRequest, _httpServletResponse);
}
private static class HttpServletResponseHttpFields implements HttpFields.Mutable

View File

@ -13,6 +13,9 @@
package org.eclipse.jetty.ee10.servlet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import jakarta.servlet.http.HttpServletMapping;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.MappingMatch;
@ -111,9 +114,10 @@ public class ServletPathMapping implements HttpServletMapping
case PREFIX_GLOB:
_mappingMatch = MappingMatch.PATH;
_servletPath = pathSpec.getPrefix();
// TODO avoid the substring on the known servletPath!
_matchValue = _servletPath.startsWith("/") ? _servletPath.substring(1) : _servletPath;
_pathInfo = matchedPath != null ? matchedPath.getPathInfo() : null;
_pathInfo = matchedPath != null
? matchedPath.getPathInfo()
: _servletPath.length() == pathInContext.length() ? null : pathInContext.substring(_servletPath.length());
break;
case SUFFIX_GLOB:
@ -135,6 +139,16 @@ public class ServletPathMapping implements HttpServletMapping
this(pathSpec, servletName, pathInContext, null);
}
private ServletPathMapping(MappingMatch mappingMatch, String matchValue, String pattern, String servletName, String servletPath, String pathInfo)
{
_mappingMatch = mappingMatch;
_matchValue = matchValue;
_pattern = pattern;
_servletName = servletName;
_servletPath = servletPath;
_pathInfo = pathInfo;
}
@Override
public String getMatchValue()
{
@ -173,12 +187,46 @@ public class ServletPathMapping implements HttpServletMapping
public String toString()
{
return "ServletPathMapping{" +
"matchValue=" + _matchValue +
"mappingMatch=" + _mappingMatch +
", matchValue=" + _matchValue +
", pattern=" + _pattern +
", servletName=" + _servletName +
", mappingMatch=" + _mappingMatch +
", servletPath=" + _servletPath +
", pathInfo=" + _pathInfo +
"}";
}
private static final Pattern DESERIALIZE = Pattern.compile("ServletPathMapping\\{" +
"mappingMatch=(?<mappingMatch>[^,]+), " +
"matchValue=(?<matchValue>[^,]+), " +
"pattern=(?<pattern>[^,]+), " +
"servletName=(?<servletName>[^,]+), " +
"servletPath=(?<servletPath>[^,]+), " +
"pathInfo=(?<pathInfo>[^}]+)}");
/**
* Obtain a {@link ServletPathMapping} instance from an object which may be an instance of a mapping
* from a different EE version obtained from a cross context cross environment dispatch
* @param o The object to caste or deserialize from the string representation.
* @return A ServletPathMapping
*/
public static ServletPathMapping from(Object o)
{
if (o == null)
return null;
if (o instanceof ServletPathMapping mapping)
return mapping;
Matcher matcher = DESERIALIZE.matcher(o.toString());
if (matcher.find())
return new ServletPathMapping(
MappingMatch.valueOf(matcher.group("mappingMatch")),
matcher.group("matchValue"),
matcher.group("pattern"),
matcher.group("servletName"),
matcher.group("servletPath"),
matcher.group("pathInfo")
);
return null;
}
}

View File

@ -21,6 +21,7 @@ import java.util.Arrays;
import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;
import jakarta.servlet.AsyncContext;
@ -48,17 +49,12 @@ import org.eclipse.jetty.logging.StacklessLogging;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.LocalConnector;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.handler.ContextHandler;
import org.eclipse.jetty.server.handler.ContextHandlerCollection;
import org.eclipse.jetty.server.handler.ResourceHandler;
import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
import org.eclipse.jetty.util.MultiMap;
import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.util.UrlEncoded;
import org.eclipse.jetty.util.resource.ResourceFactory;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
@ -91,17 +87,10 @@ public class DispatcherTest
_connector.getConnectionFactory(HttpConfiguration.ConnectionFactory.class).getHttpConfiguration().setSendServerVersion(false);
_connector.getConnectionFactory(HttpConfiguration.ConnectionFactory.class).getHttpConfiguration().setSendDateHeader(false);
ContextHandlerCollection contextCollection = new ContextHandlerCollection();
_contextHandler = new ServletContextHandler();
_contextHandler.setContextPath("/context");
_contextHandler.setBaseResourceAsPath(MavenTestingUtils.getTestResourcePathDir("contextResources"));
contextCollection.addHandler(_contextHandler);
ResourceHandler resourceHandler = new ResourceHandler();
resourceHandler.setBaseResource(ResourceFactory.root().newResource(MavenTestingUtils.getTestResourcePathDir("dispatchResourceTest")));
ContextHandler resourceContextHandler = new ContextHandler("/resource");
resourceContextHandler.setHandler(resourceHandler);
contextCollection.addHandler(resourceContextHandler);
_server.setHandler(contextCollection);
_server.setHandler(_contextHandler);
_server.addConnector(_connector);
_server.start();
@ -194,7 +183,7 @@ public class DispatcherTest
assertEquals(expected, rawResponse);
}
@Test
public void testForwardNonUTF8() throws Exception
{
@ -376,7 +365,7 @@ public class DispatcherTest
Connection: close\r
\r
""");
assertThat(rawResponse, containsString(" 500 "));
assertThat(rawResponse, containsString(" 400 "));
}
}
@ -394,7 +383,7 @@ public class DispatcherTest
Connection: close\r
\r
""");
String expected = """
HTTP/1.1 200 OK\r
specialSetHeader: specialSetHeader\r
@ -827,98 +816,6 @@ public class DispatcherTest
assertEquals(expected, rawResponse);
}
@Test
public void testWorkingResourceHandler() throws Exception
{
String rawResponse = _connector.getResponse("""
GET /resource/content.txt HTTP/1.1\r
Host: localhost\r
Connection: close\r
\r
""");
HttpTester.Response response = HttpTester.parseResponse(rawResponse);
// from inside the context.txt file
assertThat(response.getContent(), containsString("content goes here")); // from inside the context.txt file
}
@Test
@Disabled("Cross context dispatch not yet supported in jetty-12")
public void testIncludeToResourceHandler() throws Exception
{
_contextHandler.addServlet(DispatchToResourceServlet.class, "/resourceServlet/*");
String rawResponse = _connector.getResponse("""
GET /context/resourceServlet/content.txt?do=include HTTP/1.1\r
Host: localhost\r
Connection: close\r
\r
""");
HttpTester.Response response = HttpTester.parseResponse(rawResponse);
// from inside the context.txt file
assertThat(response.getContent(), containsString("content goes here"));
}
@Test
@Disabled("Cross context dispatch not yet supported in jetty-12")
public void testForwardToResourceHandler() throws Exception
{
_contextHandler.addServlet(DispatchToResourceServlet.class, "/resourceServlet/*");
String rawResponse = _connector.getResponse("""
GET /context/resourceServlet/content.txt?do=forward HTTP/1.1\r
Host: localhost\r
Connection: close\r
\r
""");
HttpTester.Response response = HttpTester.parseResponse(rawResponse);
// from inside the context.txt file
assertThat(response.getContent(), containsString("content goes here"));
}
@Test
@Disabled("Cross context dispatch not yet supported in jetty-12")
public void testWrappedIncludeToResourceHandler() throws Exception
{
_contextHandler.addServlet(DispatchToResourceServlet.class, "/resourceServlet/*");
String rawResponse = _connector.getResponse("""
GET /context/resourceServlet/content.txt?do=include&wrapped=true HTTP/1.1\r
Host: localhost\r
Connection: close\r
\r
""");
HttpTester.Response response = HttpTester.parseResponse(rawResponse);
// from inside the context.txt file
assertThat(response.getContent(), containsString("content goes here"));
}
@Test
@Disabled("Cross context dispatch not yet supported in jetty-12")
public void testWrappedForwardToResourceHandler() throws Exception
{
_contextHandler.addServlet(DispatchToResourceServlet.class, "/resourceServlet/*");
String rawResponse = _connector.getResponse("""
GET /context/resourceServlet/content.txt?do=forward&wrapped=true HTTP/1.1
Host: localhost\r
Connection: close\r
\r
""");
HttpTester.Response response = HttpTester.parseResponse(rawResponse);
// from inside the context.txt file
assertThat(response.getContent(), containsString("content goes here"));
}
@Test
public void testForwardFilterToRogerServlet() throws Exception
{
@ -1145,6 +1042,18 @@ public class DispatcherTest
}
}
public static class ParameterReadingFilter implements Filter
{
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException
{
//case the params to be parsed on the request
Map<String, String[]> params = request.getParameterMap();
chain.doFilter(request, response);
}
}
/*
* Forward filter works with roger, echo and reverse echo servlets to test various
* forwarding bits using filters.
@ -1213,6 +1122,35 @@ public class DispatcherTest
}
}
public static class CrossContextDispatchServlet extends HttpServlet implements Servlet
{
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException
{
doGet(req, resp);
}
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
{
RequestDispatcher dispatcher;
if (request.getParameter("forward") != null)
{
ServletContext foreign = getServletContext().getContext("/foreign");
assertNotNull(foreign);
dispatcher = foreign.getRequestDispatcher(request.getParameter("forward"));
if (dispatcher != null)
{
dispatcher.forward(new HttpServletRequestWrapper(request), new HttpServletResponseWrapper(response));
}
else
response.sendError(404);
}
}
}
public static class IncludeServlet extends HttpServlet implements Servlet
{
// The logic linked to this field be deleted and the writer always used once #10155 is fixed.
@ -1319,6 +1257,46 @@ public class DispatcherTest
}
}
public static class ParameterReadingServlet extends GenericServlet
{
@Override
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException
{
Map<String, String[]> params = req.getParameterMap();
for (String key : params.keySet())
{
res.getWriter().print(key + "=");
String[] val = params.get(key);
if (val == null)
res.getWriter().println();
else if (val.length == 1)
res.getWriter().println(val[0]);
else
{
res.getWriter().println(Arrays.asList(val));
}
}
/* System.err.println(req.getAttribute("jakarta.servlet.forward.mapping"));
System.err.println(req.getAttribute("jakarta.servlet.forward.request_uri"));
System.err.println(req.getAttribute("jakarta.servlet.forward.context_path"));
System.err.println(req.getAttribute("jakarta.servlet.forward.servlet_path"));
System.err.println(req.getAttribute("jakarta.servlet.forward.path_info"));
System.err.println(req.getAttribute("jakarta.servlet.forward.query_string"));*/
}
}
public static class VerifyForwardServlet extends GenericServlet
{
@Override
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException
{
if (DispatcherType.FORWARD.equals(req.getDispatcherType()))
res.getWriter().print("Verified!");
}
}
public static class RogerThatServlet extends GenericServlet
{
@Override
@ -1447,7 +1425,7 @@ public class DispatcherTest
response.getOutputStream().println(request.getRequestURI());
}
}
public static class IncludeEchoURIServlet extends HttpServlet implements Servlet
{
@Override
@ -1468,7 +1446,7 @@ public class DispatcherTest
response.getOutputStream().println((String)request.getAttribute(RequestDispatcher.INCLUDE_SERVLET_PATH));
}
}
public static class ForwardEchoURIServlet extends HttpServlet implements Servlet
{
@Override

View File

@ -18,8 +18,10 @@ import java.util.concurrent.TimeUnit;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletMapping;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.MappingMatch;
import org.eclipse.jetty.server.LocalConnector;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.util.IO;
@ -29,6 +31,7 @@ import org.junit.jupiter.api.Test;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertFalse;
public class ServletTest
@ -193,4 +196,41 @@ public class ServletTest
assertFalse(endPoint.isOpen());
}
}
@Test
public void testHttpServletMapping() throws Exception
{
_context.addServlet(new HttpServlet()
{
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException
{
HttpServletMapping mapping = req.getHttpServletMapping();
assertThat(mapping.getMappingMatch(), is(MappingMatch.EXACT));
assertThat(mapping.getMatchValue(), is("get"));
assertThat(mapping.getPattern(), is("/get"));
mapping = ServletPathMapping.from(mapping);
assertThat(mapping.getMappingMatch(), is(MappingMatch.EXACT));
assertThat(mapping.getMatchValue(), is("get"));
assertThat(mapping.getPattern(), is("/get"));
mapping = ServletPathMapping.from(mapping.toString());
assertThat(mapping.getMappingMatch(), is(MappingMatch.EXACT));
assertThat(mapping.getMatchValue(), is("get"));
assertThat(mapping.getPattern(), is("/get"));
resp.getWriter().println("Hello!");
}
}, "/get");
_server.start();
String response = _connector.getResponse("""
GET /ctx/get HTTP/1.0
""");
assertThat(response, containsString(" 200 OK"));
assertThat(response, containsString("Hello!"));
}
}

View File

@ -61,7 +61,6 @@ import org.eclipse.jetty.util.security.Password;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
@ -1051,7 +1050,6 @@ public class ConstraintTest
}
@Test
@Disabled("Dispatch unsupported")
public void testFormDispatch() throws Exception
{
_security.setAuthenticator(new FormAuthenticator("/testLoginPage", "/testErrorPage", true));
@ -1680,7 +1678,6 @@ public class ConstraintTest
}
@Test
@Disabled("Dispatch unsupported")
public void testStrictFormDispatch()
throws Exception
{

View File

@ -42,7 +42,6 @@ import org.eclipse.jetty.session.SessionDataStoreFactory;
import org.eclipse.jetty.session.test.TestSessionDataStoreFactory;
import org.eclipse.jetty.util.StringUtil;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import static org.hamcrest.MatcherAssert.assertThat;
@ -303,8 +302,6 @@ public class CreationTest
* session in it too. Check that both sessions exist after the response
* completes.
*/
//TODO - no cross context support in jetty-12
@Disabled
@Test
public void testSessionCreateForward() throws Exception
{
@ -322,8 +319,10 @@ public class CreationTest
TestServlet servlet = new TestServlet();
ServletHolder holder = new ServletHolder(servlet);
ServletContextHandler contextHandler = server1.addContext(contextPath);
contextHandler.setCrossContextDispatchSupported(true);
contextHandler.addServlet(holder, servletMapping);
ServletContextHandler ctxB = server1.addContext(contextB);
ctxB.setCrossContextDispatchSupported(true);
ctxB.addServlet(TestServletB.class, servletMapping);
server1.start();
int port1 = server1.getPort();
@ -353,8 +352,6 @@ public class CreationTest
* in it, then invalidate the session in the original context: that should invalidate the
* session in both contexts and no session should exist after the response completes.
*/
//TODO no cross context dispatch in jetty-12
@Disabled
@Test
public void testSessionCreateForwardAndInvalidate() throws Exception
{
@ -373,8 +370,10 @@ public class CreationTest
TestServlet servlet = new TestServlet();
ServletHolder holder = new ServletHolder(servlet);
ServletContextHandler contextHandler = server1.addContext(contextPath);
contextHandler.setCrossContextDispatchSupported(true);
contextHandler.addServlet(holder, servletMapping);
ServletContextHandler ctxB = server1.addContext(contextB);
ctxB.setCrossContextDispatchSupported(true);
ctxB.addServlet(TestServletB.class, servletMapping);
server1.start();
int port1 = server1.getPort();

View File

@ -1349,15 +1349,15 @@ public class WebAppContext extends ServletContextHandler implements WebAppClassL
public class ServletApiContext extends ServletContextHandler.ServletContextApi
{
@Override
public jakarta.servlet.ServletContext getContext(String uripath)
public jakarta.servlet.ServletContext getContext(String path)
{
jakarta.servlet.ServletContext servletContext = super.getContext(uripath);
jakarta.servlet.ServletContext servletContext = super.getContext(path);
if (servletContext != null && _contextWhiteList != null)
{
for (String context : _contextWhiteList)
{
if (context.equals(uripath))
if (context.equals(path))
{
return servletContext;
}

View File

@ -54,7 +54,6 @@ import org.eclipse.jetty.util.resource.ResourceFactory;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.parallel.Isolated;
@ -251,20 +250,20 @@ public class WebAppContextTest
*
* @throws Exception on test failure
*/
@Disabled // Reenabled when cross context dispatch is implemented.
@Test
public void testContextWhiteList() throws Exception
{
Server server = newServer();
Handler.Sequence handlers = new Handler.Sequence();
WebAppContext contextA = new WebAppContext(".", "/A");
contextA.addServlet(ServletA.class, "/s");
contextA.setCrossContextDispatchSupported(true);
handlers.addHandler(contextA);
WebAppContext contextB = new WebAppContext(".", "/B");
WebAppContext contextB = new WebAppContext(".", "/B");
contextB.addServlet(ServletB.class, "/s");
contextB.setContextWhiteList("/doesnotexist", "/B/s");
contextB.setCrossContextDispatchSupported(true);
handlers.addHandler(contextB);
server.setHandler(handlers);

View File

@ -49,7 +49,7 @@
<javax.servlet.jsp.jstl.impl.version>1.2.5</javax.servlet.jsp.jstl.impl.version>
<jetty.servlet.api.version>4.0.6</jetty.servlet.api.version>
<jsp.impl.version>9.0.83.1</jsp.impl.version>
<modify-sources-plugin.version>1.0.9</modify-sources-plugin.version>
<modify-sources-plugin.version>1.0.10</modify-sources-plugin.version>
<sonar.skip>true</sonar.skip>
<weld.version>3.1.9.Final</weld.version>
</properties>

View File

@ -83,8 +83,8 @@ import org.eclipse.jetty.session.SessionManager;
import org.eclipse.jetty.util.Attributes;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.ExceptionUtil;
import org.eclipse.jetty.util.Fields;
import org.eclipse.jetty.util.Loader;
import org.eclipse.jetty.util.MultiMap;
import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.util.TypeUtil;
import org.eclipse.jetty.util.URIUtil;
@ -324,6 +324,26 @@ public class ContextHandler extends ScopedHandler implements Attributes, Supplie
_coreContextHandler.setAllowNullPathInContext(allowNullPathInfo);
}
/**
* Cross context dispatch support.
* @param supported {@code True} if cross context dispatch is supported
* @see org.eclipse.jetty.server.handler.ContextHandler#setCrossContextDispatchSupported(boolean)
*/
public void setCrossContextDispatchSupported(boolean supported)
{
getCoreContextHandler().setCrossContextDispatchSupported(supported);
}
/**
* Cross context dispatch support.
* @return {@code True} if cross context dispatch is supported
* @see org.eclipse.jetty.server.handler.ContextHandler#isCrossContextDispatchSupported()
*/
public boolean isCrossContextDispatchSupported()
{
return getCoreContextHandler().isCrossContextDispatchSupported();
}
@Override
public void setServer(Server server)
{
@ -1705,7 +1725,7 @@ public class ContextHandler extends ScopedHandler implements Attributes, Supplie
// this is a dispatch with either a provided URI and/or a dispatched path
// We will have to modify the request and then revert
final HttpURI oldUri = baseRequest.getHttpURI();
final MultiMap<String> oldQueryParams = baseRequest.getQueryParameters();
final Fields oldQueryParams = baseRequest.getQueryFields();
try
{
if (encodedPathQuery == null)
@ -1747,7 +1767,7 @@ public class ContextHandler extends ScopedHandler implements Attributes, Supplie
finally
{
baseRequest.setHttpURI(oldUri);
baseRequest.setQueryParameters(oldQueryParams);
baseRequest.setQueryFields(oldQueryParams);
baseRequest.resetParameters();
}
}
@ -1836,10 +1856,15 @@ public class ContextHandler extends ScopedHandler implements Attributes, Supplie
}
@Override
public ServletContext getContext(String uripath)
public ServletContext getContext(String path)
{
// TODO No cross context dispatch
return null;
org.eclipse.jetty.server.handler.ContextHandler context = getContextHandler().getCoreContextHandler().getCrossContextHandler(path);
if (context == null)
return null;
if (context == _coreContextHandler)
return this;
return new CrossContextServletContext(_coreContextHandler, context.getContext());
}
@Override
@ -2694,10 +2719,10 @@ public class ContextHandler extends ScopedHandler implements Attributes, Supplie
private class CoreToNestedHandler extends Abstract
{
@Override
public boolean handle(org.eclipse.jetty.server.Request coreRequest, Response response, Callback callback) throws Exception
public boolean handle(org.eclipse.jetty.server.Request coreRequest, Response response, Callback callback)
{
HttpChannel httpChannel = org.eclipse.jetty.server.Request.get(coreRequest, CoreContextRequest.class, CoreContextRequest::getHttpChannel);
httpChannel.onProcess(response, callback);
Objects.requireNonNull(httpChannel).onProcess(response, callback);
httpChannel.handle();
return true;
}

View File

@ -0,0 +1,303 @@
//
// ========================================================================
// 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.ee9.nested;
import java.io.Closeable;
import java.io.IOException;
import java.util.Objects;
import java.util.Set;
import jakarta.servlet.DispatcherType;
import jakarta.servlet.RequestDispatcher;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.eclipse.jetty.http.HttpURI;
import org.eclipse.jetty.server.FormFields;
import org.eclipse.jetty.util.Attributes;
import org.eclipse.jetty.util.Blocker;
import org.eclipse.jetty.util.IO;
class CrossContextDispatcher implements RequestDispatcher
{
public static final String ORIGINAL_URI = "org.eclipse.jetty.dispatch.originalURI";
public static final String ORIGINAL_QUERY_STRING = "org.eclipse.jetty.dispatch.originalQueryString";
public static final String ORIGINAL_SERVLET_MAPPING = "org.eclipse.jetty.dispatch.originalServletMapping";
public static final String ORIGINAL_CONTEXT_PATH = "org.eclipse.jetty.dispatch.originalContextPath";
private static final String FOREIGN_SERVLET_PACKAGE = "javax.servlet."; //EE8EE9-TRANSLATE
public static final Set<String> ATTRIBUTES = Set.of(
RequestDispatcher.FORWARD_REQUEST_URI,
RequestDispatcher.FORWARD_MAPPING,
RequestDispatcher.FORWARD_CONTEXT_PATH,
RequestDispatcher.FORWARD_SERVLET_PATH,
RequestDispatcher.FORWARD_QUERY_STRING,
RequestDispatcher.FORWARD_PATH_INFO,
RequestDispatcher.INCLUDE_REQUEST_URI,
RequestDispatcher.INCLUDE_MAPPING,
RequestDispatcher.INCLUDE_CONTEXT_PATH,
RequestDispatcher.INCLUDE_SERVLET_PATH,
RequestDispatcher.INCLUDE_QUERY_STRING,
RequestDispatcher.INCLUDE_PATH_INFO,
// TODO MULTIPART_CONFIG_ELEMENT,
org.eclipse.jetty.server.handler.ContextHandler.CROSS_CONTEXT_ATTRIBUTE,
ORIGINAL_URI,
ORIGINAL_QUERY_STRING,
ORIGINAL_SERVLET_MAPPING,
ORIGINAL_CONTEXT_PATH,
FormFields.class.getName()
);
private final CrossContextServletContext _targetContext;
private final HttpURI _uri;
private final String _decodedPathInContext;
private class IncludeRequest extends ServletCoreRequest
{
private Request _baseRequest;
public IncludeRequest(ContextHandler.CoreContextRequest coreContextRequest, Request request, HttpServletRequest httpServletRequest)
{
super(coreContextRequest, httpServletRequest, new Attributes.Synthetic(new ServletAttributes(httpServletRequest))
{
@Override
public Object getAttribute(String name)
{
//handle cross-environment dispatch from ee8
if (name.startsWith(FOREIGN_SERVLET_PACKAGE))
name = "jakarta.servlet." + name.substring(FOREIGN_SERVLET_PACKAGE.length());
return super.getAttribute(name);
}
@Override
protected Object getSyntheticAttribute(String name)
{
if (name == null)
return null;
//Special include attributes refer to the target context and path
return switch (name)
{
case RequestDispatcher.INCLUDE_MAPPING -> null;
case RequestDispatcher.INCLUDE_SERVLET_PATH -> null;
case RequestDispatcher.INCLUDE_PATH_INFO -> _decodedPathInContext;
case RequestDispatcher.INCLUDE_REQUEST_URI -> (_uri == null) ? null : _uri.getPath();
case RequestDispatcher.INCLUDE_CONTEXT_PATH -> _targetContext.getContextPath();
case RequestDispatcher.INCLUDE_QUERY_STRING -> (_uri == null) ? null : _uri.getQuery();
case org.eclipse.jetty.server.handler.ContextHandler.CROSS_CONTEXT_ATTRIBUTE -> DispatcherType.INCLUDE.toString();
case ORIGINAL_URI -> httpServletRequest.getRequestURI();
case ORIGINAL_QUERY_STRING -> httpServletRequest.getQueryString();
case ORIGINAL_SERVLET_MAPPING -> httpServletRequest.getHttpServletMapping();
case ORIGINAL_CONTEXT_PATH -> httpServletRequest.getContextPath();
default -> httpServletRequest.getAttribute(name);
};
}
@Override
protected Set<String> getSyntheticNameSet()
{
return ATTRIBUTES;
}
@Override
public Object setAttribute(String name, Object attribute)
{
if (name == null)
return null;
//handle cross-environment dispatch from ee8
if (name.startsWith(FOREIGN_SERVLET_PACKAGE))
name = "jakarta.servlet." + name.substring(FOREIGN_SERVLET_PACKAGE.length());
return super.setAttribute(name, attribute);
}
});
_baseRequest = request;
}
@Override
public HttpURI getHttpURI()
{
//return the uri of the dispatch target
return _uri;
}
@Override
public Object getAttribute(String name)
{
if (MultiPart.Parser.class.getName().equals(name))
return _baseRequest.getAttribute(name);
return super.getAttribute(name);
}
}
private class IncludeResponse extends ServletCoreResponse
{
public IncludeResponse(ServletCoreRequest servletCoreRequest, HttpServletResponse httpServletResponse, Response baseResponse, org.eclipse.jetty.server.Response coreResponse)
{
super(servletCoreRequest, httpServletResponse, baseResponse, coreResponse, true);
}
}
private class ForwardRequest extends ServletCoreRequest
{
private Request _baseRequest;
public ForwardRequest(ContextHandler.CoreContextRequest coreContextRequest, Request request, HttpServletRequest httpServletRequest)
{
super(coreContextRequest, httpServletRequest, new Attributes.Synthetic(new ServletAttributes(httpServletRequest))
{
@Override
protected Object getSyntheticAttribute(String name)
{
if (name == null)
return null;
//handle cross-environment dispatch from ee8
if (name.startsWith(FOREIGN_SERVLET_PACKAGE))
name = "jakarta.servlet." + name.substring(FOREIGN_SERVLET_PACKAGE.length());
return switch (name)
{
case RequestDispatcher.FORWARD_REQUEST_URI -> httpServletRequest.getRequestURI();
case RequestDispatcher.FORWARD_SERVLET_PATH -> httpServletRequest.getServletPath();
case RequestDispatcher.FORWARD_PATH_INFO -> httpServletRequest.getPathInfo();
case RequestDispatcher.FORWARD_CONTEXT_PATH -> httpServletRequest.getContextPath();
case RequestDispatcher.FORWARD_MAPPING -> httpServletRequest.getHttpServletMapping();
case RequestDispatcher.FORWARD_QUERY_STRING -> httpServletRequest.getQueryString();
case RequestDispatcher.INCLUDE_MAPPING -> REMOVED;
case RequestDispatcher.INCLUDE_REQUEST_URI -> REMOVED;
case RequestDispatcher.INCLUDE_CONTEXT_PATH -> REMOVED;
case RequestDispatcher.INCLUDE_QUERY_STRING -> REMOVED;
case RequestDispatcher.INCLUDE_SERVLET_PATH -> REMOVED;
case RequestDispatcher.INCLUDE_PATH_INFO -> REMOVED;
// TODO case ServletContextRequest.MULTIPART_CONFIG_ELEMENT -> httpServletRequest.getAttribute(ServletMultiPartFormData.class.getName());
case org.eclipse.jetty.server.handler.ContextHandler.CROSS_CONTEXT_ATTRIBUTE -> DispatcherType.FORWARD.toString();
default ->
{
if (FormFields.class.getName().equals(name))
{
Request baseRequest = Objects.requireNonNull(Request.getBaseRequest(httpServletRequest));
yield baseRequest.peekParameters();
}
yield null;
}
};
}
@Override
protected Set<String> getSyntheticNameSet()
{
return ATTRIBUTES;
}
});
_baseRequest = request;
}
@Override
public Object getAttribute(String name)
{
if (MultiPart.Parser.class.getName().equals(name))
return _baseRequest.getAttribute(name);
return super.getAttribute(name);
}
@Override
public HttpURI getHttpURI()
{
return _uri;
}
}
CrossContextDispatcher(CrossContextServletContext targetContext, HttpURI uri, String decodedPathInContext)
{
_targetContext = targetContext;
_uri = uri;
_decodedPathInContext = decodedPathInContext;
}
@Override
public void forward(ServletRequest servletRequest, ServletResponse response) throws ServletException, IOException
{
HttpServletRequest httpServletRequest = (servletRequest instanceof HttpServletRequest) ? ((HttpServletRequest)servletRequest) : new ServletRequestHttpWrapper(servletRequest);
HttpServletResponse httpServletResponse = (response instanceof HttpServletResponse) ? (HttpServletResponse)response : new ServletResponseHttpWrapper(response);
Request baseRequest = Objects.requireNonNull(Request.getBaseRequest(servletRequest));
Response baseResponse = baseRequest.getResponse();
ContextHandler.CoreContextRequest coreContextRequest = baseRequest.getCoreRequest();
org.eclipse.jetty.server.Response coreResponse = coreContextRequest.getHttpChannel().getCoreResponse();
baseResponse.resetForForward();
ForwardRequest forwardRequest = new ForwardRequest(coreContextRequest, baseRequest, httpServletRequest);
ServletCoreResponse servletCoreResponse = new ServletCoreResponse(forwardRequest, httpServletResponse, baseResponse, coreResponse, false);
try (Blocker.Callback callback = Blocker.callback())
{
_targetContext.getTargetContext().getContextHandler().handle(forwardRequest, servletCoreResponse, callback);
callback.block();
}
catch (Exception e)
{
throw new ServletException(e);
}
// If we are not async and not closed already, then close via the possibly wrapped response.
if (!baseRequest.isAsyncStarted() && !servletCoreResponse.hasLastWrite())
{
Closeable closeable;
try
{
closeable = response.getOutputStream();
}
catch (IllegalStateException e)
{
closeable = response.getWriter();
}
IO.close(closeable);
}
}
@Override
public void include(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException
{
HttpServletRequest httpServletRequest = (servletRequest instanceof HttpServletRequest) ? ((HttpServletRequest)servletRequest) : new ServletRequestHttpWrapper(servletRequest);
HttpServletResponse httpServletResponse = (servletResponse instanceof HttpServletResponse) ? (HttpServletResponse)servletResponse : new ServletResponseHttpWrapper(servletResponse);
Request baseRequest = Objects.requireNonNull(Request.getBaseRequest(servletRequest));
Response baseResponse = baseRequest.getResponse();
ContextHandler.CoreContextRequest coreContextRequest = baseRequest.getCoreRequest();
org.eclipse.jetty.server.Response coreResponse = coreContextRequest.getHttpChannel().getCoreResponse();
IncludeRequest includeRequest = new IncludeRequest(coreContextRequest, baseRequest, httpServletRequest);
IncludeResponse includeResponse = new IncludeResponse(includeRequest, httpServletResponse, baseResponse, coreResponse);
try (Blocker.Callback callback = Blocker.callback())
{
_targetContext.getTargetContext().getContextHandler().handle(includeRequest, includeResponse, callback);
callback.block();
}
catch (Exception e)
{
throw new ServletException(e);
}
}
}

View File

@ -0,0 +1,454 @@
//
// ========================================================================
// 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.ee9.nested;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.file.Path;
import java.util.Collections;
import java.util.Enumeration;
import java.util.EventListener;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import jakarta.servlet.Filter;
import jakarta.servlet.FilterRegistration;
import jakarta.servlet.RequestDispatcher;
import jakarta.servlet.Servlet;
import jakarta.servlet.ServletContext;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRegistration;
import jakarta.servlet.SessionCookieConfig;
import jakarta.servlet.SessionTrackingMode;
import jakarta.servlet.descriptor.JspConfigDescriptor;
import org.eclipse.jetty.http.HttpURI;
import org.eclipse.jetty.server.handler.ContextHandler;
import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.util.URIUtil;
import org.eclipse.jetty.util.resource.Resource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A {@link ServletContext} used for cross context dispatch, which wraps an arbitrary {@link ContextHandler}
*/
class CrossContextServletContext implements ServletContext
{
private static final Logger LOG = LoggerFactory.getLogger(CrossContextServletContext.class);
private final ContextHandler _servletContextHandler;
private final ContextHandler.ScopedContext _targetContext;
protected CrossContextServletContext(ContextHandler servletContextHandler, ContextHandler.ScopedContext targetContext)
{
_servletContextHandler = servletContextHandler;
_targetContext = Objects.requireNonNull(targetContext);
}
@Override
public String getContextPath()
{
return _targetContext.getContextPath();
}
@Override
public ServletContext getContext(String uripath)
{
throw new UnsupportedOperationException();
}
@Override
@Deprecated
public Servlet getServlet(String name) throws ServletException
{
return null;
}
@Override
@Deprecated
public Enumeration<Servlet> getServlets()
{
return null;
}
@Override
@Deprecated
public Enumeration<String> getServletNames()
{
return null;
}
@Override
@Deprecated
public void log(Exception exception, String msg)
{
log(msg, exception);
}
@Override
public int getMajorVersion()
{
return 5;
}
@Override
public int getMinorVersion()
{
return 0;
}
@Override
public int getEffectiveMajorVersion()
{
return 5;
}
@Override
public int getEffectiveMinorVersion()
{
return 0;
}
@Override
public String getMimeType(String file)
{
return _targetContext.getMimeTypes().getMimeByExtension(file);
}
@Override
public Set<String> getResourcePaths(String path)
{
Resource resource = _targetContext.getBaseResource().resolve(path);
if (resource != null && resource.isDirectory())
return resource.list().stream().map(Resource::getPath).map(Path::getFileName).map(Path::toString).collect(Collectors.toSet());
return null;
}
@Override
public URL getResource(String path) throws MalformedURLException
{
return _targetContext.getBaseResource().resolve(path).getURI().toURL();
}
@Override
public InputStream getResourceAsStream(String path)
{
throw new UnsupportedOperationException();
}
@Override
public RequestDispatcher getRequestDispatcher(String uriInContext)
{
// uriInContext is encoded, potentially with query.
if (uriInContext == null)
return null;
if (!uriInContext.startsWith("/"))
return null;
try
{
String contextPath = getContextPath();
// uriInContext is canonical by HttpURI.
HttpURI.Mutable uri = HttpURI.build(uriInContext);
String encodedPathInContext = uri.getCanonicalPath();
if (StringUtil.isEmpty(encodedPathInContext))
return null;
if (!StringUtil.isEmpty(contextPath))
{
uri.path(URIUtil.addPaths(contextPath, uri.getPath()));
encodedPathInContext = uri.getCanonicalPath().substring(contextPath.length());
}
return new CrossContextDispatcher(this, uri, URIUtil.decodePath(encodedPathInContext));
}
catch (Exception e)
{
LOG.trace("IGNORED", e);
}
return null;
}
@Override
public RequestDispatcher getNamedDispatcher(String name)
{
throw new UnsupportedOperationException();
}
@Override
public void log(String msg)
{
LOG.info(msg);
}
@Override
public void log(String message, Throwable throwable)
{
LOG.warn(message, throwable);
}
@Override
public String getRealPath(String path)
{
throw new UnsupportedOperationException();
}
@Override
public String getServerInfo()
{
return _servletContextHandler.getServer().getServerInfo();
}
@Override
public String getInitParameter(String name)
{
throw new UnsupportedOperationException();
}
@Override
public Enumeration<String> getInitParameterNames()
{
throw new UnsupportedOperationException();
}
@Override
public boolean setInitParameter(String name, String value)
{
throw new UnsupportedOperationException();
}
@Override
public Object getAttribute(String name)
{
return _targetContext.getAttribute(name);
}
@Override
public Enumeration<String> getAttributeNames()
{
return Collections.enumeration(_targetContext.getAttributeNameSet());
}
@Override
public void setAttribute(String name, Object object)
{
_targetContext.setAttribute(name, object);
}
@Override
public void removeAttribute(String name)
{
_targetContext.removeAttribute(name);
}
@Override
public String getServletContextName()
{
return _targetContext.getContextHandler().getDisplayName();
}
@Override
public ServletRegistration.Dynamic addServlet(String servletName, String className)
{
throw new UnsupportedOperationException();
}
@Override
public ServletRegistration.Dynamic addServlet(String servletName, Servlet servlet)
{
throw new UnsupportedOperationException();
}
@Override
public ServletRegistration.Dynamic addServlet(String servletName, Class<? extends Servlet> servletClass)
{
throw new UnsupportedOperationException();
}
@Override
public ServletRegistration.Dynamic addJspFile(String servletName, String jspFile)
{
throw new UnsupportedOperationException();
}
@Override
public <T extends Servlet> T createServlet(Class<T> clazz) throws ServletException
{
throw new UnsupportedOperationException();
}
@Override
public ServletRegistration getServletRegistration(String servletName)
{
throw new UnsupportedOperationException();
}
@Override
public Map<String, ? extends ServletRegistration> getServletRegistrations()
{
throw new UnsupportedOperationException();
}
@Override
public FilterRegistration.Dynamic addFilter(String filterName, String className)
{
throw new UnsupportedOperationException();
}
@Override
public FilterRegistration.Dynamic addFilter(String filterName, Filter filter)
{
throw new UnsupportedOperationException();
}
@Override
public FilterRegistration.Dynamic addFilter(String filterName, Class<? extends Filter> filterClass)
{
throw new UnsupportedOperationException();
}
@Override
public <T extends Filter> T createFilter(Class<T> clazz) throws ServletException
{
throw new UnsupportedOperationException();
}
@Override
public FilterRegistration getFilterRegistration(String filterName)
{
throw new UnsupportedOperationException();
}
@Override
public Map<String, ? extends FilterRegistration> getFilterRegistrations()
{
throw new UnsupportedOperationException();
}
@Override
public SessionCookieConfig getSessionCookieConfig()
{
throw new UnsupportedOperationException();
}
@Override
public void setSessionTrackingModes(Set<SessionTrackingMode> sessionTrackingModes)
{
throw new UnsupportedOperationException();
}
@Override
public Set<SessionTrackingMode> getDefaultSessionTrackingModes()
{
throw new UnsupportedOperationException();
}
@Override
public Set<SessionTrackingMode> getEffectiveSessionTrackingModes()
{
throw new UnsupportedOperationException();
}
@Override
public void addListener(String className)
{
throw new UnsupportedOperationException();
}
@Override
public <T extends EventListener> void addListener(T t)
{
throw new UnsupportedOperationException();
}
@Override
public void addListener(Class<? extends EventListener> listenerClass)
{
throw new UnsupportedOperationException();
}
@Override
public <T extends EventListener> T createListener(Class<T> clazz) throws ServletException
{
throw new UnsupportedOperationException();
}
@Override
public JspConfigDescriptor getJspConfigDescriptor()
{
throw new UnsupportedOperationException();
}
@Override
public ClassLoader getClassLoader()
{
return _targetContext.getClassLoader();
}
@Override
public void declareRoles(String... roleNames)
{
throw new UnsupportedOperationException();
}
@Override
public String getVirtualServerName()
{
return null;
}
@Override
public int getSessionTimeout()
{
throw new UnsupportedOperationException();
}
@Override
public void setSessionTimeout(int sessionTimeout)
{
throw new UnsupportedOperationException();
}
@Override
public String getRequestCharacterEncoding()
{
throw new UnsupportedOperationException();
}
@Override
public void setRequestCharacterEncoding(String encoding)
{
throw new UnsupportedOperationException();
}
@Override
public String getResponseCharacterEncoding()
{
throw new UnsupportedOperationException();
}
@Override
public void setResponseCharacterEncoding(String encoding)
{
throw new UnsupportedOperationException();
}
ContextHandler.ScopedContext getTargetContext()
{
return _targetContext;
}
}

View File

@ -30,7 +30,7 @@ import org.eclipse.jetty.http.HttpException;
import org.eclipse.jetty.http.HttpURI;
import org.eclipse.jetty.http.UriCompliance;
import org.eclipse.jetty.util.Attributes;
import org.eclipse.jetty.util.MultiMap;
import org.eclipse.jetty.util.Fields;
import org.eclipse.jetty.util.URIUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -87,7 +87,7 @@ public class Dispatcher implements RequestDispatcher
final DispatcherType old_type = baseRequest.getDispatcherType();
final Attributes old_attr = baseRequest.getAttributes();
final MultiMap<String> old_query_params = baseRequest.getQueryParameters();
final Fields old_query_fields = baseRequest.getQueryFields();
final ContextHandler.APIContext old_context = baseRequest.getContext();
final ServletPathMapping old_mapping = baseRequest.getServletPathMapping();
try
@ -123,7 +123,7 @@ public class Dispatcher implements RequestDispatcher
{
baseRequest.setAttributes(old_attr);
baseRequest.getResponse().included();
baseRequest.setQueryParameters(old_query_params);
baseRequest.setQueryFields(old_query_fields);
baseRequest.resetParameters();
baseRequest.setDispatcherType(old_type);
}
@ -151,7 +151,7 @@ public class Dispatcher implements RequestDispatcher
final String old_path_in_context = baseRequest.getPathInContext();
final ServletPathMapping old_mapping = baseRequest.getServletPathMapping();
final ServletPathMapping source_mapping = baseRequest.findServletPathMapping();
final MultiMap<String> old_query_params = baseRequest.getQueryParameters();
final Fields old_query_params = baseRequest.getQueryFields();
final Attributes old_attr = baseRequest.getAttributes();
final DispatcherType old_type = baseRequest.getDispatcherType();
@ -233,7 +233,7 @@ public class Dispatcher implements RequestDispatcher
baseRequest.setHttpURI(old_uri);
baseRequest.setContext(old_context, old_path_in_context);
baseRequest.setServletPathMapping(old_mapping);
baseRequest.setQueryParameters(old_query_params);
baseRequest.setQueryFields(old_query_params);
baseRequest.resetParameters();
baseRequest.setAttributes(old_attr);
baseRequest.setDispatcherType(old_type);

View File

@ -26,9 +26,7 @@ import java.net.SocketAddress;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.security.Principal;
import java.util.AbstractList;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
@ -65,7 +63,6 @@ import jakarta.servlet.http.Part;
import jakarta.servlet.http.PushBuilder;
import org.eclipse.jetty.http.BadMessageException;
import org.eclipse.jetty.http.ComplianceViolation;
import org.eclipse.jetty.http.CookieCache;
import org.eclipse.jetty.http.CookieCompliance;
import org.eclipse.jetty.http.HttpCookie;
import org.eclipse.jetty.http.HttpField;
@ -83,6 +80,8 @@ import org.eclipse.jetty.http.SetCookieParser;
import org.eclipse.jetty.io.Connection;
import org.eclipse.jetty.io.RuntimeIOException;
import org.eclipse.jetty.security.UserIdentity;
import org.eclipse.jetty.server.CookieCache;
import org.eclipse.jetty.server.FormFields;
import org.eclipse.jetty.server.HttpCookieUtils;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.Session;
@ -90,6 +89,7 @@ import org.eclipse.jetty.session.AbstractSessionManager;
import org.eclipse.jetty.session.ManagedSession;
import org.eclipse.jetty.session.SessionManager;
import org.eclipse.jetty.util.Attributes;
import org.eclipse.jetty.util.Fields;
import org.eclipse.jetty.util.HostPort;
import org.eclipse.jetty.util.IO;
import org.eclipse.jetty.util.MultiMap;
@ -126,8 +126,8 @@ public class Request implements HttpServletRequest
private static final int INPUT_NONE = 0;
private static final int INPUT_STREAM = 1;
private static final int INPUT_READER = 2;
private static final MultiMap<String> NO_PARAMS = new MultiMap<>();
private static final MultiMap<String> BAD_PARAMS = new MultiMap<>();
private static final Fields NO_PARAMS = Fields.EMPTY;
private static final Fields BAD_PARAMS = new Fields(true);
/**
* Compare inputParameters to NO_PARAMS by Reference
@ -135,7 +135,7 @@ public class Request implements HttpServletRequest
* @param inputParameters The parameters to compare to NO_PARAMS
* @return True if the inputParameters reference is equal to NO_PARAMS otherwise False
*/
private static boolean isNoParams(MultiMap<String> inputParameters)
private static boolean isNoParams(Fields inputParameters)
{
@SuppressWarnings("ReferenceEquality")
boolean isNoParams = (inputParameters == NO_PARAMS);
@ -173,6 +173,7 @@ public class Request implements HttpServletRequest
private ContextHandler.APIContext _context;
private final List<ServletRequestAttributeListener> _requestAttributeListeners = new ArrayList<>();
private final HttpInput _input;
private final boolean _crossContextDispatchSupported;
private ContextHandler.CoreContextRequest _coreRequest;
private MetaData.Request _metaData;
private HttpFields _httpFields;
@ -184,7 +185,6 @@ public class Request implements HttpServletRequest
private Object _asyncNotSupportedSource = null;
private boolean _secure;
private boolean _handled = false;
private boolean _contentParamsExtracted;
private Attributes _attributes;
private Authentication _authentication;
private String _contentType;
@ -193,9 +193,9 @@ public class Request implements HttpServletRequest
private int _inputState = INPUT_NONE;
private BufferedReader _reader;
private String _readerEncoding;
private MultiMap<String> _queryParameters;
private MultiMap<String> _contentParameters;
private MultiMap<String> _parameters;
private Fields _queryParameters;
private Fields _contentParameters;
private Fields _parameters;
private Charset _queryEncoding;
private UserIdentityScope _scope;
private long _timeStamp;
@ -208,6 +208,7 @@ public class Request implements HttpServletRequest
{
_channel = channel;
_input = input;
_crossContextDispatchSupported = _channel.getContextHandler().getCoreContextHandler().isCrossContextDispatchSupported();
}
public HttpFields getHttpFields()
@ -353,14 +354,18 @@ public class Request implements HttpServletRequest
throw new IllegalArgumentException(listener.getClass().toString());
}
private MultiMap<String> getParameters()
Fields peekParameters()
{
if (!_contentParamsExtracted)
{
// content parameters need boolean protection as they can only be read
// once, but may be reset to null by a reset
_contentParamsExtracted = true;
return _parameters;
}
private Fields getParameters()
{
// protect against calls to recycled requests (which is illegal, but
// this gives better failures
Fields parameters = _parameters;
if (parameters == null)
{
// Extract content parameters; these cannot be replaced by a forward()
// once extracted and may have already been extracted by getParts() or
// by a processing happening after a form-based authentication.
@ -376,41 +381,47 @@ public class Request implements HttpServletRequest
throw new BadMessageException("Unable to parse form content", e);
}
}
// Extract query string parameters; these may be replaced by a forward()
// and may have already been extracted by mergeQueryParameters().
if (_queryParameters == null)
extractQueryParameters();
// Do parameters need to be combined?
if (isNoParams(_queryParameters) || _queryParameters.getSize() == 0)
_parameters = _contentParameters;
else if (isNoParams(_contentParameters) || _contentParameters.getSize() == 0)
_parameters = _queryParameters;
else if (_parameters == null)
{
_parameters = new Fields(true);
_parameters.addAll(_queryParameters);
_parameters.addAll(_contentParameters);
}
parameters = _parameters;
}
// Extract query string parameters; these may be replaced by a forward()
// and may have already been extracted by mergeQueryParameters().
if (_queryParameters == null)
extractQueryParameters();
// Do parameters need to be combined?
if (isNoParams(_queryParameters) || _queryParameters.size() == 0)
_parameters = _contentParameters;
else if (isNoParams(_contentParameters) || _contentParameters.size() == 0)
_parameters = _queryParameters;
else if (_parameters == null)
{
_parameters = new MultiMap<>();
_parameters.addAllValues(_queryParameters);
_parameters.addAllValues(_contentParameters);
}
// protect against calls to recycled requests (which is illegal, but
// this gives better failures
MultiMap<String> parameters = _parameters;
return parameters == null ? NO_PARAMS : parameters;
}
private void extractQueryParameters()
{
if (_uri == null || StringUtil.isEmpty(_uri.getQuery()))
if (_uri == null)
{
_queryParameters = NO_PARAMS;
return;
}
String query = _uri.getQuery();
if (StringUtil.isEmpty(query))
_queryParameters = NO_PARAMS;
else
{
try
{
_queryParameters = new MultiMap<>();
UrlEncoded.decodeTo(_uri.getQuery(), _queryParameters, _queryEncoding);
_queryParameters = new Fields(true);
UrlEncoded.decodeTo(query, _queryParameters::add, _queryEncoding);
}
catch (IllegalStateException | IllegalArgumentException e)
{
@ -418,6 +429,25 @@ public class Request implements HttpServletRequest
throw new BadMessageException("Unable to parse URI query", e);
}
}
if (_crossContextDispatchSupported)
{
String dispatcherType = _coreRequest.getContext().getCrossContextDispatchType(_coreRequest);
if (dispatcherType != null)
{
String sourceQuery = (String)_coreRequest.getAttribute(
DispatcherType.valueOf(dispatcherType) == DispatcherType.FORWARD
? RequestDispatcher.FORWARD_QUERY_STRING : CrossContextDispatcher.ORIGINAL_QUERY_STRING);
if (!StringUtil.isBlank(sourceQuery))
{
if (_queryParameters == NO_PARAMS)
_queryParameters = new Fields(true);
UrlEncoded.decodeTo(sourceQuery, _queryParameters::add, _queryEncoding);
}
}
}
}
private boolean isContentEncodingSupported()
@ -436,7 +466,24 @@ public class Request implements HttpServletRequest
_contentParameters = NO_PARAMS;
else
{
_contentParameters = new MultiMap<>();
if (_crossContextDispatchSupported && _coreRequest.getContext().isCrossContextDispatch(_coreRequest))
{
try
{
_contentParameters = FormFields.get(_coreRequest).get();
return;
}
catch (RuntimeException e)
{
throw e;
}
catch (Throwable t)
{
throw new RuntimeException(t);
}
}
_contentParameters = new Fields(true);
int contentLength = getContentLength();
if (contentLength != 0 && _inputState == INPUT_NONE)
{
@ -475,7 +522,7 @@ public class Request implements HttpServletRequest
}
}
public void extractFormParameters(MultiMap<String> params)
public void extractFormParameters(Fields params)
{
try
{
@ -497,7 +544,7 @@ public class Request implements HttpServletRequest
if (_input.isAsync())
throw new IllegalStateException("Cannot extract parameters with async IO");
UrlEncoded.decodeTo(in, params, UrlEncoded.decodeCharset(getCharacterEncoding()), maxFormContentSize, maxFormKeys);
UrlEncoded.decodeTo(in, params::add, UrlEncoded.decodeCharset(getCharacterEncoding()), maxFormContentSize, maxFormKeys);
}
catch (IOException e)
{
@ -567,6 +614,8 @@ public class Request implements HttpServletRequest
return _channel;
if (Connection.class.getName().equals(name))
return _channel.getCoreRequest().getConnectionMetaData().getConnection();
if (MultiPart.Parser.class.getName().equals(name))
return _multiParts;
}
return (_attributes == null) ? null : _attributes.getAttribute(name);
}
@ -706,7 +755,18 @@ public class Request implements HttpServletRequest
@Override
public String getContextPath()
{
// The context path returned is normally for the current context. Except during a cross context
//During a cross context INCLUDE dispatch, the context path is that of the originating
//request
if (_crossContextDispatchSupported)
{
String crossContextDispatchType = _coreRequest.getContext().getCrossContextDispatchType(_coreRequest);
if (DispatcherType.INCLUDE.toString().equals(crossContextDispatchType))
{
return (String)_coreRequest.getAttribute(CrossContextDispatcher.ORIGINAL_CONTEXT_PATH);
}
}
// The context path returned is normally for the current context. Except during an
// INCLUDE dispatch, in which case this method returns the context path of the source context,
// which we recover from the IncludeAttributes wrapper.
ContextHandler.APIContext context;
@ -740,22 +800,40 @@ public class Request implements HttpServletRequest
@Override
public Cookie[] getCookies()
{
ContextHandler.CoreContextRequest coreRequest = getCoreRequest();
if (coreRequest == null)
return null;
return CookieCache.getApiCookies(getCoreRequest(), Cookie.class, this::convertCookie);
}
List<HttpCookie> httpCookies = org.eclipse.jetty.server.Request.getCookies(coreRequest);
if (httpCookies.isEmpty())
return null;
private Cookie convertCookie(HttpCookie cookie)
{
try
{
Cookie result = new Cookie(cookie.getName(), cookie.getValue());
if (httpCookies instanceof ServletCookieList servletCookieList)
return servletCookieList.getServletCookies();
if (getCoreRequest().getConnectionMetaData().getHttpConfiguration().getRequestCookieCompliance().allows(CookieCompliance.Violation.ATTRIBUTE_VALUES))
{
if (cookie.getVersion() > 0)
result.setVersion(cookie.getVersion());
ServletCookieList servletCookieList = new ServletCookieList(httpCookies, coreRequest.getConnectionMetaData().getHttpConfiguration().getRequestCookieCompliance());
coreRequest.setAttribute(org.eclipse.jetty.server.Request.COOKIE_ATTRIBUTE, servletCookieList);
if (coreRequest.getComponents().getCache().getAttribute(org.eclipse.jetty.server.Request.CACHE_ATTRIBUTE) instanceof CookieCache cookieCache)
cookieCache.replaceCookieList(servletCookieList);
return servletCookieList.getServletCookies();
String path = cookie.getPath();
if (StringUtil.isNotBlank(path))
result.setPath(path);
String domain = cookie.getDomain();
if (StringUtil.isNotBlank(domain))
result.setDomain(domain);
String comment = cookie.getComment();
if (StringUtil.isNotBlank(comment))
result.setComment(comment);
}
return result;
}
catch (Exception x)
{
if (LOG.isDebugEnabled())
LOG.debug("Bad Cookie", x);
}
return null;
}
@Override
@ -929,7 +1007,7 @@ public class Request implements HttpServletRequest
@Override
public String getParameter(String name)
{
return getParameters().getValue(name, 0);
return getParameters().getValue(name);
}
@Override
@ -941,7 +1019,7 @@ public class Request implements HttpServletRequest
@Override
public Enumeration<String> getParameterNames()
{
return Collections.enumeration(getParameters().keySet());
return Collections.enumeration(getParameters().getNames());
}
@Override
@ -953,21 +1031,39 @@ public class Request implements HttpServletRequest
return vals.toArray(new String[0]);
}
@Deprecated
public MultiMap<String> getQueryParameters()
{
return _queryParameters == null ? null : _queryParameters.toMultiMap();
}
public Fields getQueryFields()
{
return _queryParameters;
}
public void setQueryParameters(MultiMap<String> queryParameters)
public void setQueryFields(Fields queryParameters)
{
_queryParameters = queryParameters;
}
public void setContentParameters(MultiMap<String> contentParameters)
@Deprecated
public void setQueryParameters(MultiMap<String> queryParameters)
{
_queryParameters = queryParameters == null ? null : new Fields(queryParameters);
}
public void setContentFields(Fields contentParameters)
{
_contentParameters = contentParameters;
}
@Deprecated
public void setContentParameters(MultiMap<String> contentParameters)
{
_contentParameters = contentParameters == null ? NO_PARAMS : new Fields(contentParameters);
}
public void resetParameters()
{
_parameters = null;
@ -1026,6 +1122,15 @@ public class Request implements HttpServletRequest
@Override
public String getQueryString()
{
if (_crossContextDispatchSupported)
{
String crossContextDispatchType = _coreRequest.getContext().getCrossContextDispatchType(_coreRequest);
if (DispatcherType.INCLUDE.toString().equals(crossContextDispatchType))
{
return (String)_coreRequest.getAttribute(CrossContextDispatcher.ORIGINAL_QUERY_STRING);
}
}
return _uri == null ? null : _uri.getQuery();
}
@ -1147,6 +1252,14 @@ public class Request implements HttpServletRequest
@Override
public String getRequestURI()
{
if (_crossContextDispatchSupported)
{
String crossContextDispatchType = _coreRequest.getContext().getCrossContextDispatchType(_coreRequest);
if (DispatcherType.INCLUDE.toString().equals(crossContextDispatchType))
{
return (String)_coreRequest.getAttribute(CrossContextDispatcher.ORIGINAL_URI);
}
}
return _uri == null ? null : _uri.getPath();
}
@ -1222,6 +1335,7 @@ public class Request implements HttpServletRequest
@Override
public ServletContext getServletContext()
{
//TODO cross context include must return the context of the originating request
return _context;
}
@ -1271,7 +1385,8 @@ public class Request implements HttpServletRequest
{
try
{
_multiParts.deleteParts();
if (!_crossContextDispatchSupported || !_coreRequest.getContext().isCrossContextDispatch(_coreRequest))
_multiParts.deleteParts();
}
catch (Throwable e)
{
@ -1548,7 +1663,6 @@ public class Request implements HttpServletRequest
_asyncNotSupportedSource = null;
_secure = false;
_handled = false;
_contentParamsExtracted = false;
_attributes = null;
_authentication = Authentication.NOT_CHECKED;
_contentType = null;
@ -1893,10 +2007,22 @@ public class Request implements HttpServletRequest
return getParts(null);
}
private Collection<Part> getParts(MultiMap<String> params) throws IOException
private Collection<Part> getParts(Fields params) throws IOException
{
if (_multiParts == null)
{
if (_crossContextDispatchSupported)
{
// the request prior or after dispatch may have parsed the multipart
Object multipart = _coreRequest.getAttribute(MultiPart.Parser.class.getName());
//TODO support cross environment multipart
if (multipart instanceof MultiPart.Parser multiPartParser)
{
_multiParts = multiPartParser;
return _multiParts.getParts();
}
}
MultipartConfigElement config = (MultipartConfigElement)getAttribute(MULTIPART_CONFIG_ELEMENT);
if (config == null)
throw new IllegalStateException("No multipart config for servlet");
@ -1974,12 +2100,15 @@ public class Request implements HttpServletRequest
String content = os.toString(charset == null ? defaultCharset : Charset.forName(charset));
if (_contentParameters == null)
_contentParameters = params == null ? new MultiMap<>() : params;
_contentParameters = params == null ? new Fields(true) : params;
_contentParameters.add(p.getName(), content);
}
os.reset();
}
}
if (_crossContextDispatchSupported)
_coreRequest.setAttribute(MultiPart.Parser.class.getName(), _multiParts);
}
return _multiParts.getParts();
@ -2025,21 +2154,21 @@ public class Request implements HttpServletRequest
public void mergeQueryParameters(String oldQuery, String newQuery)
{
MultiMap<String> newQueryParams = null;
Fields newQueryParams = null;
// Have to assume ENCODING because we can't know otherwise.
if (newQuery != null)
{
newQueryParams = new MultiMap<>();
UrlEncoded.decodeTo(newQuery, newQueryParams, UrlEncoded.ENCODING);
newQueryParams = new Fields(true);
UrlEncoded.decodeTo(newQuery, newQueryParams::add, UrlEncoded.ENCODING);
}
MultiMap<String> oldQueryParams = _queryParameters;
Fields oldQueryParams = _queryParameters;
if (oldQueryParams == null && oldQuery != null)
{
oldQueryParams = new MultiMap<>();
oldQueryParams = new Fields(true);
try
{
UrlEncoded.decodeTo(oldQuery, oldQueryParams, getQueryCharset());
UrlEncoded.decodeTo(oldQuery, oldQueryParams::add, getQueryCharset());
}
catch (Throwable th)
{
@ -2048,19 +2177,19 @@ public class Request implements HttpServletRequest
}
}
MultiMap<String> mergedQueryParams;
if (newQueryParams == null || newQueryParams.size() == 0)
Fields mergedQueryParams;
if (newQueryParams == null || newQueryParams.getSize() == 0)
mergedQueryParams = oldQueryParams == null ? NO_PARAMS : oldQueryParams;
else if (oldQueryParams == null || oldQueryParams.size() == 0)
else if (oldQueryParams == null || oldQueryParams.getSize() == 0)
mergedQueryParams = newQueryParams;
else
{
// Parameters values are accumulated.
mergedQueryParams = new MultiMap<>(newQueryParams);
mergedQueryParams.addAllValues(oldQueryParams);
mergedQueryParams = new Fields(newQueryParams);
mergedQueryParams.addAll(oldQueryParams);
}
setQueryParameters(mergedQueryParams);
setQueryFields(mergedQueryParams);
resetParameters();
}
@ -2077,6 +2206,30 @@ public class Request implements HttpServletRequest
*/
public void setServletPathMapping(ServletPathMapping servletPathMapping)
{
// Change request to cross context dispatch
if (_crossContextDispatchSupported && _dispatcherType == DispatcherType.REQUEST && _servletPathMapping == null)
{
String crossContextDispatchType = _coreRequest.getContext().getCrossContextDispatchType(_coreRequest);
if (crossContextDispatchType != null)
{
_dispatcherType = DispatcherType.valueOf(crossContextDispatchType);
if (_dispatcherType == DispatcherType.INCLUDE)
{
// make a ServletPathMapping with the original data returned by findServletPathMapping method
Object attribute = _coreRequest.getAttribute(CrossContextDispatcher.ORIGINAL_SERVLET_MAPPING);
ServletPathMapping originalMapping = ServletPathMapping.from(attribute);
if (originalMapping != attribute)
_coreRequest.setAttribute(CrossContextDispatcher.ORIGINAL_SERVLET_MAPPING, originalMapping);
// Set the include attributes to the target mapping
_coreRequest.setAttribute(RequestDispatcher.INCLUDE_MAPPING, servletPathMapping);
_coreRequest.setAttribute(RequestDispatcher.INCLUDE_SERVLET_PATH, servletPathMapping.getServletPath());
_coreRequest.setAttribute(RequestDispatcher.INCLUDE_PATH_INFO, servletPathMapping.getPathInfo());
_coreRequest.setAttribute(RequestDispatcher.INCLUDE_CONTEXT_PATH, getContext().getContextPath());
_coreRequest.setAttribute(RequestDispatcher.INCLUDE_QUERY_STRING, getHttpURI().getQuery());
}
}
}
_servletPathMapping = servletPathMapping;
}
@ -2099,8 +2252,15 @@ public class Request implements HttpServletRequest
ServletPathMapping mapping;
if (_dispatcherType == DispatcherType.INCLUDE)
{
Dispatcher.IncludeAttributes include = Attributes.unwrap(_attributes, Dispatcher.IncludeAttributes.class);
mapping = (include == null) ? _servletPathMapping : include.getSourceMapping();
if (_crossContextDispatchSupported && DispatcherType.INCLUDE.toString().equals(_coreRequest.getContext().getCrossContextDispatchType(_coreRequest)))
{
mapping = (ServletPathMapping)_coreRequest.getAttribute(CrossContextDispatcher.ORIGINAL_SERVLET_MAPPING);
}
else
{
Dispatcher.IncludeAttributes include = Attributes.unwrap(_attributes, Dispatcher.IncludeAttributes.class);
mapping = (include == null) ? _servletPathMapping : include.getSourceMapping();
}
}
else
{
@ -2125,79 +2285,4 @@ public class Request implements HttpServletRequest
// which we recover from the IncludeAttributes wrapper.
return findServletPathMapping();
}
/**
* Extended list of HttpCookies that converts and caches a servlet Cookie array.
*/
private static class ServletCookieList extends AbstractList<HttpCookie>
{
private final List<HttpCookie> _httpCookies;
private final Cookie[] _cookies;
ServletCookieList(List<HttpCookie> httpCookies, CookieCompliance compliance)
{
_httpCookies = httpCookies;
Cookie[] cookies = new Cookie[_httpCookies.size()];
int i = 0;
for (HttpCookie httpCookie : _httpCookies)
{
Cookie cookie = convertCookie(httpCookie, compliance);
if (cookie == null)
cookies = Arrays.copyOf(cookies, cookies.length - 1);
else
cookies[i++] = cookie;
}
_cookies = cookies;
}
@Override
public HttpCookie get(int index)
{
return _httpCookies.get(index);
}
public Cookie[] getServletCookies()
{
return _cookies;
}
@Override
public int size()
{
return _cookies.length;
}
private static Cookie convertCookie(HttpCookie cookie, CookieCompliance compliance)
{
try
{
Cookie result = new Cookie(cookie.getName(), cookie.getValue());
if (compliance.allows(CookieCompliance.Violation.ATTRIBUTE_VALUES))
{
if (cookie.getVersion() > 0)
result.setVersion(cookie.getVersion());
String path = cookie.getPath();
if (StringUtil.isNotBlank(path))
result.setPath(path);
String domain = cookie.getDomain();
if (StringUtil.isNotBlank(domain))
result.setDomain(domain);
String comment = cookie.getComment();
if (StringUtil.isNotBlank(comment))
result.setComment(comment);
}
return result;
}
catch (Exception x)
{
if (LOG.isDebugEnabled())
LOG.debug("Bad Cookie", x);
}
return null;
}
}
}

View File

@ -155,6 +155,19 @@ public class Response implements HttpServletResponse
private static final EnumSet<EncodingFrom> __localeOverride = EnumSet.of(EncodingFrom.NOT_SET, EncodingFrom.DEFAULT, EncodingFrom.INFERRED, EncodingFrom.SET_LOCALE);
private static final EnumSet<EncodingFrom> __explicitCharset = EnumSet.of(EncodingFrom.SET_LOCALE, EncodingFrom.SET_CHARACTER_ENCODING, EncodingFrom.SET_CONTENT_TYPE);
public static Response getBaseResponse(ServletResponse servletResponse)
{
while (true)
{
if (servletResponse instanceof Response response)
return response;
if (servletResponse instanceof ServletResponseWrapper wrapper)
servletResponse = wrapper.getResponse();
else
return null;
}
}
public Response(HttpChannel channel, HttpOutput out)
{
_channel = channel;

View File

@ -0,0 +1,378 @@
//
// ========================================================================
// 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.ee9.nested;
import java.io.IOException;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.TimeoutException;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import jakarta.servlet.DispatcherType;
import jakarta.servlet.RequestDispatcher;
import jakarta.servlet.http.HttpServletRequest;
import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpURI;
import org.eclipse.jetty.io.Content;
import org.eclipse.jetty.io.content.InputStreamContentSource;
import org.eclipse.jetty.server.Components;
import org.eclipse.jetty.server.ConnectionMetaData;
import org.eclipse.jetty.server.Context;
import org.eclipse.jetty.server.HttpStream;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Session;
import org.eclipse.jetty.server.TunnelSupport;
import org.eclipse.jetty.util.Attributes;
import org.eclipse.jetty.util.ExceptionUtil;
import org.eclipse.jetty.util.URIUtil;
import static org.eclipse.jetty.util.URIUtil.addEncodedPaths;
import static org.eclipse.jetty.util.URIUtil.encodePath;
/**
* Wrap a {@link jakarta.servlet.ServletRequest} as a core {@link Request}.
* <p>
* Whilst similar to a {@link Wrapper}, this class is not a {@code Wrapper}
* as callers should not be able to access {@link Wrapper#getWrapped()} and bypass
* the {@link jakarta.servlet.ServletRequest}.
* </p>
* <p>
* The current implementation does not support any read operations.
* </p>
*/
public class ServletCoreRequest implements Request
{
public Request wrap(HttpServletRequest httpServletRequest)
{
org.eclipse.jetty.ee9.nested.Request baseRequest = Objects.requireNonNull(org.eclipse.jetty.ee9.nested.Request.getBaseRequest(httpServletRequest));
ContextHandler.CoreContextRequest coreContextRequest = baseRequest.getCoreRequest();
return new ServletCoreRequest(coreContextRequest, httpServletRequest, coreContextRequest);
}
private final HttpServletRequest _servletRequest;
private final ContextHandler.CoreContextRequest _coreContextRequest;
private final HttpFields _httpFields;
private final HttpURI _uri;
private final Attributes _attributes;
private final boolean _wrapped;
private Content.Source _source;
ServletCoreRequest(ContextHandler.CoreContextRequest coreContextRequest, HttpServletRequest request, Attributes attributes)
{
_servletRequest = request;
_wrapped = !(request instanceof Request);
_coreContextRequest = coreContextRequest;
_attributes = attributes;
HttpFields.Mutable fields = HttpFields.build();
Enumeration<String> headerNames = request.getHeaderNames();
while (headerNames.hasMoreElements())
{
String headerName = headerNames.nextElement();
Enumeration<String> headerValues = request.getHeaders(headerName);
while (headerValues.hasMoreElements())
{
String headerValue = headerValues.nextElement();
fields.add(new HttpField(headerName, headerValue));
}
}
_httpFields = fields.asImmutable();
String includedServletPath = (String)request.getAttribute(RequestDispatcher.INCLUDE_SERVLET_PATH);
boolean included = includedServletPath != null;
HttpURI.Mutable builder = HttpURI.build();
builder.scheme(request.getScheme())
.authority(request.getServerName(), request.getServerPort());
// TODO if (included)
// builder.path(addEncodedPaths(request.getContextPath(), encodePath(DefaultServlet.getIncludedPathInContext(request, includedServletPath, false))));
// else
if (request.getDispatcherType() != DispatcherType.REQUEST)
builder.path(addEncodedPaths(request.getContextPath(), encodePath(URIUtil.addPaths(_servletRequest.getServletPath(), _servletRequest.getPathInfo()))));
else
builder.path(request.getRequestURI());
builder.query(request.getQueryString());
_uri = builder.asImmutable();
_source = _wrapped ? null : _coreContextRequest;
}
private Content.Source source() throws IOException
{
if (_source == null)
_source = _wrapped ? new InputStreamContentSource(getServletRequest().getInputStream()) : _coreContextRequest;
return _source;
}
@Override
public HttpFields getHeaders()
{
return _httpFields;
}
@Override
public HttpURI getHttpURI()
{
return _uri;
}
@Override
public String getId()
{
return _coreContextRequest.getId();
}
@Override
public String getMethod()
{
return _servletRequest.getMethod();
}
public HttpServletRequest getServletRequest()
{
return _servletRequest;
}
@Override
public boolean isSecure()
{
return _servletRequest.isSecure();
}
@Override
public Object removeAttribute(String name)
{
return _attributes.removeAttribute(name);
}
@Override
public Object setAttribute(String name, Object attribute)
{
return _attributes.setAttribute(name, attribute);
}
@Override
public Object getAttribute(String name)
{
return _attributes.getAttribute(name);
}
@Override
public Set<String> getAttributeNameSet()
{
return _attributes.getAttributeNameSet();
}
@Override
public void clearAttributes()
{
_attributes.clearAttributes();
}
@Override
public void fail(Throwable failure)
{
try
{
source().fail(failure);
}
catch (Throwable t)
{
ExceptionUtil.addSuppressedIfNotAssociated(failure, t);
}
}
@Override
public Components getComponents()
{
return _coreContextRequest.getComponents();
}
@Override
public ConnectionMetaData getConnectionMetaData()
{
return _coreContextRequest.getConnectionMetaData();
}
@Override
public Context getContext()
{
return _coreContextRequest.getContext();
}
@Override
public void demand(Runnable demandCallback)
{
try
{
source().demand(demandCallback);
}
catch (Throwable t)
{
demandCallback.run();
}
}
@Override
public HttpFields getTrailers()
{
return _coreContextRequest.getTrailers();
}
@Override
public long getBeginNanoTime()
{
return _coreContextRequest.getBeginNanoTime();
}
@Override
public long getHeadersNanoTime()
{
return _coreContextRequest.getHeadersNanoTime();
}
@Override
public Content.Chunk read()
{
try
{
return source().read();
}
catch (Throwable t)
{
return Content.Chunk.from(t, true);
}
}
@Override
public boolean consumeAvailable()
{
if (_wrapped)
{
try
{
Content.Source.consumeAll(source());
return true;
}
catch (IOException e)
{
return false;
}
}
else
{
return _coreContextRequest.consumeAvailable();
}
}
@Override
public void addIdleTimeoutListener(Predicate<TimeoutException> onIdleTimeout)
{
_coreContextRequest.addIdleTimeoutListener(onIdleTimeout);
}
@Override
public void addFailureListener(Consumer<Throwable> onFailure)
{
_coreContextRequest.addFailureListener(onFailure);
}
@Override
public TunnelSupport getTunnelSupport()
{
return null;
}
@Override
public void addHttpStreamWrapper(Function<HttpStream, HttpStream> wrapper)
{
_coreContextRequest.addHttpStreamWrapper(wrapper);
}
@Override
public Session getSession(boolean create)
{
return Session.getSession(_servletRequest.getSession(create));
}
public static class ServletAttributes implements Attributes
{
private final HttpServletRequest _servletRequest;
private Set<String> _attributeNames;
public ServletAttributes(HttpServletRequest httpServletRequest)
{
_servletRequest = httpServletRequest;
}
@Override
public Object removeAttribute(String name)
{
Object value = _servletRequest.getAttribute(name);
if (value != null)
_attributeNames = null;
_servletRequest.removeAttribute(name);
return value;
}
@Override
public Object setAttribute(String name, Object attribute)
{
Object value = _servletRequest.getAttribute(name);
if (value == null)
_attributeNames = null;
_servletRequest.setAttribute(name, attribute);
return value;
}
@Override
public Object getAttribute(String name)
{
return _servletRequest.getAttribute(name);
}
@Override
public Set<String> getAttributeNameSet()
{
Set<String> set = _attributeNames;
if (set == null)
{
set = new HashSet<>();
Enumeration<String> e = _servletRequest.getAttributeNames();
while (e.hasMoreElements())
set.add(e.nextElement());
_attributeNames = set;
}
return set;
}
@Override
public void clearAttributes()
{
Enumeration<String> e = _servletRequest.getAttributeNames();
_attributeNames = null;
while (e.hasMoreElements())
{
_servletRequest.removeAttribute(e.nextElement());
}
}
}
}

View File

@ -0,0 +1,407 @@
//
// ========================================================================
// 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.ee9.nested;
import java.io.InputStreamReader;
import java.nio.ByteBuffer;
import java.util.EnumSet;
import java.util.List;
import java.util.ListIterator;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import jakarta.servlet.http.HttpServletResponse;
import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpHeaderValue;
import org.eclipse.jetty.io.ByteBufferInputStream;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.IO;
/**
* A {@link HttpServletResponse} wrapped as a core {@link Response}.
* All write operations are internally converted to blocking writes on the servlet API.
*/
public class ServletCoreResponse implements org.eclipse.jetty.server.Response
{
public static org.eclipse.jetty.server.Response wrap(org.eclipse.jetty.server.Request coreRequest, HttpServletResponse httpServletResponse, boolean included)
{
Response baseResponse = Objects.requireNonNull(Response.getBaseResponse(httpServletResponse));
return new ServletCoreResponse(coreRequest, httpServletResponse, baseResponse, baseResponse.getHttpChannel().getCoreResponse(), included);
}
private final HttpServletResponse _httpServletResponse;
private final org.eclipse.jetty.server.Request _coreRequest;
private final HttpFields.Mutable _httpFields;
private final boolean _included;
private final Response _baseResponse;
private final org.eclipse.jetty.server.Response _coreResponse;
private final boolean _wrapped;
ServletCoreResponse(org.eclipse.jetty.server.Request coreRequest, HttpServletResponse httpServletResponse, Response baseResponse, org.eclipse.jetty.server.Response coreResponse, boolean included)
{
_coreRequest = coreRequest;
_httpServletResponse = httpServletResponse;
_baseResponse = baseResponse;
_coreResponse = coreResponse;
_wrapped = !(httpServletResponse instanceof Response);
HttpFields.Mutable fields = new HttpServletResponseHttpFields(httpServletResponse);
if (included)
{
// If included, accept but ignore mutations.
fields = new HttpFields.Mutable.Wrapper(fields)
{
@Override
public HttpField onAddField(HttpField field)
{
/* TODO
String name = field == null ? null : field.getName();
if (!StringUtil.isBlank(name) && name.startsWith(Dispatcher.JETTY_INCLUDE_HEADER_PREFIX))
{
return new HttpField(name.substring(Dispatcher.JETTY_INCLUDE_HEADER_PREFIX.length()), field.getValue());
}*/
return null;
}
@Override
public boolean onRemoveField(HttpField field)
{
return false;
}
@Override
public HttpField onReplaceField(HttpField oldField, HttpField newField)
{
return oldField;
}
};
}
_httpFields = fields;
_included = included;
}
@Override
public HttpFields.Mutable getHeaders()
{
return _httpFields;
}
public HttpServletResponse getServletResponse()
{
return _httpServletResponse;
}
@Override
public boolean hasLastWrite()
{
return _coreResponse.hasLastWrite();
}
@Override
public boolean isCompletedSuccessfully()
{
return _coreResponse.isCompletedSuccessfully();
}
@Override
public boolean isCommitted()
{
return _httpServletResponse.isCommitted();
}
private boolean isWriting()
{
return _baseResponse.isWriting();
}
@Override
public void write(boolean last, ByteBuffer byteBuffer, Callback callback)
{
if (_included)
last = false;
try
{
if (!_wrapped && !_baseResponse.isWritingOrStreaming())
{
_coreResponse.write(last, byteBuffer, callback);
}
else
{
if (BufferUtil.hasContent(byteBuffer))
{
if (isWriting())
{
String characterEncoding = _httpServletResponse.getCharacterEncoding();
try (ByteBufferInputStream bbis = new ByteBufferInputStream(byteBuffer);
InputStreamReader reader = new InputStreamReader(bbis, characterEncoding))
{
IO.copy(reader, _httpServletResponse.getWriter());
}
if (last)
_httpServletResponse.getWriter().close();
}
else
{
BufferUtil.writeTo(byteBuffer, _httpServletResponse.getOutputStream());
if (last)
_httpServletResponse.getOutputStream().close();
}
}
callback.succeeded();
}
}
catch (Throwable t)
{
callback.failed(t);
}
}
@Override
public org.eclipse.jetty.server.Request getRequest()
{
return _coreRequest;
}
@Override
public int getStatus()
{
return _httpServletResponse.getStatus();
}
@Override
public void setStatus(int code)
{
if (_included)
return;
_httpServletResponse.setStatus(code);
}
@Override
public Supplier<HttpFields> getTrailersSupplier()
{
return null;
}
@Override
public void setTrailersSupplier(Supplier<HttpFields> trailers)
{
}
@Override
public void reset()
{
_httpServletResponse.reset();
}
@Override
public CompletableFuture<Void> writeInterim(int status, HttpFields headers)
{
return _coreResponse.writeInterim(status, headers);
}
@Override
public String toString()
{
return "%s@%x{%s,%s}".formatted(this.getClass().getSimpleName(), hashCode(), this._coreRequest, _httpServletResponse);
}
private static class HttpServletResponseHttpFields implements HttpFields.Mutable
{
private final HttpServletResponse _response;
private HttpServletResponseHttpFields(HttpServletResponse response)
{
_response = response;
}
@Override
public ListIterator<HttpField> listIterator(int index)
{
// The minimum requirement is to implement the listIterator, but it is inefficient.
// Other methods are implemented for efficiency.
final ListIterator<HttpField> list = _response.getHeaderNames().stream()
.map(n -> new HttpField(n, _response.getHeader(n)))
.collect(Collectors.toList())
.listIterator(index);
return new ListIterator<>()
{
HttpField _last;
@Override
public boolean hasNext()
{
return list.hasNext();
}
@Override
public HttpField next()
{
return _last = list.next();
}
@Override
public boolean hasPrevious()
{
return list.hasPrevious();
}
@Override
public HttpField previous()
{
return _last = list.previous();
}
@Override
public int nextIndex()
{
return list.nextIndex();
}
@Override
public int previousIndex()
{
return list.previousIndex();
}
@Override
public void remove()
{
if (_last != null)
{
// This is not exactly the right semantic for repeated field names
list.remove();
_response.setHeader(_last.getName(), null);
}
}
@Override
public void set(HttpField httpField)
{
list.set(httpField);
_response.setHeader(httpField.getName(), httpField.getValue());
}
@Override
public void add(HttpField httpField)
{
list.add(httpField);
_response.addHeader(httpField.getName(), httpField.getValue());
}
};
}
@Override
public Mutable add(String name, String value)
{
_response.addHeader(name, value);
return this;
}
@Override
public Mutable add(HttpHeader header, HttpHeaderValue value)
{
_response.addHeader(header.asString(), value.asString());
return this;
}
@Override
public Mutable add(HttpHeader header, String value)
{
_response.addHeader(header.asString(), value);
return this;
}
@Override
public Mutable add(HttpField field)
{
_response.addHeader(field.getName(), field.getValue());
return this;
}
@Override
public Mutable put(HttpField field)
{
_response.setHeader(field.getName(), field.getValue());
return this;
}
@Override
public Mutable put(String name, String value)
{
_response.setHeader(name, value);
return this;
}
@Override
public Mutable put(HttpHeader header, HttpHeaderValue value)
{
_response.setHeader(header.asString(), value.asString());
return this;
}
@Override
public Mutable put(HttpHeader header, String value)
{
_response.setHeader(header.asString(), value);
return this;
}
@Override
public Mutable put(String name, List<String> list)
{
Objects.requireNonNull(name);
Objects.requireNonNull(list);
boolean first = true;
for (String s : list)
{
if (first)
_response.setHeader(name, s);
else
_response.addHeader(name, s);
first = false;
}
return this;
}
@Override
public Mutable remove(HttpHeader header)
{
_response.setHeader(header.asString(), null);
return this;
}
@Override
public Mutable remove(EnumSet<HttpHeader> fields)
{
for (HttpHeader header : fields)
remove(header);
return this;
}
@Override
public Mutable remove(String name)
{
_response.setHeader(name, null);
return this;
}
}
}

View File

@ -13,6 +13,9 @@
package org.eclipse.jetty.ee9.nested;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import jakarta.servlet.http.HttpServletMapping;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.MappingMatch;
@ -113,7 +116,9 @@ public class ServletPathMapping implements HttpServletMapping
_servletPath = pathSpec.getPrefix();
// TODO avoid the substring on the known servletPath!
_matchValue = _servletPath.startsWith("/") ? _servletPath.substring(1) : _servletPath;
_pathInfo = matchedPath != null ? matchedPath.getPathInfo() : null;
_pathInfo = matchedPath != null
? matchedPath.getPathInfo()
: _servletPath.length() == pathInContext.length() ? null : pathInContext.substring(_servletPath.length());
break;
case SUFFIX_GLOB:
@ -135,6 +140,16 @@ public class ServletPathMapping implements HttpServletMapping
this(pathSpec, servletName, pathInContext, null);
}
private ServletPathMapping(MappingMatch mappingMatch, String matchValue, String pattern, String servletName, String servletPath, String pathInfo)
{
_mappingMatch = mappingMatch;
_matchValue = matchValue;
_pattern = pattern;
_servletName = servletName;
_servletPath = servletPath;
_pathInfo = pathInfo;
}
@Override
public String getMatchValue()
{
@ -173,12 +188,46 @@ public class ServletPathMapping implements HttpServletMapping
public String toString()
{
return "ServletPathMapping{" +
"matchValue=" + _matchValue +
"mappingMatch=" + _mappingMatch +
", matchValue=" + _matchValue +
", pattern=" + _pattern +
", servletName=" + _servletName +
", mappingMatch=" + _mappingMatch +
", servletPath=" + _servletPath +
", pathInfo=" + _pathInfo +
"}";
}
private static final Pattern DESERIALIZE = Pattern.compile("ServletPathMapping\\{" +
"mappingMatch=(?<mappingMatch>[^,]+), " +
"matchValue=(?<matchValue>[^,]+), " +
"pattern=(?<pattern>[^,]+), " +
"servletName=(?<servletName>[^,]+), " +
"servletPath=(?<servletPath>[^,]+), " +
"pathInfo=(?<pathInfo>[^}]+)}");
/**
* Obtain a {@link ServletPathMapping} instance from an object which may be an instance of a mapping
* from a different EE version obtained from a cross context cross environment dispatch
* @param o The object to caste or deserialize from the string representation.
* @return A ServletPathMapping
*/
public static ServletPathMapping from(Object o)
{
if (o == null)
return null;
if (o instanceof ServletPathMapping mapping)
return mapping;
Matcher matcher = DESERIALIZE.matcher(o.toString());
if (matcher.find())
return new ServletPathMapping(
MappingMatch.valueOf(matcher.group("mappingMatch")),
matcher.group("matchValue"),
matcher.group("pattern"),
matcher.group("servletName"),
matcher.group("servletPath"),
matcher.group("pathInfo")
);
return null;
}
}

View File

@ -43,7 +43,7 @@ import org.eclipse.jetty.security.UserIdentity;
import org.eclipse.jetty.security.openid.OpenIdConfiguration;
import org.eclipse.jetty.security.openid.OpenIdCredentials;
import org.eclipse.jetty.security.openid.OpenIdLoginService;
import org.eclipse.jetty.util.MultiMap;
import org.eclipse.jetty.util.Fields;
import org.eclipse.jetty.util.URIUtil;
import org.eclipse.jetty.util.UrlEncoded;
import org.slf4j.Logger;
@ -512,12 +512,12 @@ public class OpenIdAuthenticator extends LoginAuthenticator
if (jUri.equals(buf.toString()))
{
@SuppressWarnings("unchecked")
MultiMap<String> jPost = (MultiMap<String>)session.getAttribute(J_POST);
Fields jPost = (Fields)session.getAttribute(J_POST);
if (jPost != null)
{
if (LOG.isDebugEnabled())
LOG.debug("auth rePOST {}->{}", authentication, jUri);
baseRequest.setContentParameters(jPost);
baseRequest.setContentFields(jPost);
}
session.removeAttribute(J_URI);
session.removeAttribute(J_METHOD);
@ -700,7 +700,7 @@ public class OpenIdAuthenticator extends LoginAuthenticator
private final String _uri;
private final String _method;
private final MultiMap<String> _formParameters;
private final Fields _formParameters;
public UriRedirectInfo(Request request)
{
@ -709,7 +709,7 @@ public class OpenIdAuthenticator extends LoginAuthenticator
if (MimeTypes.Type.FORM_ENCODED.is(request.getContentType()) && HttpMethod.POST.is(request.getMethod()))
{
MultiMap<String> formParameters = new MultiMap<>();
Fields formParameters = new Fields(true);
request.extractFormParameters(formParameters);
_formParameters = formParameters;
}
@ -729,7 +729,7 @@ public class OpenIdAuthenticator extends LoginAuthenticator
return _method;
}
public MultiMap<String> getFormParameters()
public Fields getFormParameters()
{
return _formParameters;
}

View File

@ -38,7 +38,7 @@ import org.eclipse.jetty.http.HttpHeaderValue;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.MimeTypes;
import org.eclipse.jetty.security.UserIdentity;
import org.eclipse.jetty.util.MultiMap;
import org.eclipse.jetty.util.Fields;
import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.util.URIUtil;
import org.slf4j.Logger;
@ -340,11 +340,11 @@ public class FormAuthenticator extends LoginAuthenticator
if (jUri.equals(buf.toString()))
{
MultiMap<String> jPost = (MultiMap<String>)session.getAttribute(__J_POST);
Fields jPost = (Fields)session.getAttribute(__J_POST);
if (jPost != null)
{
LOG.debug("auth rePOST {}->{}", authentication, jUri);
baseRequest.setContentParameters(jPost);
baseRequest.setContentFields(jPost);
}
session.removeAttribute(__J_URI);
session.removeAttribute(__J_METHOD);
@ -379,7 +379,7 @@ public class FormAuthenticator extends LoginAuthenticator
if (MimeTypes.Type.FORM_ENCODED.is(req.getContentType()) && HttpMethod.POST.is(request.getMethod()))
{
MultiMap<String> formParameters = new MultiMap<>();
Fields formParameters = new Fields(true);
baseRequest.extractFormParameters(formParameters);
session.setAttribute(__J_POST, formParameters);
}

View File

@ -41,9 +41,7 @@ import jakarta.servlet.ServletContext;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletOutputStream;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletRequestWrapper;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.ServletResponseWrapper;
import jakarta.servlet.WriteListener;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletMapping;
@ -314,7 +312,6 @@ public class DispatcherTest
}
@Test
@Disabled // TODO
public void testForwardWithParam() throws Exception
{
createDefaultContextHandlerCollection();
@ -841,7 +838,6 @@ public class DispatcherTest
}
@Test
@Disabled // TODO
public void testDispatchMapping() throws Exception
{
createDefaultContextHandlerCollection();

View File

@ -299,8 +299,6 @@ public class CreationTest
* session in it too. Check that both sessions exist after the response
* completes.
*/
//TODO - no cross context support in jetty-12
@Disabled
@Test
public void testSessionCreateForward() throws Exception
{
@ -317,8 +315,10 @@ public class CreationTest
TestServlet servlet = new TestServlet();
ServletHolder holder = new ServletHolder(servlet);
ServletContextHandler contextHandler = server1.addContext(contextPath);
contextHandler.setCrossContextDispatchSupported(true);
contextHandler.addServlet(holder, servletMapping);
ServletContextHandler ctxB = server1.addContext(contextB);
ctxB.setCrossContextDispatchSupported(true);
ctxB.addServlet(TestServletB.class, servletMapping);
server1.start();
int port1 = server1.getPort();
@ -348,8 +348,6 @@ public class CreationTest
* in it, then invalidate the session in the original context: that should invalidate the
* session in both contexts and no session should exist after the response completes.
*/
//TODO no cross context dispatch in jetty-12
@Disabled
@Test
public void testSessionCreateForwardAndInvalidate() throws Exception
{
@ -367,8 +365,10 @@ public class CreationTest
TestServlet servlet = new TestServlet();
ServletHolder holder = new ServletHolder(servlet);
ServletContextHandler contextHandler = server1.addContext(contextPath);
contextHandler.setCrossContextDispatchSupported(true);
contextHandler.addServlet(holder, servletMapping);
ServletContextHandler ctxB = server1.addContext(contextB);
ctxB.setCrossContextDispatchSupported(true);
ctxB.addServlet(TestServletB.class, servletMapping);
server1.start();
int port1 = server1.getPort();