diff options
author | Joakim Erdfelt | 2015-11-04 19:35:39 +0000 |
---|---|---|
committer | Joakim Erdfelt | 2015-11-04 19:35:39 +0000 |
commit | a69180e34470d860c875114bba0a123217f1aeba (patch) | |
tree | ebd3285694c6105709131d9744ebe08356bdc116 | |
parent | 5fa750973ab92e41de8b2dfc204f495c78002087 (diff) | |
parent | 3e2658a41b07ec2b455ea806f2ba6f8a3245f581 (diff) | |
download | org.eclipse.jetty.project-a69180e34470d860c875114bba0a123217f1aeba.tar.gz org.eclipse.jetty.project-a69180e34470d860c875114bba0a123217f1aeba.tar.xz org.eclipse.jetty.project-a69180e34470d860c875114bba0a123217f1aeba.zip |
Merge branch 'master' into feature/wsclient-httpclient
117 files changed, 3749 insertions, 1704 deletions
diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/AbstractConnectionPool.java b/jetty-client/src/main/java/org/eclipse/jetty/client/AbstractConnectionPool.java new file mode 100644 index 0000000000..d3b13b668d --- /dev/null +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/AbstractConnectionPool.java @@ -0,0 +1,199 @@ +// +// ======================================================================== +// Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.client; + +import java.util.Collection; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +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.Promise; +import org.eclipse.jetty.util.annotation.ManagedAttribute; +import org.eclipse.jetty.util.component.ContainerLifeCycle; +import org.eclipse.jetty.util.component.Dumpable; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; + +public abstract class AbstractConnectionPool implements ConnectionPool, Dumpable +{ + private static final Logger LOG = Log.getLogger(AbstractConnectionPool.class); + + private final AtomicBoolean closed = new AtomicBoolean(); + private final AtomicInteger connectionCount = new AtomicInteger(); + private final Destination destination; + private final int maxConnections; + private final Callback requester; + + protected AbstractConnectionPool(Destination destination, int maxConnections, Callback requester) + { + this.destination = destination; + this.maxConnections = maxConnections; + this.requester = requester; + } + + @ManagedAttribute(value = "The max number of connections", readonly = true) + public int getMaxConnectionCount() + { + return maxConnections; + } + + @ManagedAttribute(value = "The number of connections", readonly = true) + public int getConnectionCount() + { + return connectionCount.get(); + } + + @Override + public boolean isEmpty() + { + return connectionCount.get() == 0; + } + + @Override + public boolean isClosed() + { + return closed.get(); + } + + @Override + public Connection acquire() + { + Connection connection = activate(); + if (connection == null) + connection = tryCreate(); + return connection; + } + + private Connection tryCreate() + { + while (true) + { + int current = getConnectionCount(); + final int next = current + 1; + + if (next > maxConnections) + { + if (LOG.isDebugEnabled()) + LOG.debug("Max connections {}/{} reached", current, maxConnections); + // Try again the idle connections + return activate(); + } + + if (connectionCount.compareAndSet(current, next)) + { + if (LOG.isDebugEnabled()) + LOG.debug("Connection {}/{} creation", next, maxConnections); + + destination.newConnection(new Promise<Connection>() + { + @Override + public void succeeded(Connection connection) + { + if (LOG.isDebugEnabled()) + LOG.debug("Connection {}/{} creation succeeded {}", next, maxConnections, connection); + onCreated(connection); + proceed(); + } + + @Override + public void failed(Throwable x) + { + if (LOG.isDebugEnabled()) + LOG.debug("Connection " + next + "/" + maxConnections + " creation failed", x); + connectionCount.decrementAndGet(); + requester.failed(x); + } + }); + + // Try again the idle connections + return activate(); + } + } + } + + protected abstract void onCreated(Connection connection); + + protected void proceed() + { + requester.succeeded(); + } + + protected abstract Connection activate(); + + protected Connection active(Connection connection) + { + if (LOG.isDebugEnabled()) + LOG.debug("Connection active {}", connection); + acquired(connection); + return connection; + } + + protected void acquired(Connection connection) + { + } + + protected boolean idle(Connection connection, boolean close) + { + if (close) + { + if (LOG.isDebugEnabled()) + LOG.debug("Connection idle close {}", connection); + return false; + } + else + { + if (LOG.isDebugEnabled()) + LOG.debug("Connection idle {}", connection); + return true; + } + } + + protected void released(Connection connection) + { + } + + protected void removed(Connection connection) + { + int pooled = connectionCount.decrementAndGet(); + if (LOG.isDebugEnabled()) + LOG.debug("Connection removed {} - pooled: {}", connection, pooled); + } + + @Override + public void close() + { + if (closed.compareAndSet(false, true)) + { + connectionCount.set(0); + } + } + + protected void close(Collection<Connection> connections) + { + connections.forEach(Connection::close); + } + + @Override + public String dump() + { + return ContainerLifeCycle.dump(this); + } +} diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/ConnectionPool.java b/jetty-client/src/main/java/org/eclipse/jetty/client/ConnectionPool.java index fc95421a0b..029a388d33 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/ConnectionPool.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/ConnectionPool.java @@ -18,17 +18,24 @@ package org.eclipse.jetty.client; -import org.eclipse.jetty.client.api.Destination; -import org.eclipse.jetty.util.Callback; - -/** - * @deprecated use {@link DuplexConnectionPool} instead - */ -@Deprecated -public class ConnectionPool extends DuplexConnectionPool +import java.io.Closeable; + +import org.eclipse.jetty.client.api.Connection; + +public interface ConnectionPool extends Closeable { - public ConnectionPool(Destination destination, int maxConnections, Callback requester) - { - super(destination, maxConnections, requester); - } + boolean isActive(Connection connection); + + boolean isEmpty(); + + boolean isClosed(); + + Connection acquire(); + + boolean release(Connection connection); + + boolean remove(Connection connection); + + @Override + void close(); } diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/DuplexConnectionPool.java b/jetty-client/src/main/java/org/eclipse/jetty/client/DuplexConnectionPool.java index d887923e1c..c22966c372 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/DuplexConnectionPool.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/DuplexConnectionPool.java @@ -18,21 +18,20 @@ package org.eclipse.jetty.client; -import java.io.Closeable; import java.io.IOException; +import java.util.ArrayDeque; import java.util.ArrayList; +import java.util.Collection; import java.util.Deque; +import java.util.HashSet; import java.util.List; import java.util.Queue; -import java.util.concurrent.LinkedBlockingDeque; -import java.util.concurrent.atomic.AtomicInteger; +import java.util.Set; import java.util.concurrent.locks.ReentrantLock; import org.eclipse.jetty.client.api.Connection; import org.eclipse.jetty.client.api.Destination; -import org.eclipse.jetty.util.BlockingArrayQueue; import org.eclipse.jetty.util.Callback; -import org.eclipse.jetty.util.Promise; import org.eclipse.jetty.util.annotation.ManagedAttribute; import org.eclipse.jetty.util.annotation.ManagedObject; import org.eclipse.jetty.util.component.ContainerLifeCycle; @@ -42,43 +41,57 @@ import org.eclipse.jetty.util.log.Logger; import org.eclipse.jetty.util.thread.Sweeper; @ManagedObject("The connection pool") -public class DuplexConnectionPool implements Closeable, Dumpable, Sweeper.Sweepable +public class DuplexConnectionPool extends AbstractConnectionPool implements Dumpable, Sweeper.Sweepable { private static final Logger LOG = Log.getLogger(DuplexConnectionPool.class); - private final AtomicInteger connectionCount = new AtomicInteger(); private final ReentrantLock lock = new ReentrantLock(); - private final Destination destination; - private final int maxConnections; - private final Callback requester; private final Deque<Connection> idleConnections; - private final Queue<Connection> activeConnections; + private final Set<Connection> activeConnections; public DuplexConnectionPool(Destination destination, int maxConnections, Callback requester) { - this.destination = destination; - this.maxConnections = maxConnections; - this.requester = requester; - this.idleConnections = new LinkedBlockingDeque<>(maxConnections); - this.activeConnections = new BlockingArrayQueue<>(maxConnections); + super(destination, maxConnections, requester); + this.idleConnections = new ArrayDeque<>(maxConnections); + this.activeConnections = new HashSet<>(maxConnections); } - @ManagedAttribute(value = "The number of connections", readonly = true) - public int getConnectionCount() + protected void lock() + { + lock.lock(); + } + + protected void unlock() { - return connectionCount.get(); + lock.unlock(); } @ManagedAttribute(value = "The number of idle connections", readonly = true) public int getIdleConnectionCount() { - return idleConnections.size(); + lock(); + try + { + return idleConnections.size(); + } + finally + { + unlock(); + } } @ManagedAttribute(value = "The number of active connections", readonly = true) public int getActiveConnectionCount() { - return activeConnections.size(); + lock(); + try + { + return activeConnections.size(); + } + finally + { + unlock(); + } } public Queue<Connection> getIdleConnections() @@ -86,139 +99,76 @@ public class DuplexConnectionPool implements Closeable, Dumpable, Sweeper.Sweepa return idleConnections; } - public Queue<Connection> getActiveConnections() + public Collection<Connection> getActiveConnections() { return activeConnections; } - public Connection acquire() - { - Connection connection = activateIdle(); - if (connection == null) - connection = tryCreate(); - return connection; - } - - private Connection tryCreate() + @Override + public boolean isActive(Connection connection) { - while (true) + lock(); + try { - int current = getConnectionCount(); - final int next = current + 1; - - if (next > maxConnections) - { - if (LOG.isDebugEnabled()) - LOG.debug("Max connections {}/{} reached", current, maxConnections); - // Try again the idle connections - return activateIdle(); - } - - if (connectionCount.compareAndSet(current, next)) - { - if (LOG.isDebugEnabled()) - LOG.debug("Connection {}/{} creation", next, maxConnections); - - destination.newConnection(new Promise<Connection>() - { - @Override - public void succeeded(Connection connection) - { - if (LOG.isDebugEnabled()) - LOG.debug("Connection {}/{} creation succeeded {}", next, maxConnections, connection); - - idleCreated(connection); - - proceed(); - } - - @Override - public void failed(Throwable x) - { - if (LOG.isDebugEnabled()) - LOG.debug("Connection " + next + "/" + maxConnections + " creation failed", x); - - connectionCount.decrementAndGet(); - - requester.failed(x); - } - }); - - // Try again the idle connections - return activateIdle(); - } + return activeConnections.contains(connection); + } + finally + { + unlock(); } } - protected void proceed() - { - requester.succeeded(); - } - - protected void idleCreated(Connection connection) + @Override + protected void onCreated(Connection connection) { - boolean idle; lock(); try { // Use "cold" new connections as last. - idle = idleConnections.offerLast(connection); + idleConnections.offer(connection); } finally { unlock(); } - idle(connection, idle); + idle(connection, false); } - private Connection activateIdle() + @Override + protected Connection activate() { - boolean acquired; Connection connection; lock(); try { - connection = idleConnections.pollFirst(); + connection = idleConnections.poll(); if (connection == null) return null; - acquired = activeConnections.offer(connection); + activeConnections.add(connection); } finally { unlock(); } - if (acquired) - { - if (LOG.isDebugEnabled()) - LOG.debug("Connection active {}", connection); - acquired(connection); - return connection; - } - else - { - if (LOG.isDebugEnabled()) - LOG.debug("Connection active overflow {}", connection); - connection.close(); - return null; - } - } - - protected void acquired(Connection connection) - { + return active(connection); } public boolean release(Connection connection) { - boolean idle; + boolean closed = isClosed(); lock(); try { if (!activeConnections.remove(connection)) return false; - // Make sure we use "hot" connections first. - idle = offerIdle(connection); + + if (!closed) + { + // Make sure we use "hot" connections first. + deactivate(connection); + } } finally { @@ -226,35 +176,14 @@ public class DuplexConnectionPool implements Closeable, Dumpable, Sweeper.Sweepa } released(connection); - return idle(connection, idle); + return idle(connection, closed); } - protected boolean offerIdle(Connection connection) + protected boolean deactivate(Connection connection) { return idleConnections.offerFirst(connection); } - protected boolean idle(Connection connection, boolean idle) - { - if (idle) - { - if (LOG.isDebugEnabled()) - LOG.debug("Connection idle {}", connection); - return true; - } - else - { - if (LOG.isDebugEnabled()) - LOG.debug("Connection idle overflow {}", connection); - connection.close(); - return false; - } - } - - protected void released(Connection connection) - { - } - public boolean remove(Connection connection) { return remove(connection, false); @@ -275,59 +204,25 @@ public class DuplexConnectionPool implements Closeable, Dumpable, Sweeper.Sweepa unlock(); } - if (activeRemoved) + if (activeRemoved || force) released(connection); boolean removed = activeRemoved || idleRemoved || force; if (removed) - { - int pooled = connectionCount.decrementAndGet(); - if (LOG.isDebugEnabled()) - LOG.debug("Connection removed {} - pooled: {}", connection, pooled); - } + removed(connection); return removed; } - public boolean isActive(Connection connection) - { - lock(); - try - { - return activeConnections.contains(connection); - } - finally - { - unlock(); - } - } - - public boolean isIdle(Connection connection) - { - lock(); - try - { - return idleConnections.contains(connection); - } - finally - { - unlock(); - } - } - - public boolean isEmpty() - { - return connectionCount.get() == 0; - } - public void close() { - List<Connection> idles = new ArrayList<>(); - List<Connection> actives = new ArrayList<>(); + super.close(); + + List<Connection> connections = new ArrayList<>(); lock(); try { - idles.addAll(idleConnections); + connections.addAll(idleConnections); idleConnections.clear(); - actives.addAll(activeConnections); + connections.addAll(activeConnections); activeConnections.clear(); } finally @@ -335,32 +230,18 @@ public class DuplexConnectionPool implements Closeable, Dumpable, Sweeper.Sweepa unlock(); } - connectionCount.set(0); - - for (Connection connection : idles) - connection.close(); - - // A bit drastic, but we cannot wait for all requests to complete - for (Connection connection : actives) - connection.close(); - } - - @Override - public String dump() - { - return ContainerLifeCycle.dump(this); + close(connections); } @Override public void dump(Appendable out, String indent) throws IOException { - List<Connection> actives = new ArrayList<>(); - List<Connection> idles = new ArrayList<>(); + List<Connection> connections = new ArrayList<>(); lock(); try { - actives.addAll(activeConnections); - idles.addAll(idleConnections); + connections.addAll(activeConnections); + connections.addAll(idleConnections); } finally { @@ -368,20 +249,20 @@ public class DuplexConnectionPool implements Closeable, Dumpable, Sweeper.Sweepa } ContainerLifeCycle.dumpObject(out, this); - ContainerLifeCycle.dump(out, indent, actives, idles); + ContainerLifeCycle.dump(out, indent, connections); } @Override public boolean sweep() { - List<Sweeper.Sweepable> toSweep = new ArrayList<>(); + List<Connection> toSweep = new ArrayList<>(); lock(); try { - for (Connection connection : getActiveConnections()) + for (Connection connection : activeConnections) { if (connection instanceof Sweeper.Sweepable) - toSweep.add(((Sweeper.Sweepable)connection)); + toSweep.add(connection); } } finally @@ -389,13 +270,13 @@ public class DuplexConnectionPool implements Closeable, Dumpable, Sweeper.Sweepa unlock(); } - for (Sweeper.Sweepable candidate : toSweep) + for (Connection connection : toSweep) { - if (candidate.sweep()) + if (((Sweeper.Sweepable)connection).sweep()) { - boolean removed = getActiveConnections().remove(candidate); + boolean removed = remove(connection, true); LOG.warn("Connection swept: {}{}{} from active connections{}{}", - candidate, + connection, System.lineSeparator(), removed ? "Removed" : "Not removed", System.lineSeparator(), @@ -406,16 +287,6 @@ public class DuplexConnectionPool implements Closeable, Dumpable, Sweeper.Sweepa return false; } - protected void lock() - { - lock.lock(); - } - - protected void unlock() - { - lock.unlock(); - } - @Override public String toString() { @@ -434,8 +305,8 @@ public class DuplexConnectionPool implements Closeable, Dumpable, Sweeper.Sweepa return String.format("%s[c=%d/%d,a=%d,i=%d]", getClass().getSimpleName(), - connectionCount.get(), - maxConnections, + getConnectionCount(), + getMaxConnectionCount(), activeSize, idleSize); } diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpClient.java b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpClient.java index 0720751663..cbf15b6fff 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpClient.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpClient.java @@ -1039,7 +1039,7 @@ public class HttpClient extends ContainerLifeCycle { if (port > 0) return port; - else if (HttpScheme.HTTPS.is(scheme) || HttpScheme.WSS.is(scheme)) + else if (isSchemeSecure(scheme)) return 443; else return 80; @@ -1047,12 +1047,17 @@ public class HttpClient extends ContainerLifeCycle public boolean isDefaultPort(String scheme, int port) { - if (HttpScheme.HTTPS.is(scheme) || HttpScheme.WSS.is(scheme)) + if (isSchemeSecure(scheme)) return port == 443; - else + else return port == 80; } + public boolean isSchemeSecure(String scheme) + { + return HttpScheme.HTTPS.is(scheme) || HttpScheme.WSS.is(scheme); + } + private class ContentDecoderFactorySet implements Set<ContentDecoder.Factory> { private final Set<ContentDecoder.Factory> set = new HashSet<>(); diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpDestination.java b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpDestination.java index ff24d7b3b1..58eaee5ef5 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpDestination.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpDestination.java @@ -22,19 +22,21 @@ import java.io.Closeable; import java.io.IOException; import java.nio.channels.AsynchronousCloseException; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Queue; import java.util.concurrent.RejectedExecutionException; import org.eclipse.jetty.client.api.Connection; import org.eclipse.jetty.client.api.Destination; +import org.eclipse.jetty.client.api.Request; import org.eclipse.jetty.client.api.Response; import org.eclipse.jetty.http.HttpField; import org.eclipse.jetty.http.HttpHeader; -import org.eclipse.jetty.http.HttpScheme; import org.eclipse.jetty.io.ClientConnectionFactory; import org.eclipse.jetty.io.ssl.SslClientConnectionFactory; import org.eclipse.jetty.util.BlockingArrayQueue; +import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.util.Promise; import org.eclipse.jetty.util.annotation.ManagedAttribute; import org.eclipse.jetty.util.annotation.ManagedObject; @@ -42,9 +44,10 @@ import org.eclipse.jetty.util.component.ContainerLifeCycle; import org.eclipse.jetty.util.component.Dumpable; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; +import org.eclipse.jetty.util.thread.Sweeper; @ManagedObject -public abstract class HttpDestination extends ContainerLifeCycle implements Destination, Closeable, Dumpable +public abstract class HttpDestination extends ContainerLifeCycle implements Destination, Closeable, Callback, Dumpable { protected static final Logger LOG = Log.getLogger(HttpDestination.class); @@ -56,6 +59,7 @@ public abstract class HttpDestination extends ContainerLifeCycle implements Dest private final ProxyConfiguration.Proxy proxy; private final ClientConnectionFactory connectionFactory; private final HttpField hostField; + private ConnectionPool connectionPool; public HttpDestination(HttpClient client, Origin origin) { @@ -76,7 +80,7 @@ public abstract class HttpDestination extends ContainerLifeCycle implements Dest } else { - if (HttpScheme.HTTPS.is(getScheme()) || HttpScheme.WSS.is(getScheme())) + if (isSecure()) connectionFactory = newSslClientConnectionFactory(connectionFactory); } this.connectionFactory = connectionFactory; @@ -87,6 +91,29 @@ public abstract class HttpDestination extends ContainerLifeCycle implements Dest hostField = new HttpField(HttpHeader.HOST, host); } + @Override + protected void doStart() throws Exception + { + this.connectionPool = newConnectionPool(client); + addBean(connectionPool); + super.doStart(); + Sweeper sweeper = client.getBean(Sweeper.class); + if (sweeper != null && connectionPool instanceof Sweeper.Sweepable) + sweeper.offer((Sweeper.Sweepable)connectionPool); + } + + @Override + protected void doStop() throws Exception + { + Sweeper sweeper = client.getBean(Sweeper.class); + if (sweeper != null && connectionPool instanceof Sweeper.Sweepable) + sweeper.remove((Sweeper.Sweepable)connectionPool); + super.doStop(); + removeBean(connectionPool); + } + + protected abstract ConnectionPool newConnectionPool(HttpClient client); + protected Queue<HttpExchange> newExchangeQueue(HttpClient client) { return new BlockingArrayQueue<>(client.getMaxRequestsQueuedPerDestination()); @@ -97,6 +124,11 @@ public abstract class HttpDestination extends ContainerLifeCycle implements Dest return new SslClientConnectionFactory(client.getSslContextFactory(), client.getByteBufferPool(), client.getExecutor(), connectionFactory); } + public boolean isSecure() + { + return client.isSchemeSecure(getScheme()); + } + public HttpClient getHttpClient() { return client; @@ -171,6 +203,24 @@ public abstract class HttpDestination extends ContainerLifeCycle implements Dest return hostField; } + @ManagedAttribute(value = "The connection pool", readonly = true) + public ConnectionPool getConnectionPool() + { + return connectionPool; + } + + @Override + public void succeeded() + { + send(); + } + + @Override + public void failed(Throwable x) + { + abort(x); + } + protected void send(HttpRequest request, List<Response.ResponseListener> listeners) { if (!getScheme().equalsIgnoreCase(request.getScheme())) @@ -217,7 +267,59 @@ public abstract class HttpDestination extends ContainerLifeCycle implements Dest return queue.offer(exchange); } - public abstract void send(); + public void send() + { + if (getHttpExchanges().isEmpty()) + return; + process(); + } + + private void process() + { + Connection connection = connectionPool.acquire(); + if (connection != null) + process(connection); + } + + public void process(final Connection connection) + { + HttpClient client = getHttpClient(); + final HttpExchange exchange = getHttpExchanges().poll(); + if (LOG.isDebugEnabled()) + LOG.debug("Processing exchange {} on {} of {}", exchange, connection, this); + if (exchange == null) + { + if (!connectionPool.release(connection)) + connection.close(); + + if (!client.isRunning()) + { + if (LOG.isDebugEnabled()) + LOG.debug("{} is stopping", client); + connection.close(); + } + } + else + { + final Request request = exchange.getRequest(); + Throwable cause = request.getAbortCause(); + if (cause != null) + { + if (LOG.isDebugEnabled()) + LOG.debug("Aborted before processing {}: {}", exchange, cause); + // It may happen that the request is aborted before the exchange + // is created. Aborting the exchange a second time will result in + // a no-operation, so we just abort here to cover that edge case. + exchange.abort(cause); + } + else + { + send(connection, exchange); + } + } + } + + protected abstract void send(Connection connection, HttpExchange exchange); public void newConnection(Promise<Connection> promise) { @@ -239,14 +341,67 @@ public abstract class HttpDestination extends ContainerLifeCycle implements Dest abort(new AsynchronousCloseException()); if (LOG.isDebugEnabled()) LOG.debug("Closed {}", this); + connectionPool.close(); } public void release(Connection connection) { + if (LOG.isDebugEnabled()) + LOG.debug("Released {}", connection); + HttpClient client = getHttpClient(); + if (client.isRunning()) + { + if (connectionPool.isActive(connection)) + { + if (connectionPool.release(connection)) + send(); + else + connection.close(); + } + else + { + if (LOG.isDebugEnabled()) + LOG.debug("Released explicit {}", connection); + } + } + else + { + if (LOG.isDebugEnabled()) + LOG.debug("{} is stopped", client); + connection.close(); + } + } + + public boolean remove(Connection connection) + { + return connectionPool.remove(connection); } public void close(Connection connection) { + boolean removed = remove(connection); + + if (getHttpExchanges().isEmpty()) + { + if (getHttpClient().isRemoveIdleDestinations() && connectionPool.isEmpty()) + { + // There is a race condition between this thread removing the destination + // and another thread queueing a request to this same destination. + // If this destination is removed, but the request queued, a new connection + // will be opened, the exchange will be executed and eventually the connection + // will idle timeout and be closed. Meanwhile a new destination will be created + // in HttpClient and will be used for other requests. + getHttpClient().removeDestination(this); + } + } + 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(); + } } /** @@ -274,6 +429,7 @@ public abstract class HttpDestination extends ContainerLifeCycle implements Dest public void dump(Appendable out, String indent) throws IOException { ContainerLifeCycle.dumpObject(out, toString()); + ContainerLifeCycle.dump(out, indent, Collections.singletonList(connectionPool)); } public String asString() @@ -284,11 +440,12 @@ public abstract class HttpDestination extends ContainerLifeCycle implements Dest @Override public String toString() { - return String.format("%s[%s]%x%s,queue=%d", + return String.format("%s[%s]%x%s,queue=%d,pool=%s", HttpDestination.class.getSimpleName(), asString(), hashCode(), proxy == null ? "" : "(via " + proxy + ")", - exchanges.size()); + exchanges.size(), + connectionPool); } } diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpProxy.java b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpProxy.java index 7199f17eb7..2a92a5d42a 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpProxy.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpProxy.java @@ -107,7 +107,7 @@ public class HttpProxy extends ProxyConfiguration.Proxy public void succeeded(Connection connection) { HttpDestination destination = (HttpDestination)context.get(HttpClientTransport.HTTP_DESTINATION_CONTEXT_KEY); - if (HttpScheme.HTTPS.is(destination.getScheme()) || HttpScheme.WSS.is(destination.getScheme())) + if (destination.isSecure()) { SslContextFactory sslContextFactory = destination.getHttpClient().getSslContextFactory(); if (sslContextFactory != null) diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/LeakTrackingConnectionPool.java b/jetty-client/src/main/java/org/eclipse/jetty/client/LeakTrackingConnectionPool.java index 7762af09f9..f5d3b98580 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/LeakTrackingConnectionPool.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/LeakTrackingConnectionPool.java @@ -25,7 +25,7 @@ import org.eclipse.jetty.util.LeakDetector; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; -public class LeakTrackingConnectionPool extends ConnectionPool +public class LeakTrackingConnectionPool extends DuplexConnectionPool { private static final Logger LOG = Log.getLogger(LeakTrackingConnectionPool.class); diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/MultiplexConnectionPool.java b/jetty-client/src/main/java/org/eclipse/jetty/client/MultiplexConnectionPool.java new file mode 100644 index 0000000000..88561bb75b --- /dev/null +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/MultiplexConnectionPool.java @@ -0,0 +1,302 @@ +// +// ======================================================================== +// Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.client; + +import java.io.IOException; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Deque; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.concurrent.locks.ReentrantLock; +import java.util.stream.Collectors; + +import org.eclipse.jetty.client.api.Connection; +import org.eclipse.jetty.util.Callback; +import org.eclipse.jetty.util.component.ContainerLifeCycle; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; + +public class MultiplexConnectionPool extends AbstractConnectionPool +{ + private static final Logger LOG = Log.getLogger(MultiplexConnectionPool.class); + + private final ReentrantLock lock = new ReentrantLock(); + private final int maxMultiplexed; + private final Deque<Holder> idleConnections; + private final Map<Connection, Holder> muxedConnections; + private final Map<Connection, Holder> busyConnections; + + public MultiplexConnectionPool(HttpDestination destination, int maxConnections, Callback requester, int maxMultiplexed) + { + super(destination, maxConnections, requester); + this.maxMultiplexed = maxMultiplexed; + this.idleConnections = new ArrayDeque<>(maxConnections); + this.muxedConnections = new HashMap<>(maxConnections); + this.busyConnections = new HashMap<>(maxConnections); + } + + protected void lock() + { + lock.lock(); + } + + protected void unlock() + { + lock.unlock(); + } + + @Override + public boolean isActive(Connection connection) + { + lock(); + try + { + if (muxedConnections.containsKey(connection)) + return true; + if (busyConnections.containsKey(connection)) + return true; + return false; + } + finally + { + unlock(); + } + } + + @Override + protected void onCreated(Connection connection) + { + lock(); + try + { + // Use "cold" connections as last. + idleConnections.offer(new Holder(connection)); + } + finally + { + unlock(); + } + + idle(connection, false); + } + + @Override + protected Connection activate() + { + Holder holder; + lock(); + try + { + while (true) + { + if (muxedConnections.isEmpty()) + { + holder = idleConnections.poll(); + if (holder == null) + return null; + muxedConnections.put(holder.connection, holder); + } + else + { + holder = muxedConnections.values().iterator().next(); + } + + if (holder.count < maxMultiplexed) + { + ++holder.count; + break; + } + else + { + muxedConnections.remove(holder.connection); + busyConnections.put(holder.connection, holder); + } + } + } + finally + { + unlock(); + } + + return active(holder.connection); + } + + @Override + public boolean release(Connection connection) + { + boolean closed = isClosed(); + boolean idle = false; + Holder holder; + lock(); + try + { + holder = muxedConnections.get(connection); + if (holder != null) + { + int count = --holder.count; + if (count == 0) + { + muxedConnections.remove(connection); + if (!closed) + { + idleConnections.offerFirst(holder); + idle = true; + } + } + } + else + { + holder = busyConnections.remove(connection); + if (holder != null) + { + int count = --holder.count; + if (!closed) + { + if (count == 0) + { + idleConnections.offerFirst(holder); + idle = true; + } + else + { + muxedConnections.put(connection, holder); + } + } + } + } + } + finally + { + unlock(); + } + + if (holder == null) + return false; + + released(connection); + if (idle || closed) + return idle(connection, closed); + return true; + } + + @Override + public boolean remove(Connection connection) + { + return remove(connection, false); + } + + protected boolean remove(Connection connection, boolean force) + { + boolean activeRemoved = true; + boolean idleRemoved = false; + lock(); + try + { + Holder holder = muxedConnections.remove(connection); + if (holder == null) + holder = busyConnections.remove(connection); + if (holder == null) + { + activeRemoved = false; + for (Iterator<Holder> iterator = idleConnections.iterator(); iterator.hasNext();) + { + holder = iterator.next(); + if (holder.connection == connection) + { + idleRemoved = true; + iterator.remove(); + break; + } + } + } + } + finally + { + unlock(); + } + + if (activeRemoved || force) + released(connection); + boolean removed = activeRemoved || idleRemoved || force; + if (removed) + removed(connection); + return removed; + } + + @Override + public void close() + { + super.close(); + + List<Connection> connections; + lock(); + try + { + connections = idleConnections.stream().map(holder -> holder.connection).collect(Collectors.toList()); + connections.addAll(muxedConnections.keySet()); + connections.addAll(busyConnections.keySet()); + } + finally + { + unlock(); + } + + close(connections); + } + + @Override + public void dump(Appendable out, String indent) throws IOException + { + List<Holder> connections = new ArrayList<>(); + lock(); + try + { + connections.addAll(busyConnections.values()); + connections.addAll(muxedConnections.values()); + connections.addAll(idleConnections); + } + finally + { + unlock(); + } + + ContainerLifeCycle.dumpObject(out, this); + ContainerLifeCycle.dump(out, indent, connections); + } + + private static class Holder + { + private final Connection connection; + private int count; + + private Holder(Connection connection) + { + this.connection = connection; + } + + @Override + public String toString() + { + return String.format("%s[%d]", connection, count); + } + } +} diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/MultiplexHttpDestination.java b/jetty-client/src/main/java/org/eclipse/jetty/client/MultiplexHttpDestination.java index a50131f1ed..a23fb34a82 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/MultiplexHttpDestination.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/MultiplexHttpDestination.java @@ -18,136 +18,16 @@ package org.eclipse.jetty.client; -import java.util.concurrent.atomic.AtomicReference; - -import org.eclipse.jetty.client.api.Connection; -import org.eclipse.jetty.client.api.Request; -import org.eclipse.jetty.util.Promise; - -public abstract class MultiplexHttpDestination<C extends Connection> extends HttpDestination implements Promise<Connection> +public abstract class MultiplexHttpDestination extends HttpDestination { - private final AtomicReference<ConnectState> connect = new AtomicReference<>(ConnectState.DISCONNECTED); - private C connection; - protected MultiplexHttpDestination(HttpClient client, Origin origin) { super(client, origin); } - @Override - public void send() - { - while (true) - { - ConnectState current = connect.get(); - switch (current) - { - case DISCONNECTED: - { - if (!connect.compareAndSet(current, ConnectState.CONNECTING)) - break; - newConnection(this); - return; - } - case CONNECTING: - { - // Waiting to connect, just return - return; - } - case CONNECTED: - { - if (process(connection)) - break; - return; - } - default: - { - abort(new IllegalStateException("Invalid connection state " + current)); - return; - } - } - } - } - - @Override - @SuppressWarnings("unchecked") - public void succeeded(Connection result) - { - C connection = this.connection = (C)result; - if (connect.compareAndSet(ConnectState.CONNECTING, ConnectState.CONNECTED)) - { - process(connection); - } - else - { - connection.close(); - failed(new IllegalStateException()); - } - } - - @Override - public void failed(Throwable x) - { - connect.set(ConnectState.DISCONNECTED); - abort(x); - } - - protected boolean process(final C connection) - { - HttpClient client = getHttpClient(); - final HttpExchange exchange = getHttpExchanges().poll(); - if (LOG.isDebugEnabled()) - LOG.debug("Processing {} on {}", exchange, connection); - if (exchange == null) - return false; - - final Request request = exchange.getRequest(); - Throwable cause = request.getAbortCause(); - if (cause != null) - { - if (LOG.isDebugEnabled()) - LOG.debug("Aborted before processing {}: {}", exchange, cause); - // It may happen that the request is aborted before the exchange - // is created. Aborting the exchange a second time will result in - // a no-operation, so we just abort here to cover that edge case. - exchange.abort(cause); - } - else - { - send(connection, exchange); - } - return true; - } - - @Override - public void close() - { - super.close(); - C connection = this.connection; - if (connection != null) - connection.close(); - } - - @Override - public void close(Connection connection) - { - super.close(connection); - while (true) - { - ConnectState current = connect.get(); - if (connect.compareAndSet(current, ConnectState.DISCONNECTED)) - { - if (getHttpClient().isRemoveIdleDestinations()) - getHttpClient().removeDestination(this); - break; - } - } - } - - protected abstract void send(C connection, HttpExchange exchange); - - private enum ConnectState + protected ConnectionPool newConnectionPool(HttpClient client) { - DISCONNECTED, CONNECTING, CONNECTED + return new MultiplexConnectionPool(this, client.getMaxConnectionsPerDestination(), this, + client.getMaxRequestsQueuedPerDestination()); } } diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/PoolingHttpDestination.java b/jetty-client/src/main/java/org/eclipse/jetty/client/PoolingHttpDestination.java index 7005e16fd6..b77805aeae 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/PoolingHttpDestination.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/PoolingHttpDestination.java @@ -18,229 +18,15 @@ package org.eclipse.jetty.client; -import java.io.IOException; -import java.util.Collections; - -import org.eclipse.jetty.client.api.Connection; -import org.eclipse.jetty.client.api.Request; -import org.eclipse.jetty.util.Callback; -import org.eclipse.jetty.util.annotation.ManagedAttribute; -import org.eclipse.jetty.util.annotation.ManagedObject; -import org.eclipse.jetty.util.component.ContainerLifeCycle; -import org.eclipse.jetty.util.thread.Sweeper; - -@ManagedObject -public abstract class PoolingHttpDestination<C extends Connection> extends HttpDestination implements Callback +public abstract class PoolingHttpDestination extends HttpDestination { - private DuplexConnectionPool connectionPool; - public PoolingHttpDestination(HttpClient client, Origin origin) { super(client, origin); - this.connectionPool = newConnectionPool(client); - addBean(connectionPool); - Sweeper sweeper = client.getBean(Sweeper.class); - if (sweeper != null) - sweeper.offer(connectionPool); } - @Override - protected void doStart() throws Exception - { - HttpClient client = getHttpClient(); - this.connectionPool = newConnectionPool(client); - addBean(connectionPool); - super.doStart(); - Sweeper sweeper = client.getBean(Sweeper.class); - if (sweeper != null) - sweeper.offer(connectionPool); - } - - @Override - protected void doStop() throws Exception - { - HttpClient client = getHttpClient(); - Sweeper sweeper = client.getBean(Sweeper.class); - if (sweeper != null) - sweeper.remove(connectionPool); - super.doStop(); - removeBean(connectionPool); - } - - protected DuplexConnectionPool newConnectionPool(HttpClient client) + protected ConnectionPool newConnectionPool(HttpClient client) { return new DuplexConnectionPool(this, client.getMaxConnectionsPerDestination(), this); } - - @ManagedAttribute(value = "The connection pool", readonly = true) - public DuplexConnectionPool getConnectionPool() - { - return connectionPool; - } - - @Override - public void succeeded() - { - send(); - } - - @Override - public void failed(final Throwable x) - { - abort(x); - } - - public void send() - { - if (getHttpExchanges().isEmpty()) - return; - process(); - } - - @SuppressWarnings("unchecked") - public C acquire() - { - return (C)connectionPool.acquire(); - } - - private void process() - { - C connection = acquire(); - if (connection != null) - process(connection); - } - - /** - * <p>Processes a new connection making it idle or active depending on whether requests are waiting to be sent.</p> - * <p>A new connection is created when a request needs to be executed; it is possible that the request that - * triggered the request creation is executed by another connection that was just released, so the new connection - * may become idle.</p> - * <p>If a request is waiting to be executed, it will be dequeued and executed by the new connection.</p> - * - * @param connection the new connection - */ - public void process(final C connection) - { - HttpClient client = getHttpClient(); - final HttpExchange exchange = getHttpExchanges().poll(); - if (LOG.isDebugEnabled()) - LOG.debug("Processing exchange {} on {} of {}", exchange, connection, this); - if (exchange == null) - { - if (!connectionPool.release(connection)) - connection.close(); - - if (!client.isRunning()) - { - if (LOG.isDebugEnabled()) - LOG.debug("{} is stopping", client); - connection.close(); - } - } - else - { - final Request request = exchange.getRequest(); - Throwable cause = request.getAbortCause(); - if (cause != null) - { - if (LOG.isDebugEnabled()) - LOG.debug("Aborted before processing {}: {}", exchange, cause); - // It may happen that the request is aborted before the exchange - // is created. Aborting the exchange a second time will result in - // a no-operation, so we just abort here to cover that edge case. - exchange.abort(cause); - } - else - { - send(connection, exchange); - } - } - } - - protected abstract void send(C connection, HttpExchange exchange); - - @Override - public void release(Connection c) - { - @SuppressWarnings("unchecked") - C connection = (C)c; - if (LOG.isDebugEnabled()) - LOG.debug("Released {}", connection); - HttpClient client = getHttpClient(); - if (client.isRunning()) - { - if (connectionPool.isActive(connection)) - { - if (connectionPool.release(connection)) - send(); - else - connection.close(); - } - else - { - if (LOG.isDebugEnabled()) - LOG.debug("Released explicit {}", connection); - } - } - else - { - if (LOG.isDebugEnabled()) - LOG.debug("{} is stopped", client); - connection.close(); - } - } - - public boolean remove(Connection connection) - { - return connectionPool.remove(connection); - } - - @Override - public void close(Connection connection) - { - super.close(connection); - - boolean removed = remove(connection); - - if (getHttpExchanges().isEmpty()) - { - if (getHttpClient().isRemoveIdleDestinations() && connectionPool.isEmpty()) - { - // There is a race condition between this thread removing the destination - // and another thread queueing a request to this same destination. - // If this destination is removed, but the request queued, a new connection - // will be opened, the exchange will be executed and eventually the connection - // will idle timeout and be closed. Meanwhile a new destination will be created - // in HttpClient and will be used for other requests. - getHttpClient().removeDestination(this); - } - } - 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 close() - { - super.close(); - connectionPool.close(); - } - - @Override - public void dump(Appendable out, String indent) throws IOException - { - super.dump(out, indent); - ContainerLifeCycle.dump(out, indent, Collections.singletonList(connectionPool)); - } - - @Override - public String toString() - { - return String.format("%s,pool=%s", super.toString(), connectionPool); - } } diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/Socks4Proxy.java b/jetty-client/src/main/java/org/eclipse/jetty/client/Socks4Proxy.java index f73932c111..246681e646 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/Socks4Proxy.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/Socks4Proxy.java @@ -27,7 +27,6 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; import org.eclipse.jetty.client.api.Connection; -import org.eclipse.jetty.http.HttpScheme; import org.eclipse.jetty.io.AbstractConnection; import org.eclipse.jetty.io.ClientConnectionFactory; import org.eclipse.jetty.io.EndPoint; @@ -196,7 +195,7 @@ public class Socks4Proxy extends ProxyConfiguration.Proxy HttpDestination destination = (HttpDestination)context.get(HttpClientTransport.HTTP_DESTINATION_CONTEXT_KEY); HttpClient client = destination.getHttpClient(); ClientConnectionFactory connectionFactory = this.connectionFactory; - if (HttpScheme.HTTPS.is(destination.getScheme()) || HttpScheme.WSS.is(destination.getScheme())) + if (destination.isSecure()) connectionFactory = new SslClientConnectionFactory(client.getSslContextFactory(), client.getByteBufferPool(), client.getExecutor(), connectionFactory); org.eclipse.jetty.io.Connection newConnection = connectionFactory.newConnection(getEndPoint(), context); getEndPoint().upgrade(newConnection); diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/ValidatingConnectionPool.java b/jetty-client/src/main/java/org/eclipse/jetty/client/ValidatingConnectionPool.java index 2235f75dab..a218d14e87 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/ValidatingConnectionPool.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/ValidatingConnectionPool.java @@ -56,7 +56,7 @@ import org.eclipse.jetty.util.thread.Scheduler; * tuning the idle timeout of the servers to be larger than * that of the client.</p> */ -public class ValidatingConnectionPool extends ConnectionPool +public class ValidatingConnectionPool extends DuplexConnectionPool { private static final Logger LOG = Log.getLogger(ValidatingConnectionPool.class); @@ -154,7 +154,7 @@ public class ValidatingConnectionPool extends ConnectionPool private class Holder implements Runnable { private final long timestamp = System.nanoTime(); - private final AtomicBoolean latch = new AtomicBoolean(); + private final AtomicBoolean done = new AtomicBoolean(); private final Connection connection; public Scheduler.Task task; @@ -166,30 +166,31 @@ public class ValidatingConnectionPool extends ConnectionPool @Override public void run() { - if (latch.compareAndSet(false, true)) + if (done.compareAndSet(false, true)) { - boolean idle; + boolean closed = isClosed(); lock(); try { - quarantine.remove(connection); - idle = offerIdle(connection); if (LOG.isDebugEnabled()) LOG.debug("Validated {}", connection); + quarantine.remove(connection); + if (!closed) + deactivate(connection); } finally { unlock(); } - if (idle(connection, idle)) - proceed(); + idle(connection, closed); + proceed(); } } public boolean cancel() { - if (latch.compareAndSet(false, true)) + if (done.compareAndSet(false, true)) { task.cancel(); return true; diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/http/HttpConnectionOverHTTP.java b/jetty-client/src/main/java/org/eclipse/jetty/client/http/HttpConnectionOverHTTP.java index 5b592f6ce1..5c60de2c66 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/http/HttpConnectionOverHTTP.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/http/HttpConnectionOverHTTP.java @@ -42,8 +42,8 @@ public class HttpConnectionOverHTTP extends AbstractConnection implements Connec private static final Logger LOG = Log.getLogger(HttpConnectionOverHTTP.class); private final AtomicBoolean closed = new AtomicBoolean(); - private final Promise<Connection> promise; private final AtomicInteger sweeps = new AtomicInteger(); + private final Promise<Connection> promise; private final Delegate delegate; private final HttpChannelOverHTTP channel; private long idleTimeout; @@ -89,14 +89,7 @@ public class HttpConnectionOverHTTP extends AbstractConnection implements Connec fillInterested(); promise.succeeded(this); } - - @Override - public void onClose() - { - softClose(); - super.onClose(); - } - + public boolean isClosed() { return closed.get(); diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/http/HttpDestinationOverHTTP.java b/jetty-client/src/main/java/org/eclipse/jetty/client/http/HttpDestinationOverHTTP.java index 304ba96d35..284ce08fe5 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/http/HttpDestinationOverHTTP.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/http/HttpDestinationOverHTTP.java @@ -22,8 +22,9 @@ import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.client.HttpExchange; import org.eclipse.jetty.client.Origin; import org.eclipse.jetty.client.PoolingHttpDestination; +import org.eclipse.jetty.client.api.Connection; -public class HttpDestinationOverHTTP extends PoolingHttpDestination<HttpConnectionOverHTTP> +public class HttpDestinationOverHTTP extends PoolingHttpDestination { public HttpDestinationOverHTTP(HttpClient client, Origin origin) { @@ -31,8 +32,8 @@ public class HttpDestinationOverHTTP extends PoolingHttpDestination<HttpConnecti } @Override - protected void send(HttpConnectionOverHTTP connection, HttpExchange exchange) + protected void send(Connection connection, HttpExchange exchange) { - connection.send(exchange); + ((HttpConnectionOverHTTP)connection).send(exchange); } } diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/http/HttpReceiverOverHTTP.java b/jetty-client/src/main/java/org/eclipse/jetty/client/http/HttpReceiverOverHTTP.java index 22d8c7e030..bf6c039d46 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/http/HttpReceiverOverHTTP.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/http/HttpReceiverOverHTTP.java @@ -92,8 +92,8 @@ public class HttpReceiverOverHTTP extends HttpReceiver implements HttpParser.Res { if (BufferUtil.hasContent(buffer)) { - ByteBuffer upgradeBuffer = buffer; - releaseBuffer(); // TODO: right place to do this? + ByteBuffer upgradeBuffer = ByteBuffer.allocate(buffer.remaining()); + upgradeBuffer.put(buffer); return upgradeBuffer; } return null; diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/util/MultiPartContentProvider.java b/jetty-client/src/main/java/org/eclipse/jetty/client/util/MultiPartContentProvider.java new file mode 100644 index 0000000000..dddff6a814 --- /dev/null +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/util/MultiPartContentProvider.java @@ -0,0 +1,388 @@ +// +// ======================================================================== +// Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.client.util; + +import java.io.ByteArrayOutputStream; +import java.io.Closeable; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.Random; + +import org.eclipse.jetty.client.AsyncContentProvider; +import org.eclipse.jetty.client.Synchronizable; +import org.eclipse.jetty.client.api.ContentProvider; +import org.eclipse.jetty.http.HttpField; +import org.eclipse.jetty.http.HttpFields; +import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.io.RuntimeIOException; +import org.eclipse.jetty.util.Callback; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; + +/** + * <p>A {@link ContentProvider} for form uploads with the {@code "multipart/form-data"} + * content type.</p> + * <p>Example usage:</p> + * <pre> + * MultiPartContentProvider multiPart = new MultiPartContentProvider(); + * multiPart.addFieldPart("field", new StringContentProvider("foo"), null); + * multiPart.addFilePart("icon", "img.png", new PathContentProvider(Paths.get("/tmp/img.png")), null); + * ContentResponse response = client.newRequest("localhost", connector.getLocalPort()) + * .method(HttpMethod.POST) + * .content(multiPart) + * .send(); + * </pre> + * <p>The above example would be the equivalent of submitting this form:</p> + * <pre> + * <form method="POST" enctype="multipart/form-data" accept-charset="UTF-8"> + * <input type="text" name="field" value="foo" /> + * <input type="file" name="icon" /> + * </form> + * </pre> + */ +public class MultiPartContentProvider extends AbstractTypedContentProvider implements AsyncContentProvider +{ + private static final Logger LOG = Log.getLogger(MultiPartContentProvider.class); + private static final byte[] COLON_SPACE_BYTES = new byte[]{':', ' '}; + private static final byte[] CR_LF_BYTES = new byte[]{'\r', '\n'}; + + private final List<Part> parts = new ArrayList<>(); + private final ByteBuffer firstBoundary; + private final ByteBuffer middleBoundary; + private final ByteBuffer onlyBoundary; + private final ByteBuffer lastBoundary; + private Listener listener; + private long length; + + public MultiPartContentProvider() + { + this(makeBoundary()); + } + + public MultiPartContentProvider(String boundary) + { + super("multipart/form-data; boundary=" + boundary); + String firstBoundaryLine = "--" + boundary + "\r\n"; + this.firstBoundary = ByteBuffer.wrap(firstBoundaryLine.getBytes(StandardCharsets.US_ASCII)); + String middleBoundaryLine = "\r\n" + firstBoundaryLine; + this.middleBoundary = ByteBuffer.wrap(middleBoundaryLine.getBytes(StandardCharsets.US_ASCII)); + String onlyBoundaryLine = "--" + boundary + "--\r\n"; + this.onlyBoundary = ByteBuffer.wrap(onlyBoundaryLine.getBytes(StandardCharsets.US_ASCII)); + String lastBoundaryLine = "\r\n" + onlyBoundaryLine; + this.lastBoundary = ByteBuffer.wrap(lastBoundaryLine.getBytes(StandardCharsets.US_ASCII)); + } + + private static String makeBoundary() + { + Random random = new Random(); + StringBuilder builder = new StringBuilder("JettyHttpClientBoundary"); + int length = builder.length(); + while (builder.length() < length + 16) + { + long rnd = random.nextLong(); + builder.append(Long.toString(rnd < 0 ? -rnd : rnd, 36)); + } + builder.setLength(length + 16); + return builder.toString(); + } + + /** + * <p>Adds a field part with the given {@code name} as field name, and the given + * {@code content} as part content.</p> + * <p>The {@code Content-Type} of this part will be obtained from:</p> + * <ul> + * <li>the {@code Content-Type} header in the {@code fields} parameter; otherwise</li> + * <li>the {@link org.eclipse.jetty.client.api.ContentProvider.Typed#getContentType()} method if the {@code content} parameter + * implements {@link org.eclipse.jetty.client.api.ContentProvider.Typed}; otherwise</li> + * <li>"text/plain"</li> + * </ul> + * + * @param name the part name + * @param content the part content + * @param fields the headers associated with this part + */ + public void addFieldPart(String name, ContentProvider content, HttpFields fields) + { + addPart(new Part(name, null, "text/plain", content, fields)); + } + + /** + * <p>Adds a file part with the given {@code name} as field name, the given + * {@code fileName} as file name, and the given {@code content} as part content.</p> + * <p>The {@code Content-Type} of this part will be obtained from:</p> + * <ul> + * <li>the {@code Content-Type} header in the {@code fields} parameter; otherwise</li> + * <li>the {@link org.eclipse.jetty.client.api.ContentProvider.Typed#getContentType()} method if the {@code content} parameter + * implements {@link org.eclipse.jetty.client.api.ContentProvider.Typed}; otherwise</li> + * <li>"application/octet-stream"</li> + * </ul> + * + * @param name the part name + * @param fileName the file name associated to this part + * @param content the part content + * @param fields the headers associated with this part + */ + public void addFilePart(String name, String fileName, ContentProvider content, HttpFields fields) + { + addPart(new Part(name, fileName, "application/octet-stream", content, fields)); + } + + private void addPart(Part part) + { + parts.add(part); + if (LOG.isDebugEnabled()) + LOG.debug("Added {}", part); + } + + @Override + public void setListener(Listener listener) + { + this.listener = listener; + + // Compute the length, if possible. + if (parts.isEmpty()) + { + length = onlyBoundary.remaining(); + } + else + { + for (int i = 0; i < parts.size(); ++i) + { + length += (i == 0) ? firstBoundary.remaining() : middleBoundary.remaining(); + Part part = parts.get(i); + long partLength = part.length; + length += partLength; + if (partLength < 0) + { + length = -1; + break; + } + } + if (length > 0) + length += lastBoundary.remaining(); + } + } + + @Override + public long getLength() + { + return length; + } + + @Override + public Iterator<ByteBuffer> iterator() + { + return new MultiPartIterator(); + } + + private static class Part + { + private final String name; + private final String fileName; + private final String contentType; + private final ContentProvider content; + private final HttpFields fields; + private final ByteBuffer headers; + private final long length; + + private Part(String name, String fileName, String contentType, ContentProvider content, HttpFields fields) + { + this.name = name; + this.fileName = fileName; + this.contentType = contentType; + this.content = content; + this.fields = fields; + this.headers = headers(); + this.length = content.getLength() < 0 ? -1 : headers.remaining() + content.getLength(); + } + + private ByteBuffer headers() + { + try + { + // Compute the Content-Disposition. + String contentDisposition = "Content-Disposition: form-data; name=\"" + name + "\""; + if (fileName != null) + contentDisposition += "; filename=\"" + fileName + "\""; + contentDisposition += "\r\n"; + + // Compute the Content-Type. + String contentType = fields == null ? null : fields.get(HttpHeader.CONTENT_TYPE); + if (contentType == null) + { + if (content instanceof Typed) + contentType = ((Typed)content).getContentType(); + else + contentType = this.contentType; + } + contentType = "Content-Type: " + contentType + "\r\n"; + + if (fields == null || fields.size() == 0) + { + String headers = contentDisposition; + headers += contentType; + headers += "\r\n"; + return ByteBuffer.wrap(headers.getBytes(StandardCharsets.UTF_8)); + } + + ByteArrayOutputStream buffer = new ByteArrayOutputStream((fields.size() + 1) * contentDisposition.length()); + buffer.write(contentDisposition.getBytes(StandardCharsets.UTF_8)); + buffer.write(contentType.getBytes(StandardCharsets.UTF_8)); + for (HttpField field : fields) + { + if (HttpHeader.CONTENT_TYPE.equals(field.getHeader())) + continue; + buffer.write(field.getName().getBytes(StandardCharsets.US_ASCII)); + buffer.write(COLON_SPACE_BYTES); + buffer.write(field.getValue().getBytes(StandardCharsets.UTF_8)); + buffer.write(CR_LF_BYTES); + } + buffer.write(CR_LF_BYTES); + return ByteBuffer.wrap(buffer.toByteArray()); + } + catch (IOException x) + { + throw new RuntimeIOException(x); + } + } + + @Override + public String toString() + { + return String.format("%s@%x[name=%s,fileName=%s,length=%d,headers=%s]", + getClass().getSimpleName(), + hashCode(), + name, + fileName, + content.getLength(), + fields); + } + } + + private class MultiPartIterator implements Iterator<ByteBuffer>, Synchronizable, Callback, Closeable + { + private Iterator<ByteBuffer> iterator; + private int index; + private State state = State.FIRST_BOUNDARY; + + @Override + public boolean hasNext() + { + return state != State.COMPLETE; + } + + @Override + public ByteBuffer next() + { + while (true) + { + switch (state) + { + case FIRST_BOUNDARY: + { + if (parts.isEmpty()) + { + state = State.COMPLETE; + return onlyBoundary.slice(); + } + else + { + state = State.HEADERS; + return firstBoundary.slice(); + } + } + case HEADERS: + { + Part part = parts.get(index); + ContentProvider content = part.content; + if (content instanceof AsyncContentProvider) + ((AsyncContentProvider)content).setListener(listener); + iterator = content.iterator(); + state = State.CONTENT; + return part.headers.slice(); + } + case CONTENT: + { + if (iterator.hasNext()) + return iterator.next(); + ++index; + if (index == parts.size()) + state = State.LAST_BOUNDARY; + else + state = State.MIDDLE_BOUNDARY; + break; + } + case MIDDLE_BOUNDARY: + { + state = State.HEADERS; + return middleBoundary.slice(); + } + case LAST_BOUNDARY: + { + state = State.COMPLETE; + return lastBoundary.slice(); + } + case COMPLETE: + { + throw new NoSuchElementException(); + } + } + } + } + + @Override + public Object getLock() + { + if (iterator instanceof Synchronizable) + return ((Synchronizable)iterator).getLock(); + return this; + } + + @Override + public void succeeded() + { + if (iterator instanceof Callback) + ((Callback)iterator).succeeded(); + } + + @Override + public void failed(Throwable x) + { + if (iterator instanceof Callback) + ((Callback)iterator).failed(x); + } + + @Override + public void close() throws IOException + { + if (iterator instanceof Closeable) + ((Closeable)iterator).close(); + } + } + + private enum State + { + FIRST_BOUNDARY, HEADERS, CONTENT, MIDDLE_BOUNDARY, LAST_BOUNDARY, COMPLETE + } +} diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/AbstractHttpClientServerTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/AbstractHttpClientServerTest.java index edb6c9ce71..9cea0fd213 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/AbstractHttpClientServerTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/AbstractHttpClientServerTest.java @@ -60,6 +60,12 @@ public abstract class AbstractHttpClientServerTest public void start(Handler handler) throws Exception { + startServer(handler); + startClient(); + } + + protected void startServer(Handler handler) throws Exception + { if (sslContextFactory != null) { sslContextFactory.setEndpointIdentificationAlgorithm(""); @@ -79,8 +85,6 @@ public abstract class AbstractHttpClientServerTest server.addConnector(connector); server.setHandler(handler); server.start(); - - startClient(); } protected void startClient() throws Exception diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientCustomProxyTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientCustomProxyTest.java index 55911a8ddc..dba109cc0a 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientCustomProxyTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientCustomProxyTest.java @@ -146,7 +146,7 @@ public class HttpClientCustomProxyTest } } - private class CAFEBABEConnection extends AbstractConnection + private class CAFEBABEConnection extends AbstractConnection implements Callback { private final ClientConnectionFactory connectionFactory; private final Map<String, Object> context; @@ -162,8 +162,19 @@ public class HttpClientCustomProxyTest public void onOpen() { super.onOpen(); + getEndPoint().write(this, ByteBuffer.wrap(CAFE_BABE)); + } + + @Override + public void succeeded() + { fillInterested(); - getEndPoint().write(Callback.NOOP, ByteBuffer.wrap(CAFE_BABE)); + } + + @Override + public void failed(Throwable x) + { + close(); } @Override @@ -206,7 +217,7 @@ public class HttpClientCustomProxyTest } } - private class CAFEBABEServerConnection extends AbstractConnection + private class CAFEBABEServerConnection extends AbstractConnection implements Callback { private final org.eclipse.jetty.server.ConnectionFactory connectionFactory; @@ -232,15 +243,25 @@ public class HttpClientCustomProxyTest int filled = getEndPoint().fill(buffer); Assert.assertEquals(4, filled); Assert.assertArrayEquals(CAFE_BABE, buffer.array()); - getEndPoint().write(Callback.NOOP, buffer); - - // We are good, upgrade the connection - getEndPoint().upgrade(connectionFactory.newConnection(connector, getEndPoint())); + getEndPoint().write(this, buffer); } catch (Throwable x) { close(); } } + + @Override + public void succeeded() + { + // We are good, upgrade the connection + getEndPoint().upgrade(connectionFactory.newConnection(connector, getEndPoint())); + } + + @Override + public void failed(Throwable x) + { + close(); + } } } diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientExplicitConnectionTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientExplicitConnectionTest.java index c54f88981e..dd35154613 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientExplicitConnectionTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientExplicitConnectionTest.java @@ -59,7 +59,7 @@ public class HttpClientExplicitConnectionTest extends AbstractHttpClientServerTe Assert.assertEquals(200, response.getStatus()); HttpDestinationOverHTTP httpDestination = (HttpDestinationOverHTTP)destination; - DuplexConnectionPool connectionPool = httpDestination.getConnectionPool(); + DuplexConnectionPool connectionPool = (DuplexConnectionPool)httpDestination.getConnectionPool(); Assert.assertTrue(connectionPool.getActiveConnections().isEmpty()); Assert.assertTrue(connectionPool.getIdleConnections().isEmpty()); } @@ -94,7 +94,7 @@ public class HttpClientExplicitConnectionTest extends AbstractHttpClientServerTe Assert.assertFalse(httpConnection.getEndPoint().isOpen()); HttpDestinationOverHTTP httpDestination = (HttpDestinationOverHTTP)destination; - DuplexConnectionPool connectionPool = httpDestination.getConnectionPool(); + DuplexConnectionPool connectionPool = (DuplexConnectionPool)httpDestination.getConnectionPool(); Assert.assertTrue(connectionPool.getActiveConnections().isEmpty()); Assert.assertTrue(connectionPool.getIdleConnections().isEmpty()); } diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientFailureTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientFailureTest.java index 0db34443c2..e7ab277f20 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientFailureTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientFailureTest.java @@ -25,9 +25,6 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import org.eclipse.jetty.client.api.Connection; -import org.eclipse.jetty.client.api.Request; -import org.eclipse.jetty.client.api.Response; -import org.eclipse.jetty.client.api.Result; import org.eclipse.jetty.client.http.HttpClientTransportOverHTTP; import org.eclipse.jetty.client.http.HttpConnectionOverHTTP; import org.eclipse.jetty.client.util.DeferredContentProvider; @@ -89,14 +86,7 @@ public class HttpClientFailureTest try { client.newRequest("localhost", connector.getLocalPort()) - .onRequestHeaders(new Request.HeadersListener() - { - @Override - public void onHeaders(Request request) - { - connectionRef.get().getEndPoint().close(); - } - }) + .onRequestHeaders(request -> connectionRef.get().getEndPoint().close()) .timeout(5, TimeUnit.SECONDS) .send(); Assert.fail(); @@ -106,7 +96,7 @@ public class HttpClientFailureTest // Expected. } - DuplexConnectionPool connectionPool = connectionRef.get().getHttpDestination().getConnectionPool(); + DuplexConnectionPool connectionPool = (DuplexConnectionPool)connectionRef.get().getHttpDestination().getConnectionPool(); Assert.assertEquals(0, connectionPool.getConnectionCount()); Assert.assertEquals(0, connectionPool.getActiveConnections().size()); Assert.assertEquals(0, connectionPool.getIdleConnections().size()); @@ -134,25 +124,17 @@ public class HttpClientFailureTest final CountDownLatch completeLatch = new CountDownLatch(1); DeferredContentProvider content = new DeferredContentProvider(); client.newRequest("localhost", connector.getLocalPort()) - .onRequestCommit(new Request.CommitListener() + .onRequestCommit(request -> { - @Override - public void onCommit(Request request) - { - connectionRef.get().getEndPoint().close(); - commitLatch.countDown(); - } + connectionRef.get().getEndPoint().close(); + commitLatch.countDown(); }) .content(content) .idleTimeout(2, TimeUnit.SECONDS) - .send(new Response.CompleteListener() + .send(result -> { - @Override - public void onComplete(Result result) - { - if (result.isFailed()) - completeLatch.countDown(); - } + if (result.isFailed()) + completeLatch.countDown(); }); Assert.assertTrue(commitLatch.await(5, TimeUnit.SECONDS)); @@ -170,7 +152,7 @@ public class HttpClientFailureTest Assert.assertTrue(contentLatch.await(5, TimeUnit.SECONDS)); Assert.assertTrue(completeLatch.await(5, TimeUnit.SECONDS)); - DuplexConnectionPool connectionPool = connectionRef.get().getHttpDestination().getConnectionPool(); + DuplexConnectionPool connectionPool = (DuplexConnectionPool)connectionRef.get().getHttpDestination().getConnectionPool(); Assert.assertEquals(0, connectionPool.getConnectionCount()); Assert.assertEquals(0, connectionPool.getActiveConnections().size()); Assert.assertEquals(0, connectionPool.getIdleConnections().size()); diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientTest.java index bff369f79d..404a3e8e3f 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientTest.java @@ -47,6 +47,7 @@ import java.util.concurrent.Exchanger; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; @@ -63,6 +64,7 @@ import org.eclipse.jetty.client.api.Destination; import org.eclipse.jetty.client.api.Request; import org.eclipse.jetty.client.api.Response; import org.eclipse.jetty.client.api.Result; +import org.eclipse.jetty.client.http.HttpClientTransportOverHTTP; import org.eclipse.jetty.client.http.HttpConnectionOverHTTP; import org.eclipse.jetty.client.http.HttpDestinationOverHTTP; import org.eclipse.jetty.client.util.BufferingResponseListener; @@ -75,6 +77,7 @@ import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpHeaderValue; import org.eclipse.jetty.http.HttpMethod; import org.eclipse.jetty.http.HttpVersion; +import org.eclipse.jetty.io.EndPoint; import org.eclipse.jetty.server.handler.AbstractHandler; import org.eclipse.jetty.toolchain.test.TestingDir; import org.eclipse.jetty.toolchain.test.annotation.Slow; @@ -111,7 +114,7 @@ public class HttpClientTest extends AbstractHttpClientServerTest Assert.assertEquals(200, response.getStatus()); HttpDestinationOverHTTP destination = (HttpDestinationOverHTTP)client.getDestination(scheme, host, port); - DuplexConnectionPool connectionPool = destination.getConnectionPool(); + DuplexConnectionPool connectionPool = (DuplexConnectionPool)destination.getConnectionPool(); long start = System.nanoTime(); HttpConnectionOverHTTP connection = null; @@ -367,16 +370,12 @@ public class HttpClientTest extends AbstractHttpClientServerTest final byte[] content = {0, 1, 2, 3}; ContentResponse response = client.POST(scheme + "://localhost:" + connector.getLocalPort()) - .onRequestContent(new Request.ContentListener() + .onRequestContent((request, buffer) -> { - @Override - public void onContent(Request request, ByteBuffer buffer) - { - byte[] bytes = new byte[buffer.remaining()]; - buffer.get(bytes); - if (!Arrays.equals(content, bytes)) - request.abort(new Exception()); - } + byte[] bytes = new byte[buffer.remaining()]; + buffer.get(bytes); + if (!Arrays.equals(content, bytes)) + request.abort(new Exception()); }) .content(new BytesContentProvider(content)) .timeout(5, TimeUnit.SECONDS) @@ -401,16 +400,12 @@ public class HttpClientTest extends AbstractHttpClientServerTest final AtomicInteger progress = new AtomicInteger(); ContentResponse response = client.POST(scheme + "://localhost:" + connector.getLocalPort()) - .onRequestContent(new Request.ContentListener() + .onRequestContent((request, buffer) -> { - @Override - public void onContent(Request request, ByteBuffer buffer) - { - byte[] bytes = new byte[buffer.remaining()]; - Assert.assertEquals(1, bytes.length); - buffer.get(bytes); - Assert.assertEquals(bytes[0], progress.getAndIncrement()); - } + byte[] bytes = new byte[buffer.remaining()]; + Assert.assertEquals(1, bytes.length); + buffer.get(bytes); + Assert.assertEquals(bytes[0], progress.getAndIncrement()); }) .content(new BytesContentProvider(new byte[]{0}, new byte[]{1}, new byte[]{2}, new byte[]{3}, new byte[]{4})) .timeout(5, TimeUnit.SECONDS) @@ -432,19 +427,15 @@ public class HttpClientTest extends AbstractHttpClientServerTest final CountDownLatch successLatch = new CountDownLatch(2); client.newRequest("localhost", connector.getLocalPort()) .scheme(scheme) - .onRequestBegin(new Request.BeginListener() + .onRequestBegin(request -> { - @Override - public void onBegin(Request request) + try { - try - { - latch.await(); - } - catch (InterruptedException x) - { - x.printStackTrace(); - } + latch.await(); + } + catch (InterruptedException x) + { + x.printStackTrace(); } }) .send(new Response.Listener.Adapter() @@ -459,14 +450,7 @@ public class HttpClientTest extends AbstractHttpClientServerTest client.newRequest("localhost", connector.getLocalPort()) .scheme(scheme) - .onRequestQueued(new Request.QueuedListener() - { - @Override - public void onQueued(Request request) - { - latch.countDown(); - } - }) + .onRequestQueued(request -> latch.countDown()) .send(new Response.Listener.Adapter() { @Override @@ -514,27 +498,16 @@ public class HttpClientTest extends AbstractHttpClientServerTest latch.countDown(); } }) - .onResponseFailure(new Response.FailureListener() - { - @Override - public void onFailure(Response response, Throwable failure) - { - latch.countDown(); - } - }) + .onResponseFailure((response, failure) -> latch.countDown()) .send(null); client.newRequest("localhost", connector.getLocalPort()) .scheme(scheme) .path("/two") - .onResponseSuccess(new Response.SuccessListener() + .onResponseSuccess(response -> { - @Override - public void onSuccess(Response response) - { - Assert.assertEquals(200, response.getStatus()); - latch.countDown(); - } + Assert.assertEquals(200, response.getStatus()); + latch.countDown(); }) .send(null); @@ -564,14 +537,10 @@ public class HttpClientTest extends AbstractHttpClientServerTest client.newRequest("localhost", connector.getLocalPort()) .scheme(scheme) .file(file) - .onRequestSuccess(new Request.SuccessListener() + .onRequestSuccess(request -> { - @Override - public void onSuccess(Request request) - { - requestTime.set(System.nanoTime()); - latch.countDown(); - } + requestTime.set(System.nanoTime()); + latch.countDown(); }) .send(new Response.Listener.Adapter() { @@ -674,14 +643,11 @@ public class HttpClientTest extends AbstractHttpClientServerTest final int port = connector.getLocalPort(); client.newRequest(host, port) .scheme(scheme) - .onRequestBegin(new Request.BeginListener() + .onRequestBegin(request -> { - @Override - public void onBegin(Request request) - { - HttpDestinationOverHTTP destination = (HttpDestinationOverHTTP)client.getDestination(scheme, host, port); - destination.getConnectionPool().getActiveConnections().peek().close(); - } + HttpDestinationOverHTTP destination = (HttpDestinationOverHTTP)client.getDestination(scheme, host, port); + DuplexConnectionPool connectionPool = (DuplexConnectionPool)destination.getConnectionPool(); + connectionPool.getActiveConnections().iterator().next().close(); }) .send(new Response.Listener.Adapter() { @@ -773,14 +739,7 @@ public class HttpClientTest extends AbstractHttpClientServerTest ContentResponse response = client.newRequest("localhost", connector.getLocalPort()) .scheme(scheme) - .onResponseHeader(new Response.HeaderListener() - { - @Override - public boolean onHeader(Response response, HttpField field) - { - return !field.getName().equals(headerName); - } - }) + .onResponseHeader((response1, field) -> !field.getName().equals(headerName)) .timeout(5, TimeUnit.SECONDS) .send(); @@ -864,16 +823,12 @@ public class HttpClientTest extends AbstractHttpClientServerTest final CountDownLatch latch = new CountDownLatch(1); client.newRequest("idontexist", 80) - .send(new Response.CompleteListener() + .send(result -> { - @Override - public void onComplete(Result result) - { - Assert.assertTrue(result.isFailed()); - Throwable failure = result.getFailure(); - Assert.assertTrue(failure instanceof UnknownHostException); - latch.countDown(); - } + Assert.assertTrue(result.isFailed()); + Throwable failure = result.getFailure(); + Assert.assertTrue(failure instanceof UnknownHostException); + latch.countDown(); }); Assert.assertTrue(latch.await(10, TimeUnit.SECONDS)); } @@ -1323,14 +1278,10 @@ public class HttpClientTest extends AbstractHttpClientServerTest final CountDownLatch completeLatch = new CountDownLatch(1); client.newRequest("localhost", connector.getLocalPort()) .scheme(scheme) - .send(new Response.CompleteListener() + .send(result -> { - @Override - public void onComplete(Result result) - { - if (result.isFailed()) - completeLatch.countDown(); - } + if (result.isFailed()) + completeLatch.countDown(); }); Assert.assertTrue(latch.await(5, TimeUnit.SECONDS)); @@ -1486,6 +1437,54 @@ public class HttpClientTest extends AbstractHttpClientServerTest } @Test + public void testRequestSentOnlyAfterConnectionOpen() throws Exception + { + startServer(new AbstractHandler() + { + @Override + public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException + { + baseRequest.setHandled(true); + } + }); + + final AtomicBoolean open = new AtomicBoolean(); + client = new HttpClient(new HttpClientTransportOverHTTP() + { + @Override + protected HttpConnectionOverHTTP newHttpConnection(EndPoint endPoint, HttpDestination destination, Promise<Connection> promise) + { + return new HttpConnectionOverHTTP(endPoint, destination, promise) + { + @Override + public void onOpen() + { + open.set(true); + super.onOpen(); + } + }; + } + }, sslContextFactory); + client.start(); + + final CountDownLatch latch = new CountDownLatch(2); + client.newRequest("localhost", connector.getLocalPort()) + .scheme(scheme) + .onRequestBegin(request -> + { + Assert.assertTrue(open.get()); + latch.countDown(); + }) + .send(result -> + { + if (result.isSucceeded()) + latch.countDown(); + }); + + Assert.assertTrue(latch.await(5, TimeUnit.SECONDS)); + } + + @Test public void testCONNECTWithHTTP10() throws Exception { try (ServerSocket server = new ServerSocket(0)) diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientUploadDuringServerShutdown.java b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientUploadDuringServerShutdown.java index b899a1f211..bc80ff7aae 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientUploadDuringServerShutdown.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientUploadDuringServerShutdown.java @@ -31,8 +31,6 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.eclipse.jetty.client.api.Connection; -import org.eclipse.jetty.client.api.Response; -import org.eclipse.jetty.client.api.Result; import org.eclipse.jetty.client.http.HttpChannelOverHTTP; import org.eclipse.jetty.client.http.HttpClientTransportOverHTTP; import org.eclipse.jetty.client.http.HttpConnectionOverHTTP; @@ -121,14 +119,7 @@ public class HttpClientUploadDuringServerShutdown int length = 16 * 1024 * 1024 + random.nextInt(16 * 1024 * 1024); client.newRequest("localhost", 8888) .content(new BytesContentProvider(new byte[length])) - .send(new Response.CompleteListener() - { - @Override - public void onComplete(Result result) - { - latch.countDown(); - } - }); + .send(result -> latch.countDown()); long sleep = 1 + random.nextInt(10); TimeUnit.MILLISECONDS.sleep(sleep); } @@ -244,35 +235,24 @@ public class HttpClientUploadDuringServerShutdown final CountDownLatch completeLatch = new CountDownLatch(1); client.newRequest("localhost", connector.getLocalPort()) .timeout(10, TimeUnit.SECONDS) - .onRequestBegin(new org.eclipse.jetty.client.api.Request.BeginListener() + .onRequestBegin(request -> { - @Override - public void onBegin(org.eclipse.jetty.client.api.Request request) + try { - try - { - beginLatch.countDown(); - completeLatch.await(5, TimeUnit.SECONDS); - } - catch (InterruptedException x) - { - x.printStackTrace(); - } + beginLatch.countDown(); + completeLatch.await(5, TimeUnit.SECONDS); } - }) - .send(new Response.CompleteListener() - { - @Override - public void onComplete(Result result) + catch (InterruptedException x) { - completeLatch.countDown(); + x.printStackTrace(); } - }); + }) + .send(result -> completeLatch.countDown()); Assert.assertTrue(completeLatch.await(5, TimeUnit.SECONDS)); HttpDestinationOverHTTP destination = (HttpDestinationOverHTTP)client.getDestination("http", "localhost", connector.getLocalPort()); - DuplexConnectionPool pool = destination.getConnectionPool(); + DuplexConnectionPool pool = (DuplexConnectionPool)destination.getConnectionPool(); Assert.assertEquals(0, pool.getConnectionCount()); Assert.assertEquals(0, pool.getIdleConnections().size()); Assert.assertEquals(0, pool.getActiveConnections().size()); diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpConnectionLifecycleTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpConnectionLifecycleTest.java index f8cbb04c5a..684ff02dce 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpConnectionLifecycleTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpConnectionLifecycleTest.java @@ -21,6 +21,7 @@ package org.eclipse.jetty.client; import java.io.IOException; import java.nio.ByteBuffer; import java.util.Arrays; +import java.util.Collection; import java.util.Queue; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -69,35 +70,24 @@ public class HttpConnectionLifecycleTest extends AbstractHttpClientServerTest String host = "localhost"; int port = connector.getLocalPort(); HttpDestinationOverHTTP destination = (HttpDestinationOverHTTP)client.getDestination(scheme, host, port); - DuplexConnectionPool connectionPool = destination.getConnectionPool(); + DuplexConnectionPool connectionPool = (DuplexConnectionPool)destination.getConnectionPool(); - final Queue<Connection> idleConnections = connectionPool.getIdleConnections(); + final Collection<Connection> idleConnections = connectionPool.getIdleConnections(); Assert.assertEquals(0, idleConnections.size()); - final Queue<Connection> activeConnections = connectionPool.getActiveConnections(); + final Collection<Connection> activeConnections = connectionPool.getActiveConnections(); Assert.assertEquals(0, activeConnections.size()); final CountDownLatch headersLatch = new CountDownLatch(1); final CountDownLatch successLatch = new CountDownLatch(3); client.newRequest(host, port) .scheme(scheme) - .onRequestSuccess(new Request.SuccessListener() - { - @Override - public void onSuccess(Request request) - { - successLatch.countDown(); - } - }) - .onResponseHeaders(new Response.HeadersListener() + .onRequestSuccess(request -> successLatch.countDown()) + .onResponseHeaders(response -> { - @Override - public void onHeaders(Response response) - { - Assert.assertEquals(0, idleConnections.size()); - Assert.assertEquals(1, activeConnections.size()); - headersLatch.countDown(); - } + Assert.assertEquals(0, idleConnections.size()); + Assert.assertEquals(1, activeConnections.size()); + headersLatch.countDown(); }) .send(new Response.Listener.Adapter() { @@ -130,12 +120,12 @@ public class HttpConnectionLifecycleTest extends AbstractHttpClientServerTest String host = "localhost"; int port = connector.getLocalPort(); HttpDestinationOverHTTP destination = (HttpDestinationOverHTTP)client.getDestination(scheme, host, port); - DuplexConnectionPool connectionPool = destination.getConnectionPool(); + DuplexConnectionPool connectionPool = (DuplexConnectionPool)destination.getConnectionPool(); - final Queue<Connection> idleConnections = connectionPool.getIdleConnections(); + final Collection<Connection> idleConnections = connectionPool.getIdleConnections(); Assert.assertEquals(0, idleConnections.size()); - final Queue<Connection> activeConnections = connectionPool.getActiveConnections(); + final Collection<Connection> activeConnections = connectionPool.getActiveConnections(); Assert.assertEquals(0, activeConnections.size()); final CountDownLatch beginLatch = new CountDownLatch(1); @@ -145,7 +135,7 @@ public class HttpConnectionLifecycleTest extends AbstractHttpClientServerTest @Override public void onBegin(Request request) { - activeConnections.peek().close(); + activeConnections.iterator().next().close(); beginLatch.countDown(); } @@ -181,12 +171,12 @@ public class HttpConnectionLifecycleTest extends AbstractHttpClientServerTest String host = "localhost"; int port = connector.getLocalPort(); HttpDestinationOverHTTP destination = (HttpDestinationOverHTTP)client.getDestination(scheme, host, port); - DuplexConnectionPool connectionPool = destination.getConnectionPool(); + DuplexConnectionPool connectionPool = (DuplexConnectionPool)destination.getConnectionPool(); final Queue<Connection> idleConnections = connectionPool.getIdleConnections(); Assert.assertEquals(0, idleConnections.size()); - final Queue<Connection> activeConnections = connectionPool.getActiveConnections(); + final Collection<Connection> activeConnections = connectionPool.getActiveConnections(); Assert.assertEquals(0, activeConnections.size()); final CountDownLatch successLatch = new CountDownLatch(3); @@ -241,12 +231,12 @@ public class HttpConnectionLifecycleTest extends AbstractHttpClientServerTest String host = "localhost"; int port = connector.getLocalPort(); HttpDestinationOverHTTP destination = (HttpDestinationOverHTTP)client.getDestination(scheme, host, port); - DuplexConnectionPool connectionPool = destination.getConnectionPool(); + DuplexConnectionPool connectionPool = (DuplexConnectionPool)destination.getConnectionPool(); - final Queue<Connection> idleConnections = connectionPool.getIdleConnections(); + final Collection<Connection> idleConnections = connectionPool.getIdleConnections(); Assert.assertEquals(0, idleConnections.size()); - final Queue<Connection> activeConnections = connectionPool.getActiveConnections(); + final Collection<Connection> activeConnections = connectionPool.getActiveConnections(); Assert.assertEquals(0, activeConnections.size()); final long delay = 1000; @@ -314,12 +304,12 @@ public class HttpConnectionLifecycleTest extends AbstractHttpClientServerTest String host = "localhost"; int port = connector.getLocalPort(); HttpDestinationOverHTTP destination = (HttpDestinationOverHTTP)client.getDestination(scheme, host, port); - DuplexConnectionPool connectionPool = destination.getConnectionPool(); + DuplexConnectionPool connectionPool = (DuplexConnectionPool)destination.getConnectionPool(); - final Queue<Connection> idleConnections = connectionPool.getIdleConnections(); + final Collection<Connection> idleConnections = connectionPool.getIdleConnections(); Assert.assertEquals(0, idleConnections.size()); - final Queue<Connection> activeConnections = connectionPool.getActiveConnections(); + final Collection<Connection> activeConnections = connectionPool.getActiveConnections(); Assert.assertEquals(0, activeConnections.size()); server.stop(); @@ -327,22 +317,11 @@ public class HttpConnectionLifecycleTest extends AbstractHttpClientServerTest final CountDownLatch failureLatch = new CountDownLatch(2); client.newRequest(host, port) .scheme(scheme) - .onRequestFailure(new Request.FailureListener() + .onRequestFailure((request, failure) -> failureLatch.countDown()) + .send(result -> { - @Override - public void onFailure(Request request, Throwable failure) - { - failureLatch.countDown(); - } - }) - .send(new Response.Listener.Adapter() - { - @Override - public void onComplete(Result result) - { - Assert.assertTrue(result.isFailed()); - failureLatch.countDown(); - } + Assert.assertTrue(result.isFailed()); + failureLatch.countDown(); }); Assert.assertTrue(failureLatch.await(5, TimeUnit.SECONDS)); @@ -367,12 +346,12 @@ public class HttpConnectionLifecycleTest extends AbstractHttpClientServerTest String host = "localhost"; int port = connector.getLocalPort(); HttpDestinationOverHTTP destination = (HttpDestinationOverHTTP)client.getDestination(scheme, host, port); - DuplexConnectionPool connectionPool = destination.getConnectionPool(); + DuplexConnectionPool connectionPool = (DuplexConnectionPool)destination.getConnectionPool(); - final Queue<Connection> idleConnections = connectionPool.getIdleConnections(); + final Collection<Connection> idleConnections = connectionPool.getIdleConnections(); Assert.assertEquals(0, idleConnections.size()); - final Queue<Connection> activeConnections = connectionPool.getActiveConnections(); + final Collection<Connection> activeConnections = connectionPool.getActiveConnections(); Assert.assertEquals(0, activeConnections.size()); final CountDownLatch latch = new CountDownLatch(1); @@ -417,12 +396,12 @@ public class HttpConnectionLifecycleTest extends AbstractHttpClientServerTest String host = "localhost"; int port = connector.getLocalPort(); HttpDestinationOverHTTP destination = (HttpDestinationOverHTTP)client.getDestination(scheme, host, port); - DuplexConnectionPool connectionPool = destination.getConnectionPool(); + DuplexConnectionPool connectionPool = (DuplexConnectionPool)destination.getConnectionPool(); - final Queue<Connection> idleConnections = connectionPool.getIdleConnections(); + final Collection<Connection> idleConnections = connectionPool.getIdleConnections(); Assert.assertEquals(0, idleConnections.size()); - final Queue<Connection> activeConnections = connectionPool.getActiveConnections(); + final Collection<Connection> activeConnections = connectionPool.getActiveConnections(); Assert.assertEquals(0, activeConnections.size()); Log.getLogger(HttpConnection.class).info("Expecting java.lang.IllegalStateException: HttpParser{s=CLOSED,..."); @@ -467,12 +446,12 @@ public class HttpConnectionLifecycleTest extends AbstractHttpClientServerTest String host = "localhost"; int port = connector.getLocalPort(); HttpDestinationOverHTTP destination = (HttpDestinationOverHTTP)client.getDestination(scheme, host, port); - DuplexConnectionPool connectionPool = destination.getConnectionPool(); + DuplexConnectionPool connectionPool = (DuplexConnectionPool)destination.getConnectionPool(); - final Queue<Connection> idleConnections = connectionPool.getIdleConnections(); + final Collection<Connection> idleConnections = connectionPool.getIdleConnections(); Assert.assertEquals(0, idleConnections.size()); - final Queue<Connection> activeConnections = connectionPool.getActiveConnections(); + final Collection<Connection> activeConnections = connectionPool.getActiveConnections(); Assert.assertEquals(0, activeConnections.size()); ContentResponse response = client.newRequest(host, port) @@ -499,25 +478,21 @@ public class HttpConnectionLifecycleTest extends AbstractHttpClientServerTest String host = "localhost"; int port = connector.getLocalPort(); HttpDestinationOverHTTP destination = (HttpDestinationOverHTTP)client.getDestination(scheme, host, port); - DuplexConnectionPool connectionPool = destination.getConnectionPool(); + DuplexConnectionPool connectionPool = (DuplexConnectionPool)destination.getConnectionPool(); - final Queue<Connection> idleConnections = connectionPool.getIdleConnections(); + final Collection<Connection> idleConnections = connectionPool.getIdleConnections(); Assert.assertEquals(0, idleConnections.size()); - final Queue<Connection> activeConnections = connectionPool.getActiveConnections(); + final Collection<Connection> activeConnections = connectionPool.getActiveConnections(); Assert.assertEquals(0, activeConnections.size()); client.setStrictEventOrdering(false); ContentResponse response = client.newRequest(host, port) .scheme(scheme) - .onResponseBegin(new Response.BeginListener() + .onResponseBegin(response1 -> { - @Override - public void onBegin(Response response) - { - // Simulate a HTTP 1.0 response has been received. - ((HttpResponse)response).version(HttpVersion.HTTP_1_0); - } + // Simulate a HTTP 1.0 response has been received. + ((HttpResponse)response1).version(HttpVersion.HTTP_1_0); }) .send(); diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpRequestAbortTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpRequestAbortTest.java index 45b78e7d18..4b33e574e1 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpRequestAbortTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpRequestAbortTest.java @@ -25,12 +25,12 @@ import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; + import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.eclipse.jetty.client.api.Request; -import org.eclipse.jetty.client.api.Response; import org.eclipse.jetty.client.api.Result; import org.eclipse.jetty.client.http.HttpDestinationOverHTTP; import org.eclipse.jetty.client.util.ByteBufferContentProvider; @@ -88,7 +88,7 @@ public class HttpRequestAbortTest extends AbstractHttpClientServerTest } HttpDestinationOverHTTP destination = (HttpDestinationOverHTTP)client.getDestination(scheme, "localhost", connector.getLocalPort()); - DuplexConnectionPool connectionPool = destination.getConnectionPool(); + DuplexConnectionPool connectionPool = (DuplexConnectionPool)destination.getConnectionPool(); Assert.assertEquals(0, connectionPool.getConnectionCount()); Assert.assertEquals(0, connectionPool.getActiveConnections().size()); Assert.assertEquals(0, connectionPool.getIdleConnections().size()); @@ -135,7 +135,7 @@ public class HttpRequestAbortTest extends AbstractHttpClientServerTest } HttpDestinationOverHTTP destination = (HttpDestinationOverHTTP)client.getDestination(scheme, "localhost", connector.getLocalPort()); - DuplexConnectionPool connectionPool = destination.getConnectionPool(); + DuplexConnectionPool connectionPool = (DuplexConnectionPool)destination.getConnectionPool(); Assert.assertEquals(0, connectionPool.getConnectionCount()); Assert.assertEquals(0, connectionPool.getActiveConnections().size()); Assert.assertEquals(0, connectionPool.getIdleConnections().size()); @@ -182,7 +182,7 @@ public class HttpRequestAbortTest extends AbstractHttpClientServerTest } HttpDestinationOverHTTP destination = (HttpDestinationOverHTTP)client.getDestination(scheme, "localhost", connector.getLocalPort()); - DuplexConnectionPool connectionPool = destination.getConnectionPool(); + DuplexConnectionPool connectionPool = (DuplexConnectionPool)destination.getConnectionPool(); Assert.assertEquals(0, connectionPool.getConnectionCount()); Assert.assertEquals(0, connectionPool.getActiveConnections().size()); Assert.assertEquals(0, connectionPool.getIdleConnections().size()); @@ -204,14 +204,10 @@ public class HttpRequestAbortTest extends AbstractHttpClientServerTest { client.newRequest("localhost", connector.getLocalPort()) .scheme(scheme) - .onRequestCommit(new Request.CommitListener() + .onRequestCommit(request -> { - @Override - public void onCommit(Request request) - { - aborted.set(request.abort(cause)); - latch.countDown(); - } + aborted.set(request.abort(cause)); + latch.countDown(); }) .timeout(5, TimeUnit.SECONDS) .send(); @@ -225,7 +221,7 @@ public class HttpRequestAbortTest extends AbstractHttpClientServerTest } HttpDestinationOverHTTP destination = (HttpDestinationOverHTTP)client.getDestination(scheme, "localhost", connector.getLocalPort()); - DuplexConnectionPool connectionPool = destination.getConnectionPool(); + DuplexConnectionPool connectionPool = (DuplexConnectionPool)destination.getConnectionPool(); Assert.assertEquals(0, connectionPool.getConnectionCount()); Assert.assertEquals(0, connectionPool.getActiveConnections().size()); Assert.assertEquals(0, connectionPool.getIdleConnections().size()); @@ -260,14 +256,10 @@ public class HttpRequestAbortTest extends AbstractHttpClientServerTest { client.newRequest("localhost", connector.getLocalPort()) .scheme(scheme) - .onRequestCommit(new Request.CommitListener() + .onRequestCommit(request -> { - @Override - public void onCommit(Request request) - { - aborted.set(request.abort(cause)); - latch.countDown(); - } + aborted.set(request.abort(cause)); + latch.countDown(); }) .content(new ByteBufferContentProvider(ByteBuffer.wrap(new byte[]{0}), ByteBuffer.wrap(new byte[]{1})) { @@ -289,7 +281,7 @@ public class HttpRequestAbortTest extends AbstractHttpClientServerTest } HttpDestinationOverHTTP destination = (HttpDestinationOverHTTP)client.getDestination(scheme, "localhost", connector.getLocalPort()); - DuplexConnectionPool connectionPool = destination.getConnectionPool(); + DuplexConnectionPool connectionPool = (DuplexConnectionPool)destination.getConnectionPool(); Assert.assertEquals(0, connectionPool.getConnectionCount()); Assert.assertEquals(0, connectionPool.getActiveConnections().size()); Assert.assertEquals(0, connectionPool.getIdleConnections().size()); @@ -315,14 +307,10 @@ public class HttpRequestAbortTest extends AbstractHttpClientServerTest { client.newRequest("localhost", connector.getLocalPort()) .scheme(scheme) - .onRequestContent(new Request.ContentListener() + .onRequestContent((request, content) -> { - @Override - public void onContent(Request request, ByteBuffer content) - { - aborted.set(request.abort(cause)); - latch.countDown(); - } + aborted.set(request.abort(cause)); + latch.countDown(); }) .content(new ByteBufferContentProvider(ByteBuffer.wrap(new byte[]{0}), ByteBuffer.wrap(new byte[]{1})) { @@ -344,7 +332,7 @@ public class HttpRequestAbortTest extends AbstractHttpClientServerTest } HttpDestinationOverHTTP destination = (HttpDestinationOverHTTP)client.getDestination(scheme, "localhost", connector.getLocalPort()); - DuplexConnectionPool connectionPool = destination.getConnectionPool(); + DuplexConnectionPool connectionPool = (DuplexConnectionPool)destination.getConnectionPool(); Assert.assertEquals(0, connectionPool.getConnectionCount()); Assert.assertEquals(0, connectionPool.getActiveConnections().size()); Assert.assertEquals(0, connectionPool.getIdleConnections().size()); @@ -454,7 +442,7 @@ public class HttpRequestAbortTest extends AbstractHttpClientServerTest } HttpDestinationOverHTTP destination = (HttpDestinationOverHTTP)client.getDestination(scheme, "localhost", connector.getLocalPort()); - DuplexConnectionPool connectionPool = destination.getConnectionPool(); + DuplexConnectionPool connectionPool = (DuplexConnectionPool)destination.getConnectionPool(); Assert.assertEquals(0, connectionPool.getConnectionCount()); Assert.assertEquals(0, connectionPool.getActiveConnections().size()); Assert.assertEquals(0, connectionPool.getIdleConnections().size()); @@ -486,15 +474,11 @@ public class HttpRequestAbortTest extends AbstractHttpClientServerTest Request request = client.newRequest("localhost", connector.getLocalPort()) .scheme(scheme) .timeout(3 * delay, TimeUnit.MILLISECONDS); - request.send(new Response.CompleteListener() + request.send(result -> { - @Override - public void onComplete(Result result) - { - Assert.assertTrue(result.isFailed()); - Assert.assertSame(cause, result.getFailure()); - latch.countDown(); - } + Assert.assertTrue(result.isFailed()); + Assert.assertSame(cause, result.getFailure()); + latch.countDown(); }); TimeUnit.MILLISECONDS.sleep(delay); diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/ServerConnectionCloseTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/ServerConnectionCloseTest.java index 23d72de601..47a7760e1a 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/ServerConnectionCloseTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/ServerConnectionCloseTest.java @@ -151,7 +151,7 @@ public class ServerConnectionCloseTest // Connection should have been removed from pool. HttpDestinationOverHTTP destination = (HttpDestinationOverHTTP)client.getDestination("http", "localhost", port); - DuplexConnectionPool connectionPool = destination.getConnectionPool(); + DuplexConnectionPool connectionPool = (DuplexConnectionPool)destination.getConnectionPool(); Assert.assertEquals(0, connectionPool.getConnectionCount()); Assert.assertEquals(0, connectionPool.getIdleConnectionCount()); Assert.assertEquals(0, connectionPool.getActiveConnectionCount()); diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/TLSServerConnectionCloseTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/TLSServerConnectionCloseTest.java index 79a9cd1879..d48ed22314 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/TLSServerConnectionCloseTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/TLSServerConnectionCloseTest.java @@ -183,7 +183,7 @@ public class TLSServerConnectionCloseTest // Connection should have been removed from pool. HttpDestinationOverHTTP destination = (HttpDestinationOverHTTP)client.getDestination("http", "localhost", port); - DuplexConnectionPool connectionPool = destination.getConnectionPool(); + DuplexConnectionPool connectionPool = (DuplexConnectionPool)destination.getConnectionPool(); Assert.assertEquals(0, connectionPool.getConnectionCount()); Assert.assertEquals(0, connectionPool.getIdleConnectionCount()); Assert.assertEquals(0, connectionPool.getActiveConnectionCount()); diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/http/HttpDestinationOverHTTPTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/http/HttpDestinationOverHTTPTest.java index aa3b4f5e74..bf9af834d9 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/http/HttpDestinationOverHTTPTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/http/HttpDestinationOverHTTPTest.java @@ -24,6 +24,7 @@ import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.TimeUnit; import org.eclipse.jetty.client.AbstractHttpClientServerTest; +import org.eclipse.jetty.client.ConnectionPool; import org.eclipse.jetty.client.DuplexConnectionPool; import org.eclipse.jetty.client.EmptyServerHandler; import org.eclipse.jetty.client.HttpClient; @@ -31,9 +32,6 @@ import org.eclipse.jetty.client.Origin; import org.eclipse.jetty.client.api.Connection; 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.Response; -import org.eclipse.jetty.client.api.Result; import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpHeaderValue; import org.eclipse.jetty.util.ssl.SslContextFactory; @@ -59,11 +57,13 @@ public class HttpDestinationOverHTTPTest extends AbstractHttpClientServerTest public void test_FirstAcquire_WithEmptyQueue() throws Exception { HttpDestinationOverHTTP destination = new HttpDestinationOverHTTP(client, new Origin("http", "localhost", connector.getLocalPort())); - Connection connection = destination.acquire(); + destination.start(); + DuplexConnectionPool connectionPool = (DuplexConnectionPool)destination.getConnectionPool(); + Connection connection = connectionPool.acquire(); if (connection == null) { // There are no queued requests, so the newly created connection will be idle - connection = timedPoll(destination.getConnectionPool().getIdleConnections(), 5, TimeUnit.SECONDS); + connection = timedPoll(connectionPool.getIdleConnections(), 5, TimeUnit.SECONDS); } Assert.assertNotNull(connection); } @@ -72,7 +72,9 @@ public class HttpDestinationOverHTTPTest extends AbstractHttpClientServerTest public void test_SecondAcquire_AfterFirstAcquire_WithEmptyQueue_ReturnsSameConnection() throws Exception { HttpDestinationOverHTTP destination = new HttpDestinationOverHTTP(client, new Origin("http", "localhost", connector.getLocalPort())); - Connection connection1 = destination.acquire(); + destination.start(); + DuplexConnectionPool connectionPool = (DuplexConnectionPool)destination.getConnectionPool(); + Connection connection1 = connectionPool.acquire(); if (connection1 == null) { // There are no queued requests, so the newly created connection will be idle @@ -80,11 +82,11 @@ public class HttpDestinationOverHTTPTest extends AbstractHttpClientServerTest while (connection1 == null && TimeUnit.NANOSECONDS.toSeconds(System.nanoTime() - start) < 5) { TimeUnit.MILLISECONDS.sleep(50); - connection1 = destination.getConnectionPool().getIdleConnections().peek(); + connection1 = connectionPool.getIdleConnections().peek(); } Assert.assertNotNull(connection1); - Connection connection2 = destination.acquire(); + Connection connection2 = connectionPool.acquire(); Assert.assertSame(connection1, connection2); } } @@ -97,18 +99,18 @@ public class HttpDestinationOverHTTPTest extends AbstractHttpClientServerTest HttpDestinationOverHTTP destination = new HttpDestinationOverHTTP(client, new Origin("http", "localhost", connector.getLocalPort())) { @Override - protected DuplexConnectionPool newConnectionPool(HttpClient client) + protected ConnectionPool newConnectionPool(HttpClient client) { return new DuplexConnectionPool(this, client.getMaxConnectionsPerDestination(), this) { @Override - protected void idleCreated(Connection connection) + protected void onCreated(Connection connection) { try { idleLatch.countDown(); latch.await(5, TimeUnit.SECONDS); - super.idleCreated(connection); + super.onCreated(connection); } catch (InterruptedException x) { @@ -118,7 +120,9 @@ public class HttpDestinationOverHTTPTest extends AbstractHttpClientServerTest }; } }; - Connection connection1 = destination.acquire(); + destination.start(); + DuplexConnectionPool connectionPool = (DuplexConnectionPool)destination.getConnectionPool(); + Connection connection1 = connectionPool.acquire(); // Make sure we entered idleCreated(). Assert.assertTrue(idleLatch.await(5, TimeUnit.SECONDS)); @@ -128,13 +132,13 @@ public class HttpDestinationOverHTTPTest extends AbstractHttpClientServerTest Assert.assertNull(connection1); // Second attempt also returns null because we delayed idleCreated() above. - Connection connection2 = destination.acquire(); + Connection connection2 = connectionPool.acquire(); Assert.assertNull(connection2); latch.countDown(); // There must be 2 idle connections. - Queue<Connection> idleConnections = destination.getConnectionPool().getIdleConnections(); + Queue<Connection> idleConnections = connectionPool.getIdleConnections(); Connection connection = timedPoll(idleConnections, 5, TimeUnit.SECONDS); Assert.assertNotNull(connection); connection = timedPoll(idleConnections, 5, TimeUnit.SECONDS); @@ -145,23 +149,25 @@ public class HttpDestinationOverHTTPTest extends AbstractHttpClientServerTest public void test_Acquire_Process_Release_Acquire_ReturnsSameConnection() throws Exception { HttpDestinationOverHTTP destination = new HttpDestinationOverHTTP(client, new Origin("http", "localhost", connector.getLocalPort())); - HttpConnectionOverHTTP connection1 = destination.acquire(); + destination.start(); + DuplexConnectionPool connectionPool = (DuplexConnectionPool)destination.getConnectionPool(); + HttpConnectionOverHTTP connection1 = (HttpConnectionOverHTTP)connectionPool.acquire(); long start = System.nanoTime(); while (connection1 == null && TimeUnit.NANOSECONDS.toSeconds(System.nanoTime() - start) < 5) { TimeUnit.MILLISECONDS.sleep(50); - connection1 = (HttpConnectionOverHTTP)destination.getConnectionPool().getIdleConnections().peek(); + connection1 = (HttpConnectionOverHTTP)connectionPool.getIdleConnections().peek(); } Assert.assertNotNull(connection1); // Acquire the connection to make it active - Assert.assertSame(connection1, destination.acquire()); + Assert.assertSame(connection1, connectionPool.acquire()); destination.process(connection1); destination.release(connection1); - Connection connection2 = destination.acquire(); + Connection connection2 = connectionPool.acquire(); Assert.assertSame(connection1, connection2); } @@ -172,7 +178,9 @@ public class HttpDestinationOverHTTPTest extends AbstractHttpClientServerTest client.setIdleTimeout(idleTimeout); HttpDestinationOverHTTP destination = new HttpDestinationOverHTTP(client, new Origin("http", "localhost", connector.getLocalPort())); - Connection connection1 = destination.acquire(); + destination.start(); + DuplexConnectionPool connectionPool = (DuplexConnectionPool)destination.getConnectionPool(); + Connection connection1 = connectionPool.acquire(); if (connection1 == null) { // There are no queued requests, so the newly created connection will be idle @@ -180,13 +188,13 @@ public class HttpDestinationOverHTTPTest extends AbstractHttpClientServerTest while (connection1 == null && TimeUnit.NANOSECONDS.toSeconds(System.nanoTime() - start) < 5) { TimeUnit.MILLISECONDS.sleep(50); - connection1 = destination.getConnectionPool().getIdleConnections().peek(); + connection1 = connectionPool.getIdleConnections().peek(); } Assert.assertNotNull(connection1); TimeUnit.MILLISECONDS.sleep(2 * idleTimeout); - connection1 = destination.getConnectionPool().getIdleConnections().poll(); + connection1 = connectionPool.getIdleConnections().poll(); Assert.assertNull(connection1); } } @@ -210,35 +218,23 @@ public class HttpDestinationOverHTTPTest extends AbstractHttpClientServerTest client.newRequest("localhost", connector.getLocalPort()) .scheme(scheme) .path("/one") - .onRequestQueued(new Request.QueuedListener() + .onRequestQueued(request -> { - @Override - public void onQueued(Request request) - { - // This request exceeds the maximum queued, should fail - client.newRequest("localhost", connector.getLocalPort()) - .scheme(scheme) - .path("/two") - .send(new Response.CompleteListener() - { - @Override - public void onComplete(Result result) - { - Assert.assertTrue(result.isFailed()); - Assert.assertThat(result.getRequestFailure(), Matchers.instanceOf(RejectedExecutionException.class)); - failureLatch.countDown(); - } - }); - } + // This request exceeds the maximum queued, should fail + client.newRequest("localhost", connector.getLocalPort()) + .scheme(scheme) + .path("/two") + .send(result -> + { + Assert.assertTrue(result.isFailed()); + Assert.assertThat(result.getRequestFailure(), Matchers.instanceOf(RejectedExecutionException.class)); + failureLatch.countDown(); + }); }) - .send(new Response.CompleteListener() + .send(result -> { - @Override - public void onComplete(Result result) - { - if (result.isSucceeded()) - successLatch.countDown(); - } + if (result.isSucceeded()) + successLatch.countDown(); }); Assert.assertTrue(failureLatch.await(5, TimeUnit.SECONDS)); diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/http/HttpReceiverOverHTTPTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/http/HttpReceiverOverHTTPTest.java index e057cf34f9..feb5cf0c58 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/http/HttpReceiverOverHTTPTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/http/HttpReceiverOverHTTPTest.java @@ -30,6 +30,7 @@ import org.eclipse.jetty.client.HttpExchange; import org.eclipse.jetty.client.HttpRequest; import org.eclipse.jetty.client.HttpResponseException; import org.eclipse.jetty.client.Origin; +import org.eclipse.jetty.client.api.Connection; import org.eclipse.jetty.client.api.Response; import org.eclipse.jetty.client.util.FutureResponseListener; import org.eclipse.jetty.http.HttpFields; @@ -60,6 +61,7 @@ public class HttpReceiverOverHTTPTest client = new HttpClient(); client.start(); destination = new HttpDestinationOverHTTP(client, new Origin("http", "localhost", 8080)); + destination.start(); endPoint = new ByteArrayEndPoint(); connection = new HttpConnectionOverHTTP(endPoint, destination, new Promise.Adapter<>()); endPoint.setConnection(connection); @@ -235,7 +237,7 @@ public class HttpReceiverOverHTTPTest } }; endPoint.setConnection(connection); - + // Partial response to trigger the call to fillInterested(). endPoint.addInput("" + "HTTP/1.1 200 OK\r\n" + diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/http/HttpSenderOverHTTPTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/http/HttpSenderOverHTTPTest.java index b98aea13ab..e592c42ca8 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/http/HttpSenderOverHTTPTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/http/HttpSenderOverHTTPTest.java @@ -67,6 +67,7 @@ public class HttpSenderOverHTTPTest { ByteArrayEndPoint endPoint = new ByteArrayEndPoint(); HttpDestinationOverHTTP destination = new HttpDestinationOverHTTP(client, new Origin("http", "localhost", 8080)); + destination.start(); HttpConnectionOverHTTP connection = new HttpConnectionOverHTTP(endPoint, destination, new Promise.Adapter<Connection>()); Request request = client.newRequest(URI.create("http://localhost/")); final CountDownLatch headersLatch = new CountDownLatch(1); @@ -100,6 +101,7 @@ public class HttpSenderOverHTTPTest { ByteArrayEndPoint endPoint = new ByteArrayEndPoint("", 16); HttpDestinationOverHTTP destination = new HttpDestinationOverHTTP(client, new Origin("http", "localhost", 8080)); + destination.start(); HttpConnectionOverHTTP connection = new HttpConnectionOverHTTP(endPoint, destination, new Promise.Adapter<Connection>()); Request request = client.newRequest(URI.create("http://localhost/")); connection.send(request, null); @@ -129,6 +131,7 @@ public class HttpSenderOverHTTPTest // Shutdown output to trigger the exception on write endPoint.shutdownOutput(); HttpDestinationOverHTTP destination = new HttpDestinationOverHTTP(client, new Origin("http", "localhost", 8080)); + destination.start(); HttpConnectionOverHTTP connection = new HttpConnectionOverHTTP(endPoint, destination, new Promise.Adapter<Connection>()); Request request = client.newRequest(URI.create("http://localhost/")); final CountDownLatch failureLatch = new CountDownLatch(2); @@ -158,6 +161,7 @@ public class HttpSenderOverHTTPTest { ByteArrayEndPoint endPoint = new ByteArrayEndPoint("", 16); HttpDestinationOverHTTP destination = new HttpDestinationOverHTTP(client, new Origin("http", "localhost", 8080)); + destination.start(); HttpConnectionOverHTTP connection = new HttpConnectionOverHTTP(endPoint, destination, new Promise.Adapter<Connection>()); Request request = client.newRequest(URI.create("http://localhost/")); final CountDownLatch failureLatch = new CountDownLatch(2); @@ -193,6 +197,7 @@ public class HttpSenderOverHTTPTest { ByteArrayEndPoint endPoint = new ByteArrayEndPoint(); HttpDestinationOverHTTP destination = new HttpDestinationOverHTTP(client, new Origin("http", "localhost", 8080)); + destination.start(); HttpConnectionOverHTTP connection = new HttpConnectionOverHTTP(endPoint, destination, new Promise.Adapter<Connection>()); Request request = client.newRequest(URI.create("http://localhost/")); String content = "abcdef"; @@ -227,6 +232,7 @@ public class HttpSenderOverHTTPTest { ByteArrayEndPoint endPoint = new ByteArrayEndPoint(); HttpDestinationOverHTTP destination = new HttpDestinationOverHTTP(client, new Origin("http", "localhost", 8080)); + destination.start(); HttpConnectionOverHTTP connection = new HttpConnectionOverHTTP(endPoint, destination, new Promise.Adapter<Connection>()); Request request = client.newRequest(URI.create("http://localhost/")); String content1 = "0123456789"; @@ -262,6 +268,7 @@ public class HttpSenderOverHTTPTest { ByteArrayEndPoint endPoint = new ByteArrayEndPoint(); HttpDestinationOverHTTP destination = new HttpDestinationOverHTTP(client, new Origin("http", "localhost", 8080)); + destination.start(); HttpConnectionOverHTTP connection = new HttpConnectionOverHTTP(endPoint, destination, new Promise.Adapter<Connection>()); Request request = client.newRequest(URI.create("http://localhost/")); String content1 = "0123456789"; diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/util/MultiPartContentProviderTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/util/MultiPartContentProviderTest.java new file mode 100644 index 0000000000..4ae83b23b8 --- /dev/null +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/util/MultiPartContentProviderTest.java @@ -0,0 +1,439 @@ +// +// ======================================================================== +// Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.client.util; + +import java.io.BufferedWriter; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Random; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import javax.servlet.MultipartConfigElement; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.Part; + +import org.eclipse.jetty.client.AbstractHttpClientServerTest; +import org.eclipse.jetty.client.api.ContentProvider; +import org.eclipse.jetty.client.api.ContentResponse; +import org.eclipse.jetty.http.HttpFields; +import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.http.HttpMethod; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.handler.AbstractHandler; +import org.eclipse.jetty.toolchain.test.MavenTestingUtils; +import org.eclipse.jetty.util.IO; +import org.eclipse.jetty.util.ssl.SslContextFactory; +import org.junit.Assert; +import org.junit.Test; + +public class MultiPartContentProviderTest extends AbstractHttpClientServerTest +{ + public MultiPartContentProviderTest(SslContextFactory sslContextFactory) + { + super(sslContextFactory); + } + + @Test + public void testEmptyMultiPart() throws Exception + { + start(new AbstractMultiPartHandler() + { + @Override + protected void handle(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException + { + Collection<Part> parts = request.getParts(); + Assert.assertEquals(0, parts.size()); + } + }); + + MultiPartContentProvider multiPart = new MultiPartContentProvider(); + ContentResponse response = client.newRequest("localhost", connector.getLocalPort()) + .scheme(scheme) + .method(HttpMethod.POST) + .content(multiPart) + .send(); + + Assert.assertEquals(200, response.getStatus()); + } + + @Test + public void testSimpleField() throws Exception + { + String name = "field"; + String value = "value"; + start(new AbstractMultiPartHandler() + { + @Override + protected void handle(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException + { + Collection<Part> parts = request.getParts(); + Assert.assertEquals(1, parts.size()); + Part part = parts.iterator().next(); + Assert.assertEquals(name, part.getName()); + Assert.assertEquals(value, IO.toString(part.getInputStream())); + } + }); + + MultiPartContentProvider multiPart = new MultiPartContentProvider(); + multiPart.addFieldPart(name, new StringContentProvider(value), null); + ContentResponse response = client.newRequest("localhost", connector.getLocalPort()) + .scheme(scheme) + .method(HttpMethod.POST) + .content(multiPart) + .send(); + + Assert.assertEquals(200, response.getStatus()); + } + + @Test + public void testFieldWithOverridenContentType() throws Exception + { + String name = "field"; + String value = "\u00e8"; + Charset encoding = StandardCharsets.ISO_8859_1; + start(new AbstractMultiPartHandler() + { + @Override + protected void handle(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException + { + Collection<Part> parts = request.getParts(); + Assert.assertEquals(1, parts.size()); + Part part = parts.iterator().next(); + Assert.assertEquals(name, part.getName()); + String contentType = part.getContentType(); + Assert.assertNotNull(contentType); + int equal = contentType.lastIndexOf('='); + Charset charset = Charset.forName(contentType.substring(equal + 1)); + Assert.assertEquals(encoding, charset); + Assert.assertEquals(value, IO.toString(part.getInputStream(), charset)); + } + }); + + MultiPartContentProvider multiPart = new MultiPartContentProvider(); + HttpFields fields = new HttpFields(); + fields.put(HttpHeader.CONTENT_TYPE, "text/plain;charset=" + encoding.name()); + BytesContentProvider content = new BytesContentProvider(value.getBytes(encoding)); + multiPart.addFieldPart(name, content, fields); + ContentResponse response = client.newRequest("localhost", connector.getLocalPort()) + .scheme(scheme) + .method(HttpMethod.POST) + .content(multiPart) + .send(); + + Assert.assertEquals(200, response.getStatus()); + } + + @Test + public void testFieldDeferred() throws Exception + { + String name = "field"; + byte[] data = "Hello, World".getBytes(StandardCharsets.US_ASCII); + start(new AbstractMultiPartHandler() + { + @Override + protected void handle(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException + { + Collection<Part> parts = request.getParts(); + Assert.assertEquals(1, parts.size()); + Part part = parts.iterator().next(); + Assert.assertEquals(name, part.getName()); + Assert.assertEquals("text/plain", part.getContentType()); + Assert.assertArrayEquals(data, IO.readBytes(part.getInputStream())); + } + }); + + MultiPartContentProvider multiPart = new MultiPartContentProvider(); + DeferredContentProvider content = new DeferredContentProvider(); + multiPart.addFieldPart(name, content, null); + CountDownLatch responseLatch = new CountDownLatch(1); + client.newRequest("localhost", connector.getLocalPort()) + .scheme(scheme) + .method(HttpMethod.POST) + .content(multiPart) + .send(result -> + { + if (result.isSucceeded()) + { + Assert.assertEquals(200, result.getResponse().getStatus()); + responseLatch.countDown(); + } + }); + + // Wait until the request has been sent. + Thread.sleep(1000); + + // Provide the content. + content.offer(ByteBuffer.wrap(data)); + content.close(); + + Assert.assertTrue(responseLatch.await(5, TimeUnit.SECONDS)); + } + + @Test + public void testFileFromInputStream() throws Exception + { + String name = "file"; + String fileName = "upload.png"; + String contentType = "image/png"; + byte[] data = new byte[512]; + new Random().nextBytes(data); + start(new AbstractMultiPartHandler() + { + @Override + protected void handle(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException + { + Collection<Part> parts = request.getParts(); + Assert.assertEquals(1, parts.size()); + Part part = parts.iterator().next(); + Assert.assertEquals(name, part.getName()); + Assert.assertEquals(contentType, part.getContentType()); + Assert.assertEquals(fileName, part.getSubmittedFileName()); + Assert.assertEquals(data.length, part.getSize()); + Assert.assertArrayEquals(data, IO.readBytes(part.getInputStream())); + } + }); + + CountDownLatch closeLatch = new CountDownLatch(1); + MultiPartContentProvider multiPart = new MultiPartContentProvider(); + InputStreamContentProvider content = new InputStreamContentProvider(new ByteArrayInputStream(data) + { + @Override + public void close() throws IOException + { + super.close(); + closeLatch.countDown(); + } + }); + HttpFields fields = new HttpFields(); + fields.put(HttpHeader.CONTENT_TYPE, contentType); + multiPart.addFilePart(name, fileName, content, fields); + ContentResponse response = client.newRequest("localhost", connector.getLocalPort()) + .scheme(scheme) + .method(HttpMethod.POST) + .content(multiPart) + .send(); + + Assert.assertTrue(closeLatch.await(5, TimeUnit.SECONDS)); + Assert.assertEquals(200, response.getStatus()); + } + + @Test + public void testFileFromPath() throws Exception + { + // Prepare a file to upload. + String data = "multipart_test_\u20ac"; + Path tmpDir = MavenTestingUtils.getTargetTestingPath(); + Path tmpPath = Files.createTempFile(tmpDir, "multipart_", ".txt"); + Charset encoding = StandardCharsets.UTF_8; + try (BufferedWriter writer = Files.newBufferedWriter(tmpPath, encoding, StandardOpenOption.CREATE)) + { + writer.write(data); + } + + String name = "file"; + String contentType = "text/plain; charset=" + encoding.name(); + start(new AbstractMultiPartHandler() + { + @Override + protected void handle(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException + { + Collection<Part> parts = request.getParts(); + Assert.assertEquals(1, parts.size()); + Part part = parts.iterator().next(); + Assert.assertEquals(name, part.getName()); + Assert.assertEquals(contentType, part.getContentType()); + Assert.assertEquals(tmpPath.getFileName().toString(), part.getSubmittedFileName()); + Assert.assertEquals(Files.size(tmpPath), part.getSize()); + Assert.assertEquals(data, IO.toString(part.getInputStream(), encoding)); + } + }); + + MultiPartContentProvider multiPart = new MultiPartContentProvider(); + ContentProvider content = new PathContentProvider(contentType, tmpPath); + multiPart.addFilePart(name, tmpPath.getFileName().toString(), content, null); + ContentResponse response = client.newRequest("localhost", connector.getLocalPort()) + .scheme(scheme) + .method(HttpMethod.POST) + .content(multiPart) + .send(); + + Assert.assertEquals(200, response.getStatus()); + + Files.delete(tmpPath); + } + + @Test + public void testFieldWithFile() throws Exception + { + // Prepare a file to upload. + byte[] data = new byte[1024]; + new Random().nextBytes(data); + Path tmpDir = MavenTestingUtils.getTargetTestingPath(); + Path tmpPath = Files.createTempFile(tmpDir, "multipart_", ".txt"); + try (OutputStream output = Files.newOutputStream(tmpPath, StandardOpenOption.CREATE)) + { + output.write(data); + } + + String field = "field"; + String value = "\u20ac"; + String fileField = "file"; + Charset encoding = StandardCharsets.UTF_8; + String contentType = "text/plain;charset=" + encoding.name(); + String headerName = "foo"; + String headerValue = "bar"; + start(new AbstractMultiPartHandler() + { + @Override + protected void handle(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException + { + List<Part> parts = new ArrayList<>(request.getParts()); + Assert.assertEquals(2, parts.size()); + Part fieldPart = parts.get(0); + Part filePart = parts.get(1); + if (!field.equals(fieldPart.getName())) + { + Part swap = filePart; + filePart = fieldPart; + fieldPart = swap; + } + + Assert.assertEquals(field, fieldPart.getName()); + Assert.assertEquals(contentType, fieldPart.getContentType()); + Assert.assertEquals(value, IO.toString(fieldPart.getInputStream(), encoding)); + Assert.assertEquals(headerValue, fieldPart.getHeader(headerName)); + + Assert.assertEquals(fileField, filePart.getName()); + Assert.assertEquals("application/octet-stream", filePart.getContentType()); + Assert.assertEquals(tmpPath.getFileName().toString(), filePart.getSubmittedFileName()); + Assert.assertEquals(Files.size(tmpPath), filePart.getSize()); + Assert.assertArrayEquals(data, IO.readBytes(filePart.getInputStream())); + } + }); + + MultiPartContentProvider multiPart = new MultiPartContentProvider(); + HttpFields fields = new HttpFields(); + fields.put(headerName, headerValue); + multiPart.addFieldPart(field, new StringContentProvider(value, encoding), fields); + multiPart.addFilePart(fileField, tmpPath.getFileName().toString(), new PathContentProvider(tmpPath), null); + ContentResponse response = client.newRequest("localhost", connector.getLocalPort()) + .scheme(scheme) + .method(HttpMethod.POST) + .content(multiPart) + .send(); + + Assert.assertEquals(200, response.getStatus()); + + Files.delete(tmpPath); + } + + @Test + public void testFieldDeferredAndFileDeferred() throws Exception + { + String value = "text"; + Charset encoding = StandardCharsets.US_ASCII; + byte[] fileData = new byte[1024]; + new Random().nextBytes(fileData); + start(new AbstractMultiPartHandler() + { + @Override + protected void handle(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException + { + List<Part> parts = new ArrayList<>(request.getParts()); + Assert.assertEquals(2, parts.size()); + Part fieldPart = parts.get(0); + Part filePart = parts.get(1); + if (!"field".equals(fieldPart.getName())) + { + Part swap = filePart; + filePart = fieldPart; + fieldPart = swap; + } + + Assert.assertEquals(value, IO.toString(fieldPart.getInputStream(), encoding)); + + Assert.assertEquals("file", filePart.getName()); + Assert.assertEquals("application/octet-stream", filePart.getContentType()); + Assert.assertEquals("fileName", filePart.getSubmittedFileName()); + Assert.assertArrayEquals(fileData, IO.readBytes(filePart.getInputStream())); + } + }); + + MultiPartContentProvider multiPart = new MultiPartContentProvider(); + DeferredContentProvider fieldContent = new DeferredContentProvider(); + multiPart.addFieldPart("field", fieldContent, null); + DeferredContentProvider fileContent = new DeferredContentProvider(); + multiPart.addFilePart("file", "fileName", fileContent, null); + CountDownLatch responseLatch = new CountDownLatch(1); + client.newRequest("localhost", connector.getLocalPort()) + .scheme(scheme) + .method(HttpMethod.POST) + .content(multiPart) + .send(result -> + { + if (result.isSucceeded()) + { + Assert.assertEquals(200, result.getResponse().getStatus()); + responseLatch.countDown(); + } + }); + + // Wait until the request has been sent. + Thread.sleep(1000); + + // Provide the content, in reversed part order. + fileContent.offer(ByteBuffer.wrap(fileData)); + fileContent.close(); + + Thread.sleep(1000); + + fieldContent.offer(encoding.encode(value)); + fieldContent.close(); + + Assert.assertTrue(responseLatch.await(5, TimeUnit.SECONDS)); + } + + private static abstract class AbstractMultiPartHandler extends AbstractHandler + { + @Override + public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException + { + baseRequest.setHandled(true); + File tmpDir = MavenTestingUtils.getTargetTestingDir(); + request.setAttribute(Request.__MULTIPART_CONFIG_ELEMENT, new MultipartConfigElement(tmpDir.getAbsolutePath())); + handle(request, response); + } + + protected abstract void handle(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException; + } +} diff --git a/jetty-fcgi/fcgi-client/src/main/java/org/eclipse/jetty/fcgi/client/http/HttpConnectionOverFCGI.java b/jetty-fcgi/fcgi-client/src/main/java/org/eclipse/jetty/fcgi/client/http/HttpConnectionOverFCGI.java index 4be9cbf196..4d9691de50 100644 --- a/jetty-fcgi/fcgi-client/src/main/java/org/eclipse/jetty/fcgi/client/http/HttpConnectionOverFCGI.java +++ b/jetty-fcgi/fcgi-client/src/main/java/org/eclipse/jetty/fcgi/client/http/HttpConnectionOverFCGI.java @@ -181,7 +181,7 @@ public class HttpConnectionOverFCGI extends AbstractConnection implements Connec if (channels.isEmpty()) close(); else - failAndClose(new EOFException()); + failAndClose(new EOFException(String.valueOf(getEndPoint()))); } @Override diff --git a/jetty-fcgi/fcgi-client/src/main/java/org/eclipse/jetty/fcgi/client/http/HttpDestinationOverFCGI.java b/jetty-fcgi/fcgi-client/src/main/java/org/eclipse/jetty/fcgi/client/http/HttpDestinationOverFCGI.java index f6adf480c4..2f3447d384 100644 --- a/jetty-fcgi/fcgi-client/src/main/java/org/eclipse/jetty/fcgi/client/http/HttpDestinationOverFCGI.java +++ b/jetty-fcgi/fcgi-client/src/main/java/org/eclipse/jetty/fcgi/client/http/HttpDestinationOverFCGI.java @@ -22,8 +22,9 @@ import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.client.HttpExchange; import org.eclipse.jetty.client.Origin; import org.eclipse.jetty.client.PoolingHttpDestination; +import org.eclipse.jetty.client.api.Connection; -public class HttpDestinationOverFCGI extends PoolingHttpDestination<HttpConnectionOverFCGI> +public class HttpDestinationOverFCGI extends PoolingHttpDestination { public HttpDestinationOverFCGI(HttpClient client, Origin origin) { @@ -31,8 +32,8 @@ public class HttpDestinationOverFCGI extends PoolingHttpDestination<HttpConnecti } @Override - protected void send(HttpConnectionOverFCGI connection, HttpExchange exchange) + protected void send(Connection connection, HttpExchange exchange) { - connection.send(exchange); + ((HttpConnectionOverFCGI)connection).send(exchange); } } diff --git a/jetty-fcgi/fcgi-client/src/main/java/org/eclipse/jetty/fcgi/client/http/MultiplexHttpDestinationOverFCGI.java b/jetty-fcgi/fcgi-client/src/main/java/org/eclipse/jetty/fcgi/client/http/MultiplexHttpDestinationOverFCGI.java index 77f2259d80..80bb63cc47 100644 --- a/jetty-fcgi/fcgi-client/src/main/java/org/eclipse/jetty/fcgi/client/http/MultiplexHttpDestinationOverFCGI.java +++ b/jetty-fcgi/fcgi-client/src/main/java/org/eclipse/jetty/fcgi/client/http/MultiplexHttpDestinationOverFCGI.java @@ -22,8 +22,9 @@ import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.client.HttpExchange; import org.eclipse.jetty.client.MultiplexHttpDestination; import org.eclipse.jetty.client.Origin; +import org.eclipse.jetty.client.api.Connection; -public class MultiplexHttpDestinationOverFCGI extends MultiplexHttpDestination<HttpConnectionOverFCGI> +public class MultiplexHttpDestinationOverFCGI extends MultiplexHttpDestination { public MultiplexHttpDestinationOverFCGI(HttpClient client, Origin origin) { @@ -31,8 +32,8 @@ public class MultiplexHttpDestinationOverFCGI extends MultiplexHttpDestination<H } @Override - protected void send(HttpConnectionOverFCGI connection, HttpExchange exchange) + protected void send(Connection connection, HttpExchange exchange) { - connection.send(exchange); + ((HttpConnectionOverFCGI)connection).send(exchange); } } diff --git a/jetty-fcgi/fcgi-client/src/main/java/org/eclipse/jetty/fcgi/parser/BeginRequestContentParser.java b/jetty-fcgi/fcgi-client/src/main/java/org/eclipse/jetty/fcgi/parser/BeginRequestContentParser.java index 2367fb65fb..912f4f8140 100644 --- a/jetty-fcgi/fcgi-client/src/main/java/org/eclipse/jetty/fcgi/parser/BeginRequestContentParser.java +++ b/jetty-fcgi/fcgi-client/src/main/java/org/eclipse/jetty/fcgi/parser/BeginRequestContentParser.java @@ -22,6 +22,16 @@ import java.nio.ByteBuffer; import org.eclipse.jetty.fcgi.FCGI; +/** + * <p>Parser for the BEGIN_REQUEST frame body.</p> + * <pre> + * struct begin_request_body { + * ushort role; + * ubyte flags; + * ubyte[5] reserved; + * } + * </pre> + */ public class BeginRequestContentParser extends ContentParser { private final ServerParser.Listener listener; diff --git a/jetty-fcgi/fcgi-client/src/main/java/org/eclipse/jetty/fcgi/parser/EndRequestContentParser.java b/jetty-fcgi/fcgi-client/src/main/java/org/eclipse/jetty/fcgi/parser/EndRequestContentParser.java index b8173bf494..dc07bd544e 100644 --- a/jetty-fcgi/fcgi-client/src/main/java/org/eclipse/jetty/fcgi/parser/EndRequestContentParser.java +++ b/jetty-fcgi/fcgi-client/src/main/java/org/eclipse/jetty/fcgi/parser/EndRequestContentParser.java @@ -20,6 +20,16 @@ package org.eclipse.jetty.fcgi.parser; import java.nio.ByteBuffer; +/** + * <p>Parser for the END_REQUEST frame body.</p> + * <pre> + * struct end_request_body { + * uint applicationStatus; + * ubyte protocolStatus; + * ubyte[3] reserved; + * } + * </pre> + */ public class EndRequestContentParser extends ContentParser { private final Parser.Listener listener; @@ -80,7 +90,7 @@ public class EndRequestContentParser extends ContentParser } else { - state = State.APPLICATION_BYTES; + state = State.RESERVED_BYTES; cursor = 0; break; } @@ -88,7 +98,7 @@ public class EndRequestContentParser extends ContentParser case RESERVED_BYTES: { buffer.get(); - if (++cursor == 0) + if (++cursor == 3) { onEnd(); reset(); diff --git a/jetty-fcgi/fcgi-client/src/main/java/org/eclipse/jetty/fcgi/parser/HeaderParser.java b/jetty-fcgi/fcgi-client/src/main/java/org/eclipse/jetty/fcgi/parser/HeaderParser.java index 078105a9f3..7d43112569 100644 --- a/jetty-fcgi/fcgi-client/src/main/java/org/eclipse/jetty/fcgi/parser/HeaderParser.java +++ b/jetty-fcgi/fcgi-client/src/main/java/org/eclipse/jetty/fcgi/parser/HeaderParser.java @@ -21,9 +21,28 @@ package org.eclipse.jetty.fcgi.parser; import java.nio.ByteBuffer; import org.eclipse.jetty.fcgi.FCGI; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; +/** + * <p>Parser for FastCGI frame headers.</p> + * <pre> + * struct frame_header { + * ubyte version; + * ubyte type; + * ushort requestId; + * ushort contentLength; + * ubyte paddingLength; + * ubyte reserved; + * } + * </pre> + * + * @see Parser + */ public class HeaderParser { + private static final Logger LOG = Log.getLogger(Parser.class); + private State state = State.VERSION; private int cursor; private int version; @@ -109,6 +128,8 @@ public class HeaderParser case RESERVED: { buffer.get(); + if (LOG.isDebugEnabled()) + LOG.debug("Parsed request {} header {} length={}", getRequest(), getFrameType(), getContentLength()); return true; } default: diff --git a/jetty-fcgi/fcgi-client/src/main/java/org/eclipse/jetty/fcgi/parser/ParamsContentParser.java b/jetty-fcgi/fcgi-client/src/main/java/org/eclipse/jetty/fcgi/parser/ParamsContentParser.java index 4678ad5ebe..dcf34fefc8 100644 --- a/jetty-fcgi/fcgi-client/src/main/java/org/eclipse/jetty/fcgi/parser/ParamsContentParser.java +++ b/jetty-fcgi/fcgi-client/src/main/java/org/eclipse/jetty/fcgi/parser/ParamsContentParser.java @@ -20,11 +20,44 @@ package org.eclipse.jetty.fcgi.parser; import java.nio.ByteBuffer; import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import org.eclipse.jetty.http.HttpField; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; +/** + * <p>Parser for the PARAMS frame body.</p> + * <pre> + * struct small_name_small_value_params_body { + * ubyte nameLength; + * ubyte valueLength; + * ubyte[] nameBytes; + * ubyte[] valueBytes; + * } + * + * struct small_name_large_value_params_body { + * ubyte nameLength; + * uint valueLength; + * ubyte[] nameBytes; + * ubyte[] valueBytes; + * } + * + * struct large_name_small_value_params_body { + * uint nameLength; + * ubyte valueLength; + * ubyte[] nameBytes; + * ubyte[] valueBytes; + * } + * + * struct large_name_large_value_params_body { + * uint nameLength; + * uint valueLength; + * ubyte[] nameBytes; + * ubyte[] valueBytes; + * } + * </pre> + */ public class ParamsContentParser extends ContentParser { private static final Logger LOG = Log.getLogger(ParamsContentParser.class); @@ -179,7 +212,7 @@ public class ParamsContentParser extends ContentParser } case PARAM: { - Charset utf8 = Charset.forName("UTF-8"); + Charset utf8 = StandardCharsets.UTF_8; onParam(new String(nameBytes, utf8), new String(valueBytes, utf8)); partialReset(); if (length == 0) diff --git a/jetty-fcgi/fcgi-client/src/main/java/org/eclipse/jetty/fcgi/parser/Parser.java b/jetty-fcgi/fcgi-client/src/main/java/org/eclipse/jetty/fcgi/parser/Parser.java index 402f3a4897..be2ac480c6 100644 --- a/jetty-fcgi/fcgi-client/src/main/java/org/eclipse/jetty/fcgi/parser/Parser.java +++ b/jetty-fcgi/fcgi-client/src/main/java/org/eclipse/jetty/fcgi/parser/Parser.java @@ -22,6 +22,8 @@ import java.nio.ByteBuffer; import org.eclipse.jetty.fcgi.FCGI; import org.eclipse.jetty.http.HttpField; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; /** * <p>The FastCGI protocol exchanges <em>frames</em>.</p> @@ -39,9 +41,14 @@ import org.eclipse.jetty.http.HttpField; * </pre> * <p>Depending on the {@code type}, the content may have a different format, * so there are specialized content parsers.</p> + * + * @see HeaderParser + * @see ContentParser */ public abstract class Parser { + private static final Logger LOG = Log.getLogger(Parser.class); + protected final HeaderParser headerParser = new HeaderParser(); private State state = State.HEADER; private int padding; @@ -73,6 +80,9 @@ public abstract class Parser else { ContentParser.Result result = contentParser.parse(buffer); + if (LOG.isDebugEnabled()) + LOG.debug("Parsed request {} content {} result={}", headerParser.getRequest(), headerParser.getFrameType(), result); + if (result == ContentParser.Result.PENDING) { // Not enough data, signal to read/parse more. diff --git a/jetty-fcgi/fcgi-client/src/main/java/org/eclipse/jetty/fcgi/parser/ResponseContentParser.java b/jetty-fcgi/fcgi-client/src/main/java/org/eclipse/jetty/fcgi/parser/ResponseContentParser.java index def4394cc1..6d0fefae64 100644 --- a/jetty-fcgi/fcgi-client/src/main/java/org/eclipse/jetty/fcgi/parser/ResponseContentParser.java +++ b/jetty-fcgi/fcgi-client/src/main/java/org/eclipse/jetty/fcgi/parser/ResponseContentParser.java @@ -35,8 +35,8 @@ import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; /** - * <p>The parser for STDOUT type frames.</p> - * <p>STDOUT frames contain both the HTTP headers (but not the response line) + * <p>The parser for STDOUT type frame bodies.</p> + * <p>STDOUT frame bodies contain both the HTTP headers (but not the response line) * and the HTTP content (either Content-Length delimited or chunked).</p> * <p>For this reason, a special HTTP parser is used to parse the frames body. * This special HTTP parser is configured to skip the response line, and to @@ -99,12 +99,12 @@ public class ResponseContentParser extends StreamContentParser public boolean parse(ByteBuffer buffer) { - if (LOG.isDebugEnabled()) - LOG.debug("Response {} {} content {} {}", request, FCGI.StreamType.STD_OUT, state, buffer); - int remaining = buffer.remaining(); while (remaining > 0) { + if (LOG.isDebugEnabled()) + LOG.debug("Response {} {}, state {} {}", request, FCGI.StreamType.STD_OUT, state, buffer); + switch (state) { case HEADERS: diff --git a/jetty-fcgi/fcgi-client/src/main/java/org/eclipse/jetty/fcgi/parser/StreamContentParser.java b/jetty-fcgi/fcgi-client/src/main/java/org/eclipse/jetty/fcgi/parser/StreamContentParser.java index ae7f7bc041..70602a626a 100644 --- a/jetty-fcgi/fcgi-client/src/main/java/org/eclipse/jetty/fcgi/parser/StreamContentParser.java +++ b/jetty-fcgi/fcgi-client/src/main/java/org/eclipse/jetty/fcgi/parser/StreamContentParser.java @@ -25,8 +25,8 @@ import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; /** - * <p>A stream content parser parses frames of type STDIN, STDOUT and STDERR.</p> - * <p>STDOUT frames are handled specially by {@link ResponseContentParser}. + * <p>A stream content parser parses frame bodies of type STDIN, STDOUT and STDERR.</p> + * <p>STDOUT frame bodies are handled specially by {@link ResponseContentParser}. */ public class StreamContentParser extends ContentParser { diff --git a/jetty-fcgi/fcgi-server/src/main/java/org/eclipse/jetty/fcgi/server/proxy/FastCGIProxyServlet.java b/jetty-fcgi/fcgi-server/src/main/java/org/eclipse/jetty/fcgi/server/proxy/FastCGIProxyServlet.java index d80124e6e5..8587042304 100644 --- a/jetty-fcgi/fcgi-server/src/main/java/org/eclipse/jetty/fcgi/server/proxy/FastCGIProxyServlet.java +++ b/jetty-fcgi/fcgi-server/src/main/java/org/eclipse/jetty/fcgi/server/proxy/FastCGIProxyServlet.java @@ -20,8 +20,11 @@ package org.eclipse.jetty.fcgi.server.proxy; import java.net.URI; import java.util.List; +import java.util.TreeMap; import java.util.regex.Matcher; import java.util.regex.Pattern; +import java.util.stream.Collectors; + import javax.servlet.RequestDispatcher; import javax.servlet.ServletConfig; import javax.servlet.ServletException; @@ -32,6 +35,7 @@ import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.client.api.Request; import org.eclipse.jetty.fcgi.FCGI; import org.eclipse.jetty.fcgi.client.http.HttpClientTransportOverFCGI; +import org.eclipse.jetty.http.HttpField; import org.eclipse.jetty.http.HttpFields; import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpScheme; @@ -66,6 +70,7 @@ public class FastCGIProxyServlet extends AsyncProxyServlet.Transparent { public static final String SCRIPT_ROOT_INIT_PARAM = "scriptRoot"; public static final String SCRIPT_PATTERN_INIT_PARAM = "scriptPattern"; + public static final String ORIGINAL_URI_ATTRIBUTE_INIT_PARAM = "originalURIAttribute"; public static final String FASTCGI_HTTPS_INIT_PARAM = "fastCGI.HTTPS"; private static final String REMOTE_ADDR_ATTRIBUTE = FastCGIProxyServlet.class.getName() + ".remoteAddr"; @@ -77,6 +82,7 @@ public class FastCGIProxyServlet extends AsyncProxyServlet.Transparent private static final String REQUEST_URI_ATTRIBUTE = FastCGIProxyServlet.class.getName() + ".requestURI"; private Pattern scriptPattern; + private String originalURIAttribute; private boolean fcgiHTTPS; @Override @@ -89,6 +95,8 @@ public class FastCGIProxyServlet extends AsyncProxyServlet.Transparent value = "(.+?\\.php)"; scriptPattern = Pattern.compile(value); + originalURIAttribute = getInitParameter(ORIGINAL_URI_ATTRIBUTE_INIT_PARAM); + fcgiHTTPS = Boolean.parseBoolean(getInitParameter(FASTCGI_HTTPS_INIT_PARAM)); } @@ -110,24 +118,33 @@ public class FastCGIProxyServlet extends AsyncProxyServlet.Transparent proxyRequest.attribute(SERVER_NAME_ATTRIBUTE, request.getServerName()); proxyRequest.attribute(SERVER_ADDR_ATTRIBUTE, request.getLocalAddr()); proxyRequest.attribute(SERVER_PORT_ATTRIBUTE, String.valueOf(request.getLocalPort())); - proxyRequest.attribute(SCHEME_ATTRIBUTE, request.getScheme()); - // If we are forwarded or included, retain the original request URI. - String originalPath = (String)request.getAttribute(RequestDispatcher.FORWARD_REQUEST_URI); - String originalQuery = (String)request.getAttribute(RequestDispatcher.FORWARD_QUERY_STRING); - if (originalPath == null) + // Has the original URI been rewritten ? + String originalURI = null; + if (originalURIAttribute != null) + originalURI = (String)request.getAttribute(originalURIAttribute); + + if (originalURI == null) { - originalPath = (String)request.getAttribute(RequestDispatcher.INCLUDE_REQUEST_URI); - originalQuery = (String)request.getAttribute(RequestDispatcher.INCLUDE_QUERY_STRING); + // If we are forwarded or included, retain the original request URI. + String originalPath = (String)request.getAttribute(RequestDispatcher.FORWARD_REQUEST_URI); + String originalQuery = (String)request.getAttribute(RequestDispatcher.FORWARD_QUERY_STRING); + if (originalPath == null) + { + originalPath = (String)request.getAttribute(RequestDispatcher.INCLUDE_REQUEST_URI); + originalQuery = (String)request.getAttribute(RequestDispatcher.INCLUDE_QUERY_STRING); + } + if (originalPath != null) + { + originalURI = originalPath; + if (originalQuery != null) + originalURI += "?" + originalQuery; + } } - if (originalPath != null) - { - String originalURI = originalPath; - if (originalQuery != null) - originalURI += "?" + originalQuery; + + if (originalURI != null) proxyRequest.attribute(REQUEST_URI_ATTRIBUTE, originalURI); - } // If the Host header is missing, add it. if (!proxyRequest.getHeaders().containsKey(HttpHeader.HOST.asString())) @@ -212,6 +229,16 @@ public class FastCGIProxyServlet extends AsyncProxyServlet.Transparent { super.customize(request, fastCGIHeaders); customizeFastCGIHeaders(request, fastCGIHeaders); + if (_log.isDebugEnabled()) + { + TreeMap<String, String> fcgi = new TreeMap<>(); + for (HttpField field : fastCGIHeaders) + fcgi.put(field.getName(), field.getValue()); + String eol = System.lineSeparator(); + _log.debug("FastCGI variables{}{}", eol, fcgi.entrySet().stream() + .map(entry -> String.format("%s: %s", entry.getKey(), entry.getValue())) + .collect(Collectors.joining(eol))); + } } } } diff --git a/jetty-gcloud/gcloud-session-manager/src/main/java/org/eclipse/jetty/gcloud/session/GCloudSessionManager.java b/jetty-gcloud/gcloud-session-manager/src/main/java/org/eclipse/jetty/gcloud/session/GCloudSessionManager.java index b6b39a9bba..a6137e47ea 100644 --- a/jetty-gcloud/gcloud-session-manager/src/main/java/org/eclipse/jetty/gcloud/session/GCloudSessionManager.java +++ b/jetty-gcloud/gcloud-session-manager/src/main/java/org/eclipse/jetty/gcloud/session/GCloudSessionManager.java @@ -829,6 +829,7 @@ public class GCloudSessionManager extends AbstractSessionManager if (memSession == null) { memSession = session; + _sessionsStats.increment(); } //final check @@ -1008,6 +1009,7 @@ public class GCloudSessionManager extends AbstractSessionManager { //indicate that the session was reinflated session.didActivate(); + _sessionsStats.increment(); LOG.debug("getSession({}): loaded session from cluster", idInCluster); } return session; diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpURI.java b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpURI.java index 5da5dd4512..d5cc83519c 100644 --- a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpURI.java +++ b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpURI.java @@ -119,6 +119,7 @@ public class HttpURI public HttpURI(HttpURI uri) { this(uri._scheme,uri._host,uri._port,uri._path,uri._param,uri._query,uri._fragment); + _uri=uri._uri; } /* ------------------------------------------------------------ */ diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/MetaData.java b/jetty-http/src/main/java/org/eclipse/jetty/http/MetaData.java index b4038cbdb9..43bc07d794 100644 --- a/jetty-http/src/main/java/org/eclipse/jetty/http/MetaData.java +++ b/jetty-http/src/main/java/org/eclipse/jetty/http/MetaData.java @@ -158,7 +158,6 @@ public class MetaData implements Iterable<HttpField> this(request.getMethod(),new HttpURI(request.getURI()), request.getVersion(), new HttpFields(request.getFields()), request.getContentLength()); } - // TODO MetaData should be immuttable!!! public void recycle() { super.recycle(); diff --git a/jetty-http2/http2-client/src/test/java/org/eclipse/jetty/http2/client/ProxyProtocolTest.java b/jetty-http2/http2-client/src/test/java/org/eclipse/jetty/http2/client/ProxyProtocolTest.java index 650cc26746..f72199e465 100644 --- a/jetty-http2/http2-client/src/test/java/org/eclipse/jetty/http2/client/ProxyProtocolTest.java +++ b/jetty-http2/http2-client/src/test/java/org/eclipse/jetty/http2/client/ProxyProtocolTest.java @@ -48,6 +48,7 @@ import org.eclipse.jetty.server.ServerConnector; import org.eclipse.jetty.server.handler.AbstractHandler; import org.eclipse.jetty.util.FuturePromise; import org.eclipse.jetty.util.Promise; +import org.eclipse.jetty.util.TypeUtil; import org.junit.After; import org.junit.Assert; import org.junit.Test; @@ -80,13 +81,25 @@ public class ProxyProtocolTest } @Test - public void test_PROXY_GET() throws Exception + public void test_PROXY_GET_v1() throws Exception { startServer(new AbstractHandler() { @Override public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { + try + { + Assert.assertEquals("1.2.3.4",request.getRemoteAddr()); + Assert.assertEquals(1111,request.getRemotePort()); + Assert.assertEquals("5.6.7.8",request.getLocalAddr()); + Assert.assertEquals(2222,request.getLocalPort()); + } + catch(Throwable th) + { + th.printStackTrace(); + response.setStatus(500); + } baseRequest.setHandled(true); } }); @@ -118,4 +131,56 @@ public class ProxyProtocolTest }); Assert.assertTrue(latch.await(5, TimeUnit.SECONDS)); } + + @Test + public void test_PROXY_GET_v2() throws Exception + { + startServer(new AbstractHandler() + { + @Override + public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException + { + try + { + Assert.assertEquals("10.0.0.4",request.getRemoteAddr()); + Assert.assertEquals(33824,request.getRemotePort()); + Assert.assertEquals("10.0.0.4",request.getLocalAddr()); + Assert.assertEquals(8888,request.getLocalPort()); + } + catch(Throwable th) + { + th.printStackTrace(); + response.setStatus(500); + } + baseRequest.setHandled(true); + } + }); + + String request1 = "0D0A0D0A000D0A515549540A211100140A0000040A000004842022B82000050000000000"; + SocketChannel channel = SocketChannel.open(); + channel.connect(new InetSocketAddress("localhost", connector.getLocalPort())); + channel.write(ByteBuffer.wrap(TypeUtil.fromHexString(request1))); + + FuturePromise<Session> promise = new FuturePromise<>(); + client.accept(null, channel, new Session.Listener.Adapter(), promise); + Session session = promise.get(5, TimeUnit.SECONDS); + + HttpFields fields = new HttpFields(); + String uri = "http://localhost:" + connector.getLocalPort() + "/"; + MetaData.Request metaData = new MetaData.Request("GET", new HttpURI(uri), HttpVersion.HTTP_2, fields); + HeadersFrame frame = new HeadersFrame(metaData, null, true); + CountDownLatch latch = new CountDownLatch(1); + session.newStream(frame, new Promise.Adapter<>(), new Stream.Listener.Adapter() + { + @Override + public void onHeaders(Stream stream, HeadersFrame frame) + { + MetaData.Response response = (MetaData.Response)frame.getMetaData(); + Assert.assertEquals(HttpStatus.OK_200, response.getStatus()); + if (frame.isEndStream()) + latch.countDown(); + } + }); + Assert.assertTrue(latch.await(5, TimeUnit.SECONDS)); + } } diff --git a/jetty-http2/http2-client/src/test/java/org/eclipse/jetty/http2/client/PushCacheFilterTest.java b/jetty-http2/http2-client/src/test/java/org/eclipse/jetty/http2/client/PushCacheFilterTest.java index 4088d95d39..1037b3bd65 100644 --- a/jetty-http2/http2-client/src/test/java/org/eclipse/jetty/http2/client/PushCacheFilterTest.java +++ b/jetty-http2/http2-client/src/test/java/org/eclipse/jetty/http2/client/PushCacheFilterTest.java @@ -695,6 +695,10 @@ public class PushCacheFilterTest extends AbstractTest @Override public Stream.Listener onPush(Stream stream, PushPromiseFrame frame) { + MetaData metaData = frame.getMetaData(); + Assert.assertTrue(metaData instanceof MetaData.Request); + MetaData.Request pushedRequest = (MetaData.Request)metaData; + Assert.assertEquals(servletPath + secondaryResource, pushedRequest.getURI().getPathQuery()); return new Adapter() { @Override diff --git a/jetty-http2/http2-http-client-transport/src/main/java/org/eclipse/jetty/http2/client/http/HttpDestinationOverHTTP2.java b/jetty-http2/http2-http-client-transport/src/main/java/org/eclipse/jetty/http2/client/http/HttpDestinationOverHTTP2.java index b83d52371a..114df6f3f6 100644 --- a/jetty-http2/http2-http-client-transport/src/main/java/org/eclipse/jetty/http2/client/http/HttpDestinationOverHTTP2.java +++ b/jetty-http2/http2-http-client-transport/src/main/java/org/eclipse/jetty/http2/client/http/HttpDestinationOverHTTP2.java @@ -22,8 +22,9 @@ import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.client.HttpExchange; import org.eclipse.jetty.client.MultiplexHttpDestination; import org.eclipse.jetty.client.Origin; +import org.eclipse.jetty.client.api.Connection; -public class HttpDestinationOverHTTP2 extends MultiplexHttpDestination<HttpConnectionOverHTTP2> +public class HttpDestinationOverHTTP2 extends MultiplexHttpDestination { public HttpDestinationOverHTTP2(HttpClient client, Origin origin) { @@ -31,8 +32,8 @@ public class HttpDestinationOverHTTP2 extends MultiplexHttpDestination<HttpConne } @Override - protected void send(HttpConnectionOverHTTP2 connection, HttpExchange exchange) + protected void send(Connection connection, HttpExchange exchange) { - connection.send(exchange); + ((HttpConnectionOverHTTP2)connection).send(exchange); } } diff --git a/jetty-infinispan/src/main/java/org/eclipse/jetty/session/infinispan/InfinispanSessionManager.java b/jetty-infinispan/src/main/java/org/eclipse/jetty/session/infinispan/InfinispanSessionManager.java index 039263a46c..072d0f1a77 100644 --- a/jetty-infinispan/src/main/java/org/eclipse/jetty/session/infinispan/InfinispanSessionManager.java +++ b/jetty-infinispan/src/main/java/org/eclipse/jetty/session/infinispan/InfinispanSessionManager.java @@ -664,7 +664,7 @@ public class InfinispanSessionManager extends AbstractSessionManager for (String candidateId:candidateIds) { if (LOG.isDebugEnabled()) - LOG.debug("Session {} expired ", candidateId); + LOG.debug("Session {} candidate for expiry", candidateId); Session candidateSession = _sessions.get(candidateId); if (candidateSession != null) @@ -691,6 +691,7 @@ public class InfinispanSessionManager extends AbstractSessionManager if (LOG.isDebugEnabled()) LOG.debug("Session({}) not local to this session manager, removing from local memory", candidateId); candidateSession.willPassivate(); _sessions.remove(candidateSession.getClusterId()); + _sessionsStats.decrement(); } } @@ -870,6 +871,7 @@ public class InfinispanSessionManager extends AbstractSessionManager { //indicate that the session was reinflated session.didActivate(); + _sessionsStats.increment(); LOG.debug("getSession({}): loaded session from cluster", idInCluster); } return session; diff --git a/jetty-io/src/main/java/org/eclipse/jetty/io/AbstractEndPoint.java b/jetty-io/src/main/java/org/eclipse/jetty/io/AbstractEndPoint.java index 0444f788ab..5b49c8e44c 100644 --- a/jetty-io/src/main/java/org/eclipse/jetty/io/AbstractEndPoint.java +++ b/jetty-io/src/main/java/org/eclipse/jetty/io/AbstractEndPoint.java @@ -182,7 +182,7 @@ public abstract class AbstractEndPoint extends IdleTimeout implements EndPoint Connection old_connection = getConnection(); if (LOG.isDebugEnabled()) - LOG.debug("{} upgradeing from {} to {}", this, old_connection, newConnection); + LOG.debug("{} upgrading from {} to {}", this, old_connection, newConnection); ByteBuffer prefilled = (old_connection instanceof Connection.UpgradeFrom) ?((Connection.UpgradeFrom)old_connection).onUpgradeFrom():null; diff --git a/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/AbstractJettyMojo.java b/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/AbstractJettyMojo.java index 402ad48243..caa79c57a9 100644 --- a/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/AbstractJettyMojo.java +++ b/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/AbstractJettyMojo.java @@ -443,6 +443,9 @@ public abstract class AbstractJettyMojo extends AbstractMojo //set up a RequestLog if one is provided and the handle structure ServerSupport.configureHandlers(server, this.requestLog); + + //Set up list of default Configurations to apply to a webapp + ServerSupport.configureDefaultConfigurationClasses(server); configureWebApplication(); ServerSupport.addWebApplication(server, webApp); diff --git a/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/JettyRunForkedMojo.java b/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/JettyRunForkedMojo.java index d4ac65e2ac..22cf2f21dc 100644 --- a/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/JettyRunForkedMojo.java +++ b/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/JettyRunForkedMojo.java @@ -244,6 +244,8 @@ public class JettyRunForkedMojo extends JettyRunMojo //ensure handler structure enabled ServerSupport.configureHandlers(server, null); + + ServerSupport.configureDefaultConfigurationClasses(server); //ensure config of the webapp based on settings in plugin configureWebApplication(); diff --git a/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/JettyWebAppContext.java b/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/JettyWebAppContext.java index 8f1f343584..0a2dd8cc3b 100644 --- a/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/JettyWebAppContext.java +++ b/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/JettyWebAppContext.java @@ -72,23 +72,25 @@ public class JettyWebAppContext extends WebAppContext private static final String DEFAULT_CONTAINER_INCLUDE_JAR_PATTERN = ".*/javax.servlet-[^/]*\\.jar$|.*/servlet-api-[^/]*\\.jar$|.*javax.servlet.jsp.jstl-[^/]*\\.jar|.*taglibs-standard-impl-.*\\.jar"; private static final String WEB_INF_CLASSES_PREFIX = "/WEB-INF/classes"; private static final String WEB_INF_LIB_PREFIX = "/WEB-INF/lib"; + + + public static final String[] DEFAULT_CONFIGURATION_CLASSES = { + "org.eclipse.jetty.maven.plugin.MavenWebInfConfiguration", + "org.eclipse.jetty.webapp.WebXmlConfiguration", + "org.eclipse.jetty.webapp.MetaInfConfiguration", + "org.eclipse.jetty.webapp.FragmentConfiguration", + "org.eclipse.jetty.plus.webapp.EnvConfiguration", + "org.eclipse.jetty.plus.webapp.PlusConfiguration", + "org.eclipse.jetty.annotations.AnnotationConfiguration", + "org.eclipse.jetty.webapp.JettyWebXmlConfiguration" + }; - private final Configuration[] _defaultConfigurations = { - new MavenWebInfConfiguration(), - new WebXmlConfiguration(), - new MetaInfConfiguration(), - new FragmentConfiguration(), - new EnvConfiguration(), - new PlusConfiguration(), - new AnnotationConfiguration(), - new JettyWebXmlConfiguration() - }; - private final Configuration[] _quickStartConfigurations = { - new MavenQuickStartConfiguration(), - new EnvConfiguration(), - new PlusConfiguration(), - new JettyWebXmlConfiguration() + private final String[] QUICKSTART_CONFIGURATION_CLASSES = { + "org.eclipse.jetty.maven.plugin.MavenQuickStartConfiguration", + "org.eclipse.jetty.plus.webapp.EnvConfiguration", + "org.eclipse.jetty.plus.webapp.PlusConfiguration", + "org.eclipse.jetty.webapp.JettyWebXmlConfiguration" }; private File _classes = null; @@ -100,6 +102,7 @@ public class JettyWebAppContext extends WebAppContext private String _jettyEnvXml; private List<Overlay> _overlays; private Resource _quickStartWebXml; + @@ -338,25 +341,17 @@ public class JettyWebAppContext extends WebAppContext { //choose if this will be a quickstart or normal start if (!isGenerateQuickStart() && getQuickStartWebDescriptor() != null) - setConfigurations(_quickStartConfigurations); - else { - setConfigurations(_defaultConfigurations); + setConfigurationClasses(QUICKSTART_CONFIGURATION_CLASSES); + } + else + { if (isGenerateQuickStart()) { _preconfigProcessor = new PreconfigureDescriptorProcessor(); getMetaData().addDescriptorProcessor(_preconfigProcessor); } } - - //inject configurations with config from maven plugin - for (Configuration c:getConfigurations()) - { - if (c instanceof EnvConfiguration && getJettyEnvXml() != null) - ((EnvConfiguration)c).setJettyEnvXml(Resource.toURL(new File(getJettyEnvXml()))); - else if (c instanceof MavenQuickStartConfiguration && getQuickStartWebDescriptor() != null) - ((MavenQuickStartConfiguration)c).setQuickStartWebXml(getQuickStartWebDescriptor()); - } //Set up the pattern that tells us where the jars are that need scanning @@ -404,6 +399,22 @@ public class JettyWebAppContext extends WebAppContext } + @Override + protected void loadConfigurations() throws Exception + { + super.loadConfigurations(); + + //inject configurations with config from maven plugin + for (Configuration c:getConfigurations()) + { + if (c instanceof EnvConfiguration && getJettyEnvXml() != null) + ((EnvConfiguration)c).setJettyEnvXml(Resource.toURL(new File(getJettyEnvXml()))); + else if (c instanceof MavenQuickStartConfiguration && getQuickStartWebDescriptor() != null) + ((MavenQuickStartConfiguration)c).setQuickStartWebXml(getQuickStartWebDescriptor()); + } + } + + /* ------------------------------------------------------------ */ public void doStop () throws Exception { diff --git a/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/ServerSupport.java b/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/ServerSupport.java index babfe6830a..407a17f2a9 100644 --- a/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/ServerSupport.java +++ b/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/ServerSupport.java @@ -35,6 +35,7 @@ import org.eclipse.jetty.server.handler.DefaultHandler; import org.eclipse.jetty.server.handler.HandlerCollection; import org.eclipse.jetty.server.handler.RequestLogHandler; import org.eclipse.jetty.util.resource.Resource; +import org.eclipse.jetty.webapp.Configuration; import org.eclipse.jetty.webapp.WebAppContext; import org.eclipse.jetty.xml.XmlConfiguration; @@ -46,6 +47,13 @@ import org.eclipse.jetty.xml.XmlConfiguration; */ public class ServerSupport { + + public static void configureDefaultConfigurationClasses (Server server) + { + server.setAttribute(Configuration.ATTR, JettyWebAppContext.DEFAULT_CONFIGURATION_CLASSES); + } + + /** * Set up the handler structure to receive a webapp. * Also put in a DefaultHandler so we get a nice page diff --git a/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/Starter.java b/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/Starter.java index 0f9e361cbd..a5b94536ad 100644 --- a/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/Starter.java +++ b/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/Starter.java @@ -125,7 +125,10 @@ public class Starter //check if contexts already configured, create if not ServerSupport.configureHandlers(server, null); - + + //Set up list of default Configurations to apply to a webapp + ServerSupport.configureDefaultConfigurationClasses(server); + webApp = new JettyWebAppContext(); //configure webapp from properties file describing unassembled webapp diff --git a/jetty-nosql/src/main/java/org/eclipse/jetty/nosql/NoSqlSessionManager.java b/jetty-nosql/src/main/java/org/eclipse/jetty/nosql/NoSqlSessionManager.java index 7d5e9dca58..51b8d60912 100644 --- a/jetty-nosql/src/main/java/org/eclipse/jetty/nosql/NoSqlSessionManager.java +++ b/jetty-nosql/src/main/java/org/eclipse/jetty/nosql/NoSqlSessionManager.java @@ -96,7 +96,10 @@ public abstract class NoSqlSessionManager extends AbstractSessionManager impleme session=race; } else + { __log.debug("session loaded ", idInCluster); + _sessionsStats.increment(); + } //check if the session we just loaded has actually expired, maybe while we weren't running if (getMaxInactiveInterval() > 0 && session.getAccessed() > 0 && ((getMaxInactiveInterval()*1000L)+session.getAccessed()) < System.currentTimeMillis()) diff --git a/jetty-nosql/src/main/java/org/eclipse/jetty/nosql/mongodb/MongoSessionIdManager.java b/jetty-nosql/src/main/java/org/eclipse/jetty/nosql/mongodb/MongoSessionIdManager.java index d822ea7926..894a3fda10 100644 --- a/jetty-nosql/src/main/java/org/eclipse/jetty/nosql/mongodb/MongoSessionIdManager.java +++ b/jetty-nosql/src/main/java/org/eclipse/jetty/nosql/mongodb/MongoSessionIdManager.java @@ -206,7 +206,7 @@ public class MongoSessionIdManager extends AbstractSessionIdManager protected void scavenge() { long now = System.currentTimeMillis(); - __log.debug("SessionIdManager:scavenge:at {}", now); + __log.debug(getWorkerName()+":SessionIdManager:scavenge:at {}", now); /* * run a query returning results that: * - are in the known list of sessionIds @@ -258,7 +258,7 @@ public class MongoSessionIdManager extends AbstractSessionIdManager for ( DBObject session : checkSessions ) { - __log.debug("SessionIdManager:scavenge: expiring session {}", (String)session.get(MongoSessionManager.__ID)); + __log.debug(getWorkerName()+":SessionIdManager:scavenge: {} expiring session {}", atTime,(String)session.get(MongoSessionManager.__ID)); expireAll((String)session.get(MongoSessionManager.__ID)); } } diff --git a/jetty-nosql/src/main/java/org/eclipse/jetty/nosql/mongodb/MongoSessionManager.java b/jetty-nosql/src/main/java/org/eclipse/jetty/nosql/mongodb/MongoSessionManager.java index df8f917188..8c44e9fa47 100644 --- a/jetty-nosql/src/main/java/org/eclipse/jetty/nosql/mongodb/MongoSessionManager.java +++ b/jetty-nosql/src/main/java/org/eclipse/jetty/nosql/mongodb/MongoSessionManager.java @@ -268,7 +268,9 @@ public class MongoSessionManager extends NoSqlSessionManager if (currentMaxIdle != null && getMaxInactiveInterval() > 0 && getMaxInactiveInterval() < currentMaxIdle) sets.put(__MAX_IDLE, getMaxInactiveInterval()); if (currentExpiry != null && expiry > 0 && expiry != currentExpiry) + { sets.put(__EXPIRY, expiry); + } } } diff --git a/jetty-proxy/src/main/java/org/eclipse/jetty/proxy/ConnectHandler.java b/jetty-proxy/src/main/java/org/eclipse/jetty/proxy/ConnectHandler.java index 71a75bc899..7fd0b777fc 100644 --- a/jetty-proxy/src/main/java/org/eclipse/jetty/proxy/ConnectHandler.java +++ b/jetty-proxy/src/main/java/org/eclipse/jetty/proxy/ConnectHandler.java @@ -18,6 +18,7 @@ package org.eclipse.jetty.proxy; +import java.io.Closeable; import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; @@ -51,6 +52,7 @@ import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.handler.HandlerWrapper; import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.Callback; +import org.eclipse.jetty.util.Promise; import org.eclipse.jetty.util.TypeUtil; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; @@ -160,21 +162,17 @@ public class ConnectHandler extends HandlerWrapper protected void doStart() throws Exception { if (executor == null) - { - setExecutor(getServer().getThreadPool()); - } + executor = getServer().getThreadPool(); + if (scheduler == null) - { - setScheduler(new ScheduledExecutorScheduler()); - addBean(getScheduler()); - } + addBean(scheduler = new ScheduledExecutorScheduler()); + if (bufferPool == null) - { - setByteBufferPool(new MappedByteBufferPool()); - addBean(getByteBufferPool()); - } + addBean(bufferPool = new MappedByteBufferPool()); + addBean(selector = newSelectorManager()); selector.setConnectTimeout(getConnectTimeout()); + super.doStart(); } @@ -191,16 +189,8 @@ public class ConnectHandler extends HandlerWrapper String serverAddress = request.getRequestURI(); if (LOG.isDebugEnabled()) LOG.debug("CONNECT request for {}", serverAddress); - try - { - handleConnect(baseRequest, request, response, serverAddress); - } - catch (Exception x) - { - // TODO - LOG.warn("ConnectHandler " + baseRequest.getHttpURI() + " " + x); - LOG.debug(x); - } + + handleConnect(baseRequest, request, response, serverAddress); } else { @@ -249,32 +239,40 @@ public class ConnectHandler extends HandlerWrapper return; } - SocketChannel channel = SocketChannel.open(); - channel.socket().setTcpNoDelay(true); - channel.configureBlocking(false); - - AsyncContext asyncContext = request.startAsync(); - asyncContext.setTimeout(0); - HttpTransport transport = baseRequest.getHttpChannel().getHttpTransport(); - // TODO Handle CONNECT over HTTP2! if (!(transport instanceof HttpConnection)) { if (LOG.isDebugEnabled()) - LOG.debug("CONNECT forbidden for {}", transport); + LOG.debug("CONNECT not supported for {}", transport); sendConnectResponse(request, response, HttpServletResponse.SC_FORBIDDEN); return; } - InetSocketAddress address = newConnectAddress(host, port); + AsyncContext asyncContext = request.startAsync(); + asyncContext.setTimeout(0); + if (LOG.isDebugEnabled()) - LOG.debug("Connecting to {}", address); - ConnectContext connectContext = new ConnectContext(request, response, asyncContext, (HttpConnection)transport); - if (channel.connect(address)) - selector.accept(channel, connectContext); - else - selector.connect(channel, connectContext); + LOG.debug("Connecting to {}:{}", host, port); + + connectToServer(request, host, port, new Promise<SocketChannel>() + { + @Override + public void succeeded(SocketChannel channel) + { + ConnectContext connectContext = new ConnectContext(request, response, asyncContext, (HttpConnection)transport); + if (channel.isConnected()) + selector.accept(channel, connectContext); + else + selector.connect(channel, connectContext); + } + + @Override + public void failed(Throwable x) + { + onConnectFailure(request, response, asyncContext, x); + } + }); } catch (Exception x) { @@ -282,37 +280,59 @@ public class ConnectHandler extends HandlerWrapper } } - /* ------------------------------------------------------------ */ - /** Create the address the connect channel will connect to. - * @param host The host from the connect request - * @param port The port from the connect request + protected void connectToServer(HttpServletRequest request, String host, int port, Promise<SocketChannel> promise) + { + SocketChannel channel = null; + try + { + channel = SocketChannel.open(); + channel.socket().setTcpNoDelay(true); + channel.configureBlocking(false); + InetSocketAddress address = newConnectAddress(host, port); + channel.connect(address); + promise.succeeded(channel); + } + catch (Throwable x) + { + close(channel); + promise.failed(x); + } + } + + private void close(Closeable closeable) + { + try + { + if (closeable != null) + closeable.close(); + } + catch (Throwable x) + { + LOG.ignore(x); + } + } + + /** + * Creates the server address to connect to. + * + * @param host The host from the CONNECT request + * @param port The port from the CONNECT request * @return The InetSocketAddress to connect to. */ protected InetSocketAddress newConnectAddress(String host, int port) { return new InetSocketAddress(host, port); } - + protected void onConnectSuccess(ConnectContext connectContext, UpstreamConnection upstreamConnection) { - HttpConnection httpConnection = connectContext.getHttpConnection(); - ByteBuffer requestBuffer = httpConnection.getRequestBuffer(); - ByteBuffer buffer = BufferUtil.EMPTY_BUFFER; - int remaining = requestBuffer.remaining(); - if (remaining > 0) - { - buffer = bufferPool.acquire(remaining, requestBuffer.isDirect()); - BufferUtil.flipToFill(buffer); - buffer.put(requestBuffer); - buffer.flip(); - } - ConcurrentMap<String, Object> context = connectContext.getContext(); HttpServletRequest request = connectContext.getRequest(); prepareContext(request, context); + HttpConnection httpConnection = connectContext.getHttpConnection(); EndPoint downstreamEndPoint = httpConnection.getEndPoint(); - DownstreamConnection downstreamConnection = newDownstreamConnection(downstreamEndPoint, context, buffer); + DownstreamConnection downstreamConnection = newDownstreamConnection(downstreamEndPoint, context); downstreamConnection.setInputBufferSize(getBufferSize()); upstreamConnection.setConnection(downstreamConnection); @@ -324,6 +344,7 @@ public class ConnectHandler extends HandlerWrapper sendConnectResponse(request, response, HttpServletResponse.SC_OK); upgradeConnection(request, response, downstreamConnection); + connectContext.getAsyncContext().complete(); } @@ -349,7 +370,8 @@ public class ConnectHandler extends HandlerWrapper } catch (IOException x) { - // TODO: nothing we can do, close the connection + if (LOG.isDebugEnabled()) + LOG.debug("Could not send CONNECT response", x); } } @@ -367,9 +389,9 @@ public class ConnectHandler extends HandlerWrapper return true; } - protected DownstreamConnection newDownstreamConnection(EndPoint endPoint, ConcurrentMap<String, Object> context, ByteBuffer buffer) + protected DownstreamConnection newDownstreamConnection(EndPoint endPoint, ConcurrentMap<String, Object> context) { - return new DownstreamConnection(endPoint, getExecutor(), getByteBufferPool(), context, buffer); + return new DownstreamConnection(endPoint, getExecutor(), getByteBufferPool(), context); } protected UpstreamConnection newUpstreamConnection(EndPoint endPoint, ConnectContext connectContext) @@ -396,13 +418,17 @@ public class ConnectHandler extends HandlerWrapper * * @param endPoint the endPoint to read from * @param buffer the buffer to read data into + * @param context the context information related to the connection * @return the number of bytes read (possibly 0 since the read is non-blocking) * or -1 if the channel has been closed remotely * @throws IOException if the endPoint cannot be read */ - protected int read(EndPoint endPoint, ByteBuffer buffer) throws IOException + protected int read(EndPoint endPoint, ByteBuffer buffer, ConcurrentMap<String, Object> context) throws IOException { - return endPoint.fill(buffer); + int read = endPoint.fill(buffer); + if (LOG.isDebugEnabled()) + LOG.debug("{} read {} bytes", this, read); + return read; } /** @@ -411,8 +437,9 @@ public class ConnectHandler extends HandlerWrapper * @param endPoint the endPoint to write to * @param buffer the buffer to write * @param callback the completion callback to invoke + * @param context the context information related to the connection */ - protected void write(EndPoint endPoint, ByteBuffer buffer, Callback callback) + protected void write(EndPoint endPoint, ByteBuffer buffer, Callback callback, ConcurrentMap<String, Object> context) { if (LOG.isDebugEnabled()) LOG.debug("{} writing {} bytes", this, buffer.remaining()); @@ -494,14 +521,9 @@ public class ConnectHandler extends HandlerWrapper @Override protected void connectionFailed(SocketChannel channel, final Throwable ex, final Object attachment) { - getExecutor().execute(new Runnable() - { - public void run() - { - ConnectContext connectContext = (ConnectContext)attachment; - onConnectFailure(connectContext.request, connectContext.response, connectContext.asyncContext, ex); - } - }); + close(channel); + ConnectContext connectContext = (ConnectContext)attachment; + onConnectFailure(connectContext.request, connectContext.response, connectContext.asyncContext, ex); } } @@ -561,37 +583,36 @@ public class ConnectHandler extends HandlerWrapper public void onOpen() { super.onOpen(); - getExecutor().execute(new Runnable() - { - public void run() - { - onConnectSuccess(connectContext, UpstreamConnection.this); - fillInterested(); - } - }); + onConnectSuccess(connectContext, UpstreamConnection.this); + fillInterested(); } @Override protected int read(EndPoint endPoint, ByteBuffer buffer) throws IOException { - return ConnectHandler.this.read(endPoint, buffer); + return ConnectHandler.this.read(endPoint, buffer, getContext()); } @Override protected void write(EndPoint endPoint, ByteBuffer buffer,Callback callback) { - ConnectHandler.this.write(endPoint, buffer, callback); + ConnectHandler.this.write(endPoint, buffer, callback, getContext()); } } - public class DownstreamConnection extends ProxyConnection + public class DownstreamConnection extends ProxyConnection implements Connection.UpgradeTo { - private final ByteBuffer buffer; + private ByteBuffer buffer; - public DownstreamConnection(EndPoint endPoint, Executor executor, ByteBufferPool bufferPool, ConcurrentMap<String, Object> context, ByteBuffer buffer) + public DownstreamConnection(EndPoint endPoint, Executor executor, ByteBufferPool bufferPool, ConcurrentMap<String, Object> context) { super(endPoint, executor, bufferPool, context); - this.buffer = buffer; + } + + @Override + public void onUpgradeTo(ByteBuffer buffer) + { + this.buffer = buffer == null ? BufferUtil.EMPTY_BUFFER : buffer; } @Override @@ -623,13 +644,13 @@ public class ConnectHandler extends HandlerWrapper @Override protected int read(EndPoint endPoint, ByteBuffer buffer) throws IOException { - return ConnectHandler.this.read(endPoint, buffer); + return ConnectHandler.this.read(endPoint, buffer, getContext()); } @Override protected void write(EndPoint endPoint, ByteBuffer buffer, Callback callback) { - ConnectHandler.this.write(endPoint, buffer, callback); + ConnectHandler.this.write(endPoint, buffer, callback, getContext()); } } } diff --git a/jetty-proxy/src/test/java/org/eclipse/jetty/proxy/ConnectHandlerTest.java b/jetty-proxy/src/test/java/org/eclipse/jetty/proxy/ConnectHandlerTest.java index f601975408..780e70686a 100644 --- a/jetty-proxy/src/test/java/org/eclipse/jetty/proxy/ConnectHandlerTest.java +++ b/jetty-proxy/src/test/java/org/eclipse/jetty/proxy/ConnectHandlerTest.java @@ -27,6 +27,8 @@ import java.io.OutputStream; import java.net.InetAddress; import java.net.Socket; import java.net.UnknownHostException; +import java.nio.ByteBuffer; +import java.nio.channels.SocketChannel; import java.nio.charset.StandardCharsets; import java.util.Locale; import java.util.concurrent.ConcurrentMap; @@ -36,12 +38,15 @@ import javax.servlet.ServletOutputStream; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import org.eclipse.jetty.io.EndPoint; import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; import org.eclipse.jetty.server.handler.AbstractHandler; import org.eclipse.jetty.toolchain.test.http.SimpleHttpResponse; import org.eclipse.jetty.util.B64Code; +import org.eclipse.jetty.util.Callback; +import org.eclipse.jetty.util.Promise; import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -631,12 +636,33 @@ public class ConnectHandlerTest extends AbstractConnectHandlerTest } @Override + protected void connectToServer(HttpServletRequest request, String host, int port, Promise<SocketChannel> promise) + { + Assert.assertEquals(contextValue, request.getAttribute(contextKey)); + super.connectToServer(request, host, port, promise); + } + + @Override protected void prepareContext(HttpServletRequest request, ConcurrentMap<String, Object> context) { // Transfer data from the HTTP request to the connection context Assert.assertEquals(contextValue, request.getAttribute(contextKey)); context.put(contextKey, request.getAttribute(contextKey)); } + + @Override + protected int read(EndPoint endPoint, ByteBuffer buffer, ConcurrentMap<String, Object> context) throws IOException + { + Assert.assertEquals(contextValue, context.get(contextKey)); + return super.read(endPoint, buffer, context); + } + + @Override + protected void write(EndPoint endPoint, ByteBuffer buffer, Callback callback, ConcurrentMap<String, Object> context) + { + Assert.assertEquals(contextValue, context.get(contextKey)); + super.write(endPoint, buffer, callback, context); + } }); proxy.start(); diff --git a/jetty-proxy/src/test/java/org/eclipse/jetty/proxy/ProxyServletTest.java b/jetty-proxy/src/test/java/org/eclipse/jetty/proxy/ProxyServletTest.java index 1f96b48a51..4d2dcc9233 100644 --- a/jetty-proxy/src/test/java/org/eclipse/jetty/proxy/ProxyServletTest.java +++ b/jetty-proxy/src/test/java/org/eclipse/jetty/proxy/ProxyServletTest.java @@ -61,6 +61,7 @@ import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import org.eclipse.jetty.client.DuplexConnectionPool; import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.client.HttpContentResponse; import org.eclipse.jetty.client.HttpProxy; @@ -1081,7 +1082,8 @@ public class ProxyServletTest Assert.assertEquals(-1, input.read()); HttpDestinationOverHTTP destination = (HttpDestinationOverHTTP)client.getDestination("http", "localhost", port); - Assert.assertEquals(0, destination.getConnectionPool().getIdleConnections().size()); + DuplexConnectionPool connectionPool = (DuplexConnectionPool)destination.getConnectionPool(); + Assert.assertEquals(0, connectionPool.getIdleConnections().size()); } @Test @@ -1154,7 +1156,8 @@ public class ProxyServletTest } HttpDestinationOverHTTP destination = (HttpDestinationOverHTTP)client.getDestination("http", "localhost", port); - Assert.assertEquals(0, destination.getConnectionPool().getIdleConnections().size()); + DuplexConnectionPool connectionPool = (DuplexConnectionPool)destination.getConnectionPool(); + Assert.assertEquals(0, connectionPool.getIdleConnections().size()); } @Test diff --git a/jetty-proxy/src/test/java/org/eclipse/jetty/proxy/ProxyTunnellingTest.java b/jetty-proxy/src/test/java/org/eclipse/jetty/proxy/ProxyTunnellingTest.java index e563962b6d..fd333cd88e 100644 --- a/jetty-proxy/src/test/java/org/eclipse/jetty/proxy/ProxyTunnellingTest.java +++ b/jetty-proxy/src/test/java/org/eclipse/jetty/proxy/ProxyTunnellingTest.java @@ -54,6 +54,7 @@ import org.eclipse.jetty.toolchain.test.MavenTestingUtils; import org.eclipse.jetty.toolchain.test.TestTracker; import org.eclipse.jetty.util.Promise; import org.eclipse.jetty.util.ssl.SslContextFactory; +import org.eclipse.jetty.util.thread.QueuedThreadPool; import org.hamcrest.Matchers; import org.junit.After; import org.junit.Assert; @@ -87,7 +88,10 @@ public class ProxyTunnellingTest sslContextFactory.setKeyStorePath(keyStorePath); sslContextFactory.setKeyStorePassword("storepwd"); sslContextFactory.setKeyManagerPassword("keypwd"); - server = new Server(); + + QueuedThreadPool serverThreads = new QueuedThreadPool(); + serverThreads.setName("server"); + server = new Server(serverThreads); serverConnector = new ServerConnector(server, sslContextFactory); server.addConnector(serverConnector); server.setHandler(handler); @@ -101,7 +105,9 @@ public class ProxyTunnellingTest protected void startProxy(ConnectHandler connectHandler) throws Exception { - proxy = new Server(); + QueuedThreadPool proxyThreads = new QueuedThreadPool(); + proxyThreads.setName("proxy"); + proxy = new Server(proxyThreads); proxyConnector = new ServerConnector(proxy); proxy.addConnector(proxyConnector); // Under Windows, it takes a while to detect that a connection @@ -136,7 +142,7 @@ public class ProxyTunnellingTest } } - @Test + @Test(timeout=60000) public void testOneExchangeViaSSL() throws Exception { startSSLServer(new ServerHandler()); @@ -167,7 +173,7 @@ public class ProxyTunnellingTest } } - @Test + @Test(timeout=60000) public void testTwoExchangesViaSSL() throws Exception { startSSLServer(new ServerHandler()); @@ -210,7 +216,7 @@ public class ProxyTunnellingTest } } - @Test + @Test(timeout=60000) public void testTwoConcurrentExchangesViaSSL() throws Exception { startSSLServer(new ServerHandler()); @@ -278,7 +284,7 @@ public class ProxyTunnellingTest } } - @Test + @Test(timeout=60000) public void testShortIdleTimeoutOverriddenByRequest() throws Exception { // Short idle timeout for HttpClient. @@ -331,7 +337,7 @@ public class ProxyTunnellingTest } } - @Test + @Test(timeout=60000) public void testProxyDown() throws Exception { startSSLServer(new ServerHandler()); @@ -363,7 +369,7 @@ public class ProxyTunnellingTest } } - @Test + @Test(timeout=60000) public void testServerDown() throws Exception { startSSLServer(new ServerHandler()); @@ -395,7 +401,7 @@ public class ProxyTunnellingTest } } - @Test + @Test(timeout=60000) public void testProxyClosesConnection() throws Exception { startSSLServer(new ServerHandler()); @@ -429,7 +435,7 @@ public class ProxyTunnellingTest } } - @Test + @Test(timeout=60000) @Ignore("External Proxy Server no longer stable enough for testing") public void testExternalProxy() throws Exception { diff --git a/jetty-rewrite/src/main/config/etc/jetty-rewrite.xml b/jetty-rewrite/src/main/config/etc/jetty-rewrite.xml index 63af3c21d7..65a5dea44a 100644 --- a/jetty-rewrite/src/main/config/etc/jetty-rewrite.xml +++ b/jetty-rewrite/src/main/config/etc/jetty-rewrite.xml @@ -19,31 +19,29 @@ <Set name="rewriteRequestURI"><Property name="jetty.rewrite.rewriteRequestURI" deprecated="rewrite.rewriteRequestURI" default="true"/></Set> <Set name="rewritePathInfo"><Property name="jetty.rewrite.rewritePathInfo" deprecated="rewrite.rewritePathInfo" default="false"/></Set> <Set name="originalPathAttribute"><Property name="jetty.rewrite.originalPathAttribute" deprecated="rewrite.originalPathAttribute" default="requestedPath"/></Set> - </New> - <!-- Set DispatcherTypes --> - <Set name="dispatcherTypes"> - <Array type="javax.servlet.DispatcherType"> - <Item><Call class="javax.servlet.DispatcherType" name="valueOf"><Arg>REQUEST</Arg></Call></Item> - <Item><Call class="javax.servlet.DispatcherType" name="valueOf"><Arg>ASYNC</Arg></Call></Item> - </Array> - </Set> - </Set> - - - - <!-- example rule --> - <!-- - <Call name="addRule"> - <Arg> - <New class="org.eclipse.jetty.rewrite.handler.HeaderPatternRule"> - <Set name="pattern">/favicon.ico</Set> - <Set name="name">Cache-Control</Set> - <Set name="value">Max-Age=3600,public</Set> - <Set name="terminating">true</Set> - </New> - </Arg> - </Call> - --> + <!-- Set DispatcherTypes --> + <Set name="dispatcherTypes"> + <Array type="javax.servlet.DispatcherType"> + <Item><Call class="javax.servlet.DispatcherType" name="valueOf"><Arg>REQUEST</Arg></Call></Item> + <Item><Call class="javax.servlet.DispatcherType" name="valueOf"><Arg>ASYNC</Arg></Call></Item> + </Array> + </Set> + + <!-- example rule --> + <!-- + <Call name="addRule"> + <Arg> + <New class="org.eclipse.jetty.rewrite.handler.HeaderPatternRule"> + <Set name="pattern">/favicon.ico</Set> + <Set name="name">Cache-Control</Set> + <Set name="value">Max-Age=3600,public</Set> + <Set name="terminating">true</Set> + </New> + </Arg> + </Call> + --> + </New> + </Set> </Configure> diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/AbstractNCSARequestLog.java b/jetty-server/src/main/java/org/eclipse/jetty/server/AbstractNCSARequestLog.java index ab46bd5217..036b5142ad 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/AbstractNCSARequestLog.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/AbstractNCSARequestLog.java @@ -142,7 +142,7 @@ public abstract class AbstractNCSARequestLog extends AbstractLifeCycle implement buf.append("] \""); append(buf,request.getMethod()); buf.append(' '); - append(buf,request.getHttpURI().toString()); + append(buf,request.getOriginalURI()); buf.append(' '); append(buf,request.getProtocol()); buf.append("\" "); diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/Dispatcher.java b/jetty-server/src/main/java/org/eclipse/jetty/server/Dispatcher.java index 2bb8739591..1a04bd749a 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/Dispatcher.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/Dispatcher.java @@ -31,10 +31,7 @@ import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import org.eclipse.jetty.http.HttpFields; -import org.eclipse.jetty.http.HttpMethod; import org.eclipse.jetty.http.HttpURI; -import org.eclipse.jetty.http.MetaData; import org.eclipse.jetty.server.handler.ContextHandler; import org.eclipse.jetty.util.Attributes; import org.eclipse.jetty.util.MultiMap; @@ -222,28 +219,6 @@ public class Dispatcher implements RequestDispatcher } } - @Deprecated - public void push(ServletRequest request) - { - Request baseRequest = Request.getBaseRequest(request); - HttpFields fields = new HttpFields(baseRequest.getHttpFields()); - - String query=baseRequest.getQueryString(); - if (_uri.hasQuery()) - { - if (query==null) - query=_uri.getQuery(); - else - query=query+"&"+_uri.getQuery(); // TODO is this correct semantic? - } - - HttpURI uri = HttpURI.createHttpURI(request.getScheme(),request.getServerName(),request.getServerPort(),_uri.getPath(),baseRequest.getHttpURI().getParam(),query,null); - - MetaData.Request push = new MetaData.Request(HttpMethod.GET.asString(),uri,baseRequest.getHttpVersion(),fields); - - baseRequest.getHttpChannel().getHttpTransport().push(push); - } - @Override public String toString() { diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java index 8f423e463f..95b4f59a9e 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java @@ -29,6 +29,7 @@ import java.nio.channels.ClosedChannelException; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; import javax.servlet.DispatcherType; import javax.servlet.RequestDispatcher; @@ -264,6 +265,8 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor handle(); } + AtomicReference<Action> caller = new AtomicReference<>(); + /** * @return True if the channel is ready to continue handling (ie it is not suspended) */ @@ -297,7 +300,6 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor throw new IllegalStateException("state=" + _state); _request.setHandled(false); _response.getHttpOutput().reopen(); - _request.setDispatcherType(DispatcherType.REQUEST); List<HttpConfiguration.Customizer> customizers = _configuration.getCustomizers(); if (!customizers.isEmpty()) @@ -305,7 +307,15 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor for (HttpConfiguration.Customizer customizer : customizers) customizer.customize(getConnector(), _configuration, _request); } - getServer().handle(this); + try + { + _request.setDispatcherType(DispatcherType.REQUEST); + getServer().handle(this); + } + finally + { + _request.setDispatcherType(null); + } break; } @@ -313,8 +323,16 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor { _request.setHandled(false); _response.getHttpOutput().reopen(); - _request.setDispatcherType(DispatcherType.ASYNC); - getServer().handleAsync(this); + + try + { + _request.setDispatcherType(DispatcherType.ASYNC); + getServer().handleAsync(this); + } + finally + { + _request.setDispatcherType(null); + } break; } @@ -336,8 +354,16 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor _request.setAttribute(ERROR_STATUS_CODE,code); _request.setHandled(false); _response.getHttpOutput().reopen(); - _request.setDispatcherType(DispatcherType.ERROR); - getServer().handle(this); + + try + { + _request.setDispatcherType(DispatcherType.ERROR); + getServer().handle(this); + } + finally + { + _request.setDispatcherType(null); + } } break; } @@ -390,10 +416,6 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor else handleException(failure); } - finally - { - _request.setDispatcherType(null); - } action = _state.unhandle(); } diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/ProxyConnectionFactory.java b/jetty-server/src/main/java/org/eclipse/jetty/server/ProxyConnectionFactory.java index b72cf5718d..7a5e51cca1 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/ProxyConnectionFactory.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/ProxyConnectionFactory.java @@ -19,17 +19,23 @@ package org.eclipse.jetty.server; import java.io.IOException; +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.InetAddress; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.ReadPendingException; import java.nio.channels.WritePendingException; +import java.nio.charset.StandardCharsets; import java.util.Iterator; import org.eclipse.jetty.io.AbstractConnection; import org.eclipse.jetty.io.Connection; import org.eclipse.jetty.io.EndPoint; +import org.eclipse.jetty.util.AttributesMap; import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.Callback; +import org.eclipse.jetty.util.TypeUtil; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; @@ -38,14 +44,17 @@ import org.eclipse.jetty.util.log.Logger; /** * ConnectionFactory for the PROXY Protocol. * <p>This factory can be placed in front of any other connection factory - * to process the proxy line before the normal protocol handling</p> + * to process the proxy v1 or v2 line before the normal protocol handling</p> * * @see <a href="http://www.haproxy.org/download/1.5/doc/proxy-protocol.txt">http://www.haproxy.org/download/1.5/doc/proxy-protocol.txt</a> */ public class ProxyConnectionFactory extends AbstractConnectionFactory { + public static final String TLS_VERSION = "TLS_VERSION"; + private static final Logger LOG = Log.getLogger(ProxyConnectionFactory.class); private final String _next; + private int _maxProxyHeader=1024; /* ------------------------------------------------------------ */ /** Proxy Connection Factory that uses the next ConnectionFactory @@ -63,6 +72,16 @@ public class ProxyConnectionFactory extends AbstractConnectionFactory _next=nextProtocol; } + public int getMaxProxyHeader() + { + return _maxProxyHeader; + } + + public void setMaxProxyHeader(int maxProxyHeader) + { + _maxProxyHeader = maxProxyHeader; + } + @Override public Connection newConnection(Connector connector, EndPoint endp) { @@ -80,10 +99,79 @@ public class ProxyConnectionFactory extends AbstractConnectionFactory } } - return new ProxyConnection(endp,connector,next); + return new ProxyProtocolV1orV2Connection(endp,connector,next); + } + + public class ProxyProtocolV1orV2Connection extends AbstractConnection + { + private final Connector _connector; + private final String _next; + private ByteBuffer _buffer = BufferUtil.allocate(16); + + protected ProxyProtocolV1orV2Connection(EndPoint endp, Connector connector, String next) + { + super(endp,connector.getExecutor()); + _connector=connector; + _next=next; + } + + @Override + public void onOpen() + { + super.onOpen(); + fillInterested(); + } + + @Override + public void onFillable() + { + try + { + while(BufferUtil.space(_buffer)>0) + { + // Read data + int fill=getEndPoint().fill(_buffer); + if (fill<0) + { + getEndPoint().shutdownOutput(); + return; + } + if (fill==0) + { + fillInterested(); + return; + } + } + + // Is it a V1? + switch(_buffer.get(0)) + { + case 'P': + { + ProxyProtocolV1Connection v1 = new ProxyProtocolV1Connection(getEndPoint(),_connector,_next,_buffer); + getEndPoint().upgrade(v1); + return; + } + case 0x0D: + { + ProxyProtocolV2Connection v2 = new ProxyProtocolV2Connection(getEndPoint(),_connector,_next,_buffer); + getEndPoint().upgrade(v2); + return; + } + default: + LOG.warn("Not PROXY protocol for {}",getEndPoint()); + close(); + } + } + catch (Throwable x) + { + LOG.warn("PROXY error for "+getEndPoint(),x); + close(); + } + } } - public static class ProxyConnection extends AbstractConnection + public static class ProxyProtocolV1Connection extends AbstractConnection { // 0 1 2 3 4 5 6 // 98765432109876543210987654321 @@ -97,11 +185,13 @@ public class ProxyConnectionFactory extends AbstractConnectionFactory private int _fields; private int _length; - protected ProxyConnection(EndPoint endp, Connector connector, String next) + protected ProxyProtocolV1Connection(EndPoint endp, Connector connector, String next,ByteBuffer buffer) { super(endp,connector.getExecutor()); _connector=connector; _next=next; + _length=buffer.remaining(); + parse(buffer); } @Override @@ -110,16 +200,60 @@ public class ProxyConnectionFactory extends AbstractConnectionFactory super.onOpen(); fillInterested(); } + + + private boolean parse(ByteBuffer buffer) + { + // parse fields + while (buffer.hasRemaining()) + { + byte b = buffer.get(); + if (_fields<6) + { + if (b==' ' || b=='\r' && _fields==5) + { + _field[_fields++]=_builder.toString(); + _builder.setLength(0); + } + else if (b<' ') + { + LOG.warn("Bad character {} for {}",b&0xFF,getEndPoint()); + close(); + return false; + } + else + { + _builder.append((char)b); + } + } + else + { + if (b=='\n') + { + _fields=7; + return true; + } + LOG.warn("Bad CRLF for {}",getEndPoint()); + close(); + return false; + } + } + + return true; + } + @Override public void onFillable() { try { ByteBuffer buffer=null; - loop: while(true) + while(_fields<7) { // Create a buffer that will not read too much data + // since once read it is impossible to push back for the + // real connection to read it. int size=Math.max(1,__size[_fields]-_builder.length()); if (buffer==null || buffer.capacity()!=size) buffer=BufferUtil.allocate(size); @@ -147,38 +281,8 @@ public class ProxyConnectionFactory extends AbstractConnectionFactory return; } - // parse fields - while (buffer.hasRemaining()) - { - byte b = buffer.get(); - if (_fields<6) - { - if (b==' ' || b=='\r' && _fields==5) - { - _field[_fields++]=_builder.toString(); - _builder.setLength(0); - } - else if (b<' ') - { - LOG.warn("Bad character {} for {}",b&0xFF,getEndPoint()); - close(); - return; - } - else - { - _builder.append((char)b); - } - } - else - { - if (b=='\n') - break loop; - - LOG.warn("Bad CRLF for {}",getEndPoint()); - close(); - return; - } - } + if (!parse(buffer)) + return; } // Check proxy @@ -197,10 +301,13 @@ public class ProxyConnectionFactory extends AbstractConnectionFactory ConnectionFactory connectionFactory = _connector.getConnectionFactory(_next); if (connectionFactory == null) { - LOG.info("Next protocol '{}' for {}",_next,getEndPoint()); + LOG.warn("No Next protocol '{}' for {}",_next,getEndPoint()); close(); return; } + + if (LOG.isDebugEnabled()) + LOG.warn("Next protocol '{}' for {} r={} l={}",_next,getEndPoint(),remote,local); EndPoint endPoint = new ProxyEndPoint(getEndPoint(),remote,local); Connection newConnection = connectionFactory.newConnection(_connector, endPoint); @@ -213,8 +320,260 @@ public class ProxyConnectionFactory extends AbstractConnectionFactory } } } + + + enum Family { UNSPEC, INET, INET6, UNIX }; + enum Transport { UNSPEC, STREAM, DGRAM }; + private static final byte[] MAGIC = new byte[]{0x0D,0x0A,0x0D,0x0A,0x00,0x0D,0x0A,0x51,0x55,0x49,0x54,0x0A}; + + public class ProxyProtocolV2Connection extends AbstractConnection + { + private final Connector _connector; + private final String _next; + private final boolean _local; + private final Family _family; + private final Transport _transport; + private final int _length; + private final ByteBuffer _buffer; + + protected ProxyProtocolV2Connection(EndPoint endp, Connector connector, String next,ByteBuffer buffer) + throws IOException + { + super(endp,connector.getExecutor()); + _connector=connector; + _next=next; + + if (buffer.remaining()!=16) + throw new IllegalStateException(); + + if (LOG.isDebugEnabled()) + LOG.debug("PROXYv2 header {} for {}",BufferUtil.toHexSummary(buffer),this); + + // struct proxy_hdr_v2 { + // uint8_t sig[12]; /* hex 0D 0A 0D 0A 00 0D 0A 51 55 49 54 0A */ + // uint8_t ver_cmd; /* protocol version and command */ + // uint8_t fam; /* protocol family and address */ + // uint16_t len; /* number of following bytes part of the header */ + // }; + for (int i=0;i<MAGIC.length;i++) + if (buffer.get()!=MAGIC[i]) + throw new IOException("Bad PROXY protocol v2 signature"); + + int versionAndCommand = 0xff & buffer.get(); + if ((versionAndCommand&0xf0) != 0x20) + throw new IOException("Bad PROXY protocol v2 version"); + _local=(versionAndCommand&0xf)==0x00; + + int transportAndFamily = 0xff & buffer.get(); + switch(transportAndFamily>>4) + { + case 0: _family=Family.UNSPEC; break; + case 1: _family=Family.INET; break; + case 2: _family=Family.INET6; break; + case 3: _family=Family.UNIX; break; + default: + throw new IOException("Bad PROXY protocol v2 family"); + } + + switch(0xf&transportAndFamily) + { + case 0: _transport=Transport.UNSPEC; break; + case 1: _transport=Transport.STREAM; break; + case 2: _transport=Transport.DGRAM; break; + default: + throw new IOException("Bad PROXY protocol v2 family"); + } + + _length = buffer.getChar(); + + if (!_local && (_family==Family.UNSPEC || _family==Family.UNIX || _transport!=Transport.STREAM)) + throw new IOException(String.format("Unsupported PROXY protocol v2 mode 0x%x,0x%x",versionAndCommand,transportAndFamily)); + + if (_length>_maxProxyHeader) + throw new IOException(String.format("Unsupported PROXY protocol v2 mode 0x%x,0x%x,0x%x",versionAndCommand,transportAndFamily,_length)); + + _buffer = _length>0?BufferUtil.allocate(_length):BufferUtil.EMPTY_BUFFER; + } + + @Override + public void onOpen() + { + super.onOpen(); + if (_buffer.remaining()==_length) + next(); + else + fillInterested(); + } + + @Override + public void onFillable() + { + try + { + while(_buffer.remaining()<_length) + { + // Read data + int fill=getEndPoint().fill(_buffer); + if (fill<0) + { + getEndPoint().shutdownOutput(); + return; + } + if (fill==0) + { + fillInterested(); + return; + } + } + } + catch (Throwable x) + { + LOG.warn("PROXY error for "+getEndPoint(),x); + close(); + return; + } + + next(); + } + + private void next() + { + if (LOG.isDebugEnabled()) + LOG.debug("PROXYv2 next {} from {} for {}",_next,BufferUtil.toHexSummary(_buffer),this); + + // Create the next protocol + ConnectionFactory connectionFactory = _connector.getConnectionFactory(_next); + if (connectionFactory == null) + { + LOG.info("Next protocol '{}' for {}",_next,getEndPoint()); + close(); + return; + } + + // Do we need to wrap the endpoint? + EndPoint endPoint=getEndPoint(); + if (!_local) + { + try + { + InetAddress src; + InetAddress dst; + int sp; + int dp; + + switch(_family) + { + case INET: + { + byte[] addr=new byte[4]; + _buffer.get(addr); + src = Inet4Address.getByAddress(addr); + _buffer.get(addr); + dst = Inet4Address.getByAddress(addr); + sp = _buffer.getChar(); + dp = _buffer.getChar(); + + break; + } + + case INET6: + { + byte[] addr=new byte[16]; + _buffer.get(addr); + src = Inet6Address.getByAddress(addr); + _buffer.get(addr); + dst = Inet6Address.getByAddress(addr); + sp = _buffer.getChar(); + dp = _buffer.getChar(); + break; + } + + default: + throw new IllegalStateException(); + } + + + // Extract Addresses + InetSocketAddress remote=new InetSocketAddress(src,sp); + InetSocketAddress local =new InetSocketAddress(dst,dp); + ProxyEndPoint proxyEndPoint = new ProxyEndPoint(endPoint,remote,local); + endPoint = proxyEndPoint; + + + // Any additional info? + while(_buffer.hasRemaining()) + { + int type = 0xff & _buffer.get(); + int length = _buffer.getShort(); + byte[] value = new byte[length]; + _buffer.get(value); + + if (LOG.isDebugEnabled()) + LOG.debug(String.format("T=%x L=%d V=%s for %s",type,length,TypeUtil.toHexString(value),this)); + + // TODO interpret these values + switch(type) + { + case 0x01: // PP2_TYPE_ALPN + break; + case 0x02: // PP2_TYPE_AUTHORITY + break; + case 0x20: // PP2_TYPE_SSL + { + int i=0; + int client = 0xff & value[i++]; + int verify = (0xff & value[i++])<<24 + (0xff & value[i++])<<16 + (0xff & value[i++])<<8 + (0xff&value[i++]); + while(i<value.length) + { + int ssl_type = 0xff & value[i++]; + int ssl_length = (0xff & value[i++])*0x100 + (0xff&value[i++]); + byte[] ssl_val = new byte[ssl_length]; + System.arraycopy(value,i,ssl_val,0,ssl_length); + i+=ssl_length; + + switch(ssl_type) + { + case 0x21: // PP2_TYPE_SSL_VERSION + String version=new String(ssl_val,0,ssl_length,StandardCharsets.ISO_8859_1); + if (client==1) + proxyEndPoint.setAttribute(TLS_VERSION,version); + break; + + default: + break; + } + } + break; + } + case 0x21: // PP2_TYPE_SSL_VERSION + break; + case 0x22: // PP2_TYPE_SSL_CN + break; + case 0x30: // PP2_TYPE_NETNS + break; + default: + break; + } + } + + if (LOG.isDebugEnabled()) + LOG.debug("{} {}",getEndPoint(),proxyEndPoint.toString()); + + + } + catch(Exception e) + { + LOG.warn(e); + } + } + + Connection newConnection = connectionFactory.newConnection(_connector, endPoint); + endPoint.upgrade(newConnection); + } + } + - public static class ProxyEndPoint implements EndPoint + public static class ProxyEndPoint extends AttributesMap implements EndPoint { private final EndPoint _endp; private final InetSocketAddress _remote; diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/PushBuilder.java b/jetty-server/src/main/java/org/eclipse/jetty/server/PushBuilder.java index 5e807b45dc..a16c9960e6 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/PushBuilder.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/PushBuilder.java @@ -25,63 +25,99 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; + /** Build a request to be pushed. - * <p> - * A PushBuilder is obtained by calling {@link Request#getPushBuilder()} - * which creates an initializes the builder as follows: + * + * <p>A PushBuilder is obtained by calling {@link + * Request#getPushBuilder()} (<code>Eventually HttpServletRequest.getPushBuilder()</code>). + * Each call to this method will + * return a new instance of a PushBuilder based off the current {@code + * HttpServletRequest}. Any mutations to the returned PushBuilder are + * not reflected on future returns.</p> + * + * <p>The instance is initialized as follows:</p> + * * <ul> - * <li> Each call to getPushBuilder() will return a new instance of a - * PushBuilder based off the Request. Any mutations to the - * returned PushBuilder are not reflected on future returns.</li> + * * <li>The method is initialized to "GET"</li> - * <li>The requests headers are added to the Builder, except for:<ul> + * + * <li>The existing headers of the current {@link HttpServletRequest} + * are added to the builder, except for: + * + * <ul> * <li>Conditional headers (eg. If-Modified-Since) * <li>Range headers * <li>Expect headers * <li>Authorization headers * <li>Referrer headers - * </ul></li> - * <li>If the request was Authenticated, an Authorization header will + * </ul> + * + * </li> + * + * <li>If the request was authenticated, an Authorization header will * be set with a container generated token that will result in equivalent - * Authorization for the pushed request</li> - * <li>The query string from {@link HttpServletRequest#getQueryString()} - * <li>The {@link HttpServletRequest#getRequestedSessionId()} value, unless at the time - * of the call {@link HttpServletRequest#getSession(boolean)} - * has previously been called to create a new {@link HttpSession}, in - * which case the new session ID will be used as the PushBuilders - * requested session ID. The source of the requested session id will be the - * same as for the request</li> - * <li>The Referer header will be set to {@link HttpServletRequest#getRequestURL()} - * plus any {@link HttpServletRequest#getQueryString()} </li> + * Authorization for the pushed request.</li> + * + * <li>The {@link HttpServletRequest#getRequestedSessionId()} value, + * unless at the time of the call {@link + * HttpServletRequest#getSession(boolean)} has previously been called to + * create a new {@link HttpSession}, in which case the new session ID + * will be used as the PushBuilder's requested session ID. The source of + * the requested session id will be the same as for the request</li> + * + * <li>The Referer(sic) header will be set to {@link + * HttpServletRequest#getRequestURL()} plus any {@link + * HttpServletRequest#getQueryString()} </li> + * * <li>If {@link HttpServletResponse#addCookie(Cookie)} has been called * on the associated response, then a corresponding Cookie header will be added * to the PushBuilder, unless the {@link Cookie#getMaxAge()} is <=0, in which * case the Cookie will be removed from the builder.</li> - * <li>If this request has has the conditional headers If-Modified-Since or - * If-None-Match then the {@link #isConditional()} header is set to true.</li> - * </ul> - * <p>A PushBuilder can be customized by chained calls to mutator methods before the - * {@link #push()} method is called to initiate a push request with the current state - * of the builder. After the call to {@link #push()}, the builder may be reused for - * another push, however the {@link #path(String)}, {@link #etag(String)} and - * {@link #lastModified(String)} values will have been nulled. All other - * values are retained over calls to {@link #push()}. + * + * <li>If this request has has the conditional headers If-Modified-Since + * or If-None-Match, then the {@link #isConditional()} header is set to + * true.</li> + * + * </ul> + * + * <p>The {@link #path} method must be called on the {@code PushBuilder} + * instance before the call to {@link #push}. Failure to do so must + * cause an exception to be thrown from {@link + * #push}, as specified in that method.</p> + * + * <p>A PushBuilder can be customized by chained calls to mutator + * methods before the {@link #push()} method is called to initiate an + * asynchronous push request with the current state of the builder. + * After the call to {@link #push()}, the builder may be reused for + * another push, however the implementation must make it so the {@link + * #path(String)}, {@link #etag(String)} and {@link + * #lastModified(String)} values are cleared before returning from + * {@link #push}. All other values are retained over calls to {@link + * #push()}. + * + * @since 4.0 */ public interface PushBuilder { - /** Set the method to be used for the push. - * Defaults to GET. + /** + * <p>Set the method to be used for the push.</p> + * + * <p>Any non-empty String may be used for the method.</p> + * * @param method the method to be used for the push. * @return this builder. + * @throws NullPointerException if the argument is {@code null} + * @throws IllegalArgumentException if the argument is the empty String */ public abstract PushBuilder method(String method); /** Set the query string to be used for the push. - * Defaults to the requests query string. - * Will be appended to any query String included in a call to {@link #path(String)}. This - * method should be used instead of a query in {@link #path(String)} when multiple - * {@link #push()} calls are to be made with the same query string, or to remove a - * query string obtained from the associated request. + * + * Will be appended to any query String included in a call to {@link + * #path(String)}. Any duplicate parameters must be preserved. This + * method should be used instead of a query in {@link #path(String)} + * when multiple {@link #push()} calls are to be made with the same + * query string. * @param queryString the query string to be used for the push. * @return this builder. */ @@ -108,33 +144,55 @@ public interface PushBuilder */ public abstract PushBuilder conditional(boolean conditional); - /** Set a header to be used for the push. + /** + * <p>Set a header to be used for the push. If the builder has an + * existing header with the same name, its value is overwritten.</p> + * * @param name The header name to set * @param value The header value to set * @return this builder. */ public abstract PushBuilder setHeader(String name, String value); + - /** Add a header to be used for the push. + /** + * <p>Add a header to be used for the push.</p> * @param name The header name to add * @param value The header value to add * @return this builder. */ public abstract PushBuilder addHeader(String name, String value); + + + /** + * <p>Remove the named header. If the header does not exist, take + * no action.</p> + * + * @param name The name of the header to remove + * @return this builder. + */ + public abstract PushBuilder removeHeader(String name); + - /** Set the URI path to be used for the push. - * The path may start with "/" in which case it is treated as an - * absolute path, otherwise it is relative to the context path of - * the associated request. - * There is no path default and {@link #path(String)} must be called - * before every call to {@link #push()} + + /** + * Set the URI path to be used for the push. The path may start + * with "/" in which case it is treated as an absolute path, + * otherwise it is relative to the context path of the associated + * request. There is no path default and {@link #path(String)} must + * be called before every call to {@link #push()}. If a query + * string is present in the argument {@code path}, its contents must + * be merged with the contents previously passed to {@link + * #queryString}, preserving duplicates. + * * @param path the URI path to be used for the push, which may include a * query string. * @return this builder. */ public abstract PushBuilder path(String path); - /** Set the etag to be used for conditional pushes. + /** + * Set the etag to be used for conditional pushes. * The etag will be used only if {@link #isConditional()} is true. * Defaults to no etag. The value is nulled after every call to * {@link #push()} @@ -143,33 +201,44 @@ public interface PushBuilder */ public abstract PushBuilder etag(String etag); - /** Set the last modified date to be used for conditional pushes. - * The last modified date will be used only if {@link #isConditional()} is true. - * Defaults to no date. The value is nulled after every call to - * {@link #push()} + /** + * Set the last modified date to be used for conditional pushes. + * The last modified date will be used only if {@link + * #isConditional()} is true. Defaults to no date. The value is + * nulled after every call to {@link #push()} * @param lastModified the last modified date to be used for the push. * @return this builder. - * */ + */ public abstract PushBuilder lastModified(String lastModified); - /** Push a resource. - * Push a resource based on the current state of the PushBuilder. If {@link #isConditional()} - * is true and an etag or lastModified value is provided, then an appropriate conditional header - * will be generated. If both an etag and lastModified value are provided only an If-None-Match header - * will be generated. If the builder has a session ID, then the pushed request - * will include the session ID either as a Cookie or as a URI parameter as appropriate. The builders - * query string is merged with any passed query string. - * After initiating the push, the builder has its path, etag and lastModified fields nulled. All - * other fields are left as is for possible reuse in another push. - * @throws IllegalArgumentException if the method set expects a request body (eg POST) + /** Push a resource given the current state of the builder, + * returning immediately without blocking. + * + * <p>Push a resource based on the current state of the PushBuilder. + * If {@link #isConditional()} is true and an etag or lastModified + * value is provided, then an appropriate conditional header will be + * generated. If both an etag and lastModified value are provided + * only an If-None-Match header will be generated. If the builder + * has a session ID, then the pushed request will include the + * session ID either as a Cookie or as a URI parameter as + * appropriate. The builders query string is merged with any passed + * query string.</p> + * + * <p>Before returning from this method, the builder has its path, + * etag and lastModified fields nulled. All other fields are left as + * is for possible reuse in another push.</p> + * + * @throws IllegalArgumentException if the method set expects a + * request body (eg POST) + * + * @throws IllegalStateException if there was no call to {@link + * #path} on this instance either between its instantiation or the + * last call to {@code push()} that did not throw an + * IllegalStateException. */ public abstract void push(); - - - - public abstract String getMethod(); public abstract String getQueryString(); public abstract String getSessionId(); @@ -179,7 +248,4 @@ public interface PushBuilder public abstract String getPath(); public abstract String getEtag(); public abstract String getLastModified(); - - - }
\ No newline at end of file diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/PushBuilderImpl.java b/jetty-server/src/main/java/org/eclipse/jetty/server/PushBuilderImpl.java index f39cffa878..def6bed888 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/PushBuilderImpl.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/PushBuilderImpl.java @@ -32,14 +32,14 @@ import org.eclipse.jetty.util.log.Logger; /* ------------------------------------------------------------ */ -/** +/** */ public class PushBuilderImpl implements PushBuilder -{ +{ private static final Logger LOG = Log.getLogger(PushBuilderImpl.class); private final static HttpField JettyPush = new HttpField("x-http2-push","PushBuilder"); - + private final Request _request; private final HttpFields _fields; private String _method; @@ -49,7 +49,7 @@ public class PushBuilderImpl implements PushBuilder private String _path; private String _etag; private String _lastModified; - + public PushBuilderImpl(Request request, HttpFields fields, String method, String queryString, String sessionId, boolean conditional) { super(); @@ -65,124 +65,88 @@ public class PushBuilderImpl implements PushBuilder } /* ------------------------------------------------------------ */ - /** - * @see org.eclipse.jetty.server.PushBuilder#getMethod() - */ @Override public String getMethod() { return _method; } - + /* ------------------------------------------------------------ */ - /** - * @see org.eclipse.jetty.server.PushBuilder#method(java.lang.String) - */ @Override public PushBuilder method(String method) { _method = method; return this; } - + /* ------------------------------------------------------------ */ - /** - * @see org.eclipse.jetty.server.PushBuilder#getQueryString() - */ @Override public String getQueryString() { return _queryString; } - + /* ------------------------------------------------------------ */ - /** - * @see org.eclipse.jetty.server.PushBuilder#queryString(java.lang.String) - */ @Override public PushBuilder queryString(String queryString) { _queryString = queryString; return this; } - + /* ------------------------------------------------------------ */ - /** - * @see org.eclipse.jetty.server.PushBuilder#getSessionId() - */ @Override public String getSessionId() { return _sessionId; } - + /* ------------------------------------------------------------ */ - /** - * @see org.eclipse.jetty.server.PushBuilder#sessionId(java.lang.String) - */ @Override public PushBuilder sessionId(String sessionId) { _sessionId = sessionId; return this; } - + /* ------------------------------------------------------------ */ - /** - * @see org.eclipse.jetty.server.PushBuilder#isConditional() - */ @Override public boolean isConditional() { return _conditional; } - + /* ------------------------------------------------------------ */ - /** - * @see org.eclipse.jetty.server.PushBuilder#conditional(boolean) - */ @Override public PushBuilder conditional(boolean conditional) { _conditional = conditional; return this; } - + /* ------------------------------------------------------------ */ - /** - * @see org.eclipse.jetty.server.PushBuilder#getHeaderNames() - */ @Override public Set<String> getHeaderNames() { return _fields.getFieldNamesCollection(); } - + /* ------------------------------------------------------------ */ - /** - * @see org.eclipse.jetty.server.PushBuilder#getHeader(java.lang.String) - */ @Override public String getHeader(String name) { return _fields.get(name); } - + /* ------------------------------------------------------------ */ - /** - * @see org.eclipse.jetty.server.PushBuilder#setHeader(java.lang.String, java.lang.String) - */ @Override public PushBuilder setHeader(String name,String value) { _fields.put(name,value); return this; } - + /* ------------------------------------------------------------ */ - /** - * @see org.eclipse.jetty.server.PushBuilder#addHeader(java.lang.String, java.lang.String) - */ @Override public PushBuilder addHeader(String name,String value) { @@ -190,11 +154,15 @@ public class PushBuilderImpl implements PushBuilder return this; } - /* ------------------------------------------------------------ */ - /** - * @see org.eclipse.jetty.server.PushBuilder#getPath() - */ + @Override + public PushBuilder removeHeader(String name) + { + _fields.remove(name); + return this; + } + + /* ------------------------------------------------------------ */ @Override public String getPath() { @@ -202,9 +170,6 @@ public class PushBuilderImpl implements PushBuilder } /* ------------------------------------------------------------ */ - /** - * @see org.eclipse.jetty.server.PushBuilder#path(java.lang.String) - */ @Override public PushBuilder path(String path) { @@ -213,9 +178,6 @@ public class PushBuilderImpl implements PushBuilder } /* ------------------------------------------------------------ */ - /** - * @see org.eclipse.jetty.server.PushBuilder#getEtag() - */ @Override public String getEtag() { @@ -223,9 +185,6 @@ public class PushBuilderImpl implements PushBuilder } /* ------------------------------------------------------------ */ - /** - * @see org.eclipse.jetty.server.PushBuilder#etag(java.lang.String) - */ @Override public PushBuilder etag(String etag) { @@ -234,9 +193,6 @@ public class PushBuilderImpl implements PushBuilder } /* ------------------------------------------------------------ */ - /** - * @see org.eclipse.jetty.server.PushBuilder#getLastModified() - */ @Override public String getLastModified() { @@ -244,9 +200,6 @@ public class PushBuilderImpl implements PushBuilder } /* ------------------------------------------------------------ */ - /** - * @see org.eclipse.jetty.server.PushBuilder#lastModified(java.lang.String) - */ @Override public PushBuilder lastModified(String lastModified) { @@ -255,40 +208,36 @@ public class PushBuilderImpl implements PushBuilder } /* ------------------------------------------------------------ */ - /* ------------------------------------------------------------ */ - /** - * @see org.eclipse.jetty.server.PushBuilder#push() - */ @Override public void push() { if (HttpMethod.POST.is(_method) || HttpMethod.PUT.is(_method)) throw new IllegalStateException("Bad Method "+_method); - + if (_path==null || _path.length()==0) throw new IllegalStateException("Bad Path "+_path); - + String path=_path; String query=_queryString; int q=path.indexOf('?'); if (q>=0) { - query=(query!=null && query.length()>0)?(_path.substring(q+1)+'&'+query):_path.substring(q+1); - path=_path.substring(0,q); + query=(query!=null && query.length()>0)?(path.substring(q+1)+'&'+query):path.substring(q+1); + path=path.substring(0,q); } - + if (!path.startsWith("/")) path=URIUtil.addPaths(_request.getContextPath(),path); - + String param=null; if (_sessionId!=null) { if (_request.isRequestedSessionIdFromURL()) param="jsessionid="+_sessionId; - // TODO else + // TODO else // _fields.add("Cookie","JSESSIONID="+_sessionId); } - + if (_conditional) { if (_etag!=null) @@ -296,16 +245,17 @@ public class PushBuilderImpl implements PushBuilder else if (_lastModified!=null) _fields.add(HttpHeader.IF_MODIFIED_SINCE,_lastModified); } - - HttpURI uri = HttpURI.createHttpURI(_request.getScheme(),_request.getServerName(),_request.getServerPort(),_path,param,query,null); + + HttpURI uri = HttpURI.createHttpURI(_request.getScheme(),_request.getServerName(),_request.getServerPort(),path,param,query,null); MetaData.Request push = new MetaData.Request(_method,uri,_request.getHttpVersion(),_fields); - + if (LOG.isDebugEnabled()) LOG.debug("Push {} {} inm={} ims={}",_method,uri,_fields.get(HttpHeader.IF_NONE_MATCH),_fields.get(HttpHeader.IF_MODIFIED_SINCE)); - + _request.getHttpChannel().getHttpTransport().push(push); _path=null; _etag=null; _lastModified=null; } + } diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java b/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java index fbc42e0708..8cf22eb9fb 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java @@ -40,6 +40,7 @@ import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.concurrent.ConcurrentLinkedQueue; import javax.servlet.AsyncContext; import javax.servlet.AsyncListener; @@ -161,6 +162,7 @@ public class Request implements HttpServletRequest private final HttpInput _input; private MetaData.Request _metadata; + private String _originalURI; private String _contextPath; private String _servletPath; @@ -1580,6 +1582,14 @@ public class Request implements HttpServletRequest /* ------------------------------------------------------------ */ /** + * @return Returns the original uri passed in metadata before customization/rewrite + */ + public String getOriginalURI() + { + return _originalURI; + } + /* ------------------------------------------------------------ */ + /** * @param uri the URI to set */ public void setHttpURI(HttpURI uri) @@ -1745,8 +1755,9 @@ public class Request implements HttpServletRequest * @param request the Request metadata */ public void setMetaData(org.eclipse.jetty.http.MetaData.Request request) - { + { _metadata=request; + _originalURI=_metadata.getURIString(); setMethod(request.getMethod()); HttpURI uri = request.getURI(); @@ -1803,6 +1814,7 @@ public class Request implements HttpServletRequest protected void recycle() { _metadata=null; + _originalURI=null; if (_context != null) throw new IllegalStateException("Request in context!"); diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/SecureRequestCustomizer.java b/jetty-server/src/main/java/org/eclipse/jetty/server/SecureRequestCustomizer.java index 7dc74588d8..1ede4423c6 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/SecureRequestCustomizer.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/SecureRequestCustomizer.java @@ -71,13 +71,19 @@ public class SecureRequestCustomizer implements HttpConfiguration.Customizer request.setSecure(true); if (request.getHttpURI().getScheme()==null) - request.setScheme(HttpScheme.HTTPS.asString()); + request.getHttpURI().setScheme(HttpScheme.HTTPS.asString()); SslConnection.DecryptedEndPoint ssl_endp = (DecryptedEndPoint)request.getHttpChannel().getEndPoint(); SslConnection sslConnection = ssl_endp.getSslConnection(); SSLEngine sslEngine=sslConnection.getSSLEngine(); customize(sslEngine,request); + + if (request.getHttpURI().getScheme()==null) + request.setScheme(HttpScheme.HTTPS.asString()); } + + if (HttpScheme.HTTPS.is(request.getScheme())) + request.setSecure(true); } /** @@ -104,7 +110,6 @@ public class SecureRequestCustomizer implements HttpConfiguration.Customizer */ public void customize(SSLEngine sslEngine, Request request) { - request.setScheme(HttpScheme.HTTPS.asString()); SSLSession sslSession = sslEngine.getSession(); if (_sniHostCheck) diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/session/JDBCSessionIdManager.java b/jetty-server/src/main/java/org/eclipse/jetty/server/session/JDBCSessionIdManager.java index 99108d86d7..3bfb8ba242 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/session/JDBCSessionIdManager.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/session/JDBCSessionIdManager.java @@ -1250,6 +1250,8 @@ public class JDBCSessionIdManager extends AbstractSessionIdManager */ private void scavenge () { + Set<String> candidateIds = getAllCandidateExpiredSessionIds(); + Connection connection = null; try { @@ -1283,7 +1285,7 @@ public class JDBCSessionIdManager extends AbstractSessionIdManager } } } - scavengeSessions(expiredSessionIds, false); + scavengeSessions(candidateIds, expiredSessionIds, false); //Pass 2: find sessions that have expired a while ago for which this node was their last manager @@ -1306,7 +1308,7 @@ public class JDBCSessionIdManager extends AbstractSessionIdManager if (LOG.isDebugEnabled()) LOG.debug ("Found expired sessionId="+sessionId+" last managed by "+getWorkerName()); } } - scavengeSessions(expiredSessionIds, false); + scavengeSessions(candidateIds, expiredSessionIds, false); } @@ -1329,9 +1331,13 @@ public class JDBCSessionIdManager extends AbstractSessionIdManager if (LOG.isDebugEnabled()) LOG.debug ("Found expired sessionId="+sessionId); } } - scavengeSessions(expiredSessionIds, true); + scavengeSessions(candidateIds, expiredSessionIds, true); } } + + //Tell session managers to check remaining sessions in memory that may have expired + //but are no longer in the database + scavengeSessions(candidateIds); } } catch (Exception e) @@ -1363,24 +1369,20 @@ public class JDBCSessionIdManager extends AbstractSessionIdManager /** * @param expiredSessionIds */ - private void scavengeSessions (Set<String> expiredSessionIds, boolean forceDelete) + private void scavengeSessions (Set<String> candidateIds, Set<String> expiredSessionIds, boolean forceDelete) { Set<String> remainingIds = new HashSet<String>(expiredSessionIds); - Handler[] contexts = _server.getChildHandlersByClass(ContextHandler.class); - for (int i=0; contexts!=null && i<contexts.length; i++) + Set<SessionManager> managers = getAllSessionManagers(); + for (SessionManager m:managers) { - SessionHandler sessionHandler = ((ContextHandler)contexts[i]).getChildHandlerByClass(SessionHandler.class); - if (sessionHandler != null) + Set<String> successfullyExpiredIds = ((JDBCSessionManager)m).expire(expiredSessionIds); + if (successfullyExpiredIds != null) { - SessionManager manager = sessionHandler.getSessionManager(); - if (manager != null && manager instanceof JDBCSessionManager) - { - Set<String> successfullyExpiredIds = ((JDBCSessionManager)manager).expire(expiredSessionIds); - if (successfullyExpiredIds != null) - remainingIds.removeAll(successfullyExpiredIds); - } + remainingIds.removeAll(successfullyExpiredIds); + candidateIds.removeAll(successfullyExpiredIds); } } + //Any remaining ids are of those sessions that no context removed if (!remainingIds.isEmpty() && forceDelete) @@ -1402,6 +1404,63 @@ public class JDBCSessionIdManager extends AbstractSessionIdManager } } } + + /** + * These are the session ids that the session managers thought had + * expired, but were not expired in the database. This could be + * because the session is live on another node, or that the + * session no longer exists in the database because some other + * node removed it. + * @param candidateIds + */ + private void scavengeSessions (Set<String> candidateIds) + { + if (candidateIds.isEmpty()) + return; + + + Set<SessionManager> managers = getAllSessionManagers(); + + for (SessionManager m:managers) + { + //tell the session managers to check the sessions that have expired in memory + //if they are no longer in the database, they should be removed + ((JDBCSessionManager)m).expireCandidates(candidateIds); + } + } + + private Set<String> getAllCandidateExpiredSessionIds() + { + HashSet<String> candidateIds = new HashSet<>(); + + Set<SessionManager> managers = getAllSessionManagers(); + + for (SessionManager m:managers) + { + candidateIds.addAll(((JDBCSessionManager)m).getCandidateExpiredIds()); + } + + return candidateIds; + } + + + private Set<SessionManager> getAllSessionManagers() + { + HashSet<SessionManager> managers = new HashSet<>(); + + Handler[] contexts = _server.getChildHandlersByClass(ContextHandler.class); + for (int i=0; contexts!=null && i<contexts.length; i++) + { + SessionHandler sessionHandler = ((ContextHandler)contexts[i]).getChildHandlerByClass(SessionHandler.class); + if (sessionHandler != null) + { + SessionManager manager = sessionHandler.getSessionManager(); + if (manager != null && manager instanceof JDBCSessionManager) + managers.add(manager); + } + } + return managers; + } @@ -1411,7 +1470,7 @@ public class JDBCSessionIdManager extends AbstractSessionIdManager { if (expiredIds == null || expiredIds.isEmpty()) return; - + String[] ids = expiredIds.toArray(new String[expiredIds.size()]); try (Connection con = getConnection()) { diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/session/JDBCSessionManager.java b/jetty-server/src/main/java/org/eclipse/jetty/server/session/JDBCSessionManager.java index bae26477b3..6962694507 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/session/JDBCSessionManager.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/session/JDBCSessionManager.java @@ -535,6 +535,7 @@ public class JDBCSessionManager extends AbstractSessionManager session.setLastNode(getSessionIdManager().getWorkerName()); _sessions.put(idInCluster, session); + _sessionsStats.increment(); //update in db try @@ -569,6 +570,11 @@ public class JDBCSessionManager extends AbstractSessionManager } else { + if (memSession != null) + { + //Session must have been removed from db by another node + removeSession(memSession, true); + } //No session in db with matching id and context path. LOG.debug("getSession({}): No session in database matching id={}",idInCluster,idInCluster); } @@ -838,6 +844,7 @@ public class JDBCSessionManager extends AbstractSessionManager //loaded an expired session last managed on this node for this context, add it to the list so we can //treat it like a normal expired session _sessions.put(session.getClusterId(), session); + _sessionsStats.increment(); } else { @@ -866,7 +873,54 @@ public class JDBCSessionManager extends AbstractSessionManager } } - + protected void expireCandidates (Set<String> candidateIds) + { + Iterator<String> itor = candidateIds.iterator(); + long now = System.currentTimeMillis(); + while (itor.hasNext()) + { + String id = itor.next(); + + //check if expired in db + try + { + Session memSession = _sessions.get(id); + if (memSession == null) + { + continue; //no longer in memory + } + + Session s = loadSession(id, canonicalize(_context.getContextPath()), getVirtualHost(_context)); + if (s == null) + { + //session no longer exists, can be safely expired + memSession.timeout(); + } + } + catch (Exception e) + { + LOG.warn("Error checking db for expiry for session {}", id); + } + } + } + + protected Set<String> getCandidateExpiredIds () + { + HashSet<String> expiredIds = new HashSet<>(); + + Iterator<String> itor = _sessions.keySet().iterator(); + while (itor.hasNext()) + { + String id = itor.next(); + //check to see if session should have expired + Session session = _sessions.get(id); + if (session._expiryTime > 0 && System.currentTimeMillis() > session._expiryTime) + expiredIds.add(id); + } + return expiredIds; + } + + /** * Load a session from the database * @param id the id diff --git a/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/ServletMapping.java b/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/ServletMapping.java index ab181862f4..ae10b7d872 100644 --- a/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/ServletMapping.java +++ b/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/ServletMapping.java @@ -65,6 +65,25 @@ public class ServletMapping { _pathSpecs = pathSpecs; } + + + /* ------------------------------------------------------------ */ + /** Test if the list of path specs contains a particular one. + * @param pathSpec the path spec + * @return true if path spec matches something in mappings + */ + public boolean containsPathSpec (String pathSpec) + { + if (_pathSpecs == null || _pathSpecs.length == 0) + return false; + + for (String p:_pathSpecs) + { + if (p.equals(pathSpec)) + return true; + } + return false; + } /* ------------------------------------------------------------ */ /** diff --git a/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/PushCacheFilter.java b/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/PushCacheFilter.java index d6710d74db..544c392a9c 100644 --- a/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/PushCacheFilter.java +++ b/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/PushCacheFilter.java @@ -34,7 +34,6 @@ import java.util.concurrent.atomic.AtomicLong; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; -import javax.servlet.RequestDispatcher; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; @@ -45,7 +44,7 @@ import org.eclipse.jetty.http.HttpFields; import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpURI; import org.eclipse.jetty.http.HttpVersion; -import org.eclipse.jetty.server.Dispatcher; +import org.eclipse.jetty.server.PushBuilder; import org.eclipse.jetty.server.Request; import org.eclipse.jetty.util.StringUtil; import org.eclipse.jetty.util.URIUtil; @@ -157,6 +156,9 @@ public class PushCacheFilter implements Filter LOG.debug("{} {} referrer={} conditional={}", request.getMethod(), request.getRequestURI(), referrer, conditional); String path = URIUtil.addPaths(request.getServletPath(), request.getPathInfo()); + String query = request.getQueryString(); + if (query != null) + path += "?" + query; if (referrer != null) { HttpURI referrerURI = new HttpURI(referrer); @@ -186,14 +188,13 @@ public class PushCacheFilter implements Filter long primaryTimestamp = primaryResource._timestamp.get(); if (primaryTimestamp != 0) { - RequestDispatcher dispatcher = request.getServletContext().getRequestDispatcher(path); if (now - primaryTimestamp < TimeUnit.MILLISECONDS.toNanos(_associatePeriod)) { - ConcurrentMap<String, RequestDispatcher> associated = primaryResource._associated; + ConcurrentMap<String, String> associated = primaryResource._associated; // Not strictly concurrent-safe, just best effort to limit associations. if (associated.size() <= _maxAssociations) { - if (associated.putIfAbsent(path, dispatcher) == null) + if (associated.putIfAbsent(path, path) == null) { if (LOG.isDebugEnabled()) LOG.debug("Associated {} to {}", path, referrerPathNoContext); @@ -253,11 +254,14 @@ public class PushCacheFilter implements Filter // Push associated for non conditional if (!conditional && !primaryResource._associated.isEmpty()) { - for (RequestDispatcher dispatcher : primaryResource._associated.values()) + PushBuilder builder = Request.getBaseRequest(request).getPushBuilder(); + + for (String associated : primaryResource._associated.values()) { if (LOG.isDebugEnabled()) - LOG.debug("Pushing {} for {}", dispatcher, path); - ((Dispatcher)dispatcher).push(request); + LOG.debug("Pushing {} for {}", associated, path); + + builder.path(associated).push(); } } @@ -297,7 +301,7 @@ public class PushCacheFilter implements Filter private static class PrimaryResource { - private final ConcurrentMap<String, RequestDispatcher> _associated = new ConcurrentHashMap<>(); + private final ConcurrentMap<String, String> _associated = new ConcurrentHashMap<>(); private final AtomicLong _timestamp = new AtomicLong(); } } diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/MultiPartInputStreamParser.java b/jetty-util/src/main/java/org/eclipse/jetty/util/MultiPartInputStreamParser.java index 179b6f318d..755e6bd830 100644 --- a/jetty-util/src/main/java/org/eclipse/jetty/util/MultiPartInputStreamParser.java +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/MultiPartInputStreamParser.java @@ -127,7 +127,7 @@ public class MultiPartInputStreamParser if (MultiPartInputStreamParser.this._config.getFileSizeThreshold() > 0 && _size + length > MultiPartInputStreamParser.this._config.getFileSizeThreshold() && _file==null) createFile(); - + _out.write(bytes, offset, length); _size += length; } @@ -136,7 +136,7 @@ public class MultiPartInputStreamParser throws IOException { _file = File.createTempFile("MultiPart", "", MultiPartInputStreamParser.this._tmpDir); - + if (_deleteOnExit) _file.deleteOnExit(); FileOutputStream fos = new FileOutputStream(_file); @@ -175,7 +175,7 @@ public class MultiPartInputStreamParser { if (name == null) return null; - return (String)_headers.getValue(name.toLowerCase(Locale.ENGLISH), 0); + return _headers.getValue(name.toLowerCase(Locale.ENGLISH), 0); } /** @@ -211,8 +211,8 @@ public class MultiPartInputStreamParser } } - - /** + + /** * @see javax.servlet.http.Part#getSubmittedFileName() */ @Override @@ -241,7 +241,7 @@ public class MultiPartInputStreamParser */ public long getSize() { - return _size; + return _size; } /** @@ -252,7 +252,7 @@ public class MultiPartInputStreamParser if (_file == null) { _temporary = false; - + //part data is only in the ByteArrayOutputStream and never been written to disk _file = new File (_tmpDir, fileName); @@ -290,12 +290,12 @@ public class MultiPartInputStreamParser public void delete() throws IOException { if (_file != null && _file.exists()) - _file.delete(); + _file.delete(); } - + /** * Only remove tmp files. - * + * * @throws IOException if unable to delete the file */ public void cleanUp() throws IOException @@ -342,7 +342,7 @@ public class MultiPartInputStreamParser _contextTmpDir = contextTmpDir; if (_contextTmpDir == null) _contextTmpDir = new File (System.getProperty("java.io.tmpdir")); - + if (_config == null) _config = new MultipartConfigElement(_contextTmpDir.getAbsolutePath()); } @@ -357,7 +357,7 @@ public class MultiPartInputStreamParser return Collections.emptyList(); Collection<List<Part>> values = _parts.values(); - List<Part> parts = new ArrayList<Part>(); + List<Part> parts = new ArrayList<>(); for (List<Part> o: values) { List<Part> asList = LazyList.getList(o, false); @@ -368,7 +368,7 @@ public class MultiPartInputStreamParser /** * Delete any tmp storage for parts, and clear out the parts list. - * + * * @throws MultiException if unable to delete the parts */ public void deleteParts () @@ -381,22 +381,22 @@ public class MultiPartInputStreamParser try { ((MultiPartInputStreamParser.MultiPart)p).cleanUp(); - } + } catch(Exception e) - { - err.add(e); + { + err.add(e); } } _parts.clear(); - + err.ifExceptionThrowMulti(); } - + /** * Parse, if necessary, the multipart data and return the list of Parts. - * - * @return the parts + * + * @return the parts * @throws IOException if unable to get the parts */ public Collection<Part> getParts() @@ -404,7 +404,7 @@ public class MultiPartInputStreamParser { parse(); Collection<List<Part>> values = _parts.values(); - List<Part> parts = new ArrayList<Part>(); + List<Part> parts = new ArrayList<>(); for (List<Part> o: values) { List<Part> asList = LazyList.getList(o, false); @@ -416,7 +416,7 @@ public class MultiPartInputStreamParser /** * Get the named Part. - * + * * @param name the part name * @return the parts * @throws IOException if unable to get the part @@ -425,13 +425,13 @@ public class MultiPartInputStreamParser throws IOException { parse(); - return (Part)_parts.getValue(name, 0); + return _parts.getValue(name, 0); } /** * Parse, if necessary, the multipart stream. - * + * * @throws IOException if unable to parse */ protected void parse () @@ -443,7 +443,7 @@ public class MultiPartInputStreamParser //initialize long total = 0; //keep running total of size of bytes read from input and throw an exception if exceeds MultipartConfigElement._maxRequestSize - _parts = new MultiMap<Part>(); + _parts = new MultiMap<>(); //if its not a multipart request, don't parse it if (_contentType == null || !_contentType.startsWith("multipart/form-data")) @@ -475,28 +475,29 @@ public class MultiPartInputStreamParser bend = (bend < 0? _contentType.length(): bend); contentTypeBoundary = QuotedStringTokenizer.unquote(value(_contentType.substring(bstart,bend)).trim()); } - + String boundary="--"+contentTypeBoundary; - byte[] byteBoundary=(boundary+"--").getBytes(StandardCharsets.ISO_8859_1); + String lastBoundary=boundary+"--"; + byte[] byteBoundary=lastBoundary.getBytes(StandardCharsets.ISO_8859_1); // Get first boundary String line = null; try { - line=((ReadLineInputStream)_in).readLine(); + line=((ReadLineInputStream)_in).readLine(); } catch (IOException e) { LOG.warn("Badly formatted multipart request"); throw e; } - + if (line == null) throw new IOException("Missing content for multipart request"); - + boolean badFormatLogged = false; line=line.trim(); - while (line != null && !line.equals(boundary)) + while (line != null && !line.equals(boundary) && !line.equals(lastBoundary)) { if (!badFormatLogged) { @@ -510,6 +511,10 @@ public class MultiPartInputStreamParser if (line == null) throw new IOException("Missing initial multi part boundary"); + // Empty multipart. + if (line.equals(lastBoundary)) + return; + // Read each part boolean lastPart=false; @@ -518,20 +523,20 @@ public class MultiPartInputStreamParser String contentDisposition=null; String contentType=null; String contentTransferEncoding=null; - - MultiMap<String> headers = new MultiMap<String>(); + + MultiMap<String> headers = new MultiMap<>(); while(true) { line=((ReadLineInputStream)_in).readLine(); - + //No more input if(line==null) break outer; - + //end of headers: if("".equals(line)) break; - + total += line.length(); if (_config.getMaxRequestSize() > 0 && total > _config.getMaxRequestSize()) throw new IllegalStateException ("Request exceeds maxRequestSize ("+_config.getMaxRequestSize()+")"); @@ -595,7 +600,7 @@ public class MultiPartInputStreamParser part.setContentType(contentType); _parts.add(name, part); part.open(); - + InputStream partInput = null; if ("base64".equalsIgnoreCase(contentTransferEncoding)) { @@ -627,7 +632,7 @@ public class MultiPartInputStreamParser else partInput = _in; - + try { int state=-2; @@ -646,7 +651,7 @@ public class MultiPartInputStreamParser throw new IllegalStateException("Request exceeds maxRequestSize ("+_config.getMaxRequestSize()+")"); state=-2; - + // look for CR and/or LF if(c==13||c==10) { @@ -661,7 +666,7 @@ public class MultiPartInputStreamParser } break; } - + // Look for boundary if(b>=0&&b<byteBoundary.length&&c==byteBoundary[b]) { @@ -685,7 +690,7 @@ public class MultiPartInputStreamParser part.write(c); } } - + // Check for incomplete boundary match, writing out the chars we matched along the way if((b>0&&b<byteBoundary.length-2)||(b==byteBoundary.length-1)) { @@ -699,18 +704,18 @@ public class MultiPartInputStreamParser part.write(byteBoundary,0,b); b=-1; } - + // Boundary match. If we've run out of input or we matched the entire final boundary marker, then this is the last part. if(b>0||c==-1) { - + if(b==byteBoundary.length) lastPart=true; if(state==10) state=-2; break; } - + // handle CR LF if(cr) part.write(13); @@ -733,7 +738,7 @@ public class MultiPartInputStreamParser if (!lastPart) throw new IOException("Incomplete parts"); } - + public void setDeleteOnExit(boolean deleteOnExit) { _deleteOnExit = deleteOnExit; @@ -753,8 +758,8 @@ public class MultiPartInputStreamParser String value = nameEqualsValue.substring(idx+1).trim(); return QuotedStringTokenizer.unquoteOnly(value); } - - + + /* ------------------------------------------------------------ */ private String filenameValue(String nameEqualsValue) { @@ -782,7 +787,7 @@ public class MultiPartInputStreamParser return QuotedStringTokenizer.unquoteOnly(value, true); } - + private static class Base64InputStream extends InputStream { @@ -791,7 +796,7 @@ public class MultiPartInputStreamParser byte[] _buffer; int _pos; - + public Base64InputStream(ReadLineInputStream rlis) { _in = rlis; @@ -806,7 +811,7 @@ public class MultiPartInputStreamParser //We need to put them back into the bytes returned from this //method because the parsing of the multipart content uses them //as markers to determine when we've reached the end of a part. - _line = _in.readLine(); + _line = _in.readLine(); if (_line==null) return -1; //nothing left if (_line.startsWith("--")) @@ -824,7 +829,7 @@ public class MultiPartInputStreamParser _pos=0; } - + return _buffer[_pos++]; } } diff --git a/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/CachingWebAppClassLoader.java b/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/CachingWebAppClassLoader.java new file mode 100644 index 0000000000..b099a34810 --- /dev/null +++ b/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/CachingWebAppClassLoader.java @@ -0,0 +1,121 @@ +// +// ======================================================================== +// Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.webapp; + +import java.io.IOException; +import java.net.URL; +import java.util.concurrent.ConcurrentHashMap; + +import org.eclipse.jetty.util.ConcurrentHashSet; +import org.eclipse.jetty.util.annotation.ManagedObject; +import org.eclipse.jetty.util.annotation.ManagedOperation; + + +/** + * A WebAppClassLoader that caches {@link #getResource(String)} results. + * Specifically this ClassLoader caches not found classes and resources, + * which can greatly increase performance for applications that search + * for resources. + */ +@ManagedObject +public class CachingWebAppClassLoader extends WebAppClassLoader +{ + private final ConcurrentHashSet<String> _notFound = new ConcurrentHashSet<>(); + private final ConcurrentHashMap<String,URL> _cache = new ConcurrentHashMap<>(); + + public CachingWebAppClassLoader(ClassLoader parent, Context context) throws IOException + { + super(parent,context); + } + + public CachingWebAppClassLoader(Context context) throws IOException + { + super(context); + } + + @Override + public URL getResource(String name) + { + if (_notFound.contains(name)) + return null; + + URL url = _cache.get(name); + + if (name==null) + { + url = super.getResource(name); + + if (url==null) + { + _notFound.add(name); + } + else + { + _cache.putIfAbsent(name,url); + } + } + + return url; + } + + @Override + protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException + { + if (_notFound.contains(name)) + throw new ClassNotFoundException(name+": in notfound cache"); + try + { + return super.loadClass(name,resolve); + } + catch (ClassNotFoundException nfe) + { + _notFound.add(name); + throw nfe; + } + } + + @Override + protected Class<?> findClass(String name) throws ClassNotFoundException + { + if (_notFound.contains(name)) + throw new ClassNotFoundException(name+": in notfound cache"); + try + { + return super.findClass(name); + } + catch (ClassNotFoundException nfe) + { + _notFound.add(name); + throw nfe; + } + } + + @ManagedOperation + public void clearCache() + { + _cache.clear(); + _notFound.clear(); + } + + @Override + public String toString() + { + return "Caching["+super.toString()+"]"; + } +} diff --git a/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/StandardDescriptorProcessor.java b/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/StandardDescriptorProcessor.java index 75ee94edc9..b2dc458675 100644 --- a/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/StandardDescriptorProcessor.java +++ b/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/StandardDescriptorProcessor.java @@ -51,6 +51,7 @@ import org.eclipse.jetty.servlet.ServletHolder; import org.eclipse.jetty.servlet.ServletMapping; import org.eclipse.jetty.util.ArrayUtil; import org.eclipse.jetty.util.Loader; +import org.eclipse.jetty.util.StringUtil; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; import org.eclipse.jetty.util.security.Constraint; @@ -1210,8 +1211,9 @@ public class StandardDescriptorProcessor extends IterativeDescriptorProcessor //remove ps from the path specs on the existing mapping //if the mapping now has no pathspecs, remove it String[] updatedPaths = ArrayUtil.removeFromArray(sm.getPathSpecs(), ps); + if (updatedPaths == null || updatedPaths.length == 0) - { + { if (LOG.isDebugEnabled()) LOG.debug("Removed empty mapping {}",sm); listItor.remove(); } @@ -1230,9 +1232,9 @@ public class StandardDescriptorProcessor extends IterativeDescriptorProcessor paths.add(p); context.getMetaData().setOrigin(servletName+".servlet.mapping."+p, descriptor); } + mapping.setPathSpecs((String[]) paths.toArray(new String[paths.size()])); if (LOG.isDebugEnabled()) LOG.debug("Added mapping {} ",mapping); - _servletMappings.add(mapping); return mapping; } @@ -1367,10 +1369,52 @@ public class StandardDescriptorProcessor extends IterativeDescriptorProcessor //add mappings to the jsp servlet from the property-group mappings if (paths.size() > 0) { - ServletMapping mapping = new ServletMapping(); - mapping.setServletName("jsp"); - mapping.setPathSpecs(paths.toArray(new String[paths.size()])); - _servletMappings.add(mapping); + ServletMapping jspMapping = null; + for (ServletMapping m: _servletMappings) + { + if (m.getServletName().equals("jsp")) + { + jspMapping = m; + break; + } + } + if (jspMapping != null) + { + if (jspMapping.getPathSpecs() == null) + { + //no paths in jsp servlet mapping, we will add all of ours + if (LOG.isDebugEnabled()) LOG.debug("Adding all paths from jsp-config to jsp servlet mapping"); + jspMapping.setPathSpecs(paths.toArray(new String[paths.size()])); + } + else + { + //check if each of our paths is already present in existing mapping + ListIterator<String> piterator = paths.listIterator(); + while (piterator.hasNext()) + { + String p = piterator.next(); + if (jspMapping.containsPathSpec(p)) + piterator.remove(); + } + + //any remaining paths, add to the jspMapping + if (paths.size() > 0) + { + for (String p:jspMapping.getPathSpecs()) + paths.add(p); + if (LOG.isDebugEnabled()) LOG.debug("Adding extra paths from jsp-config to jsp servlet mapping"); + jspMapping.setPathSpecs((String[])paths.toArray(new String[paths.size()])); + } + } + } + else + { + //no mapping for jsp yet, make one + ServletMapping mapping = new ServletMapping(); + mapping.setServletName("jsp"); + mapping.setPathSpecs(paths.toArray(new String[paths.size()])); + _servletMappings.add(mapping); + } } } @@ -975,5 +975,18 @@ <alpn.version>8.1.5.v20150921</alpn.version> </properties> </profile> + + <profile> + <id>8u66</id> + <activation> + <property> + <name>java.version</name> + <value>1.8.0_66</value> + </property> + </activation> + <properties> + <alpn.version>8.1.5.v20150921</alpn.version> + </properties> + </profile> </profiles> </project> diff --git a/tests/test-http-client-transport/pom.xml b/tests/test-http-client-transport/pom.xml index 321d387c0c..ec608ac8bc 100644 --- a/tests/test-http-client-transport/pom.xml +++ b/tests/test-http-client-transport/pom.xml @@ -90,6 +90,12 @@ <scope>test</scope> </dependency> <dependency> + <groupId>org.eclipse.jetty.fcgi</groupId> + <artifactId>fcgi-server</artifactId> + <version>${project.version}</version> + <scope>test</scope> + </dependency> + <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <scope>test</scope> diff --git a/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/AbstractTest.java b/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/AbstractTest.java index ca377c311c..19c237c7da 100644 --- a/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/AbstractTest.java +++ b/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/AbstractTest.java @@ -25,6 +25,8 @@ import org.eclipse.jetty.alpn.server.ALPNServerConnectionFactory; import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.client.HttpClientTransport; import org.eclipse.jetty.client.http.HttpClientTransportOverHTTP; +import org.eclipse.jetty.fcgi.client.http.HttpClientTransportOverFCGI; +import org.eclipse.jetty.fcgi.server.ServerFCGIConnectionFactory; import org.eclipse.jetty.http2.HTTP2Cipher; import org.eclipse.jetty.http2.client.HTTP2Client; import org.eclipse.jetty.http2.client.http.HttpClientTransportOverHTTP2; @@ -39,6 +41,7 @@ import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; import org.eclipse.jetty.server.SslConnectionFactory; import org.eclipse.jetty.toolchain.test.TestTracker; +import org.eclipse.jetty.util.SocketAddressResolver; import org.eclipse.jetty.util.ssl.SslContextFactory; import org.eclipse.jetty.util.thread.QueuedThreadPool; import org.junit.After; @@ -52,7 +55,7 @@ public abstract class AbstractTest @Parameterized.Parameters(name = "transport: {0}") public static Object[] parameters() throws Exception { - return new Object[]{Transport.HTTP, Transport.HTTPS, Transport.H2C, Transport.H2}; + return Transport.values(); } @Rule @@ -87,22 +90,28 @@ public abstract class AbstractTest QueuedThreadPool serverThreads = new QueuedThreadPool(); serverThreads.setName("server"); server = new Server(serverThreads); - connector = new ServerConnector(server, provideServerConnectionFactory(transport)); + connector = newServerConnector(server); server.addConnector(connector); server.setHandler(handler); server.start(); } + protected ServerConnector newServerConnector(Server server) + { + return new ServerConnector(server, provideServerConnectionFactory(transport)); + } + private void startClient() throws Exception { QueuedThreadPool clientThreads = new QueuedThreadPool(); clientThreads.setName("client"); client = new HttpClient(provideClientTransport(transport), sslContextFactory); client.setExecutor(clientThreads); + client.setSocketAddressResolver(new SocketAddressResolver.Sync()); client.start(); } - private ConnectionFactory[] provideServerConnectionFactory(Transport transport) + protected ConnectionFactory[] provideServerConnectionFactory(Transport transport) { List<ConnectionFactory> result = new ArrayList<>(); switch (transport) @@ -139,6 +148,11 @@ public abstract class AbstractTest result.add(h2); break; } + case FCGI: + { + result.add(new ServerFCGIConnectionFactory(new HttpConfiguration())); + break; + } default: { throw new IllegalArgumentException(); @@ -147,7 +161,7 @@ public abstract class AbstractTest return result.toArray(new ConnectionFactory[result.size()]); } - private HttpClientTransport provideClientTransport(Transport transport) + protected HttpClientTransport provideClientTransport(Transport transport) { switch (transport) { @@ -163,6 +177,10 @@ public abstract class AbstractTest http2Client.setSelectors(1); return new HttpClientTransportOverHTTP2(http2Client); } + case FCGI: + { + return new HttpClientTransportOverFCGI(1, false, ""); + } default: { throw new IllegalArgumentException(); @@ -176,6 +194,7 @@ public abstract class AbstractTest { case HTTP: case H2C: + case FCGI: return "http://localhost:" + connector.getLocalPort(); case HTTPS: case H2: @@ -185,6 +204,22 @@ public abstract class AbstractTest } } + protected boolean isTransportSecure() + { + switch (transport) + { + case HTTP: + case H2C: + case FCGI: + return false; + case HTTPS: + case H2: + return true; + default: + throw new IllegalArgumentException(); + } + } + @After public void stop() throws Exception { @@ -196,6 +231,6 @@ public abstract class AbstractTest protected enum Transport { - HTTP, HTTPS, H2C, H2 + HTTP, HTTPS, H2C, H2, FCGI } } diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientLoadTest.java b/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/HttpClientLoadTest.java index 7ddeea8b90..0fab406c2f 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientLoadTest.java +++ b/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/HttpClientLoadTest.java @@ -16,12 +16,11 @@ // ======================================================================== // -package org.eclipse.jetty.client; +package org.eclipse.jetty.http.client; import java.io.IOException; import java.nio.ByteBuffer; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; import java.util.Locale; import java.util.Random; @@ -34,29 +33,33 @@ import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import org.eclipse.jetty.client.api.Connection; +import org.eclipse.jetty.client.ConnectionPool; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.HttpClientTransport; +import org.eclipse.jetty.client.HttpDestination; +import org.eclipse.jetty.client.LeakTrackingConnectionPool; +import org.eclipse.jetty.client.Origin; import org.eclipse.jetty.client.api.Request; import org.eclipse.jetty.client.api.Response; import org.eclipse.jetty.client.api.Result; import org.eclipse.jetty.client.http.HttpClientTransportOverHTTP; -import org.eclipse.jetty.client.http.HttpConnectionOverHTTP; import org.eclipse.jetty.client.http.HttpDestinationOverHTTP; import org.eclipse.jetty.client.util.BytesContentProvider; +import org.eclipse.jetty.fcgi.client.http.HttpClientTransportOverFCGI; +import org.eclipse.jetty.fcgi.client.http.HttpDestinationOverFCGI; import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpMethod; -import org.eclipse.jetty.http.HttpScheme; +import org.eclipse.jetty.io.ArrayByteBufferPool; +import org.eclipse.jetty.io.ByteBufferPool; import org.eclipse.jetty.io.LeakTrackingByteBufferPool; import org.eclipse.jetty.io.MappedByteBufferPool; -import org.eclipse.jetty.server.AbstractConnectionFactory; -import org.eclipse.jetty.server.HttpConnectionFactory; +import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; import org.eclipse.jetty.server.handler.AbstractHandler; import org.eclipse.jetty.util.IO; import org.eclipse.jetty.util.LeakDetector; -import org.eclipse.jetty.util.SocketAddressResolver; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; -import org.eclipse.jetty.util.ssl.SslContextFactory; import org.eclipse.jetty.util.thread.Scheduler; import org.hamcrest.Matchers; import org.junit.Assert; @@ -64,66 +67,99 @@ import org.junit.Test; import static org.junit.Assert.assertThat; -public class HttpClientLoadTest extends AbstractHttpClientServerTest +public class HttpClientLoadTest extends AbstractTest { private final Logger logger = Log.getLogger(HttpClientLoadTest.class); + private final AtomicLong connectionLeaks = new AtomicLong(); - public HttpClientLoadTest(SslContextFactory sslContextFactory) + public HttpClientLoadTest(Transport transport) { - super(sslContextFactory); + super(transport); } - @Test - public void testIterative() throws Exception + @Override + protected ServerConnector newServerConnector(Server server) { int cores = Runtime.getRuntime().availableProcessors(); + ByteBufferPool byteBufferPool = new ArrayByteBufferPool(); + byteBufferPool = new LeakTrackingByteBufferPool(byteBufferPool); + return new ServerConnector(server, null, null, byteBufferPool, + 1, Math.min(1, cores / 2), provideServerConnectionFactory(transport)); + } - final AtomicLong connectionLeaks = new AtomicLong(); - - start(new LoadHandler()); - server.stop(); - server.removeConnector(connector); - LeakTrackingByteBufferPool serverBufferPool = new LeakTrackingByteBufferPool(new MappedByteBufferPool.Tagged()); - connector = new ServerConnector(server, connector.getExecutor(), connector.getScheduler(), - serverBufferPool , 1, Math.min(1, cores / 2), - AbstractConnectionFactory.getFactories(sslContextFactory, new HttpConnectionFactory())); - server.addConnector(connector); - server.start(); - - client.stop(); - - HttpClient newClient = new HttpClient(new HttpClientTransportOverHTTP() + @Override + protected HttpClientTransport provideClientTransport(Transport transport) + { + switch (transport) { - @Override - public HttpDestination newHttpDestination(Origin origin) + case HTTP: + case HTTPS: + { + return new HttpClientTransportOverHTTP(1) + { + @Override + public HttpDestination newHttpDestination(Origin origin) + { + return new HttpDestinationOverHTTP(getHttpClient(), origin) + { + @Override + protected ConnectionPool newConnectionPool(HttpClient client) + { + return new LeakTrackingConnectionPool(this, client.getMaxConnectionsPerDestination(), this) + { + @Override + protected void leaked(LeakDetector.LeakInfo leakInfo) + { + super.leaked(leakInfo); + connectionLeaks.incrementAndGet(); + } + }; + } + }; + } + }; + } + case FCGI: { - return new HttpDestinationOverHTTP(getHttpClient(), origin) + return new HttpClientTransportOverFCGI(1, false, "") { @Override - protected DuplexConnectionPool newConnectionPool(HttpClient client) + public HttpDestination newHttpDestination(Origin origin) { - return new LeakTrackingConnectionPool(this, client.getMaxConnectionsPerDestination(), this) + return new HttpDestinationOverFCGI(getHttpClient(), origin) { @Override - protected void leaked(LeakDetector.LeakInfo resource) + protected ConnectionPool newConnectionPool(HttpClient client) { - connectionLeaks.incrementAndGet(); + return new LeakTrackingConnectionPool(this, client.getMaxConnectionsPerDestination(), this) + { + @Override + protected void leaked(LeakDetector.LeakInfo leakInfo) + { + super.leaked(leakInfo); + connectionLeaks.incrementAndGet(); + } + }; } }; } }; } - }, sslContextFactory); - newClient.setExecutor(client.getExecutor()); - newClient.setSocketAddressResolver(new SocketAddressResolver.Sync()); - client = newClient; - LeakTrackingByteBufferPool clientBufferPool = new LeakTrackingByteBufferPool(new MappedByteBufferPool.Tagged()); - client.setByteBufferPool(clientBufferPool); + default: + { + return super.provideClientTransport(transport); + } + } + } + + @Test + public void testIterative() throws Exception + { + start(new LoadHandler()); + + client.setByteBufferPool(new LeakTrackingByteBufferPool(new MappedByteBufferPool.Tagged())); client.setMaxConnectionsPerDestination(32768); client.setMaxRequestsQueuedPerDestination(1024 * 1024); - client.setDispatchIO(false); - client.setStrictEventOrdering(false); - client.start(); Random random = new Random(); // At least 25k requests to warmup properly (use -XX:+PrintCompilation to verify JIT activity) @@ -143,13 +179,23 @@ public class HttpClientLoadTest extends AbstractHttpClientServerTest System.gc(); - assertThat("Server BufferPool - leaked acquires", serverBufferPool.getLeakedAcquires(), Matchers.is(0L)); - assertThat("Server BufferPool - leaked releases", serverBufferPool.getLeakedReleases(), Matchers.is(0L)); - assertThat("Server BufferPool - unreleased", serverBufferPool.getLeakedResources(), Matchers.is(0L)); + ByteBufferPool byteBufferPool = connector.getByteBufferPool(); + if (byteBufferPool instanceof LeakTrackingByteBufferPool) + { + LeakTrackingByteBufferPool serverBufferPool = (LeakTrackingByteBufferPool)byteBufferPool; + assertThat("Server BufferPool - leaked acquires", serverBufferPool.getLeakedAcquires(), Matchers.is(0L)); + assertThat("Server BufferPool - leaked releases", serverBufferPool.getLeakedReleases(), Matchers.is(0L)); + assertThat("Server BufferPool - unreleased", serverBufferPool.getLeakedResources(), Matchers.is(0L)); + } - assertThat("Client BufferPool - leaked acquires", clientBufferPool.getLeakedAcquires(), Matchers.is(0L)); - assertThat("Client BufferPool - leaked releases", clientBufferPool.getLeakedReleases(), Matchers.is(0L)); - assertThat("Client BufferPool - unreleased", clientBufferPool.getLeakedResources(), Matchers.is(0L)); + byteBufferPool = client.getByteBufferPool(); + if (byteBufferPool instanceof LeakTrackingByteBufferPool) + { + LeakTrackingByteBufferPool clientBufferPool = (LeakTrackingByteBufferPool)byteBufferPool; + assertThat("Client BufferPool - leaked acquires", clientBufferPool.getLeakedAcquires(), Matchers.is(0L)); + assertThat("Client BufferPool - leaked releases", clientBufferPool.getLeakedReleases(), Matchers.is(0L)); + assertThat("Client BufferPool - unreleased", clientBufferPool.getLeakedResources(), Matchers.is(0L)); + } assertThat("Connection Leaks", connectionLeaks.get(), Matchers.is(0L)); } @@ -159,29 +205,15 @@ public class HttpClientLoadTest extends AbstractHttpClientServerTest CountDownLatch latch = new CountDownLatch(iterations); List<String> failures = new ArrayList<>(); - int factor = logger.isDebugEnabled() ? 25 : 1; - factor *= "http".equalsIgnoreCase(scheme) ? 10 : 1000; + int factor = (logger.isDebugEnabled() ? 25 : 1) * 100; // Dumps the state of the client if the test takes too long final Thread testThread = Thread.currentThread(); - Scheduler.Task task = client.getScheduler().schedule(new Runnable() + Scheduler.Task task = client.getScheduler().schedule(() -> { - @Override - public void run() - { - logger.warn("Interrupting test, it is taking too long"); - for (String host : Arrays.asList("localhost", "127.0.0.1")) - { - HttpDestinationOverHTTP destination = (HttpDestinationOverHTTP)client.getDestination(scheme, host, connector.getLocalPort()); - DuplexConnectionPool connectionPool = destination.getConnectionPool(); - for (Connection connection : new ArrayList<>(connectionPool.getActiveConnections())) - { - HttpConnectionOverHTTP active = (HttpConnectionOverHTTP)connection; - logger.warn(active.getEndPoint() + " exchange " + active.getHttpChannel().getHttpExchange()); - } - } - testThread.interrupt(); - } + logger.warn("Interrupting test, it is taking too long"); + logger.warn(client.dump()); + testThread.interrupt(); }, iterations * factor, TimeUnit.MILLISECONDS); long begin = System.nanoTime(); @@ -209,7 +241,7 @@ public class HttpClientLoadTest extends AbstractHttpClientServerTest // Choose a random method HttpMethod method = random.nextBoolean() ? HttpMethod.GET : HttpMethod.POST; - boolean ssl = HttpScheme.HTTPS.is(scheme); + boolean ssl = isTransportSecure(); // Choose randomly whether to close the connection on the client or on the server boolean clientClose = false; @@ -222,7 +254,7 @@ public class HttpClientLoadTest extends AbstractHttpClientServerTest int maxContentLength = 64 * 1024; int contentLength = random.nextInt(maxContentLength) + 1; - test(scheme, host, method.asString(), clientClose, serverClose, contentLength, true, latch, failures); + test(ssl ? "https" : "http", host, method.asString(), clientClose, serverClose, contentLength, true, latch, failures); } private void test(String scheme, String host, String method, boolean clientClose, boolean serverClose, int contentLength, final boolean checkContentLength, final CountDownLatch latch, final List<String> failures) throws InterruptedException @@ -298,6 +330,7 @@ public class HttpClientLoadTest extends AbstractHttpClientServerTest switch (method) { case "GET": + { int contentLength = request.getIntHeader("X-Download"); if (contentLength > 0) { @@ -305,10 +338,13 @@ public class HttpClientLoadTest extends AbstractHttpClientServerTest response.getOutputStream().write(new byte[contentLength]); } break; + } case "POST": + { response.setHeader("X-Content", request.getHeader("X-Upload")); IO.copy(request.getInputStream(), response.getOutputStream()); break; + } } if (Boolean.parseBoolean(request.getHeader("X-Close"))) diff --git a/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/HttpClientTest.java b/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/HttpClientTest.java index 56ffdd156d..5a17404c55 100644 --- a/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/HttpClientTest.java +++ b/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/HttpClientTest.java @@ -91,6 +91,7 @@ public class HttpClientTest extends AbstractTest public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { baseRequest.setHandled(true); + response.setContentLength(length); response.getOutputStream().write(bytes); } }); diff --git a/tests/test-sessions/test-infinispan-sessions/src/test/java/org/eclipse/jetty/server/session/LastAccessTimeTest.java b/tests/test-sessions/test-infinispan-sessions/src/test/java/org/eclipse/jetty/server/session/LastAccessTimeTest.java index 25c0ad2308..bb09b64333 100644 --- a/tests/test-sessions/test-infinispan-sessions/src/test/java/org/eclipse/jetty/server/session/LastAccessTimeTest.java +++ b/tests/test-sessions/test-infinispan-sessions/src/test/java/org/eclipse/jetty/server/session/LastAccessTimeTest.java @@ -18,14 +18,6 @@ package org.eclipse.jetty.server.session; -import java.io.File; - -import org.eclipse.jetty.util.IO; -import org.infinispan.Cache; -import org.infinispan.configuration.cache.Configuration; -import org.infinispan.configuration.cache.ConfigurationBuilder; -import org.infinispan.manager.DefaultCacheManager; -import org.infinispan.manager.EmbeddedCacheManager; import org.junit.AfterClass; import org.junit.BeforeClass; @@ -61,5 +53,13 @@ public class LastAccessTimeTest extends AbstractLastAccessTimeTest super.testLastAccessTime(); } + @Override + public void assertAfterScavenge(AbstractSessionManager manager) + { + //The infinispan session manager will remove a session from its local memory that was a candidate to be scavenged if + //it checks with the cluster and discovers that another node is managing it, so the count is 0 + assertSessionCounts(0, 1, 1, manager); + } + } diff --git a/tests/test-sessions/test-jdbc-sessions/pom.xml b/tests/test-sessions/test-jdbc-sessions/pom.xml index 9d4a1b8a15..b4a75a545a 100644 --- a/tests/test-sessions/test-jdbc-sessions/pom.xml +++ b/tests/test-sessions/test-jdbc-sessions/pom.xml @@ -65,13 +65,13 @@ <dependency> <groupId>org.apache.derby</groupId> <artifactId>derby</artifactId> - <version>10.4.1.3</version> + <version>10.12.1.1</version> <scope>test</scope> </dependency> <dependency> <groupId>org.apache.derby</groupId> <artifactId>derbytools</artifactId> - <version>10.4.1.3</version> + <version>10.12.1.1</version> <scope>test</scope> </dependency> <dependency> diff --git a/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/ClientCrossContextSessionTest.java b/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/ClientCrossContextSessionTest.java index eb4109b1f8..a5348ef81f 100644 --- a/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/ClientCrossContextSessionTest.java +++ b/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/ClientCrossContextSessionTest.java @@ -18,9 +18,6 @@ package org.eclipse.jetty.server.session; -import java.sql.DriverManager; -import java.sql.SQLException; - import org.junit.After; import org.junit.Test; @@ -46,12 +43,8 @@ public class ClientCrossContextSessionTest extends AbstractClientCrossContextSes @After public void tearDown() throws Exception { - try - { - DriverManager.getConnection( "jdbc:derby:sessions;shutdown=true" ); - } - catch( SQLException expected ) - { - } + JdbcTestServer.shutdown(null); } + + } diff --git a/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/DirtyAttributeTest.java b/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/DirtyAttributeTest.java index 5d2379ab6c..e123a80b2d 100644 --- a/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/DirtyAttributeTest.java +++ b/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/DirtyAttributeTest.java @@ -38,6 +38,7 @@ import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.client.api.ContentResponse; import org.eclipse.jetty.client.api.Request; import org.eclipse.jetty.servlet.ServletContextHandler; +import org.junit.After; import org.junit.Test; @@ -127,6 +128,14 @@ public class DirtyAttributeTest } } + + @After + public void tearDown() throws Exception + { + JdbcTestServer.shutdown(null); + } + + public static class TestValue implements HttpSessionActivationListener, HttpSessionBindingListener, Serializable { int passivates = 0; diff --git a/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/ForwardedSessionTest.java b/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/ForwardedSessionTest.java index 7d2f42b976..de843b8608 100644 --- a/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/ForwardedSessionTest.java +++ b/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/ForwardedSessionTest.java @@ -19,6 +19,7 @@ package org.eclipse.jetty.server.session; +import org.junit.After; import org.junit.Test; /** @@ -45,6 +46,12 @@ public class ForwardedSessionTest extends AbstractForwardedSessionTest } + @After + public void tearDown() throws Exception + { + JdbcTestServer.shutdown(null); + } + } diff --git a/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/ImmortalSessionTest.java b/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/ImmortalSessionTest.java index cabd7248e0..fd4b53e796 100644 --- a/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/ImmortalSessionTest.java +++ b/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/ImmortalSessionTest.java @@ -18,9 +18,6 @@ package org.eclipse.jetty.server.session; -import java.sql.DriverManager; -import java.sql.SQLException; - import org.junit.After; import org.junit.Test; @@ -46,12 +43,7 @@ public class ImmortalSessionTest extends AbstractImmortalSessionTest @After public void tearDown() throws Exception { - try - { - DriverManager.getConnection( "jdbc:derby:sessions;shutdown=true" ); - } - catch( SQLException expected ) - { - } + JdbcTestServer.shutdown(null); } + } diff --git a/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/InvalidationSessionTest.java b/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/InvalidationSessionTest.java index 8075dbdfec..dd68fff5b5 100644 --- a/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/InvalidationSessionTest.java +++ b/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/InvalidationSessionTest.java @@ -18,9 +18,6 @@ package org.eclipse.jetty.server.session; -import java.sql.DriverManager; -import java.sql.SQLException; - import org.junit.After; import org.junit.Test; @@ -60,12 +57,6 @@ public class InvalidationSessionTest extends AbstractInvalidationSessionTest @After public void tearDown() throws Exception { - try - { - DriverManager.getConnection( "jdbc:derby:sessions;shutdown=true" ); - } - catch( SQLException expected ) - { - } + JdbcTestServer.shutdown(null); } } diff --git a/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/JdbcTestServer.java b/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/JdbcTestServer.java index 5b55861707..03e9ff1d06 100644 --- a/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/JdbcTestServer.java +++ b/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/JdbcTestServer.java @@ -22,6 +22,7 @@ import java.sql.Connection; import java.sql.DriverManager; import java.sql.PreparedStatement; import java.sql.ResultSet; +import java.sql.SQLException; import java.util.HashSet; import java.util.Set; @@ -35,7 +36,8 @@ import org.eclipse.jetty.toolchain.test.MavenTestingUtils; public class JdbcTestServer extends AbstractTestServer { public static final String DRIVER_CLASS = "org.apache.derby.jdbc.EmbeddedDriver"; - public static final String DEFAULT_CONNECTION_URL = "jdbc:derby:sessions;create=true"; + public static final String DEFAULT_CONNECTION_URL = "jdbc:derby:memory:sessions;create=true"; + public static final String DEFAULT_SHUTDOWN_URL = "jdbc:derby:memory:sessions;drop=true"; public static final int SAVE_INTERVAL = 1; @@ -43,6 +45,26 @@ public class JdbcTestServer extends AbstractTestServer { System.setProperty("derby.system.home", MavenTestingUtils.getTargetFile("test-derby").getAbsolutePath()); } + + + public static void shutdown (String connectionUrl) + throws Exception + { + if (connectionUrl == null) + connectionUrl = DEFAULT_SHUTDOWN_URL; + + try + { + DriverManager.getConnection(connectionUrl); + } + catch( SQLException expected ) + { + if (!"08006".equals(expected.getSQLState())) + { + throw expected; + } + } + } public JdbcTestServer(int port) diff --git a/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/LastAccessTimeTest.java b/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/LastAccessTimeTest.java index 541c1ef41c..6939fb4683 100644 --- a/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/LastAccessTimeTest.java +++ b/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/LastAccessTimeTest.java @@ -18,9 +18,6 @@ package org.eclipse.jetty.server.session; -import java.sql.DriverManager; -import java.sql.SQLException; - import org.junit.After; import org.junit.Test; @@ -37,20 +34,13 @@ public class LastAccessTimeTest extends AbstractLastAccessTimeTest @Test public void testLastAccessTime() throws Exception { - // Log.getLog().setDebugEnabled(true); super.testLastAccessTime(); } @After public void tearDown() throws Exception { - try - { - DriverManager.getConnection( "jdbc:derby:sessions;shutdown=true" ); - } - catch( SQLException expected ) - { - } + JdbcTestServer.shutdown(null); } } diff --git a/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/LocalSessionScavengingTest.java b/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/LocalSessionScavengingTest.java index 87eec2c6dd..ac89427445 100644 --- a/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/LocalSessionScavengingTest.java +++ b/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/LocalSessionScavengingTest.java @@ -18,9 +18,6 @@ package org.eclipse.jetty.server.session; -import java.sql.DriverManager; -import java.sql.SQLException; - import org.junit.After; import org.junit.Test; @@ -56,16 +53,11 @@ public class LocalSessionScavengingTest extends AbstractLocalSessionScavengingTe { super.testLocalSessionsScavenging(); } + @After public void tearDown() throws Exception { - try - { - DriverManager.getConnection( "jdbc:derby:sessions;shutdown=true" ); - } - catch( SQLException expected ) - { - } + JdbcTestServer.shutdown(null); } } diff --git a/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/MaxInactiveMigrationTest.java b/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/MaxInactiveMigrationTest.java index 6256945aed..b32500aa77 100644 --- a/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/MaxInactiveMigrationTest.java +++ b/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/MaxInactiveMigrationTest.java @@ -23,8 +23,6 @@ import static org.junit.Assert.assertTrue; import java.io.IOException; import java.io.PrintWriter; -import java.sql.DriverManager; -import java.sql.SQLException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; @@ -81,13 +79,8 @@ public class MaxInactiveMigrationTest testServer1.stop(); testServer2.stop(); client.stop(); - try - { - DriverManager.getConnection( "jdbc:derby:sessions;shutdown=true" ); - } - catch( SQLException expected ) - { - } + + JdbcTestServer.shutdown(null); } diff --git a/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/ModifyMaxInactiveIntervalTest.java b/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/ModifyMaxInactiveIntervalTest.java index a39b655f73..d777a61ee3 100644 --- a/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/ModifyMaxInactiveIntervalTest.java +++ b/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/ModifyMaxInactiveIntervalTest.java @@ -33,6 +33,7 @@ import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.client.api.ContentResponse; import org.eclipse.jetty.client.api.Request; import org.eclipse.jetty.servlet.ServletContextHandler; +import org.junit.After; import org.junit.Test; @@ -103,6 +104,13 @@ public class ModifyMaxInactiveIntervalTest } } + + @After + public void tearDown() throws Exception + { + JdbcTestServer.shutdown(null); + } + public static class TestModServlet extends HttpServlet { @Override diff --git a/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/NewSessionTest.java b/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/NewSessionTest.java index fc05235225..5a6006cdd1 100644 --- a/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/NewSessionTest.java +++ b/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/NewSessionTest.java @@ -18,9 +18,6 @@ package org.eclipse.jetty.server.session; -import java.sql.DriverManager; -import java.sql.SQLException; - import org.junit.After; import org.junit.Test; @@ -46,12 +43,7 @@ public class NewSessionTest extends AbstractNewSessionTest @After public void tearDown() throws Exception { - try - { - DriverManager.getConnection( "jdbc:derby:sessions;shutdown=true" ); - } - catch( SQLException expected ) - { - } + JdbcTestServer.shutdown(null); } + } diff --git a/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/OrphanedSessionTest.java b/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/OrphanedSessionTest.java index 391f3e0e49..4042f373fe 100644 --- a/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/OrphanedSessionTest.java +++ b/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/OrphanedSessionTest.java @@ -18,9 +18,6 @@ package org.eclipse.jetty.server.session; -import java.sql.DriverManager; -import java.sql.SQLException; - import org.junit.After; import org.junit.Test; @@ -43,12 +40,6 @@ public class OrphanedSessionTest extends AbstractOrphanedSessionTest @After public void tearDown() throws Exception { - try - { - DriverManager.getConnection( "jdbc:derby:sessions;shutdown=true" ); - } - catch( SQLException expected ) - { - } + JdbcTestServer.shutdown(null); } } diff --git a/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/ProxySerializationTest.java b/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/ProxySerializationTest.java index 38db019ce5..6efffa406a 100644 --- a/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/ProxySerializationTest.java +++ b/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/ProxySerializationTest.java @@ -20,6 +20,7 @@ package org.eclipse.jetty.server.session; import org.eclipse.jetty.servlet.ServletContextHandler; +import org.junit.After; import org.junit.Test; /** @@ -55,4 +56,11 @@ public class ProxySerializationTest extends AbstractProxySerializationTest super.testProxySerialization(); } + + + @After + public void tearDown() throws Exception + { + JdbcTestServer.shutdown(null); + } } diff --git a/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/ReentrantRequestSessionTest.java b/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/ReentrantRequestSessionTest.java index dced30d8d2..82b3d5f6eb 100644 --- a/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/ReentrantRequestSessionTest.java +++ b/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/ReentrantRequestSessionTest.java @@ -18,9 +18,6 @@ package org.eclipse.jetty.server.session; -import java.sql.DriverManager; -import java.sql.SQLException; - import org.junit.After; import org.junit.Test; @@ -42,15 +39,11 @@ public class ReentrantRequestSessionTest extends AbstractReentrantRequestSession super.testReentrantRequestSession(); } + @After public void tearDown() throws Exception { - try - { - DriverManager.getConnection( "jdbc:derby:sessions;shutdown=true" ); - } - catch( SQLException expected ) - { - } + JdbcTestServer.shutdown(null); } + } diff --git a/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/ReloadedSessionMissingClassTest.java b/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/ReloadedSessionMissingClassTest.java index 4dbec1c397..44acb9d263 100644 --- a/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/ReloadedSessionMissingClassTest.java +++ b/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/ReloadedSessionMissingClassTest.java @@ -38,6 +38,7 @@ import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.StdErrLog; import org.eclipse.jetty.util.resource.Resource; import org.eclipse.jetty.webapp.WebAppContext; +import org.junit.After; import org.junit.Rule; import org.junit.Test; @@ -139,4 +140,11 @@ public class ReloadedSessionMissingClassTest server1.stop(); } } + + @After + public void tearDown() throws Exception + { + JdbcTestServer.shutdown(null); + } + } diff --git a/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/SaveIntervalTest.java b/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/SaveIntervalTest.java index 3b02825a9a..e031b29543 100644 --- a/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/SaveIntervalTest.java +++ b/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/SaveIntervalTest.java @@ -35,6 +35,7 @@ import org.eclipse.jetty.client.api.ContentResponse; import org.eclipse.jetty.client.api.Request; import org.eclipse.jetty.servlet.ServletContextHandler; import org.eclipse.jetty.servlet.ServletHolder; +import org.junit.After; import org.junit.Ignore; import org.junit.Test; @@ -133,6 +134,12 @@ public class SaveIntervalTest } } + @After + public void tearDown() throws Exception + { + JdbcTestServer.shutdown(null); + } + public static class TestSaveIntervalServlet extends HttpServlet { public HttpSession _session; diff --git a/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/ServerCrossContextSessionTest.java b/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/ServerCrossContextSessionTest.java index 83c69448d7..497881fc72 100644 --- a/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/ServerCrossContextSessionTest.java +++ b/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/ServerCrossContextSessionTest.java @@ -18,9 +18,6 @@ package org.eclipse.jetty.server.session; -import java.sql.DriverManager; -import java.sql.SQLException; - import org.junit.After; import org.junit.Test; @@ -44,12 +41,6 @@ public class ServerCrossContextSessionTest extends AbstractServerCrossContextSes @After public void tearDown() throws Exception { - try - { - DriverManager.getConnection( "jdbc:derby:sessions;shutdown=true" ); - } - catch( SQLException expected ) - { - } + JdbcTestServer.shutdown(null); } } diff --git a/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/SessionExpiryTest.java b/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/SessionExpiryTest.java index 5177b93987..581113df63 100644 --- a/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/SessionExpiryTest.java +++ b/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/SessionExpiryTest.java @@ -18,9 +18,6 @@ package org.eclipse.jetty.server.session; -import java.sql.DriverManager; -import java.sql.SQLException; - import org.junit.After; import org.junit.Test; @@ -58,18 +55,10 @@ public class SessionExpiryTest extends AbstractSessionExpiryTest super.testSessionNotExpired(); } - - - @After public void tearDown() throws Exception { - try - { - DriverManager.getConnection( "jdbc:derby:sessions;shutdown=true" ); - } - catch( SQLException expected ) - { - } + JdbcTestServer.shutdown(null); } + } diff --git a/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/SessionInvalidateAndCreateTest.java b/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/SessionInvalidateAndCreateTest.java index 746e0bd522..de88616878 100644 --- a/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/SessionInvalidateAndCreateTest.java +++ b/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/SessionInvalidateAndCreateTest.java @@ -19,6 +19,7 @@ package org.eclipse.jetty.server.session; +import org.junit.After; import org.junit.Test; public class SessionInvalidateAndCreateTest extends AbstractSessionInvalidateAndCreateTest @@ -35,4 +36,12 @@ public class SessionInvalidateAndCreateTest extends AbstractSessionInvalidateAnd { super.testSessionScavenge(); } + + + @After + public void tearDown() throws Exception + { + JdbcTestServer.shutdown(null); + } + } diff --git a/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/SessionMigrationTest.java b/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/SessionMigrationTest.java index 2ff6fa42bc..e8bbbf7509 100644 --- a/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/SessionMigrationTest.java +++ b/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/SessionMigrationTest.java @@ -18,9 +18,6 @@ package org.eclipse.jetty.server.session; -import java.sql.DriverManager; -import java.sql.SQLException; - import org.junit.After; import org.junit.Test; @@ -44,12 +41,6 @@ public class SessionMigrationTest extends AbstractSessionMigrationTest @After public void tearDown() throws Exception { - try - { - DriverManager.getConnection( "jdbc:derby:sessions;shutdown=true" ); - } - catch( SQLException expected ) - { - } + JdbcTestServer.shutdown(null); } } diff --git a/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/SessionRenewTest.java b/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/SessionRenewTest.java index 60416b6e8f..cfd4f15b1a 100644 --- a/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/SessionRenewTest.java +++ b/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/SessionRenewTest.java @@ -18,9 +18,6 @@ package org.eclipse.jetty.server.session; -import java.sql.DriverManager; -import java.sql.SQLException; - import org.junit.After; import org.junit.Test; @@ -39,16 +36,11 @@ public class SessionRenewTest extends AbstractSessionRenewTest super.testSessionRenewal(); } + @After public void tearDown() throws Exception { - try - { - DriverManager.getConnection( "jdbc:derby:sessions;shutdown=true" ); - } - catch( SQLException expected ) - { - } + JdbcTestServer.shutdown(null); } - + } diff --git a/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/SessionValueSavingTest.java b/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/SessionValueSavingTest.java index 41c25609f7..d647534943 100644 --- a/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/SessionValueSavingTest.java +++ b/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/SessionValueSavingTest.java @@ -18,9 +18,6 @@ package org.eclipse.jetty.server.session; -import java.sql.DriverManager; -import java.sql.SQLException; - import org.junit.After; import org.junit.Test; @@ -34,21 +31,16 @@ public class SessionValueSavingTest extends AbstractSessionValueSavingTest return new JdbcTestServer(port,max,scavenge); } - @Test - public void testSessionValueSaving() throws Exception - { - super.testSessionValueSaving(); - } + @Test + public void testSessionValueSaving() throws Exception + { + super.testSessionValueSaving(); + } + - @After - public void tearDown() throws Exception - { - try - { - DriverManager.getConnection( "jdbc:derby:sessions;shutdown=true" ); - } - catch( SQLException expected ) - { - } - } + @After + public void tearDown() throws Exception + { + JdbcTestServer.shutdown(null); + } } diff --git a/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/StopSessionManagerPreserveSessionTest.java b/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/StopSessionManagerPreserveSessionTest.java index 84b2ac32ad..1c43a75024 100644 --- a/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/StopSessionManagerPreserveSessionTest.java +++ b/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/StopSessionManagerPreserveSessionTest.java @@ -21,9 +21,6 @@ package org.eclipse.jetty.server.session; import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; -import java.sql.DriverManager; -import java.sql.SQLException; - import org.eclipse.jetty.servlet.ServletContextHandler; import org.junit.After; import org.junit.Test; @@ -31,17 +28,12 @@ import org.junit.Test; public class StopSessionManagerPreserveSessionTest extends AbstractStopSessionManagerPreserveSessionTest { JdbcTestServer _server; - + + @After public void tearDown() throws Exception { - try - { - DriverManager.getConnection( "jdbc:derby:sessions;shutdown=true" ); - } - catch( SQLException expected ) - { - } + JdbcTestServer.shutdown(null); } @Override diff --git a/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/WebAppObjectInSessionTest.java b/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/WebAppObjectInSessionTest.java index 8a480903b2..88d7df119d 100644 --- a/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/WebAppObjectInSessionTest.java +++ b/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/WebAppObjectInSessionTest.java @@ -18,9 +18,6 @@ package org.eclipse.jetty.server.session; -import java.sql.DriverManager; -import java.sql.SQLException; - import org.eclipse.jetty.util.resource.Resource; import org.junit.After; import org.junit.Test; @@ -45,17 +42,11 @@ public class WebAppObjectInSessionTest extends AbstractWebAppObjectInSessionTest super.testWebappObjectInSession(); } - @After public void tearDown() throws Exception { - try - { - DriverManager.getConnection( "jdbc:derby:sessions;shutdown=true" ); - } - catch( SQLException expected ) - { - } + JdbcTestServer.shutdown(null); } + } diff --git a/tests/test-sessions/test-mongodb-sessions/src/test/java/org/eclipse/jetty/nosql/mongodb/MongoTestServer.java b/tests/test-sessions/test-mongodb-sessions/src/test/java/org/eclipse/jetty/nosql/mongodb/MongoTestServer.java index e3c78c2222..0ada08a41d 100644 --- a/tests/test-sessions/test-mongodb-sessions/src/test/java/org/eclipse/jetty/nosql/mongodb/MongoTestServer.java +++ b/tests/test-sessions/test-mongodb-sessions/src/test/java/org/eclipse/jetty/nosql/mongodb/MongoTestServer.java @@ -39,6 +39,7 @@ public class MongoTestServer extends AbstractTestServer { static int __workers=0; private boolean _saveAllAttributes = false; // false save dirty, true save all + private int _saveInterval = 0; public static class TestMongoSessionIdManager extends MongoSessionIdManager @@ -70,13 +71,13 @@ public class MongoTestServer extends AbstractTestServer public MongoTestServer(int port, int maxInactivePeriod, int scavengePeriod) { super(port, maxInactivePeriod, scavengePeriod); + _saveInterval = 0; } public MongoTestServer(int port, int maxInactivePeriod, int scavengePeriod, boolean saveAllAttributes) { super(port, maxInactivePeriod, scavengePeriod); - _saveAllAttributes = saveAllAttributes; } @@ -109,10 +110,9 @@ public class MongoTestServer extends AbstractTestServer throw new RuntimeException(e); } - manager.setSavePeriod(1); + manager.setSavePeriod(_saveInterval); manager.setStalePeriod(0); manager.setSaveAllAttributes(_saveAllAttributes); - //manager.setScavengePeriod((int)TimeUnit.SECONDS.toMillis(_scavengePeriod)); return manager; } diff --git a/tests/test-sessions/test-sessions-common/src/main/java/org/eclipse/jetty/server/session/AbstractInvalidationSessionTest.java b/tests/test-sessions/test-sessions-common/src/main/java/org/eclipse/jetty/server/session/AbstractInvalidationSessionTest.java index 8d2d660404..de345efba3 100644 --- a/tests/test-sessions/test-sessions-common/src/main/java/org/eclipse/jetty/server/session/AbstractInvalidationSessionTest.java +++ b/tests/test-sessions/test-sessions-common/src/main/java/org/eclipse/jetty/server/session/AbstractInvalidationSessionTest.java @@ -32,6 +32,7 @@ import javax.servlet.http.HttpSession; import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.client.api.ContentResponse; import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.servlet.ServletContextHandler; import org.eclipse.jetty.util.thread.QueuedThreadPool; import org.junit.Test; @@ -51,16 +52,20 @@ public abstract class AbstractInvalidationSessionTest String contextPath = ""; String servletMapping = "/server"; AbstractTestServer server1 = createServer(0); - server1.addContext(contextPath).addServlet(TestServlet.class, servletMapping); + ServletContextHandler context1 = server1.addContext(contextPath); + context1.addServlet(TestServlet.class, servletMapping); + AbstractSessionManager m1 = (AbstractSessionManager) context1.getSessionHandler().getSessionManager(); try { server1.start(); int port1 = server1.getPort(); AbstractTestServer server2 = createServer(0); - server2.addContext(contextPath).addServlet(TestServlet.class, servletMapping); - + ServletContextHandler context2 = server2.addContext(contextPath); + context2.addServlet(TestServlet.class, servletMapping); + AbstractSessionManager m2 = (AbstractSessionManager) context2.getSessionHandler().getSessionManager(); + try { server2.start(); @@ -81,25 +86,33 @@ public abstract class AbstractInvalidationSessionTest assertEquals(HttpServletResponse.SC_OK,response1.getStatus()); String sessionCookie = response1.getHeaders().get("Set-Cookie"); assertTrue(sessionCookie != null); + assertEquals(1, m1.getSessions()); + assertEquals(1, m1.getSessionsMax()); + assertEquals(1, m1.getSessionsTotal()); + // Mangle the cookie, replacing Path with $Path, etc. sessionCookie = sessionCookie.replaceFirst("(\\W)(P|p)ath=", "$1\\$Path="); // Be sure the session is also present in node2 - Request request2 = client.newRequest(urls[1] + "?action=increment"); request2.header("Cookie", sessionCookie); ContentResponse response2 = request2.send(); assertEquals(HttpServletResponse.SC_OK,response2.getStatus()); - + assertEquals(1, m2.getSessions()); + assertEquals(1, m2.getSessionsMax()); + assertEquals(1, m2.getSessionsTotal()); + // Invalidate on node1 Request request1 = client.newRequest(urls[0] + "?action=invalidate"); request1.header("Cookie", sessionCookie); response1 = request1.send(); assertEquals(HttpServletResponse.SC_OK, response1.getStatus()); - - + assertEquals(0, m1.getSessions()); + assertEquals(1, m1.getSessionsMax()); + assertEquals(1, m1.getSessionsTotal()); + pause(); // Be sure on node2 we don't see the session anymore @@ -107,6 +120,9 @@ public abstract class AbstractInvalidationSessionTest request2.header("Cookie", sessionCookie); response2 = request2.send(); assertEquals(HttpServletResponse.SC_OK,response2.getStatus()); + assertEquals(0, m2.getSessions()); + assertEquals(1, m2.getSessionsMax()); + assertEquals(1, m2.getSessionsTotal()); } finally { diff --git a/tests/test-sessions/test-sessions-common/src/main/java/org/eclipse/jetty/server/session/AbstractLastAccessTimeTest.java b/tests/test-sessions/test-sessions-common/src/main/java/org/eclipse/jetty/server/session/AbstractLastAccessTimeTest.java index 787eb739f3..847e800dc8 100644 --- a/tests/test-sessions/test-sessions-common/src/main/java/org/eclipse/jetty/server/session/AbstractLastAccessTimeTest.java +++ b/tests/test-sessions/test-sessions-common/src/main/java/org/eclipse/jetty/server/session/AbstractLastAccessTimeTest.java @@ -65,15 +65,19 @@ public abstract class AbstractLastAccessTimeTest ServletHolder holder1 = new ServletHolder(servlet1); ServletContextHandler context = server1.addContext(contextPath); TestSessionListener listener1 = new TestSessionListener(); - context.addEventListener(listener1); + context.getSessionHandler().addEventListener(listener1); context.addServlet(holder1, servletMapping); + AbstractSessionManager m1 = (AbstractSessionManager)context.getSessionHandler().getSessionManager(); + try { server1.start(); int port1=server1.getPort(); AbstractTestServer server2 = createServer(0, maxInactivePeriod, scavengePeriod); - server2.addContext(contextPath).addServlet(TestServlet.class, servletMapping); + ServletContextHandler context2 = server2.addContext(contextPath); + context2.addServlet(TestServlet.class, servletMapping); + AbstractSessionManager m2 = (AbstractSessionManager)context2.getSessionHandler().getSessionManager(); try { @@ -89,9 +93,12 @@ public abstract class AbstractLastAccessTimeTest assertEquals("test", response1.getContentAsString()); String sessionCookie = response1.getHeaders().get("Set-Cookie"); assertTrue( sessionCookie != null ); + assertEquals(1, m1.getSessions()); + assertEquals(1, m1.getSessionsMax()); + assertEquals(1, m1.getSessionsTotal()); // Mangle the cookie, replacing Path with $Path, etc. - sessionCookie = sessionCookie.replaceFirst("(\\W)(P|p)ath=", "$1\\$Path="); - + sessionCookie = sessionCookie.replaceFirst("(\\W)(P|p)ath=", "$1\\$Path="); + // Perform some request to server2 using the session cookie from the previous request // This should migrate the session from server1 to server2, and leave server1's // session in a very stale state, while server2 has a very fresh session. @@ -111,14 +118,15 @@ public abstract class AbstractLastAccessTimeTest sessionCookie = setCookie.replaceFirst("(\\W)(P|p)ath=", "$1\\$Path="); Thread.sleep(requestInterval); + assertSessionCounts(1,1,1, m2); } - // At this point, session1 should be eligible for expiration. // Let's wait for the scavenger to run, waiting 2.5 times the scavenger period Thread.sleep(scavengePeriod * 2500L); //check that the session was not scavenged over on server1 by ensuring that the SessionListener destroy method wasn't called assertFalse(listener1.destroyed); + assertAfterScavenge(m1); } finally { @@ -135,6 +143,23 @@ public abstract class AbstractLastAccessTimeTest server1.stop(); } } + + public void assertAfterSessionCreated (AbstractSessionManager m) + { + assertSessionCounts(1, 1, 1, m); + } + + public void assertAfterScavenge (AbstractSessionManager manager) + { + assertSessionCounts(1,1,1, manager); + } + + public void assertSessionCounts (int current, int max, int total, AbstractSessionManager manager) + { + assertEquals(current, manager.getSessions()); + assertEquals(max, manager.getSessionsMax()); + assertEquals(total, manager.getSessionsTotal()); + } public static class TestSessionListener implements HttpSessionListener { diff --git a/tests/test-sessions/test-sessions-common/src/main/java/org/eclipse/jetty/server/session/AbstractRemoveSessionTest.java b/tests/test-sessions/test-sessions-common/src/main/java/org/eclipse/jetty/server/session/AbstractRemoveSessionTest.java index c344d72530..bb126f68db 100644 --- a/tests/test-sessions/test-sessions-common/src/main/java/org/eclipse/jetty/server/session/AbstractRemoveSessionTest.java +++ b/tests/test-sessions/test-sessions-common/src/main/java/org/eclipse/jetty/server/session/AbstractRemoveSessionTest.java @@ -39,6 +39,12 @@ import org.eclipse.jetty.client.api.Request; import org.eclipse.jetty.servlet.ServletContextHandler; import org.junit.Test; +/** + * AbstractRemoveSessionTest + * + * Test that invalidating a session does not return the session on the next request. + * + */ public abstract class AbstractRemoveSessionTest { public abstract AbstractTestServer createServer(int port, int max, int scavenge); @@ -55,6 +61,7 @@ public abstract class AbstractRemoveSessionTest context.addServlet(TestServlet.class, servletMapping); TestEventListener testListener = new TestEventListener(); context.getSessionHandler().addEventListener(testListener); + AbstractSessionManager m = (AbstractSessionManager)context.getSessionHandler().getSessionManager(); try { server.start(); @@ -72,7 +79,10 @@ public abstract class AbstractRemoveSessionTest sessionCookie = sessionCookie.replaceFirst("(\\W)(P|p)ath=", "$1\\$Path="); //ensure sessionCreated listener is called assertTrue (testListener.isCreated()); - + assertEquals(1, m.getSessions()); + assertEquals(1, m.getSessionsMax()); + assertEquals(1, m.getSessionsTotal()); + //now delete the session Request request = client.newRequest("http://localhost:" + port + contextPath + servletMapping + "?action=delete"); request.header("Cookie", sessionCookie); @@ -80,13 +90,18 @@ public abstract class AbstractRemoveSessionTest assertEquals(HttpServletResponse.SC_OK,response.getStatus()); //ensure sessionDestroyed listener is called assertTrue(testListener.isDestroyed()); - + assertEquals(0, m.getSessions()); + assertEquals(1, m.getSessionsMax()); + assertEquals(1, m.getSessionsTotal()); // The session is not there anymore, even if we present an old cookie request = client.newRequest("http://localhost:" + port + contextPath + servletMapping + "?action=check"); request.header("Cookie", sessionCookie); response = request.send(); assertEquals(HttpServletResponse.SC_OK,response.getStatus()); + assertEquals(0, m.getSessions()); + assertEquals(1, m.getSessionsMax()); + assertEquals(1, m.getSessionsTotal()); } finally { diff --git a/tests/test-sessions/test-sessions-common/src/main/java/org/eclipse/jetty/server/session/AbstractSameNodeLoadTest.java b/tests/test-sessions/test-sessions-common/src/main/java/org/eclipse/jetty/server/session/AbstractSameNodeLoadTest.java index 999858fb5b..28e6cbdc1e 100644 --- a/tests/test-sessions/test-sessions-common/src/main/java/org/eclipse/jetty/server/session/AbstractSameNodeLoadTest.java +++ b/tests/test-sessions/test-sessions-common/src/main/java/org/eclipse/jetty/server/session/AbstractSameNodeLoadTest.java @@ -19,8 +19,8 @@ package org.eclipse.jetty.server.session; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; import java.io.IOException; import java.io.PrintWriter; diff --git a/tests/test-sessions/test-sessions-common/src/main/java/org/eclipse/jetty/server/session/AbstractServerCrossContextSessionTest.java b/tests/test-sessions/test-sessions-common/src/main/java/org/eclipse/jetty/server/session/AbstractServerCrossContextSessionTest.java index 30a080a732..ed7737f359 100644 --- a/tests/test-sessions/test-sessions-common/src/main/java/org/eclipse/jetty/server/session/AbstractServerCrossContextSessionTest.java +++ b/tests/test-sessions/test-sessions-common/src/main/java/org/eclipse/jetty/server/session/AbstractServerCrossContextSessionTest.java @@ -22,7 +22,6 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import java.io.IOException; -import java.util.Collections; import javax.servlet.RequestDispatcher; import javax.servlet.ServletContext; @@ -87,10 +86,9 @@ public abstract class AbstractServerCrossContextSessionTest { HttpSession session = request.getSession(false); if (session == null) session = request.getSession(true); - // Add something to the session session.setAttribute("A", "A"); - System.out.println("A: session.getAttributeNames() = " + Collections.list(session.getAttributeNames())); + // Perform cross context dispatch to another context // Over there we will check that the session attribute added above is not visible @@ -101,7 +99,6 @@ public abstract class AbstractServerCrossContextSessionTest // Check that we don't see things put in session by contextB Object objectB = session.getAttribute("B"); assertTrue(objectB == null); - System.out.println("A: session.getAttributeNames() = " + Collections.list(session.getAttributeNames())); } } @@ -119,7 +116,6 @@ public abstract class AbstractServerCrossContextSessionTest // Add something, so in contextA we can check if it is visible (it must not). session.setAttribute("B", "B"); - System.out.println("B: session.getAttributeNames() = " + Collections.list(session.getAttributeNames())); } } } diff --git a/tests/test-sessions/test-sessions-common/src/main/java/org/eclipse/jetty/server/session/AbstractSessionCookieTest.java b/tests/test-sessions/test-sessions-common/src/main/java/org/eclipse/jetty/server/session/AbstractSessionCookieTest.java index b79a0a33ac..a867711ce4 100644 --- a/tests/test-sessions/test-sessions-common/src/main/java/org/eclipse/jetty/server/session/AbstractSessionCookieTest.java +++ b/tests/test-sessions/test-sessions-common/src/main/java/org/eclipse/jetty/server/session/AbstractSessionCookieTest.java @@ -29,8 +29,6 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; -import junit.framework.Assert; - import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.client.api.ContentResponse; import org.eclipse.jetty.client.api.Request; @@ -38,6 +36,8 @@ import org.eclipse.jetty.servlet.ServletContextHandler; import org.junit.Ignore; import org.junit.Test; +import junit.framework.Assert; + /** * AbstractSessionCookieTest */ diff --git a/tests/test-sessions/test-sessions-common/src/main/java/org/eclipse/jetty/server/session/AbstractSessionExpiryTest.java b/tests/test-sessions/test-sessions-common/src/main/java/org/eclipse/jetty/server/session/AbstractSessionExpiryTest.java index 5c11f8b35c..0c221a2860 100644 --- a/tests/test-sessions/test-sessions-common/src/main/java/org/eclipse/jetty/server/session/AbstractSessionExpiryTest.java +++ b/tests/test-sessions/test-sessions-common/src/main/java/org/eclipse/jetty/server/session/AbstractSessionExpiryTest.java @@ -40,6 +40,11 @@ import org.eclipse.jetty.servlet.ServletContextHandler; import org.eclipse.jetty.servlet.ServletHolder; import org.junit.Test; +/** + * AbstractSessionExpiryTest + * + * + */ public abstract class AbstractSessionExpiryTest { public abstract AbstractTestServer createServer(int port, int max, int scavenge); @@ -104,6 +109,7 @@ public abstract class AbstractSessionExpiryTest //now stop the server server1.stop(); + //start the server again, before the session times out server1.start(); @@ -161,12 +167,12 @@ public abstract class AbstractSessionExpiryTest sessionCookie = sessionCookie.replaceFirst("(\\W)(P|p)ath=", "$1\\$Path="); String sessionId = AbstractTestServer.extractSessionId(sessionCookie); - + verifySessionCreated(listener,sessionId); //now stop the server server1.stop(); - + //and wait until the expiry time has passed pause(inactivePeriod); diff --git a/tests/test-webapps/test-jetty-webapp/src/main/config/demo-base/webapps/test.xml b/tests/test-webapps/test-jetty-webapp/src/main/config/demo-base/webapps/test.xml index 0037cb4906..cae730e89d 100644 --- a/tests/test-webapps/test-jetty-webapp/src/main/config/demo-base/webapps/test.xml +++ b/tests/test-webapps/test-jetty-webapp/src/main/config/demo-base/webapps/test.xml @@ -11,7 +11,7 @@ directory, additional configuration may be specified and hot deployments detected. ===================================================================== --> -<Configure class="org.eclipse.jetty.webapp.WebAppContext"> +<Configure id="testWebapp" class="org.eclipse.jetty.webapp.WebAppContext"> <!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> <!-- Required minimal context configuration : --> @@ -41,6 +41,13 @@ detected. </New> </Set> + <!-- Set Caching Classloader that improves performance on resource searching webapps --> + <Set name="classLoader"> + <New class="org.eclipse.jetty.webapp.CachingWebAppClassLoader"> + <Arg><Ref refid="testWebapp"/></Arg> + </New> + </Set> + <!-- Enable symlinks <Call name="addAliasCheck"> <Arg><New class="org.eclipse.jetty.server.handler.AllowSymLinkAliasChecker"/></Arg> |