Merge branch 'jetty-10.0.x' into jetty-11.0.x
This commit is contained in:
commit
e677d7c6f0
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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())
|
||||
{
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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.
|
||||
* <p>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));
|
||||
}
|
||||
}
|
|
@ -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<String, Object> 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;
|
||||
}
|
||||
|
|
|
@ -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<SelectionKey> 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<SelectionKey> 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<String> 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<Closeable> _closed;
|
||||
final CountDownLatch _noEndPoints = new CountDownLatch(1);
|
||||
final CountDownLatch _complete = new CountDownLatch(1);
|
||||
private final Set<Closeable> _closed;
|
||||
private final CountDownLatch _complete = new CountDownLatch(1);
|
||||
|
||||
public CloseConnections()
|
||||
private CloseConnections()
|
||||
{
|
||||
this(null);
|
||||
}
|
||||
|
||||
public CloseConnections(Set<Closeable> closed)
|
||||
private CloseConnections(Set<Closeable> 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;
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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.
|
||||
* <p>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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
{
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
* <p>
|
||||
* 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).
|
||||
* </p>
|
||||
*
|
||||
* @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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -44,7 +44,7 @@ import static org.hamcrest.Matchers.nullValue;
|
|||
* </pre>
|
||||
*/
|
||||
@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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -42,12 +42,12 @@ import org.eclipse.jetty.server.handler.gzip.GzipHandler;
|
|||
* @see <a href="Eclipse Bug 354014">http://bugs.eclipse.org/354014</a>
|
||||
*/
|
||||
@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);
|
|
@ -42,12 +42,12 @@ import org.eclipse.jetty.server.handler.gzip.GzipHandler;
|
|||
* @see <a href="Eclipse Bug 354014">http://bugs.eclipse.org/354014</a>
|
||||
*/
|
||||
@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);
|
|
@ -42,12 +42,12 @@ import org.eclipse.jetty.server.handler.gzip.GzipHandler;
|
|||
* @see <a href="Eclipse Bug 354014">http://bugs.eclipse.org/354014</a>
|
||||
*/
|
||||
@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();
|
|
@ -42,12 +42,12 @@ import org.eclipse.jetty.server.handler.gzip.GzipHandler;
|
|||
* @see <a href="Eclipse Bug 354014">http://bugs.eclipse.org/354014</a>
|
||||
*/
|
||||
@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();
|
|
@ -42,12 +42,12 @@ import org.eclipse.jetty.server.handler.gzip.GzipHandler;
|
|||
* @see <a href="Eclipse Bug 354014">http://bugs.eclipse.org/354014</a>
|
||||
*/
|
||||
@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();
|
|
@ -42,12 +42,12 @@ import org.eclipse.jetty.server.handler.gzip.GzipHandler;
|
|||
* @see <a href="Eclipse Bug 354014">http://bugs.eclipse.org/354014</a>
|
||||
*/
|
||||
@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"))
|
|
@ -42,12 +42,12 @@ import org.eclipse.jetty.server.handler.gzip.GzipHandler;
|
|||
* @see <a href="Eclipse Bug 354014">http://bugs.eclipse.org/354014</a>
|
||||
*/
|
||||
@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"))
|
|
@ -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 <a href="Eclipse Bug 354014">http://bugs.eclipse.org/354014</a>
|
||||
* 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<Arguments> scenarios()
|
||||
{
|
||||
List<Scenario> 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<Class<? extends AbstractFileContentServlet>> 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<? extends TestDirContentServlet> 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<Arguments> scenarios = new ArrayList<>();
|
||||
|
||||
// Add content servlet
|
||||
tester.setContentServlet(contentServlet);
|
||||
|
||||
try
|
||||
for (Class<? extends Servlet> 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<? extends Servlet> 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 <a href="http://bugs.eclipse.org/354014">Eclipse Bug 354014</a>
|
||||
*/
|
||||
@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 <a href="http://bugs.eclipse.org/354014">Eclipse Bug 354014</a>
|
||||
*/
|
||||
@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 <a href="http://bugs.eclipse.org/354014">Eclipse Bug 354014</a>
|
||||
*/
|
||||
@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 <a href="http://bugs.eclipse.org/354014">Eclipse Bug 354014</a>
|
||||
*/
|
||||
@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 <a href="http://bugs.eclipse.org/354014">Eclipse Bug 354014</a>
|
||||
*/
|
||||
@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 <a href="http://bugs.eclipse.org/354014">Eclipse Bug 354014</a>
|
||||
*/
|
||||
@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 <a href="Eclipse Bug 354014">http://bugs.eclipse.org/354014</a>
|
||||
*/
|
||||
@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 <a href="http://bugs.eclipse.org/450873">Eclipse Bug 450873</a>
|
||||
*/
|
||||
@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<String> 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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<Arguments> 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);
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -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 = "<html><head></head><body><h1>COMPRESSABLE CONTENT</h1>" +
|
||||
"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!" +
|
||||
"</body></html>";
|
||||
|
||||
@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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<Arguments> 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));
|
||||
}
|
||||
}
|
|
@ -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 = "<html><head></head><body><h1>COMPRESSIBLE CONTENT</h1>" +
|
||||
"<p>" +
|
||||
"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. <br/>" +
|
||||
"How now brown cow. <br/>" +
|
||||
"The quick brown fox jumped over the lazy dog. <br/>" +
|
||||
"A woman needs a man like a fish needs a bicycle!" +
|
||||
"</p>" +
|
||||
"</body></html>";
|
||||
|
||||
@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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
* <p>
|
||||
* This is used to test exclusions and passthroughs in the GzipHandler.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* This is used to test exclusions and passthroughs in the GzipHandler.
|
||||
* <p>
|
||||
* 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<String> 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<? extends Servlet> 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<DispatcherType> 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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 */
|
||||
}
|
||||
}
|
|
@ -44,12 +44,12 @@ import org.eclipse.jetty.server.handler.gzip.GzipHandler;
|
|||
* @see <a href="Eclipse Bug 354014">http://bugs.eclipse.org/354014</a>
|
||||
*/
|
||||
@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();
|
|
@ -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 <a href="Eclipse Bug 366106">http://bugs.eclipse.org/366106</a>
|
||||
*/
|
||||
@ExtendWith(WorkDirExtension.class)
|
||||
public class IncludedGzipMinSizeTest
|
||||
{
|
||||
public IncludedGzipMinSizeTest()
|
||||
{
|
||||
this.compressionType = GzipHandler.GZIP;
|
||||
}
|
||||
|
||||
public WorkDir testdir;
|
||||
|
||||
private String compressionType;
|
||||
private Class<? extends Servlet> 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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"));
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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())
|
||||
{
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
1ccb7a0b85585d0e9bdc3863ad093d4e53a9ea68 test.svg
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
{
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -141,9 +141,9 @@ public abstract class SslContextFactory extends AbstractLifeCycle implements Dum
|
|||
private final Set<String> _includeProtocols = new LinkedHashSet<>();
|
||||
private final Set<String> _excludeCipherSuites = new LinkedHashSet<>();
|
||||
private final List<String> _includeCipherSuites = new ArrayList<>();
|
||||
protected final Map<String, X509> _aliasX509 = new HashMap<>();
|
||||
protected final Map<String, X509> _certHosts = new HashMap<>();
|
||||
protected final Map<String, X509> _certWilds = new HashMap<>();
|
||||
private final Map<String, X509> _aliasX509 = new HashMap<>();
|
||||
private final Map<String, X509> _certHosts = new HashMap<>();
|
||||
private final Map<String, X509> _certWilds = new HashMap<>();
|
||||
private String[] _selectedProtocols;
|
||||
private boolean _useCipherSuitesOrder = true;
|
||||
private Comparator<String> _cipherComparator;
|
||||
|
@ -453,6 +453,21 @@ public abstract class SslContextFactory extends AbstractLifeCycle implements Dum
|
|||
_certWilds.clear();
|
||||
}
|
||||
|
||||
Map<String, X509> aliasCerts()
|
||||
{
|
||||
return _aliasX509;
|
||||
}
|
||||
|
||||
Map<String, X509> hostCerts()
|
||||
{
|
||||
return _certHosts;
|
||||
}
|
||||
|
||||
Map<String, X509> 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
|
||||
{
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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"));
|
||||
|
|
|
@ -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<AsyncIOServletTest.AsyncTra
|
|||
@ParameterizedTest
|
||||
@ArgumentsSource(TransportProvider.class)
|
||||
@Tag("Unstable")
|
||||
@Disabled
|
||||
public void testAsyncWriteClosed(Transport transport) throws Exception
|
||||
{
|
||||
init(transport);
|
||||
|
|
|
@ -0,0 +1,379 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// 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.test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InterruptedIOException;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.SelectableChannel;
|
||||
import java.nio.channels.SelectionKey;
|
||||
import java.nio.channels.Selector;
|
||||
import java.nio.channels.SocketChannel;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.eclipse.jetty.http.HttpStatus;
|
||||
import org.eclipse.jetty.http.tools.HttpTester;
|
||||
import org.eclipse.jetty.io.Connection;
|
||||
import org.eclipse.jetty.io.EndPoint;
|
||||
import org.eclipse.jetty.io.ManagedSelector;
|
||||
import org.eclipse.jetty.io.SelectorManager;
|
||||
import org.eclipse.jetty.io.SocketChannelEndPoint;
|
||||
import org.eclipse.jetty.server.Server;
|
||||
import org.eclipse.jetty.server.ServerConnector;
|
||||
import org.eclipse.jetty.util.thread.Scheduler;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
public class RecoverFailedSelectorTest
|
||||
{
|
||||
private Server server;
|
||||
private ServerConnector connector;
|
||||
|
||||
private void start(Function<Server, ServerConnector> 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<SocketChannel> 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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue