Merge remote-tracking branch 'eclipse/jetty-10.0.x' into jetty-10.0.x-4407-JavaxAnnotatedConfigDefault

Signed-off-by: Lachlan Roberts <lachlan@webtide.com>
This commit is contained in:
Lachlan Roberts 2019-12-13 11:43:36 +11:00
commit 313f2e49f5
146 changed files with 3782 additions and 2049 deletions

2
.github/stale.yml vendored
View File

@ -13,7 +13,7 @@ staleLabel: Stale
# Comment to post when marking an issue as stale. Set to `false` to disable # Comment to post when marking an issue as stale. Set to `false` to disable
markComment: > markComment: >
This issue has been automatically marked as stale because it has been a 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. full year without activity. It will be closed if no further activity occurs.
Thank you for your contributions. Thank you for your contributions.
# Comment to post when closing a stale issue. Set to `false` to disable # Comment to post when closing a stale issue. Set to `false` to disable
closeComment: > closeComment: >

View File

@ -1,7 +1,169 @@
jetty-10.0.0-SNAPSHOT jetty-10.0.0-SNAPSHOT
jetty-10.0.0-alpha0 - 11 July jetty-10.0.0.alpha1 - 26 November 2019
2019 + 97 Permanent UnavailableException thrown during servlet request handling
should cause servlet destroy
+ 137 Support OAuth
+ 155 No way to set keystore for JSR 356 websocket clients, needed for SSL
client authentication
+ 250 Implement HTTP CONNECT for HTTP/2
+ 995 UrlEncoded.encodeString should skip more characters
+ 1036 Allow easy configuration of Scheduler-Threads and name them more
appropriate
+ 1485 Add systemd service file
+ 1743 Refactor jetty maven plugin goals to be more orthogonal
+ 2266 Jetty maven plugin reload is triggered each time the
`scanIntervalSeconds` pass
+ 2340 Remove raw ServletHandler usage examples from documentation
+ 2429 Review HttpClient backpressure semantic
+ 2578 Use addEventListener(EventListener listener)
+ 2709 current default for headerCacheSize is not large enough for many
requests
+ 2815 hpack fields are opaque octets
+ 3040 Allow RFC6265 Cookies to include optional SameSite attribute.
+ 3083 The ini-template for jetty.console-capture.dir does not match the
default value
+ 3106 Websocket connection stats and request stats
+ 3558 Error notifications can be received after a successful websocket close
+ 3601 HTTP2 stall on reset streams
+ 3705 Review ClientUpgradeRequest exception handling
+ 3734 websocket suspend when input closed
+ 3747 Make Jetty Demo work with JPMS
+ 3787 Jetty client sometimes returns EOFException instead of
SSLHandshakeException on certificate errors.
+ 3804 Weld/CDI XML backwards compat?
+ 3806 Error Page handling Async race with ProxyServlet
+ 3822 trustAll will not work on some servers
+ 3829 Avoid sending empty trailer frames for http/2 responses
+ 3840 Byte-range request performance problems with large files
+ 3856 Different behaviour with maxFormContentSize=0 if Content-Length header
is present/missing
+ 3863 Enforce use of SNI
+ 3869 Update to ASM 7.2 for jdk 13
+ 3872 Review exposure of JavaxWebSocketServletContainerInitializer
+ 3876 WebSocketPartialListener is only called for initial frames, not for
continuation frames
+ 3884 @WebSocket without @OnWebSocketMessage handler fails when receiving a
continuation frame
+ 3888 BufferUtil.toBuffer(Resource resource,boolean direct) does not like
large (4G+) Resources
+ 3906 Fix for #3840 breaks Path encapsulation in PathResource
+ 3913 Clustered HttpSession IllegalStateException: Invalid for read
+ 3929 Deadlock between new HTTP2Connection() and Server.stop()
+ 3936 Race condition when modifying session + sendRedirect()
+ 3940 Double initialization of Log
+ 3951 Consider adding demand API to HTTP/2
+ 3952 Server configuration for direct/heap ByteBuffers
+ 3956 Remove and warn on use of illegal HTTP/2 response headers
+ 3957 CustomRequestLog bad usage of MethodHandles.lookup()
+ 3960 Fix HttpConfiguration copy constructor
+ 3964 Improve efficiency of listeners
+ 3968 WebSocket sporadic ReadPendingException using suspend/resume
+ 3969 X-Forwarded-Port header customization isn't possible
+ 3978 HTTP/2 fixes for robustly handling abnormal traffic and resource
exhaustion
+ 3983 JarFileResource incorrectly lists the contents of directories with
spaces
+ 3985 Improve lenient Cookie parsing
+ 3989 Inform custom ManagedSelector of dead selector via optional
onFailedSelect()
+ 4000 Add SameFileAliasChecker to help with FileSystem static file access
normalization on Mac and Windows
+ 4003 Quickstart broken in jetty-10
+ 4007 Getting NullPointerException while trying to run jetty start.run on
Windows
+ 4009 ServletContextHandler setSecurityHandler broke handler chain
+ 4020 Revert WebSocket ExtensionFactory change to interface
+ 4022 Servlet which is added by ServletRegistration can't be started
+ 4025 Provide more write-through behaviours for DefaultSessionCache
+ 4027 Ensure AbstractSessionDataStore cannot be used unless it is started
+ 4033 Ignore bad percent encodings in paths during
URIUtil.equalsIgnoreEncodings()
+ 4047 Gracefully stopped Jetty not flushing all response data
+ 4048 Multiple values in X-Forwarded-Port throw NumberFormatException
+ 4057 NullPointerException in o.e.j.h.HttpFields
+ 4058 Review Locker
+ 4064 java.lang.NullPointerException initializing embedded servlet
+ 4075 Do not fail on servlet-mapping with url-pattern /On*
+ 4076 Restarting quickstarted webapp throws IllegalStateException:
ServletContainerInitializersStarter already exists
+ 4082 Debug logging causes NullPointerException in client
+ 4084 Use of HttpConfiguration.setBlockingTimeout(long) in jetty.xml produces
warning on jetty-home startup
+ 4096 Thread in ReservedThreadExecutor does not exit when stopped
+ 4104 Frames are sent through ExtensionStack even if WebSocket Session is
closed
+ 4105 QueuedThreadPool increased thread usage and no idle thread decay
+ 4113 HttpClient fails with JDK 13 and TLS 1.3
+ 4115 Drop HTTP/2 pseudo headers
+ 4121 QueuedThreadPool should support ThreadFactory behaviors
+ 4122 QueuedThreadPool should reset thread interrupted on failed run
+ 4124 Run websocket autobahn tests with jetty and javax apis instead of just
with core.
+ 4128 OpenIdCredentials can't decode JWT ID token
+ 4132 Should be possible to use OIDC without metadata
+ 4138 OpenID module should use HttpClient instead of HttpURLConnection
+ 4141 ClassCastException with non-async Servlet + async Filter +
HttpServletRequestWrapper
+ 4142 Configurable HTTP/2 RateControl
+ 4144 Naked cast to Request should be avoided
+ 4150 Module org.eclipse.jetty.alpn.client not found, required by
org.eclipse.jetty.proxy
+ 4152 WebSocket autoFragment does not fragment based on maxFrameSize
+ 4156 IllegalStateException when forwarding to jsp with new session
+ 4161 Regression: EofException: request lifecycle violation
+ 4170 Client-side alias selection based on SSLEngine
+ 4173 NullPointerException warning in log from WebInfConfiguration after
upgrade
+ 4174 ConcurrentModificationException when stopping jetty:run-war
+ 4176 Should not set header if sendError has been called
+ 4177 Configure HTTP proxy with SslContextFactory
+ 4179 Improve HttpChannel$SendCallback references for GC
+ 4183 Jetty considers bootstrap injected class to be a "server class"
+ 4188 Spin in HttpOutput.close
+ 4190 Jetty hangs after thread blocked in SharedBlockingCallback.block()
called by HttpOutput.close
+ 4191 Increase GzipHandler minGzipSize default value
+ 4193 InetAccessHandler - new includeConnectors/excludeConnectors not quite
correct anymore
+ 4201 Throw SSLHandshakeException in case of TLS handshake failures
+ 4203 Some Transfer-Encoding and Content-Length combinations do not result in
expected 400 Bad Request
+ 4204 Transfer-Encoding behavior does not follow RFC7230
+ 4208 304 response with Content-Length fails, not conform to RFC7230
+ 4209 Unused TLS connection is not closed in Java 11
+ 4217 SslConnection.DecryptedEnpoint.flush eternal busy loop
+ 4222 Major/Minor Version wrong (jetty 10 is servlet 4)
+ 4227 First authorization request produced by OIDC module fails due to
inclusion of sessionid
+ 4236 clean up redirect code calculation for OpenIdAuthenticator
+ 4237 simplify openid module configuration
+ 4240 CGI form post results in 500 response if no character encoding
+ 4243 ErrorHandler produces invalid json error response
+ 4247 Cookie security attributes are going to mandated by Google Chrome
+ 4248 Websocket client UpgradeListener never reports success
+ 4251 Http 2.0 clients cannot upgrade protocol
+ 4258 RateControl should be per-connection
+ 4264 Spring Boot BasicErrorController no longer invoked
+ 4265 HttpChannel SEND_ERROR should use ErrorHandler.doError()
+ 4277 Reading streamed gzipped body never terminates
+ 4279 Regression: ResponseWriter#close blocks indefinitely
+ 4282 Review HttpParser handling in case of no content
+ 4283 Wrong package for OpenJDK8ClientALPNProcessor
+ 4284 Possible NullPointerException in Main.java when stopped from command
line
+ 4287 Move getUriLastPathSegment(URI uri) to URIUtil
+ 4296 Unable to create WebSocket connect if the query string of the URL has %
symbol.
+ 4301 Demand beforeContent is not forwarded
+ 4305 Jetty server ALPN shall alert fatal no_application_protocol if no
client application protocol is supported
+ 4325 Deprecate SniX509ExtendedKeyManager constructor without
SslContextFactory$Server
+ 4334 Better test ErrorHandler changes
+ 4342 OpenID module cannot create HttpClient in Jetty 10
jetty-10.0.0-alpha0 - 11 July 2019
+ 113 Add support for NCSA Extended Log File Format + 113 Add support for NCSA Extended Log File Format
+ 114 Bring back overlay deployer + 114 Bring back overlay deployer
+ 132 ClientConnector abstraction + 132 ClientConnector abstraction
@ -193,6 +355,12 @@ jetty-10.0.0-alpha0 - 11 July
+ 3849 ClosedChannelException from jetty-test-webapp javax websocket chat + 3849 ClosedChannelException from jetty-test-webapp javax websocket chat
example example
jetty-9.4.24.v20191120 - 20 November 2019
+ 3083 The ini-template for jetty.console-capture.dir does not match the
default value
+ 4128 OpenIdCredetials can't decode JWT ID token
+ 4334 Better test ErrorHandler changes
jetty-9.4.23.v20191118 - 18 November 2019 jetty-9.4.23.v20191118 - 18 November 2019
+ 1485 Add systemd service file + 1485 Add systemd service file
+ 2266 Jetty maven plugin reload is triggered each time the + 2266 Jetty maven plugin reload is triggered each time the

View File

@ -142,6 +142,13 @@
<version>1.1.1</version> <version>1.1.1</version>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<!-- transitive dependency defined as a range and we don't want that -->
<dependency>
<groupId>net.minidev</groupId>
<artifactId>json-smart</artifactId>
<version>2.3</version>
<scope>test</scope>
</dependency>
<dependency> <dependency>
<groupId>org.slf4j</groupId> <groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId> <artifactId>slf4j-api</artifactId>

View File

@ -1101,7 +1101,7 @@ public class HttpClient extends ContainerLifeCycle
return port == 80; return port == 80;
} }
static boolean isSchemeSecure(String scheme) public static boolean isSchemeSecure(String scheme)
{ {
return HttpScheme.HTTPS.is(scheme) || HttpScheme.WSS.is(scheme); return HttpScheme.HTTPS.is(scheme) || HttpScheme.WSS.is(scheme);
} }

View File

@ -26,6 +26,7 @@ import java.util.Iterator;
import org.eclipse.jetty.client.api.ContentProvider; import org.eclipse.jetty.client.api.ContentProvider;
import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.IO;
import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger; import org.eclipse.jetty.util.log.Logger;
@ -218,15 +219,8 @@ public class HttpContent implements Callback, Closeable
@Override @Override
public void close() public void close()
{ {
try if (iterator instanceof Closeable)
{ IO.close((Closeable)iterator);
if (iterator instanceof Closeable)
((Closeable)iterator).close();
}
catch (Throwable x)
{
LOG.ignore(x);
}
} }
@Override @Override

View File

@ -88,6 +88,7 @@ public class HttpRequest implements Request
private List<RequestListener> requestListeners; private List<RequestListener> requestListeners;
private BiFunction<Request, Request, Response.CompleteListener> pushListener; private BiFunction<Request, Request, Response.CompleteListener> pushListener;
private Supplier<HttpFields> trailers; private Supplier<HttpFields> trailers;
private String upgradeProtocol;
protected HttpRequest(HttpClient client, HttpConversation conversation, URI uri) protected HttpRequest(HttpClient client, HttpConversation conversation, URI uri)
{ {
@ -635,6 +636,12 @@ public class HttpRequest implements Request
return this; return this;
} }
public HttpRequest upgradeProtocol(String upgradeProtocol)
{
this.upgradeProtocol = upgradeProtocol;
return this;
}
@Override @Override
public ContentProvider getContent() public ContentProvider getContent()
{ {
@ -791,6 +798,11 @@ public class HttpRequest implements Request
return trailers; return trailers;
} }
public String getUpgradeProtocol()
{
return upgradeProtocol;
}
@Override @Override
public boolean abort(Throwable cause) public boolean abort(Throwable cause)
{ {

View File

@ -0,0 +1,34 @@
//
// ========================================================================
// 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.client;
import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.io.EndPoint;
public interface HttpUpgrader
{
public void prepare(HttpRequest request);
public void upgrade(HttpResponse response, EndPoint endPoint);
public interface Factory
{
public HttpUpgrader newHttpUpgrader(HttpVersion version);
}
}

View File

@ -19,6 +19,7 @@
package org.eclipse.jetty.client; package org.eclipse.jetty.client;
import java.io.IOException; import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.Map; import java.util.Map;
@ -29,6 +30,7 @@ import java.util.regex.Pattern;
import org.eclipse.jetty.client.api.Connection; import org.eclipse.jetty.client.api.Connection;
import org.eclipse.jetty.io.AbstractConnection; import org.eclipse.jetty.io.AbstractConnection;
import org.eclipse.jetty.io.ClientConnectionFactory; import org.eclipse.jetty.io.ClientConnectionFactory;
import org.eclipse.jetty.io.ClientConnector;
import org.eclipse.jetty.io.EndPoint; import org.eclipse.jetty.io.EndPoint;
import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.util.Callback;
@ -195,6 +197,9 @@ public class Socks4Proxy extends ProxyConfiguration.Proxy
try try
{ {
HttpDestination destination = (HttpDestination)context.get(HttpClientTransport.HTTP_DESTINATION_CONTEXT_KEY); HttpDestination destination = (HttpDestination)context.get(HttpClientTransport.HTTP_DESTINATION_CONTEXT_KEY);
// Don't want to do DNS resolution here.
InetSocketAddress address = InetSocketAddress.createUnresolved(destination.getHost(), destination.getPort());
context.put(ClientConnector.REMOTE_SOCKET_ADDRESS_CONTEXT_KEY, address);
ClientConnectionFactory connectionFactory = this.connectionFactory; ClientConnectionFactory connectionFactory = this.connectionFactory;
if (destination.isSecure()) if (destination.isSecure())
connectionFactory = destination.newSslClientConnectionFactory(null, connectionFactory); connectionFactory = destination.newSslClientConnectionFactory(null, connectionFactory);

View File

@ -29,6 +29,7 @@ import java.util.stream.Collectors;
import org.eclipse.jetty.alpn.client.ALPNClientConnection; import org.eclipse.jetty.alpn.client.ALPNClientConnection;
import org.eclipse.jetty.alpn.client.ALPNClientConnectionFactory; import org.eclipse.jetty.alpn.client.ALPNClientConnectionFactory;
import org.eclipse.jetty.client.AbstractConnectorHttpClientTransport; import org.eclipse.jetty.client.AbstractConnectorHttpClientTransport;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.HttpClientTransport; import org.eclipse.jetty.client.HttpClientTransport;
import org.eclipse.jetty.client.HttpDestination; import org.eclipse.jetty.client.HttpDestination;
import org.eclipse.jetty.client.HttpRequest; import org.eclipse.jetty.client.HttpRequest;
@ -36,7 +37,6 @@ import org.eclipse.jetty.client.MultiplexConnectionPool;
import org.eclipse.jetty.client.MultiplexHttpDestination; import org.eclipse.jetty.client.MultiplexHttpDestination;
import org.eclipse.jetty.client.Origin; import org.eclipse.jetty.client.Origin;
import org.eclipse.jetty.client.http.HttpClientConnectionFactory; import org.eclipse.jetty.client.http.HttpClientConnectionFactory;
import org.eclipse.jetty.http.HttpScheme;
import org.eclipse.jetty.http.HttpVersion; import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.io.ClientConnectionFactory; import org.eclipse.jetty.io.ClientConnectionFactory;
import org.eclipse.jetty.io.ClientConnector; import org.eclipse.jetty.io.ClientConnector;
@ -121,7 +121,7 @@ public class HttpClientTransportDynamic extends AbstractConnectorHttpClientTrans
@Override @Override
public HttpDestination.Key newDestinationKey(HttpRequest request, Origin origin) public HttpDestination.Key newDestinationKey(HttpRequest request, Origin origin)
{ {
boolean ssl = HttpScheme.HTTPS.is(request.getScheme()); boolean ssl = HttpClient.isSchemeSecure(request.getScheme());
String http1 = "http/1.1"; String http1 = "http/1.1";
String http2 = ssl ? "h2" : "h2c"; String http2 = ssl ? "h2" : "h2c";
List<String> protocols = List.of(); List<String> protocols = List.of();

View File

@ -26,6 +26,7 @@ import org.eclipse.jetty.client.HttpExchange;
import org.eclipse.jetty.client.HttpRequest; import org.eclipse.jetty.client.HttpRequest;
import org.eclipse.jetty.client.HttpResponse; import org.eclipse.jetty.client.HttpResponse;
import org.eclipse.jetty.client.HttpResponseException; import org.eclipse.jetty.client.HttpResponseException;
import org.eclipse.jetty.client.HttpUpgrader;
import org.eclipse.jetty.client.api.Response; import org.eclipse.jetty.client.api.Response;
import org.eclipse.jetty.client.api.Result; import org.eclipse.jetty.client.api.Result;
import org.eclipse.jetty.http.HttpFields; import org.eclipse.jetty.http.HttpFields;
@ -98,29 +99,24 @@ public class HttpChannelOverHTTP extends HttpChannel
return result; return result;
HttpResponse response = exchange.getResponse(); HttpResponse response = exchange.getResponse();
if (response.getVersion() == HttpVersion.HTTP_1_1 && response.getStatus() == HttpStatus.SWITCHING_PROTOCOLS_101)
if ((response.getVersion() == HttpVersion.HTTP_1_1) &&
(response.getStatus() == HttpStatus.SWITCHING_PROTOCOLS_101))
{ {
String nextConnection = response.getHeaders().get(HttpHeader.CONNECTION); String header = response.getHeaders().get(HttpHeader.CONNECTION);
if ((nextConnection == null) || !nextConnection.toLowerCase(Locale.US).contains("upgrade")) if (header == null || !header.toLowerCase(Locale.US).contains("upgrade"))
{ return new Result(result, new HttpResponseException("101 response without 'Connection: Upgrade'", response));
return new Result(result, new HttpResponseException("101 Switching Protocols without Connection: Upgrade not supported", response));
}
// Upgrade Response
HttpRequest request = exchange.getRequest(); HttpRequest request = exchange.getRequest();
HttpConnectionUpgrader upgrader = (HttpConnectionUpgrader)request.getConversation().getAttribute(HttpConnectionUpgrader.class.getName()); HttpUpgrader upgrader = (HttpUpgrader)request.getConversation().getAttribute(HttpUpgrader.class.getName());
if (upgrader != null) if (upgrader == null)
return new Result(result, new HttpResponseException("101 response without " + HttpUpgrader.class.getSimpleName(), response));
try
{ {
try upgrader.upgrade(response, getHttpConnection().getEndPoint());
{ }
upgrader.upgrade(response, getHttpConnection()); catch (Throwable x)
} {
catch (Throwable x) return new Result(result, new HttpResponseException("Could not upgrade to WebSocket", response, x));
{
return new Result(result, x);
}
} }
} }

View File

@ -30,11 +30,14 @@ import org.eclipse.jetty.client.HttpConnection;
import org.eclipse.jetty.client.HttpDestination; import org.eclipse.jetty.client.HttpDestination;
import org.eclipse.jetty.client.HttpExchange; import org.eclipse.jetty.client.HttpExchange;
import org.eclipse.jetty.client.HttpProxy; import org.eclipse.jetty.client.HttpProxy;
import org.eclipse.jetty.client.HttpRequest;
import org.eclipse.jetty.client.HttpUpgrader;
import org.eclipse.jetty.client.IConnection; import org.eclipse.jetty.client.IConnection;
import org.eclipse.jetty.client.SendFailure; import org.eclipse.jetty.client.SendFailure;
import org.eclipse.jetty.client.api.Connection; import org.eclipse.jetty.client.api.Connection;
import org.eclipse.jetty.client.api.Request; import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.client.api.Response; import org.eclipse.jetty.client.api.Response;
import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.io.AbstractConnection; import org.eclipse.jetty.io.AbstractConnection;
import org.eclipse.jetty.io.EndPoint; import org.eclipse.jetty.io.EndPoint;
import org.eclipse.jetty.util.Promise; import org.eclipse.jetty.util.Promise;
@ -258,6 +261,12 @@ public class HttpConnectionOverHTTP extends AbstractConnection implements IConne
request.timeout(connectTimeout, TimeUnit.MILLISECONDS) request.timeout(connectTimeout, TimeUnit.MILLISECONDS)
.idleTimeout(2 * connectTimeout, TimeUnit.MILLISECONDS); .idleTimeout(2 * connectTimeout, TimeUnit.MILLISECONDS);
} }
if (request instanceof HttpUpgrader.Factory)
{
HttpUpgrader upgrader = ((HttpUpgrader.Factory)request).newHttpUpgrader(HttpVersion.HTTP_1_1);
((HttpRequest)request).getConversation().setAttribute(HttpUpgrader.class.getName(), upgrader);
upgrader.prepare((HttpRequest)request);
}
} }
@Override @Override

View File

@ -338,8 +338,7 @@ public class HttpReceiverOverHTTP extends HttpReceiver implements HttpParser.Res
if (status == HttpStatus.SWITCHING_PROTOCOLS_101) if (status == HttpStatus.SWITCHING_PROTOCOLS_101)
return true; return true;
if (HttpMethod.CONNECT.is(exchange.getRequest().getMethod()) && if (HttpMethod.CONNECT.is(exchange.getRequest().getMethod()) && status == HttpStatus.OK_200)
status == HttpStatus.OK_200)
return true; return true;
return false; return false;

View File

@ -38,6 +38,7 @@ import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.io.RuntimeIOException; import org.eclipse.jetty.io.RuntimeIOException;
import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.IO;
import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger; import org.eclipse.jetty.util.log.Logger;
@ -345,10 +346,16 @@ public class MultiPartContentProvider extends AbstractTypedContentProvider imple
if (iterator.hasNext()) if (iterator.hasNext())
return iterator.next(); return iterator.next();
++index; ++index;
if (index == parts.size()) if (index < parts.size())
state = State.LAST_BOUNDARY; {
else
state = State.MIDDLE_BOUNDARY; state = State.MIDDLE_BOUNDARY;
if (iterator instanceof Closeable)
IO.close((Closeable)iterator);
}
else
{
state = State.LAST_BOUNDARY;
}
break; break;
} }
case MIDDLE_BOUNDARY: case MIDDLE_BOUNDARY:
@ -383,14 +390,14 @@ public class MultiPartContentProvider extends AbstractTypedContentProvider imple
@Override @Override
public void succeeded() public void succeeded()
{ {
if (iterator instanceof Callback) if (state == State.CONTENT && iterator instanceof Callback)
((Callback)iterator).succeeded(); ((Callback)iterator).succeeded();
} }
@Override @Override
public void failed(Throwable x) public void failed(Throwable x)
{ {
if (iterator instanceof Callback) if (state == State.CONTENT && iterator instanceof Callback)
((Callback)iterator).failed(x); ((Callback)iterator).failed(x);
} }

View File

@ -18,6 +18,8 @@
package org.eclipse.jetty.client; package org.eclipse.jetty.client;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel; import java.nio.channels.ServerSocketChannel;
@ -25,7 +27,14 @@ import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocket;
import org.eclipse.jetty.client.http.HttpClientTransportOverHTTP;
import org.eclipse.jetty.http.HttpScheme;
import org.eclipse.jetty.io.ClientConnector;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.eclipse.jetty.util.thread.QueuedThreadPool;
import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@ -44,7 +53,13 @@ public class Socks4ProxyTest
server = ServerSocketChannel.open(); server = ServerSocketChannel.open();
server.bind(new InetSocketAddress("localhost", 0)); server.bind(new InetSocketAddress("localhost", 0));
client = new HttpClient(); ClientConnector connector = new ClientConnector();
QueuedThreadPool clientThreads = new QueuedThreadPool();
clientThreads.setName("client");
connector.setExecutor(clientThreads);
connector.setSslContextFactory(new SslContextFactory.Client());
client = new HttpClient(new HttpClientTransportOverHTTP(connector));
client.setExecutor(clientThreads);
client.start(); client.start();
} }
@ -61,7 +76,7 @@ public class Socks4ProxyTest
int proxyPort = server.socket().getLocalPort(); int proxyPort = server.socket().getLocalPort();
client.getProxyConfiguration().getProxies().add(new Socks4Proxy("localhost", proxyPort)); client.getProxyConfiguration().getProxies().add(new Socks4Proxy("localhost", proxyPort));
final CountDownLatch latch = new CountDownLatch(1); CountDownLatch latch = new CountDownLatch(1);
byte ip1 = 127; byte ip1 = 127;
byte ip2 = 0; byte ip2 = 0;
@ -111,7 +126,7 @@ public class Socks4ProxyTest
"Content-Length: 0\r\n" + "Content-Length: 0\r\n" +
"Connection: close\r\n" + "Connection: close\r\n" +
"\r\n"; "\r\n";
channel.write(ByteBuffer.wrap(response.getBytes("UTF-8"))); channel.write(ByteBuffer.wrap(response.getBytes(StandardCharsets.UTF_8)));
assertTrue(latch.await(5, TimeUnit.SECONDS)); assertTrue(latch.await(5, TimeUnit.SECONDS));
} }
@ -123,7 +138,7 @@ public class Socks4ProxyTest
int proxyPort = server.socket().getLocalPort(); int proxyPort = server.socket().getLocalPort();
client.getProxyConfiguration().getProxies().add(new Socks4Proxy("localhost", proxyPort)); client.getProxyConfiguration().getProxies().add(new Socks4Proxy("localhost", proxyPort));
final CountDownLatch latch = new CountDownLatch(1); CountDownLatch latch = new CountDownLatch(1);
String serverHost = "127.0.0.13"; // Test expects an IP address. String serverHost = "127.0.0.13"; // Test expects an IP address.
int serverPort = proxyPort + 1; // Any port will do int serverPort = proxyPort + 1; // Any port will do
@ -169,7 +184,92 @@ public class Socks4ProxyTest
"Content-Length: 0\r\n" + "Content-Length: 0\r\n" +
"Connection: close\r\n" + "Connection: close\r\n" +
"\r\n"; "\r\n";
channel.write(ByteBuffer.wrap(response.getBytes("UTF-8"))); channel.write(ByteBuffer.wrap(response.getBytes(StandardCharsets.UTF_8)));
assertTrue(latch.await(5, TimeUnit.SECONDS));
}
}
@Test
public void testSocks4ProxyWithTLSServer() throws Exception
{
String proxyHost = "localhost";
int proxyPort = server.socket().getLocalPort();
String serverHost = "127.0.0.13"; // Server host different from proxy host.
int serverPort = proxyPort + 1; // Any port will do.
SslContextFactory clientTLS = client.getSslContextFactory();
clientTLS.reload(ssl ->
{
// The client keystore contains the trustedCertEntry for the
// self-signed server certificate, so it acts as a truststore.
ssl.setTrustStorePath("src/test/resources/client_keystore.jks");
ssl.setTrustStorePassword("storepwd");
// Disable TLS hostname verification, but
// enable application hostname verification.
ssl.setEndpointIdentificationAlgorithm(null);
// The hostname must be that of the server, not of the proxy.
ssl.setHostnameVerifier((hostname, session) -> serverHost.equals(hostname));
});
client.getProxyConfiguration().getProxies().add(new Socks4Proxy(proxyHost, proxyPort));
CountDownLatch latch = new CountDownLatch(1);
client.newRequest(serverHost, serverPort)
.scheme(HttpScheme.HTTPS.asString())
.path("/path")
.send(result ->
{
if (result.isSucceeded())
latch.countDown();
else
result.getFailure().printStackTrace();
});
try (SocketChannel channel = server.accept())
{
int socks4MessageLength = 9;
ByteBuffer buffer = ByteBuffer.allocate(socks4MessageLength);
int read = channel.read(buffer);
assertEquals(socks4MessageLength, read);
// Socks4 response.
channel.write(ByteBuffer.wrap(new byte[]{0, 0x5A, 0, 0, 0, 0, 0, 0}));
// Wrap the socket with TLS.
SslContextFactory.Server serverTLS = new SslContextFactory.Server();
serverTLS.setKeyStorePath("src/test/resources/keystore.jks");
serverTLS.setKeyStorePassword("storepwd");
serverTLS.start();
SSLContext sslContext = serverTLS.getSslContext();
SSLSocket sslSocket = (SSLSocket)sslContext.getSocketFactory().createSocket(channel.socket(), serverHost, serverPort, false);
sslSocket.setUseClientMode(false);
// Read the request.
int crlfs = 0;
InputStream input = sslSocket.getInputStream();
while (true)
{
read = input.read();
if (read < 0)
break;
if (read == '\r' || read == '\n')
++crlfs;
else
crlfs = 0;
if (crlfs == 4)
break;
}
// Send the response.
String response =
"HTTP/1.1 200 OK\r\n" +
"Content-Length: 0\r\n" +
"Connection: close\r\n" +
"\r\n";
OutputStream output = sslSocket.getOutputStream();
output.write(response.getBytes(StandardCharsets.UTF_8));
output.flush();
assertTrue(latch.await(5, TimeUnit.SECONDS)); assertTrue(latch.await(5, TimeUnit.SECONDS));
} }

View File

@ -20,6 +20,7 @@ package org.eclipse.jetty.client.util;
import java.io.BufferedWriter; import java.io.BufferedWriter;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.Closeable;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.OutputStream; import java.io.OutputStream;
@ -31,10 +32,12 @@ import java.nio.file.Path;
import java.nio.file.StandardOpenOption; import java.nio.file.StandardOpenOption;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Random; import java.util.Random;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import javax.servlet.MultipartConfigElement; import javax.servlet.MultipartConfigElement;
import javax.servlet.ServletException; import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
@ -435,6 +438,46 @@ public class MultiPartContentProviderTest extends AbstractHttpClientServerTest
assertTrue(responseLatch.await(5, TimeUnit.SECONDS)); assertTrue(responseLatch.await(5, TimeUnit.SECONDS));
} }
@ParameterizedTest
@ArgumentsSource(ScenarioProvider.class)
public void testEachPartIsClosed(Scenario scenario) throws Exception
{
String name1 = "field1";
String value1 = "value1";
String name2 = "field2";
String value2 = "value2";
start(scenario, new AbstractMultiPartHandler()
{
@Override
protected void handle(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
{
Collection<Part> parts = request.getParts();
assertEquals(2, parts.size());
Iterator<Part> iterator = parts.iterator();
Part part1 = iterator.next();
assertEquals(name1, part1.getName());
assertEquals(value1, IO.toString(part1.getInputStream()));
Part part2 = iterator.next();
assertEquals(name2, part2.getName());
assertEquals(value2, IO.toString(part2.getInputStream()));
}
});
AtomicInteger closeCount = new AtomicInteger();
MultiPartContentProvider multiPart = new MultiPartContentProvider();
multiPart.addFieldPart(name1, new CloseableStringContentProvider(value1, closeCount::incrementAndGet), null);
multiPart.addFieldPart(name2, new CloseableStringContentProvider(value2, closeCount::incrementAndGet), null);
multiPart.close();
ContentResponse response = client.newRequest("localhost", connector.getLocalPort())
.scheme(scenario.getScheme())
.method(HttpMethod.POST)
.content(multiPart)
.send();
assertEquals(200, response.getStatus());
assertEquals(2, closeCount.get());
}
private abstract static class AbstractMultiPartHandler extends AbstractHandler private abstract static class AbstractMultiPartHandler extends AbstractHandler
{ {
@Override @Override
@ -448,4 +491,49 @@ public class MultiPartContentProviderTest extends AbstractHttpClientServerTest
protected abstract void handle(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException; protected abstract void handle(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException;
} }
private static class CloseableStringContentProvider extends StringContentProvider
{
private final Runnable closeFn;
private CloseableStringContentProvider(String content, Runnable closeFn)
{
super(content);
this.closeFn = closeFn;
}
@Override
public Iterator<ByteBuffer> iterator()
{
return new CloseableIterator<>(super.iterator());
}
private class CloseableIterator<T> implements Iterator<T>, Closeable
{
private final Iterator<T> iterator;
public CloseableIterator(Iterator<T> iterator)
{
this.iterator = iterator;
}
@Override
public boolean hasNext()
{
return iterator.hasNext();
}
@Override
public T next()
{
return iterator.next();
}
@Override
public void close()
{
closeFn.run();
}
}
}
} }

View File

@ -27,11 +27,12 @@ A demo Jetty base that supports HTTP/1, HTTPS/1 and deployment from a webapps di
$ JETTY_BASE=http2-demo $ JETTY_BASE=http2-demo
$ mkdir $JETTY_BASE $ mkdir $JETTY_BASE
$ cd $JETTY_BASE $ cd $JETTY_BASE
$ java -jar $JETTY_HOME/start.jar --add-to-start=http,https,deploy $ java -jar $JETTY_HOME/start.jar --add-to-start=http,https,deploy,test-keystore
.... ....
The commands above create a `$JETTY_BASE` directory called `http2-demo`, and initializes the `http,` `https` and `deploy` modules (and their dependencies) to run a typical Jetty Server on port 8080 (for HTTP/1) and 8443 (for HTTPS/1). The commands above create a `$JETTY_BASE` directory called `http2-demo`, and initializes the `http,` `https` and `deploy` modules (and their dependencies) to run a typical Jetty Server on port 8080 (for HTTP/1) and 8443 (for HTTPS/1).
Note that the HTTPS module downloads a demo keystore file with a self signed certificate, which needs to be replaced by a Certificate Authority issued certificate for real deployment. Note that the `test-keystore` module downloads a demo keystore file with a self signed certificate, which needs to be replaced by a Certificate Authority issued certificate for real deployment.
A keystore can also be added by enabling and configuring the `ssl` module.
To add HTTP/2 to this demo base, it is just a matter of enabling the `http2` module with the following command: To add HTTP/2 to this demo base, it is just a matter of enabling the `http2` module with the following command:

View File

@ -36,6 +36,12 @@ include::{SRCDIR}/jetty-home/src/main/resources/modules/jsp.mod[]
Note that the availability of some JSP features may depend on which JSP container implementation you are using. Note that the availability of some JSP features may depend on which JSP container implementation you are using.
Note also that it may not be possible to precompile your JSPs with one container and deploy to the other. Note also that it may not be possible to precompile your JSPs with one container and deploy to the other.
===== Logging
The Apache Jasper logging system is bridged to the jetty logging system.
Thus, you can enable logging for jsps in the same way you have setup for your webapp.
For example, assuming you are using Jetty's default StdErr logger, you would enable DEBUG level logging for jsps by adding the system property `-Dorg.apache.jasper.LEVEL=DEBUG` to the command line.
===== JSPs and Embedding ===== JSPs and Embedding
If you have an embedded setup for your webapp and wish to use JSPs, you will need to ensure that a JSP engine is correctly initialized. If you have an embedded setup for your webapp and wish to use JSPs, you will need to ensure that a JSP engine is correctly initialized.
@ -79,89 +85,91 @@ If you wish to use a different compiler, you will need to configure the `compile
[cols=",,,",options="header",] [cols=",,,",options="header",]
|======================================================================= |=======================================================================
|init param |Description |Default |`webdefault.xml` |init param |Description |Default |`webdefault.xml`
|classpath |`Classpath used for jsp compilation. Only used if
org.apache.catalina.jsp_classpath context attribute is not |checkInterval |If non-zero and `development` is `false`, background jsp recompilation is enabled. This value is the interval in seconds between background recompile checks.
set, which it is in Jetty.` |- | |0 |
|classpath |The classpath is dynamically generated if the context has a URL classloader. The `org.apache.catalina.jsp_classpath`
context attribute is used to add to the classpath, but if this is not set, this `classpath` configuration item is added to the classpath instead.` |- |
|classdebuginfo |Include debugging info in class file. |TRUE | |classdebuginfo |Include debugging info in class file. |TRUE |
|checkInterval |Interval in seconds between background recompile checks. |compilerClassName |If not set, defaults to the Eclipse jdt compiler. |- |
Only relevant if `
development=false`. |0 |
|development |`development=true`, recompilation checks occur on each |compiler |Used if the Eclipse jdt compiler cannot be found on the
request. See also ` classpath. It is the classname of a compiler that Ant should invoke. |
modificationTestInterval`. |TRUE | |
|compilerTargetVM |Target vm to compile for. |1.8 |1.8
|compilerSourceVM |Sets source compliance level for the jdt compiler.
|1.8 |1.8
|development |If `true` recompilation checks occur at the frequency governed by `modificationTestInterval`. |TRUE |
|displaySourceFragment |Should a source fragment be included in |displaySourceFragment |Should a source fragment be included in
exception messages |TRUE | exception messages |TRUE |
|dumpSmap |Dump SMAP JSR45 info to a file. |FALSE |
|enablePooling |Determines whether tag handler pooling is enabled. |TRUE |
|engineOptionsClass |Allows specifying the Options class used to
configure Jasper. If not present, the default EmbeddedServletOptions
will be used. |- |
|errorOnUseBeanInvalidClassAttribute |Should Jasper issue an error when |errorOnUseBeanInvalidClassAttribute |Should Jasper issue an error when
the value of the class attribute in an useBean action is not a valid the value of the class attribute in an useBean action is not a valid
bean class |TRUE | bean class |TRUE |
|fork |Should Ant fork its Java compiles of JSP pages? |TRUE |FALSE |fork |Only relevant if you use Ant to compile jsps: by default Jetty will use the Eclipse jdt compiler.|TRUE |-
|genStrAsCharArray |Option for generating Strings as char arrays. |FALSE |
|ieClassId |The class-id value to be sent to Internet Explorer when
using <jsp:plugin> tags. |clsid:8AD9C840-044E-11D1-B3E9-00805F499D93 |
|javaEncoding |Pass through the encoding to use for the compilation.
|UTF8 |
|jspIdleTimeout |The amount of time in seconds a JSP can be idle before
it is unloaded. A value of zero or less indicates never unload. |-1 |
|keepgenerated |Do you want to keep the generated Java files around? |keepgenerated |Do you want to keep the generated Java files around?
|TRUE | |TRUE |
|trimSpaces |Should white spaces between directives or actions be
trimmed? |FALSE |
|enablePooling |Determines whether tag handler pooling is enabled. |TRUE
|
|engineOptionsClass |Allows specifying the Options class used to
configure Jasper. If not present, the default EmbeddedServletOptions
will be used. |
|mappedFile |Support for mapped Files. Generates a servlet that has a |mappedFile |Support for mapped Files. Generates a servlet that has a
print statement per line of the JSP file  |TRUE | print statement per line of the JSP file  |TRUE |
|suppressSmap |Generation of SMAP info for JSR45 debugging. |FALSE |
|dumpSmap |Dump SMAP JSR45 info to a file. |FALSE |
|genStrAsCharArray |Option for generating Strings. |FALSE |
|ieClassId |The class-id value to be sent to Internet Explorer when
using <jsp:plugin> tags. |clsid:8AD9C840-044E-11D1-B3E9-00805F499D93 |
|maxLoadedJsps |The maximum number of JSPs that will be loaded for a web |maxLoadedJsps |The maximum number of JSPs that will be loaded for a web
application. If more than this number of JSPs are loaded, the least application. If more than this number of JSPs are loaded, the least
recently used JSPs will be unloaded so that the number of JSPs loaded at recently used JSPs will be unloaded so that the number of JSPs loaded at
any one time does not exceed this limit. A value of zero or less any one time does not exceed this limit. A value of zero or less
indicates no limit. |-1 | indicates no limit. |-1 |
|jspIdleTimeout |The amount of time in seconds a JSP can be idle before
it is unloaded. A value of zero or less indicates never unload. |-1 |
|scratchDir |Directory where servlets are generated. See | |
|compilerClassName |If not set, defaults to the Eclipse jdt compiler. |
|compiler |Used if the Eclipse jdt compiler cannot be found on the
classpath. It is the classname of a compiler that Ant should invoke. |
|
|compilerTargetVM |Target vm to compile for. |1.7 |
|compilerSourceVM |Sets source compliance level for the jdt compiler.
|1.7 |
|javaEncoding |Pass through the encoding to use for the compilation.
|UTF8 |
|modificationTestInterval |If `development=true`, interval between |modificationTestInterval |If `development=true`, interval between
recompilation checks, triggered by a request. |4 | recompilation checks, triggered by a request. |4 |
|xpoweredBy |Generate an X-Powered-By response header. |FALSE |FALSE |quoteAttributeEL | When EL is used in an attribute value on a JSP page, should the rules for quoting of attributes described in JSP.1.6 be applied to the expression
|TRUE |-
|recompileOnFail |If a JSP compilation fails should the |recompileOnFail |If a JSP compilation fails should the
modificationTestInterval be ignored and the next access trigger a modificationTestInterval be ignored and the next access trigger a
re-compilation attempt? Used in development mode only and is disabled by re-compilation attempt? Used in development mode only and is disabled by
default as compilation may be expensive and could lead to excessive default as compilation may be expensive and could lead to excessive
resource usage. |- | resource usage. |FALSE |
|scratchDir |Directory where servlets are generated. The default is the value of the context attribute `javax.servlet.context.tempdir`, or the system property `java.io.tmpdir` if the context attribute is not set. | |
|strictQuoteEscaping |Should the quote escaping required by section JSP.1.6 of the JSP specification be applied to scriplet expression.
|TRUE|-
|suppressSmap |Generation of SMAP info for JSR45 debugging. |FALSE |
|trimSpaces |Should template text that consists entirely of whitespace be removed from the output (true), replaced with a single space (single) or left unchanged (false)? Note that if a JSP page or tag file specifies a trimDirectiveWhitespaces value of true, that will take precedence over this configuration setting for that page/tag.
trimmed? |FALSE |
|xpoweredBy |Generate an X-Powered-By response header. |FALSE |FALSE
|======================================================================= |=======================================================================
[[configuring-jsp-for-jetty]] [[configuring-jsp-for-jetty]]
@ -171,7 +179,7 @@ The JSP engine has many configuration parameters.
Some parameters affect only precompilation, and some affect runtime recompilation checking. Some parameters affect only precompilation, and some affect runtime recompilation checking.
Parameters also differ among the various versions of the JSP engine. Parameters also differ among the various versions of the JSP engine.
This page lists the configuration parameters, their meanings, and their default settings. This page lists the configuration parameters, their meanings, and their default settings.
Set all parameters on the `org.apache.jasper.servlet.JspServlet` instance defined in the link:#webdefault-xml[`webdefault.xml`] file. Set all parameters on the `org.eclipse.jetty.jsp.JettyJspServlet` instance defined in the link:#webdefault-xml[`webdefault.xml`] file.
____ ____
[NOTE] [NOTE]
@ -225,18 +233,10 @@ You can use the entry in link:#webdefault-xml[{$jetty.home}/etc/webdefault.xml]
---- ----
<servlet id="jsp"> <servlet id="jsp">
<servlet-name>jsp</servlet-name> <servlet-name>jsp</servlet-name>
<servlet-class>org.apache.jasper.servlet.JspServlet</servlet-class> <servlet-class>org.eclipse.jetty.jsp.JettyJspServlet</servlet-class>
<init-param>
<param-name>logVerbosityLevel</param-name>
<param-value>DEBUG</param-value>
</init-param>
<init-param>
<param-name>fork</param-name>
<param-value>>false</param-value>
</init-param>
<init-param> <init-param>
<param-name>keepgenerated</param-name> <param-name>keepgenerated</param-name>
<param-value>>true</param-value> <param-value>true</param-value>
</init-param> </init-param>
... ...

View File

@ -125,6 +125,14 @@ public class HttpChannelOverFCGI extends HttpChannel
dispatcher.dispatch(); dispatcher.dispatch();
} }
public boolean onIdleTimeout(Throwable timeout)
{
boolean handle = getRequest().getHttpInput().onIdleTimeout(timeout);
if (handle)
execute(this);
return !handle;
}
private static class Dispatcher implements Runnable private static class Dispatcher implements Runnable
{ {
private final AtomicReference<State> state = new AtomicReference<>(State.IDLE); private final AtomicReference<State> state = new AtomicReference<>(State.IDLE);

View File

@ -105,6 +105,14 @@ public class ServerFCGIConnection extends AbstractConnection
} }
} }
@Override
protected boolean onReadTimeout(Throwable timeout)
{
return channels.values().stream()
.mapToInt(channel -> channel.onIdleTimeout(timeout) ? 0 : 1)
.sum() == 0;
}
private void parse(ByteBuffer buffer) private void parse(ByteBuffer buffer)
{ {
while (buffer.hasRemaining()) while (buffer.hasRemaining())

View File

@ -37,13 +37,16 @@ import org.eclipse.jetty.util.component.Destroyable;
*/ */
public class GZIPContentDecoder implements Destroyable public class GZIPContentDecoder implements Destroyable
{ {
// Unsigned Integer Max == 2^32
private static final long UINT_MAX = 0xFFFFFFFFL;
private final List<ByteBuffer> _inflateds = new ArrayList<>(); private final List<ByteBuffer> _inflateds = new ArrayList<>();
private final Inflater _inflater = new Inflater(true); private final Inflater _inflater = new Inflater(true);
private final ByteBufferPool _pool; private final ByteBufferPool _pool;
private final int _bufferSize; private final int _bufferSize;
private State _state; private State _state;
private int _size; private int _size;
private int _value; private long _value;
private byte _flags; private byte _flags;
private ByteBuffer _inflated; private ByteBuffer _inflated;
@ -375,11 +378,12 @@ public class GZIPContentDecoder implements Destroyable
} }
case ISIZE: case ISIZE:
{ {
_value += (currByte & 0xFF) << 8 * _size; _value = _value | ((currByte & 0xFFL) << (8 * _size));
++_size; ++_size;
if (_size == 4) if (_size == 4)
{ {
if (_value != _inflater.getBytesWritten()) // RFC 1952: Section 2.3.1; ISIZE is the input size modulo 2^32
if (_value != (_inflater.getBytesWritten() & UINT_MAX))
throw new ZipException("Invalid input size"); throw new ZipException("Invalid input size");
// TODO ByteBuffer result = output == null ? BufferUtil.EMPTY_BUFFER : ByteBuffer.wrap(output); // TODO ByteBuffer result = output == null ? BufferUtil.EMPTY_BUFFER : ByteBuffer.wrap(output);

View File

@ -608,6 +608,15 @@ public class HttpURI
return _param; return _param;
} }
public void setParam(String param)
{
_param = param;
if (_path != null && !_path.contains(_param))
{
_path += ";" + _param;
}
}
public String getQuery() public String getQuery()
{ {
return _query; return _query;

View File

@ -250,6 +250,11 @@ public class MetaData implements Iterable<HttpField>
private String _protocol; private String _protocol;
public ConnectRequest(HttpScheme scheme, HostPortHttpField authority, String path, HttpFields fields, String protocol) public ConnectRequest(HttpScheme scheme, HostPortHttpField authority, String path, HttpFields fields, String protocol)
{
this(scheme == null ? null : scheme.asString(), authority, path, fields, protocol);
}
public ConnectRequest(String scheme, HostPortHttpField authority, String path, HttpFields fields, String protocol)
{ {
super(HttpMethod.CONNECT.asString(), scheme, authority, path, HttpVersion.HTTP_2, fields, Long.MIN_VALUE); super(HttpMethod.CONNECT.asString(), scheme, authority, path, HttpVersion.HTTP_2, fields, Long.MIN_VALUE);
_protocol = protocol; _protocol = protocol;

View File

@ -20,6 +20,8 @@ package org.eclipse.jetty.http;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
@ -30,7 +32,11 @@ import org.eclipse.jetty.io.ArrayByteBufferPool;
import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertTrue;
@ -351,4 +357,81 @@ public class GZIPContentDecoderTest
assertTrue(buffer.hasRemaining()); assertTrue(buffer.hasRemaining());
assertEquals(data2, StandardCharsets.UTF_8.decode(buffer).toString()); assertEquals(data2, StandardCharsets.UTF_8.decode(buffer).toString());
} }
// Signed Integer Max
final long INT_MAX = Integer.MAX_VALUE;
// Unsigned Integer Max == 2^32
final long UINT_MAX = 0xFFFFFFFFL;
@ParameterizedTest
@ValueSource(longs = {INT_MAX, INT_MAX + 1, UINT_MAX, UINT_MAX + 1})
public void testLargeGzipStream(long origSize) throws IOException
{
// Size chosen for trade off between speed of I/O vs speed of Gzip
final int BUFSIZE = 1024 * 1024;
// Create a buffer to use over and over again to produce the uncompressed input
byte[] cbuf = "0123456789ABCDEFGHIJKLMOPQRSTUVWXYZ".getBytes(StandardCharsets.UTF_8);
byte[] buf = new byte[BUFSIZE];
for (int off = 0; off < buf.length; )
{
int len = Math.min(cbuf.length, buf.length - off);
System.arraycopy(cbuf, 0, buf, off, len);
off += len;
}
GZIPDecoderOutputStream out = new GZIPDecoderOutputStream(new GZIPContentDecoder(BUFSIZE));
GZIPOutputStream outputStream = new GZIPOutputStream(out, BUFSIZE);
for (long bytesLeft = origSize; bytesLeft > 0; )
{
int len = buf.length;
if (bytesLeft < buf.length)
{
len = (int)bytesLeft;
}
outputStream.write(buf, 0, len);
bytesLeft -= len;
}
// Close GZIPOutputStream to have it generate gzip trailer.
// This can cause more writes of unflushed gzip buffers
outputStream.close();
// out.decodedByteCount is only valid after close
assertThat("Decoded byte count", out.decodedByteCount, is(origSize));
}
public static class GZIPDecoderOutputStream extends OutputStream
{
private final GZIPContentDecoder decoder;
public long decodedByteCount = 0L;
public GZIPDecoderOutputStream(GZIPContentDecoder decoder)
{
this.decoder = decoder;
}
@Override
public void write(byte[] b, int off, int len) throws IOException
{
ByteBuffer buf = ByteBuffer.wrap(b, off, len);
while (buf.hasRemaining())
{
ByteBuffer decoded = decoder.decode(buf);
if (decoded.hasRemaining())
{
decodedByteCount += decoded.remaining();
}
decoder.release(decoded);
}
}
@Override
public void write(int b) throws IOException
{
write(new byte[]{(byte)b}, 0, 1);
}
}
} }

View File

@ -97,6 +97,7 @@ public abstract class HTTP2Session extends ContainerLifeCycle implements ISessio
private int initialSessionRecvWindow; private int initialSessionRecvWindow;
private int writeThreshold; private int writeThreshold;
private boolean pushEnabled; private boolean pushEnabled;
private boolean connectProtocolEnabled;
private long idleTime; private long idleTime;
private GoAwayFrame closeFrame; private GoAwayFrame closeFrame;
@ -370,6 +371,14 @@ public abstract class HTTP2Session extends ContainerLifeCycle implements ISessio
generator.setMaxHeaderListSize(value); generator.setMaxHeaderListSize(value);
break; break;
} }
case SettingsFrame.ENABLE_CONNECT_PROTOCOL:
{
boolean enabled = value == 1;
if (LOG.isDebugEnabled())
LOG.debug("{} CONNECT protocol for {}", enabled ? "Enabling" : "Disabling", this);
connectProtocolEnabled = enabled;
break;
}
default: default:
{ {
if (LOG.isDebugEnabled()) if (LOG.isDebugEnabled())
@ -906,6 +915,17 @@ public abstract class HTTP2Session extends ContainerLifeCycle implements ISessio
return pushEnabled; return pushEnabled;
} }
@ManagedAttribute(value = "Whether CONNECT requests supports a protocol", readonly = true)
public boolean isConnectProtocolEnabled()
{
return connectProtocolEnabled;
}
public void setConnectProtocolEnabled(boolean connectProtocolEnabled)
{
this.connectProtocolEnabled = connectProtocolEnabled;
}
/** /**
* A typical close by a remote peer involves a GO_AWAY frame followed by TCP FIN. * A typical close by a remote peer involves a GO_AWAY frame followed by TCP FIN.
* This method is invoked when the TCP FIN is received, or when an exception is * This method is invoked when the TCP FIN is received, or when an exception is

View File

@ -372,16 +372,18 @@ public class HTTP2Stream extends IdleTimeout implements IStream, Callback, Dumpa
dataProcess = proceed = dataDemand > 0; dataProcess = proceed = dataDemand > 0;
} }
} }
if (LOG.isDebugEnabled())
LOG.debug("{} data processing of {} for {}", initial ? "Starting" : proceed ? "Proceeding" : "Stalling", frame, this);
if (initial) if (initial)
{ {
if (LOG.isDebugEnabled())
LOG.debug("Starting data processing of {} for {}", frame, this);
notifyBeforeData(this); notifyBeforeData(this);
try (AutoLock l = lock.lock()) try (AutoLock l = lock.lock())
{ {
dataProcess = proceed = dataDemand > 0; dataProcess = proceed = dataDemand > 0;
} }
} }
if (LOG.isDebugEnabled())
LOG.debug("{} data processing of {} for {}", proceed ? "Proceeding" : "Stalling", frame, this);
if (proceed) if (proceed)
processData(); processData();
} }

View File

@ -30,6 +30,7 @@ public class SettingsFrame extends Frame
public static final int INITIAL_WINDOW_SIZE = 4; public static final int INITIAL_WINDOW_SIZE = 4;
public static final int MAX_FRAME_SIZE = 5; public static final int MAX_FRAME_SIZE = 5;
public static final int MAX_HEADER_LIST_SIZE = 6; public static final int MAX_HEADER_LIST_SIZE = 6;
public static final int ENABLE_CONNECT_PROTOCOL = 8;
private final Map<Integer, Integer> settings; private final Map<Integer, Integer> settings;
private final boolean reply; private final boolean reply;

View File

@ -177,22 +177,22 @@ public class MetaDataBuilder
_contentLength = field.getLongValue(); _contentLength = field.getLongValue();
_fields.add(field); _fields.add(field);
break; break;
case TE: case TE:
if ("trailers".equalsIgnoreCase(value)) if ("trailers".equalsIgnoreCase(value))
_fields.add(field); _fields.add(field);
else else
streamException("Unsupported TE value '%s'", value); streamException("Unsupported TE value '%s'", value);
break; break;
case CONNECTION: case CONNECTION:
if ("TE".equalsIgnoreCase(value)) if ("TE".equalsIgnoreCase(value))
_fields.add(field); _fields.add(field);
else else
streamException("Connection specific field '%s'", header); streamException("Connection specific field '%s'", header);
break; break;
default: default:
if (name.charAt(0) == ':') if (name.charAt(0) == ':')
streamException("Unknown pseudo header '%s'", name); streamException("Unknown pseudo header '%s'", name);
else else
@ -238,7 +238,7 @@ public class MetaDataBuilder
_streamException.addSuppressed(new Throwable()); _streamException.addSuppressed(new Throwable());
throw _streamException; throw _streamException;
} }
if (_request && _response) if (_request && _response)
throw new HpackException.StreamException("Request and Response headers"); throw new HpackException.StreamException("Request and Response headers");
@ -268,7 +268,7 @@ public class MetaDataBuilder
throw new HpackException.StreamException("No Status"); throw new HpackException.StreamException("No Status");
return new MetaData.Response(HttpVersion.HTTP_2, _status, fields, _contentLength); return new MetaData.Response(HttpVersion.HTTP_2, _status, fields, _contentLength);
} }
return new MetaData(HttpVersion.HTTP_2, fields, _contentLength); return new MetaData(HttpVersion.HTTP_2, fields, _contentLength);
} }
finally finally

View File

@ -32,7 +32,9 @@ import org.eclipse.jetty.client.HttpConnection;
import org.eclipse.jetty.client.HttpDestination; import org.eclipse.jetty.client.HttpDestination;
import org.eclipse.jetty.client.HttpExchange; import org.eclipse.jetty.client.HttpExchange;
import org.eclipse.jetty.client.HttpRequest; import org.eclipse.jetty.client.HttpRequest;
import org.eclipse.jetty.client.HttpUpgrader;
import org.eclipse.jetty.client.SendFailure; import org.eclipse.jetty.client.SendFailure;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.http.HttpVersion; import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.http2.ErrorCode; import org.eclipse.jetty.http2.ErrorCode;
import org.eclipse.jetty.http2.IStream; import org.eclipse.jetty.http2.IStream;
@ -88,6 +90,18 @@ public class HttpConnectionOverHTTP2 extends HttpConnection implements Sweeper.S
return send(channel, exchange); return send(channel, exchange);
} }
@Override
protected void normalizeRequest(Request request)
{
super.normalizeRequest(request);
if (request instanceof HttpUpgrader.Factory)
{
HttpUpgrader upgrader = ((HttpUpgrader.Factory)request).newHttpUpgrader(HttpVersion.HTTP_2);
((HttpRequest)request).getConversation().setAttribute(HttpUpgrader.class.getName(), upgrader);
upgrader.prepare((HttpRequest)request);
}
}
protected HttpChannelOverHTTP2 acquireHttpChannel() protected HttpChannelOverHTTP2 acquireHttpChannel()
{ {
HttpChannelOverHTTP2 channel = idleChannels.poll(); HttpChannelOverHTTP2 channel = idleChannels.poll();

View File

@ -27,10 +27,12 @@ import java.util.Queue;
import java.util.function.BiFunction; import java.util.function.BiFunction;
import org.eclipse.jetty.client.HttpChannel; import org.eclipse.jetty.client.HttpChannel;
import org.eclipse.jetty.client.HttpConversation;
import org.eclipse.jetty.client.HttpExchange; import org.eclipse.jetty.client.HttpExchange;
import org.eclipse.jetty.client.HttpReceiver; import org.eclipse.jetty.client.HttpReceiver;
import org.eclipse.jetty.client.HttpRequest; import org.eclipse.jetty.client.HttpRequest;
import org.eclipse.jetty.client.HttpResponse; import org.eclipse.jetty.client.HttpResponse;
import org.eclipse.jetty.client.HttpUpgrader;
import org.eclipse.jetty.client.api.Request; import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.client.api.Response; import org.eclipse.jetty.client.api.Response;
import org.eclipse.jetty.http.HttpField; import org.eclipse.jetty.http.HttpField;
@ -110,7 +112,11 @@ public class HttpReceiverOverHTTP2 extends HttpReceiver implements HTTP2Channel.
if (LOG.isDebugEnabled()) if (LOG.isDebugEnabled())
LOG.debug("Successful HTTP2 tunnel on {} via {}", stream, endPoint); LOG.debug("Successful HTTP2 tunnel on {} via {}", stream, endPoint);
((IStream)stream).setAttachment(endPoint); ((IStream)stream).setAttachment(endPoint);
httpRequest.getConversation().setAttribute(EndPoint.class.getName(), endPoint); HttpConversation conversation = httpRequest.getConversation();
conversation.setAttribute(EndPoint.class.getName(), endPoint);
HttpUpgrader upgrader = (HttpUpgrader)conversation.getAttribute(HttpUpgrader.class.getName());
if (upgrader != null)
upgrader.upgrade(httpResponse, endPoint);
} }
if (responseHeaders(exchange)) if (responseHeaders(exchange))

View File

@ -59,7 +59,16 @@ public class HttpSenderOverHTTP2 extends HttpSender
MetaData.Request metaData; MetaData.Request metaData;
if (isTunnel) if (isTunnel)
{ {
metaData = new MetaData.Request(request.getMethod(), null, new HostPortHttpField(request.getPath()), null, HttpVersion.HTTP_2, request.getHeaders()); String upgradeProtocol = request.getUpgradeProtocol();
if (upgradeProtocol == null)
{
metaData = new MetaData.ConnectRequest((String)null, new HostPortHttpField(request.getPath()), null, request.getHeaders(), null);
}
else
{
HostPortHttpField authority = new HostPortHttpField(request.getHost(), request.getPort());
metaData = new MetaData.ConnectRequest(request.getScheme(), authority, request.getPath(), request.getHeaders(), upgradeProtocol);
}
} }
else else
{ {

View File

@ -60,6 +60,7 @@ public abstract class AbstractHTTP2ServerConnectionFactory extends AbstractConne
private int maxHeaderBlockFragment = 0; private int maxHeaderBlockFragment = 0;
private int maxFrameLength = Frame.DEFAULT_MAX_LENGTH; private int maxFrameLength = Frame.DEFAULT_MAX_LENGTH;
private int maxSettingsKeys = SettingsFrame.DEFAULT_MAX_KEYS; private int maxSettingsKeys = SettingsFrame.DEFAULT_MAX_KEYS;
private boolean connectProtocolEnabled = true;
private RateControl.Factory rateControlFactory = new WindowRateControl.Factory(20); private RateControl.Factory rateControlFactory = new WindowRateControl.Factory(20);
private FlowControlStrategy.Factory flowControlStrategyFactory = () -> new BufferingFlowControlStrategy(0.5F); private FlowControlStrategy.Factory flowControlStrategyFactory = () -> new BufferingFlowControlStrategy(0.5F);
private long streamIdleTimeout; private long streamIdleTimeout;
@ -185,6 +186,17 @@ public abstract class AbstractHTTP2ServerConnectionFactory extends AbstractConne
this.maxSettingsKeys = maxSettingsKeys; this.maxSettingsKeys = maxSettingsKeys;
} }
@ManagedAttribute("Whether CONNECT requests supports a protocol")
public boolean isConnectProtocolEnabled()
{
return connectProtocolEnabled;
}
public void setConnectProtocolEnabled(boolean connectProtocolEnabled)
{
this.connectProtocolEnabled = connectProtocolEnabled;
}
/** /**
* @return the factory that creates RateControl objects * @return the factory that creates RateControl objects
*/ */
@ -237,6 +249,7 @@ public abstract class AbstractHTTP2ServerConnectionFactory extends AbstractConne
if (maxConcurrentStreams >= 0) if (maxConcurrentStreams >= 0)
settings.put(SettingsFrame.MAX_CONCURRENT_STREAMS, maxConcurrentStreams); settings.put(SettingsFrame.MAX_CONCURRENT_STREAMS, maxConcurrentStreams);
settings.put(SettingsFrame.MAX_HEADER_LIST_SIZE, getHttpConfiguration().getRequestHeaderSize()); settings.put(SettingsFrame.MAX_HEADER_LIST_SIZE, getHttpConfiguration().getRequestHeaderSize());
settings.put(SettingsFrame.ENABLE_CONNECT_PROTOCOL, isConnectProtocolEnabled() ? 1 : 0);
return settings; return settings;
} }
@ -259,6 +272,7 @@ public abstract class AbstractHTTP2ServerConnectionFactory extends AbstractConne
session.setStreamIdleTimeout(streamIdleTimeout); session.setStreamIdleTimeout(streamIdleTimeout);
session.setInitialSessionRecvWindow(getInitialSessionRecvWindow()); session.setInitialSessionRecvWindow(getInitialSessionRecvWindow());
session.setWriteThreshold(getHttpConfiguration().getOutputBufferSize()); session.setWriteThreshold(getHttpConfiguration().getOutputBufferSize());
session.setConnectProtocolEnabled(isConnectProtocolEnabled());
ServerParser parser = newServerParser(connector, session, getRateControlFactory().newRateControl(endPoint)); ServerParser parser = newServerParser(connector, session, getRateControlFactory().newRateControl(endPoint));
parser.setMaxFrameLength(getMaxFrameLength()); parser.setMaxFrameLength(getMaxFrameLength());

View File

@ -102,6 +102,15 @@ public class HTTP2ServerConnectionFactory extends AbstractHTTP2ServerConnectionF
return this; return this;
} }
@Override
public void onBeforeData(Stream stream)
{
// Do not notify DATA frame listeners until demanded.
// This allows CONNECT requests with pseudo header :protocol
// (e.g. WebSocket over HTTP/2) to buffer DATA frames
// until they upgrade and are ready to process them.
}
@Override @Override
public boolean onIdleTimeout(Session session) public boolean onIdleTimeout(Session session)
{ {

View File

@ -108,6 +108,16 @@ public class HTTP2ServerSession extends HTTP2Session implements ServerParser.Lis
if (stream != null) if (stream != null)
{ {
onStreamOpened(stream); onStreamOpened(stream);
if (metaData instanceof MetaData.ConnectRequest)
{
if (!isConnectProtocolEnabled() && ((MetaData.ConnectRequest)metaData).getProtocol() != null)
{
stream.reset(new ResetFrame(streamId, ErrorCode.PROTOCOL_ERROR.code), Callback.NOOP);
return;
}
}
stream.process(frame, Callback.NOOP); stream.process(frame, Callback.NOOP);
Stream.Listener listener = notifyNewStream(stream, frame); Stream.Listener listener = notifyNewStream(stream, frame);
stream.setListener(listener); stream.setListener(listener);

View File

@ -29,7 +29,6 @@ import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpGenerator; import org.eclipse.jetty.http.HttpGenerator;
import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpHeaderValue; import org.eclipse.jetty.http.HttpHeaderValue;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.MetaData; import org.eclipse.jetty.http.MetaData;
import org.eclipse.jetty.http.PreEncodedHttpField; import org.eclipse.jetty.http.PreEncodedHttpField;
@ -140,8 +139,13 @@ public class HttpChannelOverHTTP2 extends HttpChannel implements Closeable, Writ
onRequestComplete(); onRequestComplete();
} }
boolean connect = request instanceof MetaData.ConnectRequest;
_delayedUntilContent = getHttpConfiguration().isDelayDispatchUntilContent() && _delayedUntilContent = getHttpConfiguration().isDelayDispatchUntilContent() &&
!endStream && !_expect100Continue && !HttpMethod.CONNECT.is(request.getMethod()); !endStream && !_expect100Continue && !connect;
// Delay the demand of DATA frames for CONNECT with :protocol.
if (!connect || request.getProtocol() == null)
getStream().demand(1);
if (LOG.isDebugEnabled()) if (LOG.isDebugEnabled())
{ {

View File

@ -318,12 +318,19 @@ public class HttpTransportOverHTTP2 implements HttpTransport
{ {
HttpChannelOverHTTP2 channel = (HttpChannelOverHTTP2)stream.getAttachment(); HttpChannelOverHTTP2 channel = (HttpChannelOverHTTP2)stream.getAttachment();
Request request = channel.getRequest(); Request request = channel.getRequest();
if (request.getHttpInput().hasContent())
return channel.sendErrorOrAbort("Unexpected content in CONNECT request");
Connection connection = (Connection)request.getAttribute(UPGRADE_CONNECTION_ATTRIBUTE); Connection connection = (Connection)request.getAttribute(UPGRADE_CONNECTION_ATTRIBUTE);
EndPoint endPoint = connection.getEndPoint(); EndPoint endPoint = connection.getEndPoint();
endPoint.upgrade(connection); endPoint.upgrade(connection);
stream.setAttachment(endPoint); stream.setAttachment(endPoint);
if (request.getHttpInput().hasContent()) // Only now that we have switched the attachment,
return channel.sendErrorOrAbort("Unexpected content in CONNECT request"); // we can demand DATA frames to process them.
stream.demand(1);
if (LOG.isDebugEnabled())
LOG.debug("Upgrading to {}", connection);
return false; return false;
} }
@ -333,6 +340,8 @@ public class HttpTransportOverHTTP2 implements HttpTransport
Object attachment = stream.getAttachment(); Object attachment = stream.getAttachment();
if (attachment instanceof HttpChannelOverHTTP2) if (attachment instanceof HttpChannelOverHTTP2)
{ {
// TODO: we used to "fake" a 101 response to upgrade the endpoint
// but we don't anymore, so this code should be deleted.
HttpChannelOverHTTP2 channel = (HttpChannelOverHTTP2)attachment; HttpChannelOverHTTP2 channel = (HttpChannelOverHTTP2)attachment;
if (channel.getResponse().getStatus() == HttpStatus.SWITCHING_PROTOCOLS_101) if (channel.getResponse().getStatus() == HttpStatus.SWITCHING_PROTOCOLS_101)
{ {

View File

@ -119,6 +119,11 @@
</plugins> </plugins>
</build> </build>
<dependencies> <dependencies>
<dependency>
<groupId>org.apache.maven.shared</groupId>
<artifactId>maven-artifact-transfer</artifactId>
<version>0.11.0</version>
</dependency>
<dependency> <dependency>
<groupId>org.apache.maven</groupId> <groupId>org.apache.maven</groupId>
<artifactId>maven-plugin-api</artifactId> <artifactId>maven-plugin-api</artifactId>
@ -140,11 +145,6 @@
<artifactId>maven-plugin-annotations</artifactId> <artifactId>maven-plugin-annotations</artifactId>
<scope>provided</scope> <scope>provided</scope>
</dependency> </dependency>
<dependency>
<groupId>org.apache.maven.shared</groupId>
<artifactId>maven-artifact-transfer</artifactId>
<version>0.9.1</version>
</dependency>
<dependency> <dependency>
<groupId>org.eclipse.jetty</groupId> <groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-util</artifactId> <artifactId>jetty-util</artifactId>

View File

@ -69,6 +69,7 @@
</includes> </includes>
<systemPropertyVariables> <systemPropertyVariables>
<jetty.port.file>${jetty.port.file}</jetty.port.file> <jetty.port.file>${jetty.port.file}</jetty.port.file>
<context.path>/setbycontextxml</context.path>
<pingServlet>true</pingServlet> <pingServlet>true</pingServlet>
<helloServlet>true</helloServlet> <helloServlet>true</helloServlet>
<contentCheck>Counter accessed 1 times.</contentCheck> <contentCheck>Counter accessed 1 times.</contentCheck>
@ -91,6 +92,7 @@
<goal>start</goal> <goal>start</goal>
</goals> </goals>
<configuration> <configuration>
<contextXml>${basedir}/src/config/context.xml</contextXml>
<systemProperties> <systemProperties>
<jetty.port.file>${jetty.port.file}</jetty.port.file> <jetty.port.file>${jetty.port.file}</jetty.port.file>
<jetty.deployMode>EMBED</jetty.deployMode> <jetty.deployMode>EMBED</jetty.deployMode>

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_3.dtd">
<Configure class="org.eclipse.jetty.webapp.WebAppContext">
<Set name="contextPath">/setbycontextxml</Set>
</Configure>

View File

@ -50,7 +50,7 @@ import org.apache.maven.plugin.descriptor.PluginDescriptor;
import org.apache.maven.plugins.annotations.Component; import org.apache.maven.plugins.annotations.Component;
import org.apache.maven.plugins.annotations.Parameter; import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.project.MavenProject; import org.apache.maven.project.MavenProject;
import org.apache.maven.shared.artifact.resolve.ArtifactResolver; import org.apache.maven.shared.transfer.artifact.resolve.ArtifactResolver;
import org.codehaus.plexus.util.StringUtils; import org.codehaus.plexus.util.StringUtils;
import org.eclipse.jetty.maven.plugin.utils.MavenProjectHelper; import org.eclipse.jetty.maven.plugin.utils.MavenProjectHelper;
import org.eclipse.jetty.security.LoginService; import org.eclipse.jetty.security.LoginService;

View File

@ -36,9 +36,9 @@ import org.apache.maven.execution.MavenSession;
import org.apache.maven.project.DefaultProjectBuildingRequest; import org.apache.maven.project.DefaultProjectBuildingRequest;
import org.apache.maven.project.MavenProject; import org.apache.maven.project.MavenProject;
import org.apache.maven.project.ProjectBuildingRequest; import org.apache.maven.project.ProjectBuildingRequest;
import org.apache.maven.shared.artifact.DefaultArtifactCoordinate; import org.apache.maven.shared.transfer.artifact.DefaultArtifactCoordinate;
import org.apache.maven.shared.artifact.resolve.ArtifactResolver; import org.apache.maven.shared.transfer.artifact.resolve.ArtifactResolver;
import org.apache.maven.shared.artifact.resolve.ArtifactResolverException; import org.apache.maven.shared.transfer.artifact.resolve.ArtifactResolverException;
import org.eclipse.jetty.maven.plugin.OverlayManager; import org.eclipse.jetty.maven.plugin.OverlayManager;
import org.eclipse.jetty.maven.plugin.WarPluginInfo; import org.eclipse.jetty.maven.plugin.WarPluginInfo;

View File

@ -43,6 +43,10 @@ public class IntegrationTestGetContent
{ {
int port = getPort(); int port = getPort();
assertTrue(port > 0); assertTrue(port > 0);
String contextPath = getContextPath();
if (contextPath.endsWith("/"))
contextPath = contextPath.substring(0, contextPath.lastIndexOf('/'));
HttpClient httpClient = new HttpClient(); HttpClient httpClient = new HttpClient();
try try
{ {
@ -50,16 +54,16 @@ public class IntegrationTestGetContent
if (Boolean.getBoolean("helloServlet")) if (Boolean.getBoolean("helloServlet"))
{ {
String response = httpClient.GET("http://localhost:" + port + "/hello?name=beer").getContentAsString(); String response = httpClient.GET("http://localhost:" + port + contextPath + "/hello?name=beer").getContentAsString();
assertEquals("Hello beer", response.trim(), "it test " + System.getProperty("maven.it.name")); assertEquals("Hello beer", response.trim(), "it test " + System.getProperty("maven.it.name"));
response = httpClient.GET("http://localhost:" + port + "/hello?name=foo").getContentAsString(); response = httpClient.GET("http://localhost:" + port + contextPath + "/hello?name=foo").getContentAsString();
assertEquals("Hello foo", response.trim(), "it test " + System.getProperty("maven.it.name")); assertEquals("Hello foo", response.trim(), "it test " + System.getProperty("maven.it.name"));
System.out.println("helloServlet"); System.out.println("helloServlet");
} }
if (Boolean.getBoolean("pingServlet")) if (Boolean.getBoolean("pingServlet"))
{ {
System.out.println("pingServlet"); System.out.println("pingServlet");
String response = httpClient.GET("http://localhost:" + port + "/ping?name=beer").getContentAsString(); String response = httpClient.GET("http://localhost:" + port + contextPath + "/ping?name=beer").getContentAsString();
assertEquals("pong beer", response.trim(), "it test " + System.getProperty("maven.it.name")); assertEquals("pong beer", response.trim(), "it test " + System.getProperty("maven.it.name"));
System.out.println("pingServlet ok"); System.out.println("pingServlet ok");
} }
@ -67,7 +71,7 @@ public class IntegrationTestGetContent
String pathToCheck = System.getProperty("pathToCheck"); String pathToCheck = System.getProperty("pathToCheck");
if (StringUtils.isNotBlank(contentCheck)) if (StringUtils.isNotBlank(contentCheck))
{ {
String url = "http://localhost:" + port; String url = "http://localhost:" + port + contextPath;
if (pathToCheck != null) if (pathToCheck != null)
{ {
url += pathToCheck; url += pathToCheck;
@ -79,9 +83,9 @@ public class IntegrationTestGetContent
} }
if (Boolean.getBoolean("helloTestServlet")) if (Boolean.getBoolean("helloTestServlet"))
{ {
String response = httpClient.GET("http://localhost:" + port + "/testhello?name=beer").getContentAsString(); String response = httpClient.GET("http://localhost:" + port + contextPath + "/testhello?name=beer").getContentAsString();
assertEquals("Hello from test beer", response.trim(), "it test " + System.getProperty("maven.it.name")); assertEquals("Hello from test beer", response.trim(), "it test " + System.getProperty("maven.it.name"));
response = httpClient.GET("http://localhost:" + port + "/testhello?name=foo").getContentAsString(); response = httpClient.GET("http://localhost:" + port + contextPath + "/testhello?name=foo").getContentAsString();
assertEquals("Hello from test foo", response.trim(), "it test " + System.getProperty("maven.it.name")); assertEquals("Hello from test foo", response.trim(), "it test " + System.getProperty("maven.it.name"));
System.out.println("helloServlet"); System.out.println("helloServlet");
} }
@ -92,6 +96,11 @@ public class IntegrationTestGetContent
} }
} }
public static String getContextPath()
{
return System.getProperty("context.path", "/");
}
public static int getPort() public static int getPort()
throws Exception throws Exception
{ {

View File

@ -26,6 +26,8 @@ import java.util.Dictionary;
import java.util.Enumeration; import java.util.Enumeration;
import java.util.Hashtable; import java.util.Hashtable;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.StringTokenizer; import java.util.StringTokenizer;
import org.eclipse.jetty.osgi.boot.JettyBootstrapActivator; import org.eclipse.jetty.osgi.boot.JettyBootstrapActivator;
@ -148,9 +150,8 @@ public class DefaultJettyAtJettyHomeHelper
LOG.warn("No default jetty created."); LOG.warn("No default jetty created.");
return null; return null;
} }
//configure the server here rather than letting the JettyServerServiceTracker do it, because we want to be able to //resolve the jetty xml config files
//configure the ThreadPool, which can only be done via the constructor, ie from within the xml configuration processing
List<URL> configURLs = jettyHomeDir != null ? getJettyConfigurationURLs(jettyHomeDir) : getJettyConfigurationURLs(jettyHomeBundle, properties); List<URL> configURLs = jettyHomeDir != null ? getJettyConfigurationURLs(jettyHomeDir) : getJettyConfigurationURLs(jettyHomeBundle, properties);
LOG.info("Configuring the default jetty server with {}", configURLs); LOG.info("Configuring the default jetty server with {}", configURLs);
@ -174,14 +175,36 @@ public class DefaultJettyAtJettyHomeHelper
} }
Thread.currentThread().setContextClassLoader(cl); Thread.currentThread().setContextClassLoader(cl);
// these properties usually are the ones passed to this type of //the default server name
// configuration.
properties.put(OSGiServerConstants.MANAGED_JETTY_SERVER_NAME, OSGiServerConstants.MANAGED_JETTY_SERVER_DEFAULT_NAME); properties.put(OSGiServerConstants.MANAGED_JETTY_SERVER_NAME, OSGiServerConstants.MANAGED_JETTY_SERVER_DEFAULT_NAME);
Util.setProperty(properties, OSGiServerConstants.JETTY_HOST, System.getProperty(OSGiServerConstants.JETTY_HOST, System.getProperty("jetty.host")));
Util.setProperty(properties, OSGiServerConstants.JETTY_PORT, System.getProperty(OSGiServerConstants.JETTY_PORT, System.getProperty("jetty.port"))); //Always set home and base
Util.setProperty(properties, OSGiServerConstants.JETTY_PORT_SSL, System.getProperty(OSGiServerConstants.JETTY_PORT_SSL, System.getProperty("ssl.port")));
Util.setProperty(properties, OSGiServerConstants.JETTY_HOME, home); Util.setProperty(properties, OSGiServerConstants.JETTY_HOME, home);
Util.setProperty(properties, OSGiServerConstants.JETTY_BASE, base); Util.setProperty(properties, OSGiServerConstants.JETTY_BASE, base);
// copy all system properties starting with "jetty." to service properties for the jetty server service.
// these will be used as xml configuration properties.
for (Map.Entry<Object, Object> prop : System.getProperties().entrySet())
{
if (prop.getKey() instanceof String)
{
String skey = (String)prop.getKey();
//never copy the jetty xml config files into the properties as we pass them explicitly into
//the call to configure, also we set home and base explicitly
if (OSGiServerConstants.MANAGED_JETTY_XML_CONFIG_URLS.equals(skey) ||
OSGiServerConstants.JETTY_HOME.equals(skey) ||
OSGiServerConstants.JETTY_BASE.equals(skey))
continue;
if (skey.startsWith("jetty."))
{
Util.setProperty(properties, skey, prop.getValue());
}
}
}
//configure the server here rather than letting the JettyServerServiceTracker do it, because we want to be able to
//configure the ThreadPool, which can only be done via the constructor, ie from within the xml configuration processing
Server server = ServerInstanceWrapper.configure(null, configURLs, properties); Server server = ServerInstanceWrapper.configure(null, configURLs, properties);
//Register the default Server instance as an OSGi service. //Register the default Server instance as an OSGi service.

View File

@ -18,6 +18,7 @@
package org.eclipse.jetty.osgi.test; package org.eclipse.jetty.osgi.test;
import java.nio.file.Paths;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import javax.inject.Inject; import javax.inject.Inject;
@ -25,6 +26,8 @@ import javax.inject.Inject;
import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.ContentResponse; import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.api.Request; import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.client.util.MultiPartContentProvider;
import org.eclipse.jetty.client.util.StringContentProvider;
import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.http.HttpStatus;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
@ -131,6 +134,11 @@ public class TestJettyOSGiBootWithAnnotations
assertEquals("Response status code", HttpStatus.OK_200, response.getStatus()); assertEquals("Response status code", HttpStatus.OK_200, response.getStatus());
content = response.getContentAsString(); content = response.getContentAsString();
TestOSGiUtil.assertContains("Response contents", content, "<h1>FRAGMENT</h1>"); TestOSGiUtil.assertContains("Response contents", content, "<h1>FRAGMENT</h1>");
MultiPartContentProvider multiPart = new MultiPartContentProvider();
multiPart.addFieldPart("field", new StringContentProvider("foo"), null);
response = client.newRequest("http://127.0.0.1:" + port + "/multi").method("POST")
.content(multiPart).send();
assertEquals(HttpStatus.OK_200, response.getStatus());
} }
finally finally
{ {

View File

@ -22,8 +22,10 @@ import java.io.IOException;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.http.HttpURI;
import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.util.ArrayUtil; import org.eclipse.jetty.util.ArrayUtil;
import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.util.URIUtil; import org.eclipse.jetty.util.URIUtil;
import org.eclipse.jetty.util.component.Dumpable; import org.eclipse.jetty.util.component.Dumpable;
import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Log;
@ -185,7 +187,18 @@ public class RuleContainer extends Rule implements Dumpable
if (rule instanceof Rule.ApplyURI) if (rule instanceof Rule.ApplyURI)
((Rule.ApplyURI)rule).applyURI(baseRequest, baseRequest.getRequestURI(), encoded); ((Rule.ApplyURI)rule).applyURI(baseRequest, baseRequest.getRequestURI(), encoded);
else else
baseRequest.setURIPathQuery(encoded); {
String uriPathQuery = encoded;
HttpURI baseUri = baseRequest.getHttpURI();
// Copy path params from original URI if present
if ((baseUri != null) && StringUtil.isNotBlank(baseUri.getParam()))
{
HttpURI uri = new HttpURI(uriPathQuery);
uri.setParam(baseUri.getParam());
uriPathQuery = uri.toString();
}
baseRequest.setURIPathQuery(uriPathQuery);
}
} }
if (_rewritePathInfo) if (_rewritePathInfo)

View File

@ -37,7 +37,6 @@ import org.eclipse.jetty.server.handler.AbstractHandler;
import org.eclipse.jetty.server.handler.HandlerList; import org.eclipse.jetty.server.handler.HandlerList;
import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.MatcherAssert.assertThat;
@ -48,7 +47,6 @@ import static org.junit.jupiter.api.Assertions.fail;
public class CookiePatternRuleTest public class CookiePatternRuleTest
{ {
private Server server; private Server server;
private LocalConnector localConnector; private LocalConnector localConnector;
@ -150,7 +148,6 @@ public class CookiePatternRuleTest
} }
@Test @Test
@Disabled("See #2675 for details") // TODO: needs to be fixed in RuleContainer
public void testUrlParameter() throws Exception public void testUrlParameter() throws Exception
{ {
CookiePatternRule rule = new CookiePatternRule(); CookiePatternRule rule = new CookiePatternRule();
@ -170,7 +167,6 @@ public class CookiePatternRuleTest
HttpTester.Response response = HttpTester.parseResponse(rawResponse); HttpTester.Response response = HttpTester.parseResponse(rawResponse);
String responseContent = response.getContent(); String responseContent = response.getContent();
System.out.println(responseContent);
assertResponseContentLine(responseContent, "baseRequest.requestUri=", "/other;fruit=apple"); assertResponseContentLine(responseContent, "baseRequest.requestUri=", "/other;fruit=apple");
// verify // verify

View File

@ -1228,10 +1228,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor
if (_length > 0) if (_length > 0)
_combinedListener.onResponseContent(_request, _content); _combinedListener.onResponseContent(_request, _content);
if (_complete && _state.completeResponse()) if (_complete && _state.completeResponse())
{
_response.getHttpOutput().closed();
_combinedListener.onResponseEnd(_request); _combinedListener.onResponseEnd(_request);
}
super.succeeded(); super.succeeded();
} }
@ -1255,7 +1252,6 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor
@Override @Override
public void failed(Throwable th) public void failed(Throwable th)
{ {
_response.getHttpOutput().closed();
abort(x); abort(x);
super.failed(x); super.failed(x);
} }

View File

@ -412,6 +412,13 @@ public class HttpChannelOverHttp extends HttpChannel implements HttpParser.Reque
return true; return true;
} }
@Override
protected boolean checkAndPrepareUpgrade()
{
// TODO: move the code from HttpConnection.upgrade() here?
return false;
}
/** /**
* <p>Attempts to perform an HTTP/1.1 upgrade.</p> * <p>Attempts to perform an HTTP/1.1 upgrade.</p>
* <p>The upgrade looks up a {@link ConnectionFactory.Upgrading} from the connector * <p>The upgrade looks up a {@link ConnectionFactory.Upgrading} from the connector

View File

@ -28,6 +28,7 @@ import javax.servlet.UnavailableException;
import org.eclipse.jetty.http.BadMessageException; import org.eclipse.jetty.http.BadMessageException;
import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.io.QuietException;
import org.eclipse.jetty.server.handler.ContextHandler; import org.eclipse.jetty.server.handler.ContextHandler;
import org.eclipse.jetty.server.handler.ContextHandler.Context; import org.eclipse.jetty.server.handler.ContextHandler.Context;
import org.eclipse.jetty.server.handler.ErrorHandler; import org.eclipse.jetty.server.handler.ErrorHandler;
@ -406,8 +407,6 @@ public class HttpChannelState
*/ */
protected Action unhandle() protected Action unhandle()
{ {
boolean readInterested = false;
synchronized (this) synchronized (this)
{ {
if (LOG.isDebugEnabled()) if (LOG.isDebugEnabled())
@ -737,8 +736,10 @@ public class HttpChannelState
} }
else else
{ {
LOG.warn(failure.toString()); if (!(failure instanceof QuietException))
LOG.debug(failure); LOG.warn(failure.toString());
if (LOG.isDebugEnabled())
LOG.debug(failure);
} }
} }
@ -969,6 +970,9 @@ public class HttpChannelState
} }
} }
// release any aggregate buffer from a closing flush
_channel.getResponse().getHttpOutput().closed();
if (event != null) if (event != null)
{ {
cancelTimeout(event); cancelTimeout(event);
@ -1341,7 +1345,7 @@ public class HttpChannelState
* but that a handling thread may need to produce (fill/parse) * but that a handling thread may need to produce (fill/parse)
* it. Typically called by the async read success callback. * it. Typically called by the async read success callback.
* *
* @return <code>true</code> if more content may be available * @return {@code true} if more content may be available
*/ */
public boolean onReadPossible() public boolean onReadPossible()
{ {
@ -1373,7 +1377,7 @@ public class HttpChannelState
* Called to signal that a read has read -1. * Called to signal that a read has read -1.
* Will wake if the read was called while in ASYNC_WAIT state * Will wake if the read was called while in ASYNC_WAIT state
* *
* @return <code>true</code> if woken * @return {@code true} if woken
*/ */
public boolean onReadEof() public boolean onReadEof()
{ {

View File

@ -315,7 +315,7 @@ public class HttpConfiguration implements Dumpable
} }
/** /**
* @param delay if true, delay the application dispatch until content is available (default false) * @param delay if true, delays the application dispatch until content is available (defaults to true)
*/ */
public void setDelayDispatchUntilContent(boolean delay) public void setDelayDispatchUntilContent(boolean delay)
{ {

View File

@ -732,22 +732,29 @@ public class HttpInput extends ServletInputStream implements Runnable
_listener = Objects.requireNonNull(readListener); _listener = Objects.requireNonNull(readListener);
Content content = produceNextContext(); if (isError())
if (content != null)
{ {
_state = ASYNC;
woken = _channelState.onReadReady(); woken = _channelState.onReadReady();
} }
else if (_state == EOF)
{
_state = AEOF;
woken = _channelState.onReadEof();
}
else else
{ {
_state = ASYNC; Content content = produceNextContext();
_channelState.onReadUnready(); if (content != null)
_waitingForContent = true; {
_state = ASYNC;
woken = _channelState.onReadReady();
}
else if (_state == EOF)
{
_state = AEOF;
woken = _channelState.onReadEof();
}
else
{
_state = ASYNC;
_channelState.onReadUnready();
_waitingForContent = true;
}
} }
} }
} }

View File

@ -364,13 +364,6 @@ public class HttpOutput extends ServletOutputStream implements Runnable
State state = _state.get(); State state = _state.get();
switch (state) switch (state)
{ {
case CLOSING:
{
if (!_state.compareAndSet(state, State.CLOSED))
break;
releaseBuffer();
return;
}
case CLOSED: case CLOSED:
{ {
return; return;
@ -378,7 +371,12 @@ public class HttpOutput extends ServletOutputStream implements Runnable
case UNREADY: case UNREADY:
{ {
if (_state.compareAndSet(state, State.ERROR)) if (_state.compareAndSet(state, State.ERROR))
_writeListener.onError(_onError == null ? new EofException("Async closed") : _onError); {
if (_onError == null)
_onError = new EofException("Async closed");
releaseBuffer();
return;
}
break; break;
} }
default: default:
@ -484,6 +482,8 @@ public class HttpOutput extends ServletOutputStream implements Runnable
{ {
write(content, complete, blocker); write(content, complete, blocker);
blocker.block(); blocker.block();
if (complete)
closed();
} }
catch (Exception failure) catch (Exception failure)
{ {

View File

@ -1,63 +0,0 @@
//
// ========================================================================
// 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 javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;
import org.eclipse.jetty.server.handler.ContextHandler;
public class MultiPartCleanerListener implements ServletRequestListener
{
public static final MultiPartCleanerListener INSTANCE = new MultiPartCleanerListener();
protected MultiPartCleanerListener()
{
}
@Override
public void requestDestroyed(ServletRequestEvent sre)
{
//Clean up any tmp files created by MultiPartInputStream
MultiParts parts = (MultiParts)sre.getServletRequest().getAttribute(Request.__MULTIPARTS);
if (parts != null)
{
ContextHandler.Context context = parts.getContext();
//Only do the cleanup if we are exiting from the context in which a servlet parsed the multipart files
if (context == sre.getServletContext())
{
try
{
parts.close();
}
catch (Throwable e)
{
sre.getServletContext().log("Errors deleting multipart tmp files", e);
}
}
}
}
@Override
public void requestInitialized(ServletRequestEvent sre)
{
//nothing to do, multipart config set up by ServletHolder.handle()
}
}

View File

@ -1,88 +0,0 @@
//
// ========================================================================
// 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.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collection;
import javax.servlet.MultipartConfigElement;
import javax.servlet.http.Part;
import org.eclipse.jetty.http.MultiPartFormInputStream;
import org.eclipse.jetty.server.handler.ContextHandler;
import org.eclipse.jetty.server.handler.ContextHandler.Context;
/*
* Used to switch between the old and new implementation of MultiPart Form InputStream Parsing.
* The new implementation is preferred will be used as default unless specified otherwise constructor.
*/
public interface MultiParts extends Closeable
{
Collection<Part> getParts() throws IOException;
Part getPart(String name) throws IOException;
boolean isEmpty();
ContextHandler.Context getContext();
class MultiPartsHttpParser implements MultiParts
{
private final MultiPartFormInputStream _httpParser;
private final ContextHandler.Context _context;
public MultiPartsHttpParser(InputStream in, String contentType, MultipartConfigElement config, File contextTmpDir, Request request) throws IOException
{
_httpParser = new MultiPartFormInputStream(in, contentType, config, contextTmpDir);
_context = request.getContext();
}
@Override
public Collection<Part> getParts() throws IOException
{
return _httpParser.getParts();
}
@Override
public Part getPart(String name) throws IOException
{
return _httpParser.getPart(name);
}
@Override
public void close()
{
_httpParser.deleteParts();
}
@Override
public boolean isEmpty()
{
return _httpParser.isEmpty();
}
@Override
public Context getContext()
{
return _context;
}
}
}

View File

@ -81,6 +81,7 @@ import org.eclipse.jetty.http.HttpURI;
import org.eclipse.jetty.http.HttpVersion; import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.http.MetaData; import org.eclipse.jetty.http.MetaData;
import org.eclipse.jetty.http.MimeTypes; import org.eclipse.jetty.http.MimeTypes;
import org.eclipse.jetty.http.MultiPartFormInputStream;
import org.eclipse.jetty.http.pathmap.PathSpec; import org.eclipse.jetty.http.pathmap.PathSpec;
import org.eclipse.jetty.http.pathmap.ServletPathSpec; import org.eclipse.jetty.http.pathmap.ServletPathSpec;
import org.eclipse.jetty.io.RuntimeIOException; import org.eclipse.jetty.io.RuntimeIOException;
@ -141,7 +142,6 @@ import org.eclipse.jetty.util.log.Logger;
public class Request implements HttpServletRequest public class Request implements HttpServletRequest
{ {
public static final String __MULTIPART_CONFIG_ELEMENT = "org.eclipse.jetty.multipartConfig"; public static final String __MULTIPART_CONFIG_ELEMENT = "org.eclipse.jetty.multipartConfig";
public static final String __MULTIPARTS = "org.eclipse.jetty.multiParts";
private static final Logger LOG = Log.getLogger(Request.class); private static final Logger LOG = Log.getLogger(Request.class);
private static final Collection<Locale> __defaultLocale = Collections.singleton(Locale.getDefault()); private static final Collection<Locale> __defaultLocale = Collections.singleton(Locale.getDefault());
@ -228,7 +228,7 @@ public class Request implements HttpServletRequest
private HttpSession _session; private HttpSession _session;
private SessionHandler _sessionHandler; private SessionHandler _sessionHandler;
private long _timeStamp; private long _timeStamp;
private MultiParts _multiParts; //if the request is a multi-part mime private MultiPartFormInputStream _multiParts; //if the request is a multi-part mime
private AsyncContextState _async; private AsyncContextState _async;
private List<Session> _sessions; //list of sessions used during lifetime of request private List<Session> _sessions; //list of sessions used during lifetime of request
@ -1504,6 +1504,19 @@ public class Request implements HttpServletRequest
for (Session s:_sessions) for (Session s:_sessions)
leaveSession(s); leaveSession(s);
} }
//Clean up any tmp files created by MultiPartInputStream
if (_multiParts != null)
{
try
{
_multiParts.deleteParts();
}
catch (Throwable e)
{
LOG.warn("Errors deleting multipart tmp files", e);
}
}
} }
/** /**
@ -2299,15 +2312,12 @@ public class Request implements HttpServletRequest
{ {
String contentType = getContentType(); String contentType = getContentType();
if (contentType == null || !MimeTypes.Type.MULTIPART_FORM_DATA.is(HttpFields.valueParameters(contentType, null))) if (contentType == null || !MimeTypes.Type.MULTIPART_FORM_DATA.is(HttpFields.valueParameters(contentType, null)))
throw new ServletException("Content-Type != multipart/form-data"); throw new ServletException("Unsupported Content-Type [" + contentType + "], expected [multipart/form-data]");
return getParts(null); return getParts(null);
} }
private Collection<Part> getParts(MultiMap<String> params) throws IOException private Collection<Part> getParts(MultiMap<String> params) throws IOException
{ {
if (_multiParts == null)
_multiParts = (MultiParts)getAttribute(__MULTIPARTS);
if (_multiParts == null) if (_multiParts == null)
{ {
MultipartConfigElement config = (MultipartConfigElement)getAttribute(__MULTIPART_CONFIG_ELEMENT); MultipartConfigElement config = (MultipartConfigElement)getAttribute(__MULTIPART_CONFIG_ELEMENT);
@ -2315,7 +2325,6 @@ public class Request implements HttpServletRequest
throw new IllegalStateException("No multipart config for servlet"); throw new IllegalStateException("No multipart config for servlet");
_multiParts = newMultiParts(config); _multiParts = newMultiParts(config);
setAttribute(__MULTIPARTS, _multiParts);
Collection<Part> parts = _multiParts.getParts(); Collection<Part> parts = _multiParts.getParts();
String formCharset = null; String formCharset = null;
@ -2377,10 +2386,10 @@ public class Request implements HttpServletRequest
return _multiParts.getParts(); return _multiParts.getParts();
} }
private MultiParts newMultiParts(MultipartConfigElement config) throws IOException private MultiPartFormInputStream newMultiParts(MultipartConfigElement config) throws IOException
{ {
return new MultiParts.MultiPartsHttpParser(getInputStream(), getContentType(), config, return new MultiPartFormInputStream(getInputStream(), getContentType(), config,
(_context != null ? (File)_context.getAttribute("javax.servlet.context.tempdir") : null), this); (_context != null ? (File)_context.getAttribute("javax.servlet.context.tempdir") : null));
} }
@Override @Override

View File

@ -128,10 +128,13 @@ public class ResponseWriter extends PrintWriter
private void isOpen() throws IOException private void isOpen() throws IOException
{ {
if (_ioException != null) if (_ioException != null)
throw new RuntimeIOException(_ioException); throw _ioException;
if (_isClosed) if (_isClosed)
throw new EofException("Stream closed"); {
_ioException = new EofException("Stream closed");
throw _ioException;
}
} }
@Override @Override

View File

@ -21,6 +21,7 @@ package org.eclipse.jetty.server.handler;
import java.io.IOException; import java.io.IOException;
import java.io.OutputStreamWriter; import java.io.OutputStreamWriter;
import java.io.PrintWriter; import java.io.PrintWriter;
import java.io.StringWriter;
import java.io.Writer; import java.io.Writer;
import java.nio.BufferOverflowException; import java.nio.BufferOverflowException;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
@ -474,24 +475,24 @@ public class ErrorHandler extends AbstractHandler
writer.append(json.entrySet().stream() writer.append(json.entrySet().stream()
.map(e -> QuotedStringTokenizer.quote(e.getKey()) + .map(e -> QuotedStringTokenizer.quote(e.getKey()) +
":" + ":" +
QuotedStringTokenizer.quote((e.getValue()))) QuotedStringTokenizer.quote(StringUtil.sanitizeXmlString((e.getValue()))))
.collect(Collectors.joining(",\n", "{\n", "\n}"))); .collect(Collectors.joining(",\n", "{\n", "\n}")));
} }
protected void writeErrorPageStacks(HttpServletRequest request, Writer writer) protected void writeErrorPageStacks(HttpServletRequest request, Writer writer)
throws IOException throws IOException
{ {
Throwable th = (Throwable)request.getAttribute(RequestDispatcher.ERROR_EXCEPTION); Throwable th = (Throwable)request.getAttribute(RequestDispatcher.ERROR_EXCEPTION);
if (_showStacks && th != null) if (th != null)
{ {
PrintWriter pw = writer instanceof PrintWriter ? (PrintWriter)writer : new PrintWriter(writer); writer.write("<h3>Caused by:</h3><pre>");
pw.write("<pre>"); // You have to pre-generate and then use #write(writer, String)
while (th != null) try (StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw))
{ {
th.printStackTrace(pw); th.printStackTrace(pw);
th = th.getCause(); pw.flush();
write(writer, sw.getBuffer().toString()); // sanitize
} }
writer.write("</pre>\n"); writer.write("</pre>\n");
} }

View File

@ -378,7 +378,7 @@ public class Session implements SessionHandler.SessionIf
try try
{ {
HttpSessionEvent event = new HttpSessionEvent(this); HttpSessionEvent event = new HttpSessionEvent(this);
for (String name : _sessionData.getKeys()) for (String name : _sessionData.getKeys())
{ {
Object value = _sessionData.getAttribute(name); Object value = _sessionData.getAttribute(name);
if (value instanceof HttpSessionActivationListener) if (value instanceof HttpSessionActivationListener)

View File

@ -33,6 +33,12 @@ import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException; import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Stream; import java.util.stream.Stream;
import javax.servlet.AsyncContext;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.WriteListener;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.http.HttpCompliance; import org.eclipse.jetty.http.HttpCompliance;
import org.eclipse.jetty.http.tools.HttpTester; import org.eclipse.jetty.http.tools.HttpTester;
@ -41,6 +47,7 @@ import org.eclipse.jetty.io.Connection;
import org.eclipse.jetty.io.EndPoint; import org.eclipse.jetty.io.EndPoint;
import org.eclipse.jetty.io.ManagedSelector; import org.eclipse.jetty.io.ManagedSelector;
import org.eclipse.jetty.io.SocketChannelEndPoint; import org.eclipse.jetty.io.SocketChannelEndPoint;
import org.eclipse.jetty.server.handler.AbstractHandler;
import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.thread.Scheduler; import org.eclipse.jetty.util.thread.Scheduler;
import org.hamcrest.Matchers; import org.hamcrest.Matchers;
@ -89,7 +96,7 @@ public class AsyncCompletionTest extends HttpServerTestFixture
_delay.get(10, TimeUnit.SECONDS); _delay.get(10, TimeUnit.SECONDS);
getCallback().succeeded(); getCallback().succeeded();
} }
catch(Throwable th) catch (Throwable th)
{ {
th.printStackTrace(); th.printStackTrace();
getCallback().failed(th); getCallback().failed(th);
@ -97,7 +104,6 @@ public class AsyncCompletionTest extends HttpServerTestFixture
} }
} }
@BeforeEach @BeforeEach
public void init() throws Exception public void init() throws Exception
{ {
@ -153,7 +159,7 @@ public class AsyncCompletionTest extends HttpServerTestFixture
@Override @Override
public void onCompleted() public void onCompleted()
{ {
COMPLETE.compareAndSet(false,true); COMPLETE.compareAndSet(false, true);
super.onCompleted(); super.onCompleted();
} }
} }
@ -163,7 +169,8 @@ public class AsyncCompletionTest extends HttpServerTestFixture
{ {
List<Object[]> tests = new ArrayList<>(); List<Object[]> tests = new ArrayList<>();
tests.add(new Object[]{new HelloWorldHandler(), 200, "Hello world"}); tests.add(new Object[]{new HelloWorldHandler(), 200, "Hello world"});
tests.add(new Object[]{new SendErrorHandler(499,"Test async sendError"), 499, "Test async sendError"}); tests.add(new Object[]{new SendErrorHandler(499, "Test async sendError"), 499, "Test async sendError"});
tests.add(new Object[]{new AsyncReadyCompleteHandler(), 200, AsyncReadyCompleteHandler.data});
return tests.stream().map(Arguments::of); return tests.stream().map(Arguments::of);
} }
@ -197,7 +204,7 @@ public class AsyncCompletionTest extends HttpServerTestFixture
// wait for threads to return to base level // wait for threads to return to base level
long end = System.nanoTime() + TimeUnit.SECONDS.toNanos(10); long end = System.nanoTime() + TimeUnit.SECONDS.toNanos(10);
while(_threadPool.getBusyThreads() != base) while (_threadPool.getBusyThreads() != base)
{ {
if (System.nanoTime() > end) if (System.nanoTime() > end)
throw new TimeoutException(); throw new TimeoutException();
@ -210,7 +217,7 @@ public class AsyncCompletionTest extends HttpServerTestFixture
// proceed with the completion // proceed with the completion
delay.proceed(); delay.proceed();
while(!COMPLETE.get()) while (!COMPLETE.get())
{ {
if (System.nanoTime() > end) if (System.nanoTime() > end)
throw new TimeoutException(); throw new TimeoutException();
@ -218,4 +225,46 @@ public class AsyncCompletionTest extends HttpServerTestFixture
} }
} }
} }
private static class AsyncReadyCompleteHandler extends AbstractHandler
{
static String data = "Now is the time for all good men to come to the aid of the party";
@Override
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
{
AsyncContext context = request.startAsync();
ServletOutputStream out = response.getOutputStream();
out.setWriteListener(new WriteListener()
{
byte[] bytes = data.getBytes(StandardCharsets.ISO_8859_1);
@Override
public void onWritePossible() throws IOException
{
while (out.isReady())
{
if (bytes != null)
{
response.setContentType("text/plain");
response.setContentLength(bytes.length);
out.write(bytes);
bytes = null;
}
else
{
context.complete();
return;
}
}
}
@Override
public void onError(Throwable t)
{
t.printStackTrace();
}
});
}
}
} }

View File

@ -19,8 +19,9 @@
package org.eclipse.jetty.server; package org.eclipse.jetty.server;
import java.io.IOException; import java.io.IOException;
import java.util.HashSet;
import java.util.Map; import java.util.Map;
import java.util.Set;
import javax.servlet.DispatcherType; import javax.servlet.DispatcherType;
import javax.servlet.RequestDispatcher; import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException; import javax.servlet.ServletException;
@ -28,30 +29,38 @@ import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.http.BadMessageException; import org.eclipse.jetty.http.BadMessageException;
import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.tools.HttpTester; import org.eclipse.jetty.http.tools.HttpTester;
import org.eclipse.jetty.server.handler.AbstractHandler; import org.eclipse.jetty.server.handler.AbstractHandler;
import org.eclipse.jetty.util.ajax.JSON; import org.eclipse.jetty.util.ajax.JSON;
import org.eclipse.jetty.util.log.StacklessLogging;
import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.anyOf;
import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.greaterThan;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.startsWith; import static org.hamcrest.Matchers.nullValue;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class ErrorHandlerTest public class ErrorHandlerTest
{ {
static StacklessLogging stacklessLogging;
static Server server; static Server server;
static LocalConnector connector; static LocalConnector connector;
@BeforeAll @BeforeAll
public static void before() throws Exception public static void before() throws Exception
{ {
stacklessLogging = new StacklessLogging(HttpChannel.class);
server = new Server(); server = new Server();
connector = new LocalConnector(server); connector = new LocalConnector(server);
server.addConnector(connector); server.addConnector(connector);
@ -64,7 +73,7 @@ public class ErrorHandlerTest
if (baseRequest.getDispatcherType() == DispatcherType.ERROR) if (baseRequest.getDispatcherType() == DispatcherType.ERROR)
{ {
baseRequest.setHandled(true); baseRequest.setHandled(true);
response.sendError(((Integer)request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE)).intValue()); response.sendError((Integer)request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE));
return; return;
} }
@ -78,7 +87,40 @@ public class ErrorHandlerTest
if (target.startsWith("/badmessage/")) if (target.startsWith("/badmessage/"))
{ {
throw new ServletException(new BadMessageException(Integer.parseInt(target.substring(12)))); int code = Integer.parseInt(target.substring(target.lastIndexOf('/') + 1));
throw new ServletException(new BadMessageException(code));
}
// produce an exception with an JSON formatted cause message
if (target.startsWith("/jsonmessage/"))
{
String message = "\"}, \"glossary\": {\n \"title\": \"example\"\n }\n {\"";
throw new ServletException(new RuntimeException(message));
}
// produce an exception with an XML cause message
if (target.startsWith("/xmlmessage/"))
{
String message =
"<!DOCTYPE glossary PUBLIC \"-//OASIS//DTD DocBook V3.1//EN\">\n" +
" <glossary>\n" +
" <title>example glossary</title>\n" +
" </glossary>";
throw new ServletException(new RuntimeException(message));
}
// produce an exception with an HTML cause message
if (target.startsWith("/htmlmessage/"))
{
String message = "<hr/><script>alert(42)</script>%3Cscript%3E";
throw new ServletException(new RuntimeException(message));
}
// produce an exception with a UTF-8 cause message
if (target.startsWith("/utf8message/"))
{
String message = "Euro is &euro; and \u20AC and %E2%82%AC";
throw new ServletException(new RuntimeException(message));
} }
} }
}); });
@ -89,193 +131,238 @@ public class ErrorHandlerTest
public static void after() throws Exception public static void after() throws Exception
{ {
server.stop(); server.stop();
stacklessLogging.close();
} }
@Test @Test
public void test404NoAccept() throws Exception public void test404NoAccept() throws Exception
{ {
String response = connector.getResponse( String rawResponse = connector.getResponse(
"GET / HTTP/1.1\r\n" + "GET / HTTP/1.1\r\n" +
"Host: Localhost\r\n" + "Host: Localhost\r\n" +
"\r\n"); "\r\n");
assertThat(response, startsWith("HTTP/1.1 404 ")); HttpTester.Response response = HttpTester.parseResponse(rawResponse);
assertThat(response, not(containsString("Content-Length: 0")));
assertThat(response, containsString("Content-Type: text/html;charset=iso-8859-1")); assertThat("Response status code", response.getStatus(), is(404));
assertThat("Response Content-Length", response.getField(HttpHeader.CONTENT_LENGTH).getIntValue(), greaterThan(0));
assertThat("Response Content-Type", response.get(HttpHeader.CONTENT_TYPE), containsString("text/html;charset=ISO-8859-1"));
assertContent(response);
} }
@Test @Test
public void test404EmptyAccept() throws Exception public void test404EmptyAccept() throws Exception
{ {
String response = connector.getResponse( String rawResponse = connector.getResponse(
"GET / HTTP/1.1\r\n" + "GET / HTTP/1.1\r\n" +
"Accept: \r\n" + "Accept: \r\n" +
"Host: Localhost\r\n" + "Host: Localhost\r\n" +
"\r\n"); "\r\n");
assertThat(response, startsWith("HTTP/1.1 404 ")); HttpTester.Response response = HttpTester.parseResponse(rawResponse);
assertThat(response, containsString("Content-Length: 0"));
assertThat(response, not(containsString("Content-Type"))); assertThat("Response status code", response.getStatus(), is(404));
assertThat("Response Content-Length", response.getField(HttpHeader.CONTENT_LENGTH).getIntValue(), is(0));
assertThat("Response Content-Type", response.getField(HttpHeader.CONTENT_TYPE), is(nullValue()));
} }
@Test @Test
public void test404UnAccept() throws Exception public void test404UnAccept() throws Exception
{ {
String response = connector.getResponse( String rawResponse = connector.getResponse(
"GET / HTTP/1.1\r\n" + "GET / HTTP/1.1\r\n" +
"Accept: text/*;q=0\r\n" + "Accept: text/*;q=0\r\n" +
"Host: Localhost\r\n" + "Host: Localhost\r\n" +
"\r\n"); "\r\n");
HttpTester.Response response = HttpTester.parseResponse(rawResponse);
assertThat(response, startsWith("HTTP/1.1 404 ")); assertThat("Response status code", response.getStatus(), is(404));
assertThat(response, containsString("Content-Length: 0")); assertThat("Response Content-Length", response.getField(HttpHeader.CONTENT_LENGTH).getIntValue(), is(0));
assertThat(response, not(containsString("Content-Type"))); assertThat("Response Content-Type", response.getField(HttpHeader.CONTENT_TYPE), is(nullValue()));
} }
@Test @Test
public void test404AllAccept() throws Exception public void test404AllAccept() throws Exception
{ {
String response = connector.getResponse( String rawResponse = connector.getResponse(
"GET / HTTP/1.1\r\n" + "GET / HTTP/1.1\r\n" +
"Host: Localhost\r\n" + "Host: Localhost\r\n" +
"Accept: */*\r\n" + "Accept: */*\r\n" +
"\r\n"); "\r\n");
HttpTester.Response response = HttpTester.parseResponse(rawResponse);
assertThat(response, startsWith("HTTP/1.1 404 ")); assertThat("Response status code", response.getStatus(), is(404));
assertThat(response, not(containsString("Content-Length: 0"))); assertThat("Response Content-Length", response.getField(HttpHeader.CONTENT_LENGTH).getIntValue(), greaterThan(0));
assertThat(response, containsString("Content-Type: text/html;charset=iso-8859-1")); assertThat("Response Content-Type", response.get(HttpHeader.CONTENT_TYPE), containsString("text/html;charset=ISO-8859-1"));
assertContent(response);
} }
@Test @Test
public void test404HtmlAccept() throws Exception public void test404HtmlAccept() throws Exception
{ {
String response = connector.getResponse( String rawResponse = connector.getResponse(
"GET / HTTP/1.1\r\n" + "GET / HTTP/1.1\r\n" +
"Host: Localhost\r\n" + "Host: Localhost\r\n" +
"Accept: text/html\r\n" + "Accept: text/html\r\n" +
"\r\n"); "\r\n");
assertThat(response, startsWith("HTTP/1.1 404 ")); HttpTester.Response response = HttpTester.parseResponse(rawResponse);
assertThat(response, not(containsString("Content-Length: 0")));
assertThat(response, containsString("Content-Type: text/html;charset=iso-8859-1")); assertThat("Response status code", response.getStatus(), is(404));
assertThat("Response Content-Length", response.getField(HttpHeader.CONTENT_LENGTH).getIntValue(), greaterThan(0));
assertThat("Response Content-Type", response.get(HttpHeader.CONTENT_TYPE), containsString("text/html;charset=ISO-8859-1"));
assertContent(response);
} }
@Test @Test
public void testMoreSpecificAccept() throws Exception public void testMoreSpecificAccept() throws Exception
{ {
String response = connector.getResponse( String rawResponse = connector.getResponse(
"GET / HTTP/1.1\r\n" + "GET / HTTP/1.1\r\n" +
"Host: Localhost\r\n" + "Host: Localhost\r\n" +
"Accept: text/html, some/other;specific=true\r\n" + "Accept: text/html, some/other;specific=true\r\n" +
"\r\n"); "\r\n");
assertThat(response, startsWith("HTTP/1.1 404 ")); HttpTester.Response response = HttpTester.parseResponse(rawResponse);
assertThat(response, not(containsString("Content-Length: 0")));
assertThat(response, containsString("Content-Type: text/html;charset=iso-8859-1")); assertThat("Response status code", response.getStatus(), is(404));
assertThat("Response Content-Length", response.getField(HttpHeader.CONTENT_LENGTH).getIntValue(), greaterThan(0));
assertThat("Response Content-Type", response.get(HttpHeader.CONTENT_TYPE), containsString("text/html;charset=ISO-8859-1"));
assertContent(response);
} }
@Test @Test
public void test404HtmlAcceptAnyCharset() throws Exception public void test404HtmlAcceptAnyCharset() throws Exception
{ {
String response = connector.getResponse( String rawResponse = connector.getResponse(
"GET / HTTP/1.1\r\n" + "GET / HTTP/1.1\r\n" +
"Host: Localhost\r\n" + "Host: Localhost\r\n" +
"Accept: text/html\r\n" + "Accept: text/html\r\n" +
"Accept-Charset: *\r\n" + "Accept-Charset: *\r\n" +
"\r\n"); "\r\n");
assertThat(response, startsWith("HTTP/1.1 404 ")); HttpTester.Response response = HttpTester.parseResponse(rawResponse);
assertThat(response, not(containsString("Content-Length: 0")));
assertThat(response, containsString("Content-Type: text/html;charset=utf-8")); assertThat("Response status code", response.getStatus(), is(404));
assertThat("Response Content-Length", response.getField(HttpHeader.CONTENT_LENGTH).getIntValue(), greaterThan(0));
assertThat("Response Content-Type", response.get(HttpHeader.CONTENT_TYPE), containsString("text/html;charset=UTF-8"));
assertContent(response);
} }
@Test @Test
public void test404HtmlAcceptUtf8Charset() throws Exception public void test404HtmlAcceptUtf8Charset() throws Exception
{ {
String response = connector.getResponse( String rawResponse = connector.getResponse(
"GET / HTTP/1.1\r\n" + "GET / HTTP/1.1\r\n" +
"Host: Localhost\r\n" + "Host: Localhost\r\n" +
"Accept: text/html\r\n" + "Accept: text/html\r\n" +
"Accept-Charset: utf-8\r\n" + "Accept-Charset: utf-8\r\n" +
"\r\n"); "\r\n");
assertThat(response, startsWith("HTTP/1.1 404 ")); HttpTester.Response response = HttpTester.parseResponse(rawResponse);
assertThat(response, not(containsString("Content-Length: 0")));
assertThat(response, containsString("Content-Type: text/html;charset=utf-8")); assertThat("Response status code", response.getStatus(), is(404));
assertThat("Response Content-Length", response.getField(HttpHeader.CONTENT_LENGTH).getIntValue(), greaterThan(0));
assertThat("Response Content-Type", response.get(HttpHeader.CONTENT_TYPE), containsString("text/html;charset=UTF-8"));
assertContent(response);
} }
@Test @Test
public void test404HtmlAcceptNotUtf8Charset() throws Exception public void test404HtmlAcceptNotUtf8Charset() throws Exception
{ {
String response = connector.getResponse( String rawResponse = connector.getResponse(
"GET / HTTP/1.1\r\n" + "GET / HTTP/1.1\r\n" +
"Host: Localhost\r\n" + "Host: Localhost\r\n" +
"Accept: text/html\r\n" + "Accept: text/html\r\n" +
"Accept-Charset: utf-8;q=0\r\n" + "Accept-Charset: utf-8;q=0\r\n" +
"\r\n"); "\r\n");
assertThat(response, startsWith("HTTP/1.1 404 ")); HttpTester.Response response = HttpTester.parseResponse(rawResponse);
assertThat(response, not(containsString("Content-Length: 0")));
assertThat(response, containsString("Content-Type: text/html;charset=iso-8859-1")); assertThat("Response status code", response.getStatus(), is(404));
assertThat("Response Content-Length", response.getField(HttpHeader.CONTENT_LENGTH).getIntValue(), greaterThan(0));
assertThat("Response Content-Type", response.get(HttpHeader.CONTENT_TYPE), containsString("text/html;charset=ISO-8859-1"));
assertContent(response);
} }
@Test @Test
public void test404HtmlAcceptNotUtf8UnknownCharset() throws Exception public void test404HtmlAcceptNotUtf8UnknownCharset() throws Exception
{ {
String response = connector.getResponse( String rawResponse = connector.getResponse(
"GET / HTTP/1.1\r\n" + "GET / HTTP/1.1\r\n" +
"Host: Localhost\r\n" + "Host: Localhost\r\n" +
"Accept: text/html\r\n" + "Accept: text/html\r\n" +
"Accept-Charset: utf-8;q=0,unknown\r\n" + "Accept-Charset: utf-8;q=0,unknown\r\n" +
"\r\n"); "\r\n");
assertThat(response, startsWith("HTTP/1.1 404 ")); HttpTester.Response response = HttpTester.parseResponse(rawResponse);
assertThat(response, containsString("Content-Length: 0"));
assertThat(response, not(containsString("Content-Type"))); assertThat("Response status code", response.getStatus(), is(404));
assertThat("Response Content-Length", response.getField(HttpHeader.CONTENT_LENGTH).getIntValue(), is(0));
assertThat("Response Content-Type", response.getField(HttpHeader.CONTENT_TYPE), is(nullValue()));
} }
@Test @Test
public void test404HtmlAcceptUnknownUtf8Charset() throws Exception public void test404HtmlAcceptUnknownUtf8Charset() throws Exception
{ {
String response = connector.getResponse( String rawResponse = connector.getResponse(
"GET / HTTP/1.1\r\n" + "GET / HTTP/1.1\r\n" +
"Host: Localhost\r\n" + "Host: Localhost\r\n" +
"Accept: text/html\r\n" + "Accept: text/html\r\n" +
"Accept-Charset: utf-8;q=0.1,unknown\r\n" + "Accept-Charset: utf-8;q=0.1,unknown\r\n" +
"\r\n"); "\r\n");
assertThat(response, startsWith("HTTP/1.1 404 ")); HttpTester.Response response = HttpTester.parseResponse(rawResponse);
assertThat(response, not(containsString("Content-Length: 0")));
assertThat(response, containsString("Content-Type: text/html;charset=utf-8")); assertThat("Response status code", response.getStatus(), is(404));
assertThat("Response Content-Length", response.getField(HttpHeader.CONTENT_LENGTH).getIntValue(), greaterThan(0));
assertThat("Response Content-Type", response.get(HttpHeader.CONTENT_TYPE), containsString("text/html;charset=UTF-8"));
assertContent(response);
} }
@Test @Test
public void test404PreferHtml() throws Exception public void test404PreferHtml() throws Exception
{ {
String response = connector.getResponse( String rawResponse = connector.getResponse(
"GET / HTTP/1.1\r\n" + "GET / HTTP/1.1\r\n" +
"Host: Localhost\r\n" + "Host: Localhost\r\n" +
"Accept: text/html;q=1.0,text/json;q=0.5,*/*\r\n" + "Accept: text/html;q=1.0,text/json;q=0.5,*/*\r\n" +
"Accept-Charset: *\r\n" + "Accept-Charset: *\r\n" +
"\r\n"); "\r\n");
assertThat(response, startsWith("HTTP/1.1 404 ")); HttpTester.Response response = HttpTester.parseResponse(rawResponse);
assertThat(response, not(containsString("Content-Length: 0")));
assertThat(response, containsString("Content-Type: text/html;charset=utf-8")); assertThat("Response status code", response.getStatus(), is(404));
assertThat("Response Content-Length", response.getField(HttpHeader.CONTENT_LENGTH).getIntValue(), greaterThan(0));
assertThat("Response Content-Type", response.get(HttpHeader.CONTENT_TYPE), containsString("text/html;charset=UTF-8"));
assertContent(response);
} }
@Test @Test
public void test404PreferJson() throws Exception public void test404PreferJson() throws Exception
{ {
String response = connector.getResponse( String rawResponse = connector.getResponse(
"GET / HTTP/1.1\r\n" + "GET / HTTP/1.1\r\n" +
"Host: Localhost\r\n" + "Host: Localhost\r\n" +
"Accept: text/html;q=0.5,text/json;q=1.0,*/*\r\n" + "Accept: text/html;q=0.5,text/json;q=1.0,*/*\r\n" +
"Accept-Charset: *\r\n" + "Accept-Charset: *\r\n" +
"\r\n"); "\r\n");
assertThat(response, startsWith("HTTP/1.1 404 ")); HttpTester.Response response = HttpTester.parseResponse(rawResponse);
assertThat(response, not(containsString("Content-Length: 0")));
assertThat(response, containsString("Content-Type: text/json")); assertThat("Response status code", response.getStatus(), is(404));
assertThat("Response Content-Length", response.getField(HttpHeader.CONTENT_LENGTH).getIntValue(), greaterThan(0));
assertThat("Response Content-Type", response.get(HttpHeader.CONTENT_TYPE), containsString("text/json"));
assertContent(response);
} }
@Test @Test
@ -286,12 +373,14 @@ public class ErrorHandlerTest
"Host: Localhost\r\n" + "Host: Localhost\r\n" +
"Accept: text/plain\r\n" + "Accept: text/plain\r\n" +
"\r\n"); "\r\n");
HttpTester.Response response = HttpTester.parseResponse(rawResponse); HttpTester.Response response = HttpTester.parseResponse(rawResponse);
assertThat("Response status code", response.getStatus(), is(404)); assertThat("Response status code", response.getStatus(), is(404));
HttpField contentType = response.getField(HttpHeader.CONTENT_TYPE); assertThat("Response Content-Length", response.getField(HttpHeader.CONTENT_LENGTH).getIntValue(), greaterThan(0));
assertThat("Response Content-Type", contentType, is(notNullValue())); assertThat("Response Content-Type", response.get(HttpHeader.CONTENT_TYPE), containsString("text/plain"));
assertThat("Response Content-Type value", contentType.getValue(), not(containsString("null")));
assertContent(response);
} }
@Test @Test
@ -301,29 +390,173 @@ public class ErrorHandlerTest
"GET /badmessage/444 HTTP/1.1\r\n" + "GET /badmessage/444 HTTP/1.1\r\n" +
"Host: Localhost\r\n" + "Host: Localhost\r\n" +
"\r\n"); "\r\n");
HttpTester.Response response = HttpTester.parseResponse(rawResponse); HttpTester.Response response = HttpTester.parseResponse(rawResponse);
assertThat("Response status code", response.getStatus(), is(444)); assertThat("Response status code", response.getStatus(), is(444));
assertThat("Response Content-Length", response.getField(HttpHeader.CONTENT_LENGTH).getIntValue(), greaterThan(0));
assertThat("Response Content-Type", response.get(HttpHeader.CONTENT_TYPE), containsString("text/html;charset=ISO-8859-1"));
assertContent(response);
}
@ParameterizedTest
@ValueSource(strings = {
"/jsonmessage/",
"/xmlmessage/",
"/htmlmessage/",
"/utf8message/",
})
public void testComplexCauseMessageNoAcceptHeader(String path) throws Exception
{
String rawResponse = connector.getResponse(
"GET " + path + " HTTP/1.1\r\n" +
"Host: Localhost\r\n" +
"\r\n");
HttpTester.Response response = HttpTester.parseResponse(rawResponse);
assertThat("Response status code", response.getStatus(), is(500));
assertThat("Response Content-Length", response.getField(HttpHeader.CONTENT_LENGTH).getIntValue(), greaterThan(0));
assertThat("Response Content-Type", response.get(HttpHeader.CONTENT_TYPE), containsString("text/html;charset=ISO-8859-1"));
String content = assertContent(response);
if (path.startsWith("/utf8"))
{
// we are Not expecting UTF-8 output, look for mangled ISO-8859-1 version
assertThat("content", content, containsString("Euro is &amp;euro; and ? and %E2%82%AC"));
}
}
@ParameterizedTest
@ValueSource(strings = {
"/jsonmessage/",
"/xmlmessage/",
"/htmlmessage/",
"/utf8message/",
})
public void testComplexCauseMessageAcceptUtf8Header(String path) throws Exception
{
String rawResponse = connector.getResponse(
"GET " + path + " HTTP/1.1\r\n" +
"Host: Localhost\r\n" +
"Accept: text/html\r\n" +
"Accept-Charset: utf-8\r\n" +
"\r\n");
HttpTester.Response response = HttpTester.parseResponse(rawResponse);
assertThat("Response status code", response.getStatus(), is(500));
assertThat("Response Content-Length", response.getField(HttpHeader.CONTENT_LENGTH).getIntValue(), greaterThan(0));
assertThat("Response Content-Type", response.get(HttpHeader.CONTENT_TYPE), containsString("text/html;charset=UTF-8"));
String content = assertContent(response);
if (path.startsWith("/utf8"))
{
// we are Not expecting UTF-8 output, look for mangled ISO-8859-1 version
assertThat("content", content, containsString("Euro is &amp;euro; and \u20AC and %E2%82%AC"));
}
}
private String assertContent(HttpTester.Response response)
{
String contentType = response.get(HttpHeader.CONTENT_TYPE);
String content = response.getContent();
if (contentType.contains("text/html"))
{
assertThat(content, not(containsString("<script>")));
assertThat(content, not(containsString("<glossary>")));
assertThat(content, not(containsString("<!DOCTYPE>")));
assertThat(content, not(containsString("&euro;")));
}
else if (contentType.contains("text/json"))
{
Map jo = (Map)JSON.parse(response.getContent());
Set<String> acceptableKeyNames = new HashSet<>();
acceptableKeyNames.add("url");
acceptableKeyNames.add("status");
acceptableKeyNames.add("message");
acceptableKeyNames.add("servlet");
acceptableKeyNames.add("cause0");
acceptableKeyNames.add("cause1");
acceptableKeyNames.add("cause2");
for (Object key : jo.keySet())
{
String keyStr = (String)key;
assertTrue(acceptableKeyNames.contains(keyStr), "Unexpected Key [" + keyStr + "]");
Object value = jo.get(key);
assertThat("Unexpected value type (" + value.getClass().getName() + ")",
value, instanceOf(String.class));
}
assertThat("url field", jo.get("url"), is(notNullValue()));
String expectedStatus = String.valueOf(response.getStatus());
assertThat("status field", jo.get("status"), is(expectedStatus));
String message = (String)jo.get("message");
assertThat("message field", message, is(notNullValue()));
assertThat("message field", message, anyOf(
not(containsString("<")),
not(containsString(">"))));
}
else if (contentType.contains("text/plain"))
{
assertThat(content, containsString("STATUS: " + response.getStatus()));
}
else
{
System.out.println("Not checked Content-Type: " + contentType);
System.out.println(content);
}
return content;
} }
@Test @Test
public void testJsonResponse() throws Exception public void testJsonResponse() throws Exception
{ {
String rawResponse = connector.getResponse( String rawResponse = connector.getResponse(
"GET /badmessage/444 HTTP/1.1\r\n" + "GET /badmessage/444 HTTP/1.1\r\n" +
"Host: Localhost\r\n" + "Host: Localhost\r\n" +
"Accept: text/json\r\n" + "Accept: text/json\r\n" +
"\r\n"); "\r\n");
HttpTester.Response response = HttpTester.parseResponse(rawResponse); HttpTester.Response response = HttpTester.parseResponse(rawResponse);
assertThat("Response status code", response.getStatus(), is(444)); assertThat("Response status code", response.getStatus(), is(444));
System.out.println("response:" + response.getContent()); assertContent(response);
}
Map<Object,Object> jo = (Map) JSON.parse(response.getContent()); @ParameterizedTest
@ValueSource(strings = {
"/jsonmessage/",
"/xmlmessage/",
"/htmlmessage/",
"/utf8message/",
})
public void testJsonResponseWorse(String path) throws Exception
{
String rawResponse = connector.getResponse(
"GET " + path + " HTTP/1.1\r\n" +
"Host: Localhost\r\n" +
"Accept: text/json\r\n" +
"\r\n");
assertThat("url field null", jo.get("url"), is(notNullValue())); HttpTester.Response response = HttpTester.parseResponse(rawResponse);
assertThat("status field null", jo.get("status"), is(notNullValue())); assertThat("Response status code", response.getStatus(), is(500));
assertThat("message field null", jo.get("message"), is(notNullValue()));
String content = assertContent(response);
if (path.startsWith("/utf8"))
{
// we are expecting UTF-8 output, look for it.
assertThat("content", content, containsString("Euro is &amp;euro; and \u20AC and %E2%82%AC"));
}
} }
} }

View File

@ -38,7 +38,6 @@ import javax.servlet.DispatcherType;
import javax.servlet.MultipartConfigElement; import javax.servlet.MultipartConfigElement;
import javax.servlet.ServletException; import javax.servlet.ServletException;
import javax.servlet.ServletInputStream; import javax.servlet.ServletInputStream;
import javax.servlet.ServletRequestEvent;
import javax.servlet.http.Cookie; import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
@ -341,28 +340,13 @@ public class RequestTest
testTmpDir.deleteOnExit(); testTmpDir.deleteOnExit();
assertTrue(testTmpDir.list().length == 0); assertTrue(testTmpDir.list().length == 0);
// We should have two tmp files after parsing the multipart form.
RequestTester tester = (request, response) -> testTmpDir.list().length == 2;
ContextHandler contextHandler = new ContextHandler(); ContextHandler contextHandler = new ContextHandler();
contextHandler.setContextPath("/foo"); contextHandler.setContextPath("/foo");
contextHandler.setResourceBase("."); contextHandler.setResourceBase(".");
contextHandler.setHandler(new MultiPartRequestHandler(testTmpDir)); contextHandler.setHandler(new MultiPartRequestHandler(testTmpDir, tester));
contextHandler.addEventListener(new MultiPartCleanerListener()
{
@Override
public void requestDestroyed(ServletRequestEvent sre)
{
MultiParts m = (MultiParts)sre.getServletRequest().getAttribute(Request.__MULTIPARTS);
assertNotNull(m);
ContextHandler.Context c = m.getContext();
assertNotNull(c);
assertTrue(c == sre.getServletContext());
assertTrue(!m.isEmpty());
assertTrue(testTmpDir.list().length == 2);
super.requestDestroyed(sre);
String[] files = testTmpDir.list();
assertTrue(files.length == 0);
}
});
_server.stop(); _server.stop();
_server.setHandler(contextHandler); _server.setHandler(contextHandler);
_server.start(); _server.start();
@ -387,51 +371,16 @@ public class RequestTest
multipart; multipart;
String responses = _connector.getResponse(request); String responses = _connector.getResponse(request);
//System.err.println(responses);
assertTrue(responses.startsWith("HTTP/1.1 200")); assertTrue(responses.startsWith("HTTP/1.1 200"));
}
@Test // We know the previous request has completed if another request can be processed.
public void testHttpMultiPart() throws Exception String cleanupRequest = "GET /foo/cleanup HTTP/1.1\r\n" +
{
final File testTmpDir = File.createTempFile("reqtest", null);
if (testTmpDir.exists())
testTmpDir.delete();
testTmpDir.mkdir();
testTmpDir.deleteOnExit();
assertTrue(testTmpDir.list().length == 0);
ContextHandler contextHandler = new ContextHandler();
contextHandler.setContextPath("/foo");
contextHandler.setResourceBase(".");
contextHandler.setHandler(new MultiPartRequestHandler(testTmpDir));
_server.stop();
_server.setHandler(contextHandler);
_server.start();
String multipart = " --AaB03x\r" +
"content-disposition: form-data; name=\"field1\"\r" +
"\r" +
"Joe Blow\r" +
"--AaB03x\r" +
"content-disposition: form-data; name=\"stuff\"; filename=\"foo.upload\"\r" +
"Content-Type: text/plain;charset=ISO-8859-1\r" +
"\r" +
"000000000000000000000000000000000000000000000000000\r" +
"--AaB03x--\r";
String request = "GET /foo/x.html HTTP/1.1\r\n" +
"Host: whatever\r\n" + "Host: whatever\r\n" +
"Content-Type: multipart/form-data; boundary=\"AaB03x\"\r\n" +
"Content-Length: " + multipart.getBytes().length + "\r\n" +
"Connection: close\r\n" + "Connection: close\r\n" +
"\r\n" + "\r\n";
multipart; String cleanupResponse = _connector.getResponse(cleanupRequest);
assertTrue(cleanupResponse.startsWith("HTTP/1.1 200"));
String responses = _connector.getResponse(request); assertThat(testTmpDir.list().length, is(0));
//System.err.println(responses);
assertThat(responses, Matchers.startsWith("HTTP/1.1 500"));
} }
@Test @Test
@ -449,22 +398,6 @@ public class RequestTest
contextHandler.setContextPath("/foo"); contextHandler.setContextPath("/foo");
contextHandler.setResourceBase("."); contextHandler.setResourceBase(".");
contextHandler.setHandler(new BadMultiPartRequestHandler(testTmpDir)); contextHandler.setHandler(new BadMultiPartRequestHandler(testTmpDir));
contextHandler.addEventListener(new MultiPartCleanerListener()
{
@Override
public void requestDestroyed(ServletRequestEvent sre)
{
MultiParts m = (MultiParts)sre.getServletRequest().getAttribute(Request.__MULTIPARTS);
assertNotNull(m);
ContextHandler.Context c = m.getContext();
assertNotNull(c);
assertTrue(c == sre.getServletContext());
super.requestDestroyed(sre);
String[] files = testTmpDir.list();
assertTrue(files.length == 0);
}
});
_server.stop(); _server.stop();
_server.setHandler(contextHandler); _server.setHandler(contextHandler);
_server.start(); _server.start();
@ -494,6 +427,15 @@ public class RequestTest
//System.err.println(responses); //System.err.println(responses);
assertTrue(responses.startsWith("HTTP/1.1 500")); assertTrue(responses.startsWith("HTTP/1.1 500"));
} }
// We know the previous request has completed if another request can be processed.
String cleanupRequest = "GET /foo/cleanup HTTP/1.1\r\n" +
"Host: whatever\r\n" +
"Connection: close\r\n" +
"\r\n";
String cleanupResponse = _connector.getResponse(cleanupRequest);
assertTrue(cleanupResponse.startsWith("HTTP/1.1 200"));
assertThat(testTmpDir.list().length, is(0));
} }
@Test @Test
@ -1818,20 +1760,27 @@ public class RequestTest
private class MultiPartRequestHandler extends AbstractHandler private class MultiPartRequestHandler extends AbstractHandler
{ {
RequestTester checker;
File tmpDir; File tmpDir;
public MultiPartRequestHandler(File tmpDir) public MultiPartRequestHandler(File tmpDir, RequestTester checker)
{ {
this.tmpDir = tmpDir; this.tmpDir = tmpDir;
this.checker = checker;
} }
@Override @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) throws IOException, ServletException
{ {
((Request)request).setHandled(true); ((Request)request).setHandled(true);
if ("/cleanup".equals(target))
{
response.setStatus(200);
return;
}
try try
{ {
MultipartConfigElement mpce = new MultipartConfigElement(tmpDir.getAbsolutePath(), -1, -1, 2); MultipartConfigElement mpce = new MultipartConfigElement(tmpDir.getAbsolutePath(), -1, -1, 2);
request.setAttribute(Request.__MULTIPART_CONFIG_ELEMENT, mpce); request.setAttribute(Request.__MULTIPART_CONFIG_ELEMENT, mpce);
@ -1850,6 +1799,9 @@ public class RequestTest
response.addHeader("Violation", v); response.addHeader("Violation", v);
} }
} }
if (checker != null && !checker.check(request, response))
response.sendError(500);
} }
catch (IllegalStateException e) catch (IllegalStateException e)
{ {
@ -1877,6 +1829,12 @@ public class RequestTest
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) throws IOException, ServletException
{ {
((Request)request).setHandled(true); ((Request)request).setHandled(true);
if ("/cleanup".equals(target))
{
response.setStatus(200);
return;
}
try try
{ {

View File

@ -48,13 +48,11 @@ import org.eclipse.jetty.http.HttpCookie;
import org.eclipse.jetty.http.HttpField; import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpFields; import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.HttpURI; import org.eclipse.jetty.http.HttpURI;
import org.eclipse.jetty.http.HttpVersion; import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.http.MetaData; import org.eclipse.jetty.http.MetaData;
import org.eclipse.jetty.io.AbstractEndPoint; import org.eclipse.jetty.io.AbstractEndPoint;
import org.eclipse.jetty.io.ByteArrayEndPoint; import org.eclipse.jetty.io.ByteArrayEndPoint;
import org.eclipse.jetty.io.RuntimeIOException;
import org.eclipse.jetty.server.handler.AbstractHandler; import org.eclipse.jetty.server.handler.AbstractHandler;
import org.eclipse.jetty.server.handler.ContextHandler; import org.eclipse.jetty.server.handler.ContextHandler;
import org.eclipse.jetty.server.handler.ErrorHandler; import org.eclipse.jetty.server.handler.ErrorHandler;
@ -702,7 +700,7 @@ public class ResponseTest
} }
@Test @Test
public void testWriteRuntimeIOException() throws Exception public void testWriteCheckError() throws Exception
{ {
Response response = getResponse(); Response response = getResponse();
@ -716,8 +714,8 @@ public class ResponseTest
writer.println("test"); writer.println("test");
assertTrue(writer.checkError()); assertTrue(writer.checkError());
RuntimeIOException e = assertThrows(RuntimeIOException.class, () -> writer.println("test")); writer.println("test"); // this should not cause an Exception
assertEquals(cause, e.getCause()); assertTrue(writer.checkError());
} }
@Test @Test

View File

@ -49,7 +49,6 @@ import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.security.IdentityService; import org.eclipse.jetty.security.IdentityService;
import org.eclipse.jetty.security.RunAsToken; import org.eclipse.jetty.security.RunAsToken;
import org.eclipse.jetty.server.MultiPartCleanerListener;
import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.UserIdentity; import org.eclipse.jetty.server.UserIdentity;
import org.eclipse.jetty.server.handler.ContextHandler; import org.eclipse.jetty.server.handler.ContextHandler;
@ -470,21 +469,16 @@ public class ServletHolder extends Holder<Servlet> implements UserIdentity.Scope
public Servlet getServlet() public Servlet getServlet()
throws ServletException throws ServletException
{ {
Servlet servlet = _servlet; synchronized (this)
if (servlet == null)
{ {
synchronized (this) if (_servlet == null && isRunning())
{ {
servlet = _servlet; if (getHeldClass() != null)
if (servlet == null && isRunning()) initServlet();
{
if (getHeldClass() != null)
initServlet();
servlet = _servlet;
}
} }
} }
return servlet;
return _servlet;
} }
/** /**
@ -592,8 +586,6 @@ public class ServletHolder extends Holder<Servlet> implements UserIdentity.Scope
else if (_forcedPath != null) else if (_forcedPath != null)
detectJspContainer(); detectJspContainer();
initMultiPart();
if (LOG.isDebugEnabled()) if (LOG.isDebugEnabled())
LOG.debug("Servlet.init {} for {}", _servlet, getName()); LOG.debug("Servlet.init {} for {}", _servlet, getName());
_servlet.init(_config); _servlet.init(_config);
@ -626,7 +618,7 @@ public class ServletHolder extends Holder<Servlet> implements UserIdentity.Scope
ContextHandler ch = ContextHandler.getContextHandler(getServletHandler().getServletContext()); ContextHandler ch = ContextHandler.getContextHandler(getServletHandler().getServletContext());
/* Set the webapp's classpath for Jasper */ /* Set the webapp's classpath for Jasper */
ch.setAttribute("org.apache.catalina.jspgetHeldClass()path", ch.getClassPath()); ch.setAttribute("org.apache.catalina.jsp_classpath", ch.getClassPath());
/* Set up other classpath attribute */ /* Set up other classpath attribute */
if ("?".equals(getInitParameter("classpath"))) if ("?".equals(getInitParameter("classpath")))
@ -652,29 +644,6 @@ public class ServletHolder extends Holder<Servlet> implements UserIdentity.Scope
scratch.mkdir(); scratch.mkdir();
} }
/**
* Register a ServletRequestListener that will ensure tmp multipart
* files are deleted when the request goes out of scope.
*
* @throws Exception if unable to init the multipart
*/
protected void initMultiPart() throws Exception
{
//if this servlet can handle multipart requests, ensure tmp files will be
//cleaned up correctly
if (((Registration)getRegistration()).getMultipartConfig() != null)
{
if (LOG.isDebugEnabled())
LOG.debug("multipart cleanup listener added for {}", this);
//Register a listener to delete tmp files that are created as a result of this
//servlet calling Request.getPart() or Request.getParts()
ContextHandler ch = ContextHandler.getContextHandler(getServletHandler().getServletContext());
if (!ch.getEventListeners().contains(MultiPartCleanerListener.INSTANCE))
ch.addEventListener(MultiPartCleanerListener.INSTANCE);
}
}
@Override @Override
public ContextHandler getContextHandler() public ContextHandler getContextHandler()
{ {

View File

@ -0,0 +1,132 @@
//
// ========================================================================
// 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.servlet;
import java.io.IOException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.Response;
import org.eclipse.jetty.client.api.Result;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.server.NetworkConnector;
import org.eclipse.jetty.server.Server;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class InitServletTest
{
public static class DemoServlet extends HttpServlet
{
private static final long INIT_SLEEP = 2000;
private final AtomicInteger initCount = new AtomicInteger();
@Override
public void init() throws ServletException
{
super.init();
try
{
// Make the initialization last a little while.
// Other requests must wait.
Thread.sleep(INIT_SLEEP);
}
catch (InterruptedException e)
{
throw new ServletException(e);
}
initCount.incrementAndGet();
}
@Override
public void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException
{
// Check that the init() method has been totally finished (by another request)
// before the servlet service() method is called.
if (initCount.get() != 1)
resp.sendError(500, "Servlet not initialized!");
}
}
private static class AsyncResponseListener implements Response.CompleteListener
{
private final AtomicInteger index = new AtomicInteger();
private final CountDownLatch resultsLatch;
private final int[] results;
public AsyncResponseListener(CountDownLatch resultsLatch, int[] results)
{
this.resultsLatch = resultsLatch;
this.results = results;
}
public void onComplete(Result result)
{
results[index.getAndIncrement()] = result.getResponse().getStatus();
resultsLatch.countDown();
}
}
@Test
public void testServletInitialization() throws Exception
{
Server server = new Server(0);
ServletContextHandler context = new ServletContextHandler(server, "/");
server.setHandler(context);
// Add a lazily instantiated servlet.
context.addServlet(new ServletHolder(DemoServlet.class), "/*");
HttpClient client = new HttpClient();
server.addBean(client);
server.start();
try
{
int port = ((NetworkConnector)server.getConnectors()[0]).getLocalPort();
// Expect 2 responses
CountDownLatch resultsLatch = new CountDownLatch(2);
int[] results = new int[2];
AsyncResponseListener l = new AsyncResponseListener(resultsLatch, results);
// Req1: should initialize servlet.
client.newRequest("http://localhost:" + port + "/r1").send(l);
// Need to give 1st request a head start before request2.
Thread.sleep(DemoServlet.INIT_SLEEP / 4);
// Req2: should see servlet fully initialized by request1.
client.newRequest("http://localhost:" + port + "/r2").send(l);
assertTrue(resultsLatch.await(DemoServlet.INIT_SLEEP * 2, TimeUnit.MILLISECONDS));
assertEquals(HttpStatus.OK_200, results[0]);
assertEquals(HttpStatus.OK_200, results[1]);
}
finally
{
server.stop();
}
}
}

View File

@ -216,6 +216,8 @@ public class QoSFilter implements Filter
{ {
if (accepted) if (accepted)
{ {
_passes.release();
for (int p = _queues.length - 1; p >= 0; --p) for (int p = _queues.length - 1; p >= 0; --p)
{ {
AsyncContext asyncContext = _queues[p].poll(); AsyncContext asyncContext = _queues[p].poll();
@ -225,13 +227,20 @@ public class QoSFilter implements Filter
Boolean suspended = (Boolean)candidate.getAttribute(_suspended); Boolean suspended = (Boolean)candidate.getAttribute(_suspended);
if (Boolean.TRUE.equals(suspended)) if (Boolean.TRUE.equals(suspended))
{ {
candidate.setAttribute(_resumed, Boolean.TRUE); try
asyncContext.dispatch(); {
break; candidate.setAttribute(_resumed, Boolean.TRUE);
asyncContext.dispatch();
break;
}
catch (IllegalStateException x)
{
LOG.warn(x);
continue;
}
} }
} }
} }
_passes.release();
} }
} }
} }
@ -368,7 +377,8 @@ public class QoSFilter implements Filter
// redispatched again at the end of the filtering. // redispatched again at the end of the filtering.
AsyncContext asyncContext = event.getAsyncContext(); AsyncContext asyncContext = event.getAsyncContext();
_queues[priority].remove(asyncContext); _queues[priority].remove(asyncContext);
asyncContext.dispatch(); ((HttpServletResponse)event.getSuppliedResponse()).sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE);
asyncContext.complete();
} }
@Override @Override

View File

@ -706,7 +706,7 @@ public class StartArgs
} }
else else
{ {
cmd.addRawArg(x); cmd.addRawArg(getProperties().expand(x));
} }
} }

View File

@ -31,6 +31,7 @@ import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.startsWith; import static org.hamcrest.Matchers.startsWith;
@ -169,6 +170,37 @@ public class MainTest
ConfigurationAssert.assertConfiguration(baseHome, args, "assert-home-with-jvm.txt"); ConfigurationAssert.assertConfiguration(baseHome, args, "assert-home-with-jvm.txt");
} }
@Test
public void testJvmArgExpansion() throws Exception
{
List<String> cmdLineArgs = new ArrayList<>();
Path homePath = MavenTestingUtils.getTestResourceDir("dist-home").toPath().toRealPath();
cmdLineArgs.add("jetty.home=" + homePath.toString());
cmdLineArgs.add("user.dir=" + homePath.toString());
// JVM args
cmdLineArgs.add("--exec");
cmdLineArgs.add("-Xms1g");
cmdLineArgs.add("-Xmx4g");
cmdLineArgs.add("-Xloggc:${jetty.base}/logs/gc-${java.version}.log");
Main main = new Main();
StartArgs args = main.processCommandLine(cmdLineArgs.toArray(new String[cmdLineArgs.size()]));
BaseHome baseHome = main.getBaseHome();
assertThat("jetty.home", baseHome.getHome(), is(homePath.toString()));
assertThat("jetty.base", baseHome.getBase(), is(homePath.toString()));
CommandLineBuilder commandLineBuilder = args.getMainArgs(true);
String commandLine = commandLineBuilder.toString("\n");
String expectedExpansion = String.format("-Xloggc:%s/logs/gc-%s.log",
baseHome.getBase(), System.getProperty("java.version")
);
assertThat(commandLine, containsString(expectedExpansion));
}
@Test @Test
public void testWithModules() throws Exception public void testWithModules() throws Exception
{ {

View File

@ -18,7 +18,7 @@ resources/
[ini-template] [ini-template]
## Logging directory (relative to $jetty.base) ## Logging directory (relative to $jetty.base)
# jetty.console-capture.dir=logs # jetty.console-capture.dir=./logs
## Whether to append to existing file ## Whether to append to existing file
# jetty.console-capture.append=true # jetty.console-capture.append=true

View File

@ -945,7 +945,8 @@ public class UrlEncoded extends MultiMap<String> implements Cloneable
} }
else if (b >= 'a' && b <= 'z' || else if (b >= 'a' && b <= 'z' ||
b >= 'A' && b <= 'Z' || b >= 'A' && b <= 'Z' ||
b >= '0' && b <= '9') b >= '0' && b <= '9' ||
b == '-' || b == '.' || b == '_' || b == '~')
{ {
encoded[n++] = b; encoded[n++] = b;
} }

View File

@ -1140,8 +1140,6 @@ public abstract class SslContextFactory extends AbstractLifeCycle implements Dum
managers[idx] = new AliasedX509ExtendedKeyManager((X509ExtendedKeyManager)managers[idx], alias); managers[idx] = new AliasedX509ExtendedKeyManager((X509ExtendedKeyManager)managers[idx], alias);
} }
} }
} }
} }

View File

@ -18,16 +18,21 @@
package org.eclipse.jetty.util.ssl; package org.eclipse.jetty.util.ssl;
import java.nio.file.Path;
import java.security.cert.X509Certificate; import java.security.cert.X509Certificate;
import javax.net.ssl.KeyManager; import javax.net.ssl.KeyManager;
import javax.net.ssl.X509ExtendedKeyManager; import javax.net.ssl.X509ExtendedKeyManager;
import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
import org.eclipse.jetty.util.resource.PathResource;
import org.eclipse.jetty.util.resource.Resource; import org.eclipse.jetty.util.resource.Resource;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.notNullValue;
import static org.junit.jupiter.api.Assertions.assertThrows;
public class X509Test public class X509Test
{ {
@ -128,57 +133,47 @@ public class X509Test
assertThat("Normal X509", X509.isCertSign(bogusX509), is(false)); assertThat("Normal X509", X509.isCertSign(bogusX509), is(false));
} }
private X509ExtendedKeyManager getX509ExtendedKeyManager(SslContextFactory sslContextFactory) throws Exception @Test
public void testServerClass_WithSni() throws Exception
{ {
Resource keystoreResource = Resource.newSystemResource("keystore"); SslContextFactory serverSsl = new SslContextFactory.Server();
Resource truststoreResource = Resource.newSystemResource("keystore"); Path keystorePath = MavenTestingUtils.getTestResourcePathFile("keystore_sni.p12");
sslContextFactory.setKeyStoreResource(keystoreResource); serverSsl.setKeyStoreResource(new PathResource(keystorePath));
sslContextFactory.setTrustStoreResource(truststoreResource); serverSsl.setKeyStorePassword("OBF:1vny1zlo1x8e1vnw1vn61x8g1zlu1vn4");
sslContextFactory.setKeyStorePassword("storepwd"); serverSsl.setKeyManagerPassword("OBF:1u2u1wml1z7s1z7a1wnl1u2g");
sslContextFactory.setKeyManagerPassword("keypwd"); serverSsl.start();
sslContextFactory.setTrustStorePassword("storepwd");
sslContextFactory.start();
KeyManager[] keyManagers = sslContextFactory.getKeyManagers(sslContextFactory.getKeyStore());
X509ExtendedKeyManager x509ExtendedKeyManager = null;
for (KeyManager keyManager : keyManagers)
{
if (keyManager instanceof X509ExtendedKeyManager)
{
x509ExtendedKeyManager = (X509ExtendedKeyManager)keyManager;
break;
}
}
assertThat("Found X509ExtendedKeyManager", x509ExtendedKeyManager, is(notNullValue()));
return x509ExtendedKeyManager;
} }
@Test @Test
public void testSniX509ExtendedKeyManager_ServerClass() throws Exception public void testClientClass_WithSni() throws Exception
{ {
SslContextFactory.Server serverSsl = new SslContextFactory.Server(); SslContextFactory clientSsl = new SslContextFactory.Client();
Path keystorePath = MavenTestingUtils.getTestResourcePathFile("keystore_sni.p12");
clientSsl.setKeyStoreResource(new PathResource(keystorePath));
clientSsl.setKeyStorePassword("OBF:1vny1zlo1x8e1vnw1vn61x8g1zlu1vn4");
clientSsl.setKeyManagerPassword("OBF:1u2u1wml1z7s1z7a1wnl1u2g");
clientSsl.start();
}
@Test
public void testServerClass_WithoutSni() throws Exception
{
SslContextFactory serverSsl = new SslContextFactory.Server();
Resource keystoreResource = Resource.newSystemResource("keystore"); Resource keystoreResource = Resource.newSystemResource("keystore");
Resource truststoreResource = Resource.newSystemResource("keystore");
serverSsl.setKeyStoreResource(keystoreResource); serverSsl.setKeyStoreResource(keystoreResource);
serverSsl.setTrustStoreResource(truststoreResource);
serverSsl.setKeyStorePassword("storepwd"); serverSsl.setKeyStorePassword("storepwd");
serverSsl.setKeyManagerPassword("keypwd"); serverSsl.setKeyManagerPassword("keypwd");
serverSsl.setTrustStorePassword("storepwd");
serverSsl.start(); serverSsl.start();
}
KeyManager[] keyManagers = serverSsl.getKeyManagers(serverSsl.getKeyStore()); @Test
X509ExtendedKeyManager x509ExtendedKeyManager = null; public void testClientClass_WithoutSni() throws Exception
{
for (KeyManager keyManager : keyManagers) SslContextFactory clientSsl = new SslContextFactory.Client();
{ Resource keystoreResource = Resource.newSystemResource("keystore");
if (keyManager instanceof X509ExtendedKeyManager) clientSsl.setKeyStoreResource(keystoreResource);
{ clientSsl.setKeyStorePassword("storepwd");
x509ExtendedKeyManager = (X509ExtendedKeyManager)keyManager; clientSsl.setKeyManagerPassword("keypwd");
break; clientSsl.start();
}
}
assertThat("Found X509ExtendedKeyManager", x509ExtendedKeyManager, is(notNullValue()));
serverSsl.newSniX509ExtendedKeyManager(x509ExtendedKeyManager);
} }
} }

Binary file not shown.

View File

@ -194,107 +194,25 @@
<!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> <!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -->
<!-- The JSP page compiler and execution servlet, which is the mechanism --> <!-- The JSP page compiler and execution servlet, which is the mechanism -->
<!-- used by the jsp container to support JSP pages. Traditionally, --> <!-- used by the jsp container to support JSP pages. Traditionally, -->
<!-- this servlet is mapped to URL pattern "*.jsp". This servlet --> <!-- this servlet is mapped to URL pattern "*.jsp". -->
<!-- supports the following initialization parameters (default values --> <!-- See http://https://www.eclipse.org/jetty/documentation/ -->
<!-- are in square brackets): --> <!-- for applicable configuration params. -->
<!-- -->
<!-- checkInterval If development is false and reloading is true, -->
<!-- background compiles are enabled. checkInterval -->
<!-- is the time in seconds between checks to see -->
<!-- if a JSP page needs to be recompiled. [300] -->
<!-- -->
<!-- compiler Which compiler Ant should use to compile JSP -->
<!-- pages. See the Ant documentation for more -->
<!-- information. [javac] -->
<!-- -->
<!-- classdebuginfo Should the class file be compiled with -->
<!-- debugging information? [true] -->
<!-- -->
<!-- classpath What class path should I use while compiling -->
<!-- generated servlets? [Created dynamically -->
<!-- based on the current web application] -->
<!-- Set to ? to make the container explicitly set -->
<!-- this parameter. -->
<!-- -->
<!-- development Is Jasper used in development mode (will check -->
<!-- for JSP modification on every access)? [true] -->
<!-- -->
<!-- enablePooling Determines whether tag handler pooling is -->
<!-- enabled [true] -->
<!-- -->
<!-- fork Tell Ant to fork compiles of JSP pages so that -->
<!-- a separate JVM is used for JSP page compiles -->
<!-- from the one Tomcat is running in. [true] -->
<!-- -->
<!-- ieClassId The class-id value to be sent to Internet -->
<!-- Explorer when using <jsp:plugin> tags. -->
<!-- [clsid:8AD9C840-044E-11D1-B3E9-00805F499D93] -->
<!-- -->
<!-- javaEncoding Java file encoding to use for generating java -->
<!-- source files. [UTF-8] -->
<!-- -->
<!-- keepgenerated Should we keep the generated Java source code -->
<!-- for each page instead of deleting it? [true] -->
<!-- -->
<!-- logVerbosityLevel The level of detailed messages to be produced -->
<!-- by this servlet. Increasing levels cause the -->
<!-- generation of more messages. Valid values are -->
<!-- FATAL, ERROR, WARNING, INFORMATION, and DEBUG. -->
<!-- [WARNING] -->
<!-- -->
<!-- mappedfile Should we generate static content with one -->
<!-- print statement per input line, to ease -->
<!-- debugging? [false] -->
<!-- -->
<!-- -->
<!-- reloading Should Jasper check for modified JSPs? [true] -->
<!-- -->
<!-- suppressSmap Should the generation of SMAP info for JSR45 -->
<!-- debugging be suppressed? [false] -->
<!-- -->
<!-- dumpSmap Should the SMAP info for JSR45 debugging be -->
<!-- dumped to a file? [false] -->
<!-- False if suppressSmap is true -->
<!-- -->
<!-- scratchdir What scratch directory should we use when -->
<!-- compiling JSP pages? [default work directory -->
<!-- for the current web application] -->
<!-- -->
<!-- tagpoolMaxSize The maximum tag handler pool size [5] -->
<!-- -->
<!-- xpoweredBy Determines whether X-Powered-By response -->
<!-- header is added by generated servlet [false] -->
<!-- -->
<!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> <!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -->
<servlet id="jsp"> <servlet id="jsp">
<servlet-name>jsp</servlet-name> <servlet-name>jsp</servlet-name>
<servlet-class>org.eclipse.jetty.jsp.JettyJspServlet</servlet-class> <servlet-class>org.eclipse.jetty.jsp.JettyJspServlet</servlet-class>
<init-param>
<param-name>logVerbosityLevel</param-name>
<param-value>DEBUG</param-value>
</init-param>
<init-param>
<param-name>fork</param-name>
<param-value>false</param-value>
</init-param>
<init-param> <init-param>
<param-name>xpoweredBy</param-name> <param-name>xpoweredBy</param-name>
<param-value>false</param-value> <param-value>false</param-value>
</init-param> </init-param>
<init-param> <init-param>
<param-name>compilerTargetVM</param-name> <param-name>compilerTargetVM</param-name>
<param-value>1.7</param-value> <param-value>1.8</param-value>
</init-param> </init-param>
<init-param> <init-param>
<param-name>compilerSourceVM</param-name> <param-name>compilerSourceVM</param-name>
<param-value>1.7</param-value> <param-value>1.8</param-value>
</init-param> </init-param>
<!--
<init-param>
<param-name>classpath</param-name>
<param-value>?</param-value>
</init-param>
-->
<load-on-startup>0</load-on-startup> <load-on-startup>0</load-on-startup>
</servlet> </servlet>

View File

@ -21,7 +21,7 @@ package org.eclipse.jetty.websocket.javax.client;
import java.net.URI; import java.net.URI;
import org.eclipse.jetty.client.HttpResponse; import org.eclipse.jetty.client.HttpResponse;
import org.eclipse.jetty.client.http.HttpConnectionOverHTTP; import org.eclipse.jetty.io.EndPoint;
import org.eclipse.jetty.websocket.core.FrameHandler; import org.eclipse.jetty.websocket.core.FrameHandler;
import org.eclipse.jetty.websocket.core.client.ClientUpgradeRequest; import org.eclipse.jetty.websocket.core.client.ClientUpgradeRequest;
import org.eclipse.jetty.websocket.core.client.WebSocketCoreClient; import org.eclipse.jetty.websocket.core.client.WebSocketCoreClient;
@ -43,11 +43,11 @@ public class JavaxClientUpgradeRequest extends ClientUpgradeRequest
} }
@Override @Override
public void upgrade(HttpResponse response, HttpConnectionOverHTTP httpConnection) public void upgrade(HttpResponse response, EndPoint endPoint)
{ {
frameHandler.setUpgradeRequest(new DelegatedJavaxClientUpgradeRequest(this)); frameHandler.setUpgradeRequest(new DelegatedJavaxClientUpgradeRequest(this));
frameHandler.setUpgradeResponse(new DelegatedJavaxClientUpgradeResponse(response)); frameHandler.setUpgradeResponse(new DelegatedJavaxClientUpgradeResponse(response));
super.upgrade(response, httpConnection); super.upgrade(response, endPoint);
} }
@Override @Override

View File

@ -38,12 +38,9 @@ import javax.websocket.RemoteEndpoint.Basic;
import javax.websocket.Session; import javax.websocket.Session;
import javax.websocket.WebSocketContainer; import javax.websocket.WebSocketContainer;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.SharedBlockingCallback; import org.eclipse.jetty.util.SharedBlockingCallback;
import org.eclipse.jetty.util.component.AbstractLifeCycle;
import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger; import org.eclipse.jetty.util.log.Logger;
import org.eclipse.jetty.websocket.core.CloseStatus;
import org.eclipse.jetty.websocket.core.ExtensionConfig; import org.eclipse.jetty.websocket.core.ExtensionConfig;
import org.eclipse.jetty.websocket.core.FrameHandler; import org.eclipse.jetty.websocket.core.FrameHandler;
import org.eclipse.jetty.websocket.javax.common.decoders.AvailableDecoders; import org.eclipse.jetty.websocket.javax.common.decoders.AvailableDecoders;
@ -53,7 +50,7 @@ import org.eclipse.jetty.websocket.javax.common.util.ReflectUtils;
/** /**
* Client Session for the JSR. * Client Session for the JSR.
*/ */
public class JavaxWebSocketSession extends AbstractLifeCycle implements javax.websocket.Session public class JavaxWebSocketSession implements javax.websocket.Session
{ {
private static final Logger LOG = Log.getLogger(JavaxWebSocketSession.class); private static final Logger LOG = Log.getLogger(JavaxWebSocketSession.class);
@ -547,25 +544,6 @@ public class JavaxWebSocketSession extends AbstractLifeCycle implements javax.we
return coreSession.isSecure(); return coreSession.isSecure();
} }
@Override
protected void doStop()
{
coreSession.close(CloseStatus.SHUTDOWN, "Container being shut down", new Callback()
{
@Override
public void succeeded()
{
coreSession.abort();
}
@Override
public void failed(Throwable x)
{
coreSession.abort();
}
});
}
@Override @Override
public synchronized void removeMessageHandler(MessageHandler handler) public synchronized void removeMessageHandler(MessageHandler handler)
{ {

View File

@ -21,10 +21,10 @@ package org.eclipse.jetty.websocket.javax.common;
import java.util.Collections; import java.util.Collections;
import java.util.Set; import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet; import java.util.concurrent.CopyOnWriteArraySet;
import javax.websocket.CloseReason;
import javax.websocket.Session; import javax.websocket.Session;
import org.eclipse.jetty.util.component.AbstractLifeCycle; import org.eclipse.jetty.util.component.AbstractLifeCycle;
import org.eclipse.jetty.util.component.LifeCycle;
public class SessionTracker extends AbstractLifeCycle implements JavaxWebSocketSessionListener public class SessionTracker extends AbstractLifeCycle implements JavaxWebSocketSessionListener
{ {
@ -50,10 +50,12 @@ public class SessionTracker extends AbstractLifeCycle implements JavaxWebSocketS
@Override @Override
protected void doStop() throws Exception protected void doStop() throws Exception
{ {
for (JavaxWebSocketSession session : sessions) for (Session session : sessions)
{ {
LifeCycle.stop(session); // GOING_AWAY is abnormal close status so it will hard close connection after sent.
session.close(new CloseReason(CloseReason.CloseCodes.GOING_AWAY, "Container being shut down"));
} }
super.doStop(); super.doStop();
} }
} }

View File

@ -117,6 +117,7 @@ public class MessageOutputStream extends OutputStream
frame.setPayload(buffer); frame.setPayload(buffer);
frame.setFin(fin); frame.setFin(fin);
int initialBufferSize = buffer.remaining();
try (SharedBlockingCallback.Blocker b = blocker.acquire()) try (SharedBlockingCallback.Blocker b = blocker.acquire())
{ {
coreSession.sendFrame(frame, b, false); coreSession.sendFrame(frame, b, false);
@ -127,8 +128,8 @@ public class MessageOutputStream extends OutputStream
// Any flush after the first will be a CONTINUATION frame. // Any flush after the first will be a CONTINUATION frame.
frame = new Frame(OpCode.CONTINUATION); frame = new Frame(OpCode.CONTINUATION);
// Buffer has been sent, buffer should have been consumed // Buffer has been sent, but buffer should not have been consumed.
assert buffer.remaining() == 0; assert buffer.remaining() == initialBufferSize;
BufferUtil.clearToFill(buffer); BufferUtil.clearToFill(buffer);
} }

View File

@ -41,7 +41,6 @@ public abstract class AbstractSessionTest
JavaxWebSocketFrameHandler frameHandler = container.newFrameHandler(websocketPojo, upgradeRequest); JavaxWebSocketFrameHandler frameHandler = container.newFrameHandler(websocketPojo, upgradeRequest);
FrameHandler.CoreSession coreSession = new FrameHandler.CoreSession.Empty(); FrameHandler.CoreSession coreSession = new FrameHandler.CoreSession.Empty();
session = new JavaxWebSocketSession(container, coreSession, frameHandler, null); session = new JavaxWebSocketSession(container, coreSession, frameHandler, null);
container.addManaged(session);
} }
@AfterAll @AfterAll

View File

@ -20,6 +20,7 @@ package org.eclipse.jetty.websocket.javax.tests;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.BlockingQueue; import java.util.concurrent.BlockingQueue;
@ -53,7 +54,7 @@ public class LocalFuzzer extends Fuzzer.Adapter implements Fuzzer, AutoCloseable
public LocalFuzzer(Provider provider, CharSequence requestPath) throws Exception public LocalFuzzer(Provider provider, CharSequence requestPath) throws Exception
{ {
this(provider, requestPath, UpgradeUtils.newDefaultUpgradeRequestHeaders()); this(provider, requestPath, new HashMap<>());
} }
public LocalFuzzer(Provider provider, CharSequence requestPath, Map<String, String> headers) throws Exception public LocalFuzzer(Provider provider, CharSequence requestPath, Map<String, String> headers) throws Exception

View File

@ -91,12 +91,11 @@ public class NetworkFuzzer extends Fuzzer.Adapter implements Fuzzer, AutoCloseab
public ByteBuffer asNetworkBuffer(List<Frame> frames) public ByteBuffer asNetworkBuffer(List<Frame> frames)
{ {
int bufferLength = frames.stream().mapToInt((f) -> f.getPayloadLength() + Generator.MAX_HEADER_LENGTH).sum(); int bufferLength = frames.stream().mapToInt((f) -> f.getPayloadLength() + Generator.MAX_HEADER_LENGTH).sum();
ByteBuffer buffer = ByteBuffer.allocate(bufferLength); ByteBuffer buffer = BufferUtil.allocate(bufferLength);
for (Frame f : frames) for (Frame f : frames)
{ {
generator.generate(buffer, f); generator.generate(buffer, f);
} }
BufferUtil.flipToFlush(buffer, 0);
return buffer; return buffer;
} }
@ -208,9 +207,9 @@ public class NetworkFuzzer extends Fuzzer.Adapter implements Fuzzer, AutoCloseab
} }
@Override @Override
protected void customize(EndPoint endp) protected void customize(EndPoint endPoint)
{ {
frameCapture.setEndPoint(endp); frameCapture.setEndPoint(endPoint);
futureCapture.complete(frameCapture); futureCapture.complete(frameCapture);
} }

View File

@ -23,7 +23,6 @@ import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import org.eclipse.jetty.io.MappedByteBufferPool;
import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.websocket.core.Behavior; import org.eclipse.jetty.websocket.core.Behavior;
import org.eclipse.jetty.websocket.core.Frame; import org.eclipse.jetty.websocket.core.Frame;
@ -40,7 +39,6 @@ public class UnitGenerator extends Generator
public UnitGenerator(Behavior behavior) public UnitGenerator(Behavior behavior)
{ {
super(new MappedByteBufferPool());
applyMask = (behavior == Behavior.CLIENT); applyMask = (behavior == Behavior.CLIENT);
} }

View File

@ -19,9 +19,6 @@
package org.eclipse.jetty.websocket.javax.tests; package org.eclipse.jetty.websocket.javax.tests;
import java.util.Map; import java.util.Map;
import java.util.TreeMap;
import org.eclipse.jetty.http.HttpHeader;
public class UpgradeUtils public class UpgradeUtils
{ {
@ -31,32 +28,9 @@ public class UpgradeUtils
upgradeRequest.append("GET "); upgradeRequest.append("GET ");
upgradeRequest.append(requestPath == null ? "/" : requestPath); upgradeRequest.append(requestPath == null ? "/" : requestPath);
upgradeRequest.append(" HTTP/1.1\r\n"); upgradeRequest.append(" HTTP/1.1\r\n");
headers.entrySet().stream().forEach(e -> headers.entrySet().forEach(e ->
upgradeRequest.append(e.getKey()).append(": ").append(e.getValue()).append("\r\n")); upgradeRequest.append(e.getKey()).append(": ").append(e.getValue()).append("\r\n"));
upgradeRequest.append("\r\n"); upgradeRequest.append("\r\n");
return upgradeRequest.toString(); return upgradeRequest.toString();
} }
public static String generateUpgradeRequest()
{
return generateUpgradeRequest("/", newDefaultUpgradeRequestHeaders());
}
public static String generateUpgradeRequest(CharSequence requestPath)
{
return generateUpgradeRequest(requestPath, newDefaultUpgradeRequestHeaders());
}
public static Map<String, String> newDefaultUpgradeRequestHeaders()
{
Map<String, String> headers = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
headers.put("Host", "local");
headers.put("Connection", "Upgrade");
headers.put("Upgrade", "WebSocket");
headers.put(HttpHeader.SEC_WEBSOCKET_KEY.asString(), "dGhlIHNhbXBsZSBub25jZQ==");
headers.put(HttpHeader.ORIGIN.asString(), "ws://local/");
// headers.put(WSConstants.SEC_WEBSOCKET_PROTOCOL, "echo");
headers.put(HttpHeader.SEC_WEBSOCKET_VERSION.asString(), "13");
return headers;
}
} }

View File

@ -44,7 +44,6 @@ public abstract class AbstractClientSessionTest
JavaxWebSocketFrameHandler frameHandler = container.newFrameHandler(websocketPojo, upgradeRequest); JavaxWebSocketFrameHandler frameHandler = container.newFrameHandler(websocketPojo, upgradeRequest);
FrameHandler.CoreSession coreSession = new FrameHandler.CoreSession.Empty(); FrameHandler.CoreSession coreSession = new FrameHandler.CoreSession.Empty();
session = new JavaxWebSocketSession(container, coreSession, frameHandler, null); session = new JavaxWebSocketSession(container, coreSession, frameHandler, null);
container.addManaged(session);
} }
@AfterAll @AfterAll

View File

@ -82,13 +82,11 @@ public class SessionAddMessageHandlerTest
// Session // Session
session = frameHandler.getSession(); session = frameHandler.getSession();
session.start();
} }
@AfterEach @AfterEach
public void stopSession() throws Exception public void stopSession() throws Exception
{ {
session.stop();
container.stop(); container.stop();
} }

View File

@ -20,6 +20,7 @@ package org.eclipse.jetty.websocket.javax.tests.server;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import javax.websocket.server.ServerEndpointConfig; import javax.websocket.server.ServerEndpointConfig;
@ -30,7 +31,6 @@ import org.eclipse.jetty.websocket.core.Frame;
import org.eclipse.jetty.websocket.core.OpCode; import org.eclipse.jetty.websocket.core.OpCode;
import org.eclipse.jetty.websocket.javax.tests.Fuzzer; import org.eclipse.jetty.websocket.javax.tests.Fuzzer;
import org.eclipse.jetty.websocket.javax.tests.LocalServer; import org.eclipse.jetty.websocket.javax.tests.LocalServer;
import org.eclipse.jetty.websocket.javax.tests.UpgradeUtils;
import org.eclipse.jetty.websocket.javax.tests.coders.DateDecoder; import org.eclipse.jetty.websocket.javax.tests.coders.DateDecoder;
import org.eclipse.jetty.websocket.javax.tests.coders.TimeEncoder; import org.eclipse.jetty.websocket.javax.tests.coders.TimeEncoder;
import org.eclipse.jetty.websocket.javax.tests.server.configs.EchoSocketConfigurator; import org.eclipse.jetty.websocket.javax.tests.server.configs.EchoSocketConfigurator;
@ -67,8 +67,8 @@ public class AnnotatedServerEndpointTest
private void assertResponse(String message, String expectedText) throws Exception private void assertResponse(String message, String expectedText) throws Exception
{ {
Map<String, String> upgradeRequest = UpgradeUtils.newDefaultUpgradeRequestHeaders(); Map<String, String> headers = new HashMap<>();
upgradeRequest.put(HttpHeader.SEC_WEBSOCKET_SUBPROTOCOL.asString(), subprotocol); headers.put(HttpHeader.SEC_WEBSOCKET_SUBPROTOCOL.asString(), subprotocol);
List<Frame> send = new ArrayList<>(); List<Frame> send = new ArrayList<>();
send.add(new Frame(OpCode.TEXT).setPayload(message)); send.add(new Frame(OpCode.TEXT).setPayload(message));
@ -78,7 +78,7 @@ public class AnnotatedServerEndpointTest
expect.add(new Frame(OpCode.TEXT).setPayload(expectedText)); expect.add(new Frame(OpCode.TEXT).setPayload(expectedText));
expect.add(CloseStatus.toFrame(CloseStatus.NORMAL)); expect.add(CloseStatus.toFrame(CloseStatus.NORMAL));
try (Fuzzer session = server.newNetworkFuzzer(path, upgradeRequest)) try (Fuzzer session = server.newNetworkFuzzer(path, headers))
{ {
session.sendFrames(send); session.sendFrames(send);
session.expect(expect); session.expect(expect);

View File

@ -12,4 +12,4 @@ org.eclipse.jetty.util.log.class=org.eclipse.jetty.util.log.StdErrLog
# org.eclipse.jetty.websocket.LEVEL=INFO # org.eclipse.jetty.websocket.LEVEL=INFO
# org.eclipse.jetty.websocket.tests.LEVEL=DEBUG # org.eclipse.jetty.websocket.tests.LEVEL=DEBUG
# org.eclipse.jetty.websocket.tests.client.LEVEL=DEBUG # org.eclipse.jetty.websocket.tests.client.LEVEL=DEBUG
# org.eclipse.jetty.websocket.tests.server.LEVEL=DEBUG # org.eclipse.jetty.websocket.tests.server.LEVEL=DEBUG

View File

@ -255,34 +255,6 @@ public interface UpgradeRequest
*/ */
void setHeaders(Map<String, List<String>> headers); void setHeaders(Map<String, List<String>> headers);
/**
* Set the HTTP Version to use.
* <p>
* As of <a href="http://tools.ietf.org/html/rfc6455">RFC6455 (December 2011)</a> this should always be
* {@code HTTP/1.1}
*
* @param httpVersion the HTTP version to use.
*/
void setHttpVersion(String httpVersion);
/**
* Set the HTTP method to use.
* <p>
* As of <a href="http://tools.ietf.org/html/rfc6455">RFC6455 (December 2011)</a> this is always {@code GET}
*
* @param method the HTTP method to use.
*/
void setMethod(String method);
/**
* Set the Request URI to use for this request.
* <p>
* Must be an absolute URI with scheme {@code 'ws'} or {@code 'wss'}
*
* @param uri the Request URI
*/
void setRequestURI(URI uri);
/** /**
* Set the Session associated with this request. * Set the Session associated with this request.
* <p> * <p>

View File

@ -26,9 +26,11 @@ import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects;
import java.util.TreeMap; import java.util.TreeMap;
import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpScheme;
import org.eclipse.jetty.websocket.api.UpgradeRequest; import org.eclipse.jetty.websocket.api.UpgradeRequest;
import org.eclipse.jetty.websocket.api.extensions.ExtensionConfig; import org.eclipse.jetty.websocket.api.extensions.ExtensionConfig;
@ -46,7 +48,6 @@ public final class ClientUpgradeRequest implements UpgradeRequest
private String httpVersion; private String httpVersion;
private String method; private String method;
private String host; private String host;
private boolean secure;
public ClientUpgradeRequest() public ClientUpgradeRequest()
{ {
@ -57,12 +58,9 @@ public final class ClientUpgradeRequest implements UpgradeRequest
{ {
this.requestURI = uri; this.requestURI = uri;
String scheme = uri.getScheme(); String scheme = uri.getScheme();
if (!"ws".equalsIgnoreCase(scheme) && !"wss".equalsIgnoreCase(scheme)) if (!HttpScheme.WS.is(scheme) || !HttpScheme.WSS.is(scheme))
{
throw new IllegalArgumentException("URI scheme must be 'ws' or 'wss'"); throw new IllegalArgumentException("URI scheme must be 'ws' or 'wss'");
}
this.host = this.requestURI.getHost(); this.host = this.requestURI.getHost();
this.parameters.clear();
} }
@Override @Override
@ -193,11 +191,7 @@ public final class ClientUpgradeRequest implements UpgradeRequest
public String getProtocolVersion() public String getProtocolVersion()
{ {
String version = getHeader("Sec-WebSocket-Version"); String version = getHeader("Sec-WebSocket-Version");
if (version == null) return Objects.requireNonNullElse(version, "13");
{
return "13"; // Default
}
return version;
} }
@Override @Override
@ -288,8 +282,7 @@ public final class ClientUpgradeRequest implements UpgradeRequest
@Override @Override
public void setHeaders(Map<String, List<String>> headers) public void setHeaders(Map<String, List<String>> headers)
{ {
headers.clear(); this.headers.clear();
for (Map.Entry<String, List<String>> entry : headers.entrySet()) for (Map.Entry<String, List<String>> entry : headers.entrySet())
{ {
String name = entry.getKey(); String name = entry.getKey();
@ -298,24 +291,6 @@ public final class ClientUpgradeRequest implements UpgradeRequest
} }
} }
@Override
public void setHttpVersion(String httpVersion)
{
this.httpVersion = httpVersion;
}
@Override
public void setMethod(String method)
{
this.method = method;
}
@Override
public void setRequestURI(URI uri)
{
this.requestURI = uri;
}
@Override @Override
public void setSession(Object session) public void setSession(Object session)
{ {

View File

@ -231,24 +231,6 @@ public class DelegatedJettyClientUpgradeRequest implements UpgradeRequest
// TODO // TODO
} }
@Override
public void setHttpVersion(String httpVersion)
{
// TODO
}
@Override
public void setMethod(String method)
{
}
@Override
public void setRequestURI(URI uri)
{
// TODO
}
@Override @Override
public void setSession(Object session) public void setSession(Object session)
{ {

View File

@ -24,7 +24,6 @@ import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.eclipse.jetty.client.HttpResponse; import org.eclipse.jetty.client.HttpResponse;
import org.eclipse.jetty.client.http.HttpConnectionOverHTTP;
import org.eclipse.jetty.http.HttpFields; import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpVersion; import org.eclipse.jetty.http.HttpVersion;
@ -89,18 +88,18 @@ public class JettyClientUpgradeRequest extends ClientUpgradeRequest
} }
@Override @Override
protected void customize(EndPoint endp) protected void customize(EndPoint endPoint)
{ {
super.customize(endp); super.customize(endPoint);
handshakeRequest.configure(endp); handshakeRequest.configure(endPoint);
} }
@Override @Override
public void upgrade(HttpResponse response, HttpConnectionOverHTTP httpConnection) public void upgrade(HttpResponse response, EndPoint endPoint)
{ {
frameHandler.setUpgradeRequest(new DelegatedJettyClientUpgradeRequest(this)); frameHandler.setUpgradeRequest(new DelegatedJettyClientUpgradeRequest(this));
frameHandler.setUpgradeResponse(new DelegatedJettyClientUpgradeResponse(response)); frameHandler.setUpgradeResponse(new DelegatedJettyClientUpgradeResponse(response));
super.upgrade(response, httpConnection); super.upgrade(response, endPoint);
} }
@Override @Override

View File

@ -23,8 +23,8 @@ import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CopyOnWriteArrayList;
import org.eclipse.jetty.util.component.AbstractLifeCycle; import org.eclipse.jetty.util.component.AbstractLifeCycle;
import org.eclipse.jetty.util.component.LifeCycle;
import org.eclipse.jetty.websocket.api.Session; import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.StatusCode;
import org.eclipse.jetty.websocket.api.WebSocketSessionListener; import org.eclipse.jetty.websocket.api.WebSocketSessionListener;
public class SessionTracker extends AbstractLifeCycle implements WebSocketSessionListener public class SessionTracker extends AbstractLifeCycle implements WebSocketSessionListener
@ -39,7 +39,6 @@ public class SessionTracker extends AbstractLifeCycle implements WebSocketSessio
@Override @Override
public void onWebSocketSessionOpened(Session session) public void onWebSocketSessionOpened(Session session)
{ {
LifeCycle.start(session);
sessions.add(session); sessions.add(session);
} }
@ -47,7 +46,6 @@ public class SessionTracker extends AbstractLifeCycle implements WebSocketSessio
public void onWebSocketSessionClosed(Session session) public void onWebSocketSessionClosed(Session session)
{ {
sessions.remove(session); sessions.remove(session);
LifeCycle.stop(session);
} }
@Override @Override
@ -55,8 +53,10 @@ public class SessionTracker extends AbstractLifeCycle implements WebSocketSessio
{ {
for (Session session : sessions) for (Session session : sessions)
{ {
LifeCycle.stop(session); // SHUTDOWN is abnormal close status so it will hard close connection after sent.
session.close(StatusCode.SHUTDOWN, "Container being shut down");
} }
super.doStop(); super.doStop();
} }
} }

View File

@ -23,21 +23,18 @@ import java.net.SocketAddress;
import java.time.Duration; import java.time.Duration;
import java.util.Objects; import java.util.Objects;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.component.AbstractLifeCycle;
import org.eclipse.jetty.util.component.Dumpable; import org.eclipse.jetty.util.component.Dumpable;
import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger; import org.eclipse.jetty.util.log.Logger;
import org.eclipse.jetty.websocket.api.CloseStatus; import org.eclipse.jetty.websocket.api.CloseStatus;
import org.eclipse.jetty.websocket.api.Session; import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.StatusCode;
import org.eclipse.jetty.websocket.api.SuspendToken; import org.eclipse.jetty.websocket.api.SuspendToken;
import org.eclipse.jetty.websocket.api.UpgradeRequest; import org.eclipse.jetty.websocket.api.UpgradeRequest;
import org.eclipse.jetty.websocket.api.UpgradeResponse; import org.eclipse.jetty.websocket.api.UpgradeResponse;
import org.eclipse.jetty.websocket.api.WebSocketBehavior; import org.eclipse.jetty.websocket.api.WebSocketBehavior;
import org.eclipse.jetty.websocket.core.FrameHandler; import org.eclipse.jetty.websocket.core.FrameHandler;
public class WebSocketSession extends AbstractLifeCycle implements Session, SuspendToken, Dumpable public class WebSocketSession implements Session, SuspendToken, Dumpable
{ {
private static final Logger LOG = Log.getLogger(WebSocketSession.class); private static final Logger LOG = Log.getLogger(WebSocketSession.class);
private final FrameHandler.CoreSession coreSession; private final FrameHandler.CoreSession coreSession;
@ -243,25 +240,6 @@ public class WebSocketSession extends AbstractLifeCycle implements Session, Susp
return coreSession; return coreSession;
} }
@Override
protected void doStop() throws Exception
{
coreSession.close(StatusCode.SHUTDOWN, "Container being shut down", new Callback()
{
@Override
public void succeeded()
{
coreSession.abort();
}
@Override
public void failed(Throwable x)
{
coreSession.abort();
}
});
}
@Override @Override
public void dump(Appendable out, String indent) throws IOException public void dump(Appendable out, String indent) throws IOException
{ {

View File

@ -185,24 +185,6 @@ public class DummyUpgradeRequest implements UpgradeRequest
} }
@Override
public void setHttpVersion(String httpVersion)
{
}
@Override
public void setMethod(String method)
{
}
@Override
public void setRequestURI(URI uri)
{
}
@Override @Override
public void setSession(Object session) public void setSession(Object session)
{ {

View File

@ -197,24 +197,6 @@ public class UpgradeRequestAdapter implements UpgradeRequest
throw new UnsupportedOperationException("Not supported from Servlet API"); throw new UnsupportedOperationException("Not supported from Servlet API");
} }
@Override
public void setHttpVersion(String httpVersion)
{
throw new UnsupportedOperationException("Not supported from Servlet API");
}
@Override
public void setMethod(String method)
{
throw new UnsupportedOperationException("Not supported from Servlet API");
}
@Override
public void setRequestURI(URI uri)
{
throw new UnsupportedOperationException("Not supported from Servlet API");
}
@Override @Override
public void setSession(Object session) public void setSession(Object session)
{ {

View File

@ -19,26 +19,49 @@
<groupId>org.eclipse.jetty.websocket</groupId> <groupId>org.eclipse.jetty.websocket</groupId>
<artifactId>jetty-websocket-api</artifactId> <artifactId>jetty-websocket-api</artifactId>
<version>${project.version}</version> <version>${project.version}</version>
<scope>test</scope>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.eclipse.jetty.websocket</groupId> <groupId>org.eclipse.jetty.websocket</groupId>
<artifactId>jetty-websocket-client</artifactId> <artifactId>jetty-websocket-client</artifactId>
<version>${project.version}</version> <version>${project.version}</version>
<scope>test</scope>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.eclipse.jetty.websocket</groupId> <groupId>org.eclipse.jetty.websocket</groupId>
<artifactId>jetty-websocket-server</artifactId> <artifactId>jetty-websocket-server</artifactId>
<version>${project.version}</version> <version>${project.version}</version>
<scope>test</scope>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.eclipse.jetty.tests</groupId> <groupId>org.eclipse.jetty.tests</groupId>
<artifactId>jetty-http-tools</artifactId> <artifactId>jetty-http-tools</artifactId>
<version>${project.version}</version> <version>${project.version}</version>
<scope>test</scope>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.eclipse.jetty.toolchain</groupId> <groupId>org.eclipse.jetty.http2</groupId>
<artifactId>jetty-test-helper</artifactId> <artifactId>http2-server</artifactId>
<scope>compile</scope> <version>${project.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-alpn-server</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-alpn-java-server</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.http2</groupId>
<artifactId>http2-http-client-transport</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency> </dependency>
</dependencies> </dependencies>

View File

@ -134,11 +134,6 @@ public class ConcurrentConnectTest
} }
closeListener.closeLatch.await(5, TimeUnit.SECONDS); closeListener.closeLatch.await(5, TimeUnit.SECONDS);
for (EventSocket l : listeners)
{
assertTrue(((WebSocketSession)l.session).isStopped());
}
assertTrue(client.getOpenSessions().isEmpty()); assertTrue(client.getOpenSessions().isEmpty());
assertTrue(client.getContainedBeans(WebSocketSession.class).isEmpty()); assertTrue(client.getContainedBeans(WebSocketSession.class).isEmpty());
} }

View File

@ -0,0 +1,348 @@
//
// ========================================================================
// 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.websocket.tests;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.net.ConnectException;
import java.net.URI;
import java.nio.channels.ClosedChannelException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.alpn.server.ALPNServerConnectionFactory;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.dynamic.HttpClientTransportDynamic;
import org.eclipse.jetty.client.http.HttpClientConnectionFactory;
import org.eclipse.jetty.http2.ErrorCode;
import org.eclipse.jetty.http2.HTTP2Cipher;
import org.eclipse.jetty.http2.client.HTTP2Client;
import org.eclipse.jetty.http2.client.http.ClientConnectionFactoryOverHTTP2;
import org.eclipse.jetty.http2.server.AbstractHTTP2ServerConnectionFactory;
import org.eclipse.jetty.http2.server.HTTP2CServerConnectionFactory;
import org.eclipse.jetty.http2.server.HTTP2ServerConnectionFactory;
import org.eclipse.jetty.io.ClientConnectionFactory;
import org.eclipse.jetty.io.ClientConnector;
import org.eclipse.jetty.server.HttpChannel;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.HttpConnectionFactory;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.SecureRequestCustomizer;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.SslConnectionFactory;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.eclipse.jetty.util.log.StacklessLogging;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.eclipse.jetty.util.thread.QueuedThreadPool;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.StatusCode;
import org.eclipse.jetty.websocket.api.UpgradeException;
import org.eclipse.jetty.websocket.client.WebSocketClient;
import org.eclipse.jetty.websocket.server.JettyWebSocketServlet;
import org.eclipse.jetty.websocket.server.JettyWebSocketServletFactory;
import org.eclipse.jetty.websocket.server.config.JettyWebSocketServletContainerInitializer;
import org.eclipse.jetty.websocket.servlet.internal.UpgradeHttpServletRequest;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsStringIgnoringCase;
import static org.hamcrest.Matchers.instanceOf;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class WebSocketOverHTTP2Test
{
private Server server;
private ServerConnector connector;
private ServerConnector tlsConnector;
private WebSocketClient wsClient;
private void startServer() throws Exception
{
startServer(new TestJettyWebSocketServlet());
}
private void startServer(TestJettyWebSocketServlet servlet) throws Exception
{
QueuedThreadPool serverThreads = new QueuedThreadPool();
serverThreads.setName("server");
server = new Server(serverThreads);
HttpConfiguration httpConfig = new HttpConfiguration();
HttpConnectionFactory h1c = new HttpConnectionFactory(httpConfig);
HTTP2CServerConnectionFactory h2c = new HTTP2CServerConnectionFactory(httpConfig);
connector = new ServerConnector(server, 1, 1, h1c, h2c);
server.addConnector(connector);
SslContextFactory.Server sslContextFactory = new SslContextFactory.Server();
sslContextFactory.setKeyStorePath("src/test/resources/keystore.p12");
sslContextFactory.setKeyStorePassword("storepwd");
sslContextFactory.setCipherComparator(HTTP2Cipher.COMPARATOR);
HttpConfiguration httpsConfig = new HttpConfiguration(httpConfig);
httpsConfig.addCustomizer(new SecureRequestCustomizer());
HttpConnectionFactory h1s = new HttpConnectionFactory(httpsConfig);
HTTP2ServerConnectionFactory h2s = new HTTP2ServerConnectionFactory(httpsConfig);
ALPNServerConnectionFactory alpn = new ALPNServerConnectionFactory();
alpn.setDefaultProtocol(h1s.getProtocol());
SslConnectionFactory ssl = new SslConnectionFactory(sslContextFactory, alpn.getProtocol());
tlsConnector = new ServerConnector(server, 1, 1, ssl, alpn, h1s, h2s);
server.addConnector(tlsConnector);
ServletContextHandler context = new ServletContextHandler(server, "/");
context.addServlet(new ServletHolder(servlet), "/ws/*");
JettyWebSocketServletContainerInitializer.configure(context, null);
server.start();
}
private void startClient(Function<ClientConnector, ClientConnectionFactory.Info> protocolFn) throws Exception
{
ClientConnector clientConnector = new ClientConnector();
clientConnector.setSslContextFactory(new SslContextFactory.Client(true));
QueuedThreadPool clientThreads = new QueuedThreadPool();
clientThreads.setName("client");
clientConnector.setExecutor(clientThreads);
HttpClient httpClient = new HttpClient(new HttpClientTransportDynamic(clientConnector, protocolFn.apply(clientConnector)));
wsClient = new WebSocketClient(httpClient);
wsClient.start();
}
@AfterEach
public void stopServer() throws Exception
{
if (server != null)
server.stop();
if (wsClient != null)
wsClient.stop();
}
@Test
public void testWebSocketOverDynamicHTTP1() throws Exception
{
testWebSocketOverDynamicTransport(clientConnector -> HttpClientConnectionFactory.HTTP11);
}
@Test
public void testWebSocketOverDynamicHTTP2() throws Exception
{
testWebSocketOverDynamicTransport(clientConnector -> new ClientConnectionFactoryOverHTTP2.H2C(new HTTP2Client(clientConnector)));
}
private void testWebSocketOverDynamicTransport(Function<ClientConnector, ClientConnectionFactory.Info> protocolFn) throws Exception
{
startServer();
startClient(protocolFn);
EventSocket wsEndPoint = new EventSocket();
URI uri = URI.create("ws://localhost:" + connector.getLocalPort() + "/ws/echo");
Session session = wsClient.connect(wsEndPoint, uri).get(5, TimeUnit.SECONDS);
String text = "websocket";
session.getRemote().sendString(text);
String message = wsEndPoint.messageQueue.poll(5, TimeUnit.SECONDS);
assertNotNull(message);
assertEquals(text, message);
session.close(StatusCode.NORMAL, null);
assertTrue(wsEndPoint.closeLatch.await(5, TimeUnit.SECONDS));
assertEquals(StatusCode.NORMAL, wsEndPoint.statusCode);
assertNull(wsEndPoint.error);
}
@Test
public void testConnectProtocolDisabled() throws Exception
{
startServer();
AbstractHTTP2ServerConnectionFactory h2c = connector.getBean(AbstractHTTP2ServerConnectionFactory.class);
h2c.setConnectProtocolEnabled(false);
startClient(clientConnector -> new ClientConnectionFactoryOverHTTP2.H2C(new HTTP2Client(clientConnector)));
EventSocket wsEndPoint = new EventSocket();
URI uri = URI.create("ws://localhost:" + connector.getLocalPort() + "/ws/echo");
ExecutionException failure = Assertions.assertThrows(ExecutionException.class, () ->
wsClient.connect(wsEndPoint, uri).get(5, TimeUnit.SECONDS));
Throwable cause = failure.getCause();
assertThat(cause.getMessage(), containsStringIgnoringCase(ErrorCode.PROTOCOL_ERROR.name()));
}
@Test
public void testSlowWebSocketUpgradeWithHTTP2DataFramesQueued() throws Exception
{
startServer(new TestJettyWebSocketServlet()
{
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
{
try
{
super.service(request, response);
// Flush the response to the client then wait before exiting
// this method so that the client can send HTTP/2 DATA frames
// that will be processed by the server while this method sleeps.
response.flushBuffer();
Thread.sleep(1000);
}
catch (InterruptedException x)
{
throw new InterruptedIOException();
}
}
});
startClient(clientConnector -> new ClientConnectionFactoryOverHTTP2.H2(new HTTP2Client(clientConnector)));
// Connect and send immediately a message, so the message
// arrives to the server while the server is still upgrading.
EventSocket wsEndPoint = new EventSocket();
URI uri = URI.create("wss://localhost:" + tlsConnector.getLocalPort() + "/ws/echo");
Session session = wsClient.connect(wsEndPoint, uri).get(5, TimeUnit.SECONDS);
String text = "websocket";
session.getRemote().sendString(text);
String message = wsEndPoint.messageQueue.poll(5, TimeUnit.SECONDS);
assertNotNull(message);
assertEquals(text, message);
session.close(StatusCode.NORMAL, null);
assertTrue(wsEndPoint.closeLatch.await(5, TimeUnit.SECONDS));
}
@Test
public void testWebSocketConnectPortDoesNotExist() throws Exception
{
startServer();
startClient(clientConnector -> new ClientConnectionFactoryOverHTTP2.H2(new HTTP2Client(clientConnector)));
EventSocket wsEndPoint = new EventSocket();
URI uri = URI.create("ws://localhost:" + (connector.getLocalPort()+1) + "/ws/echo");
ExecutionException failure = Assertions.assertThrows(ExecutionException.class, () ->
wsClient.connect(wsEndPoint, uri).get(5, TimeUnit.SECONDS));
Throwable cause = failure.getCause();
assertThat(cause, instanceOf(ConnectException.class));
assertThat(cause.getMessage(), containsStringIgnoringCase("Connection refused"));
}
@Test
public void testWebSocketNotFound() throws Exception
{
startServer();
startClient(clientConnector -> new ClientConnectionFactoryOverHTTP2.H2(new HTTP2Client(clientConnector)));
EventSocket wsEndPoint = new EventSocket();
URI uri = URI.create("ws://localhost:" + connector.getLocalPort() + "/nothing");
ExecutionException failure = Assertions.assertThrows(ExecutionException.class, () ->
wsClient.connect(wsEndPoint, uri).get(5, TimeUnit.SECONDS));
Throwable cause = failure.getCause();
assertThat(cause, instanceOf(UpgradeException.class));
assertThat(cause.getMessage(), containsStringIgnoringCase("Unexpected HTTP Response Status Code: 501"));
}
@Test
public void testNotNegotiated() throws Exception
{
startServer();
startClient(clientConnector -> new ClientConnectionFactoryOverHTTP2.H2(new HTTP2Client(clientConnector)));
EventSocket wsEndPoint = new EventSocket();
URI uri = URI.create("ws://localhost:" + connector.getLocalPort() + "/ws/null");
ExecutionException failure = Assertions.assertThrows(ExecutionException.class, () ->
wsClient.connect(wsEndPoint, uri).get(5, TimeUnit.SECONDS));
Throwable cause = failure.getCause();
assertThat(cause, instanceOf(UpgradeException.class));
assertThat(cause.getMessage(), containsStringIgnoringCase("Unexpected HTTP Response Status Code: 503"));
}
@Test
public void testThrowFromCreator() throws Exception
{
startServer();
startClient(clientConnector -> new ClientConnectionFactoryOverHTTP2.H2(new HTTP2Client(clientConnector)));
EventSocket wsEndPoint = new EventSocket();
URI uri = URI.create("ws://localhost:" + connector.getLocalPort() + "/ws/throw");
ExecutionException failure;
try (StacklessLogging stacklessLogging = new StacklessLogging(HttpChannel.class))
{
failure = Assertions.assertThrows(ExecutionException.class, () ->
wsClient.connect(wsEndPoint, uri).get(5, TimeUnit.SECONDS));
}
Throwable cause = failure.getCause();
assertThat(cause, instanceOf(UpgradeException.class));
assertThat(cause.getMessage(), containsStringIgnoringCase("Unexpected HTTP Response Status Code: 500"));
}
@Test
public void testServerConnectionClose() throws Exception
{
startServer();
startClient(clientConnector -> new ClientConnectionFactoryOverHTTP2.H2(new HTTP2Client(clientConnector)));
EventSocket wsEndPoint = new EventSocket();
URI uri = URI.create("ws://localhost:" + connector.getLocalPort() + "/ws/connectionClose");
ExecutionException failure = Assertions.assertThrows(ExecutionException.class, () ->
wsClient.connect(wsEndPoint, uri).get(5, TimeUnit.SECONDS));
Throwable cause = failure.getCause();
assertThat(cause, instanceOf(ClosedChannelException.class));
}
private static class TestJettyWebSocketServlet extends JettyWebSocketServlet
{
@Override
protected void configure(JettyWebSocketServletFactory factory)
{
factory.addMapping("/ws/echo", (request, response) -> new EchoSocket());
factory.addMapping("/ws/null", (request, response) -> null);
factory.addMapping("/ws/throw", (request, response) ->
{
throw new RuntimeException("throwing from creator");
});
factory.addMapping("/ws/connectionClose", (request, response) ->
{
UpgradeHttpServletRequest servletRequest = (UpgradeHttpServletRequest)request.getHttpServletRequest();
Request baseRequest = servletRequest.getBaseRequest();
baseRequest.getHttpChannel().getEndPoint().close();
return new EchoSocket();
});
}
}
}

View File

@ -24,10 +24,8 @@ import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.io.Connection; import org.eclipse.jetty.io.Connection;
import org.eclipse.jetty.io.ConnectionStatistics; import org.eclipse.jetty.io.ConnectionStatistics;
import org.eclipse.jetty.io.MappedByteBufferPool;
import org.eclipse.jetty.server.HttpConnection; import org.eclipse.jetty.server.HttpConnection;
import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector; import org.eclipse.jetty.server.ServerConnector;
@ -112,12 +110,10 @@ public class WebSocketStatsTest
long getFrameByteSize(Frame frame) long getFrameByteSize(Frame frame)
{ {
ByteBufferPool bufferPool = new MappedByteBufferPool(); Generator generator = new Generator();
Generator generator = new Generator(bufferPool); ByteBuffer headerBuffer = BufferUtil.allocate(Generator.MAX_HEADER_LENGTH);
ByteBuffer buffer = bufferPool.acquire(frame.getPayloadLength() + 10, true); generator.generateHeader(frame, headerBuffer);
int pos = BufferUtil.flipToFill(buffer); return headerBuffer.remaining() + frame.getPayloadLength();
generator.generateWholeFrame(frame, buffer);
return buffer.position() - pos;
} }
@Test @Test

View File

@ -38,12 +38,12 @@ import org.junit.jupiter.api.Test;
import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertTrue;
public class WriteAfterStopTest public class WebSocketStopTest
{ {
public class UpgradeServlet extends JettyWebSocketServlet public class UpgradeServlet extends JettyWebSocketServlet
{ {
@Override @Override
@ -83,7 +83,28 @@ public class WriteAfterStopTest
} }
@Test @Test
public void test() throws Exception public void stopWithOpenSessions() throws Exception
{
final URI uri = new URI("ws://localhost:" + connector.getLocalPort() + "/");
// Connect to two sessions to the server.
EventSocket clientSocket1 = new EventSocket();
EventSocket clientSocket2 = new EventSocket();
assertNotNull(client.connect(clientSocket1, uri).get(5, TimeUnit.SECONDS));
assertNotNull(client.connect(clientSocket2, uri).get(5, TimeUnit.SECONDS));
assertTrue(clientSocket1.openLatch.await(5, TimeUnit.SECONDS));
assertTrue(clientSocket2.openLatch.await(5, TimeUnit.SECONDS));
// WS client is stopped and closes sessions with SHUTDOWN code.
client.stop();
assertTrue(clientSocket1.closeLatch.await(5, TimeUnit.SECONDS));
assertTrue(clientSocket2.closeLatch.await(5, TimeUnit.SECONDS));
assertThat(clientSocket1.statusCode, is(StatusCode.SHUTDOWN));
assertThat(clientSocket2.statusCode, is(StatusCode.SHUTDOWN));
}
@Test
public void testWriteAfterStop() throws Exception
{ {
URI uri = new URI("ws://localhost:" + connector.getLocalPort() + "/"); URI uri = new URI("ws://localhost:" + connector.getLocalPort() + "/");
EventSocket clientSocket = new EventSocket(); EventSocket clientSocket = new EventSocket();

View File

@ -7,4 +7,4 @@ org.eclipse.jetty.util.log.class=org.eclipse.jetty.util.log.StdErrLog
# org.eclipse.jetty.io.FillInterest.LEVEL=DEBUG # org.eclipse.jetty.io.FillInterest.LEVEL=DEBUG
# org.eclipse.jetty.client.LEVEL=DEBUG # org.eclipse.jetty.client.LEVEL=DEBUG
# org.eclipse.jetty.io.LEVEL=DEBUG # org.eclipse.jetty.io.LEVEL=DEBUG
# org.eclipse.jetty.io.ManagedSelector.LEVEL=INFO # org.eclipse.jetty.io.ManagedSelector.LEVEL=INFO

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