Merge remote-tracking branch 'origin/jetty-10.0.x' into jetty-10.0.x-1743-refactor-maven-plugin-redux
This commit is contained in:
commit
f2c0d86de5
|
@ -0,0 +1,18 @@
|
|||
---
|
||||
name: Issue
|
||||
about: Reporting bugs and problems in Eclipse Jetty
|
||||
title: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Jetty version**
|
||||
|
||||
**Java version**
|
||||
|
||||
**OS type/version**
|
||||
|
||||
**Description**
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
---
|
||||
name: Question
|
||||
about: Asking questions about Eclipse Jetty
|
||||
title: ''
|
||||
labels: Question
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Jetty version**
|
||||
|
||||
**Java version**
|
||||
|
||||
**Question**
|
||||
|
||||
|
|
@ -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.
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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())
|
||||
{
|
||||
|
|
|
@ -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]);
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) + ",*");
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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__"));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
*
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
{
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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]);
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
{
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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()
|
||||
{
|
||||
|
||||
/**
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"/>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"/>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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()
|
||||
{
|
||||
|
|
|
@ -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
|
||||
* >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()
|
||||
{
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
{
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
Loading…
Reference in New Issue