Skip to content

Instantly share code, notes, and snippets.

@nickbabcock
Created September 3, 2016 02:48
Show Gist options
  • Save nickbabcock/318d233c293444cb4fc22a8a80947f1f to your computer and use it in GitHub Desktop.
Save nickbabcock/318d233c293444cb4fc22a8a80947f1f to your computer and use it in GitHub Desktop.
Trying to replicate bug found in dropwizard
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>jetty</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<jetty.version>9.3.11.v20160721</jetty.version>
</properties>
<dependencies>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>19.0</version>
</dependency>
<!-- Jetty -->
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-server</artifactId>
<version>${jetty.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-util</artifactId>
<version>${jetty.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-webapp</artifactId>
<version>${jetty.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-continuation</artifactId>
<version>${jetty.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-servlet</artifactId>
<version>${jetty.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-servlet</artifactId>
<classifier>tests</classifier>
<version>${jetty.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-servlets</artifactId>
<version>${jetty.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-http</artifactId>
<version>${jetty.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-http</artifactId>
<classifier>tests</classifier>
<version>${jetty.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-alpn-server</artifactId>
<version>${jetty.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.http2</groupId>
<artifactId>http2-server</artifactId>
<version>${jetty.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.http2</groupId>
<artifactId>http2-client</artifactId>
<version>${jetty.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-client</artifactId>
<version>${jetty.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.http2</groupId>
<artifactId>http2-http-client-transport</artifactId>
<version>${jetty.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.toolchain.setuid</groupId>
<artifactId>jetty-setuid-java</artifactId>
<version>1.0.3</version>
<exclusions>
<exclusion>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-util</artifactId>
</exclusion>
<exclusion>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-server</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.5.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<optimize>true</optimize>
</configuration>
</plugin>
</plugins>
</build>
</project>
package com.example.jetty;
import com.google.common.base.Charsets;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.io.CharStreams;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.handler.AbstractHandler;
import org.eclipse.jetty.server.handler.RequestLogHandler;
import org.eclipse.jetty.server.handler.StatisticsHandler;
import org.eclipse.jetty.server.handler.gzip.GzipHandler;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.eclipse.jetty.util.ArrayTernaryTrie;
import org.eclipse.jetty.util.Trie;
import javax.servlet.DispatcherType;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ReadListener;
import javax.servlet.Servlet;
import javax.servlet.ServletException;
import javax.servlet.ServletInputStream;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Collections;
import java.util.EnumSet;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.zip.Deflater;
import java.util.zip.GZIPInputStream;
import java.util.zip.Inflater;
import java.util.zip.InflaterInputStream;
import static com.example.jetty.Program.AllowedMethodsFilter.DEFAULT_ALLOWED_METHODS;
public class Program {
public static void main(String[] args) throws Exception {
final Server server = new Server();
server.setStopAtShutdown(true);
final MutableServletContextHandler service = new MutableServletContextHandler();
service.setServer(server);
service.addServlet(new NonblockingServletHolder(new GetServlet()), "/*");
final MutableServletContextHandler secret = new MutableServletContextHandler();
secret.setServer(server);
secret.addServlet(new NonblockingServletHolder(new PostServlet()), "/*");
secret.addFilter(AllowedMethodsFilter.class, "/*", EnumSet.of(DispatcherType.REQUEST))
.setInitParameter(AllowedMethodsFilter.ALLOWED_METHODS_PARAM, Joiner.on(',').join(DEFAULT_ALLOWED_METHODS));
server.addConnector(createConnector(server));
final ContextRoutingHandler routingHandler = new ContextRoutingHandler(ImmutableMap.of(
"/service", service,
"/secret", secret
));
final BiDiGzipHandler gzipHandler = new BiDiGzipHandler();
gzipHandler.setHandler(routingHandler);
gzipHandler.setMinGzipSize(256);
gzipHandler.setInputBufferSize(8 * 1024);
gzipHandler.setCompressionLevel(Deflater.DEFAULT_COMPRESSION);
gzipHandler.setSyncFlush(false);
final RequestLogHandler requestLogHandler = new RequestLogHandler();
// server should own the request log's lifecycle since it's already started,
// the handler might not become managed in case of an error which would leave
// the request log stranded
server.addBean(requestLogHandler.getRequestLog(), true);
requestLogHandler.setHandler(gzipHandler);
// Graceful shutdown is implemented via the statistics handler,
// see https://bugs.eclipse.org/bugs/show_bug.cgi?id=420142
final StatisticsHandler statisticsHandler = new StatisticsHandler();
statisticsHandler.setHandler(requestLogHandler);
server.setHandler(statisticsHandler);
server.start();
server.join();
}
private static Connector createConnector(Server server) {
final ServerConnector connector = new ServerConnector(server, Math.max(1, Runtime.getRuntime().availableProcessors() / 2), Runtime.getRuntime().availableProcessors());
connector.setPort(8080);
connector.setInheritChannel(false);
connector.setReuseAddress(true);
return connector;
}
public static class GetServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request,
HttpServletResponse response ) throws ServletException, IOException
{
response.setContentType("text/plain");
response.setStatus(HttpServletResponse.SC_OK);
response.getWriter().println(request.getParameterMap());
}
}
public static class PostServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest request,
HttpServletResponse response ) throws ServletException, IOException
{
response.setContentType("text/plain");
response.setStatus(HttpServletResponse.SC_OK);
final String body = CharStreams.toString(new InputStreamReader(request.getInputStream(), Charsets.UTF_8));
try (PrintWriter output = response.getWriter()) {
output.println(request.getParameterMap() + " " + body);
}
}
}
public static class ContextRoutingHandler extends AbstractHandler {
private final Trie<Handler> handlers;
public ContextRoutingHandler(Map<String, ? extends Handler> handlers) {
this.handlers = new ArrayTernaryTrie<>(false);
for (Map.Entry<String, ? extends Handler> entry : handlers.entrySet()) {
if (!this.handlers.put(entry.getKey(), entry.getValue())) {
throw new IllegalStateException("Too many handlers");
}
addBean(entry.getValue());
}
}
@Override
public void handle(String target,
Request baseRequest,
HttpServletRequest request,
HttpServletResponse response) throws IOException, ServletException {
final Handler handler = handlers.getBest(baseRequest.getRequestURI());
if (handler != null) {
handler.handle(target, baseRequest, request, response);
}
}
}
public static class MutableServletContextHandler extends ServletContextHandler {
public boolean isSecurityEnabled() {
return (this._options & SECURITY) != 0;
}
public void setSecurityEnabled(boolean enabled) {
if (enabled) {
this._options |= SECURITY;
} else {
this._options &= ~SECURITY;
}
}
public boolean isSessionsEnabled() {
return (this._options & SESSIONS) != 0;
}
public void setSessionsEnabled(boolean enabled) {
if (enabled) {
this._options |= SESSIONS;
} else {
this._options &= ~SESSIONS;
}
}
}
public static class NonblockingServletHolder extends ServletHolder {
private final Servlet servlet;
public NonblockingServletHolder(Servlet servlet) {
super(servlet);
setInitOrder(1);
this.servlet = servlet;
}
@Override
public boolean equals(Object o) {
return super.equals(o);
}
@Override
public int hashCode() {
return super.hashCode();
}
@Override
public synchronized Servlet getServlet() throws ServletException {
return servlet;
}
@Override
public void handle(Request baseRequest,
ServletRequest request,
ServletResponse response) throws ServletException, IOException {
final boolean asyncSupported = baseRequest.isAsyncSupported();
if (!isAsyncSupported()) {
baseRequest.setAsyncSupported(false, null);
}
try {
servlet.service(request, response);
} finally {
baseRequest.setAsyncSupported(asyncSupported, null);
}
}
}
public static class AllowedMethodsFilter implements Filter {
public static final String ALLOWED_METHODS_PARAM = "allowedMethods";
public static final Set<String> DEFAULT_ALLOWED_METHODS = ImmutableSet.of(
"GET", "POST", "PUT", "DELETE", "HEAD", "OPTIONS", "PATCH"
);
private Set<String> allowedMethods = new HashSet<>();
@Override
public void init(FilterConfig config) {
final String allowedMethodsConfig = config.getInitParameter(ALLOWED_METHODS_PARAM);
if (allowedMethodsConfig == null) {
allowedMethods.addAll(DEFAULT_ALLOWED_METHODS);
} else {
allowedMethods.addAll(Arrays.asList(allowedMethodsConfig.split(",")));
}
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
handle((HttpServletRequest) request, (HttpServletResponse) response, chain);
}
private void handle(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
if (allowedMethods.contains(request.getMethod())) {
chain.doFilter(request, response);
} else {
response.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
}
}
@Override
public void destroy() {
allowedMethods.clear();
}
}
public static class BiDiGzipHandler extends GzipHandler {
private final ThreadLocal<Inflater> localInflater = new ThreadLocal<>();
/**
* Size of the buffer for decompressing requests
*/
private int inputBufferSize = 8192;
/**
* Whether inflating (decompressing) of deflate-encoded requests
* should be performed in the GZIP-compatible mode
*/
private boolean inflateNoWrap = true;
public boolean isInflateNoWrap() {
return inflateNoWrap;
}
public void setInflateNoWrap(boolean inflateNoWrap) {
this.inflateNoWrap = inflateNoWrap;
}
public BiDiGzipHandler() {
}
public void setInputBufferSize(int inputBufferSize) {
this.inputBufferSize = inputBufferSize;
}
@Override
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException {
final String encoding = request.getHeader(HttpHeader.CONTENT_ENCODING.asString());
if (GZIP.equalsIgnoreCase(encoding)) {
super.handle(target, baseRequest, wrapGzippedRequest(removeContentEncodingHeader(request)), response);
} else if (DEFLATE.equalsIgnoreCase(encoding)) {
super.handle(target, baseRequest, wrapDeflatedRequest(removeContentEncodingHeader(request)), response);
} else {
super.handle(target, baseRequest, request, response);
}
}
private Inflater buildInflater() {
final Inflater inflater = localInflater.get();
if (inflater != null) {
// The request could fail in the middle of decompressing, so potentially we can get
// a broken inflater in the thread local storage. That's why we need to clear the storage.
localInflater.set(null);
// Reuse the inflater from the thread local storage
inflater.reset();
return inflater;
} else {
return new Inflater(inflateNoWrap);
}
}
private WrappedServletRequest wrapDeflatedRequest(HttpServletRequest request) throws IOException {
final Inflater inflater = buildInflater();
final InflaterInputStream input = new InflaterInputStream(request.getInputStream(), inflater, inputBufferSize) {
@Override
public void close() throws IOException {
super.close();
localInflater.set(inflater);
}
};
return new WrappedServletRequest(request, input);
}
private WrappedServletRequest wrapGzippedRequest(HttpServletRequest request) throws IOException {
return new WrappedServletRequest(request, new GZIPInputStream(request.getInputStream(), inputBufferSize));
}
private HttpServletRequest removeContentEncodingHeader(final HttpServletRequest request) {
return new RemoveHttpHeaderWrapper(request, HttpHeader.CONTENT_ENCODING.asString());
}
private static class WrappedServletRequest extends HttpServletRequestWrapper {
private final ServletInputStream input;
private final BufferedReader reader;
private WrappedServletRequest(HttpServletRequest request,
InputStream inputStream) throws IOException {
super(request);
this.input = new WrappedServletInputStream(inputStream);
this.reader = new BufferedReader(new InputStreamReader(input, getCharset()));
}
private Charset getCharset() {
final String encoding = getCharacterEncoding();
if (encoding == null || !Charset.isSupported(encoding)) {
return StandardCharsets.ISO_8859_1;
}
return Charset.forName(encoding);
}
@Override
public ServletInputStream getInputStream() throws IOException {
return input;
}
@Override
public BufferedReader getReader() throws IOException {
return reader;
}
}
private static class WrappedServletInputStream extends ServletInputStream {
private final InputStream input;
private WrappedServletInputStream(InputStream input) {
this.input = input;
}
@Override
public void close() throws IOException {
input.close();
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
return input.read(b, off, len);
}
@Override
public int available() throws IOException {
return input.available();
}
@Override
public void mark(int readlimit) {
input.mark(readlimit);
}
@Override
public boolean markSupported() {
return input.markSupported();
}
@Override
public int read() throws IOException {
return input.read();
}
@Override
public void reset() throws IOException {
input.reset();
}
@Override
public long skip(long n) throws IOException {
return input.skip(n);
}
@Override
public int read(byte[] b) throws IOException {
return input.read(b);
}
@Override
public boolean isFinished() {
try {
return input.available() == 0;
} catch (IOException ignored) {
}
return true;
}
@Override
public boolean isReady() {
try {
return input.available() > 0;
} catch (IOException ignored) {
}
return false;
}
@Override
public void setReadListener(ReadListener readListener) {
throw new UnsupportedOperationException();
}
}
private static class RemoveHttpHeaderWrapper extends HttpServletRequestWrapper {
private final String headerName;
RemoveHttpHeaderWrapper(final HttpServletRequest request, final String headerName) {
super(request);
this.headerName = headerName;
}
/**
* The default behavior of this method is to return
* getIntHeader(String name) on the wrapped request object.
*
* @param name a <code>String</code> specifying the name of a request header
*/
@Override
public int getIntHeader(final String name) {
if (headerName.equalsIgnoreCase(name)) {
return -1;
} else {
return super.getIntHeader(name);
}
}
/**
* The default behavior of this method is to return getHeaders(String name)
* on the wrapped request object.
*
* @param name a <code>String</code> specifying the name of a request header
*/
@Override
public Enumeration<String> getHeaders(final String name) {
if (headerName.equalsIgnoreCase(name)) {
return Collections.emptyEnumeration();
} else {
return super.getHeaders(name);
}
}
/**
* The default behavior of this method is to return getHeader(String name)
* on the wrapped request object.
*
* @param name a <code>String</code> specifying the name of a request header
*/
@Override
public String getHeader(final String name) {
if (headerName.equalsIgnoreCase(name)) {
return null;
} else {
return super.getHeader(name);
}
}
/**
* The default behavior of this method is to return getDateHeader(String name)
* on the wrapped request object.
*
* @param name a <code>String</code> specifying the name of a request header
*/
@Override
public long getDateHeader(final String name) {
if (headerName.equalsIgnoreCase(name)) {
return -1L;
} else {
return super.getDateHeader(name);
}
}
}
}
}
package com.example.jetty;
import com.google.common.io.CharStreams;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
public class ProgramClient {
public static void main(String[] args) throws Exception {
System.out.println(httpRequest("GET", "http://localhost:8080/service?name=test_user"));
System.out.println(httpRequest("POST", "http://localhost:8080/secret?name=test_user"));
System.out.println(httpRequest("POST", "http://localhost:8080/secret?name=test_user"));
System.out.println(httpRequest("POST", "http://localhost:8080/secret?name=test_user"));
System.out.println(httpRequest("POST", "http://localhost:8080/secret?name=test_user"));
System.out.println(httpRequest("POST", "http://localhost:8080/secret?name=test_user"));
System.out.println(httpRequest("POST", "http://localhost:8080/secret?name=test_user"));
}
private static String httpRequest(String requestMethod, String url) throws Exception {
final HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
connection.setRequestMethod(requestMethod);
connection.connect();
try (InputStream inputStream = connection.getInputStream()) {
return CharStreams.toString(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
} finally {
connection.disconnect();
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment