diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/NetworkTrafficListenerTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/NetworkTrafficListenerTest.java index cdae1be57fe..d151af1bcff 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/NetworkTrafficListenerTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/NetworkTrafficListenerTest.java @@ -22,8 +22,8 @@ import java.io.IOException; import java.io.InputStream; import java.net.Socket; import java.nio.ByteBuffer; -import java.nio.channels.SelectableChannel; import java.nio.channels.SelectionKey; +import java.nio.channels.SocketChannel; import java.nio.charset.StandardCharsets; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -45,7 +45,6 @@ import org.eclipse.jetty.io.EndPoint; import org.eclipse.jetty.io.ManagedSelector; import org.eclipse.jetty.io.NetworkTrafficListener; import org.eclipse.jetty.io.NetworkTrafficSocketChannelEndPoint; -import org.eclipse.jetty.io.SelectorManager; import org.eclipse.jetty.server.Handler; import org.eclipse.jetty.server.HttpConfiguration; import org.eclipse.jetty.server.NetworkTrafficServerConnector; @@ -508,16 +507,9 @@ public class NetworkTrafficListenerTest super(new HttpClientTransportOverHTTP(new ClientConnector() { @Override - protected SelectorManager newSelectorManager() + protected EndPoint newEndPoint(SocketChannel channel, ManagedSelector selector, SelectionKey selectionKey) { - return new ClientSelectorManager(getExecutor(), getScheduler(), getSelectors()) - { - @Override - protected EndPoint newEndPoint(SelectableChannel channel, ManagedSelector selector, SelectionKey selectionKey) - { - return new NetworkTrafficSocketChannelEndPoint(channel, selector, selectionKey, getScheduler(), getIdleTimeout().toMillis(), listener.get()); - } - }; + return new NetworkTrafficSocketChannelEndPoint(channel, selector, selectionKey, getScheduler(), getIdleTimeout().toMillis(), listener.get()); } })); this.listener = listener; diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/ssl/SslBytesServerTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/ssl/SslBytesServerTest.java index b304a422741..87e310c5462 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/ssl/SslBytesServerTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/ssl/SslBytesServerTest.java @@ -50,10 +50,10 @@ import jakarta.servlet.http.HttpServletResponse; import org.eclipse.jetty.client.ssl.SslBytesTest.TLSRecord.Type; import org.eclipse.jetty.http.HttpCompliance; import org.eclipse.jetty.http.HttpParser; -import org.eclipse.jetty.io.ChannelEndPoint; import org.eclipse.jetty.io.Connection; import org.eclipse.jetty.io.EndPoint; import org.eclipse.jetty.io.ManagedSelector; +import org.eclipse.jetty.io.SocketChannelEndPoint; import org.eclipse.jetty.io.ssl.SslConnection; import org.eclipse.jetty.server.Connector; import org.eclipse.jetty.server.HttpConnection; @@ -189,11 +189,11 @@ public class SslBytesServerTest extends SslBytesTest ServerConnector connector = new ServerConnector(server, null, null, null, 1, 1, sslFactory, httpFactory) { @Override - protected ChannelEndPoint newEndPoint(SocketChannel channel, ManagedSelector selectSet, SelectionKey key) throws IOException + protected SocketChannelEndPoint newEndPoint(SocketChannel channel, ManagedSelector selectSet, SelectionKey key) throws IOException { - ChannelEndPoint endp = super.newEndPoint(channel, selectSet, key); - serverEndPoint.set(endp); - return endp; + SocketChannelEndPoint endPoint = super.newEndPoint(channel, selectSet, key); + serverEndPoint.set(endPoint); + return endPoint; } }; connector.setIdleTimeout(idleTimeout); diff --git a/jetty-client/src/test/resources/jetty-logging.properties b/jetty-client/src/test/resources/jetty-logging.properties index e6bbf9a6ca0..2bbfa1a3add 100644 --- a/jetty-client/src/test/resources/jetty-logging.properties +++ b/jetty-client/src/test/resources/jetty-logging.properties @@ -1,6 +1,6 @@ # Jetty Logging using jetty-slf4j-impl #org.eclipse.jetty.LEVEL=DEBUG #org.eclipse.jetty.client.LEVEL=DEBUG -#org.eclipse.jetty.io.ChannelEndPoint.LEVEL=DEBUG +#org.eclipse.jetty.io.SocketChannelEndPoint.LEVEL=DEBUG #org.eclipse.jetty.io.ssl.LEVEL=DEBUG #org.eclipse.jetty.http.LEVEL=DEBUG diff --git a/jetty-http2/http2-server/src/test/java/org/eclipse/jetty/http2/server/HTTP2ServerTest.java b/jetty-http2/http2-server/src/test/java/org/eclipse/jetty/http2/server/HTTP2ServerTest.java index 3579cf670b1..7340254f09f 100644 --- a/jetty-http2/http2-server/src/test/java/org/eclipse/jetty/http2/server/HTTP2ServerTest.java +++ b/jetty-http2/http2-server/src/test/java/org/eclipse/jetty/http2/server/HTTP2ServerTest.java @@ -35,7 +35,6 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; import java.util.function.UnaryOperator; -import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServlet; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; @@ -57,7 +56,6 @@ import org.eclipse.jetty.http2.frames.SettingsFrame; import org.eclipse.jetty.http2.generator.Generator; import org.eclipse.jetty.http2.parser.Parser; import org.eclipse.jetty.io.ByteBufferPool; -import org.eclipse.jetty.io.ChannelEndPoint; import org.eclipse.jetty.io.ManagedSelector; import org.eclipse.jetty.io.SocketChannelEndPoint; import org.eclipse.jetty.logging.StacklessLogging; @@ -118,7 +116,7 @@ public class HTTP2ServerTest extends AbstractServerTest startServer(new HttpServlet() { @Override - protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException + protected void service(HttpServletRequest req, HttpServletResponse resp) { latch.countDown(); } @@ -175,7 +173,7 @@ public class HTTP2ServerTest extends AbstractServerTest startServer(new HttpServlet() { @Override - protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException + protected void service(HttpServletRequest req, HttpServletResponse resp) throws IOException { latch.countDown(); resp.getOutputStream().write(content); @@ -321,7 +319,7 @@ public class HTTP2ServerTest extends AbstractServerTest startServer(new HttpServlet() { @Override - protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException + protected void service(HttpServletRequest request, HttpServletResponse response) throws IOException { try { @@ -340,7 +338,7 @@ public class HTTP2ServerTest extends AbstractServerTest ServerConnector connector2 = new ServerConnector(server, new HTTP2ServerConnectionFactory(new HttpConfiguration())) { @Override - protected ChannelEndPoint newEndPoint(SocketChannel channel, ManagedSelector selectSet, SelectionKey key) throws IOException + protected SocketChannelEndPoint newEndPoint(SocketChannel channel, ManagedSelector selectSet, SelectionKey key) { return new SocketChannelEndPoint(channel, selectSet, key, getScheduler()) { 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 7cd08ad4542..b4483f703d2 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 @@ -469,11 +469,11 @@ public abstract class AbstractEndPoint extends IdleTimeout implements EndPoint name = c.getSimpleName(); } - return String.format("%s@%h{%s<->%s,%s,fill=%s,flush=%s,to=%d/%d}", + return String.format("%s@%h{l=%s,r=%s,%s,fill=%s,flush=%s,to=%d/%d}", name, this, - getRemoteAddress(), getLocalAddress(), + getRemoteAddress(), _state.get(), _fillInterest.toStateString(), _writeFlusher.toStateString(), diff --git a/jetty-io/src/main/java/org/eclipse/jetty/io/ChannelEndPoint.java b/jetty-io/src/main/java/org/eclipse/jetty/io/ChannelEndPoint.java deleted file mode 100644 index 2c89a173753..00000000000 --- a/jetty-io/src/main/java/org/eclipse/jetty/io/ChannelEndPoint.java +++ /dev/null @@ -1,429 +0,0 @@ -// -// ======================================================================== -// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others. -// -// This program and the accompanying materials are made available under -// the terms of the Eclipse Public License 2.0 which is available at -// https://www.eclipse.org/legal/epl-2.0 -// -// This Source Code may also be made available under the following -// Secondary Licenses when the conditions for such availability set -// forth in the Eclipse Public License, v. 2.0 are satisfied: -// the Apache License v2.0 which is available at -// https://www.apache.org/licenses/LICENSE-2.0 -// -// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 -// ======================================================================== -// - -package org.eclipse.jetty.io; - -import java.io.Closeable; -import java.io.IOException; -import java.nio.ByteBuffer; -import java.nio.channels.ByteChannel; -import java.nio.channels.CancelledKeyException; -import java.nio.channels.GatheringByteChannel; -import java.nio.channels.SelectionKey; -import java.nio.channels.Selector; - -import org.eclipse.jetty.util.BufferUtil; -import org.eclipse.jetty.util.thread.Invocable; -import org.eclipse.jetty.util.thread.Scheduler; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Channel End Point. - *

Holds the channel and socket for an NIO endpoint. - */ -public abstract class ChannelEndPoint extends AbstractEndPoint implements ManagedSelector.Selectable -{ - private static final Logger LOG = LoggerFactory.getLogger(ChannelEndPoint.class); - - private final ByteChannel _channel; - private final GatheringByteChannel _gather; - protected final ManagedSelector _selector; - protected final SelectionKey _key; - private boolean _updatePending; - - /** - * The current value for {@link SelectionKey#interestOps()}. - */ - protected int _currentInterestOps; - - /** - * The desired value for {@link SelectionKey#interestOps()}. - */ - protected int _desiredInterestOps; - - private abstract class RunnableTask implements Runnable, Invocable - { - final String _operation; - - protected RunnableTask(String op) - { - _operation = op; - } - - @Override - public String toString() - { - return String.format("CEP:%s:%s:%s", ChannelEndPoint.this, _operation, getInvocationType()); - } - } - - private abstract class RunnableCloseable extends RunnableTask implements Closeable - { - protected RunnableCloseable(String op) - { - super(op); - } - - @Override - public void close() - { - try - { - ChannelEndPoint.this.close(); - } - catch (Throwable x) - { - LOG.warn("Unable to close ChannelEndPoint", x); - } - } - } - - private final ManagedSelector.SelectorUpdate _updateKeyAction = new ManagedSelector.SelectorUpdate() - { - @Override - public void update(Selector selector) - { - updateKey(); - } - }; - - private final Runnable _runFillable = new RunnableCloseable("runFillable") - { - @Override - public InvocationType getInvocationType() - { - return getFillInterest().getCallbackInvocationType(); - } - - @Override - public void run() - { - getFillInterest().fillable(); - } - }; - - private final Runnable _runCompleteWrite = new RunnableCloseable("runCompleteWrite") - { - @Override - public InvocationType getInvocationType() - { - return getWriteFlusher().getCallbackInvocationType(); - } - - @Override - public void run() - { - getWriteFlusher().completeWrite(); - } - - @Override - public String toString() - { - return String.format("CEP:%s:%s:%s->%s", ChannelEndPoint.this, _operation, getInvocationType(), getWriteFlusher()); - } - }; - - private final Runnable _runCompleteWriteFillable = new RunnableCloseable("runCompleteWriteFillable") - { - @Override - public InvocationType getInvocationType() - { - InvocationType fillT = getFillInterest().getCallbackInvocationType(); - InvocationType flushT = getWriteFlusher().getCallbackInvocationType(); - if (fillT == flushT) - return fillT; - - if (fillT == InvocationType.EITHER && flushT == InvocationType.NON_BLOCKING) - return InvocationType.EITHER; - - if (fillT == InvocationType.NON_BLOCKING && flushT == InvocationType.EITHER) - return InvocationType.EITHER; - - return InvocationType.BLOCKING; - } - - @Override - public void run() - { - getWriteFlusher().completeWrite(); - getFillInterest().fillable(); - } - }; - - public ChannelEndPoint(ByteChannel channel, ManagedSelector selector, SelectionKey key, Scheduler scheduler) - { - super(scheduler); - _channel = channel; - _selector = selector; - _key = key; - _gather = (channel instanceof GatheringByteChannel) ? (GatheringByteChannel)channel : null; - } - - @Override - public boolean isOpen() - { - return _channel.isOpen(); - } - - @Override - public void doClose() - { - if (LOG.isDebugEnabled()) - LOG.debug("doClose {}", this); - try - { - _channel.close(); - } - catch (IOException e) - { - LOG.debug("Unable to close channel", e); - } - finally - { - super.doClose(); - } - } - - @Override - public void onClose(Throwable cause) - { - try - { - super.onClose(cause); - } - finally - { - if (_selector != null) - _selector.destroyEndPoint(this, cause); - } - } - - @Override - public int fill(ByteBuffer buffer) throws IOException - { - if (isInputShutdown()) - return -1; - - int pos = BufferUtil.flipToFill(buffer); - int filled; - try - { - filled = _channel.read(buffer); - if (filled > 0) - notIdle(); - else if (filled == -1) - shutdownInput(); - } - catch (IOException e) - { - LOG.debug("Unable to shutdown output", e); - shutdownInput(); - filled = -1; - } - finally - { - BufferUtil.flipToFlush(buffer, pos); - } - if (LOG.isDebugEnabled()) - LOG.debug("filled {} {}", filled, BufferUtil.toDetailString(buffer)); - return filled; - } - - @Override - public boolean flush(ByteBuffer... buffers) throws IOException - { - long flushed = 0; - try - { - if (buffers.length == 1) - flushed = _channel.write(buffers[0]); - else if (_gather != null && buffers.length > 1) - flushed = _gather.write(buffers, 0, buffers.length); - else - { - for (ByteBuffer b : buffers) - { - if (b.hasRemaining()) - { - int l = _channel.write(b); - if (l > 0) - flushed += l; - if (b.hasRemaining()) - break; - } - } - } - if (LOG.isDebugEnabled()) - LOG.debug("flushed {} {}", flushed, this); - } - catch (IOException e) - { - throw new EofException(e); - } - - if (flushed > 0) - notIdle(); - - for (ByteBuffer b : buffers) - { - if (!BufferUtil.isEmpty(b)) - return false; - } - - return true; - } - - public ByteChannel getChannel() - { - return _channel; - } - - @Override - public Object getTransport() - { - return _channel; - } - - @Override - protected void needsFillInterest() - { - changeInterests(SelectionKey.OP_READ); - } - - @Override - protected void onIncompleteFlush() - { - changeInterests(SelectionKey.OP_WRITE); - } - - @Override - public Runnable onSelected() - { - /** - * This method may run concurrently with {@link #changeInterests(int)}. - */ - - int readyOps = _key.readyOps(); - int oldInterestOps; - int newInterestOps; - synchronized (this) - { - _updatePending = true; - // Remove the readyOps, that here can only be OP_READ or OP_WRITE (or both). - oldInterestOps = _desiredInterestOps; - newInterestOps = oldInterestOps & ~readyOps; - _desiredInterestOps = newInterestOps; - } - - boolean fillable = (readyOps & SelectionKey.OP_READ) != 0; - boolean flushable = (readyOps & SelectionKey.OP_WRITE) != 0; - - if (LOG.isDebugEnabled()) - LOG.debug("onSelected {}->{} r={} w={} for {}", oldInterestOps, newInterestOps, fillable, flushable, this); - - // return task to complete the job - Runnable task = fillable - ? (flushable - ? _runCompleteWriteFillable - : _runFillable) - : (flushable - ? _runCompleteWrite - : null); - - if (LOG.isDebugEnabled()) - LOG.debug("task {}", task); - return task; - } - - @Override - public void updateKey() - { - /** - * This method may run concurrently with {@link #changeInterests(int)}. - */ - - try - { - int oldInterestOps; - int newInterestOps; - synchronized (this) - { - _updatePending = false; - oldInterestOps = _currentInterestOps; - newInterestOps = _desiredInterestOps; - if (oldInterestOps != newInterestOps) - { - _currentInterestOps = newInterestOps; - _key.interestOps(newInterestOps); - } - } - - if (LOG.isDebugEnabled()) - LOG.debug("Key interests updated {} -> {} on {}", oldInterestOps, newInterestOps, this); - } - catch (CancelledKeyException x) - { - LOG.debug("Ignoring key update for concurrently closed channel {}", this); - close(); - } - catch (Throwable x) - { - LOG.warn("Ignoring key update for " + this, x); - close(); - } - } - - private void changeInterests(int operation) - { - /** - * This method may run concurrently with - * {@link #updateKey()} and {@link #onSelected()}. - */ - - int oldInterestOps; - int newInterestOps; - boolean pending; - synchronized (this) - { - pending = _updatePending; - oldInterestOps = _desiredInterestOps; - newInterestOps = oldInterestOps | operation; - if (newInterestOps != oldInterestOps) - _desiredInterestOps = newInterestOps; - } - - if (LOG.isDebugEnabled()) - LOG.debug("changeInterests p={} {}->{} for {}", pending, oldInterestOps, newInterestOps, this); - - if (!pending && _selector != null) - _selector.submit(_updateKeyAction); - } - - @Override - public String toEndPointString() - { - // We do a best effort to print the right toString() and that's it. - return String.format("%s{io=%d/%d,kio=%d,kro=%d}", - super.toEndPointString(), - _currentInterestOps, - _desiredInterestOps, - ManagedSelector.safeInterestOps(_key), - ManagedSelector.safeReadyOps(_key)); - } -} diff --git a/jetty-io/src/main/java/org/eclipse/jetty/io/ClientConnector.java b/jetty-io/src/main/java/org/eclipse/jetty/io/ClientConnector.java index a997de63001..e5d8730b10c 100644 --- a/jetty-io/src/main/java/org/eclipse/jetty/io/ClientConnector.java +++ b/jetty-io/src/main/java/org/eclipse/jetty/io/ClientConnector.java @@ -18,7 +18,6 @@ package org.eclipse.jetty.io; -import java.io.Closeable; import java.io.IOException; import java.net.SocketAddress; import java.net.SocketException; @@ -253,7 +252,7 @@ public class ClientConnector extends ContainerLifeCycle // exception is being thrown, so we attempt to provide a better error message. if (x.getClass() == SocketException.class) x = new SocketException("Could not connect to " + address).initCause(x); - safeClose(channel); + IO.close(channel); connectFailed(x, context); } } @@ -273,23 +272,23 @@ public class ClientConnector extends ContainerLifeCycle { if (LOG.isDebugEnabled()) LOG.debug("Could not accept {}", channel); - safeClose(channel); + IO.close(channel); Promise promise = (Promise)context.get(CONNECTION_PROMISE_CONTEXT_KEY); if (promise != null) promise.failed(failure); } } - protected void safeClose(Closeable closeable) - { - IO.close(closeable); - } - protected void configure(SocketChannel channel) throws IOException { channel.socket().setTcpNoDelay(true); } + protected EndPoint newEndPoint(SocketChannel channel, ManagedSelector selector, SelectionKey selectionKey) + { + return new SocketChannelEndPoint(channel, selector, selectionKey, getScheduler()); + } + protected void connectFailed(Throwable failure, Map context) { if (LOG.isDebugEnabled()) @@ -309,7 +308,7 @@ public class ClientConnector extends ContainerLifeCycle @Override protected EndPoint newEndPoint(SelectableChannel channel, ManagedSelector selector, SelectionKey selectionKey) { - SocketChannelEndPoint endPoint = new SocketChannelEndPoint(channel, selector, selectionKey, getScheduler()); + EndPoint endPoint = ClientConnector.this.newEndPoint((SocketChannel)channel, selector, selectionKey); endPoint.setIdleTimeout(getIdleTimeout().toMillis()); return endPoint; } diff --git a/jetty-io/src/main/java/org/eclipse/jetty/io/ManagedSelector.java b/jetty-io/src/main/java/org/eclipse/jetty/io/ManagedSelector.java index fbdd71ad77b..47651f816a0 100644 --- a/jetty-io/src/main/java/org/eclipse/jetty/io/ManagedSelector.java +++ b/jetty-io/src/main/java/org/eclipse/jetty/io/ManagedSelector.java @@ -80,7 +80,7 @@ public class ManagedSelector extends ContainerLifeCycle implements Dumpable } private final AtomicBoolean _started = new AtomicBoolean(false); - private boolean _selecting = false; + private boolean _selecting; private final SelectorManager _selectorManager; private final int _id; private final ExecutionStrategy _strategy; @@ -123,22 +123,6 @@ public class ManagedSelector extends ContainerLifeCycle implements Dumpable start._started.await(); } - protected void onSelectFailed(Throwable cause) - { - // override to change behavior - } - - public int size() - { - Selector s = _selector; - if (s == null) - return 0; - Set keys = s.keys(); - if (keys == null) - return 0; - return keys.size(); - } - @Override protected void doStop() throws Exception { @@ -160,22 +144,119 @@ public class ManagedSelector extends ContainerLifeCycle implements Dumpable super.doStop(); } + protected int nioSelect(Selector selector, boolean now) throws IOException + { + return now ? selector.selectNow() : selector.select(); + } + + protected int select(Selector selector) throws IOException + { + try + { + int selected = nioSelect(selector, false); + if (selected == 0) + { + if (LOG.isDebugEnabled()) + LOG.debug("Selector {} woken with none selected", selector); + + if (Thread.interrupted() && !isRunning()) + throw new ClosedSelectorException(); + + if (FORCE_SELECT_NOW) + selected = nioSelect(selector, true); + } + return selected; + } + catch (ClosedSelectorException x) + { + throw x; + } + catch (Throwable x) + { + handleSelectFailure(selector, x); + return 0; + } + } + + protected void handleSelectFailure(Selector selector, Throwable failure) throws IOException + { + LOG.info("Caught select() failure, trying to recover: {}", failure.toString()); + if (LOG.isDebugEnabled()) + LOG.debug("", failure); + + Selector newSelector = _selectorManager.newSelector(); + for (SelectionKey oldKey : selector.keys()) + { + SelectableChannel channel = oldKey.channel(); + int interestOps = safeInterestOps(oldKey); + if (interestOps >= 0) + { + try + { + Object attachment = oldKey.attachment(); + SelectionKey newKey = channel.register(newSelector, interestOps, attachment); + if (attachment instanceof Selectable) + ((Selectable)attachment).replaceKey(newKey); + oldKey.cancel(); + if (LOG.isDebugEnabled()) + LOG.debug("Transferred {} iOps={} att={}", channel, interestOps, attachment); + } + catch (Throwable t) + { + if (LOG.isDebugEnabled()) + LOG.debug("Could not transfer {}", channel, t); + IO.close(channel); + } + } + else + { + if (LOG.isDebugEnabled()) + LOG.debug("Invalid interestOps for {}", channel); + IO.close(channel); + } + } + + IO.close(selector); + _selector = newSelector; + } + + protected void onSelectFailed(Throwable cause) + { + // override to change behavior + } + + public int size() + { + Selector s = _selector; + if (s == null) + return 0; + Set keys = s.keys(); + if (keys == null) + return 0; + return keys.size(); + } + /** * Submit an {@link SelectorUpdate} to be acted on between calls to {@link Selector#select()} * * @param update The selector update to apply at next wakeup */ public void submit(SelectorUpdate update) + { + submit(update, false); + } + + private void submit(SelectorUpdate update, boolean lazy) { if (LOG.isDebugEnabled()) - LOG.debug("Queued change {} on {}", update, this); + LOG.debug("Queued change lazy={} {} on {}", lazy, update, this); Selector selector = null; synchronized (ManagedSelector.this) { _updates.offer(update); - if (_selecting) + if (_selecting && !lazy) { selector = _selector; // To avoid the extra select wakeup. @@ -223,7 +304,7 @@ public class ManagedSelector extends ContainerLifeCycle implements Dumpable } } - private void processConnect(SelectionKey key, final Connect connect) + private void processConnect(SelectionKey key, Connect connect) { SelectableChannel channel = key.channel(); try @@ -271,7 +352,18 @@ public class ManagedSelector extends ContainerLifeCycle implements Dumpable Object context = selectionKey.attachment(); Connection connection = _selectorManager.newConnection(channel, endPoint, context); endPoint.setConnection(connection); - selectionKey.attach(endPoint); + submit(selector -> + { + SelectionKey key = selectionKey; + if (key.selector() != selector) + { + key = channel.keyFor(selector); + if (key != null && endPoint instanceof Selectable) + ((Selectable)endPoint).replaceKey(key); + } + if (key != null) + key.attach(endPoint); + }, true); endPoint.onOpen(); endPointOpened(endPoint); _selectorManager.connectionOpened(connection, context); @@ -279,7 +371,7 @@ public class ManagedSelector extends ContainerLifeCycle implements Dumpable LOG.debug("Created {}", endPoint); } - public void destroyEndPoint(final EndPoint endPoint, Throwable cause) + void destroyEndPoint(EndPoint endPoint, Throwable cause) { // Waking up the selector is necessary to clean the // cancelled-key set and tell the TCP stack that the @@ -330,8 +422,8 @@ public class ManagedSelector extends ContainerLifeCycle implements Dumpable Selector selector = _selector; if (selector != null && selector.isOpen()) { - final DumpKeys dump = new DumpKeys(); - final String updatesAt = DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(ZonedDateTime.now()); + DumpKeys dump = new DumpKeys(); + String updatesAt = DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(ZonedDateTime.now()); synchronized (ManagedSelector.this) { updates = new ArrayList<>(_updates); @@ -387,6 +479,14 @@ public class ManagedSelector extends ContainerLifeCycle implements Dumpable * {@link ManagedSelector} for this endpoint have been processed. */ void updateKey(); + + /** + * Callback method invoked when the SelectionKey is replaced + * because the channel has been moved to a new selector. + * + * @param newKey the new SelectionKey + */ + void replaceKey(SelectionKey newKey); } private class SelectorProducer implements ExecutionStrategy.Producer @@ -434,9 +534,9 @@ public class ManagedSelector extends ContainerLifeCycle implements Dumpable LOG.debug("update {}", update); update.update(_selector); } - catch (Throwable th) + catch (Throwable x) { - LOG.warn("Cannot update selector {}", _selector, th); + LOG.warn("Cannot update selector {}", ManagedSelector.this, x); } } _updateable.clear(); @@ -466,39 +566,33 @@ public class ManagedSelector extends ContainerLifeCycle implements Dumpable try { Selector selector = _selector; - if (selector != null && selector.isOpen()) + if (selector != null) { if (LOG.isDebugEnabled()) LOG.debug("Selector {} waiting with {} keys", selector, selector.keys().size()); - int selected = selector.select(); - if (selected == 0) + int selected = ManagedSelector.this.select(selector); + // The selector may have been recreated. + selector = _selector; + if (selector != null) { if (LOG.isDebugEnabled()) - LOG.debug("Selector {} woken with none selected", selector); + LOG.debug("Selector {} woken up from select, {}/{}/{} selected", selector, selected, selector.selectedKeys().size(), selector.keys().size()); - if (Thread.interrupted() && !isRunning()) - throw new ClosedSelectorException(); + int updates; + synchronized (ManagedSelector.this) + { + // finished selecting + _selecting = false; + updates = _updates.size(); + } - if (FORCE_SELECT_NOW) - selected = selector.selectNow(); + _keys = selector.selectedKeys(); + _cursor = _keys.isEmpty() ? Collections.emptyIterator() : _keys.iterator(); + if (LOG.isDebugEnabled()) + LOG.debug("Selector {} processing {} keys, {} updates", selector, _keys.size(), updates); + + return true; } - if (LOG.isDebugEnabled()) - LOG.debug("Selector {} woken up from select, {}/{}/{} selected", selector, selected, selector.selectedKeys().size(), selector.keys().size()); - - int updates; - synchronized (ManagedSelector.this) - { - // finished selecting - _selecting = false; - updates = _updates.size(); - } - - _keys = selector.selectedKeys(); - _cursor = _keys.isEmpty() ? Collections.emptyIterator() : _keys.iterator(); - if (LOG.isDebugEnabled()) - LOG.debug("Selector {} processing {} keys, {} updates", selector, _keys.size(), updates); - - return true; } } catch (Throwable x) @@ -514,7 +608,8 @@ public class ManagedSelector extends ContainerLifeCycle implements Dumpable else { LOG.warn(x.toString()); - LOG.debug("select() failure", x); + if (LOG.isDebugEnabled()) + LOG.debug("select() failure", x); } } return false; @@ -525,9 +620,10 @@ public class ManagedSelector extends ContainerLifeCycle implements Dumpable while (_cursor.hasNext()) { SelectionKey key = _cursor.next(); + Object attachment = key.attachment(); + SelectableChannel channel = key.channel(); if (key.isValid()) { - Object attachment = key.attachment(); if (LOG.isDebugEnabled()) LOG.debug("selected {} {} {} ", safeReadyOps(key), key, attachment); try @@ -550,24 +646,21 @@ public class ManagedSelector extends ContainerLifeCycle implements Dumpable } catch (CancelledKeyException x) { - LOG.debug("Ignoring cancelled key for channel {}", key.channel()); - if (attachment instanceof EndPoint) - IO.close((EndPoint)attachment); + if (LOG.isDebugEnabled()) + LOG.debug("Ignoring cancelled key for channel {}", channel); + IO.close(attachment instanceof EndPoint ? (EndPoint)attachment : channel); } catch (Throwable x) { - LOG.warn("Could not process key for channel " + key.channel(), x); - if (attachment instanceof EndPoint) - IO.close((EndPoint)attachment); + LOG.warn("Could not process key for channel {}", channel, x); + IO.close(attachment instanceof EndPoint ? (EndPoint)attachment : channel); } } else { if (LOG.isDebugEnabled()) - LOG.debug("Selector loop ignoring invalid key for channel {}", key.channel()); - Object attachment = key.attachment(); - if (attachment instanceof EndPoint) - IO.close((EndPoint)attachment); + LOG.debug("Selector loop ignoring invalid key for channel {}", channel); + IO.close(attachment instanceof EndPoint ? (EndPoint)attachment : channel); } } return null; @@ -616,7 +709,7 @@ public class ManagedSelector extends ContainerLifeCycle implements Dumpable private static class DumpKeys implements SelectorUpdate { - private CountDownLatch latch = new CountDownLatch(1); + private final CountDownLatch latch = new CountDownLatch(1); private List keys; @Override @@ -652,9 +745,9 @@ public class ManagedSelector extends ContainerLifeCycle implements Dumpable private final SelectableChannel _channel; private SelectionKey _key; - public Acceptor(SelectableChannel channel) + Acceptor(SelectableChannel channel) { - this._channel = channel; + _channel = channel; } @Override @@ -662,31 +755,26 @@ public class ManagedSelector extends ContainerLifeCycle implements Dumpable { try { - if (_key == null) - { - _key = _channel.register(selector, SelectionKey.OP_ACCEPT, this); - } - + _key = _channel.register(selector, SelectionKey.OP_ACCEPT, this); if (LOG.isDebugEnabled()) - LOG.debug("{} acceptor={}", this, _key); + LOG.debug("{} acceptor={}", this, _channel); } catch (Throwable x) { IO.close(_channel); - LOG.warn("Unable to register OP_ACCEPT on selector", x); + LOG.warn("Unable to register OP_ACCEPT on selector for {}", _channel, x); } } @Override public Runnable onSelected() { - SelectableChannel server = _key.channel(); SelectableChannel channel = null; try { while (true) { - channel = _selectorManager.doAccept(server); + channel = _selectorManager.doAccept(_channel); if (channel == null) break; _selectorManager.accepted(channel); @@ -694,10 +782,9 @@ public class ManagedSelector extends ContainerLifeCycle implements Dumpable } catch (Throwable x) { + LOG.warn("Accept failed for channel {}", channel, x); IO.close(channel); - LOG.warn("Accept failed for channel " + channel, x); } - return null; } @@ -706,13 +793,18 @@ public class ManagedSelector extends ContainerLifeCycle implements Dumpable { } + @Override + public void replaceKey(SelectionKey newKey) + { + _key = newKey; + } + @Override public void close() throws IOException { - SelectionKey key = _key; - _key = null; - if (key != null && key.isValid()) - key.cancel(); + // May be called from any thread. + // Implements AbstractConnector.setAccepting(boolean). + submit(selector -> _key.cancel()); } } @@ -732,7 +824,8 @@ public class ManagedSelector extends ContainerLifeCycle implements Dumpable @Override public void close() { - LOG.debug("closed accept of {}", channel); + if (LOG.isDebugEnabled()) + LOG.debug("closed accept of {}", channel); IO.close(channel); } @@ -748,7 +841,8 @@ public class ManagedSelector extends ContainerLifeCycle implements Dumpable { IO.close(channel); _selectorManager.onAcceptFailed(channel, x); - LOG.debug("Unable to register update for accept", x); + if (LOG.isDebugEnabled()) + LOG.debug("Could not register channel after accept {}", channel, x); } } @@ -762,7 +856,6 @@ public class ManagedSelector extends ContainerLifeCycle implements Dumpable } catch (Throwable x) { - LOG.debug("Unable to accept", x); failed(x); } } @@ -770,10 +863,17 @@ public class ManagedSelector extends ContainerLifeCycle implements Dumpable protected void failed(Throwable failure) { IO.close(channel); - LOG.warn("ManagedSelector#Accept failure : {}", Objects.toString(failure)); - LOG.debug("ManagedSelector#Accept failure", failure); + LOG.warn("Could not accept {}: {}", channel, String.valueOf(failure)); + if (LOG.isDebugEnabled()) + LOG.debug("", failure); _selectorManager.onAcceptFailed(channel, failure); } + + @Override + public String toString() + { + return String.format("%s@%x[%s]", getClass().getSimpleName(), hashCode(), channel); + } } class Connect implements SelectorUpdate, Runnable @@ -833,16 +933,15 @@ public class ManagedSelector extends ContainerLifeCycle implements Dumpable private class CloseConnections implements SelectorUpdate { - final Set _closed; - final CountDownLatch _noEndPoints = new CountDownLatch(1); - final CountDownLatch _complete = new CountDownLatch(1); + private final Set _closed; + private final CountDownLatch _complete = new CountDownLatch(1); - public CloseConnections() + private CloseConnections() { this(null); } - public CloseConnections(Set closed) + private CloseConnections(Set closed) { _closed = closed; } @@ -852,7 +951,6 @@ public class ManagedSelector extends ContainerLifeCycle implements Dumpable { if (LOG.isDebugEnabled()) LOG.debug("Closing {} connections on {}", selector.keys().size(), ManagedSelector.this); - boolean zero = true; for (SelectionKey key : selector.keys()) { if (key != null && key.isValid()) @@ -861,14 +959,9 @@ public class ManagedSelector extends ContainerLifeCycle implements Dumpable Object attachment = key.attachment(); if (attachment instanceof EndPoint) { - EndPoint endp = (EndPoint)attachment; - if (!endp.isOutputShutdown()) - zero = false; - Connection connection = endp.getConnection(); - if (connection != null) - closeable = connection; - else - closeable = endp; + EndPoint endPoint = (EndPoint)attachment; + Connection connection = endPoint.getConnection(); + closeable = Objects.requireNonNullElse(connection, endPoint); } if (closeable != null) @@ -885,30 +978,26 @@ public class ManagedSelector extends ContainerLifeCycle implements Dumpable } } } - - if (zero) - _noEndPoints.countDown(); _complete.countDown(); } } private class StopSelector implements SelectorUpdate { - CountDownLatch _stopped = new CountDownLatch(1); + private final CountDownLatch _stopped = new CountDownLatch(1); @Override public void update(Selector selector) { for (SelectionKey key : selector.keys()) { - if (key != null && key.isValid()) - { - Object attachment = key.attachment(); - if (attachment instanceof EndPoint) - IO.close((EndPoint)attachment); - } + // Key may be null when using the UnixSocket selector. + if (key == null) + continue; + Object attachment = key.attachment(); + if (attachment instanceof Closeable) + IO.close((Closeable)attachment); } - _selector = null; IO.close(selector); _stopped.countDown(); @@ -936,8 +1025,9 @@ public class ManagedSelector extends ContainerLifeCycle implements Dumpable catch (Throwable failure) { IO.close(_connect.channel); - LOG.warn("ManagedSelector#CreateEndpoint failure : {}", Objects.toString(failure)); - LOG.debug("ManagedSelector#CreateEndpoint failure", failure); + LOG.warn("Could not create EndPoint {}: {}", _connect.channel, String.valueOf(failure)); + if (LOG.isDebugEnabled()) + LOG.debug("", failure); _connect.failed(failure); } } @@ -945,7 +1035,7 @@ public class ManagedSelector extends ContainerLifeCycle implements Dumpable @Override public String toString() { - return String.format("CreateEndPoint@%x{%s,%s}", hashCode(), _connect, _key); + return String.format("%s@%x{%s}", getClass().getSimpleName(), hashCode(), _connect); } } @@ -954,7 +1044,7 @@ public class ManagedSelector extends ContainerLifeCycle implements Dumpable private final EndPoint endPoint; private final Throwable cause; - public DestroyEndPoint(EndPoint endPoint, Throwable cause) + private DestroyEndPoint(EndPoint endPoint, Throwable cause) { this.endPoint = endPoint; this.cause = cause; diff --git a/jetty-io/src/main/java/org/eclipse/jetty/io/NetworkTrafficSocketChannelEndPoint.java b/jetty-io/src/main/java/org/eclipse/jetty/io/NetworkTrafficSocketChannelEndPoint.java index 81fe1c04c88..272c0ecc0ac 100644 --- a/jetty-io/src/main/java/org/eclipse/jetty/io/NetworkTrafficSocketChannelEndPoint.java +++ b/jetty-io/src/main/java/org/eclipse/jetty/io/NetworkTrafficSocketChannelEndPoint.java @@ -20,8 +20,8 @@ package org.eclipse.jetty.io; import java.io.IOException; import java.nio.ByteBuffer; -import java.nio.channels.SelectableChannel; import java.nio.channels.SelectionKey; +import java.nio.channels.SocketChannel; import org.eclipse.jetty.util.thread.Scheduler; import org.slf4j.Logger; @@ -36,7 +36,7 @@ public class NetworkTrafficSocketChannelEndPoint extends SocketChannelEndPoint private final NetworkTrafficListener listener; - public NetworkTrafficSocketChannelEndPoint(SelectableChannel channel, ManagedSelector selectSet, SelectionKey key, Scheduler scheduler, long idleTimeout, NetworkTrafficListener listener) + public NetworkTrafficSocketChannelEndPoint(SocketChannel channel, ManagedSelector selectSet, SelectionKey key, Scheduler scheduler, long idleTimeout, NetworkTrafficListener listener) { super(channel, selectSet, key, scheduler); setIdleTimeout(idleTimeout); @@ -80,7 +80,7 @@ public class NetworkTrafficSocketChannelEndPoint extends SocketChannelEndPoint { try { - listener.opened(getSocket()); + listener.opened(getChannel().socket()); } catch (Throwable x) { @@ -97,7 +97,7 @@ public class NetworkTrafficSocketChannelEndPoint extends SocketChannelEndPoint { try { - listener.closed(getSocket()); + listener.closed(getChannel().socket()); } catch (Throwable x) { @@ -113,7 +113,7 @@ public class NetworkTrafficSocketChannelEndPoint extends SocketChannelEndPoint try { ByteBuffer view = buffer.asReadOnlyBuffer(); - listener.incoming(getSocket(), view); + listener.incoming(getChannel().socket(), view); } catch (Throwable x) { @@ -128,7 +128,7 @@ public class NetworkTrafficSocketChannelEndPoint extends SocketChannelEndPoint { try { - listener.outgoing(getSocket(), view); + listener.outgoing(getChannel().socket(), view); } catch (Throwable x) { diff --git a/jetty-io/src/main/java/org/eclipse/jetty/io/SelectorManager.java b/jetty-io/src/main/java/org/eclipse/jetty/io/SelectorManager.java index 1bcd0080ed8..81b034f4a2d 100644 --- a/jetty-io/src/main/java/org/eclipse/jetty/io/SelectorManager.java +++ b/jetty-io/src/main/java/org/eclipse/jetty/io/SelectorManager.java @@ -195,7 +195,7 @@ public abstract class SelectorManager extends ContainerLifeCycle implements Dump */ public void accept(SelectableChannel channel, Object attachment) { - final ManagedSelector selector = chooseSelector(); + ManagedSelector selector = chooseSelector(); selector.submit(selector.new Accept(channel, attachment)); } @@ -210,7 +210,7 @@ public abstract class SelectorManager extends ContainerLifeCycle implements Dump */ public Closeable acceptor(SelectableChannel server) { - final ManagedSelector selector = chooseSelector(); + ManagedSelector selector = chooseSelector(); ManagedSelector.Acceptor acceptor = selector.new Acceptor(server); selector.submit(acceptor); return acceptor; diff --git a/jetty-io/src/main/java/org/eclipse/jetty/io/SocketChannelEndPoint.java b/jetty-io/src/main/java/org/eclipse/jetty/io/SocketChannelEndPoint.java index 23809302b8c..e662c104b58 100644 --- a/jetty-io/src/main/java/org/eclipse/jetty/io/SocketChannelEndPoint.java +++ b/jetty-io/src/main/java/org/eclipse/jetty/io/SocketChannelEndPoint.java @@ -18,53 +18,165 @@ package org.eclipse.jetty.io; +import java.io.Closeable; import java.io.IOException; import java.net.InetSocketAddress; import java.net.Socket; -import java.nio.channels.SelectableChannel; +import java.nio.ByteBuffer; +import java.nio.channels.CancelledKeyException; import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; import java.nio.channels.SocketChannel; +import org.eclipse.jetty.util.BufferUtil; +import org.eclipse.jetty.util.thread.Invocable; import org.eclipse.jetty.util.thread.Scheduler; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -public class SocketChannelEndPoint extends ChannelEndPoint +/** + * Channel End Point. + *

Holds the channel and socket for an NIO endpoint. + */ +public class SocketChannelEndPoint extends AbstractEndPoint implements ManagedSelector.Selectable { private static final Logger LOG = LoggerFactory.getLogger(SocketChannelEndPoint.class); - private final Socket _socket; - private final InetSocketAddress _local; - private final InetSocketAddress _remote; - public SocketChannelEndPoint(SelectableChannel channel, ManagedSelector selector, SelectionKey key, Scheduler scheduler) + private final SocketChannel _channel; + private final ManagedSelector _selector; + private SelectionKey _key; + private boolean _updatePending; + // The current value for interestOps. + private int _currentInterestOps; + // The desired value for interestOps. + private int _desiredInterestOps; + + private abstract class RunnableTask implements Runnable, Invocable { - this((SocketChannel)channel, selector, key, scheduler); + final String _operation; + + protected RunnableTask(String op) + { + _operation = op; + } + + @Override + public String toString() + { + return String.format("%s:%s:%s", SocketChannelEndPoint.this, _operation, getInvocationType()); + } } + private abstract class RunnableCloseable extends RunnableTask implements Closeable + { + protected RunnableCloseable(String op) + { + super(op); + } + + @Override + public void close() + { + try + { + SocketChannelEndPoint.this.close(); + } + catch (Throwable x) + { + LOG.warn("Unable to close {}", SocketChannelEndPoint.this, x); + } + } + } + + private final ManagedSelector.SelectorUpdate _updateKeyAction = this::updateKeyAction; + + private final Runnable _runFillable = new RunnableCloseable("runFillable") + { + @Override + public InvocationType getInvocationType() + { + return getFillInterest().getCallbackInvocationType(); + } + + @Override + public void run() + { + getFillInterest().fillable(); + } + }; + + private final Runnable _runCompleteWrite = new RunnableCloseable("runCompleteWrite") + { + @Override + public InvocationType getInvocationType() + { + return getWriteFlusher().getCallbackInvocationType(); + } + + @Override + public void run() + { + getWriteFlusher().completeWrite(); + } + + @Override + public String toString() + { + return String.format("%s:%s:%s->%s", SocketChannelEndPoint.this, _operation, getInvocationType(), getWriteFlusher()); + } + }; + + private final Runnable _runCompleteWriteFillable = new RunnableCloseable("runCompleteWriteFillable") + { + @Override + public InvocationType getInvocationType() + { + InvocationType fillT = getFillInterest().getCallbackInvocationType(); + InvocationType flushT = getWriteFlusher().getCallbackInvocationType(); + if (fillT == flushT) + return fillT; + + if (fillT == InvocationType.EITHER && flushT == InvocationType.NON_BLOCKING) + return InvocationType.EITHER; + + if (fillT == InvocationType.NON_BLOCKING && flushT == InvocationType.EITHER) + return InvocationType.EITHER; + + return InvocationType.BLOCKING; + } + + @Override + public void run() + { + getWriteFlusher().completeWrite(); + getFillInterest().fillable(); + } + }; + public SocketChannelEndPoint(SocketChannel channel, ManagedSelector selector, SelectionKey key, Scheduler scheduler) { - super(channel, selector, key, scheduler); - - _socket = channel.socket(); - _local = (InetSocketAddress)_socket.getLocalSocketAddress(); - _remote = (InetSocketAddress)_socket.getRemoteSocketAddress(); - } - - public Socket getSocket() - { - return _socket; + super(scheduler); + _channel = channel; + _selector = selector; + _key = key; } @Override public InetSocketAddress getLocalAddress() { - return _local; + return (InetSocketAddress)_channel.socket().getLocalSocketAddress(); } @Override public InetSocketAddress getRemoteAddress() { - return _remote; + return (InetSocketAddress)_channel.socket().getRemoteSocketAddress(); + } + + @Override + public boolean isOpen() + { + return _channel.isOpen(); } @Override @@ -72,12 +184,250 @@ public class SocketChannelEndPoint extends ChannelEndPoint { try { - if (!_socket.isOutputShutdown()) - _socket.shutdownOutput(); + Socket socket = _channel.socket(); + if (!socket.isOutputShutdown()) + socket.shutdownOutput(); + } + catch (IOException e) + { + LOG.debug("Could not shutdown output for {}", _channel, e); + } + } + + @Override + public void doClose() + { + if (LOG.isDebugEnabled()) + LOG.debug("doClose {}", this); + try + { + _channel.close(); + } + catch (IOException e) + { + LOG.debug("Unable to close channel", e); + } + finally + { + super.doClose(); + } + } + + @Override + public void onClose(Throwable cause) + { + try + { + super.onClose(cause); + } + finally + { + if (_selector != null) + _selector.destroyEndPoint(this, cause); + } + } + + @Override + public int fill(ByteBuffer buffer) throws IOException + { + if (isInputShutdown()) + return -1; + + int pos = BufferUtil.flipToFill(buffer); + int filled; + try + { + filled = _channel.read(buffer); + if (filled > 0) + notIdle(); + else if (filled == -1) + shutdownInput(); } catch (IOException e) { LOG.debug("Unable to shutdown output", e); + shutdownInput(); + filled = -1; + } + finally + { + BufferUtil.flipToFlush(buffer, pos); + } + if (LOG.isDebugEnabled()) + LOG.debug("filled {} {}", filled, BufferUtil.toDetailString(buffer)); + return filled; + } + + @Override + public boolean flush(ByteBuffer... buffers) throws IOException + { + long flushed; + try + { + flushed = _channel.write(buffers); + if (LOG.isDebugEnabled()) + LOG.debug("flushed {} {}", flushed, this); + } + catch (IOException e) + { + throw new EofException(e); + } + + if (flushed > 0) + notIdle(); + + for (ByteBuffer b : buffers) + { + if (!BufferUtil.isEmpty(b)) + return false; + } + + return true; + } + + public SocketChannel getChannel() + { + return _channel; + } + + @Override + public Object getTransport() + { + return _channel; + } + + @Override + protected void needsFillInterest() + { + changeInterests(SelectionKey.OP_READ); + } + + @Override + protected void onIncompleteFlush() + { + changeInterests(SelectionKey.OP_WRITE); + } + + @Override + public Runnable onSelected() + { + // This method runs from the selector thread, + // possibly concurrently with changeInterests(int). + + int readyOps = _key.readyOps(); + int oldInterestOps; + int newInterestOps; + synchronized (this) + { + _updatePending = true; + // Remove the readyOps, that here can only be OP_READ or OP_WRITE (or both). + oldInterestOps = _desiredInterestOps; + newInterestOps = oldInterestOps & ~readyOps; + _desiredInterestOps = newInterestOps; + } + + boolean fillable = (readyOps & SelectionKey.OP_READ) != 0; + boolean flushable = (readyOps & SelectionKey.OP_WRITE) != 0; + + if (LOG.isDebugEnabled()) + LOG.debug("onSelected {}->{} r={} w={} for {}", oldInterestOps, newInterestOps, fillable, flushable, this); + + // return task to complete the job + Runnable task = fillable + ? (flushable + ? _runCompleteWriteFillable + : _runFillable) + : (flushable + ? _runCompleteWrite + : null); + + if (LOG.isDebugEnabled()) + LOG.debug("task {}", task); + return task; + } + + private void updateKeyAction(Selector selector) + { + updateKey(); + } + + @Override + public void updateKey() + { + // This method runs from the selector thread, + // possibly concurrently with changeInterests(int). + + try + { + int oldInterestOps; + int newInterestOps; + synchronized (this) + { + _updatePending = false; + oldInterestOps = _currentInterestOps; + newInterestOps = _desiredInterestOps; + if (oldInterestOps != newInterestOps) + { + _currentInterestOps = newInterestOps; + _key.interestOps(newInterestOps); + } + } + + if (LOG.isDebugEnabled()) + LOG.debug("Key interests updated {} -> {} on {}", oldInterestOps, newInterestOps, this); + } + catch (CancelledKeyException x) + { + if (LOG.isDebugEnabled()) + LOG.debug("Ignoring key update for cancelled key {}", this, x); + close(); + } + catch (Throwable x) + { + LOG.warn("Ignoring key update for {}", this, x); + close(); } } + + @Override + public void replaceKey(SelectionKey newKey) + { + _key = newKey; + } + + private void changeInterests(int operation) + { + // This method runs from any thread, possibly + // concurrently with updateKey() and onSelected(). + + int oldInterestOps; + int newInterestOps; + boolean pending; + synchronized (this) + { + pending = _updatePending; + oldInterestOps = _desiredInterestOps; + newInterestOps = oldInterestOps | operation; + if (newInterestOps != oldInterestOps) + _desiredInterestOps = newInterestOps; + } + + if (LOG.isDebugEnabled()) + LOG.debug("changeInterests p={} {}->{} for {}", pending, oldInterestOps, newInterestOps, this); + + if (!pending && _selector != null) + _selector.submit(_updateKeyAction); + } + + @Override + public String toEndPointString() + { + // We do a best effort to print the right toString() and that's it. + return String.format("%s{io=%d/%d,kio=%d,kro=%d}", + super.toEndPointString(), + _currentInterestOps, + _desiredInterestOps, + ManagedSelector.safeInterestOps(_key), + ManagedSelector.safeReadyOps(_key)); + } } diff --git a/jetty-io/src/test/java/org/eclipse/jetty/io/SelectorManagerTest.java b/jetty-io/src/test/java/org/eclipse/jetty/io/SelectorManagerTest.java index 5d3ca7e2772..9890de59d17 100644 --- a/jetty-io/src/test/java/org/eclipse/jetty/io/SelectorManagerTest.java +++ b/jetty-io/src/test/java/org/eclipse/jetty/io/SelectorManagerTest.java @@ -72,11 +72,11 @@ public class SelectorManagerTest SelectorManager selectorManager = new SelectorManager(executor, scheduler) { @Override - protected EndPoint newEndPoint(SelectableChannel channel, ManagedSelector selector, SelectionKey key) throws IOException + protected EndPoint newEndPoint(SelectableChannel channel, ManagedSelector selector, SelectionKey key) { - SocketChannelEndPoint endp = new SocketChannelEndPoint(channel, selector, key, getScheduler()); - endp.setIdleTimeout(connectTimeout / 2); - return endp; + SocketChannelEndPoint endPoint = new SocketChannelEndPoint((SocketChannel)channel, selector, key, getScheduler()); + endPoint.setIdleTimeout(connectTimeout / 2); + return endPoint; } @Override @@ -96,7 +96,7 @@ public class SelectorManagerTest } @Override - public Connection newConnection(SelectableChannel channel, EndPoint endpoint, Object attachment) throws IOException + public Connection newConnection(SelectableChannel channel, EndPoint endpoint, Object attachment) { ((Callback)attachment).succeeded(); return new AbstractConnection(endpoint, executor) diff --git a/jetty-io/src/test/java/org/eclipse/jetty/io/SocketChannelEndPointInterestsTest.java b/jetty-io/src/test/java/org/eclipse/jetty/io/SocketChannelEndPointInterestsTest.java index 3ee35a3f160..94f79c12d45 100644 --- a/jetty-io/src/test/java/org/eclipse/jetty/io/SocketChannelEndPointInterestsTest.java +++ b/jetty-io/src/test/java/org/eclipse/jetty/io/SocketChannelEndPointInterestsTest.java @@ -69,7 +69,7 @@ public class SocketChannelEndPointInterestsTest @Override protected EndPoint newEndPoint(SelectableChannel channel, ManagedSelector selector, SelectionKey key) { - SocketChannelEndPoint endp = new SocketChannelEndPoint(channel, selector, key, getScheduler()) + SocketChannelEndPoint endp = new SocketChannelEndPoint((SocketChannel)channel, selector, key, getScheduler()) { @Override protected void onIncompleteFlush() diff --git a/jetty-io/src/test/java/org/eclipse/jetty/io/SocketChannelEndPointTest.java b/jetty-io/src/test/java/org/eclipse/jetty/io/SocketChannelEndPointTest.java index 537174b8625..595ddb52115 100644 --- a/jetty-io/src/test/java/org/eclipse/jetty/io/SocketChannelEndPointTest.java +++ b/jetty-io/src/test/java/org/eclipse/jetty/io/SocketChannelEndPointTest.java @@ -465,10 +465,10 @@ public class SocketChannelEndPointTest @Override protected EndPoint newEndPoint(SelectableChannel channel, ManagedSelector selector, SelectionKey selectionKey) { - SocketChannelEndPoint endp = new SocketChannelEndPoint(channel, selector, selectionKey, getScheduler()); - _lastEndPoint = endp; + SocketChannelEndPoint endPoint = new SocketChannelEndPoint((SocketChannel)channel, selector, selectionKey, getScheduler()); + _lastEndPoint = endPoint; _lastEndPointLatch.countDown(); - return endp; + return endPoint; } @Override @@ -580,11 +580,11 @@ public class SocketChannelEndPointTest protected EndPoint newEndPoint(SelectableChannel channel, ManagedSelector selector, SelectionKey key) { - SocketChannelEndPoint endp = new SocketChannelEndPoint(channel, selector, key, getScheduler()); - endp.setIdleTimeout(60000); - _lastEndPoint = endp; + SocketChannelEndPoint endPoint = new SocketChannelEndPoint((SocketChannel)channel, selector, key, getScheduler()); + endPoint.setIdleTimeout(60000); + _lastEndPoint = endPoint; _lastEndPointLatch.countDown(); - return endp; + return endPoint; } @Override @@ -743,7 +743,7 @@ public class SocketChannelEndPointTest return; } - EndPoint endp = getEndPoint(); + EndPoint endPoint = getEndPoint(); try { _last = TimeUnit.NANOSECONDS.toMillis(System.nanoTime()); @@ -756,17 +756,17 @@ public class SocketChannelEndPointTest BufferUtil.compact(_in); if (BufferUtil.isFull(_in)) throw new IllegalStateException("FULL " + BufferUtil.toDetailString(_in)); - int filled = endp.fill(_in); + int filled = endPoint.fill(_in); if (filled > 0) progress = true; // If the tests wants to block, then block - while (_blockAt.get() > 0 && endp.isOpen() && _in.remaining() < _blockAt.get()) + while (_blockAt.get() > 0 && endPoint.isOpen() && _in.remaining() < _blockAt.get()) { FutureCallback future = _blockingRead = new FutureCallback(); fillInterested(); future.get(); - filled = endp.fill(_in); + filled = endPoint.fill(_in); progress |= filled > 0; } @@ -782,18 +782,18 @@ public class SocketChannelEndPointTest for (int i = 0; i < _writeCount.get(); i++) { FutureCallback blockingWrite = new FutureCallback(); - endp.write(blockingWrite, out.asReadOnlyBuffer()); + endPoint.write(blockingWrite, out.asReadOnlyBuffer()); blockingWrite.get(); } progress = true; } // are we done? - if (endp.isInputShutdown()) - endp.shutdownOutput(); + if (endPoint.isInputShutdown()) + endPoint.shutdownOutput(); } - if (endp.isOpen()) + if (endPoint.isOpen()) fillInterested(); } catch (ExecutionException e) @@ -802,9 +802,9 @@ public class SocketChannelEndPointTest try { FutureCallback blockingWrite = new FutureCallback(); - endp.write(blockingWrite, BufferUtil.toBuffer("EE: " + BufferUtil.toString(_in))); + endPoint.write(blockingWrite, BufferUtil.toBuffer("EE: " + BufferUtil.toString(_in))); blockingWrite.get(); - endp.shutdownOutput(); + endPoint.shutdownOutput(); } catch (Exception e2) { 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 e42e4f670bc..a0570c70920 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 @@ -506,7 +506,7 @@ public class ConnectHandler extends HandlerWrapper @Override protected EndPoint newEndPoint(SelectableChannel channel, ManagedSelector selector, SelectionKey key) { - SocketChannelEndPoint endPoint = new SocketChannelEndPoint(channel, selector, key, getScheduler()); + SocketChannelEndPoint endPoint = new SocketChannelEndPoint((SocketChannel)channel, selector, key, getScheduler()); endPoint.setIdleTimeout(getIdleTimeout()); return endPoint; } diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/NetworkTrafficServerConnector.java b/jetty-server/src/main/java/org/eclipse/jetty/server/NetworkTrafficServerConnector.java index fef244df7fd..27ca81906a7 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/NetworkTrafficServerConnector.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/NetworkTrafficServerConnector.java @@ -23,10 +23,10 @@ import java.nio.channels.SocketChannel; import java.util.concurrent.Executor; import org.eclipse.jetty.io.ByteBufferPool; -import org.eclipse.jetty.io.ChannelEndPoint; import org.eclipse.jetty.io.ManagedSelector; import org.eclipse.jetty.io.NetworkTrafficListener; import org.eclipse.jetty.io.NetworkTrafficSocketChannelEndPoint; +import org.eclipse.jetty.io.SocketChannelEndPoint; import org.eclipse.jetty.util.ssl.SslContextFactory; import org.eclipse.jetty.util.thread.Scheduler; @@ -81,7 +81,7 @@ public class NetworkTrafficServerConnector extends ServerConnector } @Override - protected ChannelEndPoint newEndPoint(SocketChannel channel, ManagedSelector selectSet, SelectionKey key) + protected SocketChannelEndPoint newEndPoint(SocketChannel channel, ManagedSelector selectSet, SelectionKey key) { return new NetworkTrafficSocketChannelEndPoint(channel, selectSet, key, getScheduler(), getIdleTimeout(), getNetworkTrafficListener()); } diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/ServerConnector.java b/jetty-server/src/main/java/org/eclipse/jetty/server/ServerConnector.java index 5baaa6988ff..23eb3d440df 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/ServerConnector.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/ServerConnector.java @@ -35,7 +35,6 @@ import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicReference; import org.eclipse.jetty.io.ByteBufferPool; -import org.eclipse.jetty.io.ChannelEndPoint; import org.eclipse.jetty.io.Connection; import org.eclipse.jetty.io.EndPoint; import org.eclipse.jetty.io.ManagedSelector; @@ -424,7 +423,7 @@ public class ServerConnector extends AbstractNetworkConnector return _localPort; } - protected ChannelEndPoint newEndPoint(SocketChannel channel, ManagedSelector selectSet, SelectionKey key) throws IOException + protected SocketChannelEndPoint newEndPoint(SocketChannel channel, ManagedSelector selectSet, SelectionKey key) throws IOException { SocketChannelEndPoint endpoint = new SocketChannelEndPoint(channel, selectSet, key, getScheduler()); endpoint.setIdleTimeout(getIdleTimeout()); @@ -511,9 +510,9 @@ public class ServerConnector extends AbstractNetworkConnector } @Override - protected ChannelEndPoint newEndPoint(SelectableChannel channel, ManagedSelector selectSet, SelectionKey selectionKey) throws IOException + protected SocketChannelEndPoint newEndPoint(SelectableChannel channel, ManagedSelector selector, SelectionKey selectionKey) throws IOException { - return ServerConnector.this.newEndPoint((SocketChannel)channel, selectSet, selectionKey); + return ServerConnector.this.newEndPoint((SocketChannel)channel, selector, selectionKey); } @Override diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/SocketCustomizationListener.java b/jetty-server/src/main/java/org/eclipse/jetty/server/SocketCustomizationListener.java index 39f78844028..b9d6c1486a6 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/SocketCustomizationListener.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/SocketCustomizationListener.java @@ -59,18 +59,18 @@ public class SocketCustomizationListener implements Listener @Override public void onOpened(Connection connection) { - EndPoint endp = connection.getEndPoint(); + EndPoint endPoint = connection.getEndPoint(); boolean ssl = false; - if (_ssl && endp instanceof DecryptedEndPoint) + if (_ssl && endPoint instanceof DecryptedEndPoint) { - endp = ((DecryptedEndPoint)endp).getSslConnection().getEndPoint(); + endPoint = ((DecryptedEndPoint)endPoint).getSslConnection().getEndPoint(); ssl = true; } - if (endp instanceof SocketChannelEndPoint) + if (endPoint instanceof SocketChannelEndPoint) { - Socket socket = ((SocketChannelEndPoint)endp).getSocket(); + Socket socket = ((SocketChannelEndPoint)endPoint).getChannel().socket(); customize(socket, connection.getClass(), ssl); } } diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/gzip/GzipHttpOutputInterceptor.java b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/gzip/GzipHttpOutputInterceptor.java index 8b1a16d9c8c..d45008076e2 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/gzip/GzipHttpOutputInterceptor.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/gzip/GzipHttpOutputInterceptor.java @@ -223,7 +223,15 @@ public class GzipHttpOutputInterceptor implements HttpOutput.Interceptor LOG.debug("{} compressing {}", this, _deflater); _state.set(GZState.COMPRESSING); - gzip(content, complete, callback); + if (BufferUtil.isEmpty(content)) + { + // We are committing, but have no content to compress, so flush empty buffer to write headers. + _interceptor.write(BufferUtil.EMPTY_BUFFER, complete, callback); + } + else + { + gzip(content, complete, callback); + } } else callback.failed(new WritePendingException()); @@ -406,7 +414,7 @@ public class GzipHttpOutputInterceptor implements HttpOutput.Interceptor @Override public String toString() { - return String.format("%s[content=%s last=%b copy=%s buffer=%s deflate=%s", + return String.format("%s[content=%s last=%b copy=%s buffer=%s deflate=%s %s]", super.toString(), BufferUtil.toDetailString(_content), _last, diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/AsyncCompletionTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/AsyncCompletionTest.java index 5e4dd58847c..0d478f311d9 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/AsyncCompletionTest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/AsyncCompletionTest.java @@ -45,7 +45,6 @@ import jakarta.servlet.WriteListener; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.eclipse.jetty.http.tools.HttpTester; -import org.eclipse.jetty.io.ChannelEndPoint; import org.eclipse.jetty.io.Connection; import org.eclipse.jetty.io.EndPoint; import org.eclipse.jetty.io.ManagedSelector; @@ -132,7 +131,7 @@ public class AsyncCompletionTest extends HttpServerTestFixture }) { @Override - protected ChannelEndPoint newEndPoint(SocketChannel channel, ManagedSelector selectSet, SelectionKey key) throws IOException + protected SocketChannelEndPoint newEndPoint(SocketChannel channel, ManagedSelector selectSet, SelectionKey key) { return new ExtendedEndPoint(channel, selectSet, key, getScheduler()); } diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/ExtendedServerTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/ExtendedServerTest.java index 25eab5cd3b9..88b6a5dbc0e 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/ExtendedServerTest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/ExtendedServerTest.java @@ -30,7 +30,6 @@ import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.eclipse.jetty.http.HttpVersion; -import org.eclipse.jetty.io.ChannelEndPoint; import org.eclipse.jetty.io.Connection; import org.eclipse.jetty.io.EndPoint; import org.eclipse.jetty.io.ManagedSelector; @@ -61,7 +60,7 @@ public class ExtendedServerTest extends HttpServerTestBase }) { @Override - protected ChannelEndPoint newEndPoint(SocketChannel channel, ManagedSelector selectSet, SelectionKey key) throws IOException + protected SocketChannelEndPoint newEndPoint(SocketChannel channel, ManagedSelector selectSet, SelectionKey key) { return new ExtendedEndPoint(channel, selectSet, key, getScheduler()); } diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/ServerConnectorTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/ServerConnectorTest.java index 8504061b971..2bc445a2456 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/ServerConnectorTest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/ServerConnectorTest.java @@ -71,7 +71,7 @@ public class ServerConnectorTest EndPoint endPoint = baseRequest.getHttpChannel().getEndPoint(); assertThat("Endpoint", endPoint, instanceOf(SocketChannelEndPoint.class)); SocketChannelEndPoint channelEndPoint = (SocketChannelEndPoint)endPoint; - Socket socket = channelEndPoint.getSocket(); + Socket socket = channelEndPoint.getChannel().socket(); ServerConnector connector = (ServerConnector)baseRequest.getHttpChannel().getConnector(); PrintWriter out = response.getWriter(); @@ -214,7 +214,7 @@ public class ServerConnectorTest } @Test - public void testAddFirstConnectionFactory() throws Exception + public void testAddFirstConnectionFactory() { Server server = new Server(); ServerConnector connector = new ServerConnector(server); @@ -236,7 +236,7 @@ public class ServerConnectorTest public void testExceptionWhileAccepting() throws Exception { Server server = new Server(); - try (StacklessLogging stackless = new StacklessLogging(AbstractConnector.class)) + try (StacklessLogging ignored = new StacklessLogging(AbstractConnector.class)) { AtomicLong spins = new AtomicLong(); ServerConnector connector = new ServerConnector(server, 1, 1) diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SniSslConnectionFactoryTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SniSslConnectionFactoryTest.java index 65660020142..50401f40b5a 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SniSslConnectionFactoryTest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SniSslConnectionFactoryTest.java @@ -300,6 +300,29 @@ public class SniSslConnectionFactoryTest assertThat(response.getStatus(), is(400)); } + @Test + public void testWrongSNIRejectedConnectionWithNonSNIKeystore() throws Exception + { + start(ssl -> + { + // Keystore has only one certificate, but we want to enforce SNI. + ssl.setKeyStorePath("src/test/resources/keystore.p12"); + ssl.setSniRequired(true); + }); + + // Wrong SNI host. + assertThrows(SSLHandshakeException.class, () -> getResponse("wrong.com", "wrong.com", null)); + + // No SNI host. + assertThrows(SSLHandshakeException.class, () -> getResponse(null, "wrong.com", null)); + + // Good SNI host. + HttpTester.Response response = HttpTester.parseResponse(getResponse("localhost", "localhost", null)); + + assertNotNull(response); + assertThat(response.getStatus(), is(200)); + } + @Test public void testSameConnectionRequestsForManyDomains() throws Exception { diff --git a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/GzipHandlerCommitTest.java b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/GzipHandlerCommitTest.java new file mode 100644 index 00000000000..ca7a4ee4e28 --- /dev/null +++ b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/GzipHandlerCommitTest.java @@ -0,0 +1,130 @@ +// +// ======================================================================== +// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under +// the terms of the Eclipse Public License 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0 +// +// This Source Code may also be made available under the following +// Secondary Licenses when the conditions for such availability set +// forth in the Eclipse Public License, v. 2.0 are satisfied: +// the Apache License v2.0 which is available at +// https://www.apache.org/licenses/LICENSE-2.0 +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +package org.eclipse.jetty.servlet; + +import java.io.IOException; +import java.net.URI; +import java.util.Arrays; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import javax.servlet.Servlet; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.api.ContentResponse; +import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.server.handler.gzip.GzipHandler; +import org.eclipse.jetty.util.component.LifeCycle; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class GzipHandlerCommitTest +{ + private Server server; + private HttpClient client; + + public void start(Servlet servlet) throws Exception + { + server = new Server(); + ServerConnector connector = new ServerConnector(server); + connector.setPort(0); + server.addConnector(connector); + + ServletContextHandler contextHandler = new ServletContextHandler(); + contextHandler.setContextPath("/"); + ServletHolder servletHolder = new ServletHolder(servlet); + contextHandler.addServlet(servletHolder, "/test/*"); + + GzipHandler gzipHandler = new GzipHandler(); + gzipHandler.setHandler(contextHandler); + + server.setHandler(gzipHandler); + server.start(); + + client = new HttpClient(); + client.start(); + } + + @AfterEach + public void tearDown() + { + LifeCycle.stop(client); + LifeCycle.stop(server); + } + + @Test + public void testImmediateFlushNoContent() throws Exception + { + CountDownLatch latch = new CountDownLatch(1); + start(new HttpServlet() + { + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) throws IOException + { + response.flushBuffer(); + assertDoesNotThrow(() -> assertTrue(latch.await(1, TimeUnit.SECONDS))); + } + }); + + URI uri = server.getURI().resolve("/test/"); + Request request = client.newRequest(uri); + request.header(HttpHeader.CONNECTION, "Close"); + request.onResponseHeaders((r) -> latch.countDown()); + ContentResponse response = request.send(); + assertThat("Response status", response.getStatus(), is(200)); + } + + @Test + public void testImmediateFlushWithContent() throws Exception + { + int size = 8000; + CountDownLatch latch = new CountDownLatch(1); + start(new HttpServlet() + { + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) throws IOException + { + response.flushBuffer(); + assertDoesNotThrow(() -> assertTrue(latch.await(1, TimeUnit.SECONDS))); + response.getOutputStream(); + byte[] buf = new byte[size]; + Arrays.fill(buf, (byte)'a'); + response.getOutputStream().write(buf); + } + }); + + URI uri = server.getURI().resolve("/test/"); + Request request = client.newRequest(uri); + request.header(HttpHeader.CONNECTION, "Close"); + request.onResponseHeaders((r) -> latch.countDown()); + ContentResponse response = request.send(); + assertThat("Response status", response.getStatus(), is(200)); + assertThat("Response content size", response.getContent().length, is(size)); + } +} diff --git a/jetty-servlet/src/test/resources/jetty-logging.properties b/jetty-servlet/src/test/resources/jetty-logging.properties index 821cf3d2f35..bd3b391a3dc 100644 --- a/jetty-servlet/src/test/resources/jetty-logging.properties +++ b/jetty-servlet/src/test/resources/jetty-logging.properties @@ -1,8 +1,7 @@ # Jetty Logging using jetty-slf4j-impl -org.eclipse.jetty.LEVEL=INFO #org.eclipse.jetty.LEVEL=DEBUG #org.eclipse.jetty.server.LEVEL=DEBUG #org.eclipse.jetty.servlet.LEVEL=DEBUG -#org.eclipse.jetty.io.ChannelEndPoint.LEVEL=DEBUG +#org.eclipse.jetty.io.SocketChannelEndPoint.LEVEL=DEBUG #org.eclipse.jetty.server.DebugListener.LEVEL=DEBUG -#org.eclipse.jetty.server.HttpChannelState.LEVEL=DEBUG \ No newline at end of file +#org.eclipse.jetty.server.HttpChannelState.LEVEL=DEBUG diff --git a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/AbstractFileContentServlet.java b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/AbstractFileContentServlet.java new file mode 100644 index 00000000000..0857532c93c --- /dev/null +++ b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/AbstractFileContentServlet.java @@ -0,0 +1,46 @@ +// +// ======================================================================== +// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under +// the terms of the Eclipse Public License 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0 +// +// This Source Code may also be made available under the following +// Secondary Licenses when the conditions for such availability set +// forth in the Eclipse Public License, v. 2.0 are satisfied: +// the Apache License v2.0 which is available at +// https://www.apache.org/licenses/LICENSE-2.0 +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +package org.eclipse.jetty.servlets; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import javax.servlet.http.HttpServlet; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public abstract class AbstractFileContentServlet extends HttpServlet +{ + protected byte[] loadContentFileBytes(final String fileName) throws IOException + { + String relPath = fileName; + relPath = relPath.replaceFirst("^/context/", ""); + relPath = relPath.replaceFirst("^/", ""); + + String realPath = getServletContext().getRealPath(relPath); + assertNotNull(realPath, "Unable to find real path for " + relPath); + + Path realFile = Paths.get(realPath); + assertTrue(Files.exists(realFile), "Content File should exist: " + realFile); + + return Files.readAllBytes(realFile); + } +} diff --git a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/AbstractGzipTest.java b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/AbstractGzipTest.java new file mode 100644 index 00000000000..b47970ae58e --- /dev/null +++ b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/AbstractGzipTest.java @@ -0,0 +1,159 @@ +// +// ======================================================================== +// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under +// the terms of the Eclipse Public License 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0 +// +// This Source Code may also be made available under the following +// Secondary Licenses when the conditions for such availability set +// forth in the Eclipse Public License, v. 2.0 are satisfied: +// the Apache License v2.0 which is available at +// https://www.apache.org/licenses/LICENSE-2.0 +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +package org.eclipse.jetty.servlets; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.security.DigestOutputStream; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Locale; +import java.util.zip.GZIPInputStream; +import java.util.zip.Inflater; +import java.util.zip.InflaterInputStream; + +import org.eclipse.jetty.http.tools.HttpTester; +import org.eclipse.jetty.server.HttpConfiguration; +import org.eclipse.jetty.server.handler.gzip.GzipHandler; +import org.eclipse.jetty.toolchain.test.FS; +import org.eclipse.jetty.toolchain.test.IO; +import org.eclipse.jetty.toolchain.test.MavenTestingUtils; +import org.eclipse.jetty.util.TypeUtil; + +import static java.nio.charset.StandardCharsets.UTF_8; + +public abstract class AbstractGzipTest +{ + protected static final int DEFAULT_OUTPUT_BUFFER_SIZE = new HttpConfiguration().getOutputBufferSize(); + + protected Path workDir; + + public AbstractGzipTest() + { + workDir = MavenTestingUtils.getTargetTestingPath(this.getClass().getName()); + FS.ensureEmpty(workDir); + } + + protected FilterInputStream newContentEncodingFilterInputStream(String contentEncoding, InputStream inputStream) throws IOException + { + if (contentEncoding == null) + { + return new FilterInputStream(inputStream) {}; + } + else if (contentEncoding.contains(GzipHandler.GZIP)) + { + return new GZIPInputStream(inputStream); + } + else if (contentEncoding.contains(GzipHandler.DEFLATE)) + { + return new InflaterInputStream(inputStream, new Inflater(true)); + } + throw new RuntimeException("Unexpected response content-encoding: " + contentEncoding); + } + + protected UncompressedMetadata parseResponseContent(HttpTester.Response response) throws NoSuchAlgorithmException, IOException + { + UncompressedMetadata metadata = new UncompressedMetadata(); + metadata.contentLength = response.getContentBytes().length; + + String contentEncoding = response.get("Content-Encoding"); + MessageDigest digest = MessageDigest.getInstance("SHA1"); + + try (ByteArrayInputStream bais = new ByteArrayInputStream(response.getContentBytes()); + FilterInputStream streamFilter = newContentEncodingFilterInputStream(contentEncoding, bais); + ByteArrayOutputStream uncompressedStream = new ByteArrayOutputStream(metadata.contentLength); + DigestOutputStream digester = new DigestOutputStream(uncompressedStream, digest)) + { + IO.copy(streamFilter, digester); + metadata.uncompressedContent = uncompressedStream.toByteArray(); + metadata.uncompressedSize = metadata.uncompressedContent.length; + // Odd toUpperCase is because TypeUtil.toHexString is mixed case results!?? + metadata.uncompressedSha1Sum = TypeUtil.toHexString(digest.digest()).toUpperCase(Locale.ENGLISH); + return metadata; + } + } + + protected Path createFile(Path contextDir, String fileName, int fileSize) throws IOException + { + Path destPath = contextDir.resolve(fileName); + byte[] content = generateContent(fileSize); + Files.write(destPath, content, StandardOpenOption.CREATE, StandardOpenOption.WRITE); + return destPath; + } + + /** + * Generate semi-realistic text content of arbitrary length. + *

+ * Note: We don't just create a single string of repeating characters + * as that doesn't test the gzip behavior very well. (too efficient) + * We also don't just generate a random byte array as that is the opposite + * extreme of gzip handling (terribly inefficient). + *

+ * + * @param length the length of the content to generate. + * @return the content. + */ + private byte[] generateContent(int length) + { + StringBuilder builder = new StringBuilder(); + do + { + builder.append("Lorem ipsum dolor sit amet, consectetur adipiscing elit. In quis felis nunc.\n"); + builder.append("Quisque suscipit mauris et ante auctor ornare rhoncus lacus aliquet. Pellentesque\n"); + builder.append("habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas.\n"); + builder.append("Vestibulum sit amet felis augue, vel convallis dolor. Cras accumsan vehicula diam\n"); + builder.append("at faucibus. Etiam in urna turpis, sed congue mi. Morbi et lorem eros. Donec vulputate\n"); + builder.append("velit in risus suscipit lobortis. Aliquam id urna orci, nec sollicitudin ipsum.\n"); + builder.append("Cras a orci turpis. Donec suscipit vulputate cursus. Mauris nunc tellus, fermentum\n"); + builder.append("eu auctor ut, mollis at diam. Quisque porttitor ultrices metus, vitae tincidunt massa\n"); + builder.append("sollicitudin a. Vivamus porttitor libero eget purus hendrerit cursus. Integer aliquam\n"); + builder.append("consequat mauris quis luctus. Cras enim nibh, dignissim eu faucibus ac, mollis nec neque.\n"); + builder.append("Aliquam purus mauris, consectetur nec convallis lacinia, porta sed ante. Suspendisse\n"); + builder.append("et cursus magna. Donec orci enim, molestie a lobortis eu, imperdiet vitae neque.\n"); + } + while (builder.length() < length); + + // Make sure we are exactly at requested length. (truncate the extra) + if (builder.length() > length) + { + builder.setLength(length); + } + + return builder.toString().getBytes(UTF_8); + } + + public static class UncompressedMetadata + { + public byte[] uncompressedContent; + public int contentLength; + public String uncompressedSha1Sum; + public int uncompressedSize; + + public String getContentUTF8() + { + return new String(uncompressedContent, UTF_8); + } + } +} diff --git a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/AsyncScheduledDispatchWrite.java b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/AsyncScheduledDispatchWrite.java index d446eb44798..62e7cc1c545 100644 --- a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/AsyncScheduledDispatchWrite.java +++ b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/AsyncScheduledDispatchWrite.java @@ -31,7 +31,7 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; @SuppressWarnings("serial") -public abstract class AsyncScheduledDispatchWrite extends TestDirContentServlet +public abstract class AsyncScheduledDispatchWrite extends AbstractFileContentServlet { public static class Default extends AsyncScheduledDispatchWrite { @@ -103,7 +103,7 @@ public abstract class AsyncScheduledDispatchWrite extends TestDirContentServlet } else { - String fileName = request.getServletPath(); + String fileName = request.getPathInfo(); byte[] dataBytes = loadContentFileBytes(fileName); response.setContentLength(dataBytes.length); diff --git a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/AsyncTimeoutCompleteWrite.java b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/AsyncTimeoutCompleteWrite.java index 2e4fa66e2cc..b01361458a5 100644 --- a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/AsyncTimeoutCompleteWrite.java +++ b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/AsyncTimeoutCompleteWrite.java @@ -44,7 +44,7 @@ import static org.hamcrest.Matchers.nullValue; * */ @SuppressWarnings("serial") -public abstract class AsyncTimeoutCompleteWrite extends TestDirContentServlet implements AsyncListener +public abstract class AsyncTimeoutCompleteWrite extends AbstractFileContentServlet implements AsyncListener { public static class Default extends AsyncTimeoutCompleteWrite { @@ -87,7 +87,7 @@ public abstract class AsyncTimeoutCompleteWrite extends TestDirContentServlet im // Pass Request & Response ctx = request.startAsync(request, response); } - String fileName = request.getServletPath(); + String fileName = request.getPathInfo(); request.setAttribute("filename", fileName); ctx.addListener(this); ctx.setTimeout(20); diff --git a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/AsyncTimeoutDispatchWrite.java b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/AsyncTimeoutDispatchWrite.java index 22ac5b177dd..8c57d9794c4 100644 --- a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/AsyncTimeoutDispatchWrite.java +++ b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/AsyncTimeoutDispatchWrite.java @@ -29,7 +29,7 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; @SuppressWarnings("serial") -public class AsyncTimeoutDispatchWrite extends TestDirContentServlet implements AsyncListener +public class AsyncTimeoutDispatchWrite extends AbstractFileContentServlet implements AsyncListener { public static class Default extends AsyncTimeoutDispatchWrite { @@ -78,7 +78,7 @@ public class AsyncTimeoutDispatchWrite extends TestDirContentServlet implements else { // second pass through, as result of timeout -> dispatch - String fileName = request.getServletPath(); + String fileName = request.getPathInfo(); byte[] dataBytes = loadContentFileBytes(fileName); response.setContentLength(dataBytes.length); diff --git a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/TestServletLengthStreamTypeWrite.java b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/BlockingServletLengthStreamTypeWrite.java similarity index 93% rename from jetty-servlets/src/test/java/org/eclipse/jetty/servlets/TestServletLengthStreamTypeWrite.java rename to jetty-servlets/src/test/java/org/eclipse/jetty/servlets/BlockingServletLengthStreamTypeWrite.java index 508d64fcb32..4bf3e400c17 100644 --- a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/TestServletLengthStreamTypeWrite.java +++ b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/BlockingServletLengthStreamTypeWrite.java @@ -42,12 +42,12 @@ import org.eclipse.jetty.server.handler.gzip.GzipHandler; * @see http://bugs.eclipse.org/354014 */ @SuppressWarnings("serial") -public class TestServletLengthStreamTypeWrite extends TestDirContentServlet +public class BlockingServletLengthStreamTypeWrite extends AbstractFileContentServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { - String fileName = request.getServletPath(); + String fileName = request.getPathInfo(); byte[] dataBytes = loadContentFileBytes(fileName); response.setContentLength(dataBytes.length); diff --git a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/TestServletLengthTypeStreamWrite.java b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/BlockingServletLengthTypeStreamWrite.java similarity index 93% rename from jetty-servlets/src/test/java/org/eclipse/jetty/servlets/TestServletLengthTypeStreamWrite.java rename to jetty-servlets/src/test/java/org/eclipse/jetty/servlets/BlockingServletLengthTypeStreamWrite.java index 50fdc496486..132d6494eba 100644 --- a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/TestServletLengthTypeStreamWrite.java +++ b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/BlockingServletLengthTypeStreamWrite.java @@ -42,12 +42,12 @@ import org.eclipse.jetty.server.handler.gzip.GzipHandler; * @see http://bugs.eclipse.org/354014 */ @SuppressWarnings("serial") -public class TestServletLengthTypeStreamWrite extends TestDirContentServlet +public class BlockingServletLengthTypeStreamWrite extends AbstractFileContentServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { - String fileName = request.getServletPath(); + String fileName = request.getPathInfo(); byte[] dataBytes = loadContentFileBytes(fileName); response.setContentLength(dataBytes.length); diff --git a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/TestServletStreamLengthTypeWrite.java b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/BlockingServletStreamLengthTypeWrite.java similarity index 93% rename from jetty-servlets/src/test/java/org/eclipse/jetty/servlets/TestServletStreamLengthTypeWrite.java rename to jetty-servlets/src/test/java/org/eclipse/jetty/servlets/BlockingServletStreamLengthTypeWrite.java index 5c4896bed43..94fdf891b99 100644 --- a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/TestServletStreamLengthTypeWrite.java +++ b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/BlockingServletStreamLengthTypeWrite.java @@ -42,12 +42,12 @@ import org.eclipse.jetty.server.handler.gzip.GzipHandler; * @see http://bugs.eclipse.org/354014 */ @SuppressWarnings("serial") -public class TestServletStreamLengthTypeWrite extends TestDirContentServlet +public class BlockingServletStreamLengthTypeWrite extends AbstractFileContentServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { - String fileName = request.getServletPath(); + String fileName = request.getPathInfo(); byte[] dataBytes = loadContentFileBytes(fileName); ServletOutputStream out = response.getOutputStream(); diff --git a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/TestServletStreamLengthTypeWriteWithFlush.java b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/BlockingServletStreamLengthTypeWriteWithFlush.java similarity index 94% rename from jetty-servlets/src/test/java/org/eclipse/jetty/servlets/TestServletStreamLengthTypeWriteWithFlush.java rename to jetty-servlets/src/test/java/org/eclipse/jetty/servlets/BlockingServletStreamLengthTypeWriteWithFlush.java index a823cc32bd9..7a4830efd33 100644 --- a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/TestServletStreamLengthTypeWriteWithFlush.java +++ b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/BlockingServletStreamLengthTypeWriteWithFlush.java @@ -42,12 +42,12 @@ import org.eclipse.jetty.server.handler.gzip.GzipHandler; * @see http://bugs.eclipse.org/354014 */ @SuppressWarnings("serial") -public class TestServletStreamLengthTypeWriteWithFlush extends TestDirContentServlet +public class BlockingServletStreamLengthTypeWriteWithFlush extends AbstractFileContentServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { - String fileName = request.getServletPath(); + String fileName = request.getPathInfo(); byte[] dataBytes = loadContentFileBytes(fileName); ServletOutputStream out = response.getOutputStream(); diff --git a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/TestServletStreamTypeLengthWrite.java b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/BlockingServletStreamTypeLengthWrite.java similarity index 93% rename from jetty-servlets/src/test/java/org/eclipse/jetty/servlets/TestServletStreamTypeLengthWrite.java rename to jetty-servlets/src/test/java/org/eclipse/jetty/servlets/BlockingServletStreamTypeLengthWrite.java index e1d9195aa26..e40a938b930 100644 --- a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/TestServletStreamTypeLengthWrite.java +++ b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/BlockingServletStreamTypeLengthWrite.java @@ -42,12 +42,12 @@ import org.eclipse.jetty.server.handler.gzip.GzipHandler; * @see http://bugs.eclipse.org/354014 */ @SuppressWarnings("serial") -public class TestServletStreamTypeLengthWrite extends TestDirContentServlet +public class BlockingServletStreamTypeLengthWrite extends AbstractFileContentServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { - String fileName = request.getServletPath(); + String fileName = request.getPathInfo(); byte[] dataBytes = loadContentFileBytes(fileName); ServletOutputStream out = response.getOutputStream(); diff --git a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/TestServletTypeLengthStreamWrite.java b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/BlockingServletTypeLengthStreamWrite.java similarity index 93% rename from jetty-servlets/src/test/java/org/eclipse/jetty/servlets/TestServletTypeLengthStreamWrite.java rename to jetty-servlets/src/test/java/org/eclipse/jetty/servlets/BlockingServletTypeLengthStreamWrite.java index dc3b7bd769e..cd0e6550221 100644 --- a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/TestServletTypeLengthStreamWrite.java +++ b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/BlockingServletTypeLengthStreamWrite.java @@ -42,12 +42,12 @@ import org.eclipse.jetty.server.handler.gzip.GzipHandler; * @see http://bugs.eclipse.org/354014 */ @SuppressWarnings("serial") -public class TestServletTypeLengthStreamWrite extends TestDirContentServlet +public class BlockingServletTypeLengthStreamWrite extends AbstractFileContentServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { - String fileName = request.getServletPath(); + String fileName = request.getPathInfo(); byte[] dataBytes = loadContentFileBytes(fileName); if (fileName.endsWith("txt")) diff --git a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/TestServletTypeStreamLengthWrite.java b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/BlockingServletTypeStreamLengthWrite.java similarity index 93% rename from jetty-servlets/src/test/java/org/eclipse/jetty/servlets/TestServletTypeStreamLengthWrite.java rename to jetty-servlets/src/test/java/org/eclipse/jetty/servlets/BlockingServletTypeStreamLengthWrite.java index 2f255c88526..61b146f6436 100644 --- a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/TestServletTypeStreamLengthWrite.java +++ b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/BlockingServletTypeStreamLengthWrite.java @@ -42,12 +42,12 @@ import org.eclipse.jetty.server.handler.gzip.GzipHandler; * @see http://bugs.eclipse.org/354014 */ @SuppressWarnings("serial") -public class TestServletTypeStreamLengthWrite extends TestDirContentServlet +public class BlockingServletTypeStreamLengthWrite extends AbstractFileContentServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { - String fileName = request.getServletPath(); + String fileName = request.getPathInfo(); byte[] dataBytes = loadContentFileBytes(fileName); if (fileName.endsWith("txt")) diff --git a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/GzipContentLengthTest.java b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/GzipContentLengthTest.java index fda091b8e7c..3b7134a56ae 100644 --- a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/GzipContentLengthTest.java +++ b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/GzipContentLengthTest.java @@ -18,20 +18,27 @@ package org.eclipse.jetty.servlets; -import java.io.File; +import java.nio.ByteBuffer; +import java.nio.file.Path; import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeUnit; import java.util.stream.Stream; +import javax.servlet.Servlet; import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.jetty.http.HttpVersion; import org.eclipse.jetty.http.tools.HttpTester; -import org.eclipse.jetty.server.HttpConfiguration; +import org.eclipse.jetty.server.LocalConnector; +import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.handler.gzip.GzipHandler; -import org.eclipse.jetty.servlets.GzipTester.ContentMetadata; -import org.eclipse.jetty.toolchain.test.jupiter.WorkDir; -import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension; -import org.junit.jupiter.api.extension.ExtendWith; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.toolchain.test.FS; +import org.eclipse.jetty.toolchain.test.Sha1Sum; +import org.eclipse.jetty.util.component.LifeCycle; +import org.eclipse.jetty.util.resource.PathResource; +import org.hamcrest.Matcher; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; @@ -42,317 +49,178 @@ import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; /** - * Test the GzipHandler support for Content-Length setting variations. - * - * @see http://bugs.eclipse.org/354014 + * Test the {@code GzipHandler} support for the various ways that an App can set {@code Content-Length}. */ -@ExtendWith(WorkDirExtension.class) -public class GzipContentLengthTest +public class GzipContentLengthTest extends AbstractGzipTest { - public WorkDir workDir; - - private static final HttpConfiguration defaultHttp = new HttpConfiguration(); - private static final int LARGE = defaultHttp.getOutputBufferSize() * 8; - private static final int MEDIUM = defaultHttp.getOutputBufferSize(); - private static final int SMALL = defaultHttp.getOutputBufferSize() / 4; - private static final int TINY = GzipHandler.DEFAULT_MIN_GZIP_SIZE / 2; - private static final boolean EXPECT_COMPRESSED = true; + enum GzipMode + { + INTERNAL, EXTERNAL + } public static Stream scenarios() { - List ret = new ArrayList<>(); + // The list of servlets that implement various content sending behaviors + // some behaviors are more sane then others, but they are all real world scenarios + // that we have seen or had issues reported against Jetty. + List> servlets = new ArrayList<>(); - ret.add(new Scenario(0, "empty.txt", !EXPECT_COMPRESSED)); - ret.add(new Scenario(TINY, "file-tiny.txt", !EXPECT_COMPRESSED)); - ret.add(new Scenario(SMALL, "file-small.txt", EXPECT_COMPRESSED)); - ret.add(new Scenario(SMALL, "file-small.mp3", !EXPECT_COMPRESSED)); - ret.add(new Scenario(MEDIUM, "file-med.txt", EXPECT_COMPRESSED)); - ret.add(new Scenario(MEDIUM, "file-medium.mp3", !EXPECT_COMPRESSED)); - ret.add(new Scenario(LARGE, "file-large.txt", EXPECT_COMPRESSED)); - ret.add(new Scenario(LARGE, "file-large.mp3", !EXPECT_COMPRESSED)); + // AsyncContext create -> timeout -> onTimeout -> write-response -> complete + servlets.add(AsyncTimeoutCompleteWrite.Default.class); + servlets.add(AsyncTimeoutCompleteWrite.Passed.class); + // AsyncContext create -> timeout -> onTimeout -> dispatch -> write-response + servlets.add(AsyncTimeoutDispatchWrite.Default.class); + servlets.add(AsyncTimeoutDispatchWrite.Passed.class); + // AsyncContext create -> no-timeout -> scheduler.schedule -> dispatch -> write-response + servlets.add(AsyncScheduledDispatchWrite.Default.class); + servlets.add(AsyncScheduledDispatchWrite.Passed.class); - return ret.stream().map(Arguments::of); - } + // HttpOutput usage scenario from http://bugs.eclipse.org/450873 + // 1. getOutputStream() + // 2. setHeader(content-type) + // 3. setHeader(content-length) + // 4. (unwrapped) HttpOutput.write(ByteBuffer) + servlets.add(HttpOutputWriteFileContentServlet.class); - private void testWithGzip(Scenario scenario, Class contentServlet) throws Exception - { - GzipTester tester = new GzipTester(workDir.getPath(), GzipHandler.GZIP); + // The following blocking scenarios are from http://bugs.eclipse.org/354014 + // Blocking + // 1. setHeader(content-length) + // 2. getOutputStream() + // 3. setHeader(content-type) + // 4. outputStream.write() + servlets.add(BlockingServletLengthStreamTypeWrite.class); + // Blocking + // 1. setHeader(content-length) + // 2. setHeader(content-type) + // 3. getOutputStream() + // 4. outputStream.write() + servlets.add(BlockingServletLengthTypeStreamWrite.class); + // Blocking + // 1. getOutputStream() + // 2. setHeader(content-length) + // 3. setHeader(content-type) + // 4. outputStream.write() + servlets.add(BlockingServletStreamLengthTypeWrite.class); + // Blocking + // 1. getOutputStream() + // 2. setHeader(content-length) + // 3. setHeader(content-type) + // 4. outputStream.write() (with frequent response flush) + servlets.add(BlockingServletStreamLengthTypeWriteWithFlush.class); + // Blocking + // 1. getOutputStream() + // 2. setHeader(content-type) + // 3. setHeader(content-length) + // 4. outputStream.write() + servlets.add(BlockingServletStreamTypeLengthWrite.class); + // Blocking + // 1. setHeader(content-type) + // 2. setHeader(content-length) + // 3. getOutputStream() + // 4. outputStream.write() + servlets.add(BlockingServletTypeLengthStreamWrite.class); + // Blocking + // 1. setHeader(content-type) + // 2. getOutputStream() + // 3. setHeader(content-length) + // 4. outputStream.write() + servlets.add(BlockingServletTypeStreamLengthWrite.class); - // Add AsyncGzip Configuration - tester.getGzipHandler().setIncludedMimeTypes("text/plain"); - tester.getGzipHandler().setIncludedPaths("*.txt", "*.mp3"); + List scenarios = new ArrayList<>(); - // Add content servlet - tester.setContentServlet(contentServlet); - - try + for (Class servlet : servlets) { - String testFilename = String.format("%s-%s", contentServlet.getSimpleName(), scenario.fileName); - File testFile = tester.prepareServerFile(testFilename, scenario.fileSize); - - tester.start(); - - HttpTester.Response response = tester.executeRequest("GET", "/context/" + testFile.getName(), 5, TimeUnit.SECONDS); - - if (response.getStatus() != 200) - System.err.println("DANG!!!! " + response); - - assertThat("Response status", response.getStatus(), is(HttpStatus.OK_200)); - - if (scenario.expectCompressed) + for (GzipMode gzipMode : GzipMode.values()) { - // Must be gzip compressed - assertThat("Content-Encoding", response.get("Content-Encoding"), containsString(GzipHandler.GZIP)); + // Not compressible (not large enough) + scenarios.add(Arguments.of(gzipMode, servlet, 0, "empty.txt", false)); + scenarios.add(Arguments.of(gzipMode, servlet, GzipHandler.DEFAULT_MIN_GZIP_SIZE / 2, "file-tiny.txt", false)); + + // Compressible. + scenarios.add(Arguments.of(gzipMode, servlet, DEFAULT_OUTPUT_BUFFER_SIZE / 2, "file-small.txt", true)); + scenarios.add(Arguments.of(gzipMode, servlet, DEFAULT_OUTPUT_BUFFER_SIZE, "file-medium.txt", true)); + scenarios.add(Arguments.of(gzipMode, servlet, DEFAULT_OUTPUT_BUFFER_SIZE * 4, "file-large.txt", true)); + + // Not compressible (not a matching Content-Type) + scenarios.add(Arguments.of(gzipMode, servlet, DEFAULT_OUTPUT_BUFFER_SIZE / 2, "file-small.mp3", false)); + scenarios.add(Arguments.of(gzipMode, servlet, DEFAULT_OUTPUT_BUFFER_SIZE, "file-medium.mp3", false)); + scenarios.add(Arguments.of(gzipMode, servlet, DEFAULT_OUTPUT_BUFFER_SIZE * 4, "file-large.mp3", false)); } - else - { - assertThat("Content-Encoding", response.get("Content-Encoding"), not(containsString(GzipHandler.GZIP))); - } - - // Uncompressed content Size - ContentMetadata content = tester.getResponseMetadata(response); - assertThat("(Uncompressed) Content Length", content.size, is((long)scenario.fileSize)); } - finally + + return scenarios.stream(); + } + + private Server server; + + @AfterEach + public void stopServer() + { + LifeCycle.stop(server); + } + + @ParameterizedTest + @MethodSource("scenarios") + public void executeScenario(GzipMode gzipMode, Class contentServlet, int fileSize, String fileName, boolean compressible) throws Exception + { + server = new Server(); + LocalConnector localConnector = new LocalConnector(server); + server.addConnector(localConnector); + + Path contextDir = workDir.resolve("context"); + FS.ensureDirExists(contextDir); + + ServletContextHandler servletContextHandler = new ServletContextHandler(); + servletContextHandler.setContextPath("/context"); + servletContextHandler.setBaseResource(new PathResource(contextDir)); + servletContextHandler.addServlet(contentServlet, "/*"); + GzipHandler gzipHandler = new GzipHandler(); + + switch (gzipMode) { - tester.stop(); + case INTERNAL: + servletContextHandler.insertHandler(gzipHandler); + server.setHandler(servletContextHandler); + break; + case EXTERNAL: + gzipHandler.setHandler(servletContextHandler); + server.setHandler(gzipHandler); + break; } - } - /** - * Test with content servlet that does: - * AsyncContext create -> timeout -> onTimeout -> write-response -> complete - * - * @throws Exception on test failure - */ - @ParameterizedTest - @MethodSource("scenarios") - public void testAsyncTimeoutCompleteWriteDefault(Scenario scenario) throws Exception - { - testWithGzip(scenario, AsyncTimeoutCompleteWrite.Default.class); - } + Path file = createFile(contextDir, fileName, fileSize); + String expectedSha1Sum = Sha1Sum.calculate(file); - /** - * Test with content servlet that does: - * AsyncContext create -> timeout -> onTimeout -> write-response -> complete - * - * @throws Exception on test failure - */ - @ParameterizedTest - @MethodSource("scenarios") - public void testAsyncTimeoutCompleteWritePassed(Scenario scenario) throws Exception - { - testWithGzip(scenario, AsyncTimeoutCompleteWrite.Passed.class); - } + server.start(); - /** - * Test with content servlet that does: - * AsyncContext create -> timeout -> onTimeout -> dispatch -> write-response - * - * @throws Exception on test failure - */ - @ParameterizedTest - @MethodSource("scenarios") - public void testAsyncTimeoutDispatchWriteDefault(Scenario scenario) throws Exception - { - testWithGzip(scenario, AsyncTimeoutDispatchWrite.Default.class); - } + // Setup request + HttpTester.Request request = HttpTester.newRequest(); + request.setMethod("GET"); + request.setVersion(HttpVersion.HTTP_1_1); + request.setHeader("Host", "tester"); + request.setHeader("Connection", "close"); + request.setHeader("Accept-Encoding", "gzip"); + request.setURI("/context/" + file.getFileName().toString()); - /** - * Test with content servlet that does: - * AsyncContext create -> timeout -> onTimeout -> dispatch -> write-response - * - * @throws Exception on test failure - */ - @ParameterizedTest - @MethodSource("scenarios") - public void testAsyncTimeoutDispatchWritePassed(Scenario scenario) throws Exception - { - testWithGzip(scenario, AsyncTimeoutDispatchWrite.Passed.class); - } + // Issue request + ByteBuffer rawResponse = localConnector.getResponse(request.generate(), 5, TimeUnit.SECONDS); - /** - * Test with content servlet that does: - * AsyncContext create -> no-timeout -> scheduler.schedule -> dispatch -> write-response - * - * @throws Exception on test failure - */ - @ParameterizedTest - @MethodSource("scenarios") - public void testAsyncScheduledDispatchWriteDefault(Scenario scenario) throws Exception - { - testWithGzip(scenario, AsyncScheduledDispatchWrite.Default.class); - } + // Parse response + HttpTester.Response response = HttpTester.parseResponse(rawResponse); - /** - * Test with content servlet that does: - * AsyncContext create -> no-timeout -> scheduler.schedule -> dispatch -> write-response - * - * @throws Exception on test failure - */ - @ParameterizedTest - @MethodSource("scenarios") - public void testAsyncScheduledDispatchWritePassed(Scenario scenario) throws Exception - { - testWithGzip(scenario, AsyncScheduledDispatchWrite.Passed.class); - } + assertThat("Response status", response.getStatus(), is(HttpStatus.OK_200)); - /** - * Test with content servlet that does: - * 1) setHeader(content-length) - * 2) getOutputStream() - * 3) setHeader(content-type) - * 4) outputStream.write() - * - * @throws Exception on test failure - * @see Eclipse Bug 354014 - */ - @ParameterizedTest - @MethodSource("scenarios") - public void testServletLengthStreamTypeWrite(Scenario scenario) throws Exception - { - testWithGzip(scenario, TestServletLengthStreamTypeWrite.class); - } - - /** - * Test with content servlet that does: - * 1) setHeader(content-length) - * 2) setHeader(content-type) - * 3) getOutputStream() - * 4) outputStream.write() - * - * @throws Exception on test failure - * @see Eclipse Bug 354014 - */ - @ParameterizedTest - @MethodSource("scenarios") - public void testServletLengthTypeStreamWrite(Scenario scenario) throws Exception - { - testWithGzip(scenario, TestServletLengthTypeStreamWrite.class); - } - - /** - * Test with content servlet that does: - * 1) getOutputStream() - * 2) setHeader(content-length) - * 3) setHeader(content-type) - * 4) outputStream.write() - * - * @throws Exception on test failure - * @see Eclipse Bug 354014 - */ - @ParameterizedTest - @MethodSource("scenarios") - public void testServletStreamLengthTypeWrite(Scenario scenario) throws Exception - { - testWithGzip(scenario, TestServletStreamLengthTypeWrite.class); - } - - /** - * Test with content servlet that does: - * 1) getOutputStream() - * 2) setHeader(content-length) - * 3) setHeader(content-type) - * 4) outputStream.write() (with frequent response flush) - * - * @throws Exception on test failure - * @see Eclipse Bug 354014 - */ - @ParameterizedTest - @MethodSource("scenarios") - public void testServletStreamLengthTypeWriteWithFlush(Scenario scenario) throws Exception - { - testWithGzip(scenario, TestServletStreamLengthTypeWriteWithFlush.class); - } - - /** - * Test with content servlet that does: - * 1) getOutputStream() - * 2) setHeader(content-type) - * 3) setHeader(content-length) - * 4) outputStream.write() - * - * @throws Exception on test failure - * @see Eclipse Bug 354014 - */ - @ParameterizedTest - @MethodSource("scenarios") - public void testServletStreamTypeLengthWrite(Scenario scenario) throws Exception - { - testWithGzip(scenario, TestServletStreamTypeLengthWrite.class); - } - - /** - * Test with content servlet that does: - * 1) setHeader(content-type) - * 2) setHeader(content-length) - * 3) getOutputStream() - * 4) outputStream.write() - * - * @throws Exception on test failure - * @see Eclipse Bug 354014 - */ - @ParameterizedTest - @MethodSource("scenarios") - public void testServletTypeLengthStreamWrite(Scenario scenario) throws Exception - { - testWithGzip(scenario, TestServletTypeLengthStreamWrite.class); - } - - /** - * Test with content servlet that does: - * 1) setHeader(content-type) - * 2) getOutputStream() - * 3) setHeader(content-length) - * 4) outputStream.write() - * - * @throws Exception on test failure - * @see http://bugs.eclipse.org/354014 - */ - @ParameterizedTest - @MethodSource("scenarios") - public void testServletTypeStreamLengthWrite(Scenario scenario) throws Exception - { - testWithGzip(scenario, TestServletTypeStreamLengthWrite.class); - } - - /** - * Test with content servlet that does: - * 2) getOutputStream() - * 1) setHeader(content-type) - * 3) setHeader(content-length) - * 4) (unwrapped) HttpOutput.write(ByteBuffer) - * - * This is done to demonstrate a bug with using HttpOutput.write() - * while also using GzipFilter - * - * @throws Exception on test failure - * @see Eclipse Bug 450873 - */ - @ParameterizedTest - @MethodSource("scenarios") - public void testHttpOutputWrite(Scenario scenario) throws Exception - { - testWithGzip(scenario, TestServletBufferTypeLengthWrite.class); - } - - public static class Scenario - { - final int fileSize; - final String fileName; - final boolean expectCompressed; - - public Scenario(int fileSize, String fileName, boolean expectCompressed) + // Response Content-Encoding check + Matcher contentEncodingMatcher = containsString(GzipHandler.GZIP); + if (!compressible) { - this.fileSize = fileSize; - this.fileName = fileName; - this.expectCompressed = expectCompressed; + contentEncodingMatcher = not(contentEncodingMatcher); } + assertThat("Content-Encoding", response.get("Content-Encoding"), contentEncodingMatcher); - @Override - public String toString() - { - return String.format("%s [%,d bytes, compressed=%b]", fileName, fileSize, expectCompressed); - } + // Response Content checks + UncompressedMetadata metadata = parseResponseContent(response); + assertThat("(Uncompressed) Content Length", metadata.uncompressedSize, is(fileSize)); + assertThat("(Uncompressed) Content Hash", metadata.uncompressedSha1Sum, is(expectedSha1Sum)); } } diff --git a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/GzipDefaultNoRecompressTest.java b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/GzipDefaultNoRecompressTest.java deleted file mode 100644 index 28226f3d69a..00000000000 --- a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/GzipDefaultNoRecompressTest.java +++ /dev/null @@ -1,100 +0,0 @@ -// -// ======================================================================== -// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others. -// -// This program and the accompanying materials are made available under -// the terms of the Eclipse Public License 2.0 which is available at -// https://www.eclipse.org/legal/epl-2.0 -// -// This Source Code may also be made available under the following -// Secondary Licenses when the conditions for such availability set -// forth in the Eclipse Public License, v. 2.0 are satisfied: -// the Apache License v2.0 which is available at -// https://www.apache.org/licenses/LICENSE-2.0 -// -// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 -// ======================================================================== -// - -package org.eclipse.jetty.servlets; - -import java.io.File; -import java.io.IOException; -import java.util.Arrays; -import java.util.stream.Stream; - -import org.eclipse.jetty.server.handler.gzip.GzipHandler; -import org.eclipse.jetty.servlet.DefaultServlet; -import org.eclipse.jetty.toolchain.test.IO; -import org.eclipse.jetty.toolchain.test.MavenTestingUtils; -import org.eclipse.jetty.toolchain.test.jupiter.WorkDir; -import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension; -import org.junit.jupiter.api.extension.ExtendWith; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; - -/** - * Tests {@link GzipHandler} in combination with {@link DefaultServlet} for ability to configure {@link GzipHandler} to - * ignore recompress situations from upstream. - */ -@ExtendWith(WorkDirExtension.class) -public class GzipDefaultNoRecompressTest -{ - public static Stream data() - { - return Arrays.asList(new Object[][] - { - // Some already compressed files - {"test_quotes.gz", "application/gzip", GzipHandler.GZIP}, - {"test_quotes.br", "application/brotli", GzipHandler.GZIP}, - {"test_quotes.bz2", "application/bzip2", GzipHandler.GZIP}, - {"test_quotes.zip", "application/zip", GzipHandler.GZIP}, - {"test_quotes.rar", "application/x-rar-compressed", GzipHandler.GZIP}, - // Some images (common first) - {"jetty_logo.png", "image/png", GzipHandler.GZIP}, - {"jetty_logo.gif", "image/gif", GzipHandler.GZIP}, - {"jetty_logo.jpeg", "image/jpeg", GzipHandler.GZIP}, - {"jetty_logo.jpg", "image/jpeg", GzipHandler.GZIP}, - // Lesser encountered images (usually found being requested from non-browser clients) - {"jetty_logo.bmp", "image/bmp", GzipHandler.GZIP}, - {"jetty_logo.tif", "image/tiff", GzipHandler.GZIP}, - {"jetty_logo.tiff", "image/tiff", GzipHandler.GZIP}, - {"jetty_logo.xcf", "image/xcf", GzipHandler.GZIP}, - {"jetty_logo.jp2", "image/jpeg2000", GzipHandler.GZIP}, - //qvalue disables compression - {"test_quotes.txt", "text/plain", GzipHandler.GZIP + ";q=0"}, - {"test_quotes.txt", "text/plain", GzipHandler.GZIP + "; q = 0 "} - }).stream().map(Arguments::of); - } - - public WorkDir testingdir; - - @ParameterizedTest - @MethodSource("data") - public void testNotGzipHandleredDefaultAlreadyCompressed(String alreadyCompressedFilename, String expectedContentType, String compressionType) throws Exception - { - GzipTester tester = new GzipTester(testingdir.getEmptyPathDir(), compressionType); - - copyTestFileToServer(alreadyCompressedFilename); - - tester.setContentServlet(TestStaticMimeTypeServlet.class); - - try - { - tester.start(); - tester.assertIsResponseNotGziped(alreadyCompressedFilename, alreadyCompressedFilename + ".sha1", expectedContentType); - } - finally - { - tester.stop(); - } - } - - private void copyTestFileToServer(String testFilename) throws IOException - { - File testFile = MavenTestingUtils.getTestResourceFile(testFilename); - File outFile = testingdir.getPathFile(testFilename).toFile(); - IO.copy(testFile, outFile); - } -} diff --git a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/GzipDefaultServletDeferredContentTypeTest.java b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/GzipDefaultServletDeferredContentTypeTest.java new file mode 100644 index 00000000000..ad151c0654e --- /dev/null +++ b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/GzipDefaultServletDeferredContentTypeTest.java @@ -0,0 +1,138 @@ +// +// ======================================================================== +// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under +// the terms of the Eclipse Public License 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0 +// +// This Source Code may also be made available under the following +// Secondary Licenses when the conditions for such availability set +// forth in the Eclipse Public License, v. 2.0 are satisfied: +// the Apache License v2.0 which is available at +// https://www.apache.org/licenses/LICENSE-2.0 +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +package org.eclipse.jetty.servlets; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.file.Path; +import java.util.concurrent.TimeUnit; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.jetty.http.HttpVersion; +import org.eclipse.jetty.http.tools.HttpTester; +import org.eclipse.jetty.server.LocalConnector; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.handler.gzip.GzipHandler; +import org.eclipse.jetty.servlet.DefaultServlet; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.servlet.ServletHolder; +import org.eclipse.jetty.toolchain.test.FS; +import org.eclipse.jetty.toolchain.test.Sha1Sum; +import org.eclipse.jetty.util.component.LifeCycle; +import org.eclipse.jetty.util.resource.PathResource; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.emptyOrNullString; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; + +/** + * GzipHandler setting of headers when reset and/or not compressed. + * + * The GzipHandler now sets deferred headers (content-length and etag) when it decides not to commit. + * Also does not allow a reset after a decision to commit + * + * Originally from http://bugs.eclipse.org/408909 + */ +public class GzipDefaultServletDeferredContentTypeTest extends AbstractGzipTest +{ + private Server server; + + @AfterEach + public void stopServer() + { + LifeCycle.stop(server); + } + + @Test + public void testIsNotGzipCompressedByDeferredContentType() throws Exception + { + server = new Server(); + LocalConnector localConnector = new LocalConnector(server); + server.addConnector(localConnector); + + Path contextDir = workDir.resolve("context"); + FS.ensureDirExists(contextDir); + + ServletContextHandler servletContextHandler = new ServletContextHandler(); + servletContextHandler.setContextPath("/context"); + servletContextHandler.setBaseResource(new PathResource(contextDir)); + ServletHolder holder = new ServletHolder("default", new DefaultServlet() + { + @Override + public void service(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException + { + String uri = req.getRequestURI(); + if (uri.endsWith(".deferred")) + { + // System.err.println("type for "+uri.substring(0,uri.length()-9)+" is "+getServletContext().getMimeType(uri.substring(0,uri.length()-9))); + resp.setContentType(getServletContext().getMimeType(uri.substring(0, uri.length() - 9))); + } + + doGet(req, resp); + } + }); + servletContextHandler.addServlet(holder, "/"); + + GzipHandler gzipHandler = new GzipHandler(); + gzipHandler.setHandler(servletContextHandler); + server.setHandler(gzipHandler); + + int fileSize = DEFAULT_OUTPUT_BUFFER_SIZE * 4; + + Path file = createFile(contextDir, "file.mp3.deferred", fileSize); + String expectedSha1Sum = Sha1Sum.calculate(file); + + server.start(); + + // Setup request + HttpTester.Request request = HttpTester.newRequest(); + request.setMethod("GET"); + request.setVersion(HttpVersion.HTTP_1_1); + request.setHeader("Host", "tester"); + request.setHeader("Connection", "close"); + request.setHeader("Accept-Encoding", "gzip"); + request.setURI("/context/file.mp3.deferred"); + + // Issue request + ByteBuffer rawResponse = localConnector.getResponse(request.generate(), 5, TimeUnit.SECONDS); + + // Parse response + HttpTester.Response response = HttpTester.parseResponse(rawResponse); + + assertThat("Response status", response.getStatus(), is(HttpStatus.OK_200)); + + // Response Content-Encoding check + assertThat("Response[Content-Encoding]", response.get("Content-Encoding"), not(containsString("gzip"))); + + // Response Vary check + assertThat("Response[Vary]", response.get("Vary"), is(emptyOrNullString())); + + // Response Content checks + UncompressedMetadata metadata = parseResponseContent(response); + assertThat("(Uncompressed) Content Length", metadata.uncompressedSize, is(fileSize)); + assertThat("(Uncompressed) Content Hash", metadata.uncompressedSha1Sum, is(expectedSha1Sum)); + } +} diff --git a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/GzipDefaultServletTest.java b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/GzipDefaultServletTest.java new file mode 100644 index 00000000000..958058db75c --- /dev/null +++ b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/GzipDefaultServletTest.java @@ -0,0 +1,1182 @@ +// +// ======================================================================== +// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under +// the terms of the Eclipse Public License 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0 +// +// This Source Code may also be made available under the following +// Secondary Licenses when the conditions for such availability set +// forth in the Eclipse Public License, v. 2.0 are satisfied: +// the Apache License v2.0 which is available at +// https://www.apache.org/licenses/LICENSE-2.0 +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +package org.eclipse.jetty.servlets; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.concurrent.TimeUnit; +import java.util.stream.Stream; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.eclipse.jetty.http.CompressedContentFormat; +import org.eclipse.jetty.http.DateGenerator; +import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.jetty.http.HttpVersion; +import org.eclipse.jetty.http.tools.HttpTester; +import org.eclipse.jetty.server.LocalConnector; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.handler.gzip.GzipHandler; +import org.eclipse.jetty.servlet.DefaultServlet; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.servlet.ServletHolder; +import org.eclipse.jetty.toolchain.test.FS; +import org.eclipse.jetty.toolchain.test.MavenTestingUtils; +import org.eclipse.jetty.toolchain.test.Sha1Sum; +import org.eclipse.jetty.util.IO; +import org.eclipse.jetty.util.component.LifeCycle; +import org.eclipse.jetty.util.resource.PathResource; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.nullValue; +import static org.hamcrest.Matchers.startsWith; + +/** + * Test the GzipHandler support when working with the {@link DefaultServlet}. + */ +public class GzipDefaultServletTest extends AbstractGzipTest +{ + private Server server; + + @AfterEach + public void stopServer() + { + LifeCycle.stop(server); + } + + @ParameterizedTest + @ValueSource(strings = {"POST", "WIBBLE", "GET", "HEAD"}) + public void testIsGzipByMethod(String method) throws Exception + { + GzipHandler gzipHandler = new GzipHandler(); + gzipHandler.setIncludedMethods("POST", "WIBBLE", "GET", "HEAD"); + + server = new Server(); + LocalConnector localConnector = new LocalConnector(server); + server.addConnector(localConnector); + + Path contextDir = workDir.resolve("context"); + FS.ensureDirExists(contextDir); + + ServletContextHandler servletContextHandler = new ServletContextHandler(); + servletContextHandler.setContextPath("/context"); + servletContextHandler.setBaseResource(new PathResource(contextDir)); + ServletHolder holder = new ServletHolder("default", WibbleDefaultServlet.class); + holder.setInitParameter("etags", "true"); + servletContextHandler.addServlet(holder, "/"); + servletContextHandler.insertHandler(gzipHandler); + + server.setHandler(servletContextHandler); + + // Prepare Server File + int fileSize = DEFAULT_OUTPUT_BUFFER_SIZE * 8; + + Path file = createFile(contextDir, "file.txt", fileSize); + String expectedSha1Sum = Sha1Sum.calculate(file); + + server.start(); + + // Setup request + HttpTester.Request request = HttpTester.newRequest(); + request.setMethod(method); // The point of this test + request.setVersion(HttpVersion.HTTP_1_1); + request.setHeader("Host", "tester"); + request.setHeader("Connection", "close"); + request.setHeader("Accept-Encoding", "gzip"); + request.setURI("/context/file.txt"); + + // Issue request + ByteBuffer rawResponse = localConnector.getResponse(request.generate(), 5, TimeUnit.SECONDS); + + // Parse response + HttpTester.Response response = HttpTester.parseResponse(rawResponse); + + assertThat("Response status", response.getStatus(), is(HttpStatus.OK_200)); + + // Response Content-Encoding check + assertThat("Response[Content-Encoding]", response.get("Content-Encoding"), containsString("gzip")); + assertThat("Response[ETag]", response.get("ETag"), startsWith("W/")); + assertThat("Response[ETag]", response.get("ETag"), containsString(CompressedContentFormat.GZIP._etag)); + // A HEAD request should have similar headers, but no body + if (method.equals("HEAD")) + { + assertThat("Response[Content-Length]", response.get("Content-Length"), is(not(nullValue()))); + } + else + { + assertThat("Response[Content-Length]", response.get("Content-Length"), is(nullValue())); + // Response Content checks + UncompressedMetadata metadata = parseResponseContent(response); + assertThat("(Uncompressed) Content Length", metadata.uncompressedSize, is(fileSize)); + assertThat("(Uncompressed) Content Hash", metadata.uncompressedSha1Sum, is(expectedSha1Sum)); + } + } + + public static class WibbleDefaultServlet extends DefaultServlet + { + @Override + protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException + { + switch (req.getMethod()) + { + case "WIBBLE": + // Disregard the method given, use GET instead. + doGet(req, resp); + return; + default: + super.service(req, resp); + } + } + } + + @Test + public void testIsGzipCompressedEmpty() throws Exception + { + GzipHandler gzipHandler = new GzipHandler(); + gzipHandler.addIncludedMimeTypes("text/plain"); + + server = new Server(); + LocalConnector localConnector = new LocalConnector(server); + server.addConnector(localConnector); + + Path contextDir = workDir.resolve("context"); + FS.ensureDirExists(contextDir); + + ServletContextHandler servletContextHandler = new ServletContextHandler(); + servletContextHandler.setContextPath("/context"); + servletContextHandler.setBaseResource(new PathResource(contextDir)); + ServletHolder holder = new ServletHolder("default", DefaultServlet.class); + holder.setInitParameter("etags", "true"); + servletContextHandler.addServlet(holder, "/"); + servletContextHandler.insertHandler(gzipHandler); + + server.setHandler(servletContextHandler); + + // Prepare Server File + int fileSize = 0; + createFile(contextDir, "file.txt", fileSize); + + server.start(); + + // Setup request + HttpTester.Request request = HttpTester.newRequest(); + request.setMethod("GET"); + request.setVersion(HttpVersion.HTTP_1_1); + request.setHeader("Host", "tester"); + request.setHeader("Connection", "close"); + request.setHeader("Accept-Encoding", "gzip"); + request.setURI("/context/file.txt"); + + // Issue request + ByteBuffer rawResponse = localConnector.getResponse(request.generate(), 5, TimeUnit.SECONDS); + + // Parse response + HttpTester.Response response = HttpTester.parseResponse(rawResponse); + + assertThat("Response status", response.getStatus(), is(HttpStatus.OK_200)); + + // Response Content-Encoding check + assertThat("Response[Content-Encoding]", response.get("Content-Encoding"), not(containsString("gzip"))); + + // Response Content checks + UncompressedMetadata metadata = parseResponseContent(response); + assertThat("(Uncompressed) Content Length", metadata.uncompressedSize, is(0)); + } + + public static Stream compressibleSizes() + { + return Stream.of( + DEFAULT_OUTPUT_BUFFER_SIZE / 4, + DEFAULT_OUTPUT_BUFFER_SIZE, + DEFAULT_OUTPUT_BUFFER_SIZE * 4); + } + + @ParameterizedTest + @MethodSource("compressibleSizes") + public void testIsGzipCompressed(int fileSize) throws Exception + { + GzipHandler gzipHandler = new GzipHandler(); + gzipHandler.addIncludedMimeTypes("text/plain"); + + server = new Server(); + LocalConnector localConnector = new LocalConnector(server); + server.addConnector(localConnector); + + Path contextDir = workDir.resolve("context"); + FS.ensureDirExists(contextDir); + + ServletContextHandler servletContextHandler = new ServletContextHandler(); + servletContextHandler.setContextPath("/context"); + servletContextHandler.setBaseResource(new PathResource(contextDir)); + ServletHolder holder = new ServletHolder("default", DefaultServlet.class); + holder.setInitParameter("etags", "true"); + servletContextHandler.addServlet(holder, "/"); + servletContextHandler.insertHandler(gzipHandler); + + server.setHandler(servletContextHandler); + + // Prepare Server File + Path file = createFile(contextDir, "file.txt", fileSize); + String expectedSha1Sum = Sha1Sum.calculate(file); + + server.start(); + + // Setup request + HttpTester.Request request = HttpTester.newRequest(); + request.setMethod("GET"); + request.setVersion(HttpVersion.HTTP_1_1); + request.setHeader("Host", "tester"); + request.setHeader("Connection", "close"); + request.setHeader("Accept-Encoding", "gzip"); + request.setURI("/context/file.txt"); + + // Issue request + ByteBuffer rawResponse = localConnector.getResponse(request.generate(), 5, TimeUnit.SECONDS); + + // Parse response + HttpTester.Response response = HttpTester.parseResponse(rawResponse); + + assertThat("Response status", response.getStatus(), is(HttpStatus.OK_200)); + + // Response Content-Encoding check + assertThat("Response[Content-Encoding]", response.get("Content-Encoding"), containsString("gzip")); + assertThat("Response[Vary]", response.get("Vary"), containsString("Accept-Encoding, User-Agent")); + + // Response Content checks + UncompressedMetadata metadata = parseResponseContent(response); + assertThat("(Uncompressed) Content Length", metadata.uncompressedSize, is(fileSize)); + assertThat("(Uncompressed) Content Hash", metadata.uncompressedSha1Sum, is(expectedSha1Sum)); + } + + @ParameterizedTest + @MethodSource("compressibleSizes") + public void testIsGzipCompressedIfModifiedSince(int fileSize) throws Exception + { + GzipHandler gzipHandler = new GzipHandler(); + gzipHandler.addIncludedMimeTypes("text/plain"); + + server = new Server(); + LocalConnector localConnector = new LocalConnector(server); + server.addConnector(localConnector); + + Path contextDir = workDir.resolve("context"); + FS.ensureDirExists(contextDir); + + ServletContextHandler servletContextHandler = new ServletContextHandler(); + servletContextHandler.setContextPath("/context"); + servletContextHandler.setBaseResource(new PathResource(contextDir)); + ServletHolder holder = new ServletHolder("default", DefaultServlet.class); + holder.setInitParameter("etags", "true"); + servletContextHandler.addServlet(holder, "/"); + servletContextHandler.insertHandler(gzipHandler); + + server.setHandler(servletContextHandler); + + // Prepare Server File + Path file = createFile(contextDir, "file.txt", fileSize); + String expectedSha1Sum = Sha1Sum.calculate(file); + + server.start(); + + // Setup request + HttpTester.Request request = HttpTester.newRequest(); + request.setMethod("GET"); + request.setVersion(HttpVersion.HTTP_1_1); + request.setHeader("Host", "tester"); + request.setHeader("Connection", "close"); + request.setHeader("Accept-Encoding", "gzip"); + long fourSecondsAgo = TimeUnit.NANOSECONDS.toMillis(System.nanoTime()) - 4000; + request.setHeader("If-Modified-Since", DateGenerator.formatDate(fourSecondsAgo)); + request.setURI("/context/file.txt"); + + // Issue request + ByteBuffer rawResponse = localConnector.getResponse(request.generate(), 5, TimeUnit.SECONDS); + + // Parse response + HttpTester.Response response = HttpTester.parseResponse(rawResponse); + + assertThat("Response status", response.getStatus(), is(HttpStatus.OK_200)); + + // Response Content-Encoding check + assertThat("Response[Content-Encoding]", response.get("Content-Encoding"), containsString("gzip")); + assertThat("Response[ETag]", response.get("ETag"), startsWith("W/")); + assertThat("Response[ETag]", response.get("ETag"), containsString(CompressedContentFormat.GZIP._etag)); + assertThat("Response[Vary]", response.get("Vary"), containsString("Accept-Encoding, User-Agent")); + + // Response Content checks + UncompressedMetadata metadata = parseResponseContent(response); + assertThat("(Uncompressed) Content Length", metadata.uncompressedSize, is(fileSize)); + assertThat("(Uncompressed) Content Hash", metadata.uncompressedSha1Sum, is(expectedSha1Sum)); + } + + @Test + public void testGzippedIfSVG() throws Exception + { + GzipHandler gzipHandler = new GzipHandler(); + gzipHandler.addIncludedMimeTypes("image/svg+xml"); + + server = new Server(); + LocalConnector localConnector = new LocalConnector(server); + server.addConnector(localConnector); + + Path contextDir = workDir.resolve("context"); + FS.ensureDirExists(contextDir); + + ServletContextHandler servletContextHandler = new ServletContextHandler(); + servletContextHandler.setContextPath("/context"); + servletContextHandler.setBaseResource(new PathResource(contextDir)); + ServletHolder holder = new ServletHolder("default", DefaultServlet.class); + holder.setInitParameter("etags", "true"); + servletContextHandler.addServlet(holder, "/"); + servletContextHandler.insertHandler(gzipHandler); + + server.setHandler(servletContextHandler); + + // Prepare Server File + Path testResource = MavenTestingUtils.getTestResourcePath("test.svg"); + Path file = contextDir.resolve("test.svg"); + IO.copy(testResource.toFile(), file.toFile()); + String expectedSha1Sum = Sha1Sum.calculate(testResource); + int fileSize = (int)Files.size(file); + + server.start(); + + // Setup request + HttpTester.Request request = HttpTester.newRequest(); + request.setMethod("GET"); + request.setVersion(HttpVersion.HTTP_1_1); + request.setHeader("Host", "tester"); + request.setHeader("Connection", "close"); + request.setHeader("Accept-Encoding", "gzip"); + request.setURI("/context/test.svg"); + + // Issue request + ByteBuffer rawResponse = localConnector.getResponse(request.generate(), 5, TimeUnit.SECONDS); + + // Parse response + HttpTester.Response response = HttpTester.parseResponse(rawResponse); + + assertThat("Response status", response.getStatus(), is(HttpStatus.OK_200)); + + // Response Content-Encoding check + assertThat("Response[Content-Encoding]", response.get("Content-Encoding"), containsString("gzip")); + assertThat("Response[Vary]", response.get("Vary"), containsString("Accept-Encoding, User-Agent")); + + // Response Content checks + UncompressedMetadata metadata = parseResponseContent(response); + assertThat("(Uncompressed) Content Length", metadata.uncompressedSize, is(fileSize)); + assertThat("(Uncompressed) Content Hash", metadata.uncompressedSha1Sum, is(expectedSha1Sum)); + } + + @Test + public void testNotGzipedIfNotModified() throws Exception + { + GzipHandler gzipHandler = new GzipHandler(); + gzipHandler.addIncludedMimeTypes("text/plain"); + + server = new Server(); + LocalConnector localConnector = new LocalConnector(server); + server.addConnector(localConnector); + + Path contextDir = workDir.resolve("context"); + FS.ensureDirExists(contextDir); + + ServletContextHandler servletContextHandler = new ServletContextHandler(); + servletContextHandler.setContextPath("/context"); + servletContextHandler.setBaseResource(new PathResource(contextDir)); + ServletHolder holder = new ServletHolder("default", DefaultServlet.class); + holder.setInitParameter("etags", "true"); + servletContextHandler.addServlet(holder, "/"); + servletContextHandler.insertHandler(gzipHandler); + + server.setHandler(servletContextHandler); + + // Prepare Server File + int fileSize = DEFAULT_OUTPUT_BUFFER_SIZE * 4; + createFile(contextDir, "file.txt", fileSize); + + server.start(); + + // Setup request + HttpTester.Request request = HttpTester.newRequest(); + request.setMethod("GET"); + request.setVersion(HttpVersion.HTTP_1_1); + request.setHeader("Host", "tester"); + request.setHeader("Connection", "close"); + request.setHeader("Accept-Encoding", "gzip"); + long fiveMinutesLater = System.currentTimeMillis() + TimeUnit.MINUTES.toMillis(5); + request.setHeader("If-Modified-Since", DateGenerator.formatDate(fiveMinutesLater)); + request.setURI("/context/file.txt"); + + // Issue request + ByteBuffer rawResponse = localConnector.getResponse(request.generate(), 5, TimeUnit.SECONDS); + + // Parse response + HttpTester.Response response = HttpTester.parseResponse(rawResponse); + + assertThat("Response status", response.getStatus(), is(HttpStatus.NOT_MODIFIED_304)); + + // Response Content-Encoding check + assertThat("Response[Content-Encoding]", response.get("Content-Encoding"), not(containsString("gzip"))); + assertThat("Response[ETag]", response.get("ETag"), startsWith("W/")); + assertThat("Response[ETag]", response.get("ETag"), not(containsString(CompressedContentFormat.GZIP._etag))); + + // Response Content checks + UncompressedMetadata metadata = parseResponseContent(response); + assertThat("(Uncompressed) Content Length", metadata.uncompressedSize, is(0)); + } + + /** + * Gzip incorrectly gzips when {@code Accept-Encoding: gzip; q=0}. + * + *

+ * A quality of 0 results in no compression. + *

+ * + * See: http://bugs.eclipse.org/388072 + */ + @Test + public void testIsNotGzipCompressedWithZeroQ() throws Exception + { + GzipHandler gzipHandler = new GzipHandler(); + gzipHandler.addIncludedMimeTypes("text/plain"); + + server = new Server(); + LocalConnector localConnector = new LocalConnector(server); + server.addConnector(localConnector); + + Path contextDir = workDir.resolve("context"); + FS.ensureDirExists(contextDir); + + ServletContextHandler servletContextHandler = new ServletContextHandler(); + servletContextHandler.setContextPath("/context"); + servletContextHandler.setBaseResource(new PathResource(contextDir)); + ServletHolder holder = new ServletHolder("default", DefaultServlet.class); + holder.setInitParameter("etags", "true"); + servletContextHandler.addServlet(holder, "/"); + servletContextHandler.insertHandler(gzipHandler); + + server.setHandler(servletContextHandler); + + // Prepare Server File + int fileSize = DEFAULT_OUTPUT_BUFFER_SIZE / 4; + Path file = createFile(contextDir, "file.txt", fileSize); + String expectedSha1Sum = Sha1Sum.calculate(file); + + server.start(); + + // Setup request + HttpTester.Request request = HttpTester.newRequest(); + request.setMethod("GET"); + request.setVersion(HttpVersion.HTTP_1_1); + request.setHeader("Host", "tester"); + request.setHeader("Connection", "close"); + request.setHeader("Accept-Encoding", "gzip; q=0"); // TESTING THIS + request.setURI("/context/file.txt"); + + // Issue request + ByteBuffer rawResponse = localConnector.getResponse(request.generate(), 5, TimeUnit.SECONDS); + + // Parse response + HttpTester.Response response = HttpTester.parseResponse(rawResponse); + + assertThat("Response status", response.getStatus(), is(HttpStatus.OK_200)); + + // Response Content-Encoding check + assertThat("Response[Content-Encoding]", response.get("Content-Encoding"), not(containsString("gzip"))); + assertThat("Response[Vary]", response.get("Vary"), containsString("Accept-Encoding, User-Agent")); + + // Response Content checks + UncompressedMetadata metadata = parseResponseContent(response); + assertThat("(Uncompressed) Content Length", metadata.uncompressedSize, is(fileSize)); + assertThat("(Uncompressed) Content Hash", metadata.uncompressedSha1Sum, is(expectedSha1Sum)); + } + + @Test + public void testIsGzipCompressedWithQ() throws Exception + { + GzipHandler gzipHandler = new GzipHandler(); + gzipHandler.addIncludedMimeTypes("text/plain"); + + server = new Server(); + LocalConnector localConnector = new LocalConnector(server); + server.addConnector(localConnector); + + Path contextDir = workDir.resolve("context"); + FS.ensureDirExists(contextDir); + + ServletContextHandler servletContextHandler = new ServletContextHandler(); + servletContextHandler.setContextPath("/context"); + servletContextHandler.setBaseResource(new PathResource(contextDir)); + ServletHolder holder = new ServletHolder("default", DefaultServlet.class); + holder.setInitParameter("etags", "true"); + servletContextHandler.addServlet(holder, "/"); + servletContextHandler.insertHandler(gzipHandler); + + server.setHandler(servletContextHandler); + + // Prepare Server File + int fileSize = DEFAULT_OUTPUT_BUFFER_SIZE / 4; + Path file = createFile(contextDir, "file.txt", fileSize); + String expectedSha1Sum = Sha1Sum.calculate(file); + + server.start(); + + // Setup request + HttpTester.Request request = HttpTester.newRequest(); + request.setMethod("GET"); + request.setVersion(HttpVersion.HTTP_1_1); + request.setHeader("Host", "tester"); + request.setHeader("Connection", "close"); + request.setHeader("Accept-Encoding", "something; q=0.1, gzip; q=0.5"); // TESTING THIS + request.setURI("/context/file.txt"); + + // Issue request + ByteBuffer rawResponse = localConnector.getResponse(request.generate(), 5, TimeUnit.SECONDS); + + // Parse response + HttpTester.Response response = HttpTester.parseResponse(rawResponse); + + assertThat("Response status", response.getStatus(), is(HttpStatus.OK_200)); + + // Response Content-Encoding check + assertThat("Response[Content-Encoding]", response.get("Content-Encoding"), containsString("gzip")); + assertThat("Response[Vary]", response.get("Vary"), containsString("Accept-Encoding, User-Agent")); + + // Response Content checks + UncompressedMetadata metadata = parseResponseContent(response); + assertThat("(Uncompressed) Content Length", metadata.uncompressedSize, is(fileSize)); + assertThat("(Uncompressed) Content Hash", metadata.uncompressedSha1Sum, is(expectedSha1Sum)); + } + + @Test + public void testIsNotGzipCompressedByContentType() throws Exception + { + GzipHandler gzipHandler = new GzipHandler(); + gzipHandler.addIncludedMimeTypes("text/plain"); + + server = new Server(); + LocalConnector localConnector = new LocalConnector(server); + server.addConnector(localConnector); + + Path contextDir = workDir.resolve("context"); + FS.ensureDirExists(contextDir); + + ServletContextHandler servletContextHandler = new ServletContextHandler(); + servletContextHandler.setContextPath("/context"); + servletContextHandler.setBaseResource(new PathResource(contextDir)); + ServletHolder holder = new ServletHolder("default", DefaultServlet.class); + holder.setInitParameter("etags", "true"); + servletContextHandler.addServlet(holder, "/"); + servletContextHandler.insertHandler(gzipHandler); + + server.setHandler(servletContextHandler); + + // Prepare Server File + int fileSize = DEFAULT_OUTPUT_BUFFER_SIZE * 4; + Path file = createFile(contextDir, "file.mp3", fileSize); + String expectedSha1Sum = Sha1Sum.calculate(file); + + server.start(); + + // Setup request + HttpTester.Request request = HttpTester.newRequest(); + request.setMethod("GET"); + request.setVersion(HttpVersion.HTTP_1_1); + request.setHeader("Host", "tester"); + request.setHeader("Connection", "close"); + request.setHeader("Accept-Encoding", "gzip"); + request.setURI("/context/file.mp3"); + + // Issue request + ByteBuffer rawResponse = localConnector.getResponse(request.generate(), 5, TimeUnit.SECONDS); + + // Parse response + HttpTester.Response response = HttpTester.parseResponse(rawResponse); + + assertThat("Response status", response.getStatus(), is(HttpStatus.OK_200)); + + // Response Content-Encoding check + assertThat("Response[Content-Encoding]", response.get("Content-Encoding"), not(containsString("gzip"))); + assertThat("Response[Vary]", response.get("Vary"), is(nullValue())); + + // Response Content checks + UncompressedMetadata metadata = parseResponseContent(response); + assertThat("Response Content Length", metadata.contentLength, is(fileSize)); + assertThat("(Uncompressed) Content Length", metadata.uncompressedSize, is(fileSize)); + assertThat("(Uncompressed) Content Hash", metadata.uncompressedSha1Sum, is(expectedSha1Sum)); + } + + @Test + public void testIsNotGzipCompressedByExcludedContentType() throws Exception + { + GzipHandler gzipHandler = new GzipHandler(); + gzipHandler.addExcludedMimeTypes("text/plain"); + + server = new Server(); + LocalConnector localConnector = new LocalConnector(server); + server.addConnector(localConnector); + + Path contextDir = workDir.resolve("context"); + FS.ensureDirExists(contextDir); + + ServletContextHandler servletContextHandler = new ServletContextHandler(); + servletContextHandler.setContextPath("/context"); + servletContextHandler.setBaseResource(new PathResource(contextDir)); + ServletHolder holder = new ServletHolder("default", DefaultServlet.class); + holder.setInitParameter("etags", "true"); + servletContextHandler.addServlet(holder, "/"); + servletContextHandler.insertHandler(gzipHandler); + + server.setHandler(servletContextHandler); + + // Prepare Server File + int fileSize = DEFAULT_OUTPUT_BUFFER_SIZE * 4; + Path file = createFile(contextDir, "file.txt", fileSize); + String expectedSha1Sum = Sha1Sum.calculate(file); + + server.start(); + + // Setup request + HttpTester.Request request = HttpTester.newRequest(); + request.setMethod("GET"); + request.setVersion(HttpVersion.HTTP_1_1); + request.setHeader("Host", "tester"); + request.setHeader("Connection", "close"); + request.setHeader("Accept-Encoding", "gzip"); + request.setURI("/context/file.txt"); + + // Issue request + ByteBuffer rawResponse = localConnector.getResponse(request.generate(), 5, TimeUnit.SECONDS); + + // Parse response + HttpTester.Response response = HttpTester.parseResponse(rawResponse); + + assertThat("Response status", response.getStatus(), is(HttpStatus.OK_200)); + + // Response Content-Encoding check + assertThat("Response[Content-Encoding]", response.get("Content-Encoding"), not(containsString("gzip"))); + assertThat("Response[Vary]", response.get("Vary"), is(nullValue())); + + // Response Content checks + UncompressedMetadata metadata = parseResponseContent(response); + assertThat("Response Content Length", metadata.contentLength, is(fileSize)); + assertThat("(Uncompressed) Content Length", metadata.uncompressedSize, is(fileSize)); + assertThat("(Uncompressed) Content Hash", metadata.uncompressedSha1Sum, is(expectedSha1Sum)); + } + + @Test + public void testIsNotGzipCompressedByExcludedContentTypeWithCharset() throws Exception + { + GzipHandler gzipHandler = new GzipHandler(); + gzipHandler.addExcludedMimeTypes("text/plain"); + + server = new Server(); + LocalConnector localConnector = new LocalConnector(server); + server.addConnector(localConnector); + + Path contextDir = workDir.resolve("context"); + FS.ensureDirExists(contextDir); + + ServletContextHandler servletContextHandler = new ServletContextHandler(); + servletContextHandler.setContextPath("/context"); + servletContextHandler.setBaseResource(new PathResource(contextDir)); + servletContextHandler.getMimeTypes().addMimeMapping("txt", "text/plain;charset=UTF-8"); + ServletHolder holder = new ServletHolder("default", DefaultServlet.class); + holder.setInitParameter("etags", "true"); + servletContextHandler.addServlet(holder, "/"); + servletContextHandler.insertHandler(gzipHandler); + + server.setHandler(servletContextHandler); + + // Prepare Server File + int fileSize = DEFAULT_OUTPUT_BUFFER_SIZE * 4; + Path file = createFile(contextDir, "test_quotes.txt", fileSize); + String expectedSha1Sum = Sha1Sum.calculate(file); + + server.start(); + + // Setup request + HttpTester.Request request = HttpTester.newRequest(); + request.setMethod("GET"); + request.setVersion(HttpVersion.HTTP_1_1); + request.setHeader("Host", "tester"); + request.setHeader("Connection", "close"); + request.setHeader("Accept-Encoding", "gzip"); + request.setURI("/context/test_quotes.txt"); + + // Issue request + ByteBuffer rawResponse = localConnector.getResponse(request.generate(), 5, TimeUnit.SECONDS); + + // Parse response + HttpTester.Response response = HttpTester.parseResponse(rawResponse); + + assertThat("Response status", response.getStatus(), is(HttpStatus.OK_200)); + + // Response Content-Encoding check + assertThat("Response[Content-Encoding]", response.get("Content-Encoding"), not(containsString("gzip"))); + assertThat("Response[Vary]", response.get("Vary"), is(nullValue())); + + // Response Content checks + UncompressedMetadata metadata = parseResponseContent(response); + assertThat("(Uncompressed) Content Length", metadata.uncompressedSize, is(fileSize)); + assertThat("(Uncompressed) Content Hash", metadata.uncompressedSha1Sum, is(expectedSha1Sum)); + } + + @Test + public void testUserAgentExclusionNoUAProvided() throws Exception + { + GzipHandler gzipHandler = new GzipHandler(); + gzipHandler.addIncludedMimeTypes("text/plain"); + gzipHandler.setExcludedAgentPatterns("bar", "foo"); + + server = new Server(); + LocalConnector localConnector = new LocalConnector(server); + server.addConnector(localConnector); + + Path contextDir = workDir.resolve("context"); + FS.ensureDirExists(contextDir); + + ServletContextHandler servletContextHandler = new ServletContextHandler(); + servletContextHandler.setContextPath("/context"); + servletContextHandler.setBaseResource(new PathResource(contextDir)); + ServletHolder holder = new ServletHolder("default", DefaultServlet.class); + servletContextHandler.addServlet(holder, "/"); + servletContextHandler.insertHandler(gzipHandler); + + server.setHandler(servletContextHandler); + + // Prepare Server File + int fileSize = DEFAULT_OUTPUT_BUFFER_SIZE * 4; + Path file = createFile(contextDir, "file.txt", fileSize); + String expectedSha1Sum = Sha1Sum.calculate(file); + + server.start(); + + // Setup request + HttpTester.Request request = HttpTester.newRequest(); + request.setMethod("GET"); + request.setVersion(HttpVersion.HTTP_1_1); + request.setHeader("Host", "tester"); + request.setHeader("Connection", "close"); + request.setHeader("Accept-Encoding", "gzip"); + // INTENTIONALLY NOT SET - request.setHeader("User-Agent", "foo"); + request.setURI("/context/file.txt"); + + // Issue request + ByteBuffer rawResponse = localConnector.getResponse(request.generate(), 5, TimeUnit.SECONDS); + + // Parse response + HttpTester.Response response = HttpTester.parseResponse(rawResponse); + + assertThat("Response status", response.getStatus(), is(HttpStatus.OK_200)); + + // Response Content-Encoding check + assertThat("Response[Content-Encoding]", response.get("Content-Encoding"), containsString("gzip")); + assertThat("Response[Vary]", response.get("Vary"), is("Accept-Encoding, User-Agent")); + + // Response Content checks + UncompressedMetadata metadata = parseResponseContent(response); + assertThat("(Uncompressed) Content Length", metadata.uncompressedSize, is(fileSize)); + assertThat("(Uncompressed) Content Hash", metadata.uncompressedSha1Sum, is(expectedSha1Sum)); + } + + @Test + public void testUserAgentExclusionUAMatch() throws Exception + { + GzipHandler gzipHandler = new GzipHandler(); + gzipHandler.addIncludedMimeTypes("text/plain"); + gzipHandler.setExcludedAgentPatterns("bar", "foo"); + + server = new Server(); + LocalConnector localConnector = new LocalConnector(server); + server.addConnector(localConnector); + + Path contextDir = workDir.resolve("context"); + FS.ensureDirExists(contextDir); + + ServletContextHandler servletContextHandler = new ServletContextHandler(); + servletContextHandler.setContextPath("/context"); + servletContextHandler.setBaseResource(new PathResource(contextDir)); + ServletHolder holder = new ServletHolder("default", DefaultServlet.class); + servletContextHandler.addServlet(holder, "/"); + servletContextHandler.insertHandler(gzipHandler); + + server.setHandler(servletContextHandler); + + // Prepare Server File + int fileSize = DEFAULT_OUTPUT_BUFFER_SIZE * 4; + Path file = createFile(contextDir, "file.txt", fileSize); + String expectedSha1Sum = Sha1Sum.calculate(file); + + server.start(); + + // Setup request + HttpTester.Request request = HttpTester.newRequest(); + request.setMethod("GET"); + request.setVersion(HttpVersion.HTTP_1_1); + request.setHeader("Host", "tester"); + request.setHeader("Connection", "close"); + request.setHeader("Accept-Encoding", "gzip"); + request.setHeader("User-Agent", "foo"); + request.setURI("/context/file.txt"); + + // Issue request + ByteBuffer rawResponse = localConnector.getResponse(request.generate(), 5, TimeUnit.SECONDS); + + // Parse response + HttpTester.Response response = HttpTester.parseResponse(rawResponse); + + assertThat("Response status", response.getStatus(), is(HttpStatus.OK_200)); + + // Response Content-Encoding check + assertThat("Response[Content-Encoding]", response.get("Content-Encoding"), not(containsString("gzip"))); + assertThat("Response[Vary]", response.get("Vary"), is("Accept-Encoding, User-Agent")); + + // Response Content checks + UncompressedMetadata metadata = parseResponseContent(response); + assertThat("Response Content Length", metadata.contentLength, is(fileSize)); + assertThat("(Uncompressed) Content Length", metadata.uncompressedSize, is(fileSize)); + assertThat("(Uncompressed) Content Hash", metadata.uncompressedSha1Sum, is(expectedSha1Sum)); + } + + @Test + public void testUserAgentExclusionDefault() throws Exception + { + GzipHandler gzipHandler = new GzipHandler(); + gzipHandler.addIncludedMimeTypes("text/plain"); + + server = new Server(); + LocalConnector localConnector = new LocalConnector(server); + server.addConnector(localConnector); + + Path contextDir = workDir.resolve("context"); + FS.ensureDirExists(contextDir); + + ServletContextHandler servletContextHandler = new ServletContextHandler(); + servletContextHandler.setContextPath("/context"); + servletContextHandler.setBaseResource(new PathResource(contextDir)); + ServletHolder holder = new ServletHolder("default", DefaultServlet.class); + servletContextHandler.addServlet(holder, "/"); + servletContextHandler.insertHandler(gzipHandler); + + server.setHandler(servletContextHandler); + + // Prepare Server File + int fileSize = DEFAULT_OUTPUT_BUFFER_SIZE * 4; + Path file = createFile(contextDir, "file.txt", fileSize); + String expectedSha1Sum = Sha1Sum.calculate(file); + + server.start(); + + // Setup request + HttpTester.Request request = HttpTester.newRequest(); + request.setMethod("GET"); + request.setVersion(HttpVersion.HTTP_1_1); + request.setHeader("Host", "tester"); + request.setHeader("Connection", "close"); + request.setHeader("Accept-Encoding", "gzip"); + request.setHeader("User-Agent", "Some MSIE 6.0 user-agent"); + request.setURI("/context/file.txt"); + + // Issue request + ByteBuffer rawResponse = localConnector.getResponse(request.generate(), 5, TimeUnit.SECONDS); + + // Parse response + HttpTester.Response response = HttpTester.parseResponse(rawResponse); + + assertThat("Response status", response.getStatus(), is(HttpStatus.OK_200)); + + // Response Content-Encoding check + assertThat("Response[Content-Encoding]", response.get("Content-Encoding"), not(containsString("gzip"))); + assertThat("Response[Vary]", response.get("Vary"), is("Accept-Encoding, User-Agent")); + + // Response Content checks + UncompressedMetadata metadata = parseResponseContent(response); + assertThat("Response Content Length", metadata.contentLength, is(fileSize)); + assertThat("(Uncompressed) Content Length", metadata.uncompressedSize, is(fileSize)); + assertThat("(Uncompressed) Content Hash", metadata.uncompressedSha1Sum, is(expectedSha1Sum)); + } + + @Test + public void testUserAgentExclusionByExcludedAgentPatterns() throws Exception + { + GzipHandler gzipHandler = new GzipHandler(); + gzipHandler.addIncludedMimeTypes("text/plain"); + gzipHandler.setExcludedAgentPatterns("bar", "fo.*"); + + server = new Server(); + LocalConnector localConnector = new LocalConnector(server); + server.addConnector(localConnector); + + Path contextDir = workDir.resolve("context"); + FS.ensureDirExists(contextDir); + + ServletContextHandler servletContextHandler = new ServletContextHandler(); + servletContextHandler.setContextPath("/context"); + servletContextHandler.setBaseResource(new PathResource(contextDir)); + ServletHolder holder = new ServletHolder("default", DefaultServlet.class); + servletContextHandler.addServlet(holder, "/"); + servletContextHandler.insertHandler(gzipHandler); + + server.setHandler(servletContextHandler); + + // Prepare Server File + int fileSize = DEFAULT_OUTPUT_BUFFER_SIZE * 4; + Path file = createFile(contextDir, "file.txt", fileSize); + String expectedSha1Sum = Sha1Sum.calculate(file); + + server.start(); + + // Setup request + HttpTester.Request request = HttpTester.newRequest(); + request.setMethod("GET"); + request.setVersion(HttpVersion.HTTP_1_1); + request.setHeader("Host", "tester"); + request.setHeader("Connection", "close"); + request.setHeader("Accept-Encoding", "gzip"); + request.setHeader("User-Agent", "foo"); + request.setURI("/context/file.txt"); + + // Issue request + ByteBuffer rawResponse = localConnector.getResponse(request.generate(), 5, TimeUnit.SECONDS); + + // Parse response + HttpTester.Response response = HttpTester.parseResponse(rawResponse); + + assertThat("Response status", response.getStatus(), is(HttpStatus.OK_200)); + + // Response Content-Encoding check + assertThat("Response[Content-Encoding]", response.get("Content-Encoding"), not(containsString("gzip"))); + assertThat("Response[Vary]", response.get("Vary"), is("Accept-Encoding, User-Agent")); + + // Response Content checks + UncompressedMetadata metadata = parseResponseContent(response); + assertThat("Response Content Length", metadata.contentLength, is(fileSize)); + assertThat("(Uncompressed) Content Length", metadata.uncompressedSize, is(fileSize)); + assertThat("(Uncompressed) Content Hash", metadata.uncompressedSha1Sum, is(expectedSha1Sum)); + } + + @Test + public void testExcludePaths() throws Exception + { + GzipHandler gzipHandler = new GzipHandler(); + gzipHandler.addIncludedMimeTypes("text/plain"); + gzipHandler.setExcludedPaths("*.txt"); + + server = new Server(); + LocalConnector localConnector = new LocalConnector(server); + server.addConnector(localConnector); + + Path contextDir = workDir.resolve("context"); + FS.ensureDirExists(contextDir); + + ServletContextHandler servletContextHandler = new ServletContextHandler(); + servletContextHandler.setContextPath("/context"); + servletContextHandler.setBaseResource(new PathResource(contextDir)); + ServletHolder holder = new ServletHolder("default", DefaultServlet.class); + servletContextHandler.addServlet(holder, "/"); + servletContextHandler.insertHandler(gzipHandler); + + server.setHandler(servletContextHandler); + + // Prepare Server File + int fileSize = DEFAULT_OUTPUT_BUFFER_SIZE * 4; + Path file = createFile(contextDir, "file.txt", fileSize); + String expectedSha1Sum = Sha1Sum.calculate(file); + + server.start(); + + // Setup request + HttpTester.Request request = HttpTester.newRequest(); + request.setMethod("GET"); + request.setVersion(HttpVersion.HTTP_1_1); + request.setHeader("Host", "tester"); + request.setHeader("Connection", "close"); + request.setHeader("Accept-Encoding", "gzip"); + request.setURI("/context/file.txt"); + + // Issue request + ByteBuffer rawResponse = localConnector.getResponse(request.generate(), 5, TimeUnit.SECONDS); + + // Parse response + HttpTester.Response response = HttpTester.parseResponse(rawResponse); + + assertThat("Response status", response.getStatus(), is(HttpStatus.OK_200)); + + // Response Content-Encoding check + assertThat("Response[Content-Encoding]", response.get("Content-Encoding"), not(containsString("gzip"))); + assertThat("Response[Vary]", response.get("Vary"), is(nullValue())); + + // Response Content checks + UncompressedMetadata metadata = parseResponseContent(response); + assertThat("Response Content Length", metadata.contentLength, is(fileSize)); + assertThat("(Uncompressed) Content Length", metadata.uncompressedSize, is(fileSize)); + assertThat("(Uncompressed) Content Hash", metadata.uncompressedSha1Sum, is(expectedSha1Sum)); + } + + @Test + public void testIncludedPaths() throws Exception + { + GzipHandler gzipHandler = new GzipHandler(); + gzipHandler.setExcludedPaths("/bad.txt"); + gzipHandler.setIncludedPaths("*.txt"); + + server = new Server(); + LocalConnector localConnector = new LocalConnector(server); + server.addConnector(localConnector); + + Path contextDir = workDir.resolve("context"); + FS.ensureDirExists(contextDir); + + ServletContextHandler servletContextHandler = new ServletContextHandler(); + servletContextHandler.setContextPath("/context"); + servletContextHandler.setBaseResource(new PathResource(contextDir)); + ServletHolder holder = new ServletHolder("default", DefaultServlet.class); + servletContextHandler.addServlet(holder, "/"); + servletContextHandler.insertHandler(gzipHandler); + + server.setHandler(servletContextHandler); + + // Prepare Server File + Path fileGood = createFile(contextDir, "file.txt", DEFAULT_OUTPUT_BUFFER_SIZE * 4); + Path fileBad = createFile(contextDir, "bad.txt", DEFAULT_OUTPUT_BUFFER_SIZE * 2); + String expectedGoodSha1Sum = Sha1Sum.calculate(fileGood); + String expectedBadSha1Sum = Sha1Sum.calculate(fileBad); + + server.start(); + + // Test Request 1 + { + HttpTester.Request request = HttpTester.newRequest(); + request.setMethod("GET"); + request.setVersion(HttpVersion.HTTP_1_1); + request.setHeader("Host", "tester"); + request.setHeader("Connection", "close"); + request.setHeader("Accept-Encoding", "gzip"); + request.setURI("/context/file.txt"); + + ByteBuffer rawResponse = localConnector.getResponse(request.generate(), 5, TimeUnit.SECONDS); + + HttpTester.Response response = HttpTester.parseResponse(rawResponse); + + assertThat("Response status", response.getStatus(), is(HttpStatus.OK_200)); + + assertThat("Response[Content-Encoding]", response.get("Content-Encoding"), containsString("gzip")); + assertThat("Response[Vary]", response.get("Vary"), is("Accept-Encoding, User-Agent")); + + UncompressedMetadata metadata = parseResponseContent(response); + assertThat("(Uncompressed) Content Length", metadata.uncompressedSize, is((int)Files.size(fileGood))); + assertThat("(Uncompressed) Content Hash", metadata.uncompressedSha1Sum, is(expectedGoodSha1Sum)); + } + + // Test Request 2 + { + HttpTester.Request request = HttpTester.newRequest(); + request.setMethod("GET"); + request.setVersion(HttpVersion.HTTP_1_1); + request.setHeader("Host", "tester"); + request.setHeader("Connection", "close"); + request.setHeader("Accept-Encoding", "gzip"); + request.setURI("/context/bad.txt"); + + ByteBuffer rawResponse = localConnector.getResponse(request.generate(), 5, TimeUnit.SECONDS); + + HttpTester.Response response = HttpTester.parseResponse(rawResponse); + + assertThat("Response status", response.getStatus(), is(HttpStatus.OK_200)); + + assertThat("Response[Content-Encoding]", response.get("Content-Encoding"), not(containsString("gzip"))); + assertThat("Response[Vary]", response.get("Vary"), is(nullValue())); + + UncompressedMetadata metadata = parseResponseContent(response); + int fileSize = (int)Files.size(fileBad); + assertThat("Response Content Length", metadata.contentLength, is(fileSize)); + assertThat("(Uncompressed) Content Length", metadata.uncompressedSize, is(fileSize)); + assertThat("(Uncompressed) Content Hash", metadata.uncompressedSha1Sum, is(expectedBadSha1Sum)); + } + } + + @Test + public void testIsNotGzipCompressedSVGZ() throws Exception + { + GzipHandler gzipHandler = new GzipHandler(); + + server = new Server(); + LocalConnector localConnector = new LocalConnector(server); + server.addConnector(localConnector); + + Path contextDir = workDir.resolve("context"); + FS.ensureDirExists(contextDir); + + ServletContextHandler servletContextHandler = new ServletContextHandler(); + servletContextHandler.setContextPath("/context"); + servletContextHandler.setBaseResource(new PathResource(contextDir)); + ServletHolder holder = new ServletHolder("default", DefaultServlet.class); + servletContextHandler.addServlet(holder, "/"); + servletContextHandler.insertHandler(gzipHandler); + + server.setHandler(servletContextHandler); + + // Prepare Server File + Path testResource = MavenTestingUtils.getTestResourcePath("test.svgz"); + Path file = contextDir.resolve("test.svgz"); + IO.copy(testResource.toFile(), file.toFile()); + int fileSize = (int)Files.size(file); + + server.start(); + + // Setup request + HttpTester.Request request = HttpTester.newRequest(); + request.setMethod("GET"); + request.setVersion(HttpVersion.HTTP_1_1); + request.setHeader("Host", "tester"); + request.setHeader("Connection", "close"); + request.setHeader("Accept-Encoding", "gzip"); + request.setURI("/context/test.svgz"); + + // Issue request + ByteBuffer rawResponse = localConnector.getResponse(request.generate(), 5, TimeUnit.SECONDS); + + // Parse response + HttpTester.Response response = HttpTester.parseResponse(rawResponse); + + assertThat("Response status", response.getStatus(), is(HttpStatus.OK_200)); + + // Response Header checks + assertThat("Response[Content-Type]", response.get("Content-Type"), containsString("image/svg+xml")); + assertThat("Response[Content-Encoding]", response.get("Content-Encoding"), containsString("gzip")); + assertThat("Response[Vary]", response.get("Vary"), is(nullValue())); + + // Response Content checks + UncompressedMetadata metadata = parseResponseContent(response); + assertThat("Response Content Length", metadata.contentLength, is(fileSize)); + } +} diff --git a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/GzipDefaultTest.java b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/GzipDefaultTest.java deleted file mode 100644 index 15443159286..00000000000 --- a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/GzipDefaultTest.java +++ /dev/null @@ -1,765 +0,0 @@ -// -// ======================================================================== -// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others. -// -// This program and the accompanying materials are made available under -// the terms of the Eclipse Public License 2.0 which is available at -// https://www.eclipse.org/legal/epl-2.0 -// -// This Source Code may also be made available under the following -// Secondary Licenses when the conditions for such availability set -// forth in the Eclipse Public License, v. 2.0 are satisfied: -// the Apache License v2.0 which is available at -// https://www.apache.org/licenses/LICENSE-2.0 -// -// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 -// ======================================================================== -// - -package org.eclipse.jetty.servlets; - -import java.io.File; -import java.io.IOException; -import java.io.UnsupportedEncodingException; -import java.util.concurrent.TimeUnit; - -import jakarta.servlet.ServletException; -import jakarta.servlet.ServletOutputStream; -import jakarta.servlet.http.HttpServlet; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import org.eclipse.jetty.http.CompressedContentFormat; -import org.eclipse.jetty.http.HttpStatus; -import org.eclipse.jetty.http.tools.HttpTester; -import org.eclipse.jetty.server.handler.gzip.GzipHandler; -import org.eclipse.jetty.servlet.DefaultServlet; -import org.eclipse.jetty.toolchain.test.IO; -import org.eclipse.jetty.toolchain.test.jupiter.WorkDir; -import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension; -import org.eclipse.jetty.util.StringUtil; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.containsString; -import static org.hamcrest.Matchers.emptyOrNullString; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.not; -import static org.hamcrest.Matchers.notNullValue; -import static org.hamcrest.Matchers.startsWith; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNull; - -/** - * Test the GzipHandler support built into the {@link DefaultServlet} - */ -@ExtendWith(WorkDirExtension.class) -public class GzipDefaultTest -{ - private String compressionType; - - public GzipDefaultTest() - { - this.compressionType = GzipHandler.GZIP; - } - - @SuppressWarnings("serial") - public static class HttpStatusServlet extends HttpServlet - { - private int _status = 204; - - public HttpStatusServlet() - { - super(); - } - - @Override - protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException - { - resp.setStatus(_status); - resp.setHeader("ETag", "W/\"204\""); - } - } - - @SuppressWarnings("serial") - public static class HttpErrorServlet extends HttpServlet - { - private int _status = 400; - - public HttpErrorServlet() - { - super(); - } - - @Override - protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException - { - resp.getOutputStream().write("error message".getBytes()); - resp.setStatus(_status); - } - } - - @SuppressWarnings("serial") - public static class HttpContentTypeWithEncoding extends HttpServlet - { - public static final String COMPRESSED_CONTENT = "

COMPRESSABLE CONTENT

" + - "This content must be longer than the default min gzip length, which is 256 bytes. " + - "The moon is blue to a fish in love. How now brown cow. The quick brown fox jumped over the lazy dog. A woman needs a man like a fish needs a bicycle!" + - ""; - - @Override - protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException - { - resp.setContentType("text/plain;charset=UTF8"); - resp.setStatus(200); - ServletOutputStream out = resp.getOutputStream(); - out.print(COMPRESSED_CONTENT); - } - } - - public WorkDir testingdir; - - @Test - public void testIsGzipByMethod() throws Exception - { - GzipTester tester = new GzipTester(testingdir.getEmptyPathDir(), compressionType); - - // Configure Gzip Handler - tester.getGzipHandler().setIncludedMethods("POST", "WIBBLE", "HEAD"); - - // Prepare Server File - int filesize = tester.getOutputBufferSize() * 2; - tester.prepareServerFile("file.txt", filesize); - - // Content Servlet - tester.setContentServlet(GetServlet.class); - - try - { - tester.start(); - HttpTester.Response response; - - //These methods have content bodies of the compressed response - tester.assertIsResponseGzipCompressed("POST", "file.txt"); - tester.assertIsResponseGzipCompressed("WIBBLE", "file.txt"); - - //A HEAD request should have similar headers, but no body - response = tester.executeRequest("HEAD", "/context/file.txt", 5, TimeUnit.SECONDS); - assertThat("Response status", response.getStatus(), is(HttpStatus.OK_200)); - assertThat("ETag", response.get("ETag"), containsString(CompressedContentFormat.GZIP._etag)); - assertThat("Content encoding", response.get("Content-Encoding"), containsString("gzip")); - assertNull(response.get("Content-Length"), "Content length"); - - response = tester.executeRequest("GET", "/context/file.txt", 5, TimeUnit.SECONDS); - - assertThat("Response status", response.getStatus(), is(HttpStatus.OK_200)); - assertThat("Content-Encoding", response.get("Content-Encoding"), not(containsString(compressionType))); - - String content = tester.readResponse(response); - assertThat("Response content size", content.length(), is(filesize)); - String expectedContent = IO.readToString(testingdir.getPathFile("file.txt").toFile()); - assertThat("Response content", content, is(expectedContent)); - } - finally - { - tester.stop(); - } - } - - @SuppressWarnings("serial") - public static class GetServlet extends DefaultServlet - { - public GetServlet() - { - super(); - } - - @Override - public void service(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException - { - String uri = req.getRequestURI(); - if (uri.endsWith(".deferred")) - { - // System.err.println("type for "+uri.substring(0,uri.length()-9)+" is "+getServletContext().getMimeType(uri.substring(0,uri.length()-9))); - resp.setContentType(getServletContext().getMimeType(uri.substring(0, uri.length() - 9))); - } - - doGet(req, resp); - } - } - - @Test - public void testIsGzipCompressedEmpty() throws Exception - { - GzipTester tester = new GzipTester(testingdir.getEmptyPathDir(), compressionType); - - // Configure Gzip Handler - tester.getGzipHandler().addIncludedMimeTypes("text/plain"); - - // Prepare server file - tester.prepareServerFile("empty.txt", 0); - - // Set content servlet - tester.setContentServlet(DefaultServlet.class); - - try - { - tester.start(); - - HttpTester.Response response; - - response = tester.executeRequest("GET", "/context/empty.txt", 5, TimeUnit.SECONDS); - - assertThat("Response status", response.getStatus(), is(HttpStatus.OK_200)); - assertThat("Content-Encoding", response.get("Content-Encoding"), not(containsString(compressionType))); - - String content = tester.readResponse(response); - assertThat("Response content size", content.length(), is(0)); - String expectedContent = IO.readToString(testingdir.getPathFile("empty.txt").toFile()); - assertThat("Response content", content, is(expectedContent)); - } - finally - { - tester.stop(); - } - } - - @Test - public void testIsGzipCompressedTiny() throws Exception - { - GzipTester tester = new GzipTester(testingdir.getEmptyPathDir(), compressionType); - - int filesize = tester.getOutputBufferSize() / 4; - tester.prepareServerFile("file.txt", filesize); - - tester.setContentServlet(org.eclipse.jetty.servlet.DefaultServlet.class); - - try - { - tester.start(); - HttpTester.Response http = tester.assertIsResponseGzipCompressed("GET", "file.txt"); - assertEquals("Accept-Encoding, User-Agent", http.get("Vary")); - } - finally - { - tester.stop(); - } - } - - @Test - public void testIsGzipCompressedLarge() throws Exception - { - GzipTester tester = new GzipTester(testingdir.getEmptyPathDir(), compressionType); - - int filesize = tester.getOutputBufferSize() * 4; - tester.prepareServerFile("file.txt", filesize); - - tester.setContentServlet(org.eclipse.jetty.servlet.DefaultServlet.class); - tester.getGzipHandler().setExcludedAgentPatterns(); - - try - { - tester.start(); - HttpTester.Response http = tester.assertIsResponseGzipCompressed("GET", "file.txt"); - assertEquals("Accept-Encoding", http.get("Vary")); - } - finally - { - tester.stop(); - } - } - - @Test - public void testGzipedIfModified() throws Exception - { - GzipTester tester = new GzipTester(testingdir.getEmptyPathDir(), compressionType); - - int filesize = tester.getOutputBufferSize() * 4; - tester.prepareServerFile("file.txt", filesize); - - tester.setContentServlet(org.eclipse.jetty.servlet.DefaultServlet.class); - - try - { - tester.start(); - HttpTester.Response http = tester.assertIsResponseGzipCompressed("GET", "file.txt", TimeUnit.NANOSECONDS.toMillis(System.nanoTime()) - 4000); - assertEquals("Accept-Encoding, User-Agent", http.get("Vary")); - } - finally - { - tester.stop(); - } - } - - @Test - public void testGzippedIfSVG() throws Exception - { - GzipTester tester = new GzipTester(testingdir.getEmptyPathDir(), compressionType); - tester.copyTestServerFile("test.svg"); - tester.setContentServlet(org.eclipse.jetty.servlet.DefaultServlet.class); - - tester.getGzipHandler().addIncludedMimeTypes("image/svg+xml"); - - try - { - tester.start(); - HttpTester.Response http = tester.assertIsResponseGzipCompressed("GET", "test.svg", TimeUnit.NANOSECONDS.toMillis(System.nanoTime()) - 4000); - assertEquals("Accept-Encoding, User-Agent", http.get("Vary")); - } - finally - { - tester.stop(); - } - } - - @Test - public void testNotGzipedIfNotModified() throws Exception - { - GzipTester tester = new GzipTester(testingdir.getEmptyPathDir(), compressionType); - - int filesize = tester.getOutputBufferSize() * 4; - tester.prepareServerFile("file.txt", filesize); - - tester.setContentServlet(org.eclipse.jetty.servlet.DefaultServlet.class); - - try - { - tester.start(); - tester.assertIsResponseNotModified("GET", "file.txt", System.currentTimeMillis() + 4000); - } - finally - { - tester.stop(); - } - } - - @Test - public void testIsNotGzipCompressedWithZeroQ() throws Exception - { - GzipTester tester = new GzipTester(testingdir.getEmptyPathDir(), compressionType + "; q=0"); - - // Configure Gzip Handler - tester.getGzipHandler().addIncludedMimeTypes("text/plain"); - - // Prepare server file - int filesize = tester.getOutputBufferSize() / 4; - tester.prepareServerFile("file.txt", filesize); - - // Add content servlet - tester.setContentServlet(DefaultServlet.class); - - try - { - tester.start(); - HttpTester.Response http = assertIsResponseNotGzipCompressed(tester, "GET", "file.txt", filesize, HttpStatus.OK_200); - assertThat("Response[Vary]", http.get("Vary"), containsString("Accept-Encoding")); - } - finally - { - tester.stop(); - } - } - - @Test - public void testIsGzipCompressedWithQ() throws Exception - { - GzipTester tester = new GzipTester(testingdir.getEmptyPathDir(), compressionType, "something;q=0.1," + compressionType + ";q=0.5"); - - int filesize = tester.getOutputBufferSize() / 4; - tester.prepareServerFile("file.txt", filesize); - - tester.setContentServlet(org.eclipse.jetty.servlet.DefaultServlet.class); - tester.getGzipHandler().setExcludedAgentPatterns(); - - try - { - tester.start(); - HttpTester.Response http = tester.assertIsResponseGzipCompressed("GET", "file.txt"); - assertEquals("Accept-Encoding", http.get("Vary")); - } - finally - { - tester.stop(); - } - } - - @Test - public void testIsNotGzipCompressedByContentType() throws Exception - { - GzipTester tester = new GzipTester(testingdir.getEmptyPathDir(), compressionType); - - // Prepare server file - int filesize = tester.getOutputBufferSize() * 4; - tester.prepareServerFile("file.mp3", filesize); - - // Add content servlet - tester.setContentServlet(DefaultServlet.class); - - try - { - tester.start(); - HttpTester.Response http = assertIsResponseNotGzipCompressed(tester, "GET", "file.mp3", filesize, HttpStatus.OK_200); - assertNull(http.get("Vary")); - } - finally - { - tester.stop(); - } - } - - @Test - public void testIsNotGzipCompressedByExcludedContentType() throws Exception - { - GzipTester tester = new GzipTester(testingdir.getEmptyPathDir(), compressionType); - - // Configure Gzip Handler - tester.getGzipHandler().addExcludedMimeTypes("text/plain"); - - // Prepare server file - int filesize = tester.getOutputBufferSize() * 4; - tester.prepareServerFile("test_quotes.txt", filesize); - - // Add content servlet - tester.setContentServlet(DefaultServlet.class); - - try - { - tester.start(); - HttpTester.Response http = assertIsResponseNotGzipCompressed(tester, "GET", "test_quotes.txt", filesize, HttpStatus.OK_200); - assertNull(http.get("Vary")); - } - finally - { - tester.stop(); - } - } - - @Test - public void testIsNotGzipCompressedByExcludedContentTypeWithCharset() throws Exception - { - GzipTester tester = new GzipTester(testingdir.getEmptyPathDir(), compressionType); - - // Configure Gzip Handler - tester.getGzipHandler().addExcludedMimeTypes("text/plain"); - - // Prepare server file - int filesize = tester.getOutputBufferSize() * 4; - tester.prepareServerFile("test_quotes.txt", filesize); - tester.addMimeType("txt", "text/plain;charset=UTF-8"); - - // Add content servlet - tester.setContentServlet(DefaultServlet.class); - - try - { - tester.start(); - HttpTester.Response http = assertIsResponseNotGzipCompressed(tester, "GET", "test_quotes.txt", filesize, HttpStatus.OK_200); - assertNull(http.get("Vary")); - } - finally - { - tester.stop(); - } - } - - @Test - public void testGzipCompressedByContentTypeWithEncoding() throws Exception - { - GzipTester tester = new GzipTester(testingdir.getEmptyPathDir(), compressionType); - tester.setContentServlet(HttpContentTypeWithEncoding.class); - tester.getGzipHandler().setMinGzipSize(16); - tester.getGzipHandler().addIncludedMimeTypes("text/plain"); - tester.getGzipHandler().setExcludedAgentPatterns(); - try - { - tester.start(); - HttpTester.Response http = tester.assertNonStaticContentIsResponseGzipCompressed("GET", "xxx", HttpContentTypeWithEncoding.COMPRESSED_CONTENT); - assertEquals("Accept-Encoding", http.get("Vary")); - } - finally - { - tester.stop(); - } - } - - @Test - public void testIsNotGzipCompressedByDeferredContentType() throws Exception - { - GzipTester tester = new GzipTester(testingdir.getEmptyPathDir(), compressionType); - - // Configure Gzip Handler - tester.getGzipHandler().addIncludedMimeTypes("text/plain"); - - // Prepare server file - int filesize = tester.getOutputBufferSize() * 4; - tester.prepareServerFile("file.mp3.deferred", filesize); - - // Add content servlet - tester.setContentServlet(GetServlet.class); - - try - { - tester.start(); - HttpTester.Response response = assertIsResponseNotGzipCompressed(tester, "GET", "file.mp3.deferred", filesize, HttpStatus.OK_200); - assertThat("Response[Vary]", response.get("Vary"), is(emptyOrNullString())); - } - finally - { - tester.stop(); - } - } - - @Test - public void testIsNotGzipCompressedHttpStatus() throws Exception - { - GzipTester tester = new GzipTester(testingdir.getEmptyPathDir(), compressionType); - - // Configure Gzip Handler - tester.getGzipHandler().addIncludedMimeTypes("text/plain"); - - // Test error code 204 - tester.setContentServlet(HttpStatusServlet.class); - - try - { - tester.start(); - - HttpTester.Response response = tester.executeRequest("GET", "/context/", 5, TimeUnit.SECONDS); - - assertThat("Response status", response.getStatus(), is(HttpStatus.NO_CONTENT_204)); - assertThat("Content-Encoding", response.get("Content-Encoding"), not(containsString(compressionType))); - } - finally - { - tester.stop(); - } - } - - @Test - public void testIsNotGzipCompressedHttpBadRequestStatus() throws Exception - { - GzipTester tester = new GzipTester(testingdir.getEmptyPathDir(), compressionType); - - // Configure Gzip Handler - tester.getGzipHandler().addIncludedMimeTypes("text/plain"); - - // Test error code 400 - tester.setContentServlet(HttpErrorServlet.class); - - try - { - tester.start(); - - HttpTester.Response response = tester.executeRequest("GET", "/context/", 5, TimeUnit.SECONDS); - - assertThat("Response status", response.getStatus(), is(HttpStatus.BAD_REQUEST_400)); - assertThat("Content-Encoding", response.get("Content-Encoding"), not(containsString(compressionType))); - - String content = tester.readResponse(response); - assertThat("Response content", content, is("error message")); - } - finally - { - tester.stop(); - } - } - - @Test - public void testUserAgentExclusion() throws Exception - { - GzipTester tester = new GzipTester(testingdir.getEmptyPathDir(), compressionType); - tester.setUserAgent("foo"); - - // Configure Gzip Handler - tester.getGzipHandler().addIncludedMimeTypes("text/plain"); - tester.getGzipHandler().setExcludedAgentPatterns("bar", "foo"); - - // Prepare server file - int filesize = tester.getOutputBufferSize() * 4; - tester.prepareServerFile("file.txt", filesize); - - // Add content servlet - tester.setContentServlet(DefaultServlet.class); - - try - { - tester.start(); - assertIsResponseNotGzipCompressed(tester, "GET", "file.txt", filesize, HttpStatus.OK_200); - } - finally - { - tester.stop(); - } - } - - @Test - public void testUserAgentExclusionDefault() throws Exception - { - GzipTester tester = new GzipTester(testingdir.getEmptyPathDir(), compressionType); - tester.setContentServlet(DefaultServlet.class); - tester.setUserAgent("Some MSIE 6.0 user-agent"); - - int filesize = tester.getOutputBufferSize() * 4; - tester.prepareServerFile("file.txt", filesize); - - try - { - tester.start(); - HttpTester.Response http = assertIsResponseNotGzipCompressed(tester, "GET", "file.txt", filesize, HttpStatus.OK_200); - assertEquals("Accept-Encoding, User-Agent", http.get("Vary")); - } - finally - { - tester.stop(); - } - } - - @Test - public void testUserAgentExclusionByExcludedAgentPatterns() throws Exception - { - GzipTester tester = new GzipTester(testingdir.getEmptyPathDir(), compressionType); - tester.setUserAgent("foo"); - - // Configure Gzip Handler - tester.getGzipHandler().setExcludedAgentPatterns("bar", "fo.*"); - - // Prepare server file - int filesize = tester.getOutputBufferSize() * 4; - tester.prepareServerFile("file.txt", filesize); - - // Set content servlet - tester.setContentServlet(DefaultServlet.class); - - try - { - tester.start(); - assertIsResponseNotGzipCompressed(tester, "GET", "file.txt", filesize, HttpStatus.OK_200); - } - finally - { - tester.stop(); - } - } - - @Test - public void testExcludePaths() throws Exception - { - GzipTester tester = new GzipTester(testingdir.getEmptyPathDir(), compressionType); - - // Configure Gzip Handler - tester.getGzipHandler().setExcludedPaths("*.txt"); - - // Prepare server file - int filesize = tester.getOutputBufferSize() * 4; - tester.prepareServerFile("file.txt", filesize); - - // Set content servlet - tester.setContentServlet(DefaultServlet.class); - - try - { - tester.start(); - assertIsResponseNotGzipCompressed(tester, "GET", "file.txt", filesize, HttpStatus.OK_200); - } - finally - { - tester.stop(); - } - } - - @Test - public void testIncludedPaths() throws Exception - { - GzipTester tester = new GzipTester(testingdir.getEmptyPathDir(), compressionType); - - // Configure Gzip Handler - tester.getGzipHandler().setExcludedPaths(tester.getContextPath() + "/bad.txt"); - tester.getGzipHandler().setIncludedPaths("*.txt"); - - // Prepare server file - int filesize = tester.getOutputBufferSize() * 4; - tester.prepareServerFile("file.txt", filesize); - tester.prepareServerFile("bad.txt", filesize); - - // Set content servlet - tester.setContentServlet(DefaultServlet.class); - - try - { - tester.start(); - tester.assertIsResponseGzipCompressed("GET", "file.txt"); - } - finally - { - tester.stop(); - } - - try - { - tester.start(); - assertIsResponseNotGzipCompressed(tester, "GET", "bad.txt", filesize, HttpStatus.OK_200); - } - finally - { - tester.stop(); - } - } - - public HttpTester.Response assertIsResponseNotGzipCompressed(GzipTester tester, String method, String filename, int expectedFilesize, int status) - throws Exception - { - HttpTester.Response response = tester.executeRequest(method, "/context/" + filename, 5, TimeUnit.SECONDS); - - assertThat("Response status", response.getStatus(), is(status)); - assertThat("Content-Encoding", response.get("Content-Encoding"), not(containsString(compressionType))); - - assertResponseContent(tester, response, status, filename, expectedFilesize); - - return response; - } - - private void assertResponseContent(GzipTester tester, HttpTester.Response response, int status, String filename, int expectedFilesize) throws IOException, - UnsupportedEncodingException - { - if (expectedFilesize >= 0) - { - assertThat("filename", filename, notNullValue()); - assertThat("Response contentBytes.length", response.getContentBytes().length, is(expectedFilesize)); - String contentLength = response.get("Content-Length"); - if (StringUtil.isNotBlank(contentLength)) - { - assertThat("Content-Length", response.get("Content-Length"), is(Integer.toString(expectedFilesize))); - } - - if (status >= 200 && status < 300) - { - assertThat("ETag", response.get("ETAG"), startsWith("W/")); - } - - File serverFile = testingdir.getPathFile(filename).toFile(); - String expectedResponse = IO.readToString(serverFile); - - String actual = tester.readResponse(response); - assertEquals(expectedResponse, actual, "Expected response equals actual response"); - } - } - - @Test - public void testIsNotGzipCompressedSVGZ() throws Exception - { - GzipTester tester = new GzipTester(testingdir.getEmptyPathDir(), compressionType); - - tester.setContentServlet(DefaultServlet.class); - tester.copyTestServerFile("test.svgz"); - - try - { - tester.start(); - tester.assertIsResponseNotGzipFiltered("test.svgz", "test.svgz.sha1", "image/svg+xml", "gzip"); - } - finally - { - tester.stop(); - } - } -} diff --git a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/GzipHandlerNoReCompressTest.java b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/GzipHandlerNoReCompressTest.java new file mode 100644 index 00000000000..bfb00cc6a60 --- /dev/null +++ b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/GzipHandlerNoReCompressTest.java @@ -0,0 +1,147 @@ +// +// ======================================================================== +// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under +// the terms of the Eclipse Public License 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0 +// +// This Source Code may also be made available under the following +// Secondary Licenses when the conditions for such availability set +// forth in the Eclipse Public License, v. 2.0 are satisfied: +// the Apache License v2.0 which is available at +// https://www.apache.org/licenses/LICENSE-2.0 +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +package org.eclipse.jetty.servlets; + +import java.nio.ByteBuffer; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Locale; +import java.util.concurrent.TimeUnit; +import java.util.stream.Stream; + +import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.jetty.http.HttpVersion; +import org.eclipse.jetty.http.tools.HttpTester; +import org.eclipse.jetty.server.LocalConnector; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.handler.gzip.GzipHandler; +import org.eclipse.jetty.servlet.DefaultServlet; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.toolchain.test.FS; +import org.eclipse.jetty.toolchain.test.MavenTestingUtils; +import org.eclipse.jetty.toolchain.test.Sha1Sum; +import org.eclipse.jetty.util.IO; +import org.eclipse.jetty.util.component.LifeCycle; +import org.eclipse.jetty.util.resource.PathResource; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.nullValue; + +/** + * Tests {@link GzipHandler} in combination with {@link DefaultServlet} for ability to configure {@link GzipHandler} to + * ignore recompress situations from upstream. + */ +public class GzipHandlerNoReCompressTest extends AbstractGzipTest +{ + public static Stream scenarios() + { + return Stream.of( + Arguments.of("test_quotes.gz", "application/gzip"), + Arguments.of("test_quotes.br", "application/brotli"), + Arguments.of("test_quotes.bz2", "application/bzip2"), + Arguments.of("test_quotes.zip", "application/zip"), + Arguments.of("test_quotes.rar", "application/x-rar-compressed"), + // Some images (common first) + Arguments.of("jetty_logo.png", "image/png"), + Arguments.of("jetty_logo.gif", "image/gif"), + Arguments.of("jetty_logo.jpeg", "image/jpeg"), + Arguments.of("jetty_logo.jpg", "image/jpeg"), + // Lesser encountered images (usually found being requested from non-browser clients) + Arguments.of("jetty_logo.bmp", "image/bmp"), + Arguments.of("jetty_logo.tif", "image/tiff"), + Arguments.of("jetty_logo.tiff", "image/tiff"), + Arguments.of("jetty_logo.xcf", "image/xcf"), + Arguments.of("jetty_logo.jp2", "image/jpeg2000") + ); + } + + private Server server; + + @AfterEach + public void stopServer() + { + LifeCycle.stop(server); + } + + @ParameterizedTest + @MethodSource("scenarios") + public void testNotGzipAlreadyCompressed(String fileName, String expectedContentType) throws Exception + { + GzipHandler gzipHandler = new GzipHandler(); + + server = new Server(); + LocalConnector localConnector = new LocalConnector(server); + server.addConnector(localConnector); + + Path contextDir = workDir.resolve("context"); + FS.ensureDirExists(contextDir); + + ServletContextHandler servletContextHandler = new ServletContextHandler(); + servletContextHandler.setContextPath("/context"); + servletContextHandler.setBaseResource(new PathResource(contextDir)); + servletContextHandler.addServlet(TestStaticMimeTypeServlet.class, "/*"); + servletContextHandler.insertHandler(gzipHandler); + + server.setHandler(servletContextHandler); + + // Prepare Server File + Path testResource = MavenTestingUtils.getTestResourcePath(fileName); + Path file = contextDir.resolve(fileName); + IO.copy(testResource.toFile(), file.toFile()); + String expectedSha1Sum = Sha1Sum.loadSha1(MavenTestingUtils.getTestResourceFile(fileName + ".sha1")).toUpperCase(Locale.ENGLISH); + int fileSize = (int)Files.size(file); + + server.start(); + + // Setup request + HttpTester.Request request = HttpTester.newRequest(); + request.setMethod("GET"); + request.setVersion(HttpVersion.HTTP_1_1); + request.setHeader("Host", "tester"); + request.setHeader("Connection", "close"); + request.setHeader("Accept-Encoding", "gzip"); + request.setURI("/context/" + fileName); + + // Issue request + ByteBuffer rawResponse = localConnector.getResponse(request.generate(), 5, TimeUnit.SECONDS); + + // Parse response + HttpTester.Response response = HttpTester.parseResponse(rawResponse); + + assertThat("Response status", response.getStatus(), is(HttpStatus.OK_200)); + + // Response Headers check + assertThat("Response[Content-Type]", response.get("Content-Type"), is(expectedContentType)); + assertThat("Response[Content-Encoding]", response.get("Content-Encoding"), not(containsString("gzip"))); + assertThat("Response[Vary]", response.get("Vary"), is(nullValue())); + + // Response Content checks + UncompressedMetadata metadata = parseResponseContent(response); + assertThat("Response Content Length", metadata.contentLength, is(fileSize)); + assertThat("(Uncompressed) Content Length", metadata.uncompressedSize, is(fileSize)); + assertThat("(Uncompressed) Content Hash", metadata.uncompressedSha1Sum, is(expectedSha1Sum)); + } +} diff --git a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/GzipHandlerTest.java b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/GzipHandlerTest.java new file mode 100644 index 00000000000..980b254f672 --- /dev/null +++ b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/GzipHandlerTest.java @@ -0,0 +1,226 @@ +// +// ======================================================================== +// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under +// the terms of the Eclipse Public License 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0 +// +// This Source Code may also be made available under the following +// Secondary Licenses when the conditions for such availability set +// forth in the Eclipse Public License, v. 2.0 are satisfied: +// the Apache License v2.0 which is available at +// https://www.apache.org/licenses/LICENSE-2.0 +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +package org.eclipse.jetty.servlets; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.concurrent.TimeUnit; +import javax.servlet.ServletException; +import javax.servlet.ServletOutputStream; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.jetty.http.HttpVersion; +import org.eclipse.jetty.http.tools.HttpTester; +import org.eclipse.jetty.server.LocalConnector; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.handler.gzip.GzipHandler; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.util.component.LifeCycle; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; + +public class GzipHandlerTest extends AbstractGzipTest +{ + private Server server; + + @AfterEach + public void stopServer() + { + LifeCycle.stop(server); + } + + @Test + public void testGzipCompressedByContentTypeWithEncoding() throws Exception + { + GzipHandler gzipHandler = new GzipHandler(); + gzipHandler.setMinGzipSize(32); + gzipHandler.addIncludedMimeTypes("text/plain"); + gzipHandler.setExcludedAgentPatterns(); + + server = new Server(); + LocalConnector localConnector = new LocalConnector(server); + server.addConnector(localConnector); + + ServletContextHandler contextHandler = new ServletContextHandler(); + contextHandler.setContextPath("/context"); + contextHandler.addServlet(HttpContentTypeWithEncodingServlet.class, "/*"); + + gzipHandler.setHandler(contextHandler); + + server.setHandler(gzipHandler); + server.start(); + + // Setup request + HttpTester.Request request = HttpTester.newRequest(); + request.setMethod("GET"); + request.setVersion(HttpVersion.HTTP_1_1); + request.setHeader("Host", "tester"); + request.setHeader("Connection", "close"); + request.setHeader("Accept-Encoding", "gzip"); + request.setURI("/context/xxx"); + + // Issue request + ByteBuffer rawResponse = localConnector.getResponse(request.generate(), 5, TimeUnit.SECONDS); + + // Parse response + HttpTester.Response response = HttpTester.parseResponse(rawResponse); + + assertThat("Response status", response.getStatus(), is(HttpStatus.OK_200)); + + // Response Content-Encoding check + assertThat("Response[Content-Encoding]", response.get("Content-Encoding"), containsString("gzip")); + assertThat("Response[Vary]", response.get("Vary"), is("Accept-Encoding")); + + // Response Content checks + UncompressedMetadata metadata = parseResponseContent(response); + assertThat("Response[Content] raw length vs uncompressed length", metadata.contentLength, not(is(metadata.uncompressedSize))); + assertThat("(Uncompressed) Content", metadata.getContentUTF8(), is(HttpContentTypeWithEncodingServlet.CONTENT)); + } + + public static class HttpContentTypeWithEncodingServlet extends HttpServlet + { + public static final String CONTENT = "

COMPRESSIBLE CONTENT

" + + "

" + + "This content must be longer than the default min gzip length, which is " + GzipHandler.DEFAULT_MIN_GZIP_SIZE + " bytes. " + + "The moon is blue to a fish in love.
" + + "How now brown cow.
" + + "The quick brown fox jumped over the lazy dog.
" + + "A woman needs a man like a fish needs a bicycle!" + + "

" + + ""; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException + { + resp.setContentType("text/plain;charset=UTF8"); + resp.setStatus(200); + ServletOutputStream out = resp.getOutputStream(); + out.print(CONTENT); + } + } + + @Test + public void testIsNotGzipCompressedHttpStatus() throws Exception + { + GzipHandler gzipHandler = new GzipHandler(); + gzipHandler.addIncludedMimeTypes("text/plain"); + + server = new Server(); + LocalConnector localConnector = new LocalConnector(server); + server.addConnector(localConnector); + + ServletContextHandler contextHandler = new ServletContextHandler(); + contextHandler.setContextPath("/context"); + contextHandler.addServlet(HttpStatusServlet.class, "/*"); + + gzipHandler.setHandler(contextHandler); + + server.setHandler(gzipHandler); + server.start(); + + // Setup request + HttpTester.Request request = HttpTester.newRequest(); + request.setMethod("GET"); + request.setVersion(HttpVersion.HTTP_1_1); + request.setHeader("Host", "tester"); + request.setHeader("Connection", "close"); + request.setHeader("Accept-Encoding", "gzip"); + request.setURI("/context/xxx"); + + // Issue request + ByteBuffer rawResponse = localConnector.getResponse(request.generate(), 5, TimeUnit.SECONDS); + + // Parse response + HttpTester.Response response = HttpTester.parseResponse(rawResponse); + + assertThat("Response status", response.getStatus(), is(HttpStatus.NO_CONTENT_204)); + + // Response Content-Encoding check + assertThat("Response[Content-Encoding]", response.get("Content-Encoding"), not(containsString("gzip"))); + } + + public static class HttpStatusServlet extends HttpServlet + { + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException + { + resp.setStatus(HttpServletResponse.SC_NO_CONTENT); + resp.setHeader("ETag", "W/\"204\""); + } + } + + @Test + public void testIsNotGzipCompressedHttpBadRequestStatus() throws Exception + { + GzipHandler gzipHandler = new GzipHandler(); + gzipHandler.addIncludedMimeTypes("text/plain"); + + server = new Server(); + LocalConnector localConnector = new LocalConnector(server); + server.addConnector(localConnector); + + ServletContextHandler contextHandler = new ServletContextHandler(); + contextHandler.setContextPath("/context"); + contextHandler.addServlet(HttpErrorServlet.class, "/*"); + + gzipHandler.setHandler(contextHandler); + + server.setHandler(gzipHandler); + server.start(); + + // Setup request + HttpTester.Request request = HttpTester.newRequest(); + request.setMethod("GET"); + request.setVersion(HttpVersion.HTTP_1_1); + request.setHeader("Host", "tester"); + request.setHeader("Connection", "close"); + request.setHeader("Accept-Encoding", "gzip"); + request.setURI("/context/xxx"); + + // Issue request + ByteBuffer rawResponse = localConnector.getResponse(request.generate(), 5, TimeUnit.SECONDS); + + // Parse response + HttpTester.Response response = HttpTester.parseResponse(rawResponse); + + assertThat("Response status", response.getStatus(), is(HttpStatus.BAD_REQUEST_400)); + + // Response Content-Encoding check + assertThat("Response[Content-Encoding]", response.get("Content-Encoding"), not(containsString("gzip"))); + assertThat("Response Content", response.getContent(), is("error message")); + } + + public static class HttpErrorServlet extends HttpServlet + { + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException + { + resp.getOutputStream().write("error message".getBytes()); + resp.setStatus(HttpServletResponse.SC_BAD_REQUEST); + } + } +} diff --git a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/GzipTester.java b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/GzipTester.java deleted file mode 100644 index b3769c26cee..00000000000 --- a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/GzipTester.java +++ /dev/null @@ -1,635 +0,0 @@ -// -// ======================================================================== -// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others. -// -// This program and the accompanying materials are made available under -// the terms of the Eclipse Public License 2.0 which is available at -// https://www.eclipse.org/legal/epl-2.0 -// -// This Source Code may also be made available under the following -// Secondary Licenses when the conditions for such availability set -// forth in the Eclipse Public License, v. 2.0 are satisfied: -// the Apache License v2.0 which is available at -// https://www.apache.org/licenses/LICENSE-2.0 -// -// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 -// ======================================================================== -// - -package org.eclipse.jetty.servlets; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.UnsupportedEncodingException; -import java.nio.file.Path; -import java.security.DigestOutputStream; -import java.security.MessageDigest; -import java.util.EnumSet; -import java.util.Enumeration; -import java.util.concurrent.TimeUnit; -import java.util.zip.GZIPInputStream; -import java.util.zip.Inflater; -import java.util.zip.InflaterInputStream; - -import jakarta.servlet.DispatcherType; -import jakarta.servlet.Servlet; -import jakarta.servlet.http.HttpServletResponse; -import org.eclipse.jetty.http.DateGenerator; -import org.eclipse.jetty.http.HttpHeader; -import org.eclipse.jetty.http.tools.HttpTester; -import org.eclipse.jetty.server.HttpConnectionFactory; -import org.eclipse.jetty.server.handler.gzip.GzipHandler; -import org.eclipse.jetty.servlet.FilterHolder; -import org.eclipse.jetty.servlet.ServletHolder; -import org.eclipse.jetty.servlet.ServletTester; -import org.eclipse.jetty.toolchain.test.FS; -import org.eclipse.jetty.toolchain.test.IO; -import org.eclipse.jetty.toolchain.test.MavenTestingUtils; -import org.eclipse.jetty.toolchain.test.Sha1Sum; -import org.hamcrest.Matchers; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.containsString; -import static org.hamcrest.Matchers.emptyOrNullString; -import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.notNullValue; -import static org.hamcrest.Matchers.nullValue; -import static org.junit.jupiter.api.Assertions.assertEquals; - -public class GzipTester -{ - private static final Logger LOG = LoggerFactory.getLogger(GzipTester.class); - - public static class ContentMetadata - { - public final long size; - public final String sha1; - - public ContentMetadata(long size, String sha1checksum) - { - this.size = size; - this.sha1 = sha1checksum; - } - } - - private String encoding = "ISO8859_1"; - private String userAgent = null; - private final GzipHandler gzipHandler = new GzipHandler(); - private final ServletTester tester = new ServletTester("/context"); - private Path testdir; - private String accept; - private String compressionType; - - public GzipTester(Path testingdir, String compressionType) - { - this(testingdir, compressionType, compressionType); - } - - public GzipTester(Path testingdir, String compressionType, String accept) - { - this.testdir = testingdir; - this.compressionType = compressionType; - this.accept = accept; - this.tester.getServer().insertHandler(gzipHandler); - } - - public String getContextPath() - { - return tester.getContextPath(); - } - - public GzipHandler getGzipHandler() - { - return gzipHandler; - } - - public int getOutputBufferSize() - { - return tester.getConnector().getConnectionFactory(HttpConnectionFactory.class).getHttpConfiguration().getOutputBufferSize(); - } - - public ContentMetadata getResponseMetadata(HttpTester.Response response) throws Exception - { - long size = response.getContentBytes().length; - - String contentEncoding = response.get("Content-Encoding"); - - ByteArrayInputStream bais = null; - InputStream in = null; - DigestOutputStream digester = null; - ByteArrayOutputStream uncompressedStream = null; - try - { - MessageDigest digest = MessageDigest.getInstance("SHA1"); - bais = new ByteArrayInputStream(response.getContentBytes()); - - if (contentEncoding == null) - { - LOG.debug("No response content-encoding"); - in = new PassThruInputStream(bais); - } - else if (contentEncoding.contains(GzipHandler.GZIP)) - { - in = new GZIPInputStream(bais); - } - else if (contentEncoding.contains(GzipHandler.DEFLATE)) - { - in = new InflaterInputStream(bais, new Inflater(true)); - } - else - { - assertThat("Unexpected response content-encoding", contentEncoding, is(emptyOrNullString())); - } - - uncompressedStream = new ByteArrayOutputStream((int)size); - - digester = new DigestOutputStream(uncompressedStream, digest); - IO.copy(in, digester); - - byte[] output = uncompressedStream.toByteArray(); - String actualSha1Sum = Hex.asHex(digest.digest()); - return new ContentMetadata(output.length, actualSha1Sum); - } - finally - { - IO.close(digester); - IO.close(in); - IO.close(bais); - IO.close(uncompressedStream); - } - } - - public HttpTester.Response assertIsResponseGzipCompressed(String method, String filename) throws Exception - { - return assertIsResponseGzipCompressed(method, filename, filename, -1); - } - - public HttpTester.Response assertIsResponseGzipCompressed(String method, String filename, long ifmodifiedsince) throws Exception - { - return assertIsResponseGzipCompressed(method, filename, filename, ifmodifiedsince); - } - - public HttpTester.Response assertIsResponseGzipCompressed(String method, String requestedFilename, String serverFilename) throws Exception - { - return assertIsResponseGzipCompressed(method, requestedFilename, serverFilename, -1); - } - - public HttpTester.Response assertNonStaticContentIsResponseGzipCompressed(String method, String path, String expected) throws Exception - { - HttpTester.Request request = HttpTester.newRequest(); - HttpTester.Response response; - - request.setMethod(method); - request.setVersion("HTTP/1.0"); - request.setHeader("Host", "tester"); - request.setHeader("Accept-Encoding", accept); - - if (this.userAgent != null) - request.setHeader("User-Agent", this.userAgent); - request.setURI("/context/" + path); - - // Issue the request - response = HttpTester.parseResponse(tester.getResponses(request.generate())); - - int qindex = compressionType.indexOf(";"); - if (qindex < 0) - assertThat("Response.header[Content-Encoding]", response.get("Content-Encoding"), containsString(compressionType)); - else - assertThat("Response.header[Content-Encoding]", response.get("Content-Encoding"), containsString(compressionType.substring(0, qindex))); - - ByteArrayInputStream bais = null; - InputStream in = null; - ByteArrayOutputStream out = null; - String actual = null; - - try - { - bais = new ByteArrayInputStream(response.getContentBytes()); - if (compressionType.startsWith(GzipHandler.GZIP)) - { - in = new GZIPInputStream(bais); - } - else if (compressionType.startsWith(GzipHandler.DEFLATE)) - { - in = new InflaterInputStream(bais, new Inflater(true)); - } - out = new ByteArrayOutputStream(); - IO.copy(in, out); - - actual = out.toString(encoding); - assertThat("Uncompressed contents", actual, equalTo(expected)); - } - finally - { - IO.close(out); - IO.close(in); - IO.close(bais); - } - - return response; - } - - public HttpTester.Response assertIsResponseGzipCompressed(String method, String requestedFilename, String serverFilename, long ifmodifiedsince) - throws Exception - { - HttpTester.Request request = HttpTester.newRequest(); - HttpTester.Response response; - - request.setMethod(method); - request.setVersion("HTTP/1.0"); - request.setHeader("Host", "tester"); - request.setHeader("Accept-Encoding", compressionType); - if (ifmodifiedsince > 0) - request.setHeader(HttpHeader.IF_MODIFIED_SINCE.asString(), DateGenerator.formatDate(ifmodifiedsince)); - if (this.userAgent != null) - request.setHeader("User-Agent", this.userAgent); - request.setURI("/context/" + requestedFilename); - - // Issue the request - response = HttpTester.parseResponse(tester.getResponses(request.generate())); - - // Assert the response headers - // assertThat("Response.status",response.getStatus(),is(HttpServletResponse.SC_OK)); - - // Response headers should have either a Transfer-Encoding indicating chunked OR a Content-Length - /* - * TODO need to check for the 3rd option of EOF content. To do this properly you might need to look at both HTTP/1.1 and HTTP/1.0 requests String - * contentLength = response.get("Content-Length"); String transferEncoding = response.get("Transfer-Encoding"); - * - * boolean chunked = (transferEncoding != null) && (transferEncoding.indexOf("chunk") >= 0); if(!chunked) { - * assertThat("Response.header[Content-Length]",contentLength,notNullValue()); } else { - * assertThat("Response.header[Transfer-Encoding]",transferEncoding,notNullValue()); } - */ - - int qindex = compressionType.indexOf(";"); - if (qindex < 0) - assertThat("Response.header[Content-Encoding]", response.get("Content-Encoding"), containsString(compressionType)); - else - assertThat("Response.header[Content-Encoding]", response.get("Content-Encoding"), containsString(compressionType.substring(0, qindex))); - - assertThat(response.get("ETag"), Matchers.startsWith("W/")); - - // Assert that the decompressed contents are what we expect. - File serverFile = testdir.resolve(FS.separators(serverFilename)).toFile(); - String expected = IO.readToString(serverFile); - String actual = null; - - ByteArrayInputStream bais = null; - InputStream in = null; - ByteArrayOutputStream out = null; - try - { - bais = new ByteArrayInputStream(response.getContentBytes()); - if (compressionType.startsWith(GzipHandler.GZIP)) - { - in = new GZIPInputStream(bais); - } - else if (compressionType.startsWith(GzipHandler.DEFLATE)) - { - in = new InflaterInputStream(bais, new Inflater(true)); - } - out = new ByteArrayOutputStream(); - IO.copy(in, out); - - actual = out.toString(encoding); - assertThat("Uncompressed contents", actual, equalTo(expected)); - } - finally - { - IO.close(out); - IO.close(in); - IO.close(bais); - } - - return response; - } - - public HttpTester.Response assertIsResponseNotModified(String method, String requestedFilename, long ifmodifiedsince) throws Exception - { - HttpTester.Request request = HttpTester.newRequest(); - HttpTester.Response response; - - request.setMethod(method); - request.setVersion("HTTP/1.0"); - request.setHeader("Host", "tester"); - request.setHeader("Accept-Encoding", compressionType); - if (ifmodifiedsince > 0) - request.setHeader(HttpHeader.IF_MODIFIED_SINCE.asString(), DateGenerator.formatDate(ifmodifiedsince)); - if (this.userAgent != null) - request.setHeader("User-Agent", this.userAgent); - request.setURI("/context/" + requestedFilename); - - // Issue the request - response = HttpTester.parseResponse(tester.getResponses(request.generate())); - - assertThat(response.getStatus(), Matchers.equalTo(304)); - assertThat(response.get("ETag"), Matchers.startsWith("W/")); - - return response; - } - - /** - * Makes sure that the response contains an unfiltered file contents. - *

- * This is used to test exclusions and passthroughs in the GzipHandler. - *

- * An example is to test that it is possible to configure GzipFilter to not recompress content that shouldn't be compressed by the GzipFilter. - * - * @param requestedFilename the filename used to on the GET request,. - * @param testResourceSha1Sum the sha1sum file that contains the SHA1SUM checksum that will be used to verify that the response contents are what is intended. - * @param expectedContentType the expected content type - * @throws Exception on test failure - */ - public void assertIsResponseNotGziped(String requestedFilename, String testResourceSha1Sum, String expectedContentType) throws Exception - { - assertIsResponseNotGzipFiltered(requestedFilename, testResourceSha1Sum, expectedContentType, null); - } - - /** - * Makes sure that the response contains an unfiltered file contents. - *

- * This is used to test exclusions and passthroughs in the GzipHandler. - *

- * An example is to test that it is possible to configure GzipFilter to not recompress content that shouldn't be compressed by the GzipFilter. - * - * @param requestedFilename the filename used to on the GET request,. - * @param testResourceSha1Sum the sha1sum file that contains the SHA1SUM checksum that will be used to verify that the response contents are what is intended. - * @param expectedContentType the expected content type - * @param expectedContentEncoding can be non-null in some circumstances, eg when dealing with pre-gzipped .svgz files - * @throws Exception on test failure - */ - public void assertIsResponseNotGzipFiltered(String requestedFilename, String testResourceSha1Sum, String expectedContentType, String expectedContentEncoding) - throws Exception - { - HttpTester.Request request = HttpTester.newRequest(); - HttpTester.Response response; - - request.setMethod("GET"); - request.setVersion("HTTP/1.0"); - request.setHeader("Host", "tester"); - request.setHeader("Accept-Encoding", compressionType); - if (this.userAgent != null) - request.setHeader("User-Agent", this.userAgent); - request.setURI("/context/" + requestedFilename); - - // Issue the request - response = HttpTester.parseResponse(tester.getResponses(request.generate())); - - dumpHeaders(requestedFilename + " / Response Headers", response); - - // Assert the response headers - String prefix = requestedFilename + " / Response"; - assertThat(prefix + ".status", response.getStatus(), is(HttpServletResponse.SC_OK)); - assertThat(prefix + ".header[Content-Length]", response.get("Content-Length"), notNullValue()); - assertThat(prefix + ".header[Content-Encoding] (should not be recompressed by GzipHandler)", response.get("Content-Encoding"), - expectedContentEncoding == null ? nullValue() : notNullValue()); - if (expectedContentEncoding != null) - assertThat(prefix + ".header[Content-Encoding]", response.get("Content-Encoding"), is(expectedContentEncoding)); - assertThat(prefix + ".header[Content-Type] (should have a Content-Type associated with it)", response.get("Content-Type"), notNullValue()); - assertThat(prefix + ".header[Content-Type]", response.get("Content-Type"), is(expectedContentType)); - - assertThat(response.get("ETAG"), Matchers.startsWith("W/")); - - ByteArrayInputStream bais = null; - DigestOutputStream digester = null; - try - { - MessageDigest digest = MessageDigest.getInstance("SHA1"); - bais = new ByteArrayInputStream(response.getContentBytes()); - digester = new DigestOutputStream(new NoOpOutputStream(), digest); - IO.copy(bais, digester); - - String actualSha1Sum = Hex.asHex(digest.digest()); - File sha1File = MavenTestingUtils.getTestResourceFile(testResourceSha1Sum); - String expectedSha1Sum = Sha1Sum.loadSha1(sha1File); - assertEquals(expectedSha1Sum, actualSha1Sum, requestedFilename + " / SHA1Sum of content"); - } - finally - { - IO.close(digester); - IO.close(bais); - } - } - - private void dumpHeaders(String prefix, HttpTester.Message message) - { - LOG.debug("dumpHeaders: {}", prefix); - Enumeration names = message.getFieldNames(); - while (names.hasMoreElements()) - { - String name = names.nextElement(); - String value = message.get(name); - LOG.debug("dumpHeaders: {} = {}", name, value); - } - } - - public HttpTester.Response executeRequest(String method, String path, int idleFor, TimeUnit idleUnit) throws Exception - { - HttpTester.Request request = HttpTester.newRequest(); - - request.setMethod(method); - request.setVersion("HTTP/1.1"); - request.setHeader("Host", "tester"); - request.setHeader("Accept-Encoding", accept); - request.setHeader("Connection", "close"); - - if (this.userAgent != null) - { - request.setHeader("User-Agent", this.userAgent); - } - - request.setURI(path); - - // Issue the request - return HttpTester.parseResponse(tester.getResponses(request.generate(), idleFor, idleUnit)); - } - - public String readResponse(HttpTester.Response response) throws IOException, UnsupportedEncodingException - { - String actual = null; - InputStream in = null; - ByteArrayOutputStream out = null; - try - { - byte[] content = response.getContentBytes(); - if (content != null) - actual = new String(response.getContentBytes(), encoding); - else - actual = ""; - } - finally - { - IO.close(out); - IO.close(in); - } - return actual; - } - - /** - * Generate string content of arbitrary length. - * - * @param length the length of the string to generate. - * @return the string content. - */ - public String generateContent(int length) - { - StringBuilder builder = new StringBuilder(); - do - { - builder.append("Lorem ipsum dolor sit amet, consectetur adipiscing elit. In quis felis nunc.\n"); - builder.append("Quisque suscipit mauris et ante auctor ornare rhoncus lacus aliquet. Pellentesque\n"); - builder.append("habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas.\n"); - builder.append("Vestibulum sit amet felis augue, vel convallis dolor. Cras accumsan vehicula diam\n"); - builder.append("at faucibus. Etiam in urna turpis, sed congue mi. Morbi et lorem eros. Donec vulputate\n"); - builder.append("velit in risus suscipit lobortis. Aliquam id urna orci, nec sollicitudin ipsum.\n"); - builder.append("Cras a orci turpis. Donec suscipit vulputate cursus. Mauris nunc tellus, fermentum\n"); - builder.append("eu auctor ut, mollis at diam. Quisque porttitor ultrices metus, vitae tincidunt massa\n"); - builder.append("sollicitudin a. Vivamus porttitor libero eget purus hendrerit cursus. Integer aliquam\n"); - builder.append("consequat mauris quis luctus. Cras enim nibh, dignissim eu faucibus ac, mollis nec neque.\n"); - builder.append("Aliquam purus mauris, consectetur nec convallis lacinia, porta sed ante. Suspendisse\n"); - builder.append("et cursus magna. Donec orci enim, molestie a lobortis eu, imperdiet vitae neque.\n"); - } - while (builder.length() < length); - - // Make sure we are exactly at requested length. (truncate the extra) - if (builder.length() > length) - { - builder.setLength(length); - } - - return builder.toString(); - } - - public String getEncoding() - { - return encoding; - } - - /** - * Create a file on the server resource path of a specified filename and size. - * - * @param filename the filename to create - * @param filesize the file size to create (Note: this isn't suitable for creating large multi-megabyte files) - * @return the prepared file - * @throws IOException if unable to create file - */ - public File prepareServerFile(String filename, int filesize) throws IOException - { - File dir = testdir.toFile(); - File testFile = new File(dir, filename); - // Make sure we have a uniq filename (to work around windows File.delete bug) - int i = 0; - while (testFile.exists()) - { - testFile = new File(dir, (i++) + "-" + filename); - } - - FileOutputStream fos = null; - ByteArrayInputStream in = null; - try - { - fos = new FileOutputStream(testFile, false); - in = new ByteArrayInputStream(generateContent(filesize).getBytes(encoding)); - IO.copy(in, fos); - return testFile; - } - finally - { - IO.close(in); - IO.close(fos); - } - } - - /** - * Copy a src/test/resource file into the server tree for eventual serving. - * - * @param filename the filename to look for in src/test/resources - * @throws IOException if unable to copy file - */ - public void copyTestServerFile(String filename) throws IOException - { - File srcFile = MavenTestingUtils.getTestResourceFile(filename); - File testFile = testdir.resolve(FS.separators(filename)).toFile(); - - IO.copy(srcFile, testFile); - } - - /** - * Set the servlet that provides content for the GzipHandler in being tested. - * - * @param servletClass the servlet that will provide content. - * @throws IOException if unable to set content servlet - */ - public void setContentServlet(Class servletClass) throws IOException - { - String resourceBase = testdir.toString(); - tester.setContextPath("/context"); - tester.setResourceBase(resourceBase); - ServletHolder servletHolder = tester.addServlet(servletClass, "/"); - servletHolder.setInitParameter("baseDir", resourceBase); - servletHolder.setInitParameter("etags", "true"); - } - - public void setEncoding(String encoding) - { - this.encoding = encoding; - } - - public void setUserAgent(String ua) - { - this.userAgent = ua; - } - - public void addMimeType(String extension, String mimetype) - { - this.tester.getContext().getMimeTypes().addMimeMapping(extension, mimetype); - } - - /** - * Add an arbitrary filter to the test case. - * - * @param holder the filter to add - * @param pathSpec the path spec for this filter - * @param dispatches the set of {@link DispatcherType} to associate with this filter - * @throws IOException if unable to add filter - */ - public void addFilter(FilterHolder holder, String pathSpec, EnumSet dispatches) throws IOException - { - tester.addFilter(holder, pathSpec, dispatches); - } - - public void start() throws Exception - { - assertThat("No servlet defined yet. Did you use #setContentServlet()?", tester, notNullValue()); - - if (LOG.isDebugEnabled()) - { - tester.dumpStdErr(); - } - tester.start(); - } - - public void stop() - { - // NOTE: Do not cleanup the workDir. Failures can't be diagnosed if you do that. - // IO.delete(workDir.getDir()): - try - { - tester.stop(); - } - catch (Exception e) - { - // Don't toss this out into Junit as this would be the last exception - // that junit will report as being the cause of the test failure. - // when in reality, the earlier setup issue is the real cause. - e.printStackTrace(System.err); - } - } -} diff --git a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/Hex.java b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/Hex.java deleted file mode 100644 index 1b491baae23..00000000000 --- a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/Hex.java +++ /dev/null @@ -1,76 +0,0 @@ -// -// ======================================================================== -// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others. -// -// This program and the accompanying materials are made available under -// the terms of the Eclipse Public License 2.0 which is available at -// https://www.eclipse.org/legal/epl-2.0 -// -// This Source Code may also be made available under the following -// Secondary Licenses when the conditions for such availability set -// forth in the Eclipse Public License, v. 2.0 are satisfied: -// the Apache License v2.0 which is available at -// https://www.apache.org/licenses/LICENSE-2.0 -// -// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 -// ======================================================================== -// - -package org.eclipse.jetty.servlets; - -public final class Hex -{ - private static final char[] hexcodes = "0123456789abcdef".toCharArray(); - - public static byte[] asByteArray(String id, int size) - { - if ((id.length() < 0) || (id.length() > (size * 2))) - { - throw new IllegalArgumentException(String.format("Invalid ID length of <%d> expected range of <0> to <%d>", id.length(), (size * 2))); - } - - byte[] buf = new byte[size]; - byte hex; - int len = id.length(); - - int idx = (int)Math.floor(((size * 2) - (double)len) / 2); - int i = 0; - if ((len % 2) != 0) - { // deal with odd numbered chars - i -= 1; - } - - for (; i < len; i++) - { - hex = 0; - if (i >= 0) - { - hex = (byte)(Character.digit(id.charAt(i), 16) << 4); - } - i++; - hex += (byte)(Character.digit(id.charAt(i), 16)); - - buf[idx] = hex; - idx++; - } - - return buf; - } - - public static String asHex(byte[] buf) - { - int len = buf.length; - char[] out = new char[len * 2]; - for (int i = 0; i < len; i++) - { - out[i * 2] = hexcodes[(buf[i] & 0xF0) >> 4]; - out[(i * 2) + 1] = hexcodes[(buf[i] & 0x0F)]; - } - return String.valueOf(out); - } - - private Hex() - { - /* prevent instantiation */ - } -} diff --git a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/TestServletBufferTypeLengthWrite.java b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/HttpOutputWriteFileContentServlet.java similarity index 94% rename from jetty-servlets/src/test/java/org/eclipse/jetty/servlets/TestServletBufferTypeLengthWrite.java rename to jetty-servlets/src/test/java/org/eclipse/jetty/servlets/HttpOutputWriteFileContentServlet.java index 5d43a0772c9..4e4c0755851 100644 --- a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/TestServletBufferTypeLengthWrite.java +++ b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/HttpOutputWriteFileContentServlet.java @@ -44,12 +44,12 @@ import org.eclipse.jetty.server.handler.gzip.GzipHandler; * @see http://bugs.eclipse.org/354014 */ @SuppressWarnings("serial") -public class TestServletBufferTypeLengthWrite extends TestDirContentServlet +public class HttpOutputWriteFileContentServlet extends AbstractFileContentServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { - String fileName = request.getServletPath(); + String fileName = request.getPathInfo(); byte[] dataBytes = loadContentFileBytes(fileName); ServletOutputStream out = response.getOutputStream(); diff --git a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/IncludedGzipMinSizeTest.java b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/IncludedGzipMinSizeTest.java deleted file mode 100644 index 5c76b6701bd..00000000000 --- a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/IncludedGzipMinSizeTest.java +++ /dev/null @@ -1,94 +0,0 @@ -// -// ======================================================================== -// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others. -// -// This program and the accompanying materials are made available under -// the terms of the Eclipse Public License 2.0 which is available at -// https://www.eclipse.org/legal/epl-2.0 -// -// This Source Code may also be made available under the following -// Secondary Licenses when the conditions for such availability set -// forth in the Eclipse Public License, v. 2.0 are satisfied: -// the Apache License v2.0 which is available at -// https://www.apache.org/licenses/LICENSE-2.0 -// -// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 -// ======================================================================== -// - -package org.eclipse.jetty.servlets; - -import jakarta.servlet.Servlet; -import org.eclipse.jetty.server.handler.gzip.GzipHandler; -import org.eclipse.jetty.toolchain.test.jupiter.WorkDir; -import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; - -/** - * Perform specific tests on the IncludableGzipHandler's ability to manage - * minGzipSize initialization parameter. - * - * @see http://bugs.eclipse.org/366106 - */ -@ExtendWith(WorkDirExtension.class) -public class IncludedGzipMinSizeTest -{ - public IncludedGzipMinSizeTest() - { - this.compressionType = GzipHandler.GZIP; - } - - public WorkDir testdir; - - private String compressionType; - private Class testServlet = TestMinGzipSizeServlet.class; - - @Test - public void testUnderMinSize() throws Exception - { - GzipTester tester = new GzipTester(testdir.getEmptyPathDir(), compressionType); - - tester.setContentServlet(testServlet); - // A valid mime type that we will never use in this test. - // configured here to prevent mimeType==null logic - tester.getGzipHandler().addIncludedMimeTypes("application/soap+xml"); - tester.getGzipHandler().setMinGzipSize(2048); - - tester.copyTestServerFile("small_script.js"); - - try - { - tester.start(); - tester.assertIsResponseNotGziped("small_script.js", - "small_script.js.sha1", - "text/javascript; charset=utf-8"); - } - finally - { - tester.stop(); - } - } - - @Test - public void testOverMinSize() throws Exception - { - GzipTester tester = new GzipTester(testdir.getEmptyPathDir(), compressionType); - - tester.setContentServlet(testServlet); - tester.getGzipHandler().addIncludedMimeTypes("application/soap+xml", "text/javascript", "application/javascript"); - tester.getGzipHandler().setMinGzipSize(2048); - - tester.copyTestServerFile("big_script.js"); - - try - { - tester.start(); - tester.assertIsResponseGzipCompressed("GET", "big_script.js"); - } - finally - { - tester.stop(); - } - } -} diff --git a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/IncludedGzipTest.java b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/IncludedGzipTest.java deleted file mode 100644 index 81b49e5661d..00000000000 --- a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/IncludedGzipTest.java +++ /dev/null @@ -1,134 +0,0 @@ -// -// ======================================================================== -// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others. -// -// This program and the accompanying materials are made available under -// the terms of the Eclipse Public License 2.0 which is available at -// https://www.eclipse.org/legal/epl-2.0 -// -// This Source Code may also be made available under the following -// Secondary Licenses when the conditions for such availability set -// forth in the Eclipse Public License, v. 2.0 are satisfied: -// the Apache License v2.0 which is available at -// https://www.apache.org/licenses/LICENSE-2.0 -// -// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 -// ======================================================================== -// - -package org.eclipse.jetty.servlets; - -import java.io.BufferedOutputStream; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileOutputStream; -import java.io.InputStream; -import java.io.OutputStream; -import java.nio.ByteBuffer; -import java.util.zip.GZIPInputStream; -import java.util.zip.Inflater; -import java.util.zip.InflaterInputStream; - -import jakarta.servlet.http.HttpServletResponse; -import org.eclipse.jetty.http.tools.HttpTester; -import org.eclipse.jetty.server.handler.gzip.GzipHandler; -import org.eclipse.jetty.servlet.ServletTester; -import org.eclipse.jetty.toolchain.test.jupiter.WorkDir; -import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension; -import org.eclipse.jetty.util.BufferUtil; -import org.eclipse.jetty.util.IO; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -@ExtendWith(WorkDirExtension.class) -public class IncludedGzipTest -{ - public WorkDir testdir; - - private static String __content = - "Lorem ipsum dolor sit amet, consectetur adipiscing elit. In quis felis nunc. " + - "Quisque suscipit mauris et ante auctor ornare rhoncus lacus aliquet. Pellentesque " + - "habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. " + - "Vestibulum sit amet felis augue, vel convallis dolor. Cras accumsan vehicula diam " + - "at faucibus. Etiam in urna turpis, sed congue mi. Morbi et lorem eros. Donec vulputate " + - "velit in risus suscipit lobortis. Aliquam id urna orci, nec sollicitudin ipsum. " + - "Cras a orci turpis. Donec suscipit vulputate cursus. Mauris nunc tellus, fermentum " + - "eu auctor ut, mollis at diam. Quisque porttitor ultrices metus, vitae tincidunt massa " + - "sollicitudin a. Vivamus porttitor libero eget purus hendrerit cursus. Integer aliquam " + - "consequat mauris quis luctus. Cras enim nibh, dignissim eu faucibus ac, mollis nec neque. " + - "Aliquam purus mauris, consectetur nec convallis lacinia, porta sed ante. Suspendisse " + - "et cursus magna. Donec orci enim, molestie a lobortis eu, imperdiet vitae neque."; - - private ServletTester tester; - private String compressionType; - - public IncludedGzipTest() - { - this.compressionType = GzipHandler.GZIP; - } - - @BeforeEach - public void setUp() throws Exception - { - testdir.ensureEmpty(); - - File testFile = testdir.getPathFile("file.txt").toFile(); - try (OutputStream testOut = new BufferedOutputStream(new FileOutputStream(testFile))) - { - ByteArrayInputStream testIn = new ByteArrayInputStream(__content.getBytes("ISO8859_1")); - IO.copy(testIn, testOut); - } - - tester = new ServletTester("/context"); - tester.getContext().setResourceBase(testdir.getPath().toString()); - tester.getContext().addServlet(org.eclipse.jetty.servlet.DefaultServlet.class, "/"); - - GzipHandler gzipHandler = new GzipHandler(); - gzipHandler.setMinGzipSize(16); - tester.getContext().insertHandler(gzipHandler); - tester.start(); - } - - @AfterEach - public void tearDown() throws Exception - { - tester.stop(); - } - - @Test - public void testGzip() throws Exception - { - // generated and parsed test - - ByteBuffer request = BufferUtil.toBuffer( - "GET /context/file.txt HTTP/1.0\r\n" + - "Host: tester\r\n" + - "Accept-Encoding: " + compressionType + "\r\n" + - "\r\n"); - - HttpTester.Response response = HttpTester.parseResponse(tester.getResponses(request)); - - assertEquals(HttpServletResponse.SC_OK, response.getStatus()); - assertEquals(compressionType, response.get("Content-Encoding")); - - InputStream testIn = null; - ByteArrayInputStream compressedResponseStream = new ByteArrayInputStream(response.getContentBytes()); - if (compressionType.equals(GzipHandler.GZIP)) - { - testIn = new GZIPInputStream(compressedResponseStream); - } - else if (compressionType.equals(GzipHandler.DEFLATE)) - { - testIn = new InflaterInputStream(compressedResponseStream, new Inflater(true)); - } - ByteArrayOutputStream testOut = new ByteArrayOutputStream(); - IO.copy(testIn, testOut); - - assertEquals(__content, testOut.toString("ISO8859_1")); - } -} diff --git a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/TestDirContentServlet.java b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/TestDirContentServlet.java deleted file mode 100644 index eb47d4594b5..00000000000 --- a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/TestDirContentServlet.java +++ /dev/null @@ -1,73 +0,0 @@ -// -// ======================================================================== -// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others. -// -// This program and the accompanying materials are made available under -// the terms of the Eclipse Public License 2.0 which is available at -// https://www.eclipse.org/legal/epl-2.0 -// -// This Source Code may also be made available under the following -// Secondary Licenses when the conditions for such availability set -// forth in the Eclipse Public License, v. 2.0 are satisfied: -// the Apache License v2.0 which is available at -// https://www.apache.org/licenses/LICENSE-2.0 -// -// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 -// ======================================================================== -// - -package org.eclipse.jetty.servlets; - -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; - -import jakarta.servlet.ServletConfig; -import jakarta.servlet.ServletException; -import jakarta.servlet.http.HttpServlet; -import org.eclipse.jetty.toolchain.test.PathAssert; -import org.eclipse.jetty.util.IO; - -@SuppressWarnings("serial") -public class TestDirContentServlet extends HttpServlet -{ - private File basedir; - - @Override - public void init(ServletConfig config) throws ServletException - { - basedir = new File(config.getInitParameter("baseDir")); - } - - public File getTestFile(String filename) - { - File testfile = new File(basedir, filename); - PathAssert.assertFileExists("Content File should exist", testfile); - return testfile; - } - - protected byte[] loadContentFileBytes(final String fileName) throws IOException - { - String relPath = fileName; - relPath = relPath.replaceFirst("^/context/", ""); - relPath = relPath.replaceFirst("^/", ""); - - File contentFile = getTestFile(relPath); - - FileInputStream in = null; - ByteArrayOutputStream out = null; - try - { - in = new FileInputStream(contentFile); - out = new ByteArrayOutputStream(); - IO.copy(in, out); - return out.toByteArray(); - } - finally - { - IO.close(out); - IO.close(in); - } - } -} diff --git a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/TestMinGzipSizeServlet.java b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/TestMinGzipSizeServlet.java index 4eeac262bbb..082c2b89661 100644 --- a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/TestMinGzipSizeServlet.java +++ b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/TestMinGzipSizeServlet.java @@ -31,7 +31,7 @@ import org.eclipse.jetty.http.MimeTypes; * Test servlet for testing against unusual minGzip configurable. */ @SuppressWarnings("serial") -public class TestMinGzipSizeServlet extends TestDirContentServlet +public class TestMinGzipSizeServlet extends AbstractFileContentServlet { private MimeTypes mimeTypes; diff --git a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/TestStaticMimeTypeServlet.java b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/TestStaticMimeTypeServlet.java index b56d9fb3b10..9e26a9f5128 100644 --- a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/TestStaticMimeTypeServlet.java +++ b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/TestStaticMimeTypeServlet.java @@ -31,7 +31,7 @@ import org.eclipse.jetty.http.MimeTypes; * Test servlet for testing against unusual MimeTypes and Content-Types. */ @SuppressWarnings("serial") -public class TestStaticMimeTypeServlet extends TestDirContentServlet +public class TestStaticMimeTypeServlet extends AbstractFileContentServlet { private MimeTypes mimeTypes; @@ -63,7 +63,7 @@ public class TestStaticMimeTypeServlet extends TestDirContentServlet @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { - String fileName = request.getServletPath(); + String fileName = request.getPathInfo(); byte[] dataBytes = loadContentFileBytes(fileName); response.setContentLength(dataBytes.length); diff --git a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/ThreadStarvationTest.java b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/ThreadStarvationTest.java index bc851f4e1c1..0e13aa15e0e 100644 --- a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/ThreadStarvationTest.java +++ b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/ThreadStarvationTest.java @@ -45,7 +45,7 @@ import java.util.concurrent.atomic.AtomicInteger; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import org.eclipse.jetty.io.ChannelEndPoint; + import org.eclipse.jetty.io.ManagedSelector; import org.eclipse.jetty.io.SocketChannelEndPoint; import org.eclipse.jetty.logging.StacklessLogging; @@ -106,7 +106,7 @@ public class ThreadStarvationTest ServerConnector connector = new ServerConnector(_server, 0, 1) { @Override - protected ChannelEndPoint newEndPoint(SocketChannel channel, ManagedSelector selectSet, SelectionKey key) throws IOException + protected SocketChannelEndPoint newEndPoint(SocketChannel channel, ManagedSelector selectSet, SelectionKey key) { return new SocketChannelEndPoint(channel, selectSet, key, getScheduler()) { @@ -258,7 +258,7 @@ public class ThreadStarvationTest ServerConnector connector = new ServerConnector(_server, acceptors, selectors) { @Override - protected ChannelEndPoint newEndPoint(SocketChannel channel, ManagedSelector selectSet, SelectionKey key) throws IOException + protected SocketChannelEndPoint newEndPoint(SocketChannel channel, ManagedSelector selectSet, SelectionKey key) { return new SocketChannelEndPoint(channel, selectSet, key, getScheduler()) { diff --git a/jetty-servlets/src/test/resources/test.svg.sha1 b/jetty-servlets/src/test/resources/test.svg.sha1 deleted file mode 100644 index 3b170f0b098..00000000000 --- a/jetty-servlets/src/test/resources/test.svg.sha1 +++ /dev/null @@ -1 +0,0 @@ -1ccb7a0b85585d0e9bdc3863ad093d4e53a9ea68 test.svg diff --git a/jetty-unixsocket/jetty-unixsocket-client/src/main/java/org/eclipse/jetty/unixsocket/client/HttpClientTransportOverUnixSockets.java b/jetty-unixsocket/jetty-unixsocket-client/src/main/java/org/eclipse/jetty/unixsocket/client/HttpClientTransportOverUnixSockets.java index b4b20bd61cc..dad2b625e37 100644 --- a/jetty-unixsocket/jetty-unixsocket-client/src/main/java/org/eclipse/jetty/unixsocket/client/HttpClientTransportOverUnixSockets.java +++ b/jetty-unixsocket/jetty-unixsocket-client/src/main/java/org/eclipse/jetty/unixsocket/client/HttpClientTransportOverUnixSockets.java @@ -47,6 +47,7 @@ import org.eclipse.jetty.io.EndPoint; import org.eclipse.jetty.io.ManagedSelector; import org.eclipse.jetty.io.SelectorManager; import org.eclipse.jetty.unixsocket.common.UnixSocketEndPoint; +import org.eclipse.jetty.util.IO; import org.eclipse.jetty.util.thread.Scheduler; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -128,7 +129,7 @@ public class HttpClientTransportOverUnixSockets extends AbstractConnectorHttpCli } catch (Throwable x) { - safeClose(channel); + IO.close(channel); connectFailed(x, context); } } diff --git a/jetty-unixsocket/jetty-unixsocket-client/src/test/java/org/eclipse/jetty/unixsocket/UnixSocketTest.java b/jetty-unixsocket/jetty-unixsocket-client/src/test/java/org/eclipse/jetty/unixsocket/UnixSocketTest.java index b9060dae6de..a5327eb2463 100644 --- a/jetty-unixsocket/jetty-unixsocket-client/src/test/java/org/eclipse/jetty/unixsocket/UnixSocketTest.java +++ b/jetty-unixsocket/jetty-unixsocket-client/src/test/java/org/eclipse/jetty/unixsocket/UnixSocketTest.java @@ -40,6 +40,7 @@ import org.eclipse.jetty.unixsocket.server.UnixSocketConnector; import org.eclipse.jetty.util.StringUtil; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.EnabledOnOs; import org.slf4j.Logger; @@ -146,6 +147,7 @@ public class UnixSocketTest assertThat(contentResponse.getContentAsString(), containsString("Hello World")); } + @Tag("external") @Test public void testNotLocal() throws Exception { diff --git a/jetty-unixsocket/jetty-unixsocket-common/src/main/java/org/eclipse/jetty/unixsocket/common/UnixSocketEndPoint.java b/jetty-unixsocket/jetty-unixsocket-common/src/main/java/org/eclipse/jetty/unixsocket/common/UnixSocketEndPoint.java index 29c3c11d5e9..ed4e4248605 100644 --- a/jetty-unixsocket/jetty-unixsocket-common/src/main/java/org/eclipse/jetty/unixsocket/common/UnixSocketEndPoint.java +++ b/jetty-unixsocket/jetty-unixsocket-common/src/main/java/org/eclipse/jetty/unixsocket/common/UnixSocketEndPoint.java @@ -23,22 +23,25 @@ import java.net.InetSocketAddress; import java.nio.channels.SelectionKey; import jnr.unixsocket.UnixSocketChannel; -import org.eclipse.jetty.io.ChannelEndPoint; import org.eclipse.jetty.io.ManagedSelector; +import org.eclipse.jetty.io.SocketChannelEndPoint; import org.eclipse.jetty.util.thread.Scheduler; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -public class UnixSocketEndPoint extends ChannelEndPoint +public class UnixSocketEndPoint extends SocketChannelEndPoint { private static final Logger LOG = LoggerFactory.getLogger(UnixSocketEndPoint.class); - private final UnixSocketChannel _channel; - public UnixSocketEndPoint(UnixSocketChannel channel, ManagedSelector selector, SelectionKey key, Scheduler scheduler) { super(channel, selector, key, scheduler); - _channel = channel; + } + + @Override + public UnixSocketChannel getChannel() + { + return (UnixSocketChannel)super.getChannel(); } @Override @@ -56,11 +59,9 @@ public class UnixSocketEndPoint extends ChannelEndPoint @Override protected void doShutdownOutput() { - if (LOG.isDebugEnabled()) - LOG.debug("oshut {}", this); try { - _channel.shutdownOutput(); + getChannel().shutdownOutput(); super.doShutdownOutput(); } catch (IOException e) diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/ssl/SslContextFactory.java b/jetty-util/src/main/java/org/eclipse/jetty/util/ssl/SslContextFactory.java index b8b8386b3a7..8fc304278e6 100644 --- a/jetty-util/src/main/java/org/eclipse/jetty/util/ssl/SslContextFactory.java +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/ssl/SslContextFactory.java @@ -141,9 +141,9 @@ public abstract class SslContextFactory extends AbstractLifeCycle implements Dum private final Set _includeProtocols = new LinkedHashSet<>(); private final Set _excludeCipherSuites = new LinkedHashSet<>(); private final List _includeCipherSuites = new ArrayList<>(); - protected final Map _aliasX509 = new HashMap<>(); - protected final Map _certHosts = new HashMap<>(); - protected final Map _certWilds = new HashMap<>(); + private final Map _aliasX509 = new HashMap<>(); + private final Map _certHosts = new HashMap<>(); + private final Map _certWilds = new HashMap<>(); private String[] _selectedProtocols; private boolean _useCipherSuitesOrder = true; private Comparator _cipherComparator; @@ -453,6 +453,21 @@ public abstract class SslContextFactory extends AbstractLifeCycle implements Dum _certWilds.clear(); } + Map aliasCerts() + { + return _aliasX509; + } + + Map hostCerts() + { + return _certHosts; + } + + Map wildCerts() + { + return _certWilds; + } + @ManagedAttribute(value = "The selected TLS protocol versions", readonly = true) public String[] getSelectedProtocols() { @@ -2157,7 +2172,7 @@ public abstract class SslContextFactory extends AbstractLifeCycle implements Dum boolean hasSniX509ExtendedKeyManager = false; // Is SNI needed to select a certificate? - if (!_certWilds.isEmpty() || _certHosts.size() > 1 || (_certHosts.size() == 1 && _aliasX509.size() > 1)) + if (isSniRequired() || !wildCerts().isEmpty() || hostCerts().size() > 1 || (hostCerts().size() == 1 && aliasCerts().size() > 1)) { for (int idx = 0; idx < managers.length; idx++) { @@ -2201,7 +2216,7 @@ public abstract class SslContextFactory extends AbstractLifeCycle implements Dum if (sniHost == null) { // No SNI, so reject or delegate. - return _sniRequired ? null : SniX509ExtendedKeyManager.SniSelector.DELEGATE; + return isSniRequired() ? null : SniX509ExtendedKeyManager.SniSelector.DELEGATE; } else { diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/ssl/X509.java b/jetty-util/src/main/java/org/eclipse/jetty/util/ssl/X509.java index 349ef807744..73f59a8cd3f 100644 --- a/jetty-util/src/main/java/org/eclipse/jetty/util/ssl/X509.java +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/ssl/X509.java @@ -80,8 +80,7 @@ public class X509 String cn = list.get(1).toString(); if (LOG.isDebugEnabled()) LOG.debug("Certificate SAN alias={} CN={} in {}", alias, cn, this); - if (cn != null) - addName(cn); + addName(cn); } } } @@ -95,19 +94,21 @@ public class X509 String cn = rdn.getValue().toString(); if (LOG.isDebugEnabled()) LOG.debug("Certificate CN alias={} CN={} in {}", alias, cn, this); - if (cn != null && cn.contains(".") && !cn.contains(" ")) - addName(cn); + addName(cn); } } } protected void addName(String cn) { - cn = StringUtil.asciiToLowerCase(cn); - if (cn.startsWith("*.")) - _wilds.add(cn.substring(2)); - else - _hosts.add(cn); + if (cn != null) + { + cn = StringUtil.asciiToLowerCase(cn); + if (cn.startsWith("*.")) + _wilds.add(cn.substring(2)); + else + _hosts.add(cn); + } } public String getAlias() diff --git a/jetty-util/src/test/java/org/eclipse/jetty/util/ssl/SslContextFactoryTest.java b/jetty-util/src/test/java/org/eclipse/jetty/util/ssl/SslContextFactoryTest.java index f6270c1eace..ed937fe7915 100644 --- a/jetty-util/src/test/java/org/eclipse/jetty/util/ssl/SslContextFactoryTest.java +++ b/jetty-util/src/test/java/org/eclipse/jetty/util/ssl/SslContextFactoryTest.java @@ -282,7 +282,7 @@ public class SslContextFactoryTest assertTrue(cf.getX509("other").matches("www.example.com")); assertFalse(cf.getX509("other").matches("eclipse.org")); - assertThat(cf.getX509("san").getHosts(), containsInAnyOrder("www.san.com", "m.san.com")); + assertThat(cf.getX509("san").getHosts(), containsInAnyOrder("san example", "www.san.com", "m.san.com")); assertTrue(cf.getX509("san").getWilds().isEmpty()); assertTrue(cf.getX509("san").matches("www.san.com")); assertTrue(cf.getX509("san").matches("m.san.com")); diff --git a/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/AsyncIOServletTest.java b/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/AsyncIOServletTest.java index ac9e0ce1b9c..15aecd2a4a0 100644 --- a/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/AsyncIOServletTest.java +++ b/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/AsyncIOServletTest.java @@ -74,6 +74,7 @@ import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.FuturePromise; import org.hamcrest.Matchers; import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Tag; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ArgumentsSource; @@ -397,6 +398,7 @@ public class AsyncIOServletTest extends AbstractTest consumer) throws Exception + { + server = new Server(); + connector = consumer.apply(server); + server.addConnector(connector); + server.start(); + } + + @AfterEach + public void dispose() throws Exception + { + server.stop(); + } + + @Test + public void testSelectFailureBetweenReads() throws Exception + { + // There will be 3 calls to select(): one at start(), + // one to accept, and one to set read interest. + CountDownLatch selectLatch = new CountDownLatch(3); + CountDownLatch failureLatch = new CountDownLatch(1); + AtomicBoolean fail = new AtomicBoolean(); + start(server -> new ServerConnector(server, 1, 1) + { + @Override + protected SelectorManager newSelectorManager(Executor executor, Scheduler scheduler, int selectors) + { + return new ServerConnectorManager(executor, scheduler, selectors) + { + @Override + protected ManagedSelector newSelector(int id) + { + return new ManagedSelector(this, id) + { + @Override + protected int nioSelect(Selector selector, boolean now) throws IOException + { + selectLatch.countDown(); + if (fail.getAndSet(false)) + throw new IOException("explicit select() failure"); + return super.nioSelect(selector, now); + } + + @Override + protected void handleSelectFailure(Selector selector, Throwable failure) throws IOException + { + super.handleSelectFailure(selector, failure); + failureLatch.countDown(); + } + }; + } + }; + } + }); + + try (SocketChannel client = SocketChannel.open(new InetSocketAddress("localhost", connector.getLocalPort()))) + { + assertTrue(selectLatch.await(5, TimeUnit.SECONDS)); + + String request = "GET / HTTP/1.0\r\n\r\n"; + int split = request.length() / 2; + ByteBuffer chunk1 = StandardCharsets.UTF_8.encode(request.substring(0, split)); + ByteBuffer chunk2 = StandardCharsets.UTF_8.encode(request.substring(split)); + + // Wake up the selector and fail it. + fail.set(true); + client.write(chunk1); + + // Wait for the failure handling to be completed. + assertTrue(failureLatch.await(5, TimeUnit.SECONDS)); + + // Write the rest of the request, the + // server should be able to continue. + client.write(chunk2); + + HttpTester.Response response = HttpTester.parseResponse(HttpTester.from(client)); + assertNotNull(response); + assertEquals(HttpStatus.NOT_FOUND_404, response.getStatus()); + } + } + + @Test + public void testAcceptDuringSelectFailure() throws Exception + { + // There will be 3 calls to select(): one at start(), + // one to accept, and one to set read interest. + CountDownLatch selectLatch = new CountDownLatch(3); + CountDownLatch failureLatch = new CountDownLatch(1); + AtomicBoolean fail = new AtomicBoolean(); + AtomicReference socketRef = new AtomicReference<>(); + start(server -> new ServerConnector(server, 1, 1) + { + @Override + protected SelectorManager newSelectorManager(Executor executor, Scheduler scheduler, int selectors) + { + return new ServerConnectorManager(executor, scheduler, selectors) + { + @Override + protected ManagedSelector newSelector(int id) + { + return new ManagedSelector(this, id) + { + @Override + protected int nioSelect(Selector selector, boolean now) throws IOException + { + selectLatch.countDown(); + if (fail.getAndSet(false)) + throw new IOException("explicit select() failure"); + return super.nioSelect(selector, now); + } + + @Override + protected void handleSelectFailure(Selector selector, Throwable failure) throws IOException + { + // Before handling the failure, connect with another socket. + SocketChannel socket = SocketChannel.open(new InetSocketAddress("localhost", connector.getLocalPort())); + socketRef.set(socket); + super.handleSelectFailure(selector, failure); + failureLatch.countDown(); + } + }; + } + }; + } + }); + + try (SocketChannel client = SocketChannel.open(new InetSocketAddress("localhost", connector.getLocalPort()))) + { + assertTrue(selectLatch.await(5, TimeUnit.SECONDS)); + + String request = "GET / HTTP/1.0\r\n\r\n"; + ByteBuffer buffer = StandardCharsets.UTF_8.encode(request); + + // Wake up the selector and fail it. + fail.set(true); + client.write(buffer); + + // Wait for the failure handling to be completed. + assertTrue(failureLatch.await(5, TimeUnit.SECONDS)); + + HttpTester.Response response = HttpTester.parseResponse(HttpTester.from(client)); + assertNotNull(response); + assertEquals(HttpStatus.NOT_FOUND_404, response.getStatus()); + + // Verify that the newly created socket works well. + SocketChannel socket = socketRef.get(); + buffer.flip(); + socket.write(buffer); + response = HttpTester.parseResponse(HttpTester.from(socket)); + assertNotNull(response); + assertEquals(HttpStatus.NOT_FOUND_404, response.getStatus()); + } + } + + @Test + public void testSelectFailureDuringEndPointCreation() throws Exception + { + // There will be 2 calls to select(): one at start(), one to accept. + CountDownLatch selectLatch = new CountDownLatch(2); + CountDownLatch failureLatch = new CountDownLatch(1); + AtomicBoolean fail = new AtomicBoolean(); + CountDownLatch endPointLatch1 = new CountDownLatch(1); + CountDownLatch endPointLatch2 = new CountDownLatch(1); + start(server -> new ServerConnector(server, 1, 1) + { + @Override + protected SelectorManager newSelectorManager(Executor executor, Scheduler scheduler, int selectors) + { + return new ServerConnectorManager(executor, scheduler, selectors) + { + @Override + protected ManagedSelector newSelector(int id) + { + return new ManagedSelector(this, id) + { + @Override + protected int nioSelect(Selector selector, boolean now) throws IOException + { + selectLatch.countDown(); + if (fail.getAndSet(false)) + throw new IOException("explicit select() failure"); + return super.nioSelect(selector, now); + } + + @Override + protected void handleSelectFailure(Selector selector, Throwable failure) throws IOException + { + super.handleSelectFailure(selector, failure); + failureLatch.countDown(); + } + }; + } + + @Override + protected SocketChannelEndPoint newEndPoint(SelectableChannel channel, ManagedSelector selector, SelectionKey selectionKey) throws IOException + { + try + { + SocketChannelEndPoint endPoint = super.newEndPoint(channel, selector, selectionKey); + endPointLatch1.countDown(); + assertTrue(endPointLatch2.await(5, TimeUnit.SECONDS)); + return endPoint; + } + catch (InterruptedException x) + { + throw new InterruptedIOException(); + } + } + }; + } + }); + + try (SocketChannel client = SocketChannel.open(new InetSocketAddress("localhost", connector.getLocalPort()))) + { + assertTrue(selectLatch.await(5, TimeUnit.SECONDS)); + + // Wait until the server EndPoint instance is created. + assertTrue(endPointLatch1.await(5, TimeUnit.SECONDS)); + + // Wake up the selector and fail it. + fail.set(true); + SocketChannel.open(new InetSocketAddress("localhost", connector.getLocalPort())).close(); + + // Wait until the selector is replaced. + assertTrue(failureLatch.await(5, TimeUnit.SECONDS)); + + // Continue the EndPoint creation. + endPointLatch2.countDown(); + + String request = "GET / HTTP/1.0\r\n\r\n"; + ByteBuffer buffer = StandardCharsets.UTF_8.encode(request); + client.write(buffer); + + HttpTester.Response response = HttpTester.parseResponse(HttpTester.from(client)); + assertNotNull(response); + assertEquals(HttpStatus.NOT_FOUND_404, response.getStatus()); + } + } + + @Test + public void testSelectFailureDuringEndPointCreatedThenClosed() throws Exception + { + // There will be 2 calls to select(): one at start(), one to accept. + CountDownLatch selectLatch = new CountDownLatch(2); + CountDownLatch failureLatch = new CountDownLatch(1); + AtomicBoolean fail = new AtomicBoolean(); + CountDownLatch connectionLatch1 = new CountDownLatch(1); + CountDownLatch connectionLatch2 = new CountDownLatch(1); + start(server -> new ServerConnector(server, 1, 1) + { + @Override + protected SelectorManager newSelectorManager(Executor executor, Scheduler scheduler, int selectors) + { + return new ServerConnectorManager(executor, scheduler, selectors) + { + @Override + protected ManagedSelector newSelector(int id) + { + return new ManagedSelector(this, id) + { + @Override + protected int nioSelect(Selector selector, boolean now) throws IOException + { + selectLatch.countDown(); + if (fail.getAndSet(false)) + throw new IOException("explicit select() failure"); + return super.nioSelect(selector, now); + } + + @Override + protected void handleSelectFailure(Selector selector, Throwable failure) throws IOException + { + super.handleSelectFailure(selector, failure); + failureLatch.countDown(); + } + }; + } + + @Override + public Connection newConnection(SelectableChannel channel, EndPoint endPoint, Object attachment) throws IOException + { + try + { + Connection connection = super.newConnection(channel, endPoint, attachment); + endPoint.close(); + connectionLatch1.countDown(); + assertTrue(connectionLatch2.await(5, TimeUnit.SECONDS)); + return connection; + } + catch (InterruptedException e) + { + throw new InterruptedIOException(); + } + } + }; + } + }); + + try (SocketChannel client = SocketChannel.open(new InetSocketAddress("localhost", connector.getLocalPort()))) + { + assertTrue(selectLatch.await(5, TimeUnit.SECONDS)); + + // Wait until the server EndPoint is closed. + assertTrue(connectionLatch1.await(5, TimeUnit.SECONDS)); + + // Wake up the selector and fail it. + fail.set(true); + SocketChannel.open(new InetSocketAddress("localhost", connector.getLocalPort())).close(); + + // Wait until the selector is replaced. + assertTrue(failureLatch.await(5, TimeUnit.SECONDS)); + + // Continue the server processing. + connectionLatch2.countDown(); + + // The channel has been closed on the server. + int read = client.read(ByteBuffer.allocate(1)); + assertTrue(read < 0); + } + } +} diff --git a/tests/test-integration/src/test/resources/jetty-logging.properties b/tests/test-integration/src/test/resources/jetty-logging.properties index c3b261ea39e..ba4789f545f 100644 --- a/tests/test-integration/src/test/resources/jetty-logging.properties +++ b/tests/test-integration/src/test/resources/jetty-logging.properties @@ -1,5 +1,3 @@ # Jetty Logging using jetty-slf4j-impl -## Jetty Logging using jetty-slf4j-impl -org.eclipse.jetty.LEVEL=WARN #org.eclipse.jetty.LEVEL=DEBUG #org.eclipse.jetty.websocket.LEVEL=DEBUG