Created
September 3, 2016 02:48
-
-
Save nickbabcock/318d233c293444cb4fc22a8a80947f1f to your computer and use it in GitHub Desktop.
Trying to replicate bug found in dropwizard
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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