From 4ab98298795ea60bd58205d6551e63f7e14a4034 Mon Sep 17 00:00:00 2001 From: Jason Hoetger Date: Mon, 6 Feb 2017 16:42:07 -0800 Subject: [PATCH] Added allowRequestsToOriginServer option to Bootstrap to allow handling origin-form requests --- .../proxy/HttpProxyServerBootstrap.java | 10 +++++ .../proxy/impl/ClientToProxyConnection.java | 5 ++- .../proxy/impl/DefaultHttpProxyServer.java | 32 ++++++++++--- .../littleshoot/proxy/DirectRequestTest.java | 45 +++++++++++++++++++ 4 files changed, 84 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/littleshoot/proxy/HttpProxyServerBootstrap.java b/src/main/java/org/littleshoot/proxy/HttpProxyServerBootstrap.java index 57b7c85d2..367dc8dd1 100644 --- a/src/main/java/org/littleshoot/proxy/HttpProxyServerBootstrap.java +++ b/src/main/java/org/littleshoot/proxy/HttpProxyServerBootstrap.java @@ -301,6 +301,16 @@ HttpProxyServerBootstrap withConnectTimeout( HttpProxyServerBootstrap withMaxChunkSize(int maxChunkSize); + /** + * When true, the proxy will accept requests that appear to be directed at an origin server (i.e. the URI in the HTTP + * request will contain an origin-form, rather than an absolute-form, as specified in RFC 7230, section 5.3). + * This is useful when the proxy is acting as a gateway/reverse proxy. Note: This feature should not be + * enabled when running as a forward proxy; doing so may cause an infinite loop if the client requests the URI of the proxy. + * + * @param allowRequestToOriginServer when true, the proxy will accept origin-form HTTP requests + */ + HttpProxyServerBootstrap withAllowRequestToOriginServer(boolean allowRequestToOriginServer); + /** * Sets the alias to use when adding Via headers to incoming and outgoing HTTP messages. The alias may be any * pseudonym, or if not specified, defaults to the hostname of the local machine. See RFC 7230, section 5.7.1. diff --git a/src/main/java/org/littleshoot/proxy/impl/ClientToProxyConnection.java b/src/main/java/org/littleshoot/proxy/impl/ClientToProxyConnection.java index 76f0d5ab7..bdaf470af 100644 --- a/src/main/java/org/littleshoot/proxy/impl/ClientToProxyConnection.java +++ b/src/main/java/org/littleshoot/proxy/impl/ClientToProxyConnection.java @@ -252,8 +252,9 @@ private ConnectionState doReadHTTPInitial(HttpRequest httpRequest) { } } - // short-circuit requests that treat the proxy as the "origin" server, to avoid infinite loops - if (isRequestToOriginServer(httpRequest)) { + // if origin-form requests are not explicitly enabled, short-circuit requests that treat the proxy as the + // origin server, to avoid infinite loops + if (!proxyServer.isAllowRequestsToOriginServer() && isRequestToOriginServer(httpRequest)) { boolean keepAlive = writeBadRequest(httpRequest); if (keepAlive) { return AWAITING_INITIAL; diff --git a/src/main/java/org/littleshoot/proxy/impl/DefaultHttpProxyServer.java b/src/main/java/org/littleshoot/proxy/impl/DefaultHttpProxyServer.java index 95f66cdd3..1891532e4 100644 --- a/src/main/java/org/littleshoot/proxy/impl/DefaultHttpProxyServer.java +++ b/src/main/java/org/littleshoot/proxy/impl/DefaultHttpProxyServer.java @@ -116,6 +116,7 @@ public class DefaultHttpProxyServer implements HttpProxyServer { private final int maxInitialLineLength; private final int maxHeaderSize; private final int maxChunkSize; + private final boolean allowRequestsToOriginServer; /** * The alias or pseudonym for this proxy, used when adding the Via header. @@ -227,6 +228,8 @@ public static HttpProxyServerBootstrap bootstrapFromFile(String path) { * @param maxInitialLineLength * @param maxHeaderSize * @param maxChunkSize + * @param allowRequestsToOriginServer + * when true, allow the proxy to handle requests that contain an origin-form URI, as defined in RFC 7230 5.3.1 */ private DefaultHttpProxyServer(ServerGroup serverGroup, TransportProtocol transportProtocol, @@ -248,7 +251,8 @@ private DefaultHttpProxyServer(ServerGroup serverGroup, String proxyAlias, int maxInitialLineLength, int maxHeaderSize, - int maxChunkSize) { + int maxChunkSize, + boolean allowRequestsToOriginServer) { this.serverGroup = serverGroup; this.transportProtocol = transportProtocol; this.requestedAddress = requestedAddress; @@ -284,8 +288,9 @@ private DefaultHttpProxyServer(ServerGroup serverGroup, this.proxyAlias = proxyAlias; } this.maxInitialLineLength = maxInitialLineLength; - this.maxHeaderSize = maxHeaderSize; - this.maxChunkSize = maxChunkSize; + this.maxHeaderSize = maxHeaderSize; + this.maxChunkSize = maxChunkSize; + this.allowRequestsToOriginServer = allowRequestsToOriginServer; } /** @@ -375,6 +380,10 @@ public int getMaxChunkSize() { return maxChunkSize; } + public boolean isAllowRequestsToOriginServer() { + return allowRequestsToOriginServer; + } + @Override public HttpProxyServerBootstrap clone() { return new DefaultHttpProxyServerBootstrap(serverGroup, @@ -398,7 +407,8 @@ public HttpProxyServerBootstrap clone() { proxyAlias, maxInitialLineLength, maxHeaderSize, - maxChunkSize); + maxChunkSize, + allowRequestsToOriginServer); } @Override @@ -613,6 +623,7 @@ private static class DefaultHttpProxyServerBootstrap implements HttpProxyServerB private int maxInitialLineLength = MAX_INITIAL_LINE_LENGTH_DEFAULT; private int maxHeaderSize = MAX_HEADER_SIZE_DEFAULT; private int maxChunkSize = MAX_CHUNK_SIZE_DEFAULT; + private boolean allowRequestToOriginServer = false; private DefaultHttpProxyServerBootstrap() { } @@ -636,7 +647,8 @@ private DefaultHttpProxyServerBootstrap( String proxyAlias, int maxInitialLineLength, int maxHeaderSize, - int maxChunkSize) { + int maxChunkSize, + boolean allowRequestToOriginServer) { this.serverGroup = serverGroup; this.transportProtocol = transportProtocol; this.requestedAddress = requestedAddress; @@ -661,6 +673,7 @@ private DefaultHttpProxyServerBootstrap( this.maxInitialLineLength = maxInitialLineLength; this.maxHeaderSize = maxHeaderSize; this.maxChunkSize = maxChunkSize; + this.allowRequestToOriginServer = allowRequestToOriginServer; } private DefaultHttpProxyServerBootstrap(Properties props) { @@ -854,6 +867,12 @@ public HttpProxyServerBootstrap withMaxChunkSize(int maxChunkSize){ return this; } + @Override + public HttpProxyServerBootstrap withAllowRequestToOriginServer(boolean allowRequestToOriginServer) { + this.allowRequestToOriginServer = allowRequestToOriginServer; + return this; + } + @Override public HttpProxyServer start() { return build().start(); @@ -884,7 +903,8 @@ transportProtocol, determineListenAddress(), filtersSource, transparent, idleConnectionTimeout, activityTrackers, connectTimeout, serverResolver, readThrottleBytesPerSecond, writeThrottleBytesPerSecond, - localAddress, proxyAlias, maxInitialLineLength, maxHeaderSize, maxChunkSize); + localAddress, proxyAlias, maxInitialLineLength, maxHeaderSize, maxChunkSize, + allowRequestToOriginServer); } private InetSocketAddress determineListenAddress() { diff --git a/src/test/java/org/littleshoot/proxy/DirectRequestTest.java b/src/test/java/org/littleshoot/proxy/DirectRequestTest.java index c5324e37c..5eeeea1a8 100644 --- a/src/test/java/org/littleshoot/proxy/DirectRequestTest.java +++ b/src/test/java/org/littleshoot/proxy/DirectRequestTest.java @@ -1,6 +1,7 @@ package org.littleshoot.proxy; import io.netty.handler.codec.http.DefaultHttpResponse; +import io.netty.handler.codec.http.HttpHeaders; import io.netty.handler.codec.http.HttpObject; import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.HttpResponse; @@ -13,10 +14,12 @@ import org.littleshoot.proxy.test.HttpClientUtil; import javax.net.ssl.SSLException; +import java.util.concurrent.atomic.AtomicBoolean; import static org.hamcrest.Matchers.instanceOf; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; /** * This class tests direct requests to the proxy server, which causes endless @@ -97,6 +100,48 @@ public void testHttpsShouldCancelConnection() { } } + @Test(timeout = 5000) + public void testAllowRequestToOriginServerWithOverride() { + // verify that the filter is hit twice: first, on the request from the client, without a Via header; and second, when the proxy + // forwards the request to itself + final AtomicBoolean receivedRequestWithoutVia = new AtomicBoolean(); + + proxyServer = DefaultHttpProxyServer.bootstrap() + .withPort(0) + .withAllowRequestToOriginServer(true) + .withProxyAlias("testAllowRequestToOriginServerWithOverride") + .withFiltersSource(new HttpFiltersSourceAdapter() { + @Override + public HttpFilters filterRequest(HttpRequest originalRequest) { + return new HttpFiltersAdapter(originalRequest) { + @Override + public HttpResponse clientToProxyRequest(HttpObject httpObject) { + if (httpObject instanceof HttpRequest) { + HttpRequest request = (HttpRequest) httpObject; + String viaHeader = request.headers().get(HttpHeaders.Names.VIA); + if (viaHeader != null && viaHeader.contains("testAllowRequestToOriginServerWithOverride")) { + return new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.NO_CONTENT); + } else { + receivedRequestWithoutVia.set(true); + } + } + return null; + } + }; + } + }) + .start(); + + int proxyPort = proxyServer.getListenAddress().getPort(); + + org.apache.http.HttpResponse response = HttpClientUtil.performHttpGet("http://localhost:" + proxyPort + "/originrequest", proxyServer); + int statusCode = response.getStatusLine().getStatusCode(); + + assertEquals("Expected to receive a 204 response from the filter", 204, statusCode); + + assertTrue("Expected to receive a request from the client without a Via header", receivedRequestWithoutVia.get()); + } + private void startProxyServer() { proxyServer = DefaultHttpProxyServer.bootstrap() .withPort(0)