Merge remote-tracking branch 'origin/jetty-10.0.x' into jetty-10.0.x-4919-WebSocketContainerStop

Signed-off-by: Lachlan Roberts <lachlan@webtide.com>
This commit is contained in:
Lachlan Roberts 2020-06-16 18:57:40 +10:00
commit ed9c60fc90
106 changed files with 3354 additions and 1228 deletions

4
Jenkinsfile vendored
View File

@ -14,7 +14,7 @@ pipeline {
steps { steps {
container( 'jetty-build' ) { container( 'jetty-build' ) {
timeout( time: 120, unit: 'MINUTES' ) { timeout( time: 120, unit: 'MINUTES' ) {
mavenBuild( "jdk11", "-T3 -Pmongodb clean install", "maven3", true ) // -Pautobahn mavenBuild( "jdk11", "-T3 clean install", "maven3", true ) // -Pautobahn
// Collect up the jacoco execution results (only on main build) // Collect up the jacoco execution results (only on main build)
jacoco inclusionPattern: '**/org/eclipse/jetty/**/*.class', jacoco inclusionPattern: '**/org/eclipse/jetty/**/*.class',
exclusionPattern: '' + exclusionPattern: '' +
@ -44,7 +44,7 @@ pipeline {
steps { steps {
container( 'jetty-build' ) { container( 'jetty-build' ) {
timeout( time: 120, unit: 'MINUTES' ) { timeout( time: 120, unit: 'MINUTES' ) {
mavenBuild( "jdk14", "-T3 -Pmongodb clean install", "maven3", true ) mavenBuild( "jdk14", "-T3 clean install", "maven3", true )
warnings consoleParsers: [[parserName: 'Maven'], [parserName: 'Java']] warnings consoleParsers: [[parserName: 'Maven'], [parserName: 'Java']]
junit testResults: '**/target/surefire-reports/*.xml,**/target/invoker-reports/TEST*.xml' junit testResults: '**/target/surefire-reports/*.xml,**/target/invoker-reports/TEST*.xml'
} }

View File

@ -5,3 +5,4 @@ Joakim Erdfelt <joakim.erdfelt@gmail.com> 5989 BAF7 6217 B843 D66B E55B 2D0E
Joakim Erdfelt <joakime@apache.org> B59B 67FD 7904 9843 67F9 3180 0818 D9D6 8FB6 7BAC Joakim Erdfelt <joakime@apache.org> B59B 67FD 7904 9843 67F9 3180 0818 D9D6 8FB6 7BAC
Joakim Erdfelt <joakim@erdfelt.com> BFBB 21C2 46D7 7768 3628 7A48 A04E 0C74 ABB3 5FEA Joakim Erdfelt <joakim@erdfelt.com> BFBB 21C2 46D7 7768 3628 7A48 A04E 0C74 ABB3 5FEA
Simone Bordet <simone.bordet@gmail.com> 8B09 6546 B1A8 F026 56B1 5D3B 1677 D141 BCF3 584D Simone Bordet <simone.bordet@gmail.com> 8B09 6546 B1A8 F026 56B1 5D3B 1677 D141 BCF3 584D
Olivier Lamy <olamy@apache.org> F254 B356 17DC 255D 9344 BCFA 873A 8E86 B437 2146

View File

@ -1,5 +1,25 @@
jetty-10.0.0-SNAPSHOT jetty-10.0.0-SNAPSHOT
jetty-9.4.30.v20200611 - 11 June 2020
+ 4776 Incorrect path matching for WebSocket using PathMappings
+ 4826 Upgrade to Apache Jasper 8.5.54
+ 4855 occasional h2spec failures on jenkins
+ 4873 Server.join not working when used with ExecutorThreadPool
+ 4885 setCookie() must not change the headers in a response during an include
+ 4890 JettyClient behavior when SETTINGS_HEADER_TABLE_SIZE is set to 0 in
SETTINGS Frame.
+ 4894 JDBCSessionDataStore fails to create multiple JettySessions for server
with multiple databases
+ 4903 Give better errors for non public Websocket Endpoints
+ 4904 WebsocketClient creates more connections than needed
+ 4913 DirectoryNotEmptyException when using mvn jetty:run-distro
+ 4920 Restore ability to delete sessions on stop
+ 4921 Quickstart run improperly runs dynamically added context initializers
+ 4923 SecureRequestCustomizer.SslAttributes does not cache cert chain like
before
+ 4929 HttpClient: HttpCookieStore.Empty prevents sending cookies
+ 4936 Response header overflow leads to buffer corruptions
jetty-9.4.29.v20200521 - 21 May 2020 jetty-9.4.29.v20200521 - 21 May 2020
+ 2188 Lock contention creating HTTP/2 streams + 2188 Lock contention creating HTTP/2 streams
+ 4235 communicate the reason of failure to the OpenID error page + 4235 communicate the reason of failure to the OpenID error page
@ -12,8 +32,8 @@ jetty-9.4.29.v20200521 - 21 May 2020
+ 4798 Better handling of fatal Selector failures + 4798 Better handling of fatal Selector failures
+ 4814 Allow a ConnectionFactory (eg SslConnectionFactory) to automatically + 4814 Allow a ConnectionFactory (eg SslConnectionFactory) to automatically
add a Customizer add a Customizer
+ 4820 Jetty OSGi DefaultJettyAtJettyHomeHelper refers to non-existent + 4820 Jetty OSGi DefaultJettyAtJettyHomeHelper refers to non-existent config
config file file
+ 4824 WebSocket server outgoing message queue memory growth + 4824 WebSocket server outgoing message queue memory growth
+ 4828 NIO ByteBuffer corruption in embedded Jetty server + 4828 NIO ByteBuffer corruption in embedded Jetty server
+ 4835 GzipHandler and GzipHttpOutputInterceptor do not flush response when + 4835 GzipHandler and GzipHttpOutputInterceptor do not flush response when

View File

@ -54,6 +54,22 @@ import org.eclipse.jetty.util.resource.Resource;
* extent so does the {@link ResourceHandler}, so unless you have exceptional * extent so does the {@link ResourceHandler}, so unless you have exceptional
* circumstances it is best to use those classes for static content * circumstances it is best to use those classes for static content
* </p> * </p>
* <p>
* <em>WARNING</em>: This is an example on how to send content fast.
* It is not secure, is highly vulnerable, and does not contain the
* common set of mitigations for malicious requests that bypass
* your controls over what a client can access.
*
* If you want to continue this codebase, consider adding
* checks for content outside of the resourceBase, and other
* bypasses such as alias references, alternate stream references,
* filesystem case sensitivity differences, filesystem utf-8 handling
* differences, bad filename concerns, etc..
*
* Or just use the existing {@link DefaultServlet} or
* {@link ResourceHandler} that gives you all of these protections
* (and more) built-in.
* </p>
*/ */
public class FastFileServer public class FastFileServer
{ {

View File

@ -22,7 +22,6 @@ import java.util.Collection;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import org.eclipse.jetty.client.api.Connection; import org.eclipse.jetty.client.api.Connection;
import org.eclipse.jetty.client.api.Destination;
import org.eclipse.jetty.util.AtomicBiInteger; import org.eclipse.jetty.util.AtomicBiInteger;
import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.Promise; import org.eclipse.jetty.util.Promise;
@ -37,24 +36,28 @@ public abstract class AbstractConnectionPool implements ConnectionPool, Dumpable
{ {
private static final Logger LOG = LoggerFactory.getLogger(AbstractConnectionPool.class); private static final Logger LOG = LoggerFactory.getLogger(AbstractConnectionPool.class);
private final AtomicBoolean closed = new AtomicBoolean();
/** /**
* The connectionCount encodes both the total connections plus the pending connection counts, so both can be atomically changed. * The connectionCount encodes both the total connections plus the pending connection counts, so both can be atomically changed.
* The bottom 32 bits represent the total connections and the top 32 bits represent the pending connections. * The bottom 32 bits represent the total connections and the top 32 bits represent the pending connections.
*/ */
private final AtomicBiInteger connections = new AtomicBiInteger(); private final AtomicBiInteger connections = new AtomicBiInteger();
private final Destination destination; private final AtomicBoolean closed = new AtomicBoolean();
private final HttpDestination destination;
private final int maxConnections; private final int maxConnections;
private final Callback requester; private final Callback requester;
protected AbstractConnectionPool(Destination destination, int maxConnections, Callback requester) protected AbstractConnectionPool(HttpDestination destination, int maxConnections, Callback requester)
{ {
this.destination = destination; this.destination = destination;
this.maxConnections = maxConnections; this.maxConnections = maxConnections;
this.requester = requester; this.requester = requester;
} }
protected HttpDestination getHttpDestination()
{
return destination;
}
@ManagedAttribute(value = "The max number of connections", readonly = true) @ManagedAttribute(value = "The max number of connections", readonly = true)
public int getMaxConnectionCount() public int getMaxConnectionCount()
{ {
@ -86,17 +89,28 @@ public abstract class AbstractConnectionPool implements ConnectionPool, Dumpable
} }
@Override @Override
public Connection acquire() public Connection acquire(boolean create)
{ {
Connection connection = activate(); Connection connection = activate();
if (connection == null) if (connection == null)
{ {
tryCreate(-1); if (create)
tryCreate(destination.getQueuedRequestCount());
connection = activate(); connection = activate();
} }
return connection; return connection;
} }
/**
* <p>Schedules the opening of a new connection.</p>
* <p>Whether a new connection is scheduled for opening is determined by the {@code maxPending} parameter:
* if {@code maxPending} is greater than the current number of connections scheduled for opening,
* then this method returns without scheduling the opening of a new connection;
* if {@code maxPending} is negative, a new connection is always scheduled for opening.</p>
*
* @param maxPending the max desired number of connections scheduled for opening,
* or a negative number to always trigger the opening of a new connection
*/
protected void tryCreate(int maxPending) protected void tryCreate(int maxPending)
{ {
while (true) while (true)

View File

@ -45,12 +45,16 @@ public interface ConnectionPool extends Closeable
boolean isClosed(); boolean isClosed();
/** /**
* <p>Returns an idle connection, if available, or schedules the opening * <p>Returns an idle connection, if available;
* of a new connection and returns {@code null}.</p> * if an idle connection is not available, and the given {@code create} parameter is {@code true},
* then schedules the opening of a new connection, if possible within the configuration of this
* connection pool (for example, if it does not exceed the max connection count);
* otherwise returns {@code null}.</p>
* *
* @return an available connection, or null * @param create whether to schedule the opening of a connection if no idle connections are available
* @return an idle connection or {@code null} if no idle connections are available
*/ */
Connection acquire(); Connection acquire(boolean create);
/** /**
* <p>Accepts the given connection to be managed by this ConnectionPool.</p> * <p>Accepts the given connection to be managed by this ConnectionPool.</p>
@ -61,7 +65,7 @@ public interface ConnectionPool extends Closeable
boolean accept(Connection connection); boolean accept(Connection connection);
/** /**
* <p>Returns the given connection, previously obtained via {@link #acquire()}, * <p>Returns the given connection, previously obtained via {@link #acquire(boolean)},
* back to this ConnectionPool.</p> * back to this ConnectionPool.</p>
* *
* @param connection the connection to release * @param connection the connection to release

View File

@ -31,7 +31,6 @@ import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.eclipse.jetty.client.api.Connection; import org.eclipse.jetty.client.api.Connection;
import org.eclipse.jetty.client.api.Destination;
import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.annotation.ManagedAttribute; import org.eclipse.jetty.util.annotation.ManagedAttribute;
import org.eclipse.jetty.util.annotation.ManagedObject; import org.eclipse.jetty.util.annotation.ManagedObject;
@ -50,7 +49,7 @@ public class DuplexConnectionPool extends AbstractConnectionPool implements Swee
private final Deque<Connection> idleConnections; private final Deque<Connection> idleConnections;
private final Set<Connection> activeConnections; private final Set<Connection> activeConnections;
public DuplexConnectionPool(Destination destination, int maxConnections, Callback requester) public DuplexConnectionPool(HttpDestination destination, int maxConnections, Callback requester)
{ {
super(destination, maxConnections, requester); super(destination, maxConnections, requester);
this.idleConnections = new ArrayDeque<>(maxConnections); this.idleConnections = new ArrayDeque<>(maxConnections);

View File

@ -105,6 +105,8 @@ public abstract class HttpConnection implements IConnection
} }
else else
{ {
// Association may fail, for example if the application
// aborted the request, so we must release the channel.
channel.release(); channel.release();
result = new SendFailure(new HttpRequestException("Could not associate request to connection", request), false); result = new SendFailure(new HttpRequestException("Could not associate request to connection", request), false);
} }
@ -119,6 +121,8 @@ public abstract class HttpConnection implements IConnection
} }
else else
{ {
// This connection has been timed out by another thread
// that will take care of removing it from the pool.
return new SendFailure(new TimeoutException(), true); return new SendFailure(new TimeoutException(), true);
} }
} }
@ -187,19 +191,18 @@ public abstract class HttpConnection implements IConnection
} }
// Cookies // Cookies
StringBuilder cookies = convertCookies(request.getCookies(), null);
CookieStore cookieStore = getHttpClient().getCookieStore(); CookieStore cookieStore = getHttpClient().getCookieStore();
if (cookieStore != null && cookieStore.getClass() != HttpCookieStore.Empty.class) if (cookieStore != null && cookieStore.getClass() != HttpCookieStore.Empty.class)
{ {
StringBuilder cookies = null;
URI uri = request.getURI(); URI uri = request.getURI();
if (uri != null) if (uri != null)
cookies = convertCookies(HttpCookieStore.matchPath(uri, cookieStore.get(uri)), null); cookies = convertCookies(HttpCookieStore.matchPath(uri, cookieStore.get(uri)), cookies);
cookies = convertCookies(request.getCookies(), cookies); }
if (cookies != null) if (cookies != null)
{ {
HttpField cookieField = new HttpField(HttpHeader.COOKIE, cookies.toString()); HttpField cookieField = new HttpField(HttpHeader.COOKIE, cookies.toString());
request.addHeader(cookieField); request.addHeader(cookieField);
}
} }
// Authentication // Authentication

View File

@ -55,7 +55,7 @@ import org.slf4j.LoggerFactory;
@ManagedObject @ManagedObject
public abstract class HttpDestination extends ContainerLifeCycle implements Destination, Closeable, Callback, Dumpable public abstract class HttpDestination extends ContainerLifeCycle implements Destination, Closeable, Callback, Dumpable
{ {
protected static final Logger LOG = LoggerFactory.getLogger(HttpDestination.class); private static final Logger LOG = LoggerFactory.getLogger(HttpDestination.class);
private final HttpClient client; private final HttpClient client;
private final Origin origin; private final Origin origin;
@ -234,7 +234,7 @@ public abstract class HttpDestination extends ContainerLifeCycle implements Dest
@Override @Override
public void succeeded() public void succeeded()
{ {
send(); send(false);
} }
@Override @Override
@ -291,32 +291,42 @@ public abstract class HttpDestination extends ContainerLifeCycle implements Dest
} }
} }
public void send()
{
if (getHttpExchanges().isEmpty())
return;
process();
}
protected boolean enqueue(Queue<HttpExchange> queue, HttpExchange exchange) protected boolean enqueue(Queue<HttpExchange> queue, HttpExchange exchange)
{ {
return queue.offer(exchange); return queue.offer(exchange);
} }
private void process() public void send()
{ {
send(true);
}
private void send(boolean create)
{
if (getHttpExchanges().isEmpty())
return;
process(create);
}
private void process(boolean create)
{
// The loop is necessary in case of a new multiplexed connection,
// when a single thread notified of the connection opening must
// process all queued exchanges.
// In other cases looping is a work-stealing optimization.
while (true) while (true)
{ {
Connection connection = connectionPool.acquire(); Connection connection = connectionPool.acquire(create);
if (connection == null) if (connection == null)
break; break;
boolean proceed = process(connection); ProcessResult result = process(connection);
if (!proceed) if (result == ProcessResult.FINISH)
break; break;
create = result == ProcessResult.RESTART;
} }
} }
public boolean process(Connection connection) private ProcessResult process(Connection connection)
{ {
HttpClient client = getHttpClient(); HttpClient client = getHttpClient();
HttpExchange exchange = getHttpExchanges().poll(); HttpExchange exchange = getHttpExchanges().poll();
@ -332,7 +342,7 @@ public abstract class HttpDestination extends ContainerLifeCycle implements Dest
LOG.debug("{} is stopping", client); LOG.debug("{} is stopping", client);
connection.close(); connection.close();
} }
return false; return ProcessResult.FINISH;
} }
else else
{ {
@ -343,31 +353,37 @@ public abstract class HttpDestination extends ContainerLifeCycle implements Dest
if (LOG.isDebugEnabled()) if (LOG.isDebugEnabled())
LOG.debug("Aborted before processing {}: {}", exchange, cause); LOG.debug("Aborted before processing {}: {}", exchange, cause);
// Won't use this connection, release it back. // Won't use this connection, release it back.
if (!connectionPool.release(connection)) boolean released = connectionPool.release(connection);
if (!released)
connection.close(); connection.close();
// It may happen that the request is aborted before the exchange // It may happen that the request is aborted before the exchange
// is created. Aborting the exchange a second time will result in // is created. Aborting the exchange a second time will result in
// a no-operation, so we just abort here to cover that edge case. // a no-operation, so we just abort here to cover that edge case.
exchange.abort(cause); exchange.abort(cause);
return getHttpExchanges().size() > 0
? (released ? ProcessResult.CONTINUE : ProcessResult.RESTART)
: ProcessResult.FINISH;
} }
else
SendFailure failure = send((IConnection)connection, exchange);
if (failure == null)
{ {
SendFailure result = send((IConnection)connection, exchange); // Aggressively send other queued requests
if (result != null) // in case connections are multiplexed.
{ return getHttpExchanges().size() > 0 ? ProcessResult.CONTINUE : ProcessResult.FINISH;
if (LOG.isDebugEnabled())
LOG.debug("Send failed {} for {}", result, exchange);
if (result.retry)
{
// Resend this exchange, likely on another connection,
// and return false to avoid to re-enter this method.
send(exchange);
return false;
}
request.abort(result.failure);
}
} }
return getHttpExchanges().peek() != null;
if (LOG.isDebugEnabled())
LOG.debug("Send failed {} for {}", failure, exchange);
if (failure.retry)
{
// Resend this exchange, likely on another connection,
// and return false to avoid to re-enter this method.
send(exchange);
return ProcessResult.FINISH;
}
request.abort(failure.failure);
return getHttpExchanges().size() > 0 ? ProcessResult.RESTART : ProcessResult.FINISH;
} }
} }
@ -392,11 +408,6 @@ public abstract class HttpDestination extends ContainerLifeCycle implements Dest
return exchanges.remove(exchange); return exchanges.remove(exchange);
} }
public boolean remove(Connection connection)
{
return connectionPool.remove(connection);
}
@Override @Override
public void close() public void close()
{ {
@ -407,24 +418,6 @@ public abstract class HttpDestination extends ContainerLifeCycle implements Dest
timeout.destroy(); timeout.destroy();
} }
public void close(Connection connection)
{
boolean removed = remove(connection);
if (getHttpExchanges().isEmpty())
{
tryRemoveIdleDestination();
}
else
{
// We need to execute queued requests even if this connection failed.
// We may create a connection that is not needed, but it will eventually
// idle timeout, so no worries.
if (removed)
process();
}
}
public void release(Connection connection) public void release(Connection connection)
{ {
if (LOG.isDebugEnabled()) if (LOG.isDebugEnabled())
@ -435,7 +428,7 @@ public abstract class HttpDestination extends ContainerLifeCycle implements Dest
if (connectionPool.isActive(connection)) if (connectionPool.isActive(connection))
{ {
if (connectionPool.release(connection)) if (connectionPool.release(connection))
send(); send(false);
else else
connection.close(); connection.close();
} }
@ -453,6 +446,24 @@ public abstract class HttpDestination extends ContainerLifeCycle implements Dest
} }
} }
public boolean remove(Connection connection)
{
boolean removed = connectionPool.remove(connection);
if (getHttpExchanges().isEmpty())
{
tryRemoveIdleDestination();
}
else if (removed)
{
// Process queued requests that may be waiting.
// We may create a connection that is not
// needed, but it will eventually idle timeout.
process(true);
}
return removed;
}
/** /**
* Aborts all the {@link HttpExchange}s queued in this destination. * Aborts all the {@link HttpExchange}s queued in this destination.
* *
@ -580,4 +591,9 @@ public abstract class HttpDestination extends ContainerLifeCycle implements Dest
} }
} }
} }
private enum ProcessResult
{
RESTART, CONTINUE, FINISH
}
} }

View File

@ -19,7 +19,6 @@
package org.eclipse.jetty.client; package org.eclipse.jetty.client;
import org.eclipse.jetty.client.api.Connection; import org.eclipse.jetty.client.api.Connection;
import org.eclipse.jetty.client.api.Destination;
import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.LeakDetector; import org.eclipse.jetty.util.LeakDetector;
import org.slf4j.Logger; import org.slf4j.Logger;
@ -38,7 +37,7 @@ public class LeakTrackingConnectionPool extends DuplexConnectionPool
} }
}; };
public LeakTrackingConnectionPool(Destination destination, int maxConnections, Callback requester) public LeakTrackingConnectionPool(HttpDestination destination, int maxConnections, Callback requester)
{ {
super(destination, maxConnections, requester); super(destination, maxConnections, requester);
start(); start();

View File

@ -40,7 +40,6 @@ public class MultiplexConnectionPool extends AbstractConnectionPool implements C
{ {
private static final Logger LOG = LoggerFactory.getLogger(MultiplexConnectionPool.class); private static final Logger LOG = LoggerFactory.getLogger(MultiplexConnectionPool.class);
private final HttpDestination destination;
private final Deque<Holder> idleConnections; private final Deque<Holder> idleConnections;
private final Map<Connection, Holder> activeConnections; private final Map<Connection, Holder> activeConnections;
private int maxMultiplex; private int maxMultiplex;
@ -48,25 +47,36 @@ public class MultiplexConnectionPool extends AbstractConnectionPool implements C
public MultiplexConnectionPool(HttpDestination destination, int maxConnections, Callback requester, int maxMultiplex) public MultiplexConnectionPool(HttpDestination destination, int maxConnections, Callback requester, int maxMultiplex)
{ {
super(destination, maxConnections, requester); super(destination, maxConnections, requester);
this.destination = destination;
this.idleConnections = new ArrayDeque<>(maxConnections); this.idleConnections = new ArrayDeque<>(maxConnections);
this.activeConnections = new LinkedHashMap<>(maxConnections); this.activeConnections = new LinkedHashMap<>(maxConnections);
this.maxMultiplex = maxMultiplex; this.maxMultiplex = maxMultiplex;
} }
@Override @Override
public Connection acquire() public Connection acquire(boolean create)
{ {
Connection connection = activate(); Connection connection = activate();
if (connection == null) if (connection == null)
{ {
int maxPending = 1 + destination.getQueuedRequestCount() / getMaxMultiplex(); int queuedRequests = getHttpDestination().getQueuedRequestCount();
int maxMultiplex = getMaxMultiplex();
int maxPending = ceilDiv(queuedRequests, maxMultiplex);
tryCreate(maxPending); tryCreate(maxPending);
connection = activate(); connection = activate();
} }
return connection; return connection;
} }
/**
* @param a the dividend
* @param b the divisor
* @return the ceiling of the algebraic quotient
*/
private static int ceilDiv(int a, int b)
{
return (a + b - 1) / b;
}
@Override @Override
public int getMaxMultiplex() public int getMaxMultiplex()
{ {

View File

@ -23,7 +23,6 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
import org.eclipse.jetty.client.api.Connection; import org.eclipse.jetty.client.api.Connection;
import org.eclipse.jetty.client.api.Destination;
import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.annotation.ManagedObject; import org.eclipse.jetty.util.annotation.ManagedObject;
import org.eclipse.jetty.util.component.Dumpable; import org.eclipse.jetty.util.component.Dumpable;
@ -35,12 +34,12 @@ public class RoundRobinConnectionPool extends AbstractConnectionPool implements
private int maxMultiplex; private int maxMultiplex;
private int index; private int index;
public RoundRobinConnectionPool(Destination destination, int maxConnections, Callback requester) public RoundRobinConnectionPool(HttpDestination destination, int maxConnections, Callback requester)
{ {
this(destination, maxConnections, requester, 1); this(destination, maxConnections, requester, 1);
} }
public RoundRobinConnectionPool(Destination destination, int maxConnections, Callback requester, int maxMultiplex) public RoundRobinConnectionPool(HttpDestination destination, int maxConnections, Callback requester, int maxMultiplex)
{ {
super(destination, maxConnections, requester); super(destination, maxConnections, requester);
entries = new ArrayList<>(maxConnections); entries = new ArrayList<>(maxConnections);
@ -69,6 +68,21 @@ public class RoundRobinConnectionPool extends AbstractConnectionPool implements
} }
} }
/**
* <p>Returns an idle connection, if available, following a round robin algorithm;
* otherwise it always tries to create a new connection, up until the max connection count.</p>
*
* @param create this parameter is ignored and assumed to be always {@code true}
* @return an idle connection or {@code null} if no idle connections are available
*/
@Override
public Connection acquire(boolean create)
{
// The nature of this connection pool is such that a
// connection must always be present in the next slot.
return super.acquire(true);
}
@Override @Override
protected void onCreated(Connection connection) protected void onCreated(Connection connection)
{ {

View File

@ -26,7 +26,6 @@ import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Stream; import java.util.stream.Stream;
import org.eclipse.jetty.client.api.Connection; import org.eclipse.jetty.client.api.Connection;
import org.eclipse.jetty.client.api.Destination;
import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.annotation.ManagedAttribute; import org.eclipse.jetty.util.annotation.ManagedAttribute;
import org.eclipse.jetty.util.component.DumpableCollection; import org.eclipse.jetty.util.component.DumpableCollection;
@ -65,7 +64,7 @@ public class ValidatingConnectionPool extends DuplexConnectionPool
private final long timeout; private final long timeout;
private final Map<Connection, Holder> quarantine; private final Map<Connection, Holder> quarantine;
public ValidatingConnectionPool(Destination destination, int maxConnections, Callback requester, Scheduler scheduler, long timeout) public ValidatingConnectionPool(HttpDestination destination, int maxConnections, Callback requester, Scheduler scheduler, long timeout)
{ {
super(destination, maxConnections, requester); super(destination, maxConnections, requester);
this.scheduler = scheduler; this.scheduler = scheduler;

View File

@ -206,7 +206,7 @@ public class HttpConnectionOverHTTP extends AbstractConnection implements IConne
{ {
if (closed.compareAndSet(false, true)) if (closed.compareAndSet(false, true))
{ {
getHttpDestination().close(this); getHttpDestination().remove(this);
abort(failure); abort(failure);
channel.destroy(); channel.destroy();
getEndPoint().shutdownOutput(); getEndPoint().shutdownOutput();

View File

@ -119,9 +119,10 @@ public abstract class BufferingResponseListener extends Listener.Adapter
int length = content.remaining(); int length = content.remaining();
if (length > BufferUtil.space(buffer)) if (length > BufferUtil.space(buffer))
{ {
int requiredCapacity = buffer == null ? length : buffer.capacity() + length; int remaining = buffer == null ? 0 : buffer.remaining();
if (requiredCapacity > maxLength) if (remaining + length > maxLength)
response.abort(new IllegalArgumentException("Buffering capacity " + maxLength + " exceeded")); response.abort(new IllegalArgumentException("Buffering capacity " + maxLength + " exceeded"));
int requiredCapacity = buffer == null ? length : buffer.capacity() + length;
int newCapacity = Math.min(Integer.highestOneBit(requiredCapacity) << 1, maxLength); int newCapacity = Math.min(Integer.highestOneBit(requiredCapacity) << 1, maxLength);
buffer = BufferUtil.ensureCapacity(buffer, newCapacity); buffer = BufferUtil.ensureCapacity(buffer, newCapacity);
} }

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.util.List; import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
@ -30,6 +31,7 @@ import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.client.api.ContentResponse; import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.api.Destination;
import org.eclipse.jetty.client.api.Request; import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.client.http.HttpClientTransportOverHTTP; import org.eclipse.jetty.client.http.HttpClientTransportOverHTTP;
import org.eclipse.jetty.client.util.BytesRequestContent; import org.eclipse.jetty.client.util.BytesRequestContent;
@ -38,44 +40,62 @@ 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.HttpMethod;
import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.io.ClientConnector;
import org.eclipse.jetty.server.Handler; import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector; import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.util.IO; import org.eclipse.jetty.util.IO;
import org.eclipse.jetty.util.Promise;
import org.eclipse.jetty.util.SocketAddressResolver;
import org.eclipse.jetty.util.thread.QueuedThreadPool;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Assumptions;
import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.MethodSource;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertTrue;
@Disabled // Disabled by @gregw on issue #2540 - commit 621b946b10884e7308eacca241dcf8b5d6f6cff2
public class ConnectionPoolTest public class ConnectionPoolTest
{ {
private Server server; private Server server;
private ServerConnector connector; private ServerConnector connector;
private HttpClient client; private HttpClient client;
public static Stream<ConnectionPool.Factory> pools() public static Stream<ConnectionPoolFactory> pools()
{ {
return Stream.of(destination -> new DuplexConnectionPool(destination, 8, destination), return Stream.of(
destination -> new RoundRobinConnectionPool(destination, 8, destination)); new ConnectionPoolFactory("duplex", destination -> new DuplexConnectionPool(destination, destination.getHttpClient().getMaxConnectionsPerDestination(), destination)),
new ConnectionPoolFactory("round-robin", destination -> new RoundRobinConnectionPool(destination, destination.getHttpClient().getMaxConnectionsPerDestination(), destination)),
new ConnectionPoolFactory("multiplex", destination -> new MultiplexConnectionPool(destination, destination.getHttpClient().getMaxConnectionsPerDestination(), destination, 1))
);
} }
private void start(final ConnectionPool.Factory factory, Handler handler) throws Exception private void start(ConnectionPool.Factory factory, Handler handler) throws Exception
{
startServer(handler);
startClient(factory);
}
private void startClient(ConnectionPool.Factory factory) throws Exception
{
ClientConnector connector = new ClientConnector();
connector.setSelectors(1);
HttpClientTransport transport = new HttpClientTransportOverHTTP(connector);
transport.setConnectionPoolFactory(factory);
client = new HttpClient(transport);
client.start();
}
private void startServer(Handler handler) throws Exception
{ {
server = new Server(); server = new Server();
connector = new ServerConnector(server); connector = new ServerConnector(server);
server.addConnector(connector); server.addConnector(connector);
server.setHandler(handler); server.setHandler(handler);
HttpClientTransport transport = new HttpClientTransportOverHTTP(1);
transport.setConnectionPoolFactory(factory);
server.start(); server.start();
client = new HttpClient(transport);
client.start();
} }
@AfterEach @AfterEach
@ -99,11 +119,11 @@ public class ConnectionPoolTest
} }
} }
@ParameterizedTest(name = "[{index}] {0}") @ParameterizedTest
@MethodSource("pools") @MethodSource("pools")
public void test(ConnectionPool.Factory factory) throws Exception public void test(ConnectionPoolFactory factory) throws Exception
{ {
start(factory, new EmptyServerHandler() start(factory.factory, new EmptyServerHandler()
{ {
@Override @Override
protected void service(String target, org.eclipse.jetty.server.Request jettyRequest, HttpServletRequest request, HttpServletResponse response) throws IOException protected void service(String target, org.eclipse.jetty.server.Request jettyRequest, HttpServletRequest request, HttpServletResponse response) throws IOException
@ -221,4 +241,140 @@ public class ConnectionPoolTest
failures.add(x); failures.add(x);
} }
} }
@ParameterizedTest
@MethodSource("pools")
public void testQueuedRequestsDontOpenTooManyConnections(ConnectionPoolFactory factory) throws Exception
{
startServer(new EmptyServerHandler());
ClientConnector clientConnector = new ClientConnector();
clientConnector.setSelectors(1);
HttpClientTransport transport = new HttpClientTransportOverHTTP(clientConnector);
transport.setConnectionPoolFactory(factory.factory);
client = new HttpClient(transport);
long delay = 1000;
client.setSocketAddressResolver(new SocketAddressResolver.Sync()
{
@Override
public void resolve(String host, int port, Promise<List<InetSocketAddress>> promise)
{
client.getExecutor().execute(() ->
{
try
{
Thread.sleep(delay);
super.resolve(host, port, promise);
}
catch (InterruptedException x)
{
promise.failed(x);
}
});
}
});
client.start();
CountDownLatch latch = new CountDownLatch(2);
client.newRequest("localhost", connector.getLocalPort())
.path("/one")
.send(result ->
{
if (result.isSucceeded())
latch.countDown();
});
Thread.sleep(delay / 2);
client.newRequest("localhost", connector.getLocalPort())
.path("/two")
.send(result ->
{
if (result.isSucceeded())
latch.countDown();
});
assertTrue(latch.await(2 * delay, TimeUnit.MILLISECONDS));
List<Destination> destinations = client.getDestinations();
assertEquals(1, destinations.size());
HttpDestination destination = (HttpDestination)destinations.get(0);
AbstractConnectionPool connectionPool = (AbstractConnectionPool)destination.getConnectionPool();
assertEquals(2, connectionPool.getConnectionCount());
}
@ParameterizedTest
@MethodSource("pools")
public void testConcurrentRequestsDontOpenTooManyConnections(ConnectionPoolFactory factory) throws Exception
{
// Round robin connection pool does open a few more connections than expected.
Assumptions.assumeFalse(factory.name.equals("round-robin"));
startServer(new EmptyServerHandler());
int count = 500;
ClientConnector clientConnector = new ClientConnector();
clientConnector.setSelectors(1);
QueuedThreadPool clientThreads = new QueuedThreadPool(2 * count);
clientThreads.setName("client");
clientConnector.setExecutor(clientThreads);
HttpClientTransport transport = new HttpClientTransportOverHTTP(clientConnector);
transport.setConnectionPoolFactory(factory.factory);
client = new HttpClient(transport);
client.setExecutor(clientThreads);
client.setMaxConnectionsPerDestination(2 * count);
client.setSocketAddressResolver(new SocketAddressResolver.Sync()
{
@Override
public void resolve(String host, int port, Promise<List<InetSocketAddress>> promise)
{
client.getExecutor().execute(() ->
{
try
{
Thread.sleep(100);
super.resolve(host, port, promise);
}
catch (InterruptedException x)
{
promise.failed(x);
}
});
}
});
client.start();
CountDownLatch latch = new CountDownLatch(count);
for (int i = 0; i < count; ++i)
{
clientThreads.execute(() -> client.newRequest("localhost", connector.getLocalPort())
.send(result ->
{
if (result.isSucceeded())
latch.countDown();
}));
}
assertTrue(latch.await(count, TimeUnit.SECONDS));
List<Destination> destinations = client.getDestinations();
assertEquals(1, destinations.size());
HttpDestination destination = (HttpDestination)destinations.get(0);
AbstractConnectionPool connectionPool = (AbstractConnectionPool)destination.getConnectionPool();
assertThat(connectionPool.getConnectionCount(), Matchers.lessThanOrEqualTo(count));
}
private static class ConnectionPoolFactory
{
private final String name;
private final ConnectionPool.Factory factory;
private ConnectionPoolFactory(String name, ConnectionPool.Factory factory)
{
this.name = name;
this.factory = factory;
}
@Override
public String toString()
{
return name;
}
}
} }

View File

@ -18,6 +18,7 @@
package org.eclipse.jetty.client; package org.eclipse.jetty.client;
import java.net.CookieStore;
import java.net.HttpCookie; import java.net.HttpCookie;
import java.net.URI; import java.net.URI;
import java.util.Arrays; import java.util.Arrays;
@ -34,6 +35,7 @@ import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.api.Response; import org.eclipse.jetty.client.api.Response;
import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.util.HttpCookieStore;
import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ArgumentsSource; import org.junit.jupiter.params.provider.ArgumentsSource;
@ -132,10 +134,22 @@ public class HttpCookieTest extends AbstractHttpClientServerTest
@ParameterizedTest @ParameterizedTest
@ArgumentsSource(ScenarioProvider.class) @ArgumentsSource(ScenarioProvider.class)
public void testPerRequestCookieIsSent(Scenario scenario) throws Exception public void testPerRequestCookieIsSent(Scenario scenario) throws Exception
{
testPerRequestCookieIsSent(scenario, null);
}
@ParameterizedTest
@ArgumentsSource(ScenarioProvider.class)
public void testPerRequestCookieIsSentWithEmptyCookieStore(Scenario scenario) throws Exception
{
testPerRequestCookieIsSent(scenario, new HttpCookieStore.Empty());
}
private void testPerRequestCookieIsSent(Scenario scenario, CookieStore cookieStore) throws Exception
{ {
final String name = "foo"; final String name = "foo";
final String value = "bar"; final String value = "bar";
start(scenario, new EmptyServerHandler() startServer(scenario, new EmptyServerHandler()
{ {
@Override @Override
protected void service(String target, Request jettyRequest, HttpServletRequest request, HttpServletResponse response) protected void service(String target, Request jettyRequest, HttpServletRequest request, HttpServletResponse response)
@ -148,6 +162,11 @@ public class HttpCookieTest extends AbstractHttpClientServerTest
assertEquals(value, cookie.getValue()); assertEquals(value, cookie.getValue());
} }
}); });
startClient(scenario, client ->
{
if (cookieStore != null)
client.setCookieStore(cookieStore);
});
ContentResponse response = client.newRequest("localhost", connector.getLocalPort()) ContentResponse response = client.newRequest("localhost", connector.getLocalPort())
.scheme(scenario.getScheme()) .scheme(scenario.getScheme())

View File

@ -18,6 +18,7 @@
package org.eclipse.jetty.client.http; package org.eclipse.jetty.client.http;
import java.lang.reflect.Method;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@ -37,6 +38,7 @@ import org.eclipse.jetty.client.api.Destination;
import org.eclipse.jetty.client.api.Request; import org.eclipse.jetty.client.api.Request;
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.util.Callback;
import org.hamcrest.Matchers; import org.hamcrest.Matchers;
import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ArgumentsSource; import org.junit.jupiter.params.provider.ArgumentsSource;
@ -54,7 +56,7 @@ public class HttpDestinationOverHTTPTest extends AbstractHttpClientServerTest
{ {
@ParameterizedTest @ParameterizedTest
@ArgumentsSource(ScenarioProvider.class) @ArgumentsSource(ScenarioProvider.class)
public void testFirstAcquireWithEmptyQueue(Scenario scenario) throws Exception public void testAcquireWithEmptyQueue(Scenario scenario) throws Exception
{ {
start(scenario, new EmptyServerHandler()); start(scenario, new EmptyServerHandler());
@ -62,7 +64,29 @@ public class HttpDestinationOverHTTPTest extends AbstractHttpClientServerTest
{ {
destination.start(); destination.start();
DuplexConnectionPool connectionPool = (DuplexConnectionPool)destination.getConnectionPool(); DuplexConnectionPool connectionPool = (DuplexConnectionPool)destination.getConnectionPool();
Connection connection = connectionPool.acquire(); Connection connection = connectionPool.acquire(true);
assertNull(connection);
// There are no queued requests, so no connection should be created.
connection = pollIdleConnection(connectionPool, 1, TimeUnit.SECONDS);
assertNull(connection);
}
}
@ParameterizedTest
@ArgumentsSource(ScenarioProvider.class)
public void testAcquireWithOneExchangeQueued(Scenario scenario) throws Exception
{
start(scenario, new EmptyServerHandler());
try (TestDestination destination = new TestDestination(client, new Origin("http", "localhost", connector.getLocalPort())))
{
destination.start();
TestDestination.TestConnectionPool connectionPool = (TestDestination.TestConnectionPool)destination.getConnectionPool();
// Trigger creation of one connection.
connectionPool.tryCreate(1);
Connection connection = connectionPool.acquire(false);
if (connection == null) if (connection == null)
{ {
// There are no queued requests, so the newly created connection will be idle // There are no queued requests, so the newly created connection will be idle
@ -78,19 +102,22 @@ public class HttpDestinationOverHTTPTest extends AbstractHttpClientServerTest
{ {
start(scenario, new EmptyServerHandler()); start(scenario, new EmptyServerHandler());
try (HttpDestination destination = new DuplexHttpDestination(client, new Origin("http", "localhost", connector.getLocalPort()))) try (TestDestination destination = new TestDestination(client, new Origin("http", "localhost", connector.getLocalPort())))
{ {
destination.start(); destination.start();
TestDestination.TestConnectionPool connectionPool = (TestDestination.TestConnectionPool)destination.getConnectionPool();
DuplexConnectionPool connectionPool = (DuplexConnectionPool)destination.getConnectionPool(); // Trigger creation of one connection.
Connection connection1 = connectionPool.acquire(); connectionPool.tryCreate(1);
Connection connection1 = connectionPool.acquire(true);
if (connection1 == null) if (connection1 == null)
{ {
// There are no queued requests, so the newly created connection will be idle // There are no queued requests, so the newly created connection will be idle
connection1 = peekIdleConnection(connectionPool, 5, TimeUnit.SECONDS); connection1 = peekIdleConnection(connectionPool, 5, TimeUnit.SECONDS);
assertNotNull(connection1); assertNotNull(connection1);
Connection connection2 = connectionPool.acquire(); Connection connection2 = connectionPool.acquire(true);
assertSame(connection1, connection2); assertSame(connection1, connection2);
} }
} }
@ -102,14 +129,14 @@ public class HttpDestinationOverHTTPTest extends AbstractHttpClientServerTest
{ {
start(scenario, new EmptyServerHandler()); start(scenario, new EmptyServerHandler());
final CountDownLatch idleLatch = new CountDownLatch(1); CountDownLatch idleLatch = new CountDownLatch(1);
final CountDownLatch latch = new CountDownLatch(1); CountDownLatch latch = new CountDownLatch(1);
HttpDestination destination = new DuplexHttpDestination(client, new Origin("http", "localhost", connector.getLocalPort())) try (TestDestination destination = new TestDestination(client, new Origin("http", "localhost", connector.getLocalPort()))
{ {
@Override @Override
protected ConnectionPool newConnectionPool(HttpClient client) protected ConnectionPool newConnectionPool(HttpClient client)
{ {
return new DuplexConnectionPool(this, client.getMaxConnectionsPerDestination(), this) return new TestConnectionPool(this, client.getMaxConnectionsPerDestination(), this)
{ {
@Override @Override
protected void onCreated(Connection connection) protected void onCreated(Connection connection)
@ -127,30 +154,37 @@ public class HttpDestinationOverHTTPTest extends AbstractHttpClientServerTest
} }
}; };
} }
}; })
{
destination.start();
TestDestination.TestConnectionPool connectionPool = (TestDestination.TestConnectionPool)destination.getConnectionPool();
destination.start(); // Trigger creation of one connection.
DuplexConnectionPool connectionPool = (DuplexConnectionPool)destination.getConnectionPool(); connectionPool.tryCreate(1);
Connection connection1 = connectionPool.acquire();
// Make sure we entered idleCreated(). // Make sure we entered idleCreated().
assertTrue(idleLatch.await(5, TimeUnit.SECONDS)); assertTrue(idleLatch.await(5, TimeUnit.SECONDS));
// There are no available existing connections, so acquire() // There are no available existing connections, so acquire()
// returns null because we delayed idleCreated() above // returns null because we delayed idleCreated() above.
assertNull(connection1); Connection connection1 = connectionPool.acquire(true);
assertNull(connection1);
// Second attempt also returns null because we delayed idleCreated() above. // Trigger creation of a second connection.
Connection connection2 = connectionPool.acquire(); connectionPool.tryCreate(1);
assertNull(connection2);
latch.countDown(); // Second attempt also returns null because we delayed idleCreated() above.
Connection connection2 = connectionPool.acquire(true);
assertNull(connection2);
// There must be 2 idle connections. latch.countDown();
Connection connection = pollIdleConnection(connectionPool, 5, TimeUnit.SECONDS);
assertNotNull(connection); // There must be 2 idle connections.
connection = pollIdleConnection(connectionPool, 5, TimeUnit.SECONDS); Connection connection = pollIdleConnection(connectionPool, 5, TimeUnit.SECONDS);
assertNotNull(connection); assertNotNull(connection);
connection = pollIdleConnection(connectionPool, 5, TimeUnit.SECONDS);
assertNotNull(connection);
}
} }
@ParameterizedTest @ParameterizedTest
@ -159,23 +193,30 @@ public class HttpDestinationOverHTTPTest extends AbstractHttpClientServerTest
{ {
start(scenario, new EmptyServerHandler()); start(scenario, new EmptyServerHandler());
try (HttpDestination destination = new DuplexHttpDestination(client, new Origin("http", "localhost", connector.getLocalPort()))) try (TestDestination destination = new TestDestination(client, new Origin("http", "localhost", connector.getLocalPort())))
{ {
destination.start(); destination.start();
DuplexConnectionPool connectionPool = (DuplexConnectionPool)destination.getConnectionPool(); TestDestination.TestConnectionPool connectionPool = (TestDestination.TestConnectionPool)destination.getConnectionPool();
Connection connection1 = connectionPool.acquire();
// Trigger creation of one connection.
connectionPool.tryCreate(1);
Connection connection1 = connectionPool.acquire(true);
if (connection1 == null) if (connection1 == null)
{ {
connection1 = peekIdleConnection(connectionPool, 5, TimeUnit.SECONDS); connection1 = peekIdleConnection(connectionPool, 5, TimeUnit.SECONDS);
assertNotNull(connection1); assertNotNull(connection1);
// Acquire the connection to make it active. // Acquire the connection to make it active.
assertSame(connection1, connectionPool.acquire(), "From idle"); assertSame(connection1, connectionPool.acquire(true), "From idle");
} }
destination.process(connection1); // There are no exchanges so process() is a no-op.
Method process = HttpDestination.class.getDeclaredMethod("process", Connection.class);
process.setAccessible(true);
process.invoke(destination, connection1);
destination.release(connection1); destination.release(connection1);
Connection connection2 = connectionPool.acquire(); Connection connection2 = connectionPool.acquire(true);
assertSame(connection1, connection2, "After release"); assertSame(connection1, connection2, "After release");
} }
} }
@ -184,15 +225,20 @@ public class HttpDestinationOverHTTPTest extends AbstractHttpClientServerTest
@ArgumentsSource(ScenarioProvider.class) @ArgumentsSource(ScenarioProvider.class)
public void testIdleConnectionIdleTimeout(Scenario scenario) throws Exception public void testIdleConnectionIdleTimeout(Scenario scenario) throws Exception
{ {
startServer(scenario, new EmptyServerHandler()); start(scenario, new EmptyServerHandler());
long idleTimeout = 1000; long idleTimeout = 1000;
startClient(scenario, httpClient -> httpClient.setIdleTimeout(idleTimeout)); startClient(scenario, httpClient -> httpClient.setIdleTimeout(idleTimeout));
try (HttpDestination destination = new DuplexHttpDestination(client, new Origin("http", "localhost", connector.getLocalPort()))) try (TestDestination destination = new TestDestination(client, new Origin("http", "localhost", connector.getLocalPort())))
{ {
destination.start(); destination.start();
DuplexConnectionPool connectionPool = (DuplexConnectionPool)destination.getConnectionPool(); TestDestination.TestConnectionPool connectionPool = (TestDestination.TestConnectionPool)destination.getConnectionPool();
Connection connection1 = connectionPool.acquire();
// Trigger creation of one connection.
connectionPool.tryCreate(1);
Connection connection1 = connectionPool.acquire(true);
if (connection1 == null) if (connection1 == null)
{ {
connection1 = peekIdleConnection(connectionPool, 5, TimeUnit.SECONDS); connection1 = peekIdleConnection(connectionPool, 5, TimeUnit.SECONDS);
@ -261,7 +307,7 @@ public class HttpDestinationOverHTTPTest extends AbstractHttpClientServerTest
String host = "localhost"; String host = "localhost";
int port = connector.getLocalPort(); int port = connector.getLocalPort();
Request request = client.newRequest(host, port) Request request = client.newRequest(host, port)
.scheme(scenario.getScheme()) .scheme(scenario.getScheme())
.headers(headers -> headers.put(HttpHeader.CONNECTION, HttpHeaderValue.CLOSE)); .headers(headers -> headers.put(HttpHeader.CONNECTION, HttpHeaderValue.CLOSE));
Destination destinationBefore = client.resolveDestination(request); Destination destinationBefore = client.resolveDestination(request);
ContentResponse response = request.send(); ContentResponse response = request.send();
@ -297,7 +343,7 @@ public class HttpDestinationOverHTTPTest extends AbstractHttpClientServerTest
server.stop(); server.stop();
Request request = client.newRequest(host, port).scheme(scenario.getScheme()); Request request = client.newRequest(host, port).scheme(scenario.getScheme());
assertThrows(Exception.class, () -> request.send()); assertThrows(Exception.class, request::send);
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(1); long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(1);
while (!client.getDestinations().isEmpty() && System.nanoTime() < deadline) while (!client.getDestinations().isEmpty() && System.nanoTime() < deadline)
@ -329,4 +375,38 @@ public class HttpDestinationOverHTTPTest extends AbstractHttpClientServerTest
} }
return null; return null;
} }
private static class TestDestination extends DuplexHttpDestination
{
public TestDestination(HttpClient client, Origin origin)
{
super(client, origin);
}
@Override
protected ConnectionPool newConnectionPool(HttpClient client)
{
return new TestConnectionPool(this, client.getMaxConnectionsPerDestination(), this);
}
public static class TestConnectionPool extends DuplexConnectionPool
{
public TestConnectionPool(HttpDestination destination, int maxConnections, Callback requester)
{
super(destination, maxConnections, requester);
}
@Override
public void tryCreate(int maxPending)
{
super.tryCreate(maxPending);
}
@Override
public Connection acquire(boolean create)
{
return super.acquire(create);
}
}
}
} }

View File

@ -254,7 +254,7 @@ For example, if we examine the `http.ini` file in our `start.d` directory create
# jetty.http.selectors=-1 # jetty.http.selectors=-1
## ServerSocketChannel backlog (0 picks platform default) ## ServerSocketChannel backlog (0 picks platform default)
# jetty.http.acceptorQueueSize=0 # jetty.http.acceptQueueSize=0
## Thread priority delta to give to acceptor threads ## Thread priority delta to give to acceptor threads
# jetty.http.acceptorPriorityDelta=0 # jetty.http.acceptorPriorityDelta=0

View File

@ -134,7 +134,7 @@ $ cat start.d/http.ini
# jetty.http.selectors=-1 # jetty.http.selectors=-1
## ServerSocketChannel backlog (0 picks platform default) ## ServerSocketChannel backlog (0 picks platform default)
# jetty.http.acceptorQueueSize=0 # jetty.http.acceptQueueSize=0
## Thread priority delta to give to acceptor threads ## Thread priority delta to give to acceptor threads
# jetty.http.acceptorPriorityDelta=0 # jetty.http.acceptorPriorityDelta=0

View File

@ -33,13 +33,14 @@ _____
[width="100%",cols="12%,9%,15%,6%,21%,10%,6%,21%",options="header",] [width="100%",cols="12%,9%,15%,6%,21%,10%,6%,21%",options="header",]
|======================================================================= |=======================================================================
|Version |Year |Home |Min JVM |Protocols |Servlet |JSP |Status |Version |Year |Home |Min JVM |Protocols |Servlet |JSP |Status
|10 |2019- |Eclipse |11 ^(1)^ |HTTP/1.1 (RFC 7230), HTTP/2 (RFC 7540), WebSocket (RFC 6455, JSR 356), FastCGI |4.0.2 |2.3 |*UNSTABLE / Alpha* |11 |2020- |Eclipse |11 ^(2)^ |HTTP/1.1 (RFC 7230), HTTP/2 (RFC 7540), WebSocket (RFC 6455, JSR 356), FastCGI, *JakartaEE Namespace*^(1)^ |4.0.2 |2.3 |*UNSTABLE / Alpha*
|10 |2019- |Eclipse |11 ^(2)^ |HTTP/1.1 (RFC 7230), HTTP/2 (RFC 7540), WebSocket (RFC 6455, JSR 356), FastCGI |4.0.2 |2.3 |*UNSTABLE / Beta*
|9.4 |2016- |Eclipse |1.8 |HTTP/1.1 (RFC 7230), HTTP/2 (RFC 7540), WebSocket (RFC 6455, JSR 356), FastCGI |3.1 |2.3 |Stable |9.4 |2016- |Eclipse |1.8 |HTTP/1.1 (RFC 7230), HTTP/2 (RFC 7540), WebSocket (RFC 6455, JSR 356), FastCGI |3.1 |2.3 |Stable
|9.3 |2015- |Eclipse |1.8 ^(2)^ |HTTP/1.1 (RFC 7230), HTTP/2 (RFC 7540), WebSocket (RFC 6455, JSR 356), FastCGI |3.1 |2.3 |Deprecated |9.3 |2015- |Eclipse |1.8 ^(3)^ |HTTP/1.1 (RFC 7230), HTTP/2 (RFC 7540), WebSocket (RFC 6455, JSR 356), FastCGI |3.1 |2.3 |Deprecated
|9.2 |2014-2018 |Eclipse |1.7 ^(2)^ |HTTP/1.1 RFC2616, javax.websocket, SPDY v3 |3.1 |2.3 |Deprecated / *End of Life January 2018* |9.2 |2014-2018 |Eclipse |1.7 ^(3)^ |HTTP/1.1 RFC2616, javax.websocket, SPDY v3 |3.1 |2.3 |Deprecated / *End of Life January 2018*
|9.1 |2013-2014 |Eclipse |1.7 ^(2)^ |HTTP/1.1 RFC2616 |3.1 |2.3 |Deprecated / *End of Life May 2014* |9.1 |2013-2014 |Eclipse |1.7 ^(3)^ |HTTP/1.1 RFC2616 |3.1 |2.3 |Deprecated / *End of Life May 2014*
|9.0 |2013-2013 |Eclipse |1.7 ^(2)^ |HTTP/1.1 RFC2616 |3.1-beta |2.3 |Deprecated / *End of Life November 2013* |9.0 |2013-2013 |Eclipse |1.7 ^(3)^ |HTTP/1.1 RFC2616 |3.1-beta |2.3 |Deprecated / *End of Life November 2013*
|8 |2009-2014 |Eclipse/Codehaus |1.6 ^(2)^ |HTTP/1.1 RFC2616, WebSocket RFC 6455, SPDY v3 |3.0 |2.2 |Deprecated / *End of Life November 2014* |8 |2009-2014 |Eclipse/Codehaus |1.6 ^(3)^ |HTTP/1.1 RFC2616, WebSocket RFC 6455, SPDY v3 |3.0 |2.2 |Deprecated / *End of Life November 2014*
|7 |2008-2014 |Eclipse/Codehaus |1.5 |HTTP/1.1 RFC2616, WebSocket RFC 6455, SPDY v3 |2.5 |2.1 |Deprecated / *End of Life November 2014* |7 |2008-2014 |Eclipse/Codehaus |1.5 |HTTP/1.1 RFC2616, WebSocket RFC 6455, SPDY v3 |2.5 |2.1 |Deprecated / *End of Life November 2014*
|6 |2006-2010 |Codehaus |1.4-1.5 |HTTP/1.1 RFC2616 |2.5 |2.0 |Deprecated / *End of Life November 2010* |6 |2006-2010 |Codehaus |1.4-1.5 |HTTP/1.1 RFC2616 |2.5 |2.0 |Deprecated / *End of Life November 2010*
|5 |2003-2009 |Sourceforge |1.2-1.5 |HTTP/1.1 RFC2616 |2.4 |2.0 |Antique |5 |2003-2009 |Sourceforge |1.2-1.5 |HTTP/1.1 RFC2616 |2.4 |2.0 |Antique
@ -49,5 +50,6 @@ _____
|1 |1995-1998 |Mortbay |1.0 |HTTP/1.0 RFC1945 |- |- |Mythical |1 |1995-1998 |Mortbay |1.0 |HTTP/1.0 RFC1945 |- |- |Mythical
|======================================================================= |=======================================================================
1. JPMS module support is optional 1. Due to Oracle's ownership of the "Java" trademark, usage of the javax.* namespace has been restricted and the jakarta.* namespace link:https://www.eclipse.org/lists/jakartaee-platform-dev/msg00029.html[was adopted] by the Eclipse Foundation.
2. JDK9 and newer is not supported if using MultiRelease JAR Files, or Bytecode / Annotation scanning. 2. JPMS module support is optional
3. JDK9 and newer is not supported if using MultiRelease JAR Files, or Bytecode / Annotation scanning.

View File

@ -246,7 +246,7 @@ public class HttpConnectionOverFCGI extends AbstractConnection implements IConne
{ {
if (closed.compareAndSet(false, true)) if (closed.compareAndSet(false, true))
{ {
getHttpDestination().close(this); getHttpDestination().remove(this);
abort(failure); abort(failure);

View File

@ -0,0 +1,66 @@
//
// ========================================================================
// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under
// the terms of the Eclipse Public License 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0
//
// This Source Code may also be made available under the following
// Secondary Licenses when the conditions for such availability set
// forth in the Eclipse Public License, v. 2.0 are satisfied:
// the Apache License v2.0 which is available at
// https://www.apache.org/licenses/LICENSE-2.0
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.http.pathmap;
import java.util.Objects;
public abstract class AbstractPathSpec implements PathSpec
{
@Override
public int compareTo(PathSpec other)
{
// Grouping (increasing)
int diff = getGroup().ordinal() - other.getGroup().ordinal();
if (diff != 0)
return diff;
// Spec Length (decreasing)
diff = other.getSpecLength() - getSpecLength();
if (diff != 0)
return diff;
// Path Spec Name (alphabetical)
return getDeclaration().compareTo(other.getDeclaration());
}
@Override
public final boolean equals(Object obj)
{
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
return compareTo((AbstractPathSpec)obj) == 0;
}
@Override
public final int hashCode()
{
return Objects.hash(getDeclaration());
}
@Override
public String toString()
{
return String.format("%s@%s{%s}", getClass().getSimpleName(), Integer.toHexString(hashCode()), getDeclaration());
}
}

View File

@ -20,6 +20,7 @@ package org.eclipse.jetty.http.pathmap;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Comparator;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
@ -46,7 +47,7 @@ import org.slf4j.LoggerFactory;
public class PathMappings<E> implements Iterable<MappedResource<E>>, Dumpable public class PathMappings<E> implements Iterable<MappedResource<E>>, Dumpable
{ {
private static final Logger LOG = LoggerFactory.getLogger(PathMappings.class); private static final Logger LOG = LoggerFactory.getLogger(PathMappings.class);
private final Set<MappedResource<E>> _mappings = new TreeSet<>(); private final Set<MappedResource<E>> _mappings = new TreeSet<>(Comparator.comparing(MappedResource::getPathSpec));
private Trie<MappedResource<E>> _exactMap = new ArrayTernaryTrie<>(false); private Trie<MappedResource<E>> _exactMap = new ArrayTernaryTrie<>(false);
private Trie<MappedResource<E>> _prefixMap = new ArrayTernaryTrie<>(false); private Trie<MappedResource<E>> _prefixMap = new ArrayTernaryTrie<>(false);
@ -100,7 +101,7 @@ public class PathMappings<E> implements Iterable<MappedResource<E>>, Dumpable
List<MappedResource<E>> ret = new ArrayList<>(); List<MappedResource<E>> ret = new ArrayList<>();
for (MappedResource<E> mr : _mappings) for (MappedResource<E> mr : _mappings)
{ {
switch (mr.getPathSpec().group) switch (mr.getPathSpec().getGroup())
{ {
case ROOT: case ROOT:
if (isRootPath) if (isRootPath)
@ -225,7 +226,7 @@ public class PathMappings<E> implements Iterable<MappedResource<E>>, Dumpable
public boolean put(PathSpec pathSpec, E resource) public boolean put(PathSpec pathSpec, E resource)
{ {
MappedResource<E> entry = new MappedResource<>(pathSpec, resource); MappedResource<E> entry = new MappedResource<>(pathSpec, resource);
switch (pathSpec.group) switch (pathSpec.getGroup())
{ {
case EXACT: case EXACT:
String exact = pathSpec.getPrefix(); String exact = pathSpec.getPrefix();
@ -260,16 +261,21 @@ public class PathMappings<E> implements Iterable<MappedResource<E>>, Dumpable
@SuppressWarnings("incomplete-switch") @SuppressWarnings("incomplete-switch")
public boolean remove(PathSpec pathSpec) public boolean remove(PathSpec pathSpec)
{ {
switch (pathSpec.group) String prefix = pathSpec.getPrefix();
String suffix = pathSpec.getSuffix();
switch (pathSpec.getGroup())
{ {
case EXACT: case EXACT:
_exactMap.remove(pathSpec.getPrefix()); if (prefix != null)
_exactMap.remove(prefix);
break; break;
case PREFIX_GLOB: case PREFIX_GLOB:
_prefixMap.remove(pathSpec.getPrefix()); if (prefix != null)
_prefixMap.remove(prefix);
break; break;
case SUFFIX_GLOB: case SUFFIX_GLOB:
_suffixMap.remove(pathSpec.getSuffix()); if (suffix != null)
_suffixMap.remove(suffix);
break; break;
default: default:
break; break;

View File

@ -19,72 +19,25 @@
package org.eclipse.jetty.http.pathmap; package org.eclipse.jetty.http.pathmap;
/** /**
* The base PathSpec, what all other path specs are based on * A path specification is a URI path template that can be matched against.
* <p>
* Implementors <i>must</i> override {@link Object#equals(Object)} and {@link Object#hashCode()}.
*/ */
public abstract class PathSpec implements Comparable<PathSpec> public interface PathSpec extends Comparable<PathSpec>
{ {
protected String pathSpec; /**
protected PathSpecGroup group; * The length of the spec.
protected int pathDepth; *
protected int specLength; * @return the length of the spec.
protected String prefix; */
protected String suffix; int getSpecLength();
@Override /**
public int compareTo(PathSpec other) * The spec group.
{ *
// Grouping (increasing) * @return the spec group.
int diff = this.group.ordinal() - other.group.ordinal(); */
if (diff != 0) PathSpecGroup getGroup();
{
return diff;
}
// Spec Length (decreasing)
diff = other.specLength - this.specLength;
if (diff != 0)
{
return diff;
}
// Path Spec Name (alphabetical)
return this.pathSpec.compareTo(other.pathSpec);
}
@Override
public boolean equals(Object obj)
{
if (this == obj)
{
return true;
}
if (obj == null)
{
return false;
}
if (getClass() != obj.getClass())
{
return false;
}
PathSpec other = (PathSpec)obj;
if (pathSpec == null)
{
if (other.pathSpec != null)
{
return false;
}
}
else if (!pathSpec.equals(other.pathSpec))
{
return false;
}
return true;
}
public PathSpecGroup getGroup()
{
return group;
}
/** /**
* Get the number of path elements that this path spec declares. * Get the number of path elements that this path spec declares.
@ -93,10 +46,7 @@ public abstract class PathSpec implements Comparable<PathSpec>
* *
* @return the depth of the path segments that this spec declares * @return the depth of the path segments that this spec declares
*/ */
public int getPathDepth() int getPathDepth();
{
return pathDepth;
}
/** /**
* Return the portion of the path that is after the path spec. * Return the portion of the path that is after the path spec.
@ -104,7 +54,7 @@ public abstract class PathSpec implements Comparable<PathSpec>
* @param path the path to match against * @param path the path to match against
* @return the path info portion of the string * @return the path info portion of the string
*/ */
public abstract String getPathInfo(String path); String getPathInfo(String path);
/** /**
* Return the portion of the path that matches a path spec. * Return the portion of the path that matches a path spec.
@ -112,55 +62,28 @@ public abstract class PathSpec implements Comparable<PathSpec>
* @param path the path to match against * @param path the path to match against
* @return the match, or null if no match at all * @return the match, or null if no match at all
*/ */
public abstract String getPathMatch(String path); String getPathMatch(String path);
/** /**
* The as-provided path spec. * The as-provided path spec.
* *
* @return the as-provided path spec * @return the as-provided path spec
*/ */
public String getDeclaration() String getDeclaration();
{
return pathSpec;
}
/** /**
* A simple prefix match for the pathspec or null * A simple prefix match for the pathspec or null
* *
* @return A simple prefix match for the pathspec or null * @return A simple prefix match for the pathspec or null
*/ */
public String getPrefix() String getPrefix();
{
return prefix;
}
/** /**
* A simple suffix match for the pathspec or null * A simple suffix match for the pathspec or null
* *
* @return A simple suffix match for the pathspec or null * @return A simple suffix match for the pathspec or null
*/ */
public String getSuffix() String getSuffix();
{
return suffix;
}
/**
* Get the relative path.
*
* @param base the base the path is relative to
* @param path the additional path
* @return the base plus path with pathSpec portion removed
*/
public abstract String getRelativePath(String base, String path);
@Override
public int hashCode()
{
final int prime = 31;
int result = 1;
result = (prime * result) + ((pathSpec == null) ? 0 : pathSpec.hashCode());
return result;
}
/** /**
* Test to see if the provided path matches this path spec * Test to see if the provided path matches this path spec
@ -168,17 +91,5 @@ public abstract class PathSpec implements Comparable<PathSpec>
* @param path the path to test * @param path the path to test
* @return true if the path matches this path spec, false otherwise * @return true if the path matches this path spec, false otherwise
*/ */
public abstract boolean matches(String path); boolean matches(String path);
@Override
public String toString()
{
StringBuilder str = new StringBuilder();
str.append(this.getClass().getSimpleName()).append("[\"");
str.append(pathSpec);
str.append("\",pathDepth=").append(pathDepth);
str.append(",group=").append(group);
str.append("]");
return str.toString();
}
} }

View File

@ -26,8 +26,8 @@ package org.eclipse.jetty.http.pathmap;
* Search Order: * Search Order:
* <ol> * <ol>
* <li>{@link PathSpecGroup#ordinal()} [increasing]</li> * <li>{@link PathSpecGroup#ordinal()} [increasing]</li>
* <li>{@link PathSpec#specLength} [decreasing]</li> * <li>{@link PathSpec#getSpecLength()} [decreasing]</li>
* <li>{@link PathSpec#pathSpec} [natural sort order]</li> * <li>{@link PathSpec#getDeclaration()} [natural sort order]</li>
* </ol> * </ol>
*/ */
public enum PathSpecGroup public enum PathSpecGroup

View File

@ -21,27 +21,30 @@ package org.eclipse.jetty.http.pathmap;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
public class RegexPathSpec extends PathSpec public class RegexPathSpec extends AbstractPathSpec
{ {
protected Pattern pattern; private final String _declaration;
private final PathSpecGroup _group;
protected RegexPathSpec() private final int _pathDepth;
{ private final int _specLength;
super(); private final Pattern _pattern;
}
public RegexPathSpec(String regex) public RegexPathSpec(String regex)
{ {
super.pathSpec = regex; String declaration;
if (regex.startsWith("regex|")) if (regex.startsWith("regex|"))
super.pathSpec = regex.substring("regex|".length()); declaration = regex.substring("regex|".length());
this.pathDepth = 0; else
this.specLength = pathSpec.length(); declaration = regex;
int specLength = declaration.length();
// build up a simple signature we can use to identify the grouping // build up a simple signature we can use to identify the grouping
boolean inGrouping = false; boolean inGrouping = false;
StringBuilder signature = new StringBuilder(); StringBuilder signature = new StringBuilder();
for (char c : pathSpec.toCharArray())
int pathDepth = 0;
for (int i = 0; i < declaration.length(); i++)
{ {
char c = declaration.charAt(i);
switch (c) switch (c)
{ {
case '[': case '[':
@ -56,54 +59,64 @@ public class RegexPathSpec extends PathSpec
break; break;
case '/': case '/':
if (!inGrouping) if (!inGrouping)
{ pathDepth++;
this.pathDepth++;
}
break; break;
default: default:
if (!inGrouping) if (!inGrouping && Character.isLetterOrDigit(c))
{ signature.append('l'); // literal (exact)
if (Character.isLetterOrDigit(c))
{
signature.append('l'); // literal (exact)
}
}
break; break;
} }
} }
this.pattern = Pattern.compile(pathSpec); Pattern pattern = Pattern.compile(declaration);
// Figure out the grouping based on the signature // Figure out the grouping based on the signature
String sig = signature.toString(); String sig = signature.toString();
PathSpecGroup group;
if (Pattern.matches("^l*$", sig)) if (Pattern.matches("^l*$", sig))
{ group = PathSpecGroup.EXACT;
this.group = PathSpecGroup.EXACT;
}
else if (Pattern.matches("^l*g+", sig)) else if (Pattern.matches("^l*g+", sig))
{ group = PathSpecGroup.PREFIX_GLOB;
this.group = PathSpecGroup.PREFIX_GLOB;
}
else if (Pattern.matches("^g+l+$", sig)) else if (Pattern.matches("^g+l+$", sig))
{ group = PathSpecGroup.SUFFIX_GLOB;
this.group = PathSpecGroup.SUFFIX_GLOB;
}
else else
{ group = PathSpecGroup.MIDDLE_GLOB;
this.group = PathSpecGroup.MIDDLE_GLOB;
} _declaration = declaration;
_group = group;
_pathDepth = pathDepth;
_specLength = specLength;
_pattern = pattern;
} }
public Matcher getMatcher(String path) protected Matcher getMatcher(String path)
{ {
return this.pattern.matcher(path); return _pattern.matcher(path);
}
@Override
public int getSpecLength()
{
return _specLength;
}
@Override
public PathSpecGroup getGroup()
{
return _group;
}
@Override
public int getPathDepth()
{
return _pathDepth;
} }
@Override @Override
public String getPathInfo(String path) public String getPathInfo(String path)
{ {
// Path Info only valid for PREFIX_GLOB types // Path Info only valid for PREFIX_GLOB types
if (group == PathSpecGroup.PREFIX_GLOB) if (_group == PathSpecGroup.PREFIX_GLOB)
{ {
Matcher matcher = getMatcher(path); Matcher matcher = getMatcher(path);
if (matcher.matches()) if (matcher.matches())
@ -112,13 +125,9 @@ public class RegexPathSpec extends PathSpec
{ {
String pathInfo = matcher.group(1); String pathInfo = matcher.group(1);
if ("".equals(pathInfo)) if ("".equals(pathInfo))
{
return "/"; return "/";
}
else else
{
return pathInfo; return pathInfo;
}
} }
} }
} }
@ -137,9 +146,7 @@ public class RegexPathSpec extends PathSpec
if (idx > 0) if (idx > 0)
{ {
if (path.charAt(idx - 1) == '/') if (path.charAt(idx - 1) == '/')
{
idx--; idx--;
}
return path.substring(0, idx); return path.substring(0, idx);
} }
} }
@ -148,18 +155,29 @@ public class RegexPathSpec extends PathSpec
return null; return null;
} }
public Pattern getPattern() @Override
public String getDeclaration()
{ {
return this.pattern; return _declaration;
} }
@Override @Override
public String getRelativePath(String base, String path) public String getPrefix()
{ {
// TODO Auto-generated method stub
return null; return null;
} }
@Override
public String getSuffix()
{
return null;
}
public Pattern getPattern()
{
return _pattern;
}
@Override @Override
public boolean matches(final String path) public boolean matches(final String path)
{ {

View File

@ -23,11 +23,17 @@ import org.eclipse.jetty.util.URIUtil;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
public class ServletPathSpec extends PathSpec public class ServletPathSpec extends AbstractPathSpec
{ {
private static final Logger LOG = LoggerFactory.getLogger(ServletPathSpec.class); private static final Logger LOG = LoggerFactory.getLogger(ServletPathSpec.class);
private final String _declaration;
private final PathSpecGroup _group;
private final int _pathDepth;
private final int _specLength;
private final String _prefix;
private final String _suffix;
/** /**
* If a servlet or filter path mapping isn't a suffix mapping, ensure * If a servlet or filter path mapping isn't a suffix mapping, ensure
* it starts with '/' * it starts with '/'
@ -197,70 +203,79 @@ public class ServletPathSpec extends PathSpec
// The Root Path Spec // The Root Path Spec
if (servletPathSpec.isEmpty()) if (servletPathSpec.isEmpty())
{ {
super.pathSpec = ""; _declaration = "";
super.pathDepth = -1; // force this to be at the end of the sort order _group = PathSpecGroup.ROOT;
this.specLength = 1; _pathDepth = -1; // Set pathDepth to -1 to force this to be at the end of the sort order.
this.group = PathSpecGroup.ROOT; _specLength = 1;
_prefix = null;
_suffix = null;
return; return;
} }
// The Default Path Spec // The Default Path Spec
if ("/".equals(servletPathSpec)) if ("/".equals(servletPathSpec))
{ {
super.pathSpec = "/"; _declaration = "/";
super.pathDepth = -1; // force this to be at the end of the sort order _group = PathSpecGroup.DEFAULT;
this.specLength = 1; _pathDepth = -1; // Set pathDepth to -1 to force this to be at the end of the sort order.
this.group = PathSpecGroup.DEFAULT; _specLength = 1;
_prefix = null;
_suffix = null;
return; return;
} }
this.specLength = servletPathSpec.length(); int specLength = servletPathSpec.length();
super.pathDepth = 0; PathSpecGroup group;
char lastChar = servletPathSpec.charAt(specLength - 1); String prefix;
String suffix;
// prefix based // prefix based
if (servletPathSpec.charAt(0) == '/' && servletPathSpec.endsWith("/*")) if (servletPathSpec.charAt(0) == '/' && servletPathSpec.endsWith("/*"))
{ {
this.group = PathSpecGroup.PREFIX_GLOB; group = PathSpecGroup.PREFIX_GLOB;
this.prefix = servletPathSpec.substring(0, specLength - 2); prefix = servletPathSpec.substring(0, specLength - 2);
suffix = null;
} }
// suffix based // suffix based
else if (servletPathSpec.charAt(0) == '*' && servletPathSpec.length() > 1) else if (servletPathSpec.charAt(0) == '*' && servletPathSpec.length() > 1)
{ {
this.group = PathSpecGroup.SUFFIX_GLOB; group = PathSpecGroup.SUFFIX_GLOB;
this.suffix = servletPathSpec.substring(2, specLength); prefix = null;
suffix = servletPathSpec.substring(2, specLength);
} }
else else
{ {
this.group = PathSpecGroup.EXACT; group = PathSpecGroup.EXACT;
this.prefix = servletPathSpec; prefix = servletPathSpec;
suffix = null;
if (servletPathSpec.endsWith("*")) if (servletPathSpec.endsWith("*"))
{ {
LOG.warn("Suspicious URL pattern: '{}'; see sections 12.1 and 12.2 of the Servlet specification", LOG.warn("Suspicious URL pattern: '{}'; see sections 12.1 and 12.2 of the Servlet specification",
servletPathSpec); servletPathSpec);
} }
} }
int pathDepth = 0;
for (int i = 0; i < specLength; i++) for (int i = 0; i < specLength; i++)
{ {
int cp = servletPathSpec.codePointAt(i); int cp = servletPathSpec.codePointAt(i);
if (cp < 128) if (cp < 128)
{ {
char c = (char)cp; char c = (char)cp;
switch (c) if (c == '/')
{ pathDepth++;
case '/':
super.pathDepth++;
break;
default:
break;
}
} }
} }
super.pathSpec = servletPathSpec; _declaration = servletPathSpec;
_group = group;
_pathDepth = pathDepth;
_specLength = specLength;
_prefix = prefix;
_suffix = suffix;
} }
private void assertValidServletPathSpec(String servletPathSpec) private static void assertValidServletPathSpec(String servletPathSpec)
{ {
if ((servletPathSpec == null) || servletPathSpec.equals("")) if ((servletPathSpec == null) || servletPathSpec.equals(""))
{ {
@ -293,16 +308,12 @@ public class ServletPathSpec extends PathSpec
int idx = servletPathSpec.indexOf('/'); int idx = servletPathSpec.indexOf('/');
// cannot have path separator // cannot have path separator
if (idx >= 0) if (idx >= 0)
{
throw new IllegalArgumentException("Servlet Spec 12.2 violation: suffix based path spec cannot have path separators: bad spec \"" + servletPathSpec + "\""); throw new IllegalArgumentException("Servlet Spec 12.2 violation: suffix based path spec cannot have path separators: bad spec \"" + servletPathSpec + "\"");
}
idx = servletPathSpec.indexOf('*', 2); idx = servletPathSpec.indexOf('*', 2);
// only allowed to have 1 glob '*', at the start of the path spec // only allowed to have 1 glob '*', at the start of the path spec
if (idx >= 1) if (idx >= 1)
{
throw new IllegalArgumentException("Servlet Spec 12.2 violation: suffix based path spec cannot have multiple glob '*': bad spec \"" + servletPathSpec + "\""); throw new IllegalArgumentException("Servlet Spec 12.2 violation: suffix based path spec cannot have multiple glob '*': bad spec \"" + servletPathSpec + "\"");
}
} }
else else
{ {
@ -310,18 +321,36 @@ public class ServletPathSpec extends PathSpec
} }
} }
@Override
public int getSpecLength()
{
return _specLength;
}
@Override
public PathSpecGroup getGroup()
{
return _group;
}
@Override
public int getPathDepth()
{
return _pathDepth;
}
@Override @Override
public String getPathInfo(String path) public String getPathInfo(String path)
{ {
switch (group) switch (_group)
{ {
case ROOT: case ROOT:
return path; return path;
case PREFIX_GLOB: case PREFIX_GLOB:
if (path.length() == (specLength - 2)) if (path.length() == (_specLength - 2))
return null; return null;
return path.substring(specLength - 2); return path.substring(_specLength - 2);
default: default:
return null; return null;
@ -331,23 +360,23 @@ public class ServletPathSpec extends PathSpec
@Override @Override
public String getPathMatch(String path) public String getPathMatch(String path)
{ {
switch (group) switch (_group)
{ {
case ROOT: case ROOT:
return ""; return "";
case EXACT: case EXACT:
if (pathSpec.equals(path)) if (_declaration.equals(path))
return path; return path;
return null; return null;
case PREFIX_GLOB: case PREFIX_GLOB:
if (isWildcardMatch(path)) if (isWildcardMatch(path))
return path.substring(0, specLength - 2); return path.substring(0, _specLength - 2);
return null; return null;
case SUFFIX_GLOB: case SUFFIX_GLOB:
if (path.regionMatches(path.length() - (specLength - 1), pathSpec, 1, specLength - 1)) if (path.regionMatches(path.length() - (_specLength - 1), _declaration, 1, _specLength - 1))
return path; return path;
return null; return null;
@ -360,65 +389,43 @@ public class ServletPathSpec extends PathSpec
} }
@Override @Override
public String getRelativePath(String base, String path) public String getDeclaration()
{ {
String info = getPathInfo(path); return _declaration;
if (info == null) }
{
info = path;
}
if (info.startsWith("./")) @Override
{ public String getPrefix()
info = info.substring(2); {
} return _prefix;
if (base.endsWith(URIUtil.SLASH)) }
{
if (info.startsWith(URIUtil.SLASH)) @Override
{ public String getSuffix()
path = base + info.substring(1); {
} return _suffix;
else
{
path = base + info;
}
}
else if (info.startsWith(URIUtil.SLASH))
{
path = base + info;
}
else
{
path = base + URIUtil.SLASH + info;
}
return path;
} }
private boolean isWildcardMatch(String path) private boolean isWildcardMatch(String path)
{ {
// For a spec of "/foo/*" match "/foo" , "/foo/..." but not "/foobar" // For a spec of "/foo/*" match "/foo" , "/foo/..." but not "/foobar"
int cpl = specLength - 2; int cpl = _specLength - 2;
if ((group == PathSpecGroup.PREFIX_GLOB) && (path.regionMatches(0, pathSpec, 0, cpl))) if ((_group == PathSpecGroup.PREFIX_GLOB) && (path.regionMatches(0, _declaration, 0, cpl)))
{ return (path.length() == cpl) || ('/' == path.charAt(cpl));
if ((path.length() == cpl) || ('/' == path.charAt(cpl)))
{
return true;
}
}
return false; return false;
} }
@Override @Override
public boolean matches(String path) public boolean matches(String path)
{ {
switch (group) switch (_group)
{ {
case EXACT: case EXACT:
return pathSpec.equals(path); return _declaration.equals(path);
case PREFIX_GLOB: case PREFIX_GLOB:
return isWildcardMatch(path); return isWildcardMatch(path);
case SUFFIX_GLOB: case SUFFIX_GLOB:
return path.regionMatches((path.length() - specLength) + 1, pathSpec, 1, specLength - 1); return path.regionMatches((path.length() - _specLength) + 1, _declaration, 1, _specLength - 1);
case ROOT: case ROOT:
// Only "/" matches // Only "/" matches
return ("/".equals(path)); return ("/".equals(path));

View File

@ -30,7 +30,6 @@ import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import org.eclipse.jetty.util.TypeUtil; import org.eclipse.jetty.util.TypeUtil;
import org.eclipse.jetty.util.UrlEncoded;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -39,7 +38,7 @@ import org.slf4j.LoggerFactory;
* *
* @see <a href="https://tools.ietf.org/html/rfc6570">URI Templates (Level 1)</a> * @see <a href="https://tools.ietf.org/html/rfc6570">URI Templates (Level 1)</a>
*/ */
public class UriTemplatePathSpec extends RegexPathSpec public class UriTemplatePathSpec extends AbstractPathSpec
{ {
private static final Logger LOG = LoggerFactory.getLogger(UriTemplatePathSpec.class); private static final Logger LOG = LoggerFactory.getLogger(UriTemplatePathSpec.class);
@ -63,49 +62,46 @@ public class UriTemplatePathSpec extends RegexPathSpec
FORBIDDEN_SEGMENTS.add("//"); FORBIDDEN_SEGMENTS.add("//");
} }
private String[] variables; private final String _declaration;
private final PathSpecGroup _group;
private final int _pathDepth;
private final int _specLength;
private final Pattern _pattern;
private final String[] _variables;
/**
* The logical (simplified) declaration
*/
private final String _logicalDeclaration;
public UriTemplatePathSpec(String rawSpec) public UriTemplatePathSpec(String rawSpec)
{ {
super();
Objects.requireNonNull(rawSpec, "Path Param Spec cannot be null"); Objects.requireNonNull(rawSpec, "Path Param Spec cannot be null");
if ("".equals(rawSpec) || "/".equals(rawSpec)) if ("".equals(rawSpec) || "/".equals(rawSpec))
{ {
super.pathSpec = "/"; _declaration = "/";
super.pattern = Pattern.compile("^/$"); _group = PathSpecGroup.EXACT;
super.pathDepth = 1; _pathDepth = 1;
this.specLength = 1; _specLength = 1;
this.variables = new String[0]; _pattern = Pattern.compile("^/$");
this.group = PathSpecGroup.EXACT; _variables = new String[0];
_logicalDeclaration = "/";
return; return;
} }
if (rawSpec.charAt(0) != '/') if (rawSpec.charAt(0) != '/')
{ {
// path specs must start with '/' // path specs must start with '/'
StringBuilder err = new StringBuilder(); throw new IllegalArgumentException("Syntax Error: path spec \"" + rawSpec + "\" must start with '/'");
err.append("Syntax Error: path spec \"");
err.append(rawSpec);
err.append("\" must start with '/'");
throw new IllegalArgumentException(err.toString());
} }
for (String forbidden : FORBIDDEN_SEGMENTS) for (String forbidden : FORBIDDEN_SEGMENTS)
{ {
if (rawSpec.contains(forbidden)) if (rawSpec.contains(forbidden))
{ throw new IllegalArgumentException("Syntax Error: segment " + forbidden + " is forbidden in path spec: " + rawSpec);
StringBuilder err = new StringBuilder();
err.append("Syntax Error: segment ");
err.append(forbidden);
err.append(" is forbidden in path spec: ");
err.append(rawSpec);
throw new IllegalArgumentException(err.toString());
}
} }
this.pathSpec = rawSpec; String declaration = rawSpec;
StringBuilder regex = new StringBuilder(); StringBuilder regex = new StringBuilder();
regex.append('^'); regex.append('^');
@ -113,7 +109,8 @@ public class UriTemplatePathSpec extends RegexPathSpec
// split up into path segments (ignoring the first slash that will always be empty) // split up into path segments (ignoring the first slash that will always be empty)
String[] segments = rawSpec.substring(1).split("/"); String[] segments = rawSpec.substring(1).split("/");
char[] segmentSignature = new char[segments.length]; char[] segmentSignature = new char[segments.length];
this.pathDepth = segments.length; StringBuilder logicalSignature = new StringBuilder();
int pathDepth = segments.length;
for (int i = 0; i < segments.length; i++) for (int i = 0; i < segments.length; i++)
{ {
String segment = segments[i]; String segment = segments[i];
@ -126,17 +123,13 @@ public class UriTemplatePathSpec extends RegexPathSpec
if (varNames.contains(variable)) if (varNames.contains(variable))
{ {
// duplicate variable names // duplicate variable names
StringBuilder err = new StringBuilder(); throw new IllegalArgumentException("Syntax Error: variable " + variable + " is duplicated in path spec: " + rawSpec);
err.append("Syntax Error: variable ");
err.append(variable);
err.append(" is duplicated in path spec: ");
err.append(rawSpec);
throw new IllegalArgumentException(err.toString());
} }
assertIsValidVariableLiteral(variable); assertIsValidVariableLiteral(variable, declaration);
segmentSignature[i] = 'v'; // variable segmentSignature[i] = 'v'; // variable
logicalSignature.append("/*");
// valid variable name // valid variable name
varNames.add(variable); varNames.add(variable);
// build regex // build regex
@ -145,46 +138,31 @@ public class UriTemplatePathSpec extends RegexPathSpec
else if (mat.find(0)) else if (mat.find(0))
{ {
// variable exists as partial segment // variable exists as partial segment
StringBuilder err = new StringBuilder(); throw new IllegalArgumentException("Syntax Error: variable " + mat.group() + " must exist as entire path segment: " + rawSpec);
err.append("Syntax Error: variable ");
err.append(mat.group());
err.append(" must exist as entire path segment: ");
err.append(rawSpec);
throw new IllegalArgumentException(err.toString());
} }
else if ((segment.indexOf('{') >= 0) || (segment.indexOf('}') >= 0)) else if ((segment.indexOf('{') >= 0) || (segment.indexOf('}') >= 0))
{ {
// variable is split with a path separator // variable is split with a path separator
StringBuilder err = new StringBuilder(); throw new IllegalArgumentException("Syntax Error: invalid path segment /" + segment + "/ variable declaration incomplete: " + rawSpec);
err.append("Syntax Error: invalid path segment /");
err.append(segment);
err.append("/ variable declaration incomplete: ");
err.append(rawSpec);
throw new IllegalArgumentException(err.toString());
} }
else if (segment.indexOf('*') >= 0) else if (segment.indexOf('*') >= 0)
{ {
// glob segment // glob segment
StringBuilder err = new StringBuilder(); throw new IllegalArgumentException("Syntax Error: path segment /" + segment + "/ contains a wildcard symbol (not supported by this uri-template implementation): " + rawSpec);
err.append("Syntax Error: path segment /");
err.append(segment);
err.append("/ contains a wildcard symbol (not supported by this uri-template implementation): ");
err.append(rawSpec);
throw new IllegalArgumentException(err.toString());
} }
else else
{ {
// valid path segment // valid path segment
segmentSignature[i] = 'e'; // exact segmentSignature[i] = 'e'; // exact
logicalSignature.append('/').append(segment);
// build regex // build regex
regex.append('/'); regex.append('/');
// escape regex special characters // escape regex special characters
for (char c : segment.toCharArray()) for (int j = 0; j < segment.length(); j++)
{ {
char c = segment.charAt(j);
if ((c == '.') || (c == '[') || (c == ']') || (c == '\\')) if ((c == '.') || (c == '[') || (c == ']') || (c == '\\'))
{
regex.append('\\'); regex.append('\\');
}
regex.append(c); regex.append(c);
} }
} }
@ -194,40 +172,42 @@ public class UriTemplatePathSpec extends RegexPathSpec
if (rawSpec.charAt(rawSpec.length() - 1) == '/') if (rawSpec.charAt(rawSpec.length() - 1) == '/')
{ {
regex.append('/'); regex.append('/');
logicalSignature.append('/');
} }
regex.append('$'); regex.append('$');
this.pattern = Pattern.compile(regex.toString()); Pattern pattern = Pattern.compile(regex.toString());
int varcount = varNames.size(); int varcount = varNames.size();
this.variables = varNames.toArray(new String[varcount]); String[] variables = varNames.toArray(new String[varcount]);
// Convert signature to group // Convert signature to group
String sig = String.valueOf(segmentSignature); String sig = String.valueOf(segmentSignature);
PathSpecGroup group;
if (Pattern.matches("^e*$", sig)) if (Pattern.matches("^e*$", sig))
{ group = PathSpecGroup.EXACT;
this.group = PathSpecGroup.EXACT;
}
else if (Pattern.matches("^e*v+", sig)) else if (Pattern.matches("^e*v+", sig))
{ group = PathSpecGroup.PREFIX_GLOB;
this.group = PathSpecGroup.PREFIX_GLOB;
}
else if (Pattern.matches("^v+e+", sig)) else if (Pattern.matches("^v+e+", sig))
{ group = PathSpecGroup.SUFFIX_GLOB;
this.group = PathSpecGroup.SUFFIX_GLOB;
}
else else
{ group = PathSpecGroup.MIDDLE_GLOB;
this.group = PathSpecGroup.MIDDLE_GLOB;
} _declaration = declaration;
_group = group;
_pathDepth = pathDepth;
_specLength = declaration.length();
_pattern = pattern;
_variables = variables;
_logicalDeclaration = logicalSignature.toString();
} }
/** /**
* Validate variable literal name, per RFC6570, Section 2.1 Literals * Validate variable literal name, per RFC6570, Section 2.1 Literals
*/ */
private void assertIsValidVariableLiteral(String variable) private static void assertIsValidVariableLiteral(String variable, String declaration)
{ {
int len = variable.length(); int len = variable.length();
@ -241,16 +221,12 @@ public class UriTemplatePathSpec extends RegexPathSpec
i += Character.charCount(codepoint); i += Character.charCount(codepoint);
// basic letters, digits, or symbols // basic letters, digits, or symbols
if (isValidBasicLiteralCodepoint(codepoint)) if (isValidBasicLiteralCodepoint(codepoint, declaration))
{
continue; continue;
}
// The ucschar and iprivate pieces // The ucschar and iprivate pieces
if (Character.isSupplementaryCodePoint(codepoint)) if (Character.isSupplementaryCodePoint(codepoint))
{
continue; continue;
}
// pct-encoded // pct-encoded
if (codepoint == '%') if (codepoint == '%')
@ -265,10 +241,8 @@ public class UriTemplatePathSpec extends RegexPathSpec
codepoint |= TypeUtil.convertHexDigit(variable.codePointAt(i++)); codepoint |= TypeUtil.convertHexDigit(variable.codePointAt(i++));
// validate basic literal // validate basic literal
if (isValidBasicLiteralCodepoint(codepoint)) if (isValidBasicLiteralCodepoint(codepoint, declaration))
{
continue; continue;
}
} }
valid = false; valid = false;
@ -277,69 +251,174 @@ public class UriTemplatePathSpec extends RegexPathSpec
if (!valid) if (!valid)
{ {
// invalid variable name // invalid variable name
StringBuilder err = new StringBuilder(); throw new IllegalArgumentException("Syntax Error: variable {" + variable + "} an invalid variable name: " + declaration);
err.append("Syntax Error: variable {");
err.append(variable);
err.append("} an invalid variable name: ");
err.append(pathSpec);
throw new IllegalArgumentException(err.toString());
} }
} }
private boolean isValidBasicLiteralCodepoint(int codepoint) private static boolean isValidBasicLiteralCodepoint(int codepoint, String declaration)
{ {
// basic letters or digits // basic letters or digits
if ((codepoint >= 'a' && codepoint <= 'z') || if ((codepoint >= 'a' && codepoint <= 'z') ||
(codepoint >= 'A' && codepoint <= 'Z') || (codepoint >= 'A' && codepoint <= 'Z') ||
(codepoint >= '0' && codepoint <= '9')) (codepoint >= '0' && codepoint <= '9'))
{
return true; return true;
}
// basic allowed symbols // basic allowed symbols
if (VARIABLE_SYMBOLS.indexOf(codepoint) >= 0) if (VARIABLE_SYMBOLS.indexOf(codepoint) >= 0)
{
return true; // valid simple value return true; // valid simple value
}
// basic reserved symbols // basic reserved symbols
if (VARIABLE_RESERVED.indexOf(codepoint) >= 0) if (VARIABLE_RESERVED.indexOf(codepoint) >= 0)
{ {
LOG.warn("Detected URI Template reserved symbol [{}] in path spec \"{}\"", (char)codepoint, pathSpec); LOG.warn("Detected URI Template reserved symbol [{}] in path spec \"{}\"", (char)codepoint, declaration);
return false; // valid simple value return false; // valid simple value
} }
return false; return false;
} }
@Override
public int compareTo(PathSpec other)
{
if (other instanceof UriTemplatePathSpec)
{
UriTemplatePathSpec otherUriPathSpec = (UriTemplatePathSpec)other;
return otherUriPathSpec._logicalDeclaration.compareTo(this._logicalDeclaration);
}
else
{
return super.compareTo(other);
}
}
public Map<String, String> getPathParams(String path) public Map<String, String> getPathParams(String path)
{ {
Matcher matcher = getMatcher(path); Matcher matcher = getMatcher(path);
if (matcher.matches()) if (matcher.matches())
{ {
if (group == PathSpecGroup.EXACT) if (_group == PathSpecGroup.EXACT)
{
return Collections.emptyMap(); return Collections.emptyMap();
}
Map<String, String> ret = new HashMap<>(); Map<String, String> ret = new HashMap<>();
int groupCount = matcher.groupCount(); int groupCount = matcher.groupCount();
for (int i = 1; i <= groupCount; i++) for (int i = 1; i <= groupCount; i++)
{ ret.put(_variables[i - 1], matcher.group(i));
String value = UrlEncoded.decodeString(matcher.group(i));
ret.put(this.variables[i - 1], value);
}
return ret; return ret;
} }
return null; return null;
} }
protected Matcher getMatcher(String path)
{
return _pattern.matcher(path);
}
@Override
public int getSpecLength()
{
return _specLength;
}
@Override
public PathSpecGroup getGroup()
{
return _group;
}
@Override
public int getPathDepth()
{
return _pathDepth;
}
@Override
public String getPathInfo(String path)
{
// Path Info only valid for PREFIX_GLOB types
if (_group == PathSpecGroup.PREFIX_GLOB)
{
Matcher matcher = getMatcher(path);
if (matcher.matches())
{
if (matcher.groupCount() >= 1)
{
String pathInfo = matcher.group(1);
if ("".equals(pathInfo))
return "/";
else
return pathInfo;
}
}
}
return null;
}
@Override
public String getPathMatch(String path)
{
Matcher matcher = getMatcher(path);
if (matcher.matches())
{
if (matcher.groupCount() >= 1)
{
int idx = matcher.start(1);
if (idx > 0)
{
if (path.charAt(idx - 1) == '/')
idx--;
return path.substring(0, idx);
}
}
return path;
}
return null;
}
@Override
public String getDeclaration()
{
return _declaration;
}
@Override
public String getPrefix()
{
return null;
}
@Override
public String getSuffix()
{
return null;
}
public Pattern getPattern()
{
return _pattern;
}
@Override
public boolean matches(final String path)
{
int idx = path.indexOf('?');
if (idx >= 0)
{
// match only non-query part
return getMatcher(path.substring(0, idx)).matches();
}
else
{
// match entire path
return getMatcher(path).matches();
}
}
public int getVariableCount() public int getVariableCount()
{ {
return variables.length; return _variables.length;
} }
public String[] getVariables() public String[] getVariables()
{ {
return this.variables; return _variables;
} }
} }

View File

@ -22,8 +22,11 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource; import org.junit.jupiter.params.provider.ValueSource;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.nullValue;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
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;
@ -199,7 +202,7 @@ public class PathMappingsTest
p.put(new ServletPathSpec("/*"), "0"); p.put(new ServletPathSpec("/*"), "0");
// assertEquals("1", p.get("/abs/path"), "Get absolute path"); // assertEquals("1", p.get("/abs/path"), "Get absolute path");
assertEquals("/abs/path", p.getMatch("/abs/path").getPathSpec().pathSpec, "Match absolute path"); assertEquals("/abs/path", p.getMatch("/abs/path").getPathSpec().getDeclaration(), "Match absolute path");
assertEquals("1", p.getMatch("/abs/path").getResource(), "Match absolute path"); assertEquals("1", p.getMatch("/abs/path").getResource(), "Match absolute path");
assertEquals("0", p.getMatch("/abs/path/xxx").getResource(), "Mismatch absolute path"); assertEquals("0", p.getMatch("/abs/path/xxx").getResource(), "Mismatch absolute path");
assertEquals("0", p.getMatch("/abs/pith").getResource(), "Mismatch absolute path"); assertEquals("0", p.getMatch("/abs/pith").getResource(), "Mismatch absolute path");
@ -302,4 +305,160 @@ public class PathMappingsTest
new ServletPathSpec(str); new ServletPathSpec(str);
}); });
} }
@Test
public void testPutRejectsDuplicates()
{
PathMappings<String> p = new PathMappings<>();
assertThat(p.put(new UriTemplatePathSpec("/a/{var1}/c"), "resourceA"), is(true));
assertThat(p.put(new UriTemplatePathSpec("/a/{var2}/c"), "resourceAA"), is(false));
assertThat(p.put(new UriTemplatePathSpec("/a/b/c"), "resourceB"), is(true));
assertThat(p.put(new UriTemplatePathSpec("/a/b/c"), "resourceBB"), is(false));
assertThat(p.put(new ServletPathSpec("/a/b/c"), "resourceBB"), is(false));
assertThat(p.put(new RegexPathSpec("/a/b/c"), "resourceBB"), is(false));
assertThat(p.put(new ServletPathSpec("/*"), "resourceC"), is(true));
assertThat(p.put(new RegexPathSpec("/(.*)"), "resourceCC"), is(true));
}
@Test
public void testGetUriTemplatePathSpec()
{
PathMappings<String> p = new PathMappings<>();
p.put(new UriTemplatePathSpec("/a/{var1}/c"), "resourceA");
p.put(new UriTemplatePathSpec("/a/b/c"), "resourceB");
assertThat(p.get(new UriTemplatePathSpec("/a/{var1}/c")), equalTo("resourceA"));
assertThat(p.get(new UriTemplatePathSpec("/a/{foo}/c")), equalTo("resourceA"));
assertThat(p.get(new UriTemplatePathSpec("/a/b/c")), equalTo("resourceB"));
assertThat(p.get(new UriTemplatePathSpec("/a/d/c")), nullValue());
assertThat(p.get(new RegexPathSpec("/a/b/c")), nullValue());
}
@Test
public void testGetRegexPathSpec()
{
PathMappings<String> p = new PathMappings<>();
p.put(new RegexPathSpec("/a/b/c"), "resourceA");
p.put(new RegexPathSpec("/(.*)/b/c"), "resourceB");
p.put(new RegexPathSpec("/a/(.*)/c"), "resourceC");
p.put(new RegexPathSpec("/a/b/(.*)"), "resourceD");
assertThat(p.get(new RegexPathSpec("/a/(.*)/c")), equalTo("resourceC"));
assertThat(p.get(new RegexPathSpec("/a/b/c")), equalTo("resourceA"));
assertThat(p.get(new RegexPathSpec("/(.*)/b/c")), equalTo("resourceB"));
assertThat(p.get(new RegexPathSpec("/a/b/(.*)")), equalTo("resourceD"));
assertThat(p.get(new RegexPathSpec("/a/d/c")), nullValue());
assertThat(p.get(new ServletPathSpec("/a/b/c")), nullValue());
}
@Test
public void testGetServletPathSpec()
{
PathMappings<String> p = new PathMappings<>();
p.put(new ServletPathSpec("/"), "resourceA");
p.put(new ServletPathSpec("/*"), "resourceB");
p.put(new ServletPathSpec("/a/*"), "resourceC");
p.put(new ServletPathSpec("*.do"), "resourceD");
assertThat(p.get(new ServletPathSpec("/")), equalTo("resourceA"));
assertThat(p.get(new ServletPathSpec("/*")), equalTo("resourceB"));
assertThat(p.get(new ServletPathSpec("/a/*")), equalTo("resourceC"));
assertThat(p.get(new ServletPathSpec("*.do")), equalTo("resourceD"));
assertThat(p.get(new ServletPathSpec("*.gz")), nullValue());
assertThat(p.get(new ServletPathSpec("/a/b/*")), nullValue());
assertThat(p.get(new ServletPathSpec("/a/d/c")), nullValue());
assertThat(p.get(new RegexPathSpec("/a/b/c")), nullValue());
}
@Test
public void testRemoveUriTemplatePathSpec()
{
PathMappings<String> p = new PathMappings<>();
p.put(new UriTemplatePathSpec("/a/{var1}/c"), "resourceA");
assertThat(p.remove(new UriTemplatePathSpec("/a/{var1}/c")), is(true));
p.put(new UriTemplatePathSpec("/a/{var1}/c"), "resourceA");
assertThat(p.remove(new UriTemplatePathSpec("/a/b/c")), is(false));
assertThat(p.remove(new UriTemplatePathSpec("/a/{b}/c")), is(true));
assertThat(p.remove(new UriTemplatePathSpec("/a/{b}/c")), is(false));
p.put(new UriTemplatePathSpec("/{var1}/b/c"), "resourceA");
assertThat(p.remove(new UriTemplatePathSpec("/a/b/c")), is(false));
assertThat(p.remove(new UriTemplatePathSpec("/{a}/b/c")), is(true));
assertThat(p.remove(new UriTemplatePathSpec("/{a}/b/c")), is(false));
p.put(new UriTemplatePathSpec("/a/b/{var1}"), "resourceA");
assertThat(p.remove(new UriTemplatePathSpec("/a/b/c")), is(false));
assertThat(p.remove(new UriTemplatePathSpec("/a/b/{c}")), is(true));
assertThat(p.remove(new UriTemplatePathSpec("/a/b/{c}")), is(false));
p.put(new UriTemplatePathSpec("/{var1}/{var2}/{var3}"), "resourceA");
assertThat(p.remove(new UriTemplatePathSpec("/a/b/c")), is(false));
assertThat(p.remove(new UriTemplatePathSpec("/{a}/{b}/{c}")), is(true));
assertThat(p.remove(new UriTemplatePathSpec("/{a}/{b}/{c}")), is(false));
}
@Test
public void testRemoveRegexPathSpec()
{
PathMappings<String> p = new PathMappings<>();
p.put(new RegexPathSpec("/a/(.*)/c"), "resourceA");
assertThat(p.remove(new RegexPathSpec("/a/b/c")), is(false));
assertThat(p.remove(new RegexPathSpec("/a/(.*)/c")), is(true));
assertThat(p.remove(new RegexPathSpec("/a/(.*)/c")), is(false));
p.put(new RegexPathSpec("/(.*)/b/c"), "resourceA");
assertThat(p.remove(new RegexPathSpec("/a/b/c")), is(false));
assertThat(p.remove(new RegexPathSpec("/(.*)/b/c")), is(true));
assertThat(p.remove(new RegexPathSpec("/(.*)/b/c")), is(false));
p.put(new RegexPathSpec("/a/b/(.*)"), "resourceA");
assertThat(p.remove(new RegexPathSpec("/a/b/c")), is(false));
assertThat(p.remove(new RegexPathSpec("/a/b/(.*)")), is(true));
assertThat(p.remove(new RegexPathSpec("/a/b/(.*)")), is(false));
p.put(new RegexPathSpec("/a/b/c"), "resourceA");
assertThat(p.remove(new RegexPathSpec("/a/b/d")), is(false));
assertThat(p.remove(new RegexPathSpec("/a/b/c")), is(true));
assertThat(p.remove(new RegexPathSpec("/a/b/c")), is(false));
}
@Test
public void testRemoveServletPathSpec()
{
PathMappings<String> p = new PathMappings<>();
p.put(new ServletPathSpec("/a/*"), "resourceA");
assertThat(p.remove(new ServletPathSpec("/a/b")), is(false));
assertThat(p.remove(new ServletPathSpec("/a/*")), is(true));
assertThat(p.remove(new ServletPathSpec("/a/*")), is(false));
p.put(new ServletPathSpec("/a/b/*"), "resourceA");
assertThat(p.remove(new ServletPathSpec("/a/b/c")), is(false));
assertThat(p.remove(new ServletPathSpec("/a/b/*")), is(true));
assertThat(p.remove(new ServletPathSpec("/a/b/*")), is(false));
p.put(new ServletPathSpec("*.do"), "resourceA");
assertThat(p.remove(new ServletPathSpec("*.gz")), is(false));
assertThat(p.remove(new ServletPathSpec("*.do")), is(true));
assertThat(p.remove(new ServletPathSpec("*.do")), is(false));
p.put(new ServletPathSpec("/"), "resourceA");
assertThat(p.remove(new ServletPathSpec("/a")), is(false));
assertThat(p.remove(new ServletPathSpec("/")), is(true));
assertThat(p.remove(new ServletPathSpec("/")), is(false));
p.put(new ServletPathSpec(""), "resourceA");
assertThat(p.remove(new ServletPathSpec("/")), is(false));
assertThat(p.remove(new ServletPathSpec("")), is(true));
assertThat(p.remove(new ServletPathSpec("")), is(false));
p.put(new ServletPathSpec("/a/b/c"), "resourceA");
assertThat(p.remove(new ServletPathSpec("/a/b/d")), is(false));
assertThat(p.remove(new ServletPathSpec("/a/b/c")), is(true));
assertThat(p.remove(new ServletPathSpec("/a/b/c")), is(false));
}
} }

View File

@ -21,7 +21,9 @@ package org.eclipse.jetty.http.pathmap;
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.equalTo;
import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
public class RegexPathSpecTest public class RegexPathSpecTest
@ -45,7 +47,7 @@ public class RegexPathSpecTest
assertEquals("^/a$", spec.getDeclaration(), "Spec.pathSpec"); assertEquals("^/a$", spec.getDeclaration(), "Spec.pathSpec");
assertEquals("^/a$", spec.getPattern().pattern(), "Spec.pattern"); assertEquals("^/a$", spec.getPattern().pattern(), "Spec.pattern");
assertEquals(1, spec.getPathDepth(), "Spec.pathDepth"); assertEquals(1, spec.getPathDepth(), "Spec.pathDepth");
assertEquals(PathSpecGroup.EXACT, spec.group, "Spec.group"); assertEquals(PathSpecGroup.EXACT, spec.getGroup(), "Spec.group");
assertMatches(spec, "/a"); assertMatches(spec, "/a");
@ -60,7 +62,7 @@ public class RegexPathSpecTest
assertEquals("^/rest/([^/]*)/list$", spec.getDeclaration(), "Spec.pathSpec"); assertEquals("^/rest/([^/]*)/list$", spec.getDeclaration(), "Spec.pathSpec");
assertEquals("^/rest/([^/]*)/list$", spec.getPattern().pattern(), "Spec.pattern"); assertEquals("^/rest/([^/]*)/list$", spec.getPattern().pattern(), "Spec.pattern");
assertEquals(3, spec.getPathDepth(), "Spec.pathDepth"); assertEquals(3, spec.getPathDepth(), "Spec.pathDepth");
assertEquals(PathSpecGroup.MIDDLE_GLOB, spec.group, "Spec.group"); assertEquals(PathSpecGroup.MIDDLE_GLOB, spec.getGroup(), "Spec.group");
assertMatches(spec, "/rest/api/list"); assertMatches(spec, "/rest/api/list");
assertMatches(spec, "/rest/1.0/list"); assertMatches(spec, "/rest/1.0/list");
@ -81,7 +83,7 @@ public class RegexPathSpecTest
assertEquals("^/rest/[^/]+/list$", spec.getDeclaration(), "Spec.pathSpec"); assertEquals("^/rest/[^/]+/list$", spec.getDeclaration(), "Spec.pathSpec");
assertEquals("^/rest/[^/]+/list$", spec.getPattern().pattern(), "Spec.pattern"); assertEquals("^/rest/[^/]+/list$", spec.getPattern().pattern(), "Spec.pattern");
assertEquals(3, spec.getPathDepth(), "Spec.pathDepth"); assertEquals(3, spec.getPathDepth(), "Spec.pathDepth");
assertEquals(PathSpecGroup.MIDDLE_GLOB, spec.group, "Spec.group"); assertEquals(PathSpecGroup.MIDDLE_GLOB, spec.getGroup(), "Spec.group");
assertMatches(spec, "/rest/api/list"); assertMatches(spec, "/rest/api/list");
assertMatches(spec, "/rest/1.0/list"); assertMatches(spec, "/rest/1.0/list");
@ -102,7 +104,7 @@ public class RegexPathSpecTest
assertEquals("^/a/(.*)$", spec.getDeclaration(), "Spec.pathSpec"); assertEquals("^/a/(.*)$", spec.getDeclaration(), "Spec.pathSpec");
assertEquals("^/a/(.*)$", spec.getPattern().pattern(), "Spec.pattern"); assertEquals("^/a/(.*)$", spec.getPattern().pattern(), "Spec.pattern");
assertEquals(2, spec.getPathDepth(), "Spec.pathDepth"); assertEquals(2, spec.getPathDepth(), "Spec.pathDepth");
assertEquals(PathSpecGroup.PREFIX_GLOB, spec.group, "Spec.group"); assertEquals(PathSpecGroup.PREFIX_GLOB, spec.getGroup(), "Spec.group");
assertMatches(spec, "/a/"); assertMatches(spec, "/a/");
assertMatches(spec, "/a/b"); assertMatches(spec, "/a/b");
@ -120,7 +122,7 @@ public class RegexPathSpecTest
assertEquals("^(.*).do$", spec.getDeclaration(), "Spec.pathSpec"); assertEquals("^(.*).do$", spec.getDeclaration(), "Spec.pathSpec");
assertEquals("^(.*).do$", spec.getPattern().pattern(), "Spec.pattern"); assertEquals("^(.*).do$", spec.getPattern().pattern(), "Spec.pattern");
assertEquals(0, spec.getPathDepth(), "Spec.pathDepth"); assertEquals(0, spec.getPathDepth(), "Spec.pathDepth");
assertEquals(PathSpecGroup.SUFFIX_GLOB, spec.group, "Spec.group"); assertEquals(PathSpecGroup.SUFFIX_GLOB, spec.getGroup(), "Spec.group");
assertMatches(spec, "/a.do"); assertMatches(spec, "/a.do");
assertMatches(spec, "/a/b/c.do"); assertMatches(spec, "/a/b/c.do");
@ -132,4 +134,14 @@ public class RegexPathSpecTest
assertNotMatches(spec, "/aa/bb"); assertNotMatches(spec, "/aa/bb");
assertNotMatches(spec, "/aa/bb.do/more"); assertNotMatches(spec, "/aa/bb.do/more");
} }
@Test
public void testEquals()
{
assertThat(new RegexPathSpec("^(.*).do$"), equalTo(new RegexPathSpec("^(.*).do$")));
assertThat(new RegexPathSpec("/foo"), equalTo(new RegexPathSpec("/foo")));
assertThat(new RegexPathSpec("^(.*).do$"), not(equalTo(new RegexPathSpec("^(.*).gz$"))));
assertThat(new RegexPathSpec("^(.*).do$"), not(equalTo(new RegexPathSpec("^.*.do$"))));
assertThat(new RegexPathSpec("/foo"), not(equalTo(new ServletPathSpec("/foo"))));
}
} }

View File

@ -82,7 +82,7 @@ public class ServletPathSpecMatchListTest
{ {
if (delim) if (delim)
actual.append(", "); actual.append(", ");
actual.append(res.getPathSpec().pathSpec).append('=').append(res.getResource()); actual.append(res.getPathSpec().getDeclaration()).append('=').append(res.getResource());
delim = true; delim = true;
} }
actual.append(']'); actual.append(']');

View File

@ -21,7 +21,9 @@ package org.eclipse.jetty.http.pathmap;
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.equalTo;
import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.fail; import static org.junit.jupiter.api.Assertions.fail;
@ -187,4 +189,17 @@ public class ServletPathSpecTest
assertEquals(null, spec.getPathInfo("/downloads/distribution.tar.gz"), "Spec.pathInfo"); assertEquals(null, spec.getPathInfo("/downloads/distribution.tar.gz"), "Spec.pathInfo");
} }
@Test
public void testEquals()
{
assertThat(new ServletPathSpec("*.gz"), equalTo(new ServletPathSpec("*.gz")));
assertThat(new ServletPathSpec("/foo"), equalTo(new ServletPathSpec("/foo")));
assertThat(new ServletPathSpec("/foo/bar"), equalTo(new ServletPathSpec("/foo/bar")));
assertThat(new ServletPathSpec("*.gz"), not(equalTo(new ServletPathSpec("*.do"))));
assertThat(new ServletPathSpec("/foo"), not(equalTo(new ServletPathSpec("/bar"))));
assertThat(new ServletPathSpec("/bar/foo"), not(equalTo(new ServletPathSpec("/foo/bar"))));
assertThat(new ServletPathSpec("/foo"), not(equalTo(new RegexPathSpec("/foo"))));
}
} }

View File

@ -23,7 +23,9 @@ import java.util.Map;
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.equalTo;
import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.notNullValue;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
@ -281,4 +283,15 @@ public class UriTemplatePathSpecTest
assertThat("Spec.pathParams.size", mapped.size(), is(1)); assertThat("Spec.pathParams.size", mapped.size(), is(1));
assertEquals("a", mapped.get("var1"), "Spec.pathParams[var1]"); assertEquals("a", mapped.get("var1"), "Spec.pathParams[var1]");
} }
@Test
public void testEquals()
{
assertThat(new UriTemplatePathSpec("/{var1}"), equalTo(new UriTemplatePathSpec("/{var1}")));
assertThat(new UriTemplatePathSpec("/{var1}"), equalTo(new UriTemplatePathSpec("/{var2}")));
assertThat(new UriTemplatePathSpec("/{var1}/{var2}"), equalTo(new UriTemplatePathSpec("/{var2}/{var1}")));
assertThat(new UriTemplatePathSpec("/{var1}"), not(equalTo(new UriTemplatePathSpec("/{var1}/{var2}"))));
assertThat(new UriTemplatePathSpec("/a/b/c"), not(equalTo(new UriTemplatePathSpec("/a/{var}/c"))));
assertThat(new UriTemplatePathSpec("/foo"), not(equalTo(new ServletPathSpec("/foo"))));
}
} }

View File

@ -0,0 +1,182 @@
//
// ========================================================================
// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under
// the terms of the Eclipse Public License 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0
//
// This Source Code may also be made available under the following
// Secondary Licenses when the conditions for such availability set
// forth in the Eclipse Public License, v. 2.0 are satisfied:
// the Apache License v2.0 which is available at
// https://www.apache.org/licenses/LICENSE-2.0
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.http.pathmap;
import java.util.List;
import org.junit.jupiter.api.Test;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.nullValue;
import static org.junit.jupiter.api.Assertions.assertNull;
public class WebSocketUriMappingTest
{
private PathMappings<String> mapping = new PathMappings<>();
private String getBestMatch(String uriPath)
{
List<MappedResource<String>> resources = mapping.getMatches(uriPath);
assertThat("Matches on " + uriPath, resources, is(not(nullValue())));
if (resources.isEmpty())
return null;
return resources.get(0).getResource();
}
@Test
public void testJsrExampleI()
{
mapping.put("/a/b", "endpointA");
assertThat(getBestMatch("/a/b"), is("endpointA"));
assertNull(getBestMatch("/a/c"));
}
@Test
public void testJsrExampleII()
{
mapping.put(new UriTemplatePathSpec("/a/{var}"), "endpointA");
assertThat(getBestMatch("/a/b"), is("endpointA"));
assertThat(getBestMatch("/a/apple"), is("endpointA"));
assertNull(getBestMatch("/a"));
assertNull(getBestMatch("/a/b/c"));
}
@Test
public void testJsrExampleIII()
{
mapping.put(new UriTemplatePathSpec("/a/{var}/c"), "endpointA");
mapping.put(new UriTemplatePathSpec("/a/b/c"), "endpointB");
mapping.put(new UriTemplatePathSpec("/a/{var1}/{var2}"), "endpointC");
assertThat(getBestMatch("/a/b/c"), is("endpointB"));
assertThat(getBestMatch("/a/d/c"), is("endpointA"));
assertThat(getBestMatch("/a/x/y"), is("endpointC"));
}
@Test
public void testJsrExampleIV()
{
mapping.put(new UriTemplatePathSpec("/{var1}/d"), "endpointA");
mapping.put(new UriTemplatePathSpec("/b/{var2}"), "endpointB");
assertThat(getBestMatch("/b/d"), is("endpointB"));
}
@Test
public void testPrefixVsSuffix()
{
mapping.put(new UriTemplatePathSpec("/{a}/b"), "suffix");
mapping.put(new UriTemplatePathSpec("/{a}/{b}"), "prefix");
List<MappedResource<String>> matches = mapping.getMatches("/a/b");
assertThat(getBestMatch("/a/b"), is("suffix"));
}
@Test
public void testMiddleVsSuffix()
{
mapping.put(new UriTemplatePathSpec("/a/{b}/c"), "middle");
mapping.put(new UriTemplatePathSpec("/a/b/{c}"), "suffix");
assertThat(getBestMatch("/a/b/c"), is("suffix"));
}
@Test
public void testMiddleVsSuffix2()
{
mapping.put(new UriTemplatePathSpec("/{a}/b/{c}"), "middle");
mapping.put(new UriTemplatePathSpec("/{a}/b/c"), "suffix");
assertThat(getBestMatch("/a/b/c"), is("suffix"));
}
@Test
public void testMiddleVsPrefix()
{
mapping.put(new UriTemplatePathSpec("/a/{b}/{c}/d"), "middle");
mapping.put(new UriTemplatePathSpec("/a/b/c/{d}"), "prefix");
assertThat(getBestMatch("/a/b/c/d"), is("prefix"));
}
@Test
public void testMiddleVsMiddle()
{
// This works but only because its an alphabetical check and '{' > 'c'.
mapping.put(new UriTemplatePathSpec("/a/{b}/{c}/d"), "middle1");
mapping.put(new UriTemplatePathSpec("/a/{b}/c/d"), "middle2");
assertThat(getBestMatch("/a/b/c/d"), is("middle2"));
}
@Test
public void testMiddleVsMiddle2()
{
mapping.put(new UriTemplatePathSpec("/{a}/{bz}/c/{d}"), "middle1");
mapping.put(new UriTemplatePathSpec("/{a}/{ba}/{c}/d"), "middle2");
assertThat(getBestMatch("/a/b/c/d"), is("middle1"));
}
@Test
public void testMiddleVsMiddle3()
{
mapping.put(new UriTemplatePathSpec("/{a}/{ba}/c/{d}"), "middle1");
mapping.put(new UriTemplatePathSpec("/{a}/{bz}/{c}/d"), "middle2");
assertThat(getBestMatch("/a/b/c/d"), is("middle1"));
}
@Test
public void testPrefixVsPrefix()
{
// This works but only because its an alphabetical check and '{' > 'b'.
mapping.put(new UriTemplatePathSpec("/a/{b}/{c}"), "prefix1");
mapping.put(new UriTemplatePathSpec("/a/b/{c}"), "prefix2");
assertThat(getBestMatch("/a/b/c"), is("prefix2"));
}
@Test
public void testSuffixVsSuffix()
{
// This works but only because its an alphabetical check and '{' > 'b'.
mapping.put(new UriTemplatePathSpec("/{a}/{b}/c"), "suffix1");
mapping.put(new UriTemplatePathSpec("/{a}/b/c"), "suffix2");
assertThat(getBestMatch("/a/b/c"), is("suffix2"));
}
@Test
public void testDifferentLengths()
{
mapping.put(new UriTemplatePathSpec("/a/{var}/c"), "endpointA");
mapping.put(new UriTemplatePathSpec("/a/{var}/c/d"), "endpointB");
mapping.put(new UriTemplatePathSpec("/a/{var1}/{var2}/d/e"), "endpointC");
assertThat(getBestMatch("/a/b/c"), is("endpointA"));
assertThat(getBestMatch("/a/d/c/d"), is("endpointB"));
assertThat(getBestMatch("/a/x/y/d/e"), is("endpointC"));
}
}

View File

@ -21,7 +21,6 @@ package org.eclipse.jetty.http2.client;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.function.Consumer; import java.util.function.Consumer;
import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServlet;
import org.eclipse.jetty.http.HostPortHttpField; import org.eclipse.jetty.http.HostPortHttpField;
@ -33,7 +32,7 @@ import org.eclipse.jetty.http2.FlowControlStrategy;
import org.eclipse.jetty.http2.api.Session; import org.eclipse.jetty.http2.api.Session;
import org.eclipse.jetty.http2.api.server.ServerSessionListener; import org.eclipse.jetty.http2.api.server.ServerSessionListener;
import org.eclipse.jetty.http2.server.AbstractHTTP2ServerConnectionFactory; import org.eclipse.jetty.http2.server.AbstractHTTP2ServerConnectionFactory;
import org.eclipse.jetty.http2.server.HTTP2ServerConnectionFactory; import org.eclipse.jetty.http2.server.HTTP2CServerConnectionFactory;
import org.eclipse.jetty.http2.server.RawHTTP2ServerConnectionFactory; import org.eclipse.jetty.http2.server.RawHTTP2ServerConnectionFactory;
import org.eclipse.jetty.server.ConnectionFactory; import org.eclipse.jetty.server.ConnectionFactory;
import org.eclipse.jetty.server.HttpConfiguration; import org.eclipse.jetty.server.HttpConfiguration;
@ -54,7 +53,7 @@ public class AbstractTest
protected void start(HttpServlet servlet) throws Exception protected void start(HttpServlet servlet) throws Exception
{ {
HTTP2ServerConnectionFactory connectionFactory = new HTTP2ServerConnectionFactory(new HttpConfiguration()); HTTP2CServerConnectionFactory connectionFactory = new HTTP2CServerConnectionFactory(new HttpConfiguration());
connectionFactory.setInitialSessionRecvWindow(FlowControlStrategy.DEFAULT_WINDOW_SIZE); connectionFactory.setInitialSessionRecvWindow(FlowControlStrategy.DEFAULT_WINDOW_SIZE);
connectionFactory.setInitialStreamRecvWindow(FlowControlStrategy.DEFAULT_WINDOW_SIZE); connectionFactory.setInitialStreamRecvWindow(FlowControlStrategy.DEFAULT_WINDOW_SIZE);
prepareServer(connectionFactory); prepareServer(connectionFactory);

View File

@ -1,80 +0,0 @@
//
// ========================================================================
// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under
// the terms of the Eclipse Public License 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0
//
// This Source Code may also be made available under the following
// Secondary Licenses when the conditions for such availability set
// forth in the Eclipse Public License, v. 2.0 are satisfied:
// the Apache License v2.0 which is available at
// https://www.apache.org/licenses/LICENSE-2.0
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.http2.client;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import org.eclipse.jetty.http2.api.Session;
import org.eclipse.jetty.util.Promise;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class InvalidServerTest extends AbstractTest
{
@Test
public void testInvalidPreface() throws Exception
{
try (ServerSocket server = new ServerSocket(0))
{
prepareClient();
client.start();
CountDownLatch failureLatch = new CountDownLatch(1);
Promise.Completable<Session> promise = new Promise.Completable<>();
InetSocketAddress address = new InetSocketAddress("localhost", server.getLocalPort());
client.connect(address, new Session.Listener.Adapter()
{
@Override
public void onFailure(Session session, Throwable failure)
{
failureLatch.countDown();
}
}, promise);
try (Socket socket = server.accept())
{
OutputStream output = socket.getOutputStream();
output.write("enough_junk_bytes".getBytes(StandardCharsets.UTF_8));
Session session = promise.get(5, TimeUnit.SECONDS);
assertNotNull(session);
assertTrue(failureLatch.await(5, TimeUnit.SECONDS));
// Verify that the client closed the socket.
InputStream input = socket.getInputStream();
while (true)
{
int read = input.read();
if (read < 0)
break;
}
}
}
}
}

View File

@ -18,7 +18,11 @@
package org.eclipse.jetty.http2.client; package org.eclipse.jetty.http2.client;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel; import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
@ -41,6 +45,7 @@ import org.eclipse.jetty.http2.ErrorCode;
import org.eclipse.jetty.http2.api.Session; import org.eclipse.jetty.http2.api.Session;
import org.eclipse.jetty.http2.api.Stream; import org.eclipse.jetty.http2.api.Stream;
import org.eclipse.jetty.http2.api.server.ServerSessionListener; import org.eclipse.jetty.http2.api.server.ServerSessionListener;
import org.eclipse.jetty.http2.frames.FrameType;
import org.eclipse.jetty.http2.frames.HeadersFrame; import org.eclipse.jetty.http2.frames.HeadersFrame;
import org.eclipse.jetty.http2.frames.PingFrame; import org.eclipse.jetty.http2.frames.PingFrame;
import org.eclipse.jetty.http2.frames.PrefaceFrame; import org.eclipse.jetty.http2.frames.PrefaceFrame;
@ -63,6 +68,7 @@ import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.greaterThanOrEqualTo; import static org.hamcrest.Matchers.greaterThanOrEqualTo;
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.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertTrue;
public class PrefaceTest extends AbstractTest public class PrefaceTest extends AbstractTest
@ -332,4 +338,71 @@ public class PrefaceTest extends AbstractTest
assertTrue(clientSettingsLatch.await(5, TimeUnit.SECONDS)); assertTrue(clientSettingsLatch.await(5, TimeUnit.SECONDS));
} }
} }
@Test
public void testInvalidServerPreface() throws Exception
{
try (ServerSocket server = new ServerSocket(0))
{
prepareClient();
client.start();
CountDownLatch failureLatch = new CountDownLatch(1);
Promise.Completable<Session> promise = new Promise.Completable<>();
InetSocketAddress address = new InetSocketAddress("localhost", server.getLocalPort());
client.connect(address, new Session.Listener.Adapter()
{
@Override
public void onFailure(Session session, Throwable failure)
{
failureLatch.countDown();
}
}, promise);
try (Socket socket = server.accept())
{
OutputStream output = socket.getOutputStream();
output.write("enough_junk_bytes".getBytes(StandardCharsets.UTF_8));
Session session = promise.get(5, TimeUnit.SECONDS);
assertNotNull(session);
assertTrue(failureLatch.await(5, TimeUnit.SECONDS));
// Verify that the client closed the socket.
InputStream input = socket.getInputStream();
while (true)
{
int read = input.read();
if (read < 0)
break;
}
}
}
}
@Test
public void testInvalidClientPreface() throws Exception
{
start(new ServerSessionListener.Adapter());
try (Socket client = new Socket("localhost", connector.getLocalPort()))
{
OutputStream output = client.getOutputStream();
output.write("enough_junk_bytes".getBytes(StandardCharsets.UTF_8));
output.flush();
byte[] bytes = new byte[1024];
InputStream input = client.getInputStream();
int read = input.read(bytes);
if (read < 0)
{
// Closing the connection without GOAWAY frame is fine.
return;
}
int type = bytes[3];
assertEquals(FrameType.GO_AWAY.getType(), type);
}
}
} }

View File

@ -425,12 +425,21 @@ public class HTTP2Flusher extends IteratingCallback implements Dumpable
super.failed(x); super.failed(x);
} }
/**
* @return whether the entry is stale and must not be processed
*/
private boolean isStale() private boolean isStale()
{ {
return !isProtocol() && stream != null && stream.isReset(); // If it is a protocol frame, process it.
if (isProtocolFrame(frame))
return false;
// It's an application frame; is the stream gone already?
if (stream == null)
return true;
return stream.isReset();
} }
private boolean isProtocol() private boolean isProtocolFrame(Frame frame)
{ {
switch (frame.getType()) switch (frame.getType())
{ {

View File

@ -447,7 +447,7 @@ public abstract class HTTP2Session extends ContainerLifeCycle implements ISessio
// We received a GO_AWAY, so try to write // We received a GO_AWAY, so try to write
// what's in the queue and then disconnect. // what's in the queue and then disconnect.
closeFrame = frame; closeFrame = frame;
notifyClose(this, frame, new DisconnectCallback()); onClose(frame, new DisconnectCallback());
return; return;
} }
@ -514,9 +514,15 @@ public abstract class HTTP2Session extends ContainerLifeCycle implements ISessio
public void onStreamFailure(int streamId, int error, String reason) public void onStreamFailure(int streamId, int error, String reason)
{ {
Callback callback = new ResetCallback(streamId, error, Callback.NOOP); Callback callback = new ResetCallback(streamId, error, Callback.NOOP);
Throwable failure = toFailure("Stream failure", error, reason);
onStreamFailure(streamId, error, reason, failure, callback);
}
private void onStreamFailure(int streamId, int error, String reason, Throwable failure, Callback callback)
{
IStream stream = getStream(streamId); IStream stream = getStream(streamId);
if (stream != null) if (stream != null)
stream.process(new FailureFrame(error, reason), callback); stream.process(new FailureFrame(error, reason, failure), callback);
else else
callback.succeeded(); callback.succeeded();
} }
@ -529,38 +535,51 @@ public abstract class HTTP2Session extends ContainerLifeCycle implements ISessio
protected void onConnectionFailure(int error, String reason, Callback callback) protected void onConnectionFailure(int error, String reason, Callback callback)
{ {
notifyFailure(this, new IOException(String.format("%d/%s", error, reason)), new FailureCallback(error, reason, callback)); Throwable failure = toFailure("Session failure", error, reason);
onFailure(error, reason, failure, new FailureCallback(error, reason, callback));
}
protected void abort(Throwable failure)
{
onFailure(ErrorCode.NO_ERROR.code, null, failure, new TerminateCallback(failure));
}
private void onFailure(int error, String reason, Throwable failure, Callback callback)
{
Collection<Stream> streams = getStreams();
int count = streams.size();
Callback countCallback = new CountingCallback(callback, count + 1);
for (Stream stream : streams)
{
onStreamFailure(stream.getId(), error, reason, failure, countCallback);
}
notifyFailure(this, failure, countCallback);
}
private void onClose(GoAwayFrame frame, Callback callback)
{
int error = frame.getError();
String reason = frame.tryConvertPayload();
Throwable failure = toFailure("Session close", error, reason);
Collection<Stream> streams = getStreams();
int count = streams.size();
Callback countCallback = new CountingCallback(callback, count + 1);
for (Stream stream : streams)
{
onStreamFailure(stream.getId(), error, reason, failure, countCallback);
}
notifyClose(this, frame, countCallback);
}
private Throwable toFailure(String message, int error, String reason)
{
return new IOException(String.format("%s %s/%s", message, ErrorCode.toString(error, null), reason));
} }
@Override @Override
public void newStream(HeadersFrame frame, Promise<Stream> promise, Stream.Listener listener) public void newStream(HeadersFrame frame, Promise<Stream> promise, Stream.Listener listener)
{ {
streamCreator.newStream(frame, promise, listener); streamCreator.newStream(frame, promise, listener);
/*
try
{
// Synchronization is necessary to atomically create
// the stream id and enqueue the frame to be sent.
IStream stream;
boolean queued;
synchronized (this)
{
HeadersFrame[] frameOut = new HeadersFrame[1];
stream = newLocalStream(frame, frameOut);
stream.setListener(listener);
ControlEntry entry = new ControlEntry(frameOut[0], stream, new StreamPromiseCallback(promise, stream));
queued = flusher.append(entry);
}
stream.process(new PrefaceFrame(), Callback.NOOP);
// Iterate outside the synchronized block.
if (queued)
flusher.iterate();
}
catch (Throwable x)
{
promise.failed(x);
}
*/
} }
/** /**
@ -1100,11 +1119,6 @@ public abstract class HTTP2Session extends ContainerLifeCycle implements ISessio
} }
} }
protected void abort(Throwable failure)
{
notifyFailure(this, failure, new TerminateCallback(failure));
}
public boolean isDisconnected() public boolean isDisconnected()
{ {
return !endPoint.isOpen(); return !endPoint.isOpen();
@ -1629,7 +1643,7 @@ public abstract class HTTP2Session extends ContainerLifeCycle implements ISessio
public void failed(Throwable x) public void failed(Throwable x)
{ {
if (LOG.isDebugEnabled()) if (LOG.isDebugEnabled())
LOG.debug("CloseCallback failed", x); LOG.debug("FailureCallback failed", x);
complete(); complete();
} }

View File

@ -151,7 +151,6 @@ public class HTTP2Stream extends IdleTimeout implements IStream, Callback, Dumpa
{ {
if (writing.compareAndSet(null, callback)) if (writing.compareAndSet(null, callback))
return true; return true;
close();
callback.failed(new WritePendingException()); callback.failed(new WritePendingException());
return false; return false;
} }
@ -190,7 +189,7 @@ public class HTTP2Stream extends IdleTimeout implements IStream, Callback, Dumpa
public boolean isRemotelyClosed() public boolean isRemotelyClosed()
{ {
CloseState state = closeState.get(); CloseState state = closeState.get();
return state == CloseState.REMOTELY_CLOSED || state == CloseState.CLOSING; return state == CloseState.REMOTELY_CLOSED || state == CloseState.CLOSING || state == CloseState.CLOSED;
} }
public boolean isLocallyClosed() public boolean isLocallyClosed()
@ -345,7 +344,7 @@ public class HTTP2Stream extends IdleTimeout implements IStream, Callback, Dumpa
if (dataLength != Long.MIN_VALUE) if (dataLength != Long.MIN_VALUE)
{ {
dataLength -= frame.remaining(); dataLength -= frame.remaining();
if (frame.isEndStream() && dataLength != 0) if (dataLength < 0 || (frame.isEndStream() && dataLength != 0))
{ {
reset(new ResetFrame(streamId, ErrorCode.PROTOCOL_ERROR.code), Callback.NOOP); reset(new ResetFrame(streamId, ErrorCode.PROTOCOL_ERROR.code), Callback.NOOP);
callback.failed(new IOException("invalid_data_length")); callback.failed(new IOException("invalid_data_length"));
@ -462,6 +461,8 @@ public class HTTP2Stream extends IdleTimeout implements IStream, Callback, Dumpa
private void onFailure(FailureFrame frame, Callback callback) private void onFailure(FailureFrame frame, Callback callback)
{ {
// Don't close or remove the stream, as the listener may
// want to use it, for example to send a RST_STREAM frame.
notifyFailure(this, frame, callback); notifyFailure(this, frame, callback);
} }
@ -749,7 +750,7 @@ public class HTTP2Stream extends IdleTimeout implements IStream, Callback, Dumpa
{ {
try try
{ {
listener.onFailure(stream, frame.getError(), frame.getReason(), callback); listener.onFailure(stream, frame.getError(), frame.getReason(), frame.getFailure(), callback);
} }
catch (Throwable x) catch (Throwable x)
{ {

View File

@ -305,9 +305,10 @@ public interface Stream
* @param stream the stream * @param stream the stream
* @param error the error code * @param error the error code
* @param reason the error reason, or null * @param reason the error reason, or null
* @param failure the failure
* @param callback the callback to complete when the failure has been handled * @param callback the callback to complete when the failure has been handled
*/ */
public default void onFailure(Stream stream, int error, String reason, Callback callback) public default void onFailure(Stream stream, int error, String reason, Throwable failure, Callback callback)
{ {
callback.succeeded(); callback.succeeded();
} }

View File

@ -22,12 +22,14 @@ public class FailureFrame extends Frame
{ {
private final int error; private final int error;
private final String reason; private final String reason;
private final Throwable failure;
public FailureFrame(int error, String reason) public FailureFrame(int error, String reason, Throwable failure)
{ {
super(FrameType.FAILURE); super(FrameType.FAILURE);
this.error = error; this.error = error;
this.reason = reason; this.reason = reason;
this.failure = failure;
} }
public int getError() public int getError()
@ -39,4 +41,9 @@ public class FailureFrame extends Frame
{ {
return reason; return reason;
} }
public Throwable getFailure()
{
return failure;
}
} }

View File

@ -61,7 +61,7 @@ public class Generator
this.generators[FrameType.WINDOW_UPDATE.getType()] = new WindowUpdateGenerator(headerGenerator); this.generators[FrameType.WINDOW_UPDATE.getType()] = new WindowUpdateGenerator(headerGenerator);
this.generators[FrameType.CONTINUATION.getType()] = null; // Never generated explicitly. this.generators[FrameType.CONTINUATION.getType()] = null; // Never generated explicitly.
this.generators[FrameType.PREFACE.getType()] = new PrefaceGenerator(); this.generators[FrameType.PREFACE.getType()] = new PrefaceGenerator();
this.generators[FrameType.DISCONNECT.getType()] = new DisconnectGenerator(); this.generators[FrameType.DISCONNECT.getType()] = new NoOpGenerator();
this.dataGenerator = new DataGenerator(headerGenerator); this.dataGenerator = new DataGenerator(headerGenerator);
} }

View File

@ -21,9 +21,9 @@ package org.eclipse.jetty.http2.generator;
import org.eclipse.jetty.http2.frames.Frame; import org.eclipse.jetty.http2.frames.Frame;
import org.eclipse.jetty.io.ByteBufferPool; import org.eclipse.jetty.io.ByteBufferPool;
public class DisconnectGenerator extends FrameGenerator public class NoOpGenerator extends FrameGenerator
{ {
public DisconnectGenerator() public NoOpGenerator()
{ {
super(null); super(null);
} }

View File

@ -304,11 +304,11 @@ public class HpackEncoder
String encoding = null; String encoding = null;
// Is there an entry for the field? // Is there an index entry for the field?
Entry entry = _context.get(field); Entry entry = _context.get(field);
if (entry != null) if (entry != null)
{ {
// Known field entry, so encode it as indexed // This is a known indexed field, send as static or dynamic indexed.
if (entry.isStatic()) if (entry.isStatic())
{ {
buffer.put(((StaticEntry)entry).getEncodedField()); buffer.put(((StaticEntry)entry).getEncodedField());
@ -326,10 +326,10 @@ public class HpackEncoder
} }
else else
{ {
// Unknown field entry, so we will have to send literally. // Unknown field entry, so we will have to send literally, but perhaps add an index.
final boolean indexed; final boolean indexed;
// But do we know it's name? // Do we know its name?
HttpHeader header = field.getHeader(); HttpHeader header = field.getHeader();
// Select encoding strategy // Select encoding strategy
@ -347,12 +347,11 @@ public class HpackEncoder
if (_debug) if (_debug)
encoding = indexed ? "PreEncodedIdx" : "PreEncoded"; encoding = indexed ? "PreEncodedIdx" : "PreEncoded";
} }
// has the custom header name been seen before? else if (name == null && fieldSize < _context.getMaxDynamicTableSize())
else if (name == null)
{ {
// unknown name and value, so let's index this just in case it is // unknown name and value that will fit in dynamic table, so let's index
// the first time we have seen a custom name or a custom field. // this just in case it is the first time we have seen a custom name or a
// unless the name is changing, this is worthwhile // custom field. Unless the name is once only, this is worthwhile
indexed = true; indexed = true;
encodeName(buffer, (byte)0x40, 6, field.getName(), null); encodeName(buffer, (byte)0x40, 6, field.getName(), null);
encodeValue(buffer, true, field.getValue()); encodeValue(buffer, true, field.getValue());
@ -361,7 +360,7 @@ public class HpackEncoder
} }
else else
{ {
// known custom name, but unknown value. // Known name, but different value.
// This is probably a custom field with changing value, so don't index. // This is probably a custom field with changing value, so don't index.
indexed = false; indexed = false;
encodeName(buffer, (byte)0x00, 4, field.getName(), null); encodeName(buffer, (byte)0x00, 4, field.getName(), null);
@ -400,9 +399,9 @@ public class HpackEncoder
(huffman ? "HuffV" : "LitV") + (huffman ? "HuffV" : "LitV") +
(neverIndex ? "!!Idx" : "!Idx"); (neverIndex ? "!!Idx" : "!Idx");
} }
else if (fieldSize >= _context.getMaxDynamicTableSize() || header == HttpHeader.CONTENT_LENGTH && field.getValue().length() > 2) else if (fieldSize >= _context.getMaxDynamicTableSize() || header == HttpHeader.CONTENT_LENGTH && !"0".equals(field.getValue()))
{ {
// Non indexed if field too large or a content length for 3 digits or more // The field is too large or a non zero content length, so do not index.
indexed = false; indexed = false;
encodeName(buffer, (byte)0x00, 4, header.asString(), name); encodeName(buffer, (byte)0x00, 4, header.asString(), name);
encodeValue(buffer, true, field.getValue()); encodeValue(buffer, true, field.getValue());

View File

@ -22,6 +22,7 @@ import java.nio.ByteBuffer;
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.HttpVersion; import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.http.MetaData; import org.eclipse.jetty.http.MetaData;
import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.BufferUtil;
@ -145,6 +146,54 @@ public class HpackEncoderTest
assertEquals(5, encoder.getHpackContext().size()); assertEquals(5, encoder.getHpackContext().size());
} }
@Test
public void testLargeFieldsNotIndexed()
{
HpackEncoder encoder = new HpackEncoder(38 * 5);
HpackContext ctx = encoder.getHpackContext();
ByteBuffer buffer = BufferUtil.allocate(4096);
// Index little fields
int pos = BufferUtil.flipToFill(buffer);
encoder.encode(buffer, new HttpField("Name", "Value"));
BufferUtil.flipToFlush(buffer, pos);
int dynamicTableSize = ctx.getDynamicTableSize();
assertThat(dynamicTableSize, Matchers.greaterThan(0));
// Do not index big field
StringBuilder largeName = new StringBuilder("largeName-");
String filler = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX";
while (largeName.length() < ctx.getMaxDynamicTableSize())
largeName.append(filler, 0, Math.min(filler.length(), ctx.getMaxDynamicTableSize() - largeName.length()));
pos = BufferUtil.flipToFill(buffer);
encoder.encode(buffer, new HttpField(largeName.toString(), "Value"));
BufferUtil.flipToFlush(buffer, pos);
assertThat(ctx.getDynamicTableSize(), Matchers.is(dynamicTableSize));
}
@Test
public void testIndexContentLength()
{
HpackEncoder encoder = new HpackEncoder(38 * 5);
HpackContext ctx = encoder.getHpackContext();
ByteBuffer buffer = BufferUtil.allocate(4096);
// Index zero content length
int pos = BufferUtil.flipToFill(buffer);
encoder.encode(buffer, new HttpField(HttpHeader.CONTENT_LENGTH, "0"));
BufferUtil.flipToFlush(buffer, pos);
int dynamicTableSize = ctx.getDynamicTableSize();
assertThat(dynamicTableSize, Matchers.greaterThan(0));
// Do not index non zero content length
pos = BufferUtil.flipToFill(buffer);
encoder.encode(buffer, new HttpField(HttpHeader.CONTENT_LENGTH, "42"));
BufferUtil.flipToFlush(buffer, pos);
assertThat(ctx.getDynamicTableSize(), Matchers.is(dynamicTableSize));
}
@Test @Test
public void testNeverIndexSetCookie() throws Exception public void testNeverIndexSetCookie() throws Exception
{ {

View File

@ -18,8 +18,6 @@
package org.eclipse.jetty.http2.client.http; package org.eclipse.jetty.http2.client.http;
import java.io.IOException;
import org.eclipse.jetty.client.HttpChannel; import org.eclipse.jetty.client.HttpChannel;
import org.eclipse.jetty.client.HttpDestination; import org.eclipse.jetty.client.HttpDestination;
import org.eclipse.jetty.client.HttpExchange; import org.eclipse.jetty.client.HttpExchange;
@ -208,10 +206,10 @@ public class HttpChannelOverHTTP2 extends HttpChannel
} }
@Override @Override
public void onFailure(Stream stream, int error, String reason, Callback callback) public void onFailure(Stream stream, int error, String reason, Throwable failure, Callback callback)
{ {
HTTP2Channel.Client channel = (HTTP2Channel.Client)((IStream)stream).getAttachment(); HTTP2Channel.Client channel = (HTTP2Channel.Client)((IStream)stream).getAttachment();
channel.onFailure(new IOException(String.format("Failure %s/%s", ErrorCode.toString(error, null), reason)), callback); channel.onFailure(failure, callback);
} }
@Override @Override

View File

@ -196,7 +196,7 @@ public class HttpConnectionOverHTTP2 extends HttpConnection implements Sweeper.S
{ {
if (closed.compareAndSet(false, true)) if (closed.compareAndSet(false, true))
{ {
getHttpDestination().close(this); getHttpDestination().remove(this);
abort(failure); abort(failure);

View File

@ -30,6 +30,10 @@
<configuration> <configuration>
<mainClass>org.eclipse.jetty.http2.server.H2SpecServer</mainClass> <mainClass>org.eclipse.jetty.http2.server.H2SpecServer</mainClass>
<skip>${skipTests}</skip> <skip>${skipTests}</skip>
<excludeSpecs>
<!-- see: https://github.com/summerwind/h2spec/issues/115 -->
<excludeSpec>6.9.2 - Changes SETTINGS_INITIAL_WINDOW_SIZE after sending HEADERS frame</excludeSpec>
</excludeSpecs>
</configuration> </configuration>
<executions> <executions>
<execution> <execution>

View File

@ -23,7 +23,6 @@ import java.io.IOException;
import java.util.ArrayDeque; import java.util.ArrayDeque;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Base64; import java.util.Base64;
import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.Queue; import java.util.Queue;
@ -43,7 +42,6 @@ import org.eclipse.jetty.http2.HTTP2Channel;
import org.eclipse.jetty.http2.HTTP2Connection; import org.eclipse.jetty.http2.HTTP2Connection;
import org.eclipse.jetty.http2.ISession; import org.eclipse.jetty.http2.ISession;
import org.eclipse.jetty.http2.IStream; import org.eclipse.jetty.http2.IStream;
import org.eclipse.jetty.http2.api.Stream;
import org.eclipse.jetty.http2.api.server.ServerSessionListener; import org.eclipse.jetty.http2.api.server.ServerSessionListener;
import org.eclipse.jetty.http2.frames.DataFrame; import org.eclipse.jetty.http2.frames.DataFrame;
import org.eclipse.jetty.http2.frames.Frame; import org.eclipse.jetty.http2.frames.Frame;
@ -59,7 +57,6 @@ import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.HttpConfiguration; import org.eclipse.jetty.server.HttpConfiguration;
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.CountingCallback;
import org.eclipse.jetty.util.TypeUtil; import org.eclipse.jetty.util.TypeUtil;
public class HTTP2ServerConnection extends HTTP2Connection public class HTTP2ServerConnection extends HTTP2Connection
@ -208,13 +205,17 @@ public class HTTP2ServerConnection extends HTTP2Connection
public void onStreamFailure(IStream stream, Throwable failure, Callback callback) public void onStreamFailure(IStream stream, Throwable failure, Callback callback)
{ {
if (LOG.isDebugEnabled()) if (LOG.isDebugEnabled())
LOG.debug("Processing failure on {}: {}", stream, failure); LOG.debug("Processing stream failure on {}", stream, failure);
HTTP2Channel.Server channel = (HTTP2Channel.Server)stream.getAttachment(); HTTP2Channel.Server channel = (HTTP2Channel.Server)stream.getAttachment();
if (channel != null) if (channel != null)
{ {
Runnable task = channel.onFailure(failure, callback); Runnable task = channel.onFailure(failure, callback);
if (task != null) if (task != null)
{
// We must dispatch to another thread because the task
// may call application code that performs blocking I/O.
offerTask(task, true); offerTask(task, true);
}
} }
else else
{ {
@ -239,22 +240,10 @@ public class HTTP2ServerConnection extends HTTP2Connection
public void onSessionFailure(Throwable failure, Callback callback) public void onSessionFailure(Throwable failure, Callback callback)
{ {
ISession session = getSession();
if (LOG.isDebugEnabled()) if (LOG.isDebugEnabled())
LOG.debug("Processing failure on {}: {}", session, failure); LOG.debug("Processing session failure on {}", getSession(), failure);
Collection<Stream> streams = session.getStreams(); // All the streams have already been failed, just succeed the callback.
if (streams.isEmpty()) callback.succeeded();
{
callback.succeeded();
}
else
{
CountingCallback counter = new CountingCallback(callback, streams.size());
for (Stream stream : streams)
{
onStreamFailure((IStream)stream, failure, counter);
}
}
} }
public void push(Connector connector, IStream stream, MetaData.Request request) public void push(Connector connector, IStream stream, MetaData.Request request)

View File

@ -34,6 +34,7 @@ import org.eclipse.jetty.http2.frames.PushPromiseFrame;
import org.eclipse.jetty.http2.frames.ResetFrame; import org.eclipse.jetty.http2.frames.ResetFrame;
import org.eclipse.jetty.io.EndPoint; import org.eclipse.jetty.io.EndPoint;
import org.eclipse.jetty.io.EofException; import org.eclipse.jetty.io.EofException;
import org.eclipse.jetty.io.QuietException;
import org.eclipse.jetty.server.Connector; import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.HttpConfiguration; import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.NegotiatingServerConnection.CipherDiscriminator; import org.eclipse.jetty.server.NegotiatingServerConnection.CipherDiscriminator;
@ -122,19 +123,14 @@ public class HTTP2ServerConnectionFactory extends AbstractHTTP2ServerConnectionF
return getConnection().onSessionTimeout(new TimeoutException("Session idle timeout " + idleTimeout + " ms")); return getConnection().onSessionTimeout(new TimeoutException("Session idle timeout " + idleTimeout + " ms"));
} }
@Override
public boolean onIdleTimeout(Stream stream, Throwable x)
{
return getConnection().onStreamTimeout((IStream)stream, x);
}
@Override @Override
public void onClose(Session session, GoAwayFrame frame, Callback callback) public void onClose(Session session, GoAwayFrame frame, Callback callback)
{ {
String reason = frame.tryConvertPayload(); String reason = frame.tryConvertPayload();
if (!StringUtil.isEmpty(reason)) if (!StringUtil.isEmpty(reason))
reason = " (" + reason + ")"; reason = " (" + reason + ")";
getConnection().onSessionFailure(new EofException(String.format("Close %s/%s", ErrorCode.toString(frame.getError(), null), reason)), callback); EofException failure = new EofException(String.format("Close %s/%s", ErrorCode.toString(frame.getError(), null), reason));
onFailure(session, failure, callback);
} }
@Override @Override
@ -143,12 +139,6 @@ public class HTTP2ServerConnectionFactory extends AbstractHTTP2ServerConnectionF
getConnection().onSessionFailure(failure, callback); getConnection().onSessionFailure(failure, callback);
} }
@Override
public void onFailure(Stream stream, int error, String reason, Callback callback)
{
getConnection().onStreamFailure((IStream)stream, new EofException(String.format("Failure %s/%s", ErrorCode.toString(error, null), reason)), callback);
}
@Override @Override
public void onHeaders(Stream stream, HeadersFrame frame) public void onHeaders(Stream stream, HeadersFrame frame)
{ {
@ -175,7 +165,27 @@ public class HTTP2ServerConnectionFactory extends AbstractHTTP2ServerConnectionF
@Override @Override
public void onReset(Stream stream, ResetFrame frame, Callback callback) public void onReset(Stream stream, ResetFrame frame, Callback callback)
{ {
getConnection().onStreamFailure((IStream)stream, new EofException("Reset " + ErrorCode.toString(frame.getError(), null)), callback); EofException failure = new EofException("Reset " + ErrorCode.toString(frame.getError(), null));
onFailure(stream, failure, callback);
}
@Override
public void onFailure(Stream stream, int error, String reason, Throwable failure, Callback callback)
{
if (!(failure instanceof QuietException))
failure = new EofException(failure);
onFailure(stream, failure, callback);
}
private void onFailure(Stream stream, Throwable failure, Callback callback)
{
getConnection().onStreamFailure((IStream)stream, failure, callback);
}
@Override
public boolean onIdleTimeout(Stream stream, Throwable x)
{
return getConnection().onStreamTimeout((IStream)stream, x);
} }
private void close(Stream stream, String reason) private void close(Stream stream, String reason)

View File

@ -20,6 +20,7 @@ package org.eclipse.jetty.http2.server;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;
import java.util.function.Supplier; import java.util.function.Supplier;
import org.eclipse.jetty.http.BadMessageException; import org.eclipse.jetty.http.BadMessageException;
@ -82,7 +83,7 @@ public class HttpTransportOverHTTP2 implements HttpTransport
} }
@Override @Override
public void send(MetaData.Request request, MetaData.Response response, ByteBuffer content, boolean lastContent, Callback callback) public void send(MetaData.Request request, final MetaData.Response response, ByteBuffer content, boolean lastContent, Callback callback)
{ {
boolean isHeadRequest = HttpMethod.HEAD.is(request.getMethod()); boolean isHeadRequest = HttpMethod.HEAD.is(request.getMethod());
boolean hasContent = BufferUtil.hasContent(content) && !isHeadRequest; boolean hasContent = BufferUtil.hasContent(content) && !isHeadRequest;
@ -100,8 +101,8 @@ public class HttpTransportOverHTTP2 implements HttpTransport
} }
else else
{ {
if (transportCallback.start(callback, false)) transportCallback.send(callback, false, c ->
sendHeadersFrame(response, false, transportCallback); sendHeadersFrame(metaData, false, c));
} }
} }
else else
@ -114,7 +115,7 @@ public class HttpTransportOverHTTP2 implements HttpTransport
long contentLength = response.getContentLength(); long contentLength = response.getContentLength();
if (contentLength < 0) if (contentLength < 0)
{ {
response = new MetaData.Response( metaData = new MetaData.Response(
response.getHttpVersion(), response.getHttpVersion(),
response.getStatus(), response.getStatus(),
response.getReason(), response.getReason(),
@ -142,53 +143,53 @@ public class HttpTransportOverHTTP2 implements HttpTransport
HttpFields trailers = retrieveTrailers(); HttpFields trailers = retrieveTrailers();
if (trailers != null) if (trailers != null)
{ {
if (transportCallback.start(new SendTrailers(getCallback(), trailers), false)) transportCallback.send(new SendTrailers(getCallback(), trailers), false, c ->
sendDataFrame(content, true, false, transportCallback); sendDataFrame(content, true, false, c));
} }
else else
{ {
if (transportCallback.start(getCallback(), false)) transportCallback.send(getCallback(), false, c ->
sendDataFrame(content, true, true, transportCallback); sendDataFrame(content, true, true, c));
} }
} }
else else
{ {
if (transportCallback.start(getCallback(), false)) transportCallback.send(getCallback(), false, c ->
sendDataFrame(content, false, false, transportCallback); sendDataFrame(content, false, false, c));
} }
} }
}; };
if (transportCallback.start(commitCallback, true)) transportCallback.send(commitCallback, true, c ->
sendHeadersFrame(response, false, transportCallback); sendHeadersFrame(metaData, false, c));
} }
else else
{ {
if (lastContent) if (lastContent)
{ {
if (isTunnel(request, response)) if (isTunnel(request, metaData))
{ {
if (transportCallback.start(callback, true)) transportCallback.send(callback, true, c ->
sendHeadersFrame(response, false, transportCallback); sendHeadersFrame(metaData, false, c));
} }
else else
{ {
HttpFields trailers = retrieveTrailers(); HttpFields trailers = retrieveTrailers();
if (trailers != null) if (trailers != null)
{ {
if (transportCallback.start(new SendTrailers(callback, trailers), true)) transportCallback.send(new SendTrailers(callback, trailers), true, c ->
sendHeadersFrame(response, false, transportCallback); sendHeadersFrame(metaData, false, c));
} }
else else
{ {
if (transportCallback.start(callback, true)) transportCallback.send(callback, true, c ->
sendHeadersFrame(response, true, transportCallback); sendHeadersFrame(metaData, true, c));
} }
} }
} }
else else
{ {
if (transportCallback.start(callback, true)) transportCallback.send(callback, true, c ->
sendHeadersFrame(response, false, transportCallback); sendHeadersFrame(metaData, false, c));
} }
} }
} }
@ -210,8 +211,8 @@ public class HttpTransportOverHTTP2 implements HttpTransport
SendTrailers sendTrailers = new SendTrailers(callback, trailers); SendTrailers sendTrailers = new SendTrailers(callback, trailers);
if (hasContent) if (hasContent)
{ {
if (transportCallback.start(sendTrailers, false)) transportCallback.send(sendTrailers, false, c ->
sendDataFrame(content, true, false, transportCallback); sendDataFrame(content, true, false, c));
} }
else else
{ {
@ -220,14 +221,14 @@ public class HttpTransportOverHTTP2 implements HttpTransport
} }
else else
{ {
if (transportCallback.start(callback, false)) transportCallback.send(callback, false, c ->
sendDataFrame(content, true, true, transportCallback); sendDataFrame(content, true, true, c));
} }
} }
else else
{ {
if (transportCallback.start(callback, false)) transportCallback.send(callback, false, c ->
sendDataFrame(content, false, false, transportCallback); sendDataFrame(content, false, false, c));
} }
} }
else else
@ -334,7 +335,7 @@ public class HttpTransportOverHTTP2 implements HttpTransport
public boolean onStreamTimeout(Throwable failure) public boolean onStreamTimeout(Throwable failure)
{ {
return transportCallback.onIdleTimeout(failure); return transportCallback.idleTimeout(failure);
} }
/** /**
@ -397,119 +398,359 @@ public class HttpTransportOverHTTP2 implements HttpTransport
stream.reset(new ResetFrame(stream.getId(), ErrorCode.CANCEL_STREAM_ERROR.code), Callback.NOOP); stream.reset(new ResetFrame(stream.getId(), ErrorCode.CANCEL_STREAM_ERROR.code), Callback.NOOP);
} }
/**
* <p>Callback that controls sends initiated by the transport, by eventually
* notifying a nested callback.</p>
* <p>There are 3 sources of concurrency after a send is initiated:</p>
* <ul>
* <li>the completion of the send operation, either success or failure</li>
* <li>an asynchronous failure coming from the read side such as a stream
* being reset, or the connection being closed</li>
* <li>an asynchronous idle timeout</li>
* </ul>
* <p>The last 2 cases may happen <em>during</em> a send, when the frames
* are being generated in the flusher.
* In such cases, this class must avoid that the nested callback is notified
* while the frame generation is in progress, because the nested callback
* may modify other states (such as clearing the {@code HttpOutput._buffer})
* that are accessed during frame generation.</p>
* <p>The solution implemented in this class works by splitting the send
* operation in 3 parts: {@code pre-send}, {@code send} and {@code post-send}.
* Asynchronous state changes happening during {@code send} are stored
* and only executed in {@code post-send}, therefore never interfering
* with frame generation.</p>
*
* @see State
*/
private class TransportCallback implements Callback private class TransportCallback implements Callback
{ {
private State state = State.IDLE; private State _state = State.IDLE;
private Callback callback; private Callback _callback;
private Throwable failure; private boolean _commit;
private boolean commit; private Throwable _failure;
public boolean start(Callback callback, boolean commit) private void reset(Throwable failure)
{ {
State state; assert Thread.holdsLock(this);
_state = failure != null ? State.FAILED : State.IDLE;
_callback = null;
_commit = false;
_failure = failure;
}
private void send(Callback callback, boolean commit, Consumer<Callback> sendFrame)
{
Throwable failure = sending(callback, commit);
if (failure == null)
{
sendFrame.accept(this);
pending();
}
else
{
callback.failed(failure);
}
}
private Throwable sending(Callback callback, boolean commit)
{
synchronized (this)
{
switch (_state)
{
case IDLE:
{
_state = State.SENDING;
_callback = callback;
_commit = commit;
return null;
}
case FAILED:
{
return _failure;
}
default:
{
return new IllegalStateException("Invalid transport state: " + _state);
}
}
}
}
private void pending()
{
Callback callback;
boolean commit;
Throwable failure; Throwable failure;
synchronized (this) synchronized (this)
{ {
state = this.state; switch (_state)
failure = this.failure;
if (state == State.IDLE)
{ {
this.state = State.WRITING; case SENDING:
this.callback = callback; {
this.commit = commit; // The send has not completed the callback yet,
return true; // wait for succeeded() or failed() to be called.
_state = State.PENDING;
return;
}
case SUCCEEDING:
{
// The send already completed successfully, but the
// call to succeeded() was delayed, so call it now.
callback = _callback;
commit = _commit;
failure = null;
reset(null);
break;
}
case FAILING:
{
// The send already completed with a failure, but
// the call to failed() was delayed, so call it now.
callback = _callback;
commit = _commit;
failure = _failure;
reset(failure);
break;
}
default:
{
callback = _callback;
commit = _commit;
failure = new IllegalStateException("Invalid transport state: " + _state);
reset(failure);
break;
}
} }
} }
if (failure == null) if (failure == null)
failure = new IllegalStateException("Invalid transport state: " + state); succeed(callback, commit);
callback.failed(failure); else
return false; fail(callback, commit, failure);
} }
@Override @Override
public void succeeded() public void succeeded()
{ {
Callback callback;
boolean commit; boolean commit;
Callback callback = null;
synchronized (this) synchronized (this)
{ {
commit = this.commit; switch (_state)
if (state == State.WRITING)
{ {
this.state = State.IDLE; case SENDING:
callback = this.callback; {
this.callback = null; _state = State.SUCCEEDING;
this.commit = false; // Succeeding the callback will be done in postSend().
return;
}
case PENDING:
{
callback = _callback;
commit = _commit;
reset(null);
break;
}
default:
{
// This thread lost the race to succeed the current
// send, as other threads likely already failed it.
return;
}
} }
} }
if (LOG.isDebugEnabled()) succeed(callback, commit);
LOG.debug("HTTP2 Response #{}/{} {} {}",
stream.getId(), Integer.toHexString(stream.getSession().hashCode()),
commit ? "commit" : "flush",
callback == null ? "failure" : "success");
if (callback != null)
callback.succeeded();
} }
@Override @Override
public void failed(Throwable failure) public void failed(Throwable failure)
{ {
boolean commit;
Callback callback; Callback callback;
boolean commit;
synchronized (this) synchronized (this)
{ {
commit = this.commit; switch (_state)
this.state = State.FAILED; {
callback = this.callback; case SENDING:
this.callback = null; {
this.failure = failure; _state = State.FAILING;
_failure = failure;
// Failing the callback will be done in postSend().
return;
}
case IDLE:
case PENDING:
{
callback = _callback;
commit = _commit;
reset(failure);
break;
}
default:
{
// This thread lost the race to fail the current send,
// as other threads already succeeded or failed it.
return;
}
}
} }
fail(callback, commit, failure);
}
private boolean idleTimeout(Throwable failure)
{
Callback callback;
boolean timeout;
synchronized (this)
{
switch (_state)
{
case PENDING:
{
// The send was started but idle timed out, fail it.
callback = _callback;
timeout = true;
reset(failure);
break;
}
case IDLE:
// The application may be suspended, ignore the idle timeout.
case SENDING:
// A send has been started at the same time of an idle timeout;
// Ignore the idle timeout and let the write continue normally.
case SUCCEEDING:
case FAILING:
// An idle timeout during these transient states is ignored.
case FAILED:
// Already failed, ignore the idle timeout.
{
callback = null;
timeout = false;
break;
}
default:
{
// Should not happen, but just in case.
callback = _callback;
if (callback == null)
callback = Callback.NOOP;
timeout = true;
failure = new IllegalStateException("Invalid transport state: " + _state, failure);
reset(failure);
break;
}
}
}
idleTimeout(callback, timeout, failure);
return timeout;
}
private void succeed(Callback callback, boolean commit)
{
if (LOG.isDebugEnabled()) if (LOG.isDebugEnabled())
LOG.debug(String.format("HTTP2 Response #%d/%h %s %s", stream.getId(), stream.getSession(), LOG.debug("HTTP2 Response #{}/{} {} success",
commit ? "commit" : "flush", callback == null ? "ignored" : "failed"), failure); stream.getId(), Integer.toHexString(stream.getSession().hashCode()),
commit ? "commit" : "flush");
callback.succeeded();
}
private void fail(Callback callback, boolean commit, Throwable failure)
{
if (LOG.isDebugEnabled())
LOG.debug("HTTP2 Response #{}/{} {} failure",
stream.getId(), Integer.toHexString(stream.getSession().hashCode()),
commit ? "commit" : "flush",
failure);
if (callback != null) if (callback != null)
callback.failed(failure); callback.failed(failure);
} }
private boolean onIdleTimeout(Throwable failure) private void idleTimeout(Callback callback, boolean timeout, Throwable failure)
{ {
boolean result;
Callback callback = null;
synchronized (this)
{
// Ignore idle timeouts if not writing,
// as the application may be suspended.
result = state == State.WRITING;
if (result)
{
this.state = State.TIMEOUT;
callback = this.callback;
this.callback = null;
this.failure = failure;
}
}
if (LOG.isDebugEnabled()) if (LOG.isDebugEnabled())
LOG.debug(String.format("HTTP2 Response #%d/%h idle timeout %s", stream.getId(), stream.getSession(), result ? "expired" : "ignored"), failure); LOG.debug("HTTP2 Response #{}/{} idle timeout {}",
if (result) stream.getId(), Integer.toHexString(stream.getSession().hashCode()),
timeout ? "expired" : "ignored",
failure);
if (timeout)
callback.failed(failure); callback.failed(failure);
return result;
}
@Override
public InvocationType getInvocationType()
{
Callback callback;
synchronized (this)
{
callback = this.callback;
}
return callback != null ? callback.getInvocationType() : Callback.super.getInvocationType();
} }
} }
/**
* <p>Send states for {@link TransportCallback}.</p>
*
* @see TransportCallback
*/
private enum State private enum State
{ {
IDLE, WRITING, FAILED, TIMEOUT /**
* <p>No send initiated or in progress.</p>
* <p>Next states could be:</p>
* <ul>
* <li>{@link #SENDING}, when {@link TransportCallback#send(Callback, boolean, Consumer)}
* is called by the transport to initiate a send</li>
* <li>{@link #FAILED}, when {@link TransportCallback#failed(Throwable)}
* is called by an asynchronous failure</li>
* </ul>
*/
IDLE,
/**
* <p>A send is initiated; the nested callback in {@link TransportCallback}
* cannot be notified while in this state.</p>
* <p>Next states could be:</p>
* <ul>
* <li>{@link #SUCCEEDING}, when {@link TransportCallback#succeeded()}
* is called synchronously because the send succeeded</li>
* <li>{@link #FAILING}, when {@link TransportCallback#failed(Throwable)}
* is called synchronously because the send failed</li>
* <li>{@link #PENDING}, when {@link TransportCallback#pending()}
* is called before the send completes</li>
* </ul>
*/
SENDING,
/**
* <p>A send was initiated and is now pending, waiting for the {@link TransportCallback}
* to be notified of success or failure.</p>
* <p>Next states could be:</p>
* <ul>
* <li>{@link #IDLE}, when {@link TransportCallback#succeeded()}
* is called because the send succeeded</li>
* <li>{@link #FAILED}, when {@link TransportCallback#failed(Throwable)}
* is called because either the send failed, or an asynchronous failure happened</li>
* </ul>
*/
PENDING,
/**
* <p>A send was initiated and succeeded, but {@link TransportCallback#pending()}
* has not been called yet.</p>
* <p>This state indicates that the success actions (such as notifying the
* {@link TransportCallback} nested callback) must be performed when
* {@link TransportCallback#pending()} is called.</p>
* <p>Next states could be:</p>
* <ul>
* <li>{@link #IDLE}, when {@link TransportCallback#pending()}
* is called</li>
* </ul>
*/
SUCCEEDING,
/**
* <p>A send was initiated and failed, but {@link TransportCallback#pending()}
* has not been called yet.</p>
* <p>This state indicates that the failure actions (such as notifying the
* {@link TransportCallback} nested callback) must be performed when
* {@link TransportCallback#pending()} is called.</p>
* <p>Next states could be:</p>
* <ul>
* <li>{@link #FAILED}, when {@link TransportCallback#pending()}
* is called</li>
* </ul>
*/
FAILING,
/**
* <p>The terminal state indicating failure of the send.</p>
*/
FAILED
} }
private class SendTrailers extends Callback.Nested private class SendTrailers extends Callback.Nested
@ -525,8 +766,8 @@ public class HttpTransportOverHTTP2 implements HttpTransport
@Override @Override
public void succeeded() public void succeeded()
{ {
if (transportCallback.start(getCallback(), false)) transportCallback.send(getCallback(), false, c ->
sendTrailersFrame(new MetaData(HttpVersion.HTTP_2, trailers), transportCallback); sendTrailersFrame(new MetaData(HttpVersion.HTTP_2, trailers), c));
} }
} }
} }

View File

@ -249,7 +249,7 @@ public class OpenIdAuthenticator extends LoginAuthenticator
if (!mandatory) if (!mandatory)
return new DeferredAuthentication(this); return new DeferredAuthentication(this);
if (isErrorPage(URIUtil.addPaths(request.getServletPath(), request.getPathInfo())) && !DeferredAuthentication.isDeferred(response)) if (isErrorPage(baseRequest.getPathInContext()) && !DeferredAuthentication.isDeferred(response))
return new DeferredAuthentication(this); return new DeferredAuthentication(this);
try try

View File

@ -34,21 +34,31 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
*/ */
public class FooContextListener implements ServletContextListener public class FooContextListener implements ServletContextListener
{ {
static int ___initialized;
static int __destroyed;
@Override @Override
public void contextInitialized(ServletContextEvent sce) public void contextInitialized(ServletContextEvent sce)
{ {
++___initialized;
ServletRegistration defaultRego = sce.getServletContext().getServletRegistration("default"); ServletRegistration defaultRego = sce.getServletContext().getServletRegistration("default");
Collection<String> mappings = defaultRego.getMappings(); Collection<String> mappings = defaultRego.getMappings();
assertThat("/", is(in(mappings))); assertThat("/", is(in(mappings)));
Set<String> otherMappings = sce.getServletContext().getServletRegistration("foo").addMapping("/"); ServletRegistration rego = sce.getServletContext().getServletRegistration("foo");
assertTrue(otherMappings.isEmpty()); if (rego != null)
Collection<String> fooMappings = sce.getServletContext().getServletRegistration("foo").getMappings(); {
assertThat("/", is(in(fooMappings))); Set<String> otherMappings = rego.addMapping("/");
assertTrue(otherMappings.isEmpty());
Collection<String> fooMappings = rego.getMappings();
assertThat("/", is(in(fooMappings)));
}
} }
@Override @Override
public void contextDestroyed(ServletContextEvent sce) public void contextDestroyed(ServletContextEvent sce)
{ {
++__destroyed;
} }
} }

View File

@ -79,7 +79,7 @@ public class TestQuickStart
fooHolder.setName("foo"); fooHolder.setName("foo");
quickstart.getServletHandler().addServlet(fooHolder); quickstart.getServletHandler().addServlet(fooHolder);
ListenerHolder lholder = new ListenerHolder(); ListenerHolder lholder = new ListenerHolder();
lholder.setListener(new FooContextListener()); lholder.setClassName("org.eclipse.jetty.quickstart.FooContextListener");
quickstart.getServletHandler().addListener(lholder); quickstart.getServletHandler().addListener(lholder);
server.setHandler(quickstart); server.setHandler(quickstart);
server.setDryRun(true); server.setDryRun(true);
@ -177,4 +177,30 @@ public class TestQuickStart
assertEquals("ascii", webapp.getDefaultRequestCharacterEncoding()); assertEquals("ascii", webapp.getDefaultRequestCharacterEncoding());
assertEquals("utf-16", webapp.getDefaultResponseCharacterEncoding()); assertEquals("utf-16", webapp.getDefaultResponseCharacterEncoding());
} }
@Test
public void testListenersNotCalledInPreConfigure() throws Exception
{
File quickstartXml = new File(webInf, "quickstart-web.xml");
assertFalse(quickstartXml.exists());
Server server = new Server();
WebAppContext quickstart = new WebAppContext();
quickstart.addConfiguration(new QuickStartConfiguration());
quickstart.setAttribute(QuickStartConfiguration.MODE, QuickStartConfiguration.Mode.GENERATE);
quickstart.setAttribute(QuickStartConfiguration.ORIGIN_ATTRIBUTE, "origin");
//add a listener directly to the ContextHandler so it is there when we start -
//if you add them to the ServletHandler (like StandardDescriptorProcessor does)
//then they are not added to the ContextHandler in a pre-generate.
quickstart.addEventListener(new FooContextListener());
quickstart.setResourceBase(testDir.getAbsolutePath());
server.setHandler(quickstart);
server.setDryRun(true);
server.start();
assertTrue(quickstartXml.exists());
assertEquals(0, FooContextListener.___initialized);
}
} }

View File

@ -195,7 +195,7 @@ public class RuleContainer extends Rule implements Dumpable
} }
if (_rewritePathInfo) if (_rewritePathInfo)
baseRequest.setPathInfo(applied); baseRequest.setContext(baseRequest.getContext(), applied);
target = applied; target = applied;

View File

@ -85,7 +85,7 @@ public class RewriteHandlerTest extends AbstractRuleTestCase
_handler.setRewriteRequestURI(true); _handler.setRewriteRequestURI(true);
_handler.setRewritePathInfo(true); _handler.setRewritePathInfo(true);
_request.setHttpURI(HttpURI.build(_request.getHttpURI(), "/xxx/bar")); _request.setHttpURI(HttpURI.build(_request.getHttpURI(), "/xxx/bar"));
_request.setPathInfo("/xxx/bar"); _request.setContext(_request.getContext(), "/xxx/bar");
_handler.handle("/xxx/bar", _request, _request, _response); _handler.handle("/xxx/bar", _request, _request, _response);
assertEquals(201, _response.getStatus()); assertEquals(201, _response.getStatus());
assertEquals("/bar/zzz", _request.getAttribute("target")); assertEquals("/bar/zzz", _request.getAttribute("target"));
@ -99,7 +99,7 @@ public class RewriteHandlerTest extends AbstractRuleTestCase
_handler.setRewriteRequestURI(false); _handler.setRewriteRequestURI(false);
_handler.setRewritePathInfo(false); _handler.setRewritePathInfo(false);
_request.setHttpURI(HttpURI.build(_request.getHttpURI(), "/foo/bar")); _request.setHttpURI(HttpURI.build(_request.getHttpURI(), "/foo/bar"));
_request.setPathInfo("/foo/bar"); _request.setContext(_request.getContext(), "/foo/bar");
_handler.handle("/foo/bar", _request, _request, _response); _handler.handle("/foo/bar", _request, _request, _response);
assertEquals(201, _response.getStatus()); assertEquals(201, _response.getStatus());
@ -112,7 +112,7 @@ public class RewriteHandlerTest extends AbstractRuleTestCase
_request.setHandled(false); _request.setHandled(false);
_handler.setOriginalPathAttribute(null); _handler.setOriginalPathAttribute(null);
_request.setHttpURI(HttpURI.build(_request.getHttpURI(), "/aaa/bar")); _request.setHttpURI(HttpURI.build(_request.getHttpURI(), "/aaa/bar"));
_request.setPathInfo("/aaa/bar"); _request.setContext(_request.getContext(), "/aaa/bar");
_handler.handle("/aaa/bar", _request, _request, _response); _handler.handle("/aaa/bar", _request, _request, _response);
assertEquals(201, _response.getStatus()); assertEquals(201, _response.getStatus());
assertEquals("/ddd/bar", _request.getAttribute("target")); assertEquals("/ddd/bar", _request.getAttribute("target"));
@ -126,7 +126,7 @@ public class RewriteHandlerTest extends AbstractRuleTestCase
_handler.setRewriteRequestURI(true); _handler.setRewriteRequestURI(true);
_handler.setRewritePathInfo(true); _handler.setRewritePathInfo(true);
_request.setHttpURI(HttpURI.build(_request.getHttpURI(), "/aaa/bar")); _request.setHttpURI(HttpURI.build(_request.getHttpURI(), "/aaa/bar"));
_request.setPathInfo("/aaa/bar"); _request.setContext(_request.getContext(), "/aaa/bar");
_handler.handle("/aaa/bar", _request, _request, _response); _handler.handle("/aaa/bar", _request, _request, _response);
assertEquals(201, _response.getStatus()); assertEquals(201, _response.getStatus());
assertEquals("/ddd/bar", _request.getAttribute("target")); assertEquals("/ddd/bar", _request.getAttribute("target"));
@ -138,7 +138,7 @@ public class RewriteHandlerTest extends AbstractRuleTestCase
_request.setHandled(false); _request.setHandled(false);
_rule2.setTerminating(true); _rule2.setTerminating(true);
_request.setHttpURI(HttpURI.build(_request.getHttpURI(), "/aaa/bar")); _request.setHttpURI(HttpURI.build(_request.getHttpURI(), "/aaa/bar"));
_request.setPathInfo("/aaa/bar"); _request.setContext(_request.getContext(), "/aaa/bar");
_handler.handle("/aaa/bar", _request, _request, _response); _handler.handle("/aaa/bar", _request, _request, _response);
assertEquals(201, _response.getStatus()); assertEquals(201, _response.getStatus());
assertEquals("/ccc/bar", _request.getAttribute("target")); assertEquals("/ccc/bar", _request.getAttribute("target"));
@ -154,7 +154,7 @@ public class RewriteHandlerTest extends AbstractRuleTestCase
_request.setAttribute("URI", null); _request.setAttribute("URI", null);
_request.setAttribute("info", null); _request.setAttribute("info", null);
_request.setHttpURI(HttpURI.build(_request.getHttpURI(), "/aaa/bar")); _request.setHttpURI(HttpURI.build(_request.getHttpURI(), "/aaa/bar"));
_request.setPathInfo("/aaa/bar"); _request.setContext(_request.getContext(), "/aaa/bar");
_handler.handle("/aaa/bar", _request, _request, _response); _handler.handle("/aaa/bar", _request, _request, _response);
assertEquals(200, _response.getStatus()); assertEquals(200, _response.getStatus());
assertEquals(null, _request.getAttribute("target")); assertEquals(null, _request.getAttribute("target"));
@ -173,7 +173,7 @@ public class RewriteHandlerTest extends AbstractRuleTestCase
_handler.setRewriteRequestURI(true); _handler.setRewriteRequestURI(true);
_handler.setRewritePathInfo(false); _handler.setRewritePathInfo(false);
_request.setHttpURI(HttpURI.build(_request.getHttpURI(), "/ccc/x%20y")); _request.setHttpURI(HttpURI.build(_request.getHttpURI(), "/ccc/x%20y"));
_request.setPathInfo("/ccc/x y"); _request.setContext(_request.getContext(), "/ccc/x y");
_handler.handle("/ccc/x y", _request, _request, _response); _handler.handle("/ccc/x y", _request, _request, _response);
assertEquals(201, _response.getStatus()); assertEquals(201, _response.getStatus());
assertEquals("/ddd/x y", _request.getAttribute("target")); assertEquals("/ddd/x y", _request.getAttribute("target"));
@ -190,7 +190,7 @@ public class RewriteHandlerTest extends AbstractRuleTestCase
_handler.setRewriteRequestURI(true); _handler.setRewriteRequestURI(true);
_handler.setRewritePathInfo(false); _handler.setRewritePathInfo(false);
_request.setHttpURI(HttpURI.build(_request.getHttpURI(), "/xxx/x%20y")); _request.setHttpURI(HttpURI.build(_request.getHttpURI(), "/xxx/x%20y"));
_request.setPathInfo("/xxx/x y"); _request.setContext(_request.getContext(), "/xxx/x y");
_handler.handle("/xxx/x y", _request, _request, _response); _handler.handle("/xxx/x y", _request, _request, _response);
assertEquals(201, _response.getStatus()); assertEquals(201, _response.getStatus());
assertEquals("/x y/zzz", _request.getAttribute("target")); assertEquals("/x y/zzz", _request.getAttribute("target"));

View File

@ -254,7 +254,7 @@ public class FormAuthenticator extends LoginAuthenticator
if (!mandatory) if (!mandatory)
return new DeferredAuthentication(this); return new DeferredAuthentication(this);
if (isLoginOrErrorPage(URIUtil.addPaths(request.getServletPath(), request.getPathInfo())) && !DeferredAuthentication.isDeferred(response)) if (isLoginOrErrorPage(baseRequest.getPathInContext()) && !DeferredAuthentication.isDeferred(response))
return new DeferredAuthentication(this); return new DeferredAuthentication(this);
try try

View File

@ -15,6 +15,7 @@
<Set name="saveOnCreate"><Property name="jetty.session.saveOnCreate" default="false" /></Set> <Set name="saveOnCreate"><Property name="jetty.session.saveOnCreate" default="false" /></Set>
<Set name="removeUnloadableSessions"><Property name="jetty.session.removeUnloadableSessions" default="false"/></Set> <Set name="removeUnloadableSessions"><Property name="jetty.session.removeUnloadableSessions" default="false"/></Set>
<Set name="flushOnResponseCommit"><Property name="jetty.session.flushOnResponseCommit" default="false"/></Set> <Set name="flushOnResponseCommit"><Property name="jetty.session.flushOnResponseCommit" default="false"/></Set>
<Set name="invalidateOnShutdown"><Property name="jetty.session.invalidateOnShutdown" default="false"/></Set>
</New> </New>
</Arg> </Arg>
</Call> </Call>

View File

@ -34,7 +34,7 @@ etc/jetty-http.xml
# jetty.http.selectors=-1 # jetty.http.selectors=-1
## ServerSocketChannel backlog (0 picks platform default) ## ServerSocketChannel backlog (0 picks platform default)
# jetty.http.acceptorQueueSize=0 # jetty.http.acceptQueueSize=0
## Thread priority delta to give to acceptor threads ## Thread priority delta to give to acceptor threads
# jetty.http.acceptorPriorityDelta=0 # jetty.http.acceptorPriorityDelta=0

View File

@ -23,3 +23,4 @@ etc/sessions/session-cache-hash.xml
#jetty.session.saveOnCreate=false #jetty.session.saveOnCreate=false
#jetty.session.removeUnloadableSessions=false #jetty.session.removeUnloadableSessions=false
#jetty.session.flushOnResponseCommit=false #jetty.session.flushOnResponseCommit=false
#jetty.session.invalidateOnShutdown=false

View File

@ -18,4 +18,4 @@ etc/sessions/session-cache-null.xml
[ini-template] [ini-template]
#jetty.session.saveOnCreate=false #jetty.session.saveOnCreate=false
#jetty.session.removeUnloadableSessions=false #jetty.session.removeUnloadableSessions=false
#jetty.session.flushOnResponseCommit=false #jetty.session.flushOnResponseCommit=false

View File

@ -35,7 +35,7 @@ etc/jetty-ssl-context.xml
# jetty.ssl.selectors=-1 # jetty.ssl.selectors=-1
## ServerSocketChannel backlog (0 picks platform default) ## ServerSocketChannel backlog (0 picks platform default)
# jetty.ssl.acceptorQueueSize=0 # jetty.ssl.acceptQueueSize=0
## Thread priority delta to give to acceptor threads ## Thread priority delta to give to acceptor threads
# jetty.ssl.acceptorPriorityDelta=0 # jetty.ssl.acceptorPriorityDelta=0

View File

@ -90,6 +90,8 @@ public class Dispatcher implements RequestDispatcher
final DispatcherType old_type = baseRequest.getDispatcherType(); final DispatcherType old_type = baseRequest.getDispatcherType();
final Attributes old_attr = baseRequest.getAttributes(); final Attributes old_attr = baseRequest.getAttributes();
final MultiMap<String> old_query_params = baseRequest.getQueryParameters(); final MultiMap<String> old_query_params = baseRequest.getQueryParameters();
final ContextHandler.Context old_context = baseRequest.getContext();
final ServletPathMapping old_mapping = baseRequest.getServletPathMapping();
try try
{ {
baseRequest.setDispatcherType(DispatcherType.INCLUDE); baseRequest.setDispatcherType(DispatcherType.INCLUDE);
@ -100,7 +102,14 @@ public class Dispatcher implements RequestDispatcher
} }
else else
{ {
IncludeAttributes attr = new IncludeAttributes(old_attr, _uri.getPath(), _contextHandler.getContextPath(), _pathInContext, _uri.getQuery()); IncludeAttributes attr = new IncludeAttributes(
old_attr,
baseRequest,
old_context,
old_mapping,
_uri.getPath(),
_pathInContext,
_uri.getQuery());
if (attr._query != null) if (attr._query != null)
baseRequest.mergeQueryParameters(baseRequest.getQueryString(), attr._query); baseRequest.mergeQueryParameters(baseRequest.getQueryString(), attr._query);
baseRequest.setAttributes(attr); baseRequest.setAttributes(attr);
@ -136,11 +145,10 @@ public class Dispatcher implements RequestDispatcher
response = new ServletResponseHttpWrapper(response); response = new ServletResponseHttpWrapper(response);
final HttpURI old_uri = baseRequest.getHttpURI(); final HttpURI old_uri = baseRequest.getHttpURI();
final String old_context_path = baseRequest.getContextPath(); final ContextHandler.Context old_context = baseRequest.getContext();
final String old_servlet_path = baseRequest.getServletPath(); final String old_path_in_context = baseRequest.getPathInContext();
final String old_path_info = baseRequest.getPathInfo();
final ServletPathMapping old_mapping = baseRequest.getServletPathMapping(); final ServletPathMapping old_mapping = baseRequest.getServletPathMapping();
final ServletPathMapping source_mapping = baseRequest.findServletPathMapping();
final MultiMap<String> old_query_params = baseRequest.getQueryParameters(); final MultiMap<String> old_query_params = baseRequest.getQueryParameters();
final Attributes old_attr = baseRequest.getAttributes(); final Attributes old_attr = baseRequest.getAttributes();
final DispatcherType old_type = baseRequest.getDispatcherType(); final DispatcherType old_type = baseRequest.getDispatcherType();
@ -161,30 +169,21 @@ public class Dispatcher implements RequestDispatcher
// for queryString is allowed to be null, but cannot be null for the other values. // for queryString is allowed to be null, but cannot be null for the other values.
// Note: the pathInfo is passed as the pathInContext since it is only used when there is // Note: the pathInfo is passed as the pathInContext since it is only used when there is
// no mapping, and when there is no mapping the pathInfo is the pathInContext. // no mapping, and when there is no mapping the pathInfo is the pathInContext.
// TODO Ultimately it is intended for the request to carry the pathInContext for easy access if (old_attr.getAttribute(FORWARD_REQUEST_URI) == null)
ForwardAttributes attr = old_attr.getAttribute(FORWARD_REQUEST_URI) != null baseRequest.setAttributes(new ForwardAttributes(old_attr,
? new ForwardAttributes(old_attr, old_uri.getPath(),
(String)old_attr.getAttribute(FORWARD_REQUEST_URI), old_context == null ? null : old_context.getContextHandler().getContextPathEncoded(),
(String)old_attr.getAttribute(FORWARD_CONTEXT_PATH), baseRequest.getPathInContext(),
(String)old_attr.getAttribute(FORWARD_PATH_INFO), source_mapping,
(ServletPathMapping)old_attr.getAttribute(FORWARD_MAPPING), old_uri.getQuery()));
(String)old_attr.getAttribute(FORWARD_QUERY_STRING))
: new ForwardAttributes(old_attr,
old_uri.getPath(),
old_context_path,
baseRequest.getPathInfo(), // TODO replace with pathInContext
old_mapping,
old_uri.getQuery());
String query = _uri.getQuery(); String query = _uri.getQuery();
if (query == null) if (query == null)
query = old_uri.getQuery(); query = old_uri.getQuery();
baseRequest.setHttpURI(HttpURI.build(old_uri, _uri.getPath(), _uri.getParam(), query)); baseRequest.setHttpURI(HttpURI.build(old_uri, _uri.getPath(), _uri.getParam(), query));
baseRequest.setContextPath(_contextHandler.getContextPath()); baseRequest.setContext(_contextHandler.getServletContext(), _pathInContext);
baseRequest.setServletPathMapping(null); baseRequest.setServletPathMapping(null);
baseRequest.setServletPath(null);
baseRequest.setPathInfo(_pathInContext);
if (_uri.getQuery() != null || old_uri.getQuery() != null) if (_uri.getQuery() != null || old_uri.getQuery() != null)
{ {
@ -207,8 +206,6 @@ public class Dispatcher implements RequestDispatcher
} }
} }
baseRequest.setAttributes(attr);
_contextHandler.handle(_pathInContext, baseRequest, (HttpServletRequest)request, (HttpServletResponse)response); _contextHandler.handle(_pathInContext, baseRequest, (HttpServletRequest)request, (HttpServletResponse)response);
// If we are not async and not closed already, then close via the possibly wrapped response. // If we are not async and not closed already, then close via the possibly wrapped response.
@ -228,9 +225,8 @@ public class Dispatcher implements RequestDispatcher
finally finally
{ {
baseRequest.setHttpURI(old_uri); baseRequest.setHttpURI(old_uri);
baseRequest.setContextPath(old_context_path); baseRequest.setContext(old_context, old_path_in_context);
baseRequest.setServletPath(old_servlet_path); baseRequest.setServletPathMapping(old_mapping);
baseRequest.setPathInfo(old_path_info);
baseRequest.setQueryParameters(old_query_params); baseRequest.setQueryParameters(old_query_params);
baseRequest.resetParameters(); baseRequest.resetParameters();
baseRequest.setAttributes(old_attr); baseRequest.setAttributes(old_attr);
@ -346,23 +342,44 @@ public class Dispatcher implements RequestDispatcher
} }
} }
private class IncludeAttributes extends Attributes.Wrapper /**
* Attributes Wrapper to provide the {@link DispatcherType#INCLUDE} attributes.
*
* The source {@link org.eclipse.jetty.server.handler.ContextHandler.Context} and
* {@link ServletPathMapping} instances are also retained by this wrapper so they
* may be used by {@link Request#getContextPath()}, {@link Request#getServletPath()},
* {@link Request#getPathInfo()} and {@link Request#getHttpServletMapping()}.
*/
class IncludeAttributes extends Attributes.Wrapper
{ {
private final Request _baseRequest;
private final ContextHandler.Context _sourceContext;
private final ServletPathMapping _sourceMapping;
private final String _requestURI; private final String _requestURI;
private final String _contextPath;
private final String _pathInContext; private final String _pathInContext;
private ServletPathMapping _servletPathMapping; // Set later by ServletHandler
private final String _query; private final String _query;
public IncludeAttributes(Attributes attributes, String requestURI, String contextPath, String pathInContext, String query) public IncludeAttributes(Attributes attributes, Request baseRequest, ContextHandler.Context sourceContext, ServletPathMapping sourceMapping, String requestURI, String pathInContext, String query)
{ {
super(attributes); super(attributes);
_baseRequest = baseRequest;
_sourceMapping = sourceMapping;
_requestURI = requestURI; _requestURI = requestURI;
_contextPath = contextPath; _sourceContext = sourceContext;
_pathInContext = pathInContext; _pathInContext = pathInContext;
_query = query; _query = query;
} }
ContextHandler.Context getSourceContext()
{
return _sourceContext;
}
ServletPathMapping getSourceMapping()
{
return _sourceMapping;
}
@Override @Override
public Object getAttribute(String key) public Object getAttribute(String key)
{ {
@ -371,17 +388,26 @@ public class Dispatcher implements RequestDispatcher
switch (key) switch (key)
{ {
case INCLUDE_PATH_INFO: case INCLUDE_PATH_INFO:
return _servletPathMapping == null ? _pathInContext : _servletPathMapping.getPathInfo(); {
ServletPathMapping mapping = _baseRequest.getServletPathMapping();
return mapping == null ? _pathInContext : mapping.getPathInfo();
}
case INCLUDE_SERVLET_PATH: case INCLUDE_SERVLET_PATH:
return _servletPathMapping == null ? null : _servletPathMapping.getServletPath(); {
ServletPathMapping mapping = _baseRequest.getServletPathMapping();
return mapping == null ? null : mapping.getServletPath();
}
case INCLUDE_CONTEXT_PATH: case INCLUDE_CONTEXT_PATH:
return _contextPath; {
ContextHandler.Context context = _baseRequest.getContext();
return context == null ? null : context.getContextHandler().getContextPathEncoded();
}
case INCLUDE_QUERY_STRING: case INCLUDE_QUERY_STRING:
return _query; return _query;
case INCLUDE_REQUEST_URI: case INCLUDE_REQUEST_URI:
return _requestURI; return _requestURI;
case INCLUDE_MAPPING: case INCLUDE_MAPPING:
return _servletPathMapping; return _baseRequest.getServletPathMapping();
default: default:
break; break;
} }
@ -416,12 +442,9 @@ public class Dispatcher implements RequestDispatcher
@Override @Override
public void setAttribute(String key, Object value) public void setAttribute(String key, Object value)
{ {
if (_servletPathMapping == null && _named == null && INCLUDE_MAPPING.equals(key)) // Allow any attribute to be set, even if a reserved name. If a reserved
_servletPathMapping = (ServletPathMapping)value; // name is set here, it will be revealed after the include is complete.
else _attributes.setAttribute(key, value);
// Allow any attribute to be set, even if a reserved name. If a reserved
// name is set here, it will be revealed after the include is complete.
_attributes.setAttribute(key, value);
} }
@Override @Override

View File

@ -822,13 +822,16 @@ public class HttpChannelState
// check the actions of the listeners // check the actions of the listeners
synchronized (this) synchronized (this)
{ {
// If we are still async and nobody has called sendError
if (_requestState == RequestState.ASYNC && !_sendError) if (_requestState == RequestState.ASYNC && !_sendError)
// Then the listeners did not invoke API methods {
// and the container must provide a default error dispatch. // The listeners did not invoke API methods and the
// container must provide a default error dispatch.
sendError(th); sendError(th);
else }
else if (_requestState != RequestState.COMPLETE)
{
LOG.warn("unhandled in state " + _requestState, new IllegalStateException(th)); LOG.warn("unhandled in state " + _requestState, new IllegalStateException(th));
}
} }
} }

View File

@ -34,7 +34,6 @@ import org.eclipse.jetty.http.HttpHeaderValue;
import org.eclipse.jetty.http.HttpMethod; import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpParser; import org.eclipse.jetty.http.HttpParser;
import org.eclipse.jetty.http.HttpParser.RequestHandler; import org.eclipse.jetty.http.HttpParser.RequestHandler;
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;
import org.eclipse.jetty.io.AbstractConnection; import org.eclipse.jetty.io.AbstractConnection;
@ -49,6 +48,8 @@ import org.eclipse.jetty.util.IteratingCallback;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import static org.eclipse.jetty.http.HttpStatus.INTERNAL_SERVER_ERROR_500;
/** /**
* <p>A {@link Connection} that handles the HTTP protocol.</p> * <p>A {@link Connection} that handles the HTTP protocol.</p>
*/ */
@ -67,7 +68,6 @@ public class HttpConnection extends AbstractConnection implements Runnable, Http
private final HttpParser _parser; private final HttpParser _parser;
private final AtomicInteger _contentBufferReferences = new AtomicInteger(); private final AtomicInteger _contentBufferReferences = new AtomicInteger();
private volatile ByteBuffer _requestBuffer = null; private volatile ByteBuffer _requestBuffer = null;
private volatile ByteBuffer _chunk = null;
private final BlockingReadCallback _blockingReadCallback = new BlockingReadCallback(); private final BlockingReadCallback _blockingReadCallback = new BlockingReadCallback();
private final AsyncReadCallback _asyncReadCallback = new AsyncReadCallback(); private final AsyncReadCallback _asyncReadCallback = new AsyncReadCallback();
private final SendCallback _sendCallback = new SendCallback(); private final SendCallback _sendCallback = new SendCallback();
@ -464,11 +464,6 @@ public class HttpConnection extends AbstractConnection implements Runnable, Http
_parser.close(); _parser.close();
} }
// Not in a race here with onFillable, because it has given up control before calling handle.
// in a slight race with #completed, but not sure what to do with that anyway.
if (_chunk != null)
_bufferPool.release(_chunk);
_chunk = null;
_generator.reset(); _generator.reset();
// if we are not called from the onfillable thread, schedule completion // if we are not called from the onfillable thread, schedule completion
@ -718,6 +713,7 @@ public class HttpConnection extends AbstractConnection implements Runnable, Http
private boolean _lastContent; private boolean _lastContent;
private Callback _callback; private Callback _callback;
private ByteBuffer _header; private ByteBuffer _header;
private ByteBuffer _chunk;
private boolean _shutdownOut; private boolean _shutdownOut;
private SendCallback() private SendCallback()
@ -763,10 +759,9 @@ public class HttpConnection extends AbstractConnection implements Runnable, Http
throw new IllegalStateException(); throw new IllegalStateException();
boolean useDirectByteBuffers = isUseOutputDirectByteBuffers(); boolean useDirectByteBuffers = isUseOutputDirectByteBuffers();
ByteBuffer chunk = _chunk;
while (true) while (true)
{ {
HttpGenerator.Result result = _generator.generateResponse(_info, _head, _header, chunk, _content, _lastContent); HttpGenerator.Result result = _generator.generateResponse(_info, _head, _header, _chunk, _content, _lastContent);
if (LOG.isDebugEnabled()) if (LOG.isDebugEnabled())
LOG.debug("generate: {} for {} ({},{},{})@{}", LOG.debug("generate: {} for {} ({},{},{})@{}",
result, result,
@ -788,23 +783,21 @@ public class HttpConnection extends AbstractConnection implements Runnable, Http
} }
case HEADER_OVERFLOW: case HEADER_OVERFLOW:
{ {
int capacity = _header.capacity(); if (_header.capacity() >= _config.getResponseHeaderSize())
_bufferPool.release(_header); throw new BadMessageException(INTERNAL_SERVER_ERROR_500, "Response header too large");
if (capacity >= _config.getResponseHeaderSize()) releaseHeader();
throw new BadMessageException(HttpStatus.INTERNAL_SERVER_ERROR_500, "Response header too large");
_header = _bufferPool.acquire(_config.getResponseHeaderSize(), useDirectByteBuffers); _header = _bufferPool.acquire(_config.getResponseHeaderSize(), useDirectByteBuffers);
continue; continue;
} }
case NEED_CHUNK: case NEED_CHUNK:
{ {
chunk = _chunk = _bufferPool.acquire(HttpGenerator.CHUNK_SIZE, useDirectByteBuffers); _chunk = _bufferPool.acquire(HttpGenerator.CHUNK_SIZE, useDirectByteBuffers);
continue; continue;
} }
case NEED_CHUNK_TRAILER: case NEED_CHUNK_TRAILER:
{ {
if (_chunk != null) releaseChunk();
_bufferPool.release(_chunk); _chunk = _bufferPool.acquire(_config.getResponseHeaderSize(), useDirectByteBuffers);
chunk = _chunk = _bufferPool.acquire(_config.getResponseHeaderSize(), useDirectByteBuffers);
continue; continue;
} }
case FLUSH: case FLUSH:
@ -812,7 +805,7 @@ public class HttpConnection extends AbstractConnection implements Runnable, Http
// Don't write the chunk or the content if this is a HEAD response, or any other type of response that should have no content // Don't write the chunk or the content if this is a HEAD response, or any other type of response that should have no content
if (_head || _generator.isNoContent()) if (_head || _generator.isNoContent())
{ {
BufferUtil.clear(chunk); BufferUtil.clear(_chunk);
BufferUtil.clear(_content); BufferUtil.clear(_content);
} }
@ -823,10 +816,10 @@ public class HttpConnection extends AbstractConnection implements Runnable, Http
gatherWrite += 4; gatherWrite += 4;
bytes += _header.remaining(); bytes += _header.remaining();
} }
if (BufferUtil.hasContent(chunk)) if (BufferUtil.hasContent(_chunk))
{ {
gatherWrite += 2; gatherWrite += 2;
bytes += chunk.remaining(); bytes += _chunk.remaining();
} }
if (BufferUtil.hasContent(_content)) if (BufferUtil.hasContent(_content))
{ {
@ -837,10 +830,10 @@ public class HttpConnection extends AbstractConnection implements Runnable, Http
switch (gatherWrite) switch (gatherWrite)
{ {
case 7: case 7:
getEndPoint().write(this, _header, chunk, _content); getEndPoint().write(this, _header, _chunk, _content);
break; break;
case 6: case 6:
getEndPoint().write(this, _header, chunk); getEndPoint().write(this, _header, _chunk);
break; break;
case 5: case 5:
getEndPoint().write(this, _header, _content); getEndPoint().write(this, _header, _content);
@ -849,10 +842,10 @@ public class HttpConnection extends AbstractConnection implements Runnable, Http
getEndPoint().write(this, _header); getEndPoint().write(this, _header);
break; break;
case 3: case 3:
getEndPoint().write(this, chunk, _content); getEndPoint().write(this, _chunk, _content);
break; break;
case 2: case 2:
getEndPoint().write(this, chunk); getEndPoint().write(this, _chunk);
break; break;
case 1: case 1:
getEndPoint().write(this, _content); getEndPoint().write(this, _content);
@ -896,10 +889,23 @@ public class HttpConnection extends AbstractConnection implements Runnable, Http
_callback = null; _callback = null;
_info = null; _info = null;
_content = null; _content = null;
releaseHeader();
releaseChunk();
return complete;
}
private void releaseHeader()
{
if (_header != null) if (_header != null)
_bufferPool.release(_header); _bufferPool.release(_header);
_header = null; _header = null;
return complete; }
private void releaseChunk()
{
if (_chunk != null)
_bufferPool.release(_chunk);
_chunk = null;
} }
@Override @Override

View File

@ -107,14 +107,16 @@ import org.slf4j.LoggerFactory;
* request object to be as lightweight as possible and not actually implement any significant behavior. For example * request object to be as lightweight as possible and not actually implement any significant behavior. For example
* <ul> * <ul>
* *
* <li>The {@link Request#getContextPath()} method will return null, until the request has been passed to a {@link ContextHandler} which matches the * <li>the {@link Request#getContextPath()} method will return null, until the request has been passed to a {@link ContextHandler} which matches the
* {@link Request#getPathInfo()} with a context path and calls {@link Request#setContextPath(String)} as a result.</li> * {@link Request#getPathInfo()} with a context path and calls {@link Request#setContext(Context,String)} as a result. For
* some dispatch types (ie include and named dispatch) the context path may not reflect the {@link ServletContext} set
* by {@link Request#setContext(Context, String)}.</li>
* *
* <li>the HTTP session methods will all return null sessions until such time as a request has been passed to a * <li>the HTTP session methods will all return null sessions until such time as a request has been passed to a
* {@link org.eclipse.jetty.server.session.SessionHandler} which checks for session cookies and enables the ability to create new sessions.</li> * {@link org.eclipse.jetty.server.session.SessionHandler} which checks for session cookies and enables the ability to create new sessions.</li>
* *
* <li>The {@link Request#getServletPath()} method will return null until the request has been passed to a <code>org.eclipse.jetty.servlet.ServletHandler</code> * <li>The {@link Request#getServletPath()} method will return "" until the request has been passed to a <code>org.eclipse.jetty.servlet.ServletHandler</code>
* and the pathInfo matched against the servlet URL patterns and {@link Request#setServletPath(String)} called as a result.</li> * and the pathInfo matched against the servlet URL patterns and {@link Request#setServletPathMapping(ServletPathMapping)} called as a result.</li>
* </ul> * </ul>
* *
* <p> * <p>
@ -198,9 +200,7 @@ public class Request implements HttpServletRequest
private HttpFields _trailers; private HttpFields _trailers;
private HttpURI _uri; private HttpURI _uri;
private String _method; private String _method;
private String _contextPath; private String _pathInContext;
private String _servletPath;
private String _pathInfo;
private ServletPathMapping _servletPathMapping; private ServletPathMapping _servletPathMapping;
private boolean _secure; private boolean _secure;
private String _asyncNotSupportedSource = null; private String _asyncNotSupportedSource = null;
@ -777,7 +777,44 @@ public class Request implements HttpServletRequest
@Override @Override
public String getContextPath() public String getContextPath()
{ {
return _contextPath; // The context path returned is normally for the current context. Except during a cross context
// INCLUDE dispatch, in which case this method returns the context path of the source context,
// which we recover from the IncludeAttributes wrapper.
Context context;
if (_dispatcherType == DispatcherType.INCLUDE)
{
Dispatcher.IncludeAttributes include = Attributes.unwrap(_attributes, Dispatcher.IncludeAttributes.class);
context = (include == null) ? _context : include.getSourceContext();
}
else
{
context = _context;
}
if (context == null)
return null;
// For some reason the spec requires the context path to be encoded (unlike getServletPath).
String contextPath = context.getContextHandler().getContextPathEncoded();
// For the root context, the spec requires that the empty string is returned instead of the leading '/'
// which is included in the pathInContext
if (URIUtil.SLASH.equals(contextPath))
return "";
return contextPath;
}
/** Get the path in the context.
*
* The path relative to the context path, analogous to {@link #getServletPath()} + {@link #getPathInfo()}.
* If no context is set, then the path in context is the full path.
* @return The decoded part of the {@link #getRequestURI()} path after any {@link #getContextPath()}
* up to any {@link #getQueryString()}, excluding path parameters.
* @see #setContext(Context, String)
*/
public String getPathInContext()
{
return _pathInContext;
} }
@Override @Override
@ -1048,15 +1085,20 @@ public class Request implements HttpServletRequest
@Override @Override
public String getPathInfo() public String getPathInfo()
{ {
return _pathInfo; // The pathInfo returned is normally for the current servlet. Except during an
// INCLUDE dispatch, in which case this method returns the pathInfo of the source servlet,
// which we recover from the IncludeAttributes wrapper.
ServletPathMapping mapping = findServletPathMapping();
return mapping == null ? _pathInContext : mapping.getPathInfo();
} }
@Override @Override
public String getPathTranslated() public String getPathTranslated()
{ {
if (_pathInfo == null || _context == null) String pathInfo = getPathInfo();
if (pathInfo == null || _context == null)
return null; return null;
return _context.getRealPath(_pathInfo); return _context.getRealPath(pathInfo);
} }
@Override @Override
@ -1207,7 +1249,7 @@ public class Request implements HttpServletRequest
// handle relative path // handle relative path
if (!path.startsWith("/")) if (!path.startsWith("/"))
{ {
String relTo = URIUtil.addPaths(_servletPath, _pathInfo); String relTo = _pathInContext;
int slash = relTo.lastIndexOf("/"); int slash = relTo.lastIndexOf("/");
if (slash > 1) if (slash > 1)
relTo = relTo.substring(0, slash + 1); relTo = relTo.substring(0, slash + 1);
@ -1335,9 +1377,11 @@ public class Request implements HttpServletRequest
@Override @Override
public String getServletPath() public String getServletPath()
{ {
if (_servletPath == null) // The servletPath returned is normally for the current servlet. Except during an
_servletPath = ""; // INCLUDE dispatch, in which case this method returns the servletPath of the source servlet,
return _servletPath; // which we recover from the IncludeAttributes wrapper.
ServletPathMapping mapping = findServletPathMapping();
return mapping == null ? "" : mapping.getServletPath();
} }
public ServletResponse getServletResponse() public ServletResponse getServletResponse()
@ -1678,10 +1722,10 @@ public class Request implements HttpServletRequest
if (path == null || path.isEmpty()) if (path == null || path.isEmpty())
{ {
setPathInfo(encoded == null ? "" : encoded); _pathInContext = encoded == null ? "" : encoded;
throw new BadMessageException(400, "Bad URI"); throw new BadMessageException(400, "Bad URI");
} }
setPathInfo(path); _pathInContext = path;
} }
public org.eclipse.jetty.http.MetaData.Request getMetaData() public org.eclipse.jetty.http.MetaData.Request getMetaData()
@ -1740,13 +1784,12 @@ public class Request implements HttpServletRequest
} }
_contentType = null; _contentType = null;
_characterEncoding = null; _characterEncoding = null;
_contextPath = null; _pathInContext = null;
if (_cookies != null) if (_cookies != null)
_cookies.reset(); _cookies.reset();
_cookiesExtracted = false; _cookiesExtracted = false;
_context = null; _context = null;
_newContext = false; _newContext = false;
_pathInfo = null;
_queryEncoding = null; _queryEncoding = null;
_requestedSessionId = null; _requestedSessionId = null;
_requestedSessionIdFromCookie = false; _requestedSessionIdFromCookie = false;
@ -1754,7 +1797,6 @@ public class Request implements HttpServletRequest
_session = null; _session = null;
_sessionHandler = null; _sessionHandler = null;
_scope = null; _scope = null;
_servletPath = null;
_timeStamp = 0; _timeStamp = 0;
_queryParameters = null; _queryParameters = null;
_contentParameters = null; _contentParameters = null;
@ -1831,6 +1873,13 @@ public class Request implements HttpServletRequest
} }
} }
/**
* Set the attributes for the request.
*
* @param attributes The attributes, which must be a {@link org.eclipse.jetty.util.Attributes.Wrapper}
* for which {@link Attributes#unwrap(Attributes)} will return the
* original {@link ServletAttributes}.
*/
public void setAttributes(Attributes attributes) public void setAttributes(Attributes attributes)
{ {
_attributes = attributes; _attributes = attributes;
@ -1864,7 +1913,7 @@ public class Request implements HttpServletRequest
// attributes there, under any other wrappers. // attributes there, under any other wrappers.
((ServletAttributes)baseAttributes).setAsyncAttributes(getRequestURI(), ((ServletAttributes)baseAttributes).setAsyncAttributes(getRequestURI(),
getContextPath(), getContextPath(),
getPathInfo(), // TODO change to pathInContext when cheaply available getPathInContext(),
getServletPathMapping(), getServletPathMapping(),
getQueryString()); getQueryString());
} }
@ -1955,25 +2004,24 @@ public class Request implements HttpServletRequest
} }
/** /**
* Set request context * Set request context and path in the context.
* *
* @param context context object * @param context context object
* @param pathInContext the part of the URI path that is withing the context.
* For servlets, this is equal to servletPath + pathInfo
*/ */
public void setContext(Context context) public void setContext(Context context, String pathInContext)
{ {
_newContext = _context != context; _newContext = _context != context;
if (context == null) _context = context;
_context = null; _pathInContext = pathInContext;
else if (context != null)
{
_context = context;
_errorContext = context; _errorContext = context;
}
} }
/** /**
* @return True if this is the first call of <code>takeNewContext()</code> since the last * @return True if this is the first call of <code>takeNewContext()</code> since the last
* {@link #setContext(org.eclipse.jetty.server.handler.ContextHandler.Context)} call. * {@link #setContext(org.eclipse.jetty.server.handler.ContextHandler.Context, String)} call.
*/ */
public boolean takeNewContext() public boolean takeNewContext()
{ {
@ -1982,17 +2030,6 @@ public class Request implements HttpServletRequest
return nc; return nc;
} }
/**
* Sets the "context path" for this request
*
* @param contextPath the context path for this request
* @see HttpServletRequest#getContextPath()
*/
public void setContextPath(String contextPath)
{
_contextPath = contextPath;
}
/** /**
* @param cookies The cookies to set. * @param cookies The cookies to set.
*/ */
@ -2026,14 +2063,6 @@ public class Request implements HttpServletRequest
return HttpMethod.HEAD.is(getMethod()); return HttpMethod.HEAD.is(getMethod());
} }
/**
* @param pathInfo The pathInfo to set.
*/
public void setPathInfo(String pathInfo)
{
_pathInfo = pathInfo;
}
/** /**
* Set the character encoding used for the query string. This call will effect the return of getQueryString and getParamaters. It must be called before any * Set the character encoding used for the query string. This call will effect the return of getQueryString and getParamaters. It must be called before any
* getParameter methods. * getParameter methods.
@ -2071,14 +2100,6 @@ public class Request implements HttpServletRequest
_requestedSessionIdFromCookie = requestedSessionIdCookie; _requestedSessionIdFromCookie = requestedSessionIdCookie;
} }
/**
* @param servletPath The servletPath to set.
*/
public void setServletPath(String servletPath)
{
_servletPath = servletPath;
}
/** /**
* @param session The session to set. * @param session The session to set.
*/ */
@ -2347,32 +2368,48 @@ public class Request implements HttpServletRequest
/** /**
* Set the servletPathMapping, the servletPath and the pathInfo. * Set the servletPathMapping, the servletPath and the pathInfo.
* TODO remove the side effect on servletPath and pathInfo by removing those fields.
* @param servletPathMapping The mapping used to return from {@link #getHttpServletMapping()} * @param servletPathMapping The mapping used to return from {@link #getHttpServletMapping()}
*/ */
public void setServletPathMapping(ServletPathMapping servletPathMapping) public void setServletPathMapping(ServletPathMapping servletPathMapping)
{ {
_servletPathMapping = servletPathMapping; _servletPathMapping = servletPathMapping;
if (servletPathMapping == null)
{
// TODO reset the servletPath and pathInfo, but currently cannot do that
// as we don't know the pathInContext.
}
else
{
_servletPath = servletPathMapping.getServletPath();
_pathInfo = servletPathMapping.getPathInfo();
}
} }
/**
* @return The mapping for the current target servlet, regardless of dispatch type.
*/
public ServletPathMapping getServletPathMapping() public ServletPathMapping getServletPathMapping()
{ {
return _servletPathMapping; return _servletPathMapping;
} }
/**
* @return The mapping for the target servlet reported by the {@link #getServletPath()} and
* {@link #getPathInfo()} methods. For {@link DispatcherType#INCLUDE} dispatches, this
* method returns the mapping of the source servlet, otherwise it returns the mapping of
* the target servlet.
*/
ServletPathMapping findServletPathMapping()
{
ServletPathMapping mapping;
if (_dispatcherType == DispatcherType.INCLUDE)
{
Dispatcher.IncludeAttributes include = Attributes.unwrap(_attributes, Dispatcher.IncludeAttributes.class);
mapping = (include == null) ? _servletPathMapping : include.getSourceMapping();
}
else
{
mapping = _servletPathMapping;
}
return mapping;
}
@Override @Override
public HttpServletMapping getHttpServletMapping() public HttpServletMapping getHttpServletMapping()
{ {
return _servletPathMapping; // The mapping returned is normally for the current servlet. Except during an
// INCLUDE dispatch, in which case this method returns the mapping of the source servlet,
// which we recover from the IncludeAttributes wrapper.
return findServletPathMapping();
} }
} }

View File

@ -230,26 +230,30 @@ public class Response implements HttpServletResponse
@Override @Override
public void addCookie(Cookie cookie) public void addCookie(Cookie cookie)
{ {
if (StringUtil.isBlank(cookie.getName())) //Servlet Spec 9.3 Include method: cannot set a cookie if handling an include
throw new IllegalArgumentException("Cookie.name cannot be blank/null"); if (isMutable())
{
if (StringUtil.isBlank(cookie.getName()))
throw new IllegalArgumentException("Cookie.name cannot be blank/null");
String comment = cookie.getComment(); String comment = cookie.getComment();
// HttpOnly was supported as a comment in cookie flags before the java.net.HttpCookie implementation so need to check that // HttpOnly was supported as a comment in cookie flags before the java.net.HttpCookie implementation so need to check that
boolean httpOnly = cookie.isHttpOnly() || HttpCookie.isHttpOnlyInComment(comment); boolean httpOnly = cookie.isHttpOnly() || HttpCookie.isHttpOnlyInComment(comment);
SameSite sameSite = HttpCookie.getSameSiteFromComment(comment); SameSite sameSite = HttpCookie.getSameSiteFromComment(comment);
comment = HttpCookie.getCommentWithoutAttributes(comment); comment = HttpCookie.getCommentWithoutAttributes(comment);
addCookie(new HttpCookie( addCookie(new HttpCookie(
cookie.getName(), cookie.getName(),
cookie.getValue(), cookie.getValue(),
cookie.getDomain(), cookie.getDomain(),
cookie.getPath(), cookie.getPath(),
cookie.getMaxAge(), cookie.getMaxAge(),
httpOnly, httpOnly,
cookie.getSecure(), cookie.getSecure(),
comment, comment,
cookie.getVersion(), cookie.getVersion(),
sameSite)); sameSite));
}
} }
/** /**
@ -302,7 +306,6 @@ public class Response implements HttpServletResponse
addCookie(cookie); addCookie(cookie);
} }
@Override
public boolean containsHeader(String name) public boolean containsHeader(String name)
{ {
return _fields.contains(name); return _fields.contains(name);
@ -332,7 +335,7 @@ public class Response implements HttpServletResponse
return url; return url;
if (request.getServerPort() != port) if (request.getServerPort() != port)
return url; return url;
if (!path.startsWith(request.getContextPath())) //TODO the root context path is "", with which every non null string starts if (request.getContext() != null && !path.startsWith(request.getContextPath()))
return url; return url;
} }

View File

@ -283,10 +283,9 @@ public class SecureRequestCustomizer implements HttpConfiguration.Customizer
request.getResponse().getHttpFields().add(_stsField); request.getResponse().getHttpFields().add(_stsField);
} }
private X509Certificate[] getCertChain(Request request, SSLSession sslSession) private X509Certificate[] getCertChain(Connector connector, SSLSession sslSession)
{ {
// The in-use SslContextFactory should be present in the Connector's SslConnectionFactory // The in-use SslContextFactory should be present in the Connector's SslConnectionFactory
Connector connector = request.getHttpChannel().getConnector();
SslConnectionFactory sslConnectionFactory = connector.getConnectionFactory(SslConnectionFactory.class); SslConnectionFactory sslConnectionFactory = connector.getConnectionFactory(SslConnectionFactory.class);
if (sslConnectionFactory != null) if (sslConnectionFactory != null)
{ {
@ -338,16 +337,16 @@ public class SecureRequestCustomizer implements HttpConfiguration.Customizer
switch (name) switch (name)
{ {
case JAVAX_SERVLET_REQUEST_X_509_CERTIFICATE: case JAVAX_SERVLET_REQUEST_X_509_CERTIFICATE:
return SecureRequestCustomizer.this.getCertChain(_request, _session); return getSslSessionData().getCerts();
case JAVAX_SERVLET_REQUEST_CIPHER_SUITE: case JAVAX_SERVLET_REQUEST_CIPHER_SUITE:
return _session.getCipherSuite(); return _session.getCipherSuite();
case JAVAX_SERVLET_REQUEST_KEY_SIZE: case JAVAX_SERVLET_REQUEST_KEY_SIZE:
return SslContextFactory.deduceKeyLength(_session.getCipherSuite()); return getSslSessionData().getKeySize();
case JAVAX_SERVLET_REQUEST_SSL_SESSION_ID: case JAVAX_SERVLET_REQUEST_SSL_SESSION_ID:
return TypeUtil.toHexString(_session.getId()); return getSslSessionData().getIdStr();
default: default:
String sessionAttribute = getSslSessionAttribute(); String sessionAttribute = getSslSessionAttribute();
@ -363,6 +362,31 @@ public class SecureRequestCustomizer implements HttpConfiguration.Customizer
return null; return null;
} }
/**
* Get data belonging to the {@link SSLSession}.
*
* @return the SslSessionData
*/
private SslSessionData getSslSessionData()
{
String key = SslSessionData.class.getName();
SslSessionData sslSessionData = (SslSessionData)_session.getValue(key);
if (sslSessionData == null)
{
String cipherSuite = _session.getCipherSuite();
int keySize = SslContextFactory.deduceKeyLength(cipherSuite);
X509Certificate[] certs = getCertChain(_request.getHttpChannel().getConnector(), _session);
byte[] bytes = _session.getId();
String idStr = TypeUtil.toHexString(bytes);
sslSessionData = new SslSessionData(keySize, certs, idStr);
_session.putValue(key, sslSessionData);
}
return sslSessionData;
}
@Override @Override
public Set<String> getAttributeNameSet() public Set<String> getAttributeNameSet()
{ {
@ -377,4 +401,36 @@ public class SecureRequestCustomizer implements HttpConfiguration.Customizer
return names; return names;
} }
} }
/**
* Simple bundle of data that is cached in the SSLSession.
*/
private static class SslSessionData
{
private final Integer _keySize;
private final X509Certificate[] _certs;
private final String _idStr;
private SslSessionData(Integer keySize, X509Certificate[] certs, String idStr)
{
this._keySize = keySize;
this._certs = certs;
this._idStr = idStr;
}
private Integer getKeySize()
{
return _keySize;
}
private X509Certificate[] getCerts()
{
return _certs;
}
private String getIdStr()
{
return _idStr;
}
}
} }

View File

@ -622,7 +622,7 @@ public class Server extends HandlerWrapper implements Attributes
baseRequest.mergeQueryParameters(oldUri.getQuery(), baseRequest.getQueryString()); baseRequest.mergeQueryParameters(oldUri.getQuery(), baseRequest.getQueryString());
} }
baseRequest.setPathInfo(baseRequest.getHttpURI().getDecodedPath()); baseRequest.setContext(null, baseRequest.getHttpURI().getDecodedPath());
handleAsync(channel, event, baseRequest); handleAsync(channel, event, baseRequest);
} }
finally finally

View File

@ -40,7 +40,6 @@ import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.IncludeExclude; import org.eclipse.jetty.util.IncludeExclude;
import org.eclipse.jetty.util.IteratingCallback; import org.eclipse.jetty.util.IteratingCallback;
import org.eclipse.jetty.util.StringUtil; import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.util.URIUtil;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -104,7 +103,7 @@ public class BufferedResponseHandler extends HandlerWrapper
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
{ {
final ServletContext context = baseRequest.getServletContext(); final ServletContext context = baseRequest.getServletContext();
final String path = context == null ? baseRequest.getRequestURI() : URIUtil.addPaths(baseRequest.getServletPath(), baseRequest.getPathInfo()); final String path = baseRequest.getPathInContext();
LOG.debug("{} handle {} in {}", this, baseRequest, context); LOG.debug("{} handle {} in {}", this, baseRequest, context);
HttpOutput out = baseRequest.getResponse().getHttpOutput(); HttpOutput out = baseRequest.getResponse().getHttpOutput();

View File

@ -956,6 +956,9 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu
protected void callContextInitialized(ServletContextListener l, ServletContextEvent e) protected void callContextInitialized(ServletContextListener l, ServletContextEvent e)
{ {
if (getServer().isDryRun())
return;
if (LOG.isDebugEnabled()) if (LOG.isDebugEnabled())
LOG.debug("contextInitialized: {}->{}", e, l); LOG.debug("contextInitialized: {}->{}", e, l);
l.contextInitialized(e); l.contextInitialized(e);
@ -963,6 +966,9 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu
protected void callContextDestroyed(ServletContextListener l, ServletContextEvent e) protected void callContextDestroyed(ServletContextListener l, ServletContextEvent e)
{ {
if (getServer().isDryRun())
return;
if (LOG.isDebugEnabled()) if (LOG.isDebugEnabled())
LOG.debug("contextDestroyed: {}->{}", e, l); LOG.debug("contextDestroyed: {}->{}", e, l);
l.contextDestroyed(e); l.contextDestroyed(e);
@ -1150,13 +1156,11 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu
if (LOG.isDebugEnabled()) if (LOG.isDebugEnabled())
LOG.debug("scope {}|{}|{} @ {}", baseRequest.getContextPath(), baseRequest.getServletPath(), baseRequest.getPathInfo(), this); LOG.debug("scope {}|{}|{} @ {}", baseRequest.getContextPath(), baseRequest.getServletPath(), baseRequest.getPathInfo(), this);
final Thread currentThread = Thread.currentThread();
final ClassLoader oldClassloader = currentThread.getContextClassLoader();
Context oldContext; Context oldContext;
String oldContextPath = null; String oldPathInContext = null;
String oldServletPath = null; String pathInContext = target;
String oldPathInfo = null;
ClassLoader oldClassloader = null;
Thread currentThread = null;
String pathInfo = target;
DispatcherType dispatch = baseRequest.getDispatcherType(); DispatcherType dispatch = baseRequest.getDispatcherType();
@ -1177,47 +1181,31 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu
{ {
if (_contextPath.length() > 1) if (_contextPath.length() > 1)
target = target.substring(_contextPath.length()); target = target.substring(_contextPath.length());
pathInfo = target; pathInContext = target;
} }
else if (_contextPath.length() == 1) else if (_contextPath.length() == 1)
{ {
target = URIUtil.SLASH; target = URIUtil.SLASH;
pathInfo = URIUtil.SLASH; pathInContext = URIUtil.SLASH;
} }
else else
{ {
target = URIUtil.SLASH; target = URIUtil.SLASH;
pathInfo = null; pathInContext = null;
} }
} }
// Set the classloader
if (_classLoader != null)
{
currentThread = Thread.currentThread();
oldClassloader = currentThread.getContextClassLoader();
currentThread.setContextClassLoader(_classLoader);
}
} }
if (_classLoader != null)
currentThread.setContextClassLoader(_classLoader);
try try
{ {
oldContextPath = baseRequest.getContextPath(); oldPathInContext = baseRequest.getPathInContext();
oldServletPath = baseRequest.getServletPath();
oldPathInfo = baseRequest.getPathInfo();
// Update the paths // Update the paths
baseRequest.setContext(_scontext); baseRequest.setContext(_scontext, pathInContext);
__context.set(_scontext); __context.set(_scontext);
if (!DispatcherType.INCLUDE.equals(dispatch) && target.startsWith("/"))
{
if (_contextPath.length() == 1)
baseRequest.setContextPath("");
else
baseRequest.setContextPath(getContextPathEncoded());
baseRequest.setServletPath(null);
baseRequest.setPathInfo(pathInfo);
}
if (oldContext != _scontext) if (oldContext != _scontext)
enterScope(baseRequest, dispatch); enterScope(baseRequest, dispatch);
@ -1234,17 +1222,12 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu
exitScope(baseRequest); exitScope(baseRequest);
// reset the classloader // reset the classloader
if (_classLoader != null && currentThread != null) if (_classLoader != null)
{
currentThread.setContextClassLoader(oldClassloader); currentThread.setContextClassLoader(oldClassloader);
}
// reset the context and servlet path. // reset the context and servlet path.
baseRequest.setContext(oldContext); baseRequest.setContext(oldContext, oldPathInContext);
__context.set(oldContext); __context.set(oldContext);
baseRequest.setContextPath(oldContextPath);
baseRequest.setServletPath(oldServletPath);
baseRequest.setPathInfo(oldPathInfo);
} }
} }
} }

View File

@ -45,7 +45,6 @@ import org.eclipse.jetty.server.handler.HandlerWrapper;
import org.eclipse.jetty.util.IncludeExclude; import org.eclipse.jetty.util.IncludeExclude;
import org.eclipse.jetty.util.RegexSet; import org.eclipse.jetty.util.RegexSet;
import org.eclipse.jetty.util.StringUtil; import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.util.URIUtil;
import org.eclipse.jetty.util.compression.DeflaterPool; import org.eclipse.jetty.util.compression.DeflaterPool;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -582,7 +581,7 @@ public class GzipHandler extends HandlerWrapper implements GzipFactory
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
{ {
final ServletContext context = baseRequest.getServletContext(); final ServletContext context = baseRequest.getServletContext();
final String path = context == null ? baseRequest.getRequestURI() : URIUtil.addPaths(baseRequest.getServletPath(), baseRequest.getPathInfo()); final String path = baseRequest.getPathInContext();
LOG.debug("{} handle {} in {}", this, baseRequest, context); LOG.debug("{} handle {} in {}", this, baseRequest, context);
if (!_dispatchers.contains(baseRequest.getDispatcherType())) if (!_dispatchers.contains(baseRequest.getDispatcherType()))

View File

@ -99,6 +99,12 @@ public abstract class AbstractSessionCache extends ContainerLifeCycle implements
* a dirty session will be flushed to the session store. * a dirty session will be flushed to the session store.
*/ */
protected boolean _flushOnResponseCommit; protected boolean _flushOnResponseCommit;
/**
* If true, when the server shuts down, all sessions in the
* cache will be invalidated before being removed.
*/
protected boolean _invalidateOnShutdown;
/** /**
* Create a new Session object from pre-existing session data * Create a new Session object from pre-existing session data
@ -796,6 +802,18 @@ public abstract class AbstractSessionCache extends ContainerLifeCycle implements
_saveOnInactiveEviction = saveOnEvict; _saveOnInactiveEviction = saveOnEvict;
} }
@Override
public void setInvalidateOnShutdown(boolean invalidateOnShutdown)
{
_invalidateOnShutdown = invalidateOnShutdown;
}
@Override
public boolean isInvalidateOnShutdown()
{
return _invalidateOnShutdown;
}
/** /**
* Whether we should save a session that has been inactive before * Whether we should save a session that has been inactive before
* we boot it from the cache. * we boot it from the cache.

View File

@ -31,6 +31,19 @@ public abstract class AbstractSessionCacheFactory implements SessionCacheFactory
boolean _saveOnCreate; boolean _saveOnCreate;
boolean _removeUnloadableSessions; boolean _removeUnloadableSessions;
boolean _flushOnResponseCommit; boolean _flushOnResponseCommit;
boolean _invalidateOnShutdown;
public abstract SessionCache newSessionCache(SessionHandler handler);
public boolean isInvalidateOnShutdown()
{
return _invalidateOnShutdown;
}
public void setInvalidateOnShutdown(boolean invalidateOnShutdown)
{
_invalidateOnShutdown = invalidateOnShutdown;
}
/** /**
* @return the flushOnResponseCommit * @return the flushOnResponseCommit
@ -111,4 +124,17 @@ public abstract class AbstractSessionCacheFactory implements SessionCacheFactory
{ {
_saveOnInactiveEvict = saveOnInactiveEvict; _saveOnInactiveEvict = saveOnInactiveEvict;
} }
@Override
public SessionCache getSessionCache(SessionHandler handler)
{
SessionCache cache = newSessionCache(handler);
cache.setEvictionPolicy(getEvictionPolicy());
cache.setSaveOnInactiveEviction(isSaveOnInactiveEvict());
cache.setSaveOnCreate(isSaveOnCreate());
cache.setRemoveUnloadableSessions(isRemoveUnloadableSessions());
cache.setFlushOnResponseCommit(isFlushOnResponseCommit());
cache.setInvalidateOnShutdown(isInvalidateOnShutdown());
return cache;
}
} }

View File

@ -131,29 +131,18 @@ public class DefaultSessionCache extends AbstractSessionCache
@Override @Override
public void shutdown() public void shutdown()
{ {
if (LOG.isDebugEnabled())
LOG.debug("Shutdown sessions, invalidating = {}", isInvalidateOnShutdown());
// loop over all the sessions in memory (a few times if necessary to catch sessions that have been // loop over all the sessions in memory (a few times if necessary to catch sessions that have been
// added while we're running // added while we're running
int loop = 100; int loop = 100;
while (!_sessions.isEmpty() && loop-- > 0) while (!_sessions.isEmpty() && loop-- > 0)
{ {
for (Session session : _sessions.values()) for (Session session : _sessions.values())
{ {
//if we have a backing store so give the session to it to write out if necessary if (isInvalidateOnShutdown())
if (_sessionDataStore != null)
{
session.willPassivate();
try
{
_sessionDataStore.store(session.getId(), session.getSessionData());
}
catch (Exception e)
{
LOG.warn("Unable to store {}", session, e);
}
doDelete(session.getId()); //remove from memory
session.setResident(false);
}
else
{ {
//not preserving sessions on exit //not preserving sessions on exit
try try
@ -165,6 +154,22 @@ public class DefaultSessionCache extends AbstractSessionCache
LOG.trace("IGNORED", e); LOG.trace("IGNORED", e);
} }
} }
else
{
//write out the session and remove from the cache
if (_sessionDataStore.isPassivating())
session.willPassivate();
try
{
_sessionDataStore.store(session.getId(), session.getSessionData());
}
catch (Exception e)
{
LOG.warn("Unable to store {}", session, e);
}
doDelete(session.getId()); //remove from memory
session.setResident(false);
}
} }
} }
} }

View File

@ -26,14 +26,8 @@ package org.eclipse.jetty.server.session;
public class DefaultSessionCacheFactory extends AbstractSessionCacheFactory public class DefaultSessionCacheFactory extends AbstractSessionCacheFactory
{ {
@Override @Override
public SessionCache getSessionCache(SessionHandler handler) public SessionCache newSessionCache(SessionHandler handler)
{ {
DefaultSessionCache cache = new DefaultSessionCache(handler); return new DefaultSessionCache(handler);
cache.setEvictionPolicy(getEvictionPolicy());
cache.setSaveOnInactiveEviction(isSaveOnInactiveEvict());
cache.setSaveOnCreate(isSaveOnCreate());
cache.setRemoveUnloadableSessions(isRemoveUnloadableSessions());
cache.setFlushOnResponseCommit(isFlushOnResponseCommit());
return cache;
} }
} }

View File

@ -471,7 +471,9 @@ public class DefaultSessionIdManager extends ContainerLifeCycle implements Sessi
{ {
for (Handler h : tmp) for (Handler h : tmp)
{ {
if (h.isStarted()) //This method can be called on shutdown when the handlers are STOPPING, so only
//check that they are not already stopped
if (!h.isStopped() && !h.isFailed())
handlers.add((SessionHandler)h); handlers.add((SessionHandler)h);
} }
} }

View File

@ -55,14 +55,23 @@ public class NullSessionCacheFactory extends AbstractSessionCacheFactory
if (LOG.isDebugEnabled()) if (LOG.isDebugEnabled())
LOG.debug("Ignoring eviction policy setting for NullSessionCaches"); LOG.debug("Ignoring eviction policy setting for NullSessionCaches");
} }
@Override
public boolean isInvalidateOnShutdown()
{
return false; //meaningless for NullSessionCache
}
@Override @Override
public SessionCache getSessionCache(SessionHandler handler) public void setInvalidateOnShutdown(boolean invalidateOnShutdown)
{ {
NullSessionCache cache = new NullSessionCache(handler); if (LOG.isDebugEnabled())
cache.setSaveOnCreate(isSaveOnCreate()); LOG.debug("Ignoring invalidateOnShutdown setting for NullSessionCaches");
cache.setRemoveUnloadableSessions(isRemoveUnloadableSessions()); }
cache.setFlushOnResponseCommit(isFlushOnResponseCommit());
return cache; @Override
public SessionCache newSessionCache(SessionHandler handler)
{
return new NullSessionCache(handler);
} }
} }

View File

@ -290,4 +290,13 @@ public interface SessionCache extends LifeCycle
* before the response is committed. * before the response is committed.
*/ */
boolean isFlushOnResponseCommit(); boolean isFlushOnResponseCommit();
/**
* If true, all existing sessions in the cache will be invalidated when
* the server shuts down. Default is false.
* @param invalidateOnShutdown
*/
void setInvalidateOnShutdown(boolean invalidateOnShutdown);
boolean isInvalidateOnShutdown();
} }

View File

@ -0,0 +1,142 @@
//
// ========================================================================
// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under
// the terms of the Eclipse Public License 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0
//
// This Source Code may also be made available under the following
// Secondary Licenses when the conditions for such availability set
// forth in the Eclipse Public License, v. 2.0 are satisfied:
// the Apache License v2.0 which is available at
// https://www.apache.org/licenses/LICENSE-2.0
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.server;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Arrays;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.MimeTypes;
import org.eclipse.jetty.http.tools.HttpTester;
import org.eclipse.jetty.server.handler.AbstractHandler;
import org.eclipse.jetty.server.handler.ErrorHandler;
import org.eclipse.jetty.util.IO;
import org.eclipse.jetty.util.component.LifeCycle;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
public class LargeHeaderTest
{
private Server server;
@BeforeEach
public void setup() throws Exception
{
server = new Server();
HttpConfiguration config = new HttpConfiguration();
HttpConnectionFactory http = new HttpConnectionFactory(config);
ServerConnector connector = new ServerConnector(server, http);
connector.setPort(0);
connector.setIdleTimeout(5000);
server.addConnector(connector);
server.setErrorHandler(new ErrorHandler());
server.setHandler(new AbstractHandler()
{
final String largeHeaderValue;
{
byte[] bytes = new byte[8 * 1024];
Arrays.fill(bytes, (byte)'X');
largeHeaderValue = "LargeHeaderOver8k-" + new String(bytes, UTF_8) + "_Z_";
}
@Override
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException
{
response.setHeader(HttpHeader.CONTENT_TYPE.toString(), MimeTypes.Type.TEXT_HTML.toString());
response.setHeader("LongStr", largeHeaderValue);
PrintWriter writer = response.getWriter();
writer.write("<html><h1>FOO</h1></html>");
writer.flush();
response.flushBuffer();
baseRequest.setHandled(true);
}
});
server.start();
}
@AfterEach
public void teardown()
{
LifeCycle.stop(server);
}
@Test
public void testLargeHeader() throws Throwable
{
final Logger CLIENTLOG = Log.getLogger(LargeHeaderTest.class).getLogger(".client");
ExecutorService executorService = Executors.newFixedThreadPool(8);
int localPort = server.getURI().getPort();
String rawRequest = "GET / HTTP/1.1\r\n" +
"Host: localhost:" + localPort + "\r\n" +
"\r\n";
Throwable issues = new Throwable();
for (int i = 0; i < 500; ++i)
{
executorService.submit(() ->
{
try (Socket client = new Socket("localhost", localPort);
OutputStream output = client.getOutputStream();
InputStream input = client.getInputStream())
{
output.write(rawRequest.getBytes(UTF_8));
output.flush();
String rawResponse = IO.toString(input, UTF_8);
HttpTester.Response response = HttpTester.parseResponse(rawResponse);
assertThat(response.getStatus(), is(500));
}
catch (Throwable t)
{
CLIENTLOG.warn("Client Issue", t);
issues.addSuppressed(t);
}
});
}
executorService.awaitTermination(5, TimeUnit.SECONDS);
if (issues.getSuppressed().length > 0)
{
throw issues;
}
}
}

View File

@ -128,7 +128,7 @@ public class ProxyCustomizerTest
} }
@Test @Test
void testProxyCustomizerWithProxyData() throws Exception public void testProxyCustomizerWithProxyData() throws Exception
{ {
String proxy = String proxy =
// Preamble // Preamble
@ -159,7 +159,7 @@ public class ProxyCustomizerTest
} }
@Test @Test
void testProxyCustomizerWithoutProxyData() throws Exception public void testProxyCustomizerWithoutProxyData() throws Exception
{ {
String proxy = ""; String proxy = "";
String http = "GET /1 HTTP/1.1\r\n" + String http = "GET /1 HTTP/1.1\r\n" +

View File

@ -43,7 +43,6 @@ import javax.servlet.MultipartConfigElement;
import javax.servlet.ServletException; import javax.servlet.ServletException;
import javax.servlet.ServletInputStream; import javax.servlet.ServletInputStream;
import javax.servlet.http.Cookie; import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletMapping;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession; import javax.servlet.http.HttpSession;
@ -2175,29 +2174,4 @@ public class RequestTest
return null; return null;
} }
} }
private class PathMappingHandler extends AbstractHandler
{
private ServletPathSpec _spec;
private String _servletPath;
private String _servletName;
public PathMappingHandler(ServletPathSpec spec, String servletPath, String servletName)
{
_spec = spec;
_servletPath = servletPath;
_servletName = servletName;
}
@Override
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
{
((Request)request).setHandled(true);
baseRequest.setServletPath(_servletPath);
if (_servletName != null)
baseRequest.setUserIdentityScope(new TestUserIdentityScope(null, null, _servletName));
HttpServletMapping mapping = baseRequest.getHttpServletMapping();
response.getWriter().println(mapping);
}
}
} }

View File

@ -95,7 +95,6 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
// @checkstyle-disable-check : AvoidEscapedUnicodeCharactersCheck // @checkstyle-disable-check : AvoidEscapedUnicodeCharactersCheck
public class ResponseTest public class ResponseTest
{ {
static final InetSocketAddress LOCALADDRESS; static final InetSocketAddress LOCALADDRESS;
static static
@ -353,7 +352,7 @@ public class ResponseTest
ContextHandler context = new ContextHandler(); ContextHandler context = new ContextHandler();
context.addLocaleEncoding(Locale.ENGLISH.toString(), "ISO-8859-1"); context.addLocaleEncoding(Locale.ENGLISH.toString(), "ISO-8859-1");
context.addLocaleEncoding(Locale.ITALIAN.toString(), "ISO-8859-2"); context.addLocaleEncoding(Locale.ITALIAN.toString(), "ISO-8859-2");
response.getHttpChannel().getRequest().setContext(context.getServletContext()); response.getHttpChannel().getRequest().setContext(context.getServletContext(), "/");
response.setLocale(java.util.Locale.ITALIAN); response.setLocale(java.util.Locale.ITALIAN);
assertNull(response.getContentType()); assertNull(response.getContentType());
@ -376,7 +375,7 @@ public class ResponseTest
ContextHandler context = new ContextHandler(); ContextHandler context = new ContextHandler();
context.addLocaleEncoding(Locale.ENGLISH.toString(), "ISO-8859-1"); context.addLocaleEncoding(Locale.ENGLISH.toString(), "ISO-8859-1");
context.addLocaleEncoding(Locale.ITALIAN.toString(), "ISO-8859-2"); context.addLocaleEncoding(Locale.ITALIAN.toString(), "ISO-8859-2");
response.getHttpChannel().getRequest().setContext(context.getServletContext()); response.getHttpChannel().getRequest().setContext(context.getServletContext(), "/");
response.setLocale(java.util.Locale.ITALIAN); response.setLocale(java.util.Locale.ITALIAN);
@ -425,46 +424,46 @@ public class ResponseTest
//test setting the default response character encoding //test setting the default response character encoding
Response response = getResponse(); Response response = getResponse();
_channel.getRequest().setContext(handler.getServletContext()); response.getHttpChannel().getRequest().setContext(handler.getServletContext(), "/");
assertThat("utf-16", Matchers.equalTo(response.getCharacterEncoding())); assertThat("utf-16", Matchers.equalTo(response.getCharacterEncoding()));
_channel.getRequest().setContext(null); _channel.getRequest().setContext(null, "/");
response.recycle(); response.recycle();
//test that explicit overrides default //test that explicit overrides default
response = getResponse(); response = getResponse();
_channel.getRequest().setContext(handler.getServletContext()); _channel.getRequest().setContext(handler.getServletContext(), "/");
response.setCharacterEncoding("ascii"); response.setCharacterEncoding("ascii");
assertThat("ascii", Matchers.equalTo(response.getCharacterEncoding())); assertThat("ascii", Matchers.equalTo(response.getCharacterEncoding()));
//getWriter should not change explicit character encoding //getWriter should not change explicit character encoding
response.getWriter(); response.getWriter();
assertThat("ascii", Matchers.equalTo(response.getCharacterEncoding())); assertThat("ascii", Matchers.equalTo(response.getCharacterEncoding()));
_channel.getRequest().setContext(null); _channel.getRequest().setContext(null, "/");
response.recycle(); response.recycle();
//test that assumed overrides default //test that assumed overrides default
response = getResponse(); response = getResponse();
_channel.getRequest().setContext(handler.getServletContext()); _channel.getRequest().setContext(handler.getServletContext(), "/");
response.setContentType("application/json"); response.setContentType("application/json");
assertThat("utf-8", Matchers.equalTo(response.getCharacterEncoding())); assertThat("utf-8", Matchers.equalTo(response.getCharacterEncoding()));
response.getWriter(); response.getWriter();
//getWriter should not have modified character encoding //getWriter should not have modified character encoding
assertThat("utf-8", Matchers.equalTo(response.getCharacterEncoding())); assertThat("utf-8", Matchers.equalTo(response.getCharacterEncoding()));
_channel.getRequest().setContext(null); _channel.getRequest().setContext(null, "/");
response.recycle(); response.recycle();
//test that inferred overrides default //test that inferred overrides default
response = getResponse(); response = getResponse();
_channel.getRequest().setContext(handler.getServletContext()); _channel.getRequest().setContext(handler.getServletContext(), "/");
response.setContentType("application/xhtml+xml"); response.setContentType("application/xhtml+xml");
assertThat("utf-8", Matchers.equalTo(response.getCharacterEncoding())); assertThat("utf-8", Matchers.equalTo(response.getCharacterEncoding()));
//getWriter should not have modified character encoding //getWriter should not have modified character encoding
response.getWriter(); response.getWriter();
assertThat("utf-8", Matchers.equalTo(response.getCharacterEncoding())); assertThat("utf-8", Matchers.equalTo(response.getCharacterEncoding()));
_channel.getRequest().setContext(null); _channel.getRequest().setContext(null, "/");
response.recycle(); response.recycle();
//test that without a default or any content type, use iso-8859-1 //test that without a default or any content type, use iso-8859-1
@ -488,7 +487,7 @@ public class ResponseTest
_server.start(); _server.start();
Response response = getResponse(); Response response = getResponse();
response.getHttpChannel().getRequest().setContext(handler.getServletContext()); response.getHttpChannel().getRequest().setContext(handler.getServletContext(), "/");
response.setContentType("text/html"); response.setContentType("text/html");
assertEquals("iso-8859-1", response.getCharacterEncoding()); assertEquals("iso-8859-1", response.getCharacterEncoding());
@ -859,10 +858,11 @@ public class ResponseTest
@Test @Test
public void testEncodeRedirect() public void testEncodeRedirect()
{ {
ContextHandler context = new ContextHandler("/path");
Response response = getResponse(); Response response = getResponse();
Request request = response.getHttpChannel().getRequest(); Request request = response.getHttpChannel().getRequest();
request.setHttpURI(HttpURI.build(request.getHttpURI()).host("myhost").port(8888)); request.setHttpURI(HttpURI.build(request.getHttpURI()).host("myhost").port(8888));
request.setContextPath("/path"); request.setContext(context.getServletContext(), "/info");
assertEquals("http://myhost:8888/path/info;param?query=0&more=1#target", response.encodeURL("http://myhost:8888/path/info;param?query=0&more=1#target")); assertEquals("http://myhost:8888/path/info;param?query=0&more=1#target", response.encodeURL("http://myhost:8888/path/info;param?query=0&more=1#target"));
@ -893,7 +893,23 @@ public class ResponseTest
assertEquals("http://myhost/path/info;param?query=0&more=1#target", response.encodeURL("http://myhost/path/info;param?query=0&more=1#target")); assertEquals("http://myhost/path/info;param?query=0&more=1#target", response.encodeURL("http://myhost/path/info;param?query=0&more=1#target"));
assertEquals("http://myhost:8888/other/info;param?query=0&more=1#target", response.encodeURL("http://myhost:8888/other/info;param?query=0&more=1#target")); assertEquals("http://myhost:8888/other/info;param?query=0&more=1#target", response.encodeURL("http://myhost:8888/other/info;param?query=0&more=1#target"));
request.setContextPath(""); context = new ContextHandler("/");
request.setContext(context.getServletContext(), "/");
assertEquals("http://myhost:8888/;jsessionid=12345", response.encodeURL("http://myhost:8888"));
assertEquals("https://myhost:8888/;jsessionid=12345", response.encodeURL("https://myhost:8888"));
assertEquals("mailto:/foo", response.encodeURL("mailto:/foo"));
assertEquals("http://myhost:8888/;jsessionid=12345", response.encodeURL("http://myhost:8888/"));
assertEquals("http://myhost:8888/;jsessionid=12345", response.encodeURL("http://myhost:8888/;jsessionid=7777"));
assertEquals("http://myhost:8888/;param;jsessionid=12345?query=0&more=1#target", response.encodeURL("http://myhost:8888/;param?query=0&more=1#target"));
assertEquals("http://other:8888/path/info;param?query=0&more=1#target", response.encodeURL("http://other:8888/path/info;param?query=0&more=1#target"));
handler.setCheckingRemoteSessionIdEncoding(false);
assertEquals("/foo;jsessionid=12345", response.encodeURL("/foo"));
assertEquals("/;jsessionid=12345", response.encodeURL("/"));
assertEquals("/foo.html;jsessionid=12345#target", response.encodeURL("/foo.html#target"));
assertEquals(";jsessionid=12345", response.encodeURL(""));
request.setContext(null, "/");
handler.setCheckingRemoteSessionIdEncoding(true);
assertEquals("http://myhost:8888/;jsessionid=12345", response.encodeURL("http://myhost:8888")); assertEquals("http://myhost:8888/;jsessionid=12345", response.encodeURL("http://myhost:8888"));
assertEquals("https://myhost:8888/;jsessionid=12345", response.encodeURL("https://myhost:8888")); assertEquals("https://myhost:8888/;jsessionid=12345", response.encodeURL("https://myhost:8888"));
assertEquals("mailto:/foo", response.encodeURL("mailto:/foo")); assertEquals("mailto:/foo", response.encodeURL("mailto:/foo"));
@ -937,6 +953,7 @@ public class ResponseTest
{"http://somehost.com/other/location", "http://somehost.com/other/location"}, {"http://somehost.com/other/location", "http://somehost.com/other/location"},
}; };
ContextHandler context = new ContextHandler("/path");
int[] ports = new int[]{8080, 80}; int[] ports = new int[]{8080, 80};
String[] hosts = new String[]{null, "myhost", "192.168.0.1", "0::1"}; String[] hosts = new String[]{null, "myhost", "192.168.0.1", "0::1"};
for (int port : ports) for (int port : ports)
@ -956,7 +973,7 @@ public class ResponseTest
if (host != null) if (host != null)
uri.host(host).port(port); uri.host(host).port(port);
request.setHttpURI(uri); request.setHttpURI(uri);
request.setContextPath("/path"); request.setContext(context.getServletContext(), "/info");
request.setRequestedSessionId("12345"); request.setRequestedSessionId("12345");
request.setRequestedSessionIdFromCookie(i > 2); request.setRequestedSessionIdFromCookie(i > 2);
SessionHandler handler = new SessionHandler(); SessionHandler handler = new SessionHandler();
@ -980,6 +997,7 @@ public class ResponseTest
.replace("@HOST@", host == null ? request.getLocalAddr() : (host.contains(":") ? ("[" + host + "]") : host)) .replace("@HOST@", host == null ? request.getLocalAddr() : (host.contains(":") ? ("[" + host + "]") : host))
.replace("@PORT@", host == null ? ":8888" : (port == 80 ? "" : (":" + port))); .replace("@PORT@", host == null ? ":8888" : (port == 80 ? "" : (":" + port)));
assertEquals(expected, location, "test-" + i + " " + host + ":" + port); assertEquals(expected, location, "test-" + i + " " + host + ":" + port);
request.setContext(null, "/info");
} }
} }
} }
@ -1094,12 +1112,29 @@ public class ResponseTest
assertEquals("name=value; Path=/path; Domain=domain; Secure; HttpOnly", set); assertEquals("name=value; Path=/path; Domain=domain; Secure; HttpOnly", set);
} }
@Test
public void testAddCookieInInclude() throws Exception
{
Response response = getResponse();
response.include();
Cookie cookie = new Cookie("naughty", "value");
cookie.setDomain("domain");
cookie.setPath("/path");
cookie.setSecure(true);
cookie.setComment("comment__HTTP_ONLY__");
response.addCookie(cookie);
assertNull(response.getHttpFields().get("Set-Cookie"));
}
@Test @Test
public void testAddCookieSameSiteDefault() throws Exception public void testAddCookieSameSiteDefault() throws Exception
{ {
Response response = getResponse(); Response response = getResponse();
TestServletContextHandler context = new TestServletContextHandler(); TestServletContextHandler context = new TestServletContextHandler();
_channel.getRequest().setContext(context.getServletContext()); _channel.getRequest().setContext(context.getServletContext(), "/");
context.setAttribute(HttpCookie.SAME_SITE_DEFAULT_ATTRIBUTE, HttpCookie.SameSite.STRICT); context.setAttribute(HttpCookie.SAME_SITE_DEFAULT_ATTRIBUTE, HttpCookie.SameSite.STRICT);
Cookie cookie = new Cookie("name", "value"); Cookie cookie = new Cookie("name", "value");
cookie.setDomain("domain"); cookie.setDomain("domain");
@ -1265,7 +1300,7 @@ public class ResponseTest
Response response = getResponse(); Response response = getResponse();
TestServletContextHandler context = new TestServletContextHandler(); TestServletContextHandler context = new TestServletContextHandler();
context.setAttribute(HttpCookie.SAME_SITE_DEFAULT_ATTRIBUTE, "LAX"); context.setAttribute(HttpCookie.SAME_SITE_DEFAULT_ATTRIBUTE, "LAX");
_channel.getRequest().setContext(context.getServletContext()); _channel.getRequest().setContext(context.getServletContext(), "/");
//replace with no prior does an add //replace with no prior does an add
response.replaceCookie(new HttpCookie("Foo", "123456")); response.replaceCookie(new HttpCookie("Foo", "123456"));
String set = response.getHttpFields().get("Set-Cookie"); String set = response.getHttpFields().get("Set-Cookie");
@ -1307,7 +1342,7 @@ public class ResponseTest
Response response = getResponse(); Response response = getResponse();
TestServletContextHandler context = new TestServletContextHandler(); TestServletContextHandler context = new TestServletContextHandler();
context.setAttribute(HttpCookie.SAME_SITE_DEFAULT_ATTRIBUTE, "LAX"); context.setAttribute(HttpCookie.SAME_SITE_DEFAULT_ATTRIBUTE, "LAX");
_channel.getRequest().setContext(context.getServletContext()); _channel.getRequest().setContext(context.getServletContext(), "/");
response.addHeader(HttpHeader.SET_COOKIE.asString(), "Foo=123456"); response.addHeader(HttpHeader.SET_COOKIE.asString(), "Foo=123456");
response.replaceCookie(new HttpCookie("Foo", "value")); response.replaceCookie(new HttpCookie("Foo", "value"));

View File

@ -37,7 +37,6 @@ import java.util.stream.Stream;
import javax.servlet.DispatcherType; import javax.servlet.DispatcherType;
import javax.servlet.Filter; import javax.servlet.Filter;
import javax.servlet.FilterChain; import javax.servlet.FilterChain;
import javax.servlet.RequestDispatcher;
import javax.servlet.Servlet; import javax.servlet.Servlet;
import javax.servlet.ServletContext; import javax.servlet.ServletContext;
import javax.servlet.ServletException; import javax.servlet.ServletException;
@ -66,7 +65,6 @@ import org.eclipse.jetty.util.ArrayUtil;
import org.eclipse.jetty.util.LazyList; import org.eclipse.jetty.util.LazyList;
import org.eclipse.jetty.util.MultiException; import org.eclipse.jetty.util.MultiException;
import org.eclipse.jetty.util.MultiMap; import org.eclipse.jetty.util.MultiMap;
import org.eclipse.jetty.util.URIUtil;
import org.eclipse.jetty.util.annotation.ManagedAttribute; import org.eclipse.jetty.util.annotation.ManagedAttribute;
import org.eclipse.jetty.util.annotation.ManagedObject; import org.eclipse.jetty.util.annotation.ManagedObject;
import org.eclipse.jetty.util.component.DumpableCollection; import org.eclipse.jetty.util.component.DumpableCollection;
@ -431,10 +429,7 @@ public class ServletHandler extends ScopedHandler
if (servletPathMapping != null) if (servletPathMapping != null)
{ {
// Setting the servletPathMapping also provides the servletPath and pathInfo // Setting the servletPathMapping also provides the servletPath and pathInfo
if (DispatcherType.INCLUDE.equals(type)) baseRequest.setServletPathMapping(servletPathMapping);
baseRequest.setAttribute(RequestDispatcher.INCLUDE_MAPPING, servletPathMapping);
else
baseRequest.setServletPathMapping(servletPathMapping);
} }
} }
@ -1405,7 +1400,7 @@ public class ServletHandler extends ScopedHandler
if (LOG.isDebugEnabled()) if (LOG.isDebugEnabled())
LOG.debug("Not Found {}", request.getRequestURI()); LOG.debug("Not Found {}", request.getRequestURI());
if (getHandler() != null) if (getHandler() != null)
nextHandle(URIUtil.addPaths(request.getServletPath(), request.getPathInfo()), baseRequest, request, response); nextHandle(baseRequest.getPathInContext(), baseRequest, request, response);
} }
protected synchronized boolean containsFilterHolder(FilterHolder holder) protected synchronized boolean containsFilterHolder(FilterHolder holder)

View File

@ -57,4 +57,4 @@ http://www.apache.org/licenses/LICENSE-2.0.html
# jetty.unixsocket.selectors=-1 # jetty.unixsocket.selectors=-1
## ServerSocketChannel backlog (0 picks platform default) ## ServerSocketChannel backlog (0 picks platform default)
# jetty.unixsocket.acceptorQueueSize=0 # jetty.unixsocket.acceptQueueSize=0

View File

@ -0,0 +1,59 @@
DO NOT EDIT - See: https://www.eclipse.org/jetty/documentation/current/startup-modules.html
[description]
Enables a Unix Domain Socket Connector that can receive
requests from a local proxy and/or SSL offloader (eg haproxy) in either
HTTP or TCP mode. Unix Domain Sockets are more efficient than
localhost TCP/IP connections as they reduce data copies, avoid
needless fragmentation and have better dispatch behaviours.
When enabled with corresponding support modules, the connector can
accept HTTP, HTTPS or HTTP2C traffic.
[tags]
connector
[depend]
server
[xml]
etc/jetty-unixsocket.xml
[files]
maven://com.github.jnr/jnr-unixsocket/0.22|lib/jnr/jnr-unixsocket-0.22.jar
maven://com.github.jnr/jnr-ffi/2.1.9|lib/jnr/jnr-ffi-2.1.9.jar
maven://com.github.jnr/jffi/1.2.17|lib/jnr/jffi-1.2.17.jar
maven://com.github.jnr/jffi/1.2.16/jar/native|lib/jnr/jffi-1.2.16-native.jar
maven://org.ow2.asm/asm/7.0|lib/jnr/asm-7.0.jar
maven://org.ow2.asm/asm-commons/7.0|lib/jnr/asm-commons-7.0.jar
maven://org.ow2.asm/asm-analysis/7.0|lib/jnr/asm-analysis-7.0.jar
maven://org.ow2.asm/asm-tree/7.0|lib/jnr/asm-tree-7.0.jar
maven://org.ow2.asm/asm-util/7.0|lib/jnr/asm-util-7.0.jar
maven://com.github.jnr/jnr-x86asm/1.0.2|lib/jnr/jnr-x86asm-1.0.2.jar
maven://com.github.jnr/jnr-constants/0.9.11|lib/jnr/jnr-constants-0.9.11.jar
maven://com.github.jnr/jnr-enxio/0.20|lib/jnr/jnr-enxio-0.20.jar
maven://com.github.jnr/jnr-posix/3.0.47|lib/jnr/jnr-posix-3.0.47.jar
[lib]
lib/jetty-unixsocket-${jetty.version}.jar
lib/jnr/*.jar
[license]
Jetty UnixSockets is implemented using the Java Native Runtime, which is an
open source project hosted on Github and released under the Apache 2.0 license.
https://github.com/jnr/jnr-unixsocket
http://www.apache.org/licenses/LICENSE-2.0.html
[ini-template]
### Unix SocketHTTP Connector Configuration
## Unix socket path to bind to
# jetty.unixsocket.path=/tmp/jetty.sock
## Connector idle timeout in milliseconds
# jetty.unixsocket.idleTimeout=30000
## Number of selectors (-1 picks default)
# jetty.unixsocket.selectors=-1
## ServerSocketChannel backlog (0 picks platform default)
# jetty.unixsocket.acceptQueueSize=0

View File

@ -43,6 +43,10 @@ public interface Attributes
void clearAttributes(); void clearAttributes();
/** Unwrap all {@link Wrapper}s of the attributes
* @param attributes The attributes to unwrap, which may be a {@link Wrapper}.
* @return The core attributes
*/
static Attributes unwrap(Attributes attributes) static Attributes unwrap(Attributes attributes)
{ {
while (attributes instanceof Wrapper) while (attributes instanceof Wrapper)
@ -52,6 +56,26 @@ public interface Attributes
return attributes; return attributes;
} }
/** Unwrap attributes to a specific attribute {@link Wrapper}.
* @param attributes The attributes to unwrap, which may be a {@link Wrapper}
* @param target The target {@link Wrapper} class.
* @param <T> The type of the target {@link Wrapper}.
* @return The outermost {@link Wrapper} of the matching type of null if not found.
*/
static <T extends Attributes.Wrapper> T unwrap(Attributes attributes, Class<T> target)
{
while (attributes instanceof Wrapper)
{
if (target.isAssignableFrom(attributes.getClass()))
return (T)attributes;
attributes = ((Wrapper)attributes).getAttributes();
}
return null;
}
/**
* A Wrapper of attributes
*/
abstract class Wrapper implements Attributes abstract class Wrapper implements Attributes
{ {
protected final Attributes _attributes; protected final Attributes _attributes;

View File

@ -29,9 +29,9 @@ module org.eclipse.jetty.websocket.core.common
exports org.eclipse.jetty.websocket.core.internal to org.eclipse.jetty.websocket.core.client, org.eclipse.jetty.websocket.core.server, org.eclipse.jetty.util; exports org.eclipse.jetty.websocket.core.internal to org.eclipse.jetty.websocket.core.client, org.eclipse.jetty.websocket.core.server, org.eclipse.jetty.util;
requires org.eclipse.jetty.http; requires org.eclipse.jetty.http;
requires org.slf4j;
requires transitive org.eclipse.jetty.io; requires transitive org.eclipse.jetty.io;
requires transitive org.eclipse.jetty.util; requires transitive org.eclipse.jetty.util;
requires org.slf4j;
uses Extension; uses Extension;

View File

@ -68,11 +68,12 @@ public final class ContainerDefaultConfigurator extends Configurator
{ {
// Since this is started via a ServiceLoader, this class has no Scope or context // Since this is started via a ServiceLoader, this class has no Scope or context
// that can be used to obtain a ObjectFactory from. // that can be used to obtain a ObjectFactory from.
return endpointClass.getDeclaredConstructor().newInstance(); return endpointClass.getConstructor().newInstance();
} }
catch (Exception e) catch (Exception e)
{ {
InstantiationException instantiationException = new InstantiationException(); String errorMsg = String.format("%s: %s", e.getClass().getName(), e.getMessage());
InstantiationException instantiationException = new InstantiationException(errorMsg);
instantiationException.initCause(e); instantiationException.initCause(e);
throw instantiationException; throw instantiationException;
} }

View File

@ -24,6 +24,7 @@ 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 javax.websocket.Extension; import javax.websocket.Extension;
import javax.websocket.Extension.Parameter; import javax.websocket.Extension.Parameter;
import javax.websocket.server.ServerEndpointConfig; import javax.websocket.server.ServerEndpointConfig;
@ -70,7 +71,7 @@ public class JavaxWebSocketCreator implements WebSocketCreator
// per upgrade request. // per upgrade request.
ServerEndpointConfig config = new ServerEndpointConfigWrapper(baseConfig) ServerEndpointConfig config = new ServerEndpointConfigWrapper(baseConfig)
{ {
Map<String, Object> userProperties = new HashMap<>(baseConfig.getUserProperties()); final Map<String, Object> userProperties = new HashMap<>(baseConfig.getUserProperties());
@Override @Override
public Map<String, Object> getUserProperties() public Map<String, Object> getUserProperties()
@ -183,15 +184,13 @@ public class JavaxWebSocketCreator implements WebSocketCreator
return false; return false;
JavaxWebSocketCreator that = (JavaxWebSocketCreator)o; JavaxWebSocketCreator that = (JavaxWebSocketCreator)o;
return Objects.equals(baseConfig, that.baseConfig);
return baseConfig != null ? baseConfig.equals(that.baseConfig) : that.baseConfig == null;
} }
@Override @Override
public int hashCode() public int hashCode()
{ {
int result = (baseConfig != null ? baseConfig.hashCode() : 0); return (baseConfig != null ? baseConfig.hashCode() : 0);
return result;
} }
@Override @Override

View File

@ -18,6 +18,7 @@
package org.eclipse.jetty.websocket.javax.server.internal; package org.eclipse.jetty.websocket.javax.server.internal;
import java.lang.reflect.Modifier;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
@ -38,10 +39,12 @@ import org.eclipse.jetty.util.component.Graceful;
import org.eclipse.jetty.util.component.LifeCycle; import org.eclipse.jetty.util.component.LifeCycle;
import org.eclipse.jetty.websocket.core.WebSocketComponents; import org.eclipse.jetty.websocket.core.WebSocketComponents;
import org.eclipse.jetty.websocket.core.client.WebSocketCoreClient; import org.eclipse.jetty.websocket.core.client.WebSocketCoreClient;
import org.eclipse.jetty.websocket.core.exception.WebSocketException;
import org.eclipse.jetty.websocket.core.server.WebSocketServerComponents; import org.eclipse.jetty.websocket.core.server.WebSocketServerComponents;
import org.eclipse.jetty.websocket.javax.client.internal.JavaxWebSocketClientContainer; import org.eclipse.jetty.websocket.javax.client.internal.JavaxWebSocketClientContainer;
import org.eclipse.jetty.websocket.javax.server.config.ContainerDefaultConfigurator;
import org.eclipse.jetty.websocket.javax.server.config.JavaxWebSocketServletContainerInitializer; import org.eclipse.jetty.websocket.javax.server.config.JavaxWebSocketServletContainerInitializer;
import org.eclipse.jetty.websocket.util.InvalidSignatureException;
import org.eclipse.jetty.websocket.util.ReflectUtils;
import org.eclipse.jetty.websocket.util.server.internal.WebSocketMapping; import org.eclipse.jetty.websocket.util.server.internal.WebSocketMapping;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -162,28 +165,63 @@ public class JavaxWebSocketServerContainer extends JavaxWebSocketClientContainer
return frameHandlerFactory; return frameHandlerFactory;
} }
private void validateEndpointConfig(ServerEndpointConfig config) throws DeploymentException
{
if (config == null)
{
throw new DeploymentException("Unable to deploy null ServerEndpointConfig");
}
ServerEndpointConfig.Configurator configurator = config.getConfigurator();
if (configurator == null)
{
throw new DeploymentException("Unable to deploy with null ServerEndpointConfig.Configurator");
}
Class<?> endpointClass = config.getEndpointClass();
if (endpointClass == null)
{
throw new DeploymentException("Unable to deploy null endpoint class from ServerEndpointConfig: " + config.getClass().getName());
}
if (!Modifier.isPublic(endpointClass.getModifiers()))
{
throw new DeploymentException("Class is not public: " + endpointClass.getName());
}
if (configurator.getClass() == ContainerDefaultConfigurator.class)
{
if (!ReflectUtils.isDefaultConstructable(endpointClass))
{
throw new DeploymentException("Cannot access default constructor for the class: " + endpointClass.getName());
}
}
}
@Override
public void addEndpoint(Class<?> endpointClass) throws DeploymentException public void addEndpoint(Class<?> endpointClass) throws DeploymentException
{ {
if (endpointClass == null) if (endpointClass == null)
{ {
throw new DeploymentException("EndpointClass is null"); throw new DeploymentException("Unable to deploy null endpoint class");
} }
if (isStarted() || isStarting()) if (isStarted() || isStarting())
{ {
try ServerEndpoint anno = endpointClass.getAnnotation(ServerEndpoint.class);
if (anno == null)
{ {
ServerEndpoint anno = endpointClass.getAnnotation(ServerEndpoint.class); throw new DeploymentException(String.format("Class must be @%s annotated: %s", ServerEndpoint.class.getName(), endpointClass.getName()));
if (anno == null) }
throw new DeploymentException(String.format("Class must be @%s annotated: %s", ServerEndpoint.class.getName(), endpointClass.getName()));
ServerEndpointConfig config = new AnnotatedServerEndpointConfig(this, endpointClass, anno); if (LOG.isDebugEnabled())
addEndpointMapping(config);
}
catch (WebSocketException e)
{ {
throw new DeploymentException("Unable to deploy: " + endpointClass.getName(), e); LOG.debug("addEndpoint({})", endpointClass);
} }
ServerEndpointConfig config = new AnnotatedServerEndpointConfig(this, endpointClass, anno);
validateEndpointConfig(config);
addEndpointMapping(config);
} }
else else
{ {
@ -201,23 +239,17 @@ public class JavaxWebSocketServerContainer extends JavaxWebSocketClientContainer
if (isStarted() || isStarting()) if (isStarted() || isStarting())
{ {
// If we have annotations merge the annotated ServerEndpointConfig with the provided one.
Class<?> endpointClass = providedConfig.getEndpointClass(); Class<?> endpointClass = providedConfig.getEndpointClass();
try ServerEndpoint anno = endpointClass.getAnnotation(ServerEndpoint.class);
{ ServerEndpointConfig config = (anno == null) ? providedConfig
// If we have annotations merge the annotated ServerEndpointConfig with the provided one. : new AnnotatedServerEndpointConfig(this, endpointClass, anno, providedConfig);
ServerEndpoint anno = endpointClass.getAnnotation(ServerEndpoint.class);
ServerEndpointConfig config = (anno == null) ? providedConfig
: new AnnotatedServerEndpointConfig(this, endpointClass, anno, providedConfig);
if (LOG.isDebugEnabled()) if (LOG.isDebugEnabled())
LOG.debug("addEndpoint({}) path={} endpoint={}", config, config.getPath(), endpointClass); LOG.debug("addEndpoint({}) path={} endpoint={}", config, config.getPath(), endpointClass);
addEndpointMapping(config); validateEndpointConfig(config);
} addEndpointMapping(config);
catch (WebSocketException e)
{
throw new DeploymentException("Unable to deploy: " + endpointClass.getName(), e);
}
} }
else else
{ {
@ -227,14 +259,23 @@ public class JavaxWebSocketServerContainer extends JavaxWebSocketClientContainer
} }
} }
private void addEndpointMapping(ServerEndpointConfig config) throws WebSocketException private void addEndpointMapping(ServerEndpointConfig config) throws DeploymentException
{ {
frameHandlerFactory.getMetadata(config.getEndpointClass(), config); try
{
JavaxWebSocketCreator creator = new JavaxWebSocketCreator(this, config, getExtensionRegistry()); frameHandlerFactory.getMetadata(config.getEndpointClass(), config);
JavaxWebSocketCreator creator = new JavaxWebSocketCreator(this, config, getExtensionRegistry());
PathSpec pathSpec = new UriTemplatePathSpec(config.getPath()); PathSpec pathSpec = new UriTemplatePathSpec(config.getPath());
webSocketMapping.addMapping(pathSpec, creator, frameHandlerFactory, defaultCustomizer); webSocketMapping.addMapping(pathSpec, creator, frameHandlerFactory, defaultCustomizer);
}
catch (InvalidSignatureException e)
{
throw new DeploymentException(e.getMessage(), e);
}
catch (Throwable t)
{
throw new DeploymentException("Unable to deploy: " + config.getEndpointClass().getName(), t);
}
} }
@Override @Override

View File

@ -0,0 +1,317 @@
//
// ========================================================================
// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under
// the terms of the Eclipse Public License 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0
//
// This Source Code may also be made available under the following
// Secondary Licenses when the conditions for such availability set
// forth in the Eclipse Public License, v. 2.0 are satisfied:
// the Apache License v2.0 which is available at
// https://www.apache.org/licenses/LICENSE-2.0
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.websocket.javax.tests.server;
import java.util.concurrent.TimeUnit;
import javax.websocket.CloseReason;
import javax.websocket.ContainerProvider;
import javax.websocket.DeploymentException;
import javax.websocket.Endpoint;
import javax.websocket.EndpointConfig;
import javax.websocket.MessageHandler;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.WebSocketContainer;
import javax.websocket.server.ServerContainer;
import javax.websocket.server.ServerEndpoint;
import javax.websocket.server.ServerEndpointConfig;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.util.component.LifeCycle;
import org.eclipse.jetty.websocket.javax.server.config.JavaxWebSocketServletContainerInitializer;
import org.eclipse.jetty.websocket.javax.tests.WSURI;
import org.eclipse.jetty.websocket.javax.tests.client.samples.CloseSocket;
import org.eclipse.jetty.websocket.javax.tests.server.sockets.BasicOpenCloseSocket;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.anyOf;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.nullValue;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class AddEndpointTest
{
private Server server;
private WebSocketContainer client;
private ServletContextHandler contextHandler;
@BeforeEach
public void before()
{
server = new Server();
ServerConnector connector = new ServerConnector(server);
server.addConnector(connector);
contextHandler = new ServletContextHandler();
contextHandler.setContextPath("/");
server.setHandler(contextHandler);
client = ContainerProvider.getWebSocketContainer();
}
@AfterEach
public void after() throws Exception
{
LifeCycle.stop(client);
server.stop();
}
public interface CheckedConsumer<T>
{
void accept(T t) throws DeploymentException;
}
public void start(CheckedConsumer<ServerContainer> containerConsumer) throws Exception
{
JavaxWebSocketServletContainerInitializer.configure(contextHandler, (context, container) -> containerConsumer.accept(container));
server.start();
}
private static class ServerSocket extends Endpoint implements MessageHandler.Whole<String>
{
@Override
public void onOpen(Session session, EndpointConfig config)
{
session.addMessageHandler(this);
}
@Override
public void onMessage(String message)
{
}
}
@SuppressWarnings("InnerClassMayBeStatic")
private class CustomPrivateEndpoint extends Endpoint
{
@Override
public void onOpen(Session session, EndpointConfig config)
{
}
}
@SuppressWarnings("InnerClassMayBeStatic")
@ServerEndpoint(value = "/", configurator = CustomAnnotatedEndpointConfigurator.class)
public static class CustomAnnotatedEndpoint
{
public CustomAnnotatedEndpoint(String id)
{
}
@OnOpen
public void onOpen(Session session, EndpointConfig config)
{
}
}
public static class CustomAnnotatedEndpointConfigurator extends ServerEndpointConfig.Configurator
{
@SuppressWarnings("unchecked")
@Override
public <T> T getEndpointInstance(Class<T> endpointClass)
{
return (T)new CustomAnnotatedEndpoint("server");
}
}
public static class CustomEndpoint extends Endpoint implements MessageHandler.Whole<String>
{
public CustomEndpoint(String id)
{
// This is a valid no-default-constructor implementation, and can be added via a custom
// ServerEndpointConfig.Configurator
}
@Override
public void onOpen(Session session, EndpointConfig config)
{
session.addMessageHandler(this);
}
@Override
public void onMessage(String message)
{
}
}
@SuppressWarnings("InnerClassMayBeStatic")
public class ServerSocketNonStatic extends Endpoint implements MessageHandler.Whole<String>
{
@Override
public void onOpen(Session session, EndpointConfig config)
{
session.addMessageHandler(this);
}
@Override
public void onMessage(String message)
{
}
}
@ServerEndpoint("/annotated")
private static class AnnotatedServerSocket
{
@OnMessage
public void onMessage(String message)
{
}
}
@ServerEndpoint("/annotatedMethod")
public static class AnnotatedServerMethod
{
@OnMessage
private void onMessage(String message)
{
}
}
@Test
public void testEndpoint()
{
RuntimeException error = assertThrows(RuntimeException.class, () ->
{
ServerEndpointConfig config = ServerEndpointConfig.Builder.create(ServerSocket.class, "/").build();
start(container -> container.addEndpoint(config));
});
assertThat(error.getCause(), instanceOf(DeploymentException.class));
DeploymentException deploymentException = (DeploymentException)error.getCause();
assertThat(deploymentException.getMessage(), containsString("Class is not public"));
}
@Test
public void testCustomEndpoint() throws Exception
{
ServerEndpointConfig config = ServerEndpointConfig.Builder.create(CustomEndpoint.class, "/")
.configurator(new ServerEndpointConfig.Configurator()
{
@SuppressWarnings("unchecked")
@Override
public <T> T getEndpointInstance(Class<T> endpointClass)
{
return (T)new CustomEndpoint("server");
}
}).build();
start(container -> container.addEndpoint(config));
CloseSocket clientEndpoint = new CloseSocket();
Session session = client.connectToServer(clientEndpoint, WSURI.toWebsocket(server.getURI().resolve("/")));
assertNotNull(session);
session.close();
assertTrue(clientEndpoint.closeLatch.await(5, TimeUnit.SECONDS));
CloseReason closeReason = clientEndpoint.closeDetail.get();
assertThat(closeReason, anyOf(nullValue(), is(CloseReason.CloseCodes.NORMAL_CLOSURE)));
}
@Test
public void testCustomPrivateEndpoint()
{
RuntimeException error = assertThrows(RuntimeException.class, () ->
{
ServerEndpointConfig config = ServerEndpointConfig.Builder.create(CustomPrivateEndpoint.class, "/")
.configurator(new ServerEndpointConfig.Configurator()
{
@SuppressWarnings("unchecked")
@Override
public <T> T getEndpointInstance(Class<T> endpointClass)
{
return (T)new CustomPrivateEndpoint();
}
}).build();
start(container -> container.addEndpoint(config));
});
assertThat(error.getCause(), instanceOf(DeploymentException.class));
DeploymentException deploymentException = (DeploymentException)error.getCause();
assertThat(deploymentException.getMessage(), containsString("Class is not public"));
}
@Test
public void testCustomAnnotatedEndpoint() throws Exception
{
start(container -> container.addEndpoint(CustomAnnotatedEndpoint.class));
CloseSocket clientEndpoint = new CloseSocket();
Session session = client.connectToServer(clientEndpoint, WSURI.toWebsocket(server.getURI().resolve("/")));
assertNotNull(session);
session.close();
assertTrue(clientEndpoint.closeLatch.await(5, TimeUnit.SECONDS));
CloseReason closeReason = clientEndpoint.closeDetail.get();
assertThat(closeReason, anyOf(nullValue(), is(CloseReason.CloseCodes.NORMAL_CLOSURE)));
}
@Test
public void testCustomEndpointNoConfigurator()
{
RuntimeException error = assertThrows(RuntimeException.class, () ->
{
ServerEndpointConfig config = ServerEndpointConfig.Builder.create(CustomEndpoint.class, "/").build();
start(container -> container.addEndpoint(config));
});
assertThat(error.getCause(), instanceOf(DeploymentException.class));
DeploymentException deploymentException = (DeploymentException)error.getCause();
assertThat(deploymentException.getMessage(), containsString("Cannot access default constructor"));
}
@Test
public void testInnerEndpoint()
{
RuntimeException error = assertThrows(RuntimeException.class, () ->
start(container -> container.addEndpoint(ServerEndpointConfig.Builder.create(ServerSocketNonStatic.class, "/").build())));
assertThat(error.getCause(), instanceOf(DeploymentException.class));
DeploymentException deploymentException = (DeploymentException)error.getCause();
assertThat(deploymentException.getMessage(), containsString("Cannot access default constructor"));
}
@Test
public void testAnnotatedEndpoint()
{
RuntimeException error = assertThrows(RuntimeException.class, () ->
start(container -> container.addEndpoint(AnnotatedServerSocket.class)));
assertThat(error.getCause(), instanceOf(DeploymentException.class));
DeploymentException deploymentException = (DeploymentException)error.getCause();
assertThat(deploymentException.getMessage(), containsString("Class is not public"));
}
@Test
public void testAnnotatedMethod()
{
RuntimeException error = assertThrows(RuntimeException.class, () ->
start(container ->
container.addEndpoint(AnnotatedServerMethod.class)));
assertThat(error.getCause(), instanceOf(DeploymentException.class));
DeploymentException deploymentException = (DeploymentException)error.getCause();
assertThat(deploymentException.getMessage(), containsString("method must be public"));
}
}

View File

@ -21,8 +21,6 @@ module org.eclipse.jetty.websocket.util
exports org.eclipse.jetty.websocket.util; exports org.eclipse.jetty.websocket.util;
exports org.eclipse.jetty.websocket.util.messages; exports org.eclipse.jetty.websocket.util.messages;
requires org.slf4j;
requires transitive org.eclipse.jetty.util;
requires transitive org.eclipse.jetty.io;
requires transitive org.eclipse.jetty.websocket.core.common; requires transitive org.eclipse.jetty.websocket.core.common;
requires org.slf4j;
} }

View File

@ -1287,7 +1287,7 @@
<execution> <execution>
<id>attach-sources</id> <id>attach-sources</id>
<goals> <goals>
<goal>jar</goal> <goal>jar-no-fork</goal>
</goals> </goals>
</execution> </execution>
</executions> </executions>

View File

@ -167,7 +167,7 @@ if proceedyn "Are you sure you want to release using above? (y/N)" n; then
# This is equivalent to 'mvn release:perform' # This is equivalent to 'mvn release:perform'
if proceedyn "Build/Deploy from tag $TAG_NAME? (Y/n)" y; then if proceedyn "Build/Deploy from tag $TAG_NAME? (Y/n)" y; then
git checkout $TAG_NAME git checkout $TAG_NAME
mvn clean package source:jar javadoc:jar gpg:sign javadoc:aggregate-jar deploy \ mvn clean package javadoc:aggregate-jar deploy \
-Peclipse-release $DEPLOY_OPTS -Peclipse-release $DEPLOY_OPTS
reportMavenTestFailures reportMavenTestFailures
git checkout $GIT_BRANCH_ID git checkout $GIT_BRANCH_ID

View File

@ -20,6 +20,7 @@ package org.eclipse.jetty.server.session;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import org.eclipse.jetty.logging.StacklessLogging;
import org.eclipse.jetty.servlet.ServletContextHandler; import org.eclipse.jetty.servlet.ServletContextHandler;
import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
@ -255,8 +256,12 @@ public class TestFileSessions extends AbstractTestBase
FileTestHelper.createFile(foreignNeverExpired); FileTestHelper.createFile(foreignNeverExpired);
FileTestHelper.assertFileExists(foreignNeverExpired, true); FileTestHelper.assertFileExists(foreignNeverExpired, true);
//sweep //sweep - we're expecting a debug log with exception stacktrace due to file named
((FileSessionDataStore)store).sweepDisk(); //nonNumber__0.0.0.0_spuriousFile so suppress it
try (StacklessLogging ignored = new StacklessLogging(TestFileSessions.class.getPackage()))
{
((FileSessionDataStore)store).sweepDisk();
}
//check results //check results
FileTestHelper.assertSessionExists("sessiona", false); FileTestHelper.assertSessionExists("sessiona", false);

View File

@ -63,11 +63,6 @@
<artifactId>slf4j-api</artifactId> <artifactId>slf4j-api</artifactId>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency>
<groupId>org.eclipse.jetty.toolchain</groupId>
<artifactId>jetty-test-helper</artifactId>
<scope>test</scope>
</dependency>
<dependency> <dependency>
<groupId>org.testcontainers</groupId> <groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId> <artifactId>testcontainers</artifactId>

View File

@ -72,10 +72,20 @@
<artifactId>jetty-slf4j-impl</artifactId> <artifactId>jetty-slf4j-impl</artifactId>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
</dependencies> </dependencies>
<profiles> <profiles>
<profile> <profile>
<id>memcached</id> <id>remote-session-tests</id>
<activation> <activation>
<property> <property>
<name>memcached.enabled</name> <name>memcached.enabled</name>

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