Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reintroduce Cross Context Dispatch in Jetty 12 #11451

Merged
merged 61 commits into from
Mar 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
093d735
Begin impl of cross context dispatch
janbartel Feb 21, 2024
986d7b5
Use Synthetic attributes for ServletCoreRequest
gregw Feb 21, 2024
e683e75
Implemented Content.Source methods
gregw Feb 21, 2024
6ebeff1
fix javadoc
gregw Feb 21, 2024
17814f1
Undisable cross context test with session
janbartel Feb 21, 2024
ef73856
response updates
gregw Feb 21, 2024
997810c
Add test of parameters during forward
janbartel Feb 21, 2024
fba75dd
Skip security check; return DispatcherType.FORWARD; test params
janbartel Feb 22, 2024
1ad2745
Made cross context dispatch support optional
gregw Feb 22, 2024
605bcaf
Improved parameter efficiency
gregw Feb 22, 2024
b02ac5c
Split tests
gregw Feb 22, 2024
93f3e12
Merge branch 'jetty-12.0.x' into jetty-12.0.x-cross-context-dispatch
gregw Feb 25, 2024
6b2953c
started on ee9 cross context
gregw Feb 25, 2024
9a5944d
WIP
janbartel Feb 25, 2024
7e9b67b
WIP
janbartel Feb 25, 2024
d284a1e
WIP First hint include dispatch works
janbartel Feb 25, 2024
5fcd3cc
started on ee9 cross context
gregw Feb 26, 2024
a02ce1a
started on ee9 cross context
gregw Feb 26, 2024
d4fc7d1
started on ee9 cross context
gregw Feb 26, 2024
3db3f48
WIP impmement query/content params cross context
janbartel Feb 26, 2024
c781a1d
cleanup normal dispatch parameters
gregw Feb 26, 2024
af09903
Removed additional content Parameter extracted boolean
gregw Feb 27, 2024
67cca3c
Re-enable more cross context tests
janbartel Feb 27, 2024
1e65324
Reworked ee9 to use Fields for parameters instead of MultiMap
gregw Feb 27, 2024
0796ec9
optimized Field construction
gregw Feb 27, 2024
bb51460
Port ee10 cross context tests to ee9
janbartel Feb 27, 2024
e87f80d
Merge branch 'jetty-12.0.x' into jetty-12.0.x-cross-context-dispatch
gregw Feb 27, 2024
66cb5d5
WIP
janbartel Feb 27, 2024
c2cc24a
renames
gregw Feb 27, 2024
b2998f7
decode cross context query for forwards
gregw Feb 27, 2024
c78548c
decode cross context query for forwards
gregw Feb 27, 2024
809d5a8
WIP
janbartel Feb 28, 2024
eef828c
serialize servlet mapping for cross environment dispatch
gregw Feb 28, 2024
dcfb594
progress on ee9 include
gregw Feb 28, 2024
96af3a1
Make include work
janbartel Feb 28, 2024
81dc1fc
updated test utils
gregw Feb 28, 2024
53ff79a
wip on session tests
gregw Feb 28, 2024
06d564a
updates from review
gregw Feb 28, 2024
cc46668
don't cross context dispatch to self
gregw Feb 28, 2024
fcb0df4
don't cross context dispatch to self
gregw Feb 28, 2024
0f13e9e
Made core servlet request/response wrappers public
gregw Feb 29, 2024
86821e6
Reenable ee9 cross context session tests
janbartel Feb 29, 2024
e68650c
Tidy up test
janbartel Feb 29, 2024
d299449
WIP start support for cross-environment dispatch
janbartel Mar 1, 2024
c7a97fd
Merge remote-tracking branch 'origin/jetty-12.0.x' into jetty-12.0.x-…
janbartel Mar 1, 2024
5dc91eb
Merge branch 'jetty-12.0.x' into jetty-12.0.x-cross-context-dispatch
gregw Mar 2, 2024
4d1eded
Cross Environment Cookies
gregw Mar 2, 2024
ed5fe37
Cross Environment Cookies
gregw Mar 2, 2024
1419869
Better comments
gregw Mar 3, 2024
68198ba
Deprecated old CookieCache
gregw Mar 3, 2024
ba2fbd9
Field optimizations
gregw Mar 3, 2024
f131055
Field optimizations
gregw Mar 3, 2024
1f3ea98
WIP make ee9 cross context multipart work
janbartel Mar 3, 2024
ce5cc32
Fixed fields constructor
gregw Mar 3, 2024
6205b52
support of skipping translation ee9 ee8 per block
olamy Mar 4, 2024
3889168
make it work with translate tool
olamy Mar 5, 2024
492ed65
Test cross context multipart ee10->ee10
janbartel Mar 5, 2024
3cb1cff
Test ee9/10 request constants in ee8; test ee8 constants in ee9
janbartel Mar 5, 2024
b5bc8ab
jetty modify sources plugin 10.0.10
olamy Mar 6, 2024
f66e76b
Merge remote-tracking branch 'origin/jetty-12.0.x' into jetty-12.0.x-…
janbartel Mar 25, 2024
cf2bfad
Remove debug println
janbartel Mar 25, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,9 @@

/**
* 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);
Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -474,6 +474,10 @@ public boolean handle(Request request, Response response, Callback callback) thr
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())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,29 @@ default String getPathInContext(String canonicallyEncodedPath)
*/
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
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,8 @@ public static CompletableFuture<Fields> get(Request request)
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;
}

Expand Down Expand Up @@ -221,7 +223,7 @@ private FormFields(Content.Source source, Charset charset, int maxFields, int ma
_maxFields = maxFields;
_maxLength = maxSize;
_builder = CharsetStringBuilder.forCharset(charset);
_fields = new Fields();
_fields = new Fields(true);
}

@Override
Expand Down
Loading
Loading