Merge remote-tracking branch 'origin/jetty-10.0.x' into jetty-10.0.x-1743-refactor-maven-plugin-redux

This commit is contained in:
Jan Bartel 2019-11-18 10:05:50 +11:00
commit f2c0d86de5
143 changed files with 3655 additions and 1620 deletions

View File

@ -0,0 +1,18 @@
---
name: Issue
about: Reporting bugs and problems in Eclipse Jetty
title: ''
assignees: ''
---
**Jetty version**
**Java version**
**OS type/version**
**Description**

View File

@ -0,0 +1,16 @@
---
name: Question
about: Asking questions about Eclipse Jetty
title: ''
labels: Question
assignees: ''
---
**Jetty version**
**Java version**
**Question**

20
.github/stale.yml vendored Normal file
View File

@ -0,0 +1,20 @@
# Number of days of inactivity before an issue becomes stale
daysUntilStale: 365
# Number of days of inactivity before a stale issue is closed
daysUntilClose: 30
# Issues with these labels will never be considered stale
exemptLabels:
- Pinned
- Security
- Specification
- TCK
# Label to use when marking an issue as stale
staleLabel: Stale
# Comment to post when marking an issue as stale. Set to `false` to disable
markComment: >
This issue has been automatically marked as stale because it has been a
full year without activit. It will be closed if no further activity occurs.
Thank you for your contributions.
# Comment to post when closing a stale issue. Set to `false` to disable
closeComment: >
This issue has been closed due to it having no activity.

View File

@ -55,13 +55,13 @@ public class JDK9ClientALPNProcessor implements ALPNProcessor.Client
ALPNClientConnection alpn = (ALPNClientConnection)connection;
SSLParameters sslParameters = sslEngine.getSSLParameters();
List<String> protocols = alpn.getProtocols();
sslParameters.setApplicationProtocols(protocols.toArray(new String[protocols.size()]));
sslParameters.setApplicationProtocols(protocols.toArray(new String[0]));
sslEngine.setSSLParameters(sslParameters);
((DecryptedEndPoint)connection.getEndPoint()).getSslConnection()
.addHandshakeListener(new ALPNListener(alpn));
}
private final class ALPNListener implements SslHandshakeListener
private static final class ALPNListener implements SslHandshakeListener
{
private final ALPNClientConnection alpnConnection;

View File

@ -55,7 +55,7 @@ public class JDK9ServerALPNProcessor implements ALPNProcessor.Server, SslHandsha
sslEngine.setHandshakeApplicationProtocolSelector(new ALPNCallback((ALPNServerConnection)connection));
}
private final class ALPNCallback implements BiFunction<SSLEngine, List<String>, String>, SslHandshakeListener
private static final class ALPNCallback implements BiFunction<SSLEngine, List<String>, String>, SslHandshakeListener
{
private final ALPNServerConnection alpnConnection;
@ -68,10 +68,19 @@ public class JDK9ServerALPNProcessor implements ALPNProcessor.Server, SslHandsha
@Override
public String apply(SSLEngine engine, List<String> protocols)
{
if (LOG.isDebugEnabled())
LOG.debug("apply {} {}", alpnConnection, protocols);
alpnConnection.select(protocols);
return alpnConnection.getProtocol();
try
{
if (LOG.isDebugEnabled())
LOG.debug("apply {} {}", alpnConnection, protocols);
alpnConnection.select(protocols);
return alpnConnection.getProtocol();
}
catch (Throwable x)
{
// Cannot negotiate the protocol, return null to have
// JSSE send Alert.NO_APPLICATION_PROTOCOL to the client.
return null;
}
}
@Override

View File

@ -19,15 +19,18 @@
package org.eclipse.jetty.alpn.java.server;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLEngineResult;
import javax.net.ssl.SSLParameters;
import javax.net.ssl.SSLSocket;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@ -40,12 +43,16 @@ import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.handler.AbstractHandler;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.greaterThan;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertSame;
public class JDK9ALPNTest
{
@ -90,7 +97,7 @@ public class JDK9ALPNTest
startServer(new AbstractHandler()
{
@Override
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response)
{
baseRequest.setHandled(true);
}
@ -132,7 +139,7 @@ public class JDK9ALPNTest
startServer(new AbstractHandler()
{
@Override
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response)
{
baseRequest.setHandled(true);
}
@ -170,4 +177,57 @@ public class JDK9ALPNTest
}
}
}
@Test
public void testClientSupportingALPNCannotNegotiateProtocol() throws Exception
{
startServer(new AbstractHandler() {
@Override
public void handle(String target, Request jettyRequest, HttpServletRequest request, HttpServletResponse response)
{
jettyRequest.setHandled(true);
}
});
SslContextFactory sslContextFactory = new SslContextFactory.Client(true);
sslContextFactory.start();
String host = "localhost";
int port = connector.getLocalPort();
try (SocketChannel client = SocketChannel.open(new InetSocketAddress(host, port)))
{
client.socket().setSoTimeout(5000);
SSLEngine sslEngine = sslContextFactory.newSSLEngine(host, port);
sslEngine.setUseClientMode(true);
SSLParameters sslParameters = sslEngine.getSSLParameters();
sslParameters.setApplicationProtocols(new String[]{"unknown/1.0"});
sslEngine.setSSLParameters(sslParameters);
sslEngine.beginHandshake();
assertSame(SSLEngineResult.HandshakeStatus.NEED_WRAP, sslEngine.getHandshakeStatus());
ByteBuffer sslBuffer = ByteBuffer.allocate(sslEngine.getSession().getPacketBufferSize());
SSLEngineResult result = sslEngine.wrap(BufferUtil.EMPTY_BUFFER, sslBuffer);
assertSame(SSLEngineResult.Status.OK, result.getStatus());
sslBuffer.flip();
client.write(sslBuffer);
assertSame(SSLEngineResult.HandshakeStatus.NEED_UNWRAP, sslEngine.getHandshakeStatus());
sslBuffer.clear();
int read = client.read(sslBuffer);
assertThat(read, greaterThan(0));
sslBuffer.flip();
// TLS frame layout: record_type, major_version, minor_version, hi_length, lo_length
int recordTypeAlert = 21;
assertEquals(recordTypeAlert, sslBuffer.get(0) & 0xFF);
// Alert record layout: alert_level, alert_code
int alertLevelFatal = 2;
assertEquals(alertLevelFatal, sslBuffer.get(5) & 0xFF);
int alertCodeNoApplicationProtocol = 120;
assertEquals(alertCodeNoApplicationProtocol, sslBuffer.get(6) & 0xFF);
}
}
}

View File

@ -1,4 +1,4 @@
DO NOT EDIT - See: https://www.eclipse.org/jetty/documentation/current/startup-modules.html
[depend]
alpn-impl/alpn-9
alpn-impl/alpn-11

View File

@ -1,4 +1,4 @@
DO NOT EDIT - See: https://www.eclipse.org/jetty/documentation/current/startup-modules.html
[depend]
alpn-impl/alpn-9
alpn-impl/alpn-11

View File

@ -481,7 +481,7 @@ public class AnnotationConfiguration extends AbstractConfiguration
boolean timeout = !latch.await(getMaxScanWait(context), TimeUnit.SECONDS);
long elapsedMs = TimeUnit.MILLISECONDS.convert(System.nanoTime() - start, TimeUnit.NANOSECONDS);
LOG.info("Scanning elapsed time={}ms", elapsedMs);
LOG.info("Annotation scanning elapsed time={}ms", elapsedMs);
if (LOG.isDebugEnabled())
{

View File

@ -27,7 +27,6 @@ import java.security.CodeSource;
import java.security.PermissionCollection;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.EventListener;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
@ -615,9 +614,9 @@ public class AntWebAppContext extends WebAppContext
TaskLog.logWithTimestamp("Stopping web application " + this);
Thread.currentThread().sleep(500L);
super.doStop();
//remove all filters, servlets and listeners. They will be recreated
//either via application of a context xml file or web.xml or annotation or servlet api
setEventListeners(new EventListener[0]);
// remove all filters and servlets. They will be recreated
// either via application of a context xml file or web.xml or annotation or servlet api.
// Event listeners are reset in ContextHandler.doStop()
getServletHandler().setFilters(new FilterHolder[0]);
getServletHandler().setFilterMappings(new FilterMapping[0]);
getServletHandler().setServlets(new ServletHolder[0]);

View File

@ -119,7 +119,7 @@ public abstract class HttpReceiver
}
}
private long demand()
protected long demand()
{
return demand(LongUnaryOperator.identity());
}
@ -381,40 +381,67 @@ public abstract class HttpReceiver
}
}
boolean proceed = true;
if (demand() <= 0)
{
callback.failed(new IllegalStateException("No demand for response content"));
return false;
proceed = false;
}
HttpResponse response = exchange.getResponse();
if (LOG.isDebugEnabled())
LOG.debug("Response content {}{}{}", response, System.lineSeparator(), BufferUtil.toDetailString(buffer));
if (proceed)
{
if (LOG.isDebugEnabled())
LOG.debug("Response content {}{}{}", response, System.lineSeparator(), BufferUtil.toDetailString(buffer));
if (contentListeners.isEmpty())
{
callback.succeeded();
}
else
{
Decoder decoder = this.decoder;
if (decoder == null)
ContentListeners listeners = this.contentListeners;
if (listeners != null)
{
contentListeners.notifyContent(response, buffer, callback);
if (listeners.isEmpty())
{
callback.succeeded();
}
else
{
Decoder decoder = this.decoder;
if (decoder == null)
{
listeners.notifyContent(response, buffer, callback);
}
else
{
try
{
proceed = decoder.decode(buffer, callback);
}
catch (Throwable x)
{
callback.failed(x);
proceed = false;
}
}
}
}
else
{
if (!decoder.decode(buffer, callback))
return false;
// May happen in case of concurrent abort.
proceed = false;
}
}
if (updateResponseState(ResponseState.TRANSIENT, ResponseState.CONTENT))
{
boolean hasDemand = hasDemandOrStall();
if (LOG.isDebugEnabled())
LOG.debug("Response content {}, hasDemand={}", response, hasDemand);
return hasDemand;
if (proceed)
{
boolean hasDemand = hasDemandOrStall();
if (LOG.isDebugEnabled())
LOG.debug("Response content {}, hasDemand={}", response, hasDemand);
return hasDemand;
}
else
{
return false;
}
}
terminateResponse(exchange);
@ -580,6 +607,9 @@ public abstract class HttpReceiver
ResponseNotifier notifier = getHttpDestination().getResponseNotifier();
notifier.notifyFailure(listeners, response, failure);
// We want to deliver the "complete" event as last,
// so we emit it here only if no event handlers are
// executing, otherwise they will emit it.
if (terminate)
{
// Mark atomically the response as terminated, with
@ -758,56 +788,48 @@ public abstract class HttpReceiver
private boolean decode(ByteBuffer encoded, Callback callback)
{
try
this.encoded = encoded;
this.callback = callback;
return decode();
}
private boolean decode()
{
while (true)
{
ByteBuffer buffer;
while (true)
{
ByteBuffer buffer;
while (true)
buffer = decoder.decode(encoded);
if (buffer.hasRemaining())
break;
if (!encoded.hasRemaining())
{
buffer = decoder.decode(encoded);
if (buffer.hasRemaining())
break;
if (!encoded.hasRemaining())
{
callback.succeeded();
return true;
}
}
ByteBuffer decoded = buffer;
if (LOG.isDebugEnabled())
LOG.debug("Response content decoded ({}) {}{}{}", decoder, response, System.lineSeparator(), BufferUtil.toDetailString(decoded));
contentListeners.notifyContent(response, decoded, Callback.from(() -> decoder.release(decoded), callback::failed));
synchronized (this)
{
if (demand() <= 0)
{
this.encoded = encoded;
this.callback = callback;
return false;
}
callback.succeeded();
encoded = null;
callback = null;
return true;
}
}
}
catch (Throwable x)
{
callback.failed(x);
return true;
ByteBuffer decoded = buffer;
if (LOG.isDebugEnabled())
LOG.debug("Response content decoded ({}) {}{}{}", decoder, response, System.lineSeparator(), BufferUtil.toDetailString(decoded));
contentListeners.notifyContent(response, decoded, Callback.from(() -> decoder.release(decoded), callback::failed));
boolean hasDemand = hasDemandOrStall();
if (LOG.isDebugEnabled())
LOG.debug("Response content decoded {}, hasDemand={}", response, hasDemand);
if (!hasDemand)
return false;
}
}
private void resume()
{
ByteBuffer encoded;
Callback callback;
synchronized (this)
{
encoded = this.encoded;
callback = this.callback;
}
if (decode(encoded, callback))
if (LOG.isDebugEnabled())
LOG.debug("Response content resuming decoding {}", response);
if (decode())
receive();
}

View File

@ -552,6 +552,12 @@ public class HttpRequest implements Request
{
this.responseListeners.add(new Response.DemandedContentListener()
{
@Override
public void onBeforeContent(Response response, LongConsumer demand)
{
listener.onBeforeContent(response, demand);
}
@Override
public void onContent(Response response, LongConsumer demand, ByteBuffer content, Callback callback)
{

View File

@ -33,6 +33,7 @@ import org.eclipse.jetty.io.ClientConnector;
import org.eclipse.jetty.io.EndPoint;
import org.eclipse.jetty.util.ProcessorUtils;
import org.eclipse.jetty.util.Promise;
import org.eclipse.jetty.util.annotation.ManagedAttribute;
import org.eclipse.jetty.util.annotation.ManagedObject;
@ManagedObject("The HTTP/1.1 client transport")
@ -40,6 +41,9 @@ public class HttpClientTransportOverHTTP extends AbstractConnectorHttpClientTran
{
public static final HttpDestination.Protocol HTTP11 = new HttpDestination.Protocol(List.of("http/1.1"), false);
private int headerCacheSize = 1024;
private boolean headerCacheCaseSensitive;
public HttpClientTransportOverHTTP()
{
this(Math.max(1, ProcessorUtils.availableProcessors() / 2));
@ -75,7 +79,7 @@ public class HttpClientTransportOverHTTP extends AbstractConnectorHttpClientTran
HttpDestination destination = (HttpDestination)context.get(HTTP_DESTINATION_CONTEXT_KEY);
@SuppressWarnings("unchecked")
Promise<Connection> promise = (Promise<Connection>)context.get(HTTP_CONNECTION_PROMISE_CONTEXT_KEY);
org.eclipse.jetty.io.Connection connection = newHttpConnection(endPoint, destination, promise);
var connection = newHttpConnection(endPoint, destination, promise);
if (LOG.isDebugEnabled())
LOG.debug("Created {}", connection);
return customize(connection, context);
@ -85,4 +89,26 @@ public class HttpClientTransportOverHTTP extends AbstractConnectorHttpClientTran
{
return new HttpConnectionOverHTTP(endPoint, destination, promise);
}
@ManagedAttribute("The maximum allowed size in bytes for an HTTP header field cache")
public int getHeaderCacheSize()
{
return headerCacheSize;
}
public void setHeaderCacheSize(int headerCacheSize)
{
this.headerCacheSize = headerCacheSize;
}
@ManagedAttribute("Whether the header field cache is case sensitive")
public boolean isHeaderCacheCaseSensitive()
{
return headerCacheCaseSensitive;
}
public void setHeaderCacheCaseSensitive(boolean headerCacheCaseSensitive)
{
this.headerCacheCaseSensitive = headerCacheCaseSensitive;
}
}

View File

@ -22,6 +22,7 @@ import java.io.EOFException;
import java.nio.ByteBuffer;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.HttpClientTransport;
import org.eclipse.jetty.client.HttpExchange;
import org.eclipse.jetty.client.HttpReceiver;
import org.eclipse.jetty.client.HttpResponse;
@ -48,7 +49,15 @@ public class HttpReceiverOverHTTP extends HttpReceiver implements HttpParser.Res
public HttpReceiverOverHTTP(HttpChannelOverHTTP channel)
{
super(channel);
parser = new HttpParser(this, -1, channel.getHttpDestination().getHttpClient().getHttpCompliance());
HttpClient httpClient = channel.getHttpDestination().getHttpClient();
parser = new HttpParser(this, -1, httpClient.getHttpCompliance());
HttpClientTransport transport = httpClient.getTransport();
if (transport instanceof HttpClientTransportOverHTTP)
{
HttpClientTransportOverHTTP httpTransport = (HttpClientTransportOverHTTP)transport;
parser.setHeaderCacheSize(httpTransport.getHeaderCacheSize());
parser.setHeaderCacheCaseSensitive(httpTransport.isHeaderCacheCaseSensitive());
}
}
@Override
@ -243,32 +252,18 @@ public class HttpReceiverOverHTTP extends HttpReceiver implements HttpParser.Res
}
@Override
public int getHeaderCacheSize()
{
// TODO get from configuration
return 4096;
}
@Override
public boolean isHeaderCacheCaseSensitive()
{
// TODO get from configuration
return false;
}
@Override
public boolean startResponse(HttpVersion version, int status, String reason)
public void startResponse(HttpVersion version, int status, String reason)
{
HttpExchange exchange = getHttpExchange();
if (exchange == null)
return false;
return;
String method = exchange.getRequest().getMethod();
parser.setHeadResponse(HttpMethod.HEAD.is(method) ||
(HttpMethod.CONNECT.is(method) && status == HttpStatus.OK_200));
exchange.getResponse().version(version).status(status).reason(reason);
return !responseBegin(exchange);
responseBegin(exchange);
}
@Override

View File

@ -39,7 +39,6 @@ public class HttpClientJMXTest
String name = "foo";
HttpClient httpClient = new HttpClient();
httpClient.setName(name);
httpClient.start();
try
{
@ -47,6 +46,7 @@ public class HttpClientJMXTest
MBeanContainer mbeanContainer = new MBeanContainer(mbeanServer);
// Adding MBeanContainer as a bean will trigger the registration of MBeans.
httpClient.addBean(mbeanContainer);
httpClient.start();
String domain = HttpClient.class.getPackage().getName();
ObjectName pattern = new ObjectName(domain + ":type=" + HttpClient.class.getSimpleName().toLowerCase(Locale.ENGLISH) + ",*");

View File

@ -84,7 +84,7 @@
<Set name="responseHeaderSize">8192</Set>
<Set name="sendServerVersion">true</Set>
<Set name="sendDateHeader">false</Set>
<Set name="headerCacheSize">4096</Set>
<Set name="headerCacheSize">1024</Set>
<!-- Uncomment to enable handling of X-Forwarded- style headers
<Call name="addCustomizer">

View File

@ -416,13 +416,13 @@ public class HttpConnectionOverFCGI extends AbstractConnection implements IConne
}
@Override
public void onHeaders(int request)
public boolean onHeaders(int request)
{
HttpChannelOverFCGI channel = activeChannels.get(request);
if (channel != null)
channel.responseHeaders();
else
noChannel(request);
return !channel.responseHeaders();
noChannel(request);
return false;
}
@Override

View File

@ -80,9 +80,9 @@ public class ClientParser extends Parser
}
@Override
public void onHeaders(int request)
public boolean onHeaders(int request)
{
listener.onHeaders(request);
return listener.onHeaders(request);
}
@Override

View File

@ -135,7 +135,11 @@ public abstract class Parser
{
public void onHeader(int request, HttpField field);
public void onHeaders(int request);
/**
* @param request the request id
* @return true to signal to the parser to stop parsing, false to continue parsing
*/
public boolean onHeaders(int request);
/**
* @param request the request id
@ -158,8 +162,9 @@ public abstract class Parser
}
@Override
public void onHeaders(int request)
public boolean onHeaders(int request)
{
return false;
}
@Override

View File

@ -82,7 +82,7 @@ public class ResponseContentParser extends StreamContentParser
parsers.remove(request);
}
private class ResponseParser implements HttpParser.ResponseHandler
private static class ResponseParser implements HttpParser.ResponseHandler
{
private final HttpFields fields = new HttpFields();
private ClientParser.Listener listener;
@ -90,6 +90,7 @@ public class ResponseContentParser extends StreamContentParser
private final FCGIHttpParser httpParser;
private State state = State.HEADERS;
private boolean seenResponseCode;
private boolean stalled;
private ResponseParser(ClientParser.Listener listener, int request)
{
@ -111,7 +112,11 @@ public class ResponseContentParser extends StreamContentParser
case HEADERS:
{
if (httpParser.parseNext(buffer))
{
state = State.CONTENT_MODE;
if (stalled)
return true;
}
remaining = buffer.remaining();
break;
}
@ -151,21 +156,7 @@ public class ResponseContentParser extends StreamContentParser
}
@Override
public int getHeaderCacheSize()
{
// TODO: configure this
return 4096;
}
@Override
public boolean isHeaderCacheCaseSensitive()
{
// TODO get from configuration
return false;
}
@Override
public boolean startResponse(HttpVersion version, int status, String reason)
public void startResponse(HttpVersion version, int status, String reason)
{
// The HTTP request line does not exist in FCGI responses
throw new IllegalStateException();
@ -247,16 +238,17 @@ public class ResponseContentParser extends StreamContentParser
}
}
private void notifyHeaders()
private boolean notifyHeaders()
{
try
{
listener.onHeaders(request);
return listener.onHeaders(request);
}
catch (Throwable x)
{
if (LOG.isDebugEnabled())
LOG.debug("Exception while invoking listener " + listener, x);
return false;
}
}
@ -269,8 +261,10 @@ public class ResponseContentParser extends StreamContentParser
notifyBegin(200, "OK");
notifyHeaders(fields);
}
notifyHeaders();
// Return from HTTP parsing so that we can parse the content.
// Remember whether we have demand.
stalled = notifyHeaders();
// Always return from HTTP parsing so that we
// can parse the content with the FCGI parser.
return true;
}

View File

@ -109,10 +109,11 @@ public class ClientGeneratorTest
}
@Override
public void onHeaders(int request)
public boolean onHeaders(int request)
{
assertEquals(id, request);
params.set(params.get() * primes[4]);
return false;
}
});

View File

@ -91,10 +91,11 @@ public class ClientParserTest
}
@Override
public void onHeaders(int request)
public boolean onHeaders(int request)
{
assertEquals(id, request);
params.set(params.get() * primes[2]);
return false;
}
});

View File

@ -144,7 +144,7 @@ public class ServerFCGIConnection extends AbstractConnection
}
@Override
public void onHeaders(int request)
public boolean onHeaders(int request)
{
HttpChannelOverFCGI channel = channels.get(request);
if (LOG.isDebugEnabled())
@ -154,6 +154,7 @@ public class ServerFCGIConnection extends AbstractConnection
channel.onRequest();
channel.dispatch();
}
return false;
}
@Override

View File

@ -6,7 +6,7 @@
<!-- ================================================================ -->
<Configure id="Server" class="org.eclipse.jetty.server.Server">
<Call name="addLifeCycleListener">
<Call name="addEventListener">
<Arg>
<New class="org.eclipse.jetty.setuid.SetUIDListener">
<Set name="startServerAsPrivileged"><Property name="jetty.setuid.startServerAsPrivileged" default="false"/></Set>

View File

@ -5,7 +5,7 @@
<!-- Mixin the Start FileNoticeLifeCycleListener -->
<!-- =============================================================== -->
<Configure id="Server" class="org.eclipse.jetty.server.Server">
<Call name="addLifeCycleListener">
<Call name="addEventListener">
<Arg>
<New class="org.eclipse.jetty.util.component.FileNoticeLifeCycleListener">
<Arg><Property name="jetty.state" default="./jetty.state"/></Arg>

View File

@ -24,7 +24,7 @@ import java.util.concurrent.TimeUnit;
import org.eclipse.jetty.util.QuotedStringTokenizer;
import org.eclipse.jetty.util.StringUtil;
// TODO consider replacing this with java.net.HttpCookie
// TODO consider replacing this with java.net.HttpCookie (once it supports RFC6265)
public class HttpCookie
{
private static final String __COOKIE_DELIM = "\",;\\ \t";
@ -33,14 +33,14 @@ public class HttpCookie
/**
*If this string is found within the comment parsed with {@link #isHttpOnlyInComment(String)} the check will return true
**/
private static final String HTTP_ONLY_COMMENT = "__HTTP_ONLY__";
public static final String HTTP_ONLY_COMMENT = "__HTTP_ONLY__";
/**
*These strings are used by {@link #getSameSiteFromComment(String)} to check for a SameSite specifier in the comment
**/
private static final String SAME_SITE_COMMENT = "__SAME_SITE_";
private static final String SAME_SITE_NONE_COMMENT = SAME_SITE_COMMENT + "NONE__";
private static final String SAME_SITE_LAX_COMMENT = SAME_SITE_COMMENT + "LAX__";
private static final String SAME_SITE_STRICT_COMMENT = SAME_SITE_COMMENT + "STRICT__";
public static final String SAME_SITE_NONE_COMMENT = SAME_SITE_COMMENT + "NONE__";
public static final String SAME_SITE_LAX_COMMENT = SAME_SITE_COMMENT + "LAX__";
public static final String SAME_SITE_STRICT_COMMENT = SAME_SITE_COMMENT + "STRICT__";
public enum SameSite
{
@ -428,17 +428,17 @@ public class HttpCookie
{
if (comment != null)
{
if (comment.contains(SAME_SITE_NONE_COMMENT))
if (comment.contains(SAME_SITE_STRICT_COMMENT))
{
return SameSite.NONE;
return SameSite.STRICT;
}
if (comment.contains(SAME_SITE_LAX_COMMENT))
{
return SameSite.LAX;
}
if (comment.contains(SAME_SITE_STRICT_COMMENT))
if (comment.contains(SAME_SITE_NONE_COMMENT))
{
return SameSite.STRICT;
return SameSite.NONE;
}
}
@ -462,6 +462,44 @@ public class HttpCookie
return strippedComment.length() == 0 ? null : strippedComment;
}
public static String getCommentWithAttributes(String comment, boolean httpOnly, SameSite sameSite)
{
if (comment == null && sameSite == null)
return null;
StringBuilder builder = new StringBuilder();
if (StringUtil.isNotBlank(comment))
{
comment = getCommentWithoutAttributes(comment);
if (StringUtil.isNotBlank(comment))
builder.append(comment);
}
if (httpOnly)
builder.append(HTTP_ONLY_COMMENT);
if (sameSite != null)
{
switch (sameSite)
{
case NONE:
builder.append(SAME_SITE_NONE_COMMENT);
break;
case STRICT:
builder.append(SAME_SITE_STRICT_COMMENT);
break;
case LAX:
builder.append(SAME_SITE_LAX_COMMENT);
break;
default:
throw new IllegalArgumentException(sameSite.toString());
}
}
if (builder.length() == 0)
return null;
return builder.toString();
}
public static class SetCookieHttpField extends HttpField
{
final HttpCookie _cookie;

View File

@ -24,7 +24,6 @@ import java.util.EnumSet;
import java.util.List;
import java.util.Locale;
import org.eclipse.jetty.http.HttpCompliance.Violation;
import org.eclipse.jetty.http.HttpTokens.EndOfContent;
import org.eclipse.jetty.util.ArrayTernaryTrie;
import org.eclipse.jetty.util.ArrayTrie;
@ -35,6 +34,8 @@ import org.eclipse.jetty.util.Utf8StringBuilder;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
import static org.eclipse.jetty.http.HttpCompliance.RFC7230;
import static org.eclipse.jetty.http.HttpCompliance.Violation;
import static org.eclipse.jetty.http.HttpCompliance.Violation.CASE_SENSITIVE_FIELD_NAME;
import static org.eclipse.jetty.http.HttpCompliance.Violation.MULTIPLE_CONTENT_LENGTHS;
import static org.eclipse.jetty.http.HttpCompliance.Violation.NO_COLON_AFTER_FIELD_NAME;
@ -136,6 +137,7 @@ public class HttpParser
CHUNK_SIZE,
CHUNK_PARAMS,
CHUNK,
CONTENT_END,
TRAILER,
END,
CLOSE, // The associated stream/endpoint should be closed
@ -160,7 +162,6 @@ public class HttpParser
private int _headerBytes;
private boolean _host;
private boolean _headerComplete;
private volatile State _state = State.START;
private volatile FieldState _fieldState = FieldState.FIELD;
private volatile boolean _eof;
@ -179,9 +180,10 @@ public class HttpParser
private boolean _cr;
private ByteBuffer _contentChunk;
private Trie<HttpField> _fieldCache;
private int _length;
private final StringBuilder _string = new StringBuilder();
private int _headerCacheSize = 1024;
private boolean _headerCacheCaseSensitive;
static
{
@ -238,7 +240,7 @@ public class HttpParser
private static HttpCompliance compliance()
{
return HttpCompliance.RFC7230;
return RFC7230;
}
public HttpParser(RequestHandler handler)
@ -291,6 +293,26 @@ public class HttpParser
return _handler;
}
public int getHeaderCacheSize()
{
return _headerCacheSize;
}
public void setHeaderCacheSize(int headerCacheSize)
{
_headerCacheSize = headerCacheSize;
}
public boolean isHeaderCacheCaseSensitive()
{
return _headerCacheCaseSensitive;
}
public void setHeaderCacheCaseSensitive(boolean headerCacheCaseSensitive)
{
_headerCacheCaseSensitive = headerCacheCaseSensitive;
}
protected void checkViolation(Violation violation) throws BadMessageException
{
if (violation.isAllowedBy(_complianceMode))
@ -529,16 +551,19 @@ public class HttpParser
{
boolean handleHeader = _handler.headerComplete();
_headerComplete = true;
boolean handleContent = _handler.contentComplete();
boolean handleMessage = _handler.messageComplete();
return handleHeader || handleContent || handleMessage;
if (handleHeader)
return true;
setState(State.CONTENT_END);
return handleContentMessage();
}
private boolean handleContentMessage()
{
boolean handleContent = _handler.contentComplete();
boolean handleMessage = _handler.messageComplete();
return handleContent || handleMessage;
if (handleContent)
return true;
setState(State.END);
return _handler.messageComplete();
}
/* Parse a request or response line
@ -708,7 +733,7 @@ public class HttpParser
case LF:
setState(State.HEADER);
handle = _responseHandler.startResponse(_version, _responseStatus, null);
_responseHandler.startResponse(_version, _responseStatus, null);
break;
default:
@ -726,10 +751,11 @@ public class HttpParser
case LF:
// HTTP/0.9
checkViolation(Violation.HTTP_0_9);
handle = _requestHandler.startRequest(_methodString, _uri.toString(), HttpVersion.HTTP_0_9);
setState(State.END);
_requestHandler.startRequest(_methodString, _uri.toString(), HttpVersion.HTTP_0_9);
setState(State.CONTENT);
_endOfContent = EndOfContent.NO_CONTENT;
BufferUtil.clear(buffer);
handle |= handleHeaderContentMessage();
handle = handleHeaderContentMessage();
break;
case ALPHA:
@ -805,16 +831,17 @@ public class HttpParser
if (_responseHandler != null)
{
setState(State.HEADER);
handle = _responseHandler.startResponse(_version, _responseStatus, null);
_responseHandler.startResponse(_version, _responseStatus, null);
}
else
{
// HTTP/0.9
checkViolation(Violation.HTTP_0_9);
handle = _requestHandler.startRequest(_methodString, _uri.toString(), HttpVersion.HTTP_0_9);
setState(State.END);
_requestHandler.startRequest(_methodString, _uri.toString(), HttpVersion.HTTP_0_9);
setState(State.CONTENT);
_endOfContent = EndOfContent.NO_CONTENT;
BufferUtil.clear(buffer);
handle |= handleHeaderContentMessage();
handle = handleHeaderContentMessage();
}
break;
@ -835,13 +862,13 @@ public class HttpParser
checkVersion();
// Should we try to cache header fields?
int headerCache = _handler.getHeaderCacheSize();
int headerCache = getHeaderCacheSize();
if (_fieldCache == null && _version.getVersion() >= HttpVersion.HTTP_1_1.getVersion() && headerCache > 0)
_fieldCache = new ArrayTernaryTrie<>(headerCache);
setState(State.HEADER);
handle = _requestHandler.startRequest(_methodString, _uri.toString(), _version);
_requestHandler.startRequest(_methodString, _uri.toString(), _version);
continue;
case ALPHA:
@ -863,7 +890,7 @@ public class HttpParser
case LF:
String reason = takeString();
setState(State.HEADER);
handle = _responseHandler.startResponse(_version, _responseStatus, reason);
_responseHandler.startResponse(_version, _responseStatus, reason);
continue;
case ALPHA:
@ -1167,11 +1194,6 @@ public class HttpParser
_headerComplete = true;
return handle;
}
case NO_CONTENT:
{
setState(State.END);
return handleHeaderContentMessage();
}
default:
{
setState(State.CONTENT);
@ -1217,7 +1239,7 @@ public class HttpParser
}
}
if (v != null && _handler.isHeaderCacheCaseSensitive())
if (v != null && isHeaderCacheCaseSensitive())
{
String ev = BufferUtil.toString(buffer, buffer.position() + n.length() + 1, v.length(), StandardCharsets.ISO_8859_1);
if (!v.equals(ev))
@ -1470,8 +1492,16 @@ public class HttpParser
// Handle HEAD response
if (_responseStatus > 0 && _headResponse)
{
setState(State.END);
return handleContentMessage();
if (_state != State.CONTENT_END)
{
setState(State.CONTENT_END);
return handleContentMessage();
}
else
{
setState(State.END);
return _handler.messageComplete();
}
}
else
{
@ -1490,11 +1520,18 @@ public class HttpParser
// handle end states
if (_state == State.END)
{
// eat white space
while (buffer.remaining() > 0 && buffer.get(buffer.position()) <= HttpTokens.SPACE)
// Eat CR or LF white space, but not SP.
int whiteSpace = 0;
while (buffer.remaining() > 0)
{
byte b = buffer.get(buffer.position());
if (b != HttpTokens.CARRIAGE_RETURN && b != HttpTokens.LINE_FEED)
break;
buffer.get();
++whiteSpace;
}
if (debugEnabled && whiteSpace > 0)
LOG.debug("Discarded {} CR or LF characters", whiteSpace);
}
else if (isClose() || isClosed())
{
@ -1502,18 +1539,13 @@ public class HttpParser
}
// Handle EOF
if (_eof && !buffer.hasRemaining())
if (isAtEOF() && !buffer.hasRemaining())
{
switch (_state)
{
case CLOSED:
break;
case START:
setState(State.CLOSED);
_handler.earlyEOF();
break;
case END:
case CLOSE:
setState(State.CLOSED);
@ -1524,13 +1556,18 @@ public class HttpParser
if (_fieldState == FieldState.FIELD)
{
// Be forgiving of missing last CRLF
setState(State.CONTENT_END);
boolean handle = handleContentMessage();
if (handle && _state == State.CONTENT_END)
return true;
setState(State.CLOSED);
return handleContentMessage();
return handle;
}
setState(State.CLOSED);
_handler.earlyEOF();
break;
case START:
case CONTENT:
case CHUNKED_CONTENT:
case CHUNK_SIZE:
@ -1576,17 +1613,28 @@ public class HttpParser
protected boolean parseContent(ByteBuffer buffer)
{
int remaining = buffer.remaining();
if (remaining == 0 && _state == State.CONTENT)
if (remaining == 0)
{
long content = _contentLength - _contentPosition;
if (content == 0)
switch (_state)
{
setState(State.END);
return handleContentMessage();
case CONTENT:
long content = _contentLength - _contentPosition;
if (_endOfContent == EndOfContent.NO_CONTENT || content == 0)
{
setState(State.CONTENT_END);
return handleContentMessage();
}
break;
case CONTENT_END:
setState(_endOfContent == EndOfContent.EOF_CONTENT ? State.CLOSED : State.END);
return _handler.messageComplete();
default:
// No bytes to parse, return immediately.
return false;
}
}
// Handle _content
// Handle content.
while (_state.ordinal() < State.TRAILER.ordinal() && remaining > 0)
{
switch (_state)
@ -1602,9 +1650,9 @@ public class HttpParser
case CONTENT:
{
long content = _contentLength - _contentPosition;
if (content == 0)
if (_endOfContent == EndOfContent.NO_CONTENT || content == 0)
{
setState(State.END);
setState(State.CONTENT_END);
return handleContentMessage();
}
else
@ -1627,7 +1675,7 @@ public class HttpParser
if (_contentPosition == _contentLength)
{
setState(State.END);
setState(State.CONTENT_END);
return handleContentMessage();
}
}
@ -1752,10 +1800,10 @@ public class HttpParser
break;
}
case CLOSED:
case CONTENT_END:
{
BufferUtil.clear(buffer);
return false;
setState(_endOfContent == EndOfContent.EOF_CONTENT ? State.CLOSED : State.END);
return _handler.messageComplete();
}
default:
@ -1839,8 +1887,8 @@ public class HttpParser
return String.format("%s{s=%s,%d of %d}",
getClass().getSimpleName(),
_state,
_contentPosition,
_contentLength);
getContentRead(),
getContentLength());
}
/* Event Handler interface
@ -1890,13 +1938,6 @@ public class HttpParser
default void badMessage(BadMessageException failure)
{
}
/**
* @return the size in bytes of the per parser header cache
*/
int getHeaderCacheSize();
boolean isHeaderCacheCaseSensitive();
}
public interface RequestHandler extends HttpHandler
@ -1907,9 +1948,8 @@ public class HttpParser
* @param method The method
* @param uri The raw bytes of the URI. These are copied into a ByteBuffer that will not be changed until this parser is reset and reused.
* @param version the http version in use
* @return true if handling parsing should return.
*/
boolean startRequest(String method, String uri, HttpVersion version);
void startRequest(String method, String uri, HttpVersion version);
}
public interface ResponseHandler extends HttpHandler
@ -1920,9 +1960,8 @@ public class HttpParser
* @param version the http version in use
* @param status the response status
* @param reason the response reason phrase
* @return true if handling parsing should return
*/
boolean startResponse(HttpVersion version, int status, String reason);
void startResponse(HttpVersion version, int status, String reason);
}
@SuppressWarnings("serial")

View File

@ -18,17 +18,25 @@
package org.eclipse.jetty.http;
import java.util.stream.Stream;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.ValueSource;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.nullValue;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
public class HttpCookieTest
{
@ -93,7 +101,6 @@ public class HttpCookieTest
httpCookie = new HttpCookie("everything", "value", "domain", "path", 0, true, true, null, -1);
assertEquals("everything=value; Path=path; Domain=domain; Expires=Thu, 01-Jan-1970 00:00:00 GMT; Max-Age=0; Secure; HttpOnly", httpCookie.getRFC6265SetCookie());
httpCookie = new HttpCookie("everything", "value", "domain", "path", 0, true, true, null, -1, HttpCookie.SameSite.NONE);
assertEquals("everything=value; Path=path; Domain=domain; Expires=Thu, 01-Jan-1970 00:00:00 GMT; Max-Age=0; Secure; HttpOnly; SameSite=None", httpCookie.getRFC6265SetCookie());
@ -102,8 +109,11 @@ public class HttpCookieTest
httpCookie = new HttpCookie("everything", "value", "domain", "path", 0, true, true, null, -1, HttpCookie.SameSite.STRICT);
assertEquals("everything=value; Path=path; Domain=domain; Expires=Thu, 01-Jan-1970 00:00:00 GMT; Max-Age=0; Secure; HttpOnly; SameSite=Strict", httpCookie.getRFC6265SetCookie());
}
String[] badNameExamples = {
public static Stream<String> rfc6265BadNameSource()
{
return Stream.of(
"\"name\"",
"name\t",
"na me",
@ -113,25 +123,32 @@ public class HttpCookieTest
"{name}",
"[name]",
"\""
};
);
}
for (String badNameExample : badNameExamples)
{
try
@ParameterizedTest
@MethodSource("rfc6265BadNameSource")
public void testSetRFC6265CookieBadName(String badNameExample)
{
IllegalArgumentException ex = assertThrows(IllegalArgumentException.class,
() ->
{
httpCookie = new HttpCookie(badNameExample, "value", null, "/", 1, true, true, null, -1);
HttpCookie httpCookie = new HttpCookie(badNameExample, "value", null, "/", 1, true, true, null, -1);
httpCookie.getRFC6265SetCookie();
fail(badNameExample);
}
catch (IllegalArgumentException ex)
{
// System.err.printf("%s: %s%n", ex.getClass().getSimpleName(), ex.getMessage());
assertThat("Testing bad name: [" + badNameExample + "]", ex.getMessage(),
allOf(containsString("RFC6265"), containsString("RFC2616")));
}
}
});
// make sure that exception mentions just how mad of a name it truly is
assertThat("message", ex.getMessage(),
allOf(
// violation of Cookie spec
containsString("RFC6265"),
// violation of HTTP spec
containsString("RFC2616")
));
}
String[] badValueExamples = {
public static Stream<String> rfc6265BadValueSource()
{
return Stream.of(
"va\tlue",
"\t",
"value\u0000",
@ -143,39 +160,44 @@ public class HttpCookieTest
"val\\ue",
"val\"ue",
"\""
};
);
}
for (String badValueExample : badValueExamples)
{
try
@ParameterizedTest
@MethodSource("rfc6265BadValueSource")
public void testSetRFC6265CookieBadValue(String badValueExample)
{
IllegalArgumentException ex = assertThrows(IllegalArgumentException.class,
() ->
{
httpCookie = new HttpCookie("name", badValueExample, null, "/", 1, true, true, null, -1);
HttpCookie httpCookie = new HttpCookie("name", badValueExample, null, "/", 1, true, true, null, -1);
httpCookie.getRFC6265SetCookie();
fail();
}
catch (IllegalArgumentException ex)
{
// System.err.printf("%s: %s%n", ex.getClass().getSimpleName(), ex.getMessage());
assertThat("Testing bad value [" + badValueExample + "]", ex.getMessage(), Matchers.containsString("RFC6265"));
}
}
});
assertThat("message", ex.getMessage(), containsString("RFC6265"));
}
String[] goodNameExamples = {
public static Stream<String> rfc6265GoodNameSource()
{
return Stream.of(
"name",
"n.a.m.e",
"na-me",
"+name",
"na*me",
"na$me",
"#name"
};
"#name");
}
for (String goodNameExample : goodNameExamples)
{
httpCookie = new HttpCookie(goodNameExample, "value", null, "/", 1, true, true, null, -1);
// should not throw an exception
}
@ParameterizedTest
@MethodSource("rfc6265GoodNameSource")
public void testSetRFC6265CookieGoodName(String goodNameExample)
{
new HttpCookie(goodNameExample, "value", null, "/", 1, true, true, null, -1);
// should not throw an exception
}
public static Stream<String> rfc6265GoodValueSource()
{
String[] goodValueExamples = {
"value",
"",
@ -185,37 +207,150 @@ public class HttpCookieTest
"val/ue",
"v.a.l.u.e"
};
return Stream.of(goodValueExamples);
}
for (String goodValueExample : goodValueExamples)
@ParameterizedTest
@MethodSource("rfc6265GoodValueSource")
public void testSetRFC6265CookieGoodValue(String goodValueExample)
{
new HttpCookie("name", goodValueExample, null, "/", 1, true, true, null, -1);
// should not throw an exception
}
@ParameterizedTest
@ValueSource(strings = {
"__HTTP_ONLY__",
"__HTTP_ONLY__comment",
"comment__HTTP_ONLY__"
})
public void testIsHttpOnlyInCommentTrue(String comment)
{
assertTrue(HttpCookie.isHttpOnlyInComment(comment), "Comment \"" + comment + "\"");
}
@ParameterizedTest
@ValueSource(strings = {
"comment",
"",
"__",
"__HTTP__ONLY__",
"__http_only__",
"HTTP_ONLY",
"__HTTP__comment__ONLY__"
})
public void testIsHttpOnlyInCommentFalse(String comment)
{
assertFalse(HttpCookie.isHttpOnlyInComment(comment), "Comment \"" + comment + "\"");
}
@ParameterizedTest
@ValueSource(strings = {
"__SAME_SITE_NONE__",
"__SAME_SITE_NONE____SAME_SITE_NONE__"
})
public void testGetSameSiteFromCommentNONE(String comment)
{
assertEquals(HttpCookie.getSameSiteFromComment(comment), HttpCookie.SameSite.NONE, "Comment \"" + comment + "\"");
}
@ParameterizedTest
@ValueSource(strings = {
"__SAME_SITE_LAX__",
"__SAME_SITE_LAX____SAME_SITE_NONE__",
"__SAME_SITE_NONE____SAME_SITE_LAX__",
"__SAME_SITE_LAX____SAME_SITE_NONE__"
})
public void testGetSameSiteFromCommentLAX(String comment)
{
assertEquals(HttpCookie.getSameSiteFromComment(comment), HttpCookie.SameSite.LAX, "Comment \"" + comment + "\"");
}
@ParameterizedTest
@ValueSource(strings = {
"__SAME_SITE_STRICT__",
"__SAME_SITE_NONE____SAME_SITE_STRICT____SAME_SITE_LAX__",
"__SAME_SITE_STRICT____SAME_SITE_LAX____SAME_SITE_NONE__",
"__SAME_SITE_STRICT____SAME_SITE_STRICT__"
})
public void testGetSameSiteFromCommentSTRICT(String comment)
{
assertEquals(HttpCookie.getSameSiteFromComment(comment), HttpCookie.SameSite.STRICT, "Comment \"" + comment + "\"");
}
/**
* A comment that does not have a declared SamesSite attribute defined
*/
@ParameterizedTest
@ValueSource(strings = {
"__HTTP_ONLY__",
"comment",
// not jetty attributes
"SameSite=None",
"SameSite=Lax",
"SameSite=Strict",
// incomplete jetty attributes
"SAME_SITE_NONE",
"SAME_SITE_LAX",
"SAME_SITE_STRICT",
})
public void testGetSameSiteFromCommentUndefined(String comment)
{
assertNull(HttpCookie.getSameSiteFromComment(comment), "Comment \"" + comment + "\"");
}
public static Stream<Arguments> getCommentWithoutAttributesSource()
{
return Stream.of(
// normal - only attribute comment
Arguments.of("__SAME_SITE_LAX__", null),
// normal - no attribute comment
Arguments.of("comment", "comment"),
// mixed - attributes at end
Arguments.of("comment__SAME_SITE_NONE__", "comment"),
Arguments.of("comment__HTTP_ONLY____SAME_SITE_NONE__", "comment"),
// mixed - attributes at start
Arguments.of("__SAME_SITE_NONE__comment", "comment"),
Arguments.of("__HTTP_ONLY____SAME_SITE_NONE__comment", "comment"),
// mixed - attributes at start and end
Arguments.of("__SAME_SITE_NONE__comment__HTTP_ONLY__", "comment"),
Arguments.of("__HTTP_ONLY__comment__SAME_SITE_NONE__", "comment")
);
}
@ParameterizedTest
@MethodSource("getCommentWithoutAttributesSource")
public void testGetCommentWithoutAttributes(String rawComment, String expectedComment)
{
String actualComment = HttpCookie.getCommentWithoutAttributes(rawComment);
if (expectedComment == null)
{
httpCookie = new HttpCookie("name", goodValueExample, null, "/", 1, true, true, null, -1);
// should not throw an exception
assertNull(actualComment);
}
else
{
assertEquals(actualComment, expectedComment);
}
}
@Test
public void testGetHttpOnlyFromComment()
public void testGetCommentWithAttributes()
{
assertTrue(HttpCookie.isHttpOnlyInComment("__HTTP_ONLY__"));
assertTrue(HttpCookie.isHttpOnlyInComment("__HTTP_ONLY__comment"));
assertFalse(HttpCookie.isHttpOnlyInComment("comment"));
}
assertThat(HttpCookie.getCommentWithAttributes(null, false, null), nullValue());
assertThat(HttpCookie.getCommentWithAttributes("", false, null), nullValue());
assertThat(HttpCookie.getCommentWithAttributes("hello", false, null), is("hello"));
@Test
public void testGetSameSiteFromComment()
{
assertEquals(HttpCookie.getSameSiteFromComment("__SAME_SITE_NONE__"), HttpCookie.SameSite.NONE);
assertEquals(HttpCookie.getSameSiteFromComment("__SAME_SITE_LAX__"), HttpCookie.SameSite.LAX);
assertEquals(HttpCookie.getSameSiteFromComment("__SAME_SITE_STRICT__"), HttpCookie.SameSite.STRICT);
assertEquals(HttpCookie.getSameSiteFromComment("__SAME_SITE_NONE____SAME_SITE_STRICT__"), HttpCookie.SameSite.NONE);
assertNull(HttpCookie.getSameSiteFromComment("comment"));
}
assertThat(HttpCookie.getCommentWithAttributes(null, true, HttpCookie.SameSite.STRICT),
is("__HTTP_ONLY____SAME_SITE_STRICT__"));
assertThat(HttpCookie.getCommentWithAttributes("", true, HttpCookie.SameSite.NONE),
is("__HTTP_ONLY____SAME_SITE_NONE__"));
assertThat(HttpCookie.getCommentWithAttributes("hello", true, HttpCookie.SameSite.LAX),
is("hello__HTTP_ONLY____SAME_SITE_LAX__"));
@Test
public void getCommentWithoutAttributes()
{
assertEquals(HttpCookie.getCommentWithoutAttributes("comment__SAME_SITE_NONE__"), "comment");
assertEquals(HttpCookie.getCommentWithoutAttributes("comment__HTTP_ONLY____SAME_SITE_NONE__"), "comment");
assertNull(HttpCookie.getCommentWithoutAttributes("__SAME_SITE_LAX__"));
assertThat(HttpCookie.getCommentWithAttributes("__HTTP_ONLY____SAME_SITE_LAX__", false, null), nullValue());
assertThat(HttpCookie.getCommentWithAttributes("__HTTP_ONLY____SAME_SITE_LAX__", true, HttpCookie.SameSite.NONE),
is("__HTTP_ONLY____SAME_SITE_NONE__"));
assertThat(HttpCookie.getCommentWithAttributes("__HTTP_ONLY____SAME_SITE_LAX__hello", true, HttpCookie.SameSite.LAX),
is("hello__HTTP_ONLY____SAME_SITE_LAX__"));
}
}

View File

@ -32,6 +32,7 @@ import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.either;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class HttpGeneratorServerHTTPTest
@ -70,7 +71,7 @@ public class HttpGeneratorServerHTTPTest
assertEquals("OK??Test", _reason);
if (_content == null)
assertTrue(run.result._body == null, msg);
assertNull(run.result._body, msg);
else
assertThat(msg, run.result._contentLength, either(equalTo(_content.length())).or(equalTo(-1)));
}
@ -248,10 +249,9 @@ public class HttpGeneratorServerHTTPTest
}
@Override
public boolean startResponse(HttpVersion version, int status, String reason)
public void startResponse(HttpVersion version, int status, String reason)
{
_reason = reason;
return false;
}
@Override
@ -259,18 +259,6 @@ public class HttpGeneratorServerHTTPTest
{
throw failure;
}
@Override
public int getHeaderCacheSize()
{
return 4096;
}
@Override
public boolean isHeaderCacheCaseSensitive()
{
return false;
}
}
public static final String CONTENT = "The quick brown fox jumped over the lazy dog.\nNow is the time for all good men to come to the aid of the party\nThe moon is blue to a fish in love.\n";

View File

@ -43,6 +43,7 @@ import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.nullValue;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertTrue;
@ -859,8 +860,9 @@ public class HttpParserTest
"HOST: localhost\r\n" +
"cOnNeCtIoN: ClOsE\r\n" +
"\r\n");
HttpParser.RequestHandler handler = new Handler(true);
HttpParser.RequestHandler handler = new Handler();
HttpParser parser = new HttpParser(handler, -1, HttpCompliance.LEGACY);
parser.setHeaderCacheCaseSensitive(true);
parseAll(parser, buffer);
assertNull(_bad);
assertEquals("GET", _methodOrVersion);
@ -2156,6 +2158,662 @@ public class HttpParserTest
assertNull(_bad);
}
@Test
public void testForHTTP09HeaderCompleteTrueDoesNotEmitContentComplete()
{
HttpParser.RequestHandler handler = new Handler()
{
@Override
public boolean headerComplete()
{
super.headerComplete();
return true;
}
};
HttpParser parser = new HttpParser(handler, HttpCompliance.RFC2616_LEGACY);
ByteBuffer buffer = BufferUtil.toBuffer("GET /path\r\n");
boolean handle = parser.parseNext(buffer);
assertTrue(handle);
assertFalse(buffer.hasRemaining());
assertFalse(_contentCompleted);
assertFalse(_messageCompleted);
assertEquals("GET", _methodOrVersion);
assertEquals("/path", _uriOrStatus);
assertEquals("HTTP/0.9", _versionOrReason);
assertEquals(-1, _headers);
// Need to parse more to advance the parser.
handle = parser.parseNext(buffer);
assertTrue(handle);
assertTrue(_contentCompleted);
assertTrue(_messageCompleted);
}
@Test
public void testForContentLengthZeroHeaderCompleteTrueDoesNotEmitContentComplete()
{
HttpParser.ResponseHandler handler = new Handler()
{
@Override
public boolean headerComplete()
{
super.headerComplete();
return true;
}
};
HttpParser parser = new HttpParser(handler);
ByteBuffer buffer = BufferUtil.toBuffer(
"HTTP/1.1 200 OK\r\n" +
"Content-Length: 0\r\n" +
"\r\n");
boolean handle = parser.parseNext(buffer);
assertTrue(handle);
assertFalse(buffer.hasRemaining());
assertFalse(_contentCompleted);
assertFalse(_messageCompleted);
// Need to parse more to advance the parser.
handle = parser.parseNext(buffer);
assertTrue(handle);
assertTrue(_contentCompleted);
assertTrue(_messageCompleted);
}
@Test
public void testForEmptyChunkedContentHeaderCompleteTrueDoesNotEmitContentComplete()
{
HttpParser.ResponseHandler handler = new Handler()
{
@Override
public boolean headerComplete()
{
super.headerComplete();
return true;
}
};
HttpParser parser = new HttpParser(handler);
ByteBuffer buffer = BufferUtil.toBuffer(
"HTTP/1.1 200 OK\r\n" +
"Transfer-Encoding: chunked\r\n" +
"\r\n" +
"0\r\n" +
"\r\n");
boolean handle = parser.parseNext(buffer);
assertTrue(handle);
assertTrue(buffer.hasRemaining());
assertFalse(_contentCompleted);
assertFalse(_messageCompleted);
// Need to parse more to advance the parser.
handle = parser.parseNext(buffer);
assertTrue(handle);
assertTrue(_contentCompleted);
assertTrue(_messageCompleted);
}
@Test
public void testForContentLengthZeroContentCompleteTrueDoesNotEmitMessageComplete()
{
HttpParser.ResponseHandler handler = new Handler()
{
@Override
public boolean contentComplete()
{
super.contentComplete();
return true;
}
};
HttpParser parser = new HttpParser(handler);
ByteBuffer buffer = BufferUtil.toBuffer(
"HTTP/1.1 200 OK\r\n" +
"Content-Length: 0\r\n" +
"\r\n");
boolean handle = parser.parseNext(buffer);
assertTrue(handle);
assertFalse(buffer.hasRemaining());
assertFalse(_messageCompleted);
// Need to parse more to advance the parser.
handle = parser.parseNext(buffer);
assertTrue(handle);
assertTrue(_messageCompleted);
}
@Test
public void testForEmptyChunkedContentContentCompleteTrueDoesNotEmitMessageComplete()
{
HttpParser.ResponseHandler handler = new Handler()
{
@Override
public boolean contentComplete()
{
super.contentComplete();
return true;
}
};
HttpParser parser = new HttpParser(handler);
ByteBuffer buffer = BufferUtil.toBuffer(
"HTTP/1.1 200 OK\r\n" +
"Transfer-Encoding: chunked\r\n" +
"\r\n" +
"0\r\n" +
"\r\n");
boolean handle = parser.parseNext(buffer);
assertTrue(handle);
assertTrue(buffer.hasRemaining());
assertFalse(_messageCompleted);
// Need to parse more to advance the parser.
handle = parser.parseNext(buffer);
assertTrue(handle);
assertTrue(_messageCompleted);
}
@Test
public void testHeaderAfterContentLengthZeroContentCompleteTrue()
{
HttpParser.ResponseHandler handler = new Handler()
{
@Override
public boolean contentComplete()
{
super.contentComplete();
return true;
}
};
HttpParser parser = new HttpParser(handler);
String header = "Header: Foobar\r\n";
ByteBuffer buffer = BufferUtil.toBuffer(
"HTTP/1.1 200 OK\r\n" +
"Content-Length: 0\r\n" +
"\r\n" +
header);
boolean handle = parser.parseNext(buffer);
assertTrue(handle);
assertTrue(buffer.hasRemaining());
assertEquals(header, BufferUtil.toString(buffer));
assertTrue(_contentCompleted);
assertFalse(_messageCompleted);
// Need to parse more to advance the parser.
handle = parser.parseNext(buffer);
assertTrue(handle);
assertTrue(buffer.hasRemaining());
assertEquals(header, BufferUtil.toString(buffer));
assertTrue(_messageCompleted);
}
@Test
public void testSmallContentLengthContentCompleteTrue()
{
HttpParser.ResponseHandler handler = new Handler()
{
@Override
public boolean contentComplete()
{
super.contentComplete();
return true;
}
};
HttpParser parser = new HttpParser(handler);
String header = "Header: Foobar\r\n";
ByteBuffer buffer = BufferUtil.toBuffer(
"HTTP/1.1 200 OK\r\n" +
"Content-Length: 1\r\n" +
"\r\n" +
"0" +
header);
boolean handle = parser.parseNext(buffer);
assertTrue(handle);
assertTrue(buffer.hasRemaining());
assertEquals(header, BufferUtil.toString(buffer));
assertTrue(_contentCompleted);
assertFalse(_messageCompleted);
// Need to parse more to advance the parser.
handle = parser.parseNext(buffer);
assertTrue(handle);
assertTrue(buffer.hasRemaining());
assertEquals(header, BufferUtil.toString(buffer));
assertTrue(_messageCompleted);
}
@Test
public void testHeaderAfterSmallContentLengthContentCompleteTrue()
{
HttpParser.ResponseHandler handler = new Handler()
{
@Override
public boolean contentComplete()
{
super.contentComplete();
return true;
}
};
HttpParser parser = new HttpParser(handler);
ByteBuffer buffer = BufferUtil.toBuffer(
"HTTP/1.1 200 OK\r\n" +
"Content-Length: 1\r\n" +
"\r\n" +
"0");
boolean handle = parser.parseNext(buffer);
assertTrue(handle);
assertFalse(buffer.hasRemaining());
assertTrue(_contentCompleted);
assertFalse(_messageCompleted);
// Need to parse more to advance the parser.
handle = parser.parseNext(buffer);
assertTrue(handle);
assertFalse(buffer.hasRemaining());
assertTrue(_messageCompleted);
}
@Test
public void testEOFContentContentCompleteTrue()
{
HttpParser.ResponseHandler handler = new Handler()
{
@Override
public boolean contentComplete()
{
super.contentComplete();
return true;
}
};
HttpParser parser = new HttpParser(handler);
ByteBuffer buffer = BufferUtil.toBuffer(
"HTTP/1.1 200 OK\r\n" +
"\r\n" +
"0");
boolean handle = parser.parseNext(buffer);
assertFalse(handle);
assertFalse(buffer.hasRemaining());
assertEquals("0", _content);
assertFalse(_contentCompleted);
assertFalse(_messageCompleted);
parser.atEOF();
// Need to parse more to advance the parser.
handle = parser.parseNext(buffer);
assertTrue(handle);
assertFalse(buffer.hasRemaining());
assertTrue(_contentCompleted);
assertFalse(_messageCompleted);
// Need to parse more to advance the parser.
handle = parser.parseNext(buffer);
assertTrue(handle);
assertFalse(buffer.hasRemaining());
assertTrue(_messageCompleted);
}
@Test
public void testHEADRequestHeaderCompleteTrue()
{
HttpParser.ResponseHandler handler = new Handler()
{
@Override
public boolean headerComplete()
{
super.headerComplete();
return true;
}
@Override
public boolean contentComplete()
{
super.contentComplete();
return true;
}
};
HttpParser parser = new HttpParser(handler);
parser.setHeadResponse(true);
ByteBuffer buffer = BufferUtil.toBuffer(
"HTTP/1.1 200 OK\r\n" +
"\r\n");
boolean handle = parser.parseNext(buffer);
assertTrue(handle);
assertFalse(buffer.hasRemaining());
assertFalse(_contentCompleted);
assertFalse(_messageCompleted);
// Need to parse more to advance the parser.
handle = parser.parseNext(buffer);
assertTrue(handle);
assertFalse(buffer.hasRemaining());
assertTrue(_contentCompleted);
assertFalse(_messageCompleted);
// Need to parse more to advance the parser.
handle = parser.parseNext(buffer);
assertTrue(handle);
assertFalse(buffer.hasRemaining());
assertTrue(_messageCompleted);
}
@Test
public void testNoContentHeaderCompleteTrue()
{
HttpParser.ResponseHandler handler = new Handler()
{
@Override
public boolean headerComplete()
{
super.headerComplete();
return true;
}
@Override
public boolean contentComplete()
{
super.contentComplete();
return true;
}
};
HttpParser parser = new HttpParser(handler);
// HTTP 304 does not have a body.
ByteBuffer buffer = BufferUtil.toBuffer(
"HTTP/1.1 304 Not Modified\r\n" +
"\r\n");
boolean handle = parser.parseNext(buffer);
assertTrue(handle);
assertFalse(buffer.hasRemaining());
assertFalse(_contentCompleted);
assertFalse(_messageCompleted);
// Need to parse more to advance the parser.
handle = parser.parseNext(buffer);
assertTrue(handle);
assertFalse(buffer.hasRemaining());
assertTrue(_contentCompleted);
assertFalse(_messageCompleted);
// Need to parse more to advance the parser.
handle = parser.parseNext(buffer);
assertTrue(handle);
assertFalse(buffer.hasRemaining());
assertTrue(_messageCompleted);
}
@Test
public void testCRLFAfterResponseHeaderCompleteTrue()
{
HttpParser.ResponseHandler handler = new Handler()
{
@Override
public boolean headerComplete()
{
super.headerComplete();
return true;
}
};
HttpParser parser = new HttpParser(handler);
ByteBuffer buffer = BufferUtil.toBuffer(
"HTTP/1.1 304 Not Modified\r\n" +
"\r\n" +
"\r\n" +
"\r\n" +
"HTTP/1.1 200 OK\r\n" +
"Content-Length: 0\r\n" +
"\r\n" +
"\r\n" +
"\r\n" +
"HTTP/1.1 303 See Other\r\n" +
"Content-Length: 0\r\n" +
"\r\n");
boolean handle = parser.parseNext(buffer);
assertTrue(handle);
assertTrue(buffer.hasRemaining());
assertEquals("304", _uriOrStatus);
assertFalse(_contentCompleted);
assertFalse(_messageCompleted);
// Need to parse more to advance the parser.
handle = parser.parseNext(buffer);
assertTrue(handle);
assertTrue(buffer.hasRemaining());
assertTrue(_contentCompleted);
assertTrue(_messageCompleted);
// Parse next response.
parser.reset();
init();
handle = parser.parseNext(buffer);
assertTrue(handle);
assertTrue(buffer.hasRemaining());
assertEquals("200", _uriOrStatus);
assertFalse(_contentCompleted);
assertFalse(_messageCompleted);
// Need to parse more to advance the parser.
handle = parser.parseNext(buffer);
assertTrue(handle);
assertTrue(buffer.hasRemaining());
assertTrue(_contentCompleted);
assertTrue(_messageCompleted);
// Parse next response.
parser.reset();
init();
handle = parser.parseNext(buffer);
assertTrue(handle);
assertFalse(buffer.hasRemaining());
assertEquals("303", _uriOrStatus);
assertFalse(_contentCompleted);
assertFalse(_messageCompleted);
// Need to parse more to advance the parser.
handle = parser.parseNext(buffer);
assertTrue(handle);
assertFalse(buffer.hasRemaining());
assertTrue(_contentCompleted);
assertTrue(_messageCompleted);
}
@Test
public void testCRLFAfterResponseContentCompleteTrue()
{
HttpParser.ResponseHandler handler = new Handler()
{
@Override
public boolean contentComplete()
{
super.contentComplete();
return true;
}
};
HttpParser parser = new HttpParser(handler);
ByteBuffer buffer = BufferUtil.toBuffer(
"HTTP/1.1 304 Not Modified\r\n" +
"\r\n" +
"\r\n" +
"\r\n" +
"HTTP/1.1 200 OK\r\n" +
"Content-Length: 0\r\n" +
"\r\n" +
"\r\n" +
"\r\n" +
"HTTP/1.1 303 See Other\r\n" +
"Content-Length: 0\r\n" +
"\r\n");
boolean handle = parser.parseNext(buffer);
assertTrue(handle);
assertTrue(buffer.hasRemaining());
assertEquals("304", _uriOrStatus);
assertTrue(_contentCompleted);
assertFalse(_messageCompleted);
// Need to parse more to advance the parser.
handle = parser.parseNext(buffer);
assertTrue(handle);
assertTrue(buffer.hasRemaining());
assertTrue(_messageCompleted);
// Parse next response.
parser.reset();
init();
handle = parser.parseNext(buffer);
assertTrue(handle);
assertTrue(buffer.hasRemaining());
assertEquals("200", _uriOrStatus);
assertTrue(_contentCompleted);
assertFalse(_messageCompleted);
// Need to parse more to advance the parser.
handle = parser.parseNext(buffer);
assertTrue(handle);
assertTrue(buffer.hasRemaining());
assertTrue(_messageCompleted);
// Parse next response.
parser.reset();
init();
handle = parser.parseNext(buffer);
assertTrue(handle);
assertFalse(buffer.hasRemaining());
assertEquals("303", _uriOrStatus);
assertTrue(_contentCompleted);
assertFalse(_messageCompleted);
// Need to parse more to advance the parser.
handle = parser.parseNext(buffer);
assertTrue(handle);
assertFalse(buffer.hasRemaining());
assertTrue(_messageCompleted);
}
@Test
public void testCRLFAfterResponseMessageCompleteFalse()
{
HttpParser.ResponseHandler handler = new Handler()
{
@Override
public boolean messageComplete()
{
super.messageComplete();
return false;
}
};
HttpParser parser = new HttpParser(handler);
ByteBuffer buffer = BufferUtil.toBuffer(
"HTTP/1.1 304 Not Modified\r\n" +
"\r\n" +
"\r\n" +
"\r\n" +
"HTTP/1.1 200 OK\r\n" +
"Content-Length: 0\r\n" +
"\r\n" +
"\r\n" +
"\r\n" +
"HTTP/1.1 303 See Other\r\n" +
"Content-Length: 0\r\n" +
"\r\n");
boolean handle = parser.parseNext(buffer);
assertFalse(handle);
assertTrue(buffer.hasRemaining());
assertEquals("304", _uriOrStatus);
assertTrue(_contentCompleted);
assertTrue(_messageCompleted);
// Parse next response.
parser.reset();
init();
handle = parser.parseNext(buffer);
assertFalse(handle);
assertTrue(buffer.hasRemaining());
assertEquals("200", _uriOrStatus);
assertTrue(_contentCompleted);
assertTrue(_messageCompleted);
// Parse next response.
parser.reset();
init();
handle = parser.parseNext(buffer);
assertFalse(handle);
assertFalse(buffer.hasRemaining());
assertEquals("303", _uriOrStatus);
assertTrue(_contentCompleted);
assertTrue(_messageCompleted);
}
@Test
public void testSPAfterResponseMessageCompleteFalse()
{
HttpParser.ResponseHandler handler = new Handler()
{
@Override
public boolean messageComplete()
{
super.messageComplete();
return false;
}
};
HttpParser parser = new HttpParser(handler);
ByteBuffer buffer = BufferUtil.toBuffer(
"HTTP/1.1 304 Not Modified\r\n" +
"\r\n" +
" " + // Single SP.
"HTTP/1.1 200 OK\r\n" +
"Content-Length: 0\r\n" +
"\r\n");
boolean handle = parser.parseNext(buffer);
assertFalse(handle);
assertTrue(buffer.hasRemaining());
assertEquals("304", _uriOrStatus);
assertTrue(_contentCompleted);
assertTrue(_messageCompleted);
// Parse next response.
parser.reset();
init();
handle = parser.parseNext(buffer);
assertFalse(handle);
assertFalse(buffer.hasRemaining());
assertNotNull(_bad);
buffer = BufferUtil.toBuffer(
"HTTP/1.1 200 OK\r\n" +
"Content-Length: 0\r\n" +
"\r\n" +
" " + // Single SP.
"HTTP/1.1 303 See Other\r\n" +
"Content-Length: 0\r\n" +
"\r\n");
parser = new HttpParser(handler);
handle = parser.parseNext(buffer);
assertFalse(handle);
assertTrue(buffer.hasRemaining());
assertEquals("200", _uriOrStatus);
assertTrue(_contentCompleted);
assertTrue(_messageCompleted);
// Parse next response.
parser.reset();
init();
handle = parser.parseNext(buffer);
assertFalse(handle);
assertFalse(buffer.hasRemaining());
assertNotNull(_bad);
}
@BeforeEach
public void init()
{
@ -2168,6 +2826,7 @@ public class HttpParserTest
_val = null;
_headers = 0;
_headerCompleted = false;
_contentCompleted = false;
_messageCompleted = false;
_complianceViolation.clear();
}
@ -2186,23 +2845,12 @@ public class HttpParserTest
private int _headers;
private boolean _early;
private boolean _headerCompleted;
private boolean _contentCompleted;
private boolean _messageCompleted;
private final List<ComplianceViolation> _complianceViolation = new ArrayList<>();
private class Handler implements HttpParser.RequestHandler, HttpParser.ResponseHandler, ComplianceViolation.Listener
{
private boolean _headerCacheCaseSensitive;
public Handler()
{
this(false);
}
public Handler(boolean headerCacheCaseSensitive)
{
_headerCacheCaseSensitive = headerCacheCaseSensitive;
}
@Override
public boolean content(ByteBuffer ref)
{
@ -2215,7 +2863,7 @@ public class HttpParserTest
}
@Override
public boolean startRequest(String method, String uri, HttpVersion version)
public void startRequest(String method, String uri, HttpVersion version)
{
_fields.clear();
_trailers.clear();
@ -2228,7 +2876,6 @@ public class HttpParserTest
_messageCompleted = false;
_headerCompleted = false;
_early = false;
return false;
}
@Override
@ -2263,6 +2910,7 @@ public class HttpParserTest
@Override
public boolean contentComplete()
{
_contentCompleted = true;
return false;
}
@ -2281,7 +2929,7 @@ public class HttpParserTest
}
@Override
public boolean startResponse(HttpVersion version, int status, String reason)
public void startResponse(HttpVersion version, int status, String reason)
{
_fields.clear();
_trailers.clear();
@ -2293,7 +2941,6 @@ public class HttpParserTest
_val = new String[10];
_messageCompleted = false;
_headerCompleted = false;
return false;
}
@Override
@ -2302,18 +2949,6 @@ public class HttpParserTest
_early = true;
}
@Override
public int getHeaderCacheSize()
{
return 4096;
}
@Override
public boolean isHeaderCacheCaseSensitive()
{
return _headerCacheCaseSensitive;
}
@Override
public void onComplianceViolation(ComplianceViolation.Mode mode, ComplianceViolation violation, String reason)
{

View File

@ -73,7 +73,7 @@ public class HTTP2ClientConnectionFactory implements ClientConnectionFactory
final HTTP2ClientConnection connection = new HTTP2ClientConnection(client, byteBufferPool, executor, endPoint,
parser, session, client.getInputBufferSize(), promise, listener);
connection.addListener(connectionListener);
connection.addEventListener(connectionListener);
return customize(connection, context);
}

View File

@ -0,0 +1,386 @@
//
// ========================================================================
// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.http2.client;
import java.nio.ByteBuffer;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.http.MetaData;
import org.eclipse.jetty.http2.FlowControlStrategy;
import org.eclipse.jetty.http2.HTTP2Session;
import org.eclipse.jetty.http2.ISession;
import org.eclipse.jetty.http2.api.Session;
import org.eclipse.jetty.http2.api.Stream;
import org.eclipse.jetty.http2.api.server.ServerSessionListener;
import org.eclipse.jetty.http2.frames.DataFrame;
import org.eclipse.jetty.http2.frames.HeadersFrame;
import org.eclipse.jetty.http2.generator.Generator;
import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.io.MappedByteBufferPool;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.FuturePromise;
import org.eclipse.jetty.util.Promise;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
public class DataDemandTest extends AbstractTest
{
@Test
public void testExplicitDemand() throws Exception
{
int length = FlowControlStrategy.DEFAULT_WINDOW_SIZE - 1;
AtomicReference<Stream> serverStreamRef = new AtomicReference<>();
Queue<DataFrame> serverQueue = new ConcurrentLinkedQueue<>();
start(new ServerSessionListener.Adapter()
{
@Override
public Stream.Listener onNewStream(Stream stream, HeadersFrame frame)
{
serverStreamRef.set(stream);
return new Stream.Listener.Adapter()
{
@Override
public void onDataDemanded(Stream stream, DataFrame frame, Callback callback)
{
// Don't demand and don't complete callbacks.
serverQueue.offer(frame);
}
};
}
});
Session client = newClient(new Session.Listener.Adapter());
MetaData.Request post = newRequest("POST", new HttpFields());
FuturePromise<Stream> promise = new FuturePromise<>();
Queue<DataFrame> clientQueue = new ConcurrentLinkedQueue<>();
client.newStream(new HeadersFrame(post, null, false), promise, new Stream.Listener.Adapter()
{
@Override
public void onDataDemanded(Stream stream, DataFrame frame, Callback callback)
{
clientQueue.offer(frame);
}
});
Stream clientStream = promise.get(5, TimeUnit.SECONDS);
// Send a single frame larger than the default frame size,
// so that it will be split on the server in multiple frames.
clientStream.data(new DataFrame(clientStream.getId(), ByteBuffer.allocate(length), true), Callback.NOOP);
// The server should receive only 1 DATA frame because it does explicit demand.
// Wait a bit more to be sure it only receives 1 DATA frame.
Thread.sleep(1000);
assertEquals(1, serverQueue.size());
Stream serverStream = serverStreamRef.get();
assertNotNull(serverStream);
// Demand more DATA frames.
int count = 2;
serverStream.demand(count);
Thread.sleep(1000);
// The server should have received `count` more DATA frames.
assertEquals(1 + count, serverQueue.size());
// Demand all the rest.
serverStream.demand(Long.MAX_VALUE);
int loops = 0;
while (true)
{
if (++loops > 100)
fail();
Thread.sleep(100);
long sum = serverQueue.stream()
.mapToLong(frame -> frame.getData().remaining())
.sum();
if (sum == length)
break;
}
// Even if demanded, the flow control window should not have
// decreased because the callbacks have not been completed.
int recvWindow = ((ISession)serverStream.getSession()).updateRecvWindow(0);
assertEquals(FlowControlStrategy.DEFAULT_WINDOW_SIZE - length, recvWindow);
// Send a large DATA frame to the client.
serverStream.data(new DataFrame(serverStream.getId(), ByteBuffer.allocate(length), true), Callback.NOOP);
// The client should receive only 1 DATA frame because it does explicit demand.
// Wait a bit more to be sure it only receives 1 DATA frame.
Thread.sleep(1000);
assertEquals(1, clientQueue.size());
// Demand more DATA frames.
clientStream.demand(count);
Thread.sleep(1000);
// The client should have received `count` more DATA frames.
assertEquals(1 + count, clientQueue.size());
// Demand all the rest.
clientStream.demand(Long.MAX_VALUE);
loops = 0;
while (true)
{
if (++loops > 100)
fail();
Thread.sleep(100);
long sum = clientQueue.stream()
.mapToLong(frame -> frame.getData().remaining())
.sum();
if (sum == length)
break;
}
// Both the client and server streams should be gone now.
assertNull(clientStream.getSession().getStream(clientStream.getId()));
assertNull(serverStream.getSession().getStream(serverStream.getId()));
}
@Test
public void testOnBeforeData() throws Exception
{
start(new ServerSessionListener.Adapter()
{
@Override
public Stream.Listener onNewStream(Stream stream, HeadersFrame frame)
{
MetaData.Response response = new MetaData.Response(HttpVersion.HTTP_2, HttpStatus.OK_200, new HttpFields());
stream.headers(new HeadersFrame(stream.getId(), response, null, false), Callback.from(() -> sendData(stream), x -> {}));
return null;
}
private void sendData(Stream stream)
{
stream.data(new DataFrame(stream.getId(), ByteBuffer.allocate(1024 * 1024), true), Callback.NOOP);
}
});
Session client = newClient(new Session.Listener.Adapter());
MetaData.Request post = newRequest("GET", new HttpFields());
FuturePromise<Stream> promise = new FuturePromise<>();
CountDownLatch responseLatch = new CountDownLatch(1);
CountDownLatch beforeDataLatch = new CountDownLatch(1);
CountDownLatch latch = new CountDownLatch(1);
client.newStream(new HeadersFrame(post, null, true), promise, new Stream.Listener.Adapter()
{
@Override
public void onHeaders(Stream stream, HeadersFrame frame)
{
MetaData.Response response = (MetaData.Response)frame.getMetaData();
assertEquals(HttpStatus.OK_200, response.getStatus());
responseLatch.countDown();
}
@Override
public void onBeforeData(Stream stream)
{
beforeDataLatch.countDown();
// Don't demand.
}
@Override
public void onData(Stream stream, DataFrame frame, Callback callback)
{
callback.succeeded();
if (frame.isEndStream())
latch.countDown();
}
});
Stream clientStream = promise.get(5, TimeUnit.SECONDS);
assertTrue(responseLatch.await(5, TimeUnit.SECONDS));
assertTrue(beforeDataLatch.await(5, TimeUnit.SECONDS));
// Should not receive DATA frames until demanded.
assertFalse(latch.await(1, TimeUnit.SECONDS));
// Now demand the first DATA frame.
clientStream.demand(1);
assertTrue(latch.await(5, TimeUnit.SECONDS));
}
@Test
public void testDemandFromOnHeaders() throws Exception
{
start(new ServerSessionListener.Adapter()
{
@Override
public Stream.Listener onNewStream(Stream stream, HeadersFrame frame)
{
MetaData.Response response = new MetaData.Response(HttpVersion.HTTP_2, HttpStatus.OK_200, new HttpFields());
stream.headers(new HeadersFrame(stream.getId(), response, null, false), Callback.from(() -> sendData(stream), x -> {}));
return null;
}
private void sendData(Stream stream)
{
stream.data(new DataFrame(stream.getId(), ByteBuffer.allocate(1024 * 1024), true), Callback.NOOP);
}
});
Session client = newClient(new Session.Listener.Adapter());
MetaData.Request post = newRequest("GET", new HttpFields());
CountDownLatch latch = new CountDownLatch(1);
client.newStream(new HeadersFrame(post, null, true), new Promise.Adapter<>(), new Stream.Listener.Adapter()
{
@Override
public void onHeaders(Stream stream, HeadersFrame frame)
{
stream.demand(1);
}
@Override
public void onBeforeData(Stream stream)
{
// Do not demand from here, we have already demanded in onHeaders().
}
@Override
public void onData(Stream stream, DataFrame frame, Callback callback)
{
callback.succeeded();
if (frame.isEndStream())
latch.countDown();
}
});
assertTrue(latch.await(5, TimeUnit.SECONDS));
}
@Test
public void testOnBeforeDataDoesNotReenter() throws Exception
{
start(new ServerSessionListener.Adapter()
{
@Override
public Stream.Listener onNewStream(Stream stream, HeadersFrame frame)
{
MetaData.Response response = new MetaData.Response(HttpVersion.HTTP_2, HttpStatus.OK_200, new HttpFields());
stream.headers(new HeadersFrame(stream.getId(), response, null, false), Callback.from(() -> sendData(stream), x -> {}));
return null;
}
private void sendData(Stream stream)
{
stream.data(new DataFrame(stream.getId(), ByteBuffer.allocate(1024 * 1024), true), Callback.NOOP);
}
});
Session client = newClient(new Session.Listener.Adapter());
MetaData.Request post = newRequest("GET", new HttpFields());
CountDownLatch latch = new CountDownLatch(1);
client.newStream(new HeadersFrame(post, null, true), new Promise.Adapter<>(), new Stream.Listener.Adapter()
{
private boolean inBeforeData;
@Override
public void onBeforeData(Stream stream)
{
inBeforeData = true;
stream.demand(1);
inBeforeData = false;
}
@Override
public void onData(Stream stream, DataFrame frame, Callback callback)
{
assertFalse(inBeforeData);
callback.succeeded();
if (frame.isEndStream())
latch.countDown();
}
});
assertTrue(latch.await(5, TimeUnit.SECONDS));
}
@Test
public void testSynchronousDemandDoesNotStackOverflow() throws Exception
{
start(new ServerSessionListener.Adapter()
{
@Override
public Stream.Listener onNewStream(Stream stream, HeadersFrame frame)
{
return new Stream.Listener.Adapter()
{
@Override
public void onDataDemanded(Stream stream, DataFrame frame, Callback callback)
{
callback.succeeded();
stream.demand(1);
if (frame.isEndStream())
{
MetaData.Response response = new MetaData.Response(HttpVersion.HTTP_2, HttpStatus.OK_200, new HttpFields());
stream.headers(new HeadersFrame(stream.getId(), response, null, true), Callback.NOOP);
}
}
};
}
});
Session client = newClient(new Session.Listener.Adapter());
MetaData.Request post = newRequest("POST", new HttpFields());
FuturePromise<Stream> promise = new FuturePromise<>();
CountDownLatch latch = new CountDownLatch(1);
client.newStream(new HeadersFrame(post, null, false), promise, new Stream.Listener.Adapter()
{
@Override
public void onHeaders(Stream stream, HeadersFrame frame)
{
if (frame.isEndStream())
{
MetaData.Response response = (MetaData.Response)frame.getMetaData();
assertEquals(HttpStatus.OK_200, response.getStatus());
latch.countDown();
}
}
});
Stream clientStream = promise.get(5, TimeUnit.SECONDS);
// Generate a lot of small DATA frames and write them in a single
// write so that the server will continuously be notified and demand,
// which will test that it won't throw StackOverflowError.
MappedByteBufferPool byteBufferPool = new MappedByteBufferPool();
Generator generator = new Generator(byteBufferPool);
ByteBufferPool.Lease lease = new ByteBufferPool.Lease(byteBufferPool);
for (int i = 512; i >= 0; --i)
generator.data(lease, new DataFrame(clientStream.getId(), ByteBuffer.allocate(1), i == 0), 1);
// Since this is a naked write, we need to wait that the
// client finishes writing the SETTINGS reply to the server
// during connection initialization, or we risk a WritePendingException.
Thread.sleep(1000);
((HTTP2Session)clientStream.getSession()).getEndPoint().write(Callback.NOOP, lease.getByteBuffers().toArray(new ByteBuffer[0]));
assertTrue(latch.await(15, TimeUnit.SECONDS));
}
}

View File

@ -21,6 +21,8 @@ package org.eclipse.jetty.http2;
import java.io.EOFException;
import java.io.IOException;
import java.nio.channels.WritePendingException;
import java.util.ArrayDeque;
import java.util.Queue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
@ -42,16 +44,20 @@ import org.eclipse.jetty.http2.frames.ResetFrame;
import org.eclipse.jetty.http2.frames.WindowUpdateFrame;
import org.eclipse.jetty.io.IdleTimeout;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.MathUtils;
import org.eclipse.jetty.util.Promise;
import org.eclipse.jetty.util.component.Dumpable;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
import org.eclipse.jetty.util.thread.AutoLock;
import org.eclipse.jetty.util.thread.Scheduler;
public class HTTP2Stream extends IdleTimeout implements IStream, Callback, Dumpable
{
private static final Logger LOG = Log.getLogger(HTTP2Stream.class);
private final AutoLock lock = new AutoLock();
private final Queue<DataEntry> dataQueue = new ArrayDeque<>();
private final AtomicReference<Object> attachment = new AtomicReference<>();
private final AtomicReference<ConcurrentMap<String, Object>> attributes = new AtomicReference<>();
private final AtomicReference<CloseState> closeState = new AtomicReference<>(CloseState.NOT_CLOSED);
@ -67,6 +73,9 @@ public class HTTP2Stream extends IdleTimeout implements IStream, Callback, Dumpa
private Listener listener;
private boolean remoteReset;
private long dataLength;
private long dataDemand;
private boolean dataInitial;
private boolean dataProcess;
public HTTP2Stream(Scheduler scheduler, ISession session, int streamId, MetaData.Request request, boolean local)
{
@ -76,6 +85,7 @@ public class HTTP2Stream extends IdleTimeout implements IStream, Callback, Dumpa
this.request = request;
this.local = local;
this.dataLength = Long.MIN_VALUE;
this.dataInitial = true;
}
@Override
@ -343,10 +353,88 @@ public class HTTP2Stream extends IdleTimeout implements IStream, Callback, Dumpa
}
}
if (updateClose(frame.isEndStream(), CloseState.Event.RECEIVED))
session.removeStream(this);
boolean initial;
boolean proceed = false;
DataEntry entry = new DataEntry(frame, callback);
try (AutoLock l = lock.lock())
{
dataQueue.offer(entry);
initial = dataInitial;
if (initial)
{
dataInitial = false;
// Fake that we are processing data so we return
// from onBeforeData() before calling onData().
dataProcess = true;
}
else if (!dataProcess)
{
dataProcess = proceed = dataDemand > 0;
}
}
if (LOG.isDebugEnabled())
LOG.debug("{} data processing of {} for {}", initial ? "Starting" : proceed ? "Proceeding" : "Stalling", frame, this);
if (initial)
{
notifyBeforeData(this);
try (AutoLock l = lock.lock())
{
dataProcess = proceed = dataDemand > 0;
}
}
if (proceed)
processData();
}
notifyData(this, frame, callback);
@Override
public void demand(long n)
{
if (n <= 0)
throw new IllegalArgumentException("Invalid demand " + n);
long demand;
boolean proceed = false;
try (AutoLock l = lock.lock())
{
demand = dataDemand = MathUtils.cappedAdd(dataDemand, n);
if (!dataProcess)
dataProcess = proceed = !dataQueue.isEmpty();
}
if (LOG.isDebugEnabled())
LOG.debug("Demand {}/{}, {} data processing for {}", n, demand, proceed ? "proceeding" : "stalling", this);
if (proceed)
processData();
}
private void processData()
{
while (true)
{
DataEntry dataEntry;
try (AutoLock l = lock.lock())
{
if (dataQueue.isEmpty() || dataDemand == 0)
{
if (LOG.isDebugEnabled())
LOG.debug("Stalling data processing for {}", this);
dataProcess = false;
return;
}
--dataDemand;
dataEntry = dataQueue.poll();
}
DataFrame frame = dataEntry.frame;
if (updateClose(frame.isEndStream(), CloseState.Event.RECEIVED))
session.removeStream(this);
notifyDataDemanded(this, frame, dataEntry.callback);
}
}
private long demand()
{
try (AutoLock l = lock.lock())
{
return dataDemand;
}
}
private void onReset(ResetFrame frame, Callback callback)
@ -573,14 +661,34 @@ public class HTTP2Stream extends IdleTimeout implements IStream, Callback, Dumpa
}
}
private void notifyData(Stream stream, DataFrame frame, Callback callback)
private void notifyBeforeData(Stream stream)
{
Listener listener = this.listener;
if (listener != null)
{
try
{
listener.onData(stream, frame, callback);
listener.onBeforeData(stream);
}
catch (Throwable x)
{
LOG.info("Failure while notifying listener " + listener, x);
}
}
else
{
stream.demand(1);
}
}
private void notifyDataDemanded(Stream stream, DataFrame frame, Callback callback)
{
Listener listener = this.listener;
if (listener != null)
{
try
{
listener.onDataDemanded(stream, frame, callback);
}
catch (Throwable x)
{
@ -591,6 +699,7 @@ public class HTTP2Stream extends IdleTimeout implements IStream, Callback, Dumpa
else
{
callback.succeeded();
stream.demand(1);
}
}
@ -682,16 +791,29 @@ public class HTTP2Stream extends IdleTimeout implements IStream, Callback, Dumpa
@Override
public String toString()
{
return String.format("%s@%x#%d{sendWindow=%s,recvWindow=%s,reset=%b/%b,%s,age=%d,attachment=%s}",
return String.format("%s@%x#%d{sendWindow=%s,recvWindow=%s,demand=%d,reset=%b/%b,%s,age=%d,attachment=%s}",
getClass().getSimpleName(),
hashCode(),
getId(),
sendWindow,
recvWindow,
demand(),
localReset,
remoteReset,
closeState,
TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - timeStamp),
attachment);
}
private static class DataEntry
{
private final DataFrame frame;
private final Callback callback;
private DataEntry(DataFrame frame, Callback callback)
{
this.frame = frame;
this.callback = callback;
}
}
}

View File

@ -29,7 +29,7 @@ import org.eclipse.jetty.util.Promise;
* <p>A {@link Stream} represents a bidirectional exchange of data on top of a {@link Session}.</p>
* <p>Differently from socket streams, where the input and output streams are permanently associated
* with the socket (and hence with the connection that the socket represents), there can be multiple
* HTTP/2 streams present concurrent for an HTTP/2 session.</p>
* HTTP/2 streams present concurrently for an HTTP/2 session.</p>
* <p>A {@link Stream} maps to an HTTP request/response cycle, and after the request/response cycle is
* completed, the stream is closed and removed from the session.</p>
* <p>Like {@link Session}, {@link Stream} is the active part and by calling its API applications
@ -129,9 +129,25 @@ public interface Stream
*/
public void setIdleTimeout(long idleTimeout);
/**
* <p>Demands {@code n} more {@code DATA} frames for this stream.</p>
*
* @param n the increment of the demand, must be greater than zero
* @see Listener#onDataDemanded(Stream, DataFrame, Callback)
*/
public void demand(long n);
/**
* <p>A {@link Stream.Listener} is the passive counterpart of a {@link Stream} and receives
* events happening on an HTTP/2 stream.</p>
* <p>HTTP/2 data is flow controlled - this means that only a finite number of data events
* are delivered, until the flow control window is exhausted.</p>
* <p>Applications control the delivery of data events by requesting them via
* {@link Stream#demand(long)}; the first event is always delivered, while subsequent
* events must be explicitly demanded.</p>
* <p>Applications control the HTTP/2 flow control by completing the callback associated
* with data events - this allows the implementation to recycle the data buffer and
* eventually to enlarge the flow control window so that the sender can send more data.</p>
*
* @see Stream
*/
@ -164,15 +180,42 @@ public interface Stream
*/
public Listener onPush(Stream stream, PushPromiseFrame frame);
/**
* <p>Callback method invoked before notifying the first DATA frame.</p>
* <p>The default implementation initializes the demand for DATA frames.</p>
*
* @param stream the stream
*/
public default void onBeforeData(Stream stream)
{
stream.demand(1);
}
/**
* <p>Callback method invoked when a DATA frame has been received.</p>
*
* @param stream the stream
* @param frame the DATA frame received
* @param callback the callback to complete when the bytes of the DATA frame have been consumed
* @see #onDataDemanded(Stream, DataFrame, Callback)
*/
public void onData(Stream stream, DataFrame frame, Callback callback);
/**
* <p>Callback method invoked when a DATA frame has been demanded.</p>
* <p>Implementations of this method must arrange to call (within the
* method or otherwise asynchronously) {@link #demand(long)}.</p>
*
* @param stream the stream
* @param frame the DATA frame received
* @param callback the callback to complete when the bytes of the DATA frame have been consumed
*/
public default void onDataDemanded(Stream stream, DataFrame frame, Callback callback)
{
onData(stream, frame, callback);
stream.demand(1);
}
/**
* <p>Callback method invoked when a RST_STREAM frame has been received for this stream.</p>
*

View File

@ -18,6 +18,8 @@
package org.eclipse.jetty.http2.parser;
import org.eclipse.jetty.io.EndPoint;
/**
* Controls rate of events via {@link #onEvent(Object)}.
*/
@ -36,4 +38,19 @@ public interface RateControl
* @return true IFF the rate is within limits
*/
public boolean onEvent(Object event);
/**
* Factory to create RateControl instances.
*/
public interface Factory
{
/**
* @return a new RateControl instance for the given EndPoint
* @param endPoint the EndPoint for which the RateControl is created
*/
public default RateControl newRateControl(EndPoint endPoint)
{
return NO_RATE_CONTROL;
}
}
}

View File

@ -23,6 +23,8 @@ import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicInteger;
import org.eclipse.jetty.io.EndPoint;
/**
* <p>An implementation of {@link RateControl} that limits the number of
* events within a time period.</p>
@ -48,6 +50,21 @@ public class WindowRateControl implements RateControl
{
this.maxEvents = maxEvents;
this.window = window.toNanos();
if (this.window == 0)
throw new IllegalArgumentException("Invalid duration " + window);
}
public int getEventsPerSecond()
{
try
{
long rate = maxEvents * 1_000_000_000L / window;
return Math.toIntExact(rate);
}
catch (ArithmeticException x)
{
return Integer.MAX_VALUE;
}
}
@Override
@ -67,4 +84,20 @@ public class WindowRateControl implements RateControl
events.add(now + window);
return size.incrementAndGet() <= maxEvents;
}
public static class Factory implements RateControl.Factory
{
private final int maxEventRate;
public Factory(int maxEventRate)
{
this.maxEventRate = maxEventRate;
}
@Override
public RateControl newRateControl(EndPoint endPoint)
{
return WindowRateControl.fromEventsPerSecond(maxEventRate);
}
}
}

View File

@ -418,7 +418,7 @@ public class HpackEncoder
if (_debug)
{
if (LOG.isDebugEnabled())
LOG.debug("encode {}:'{}' to '{}'", encoding, field, BufferUtil.toHexString(buffer));
LOG.debug("encode {}:'{}' to '{}'", encoding, field, BufferUtil.toHexString(buffer.duplicate().flip()));
}
}

View File

@ -120,12 +120,24 @@ public class HttpReceiverOverHTTP2 extends HttpReceiver implements HTTP2Channel.
if (frame.isEndStream() || informational)
responseSuccess(exchange);
}
else
{
if (frame.isEndStream())
{
// There is no demand to trigger response success, so add
// a poison pill to trigger it when there will be demand.
notifyContent(exchange, new DataFrame(stream.getId(), BufferUtil.EMPTY_BUFFER, true), Callback.NOOP);
}
}
}
}
else // Response trailers.
{
HttpFields trailers = metaData.getFields();
trailers.forEach(httpResponse::trailer);
// Previous DataFrames had endStream=false, so
// add a poison pill to trigger response success
// after all normal DataFrames have been consumed.
notifyContent(exchange, new DataFrame(stream.getId(), BufferUtil.EMPTY_BUFFER, true), Callback.NOOP);
}
}
@ -212,7 +224,7 @@ public class HttpReceiverOverHTTP2 extends HttpReceiver implements HTTP2Channel.
contentNotifier.offer(exchange, frame, callback);
}
private static class ContentNotifier
private class ContentNotifier
{
private final Queue<DataInfo> queue = new ArrayDeque<>();
private final HttpReceiverOverHTTP2 receiver;
@ -246,9 +258,25 @@ public class HttpReceiverOverHTTP2 extends HttpReceiver implements HTTP2Channel.
private void process(boolean resume)
{
// Allow only one thread at a time.
if (active(resume))
boolean busy = active(resume);
if (LOG.isDebugEnabled())
LOG.debug("Resuming({}) processing({}) of content", resume, !busy);
if (busy)
return;
// Process only if there is demand.
synchronized (this)
{
if (!resume && demand() <= 0)
{
if (LOG.isDebugEnabled())
LOG.debug("Stalling processing, content available but no demand");
active = false;
stalled = true;
return;
}
}
while (true)
{
if (dataInfo != null)
@ -265,7 +293,7 @@ public class HttpReceiverOverHTTP2 extends HttpReceiver implements HTTP2Channel.
{
dataInfo = queue.poll();
if (LOG.isDebugEnabled())
LOG.debug("Dequeued content {}", dataInfo);
LOG.debug("Processing content {}", dataInfo);
if (dataInfo == null)
{
active = false;
@ -281,8 +309,12 @@ public class HttpReceiverOverHTTP2 extends HttpReceiver implements HTTP2Channel.
boolean proceed = receiver.responseContent(dataInfo.exchange, buffer, Callback.from(callback::succeeded, x -> fail(callback, x)));
if (!proceed)
{
// Should stall, unless just resumed.
if (stall())
// The call to responseContent() said we should
// stall, but another thread may have just resumed.
boolean stall = stall();
if (LOG.isDebugEnabled())
LOG.debug("Stalling({}) processing", stall);
if (stall)
return;
}
}
@ -299,27 +331,46 @@ public class HttpReceiverOverHTTP2 extends HttpReceiver implements HTTP2Channel.
{
if (active)
{
// There is a thread in process(),
// but it may be about to exit, so
// remember "resume" to signal the
// processing thread to continue.
if (resume)
this.resume = true;
return true;
}
// If there is no demand (i.e. stalled
// and not resuming) then don't process.
if (stalled && !resume)
return true;
// Start processing.
active = true;
stalled = false;
return false;
}
}
/**
* Called when there is no demand, this method checks whether
* the processing should really stop or it should continue.
*
* @return true to stop processing, false to continue processing
*/
private boolean stall()
{
synchronized (this)
{
if (resume)
{
// There was no demand, but another thread
// just demanded, continue processing.
resume = false;
return false;
}
// There is no demand, stop processing.
active = false;
stalled = true;
return true;
@ -344,7 +395,7 @@ public class HttpReceiverOverHTTP2 extends HttpReceiver implements HTTP2Channel.
receiver.responseFailure(failure);
}
private static class DataInfo
private class DataInfo
{
private final HttpExchange exchange;
private final DataFrame frame;

View File

@ -1,9 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "https://www.eclipse.org/jetty/configure_10_0.dtd">
<!-- ============================================================= -->
<!-- Configure an HTTP2 on the ssl connector. -->
<!-- ============================================================= -->
<Configure id="sslConnector" class="org.eclipse.jetty.server.ServerConnector">
<Call name="addConnectionFactory">
<Arg>
@ -13,10 +10,10 @@
<Set name="initialStreamRecvWindow" property="jetty.http2.initialStreamRecvWindow"/>
<Set name="initialSessionRecvWindow" property="jetty.http2.initialSessionRecvWindow"/>
<Set name="maxSettingsKeys"><Property name="jetty.http2.maxSettingsKeys" default="64"/></Set>
<Set name="rateControl">
<Call class="org.eclipse.jetty.http2.parser.WindowRateControl" name="fromEventsPerSecond">
<Set name="rateControlFactory">
<New class="org.eclipse.jetty.http2.parser.WindowRateControl$Factory">
<Arg type="int"><Property name="jetty.http2.rateControl.maxEventsPerSecond" default="20"/></Arg>
</Call>
</New>
</Set>
</New>
</Arg>

View File

@ -1,9 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "https://www.eclipse.org/jetty/configure_10_0.dtd">
<!-- ============================================================= -->
<!-- Configure an HTTP2 on the ssl connector. -->
<!-- ============================================================= -->
<Configure id="httpConnector" class="org.eclipse.jetty.server.ServerConnector">
<Call name="addConnectionFactory">
<Arg>
@ -12,10 +9,10 @@
<Set name="maxConcurrentStreams" property="jetty.http2c.maxConcurrentStreams"/>
<Set name="initialStreamRecvWindow" property="jetty.http2c.initialStreamRecvWindow"/>
<Set name="maxSettingsKeys"><Property name="jetty.http2.maxSettingsKeys" default="64"/></Set>
<Set name="rateControl">
<Call class="org.eclipse.jetty.http2.parser.WindowRateControl" name="fromEventsPerSecond">
<Set name="rateControlFactory">
<New class="org.eclipse.jetty.http2.parser.WindowRateControl$Factory">
<Arg type="int"><Property name="jetty.http2.rateControl.maxEventsPerSecond" default="20"/></Arg>
</Call>
</New>
</Set>
</New>
</Arg>

View File

@ -19,7 +19,6 @@
package org.eclipse.jetty.http2.server;
import java.io.IOException;
import java.time.Duration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
@ -61,7 +60,7 @@ public abstract class AbstractHTTP2ServerConnectionFactory extends AbstractConne
private int maxHeaderBlockFragment = 0;
private int maxFrameLength = Frame.DEFAULT_MAX_LENGTH;
private int maxSettingsKeys = SettingsFrame.DEFAULT_MAX_KEYS;
private RateControl rateControl = new WindowRateControl(20, Duration.ofSeconds(1));
private RateControl.Factory rateControlFactory = new WindowRateControl.Factory(20);
private FlowControlStrategy.Factory flowControlStrategyFactory = () -> new BufferingFlowControlStrategy(0.5F);
private long streamIdleTimeout;
private boolean _useInputDirectByteBuffers;
@ -186,14 +185,22 @@ public abstract class AbstractHTTP2ServerConnectionFactory extends AbstractConne
this.maxSettingsKeys = maxSettingsKeys;
}
public RateControl getRateControl()
/**
* @return the factory that creates RateControl objects
*/
public RateControl.Factory getRateControlFactory()
{
return rateControl;
return rateControlFactory;
}
public void setRateControl(RateControl rateControl)
/**
* <p>Sets the factory that creates a per-connection RateControl object.</p>
*
* @param rateControlFactory the factory that creates RateControl objects
*/
public void setRateControlFactory(RateControl.Factory rateControlFactory)
{
this.rateControl = rateControl;
this.rateControlFactory = Objects.requireNonNull(rateControlFactory);
}
public boolean isUseInputDirectByteBuffers()
@ -253,7 +260,7 @@ public abstract class AbstractHTTP2ServerConnectionFactory extends AbstractConne
session.setInitialSessionRecvWindow(getInitialSessionRecvWindow());
session.setWriteThreshold(getHttpConfiguration().getOutputBufferSize());
ServerParser parser = newServerParser(connector, session, getRateControl());
ServerParser parser = newServerParser(connector, session, getRateControlFactory().newRateControl(endPoint));
parser.setMaxFrameLength(getMaxFrameLength());
parser.setMaxSettingsKeys(getMaxSettingsKeys());
@ -261,7 +268,7 @@ public abstract class AbstractHTTP2ServerConnectionFactory extends AbstractConne
endPoint, httpConfiguration, parser, session, getInputBufferSize(), listener);
connection.setUseInputDirectByteBuffers(isUseInputDirectByteBuffers());
connection.setUseOutputDirectByteBuffers(isUseOutputDirectByteBuffers());
connection.addListener(sessionContainer);
connection.addEventListener(sessionContainer);
return configure(connection, connector, endPoint);
}

View File

@ -18,6 +18,7 @@
package org.eclipse.jetty.io;
import java.util.EventListener;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.Executor;
@ -57,13 +58,14 @@ public abstract class AbstractConnection implements Connection
}
@Override
public void addListener(Listener listener)
public void addEventListener(EventListener listener)
{
_listeners.add(listener);
if (listener instanceof Listener)
_listeners.add((Listener)listener);
}
@Override
public void removeListener(Listener listener)
public void removeEventListener(EventListener listener)
{
_listeners.remove(listener);
}

View File

@ -19,6 +19,7 @@
package org.eclipse.jetty.io;
import java.io.IOException;
import java.util.EventListener;
import java.util.List;
import java.util.Map;
@ -43,7 +44,7 @@ public interface ClientConnectionFactory
{
ContainerLifeCycle client = (ContainerLifeCycle)context.get(CLIENT_CONTEXT_KEY);
if (client != null)
client.getBeans(Connection.Listener.class).forEach(connection::addListener);
client.getBeans(EventListener.class).forEach(connection::addEventListener);
return connection;
}

View File

@ -20,6 +20,7 @@ package org.eclipse.jetty.io;
import java.io.Closeable;
import java.nio.ByteBuffer;
import java.util.EventListener;
import org.eclipse.jetty.util.component.Container;
@ -38,14 +39,14 @@ public interface Connection extends Closeable
*
* @param listener the listener to add
*/
public void addListener(Listener listener);
public void addEventListener(EventListener listener);
/**
* <p>Removes a listener of connection events.</p>
*
* @param listener the listener to remove
*/
public void removeListener(Listener listener);
public void removeEventListener(EventListener listener);
/**
* <p>Callback method invoked when this connection is opened.</p>
@ -133,7 +134,7 @@ public interface Connection extends Closeable
* the Connector or ConnectionFactory are added as listeners to all new connections
* </p>
*/
public interface Listener
public interface Listener extends EventListener
{
public void onOpened(Connection connection);

View File

@ -38,6 +38,7 @@ import java.util.function.IntUnaryOperator;
import org.eclipse.jetty.util.ProcessorUtils;
import org.eclipse.jetty.util.annotation.ManagedAttribute;
import org.eclipse.jetty.util.annotation.ManagedObject;
import org.eclipse.jetty.util.component.Container;
import org.eclipse.jetty.util.component.ContainerLifeCycle;
import org.eclipse.jetty.util.component.Dumpable;
import org.eclipse.jetty.util.log.Log;
@ -389,31 +390,37 @@ public abstract class SelectorManager extends ContainerLifeCycle implements Dump
*/
public abstract Connection newConnection(SelectableChannel channel, EndPoint endpoint, Object attachment) throws IOException;
public void addEventListener(EventListener listener)
/**
* @param listener An EventListener
* @see AcceptListener
* @see Container#addEventListener(EventListener)
*/
@Override
public boolean addEventListener(EventListener listener)
{
if (isRunning())
throw new IllegalStateException(this.toString());
if (listener instanceof AcceptListener)
addAcceptListener(AcceptListener.class.cast(listener));
if (super.addEventListener(listener))
{
if (listener instanceof AcceptListener)
_acceptListeners.add((AcceptListener)listener);
return true;
}
return false;
}
public void removeEventListener(EventListener listener)
@Override
public boolean removeEventListener(EventListener listener)
{
if (isRunning())
throw new IllegalStateException(this.toString());
if (listener instanceof AcceptListener)
removeAcceptListener(AcceptListener.class.cast(listener));
}
public void addAcceptListener(AcceptListener listener)
{
if (!_acceptListeners.contains(listener))
_acceptListeners.add(listener);
}
public void removeAcceptListener(AcceptListener listener)
{
_acceptListeners.remove(listener);
if (super.removeEventListener(listener))
{
if (listener instanceof AcceptListener)
_acceptListeners.remove(listener);
return true;
}
return false;
}
protected void onAccepting(SelectableChannel channel)
@ -461,12 +468,16 @@ public abstract class SelectorManager extends ContainerLifeCycle implements Dump
}
}
public interface SelectorManagerListener extends EventListener
{
}
/**
* <p>A listener for accept events.</p>
* <p>This listener is called from either the selector or acceptor thread
* and implementations must be non blocking and fast.</p>
*/
public interface AcceptListener extends EventListener
public interface AcceptListener extends SelectorManagerListener
{
/**
* Called immediately after a new SelectableChannel is accepted, but

View File

@ -571,7 +571,8 @@ public abstract class WriteFlusher
}
/**
* <p>A listener of {@link WriteFlusher} events.</p>
* <p>A listener of {@link WriteFlusher} events.
* If implemented by a Connection class, the {@link #onFlushed(long)} event will be delivered to it.</p>
*/
public interface Listener
{

View File

@ -8,7 +8,7 @@
<Set name="outputBufferSize">32768</Set>
<Set name="requestHeaderSize">8192</Set>
<Set name="responseHeaderSize">8192</Set>
<Set name="headerCacheSize">4096</Set>
<Set name="headerCacheSize">1024</Set>
</New>
<Call name="addConnector">
@ -24,7 +24,7 @@
</Item>
</Array>
</Arg>
<Call name="addLifeCycleListener">
<Call name="addEventListener">
<Arg>
<New class="org.eclipse.jetty.maven.plugin.ServerConnectorListener">
<Set name="fileName"><Property name="jetty.port.file" default="port.txt"/></Set>

View File

@ -8,7 +8,7 @@
<Set name="outputBufferSize">32768</Set>
<Set name="requestHeaderSize">8192</Set>
<Set name="responseHeaderSize">8192</Set>
<Set name="headerCacheSize">4096</Set>
<Set name="headerCacheSize">1024</Set>
</New>
<Call name="addConnector">
@ -24,7 +24,7 @@
</Item>
</Array>
</Arg>
<Call name="addLifeCycleListener">
<Call name="addEventListener">
<Arg>
<New class="org.eclipse.jetty.maven.plugin.ServerConnectorListener">
<Set name="fileName"><Property name="jetty.port.file" default="port.txt"/></Set>

View File

@ -10,7 +10,7 @@
<Set name="outputBufferSize">32768</Set>
<Set name="requestHeaderSize">8192</Set>
<Set name="responseHeaderSize">8192</Set>
<Set name="headerCacheSize">4096</Set>
<Set name="headerCacheSize">1024</Set>
</New>
<Call name="addConnector">
@ -30,7 +30,7 @@
</Item>
</Array>
</Arg>
<Call name="addLifeCycleListener">
<Call name="addEventListener">
<Arg>
<New class="org.eclipse.jetty.maven.plugin.ServerConnectorListener">
</New>

View File

@ -3,7 +3,7 @@
<Configure id="Server" class="org.eclipse.jetty.server.Server">
<Ref id="httpConnector">
<Call name="addLifeCycleListener">
<Call name="addEventListener">
<Arg>
<New class="org.eclipse.jetty.maven.plugin.ServerConnectorListener">
<Set name="fileName"><Property name="jetty.port.file" default="port.txt"/></Set>

View File

@ -8,7 +8,7 @@
<Set name="outputBufferSize">32768</Set>
<Set name="requestHeaderSize">8192</Set>
<Set name="responseHeaderSize">8192</Set>
<Set name="headerCacheSize">4096</Set>
<Set name="headerCacheSize">1024</Set>
</New>
<Call name="addConnector">
@ -24,7 +24,7 @@
</Item>
</Array>
</Arg>
<Call name="addLifeCycleListener">
<Call name="addEventListener">
<Arg>
<New class="org.eclipse.jetty.maven.plugin.ServerConnectorListener">
<Set name="fileName"><Property name="jetty.port.file" default="port.txt"/></Set>

View File

@ -8,7 +8,7 @@
<Set name="outputBufferSize">32768</Set>
<Set name="requestHeaderSize">8192</Set>
<Set name="responseHeaderSize">8192</Set>
<Set name="headerCacheSize">4096</Set>
<Set name="headerCacheSize">1024</Set>
</New>
<Call name="addConnector">
@ -24,7 +24,7 @@
</Item>
</Array>
</Arg>
<Call name="addLifeCycleListener">
<Call name="addEventListener">
<Arg>
<New class="org.eclipse.jetty.maven.plugin.ServerConnectorListener">
<Set name="fileName"><Property name="jetty.port.file" default="port.txt"/></Set>

View File

@ -8,7 +8,7 @@
<Set name="outputBufferSize">32768</Set>
<Set name="requestHeaderSize">8192</Set>
<Set name="responseHeaderSize">8192</Set>
<Set name="headerCacheSize">4096</Set>
<Set name="headerCacheSize">1024</Set>
</New>
<Call name="addConnector">
@ -24,7 +24,7 @@
</Item>
</Array>
</Arg>
<Call name="addLifeCycleListener">
<Call name="addEventListener">
<Arg>
<New class="org.eclipse.jetty.maven.plugin.ServerConnectorListener">
<Set name="fileName"><Property name="jetty.port.file" default="port.txt"/></Set>

View File

@ -8,7 +8,7 @@
<Set name="outputBufferSize">32768</Set>
<Set name="requestHeaderSize">8192</Set>
<Set name="responseHeaderSize">8192</Set>
<Set name="headerCacheSize">4096</Set>
<Set name="headerCacheSize">1024</Set>
</New>
<Call name="addConnector">
@ -24,7 +24,7 @@
</Item>
</Array>
</Arg>
<Call name="addLifeCycleListener">
<Call name="addEventListener">
<Arg>
<New class="org.eclipse.jetty.maven.plugin.ServerConnectorListener">
<Set name="fileName"><Property name="jetty.port.file" default="port.txt"/></Set>

View File

@ -8,7 +8,7 @@
<Set name="outputBufferSize">32768</Set>
<Set name="requestHeaderSize">8192</Set>
<Set name="responseHeaderSize">8192</Set>
<Set name="headerCacheSize">4096</Set>
<Set name="headerCacheSize">1024</Set>
</New>
<Call name="addConnector">
@ -24,7 +24,7 @@
</Item>
</Array>
</Arg>
<Call name="addLifeCycleListener">
<Call name="addEventListener">
<Arg>
<New class="org.eclipse.jetty.maven.plugin.ServerConnectorListener">
<Set name="fileName"><Property name="jetty.port.file" default="port.txt"/></Set>

View File

@ -8,7 +8,7 @@
<Set name="outputBufferSize">32768</Set>
<Set name="requestHeaderSize">8192</Set>
<Set name="responseHeaderSize">8192</Set>
<Set name="headerCacheSize">4096</Set>
<Set name="headerCacheSize">1024</Set>
</New>
<Call name="addConnector">
@ -24,7 +24,7 @@
</Item>
</Array>
</Arg>
<Call name="addLifeCycleListener">
<Call name="addEventListener">
<Arg>
<New class="org.eclipse.jetty.maven.plugin.ServerConnectorListener">
<Set name="fileName"><Property name="jetty.port.file" default="port.txt"/></Set>

View File

@ -23,7 +23,6 @@ import java.io.IOException;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.util.ArrayList;
import java.util.EventListener;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@ -347,11 +346,9 @@ public class MavenWebAppContext extends WebAppContext
super.doStop();
// remove all listeners, servlets and filters. This is because we will
// re-apply
// any context xml file, which means they would potentially be added
// multiple times.
setEventListeners(new EventListener[0]);
// remove all servlets and filters. This is because we will
// re-appy any context xml file, which means they would potentially be
// added multiple times.
getServletHandler().setFilters(new FilterHolder[0]);
getServletHandler().setFilterMappings(new FilterMapping[0]);
getServletHandler().setServlets(new ServletHolder[0]);

View File

@ -2,7 +2,7 @@
<!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "https://www.eclipse.org/jetty/configure_9_3.dtd">
<Configure id="Server" class="org.eclipse.jetty.server.Server">
<Call name="addLifeCycleListener">
<Call name="addEventListener">
<Arg>
<New class="org.eclipse.jetty.maven.plugin.ServerListener">
<Set name="tokenFile"><Property name="jetty.token.file"/></Set>

View File

@ -20,6 +20,7 @@ package org.eclipse.jetty.memcached.session;
import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
@ -31,12 +32,9 @@ import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.server.NetworkConnector;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.session.AbstractSessionCache;
import org.eclipse.jetty.server.session.CachingSessionDataStore;
import org.eclipse.jetty.server.session.NullSessionCache;
import org.eclipse.jetty.server.session.NullSessionDataStore;
import org.eclipse.jetty.server.session.Session;
import org.eclipse.jetty.server.session.SessionData;
import org.eclipse.jetty.server.session.SessionHandler;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.junit.jupiter.api.Test;
@ -88,56 +86,6 @@ public class TestMemcachedSessions
}
}
public static class NullSessionCache extends AbstractSessionCache
{
public NullSessionCache(SessionHandler handler)
{
super(handler);
}
@Override
public void shutdown()
{
}
@Override
public Session newSession(SessionData data)
{
return new Session(_handler, data);
}
@Override
public Session newSession(HttpServletRequest request, SessionData data)
{
return new Session(_handler, request, data);
}
@Override
public Session doGet(String id)
{
return null;
}
@Override
public Session doPutIfAbsent(String id, Session session)
{
return null;
}
@Override
public boolean doReplace(String id, Session oldValue, Session newValue)
{
return true;
}
@Override
public Session doDelete(String id)
{
return null;
}
}
@Test
public void testMemcached() throws Exception
{

View File

@ -51,6 +51,11 @@
<artifactId>jetty-server</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-client</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-security</artifactId>
@ -72,11 +77,5 @@
<artifactId>jetty-test-helper</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-client</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@ -1,12 +1,22 @@
<?xml version="1.0"?>
<!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_3.dtd">
<Configure id="Server" class="org.eclipse.jetty.server.Server">
<Get id="ThreadPool" name="threadPool"/>
<New id="HttpClient" class="org.eclipse.jetty.client.HttpClient">
<Arg>
<New class="org.eclipse.jetty.util.ssl.SslContextFactory$Client">
<Set name="trustAll" type="boolean"><Property name="jetty.openid.sslContextFactory.trustAll" default="false"/></Set>
</New>
</Arg>
<Set name="executor"><Ref refid="ThreadPool"/></Set>
</New>
<New id="OpenIdConfiguration" class="org.eclipse.jetty.security.openid.OpenIdConfiguration">
<Arg><Property name="jetty.openid.provider" deprecated="jetty.openid.openIdProvider"/></Arg>
<Arg><Property name="jetty.openid.provider.authorizationEndpoint"/></Arg>
<Arg><Property name="jetty.openid.provider.tokenEndpoint"/></Arg>
<Arg><Property name="jetty.openid.clientId"/></Arg>
<Arg><Property name="jetty.openid.clientSecret"/></Arg>
<Arg><Ref refid="HttpClient"/></Arg>
<Call name="addScopes">
<Arg>
<Call class="org.eclipse.jetty.util.StringUtil" name="csvSplit">

View File

@ -5,6 +5,7 @@ Adds OpenId Connect authentication.
[depend]
security
client
[lib]
lib/jetty-openid-${jetty.version}.jar
@ -37,4 +38,7 @@ etc/jetty-openid.xml
# jetty.openid.scopes=email,profile
## Whether to Authenticate users not found by base LoginService
# jetty.openid.authenticateNewUsers=false
# jetty.openid.authenticateNewUsers=false
## True if all certificates should be trusted by the default SslContextFactory
# jetty.openid.sslContextFactory.trustAll=false

View File

@ -1,6 +1,6 @@
<?xml version="1.0"?>
<!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_3.dtd">
<Configure id="BaseLoginService">
<Configure>
<!-- Optional code to configure the base LoginService used by the OpenIdLoginService
<New id="BaseLoginService" class="org.eclipse.jetty.security.HashLoginService">
<Set name="config"><SystemProperty name="jetty.home" default="."/>/etc/realm.properties</Set>

View File

@ -251,8 +251,7 @@ public class OpenIdAuthenticator extends LoginAuthenticator
if (LOG.isDebugEnabled())
LOG.debug("Session ID should be cookie for OpenID authentication to work");
int redirectCode = (baseRequest.getHttpVersion().getVersion() < HttpVersion.HTTP_1_1.getVersion() ? HttpServletResponse.SC_MOVED_TEMPORARILY : HttpServletResponse.SC_SEE_OTHER);
baseResponse.sendRedirect(redirectCode, URIUtil.addPaths(request.getContextPath(), _errorPage));
baseResponse.sendRedirect(getRedirectCode(baseRequest.getHttpVersion()), URIUtil.addPaths(request.getContextPath(), _errorPage));
return Authentication.SEND_FAILURE;
}
@ -297,8 +296,7 @@ public class OpenIdAuthenticator extends LoginAuthenticator
LOG.debug("authenticated {}->{}", openIdAuth, nuri);
response.setContentLength(0);
int redirectCode = (baseRequest.getHttpVersion().getVersion() < HttpVersion.HTTP_1_1.getVersion() ? HttpServletResponse.SC_MOVED_TEMPORARILY : HttpServletResponse.SC_SEE_OTHER);
baseResponse.sendRedirect(redirectCode, nuri);
baseResponse.sendRedirect(getRedirectCode(baseRequest.getHttpVersion()), nuri);
return openIdAuth;
}
}
@ -317,8 +315,7 @@ public class OpenIdAuthenticator extends LoginAuthenticator
{
if (LOG.isDebugEnabled())
LOG.debug("auth failed {}", _errorPage);
int redirectCode = (baseRequest.getHttpVersion().getVersion() < HttpVersion.HTTP_1_1.getVersion() ? HttpServletResponse.SC_MOVED_TEMPORARILY : HttpServletResponse.SC_SEE_OTHER);
baseResponse.sendRedirect(redirectCode, URIUtil.addPaths(request.getContextPath(), _errorPage));
baseResponse.sendRedirect(getRedirectCode(baseRequest.getHttpVersion()), URIUtil.addPaths(request.getContextPath(), _errorPage));
}
return Authentication.SEND_FAILURE;
@ -408,8 +405,7 @@ public class OpenIdAuthenticator extends LoginAuthenticator
String challengeUri = getChallengeUri(request);
if (LOG.isDebugEnabled())
LOG.debug("challenge {}->{}", session.getId(), challengeUri);
int redirectCode = (baseRequest.getHttpVersion().getVersion() < HttpVersion.HTTP_1_1.getVersion() ? HttpServletResponse.SC_MOVED_TEMPORARILY : HttpServletResponse.SC_SEE_OTHER);
baseResponse.sendRedirect(redirectCode, challengeUri);
baseResponse.sendRedirect(getRedirectCode(baseRequest.getHttpVersion()), challengeUri);
return Authentication.SEND_CONTINUE;
}
@ -437,6 +433,12 @@ public class OpenIdAuthenticator extends LoginAuthenticator
return pathInContext != null && (pathInContext.equals(_errorPath));
}
private static int getRedirectCode(HttpVersion httpVersion)
{
return (httpVersion.getVersion() < HttpVersion.HTTP_1_1.getVersion()
? HttpServletResponse.SC_MOVED_TEMPORARILY : HttpServletResponse.SC_SEE_OTHER);
}
private String getRedirectUri(HttpServletRequest request)
{
final StringBuffer redirectUri = new StringBuffer(128);

View File

@ -18,19 +18,21 @@
package org.eclipse.jetty.security.openid;
import java.io.InputStream;
import java.io.Serializable;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import org.eclipse.jetty.util.IO;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.http.HttpClientTransportOverHTTP;
import org.eclipse.jetty.io.ClientConnector;
import org.eclipse.jetty.util.ajax.JSON;
import org.eclipse.jetty.util.component.ContainerLifeCycle;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
import org.eclipse.jetty.util.ssl.SslContextFactory;
/**
* Holds the configuration for an OpenID Connect service.
@ -38,18 +40,18 @@ import org.eclipse.jetty.util.log.Logger;
* This uses the OpenID Provider URL with the path {@link #CONFIG_PATH} to discover
* the required information about the OIDC service.
*/
public class OpenIdConfiguration implements Serializable
public class OpenIdConfiguration extends ContainerLifeCycle
{
private static final Logger LOG = Log.getLogger(OpenIdConfiguration.class);
private static final long serialVersionUID = 2227941990601349102L;
private static final String CONFIG_PATH = "/.well-known/openid-configuration";
private final HttpClient httpClient;
private final String issuer;
private final String authEndpoint;
private final String tokenEndpoint;
private final String clientId;
private final String clientSecret;
private final List<String> scopes = new ArrayList<>();
private String authEndpoint;
private String tokenEndpoint;
/**
* Create an OpenID configuration for a specific OIDC provider.
@ -59,7 +61,7 @@ public class OpenIdConfiguration implements Serializable
*/
public OpenIdConfiguration(String provider, String clientId, String clientSecret)
{
this(provider, null, null, clientId, clientSecret);
this(provider, null, null, clientId, clientSecret, null);
}
/**
@ -69,60 +71,92 @@ public class OpenIdConfiguration implements Serializable
* @param tokenEndpoint the URL of the OpenID provider's token endpoint if configured.
* @param clientId OAuth 2.0 Client Identifier valid at the Authorization Server.
* @param clientSecret The client secret known only by the Client and the Authorization Server.
* @param httpClient The {@link HttpClient} instance to use.
*/
public OpenIdConfiguration(String issuer, String authorizationEndpoint, String tokenEndpoint, String clientId, String clientSecret)
public OpenIdConfiguration(String issuer, String authorizationEndpoint, String tokenEndpoint,
String clientId, String clientSecret, HttpClient httpClient)
{
this.issuer = issuer;
this.clientId = clientId;
this.clientSecret = clientSecret;
this.authEndpoint = authorizationEndpoint;
this.tokenEndpoint = tokenEndpoint;
this.httpClient = httpClient != null ? httpClient : newHttpClient();
if (issuer == null)
throw new IllegalArgumentException("Provider was not configured");
if (this.issuer == null)
throw new IllegalArgumentException("Issuer was not configured");
if (tokenEndpoint == null || authorizationEndpoint == null)
addBean(this.httpClient);
}
@Override
protected void doStart() throws Exception
{
super.doStart();
if (authEndpoint == null || tokenEndpoint == null)
{
Map<String, Object> discoveryDocument = fetchOpenIdConnectMetadata(issuer);
Map<String, Object> discoveryDocument = fetchOpenIdConnectMetadata(issuer, httpClient);
this.authEndpoint = (String)discoveryDocument.get("authorization_endpoint");
if (this.authEndpoint == null)
authEndpoint = (String)discoveryDocument.get("authorization_endpoint");
if (authEndpoint == null)
throw new IllegalArgumentException("authorization_endpoint");
this.tokenEndpoint = (String)discoveryDocument.get("token_endpoint");
if (this.tokenEndpoint == null)
tokenEndpoint = (String)discoveryDocument.get("token_endpoint");
if (tokenEndpoint == null)
throw new IllegalArgumentException("token_endpoint");
if (!Objects.equals(discoveryDocument.get("issuer"), issuer))
LOG.warn("The provider in the metadata is not correct.");
}
else
{
this.authEndpoint = authorizationEndpoint;
this.tokenEndpoint = tokenEndpoint;
LOG.warn("The issuer in the metadata is not correct.");
}
}
private static Map<String, Object> fetchOpenIdConnectMetadata(String provider)
private static HttpClient newHttpClient()
{
ClientConnector connector = new ClientConnector();
connector.setSslContextFactory(new SslContextFactory.Client(false));
return new HttpClient(new HttpClientTransportOverHTTP(connector));
}
private static Map<String, Object> fetchOpenIdConnectMetadata(String provider, HttpClient httpClient)
{
try
{
if (provider.endsWith("/"))
provider = provider.substring(0, provider.length() - 1);
URI providerUri = URI.create(provider + CONFIG_PATH);
InputStream inputStream = providerUri.toURL().openConnection().getInputStream();
String content = IO.toString(inputStream);
Map<String, Object> discoveryDocument = (Map)JSON.parse(content);
if (LOG.isDebugEnabled())
LOG.debug("discovery document {}", discoveryDocument);
Map<String, Object> result;
String responseBody = httpClient.GET(provider + CONFIG_PATH)
.getContentAsString();
Object parsedResult = JSON.parse(responseBody);
return discoveryDocument;
if (parsedResult instanceof Map)
{
Map<?, ?> rawResult = (Map)parsedResult;
result = rawResult.entrySet().stream()
.collect(Collectors.toMap(it -> it.getKey().toString(), Map.Entry::getValue));
}
else
{
LOG.warn("OpenID provider did not return a proper JSON object response. Result was '{}'", responseBody);
throw new IllegalStateException("Could not parse OpenID provider's malformed response");
}
LOG.debug("discovery document {}", result);
return result;
}
catch (Throwable e)
catch (Exception e)
{
throw new IllegalArgumentException("invalid identity provider", e);
}
}
public HttpClient getHttpClient()
{
return httpClient;
}
public String getAuthEndpoint()
{
return authEndpoint;

View File

@ -18,20 +18,19 @@
package org.eclipse.jetty.security.openid;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Base64;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import org.eclipse.jetty.util.IO;
import org.eclipse.jetty.util.UrlEncoded;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.client.util.FormContentProvider;
import org.eclipse.jetty.util.Fields;
import org.eclipse.jetty.util.ajax.JSON;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
@ -42,7 +41,7 @@ import org.eclipse.jetty.util.log.Logger;
*
* <p>
* This is constructed with an authorization code from the authentication request. This authorization code
* is then exchanged using {@link #redeemAuthCode()} for a response containing the ID Token and Access Token.
* is then exchanged using {@link #redeemAuthCode(HttpClient)} for a response containing the ID Token and Access Token.
* The response is then validated against the {@link OpenIdConfiguration}.
* </p>
*/
@ -79,7 +78,7 @@ public class OpenIdCredentials implements Serializable
return response;
}
public void redeemAuthCode() throws IOException
public void redeemAuthCode(HttpClient httpClient) throws Exception
{
if (LOG.isDebugEnabled())
LOG.debug("redeemAuthCode() {}", this);
@ -88,7 +87,7 @@ public class OpenIdCredentials implements Serializable
{
try
{
response = claimAuthCode(authCode);
response = claimAuthCode(httpClient, authCode);
if (LOG.isDebugEnabled())
LOG.debug("response: {}", response);
@ -186,7 +185,10 @@ public class OpenIdCredentials implements Serializable
String jwtClaimString = new String(decoder.decode(padJWTSection(sections[1])), StandardCharsets.UTF_8);
String jwtSignature = sections[2];
Map<String, Object> jwtHeader = (Map)JSON.parse(jwtHeaderString);
Object parsedJwtHeader = JSON.parse(jwtHeaderString);
if (!(parsedJwtHeader instanceof Map))
throw new IllegalStateException("Invalid JWT header");
Map<String, Object> jwtHeader = (Map)parsedJwtHeader;
LOG.debug("JWT Header: {}", jwtHeader);
/* If the ID Token is received via direct communication between the Client
@ -195,7 +197,11 @@ public class OpenIdCredentials implements Serializable
if (LOG.isDebugEnabled())
LOG.debug("JWT signature not validated {}", jwtSignature);
return (Map)JSON.parse(jwtClaimString);
Object parsedClaims = JSON.parse(jwtClaimString);
if (!(parsedClaims instanceof Map))
throw new IllegalStateException("Could not decode JSON for JWT claims.");
return (Map)parsedClaims;
}
private static byte[] padJWTSection(String unpaddedEncodedJwtSection)
@ -224,40 +230,27 @@ public class OpenIdCredentials implements Serializable
return paddedEncodedJwtSection;
}
private Map<String, Object> claimAuthCode(String authCode) throws IOException
private Map<String, Object> claimAuthCode(HttpClient httpClient, String authCode) throws Exception
{
Fields fields = new Fields();
fields.add("code", authCode);
fields.add("client_id", configuration.getClientId());
fields.add("client_secret", configuration.getClientSecret());
fields.add("redirect_uri", redirectUri);
fields.add("grant_type", "authorization_code");
FormContentProvider formContentProvider = new FormContentProvider(fields);
Request request = httpClient.POST(configuration.getTokenEndpoint())
.content(formContentProvider)
.timeout(10, TimeUnit.SECONDS);
ContentResponse response = request.send();
String responseBody = response.getContentAsString();
if (LOG.isDebugEnabled())
LOG.debug("claimAuthCode {}", authCode);
LOG.debug("Authentication response: {}", responseBody);
// Use the authorization code to get the id_token from the OpenID Provider
String urlParameters = "code=" + authCode +
"&client_id=" + UrlEncoded.encodeString(configuration.getClientId(), StandardCharsets.UTF_8) +
"&client_secret=" + UrlEncoded.encodeString(configuration.getClientSecret(), StandardCharsets.UTF_8) +
"&redirect_uri=" + UrlEncoded.encodeString(redirectUri, StandardCharsets.UTF_8) +
"&grant_type=authorization_code";
Object parsedResponse = JSON.parse(responseBody);
if (!(parsedResponse instanceof Map))
throw new IllegalStateException("Malformed response from OpenID Provider");
URL url = new URL(configuration.getTokenEndpoint());
HttpURLConnection connection = (HttpURLConnection)url.openConnection();
try
{
connection.setDoOutput(true);
connection.setRequestMethod("POST");
connection.setRequestProperty("Host", configuration.getIssuer());
connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
try (DataOutputStream wr = new DataOutputStream(connection.getOutputStream()))
{
wr.write(urlParameters.getBytes(StandardCharsets.UTF_8));
}
try (InputStream content = (InputStream)connection.getContent())
{
return (Map)JSON.parse(IO.toString(content));
}
}
finally
{
connection.disconnect();
}
return (Map)parsedResponse;
}
}

View File

@ -22,6 +22,7 @@ import java.security.Principal;
import javax.security.auth.Subject;
import javax.servlet.ServletRequest;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.security.IdentityService;
import org.eclipse.jetty.security.LoginService;
import org.eclipse.jetty.server.UserIdentity;
@ -40,8 +41,9 @@ public class OpenIdLoginService extends ContainerLifeCycle implements LoginServi
{
private static final Logger LOG = Log.getLogger(OpenIdLoginService.class);
private final OpenIdConfiguration _configuration;
private final OpenIdConfiguration configuration;
private final LoginService loginService;
private final HttpClient httpClient;
private IdentityService identityService;
private boolean authenticateNewUsers;
@ -59,20 +61,22 @@ public class OpenIdLoginService extends ContainerLifeCycle implements LoginServi
*/
public OpenIdLoginService(OpenIdConfiguration configuration, LoginService loginService)
{
_configuration = configuration;
this.configuration = configuration;
this.loginService = loginService;
this.httpClient = configuration.getHttpClient();
addBean(this.configuration);
addBean(this.loginService);
}
@Override
public String getName()
{
return _configuration.getIssuer();
return configuration.getIssuer();
}
public OpenIdConfiguration getConfiguration()
{
return _configuration;
return configuration;
}
@Override
@ -84,7 +88,7 @@ public class OpenIdLoginService extends ContainerLifeCycle implements LoginServi
OpenIdCredentials openIdCredentials = (OpenIdCredentials)credentials;
try
{
openIdCredentials.redeemAuthCode();
openIdCredentials.redeemAuthCode(httpClient);
if (openIdCredentials.isExpired())
return null;
}

View File

@ -49,7 +49,7 @@
<Set name="responseHeaderSize">8192</Set>
<Set name="sendServerVersion">true</Set>
<Set name="sendDateHeader">false</Set>
<Set name="headerCacheSize">4096</Set>
<Set name="headerCacheSize">1024</Set>
</New>
-->
<New id="httpConfig" class="org.eclipse.jetty.server.HttpConfiguration">
@ -61,7 +61,7 @@
<Set name="responseHeaderSize"><Property name="jetty.httpConfig.responseHeaderSize" default="8192" /></Set>
<Set name="sendServerVersion"><Property name="jetty.httpConfig.sendServerVersion" default="true" /></Set>
<Set name="sendDateHeader"><Property name="jetty.httpConfig.sendDateHeader" default="false" /></Set>
<Set name="headerCacheSize"><Property name="jetty.httpConfig.headerCacheSize" default="4096" /></Set>
<Set name="headerCacheSize"><Property name="jetty.httpConfig.headerCacheSize" default="1024" /></Set>
<Set name="delayDispatchUntilContent"><Property name="jetty.httpConfig.delayDispatchUntilContent" default="true"/></Set>
<Set name="maxErrorDispatches"><Property name="jetty.httpConfig.maxErrorDispatches" default="10"/></Set>
<Set name="persistentConnectionsEnabled"><Property name="jetty.httpConfig.persistentConnectionsEnabled" default="true"/></Set>

View File

@ -46,7 +46,7 @@ public class Activator implements BundleActivator
{
//For test purposes, use a random port
Server server = new Server(0);
server.getConnectors()[0].addLifeCycleListener(new AbstractLifeCycleListener()
server.getConnectors()[0].addEventListener(new AbstractLifeCycleListener()
{
/**

View File

@ -31,7 +31,7 @@
</Item>
</Array>
</Arg>
<Call name="addLifeCycleListener">
<Call name="addEventListener">
<Arg>
<New class="org.eclipse.jetty.osgi.boot.utils.ServerConnectorListener">
<Set name="sysPropertyName">boot.context.service.port</Set>

View File

@ -31,7 +31,7 @@
</Item>
</Array>
</Arg>
<Call name="addLifeCycleListener">
<Call name="addEventListener">
<Arg>
<New class="org.eclipse.jetty.osgi.boot.utils.ServerConnectorListener">
<Set name="sysPropertyName">boot.webapp.service.port</Set>

View File

@ -31,7 +31,7 @@
</Item>
</Array>
</Arg>
<Call name="addLifeCycleListener">
<Call name="addEventListener">
<Arg>
<New class="org.eclipse.jetty.osgi.boot.utils.ServerConnectorListener">
<Set name="sysPropertyName">boot.annotations.port</Set>

View File

@ -31,7 +31,7 @@
</Item>
</Array>
</Arg>
<Call name="addLifeCycleListener">
<Call name="addEventListener">
<Arg>
<New class="org.eclipse.jetty.osgi.boot.utils.ServerConnectorListener">
<Set name="sysPropertyName">boot.bundle.port</Set>

View File

@ -31,7 +31,7 @@
</Item>
</Array>
</Arg>
<Call name="addLifeCycleListener">
<Call name="addEventListener">
<Arg>
<New class="org.eclipse.jetty.osgi.boot.utils.ServerConnectorListener">
<Set name="sysPropertyName">boot.javax.websocket.port</Set>

View File

@ -31,7 +31,7 @@
</Item>
</Array>
</Arg>
<Call name="addLifeCycleListener">
<Call name="addEventListener">
<Arg>
<New class="org.eclipse.jetty.osgi.boot.utils.ServerConnectorListener">
<Set name="sysPropertyName">boot.jsp.port</Set>

View File

@ -31,7 +31,7 @@
</Item>
</Array>
</Arg>
<Call name="addLifeCycleListener">
<Call name="addEventListener">
<Arg>
<New class="org.eclipse.jetty.osgi.boot.utils.ServerConnectorListener">
<Set name="sysPropertyName">boot.websocket.port</Set>

View File

@ -31,7 +31,7 @@
</Item>
</Array>
</Arg>
<Call name="addLifeCycleListener">
<Call name="addEventListener">
<Arg>
<New class="org.eclipse.jetty.osgi.boot.utils.ServerConnectorListener">
<Set name="sysPropertyName">foo.foo</Set>

View File

@ -16,7 +16,7 @@
</New>
</Arg>
</Call>
<Call name="addLifeCycleListener">
<Call name="addEventListener">
<Arg>
<New class="org.eclipse.jetty.osgi.boot.utils.ServerConnectorListener">
<Set name="sysPropertyName">boot.https.port</Set>

View File

@ -42,8 +42,8 @@
</Set>
<New id="httpConfig" class="org.eclipse.jetty.server.HttpConfiguration">
<Set name="secureScheme"><Property name="jetty.httpConfig.secureScheme" default="https"/></Set>
<Set name="secureScheme"><Property name="jetty.httpConfig.secureScheme" default="https"/></Set>
<Set name="secureScheme" property="jetty.httpConfig.secureScheme"/>
<Set name="securePort" property="jetty.httpConfig.securePort"/>
<Set name="outputBufferSize" property="jetty.httpConfig.outputBufferSize"/>
<Set name="outputAggregationSize" property="jetty.httpConfig.outputAggregationSize"/>
<Set name="requestHeaderSize" property="jetty.httpConfig.requestHeaderSize"/>

View File

@ -125,6 +125,31 @@ public class SpnegoAuthenticatorTest
assertEquals(HttpHeader.NEGOTIATE.asString(), res.getHeader(HttpHeader.WWW_AUTHENTICATE.asString()));
assertEquals(HttpServletResponse.SC_UNAUTHORIZED, res.getStatus());
}
class MockConnector extends AbstractConnector
{
public MockConnector()
{
super(new Server() , null, null, null, 0);
}
@Override
protected void accept(int acceptorID) throws IOException, InterruptedException
{
}
@Override
public Object getTransport()
{
return null;
}
@Override
public String dumpSelf()
{
return null;
}
}
}
class MockConnector extends AbstractConnector

View File

@ -59,8 +59,8 @@
<!-- for all configuration that may be set here. -->
<!-- =========================================================== -->
<New id="httpConfig" class="org.eclipse.jetty.server.HttpConfiguration">
<Set name="secureScheme"><Property name="jetty.httpConfig.secureScheme" default="https" /></Set>
<Set name="securePort"><Property name="jetty.httpConfig.securePort" deprecated="jetty.secure.port" default="8443" /></Set>
<Set name="secureScheme" property="jetty.httpConfig.secureScheme"/>
<Set name="securePort" property="jetty.httpConfig.securePort"/>
<Set name="outputBufferSize" property="jetty.httpConfig.outputBufferSize"/>
<Set name="outputAggregationSize" property="jetty.httpConfig.outputAggregationSize"/>
<Set name="requestHeaderSize" property="jetty.httpConfig.requestHeaderSize"/>

View File

@ -51,7 +51,7 @@ etc/jetty.xml
# jetty.httpConfig.sendDateHeader=false
## Max per-connection header cache size (in nodes)
# jetty.httpConfig.headerCacheSize=4096
# jetty.httpConfig.headerCacheSize=1024
## Whether, for requests with content, delay dispatch until some content has arrived
# jetty.httpConfig.delayDispatchUntilContent=true

View File

@ -23,7 +23,6 @@ import java.util.Collections;
import java.util.List;
import org.eclipse.jetty.io.AbstractConnection;
import org.eclipse.jetty.io.Connection;
import org.eclipse.jetty.io.EndPoint;
import org.eclipse.jetty.util.ArrayUtil;
import org.eclipse.jetty.util.annotation.ManagedAttribute;
@ -32,17 +31,7 @@ import org.eclipse.jetty.util.component.ContainerLifeCycle;
import org.eclipse.jetty.util.ssl.SslContextFactory;
/**
* <p>Provides the common handling for {@link ConnectionFactory} implementations including:</p>
* <ul>
* <li>Protocol identification</li>
* <li>Configuration of new Connections:
* <ul>
* <li>Setting inputbuffer size</li>
* <li>Calling {@link Connection#addListener(Connection.Listener)} for all
* Connection.Listener instances found as beans on the {@link Connector}
* and this {@link ConnectionFactory}</li>
* </ul>
* </ul>
* <p>Provides the common handling for {@link ConnectionFactory} implementations.</p>
*/
@ManagedObject
public abstract class AbstractConnectionFactory extends ContainerLifeCycle implements ConnectionFactory
@ -92,19 +81,10 @@ public abstract class AbstractConnectionFactory extends ContainerLifeCycle imple
connection.setInputBufferSize(getInputBufferSize());
// Add Connection.Listeners from Connector
if (connector instanceof ContainerLifeCycle)
{
ContainerLifeCycle aggregate = (ContainerLifeCycle)connector;
for (Connection.Listener listener : aggregate.getBeans(Connection.Listener.class))
{
connection.addListener(listener);
}
}
connector.getEventListeners().forEach(connection::addEventListener);
// Add Connection.Listeners from this factory
for (Connection.Listener listener : getBeans(Connection.Listener.class))
{
connection.addListener(listener);
}
getEventListeners().forEach(connection::addEventListener);
return connection;
}

View File

@ -104,7 +104,6 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor
_state = new HttpChannelState(this);
_request = new Request(this, newHttpInput(_state));
_response = new Response(this, newHttpOutput());
_executor = connector.getServer().getThreadPool();
_requestLog = connector.getServer().getRequestLog();
_combinedListener = (connector instanceof AbstractConnector)
@ -1224,15 +1223,16 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor
public void succeeded()
{
_written += _length;
if (_complete)
_response.getHttpOutput().closed();
super.succeeded();
if (_commit)
_combinedListener.onResponseCommit(_request);
if (_length > 0)
_combinedListener.onResponseContent(_request, _content);
if (_complete && _state.completeResponse())
{
_response.getHttpOutput().closed();
_combinedListener.onResponseEnd(_request);
}
super.succeeded();
}
@Override

View File

@ -108,7 +108,7 @@ public class HttpChannelOverHttp extends HttpChannel implements HttpParser.Reque
}
@Override
public boolean startRequest(String method, String uri, HttpVersion version)
public void startRequest(String method, String uri, HttpVersion version)
{
_metadata.setMethod(method);
_metadata.getURI().parseRequestTarget(method, uri);
@ -116,7 +116,6 @@ public class HttpChannelOverHttp extends HttpChannel implements HttpParser.Reque
_unknownExpectation = false;
_expect100Continue = false;
_expect102Processing = false;
return false;
}
@Override
@ -514,18 +513,6 @@ public class HttpChannelOverHttp extends HttpChannel implements HttpParser.Reque
return onRequestComplete();
}
@Override
public int getHeaderCacheSize()
{
return getHttpConfiguration().getHeaderCacheSize();
}
@Override
public boolean isHeaderCacheCaseSensitive()
{
return getHttpConfiguration().isHeaderCacheCaseSensitive();
}
@Override
public void onComplianceViolation(ComplianceViolation.Mode mode, ComplianceViolation violation, String details)
{

View File

@ -56,7 +56,7 @@ public class HttpConfiguration implements Dumpable
private int _outputAggregationSize = _outputBufferSize / 4;
private int _requestHeaderSize = 8 * 1024;
private int _responseHeaderSize = 8 * 1024;
private int _headerCacheSize = 4 * 1024;
private int _headerCacheSize = 1024;
private boolean _headerCacheCaseSensitive = false;
private int _securePort;
private long _idleTimeout = -1;

View File

@ -132,7 +132,10 @@ public class HttpConnection extends AbstractConnection implements Runnable, Http
protected HttpParser newHttpParser(HttpCompliance compliance)
{
return new HttpParser(newRequestHandler(), getHttpConfiguration().getRequestHeaderSize(), compliance);
HttpParser parser = new HttpParser(newRequestHandler(), getHttpConfiguration().getRequestHeaderSize(), compliance);
parser.setHeaderCacheSize(getHttpConfiguration().getHeaderCacheSize());
parser.setHeaderCacheCaseSensitive(getHttpConfiguration().isHeaderCacheCaseSensitive());
return parser;
}
protected HttpParser.RequestHandler newRequestHandler()

View File

@ -386,22 +386,7 @@ public class HttpOutput extends ServletOutputStream implements Runnable
if (!_state.compareAndSet(state, State.CLOSED))
break;
// Just make sure write and output stream really are closed
try
{
_channel.getResponse().closeOutput();
}
catch (Throwable x)
{
if (LOG.isDebugEnabled())
LOG.debug(x);
abort(x);
}
finally
{
releaseBuffer();
}
// Return even if an exception is thrown by closeOutput().
releaseBuffer();
return;
}
}

View File

@ -381,18 +381,6 @@ public class LocalConnector extends AbstractConnector
return false;
}
@Override
public int getHeaderCacheSize()
{
return 0;
}
@Override
public boolean isHeaderCacheCaseSensitive()
{
return false;
}
@Override
public void earlyEOF()
{
@ -405,9 +393,8 @@ public class LocalConnector extends AbstractConnector
}
@Override
public boolean startResponse(HttpVersion version, int status, String reason)
public void startResponse(HttpVersion version, int status, String reason)
{
return false;
}
};

View File

@ -223,10 +223,8 @@ public class ServerConnector extends AbstractNetworkConnector
@Override
protected void doStart() throws Exception
{
for (EventListener l : getBeans(EventListener.class))
{
for (EventListener l : getBeans(SelectorManager.SelectorManagerListener.class))
_manager.addEventListener(l);
}
super.doStart();

View File

@ -124,13 +124,14 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu
};
public static final int DEFAULT_LISTENER_TYPE_INDEX = 1;
public static final int EXTENDED_LISTENER_TYPE_INDEX = 0;
private static final String UNIMPLEMENTED_USE_SERVLET_CONTEXT_HANDLER = "Unimplemented {} - use org.eclipse.jetty.servlet.ServletContextHandler";
private static final Logger LOG = Log.getLogger(ContextHandler.class);
private static final ThreadLocal<Context> __context = new ThreadLocal<Context>();
private static final ThreadLocal<Context> __context = new ThreadLocal<>();
private static String __serverInfo = "jetty/" + Server.getVersion();
@ -198,7 +199,6 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu
private boolean _compactPath = false;
private boolean _usingSecurityManager = System.getSecurityManager() != null;
private final List<EventListener> _eventListeners = new CopyOnWriteArrayList<>();
private final List<EventListener> _programmaticListeners = new CopyOnWriteArrayList<>();
private final List<ServletContextListener> _servletContextListeners = new CopyOnWriteArrayList<>();
private final List<ServletContextListener> _destroySerletContextListeners = new ArrayList<>();
@ -206,9 +206,9 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu
private final List<ServletRequestListener> _servletRequestListeners = new CopyOnWriteArrayList<>();
private final List<ServletRequestAttributeListener> _servletRequestAttributeListeners = new CopyOnWriteArrayList<>();
private final List<ContextScopeListener> _contextListeners = new CopyOnWriteArrayList<>();
private final List<EventListener> _durableListeners = new CopyOnWriteArrayList<>();
private final Set<EventListener> _durableListeners = new HashSet<>();
private String[] _protectedTargets;
private final CopyOnWriteArrayList<AliasCheck> _aliasChecks = new CopyOnWriteArrayList<ContextHandler.AliasCheck>();
private final CopyOnWriteArrayList<AliasCheck> _aliasChecks = new CopyOnWriteArrayList<>();
public enum Availability
{
@ -242,7 +242,7 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu
{
_scontext = context == null ? new Context() : context;
_attributes = new AttributesMap();
_initParams = new HashMap<String, String>();
_initParams = new HashMap<>();
addAliasCheck(new ApproveNonExistentDirectoryAliases());
if (File.separatorChar == '/')
addAliasCheck(new AllowSymLinkAliasChecker());
@ -260,7 +260,6 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu
{
dumpObjects(out, indent,
new ClassLoaderDump(getClassLoader()),
new DumpableCollection("eventListeners " + this, _eventListeners),
new DumpableCollection("handler attributes " + this, ((AttributesMap)getAttributes()).getAttributeEntrySet()),
new DumpableCollection("context attributes " + this, ((Context)getServletContext()).getAttributeEntrySet()),
new DumpableCollection("initparams " + this, getInitParams().entrySet()));
@ -352,7 +351,7 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu
if (connectorIndex == 0)
{
if (connectorOnlyIndexes == null)
connectorOnlyIndexes = new ArrayList<Integer>();
connectorOnlyIndexes = new ArrayList<>();
connectorOnlyIndexes.add(i);
}
}
@ -410,7 +409,7 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu
}
else
{
Set<String> currentVirtualHosts = new HashSet<String>(Arrays.asList(getVirtualHosts()));
Set<String> currentVirtualHosts = new HashSet<>(Arrays.asList(getVirtualHosts()));
for (String vh : virtualHosts)
{
currentVirtualHosts.add(normalizeHostname(vh));
@ -435,7 +434,7 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu
if (virtualHosts == null || virtualHosts.length == 0 || _vhosts == null || _vhosts.length == 0)
return; // do nothing
Set<String> existingVirtualHosts = new HashSet<String>(Arrays.asList(getVirtualHosts()));
Set<String> existingVirtualHosts = new HashSet<>(Arrays.asList(getVirtualHosts()));
for (String vh : virtualHosts)
{
existingVirtualHosts.remove(normalizeHostname(vh));
@ -606,101 +605,68 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu
return _displayName;
}
public EventListener[] getEventListeners()
{
return _eventListeners.toArray(new EventListener[_eventListeners.size()]);
}
/**
* Set the context event listeners.
*
* @param eventListeners the event listeners
* @see ServletContextListener
* @see ServletContextAttributeListener
* @see ServletRequestListener
* @see ServletRequestAttributeListener
*/
public void setEventListeners(EventListener[] eventListeners)
{
_contextListeners.clear();
_servletContextListeners.clear();
_servletContextAttributeListeners.clear();
_servletRequestListeners.clear();
_servletRequestAttributeListeners.clear();
_eventListeners.clear();
if (eventListeners != null)
for (EventListener listener : eventListeners)
{
addEventListener(listener);
}
}
/**
* Add a context event listeners.
*
* @param listener the event listener to add
* @return true if the listener was added
* @see ContextScopeListener
* @see ServletContextListener
* @see ServletContextAttributeListener
* @see ServletRequestListener
* @see ServletRequestAttributeListener
*/
public void addEventListener(EventListener listener)
@Override
public boolean addEventListener(EventListener listener)
{
_eventListeners.add(listener);
if (!(isStarted() || isStarting()))
if (super.addEventListener(listener))
{
_durableListeners.add(listener);
if (listener instanceof ContextScopeListener)
{
_contextListeners.add((ContextScopeListener)listener);
if (__context.get() != null)
((ContextScopeListener)listener).enterScope(__context.get(), null, "Listener registered");
}
if (listener instanceof ServletContextListener)
_servletContextListeners.add((ServletContextListener)listener);
if (listener instanceof ServletContextAttributeListener)
_servletContextAttributeListeners.add((ServletContextAttributeListener)listener);
if (listener instanceof ServletRequestListener)
_servletRequestListeners.add((ServletRequestListener)listener);
if (listener instanceof ServletRequestAttributeListener)
_servletRequestAttributeListeners.add((ServletRequestAttributeListener)listener);
return true;
}
if (listener instanceof ContextScopeListener)
{
_contextListeners.add((ContextScopeListener)listener);
if (__context.get() != null)
((ContextScopeListener)listener).enterScope(__context.get(), null, "Listener registered");
}
if (listener instanceof ServletContextListener)
_servletContextListeners.add((ServletContextListener)listener);
if (listener instanceof ServletContextAttributeListener)
_servletContextAttributeListeners.add((ServletContextAttributeListener)listener);
if (listener instanceof ServletRequestListener)
_servletRequestListeners.add((ServletRequestListener)listener);
if (listener instanceof ServletRequestAttributeListener)
_servletRequestAttributeListeners.add((ServletRequestAttributeListener)listener);
return false;
}
/**
* Remove a context event listeners.
*
* @param listener the event listener to remove
* @see ServletContextListener
* @see ServletContextAttributeListener
* @see ServletRequestListener
* @see ServletRequestAttributeListener
*/
public void removeEventListener(EventListener listener)
@Override
public boolean removeEventListener(EventListener listener)
{
_eventListeners.remove(listener);
if (super.removeEventListener(listener))
{
if (listener instanceof ContextScopeListener)
_contextListeners.remove(listener);
if (listener instanceof ContextScopeListener)
_contextListeners.remove(listener);
if (listener instanceof ServletContextListener)
_servletContextListeners.remove(listener);
if (listener instanceof ServletContextListener)
_servletContextListeners.remove(listener);
if (listener instanceof ServletContextAttributeListener)
_servletContextAttributeListeners.remove(listener);
if (listener instanceof ServletContextAttributeListener)
_servletContextAttributeListeners.remove(listener);
if (listener instanceof ServletRequestListener)
_servletRequestListeners.remove(listener);
if (listener instanceof ServletRequestListener)
_servletRequestListeners.remove(listener);
if (listener instanceof ServletRequestAttributeListener)
_servletRequestAttributeListeners.remove(listener);
if (listener instanceof ServletRequestAttributeListener)
_servletRequestAttributeListeners.remove(listener);
return true;
}
return false;
}
/**
@ -720,7 +686,11 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu
public boolean isDurableListener(EventListener listener)
{
return _durableListeners.contains(listener);
// The durable listeners are those set when the context is started
if (isStarted())
return _durableListeners.contains(listener);
// If we are not yet started then all set listeners are durable
return getEventListeners().contains(listener);
}
/**
@ -800,6 +770,8 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu
if (_mimeTypes == null)
_mimeTypes = new MimeTypes();
_durableListeners.addAll(getEventListeners());
try
{
// Set the classloader, context and enter scope
@ -975,7 +947,7 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu
stopContext();
// retain only durable listeners
setEventListeners(_durableListeners.toArray(new EventListener[_durableListeners.size()]));
setEventListeners(_durableListeners);
_durableListeners.clear();
if (_errorHandler != null)
@ -1130,7 +1102,7 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu
if (LOG.isDebugEnabled())
LOG.debug("scope {}|{}|{} @ {}", baseRequest.getContextPath(), baseRequest.getServletPath(), baseRequest.getPathInfo(), this);
Context oldContext = null;
Context oldContext;
String oldContextPath = null;
String oldServletPath = null;
String oldPathInfo = null;
@ -1148,7 +1120,7 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu
// check the target.
if (DispatcherType.REQUEST.equals(dispatch) || DispatcherType.ASYNC.equals(dispatch))
{
if (_compactPath)
if (isCompactPath())
target = URIUtil.compactPath(target);
if (!checkContext(target, baseRequest, response))
return;
@ -1194,7 +1166,7 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu
if (_contextPath.length() == 1)
baseRequest.setContextPath("");
else
baseRequest.setContextPath(_contextPathEncoded);
baseRequest.setContextPath(getContextPathEncoded());
baseRequest.setServletPath(null);
baseRequest.setPathInfo(pathInfo);
}
@ -1770,7 +1742,7 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu
public void addLocaleEncoding(String locale, String encoding)
{
if (_localeEncodingMap == null)
_localeEncodingMap = new HashMap<String, String>();
_localeEncodingMap = new HashMap<>();
_localeEncodingMap.put(locale, encoding);
}
@ -1850,7 +1822,7 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu
LOG.debug("Aliased resource: " + resource + "~=" + resource.getAlias());
// alias checks
for (Iterator<AliasCheck> i = _aliasChecks.iterator(); i.hasNext(); )
for (Iterator<AliasCheck> i = getAliasChecks().iterator(); i.hasNext(); )
{
AliasCheck check = i.next();
if (check.check(path, resource))
@ -1916,7 +1888,7 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu
String[] l = resource.list();
if (l != null)
{
HashSet<String> set = new HashSet<String>();
HashSet<String> set = new HashSet<>();
for (int i = 0; i < l.length; i++)
{
set.add(path + l[i]);
@ -1959,7 +1931,7 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu
*/
public void addAliasCheck(AliasCheck check)
{
_aliasChecks.add(check);
getAliasChecks().add(check);
}
/**
@ -1975,8 +1947,8 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu
*/
public void setAliasChecks(List<AliasCheck> checks)
{
_aliasChecks.clear();
_aliasChecks.addAll(checks);
getAliasChecks().clear();
getAliasChecks().addAll(checks);
}
/**
@ -1984,13 +1956,14 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu
*/
public void clearAliasChecks()
{
_aliasChecks.clear();
getAliasChecks().clear();
}
/**
* Context.
* <p>
* A partial implementation of {@link javax.servlet.ServletContext}. A complete implementation is provided by the derived {@link ContextHandler}.
* A partial implementation of {@link javax.servlet.ServletContext}. A complete implementation is provided by the
* derived {@link ContextHandler} implementations.
* </p>
*/
public class Context extends StaticContext
@ -2013,7 +1986,7 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu
@Override
public ServletContext getContext(String uripath)
{
List<ContextHandler> contexts = new ArrayList<ContextHandler>();
List<ContextHandler> contexts = new ArrayList<>();
Handler[] handlers = getServer().getChildHandlersByClass(ContextHandler.class);
String matchedPath = null;
@ -2086,7 +2059,7 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu
matchedPath = contextPath;
}
if (matchedPath != null && matchedPath.equals(contextPath))
if (matchedPath.equals(contextPath))
contexts.add(ch);
}
}
@ -2281,7 +2254,7 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu
@Override
public synchronized Enumeration<String> getAttributeNames()
{
HashSet<String> set = new HashSet<String>();
HashSet<String> set = new HashSet<>();
Enumeration<String> e = super.getAttributeNames();
while (e.hasMoreElements())
{
@ -2428,19 +2401,6 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu
}
}
@Override
public <T extends EventListener> T createListener(Class<T> clazz) throws ServletException
{
try
{
return createInstance(clazz);
}
catch (Exception e)
{
throw new ServletException(e);
}
}
public void checkListener(Class<? extends EventListener> listener) throws IllegalStateException
{
boolean ok = false;
@ -2474,7 +2434,7 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu
throw new UnsupportedOperationException();
// no security manager just return the classloader
if (!_usingSecurityManager)
if (!isUsingSecurityManager())
{
return _classLoader;
}
@ -2530,12 +2490,6 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu
return _enabled;
}
public <T> T createInstance(Class<T> clazz) throws Exception
{
T o = clazz.getDeclaredConstructor().newInstance();
return o;
}
@Override
public String getVirtualServerName()
{
@ -2546,15 +2500,16 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu
}
}
/**
* A simple implementation of ServletContext that is used when there is no
* ContextHandler. This is also used as the base for all other ServletContext
* implementations.
*/
public static class StaticContext extends AttributesMap implements ServletContext
{
private int _effectiveMajorVersion = SERVLET_MAJOR_VERSION;
private int _effectiveMinorVersion = SERVLET_MINOR_VERSION;
public StaticContext()
{
}
@Override
public ServletContext getContext(String uripath)
{
@ -2618,7 +2573,7 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu
@Override
public String getServerInfo()
{
return __serverInfo;
return ContextHandler.getServerInfo();
}
@Override
@ -2745,20 +2700,6 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu
return null;
}
@Override
public <T extends Filter> T createFilter(Class<T> c) throws ServletException
{
LOG.warn(UNIMPLEMENTED_USE_SERVLET_CONTEXT_HANDLER, "createFilter(Class)");
return null;
}
@Override
public <T extends Servlet> T createServlet(Class<T> c) throws ServletException
{
LOG.warn(UNIMPLEMENTED_USE_SERVLET_CONTEXT_HANDLER, "createServlet(Class)");
return null;
}
@Override
public Set<SessionTrackingMode> getDefaultSessionTrackingModes()
{
@ -2832,8 +2773,7 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu
LOG.warn(UNIMPLEMENTED_USE_SERVLET_CONTEXT_HANDLER, "addListener(Class)");
}
@Override
public <T extends EventListener> T createListener(Class<T> clazz) throws ServletException
protected <T> T createInstance(Class<T> clazz) throws ServletException
{
try
{
@ -2845,6 +2785,24 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu
}
}
@Override
public <T extends EventListener> T createListener(Class<T> clazz) throws ServletException
{
return createInstance(clazz);
}
@Override
public <T extends Servlet> T createServlet(Class<T> clazz) throws ServletException
{
return createInstance(clazz);
}
@Override
public <T extends Filter> T createFilter(Class<T> clazz) throws ServletException
{
return createInstance(clazz);
}
@Override
public ClassLoader getClassLoader()
{

View File

@ -21,6 +21,8 @@ package org.eclipse.jetty.server.session;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.function.Function;
import javax.servlet.http.HttpServletRequest;
import org.eclipse.jetty.util.StringUtil;
@ -116,26 +118,9 @@ public abstract class AbstractSessionCache extends ContainerLifeCycle implements
*/
public abstract Session newSession(HttpServletRequest request, SessionData data);
@Override
public Session newSession(HttpServletRequest request, String id, long time, long maxInactiveMs)
{
if (LOG.isDebugEnabled())
LOG.debug("Creating new session id=" + id);
Session session = newSession(request, _sessionDataStore.newSessionData(id, time, time, time, maxInactiveMs));
session.getSessionData().setLastNode(_context.getWorkerName());
try
{
if (isSaveOnCreate() && _sessionDataStore != null)
_sessionDataStore.store(id, session.getSessionData());
}
catch (Exception e)
{
LOG.warn("Save of new session {} failed", id, e);
}
return session;
}
/**
* Get the session matching the key from the cache. Does not load
* the session.
*
* @param id session id
* @return the Session object matching the id
@ -150,6 +135,18 @@ public abstract class AbstractSessionCache extends ContainerLifeCycle implements
* @return null if the session wasn't already in the map, or the existing entry otherwise
*/
protected abstract Session doPutIfAbsent(String id, Session session);
/**
* Compute the mappingFunction to create a Session object iff the session
* with the given id isn't already in the map, otherwise return the existing Session.
* This method is expected to have precisely the same behaviour as
* {@link java.util.concurrent.ConcurrentHashMap#computeIfAbsent}
*
* @param id the session id
* @param mappingFunction the function to load the data for the session
* @return an existing Session from the cache
*/
protected abstract Session doComputeIfAbsent(String id, Function<String, Session> mappingFunction);
/**
* Replace the mapping from id to oldValue with newValue
@ -169,22 +166,6 @@ public abstract class AbstractSessionCache extends ContainerLifeCycle implements
*/
public abstract Session doDelete(String id);
/**
* PlaceHolder
*/
protected static class PlaceHolderSession extends Session
{
/**
* @param handler SessionHandler to which this session belongs
* @param data the session data
*/
public PlaceHolderSession(SessionHandler handler, SessionData data)
{
super(handler, data);
}
}
/**
* @param handler the {@link SessionHandler} to use
*/
@ -202,6 +183,9 @@ public abstract class AbstractSessionCache extends ContainerLifeCycle implements
return _handler;
}
/**
* @see org.eclipse.jetty.server.session.SessionCache#initialize(org.eclipse.jetty.server.session.SessionContext)
*/
@Override
public void initialize(SessionContext context)
{
@ -248,6 +232,9 @@ public abstract class AbstractSessionCache extends ContainerLifeCycle implements
return _sessionDataStore;
}
/**
* @see org.eclipse.jetty.server.session.SessionCache#setSessionDataStore(org.eclipse.jetty.server.session.SessionDataStore)
*/
@Override
public void setSessionDataStore(SessionDataStore sessionStore)
{
@ -255,6 +242,9 @@ public abstract class AbstractSessionCache extends ContainerLifeCycle implements
_sessionDataStore = sessionStore;
}
/**
* @see org.eclipse.jetty.server.session.SessionCache#getEvictionPolicy()
*/
@ManagedAttribute(value = "session eviction policy", readonly = true)
@Override
public int getEvictionPolicy()
@ -266,6 +256,8 @@ public abstract class AbstractSessionCache extends ContainerLifeCycle implements
* -1 means we never evict inactive sessions.
* 0 means we evict a session after the last request for it exits
* &gt;0 is the number of seconds after which we evict inactive sessions from the cache
*
* @see org.eclipse.jetty.server.session.SessionCache#setEvictionPolicy(int)
*/
@Override
public void setEvictionPolicy(int evictionTimeout)
@ -300,7 +292,7 @@ public abstract class AbstractSessionCache extends ContainerLifeCycle implements
* If a session's data cannot be loaded from the store without error, remove
* it from the persistent store.
*
* @param removeUnloadableSessions if {@code true} unloadable sessions will be removed from session store
* @param removeUnloadableSessions if <code>true</code> unloadable sessions will be removed from session store
*/
@Override
public void setRemoveUnloadableSessions(boolean removeUnloadableSessions)
@ -326,6 +318,8 @@ public abstract class AbstractSessionCache extends ContainerLifeCycle implements
* If the session object is not in this session store, try getting
* the data for it from a SessionDataStore associated with the
* session manager. The usage count of the session is incremented.
*
* @see org.eclipse.jetty.server.session.SessionCache#get(java.lang.String)
*/
@Override
public Session get(String id) throws Exception
@ -341,119 +335,59 @@ public abstract class AbstractSessionCache extends ContainerLifeCycle implements
*
* @param id The session to retrieve
* @param enter if true, the usage count of the session will be incremented
* @return the Session object
* @throws Exception if the session cannot be retrieved
* @return
* @throws Exception
*/
protected Session getAndEnter(String id, boolean enter) throws Exception
{
Session session = null;
Exception ex = null;
while (true)
session = doComputeIfAbsent(id, k ->
{
session = doGet(id);
if (LOG.isDebugEnabled())
LOG.debug("Session {} not found locally in {}, attempting to load", id, this);
if (_sessionDataStore == null)
break; //can't load any session data so just return null or the session object
if (session == null)
try
{
if (LOG.isDebugEnabled())
LOG.debug("Session {} not found locally in {}, attempting to load", id, this);
//didn't get a session, try and create one and put in a placeholder for it
PlaceHolderSession phs = new PlaceHolderSession(_handler, new SessionData(id, null, null, 0, 0, 0, 0));
AutoLock phsLock = phs.lock();
Session s = doPutIfAbsent(id, phs);
if (s == null)
Session s = loadSession(k);
if (s != null)
{
//My placeholder won, go ahead and load the full session data
try
try (AutoLock lock = s.lock())
{
session = loadSession(id);
if (session == null)
{
//session does not exist, remove the placeholder
doDelete(id);
phsLock.close();
break;
}
try (AutoLock lock = session.lock())
{
//swap it in instead of the placeholder
boolean success = doReplace(id, phs, session);
if (!success)
{
//something has gone wrong, it should have been our placeholder
doDelete(id);
session = null;
LOG.warn("Replacement of placeholder for session {} failed", id);
phsLock.close();
break;
}
else
{
//successfully swapped in the session
session.setResident(true);
if (enter)
session.use();
phsLock.close();
break;
}
}
}
catch (Exception e)
{
ex = e; //remember a problem happened loading the session
doDelete(id); //remove the placeholder
phsLock.close();
session = null;
break;
s.setResident(true); //ensure freshly loaded session is resident
}
}
else
{
//my placeholder didn't win, check the session returned
phsLock.close();
try (AutoLock lock = s.lock())
{
//is it a placeholder? or is a non-resident session? In both cases, chuck it away and start again
if (!s.isResident() || s instanceof PlaceHolderSession)
{
session = null;
continue;
}
//I will use this session too
session = s;
if (enter)
session.use();
break;
}
if (LOG.isDebugEnabled())
LOG.debug("Session {} not loaded by store", id);
}
return s;
}
else
catch (Exception e)
{
//check the session returned
try (AutoLock lock = session.lock())
{
//is it a placeholder? or is it passivated? In both cases, chuck it away and start again
if (!session.isResident() || session instanceof PlaceHolderSession)
{
session = null;
continue;
}
LOG.warn("Error loading session {}", id, e);
return null;
}
});
//got the session
if (enter)
session.use();
break;
if (session != null)
{
try (AutoLock lock = session.lock())
{
if (!session.isResident()) //session isn't marked as resident in cache
{
if (LOG.isDebugEnabled())
LOG.debug("Non-resident session {} in cache", id);
return null;
}
else if (enter)
{
session.use();
}
}
}
if (ex != null)
throw ex;
return session;
}
@ -496,8 +430,8 @@ public abstract class AbstractSessionCache extends ContainerLifeCycle implements
* Add an entirely new session (created by the application calling Request.getSession(true))
* to the cache. The usage count of the fresh session is incremented.
*
* @param id the session id
* @param session the new session to add
* @param id the id
* @param session
*/
@Override
public void add(String id, Session session) throws Exception
@ -559,6 +493,9 @@ public abstract class AbstractSessionCache extends ContainerLifeCycle implements
}
}
/**
* @deprecated
*/
@Override
public void put(String id, Session session) throws Exception
{
@ -577,6 +514,8 @@ public abstract class AbstractSessionCache extends ContainerLifeCycle implements
*
* If the evictionPolicy == SessionCache.EVICT_ON_SESSION_EXIT then after we have saved
* the session, we evict it from the cache.
*
* @see org.eclipse.jetty.server.session.SessionCache#release(java.lang.String, org.eclipse.jetty.server.session.Session)
*/
@Override
public void release(String id, Session session) throws Exception
@ -663,6 +602,7 @@ public abstract class AbstractSessionCache extends ContainerLifeCycle implements
* it will check with the data store.
*
* @throws Exception the Exception
* @see org.eclipse.jetty.server.session.SessionCache#exists(java.lang.String)
*/
@Override
public boolean exists(String id) throws Exception
@ -685,6 +625,8 @@ public abstract class AbstractSessionCache extends ContainerLifeCycle implements
/**
* Check to see if this cache contains an entry for the session
* corresponding to the session id.
*
* @see org.eclipse.jetty.server.session.SessionCache#contains(java.lang.String)
*/
@Override
public boolean contains(String id) throws Exception
@ -695,6 +637,8 @@ public abstract class AbstractSessionCache extends ContainerLifeCycle implements
/**
* Remove a session object from this store and from any backing store.
*
* @see org.eclipse.jetty.server.session.SessionCache#delete(java.lang.String)
*/
@Override
public Session delete(String id) throws Exception
@ -719,6 +663,9 @@ public abstract class AbstractSessionCache extends ContainerLifeCycle implements
return doDelete(id);
}
/**
* @see org.eclipse.jetty.server.session.SessionCache#checkExpiration(Set)
*/
@Override
public Set<String> checkExpiration(Set<String> candidates)
{
@ -766,7 +713,7 @@ public abstract class AbstractSessionCache extends ContainerLifeCycle implements
if (LOG.isDebugEnabled())
LOG.debug("Checking for idle {}", session.getId());
try (AutoLock lock = session.lock())
try (AutoLock s = session.lock())
{
if (getEvictionPolicy() > 0 && session.isIdleLongerThan(getEvictionPolicy()) &&
session.isValid() && session.isResident() && session.getRequests() <= 0)
@ -831,7 +778,7 @@ public abstract class AbstractSessionCache extends ContainerLifeCycle implements
try (AutoLock lock = session.lock())
{
final String oldId = session.getId();
String oldId = session.getId();
session.checkValidForWrite(); //can't change id on invalid session
session.getSessionData().setId(newId);
session.getSessionData().setLastSaved(0); //pretend that the session has never been saved before to get a full save
@ -852,6 +799,9 @@ public abstract class AbstractSessionCache extends ContainerLifeCycle implements
}
}
/**
* @see org.eclipse.jetty.server.session.SessionCache#setSaveOnInactiveEviction(boolean)
*/
@Override
public void setSaveOnInactiveEviction(boolean saveOnEvict)
{
@ -871,6 +821,28 @@ public abstract class AbstractSessionCache extends ContainerLifeCycle implements
return _saveOnInactiveEviction;
}
/**
* @see org.eclipse.jetty.server.session.SessionCache#newSession(javax.servlet.http.HttpServletRequest, java.lang.String, long, long)
*/
@Override
public Session newSession(HttpServletRequest request, String id, long time, long maxInactiveMs)
{
if (LOG.isDebugEnabled())
LOG.debug("Creating new session id=" + id);
Session session = newSession(request, _sessionDataStore.newSessionData(id, time, time, time, maxInactiveMs));
session.getSessionData().setLastNode(_context.getWorkerName());
try
{
if (isSaveOnCreate() && _sessionDataStore != null)
_sessionDataStore.store(id, session.getSessionData());
}
catch (Exception e)
{
LOG.warn("Save of new session {} failed", id, e);
}
return session;
}
@Override
public String toString()
{

View File

@ -19,6 +19,8 @@
package org.eclipse.jetty.server.session;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import javax.servlet.http.HttpServletRequest;
import org.eclipse.jetty.util.annotation.ManagedAttribute;
@ -80,18 +82,12 @@ public class DefaultSessionCache extends AbstractSessionCache
return _stats.getTotal();
}
/**
*
*/
@ManagedOperation(value = "reset statistics", impact = "ACTION")
public void resetStats()
{
_stats.reset();
}
/**
* @see org.eclipse.jetty.server.session.AbstractSessionCache#doGet(java.lang.String)
*/
@Override
public Session doGet(String id)
{
@ -103,26 +99,32 @@ public class DefaultSessionCache extends AbstractSessionCache
return session;
}
/**
* @see org.eclipse.jetty.server.session.AbstractSessionCache#doPutIfAbsent(java.lang.String, org.eclipse.jetty.server.session.Session)
*/
@Override
public Session doPutIfAbsent(String id, Session session)
{
Session s = _sessions.putIfAbsent(id, session);
if (s == null && !(session instanceof PlaceHolderSession))
if (s == null)
_stats.increment();
return s;
}
/**
* @see org.eclipse.jetty.server.session.AbstractSessionCache#doDelete(java.lang.String)
*/
@Override
protected Session doComputeIfAbsent(String id, Function<String, Session> mappingFunction)
{
return _sessions.computeIfAbsent(id, k ->
{
Session s = mappingFunction.apply(k);
if (s != null)
_stats.increment();
return s;
});
}
@Override
public Session doDelete(String id)
{
Session s = _sessions.remove(id);
if (s != null && !(s instanceof PlaceHolderSession))
if (s != null)
_stats.decrement();
return s;
}
@ -168,9 +170,6 @@ public class DefaultSessionCache extends AbstractSessionCache
}
}
/**
* @see org.eclipse.jetty.server.session.AbstractSessionCache#newSession(javax.servlet.http.HttpServletRequest, org.eclipse.jetty.server.session.SessionData)
*/
@Override
public Session newSession(HttpServletRequest request, SessionData data)
{
@ -178,9 +177,6 @@ public class DefaultSessionCache extends AbstractSessionCache
return s;
}
/**
* @see org.eclipse.jetty.server.session.AbstractSessionCache#newSession(org.eclipse.jetty.server.session.SessionData)
*/
@Override
public Session newSession(SessionData data)
{
@ -188,15 +184,10 @@ public class DefaultSessionCache extends AbstractSessionCache
return s;
}
/**
* @see org.eclipse.jetty.server.session.AbstractSessionCache#doReplace(java.lang.String, org.eclipse.jetty.server.session.Session, org.eclipse.jetty.server.session.Session)
*/
@Override
public boolean doReplace(String id, Session oldValue, Session newValue)
{
boolean result = _sessions.replace(id, oldValue, newValue);
if (result && (oldValue instanceof PlaceHolderSession))
_stats.increment();
return result;
}
}

View File

@ -18,6 +18,8 @@
package org.eclipse.jetty.server.session;
import java.util.function.Function;
import javax.servlet.http.HttpServletRequest;
/**
@ -39,35 +41,23 @@ public class NullSessionCache extends AbstractSessionCache
super.setEvictionPolicy(EVICT_ON_SESSION_EXIT);
}
/**
* @see org.eclipse.jetty.server.session.SessionCache#shutdown()
*/
@Override
public void shutdown()
{
}
/**
* @see org.eclipse.jetty.server.session.AbstractSessionCache#newSession(org.eclipse.jetty.server.session.SessionData)
*/
@Override
public Session newSession(SessionData data)
{
return new Session(getSessionHandler(), data);
}
/**
* @see org.eclipse.jetty.server.session.AbstractSessionCache#newSession(javax.servlet.http.HttpServletRequest, org.eclipse.jetty.server.session.SessionData)
*/
@Override
public Session newSession(HttpServletRequest request, SessionData data)
{
return new Session(getSessionHandler(), request, data);
}
/**
* @see org.eclipse.jetty.server.session.AbstractSessionCache#doGet(java.lang.String)
*/
@Override
public Session doGet(String id)
{
@ -75,9 +65,6 @@ public class NullSessionCache extends AbstractSessionCache
return null;
}
/**
* @see org.eclipse.jetty.server.session.AbstractSessionCache#doPutIfAbsent(java.lang.String, org.eclipse.jetty.server.session.Session)
*/
@Override
public Session doPutIfAbsent(String id, Session session)
{
@ -85,9 +72,6 @@ public class NullSessionCache extends AbstractSessionCache
return null;
}
/**
* @see org.eclipse.jetty.server.session.AbstractSessionCache#doReplace(java.lang.String, org.eclipse.jetty.server.session.Session, org.eclipse.jetty.server.session.Session)
*/
@Override
public boolean doReplace(String id, Session oldValue, Session newValue)
{
@ -95,21 +79,21 @@ public class NullSessionCache extends AbstractSessionCache
return true;
}
/**
* @see org.eclipse.jetty.server.session.AbstractSessionCache#doDelete(java.lang.String)
*/
@Override
public Session doDelete(String id)
{
return null;
}
/**
* @see org.eclipse.jetty.server.session.AbstractSessionCache#setEvictionPolicy(int)
*/
@Override
public void setEvictionPolicy(int evictionTimeout)
{
LOG.warn("Ignoring eviction setting:" + evictionTimeout);
}
@Override
protected Session doComputeIfAbsent(String id, Function<String, Session> mappingFunction)
{
return mappingFunction.apply(id);
}
}

View File

@ -261,33 +261,26 @@ public class SessionHandler extends ScopedHandler
* Individual SessionManagers implementations may accept arbitrary listener types,
* but they are expected to at least handle HttpSessionActivationListener,
* HttpSessionAttributeListener, HttpSessionBindingListener and HttpSessionListener.
* @return true if the listener was added
* @see #removeEventListener(EventListener)
* @see HttpSessionAttributeListener
* @see HttpSessionListener
* @see HttpSessionIdListener
*/
public void addEventListener(EventListener listener)
@Override
public boolean addEventListener(EventListener listener)
{
if (listener instanceof HttpSessionAttributeListener)
_sessionAttributeListeners.add((HttpSessionAttributeListener)listener);
if (listener instanceof HttpSessionListener)
_sessionListeners.add((HttpSessionListener)listener);
if (listener instanceof HttpSessionIdListener)
_sessionIdListeners.add((HttpSessionIdListener)listener);
addBean(listener, false);
}
/**
* Removes all event listeners for session-related events.
*
* @see #removeEventListener(EventListener)
*/
public void clearEventListeners()
{
for (EventListener e : getBeans(EventListener.class))
if (super.addEventListener(listener))
{
removeBean(e);
if (listener instanceof HttpSessionAttributeListener)
_sessionAttributeListeners.add((HttpSessionAttributeListener)listener);
if (listener instanceof HttpSessionListener)
_sessionListeners.add((HttpSessionListener)listener);
if (listener instanceof HttpSessionIdListener)
_sessionIdListeners.add((HttpSessionIdListener)listener);
return true;
}
_sessionAttributeListeners.clear();
_sessionListeners.clear();
_sessionIdListeners.clear();
return false;
}
/**
@ -529,6 +522,16 @@ public class SessionHandler extends ScopedHandler
return _httpOnly;
}
/**
* @return The sameSite setting for session cookies or null for no setting
* @see HttpCookie#getSameSite()
*/
@ManagedAttribute("SameSite setting for session cookies")
public HttpCookie.SameSite getSameSite()
{
return HttpCookie.getSameSiteFromComment(_sessionComment);
}
/**
* Returns the <code>HttpSession</code> with the given session id
*
@ -649,30 +652,18 @@ public class SessionHandler extends ScopedHandler
sessionPath = (StringUtil.isEmpty(sessionPath)) ? "/" : sessionPath;
String id = getExtendedId(session);
HttpCookie cookie = null;
if (_sessionComment == null)
{
cookie = new HttpCookie(
_cookieConfig.getName(),
id,
_cookieConfig.getDomain(),
sessionPath,
_cookieConfig.getMaxAge(),
_cookieConfig.isHttpOnly(),
_cookieConfig.isSecure() || (isSecureRequestOnly() && requestIsSecure));
}
else
{
cookie = new HttpCookie(
_cookieConfig.getName(),
id,
_cookieConfig.getDomain(),
sessionPath,
_cookieConfig.getMaxAge(),
_cookieConfig.isHttpOnly(),
_cookieConfig.isSecure() || (isSecureRequestOnly() && requestIsSecure),
_sessionComment,
1);
}
cookie = new HttpCookie(
_cookieConfig.getName(),
id,
_cookieConfig.getDomain(),
sessionPath,
_cookieConfig.getMaxAge(),
_cookieConfig.isHttpOnly(),
_cookieConfig.isSecure() || (isSecureRequestOnly() && requestIsSecure),
HttpCookie.getCommentWithoutAttributes(_cookieConfig.getComment()),
0,
HttpCookie.getSameSiteFromComment(_cookieConfig.getComment()));
return cookie;
}
@ -785,21 +776,20 @@ public class SessionHandler extends ScopedHandler
}
}
/**
* Removes an event listener for for session-related events.
*
* @param listener the session event listener to remove
* @see #addEventListener(EventListener)
*/
public void removeEventListener(EventListener listener)
@Override
public boolean removeEventListener(EventListener listener)
{
if (listener instanceof HttpSessionAttributeListener)
_sessionAttributeListeners.remove(listener);
if (listener instanceof HttpSessionListener)
_sessionListeners.remove(listener);
if (listener instanceof HttpSessionIdListener)
_sessionIdListeners.remove(listener);
removeBean(listener);
if (super.removeEventListener(listener))
{
if (listener instanceof HttpSessionAttributeListener)
_sessionAttributeListeners.remove(listener);
if (listener instanceof HttpSessionListener)
_sessionListeners.remove(listener);
if (listener instanceof HttpSessionIdListener)
_sessionIdListeners.remove(listener);
return true;
}
return false;
}
/**
@ -813,13 +803,28 @@ public class SessionHandler extends ScopedHandler
}
/**
* @param httpOnly The httpOnly to set.
* Set if Session cookies should use HTTP Only
* @param httpOnly True if cookies should be HttpOnly.
* @see HttpCookie
*/
public void setHttpOnly(boolean httpOnly)
{
_httpOnly = httpOnly;
}
/**
* Set Session cookie sameSite mode.
* Currently this is encoded in the session comment until sameSite is supported by {@link SessionCookieConfig}
* @param sameSite The sameSite setting for Session cookies (or null for no sameSite setting)
*/
public void setSameSite(HttpCookie.SameSite sameSite)
{
// Encode in comment whilst not supported by SessionConfig, so that it can be set/saved in
// web.xml and quickstart.
// Always pass false for httpOnly as it has it's own setter.
_sessionComment = HttpCookie.getCommentWithAttributes(_sessionComment, false, sameSite);
}
/**
* @param metaManager The metaManager used for cross context session management.
*/
@ -1353,6 +1358,8 @@ public class SessionHandler extends ScopedHandler
* CookieConfig
*
* Implementation of the javax.servlet.SessionCookieConfig.
* SameSite configuration can be achieved by using setComment
* @see HttpCookie
*/
public final class CookieConfig implements SessionCookieConfig
{

View File

@ -0,0 +1,112 @@
//
// ========================================================================
// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.server;
import java.nio.ByteBuffer;
import org.eclipse.jetty.http.HttpCompliance;
import org.eclipse.jetty.http.MetaData;
import org.eclipse.jetty.io.Connection;
import org.eclipse.jetty.io.EndPoint;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.thread.ThreadPool;
import org.junit.jupiter.api.BeforeEach;
/**
* Extended Server Tester.
*/
public class DelayedServerTest extends HttpServerTestBase
{
@BeforeEach
public void init() throws Exception
{
startServer(new ServerConnector(_server, new HttpConnectionFactory()
{
@Override
public Connection newConnection(Connector connector, EndPoint endPoint)
{
return configure(new DelayedHttpConnection(getHttpConfiguration(), connector, endPoint), connector, endPoint);
}
}));
}
private static class DelayedHttpConnection extends HttpConnection
{
public DelayedHttpConnection(HttpConfiguration config, Connector connector, EndPoint endPoint)
{
super(config, connector, endPoint, false);
}
@Override
public void send(MetaData.Request request, MetaData.Response response, ByteBuffer content, boolean lastContent, Callback callback)
{
DelayedCallback delay = new DelayedCallback(callback, getServer().getThreadPool());
super.send(request, response, content, lastContent, delay);
}
}
private static class DelayedCallback extends Callback.Nested
{
final ThreadPool pool;
public DelayedCallback(Callback callback, ThreadPool threadPool)
{
super(callback);
pool = threadPool;
}
@Override
public void succeeded()
{
pool.execute(()->
{
try
{
Thread.sleep(10);
}
catch (InterruptedException e)
{
}
finally
{
super.succeeded();
}
});
}
@Override
public void failed(Throwable x)
{
pool.execute(()->
{
try
{
Thread.sleep(20);
}
catch (InterruptedException e)
{
}
finally
{
super.failed(x);
}
});
}
}
}

View File

@ -21,7 +21,6 @@ package org.eclipse.jetty.server;
import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
import java.nio.channels.SelectableChannel;
import java.nio.channels.SelectionKey;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;
@ -73,11 +72,6 @@ public class ExtendedServerTest extends HttpServerTestBase
{
private volatile long _lastSelected;
public ExtendedEndPoint(SelectableChannel channel, ManagedSelector selector, SelectionKey key, Scheduler scheduler)
{
super(channel, selector, key, scheduler);
}
public ExtendedEndPoint(SocketChannel channel, ManagedSelector selector, SelectionKey key, Scheduler scheduler)
{
super(channel, selector, key, scheduler);
@ -109,10 +103,10 @@ public class ExtendedServerTest extends HttpServerTestBase
return new HttpChannelOverHttp(this, getConnector(), getHttpConfiguration(), getEndPoint(), this)
{
@Override
public boolean startRequest(String method, String uri, HttpVersion version)
public void startRequest(String method, String uri, HttpVersion version)
{
getRequest().setAttribute("DispatchedAt", ((ExtendedEndPoint)getEndPoint()).getLastSelected());
return super.startRequest(method, uri, version);
super.startRequest(method, uri, version);
}
};
}

View File

@ -832,12 +832,11 @@ public class HttpConnectionTest
@Test
public void testBadURIencoding() throws Exception
{
// The URI is being leniently decoded, leaving the "%x" alone
String response = connector.getResponse("GET /bad/encoding%x HTTP/1.1\r\n" +
"Host: localhost\r\n" +
"Connection: close\r\n" +
"\r\n");
checkContains(response, 0, "HTTP/1.1 200");
checkContains(response, 0, "HTTP/1.1 400");
}
@Test

View File

@ -28,10 +28,12 @@ import java.io.LineNumberReader;
import java.io.OutputStream;
import java.net.Socket;
import java.net.URL;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.concurrent.Exchanger;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.servlet.AsyncContext;
import javax.servlet.ServletException;
import javax.servlet.ServletInputStream;
import javax.servlet.ServletOutputStream;
@ -42,6 +44,8 @@ import org.eclipse.jetty.http.tools.HttpTester;
import org.eclipse.jetty.io.EndPoint;
import org.eclipse.jetty.io.EofException;
import org.eclipse.jetty.server.handler.AbstractHandler;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.IO;
import org.eclipse.jetty.util.log.AbstractLogger;
import org.eclipse.jetty.util.log.Log;
@ -251,6 +255,24 @@ public abstract class HttpServerTestBase extends HttpServerTestFixture
}
}
@Test
public void testBadURI() throws Exception
{
configureServer(new HelloWorldHandler());
try (Socket client = newSocket(_serverURI.getHost(), _serverURI.getPort()))
{
OutputStream os = client.getOutputStream();
os.write("GET /%xx HTTP/1.0\r\n\r\n".getBytes(StandardCharsets.ISO_8859_1));
os.flush();
// Read the response.
String response = readResponse(client);
assertThat(response, Matchers.containsString("HTTP/1.1 400 "));
}
}
@Test
public void testExceptionThrownInHandlerLoop() throws Exception
{
@ -1798,4 +1820,53 @@ public abstract class HttpServerTestBase extends HttpServerTestFixture
assertThat(client.getInputStream().read(), is(-1));
}
}
@Test
public void testSendAsyncContent() throws Exception
{
int size = 64 * 1024;
configureServer(new SendAsyncContentHandler(size));
try (Socket client = newSocket(_serverURI.getHost(), _serverURI.getPort()))
{
OutputStream os = client.getOutputStream();
os.write(("GET / HTTP/1.1\r\nHost: localhost\r\n\r\n").getBytes(StandardCharsets.ISO_8859_1));
os.flush();
HttpTester.Response response = HttpTester.parseResponse(client.getInputStream());
assertThat(response.getStatus(), is(200));
assertThat(response.getContentBytes().length, is(size));
// Try again to check previous request completed OK
os.write(("GET / HTTP/1.1\r\nHost: localhost\r\n\r\n").getBytes(StandardCharsets.ISO_8859_1));
os.flush();
response = HttpTester.parseResponse(client.getInputStream());
assertThat(response.getStatus(), is(200));
assertThat(response.getContentBytes().length, is(size));
}
}
private class SendAsyncContentHandler extends AbstractHandler
{
final ByteBuffer content;
public SendAsyncContentHandler(int size)
{
content = BufferUtil.allocate(size);
Arrays.fill(content.array(),0,size,(byte)'X');
content.position(0);
content.limit(size);
}
@Override
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
{
baseRequest.setHandled(true);
response.setStatus(200);
response.setContentType("application/unknown");
response.setContentLength(content.remaining());
AsyncContext async = request.startAsync();
((HttpOutput)response.getOutputStream()).sendContent(content.slice(), Callback.from(async::complete));
}
}
}

View File

@ -70,7 +70,7 @@ public class ShutdownHandlerTest
start(null);
CountDownLatch stopLatch = new CountDownLatch(1);
server.addLifeCycleListener(new AbstractLifeCycle.AbstractLifeCycleListener()
server.addEventListener(new AbstractLifeCycle.AbstractLifeCycleListener()
{
@Override
public void lifeCycleStopped(LifeCycle event)

Some files were not shown because too many files have changed in this diff Show More