From 28fd4cceac93ccb2a3652ec143e865f97750c278 Mon Sep 17 00:00:00 2001 From: Simone Bordet Date: Tue, 6 Nov 2012 11:04:43 +0100 Subject: [PATCH 1/5] Implemented proxy functionalities for both client and server. --- .../org/eclipse/jetty/client/HttpClient.java | 15 +- .../eclipse/jetty/client/HttpConnection.java | 7 +- .../jetty/client/HttpContentResponse.java | 2 +- .../jetty/client/HttpConversation.java | 2 +- .../eclipse/jetty/client/HttpDestination.java | 168 ++- .../eclipse/jetty/client/HttpExchange.java | 2 +- .../eclipse/jetty/client/HttpReceiver.java | 4 +- .../org/eclipse/jetty/client/HttpRequest.java | 9 +- .../eclipse/jetty/client/HttpResponse.java | 2 +- .../org/eclipse/jetty/client/HttpSender.java | 11 +- .../jetty/client/RedirectProtocolHandler.java | 5 +- .../jetty/client/api/ProxyConfiguration.java | 41 +- .../org/eclipse/jetty/client/api/Request.java | 9 +- .../eclipse/jetty/client/api/Response.java | 2 +- .../client/util/BlockingResponseListener.java | 7 +- .../util/BufferingResponseListener.java | 2 +- .../util/InputStreamContentProvider.java | 13 +- .../client/util/TimedResponseListener.java | 22 +- .../jetty/client/HttpClientProxyTest.java | 89 ++ .../jetty/client/HttpClientTimeoutTest.java | 4 +- jetty-proxy/pom.xml | 177 +-- .../eclipse/jetty/proxy/BalancerServlet.java | 579 +++----- .../eclipse/jetty/proxy/ConnectHandler.java | 270 ++-- .../org/eclipse/jetty/proxy/ProxyServlet.java | 1231 +++++++--------- .../proxy/AbstractBalancerServletTest.java | 164 --- .../proxy/AbstractConnectHandlerTest.java | 364 +++-- .../eclipse/jetty/proxy/AsyncProxyServer.java | 53 - .../jetty/proxy/BalancerServletTest.java | 149 +- .../jetty/proxy/ConnectHandlerSSLTest.java | 407 +++--- .../jetty/proxy/ConnectHandlerTest.java | 1286 ++++++++--------- .../jetty/proxy/ConnectHandlerUnitTest.java | 86 +- .../org/eclipse/jetty/proxy/ProxyServer.java | 24 +- .../eclipse/jetty/proxy/ProxyServletTest.java | 775 ++++++++-- .../test/resources/jetty-logging.properties | 4 + pom.xml | 2 +- 35 files changed, 3103 insertions(+), 2884 deletions(-) rename jetty-proxy/src/main/java/org/eclipse/jetty/proxy/ProxyHandler.java => jetty-client/src/main/java/org/eclipse/jetty/client/api/ProxyConfiguration.java (54%) create mode 100644 jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientProxyTest.java delete mode 100644 jetty-proxy/src/test/java/org/eclipse/jetty/proxy/AbstractBalancerServletTest.java delete mode 100644 jetty-proxy/src/test/java/org/eclipse/jetty/proxy/AsyncProxyServer.java rename jetty-proxy/src/{main => test}/java/org/eclipse/jetty/proxy/ProxyServer.java (71%) create mode 100644 jetty-proxy/src/test/resources/jetty-logging.properties diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpClient.java b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpClient.java index d4fc27fb973..72364086332 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpClient.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpClient.java @@ -20,7 +20,6 @@ package org.eclipse.jetty.client; import java.io.IOException; import java.net.ConnectException; -import java.net.InetSocketAddress; import java.net.SocketAddress; import java.net.SocketException; import java.net.URI; @@ -43,6 +42,7 @@ import org.eclipse.jetty.client.api.Connection; import org.eclipse.jetty.client.api.ContentResponse; import org.eclipse.jetty.client.api.CookieStore; import org.eclipse.jetty.client.api.Destination; +import org.eclipse.jetty.client.api.ProxyConfiguration; import org.eclipse.jetty.client.api.Request; import org.eclipse.jetty.client.api.Response; import org.eclipse.jetty.http.HttpFields; @@ -126,6 +126,7 @@ public class HttpClient extends ContainerLifeCycle private volatile long idleTimeout; private volatile boolean tcpNoDelay = true; private volatile boolean dispatchIO = true; + private volatile ProxyConfiguration proxyConfig; public HttpClient() { @@ -350,7 +351,7 @@ public class HttpClient extends ContainerLifeCycle channel.bind(bindAddress); configure(channel); channel.configureBlocking(false); - channel.connect(new InetSocketAddress(destination.getHost(), destination.getPort())); + channel.connect(destination.getAddress()); Future result = new ConnectionCallback(destination, callback); selectorManager.connect(channel, result); @@ -595,6 +596,16 @@ public class HttpClient extends ContainerLifeCycle this.dispatchIO = dispatchIO; } + public ProxyConfiguration getProxyConfiguration() + { + return proxyConfig; + } + + public void setProxyConfiguration(ProxyConfiguration proxyConfig) + { + this.proxyConfig = proxyConfig; + } + @Override public void dump(Appendable out, String indent) throws IOException { diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpConnection.java b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpConnection.java index aae92bcac98..0b56761de19 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpConnection.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpConnection.java @@ -155,6 +155,11 @@ public class HttpConnection extends AbstractConnection implements Connection path = "/"; request.path(path); } + if (destination.isProxied() && HttpMethod.CONNECT != request.getMethod()) + { + path = request.getURI(); + request.path(path); + } Fields fields = request.getParams(); if (!fields.isEmpty()) @@ -359,7 +364,7 @@ public class HttpConnection extends AbstractConnection implements Connection } } - public boolean abort(HttpExchange exchange, String reason) + public boolean abort(HttpExchange exchange, Throwable reason) { // We want the return value to be that of the response // because if the response has already successfully diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpContentResponse.java b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpContentResponse.java index aa9dd777eab..b0783f5df72 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpContentResponse.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpContentResponse.java @@ -77,7 +77,7 @@ public class HttpContentResponse implements ContentResponse } @Override - public boolean abort(String reason) + public boolean abort(Throwable reason) { return response.abort(reason); } diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpConversation.java b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpConversation.java index bacb391e20e..40b088759af 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpConversation.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpConversation.java @@ -98,7 +98,7 @@ public class HttpConversation implements Attributes attributes.clear(); } - public boolean abort(String reason) + public boolean abort(Throwable reason) { HttpExchange exchange = exchanges.peekLast(); return exchange != null && exchange.abort(reason); diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpDestination.java b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpDestination.java index 4145d1d6bf2..770c55e738c 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpDestination.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpDestination.java @@ -19,6 +19,7 @@ package org.eclipse.jetty.client; import java.io.IOException; +import java.net.InetSocketAddress; import java.nio.channels.AsynchronousCloseException; import java.util.ArrayList; import java.util.List; @@ -27,13 +28,19 @@ import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; import java.util.concurrent.Future; import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import org.eclipse.jetty.client.api.Connection; import org.eclipse.jetty.client.api.Destination; +import org.eclipse.jetty.client.api.ProxyConfiguration; import org.eclipse.jetty.client.api.Request; import org.eclipse.jetty.client.api.Response; import org.eclipse.jetty.client.api.Result; +import org.eclipse.jetty.client.util.TimedResponseListener; +import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.http.HttpMethod; +import org.eclipse.jetty.http.HttpScheme; import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.util.FutureCallback; import org.eclipse.jetty.util.component.ContainerLifeCycle; @@ -48,25 +55,28 @@ public class HttpDestination implements Destination, AutoCloseable, Dumpable private final AtomicInteger connectionCount = new AtomicInteger(); private final HttpClient client; private final String scheme; - private final String host; - private final int port; + private final InetSocketAddress address; private final Queue requests; private final BlockingQueue idleConnections; private final BlockingQueue activeConnections; private final RequestNotifier requestNotifier; private final ResponseNotifier responseNotifier; + private final InetSocketAddress proxyAddress; public HttpDestination(HttpClient client, String scheme, String host, int port) { this.client = client; this.scheme = scheme; - this.host = host; - this.port = port; + this.address = new InetSocketAddress(host, port); this.requests = new ArrayBlockingQueue<>(client.getMaxQueueSizePerAddress()); this.idleConnections = new ArrayBlockingQueue<>(client.getMaxConnectionsPerAddress()); this.activeConnections = new ArrayBlockingQueue<>(client.getMaxConnectionsPerAddress()); this.requestNotifier = new RequestNotifier(client); this.responseNotifier = new ResponseNotifier(client); + + ProxyConfiguration proxyConfig = client.getProxyConfiguration(); + proxyAddress = proxyConfig != null && proxyConfig.matches(host, port) ? + new InetSocketAddress(proxyConfig.getHost(), proxyConfig.getPort()) : null; } protected BlockingQueue getIdleConnections() @@ -88,23 +98,33 @@ public class HttpDestination implements Destination, AutoCloseable, Dumpable @Override public String getHost() { - return host; + return address.getHostString(); } @Override public int getPort() { - return port; + return address.getPort(); + } + + public InetSocketAddress getAddress() + { + return isProxied() ? proxyAddress : address; + } + + public boolean isProxied() + { + return proxyAddress != null; } public void send(Request request, List listeners) { if (!scheme.equals(request.getScheme())) throw new IllegalArgumentException("Invalid request scheme " + request.getScheme() + " for destination " + this); - if (!host.equals(request.getHost())) + if (!getHost().equals(request.getHost())) throw new IllegalArgumentException("Invalid request host " + request.getHost() + " for destination " + this); int port = request.getPort(); - if (port >= 0 && this.port != port) + if (port >= 0 && getPort() != port) throw new IllegalArgumentException("Invalid request port " + port + " for destination " + this); RequestContext requestContext = new RequestContext(request, listeners); @@ -139,7 +159,7 @@ public class HttpDestination implements Destination, AutoCloseable, Dumpable public Future newConnection() { FutureCallback result = new FutureCallback<>(); - newConnection(result); + newConnection(new CONNECTCallback(result)); return result; } @@ -170,30 +190,31 @@ public class HttpDestination implements Destination, AutoCloseable, Dumpable if (connectionCount.compareAndSet(current, next)) { LOG.debug("Creating connection {}/{} for {}", next, maxConnections, this); - newConnection(new Callback() + + CONNECTCallback connectCallback = new CONNECTCallback(new Callback() { @Override public void completed(Connection connection) { - LOG.debug("Created connection {}/{} {} for {}", next, maxConnections, connection, HttpDestination.this); process(connection, true); } @Override - public void failed(Connection connection, final Throwable x) + public void failed(final Connection connection, final Throwable x) { - LOG.debug("Connection failed {} for {}", x, HttpDestination.this); - connectionCount.decrementAndGet(); client.getExecutor().execute(new Runnable() { @Override public void run() { drain(x); + if (connection != null) + connection.close(); } }); } }); + newConnection(new TCPCallback(next, maxConnections, connectCallback)); // Try again the idle connections return idleConnections.poll(); } @@ -248,9 +269,10 @@ public class HttpDestination implements Destination, AutoCloseable, Dumpable { final Request request = requestContext.request; final List listeners = requestContext.listeners; - if (request.aborted()) + Throwable aborted = request.aborted(); + if (aborted != null) { - abort(request, listeners, "Aborted"); + abort(request, listeners, aborted); LOG.debug("Aborted {} before processing", request); } else @@ -300,10 +322,13 @@ public class HttpDestination implements Destination, AutoCloseable, Dumpable public void remove(Connection connection) { - LOG.debug("{} removed", connection); - connectionCount.decrementAndGet(); - activeConnections.remove(connection); - idleConnections.remove(connection); + boolean removed = activeConnections.remove(connection); + removed |= idleConnections.remove(connection); + if (removed) + { + LOG.debug("{} removed", connection); + connectionCount.decrementAndGet(); + } // We need to execute queued requests even if this connection failed. // We may create a connection that is not needed, but it will eventually @@ -334,7 +359,7 @@ public class HttpDestination implements Destination, AutoCloseable, Dumpable LOG.debug("Closed {}", this); } - public boolean abort(Request request, String reason) + public boolean abort(Request request, Throwable reason) { for (RequestContext requestContext : requests) { @@ -352,13 +377,11 @@ public class HttpDestination implements Destination, AutoCloseable, Dumpable return false; } - private void abort(Request request, List listeners, String reason) + private void abort(Request request, List listeners, Throwable reason) { HttpResponse response = new HttpResponse(request, listeners); - HttpResponseException responseFailure = new HttpResponseException(reason, response); - responseNotifier.notifyFailure(listeners, response, responseFailure); - HttpRequestException requestFailure = new HttpRequestException(reason, request); - responseNotifier.notifyComplete(listeners, new Result(request, requestFailure, response, responseFailure)); + responseNotifier.notifyFailure(listeners, response, reason); + responseNotifier.notifyComplete(listeners, new Result(request, reason, response, reason)); } @Override @@ -382,7 +405,12 @@ public class HttpDestination implements Destination, AutoCloseable, Dumpable @Override public String toString() { - return String.format("%s(%s://%s:%d)", HttpDestination.class.getSimpleName(), getScheme(), getHost(), getPort()); + return String.format("%s(%s://%s:%d)%s", + HttpDestination.class.getSimpleName(), + getScheme(), + getHost(), + getPort(), + proxyAddress == null ? "" : " via " + proxyAddress.getHostString() + ":" + proxyAddress.getPort()); } private static class RequestContext @@ -396,4 +424,90 @@ public class HttpDestination implements Destination, AutoCloseable, Dumpable this.listeners = listeners; } } + + private class TCPCallback implements Callback + { + private final int current; + private final int max; + private final Callback delegate; + + private TCPCallback(int current, int max, Callback delegate) + { + this.current = current; + this.max = max; + this.delegate = delegate; + } + + @Override + public void completed(Connection connection) + { + LOG.debug("Created connection {}/{} {} for {}", current, max, connection, HttpDestination.this); + delegate.completed(connection); + } + + @Override + public void failed(Connection connection, Throwable x) + { + LOG.debug("Connection failed {} for {}", x, HttpDestination.this); + connectionCount.decrementAndGet(); + delegate.failed(connection, x); + } + } + + private class CONNECTCallback implements Callback + { + private final Callback delegate; + + private CONNECTCallback(Callback delegate) + { + this.delegate = delegate; + } + + @Override + public void completed(Connection connection) + { + boolean tunnel = isProxied() && + "https".equalsIgnoreCase(getScheme()) && + client.getSslContextFactory() != null; + if (tunnel) + tunnel(connection); + else + delegate.completed(connection); + } + + @Override + public void failed(Connection connection, Throwable x) + { + delegate.failed(connection, x); + } + + private void tunnel(final Connection connection) + { + String target = address.getHostString() + ":" + address.getPort(); + Request connect = client.newRequest(proxyAddress.getHostString(), proxyAddress.getPort()) + .scheme(HttpScheme.HTTP.asString()) + .method(HttpMethod.CONNECT) + .path(target) + .header(HttpHeader.HOST.asString(), target); + connection.send(connect, new TimedResponseListener(client.getConnectTimeout(), TimeUnit.MILLISECONDS, connect, new Response.CompleteListener() + { + @Override + public void onComplete(Result result) + { + if (result.isFailed()) + { + failed(connection, result.getFailure()); + } + else + { + Response response = result.getResponse(); + if (response.getStatus() == 200) + delegate.completed(connection); + else + failed(connection, new HttpResponseException("Received " + response + " for " + result.getRequest(), response)); + } + } + })); + } + } } diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpExchange.java b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpExchange.java index 8abcd3d2201..d41d9b82de9 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpExchange.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpExchange.java @@ -193,7 +193,7 @@ public class HttpExchange return new AtomicMarkableReference<>(result, modified); } - public boolean abort(String reason) + public boolean abort(Throwable reason) { LOG.debug("Aborting {} reason {}", this, reason); boolean aborted = connection.abort(this, reason); diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpReceiver.java b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpReceiver.java index b922ef24fa4..aaf690833e1 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpReceiver.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpReceiver.java @@ -386,9 +386,9 @@ public class HttpReceiver implements HttpParser.ResponseHandler fail(new TimeoutException()); } - public boolean abort(HttpExchange exchange, String reason) + public boolean abort(HttpExchange exchange, Throwable reason) { - return fail(new HttpResponseException(reason == null ? "Response aborted" : reason, exchange.getResponse())); + return fail(reason); } private boolean updateState(State from, State to) diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpRequest.java b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpRequest.java index 7dffe62ac6e..48f18fcd72b 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpRequest.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpRequest.java @@ -28,6 +28,7 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.concurrent.Future; import java.util.concurrent.atomic.AtomicLong; @@ -63,7 +64,7 @@ public class HttpRequest implements Request private long idleTimeout; private ContentProvider content; private boolean followRedirects; - private volatile boolean aborted; + private volatile Throwable aborted; public HttpRequest(HttpClient client, URI uri) { @@ -403,9 +404,9 @@ public class HttpRequest implements Request } @Override - public boolean abort(String reason) + public boolean abort(Throwable reason) { - aborted = true; + aborted = Objects.requireNonNull(reason); if (client.provideDestination(getScheme(), getHost(), getPort()).abort(this, reason)) return true; HttpConversation conversation = client.getConversation(getConversationID(), false); @@ -413,7 +414,7 @@ public class HttpRequest implements Request } @Override - public boolean aborted() + public Throwable aborted() { return aborted; } diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpResponse.java b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpResponse.java index 0b49f3be92e..7948248010b 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpResponse.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpResponse.java @@ -98,7 +98,7 @@ public class HttpResponse implements Response } @Override - public boolean abort(String reason) + public boolean abort(Throwable reason) { return request.abort(reason); } diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpSender.java b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpSender.java index bd38c9119f6..a5422759e29 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpSender.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpSender.java @@ -82,9 +82,10 @@ public class HttpSender } Request request = exchange.getRequest(); - if (request.aborted()) + Throwable aborted = request.aborted(); + if (aborted != null) { - exchange.abort(null); + exchange.abort(aborted); } else { @@ -400,7 +401,7 @@ public class HttpSender Result result = completion.getReference(); boolean notCommitted = current == State.IDLE || current == State.SEND; - if (result == null && notCommitted && !request.aborted()) + if (result == null && notCommitted && request.aborted() == null) { result = exchange.responseComplete(failure).getReference(); exchange.terminateResponse(); @@ -418,12 +419,12 @@ public class HttpSender return true; } - public boolean abort(HttpExchange exchange, String reason) + public boolean abort(HttpExchange exchange, Throwable reason) { State current = state.get(); boolean abortable = current == State.IDLE || current == State.SEND || current == State.COMMIT && contentIterator.hasNext(); - return abortable && fail(new HttpRequestException(reason == null ? "Request aborted" : reason, exchange.getRequest())); + return abortable && fail(reason); } private void releaseBuffers(ByteBufferPool bufferPool, ByteBuffer header, ByteBuffer chunk) diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/RedirectProtocolHandler.java b/jetty-client/src/main/java/org/eclipse/jetty/client/RedirectProtocolHandler.java index 82895958ae6..6910fece05e 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/RedirectProtocolHandler.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/RedirectProtocolHandler.java @@ -126,8 +126,9 @@ public class RedirectProtocolHandler extends Response.Listener.Empty implements @Override public void onBegin(Request redirect) { - if (request.aborted()) - redirect.abort(null); + Throwable aborted = request.aborted(); + if (aborted != null) + redirect.abort(aborted); } }); diff --git a/jetty-proxy/src/main/java/org/eclipse/jetty/proxy/ProxyHandler.java b/jetty-client/src/main/java/org/eclipse/jetty/client/api/ProxyConfiguration.java similarity index 54% rename from jetty-proxy/src/main/java/org/eclipse/jetty/proxy/ProxyHandler.java rename to jetty-client/src/main/java/org/eclipse/jetty/client/api/ProxyConfiguration.java index 819e59460ef..f62fb547ab7 100644 --- a/jetty-proxy/src/main/java/org/eclipse/jetty/proxy/ProxyHandler.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/api/ProxyConfiguration.java @@ -16,36 +16,41 @@ // ======================================================================== // -package org.eclipse.jetty.server.handler; +package org.eclipse.jetty.client.api; -import org.eclipse.jetty.server.Handler; +import java.util.HashSet; +import java.util.Set; - -/* ------------------------------------------------------------ */ -/** ProxyHandler. - *

This class has been renamed to ConnectHandler, as it only implements - * the CONNECT method (and a ProxyServlet must be used for full proxy handling). - * @deprecated Use {@link ConnectHandler} - */ -public class ProxyHandler extends ConnectHandler +public class ProxyConfiguration { - public ProxyHandler() + private final Set excluded = new HashSet<>(); + private final String host; + private final int port; + + public ProxyConfiguration(String host, int port) { - super(); + this.host = host; + this.port = port; } - public ProxyHandler(Handler handler, String[] white, String[] black) + public String getHost() { - super(handler,white,black); + return host; } - public ProxyHandler(Handler handler) + public int getPort() { - super(handler); + return port; } - public ProxyHandler(String[] white, String[] black) + public boolean matches(String host, int port) { - super(white,black); + String hostPort = host + ":" + port; + return !getExcludedHosts().contains(hostPort); + } + + public Set getExcludedHosts() + { + return excluded; } } diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/api/Request.java b/jetty-client/src/main/java/org/eclipse/jetty/client/api/Request.java index 681bfe81423..0c2245a02a7 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/api/Request.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/api/Request.java @@ -308,15 +308,16 @@ public interface Request /** * Attempts to abort the send of this request. * - * @param reason the abort reason + * @param reason the abort reason, must not be null * @return whether the abort succeeded */ - boolean abort(String reason); + boolean abort(Throwable reason); /** - * @return whether {@link #abort(String)} was called + * @return the abort reason passed to {@link #abort(Throwable)}, + * or null if this request has not been aborted */ - boolean aborted(); + Throwable aborted(); public interface RequestListener extends EventListener { diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/api/Response.java b/jetty-client/src/main/java/org/eclipse/jetty/client/api/Response.java index 951d4853f66..cdee6714117 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/api/Response.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/api/Response.java @@ -74,7 +74,7 @@ public interface Response * @param reason the abort reason * @return whether the abort succeeded */ - boolean abort(String reason); + boolean abort(Throwable reason); public interface ResponseListener extends EventListener { diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/util/BlockingResponseListener.java b/jetty-client/src/main/java/org/eclipse/jetty/client/util/BlockingResponseListener.java index bef0fbf305e..1adf4b387f9 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/util/BlockingResponseListener.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/util/BlockingResponseListener.java @@ -55,7 +55,7 @@ public class BlockingResponseListener extends BufferingResponseListener implemen public boolean cancel(boolean mayInterruptIfRunning) { cancelled = true; - return request.abort("Cancelled"); + return request.abort(new CancellationException()); } @Override @@ -83,8 +83,9 @@ public class BlockingResponseListener extends BufferingResponseListener implemen boolean expired = !latch.await(timeout, unit); if (expired) { - request.abort("Total timeout elapsed"); - throw new TimeoutException(); + TimeoutException reason = new TimeoutException(); + request.abort(reason); + throw reason; } return getResult(); } diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/util/BufferingResponseListener.java b/jetty-client/src/main/java/org/eclipse/jetty/client/util/BufferingResponseListener.java index 1eba12ed74e..bcda12658ea 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/util/BufferingResponseListener.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/util/BufferingResponseListener.java @@ -63,7 +63,7 @@ public abstract class BufferingResponseListener extends Response.Listener.Empty HttpFields headers = response.getHeaders(); long length = headers.getLongField(HttpHeader.CONTENT_LENGTH.asString()); if (length > maxLength) - response.abort("Buffering capacity exceeded"); + response.abort(new IllegalArgumentException("Buffering capacity exceeded")); String contentType = headers.get(HttpHeader.CONTENT_TYPE); if (contentType != null) diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/util/InputStreamContentProvider.java b/jetty-client/src/main/java/org/eclipse/jetty/client/util/InputStreamContentProvider.java index 5fd0ddc39b5..5a9898b96df 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/util/InputStreamContentProvider.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/util/InputStreamContentProvider.java @@ -49,6 +49,13 @@ public class InputStreamContentProvider implements ContentProvider return -1; } + protected ByteBuffer onRead(byte[] buffer, int offset, int length) + { + if (length <= 0) + return BufferUtil.EMPTY_BUFFER; + return ByteBuffer.wrap(buffer, offset, length); + } + @Override public Iterator iterator() { @@ -71,18 +78,18 @@ public class InputStreamContentProvider implements ContentProvider int read = stream.read(buffer); if (read > 0) { - return ByteBuffer.wrap(buffer, 0, read); + return onRead(buffer, 0, read); } else if (read < 0) { if (eof) throw new NoSuchElementException(); eof = true; - return BufferUtil.EMPTY_BUFFER; + return onRead(buffer, 0, -1); } else { - return BufferUtil.EMPTY_BUFFER; + return onRead(buffer, 0, 0); } } catch (IOException x) diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/util/TimedResponseListener.java b/jetty-client/src/main/java/org/eclipse/jetty/client/util/TimedResponseListener.java index 6f2cee60697..cd12b24506b 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/util/TimedResponseListener.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/util/TimedResponseListener.java @@ -20,6 +20,7 @@ package org.eclipse.jetty.client.util; import java.nio.ByteBuffer; import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicReference; import org.eclipse.jetty.client.Schedulable; @@ -38,14 +39,14 @@ public class TimedResponseListener implements Response.Listener, Schedulable, Ru private final long timeout; private final TimeUnit unit; private final Request request; - private final Response.Listener delegate; + private final Response.CompleteListener delegate; public TimedResponseListener(long timeout, TimeUnit unit, Request request) { this(timeout, unit, request, new Empty()); } - public TimedResponseListener(long timeout, TimeUnit unit, Request request, Response.Listener delegate) + public TimedResponseListener(long timeout, TimeUnit unit, Request request, Response.CompleteListener delegate) { this.timeout = timeout; this.unit = unit; @@ -56,31 +57,36 @@ public class TimedResponseListener implements Response.Listener, Schedulable, Ru @Override public void onBegin(Response response) { - delegate.onBegin(response); + if (delegate instanceof Response.BeginListener) + ((Response.BeginListener)delegate).onBegin(response); } @Override public void onHeaders(Response response) { - delegate.onHeaders(response); + if (delegate instanceof Response.HeadersListener) + ((Response.HeadersListener)delegate).onHeaders(response); } @Override public void onContent(Response response, ByteBuffer content) { - delegate.onContent(response, content); + if (delegate instanceof Response.ContentListener) + ((Response.ContentListener)delegate).onContent(response, content); } @Override public void onSuccess(Response response) { - delegate.onSuccess(response); + if (delegate instanceof Response.SuccessListener) + ((Response.SuccessListener)delegate).onSuccess(response); } @Override public void onFailure(Response response, Throwable failure) { - delegate.onFailure(response, failure); + if (delegate instanceof Response.FailureListener) + ((Response.FailureListener)delegate).onFailure(response, failure); } @Override @@ -111,7 +117,7 @@ public class TimedResponseListener implements Response.Listener, Schedulable, Ru @Override public void run() { - request.abort("Total timeout elapsed"); + request.abort(new TimeoutException("Total timeout elapsed")); } public boolean cancel() diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientProxyTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientProxyTest.java new file mode 100644 index 00000000000..3de71ab6ba0 --- /dev/null +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientProxyTest.java @@ -0,0 +1,89 @@ +// +// ======================================================================== +// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.client; + +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.util.ssl.SslContextFactory; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +public class HttpClientProxyTest extends AbstractHttpClientServerTest +{ + + private Server proxy; + private ServerConnector proxyConnector; + + public HttpClientProxyTest(SslContextFactory sslContextFactory) + { + super(sslContextFactory); + } + + @Before + public void prepareProxy() throws Exception + { + proxy = new Server(); + proxyConnector = new ServerConnector(proxy); + proxy.addConnector(proxyConnector); + proxy.start(); + } + + @After + public void disposeProxy() throws Exception + { + proxy.stop(); + } + + private static final String PROXIED_HEADER = "X-Proxied"; + + @Test + public void testProxyWithExcludedHosts() throws Exception + { +// ProxyConfiguration proxyConfiguration = new ProxyConfiguration("localhost", proxy.getLocalPort()); +// proxyConfiguration.addExcludedHost("wikipedia.org"); +// proxyConfiguration.addExcludedHost(".wikipedia.org"); +// client.setProxyConfiguration(proxyConfiguration); + } + + @Test + public void testProxyWithAuthentication() throws Exception + { +// client.getAuthenticationStore().addAuthentication(new BasicAuthentication("/", "proxy-realm", "basic", "basic")); + } + + @Test + public void testHTTPTunnel() throws Exception + { + // Usually, plain text HTTP requests do not require a CONNECT to be sent to the proxy + // However, I may want to do that anyway, for example to avoid that the proxy modifies + // in any way the request + +// Destination destination = client.getDestination("http", proxyHost, proxyPort); +// Connection connection = destination.newConnection().get(5, TimeUnit.SECONDS); + + // Create and send an explicit CONNECT +// Request request = client.newRequest(serverHost, serverPort).method(HttpMethod.CONNECT); +// connection.send(request, null); + + // Now anything I send over this connection is tunneled by the proxy + // e.g. SMTP +// ((HttpConnection)connection).getEndPoint().write(null, null, ByteBuffer.wrap("HELO localhost\r\n")); + } +} diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientTimeoutTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientTimeoutTest.java index 700c14e6296..9bc49acae36 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientTimeoutTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientTimeoutTest.java @@ -151,7 +151,7 @@ public class HttpClientTimeoutTest extends AbstractHttpClientServerTest TimeUnit.MILLISECONDS.sleep(2 * timeout); - Assert.assertFalse(request.aborted()); + Assert.assertNull(request.aborted()); } @Slow @@ -208,7 +208,7 @@ public class HttpClientTimeoutTest extends AbstractHttpClientServerTest TimeUnit.MILLISECONDS.sleep(2 * timeout); - Assert.assertFalse(request.aborted()); + Assert.assertNull(request.aborted()); } } diff --git a/jetty-proxy/pom.xml b/jetty-proxy/pom.xml index a2ebe532829..3df08ef7a3a 100644 --- a/jetty-proxy/pom.xml +++ b/jetty-proxy/pom.xml @@ -1,88 +1,93 @@ - - - jetty-project - org.eclipse.jetty - 9.0.0-SNAPSHOT - - 4.0.0 - jetty-proxy - Jetty :: Proxy - Jetty Proxy - - ${project.groupId}.servlets - - - - - org.apache.felix - maven-bundle-plugin - true - - - - manifest - - - - javax.servlet.*;version="2.6.0",* - - - - - - - - org.apache.maven.plugins - maven-jar-plugin - - - ${project.build.outputDirectory}/META-INF/MANIFEST.MF - - - - - org.codehaus.mojo - findbugs-maven-plugin - - org.eclipse.jetty.servlets.* - - - - - - - org.eclipse.jetty.toolchain - jetty-test-helper - test - - - org.eclipse.jetty - jetty-continuation - ${project.version} - - - org.eclipse.jetty - jetty-webapp - ${project.version} - provided - - - org.eclipse.jetty - jetty-client - ${project.version} - - - org.eclipse.jetty - jetty-util - ${project.version} - - - org.eclipse.jetty.orbit - javax.servlet - provided - - + + + org.eclipse.jetty + jetty-project + 9.0.0-SNAPSHOT + + + 4.0.0 + jetty-proxy + Jetty :: Proxy + Jetty Proxy + + + ${project.groupId}.proxy + + + + + + org.apache.felix + maven-bundle-plugin + true + + + + manifest + + + + javax.servlet.*;version="2.6.0",* + + + + + + + + org.apache.maven.plugins + maven-jar-plugin + + + ${project.build.outputDirectory}/META-INF/MANIFEST.MF + + + + + org.codehaus.mojo + findbugs-maven-plugin + + org.eclipse.jetty.proxy.* + + + + + + + + org.eclipse.jetty + jetty-client + ${project.version} + + + org.eclipse.jetty + jetty-util + ${project.version} + + + org.eclipse.jetty + jetty-server + ${project.version} + + + org.eclipse.jetty.orbit + javax.servlet + provided + + + org.eclipse.jetty + jetty-webapp + ${project.version} + test + + + org.eclipse.jetty.toolchain + jetty-test-helper + test + + diff --git a/jetty-proxy/src/main/java/org/eclipse/jetty/proxy/BalancerServlet.java b/jetty-proxy/src/main/java/org/eclipse/jetty/proxy/BalancerServlet.java index 3ae4d4dd489..8daeb2c1cc7 100644 --- a/jetty-proxy/src/main/java/org/eclipse/jetty/proxy/BalancerServlet.java +++ b/jetty-proxy/src/main/java/org/eclipse/jetty/proxy/BalancerServlet.java @@ -18,48 +18,255 @@ package org.eclipse.jetty.proxy; -import java.net.MalformedURLException; import java.net.URI; -import java.net.URISyntaxException; -import java.util.Collection; +import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; -import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Set; -import java.util.concurrent.atomic.AtomicInteger; - -import javax.servlet.ServletConfig; +import java.util.concurrent.atomic.AtomicLong; import javax.servlet.ServletException; import javax.servlet.UnavailableException; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; -import org.eclipse.jetty.http.HttpURI; -import org.eclipse.jetty.server.Request; - -/** - * 6 - */ public class BalancerServlet extends ProxyServlet { + private static final String BALANCER_MEMBER_PREFIX = "balancerMember."; + private static final List FORBIDDEN_CONFIG_PARAMETERS; - private static final class BalancerMember + static { + List params = new LinkedList<>(); + params.add("hostHeader"); + params.add("whiteList"); + params.add("blackList"); + FORBIDDEN_CONFIG_PARAMETERS = Collections.unmodifiableList(params); + } - private String _name; + private static final List REVERSE_PROXY_HEADERS; - private String _proxyTo; + static + { + List params = new LinkedList<>(); + params.add("Location"); + params.add("Content-Location"); + params.add("URI"); + REVERSE_PROXY_HEADERS = Collections.unmodifiableList(params); + } - private HttpURI _backendURI; + private static final String JSESSIONID = "jsessionid"; + private static final String JSESSIONID_URL_PREFIX = JSESSIONID + "="; + + private final List _balancerMembers = new ArrayList<>(); + private final AtomicLong counter = new AtomicLong(); + private boolean _stickySessions; + private boolean _proxyPassReverse; + + @Override + public void init() throws ServletException + { + validateConfig(); + super.init(); + initStickySessions(); + initBalancers(); + initProxyPassReverse(); + } + + private void validateConfig() throws ServletException + { + for (String initParameterName : Collections.list(getServletConfig().getInitParameterNames())) + { + if (FORBIDDEN_CONFIG_PARAMETERS.contains(initParameterName)) + { + throw new UnavailableException(initParameterName + " not supported in " + getClass().getName()); + } + } + } + + private void initStickySessions() throws ServletException + { + _stickySessions = Boolean.parseBoolean(getServletConfig().getInitParameter("stickySessions")); + } + + private void initBalancers() throws ServletException + { + Set members = new HashSet<>(); + for (String balancerName : getBalancerNames()) + { + String memberProxyToParam = BALANCER_MEMBER_PREFIX + balancerName + ".proxyTo"; + String proxyTo = getServletConfig().getInitParameter(memberProxyToParam); + if (proxyTo == null || proxyTo.trim().length() == 0) + throw new UnavailableException(memberProxyToParam + " parameter is empty."); + members.add(new BalancerMember(balancerName, proxyTo)); + } + _balancerMembers.addAll(members); + } + + private void initProxyPassReverse() + { + _proxyPassReverse = Boolean.parseBoolean(getServletConfig().getInitParameter("proxyPassReverse")); + } + + private Set getBalancerNames() throws ServletException + { + Set names = new HashSet<>(); + for (String initParameterName : Collections.list(getServletConfig().getInitParameterNames())) + { + if (!initParameterName.startsWith(BALANCER_MEMBER_PREFIX)) + continue; + + int endOfNameIndex = initParameterName.lastIndexOf("."); + if (endOfNameIndex <= BALANCER_MEMBER_PREFIX.length()) + throw new UnavailableException(initParameterName + " parameter does not provide a balancer member name"); + + names.add(initParameterName.substring(BALANCER_MEMBER_PREFIX.length(), endOfNameIndex)); + } + return names; + } + + @Override + protected URI rewriteURI(HttpServletRequest request) + { + BalancerMember balancerMember = selectBalancerMember(request); + _log.debug("Selected {}", balancerMember); + String path = request.getRequestURI(); + String query = request.getQueryString(); + if (query != null) + path += "?" + query; + return URI.create(balancerMember.getProxyTo() + "/" + path).normalize(); + } + + private BalancerMember selectBalancerMember(HttpServletRequest request) + { + if (_stickySessions) + { + String name = getBalancerMemberNameFromSessionId(request); + if (name != null) + { + BalancerMember balancerMember = findBalancerMemberByName(name); + if (balancerMember != null) + return balancerMember; + } + } + int index = (int)(counter.getAndIncrement() % _balancerMembers.size()); + return _balancerMembers.get(index); + } + + private BalancerMember findBalancerMemberByName(String name) + { + for (BalancerMember balancerMember : _balancerMembers) + { + if (balancerMember.getName().equals(name)) + return balancerMember; + } + return null; + } + + private String getBalancerMemberNameFromSessionId(HttpServletRequest request) + { + String name = getBalancerMemberNameFromSessionCookie(request); + if (name == null) + name = getBalancerMemberNameFromURL(request); + return name; + } + + private String getBalancerMemberNameFromSessionCookie(HttpServletRequest request) + { + for (Cookie cookie : request.getCookies()) + { + if (JSESSIONID.equalsIgnoreCase(cookie.getName())) + return extractBalancerMemberNameFromSessionId(cookie.getValue()); + } + return null; + } + + private String getBalancerMemberNameFromURL(HttpServletRequest request) + { + String requestURI = request.getRequestURI(); + int idx = requestURI.lastIndexOf(";"); + if (idx > 0) + { + String requestURISuffix = requestURI.substring(idx + 1); + if (requestURISuffix.startsWith(JSESSIONID_URL_PREFIX)) + return extractBalancerMemberNameFromSessionId(requestURISuffix.substring(JSESSIONID_URL_PREFIX.length())); + } + return null; + } + + private String extractBalancerMemberNameFromSessionId(String sessionId) + { + int idx = sessionId.lastIndexOf("."); + if (idx > 0) + { + String sessionIdSuffix = sessionId.substring(idx + 1); + return sessionIdSuffix.length() > 0 ? sessionIdSuffix : null; + } + return null; + } + + @Override + protected String filterResponseHeader(HttpServletRequest request, String headerName, String headerValue) + { + if (_proxyPassReverse && REVERSE_PROXY_HEADERS.contains(headerName)) + { + URI locationURI = URI.create(headerValue).normalize(); + if (locationURI.isAbsolute() && isBackendLocation(locationURI)) + { + String newURI = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort(); + String component = locationURI.getRawPath(); + if (component != null) + newURI += component; + component = locationURI.getRawQuery(); + if (component != null) + newURI += "?" + component; + component = locationURI.getRawFragment(); + if (component != null) + newURI += "#" + component; + return URI.create(newURI).normalize().toString(); + } + } + return headerValue; + } + + private boolean isBackendLocation(URI locationURI) + { + for (BalancerMember balancerMember : _balancerMembers) + { + URI backendURI = balancerMember.getBackendURI(); + if (backendURI.getHost().equals(locationURI.getHost()) && + backendURI.getScheme().equals(locationURI.getScheme()) + && backendURI.getPort() == locationURI.getPort()) + { + return true; + } + } + return false; + } + + @Override + public boolean validateDestination(String host, int port) + { + return true; + } + + private static class BalancerMember + { + private final String _name; + private final String _proxyTo; + private final URI _backendURI; public BalancerMember(String name, String proxyTo) { - super(); _name = name; _proxyTo = proxyTo; - _backendURI = new HttpURI(_proxyTo); + _backendURI = URI.create(_proxyTo).normalize(); + } + + public String getName() + { + return _name; } public String getProxyTo() @@ -67,7 +274,7 @@ public class BalancerServlet extends ProxyServlet return _proxyTo; } - public HttpURI getBackendURI() + public URI getBackendURI() { return _backendURI; } @@ -75,16 +282,13 @@ public class BalancerServlet extends ProxyServlet @Override public String toString() { - return "BalancerMember [_name=" + _name + ", _proxyTo=" + _proxyTo + "]"; + return String.format("%s[name=%s,proxyTo=%s]", getClass().getSimpleName(), _name, _proxyTo); } @Override public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + ((_name == null)?0:_name.hashCode()); - return result; + return _name.hashCode(); } @Override @@ -96,327 +300,8 @@ public class BalancerServlet extends ProxyServlet return false; if (getClass() != obj.getClass()) return false; - BalancerMember other = (BalancerMember)obj; - if (_name == null) - { - if (other._name != null) - return false; - } - else if (!_name.equals(other._name)) - return false; - return true; - } - - } - - private static final class RoundRobinIterator implements Iterator - { - - private BalancerMember[] _balancerMembers; - - private AtomicInteger _index; - - public RoundRobinIterator(Collection balancerMembers) - { - _balancerMembers = (BalancerMember[])balancerMembers.toArray(new BalancerMember[balancerMembers.size()]); - _index = new AtomicInteger(-1); - } - - public boolean hasNext() - { - return true; - } - - public BalancerMember next() - { - BalancerMember balancerMember = null; - while (balancerMember == null) - { - int currentIndex = _index.get(); - int nextIndex = (currentIndex + 1) % _balancerMembers.length; - if (_index.compareAndSet(currentIndex,nextIndex)) - { - balancerMember = _balancerMembers[nextIndex]; - } - } - return balancerMember; - } - - public void remove() - { - throw new UnsupportedOperationException(); - } - - } - - private static final String BALANCER_MEMBER_PREFIX = "BalancerMember."; - - private static final List FORBIDDEN_CONFIG_PARAMETERS; - static - { - List params = new LinkedList(); - params.add("HostHeader"); - params.add("whiteList"); - params.add("blackList"); - FORBIDDEN_CONFIG_PARAMETERS = Collections.unmodifiableList(params); - } - - private static final List REVERSE_PROXY_HEADERS; - static - { - List params = new LinkedList(); - params.add("Location"); - params.add("Content-Location"); - params.add("URI"); - REVERSE_PROXY_HEADERS = Collections.unmodifiableList(params); - } - - private static final String JSESSIONID = "jsessionid"; - - private static final String JSESSIONID_URL_PREFIX = JSESSIONID + "="; - - private boolean _stickySessions; - - private Set _balancerMembers = new HashSet(); - - private boolean _proxyPassReverse; - - private RoundRobinIterator _roundRobinIterator; - - @Override - public void init(ServletConfig config) throws ServletException - { - validateConfig(config); - super.init(config); - initStickySessions(config); - initBalancers(config); - initProxyPassReverse(config); - postInit(); - } - - private void validateConfig(ServletConfig config) throws ServletException - { - @SuppressWarnings("unchecked") - List initParameterNames = Collections.list(config.getInitParameterNames()); - for (String initParameterName : initParameterNames) - { - if (FORBIDDEN_CONFIG_PARAMETERS.contains(initParameterName)) - { - throw new UnavailableException(initParameterName + " not supported in " + getClass().getName()); - } + BalancerMember that = (BalancerMember)obj; + return _name.equals(that._name); } } - - private void initStickySessions(ServletConfig config) throws ServletException - { - _stickySessions = "true".equalsIgnoreCase(config.getInitParameter("StickySessions")); - } - - private void initBalancers(ServletConfig config) throws ServletException - { - Set balancerNames = getBalancerNames(config); - for (String balancerName : balancerNames) - { - String memberProxyToParam = BALANCER_MEMBER_PREFIX + balancerName + ".ProxyTo"; - String proxyTo = config.getInitParameter(memberProxyToParam); - if (proxyTo == null || proxyTo.trim().length() == 0) - { - throw new UnavailableException(memberProxyToParam + " parameter is empty."); - } - _balancerMembers.add(new BalancerMember(balancerName,proxyTo)); - } - } - - private void initProxyPassReverse(ServletConfig config) - { - _proxyPassReverse = "true".equalsIgnoreCase(config.getInitParameter("ProxyPassReverse")); - } - - private void postInit() - { - _roundRobinIterator = new RoundRobinIterator(_balancerMembers); - } - - private Set getBalancerNames(ServletConfig config) throws ServletException - { - Set names = new HashSet(); - @SuppressWarnings("unchecked") - List initParameterNames = Collections.list(config.getInitParameterNames()); - for (String initParameterName : initParameterNames) - { - if (!initParameterName.startsWith(BALANCER_MEMBER_PREFIX)) - { - continue; - } - int endOfNameIndex = initParameterName.lastIndexOf("."); - if (endOfNameIndex <= BALANCER_MEMBER_PREFIX.length()) - { - throw new UnavailableException(initParameterName + " parameter does not provide a balancer member name"); - } - names.add(initParameterName.substring(BALANCER_MEMBER_PREFIX.length(),endOfNameIndex)); - } - return names; - } - - @Override - protected HttpURI proxyHttpURI(HttpServletRequest request, String uri) throws MalformedURLException - { - BalancerMember balancerMember = selectBalancerMember(request); - try - { - URI dstUri = new URI(balancerMember.getProxyTo() + "/" + uri).normalize(); - return new HttpURI(dstUri.toString()); - } - catch (URISyntaxException e) - { - throw new MalformedURLException(e.getMessage()); - } - } - - private BalancerMember selectBalancerMember(HttpServletRequest request) - { - BalancerMember balancerMember = null; - if (_stickySessions) - { - String name = getBalancerMemberNameFromSessionId(request); - if (name != null) - { - balancerMember = findBalancerMemberByName(name); - if (balancerMember != null) - { - return balancerMember; - } - } - } - return _roundRobinIterator.next(); - } - - private BalancerMember findBalancerMemberByName(String name) - { - BalancerMember example = new BalancerMember(name,""); - for (BalancerMember balancerMember : _balancerMembers) - { - if (balancerMember.equals(example)) - { - return balancerMember; - } - } - return null; - } - - private String getBalancerMemberNameFromSessionId(HttpServletRequest request) - { - String name = getBalancerMemberNameFromSessionCookie(request); - if (name == null) - { - name = getBalancerMemberNameFromURL(request); - } - return name; - } - - private String getBalancerMemberNameFromSessionCookie(HttpServletRequest request) - { - Cookie[] cookies = request.getCookies(); - String name = null; - for (Cookie cookie : cookies) - { - if (JSESSIONID.equalsIgnoreCase(cookie.getName())) - { - name = extractBalancerMemberNameFromSessionId(cookie.getValue()); - break; - } - } - return name; - } - - private String getBalancerMemberNameFromURL(HttpServletRequest request) - { - String name = null; - String requestURI = request.getRequestURI(); - int idx = requestURI.lastIndexOf(";"); - if (idx != -1) - { - String requestURISuffix = requestURI.substring(idx); - if (requestURISuffix.startsWith(JSESSIONID_URL_PREFIX)) - { - name = extractBalancerMemberNameFromSessionId(requestURISuffix.substring(JSESSIONID_URL_PREFIX.length())); - } - } - return name; - } - - private String extractBalancerMemberNameFromSessionId(String sessionId) - { - String name = null; - int idx = sessionId.lastIndexOf("."); - if (idx != -1) - { - String sessionIdSuffix = sessionId.substring(idx + 1); - name = (sessionIdSuffix.length() > 0)?sessionIdSuffix:null; - } - return name; - } - - @Override - protected String filterResponseHeaderValue(String headerName, String headerValue, HttpServletRequest request) - { - if (_proxyPassReverse && REVERSE_PROXY_HEADERS.contains(headerName)) - { - HttpURI locationURI = new HttpURI(headerValue); - if (isAbsoluteLocation(locationURI) && isBackendLocation(locationURI)) - { - Request jettyRequest = (Request)request; - URI reverseUri; - try - { - reverseUri = new URI(jettyRequest.getRootURL().append(locationURI.getCompletePath()).toString()).normalize(); - return reverseUri.toURL().toString(); - } - catch (Exception e) - { - _log.warn("Not filtering header response",e); - return headerValue; - } - } - } - return headerValue; - } - - private boolean isBackendLocation(HttpURI locationURI) - { - for (BalancerMember balancerMember : _balancerMembers) - { - HttpURI backendURI = balancerMember.getBackendURI(); - if (backendURI.getHost().equals(locationURI.getHost()) && backendURI.getScheme().equals(locationURI.getScheme()) - && backendURI.getPort() == locationURI.getPort()) - { - return true; - } - } - return false; - } - - private boolean isAbsoluteLocation(HttpURI locationURI) - { - return locationURI.getHost() != null; - } - - @Override - public String getHostHeader() - { - throw new UnsupportedOperationException("HostHeader not supported in " + getClass().getName()); - } - - @Override - public void setHostHeader(String hostHeader) - { - throw new UnsupportedOperationException("HostHeader not supported in " + getClass().getName()); - } - - @Override - public boolean validateDestination(String host, String path) - { - return true; - } - -} \ No newline at end of file +} diff --git a/jetty-proxy/src/main/java/org/eclipse/jetty/proxy/ConnectHandler.java b/jetty-proxy/src/main/java/org/eclipse/jetty/proxy/ConnectHandler.java index 5d205a8b3df..063cdbd5283 100644 --- a/jetty-proxy/src/main/java/org/eclipse/jetty/proxy/ConnectHandler.java +++ b/jetty-proxy/src/main/java/org/eclipse/jetty/proxy/ConnectHandler.java @@ -16,70 +16,58 @@ // ======================================================================== // -package org.eclipse.jetty.server.handler; +package org.eclipse.jetty.proxy; import java.io.IOException; import java.net.InetSocketAddress; import java.net.SocketException; import java.net.SocketTimeoutException; import java.nio.ByteBuffer; +import java.nio.channels.SelectionKey; import java.nio.channels.SocketChannel; +import java.util.HashSet; +import java.util.Set; import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.Executor; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.eclipse.jetty.http.HttpMethod; -import org.eclipse.jetty.io.AsyncConnection; +import org.eclipse.jetty.io.Connection; import org.eclipse.jetty.io.EndPoint; +import org.eclipse.jetty.io.SelectorManager; import org.eclipse.jetty.server.Handler; import org.eclipse.jetty.server.Request; -import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.handler.HandlerWrapper; import org.eclipse.jetty.util.HostMap; -import org.eclipse.jetty.util.component.LifeCycle; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; -import org.eclipse.jetty.util.thread.ThreadPool; +import org.eclipse.jetty.util.thread.Scheduler; /** - *

Implementation of a tunneling proxy that supports HTTP CONNECT.

- *

To work as CONNECT proxy, objects of this class must be instantiated using the no-arguments - * constructor, since the remote server information will be present in the CONNECT URI.

+ *

Implementation of a {@link Handler} that supports HTTP CONNECT.

*/ public class ConnectHandler extends HandlerWrapper { private static final Logger LOG = Log.getLogger(ConnectHandler.class); - // TODO private final SelectorManager _selectorManager = new Manager(); - private volatile int _connectTimeout = 5000; - private volatile int _writeTimeout = 30000; - private volatile ThreadPool _threadPool; - private volatile boolean _privateThreadPool; - private HostMap _white = new HostMap(); - private HostMap _black = new HostMap(); + + private final Set _whiteList = new HashSet<>(); + private final Set _blackList = new HashSet<>(); + private final SelectorManager _selector = new Manager(null, null, 1); + private volatile int _connectTimeout = 15000; + private volatile int _idleTimeout = 30000; public ConnectHandler() { this(null); } - public ConnectHandler(String[] white, String[] black) - { - this(null, white, black); - } - public ConnectHandler(Handler handler) { setHandler(handler); } - public ConnectHandler(Handler handler, String[] white, String[] black) - { - setHandler(handler); - set(white, _white); - set(black, _black); - } - /** * @return the timeout, in milliseconds, to connect to the remote server */ @@ -97,79 +85,34 @@ public class ConnectHandler extends HandlerWrapper } /** - * @return the timeout, in milliseconds, to write data to a peer + * @return the idle timeout, in milliseconds */ - public int getWriteTimeout() + public int getIdleTimeout() { - return _writeTimeout; + return _idleTimeout; } /** - * @param writeTimeout the timeout, in milliseconds, to write data to a peer + * @param idleTimeout the idle timeout, in milliseconds */ - public void setWriteTimeout(int writeTimeout) + public void setIdleTimeout(int idleTimeout) { - _writeTimeout = writeTimeout; - } - - @Override - public void setServer(Server server) - { - super.setServer(server); - - // TODO server.getContainer().update(this, null, _selectorManager, "selectManager"); - - if (_privateThreadPool) - server.getContainer().update(this, null, _privateThreadPool, "threadpool", true); - else - _threadPool = server.getThreadPool(); - } - - /** - * @return the thread pool - */ - public ThreadPool getThreadPool() - { - return _threadPool; - } - - /** - * @param threadPool the thread pool - */ - public void setThreadPool(ThreadPool threadPool) - { - if (getServer() != null) - getServer().getContainer().update(this, _privateThreadPool ? _threadPool : null, threadPool, "threadpool", true); - _privateThreadPool = threadPool != null; - _threadPool = threadPool; + _idleTimeout = idleTimeout; } @Override protected void doStart() throws Exception { + _selector.setConnectTimeout(getConnectTimeout()); + _selector.start(); super.doStart(); - - if (_threadPool == null) - { - _threadPool = getServer().getThreadPool(); - _privateThreadPool = false; - } - if (_threadPool instanceof LifeCycle && !((LifeCycle)_threadPool).isRunning()) - ((LifeCycle)_threadPool).start(); - - // TODO _selectorManager.start(); } @Override protected void doStop() throws Exception { - // TODO _selectorManager.stop(); - - ThreadPool threadPool = _threadPool; - if (_privateThreadPool && _threadPool != null && threadPool instanceof LifeCycle) - ((LifeCycle)threadPool).stop(); - super.doStop(); + _selector.stop(); } @Override @@ -196,17 +139,17 @@ public class ConnectHandler extends HandlerWrapper /** *

Handles a CONNECT request.

- *

CONNECT requests may have authentication headers such as Proxy-Authorization + *

CONNECT requests may have authentication headers such as {@code Proxy-Authorization} * that authenticate the client with the proxy.

* - * @param baseRequest Jetty-specific http request + * @param jettyRequest Jetty-specific http request * @param request the http request * @param response the http response * @param serverAddress the remote server address in the form {@code host:port} * @throws ServletException if an application error occurs * @throws IOException if an I/O error occurs */ - protected void handleConnect(Request baseRequest, HttpServletRequest request, HttpServletResponse response, String serverAddress) throws ServletException, IOException + protected void handleConnect(Request jettyRequest, HttpServletRequest request, HttpServletResponse response, String serverAddress) throws ServletException, IOException { boolean proceed = handleAuthentication(request, response, serverAddress); if (!proceed) @@ -225,11 +168,18 @@ public class ConnectHandler extends HandlerWrapper { LOG.info("ProxyHandler: Forbidden destination " + host); response.setStatus(HttpServletResponse.SC_FORBIDDEN); - baseRequest.setHandled(true); + jettyRequest.setHandled(true); return; } - SocketChannel channel; + SocketChannel channel = SocketChannel.open(); + channel.socket().setTcpNoDelay(true); + channel.configureBlocking(false); + channel.connect(new InetSocketAddress(host, port)); + +// _selector.connect(channel, ); + +// SocketChannel channel; try { @@ -239,21 +189,21 @@ public class ConnectHandler extends HandlerWrapper { LOG.info("ConnectHandler: SocketException " + se.getMessage()); response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); - baseRequest.setHandled(true); + jettyRequest.setHandled(true); return; } catch (SocketTimeoutException ste) { LOG.info("ConnectHandler: SocketTimeoutException" + ste.getMessage()); response.setStatus(HttpServletResponse.SC_GATEWAY_TIMEOUT); - baseRequest.setHandled(true); + jettyRequest.setHandled(true); return; } catch (IOException ioe) { LOG.info("ConnectHandler: IOException" + ioe.getMessage()); response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); - baseRequest.setHandled(true); + jettyRequest.setHandled(true); return; } @@ -294,7 +244,7 @@ public class ConnectHandler extends HandlerWrapper response.setStatus(HttpServletResponse.SC_OK); // Prevent close - baseRequest.getConnection().getGenerator().setPersistent(true); + jettyRequest.getConnection().getGenerator().setPersistent(true); // Close to force last flush it so that the client receives it response.getOutputStream().close(); @@ -359,19 +309,16 @@ public class ConnectHandler extends HandlerWrapper * @param host the host to connect to * @param port the port to connect to * @return a {@link SocketChannel} connected to the remote server - * @throws IOException if the connection cannot be established */ protected SocketChannel connect(HttpServletRequest request, String host, int port) throws IOException { - SocketChannel channel = SocketChannel.open(); - - if (channel == null) - { - throw new IOException("unable to connect to " + host + ":" + port); - } - + SocketChannel channel = null; try { + channel = SocketChannel.open(); + if (channel == null) + throw new IOException("Unable to connect to " + host + ":" + port); + // Connect to remote server LOG.debug("Establishing connection to {}:{}", host, port); channel.socket().setTcpNoDelay(true); @@ -381,15 +328,16 @@ public class ConnectHandler extends HandlerWrapper } catch (IOException x) { - LOG.debug("Failed to establish connection to " + host + ":" + port, x); try { - channel.close(); + if (channel != null) + channel.close(); } catch (IOException xx) { LOG.ignore(xx); } + LOG.debug("Failed to establish connection to " + host + ":" + port, x); throw x; } } @@ -398,7 +346,7 @@ public class ConnectHandler extends HandlerWrapper { } - private void upgradeConnection(HttpServletRequest request, HttpServletResponse response, AsyncConnection connection) throws IOException + private void upgradeConnection(HttpServletRequest request, HttpServletResponse response, Connection connection) throws IOException { // Set the new connection as request attribute and change the status to 101 // so that Jetty understands that it has to upgrade the connection @@ -410,7 +358,7 @@ public class ConnectHandler extends HandlerWrapper /* TODO private void register(SocketChannel channel, ProxyToServerConnection proxyToServer) throws IOException { - _selectorManager.register(channel, proxyToServer); + _selector.register(channel, proxyToServer); proxyToServer.waitReady(_connectTimeout); } */ @@ -456,7 +404,7 @@ public class ConnectHandler extends HandlerWrapper { if (!endPoint.isBlocking()) { - boolean ready = endPoint.blockWritable(getWriteTimeout()); + boolean ready = endPoint.blockWritable(getIdleTimeout()); if (!ready) throw new IOException("Write timeout"); } @@ -472,53 +420,69 @@ public class ConnectHandler extends HandlerWrapper return -1; } - /* TODO + // TODO private class Manager extends SelectorManager { - @Override - protected SelectChannelEndPoint newEndPoint(SocketChannel channel, SelectSet selectSet, SelectionKey key) throws IOException + private Manager(Executor executor, Scheduler scheduler, int selectors) { - SelectChannelEndPoint endp = new SelectChannelEndPoint(channel, selectSet, key, channel.socket().getSoTimeout()); - endp.setConnection(selectSet.getSelectorManager().newConnection(channel,endp, key.attachment())); - endp.setIdleTimeout(_writeTimeout); - return endp; + super(executor, scheduler, selectors); } @Override - public AsyncConnection newConnection(SocketChannel channel, AsyncEndPoint endpoint, Object attachment) + protected EndPoint newEndPoint(SocketChannel channel, ManagedSelector selector, SelectionKey selectionKey) throws IOException { - ProxyToServerConnection proxyToServer = (ProxyToServerConnection)attachment; - proxyToServer.setTimeStamp(System.currentTimeMillis()); - proxyToServer.setEndPoint(endpoint); - return proxyToServer; + return null; } @Override - protected void endPointOpened(SelectChannelEndPoint endpoint) - { - ProxyToServerConnection proxyToServer = (ProxyToServerConnection)endpoint.getSelectionKey().attachment(); - proxyToServer.complete(); - } - - @Override - public boolean dispatch(Runnable task) - { - return _threadPool.dispatch(task); - } - - @Override - protected void endPointClosed(AsyncEndPoint endpoint) - { - } - - @Override - protected void connectionUpgraded(ConnectedEndPoint endpoint, AsyncConnection oldConnection) + public Connection newConnection(SocketChannel channel, EndPoint endpoint, Object attachment) throws IOException { + return null; } + // @Override +// protected SelectChannelEndPoint newEndPoint(SocketChannel channel, SelectSet selectSet, SelectionKey key) throws IOException +// { +// SelectChannelEndPoint endp = new SelectChannelEndPoint(channel, selectSet, key, channel.socket().getSoTimeout()); +// endp.setConnection(selectSet.getSelectorManager().newConnection(channel,endp, key.attachment())); +// endp.setIdleTimeout(_idleTimeout); +// return endp; +// } +// +// @Override +// public AsyncConnection newConnection(SocketChannel channel, AsyncEndPoint endpoint, Object attachment) +// { +// ProxyToServerConnection proxyToServer = (ProxyToServerConnection)attachment; +// proxyToServer.setTimeStamp(System.currentTimeMillis()); +// proxyToServer.setEndPoint(endpoint); +// return proxyToServer; +// } +// +// @Override +// protected void endPointOpened(SelectChannelEndPoint endpoint) +// { +// ProxyToServerConnection proxyToServer = (ProxyToServerConnection)endpoint.getSelectionKey().attachment(); +// proxyToServer.complete(); +// } +// +// @Override +// public boolean dispatch(Runnable task) +// { +// return _threadPool.dispatch(task); +// } +// +// @Override +// protected void endPointClosed(AsyncEndPoint endpoint) +// { +// } +// +// @Override +// protected void connectionUpgraded(ConnectedEndPoint endpoint, AsyncConnection oldConnection) +// { +// } } - +/* public class ProxyToServerConnection implements AsyncConnection { private final CountDownLatch _ready = new CountDownLatch(1); @@ -902,23 +866,23 @@ public class ConnectHandler extends HandlerWrapper } /** - * Add a whitelist entry to an existing handler configuration + * Adds the given {@code host} to the whitelist * - * @param entry new whitelist entry + * @param host the whitelisted host */ - public void addWhite(String entry) + public void addWhitelistHost(String host) { - add(entry, _white); + _whiteList.add(host); } /** - * Add a blacklist entry to an existing handler configuration + * Adds the given {@code host} to the blacklist * - * @param entry new blacklist entry + * @param host the blacklisted host */ - public void addBlack(String entry) + public void addBlacklistHost(String host) { - add(entry, _black); + _blackList.add(host); } /** @@ -928,7 +892,7 @@ public class ConnectHandler extends HandlerWrapper */ public void setWhite(String[] entries) { - set(entries, _white); +// set(entries, _whiteList); } /** @@ -938,7 +902,7 @@ public class ConnectHandler extends HandlerWrapper */ public void setBlack(String[] entries) { - set(entries, _black); +// set(entries, _blackList); } /** @@ -988,19 +952,19 @@ public class ConnectHandler extends HandlerWrapper */ public boolean validateDestination(String host) { - if (_white.size() > 0) + if (_whiteList.size() > 0) { - Object whiteObj = _white.getLazyMatches(host); - if (whiteObj == null) +// Object whiteObj = _whiteList.getLazyMatches(host); +// if (whiteObj == null) { return false; } } - if (_black.size() > 0) + if (_blackList.size() > 0) { - Object blackObj = _black.getLazyMatches(host); - if (blackObj != null) +// Object blackObj = _blackList.getLazyMatches(host); +// if (blackObj != null) { return false; } @@ -1015,9 +979,9 @@ public class ConnectHandler extends HandlerWrapper dumpThis(out); /* TODO if (_privateThreadPool) - dump(out, indent, Arrays.asList(_threadPool, _selectorManager), TypeUtil.asList(getHandlers()), getBeans()); + dump(out, indent, Arrays.asList(_threadPool, _selector), TypeUtil.asList(getHandlers()), getBeans()); else - dump(out, indent, Arrays.asList(_selectorManager), TypeUtil.asList(getHandlers()), getBeans()); + dump(out, indent, Arrays.asList(_selector), TypeUtil.asList(getHandlers()), getBeans()); */ } } diff --git a/jetty-proxy/src/main/java/org/eclipse/jetty/proxy/ProxyServlet.java b/jetty-proxy/src/main/java/org/eclipse/jetty/proxy/ProxyServlet.java index 18c8e72ea32..99d1ca247f9 100644 --- a/jetty-proxy/src/main/java/org/eclipse/jetty/proxy/ProxyServlet.java +++ b/jetty-proxy/src/main/java/org/eclipse/jetty/proxy/ProxyServlet.java @@ -19,133 +19,116 @@ package org.eclipse.jetty.proxy; import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.InetSocketAddress; -import java.net.MalformedURLException; -import java.net.Socket; +import java.net.InetAddress; import java.net.URI; -import java.net.URISyntaxException; -import java.util.Collections; +import java.net.UnknownHostException; +import java.nio.ByteBuffer; import java.util.Enumeration; import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.StringTokenizer; - -import javax.servlet.Servlet; +import java.util.Iterator; +import java.util.Locale; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import javax.servlet.AsyncContext; import javax.servlet.ServletConfig; -import javax.servlet.ServletContext; import javax.servlet.ServletException; -import javax.servlet.ServletRequest; -import javax.servlet.ServletResponse; import javax.servlet.UnavailableException; +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.HttpExchange; -import org.eclipse.jetty.continuation.Continuation; -import org.eclipse.jetty.continuation.ContinuationSupport; -import org.eclipse.jetty.http.HttpHeaderValue; -import org.eclipse.jetty.http.HttpHeader; -import org.eclipse.jetty.http.HttpScheme; -import org.eclipse.jetty.http.HttpURI; +import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.client.api.Response; +import org.eclipse.jetty.client.api.Result; +import org.eclipse.jetty.client.util.InputStreamContentProvider; +import org.eclipse.jetty.client.util.TimedResponseListener; +import org.eclipse.jetty.http.HttpFields; +import org.eclipse.jetty.http.HttpMethod; import org.eclipse.jetty.http.HttpVersion; -import org.eclipse.jetty.http.PathMap; -import org.eclipse.jetty.io.EofException; -import org.eclipse.jetty.util.HostMap; -import org.eclipse.jetty.util.IO; +import org.eclipse.jetty.server.handler.ContextHandler; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; import org.eclipse.jetty.util.thread.QueuedThreadPool; /** - * Asynchronous Proxy Servlet. - * - * Forward requests to another server either as a standard web proxy (as defined by RFC2616) or as a transparent proxy. - *

- * This servlet needs the jetty-util and jetty-client classes to be available to the web application. - *

- * To facilitate JMX monitoring, the "HttpClient" and "ThreadPool" are set as context attributes prefixed with the servlet name. - *

+ * Asynchronous ProxyServlet. + *

+ * Forwards requests to another server either as a standard web reverse proxy + * (as defined by RFC2616) or as a transparent reverse proxy. + *

+ * To facilitate JMX monitoring, the {@link HttpClient} instance is set as context attribute, + * prefixed with the servlet's name and exposed by the mechanism provided by + * {@link ContextHandler#MANAGED_ATTRIBUTES}. + *

* The following init parameters may be used to configure the servlet: *

    - *
  • name - Name of Proxy servlet (default: "ProxyServlet" - *
  • maxThreads - maximum threads - *
  • maxConnections - maximum connections per destination - *
  • timeout - the period in ms the client will wait for a response from the proxied server - *
  • idleTimeout - the period in ms a connection to proxied server can be idle for before it is closed - *
  • requestHeaderSize - the size of the request header buffer (d. 6,144) - *
  • requestBufferSize - the size of the request buffer (d. 12,288) - *
  • responseHeaderSize - the size of the response header buffer (d. 6,144) - *
  • responseBufferSize - the size of the response buffer (d. 32,768) - *
  • HostHeader - Force the host header to a particular value - *
  • whiteList - comma-separated list of allowed proxy destinations - *
  • blackList - comma-separated list of forbidden proxy destinations + *
  • hostHeader - forces the host header to a particular value
  • + *
  • viaHost - the name to use in the Via header: Via: http/1.1 <viaHost>
  • + *
  • whiteList - comma-separated list of allowed proxy hosts
  • + *
  • blackList - comma-separated list of forbidden proxy hosts
  • *
+ *

+ * In addition, see {@link #createHttpClient()} for init parameters used to configure + * the {@link HttpClient} instance. * - * @see org.eclipse.jetty.server.handler.ConnectHandler + * @see ConnectHandler */ -public class ProxyServlet implements Servlet +public class ProxyServlet extends HttpServlet { - protected Logger _log; - protected HttpClient _client; - protected String _hostHeader; - - protected HashSet _DontProxyHeaders = new HashSet(); + protected static final String ASYNC_CONTEXT = ProxyServlet.class.getName() + ".asyncContext"; + private static final Set HOP_HEADERS = new HashSet<>(); + static { - _DontProxyHeaders.add("proxy-connection"); - _DontProxyHeaders.add("connection"); - _DontProxyHeaders.add("keep-alive"); - _DontProxyHeaders.add("transfer-encoding"); - _DontProxyHeaders.add("te"); - _DontProxyHeaders.add("trailer"); - _DontProxyHeaders.add("proxy-authorization"); - _DontProxyHeaders.add("proxy-authenticate"); - _DontProxyHeaders.add("upgrade"); + HOP_HEADERS.add("proxy-connection"); + HOP_HEADERS.add("connection"); + HOP_HEADERS.add("keep-alive"); + HOP_HEADERS.add("transfer-encoding"); + HOP_HEADERS.add("te"); + HOP_HEADERS.add("trailer"); + HOP_HEADERS.add("proxy-authorization"); + HOP_HEADERS.add("proxy-authenticate"); + HOP_HEADERS.add("upgrade"); } - protected ServletConfig _config; - protected ServletContext _context; - protected HostMap _white = new HostMap(); - protected HostMap _black = new HostMap(); + private final Set _whiteList = new HashSet<>(); + private final Set _blackList = new HashSet<>(); - /* ------------------------------------------------------------ */ - /* - * (non-Javadoc) - * - * @see javax.servlet.Servlet#init(javax.servlet.ServletConfig) - */ - public void init(ServletConfig config) throws ServletException + protected Logger _log; + private String _hostHeader; + private String _viaHost; + private HttpClient _client; + private long _timeout; + + @Override + public void init() throws ServletException { - _config = config; - _context = config.getServletContext(); + _log = createLogger(); - _hostHeader = config.getInitParameter("HostHeader"); + ServletConfig config = getServletConfig(); + + _hostHeader = config.getInitParameter("hostHeader"); + + _viaHost = config.getInitParameter("viaHost"); + if (_viaHost == null) + _viaHost = viaHost(); try { - _log = createLogger(config); - _client = createHttpClient(config); + _client = createHttpClient(); - if (_context != null) - { - _context.setAttribute(config.getServletName() + ".ThreadPool",_client.getThreadPool()); - _context.setAttribute(config.getServletName() + ".HttpClient",_client); - } + // Put the HttpClient in the context to leverage ContextHandler.MANAGED_ATTRIBUTES + getServletContext().setAttribute(config.getServletName() + ".HttpClient", _client); - String white = config.getInitParameter("whiteList"); - if (white != null) - { - parseList(white,_white); - } - String black = config.getInitParameter("blackList"); - if (black != null) - { - parseList(black,_black); - } + String whiteList = config.getInitParameter("whiteList"); + if (whiteList != null) + getWhitelistHosts().addAll(parseList(whiteList)); + + String blackList = config.getInitParameter("blackList"); + if (blackList != null) + getBlacklistHosts().addAll(parseList(blackList)); } catch (Exception e) { @@ -153,6 +136,46 @@ public class ProxyServlet implements Servlet } } + public long getTimeout() + { + return _timeout; + } + + public void setTimeout(long timeout) + { + this._timeout = timeout; + } + + public Set getWhitelistHosts() + { + return _whiteList; + } + + public Set getBlacklistHosts() + { + return _blackList; + } + + protected static String viaHost() + { + try + { + return InetAddress.getLocalHost().getHostName(); + } + catch (UnknownHostException x) + { + return "localhost"; + } + } + + /** + * @return a logger instance with a name derived from this servlet's name. + */ + protected Logger createLogger() + { + return Log.getLogger(getServletConfig().getServletName()); + } + public void destroy() { try @@ -165,737 +188,523 @@ public class ProxyServlet implements Servlet } } - /** - * Create and return a logger based on the ServletConfig for use in the - * proxy servlet + * Creates a {@link HttpClient} instance, configured with init parameters of this servlet. + *

+ * The init parameters used to configure the {@link HttpClient} instance are: + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
init-paramdefaultdescription
maxThreads256The max number of threads of HttpClient's Executor
maxConnections32768The max number of connection per address, see {@link HttpClient#setMaxConnectionsPerAddress(int)}
idleTimeout30000The idle timeout in milliseconds, see {@link HttpClient#setIdleTimeout(long)}
timeout60000The total timeout in milliseconds, see {@link TimedResponseListener}
requestBufferSizeHttpClient's defaultThe request buffer size, see {@link HttpClient#setRequestBufferSize(int)}
responseBufferSizeHttpClient's defaultThe response buffer size, see {@link HttpClient#setResponseBufferSize(int)}
* - * @param config - * @return Logger + * @return a {@link HttpClient} configured from the {@link #getServletConfig() servlet configuration} + * @throws ServletException if the {@link HttpClient} cannot be created */ - protected Logger createLogger(ServletConfig config) + protected HttpClient createHttpClient() throws ServletException { - return Log.getLogger("org.eclipse.jetty.servlets." + config.getServletName()); + ServletConfig config = getServletConfig(); + + HttpClient client = newHttpClient(); + // Redirects must be proxied as is, not followed + client.setFollowRedirects(false); + + String value = config.getInitParameter("maxThreads"); + if (value == null) + value = "256"; + QueuedThreadPool executor = new QueuedThreadPool(Integer.parseInt(value)); + String servletName = config.getServletName(); + int dot = servletName.lastIndexOf('.'); + if (dot >= 0) + servletName = servletName.substring(dot + 1); + executor.setName(servletName); + client.setExecutor(executor); + + value = config.getInitParameter("maxConnections"); + if (value == null) + value = "32768"; + client.setMaxConnectionsPerAddress(Integer.parseInt(value)); + + value = config.getInitParameter("idleTimeout"); + if (value == null) + value = "30000"; + client.setIdleTimeout(Long.parseLong(value)); + + value = config.getInitParameter("timeout"); + if (value == null) + value = "60000"; + _timeout = Long.parseLong(value); + + value = config.getInitParameter("requestBufferSize"); + if (value != null) + client.setRequestBufferSize(Integer.parseInt(value)); + + value = config.getInitParameter("responseBufferSize"); + if (value != null) + client.setResponseBufferSize(Integer.parseInt(value)); + + try + { + client.start(); + return client; + } + catch (Exception x) + { + throw new ServletException(x); + } } /** - * Create and return an HttpClientInstance - * - * @return HttpClient + * @return a new HttpClient instance */ - protected HttpClient createHttpClientInstance() + protected HttpClient newHttpClient() { return new HttpClient(); } - /** - * Create and return an HttpClient based on ServletConfig - * - * By default this implementation will create an instance of the - * HttpClient for use by this proxy servlet. - * - * @param config - * @return HttpClient - * @throws Exception - */ - protected HttpClient createHttpClient(ServletConfig config) throws Exception + private Set parseList(String list) { - HttpClient client = createHttpClientInstance(); - client.setConnectorType(HttpClient.CONNECTOR_SELECT_CHANNEL); - - String t = config.getInitParameter("maxThreads"); - - if (t != null) + Set result = new HashSet<>(); + String[] hosts = list.split(","); + for (String host : hosts) { - client.setThreadPool(new QueuedThreadPool(Integer.parseInt(t))); + host = host.trim(); + if (host.length() == 0) + continue; + result.add(host); } - else - { - client.setThreadPool(new QueuedThreadPool()); - } - - ((QueuedThreadPool)client.getThreadPool()).setName(config.getServletName()); - - t = config.getInitParameter("maxConnections"); - - if (t != null) - { - client.setMaxConnectionsPerAddress(Integer.parseInt(t)); - } - - t = config.getInitParameter("timeout"); - - if ( t != null ) - { - client.setTimeout(Long.parseLong(t)); - } - - t = config.getInitParameter("idleTimeout"); - - if ( t != null ) - { - client.setIdleTimeout(Long.parseLong(t)); - } - - t = config.getInitParameter("requestHeaderSize"); - - if ( t != null ) - { - client.setRequestHeaderSize(Integer.parseInt(t)); - } - - t = config.getInitParameter("requestBufferSize"); - - if ( t != null ) - { - client.setRequestBufferSize(Integer.parseInt(t)); - } - - t = config.getInitParameter("responseHeaderSize"); - - if ( t != null ) - { - client.setResponseHeaderSize(Integer.parseInt(t)); - } - - t = config.getInitParameter("responseBufferSize"); - - if ( t != null ) - { - client.setResponseBufferSize(Integer.parseInt(t)); - } - - client.start(); - - return client; + return result; } - /* ------------------------------------------------------------ */ /** - * Helper function to process a parameter value containing a list of new entries and initialize the specified host map. + * Checks the request hostname and path against whitelist and blacklist. * - * @param list - * comma-separated list of new entries - * @param hostMap - * target host map - */ - private void parseList(String list, HostMap hostMap) - { - if (list != null && list.length() > 0) - { - int idx; - String entry; - - StringTokenizer entries = new StringTokenizer(list,","); - while (entries.hasMoreTokens()) - { - entry = entries.nextToken(); - idx = entry.indexOf('/'); - - String host = idx > 0?entry.substring(0,idx):entry; - String path = idx > 0?entry.substring(idx):"/*"; - - host = host.trim(); - PathMap pathMap = hostMap.get(host); - if (pathMap == null) - { - pathMap = new PathMap(true); - hostMap.put(host,pathMap); - } - if (path != null) - { - pathMap.put(path,path); - } - } - } - } - - /* ------------------------------------------------------------ */ - /** - * Check the request hostname and path against white- and blacklist. * - * @param host - * hostname to check - * @param path - * path to check + * @param host host to check + * @param port the port to check * @return true if request is allowed to be proxied */ - public boolean validateDestination(String host, String path) + public boolean validateDestination(String host, int port) { - if (_white.size() > 0) + String hostPort = host + ":" + port; + if (!_whiteList.isEmpty()) { - boolean match = false; - - Object whiteObj = _white.getLazyMatches(host); - if (whiteObj != null) + if (!_whiteList.contains(hostPort)) { - List whiteList = (whiteObj instanceof List)?(List)whiteObj:Collections.singletonList(whiteObj); - - for (Object entry : whiteList) - { - PathMap pathMap = ((Map.Entry)entry).getValue(); - if (match = (pathMap != null && (pathMap.size() == 0 || pathMap.match(path) != null))) - break; - } - } - - if (!match) + _log.debug("Host {}:{} not whitelisted", host, port); + return false; + } + } + if (!_blackList.isEmpty()) + { + if (_blackList.contains(hostPort)) + { + _log.debug("Host {}:{} blacklisted", host, port); return false; - } - - if (_black.size() > 0) - { - Object blackObj = _black.getLazyMatches(host); - if (blackObj != null) - { - List blackList = (blackObj instanceof List)?(List)blackObj:Collections.singletonList(blackObj); - - for (Object entry : blackList) - { - PathMap pathMap = ((Map.Entry)entry).getValue(); - if (pathMap != null && (pathMap.size() == 0 || pathMap.match(path) != null)) - return false; - } } } - return true; } - /* ------------------------------------------------------------ */ - /* - * (non-Javadoc) - * - * @see javax.servlet.Servlet#getServletConfig() - */ - public ServletConfig getServletConfig() + protected void service(final HttpServletRequest request, final HttpServletResponse response) throws ServletException, IOException { - return _config; - } + final int requestId = getRequestId(request); - /* ------------------------------------------------------------ */ - /** - * Get the hostHeader. - * - * @return the hostHeader - */ - public String getHostHeader() - { - return _hostHeader; - } + URI rewrittenURI = rewriteURI(request); - /* ------------------------------------------------------------ */ - /** - * Set the hostHeader. - * - * @param hostHeader - * the hostHeader to set - */ - public void setHostHeader(String hostHeader) - { - _hostHeader = hostHeader; - } - - /* ------------------------------------------------------------ */ - /* - * (non-Javadoc) - * - * @see javax.servlet.Servlet#service(javax.servlet.ServletRequest, javax.servlet.ServletResponse) - */ - public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException - { - final int debug = _log.isDebugEnabled()?req.hashCode():0; - - final HttpServletRequest request = (HttpServletRequest)req; - final HttpServletResponse response = (HttpServletResponse)res; - - if ("CONNECT".equalsIgnoreCase(request.getMethod())) + if (_log.isDebugEnabled()) { - handleConnect(request,response); + StringBuffer uri = request.getRequestURL(); + if (request.getQueryString() != null) + uri.append("?").append(request.getQueryString()); + _log.debug("{} rewriting: {} -> {}", requestId, uri, rewrittenURI); } - else + + if (rewrittenURI == null) { - final InputStream in = request.getInputStream(); - final OutputStream out = response.getOutputStream(); + response.sendError(HttpServletResponse.SC_FORBIDDEN); + return; + } - final Continuation continuation = ContinuationSupport.getContinuation(request); + final Request proxyRequest = _client.newRequest(rewrittenURI) + .method(HttpMethod.fromString(request.getMethod())) + .version(HttpVersion.fromString(request.getProtocol())); - if (!continuation.isInitial()) - response.sendError(HttpServletResponse.SC_GATEWAY_TIMEOUT); // Need better test that isInitial - else + // Copy headers + for (Enumeration headerNames = request.getHeaderNames(); headerNames.hasMoreElements();) + { + String headerName = headerNames.nextElement(); + String lowerHeaderName = headerName.toLowerCase(Locale.ENGLISH); + + // Remove hop-by-hop headers + if (HOP_HEADERS.contains(lowerHeaderName)) + continue; + + for (Enumeration headerValues = request.getHeaders(headerName); headerValues.hasMoreElements();) { - - String uri = request.getRequestURI(); - if (request.getQueryString() != null) - uri += "?" + request.getQueryString(); - - HttpURI url = proxyHttpURI(request,uri); - - if (debug != 0) - _log.debug(debug + " proxy " + uri + "-->" + url); - - if (url == null) - { - response.sendError(HttpServletResponse.SC_FORBIDDEN); - return; - } - - HttpExchange exchange = new HttpExchange() - { - @Override - protected void onRequestCommitted() throws IOException - { - } - - @Override - protected void onRequestComplete() throws IOException - { - } - - @Override - protected void onResponseComplete() throws IOException - { - if (debug != 0) - _log.debug(debug + " complete"); - continuation.complete(); - } - - @Override - protected void onResponseContent(Buffer content) throws IOException - { - if (debug != 0) - _log.debug(debug + " content" + content.length()); - content.writeTo(out); - } - - @Override - protected void onResponseHeaderComplete() throws IOException - { - } - - @Override - protected void onResponseStatus(Buffer version, int status, Buffer reason) throws IOException - { - if (debug != 0) - _log.debug(debug + " " + version + " " + status + " " + reason); - - if (reason != null && reason.length() > 0) - response.setStatus(status,reason.toString()); - else - response.setStatus(status); - } - - @Override - protected void onResponseHeader(Buffer name, Buffer value) throws IOException - { - String nameString = name.toString(); - String s = nameString.toLowerCase(); - if (!_DontProxyHeaders.contains(s) || (HttpHeader.CONNECTION.is(name) && HttpHeaderValue.CLOSE.is(value))) - { - if (debug != 0) - _log.debug(debug + " " + name + ": " + value); - - String filteredHeaderValue = filterResponseHeaderValue(nameString,value.toString(),request); - if (filteredHeaderValue != null && filteredHeaderValue.trim().length() > 0) - { - if (debug != 0) - _log.debug(debug + " " + name + ": (filtered): " + filteredHeaderValue); - response.addHeader(nameString,filteredHeaderValue); - } - } - else if (debug != 0) - _log.debug(debug + " " + name + "! " + value); - } - - @Override - protected void onConnectionFailed(Throwable ex) - { - handleOnConnectionFailed(ex,request,response); - - // it is possible this might trigger before the - // continuation.suspend() - if (!continuation.isInitial()) - { - continuation.complete(); - } - } - - @Override - protected void onException(Throwable ex) - { - if (ex instanceof EofException) - { - _log.ignore(ex); - return; - } - handleOnException(ex,request,response); - - // it is possible this might trigger before the - // continuation.suspend() - if (!continuation.isInitial()) - { - continuation.complete(); - } - } - - @Override - protected void onExpire() - { - handleOnExpire(request,response); - continuation.complete(); - } - - }; - - exchange.setScheme((HttpScheme.HTTPS.is(request.getScheme())?HttpScheme.HTTPS:HttpScheme.HTTP).asString()); - exchange.setMethod(request.getMethod()); - exchange.setURL(url.toString()); - exchange.setVersion(HttpVersion.CACHE.get(request.getProtocol())); - - - if (debug != 0) - _log.debug(debug + " " + request.getMethod() + " " + url + " " + request.getProtocol()); - - // check connection header - String connectionHdr = request.getHeader("Connection"); - if (connectionHdr != null) - { - connectionHdr = connectionHdr.toLowerCase(); - if (connectionHdr.indexOf("keep-alive") < 0 && connectionHdr.indexOf("close") < 0) - connectionHdr = null; - } - - // force host - if (_hostHeader != null) - exchange.setRequestHeader("Host",_hostHeader); - - // copy headers - boolean xForwardedFor = false; - boolean hasContent = false; - long contentLength = -1; - Enumeration enm = request.getHeaderNames(); - while (enm.hasMoreElements()) - { - // TODO could be better than this! - String hdr = (String)enm.nextElement(); - String lhdr = hdr.toLowerCase(); - - if (_DontProxyHeaders.contains(lhdr)) - continue; - if (connectionHdr != null && connectionHdr.indexOf(lhdr) >= 0) - continue; - if (_hostHeader != null && "host".equals(lhdr)) - continue; - - if ("content-type".equals(lhdr)) - hasContent = true; - else if ("content-length".equals(lhdr)) - { - contentLength = request.getContentLength(); - exchange.setRequestHeader(HttpHeader.CONTENT_LENGTH.asString(),Long.toString(contentLength)); - if (contentLength > 0) - hasContent = true; - } - else if ("x-forwarded-for".equals(lhdr)) - xForwardedFor = true; - - Enumeration vals = request.getHeaders(hdr); - while (vals.hasMoreElements()) - { - String val = (String)vals.nextElement(); - if (val != null) - { - if (debug != 0) - _log.debug(debug + " " + hdr + ": " + val); - - exchange.setRequestHeader(hdr,val); - } - } - } - - // Proxy headers - exchange.setRequestHeader("Via","1.1 (jetty)"); - if (!xForwardedFor) - { - exchange.addRequestHeader("X-Forwarded-For",request.getRemoteAddr()); - exchange.addRequestHeader("X-Forwarded-Proto",request.getScheme()); - exchange.addRequestHeader("X-Forwarded-Host",request.getHeader("Host")); - exchange.addRequestHeader("X-Forwarded-Server",request.getLocalName()); - } - - if (hasContent) - { - exchange.setRequestContentSource(in); - } - - customizeExchange(exchange, request); - - /* - * we need to set the timeout on the continuation to take into - * account the timeout of the HttpClient and the HttpExchange - */ - long ctimeout = (_client.getTimeout() > exchange.getTimeout()) ? _client.getTimeout() : exchange.getTimeout(); - - // continuation fudge factor of 1000, underlying components - // should fail/expire first from exchange - if ( ctimeout == 0 ) - { - continuation.setTimeout(0); // ideally never times out - } - else - { - continuation.setTimeout(ctimeout + 1000); - } - - customizeContinuation(continuation); - - continuation.suspend(response); - _client.send(exchange); - + String headerValue = headerValues.nextElement(); + if (headerValue != null) + proxyRequest.header(headerName, headerValue); } } - } - /* ------------------------------------------------------------ */ - public void handleConnect(HttpServletRequest request, HttpServletResponse response) throws IOException - { - String uri = request.getRequestURI(); + // Force the Host header if configured + if (_hostHeader != null) + proxyRequest.header("Host", _hostHeader); - String port = ""; - String host = ""; + // Add proxy headers + proxyRequest.header("Via", "http/1.1 " + _viaHost); + proxyRequest.header("X-Forwarded-For", request.getRemoteAddr()); + proxyRequest.header("X-Forwarded-Proto", request.getScheme()); + proxyRequest.header("X-Forwarded-Host", request.getHeader("Host")); + proxyRequest.header("X-Forwarded-Server", request.getLocalName()); - int c = uri.indexOf(':'); - if (c >= 0) + proxyRequest.content(new InputStreamContentProvider(request.getInputStream()) { - port = uri.substring(c + 1); - host = uri.substring(0,c); - if (host.indexOf('/') > 0) - host = host.substring(host.indexOf('/') + 1); + @Override + public long getLength() + { + return request.getContentLength(); + } + + @Override + protected ByteBuffer onRead(byte[] buffer, int offset, int length) + { + _log.debug("{} proxying content to upstream: {} bytes", requestId, length); + return super.onRead(buffer, offset, length); + } + }); + + final AsyncContext asyncContext = request.startAsync(); + // We do not timeout the continuation, but the proxy request + asyncContext.setTimeout(0); + request.setAttribute(ASYNC_CONTEXT, asyncContext); + + customizeProxyRequest(proxyRequest, request); + + if (_log.isDebugEnabled()) + { + StringBuilder builder = new StringBuilder(request.getMethod()); + builder.append(" ").append(request.getRequestURI()); + String query = request.getQueryString(); + if (query != null) + builder.append("?").append(query); + builder.append(" ").append(request.getProtocol()).append("\r\n"); + for (Enumeration headerNames = request.getHeaderNames(); headerNames.hasMoreElements();) + { + String headerName = headerNames.nextElement(); + builder.append(headerName).append(": "); + for (Enumeration headerValues = request.getHeaders(headerName); headerValues.hasMoreElements();) + { + String headerValue = headerValues.nextElement(); + if (headerValue != null) + builder.append(headerValue); + if (headerValues.hasMoreElements()) + builder.append(","); + } + builder.append("\r\n"); + } + builder.append("\r\n"); + + _log.debug("{} proxying to upstream:{}{}{}{}", + requestId, + System.lineSeparator(), + builder, + proxyRequest, + System.lineSeparator(), + proxyRequest.getHeaders().toString().trim()); } - // TODO - make this async! + proxyRequest.send(new TimedResponseListener(getTimeout(), TimeUnit.MILLISECONDS, proxyRequest, new ProxyResponseListener(request, response))); + } - InetSocketAddress inetAddress = new InetSocketAddress(host,Integer.parseInt(port)); - - // if (isForbidden(HttpMessage.__SSL_SCHEME,addrPort.getHost(),addrPort.getPort(),false)) - // { - // sendForbid(request,response,uri); - // } - // else + protected void onResponseHeaders(HttpServletRequest request, HttpServletResponse response, Response proxyResponse) + { + for (HttpFields.Field field : proxyResponse.getHeaders()) { - InputStream in = request.getInputStream(); - OutputStream out = response.getOutputStream(); + String headerName = field.getName(); + String lowerHeaderName = headerName.toLowerCase(Locale.ENGLISH); + if (HOP_HEADERS.contains(lowerHeaderName)) + continue; - Socket socket = new Socket(inetAddress.getAddress(),inetAddress.getPort()); + String newHeaderValue = filterResponseHeader(request, headerName, field.getValue()); + if (newHeaderValue == null || newHeaderValue.trim().length() == 0) + continue; - response.setStatus(200); - response.setHeader("Connection","close"); - response.flushBuffer(); - // TODO prevent real close! - - IO.copyThread(socket.getInputStream(),out); - IO.copy(in,socket.getOutputStream()); + response.addHeader(headerName, newHeaderValue); } } - /* ------------------------------------------------------------ */ - protected HttpURI proxyHttpURI(HttpServletRequest request, String uri) throws MalformedURLException + protected void onResponseContent(HttpServletRequest request, HttpServletResponse response, Response proxyResponse, byte[] buffer, int offset, int length) throws IOException { - return proxyHttpURI(request.getScheme(), request.getServerName(), request.getServerPort(), uri); + response.getOutputStream().write(buffer, offset, length); + _log.debug("{} proxying content to downstream: {} bytes", getRequestId(request), length); } - protected HttpURI proxyHttpURI(String scheme, String serverName, int serverPort, String uri) throws MalformedURLException + protected void onResponseSuccess(HttpServletRequest request, HttpServletResponse response, Response proxyResponse) { - if (!validateDestination(serverName,uri)) + AsyncContext asyncContext = (AsyncContext)request.getAttribute(ASYNC_CONTEXT); + asyncContext.complete(); + _log.debug("{} proxying successful", getRequestId(request)); + } + + protected void onResponseFailure(HttpServletRequest request, HttpServletResponse response, Response proxyResponse, Throwable failure) + { + _log.debug(getRequestId(request) + " proxying failed", failure); + if (!response.isCommitted()) + { + if (failure instanceof TimeoutException) + response.setStatus(HttpServletResponse.SC_GATEWAY_TIMEOUT); + else + response.setStatus(HttpServletResponse.SC_BAD_GATEWAY); + } + AsyncContext asyncContext = (AsyncContext)request.getAttribute(ASYNC_CONTEXT); + asyncContext.complete(); + } + + protected int getRequestId(HttpServletRequest request) + { + return System.identityHashCode(request); + } + + protected URI rewriteURI(HttpServletRequest request) + { + if (!validateDestination(request.getServerName(), request.getServerPort())) return null; - return new HttpURI(scheme + "://" + serverName + ":" + serverPort + uri); - } - - /* - * (non-Javadoc) - * - * @see javax.servlet.Servlet#getServletInfo() - */ - public String getServletInfo() - { - return "Proxy Servlet"; - } - - - /** - * Extension point for subclasses to customize an exchange. Useful for setting timeouts etc. The default implementation does nothing. - * - * @param exchange - * @param request - */ - protected void customizeExchange(HttpExchange exchange, HttpServletRequest request) - { + StringBuffer uri = request.getRequestURL(); + String query = request.getQueryString(); + if (query != null) + uri.append("?").append(query); + return URI.create(uri.toString()); } /** - * Extension point for subclasses to customize the Continuation after it's initial creation in the service method. Useful for setting timeouts etc. The - * default implementation does nothing. + * Extension point for subclasses to customize the proxy request. + * The default implementation does nothing. * - * @param continuation + * @param proxyRequest the proxy request to customize + * @param request the request to be proxied */ - protected void customizeContinuation(Continuation continuation) + protected void customizeProxyRequest(Request proxyRequest, HttpServletRequest request) { - } /** - * Extension point for custom handling of an HttpExchange's onConnectionFailed method. The default implementation delegates to - * {@link #handleOnException(Throwable, javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)} + * Extension point for remote server response header filtering. + * The default implementation returns the header value as is. + * If null is returned, this header won't be forwarded back to the client. * - * @param ex - * @param request - * @param response + * @param headerName the header name + * @param headerValue the header value + * @param request the request to proxy + * @return filteredHeaderValue the new header value */ - protected void handleOnConnectionFailed(Throwable ex, HttpServletRequest request, HttpServletResponse response) - { - handleOnException(ex,request,response); - } - - /** - * Extension point for custom handling of an HttpExchange's onException method. The default implementation sets the response status to - * HttpServletResponse.SC_INTERNAL_SERVER_ERROR (503) - * - * @param ex - * @param request - * @param response - */ - protected void handleOnException(Throwable ex, HttpServletRequest request, HttpServletResponse response) - { - if (ex instanceof IOException) - { - _log.warn(ex.toString()); - _log.debug(ex); - } - else - _log.warn(ex); - - if (!response.isCommitted()) - { - response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); - } - } - - /** - * Extension point for custom handling of an HttpExchange's onExpire method. The default implementation sets the response status to - * HttpServletResponse.SC_GATEWAY_TIMEOUT (504) - * - * @param request - * @param response - */ - protected void handleOnExpire(HttpServletRequest request, HttpServletResponse response) - { - if (!response.isCommitted()) - { - response.setStatus(HttpServletResponse.SC_GATEWAY_TIMEOUT); - } - } - - /** - * Extension point for remote server response header filtering. The default implementation returns the header value as is. If null is returned, this header - * won't be forwarded back to the client. - * - * @param headerName - * @param headerValue - * @param request - * @return filteredHeaderValue - */ - protected String filterResponseHeaderValue(String headerName, String headerValue, HttpServletRequest request) + protected String filterResponseHeader(HttpServletRequest request, String headerName, String headerValue) { return headerValue; } /** * Transparent Proxy. - * - * This convenience extension to ProxyServlet configures the servlet as a transparent proxy. The servlet is configured with init parameters: + *

+ * This convenience extension to ProxyServlet configures the servlet as a transparent proxy. + * The servlet is configured with init parameters: *

    - *
  • ProxyTo - a URI like http://host:80/context to which the request is proxied. - *
  • Prefix - a URI prefix that is striped from the start of the forwarded URI. + *
  • proxyTo - a URI like http://host:80/context to which the request is proxied. + *
  • prefix - a URI prefix that is striped from the start of the forwarded URI. *
- * For example, if a request was received at /foo/bar and the ProxyTo was http://host:80/context and the Prefix was /foo, then the request would be proxied - * to http://host:80/context/bar - * + * For example, if a request is received at /foo/bar and the 'proxyTo' parameter is "http://host:80/context" + * and the 'prefix' parameter is "/foo", then the request would be proxied to "http://host:80/context/bar". */ public static class Transparent extends ProxyServlet { - String _prefix; - String _proxyTo; + private String _proxyTo; + private String _prefix; public Transparent() { } - public Transparent(String prefix, String host, int port) + public Transparent(String proxyTo, String prefix) { - this(prefix,"http",host,port,null); - } - - public Transparent(String prefix, String schema, String host, int port, String path) - { - try - { - if (prefix != null) - { - _prefix = new URI(prefix).normalize().toString(); - } - _proxyTo = new URI(schema,null,host,port,path,null,null).normalize().toString(); - } - catch (URISyntaxException ex) - { - _log.debug("Invalid URI syntax",ex); - } + _proxyTo = URI.create(proxyTo).normalize().toString(); + _prefix = URI.create(prefix).normalize().toString(); } @Override - public void init(ServletConfig config) throws ServletException + public void init() throws ServletException { - super.init(config); + super.init(); - String prefix = config.getInitParameter("Prefix"); - _prefix = prefix == null?_prefix:prefix; + ServletConfig config = getServletConfig(); + + String prefix = config.getInitParameter("prefix"); + _prefix = prefix == null ? _prefix : prefix; // Adjust prefix value to account for context path - String contextPath = _context.getContextPath(); - _prefix = _prefix == null?contextPath:(contextPath + _prefix); + String contextPath = getServletContext().getContextPath(); + _prefix = _prefix == null ? contextPath : (contextPath + _prefix); - String proxyTo = config.getInitParameter("ProxyTo"); - _proxyTo = proxyTo == null?_proxyTo:proxyTo; + String proxyTo = config.getInitParameter("proxyTo"); + _proxyTo = proxyTo == null ? _proxyTo : proxyTo; if (_proxyTo == null) - throw new UnavailableException("ProxyTo parameter is requred."); + throw new UnavailableException("Init parameter 'proxyTo' is required."); if (!_prefix.startsWith("/")) - throw new UnavailableException("Prefix parameter must start with a '/'."); + throw new UnavailableException("Init parameter 'prefix' parameter must start with a '/'."); _log.info(config.getServletName() + " @ " + _prefix + " to " + _proxyTo); } @Override - protected HttpURI proxyHttpURI(final String scheme, final String serverName, int serverPort, final String uri) throws MalformedURLException + protected URI rewriteURI(HttpServletRequest request) { + String path = request.getRequestURI(); + if (!path.startsWith(_prefix)) + return null; + + URI rewrittenURI = URI.create(_proxyTo + path.substring(_prefix.length())).normalize(); + + if (!validateDestination(rewrittenURI.getHost(), rewrittenURI.getPort())) + return null; + + return rewrittenURI; + } + } + + private class ProxyResponseListener extends Response.Listener.Empty + { + private final HttpServletRequest request; + private final HttpServletResponse response; + + public ProxyResponseListener(HttpServletRequest request, HttpServletResponse response) + { + this.request = request; + this.response = response; + } + + @Override + public void onBegin(Response proxyResponse) + { + response.setStatus(proxyResponse.getStatus()); + } + + @Override + public void onHeaders(Response proxyResponse) + { + onResponseHeaders(request, response, proxyResponse); + + if (_log.isDebugEnabled()) + { + StringBuilder builder = new StringBuilder("\r\n"); + builder.append(request.getProtocol()).append(" ").append(response.getStatus()).append(" ").append(proxyResponse.getReason()).append("\r\n"); + for (String headerName : response.getHeaderNames()) + { + builder.append(headerName).append(": "); + for (Iterator headerValues = response.getHeaders(headerName).iterator(); headerValues.hasNext();) + { + String headerValue = headerValues.next(); + if (headerValue != null) + builder.append(headerValue); + if (headerValues.hasNext()) + builder.append(","); + } + builder.append("\r\n"); + } + _log.debug("{} proxying to downstream:{}{}{}{}{}", + getRequestId(request), + System.lineSeparator(), + proxyResponse, + System.lineSeparator(), + proxyResponse.getHeaders().toString().trim(), + System.lineSeparator(), + builder); + } + } + + @Override + public void onContent(Response proxyResponse, ByteBuffer content) + { + byte[] buffer; + int offset; + int length = content.remaining(); + if (content.hasArray()) + { + buffer = content.array(); + offset = content.arrayOffset(); + } + else + { + buffer = new byte[length]; + content.get(buffer); + offset = 0; + } + try { - if (!uri.startsWith(_prefix)) - return null; - - URI dstUri = new URI(_proxyTo + uri.substring(_prefix.length())).normalize(); - - if (!validateDestination(dstUri.getHost(),dstUri.getPath())) - return null; - - return new HttpURI(dstUri.toString()); + onResponseContent(request, response, proxyResponse, buffer, offset, length); } - catch (URISyntaxException ex) + catch (IOException x) { - throw new MalformedURLException(ex.getMessage()); + proxyResponse.abort(x); } } + + @Override + public void onSuccess(Response proxyResponse) + { + onResponseSuccess(request, response, proxyResponse); + } + + @Override + public void onFailure(Response proxyResponse, Throwable failure) + { + onResponseFailure(request, response, proxyResponse, failure); + } + + @Override + public void onComplete(Result result) + { + _log.debug("{} proxying complete", getRequestId(request)); + } } } diff --git a/jetty-proxy/src/test/java/org/eclipse/jetty/proxy/AbstractBalancerServletTest.java b/jetty-proxy/src/test/java/org/eclipse/jetty/proxy/AbstractBalancerServletTest.java deleted file mode 100644 index 2f3b86b8f33..00000000000 --- a/jetty-proxy/src/test/java/org/eclipse/jetty/proxy/AbstractBalancerServletTest.java +++ /dev/null @@ -1,164 +0,0 @@ -// -// ======================================================================== -// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd. -// ------------------------------------------------------------------------ -// All rights reserved. This program and the accompanying materials -// are made available under the terms of the Eclipse Public License v1.0 -// and Apache License v2.0 which accompanies this distribution. -// -// The Eclipse Public License is available at -// http://www.eclipse.org/legal/epl-v10.html -// -// The Apache License v2.0 is available at -// http://www.opensource.org/licenses/apache2.0.php -// -// You may elect to redistribute this code under either of these licenses. -// ======================================================================== -// - -package org.eclipse.jetty.servlets; - -import java.io.IOException; - -import javax.servlet.http.HttpServlet; - -import org.eclipse.jetty.client.ContentExchange; -import org.eclipse.jetty.client.HttpClient; -import org.eclipse.jetty.http.HttpCookie; -import org.eclipse.jetty.http.HttpMethods; -import org.eclipse.jetty.io.Buffer; -import org.eclipse.jetty.proxy.BalancerServlet; -import org.eclipse.jetty.server.Connector; -import org.eclipse.jetty.server.Server; -import org.eclipse.jetty.server.SelectChannelConnector; -import org.eclipse.jetty.server.session.HashSessionIdManager; -import org.eclipse.jetty.servlet.ServletContextHandler; -import org.eclipse.jetty.servlet.ServletHolder; -import org.junit.After; -import org.junit.Before; - - -public abstract class AbstractBalancerServletTest -{ - - private boolean _stickySessions; - - private Server _node1; - - private Server _node2; - - private Server _balancerServer; - - private HttpClient _httpClient; - - @Before - public void setUp() throws Exception - { - _httpClient = new HttpClient(); - _httpClient.registerListener("org.eclipse.jetty.client.RedirectListener"); - _httpClient.start(); - } - - @After - public void tearDown() throws Exception - { - stopServer(_node1); - stopServer(_node2); - stopServer(_balancerServer); - _httpClient.stop(); - } - - private void stopServer(Server server) - { - try - { - server.stop(); - } - catch (Exception e) - { - // Do nothing - } - } - - protected void setStickySessions(boolean stickySessions) - { - _stickySessions = stickySessions; - } - - protected void startBalancer(Class httpServletClass) throws Exception - { - _node1 = createServer(new ServletHolder(httpServletClass.newInstance()),"/pipo","/molo/*"); - setSessionIdManager(_node1,"node1"); - _node1.start(); - - _node2 = createServer(new ServletHolder(httpServletClass.newInstance()),"/pipo","/molo/*"); - setSessionIdManager(_node2,"node2"); - _node2.start(); - - BalancerServlet balancerServlet = new BalancerServlet(); - ServletHolder balancerServletHolder = new ServletHolder(balancerServlet); - balancerServletHolder.setInitParameter("StickySessions",String.valueOf(_stickySessions)); - balancerServletHolder.setInitParameter("ProxyPassReverse","true"); - balancerServletHolder.setInitParameter("BalancerMember." + "node1" + ".ProxyTo","http://localhost:" + getServerPort(_node1)); - balancerServletHolder.setInitParameter("BalancerMember." + "node2" + ".ProxyTo","http://localhost:" + getServerPort(_node2)); - - _balancerServer = createServer(balancerServletHolder,"/pipo","/molo/*"); - _balancerServer.start(); - } - - private Server createServer(ServletHolder servletHolder, String appContext, String servletUrlPattern) - { - Server server = new Server(); - SelectChannelConnector httpConnector = new SelectChannelConnector(server); - server.addConnector(httpConnector); - - ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS); - context.setContextPath(appContext); - server.setHandler(context); - - context.addServlet(servletHolder,servletUrlPattern); - - return server; - } - - private void setSessionIdManager(Server node, String nodeName) - { - HashSessionIdManager sessionIdManager = new HashSessionIdManager(); - sessionIdManager.setWorkerName(nodeName); - node.setSessionIdManager(sessionIdManager); - } - - private int getServerPort(Server server) - { - return ((Connector.NetConnector)server.getConnectors()[0]).getLocalPort(); - } - - protected byte[] sendRequestToBalancer(String requestUri) throws IOException, InterruptedException - { - ContentExchange exchange = new ContentExchange() - { - @Override - protected void onResponseHeader(Buffer name, Buffer value) throws IOException - { - // Cookie persistence - if (name.toString().equals("Set-Cookie")) - { - String cookieVal = value.toString(); - if (cookieVal.startsWith("JSESSIONID=")) - { - String jsessionid = cookieVal.split(";")[0].substring("JSESSIONID=".length()); - _httpClient.getDestination(getAddress(),false).addCookie(new HttpCookie("JSESSIONID",jsessionid)); - } - } - } - }; - exchange.setURL("http://localhost:" + getServerPort(_balancerServer) + "/pipo/molo/" + requestUri); - exchange.setMethod(HttpMethods.GET); - - _httpClient.send(exchange); - exchange.waitForDone(); - - return exchange.getResponseContentBytes(); - } - -} diff --git a/jetty-proxy/src/test/java/org/eclipse/jetty/proxy/AbstractConnectHandlerTest.java b/jetty-proxy/src/test/java/org/eclipse/jetty/proxy/AbstractConnectHandlerTest.java index bb80dcd90a3..ec55d5e9c71 100644 --- a/jetty-proxy/src/test/java/org/eclipse/jetty/proxy/AbstractConnectHandlerTest.java +++ b/jetty-proxy/src/test/java/org/eclipse/jetty/proxy/AbstractConnectHandlerTest.java @@ -16,202 +16,182 @@ // ======================================================================== // -package org.eclipse.jetty.server.handler; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; - -import java.io.BufferedReader; -import java.io.EOFException; -import java.io.IOException; -import java.net.Socket; -import java.net.SocketTimeoutException; -import java.util.LinkedHashMap; -import java.util.Map; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import org.eclipse.jetty.proxy.ConnectHandler; -import org.eclipse.jetty.server.Connector; -import org.eclipse.jetty.server.Handler; -import org.eclipse.jetty.server.SelectChannelConnector; -import org.eclipse.jetty.server.Server; -import org.junit.AfterClass; +package org.eclipse.jetty.proxy; /** * @version $Revision$ $Date$ */ public abstract class AbstractConnectHandlerTest { - protected static Server server; - protected static Connector.NetConnector serverConnector; - protected static Server proxy; - protected static Connector proxyConnector; - - protected static void startServer(Connector.NetConnector connector, Handler handler) throws Exception - { - server = new Server(); - serverConnector = connector; - server.addConnector(serverConnector); - server.setHandler(handler); - server.start(); - } - - protected static void startProxy() throws Exception - { - proxy = new Server(); - proxyConnector = new SelectChannelConnector(); - proxy.addConnector(proxyConnector); - proxy.setHandler(new ConnectHandler()); - proxy.start(); - } - - @AfterClass - public static void stop() throws Exception - { - stopProxy(); - stopServer(); - } - - protected static void stopServer() throws Exception - { - server.stop(); - server.join(); - } - - protected static void stopProxy() throws Exception - { - proxy.stop(); - proxy.join(); - } - - protected Response readResponse(BufferedReader reader) throws IOException - { - // Simplified parser for HTTP responses - String line = reader.readLine(); - if (line == null) - throw new EOFException(); - Matcher responseLine = Pattern.compile("HTTP/1\\.1\\s+(\\d+)").matcher(line); - assertTrue(responseLine.lookingAt()); - String code = responseLine.group(1); - - Map headers = new LinkedHashMap(); - while ((line = reader.readLine()) != null) - { - if (line.trim().length() == 0) - break; - - Matcher header = Pattern.compile("([^:]+):\\s*(.*)").matcher(line); - assertTrue(header.lookingAt()); - String headerName = header.group(1); - String headerValue = header.group(2); - headers.put(headerName.toLowerCase(), headerValue.toLowerCase()); - } - - StringBuilder body; - if (headers.containsKey("content-length")) - { - int readLen = 0; - int length = Integer.parseInt(headers.get("content-length")); - body=new StringBuilder(length); - try - { - for (int i = 0; i < length; ++i) - { - char c = (char)reader.read(); - body.append(c); - readLen++; - - } - - } - catch (SocketTimeoutException e) - { - System.err.printf("Read %,d bytes (out of an expected %,d bytes)%n",readLen,length); - throw e; - } - } - else if ("chunked".equals(headers.get("transfer-encoding"))) - { - body = new StringBuilder(64*1024); - while ((line = reader.readLine()) != null) - { - if ("0".equals(line)) - { - line = reader.readLine(); - assertEquals("", line); - break; - } - - try - { - Thread.sleep(5); - } - catch (InterruptedException e) - { - e.printStackTrace(); - } - - int length = Integer.parseInt(line, 16); - for (int i = 0; i < length; ++i) - { - char c = (char)reader.read(); - body.append(c); - } - line = reader.readLine(); - assertEquals("", line); - } - } - else throw new IllegalStateException(); - - return new Response(code, headers, body.toString().trim()); - } - - protected Socket newSocket() throws IOException - { - Socket socket = new Socket("localhost", ((Connector.NetConnector)proxyConnector).getLocalPort()); - socket.setSoTimeout(10000); - return socket; - } - - protected class Response - { - private final String code; - private final Map headers; - private final String body; - - private Response(String code, Map headers, String body) - { - this.code = code; - this.headers = headers; - this.body = body; - } - - public String getCode() - { - return code; - } - - public Map getHeaders() - { - return headers; - } - - public String getBody() - { - return body; - } - - @Override - public String toString() - { - StringBuilder builder = new StringBuilder(); - builder.append(code).append("\r\n"); - for (Map.Entry entry : headers.entrySet()) - builder.append(entry.getKey()).append(": ").append(entry.getValue()).append("\r\n"); - builder.append("\r\n"); - builder.append(body); - return builder.toString(); - } - } +// protected static Server server; +// protected static Connector.NetConnector serverConnector; +// protected static Server proxy; +// protected static Connector proxyConnector; +// +// protected static void startServer(Connector.NetConnector connector, Handler handler) throws Exception +// { +// server = new Server(); +// serverConnector = connector; +// server.addConnector(serverConnector); +// server.setHandler(handler); +// server.start(); +// } +// +// protected static void startProxy() throws Exception +// { +// proxy = new Server(); +// proxyConnector = new SelectChannelConnector(); +// proxy.addConnector(proxyConnector); +// proxy.setHandler(new ConnectHandler()); +// proxy.start(); +// } +// +// @AfterClass +// public static void stop() throws Exception +// { +// stopProxy(); +// stopServer(); +// } +// +// protected static void stopServer() throws Exception +// { +// server.stop(); +// server.join(); +// } +// +// protected static void stopProxy() throws Exception +// { +// proxy.stop(); +// proxy.join(); +// } +// +// protected Response readResponse(BufferedReader reader) throws IOException +// { +// // Simplified parser for HTTP responses +// String line = reader.readLine(); +// if (line == null) +// throw new EOFException(); +// Matcher responseLine = Pattern.compile("HTTP/1\\.1\\s+(\\d+)").matcher(line); +// assertTrue(responseLine.lookingAt()); +// String code = responseLine.group(1); +// +// Map headers = new LinkedHashMap(); +// while ((line = reader.readLine()) != null) +// { +// if (line.trim().length() == 0) +// break; +// +// Matcher header = Pattern.compile("([^:]+):\\s*(.*)").matcher(line); +// assertTrue(header.lookingAt()); +// String headerName = header.group(1); +// String headerValue = header.group(2); +// headers.put(headerName.toLowerCase(), headerValue.toLowerCase()); +// } +// +// StringBuilder body; +// if (headers.containsKey("content-length")) +// { +// int readLen = 0; +// int length = Integer.parseInt(headers.get("content-length")); +// body=new StringBuilder(length); +// try +// { +// for (int i = 0; i < length; ++i) +// { +// char c = (char)reader.read(); +// body.append(c); +// readLen++; +// +// } +// +// } +// catch (SocketTimeoutException e) +// { +// System.err.printf("Read %,d bytes (out of an expected %,d bytes)%n",readLen,length); +// throw e; +// } +// } +// else if ("chunked".equals(headers.get("transfer-encoding"))) +// { +// body = new StringBuilder(64*1024); +// while ((line = reader.readLine()) != null) +// { +// if ("0".equals(line)) +// { +// line = reader.readLine(); +// assertEquals("", line); +// break; +// } +// +// try +// { +// Thread.sleep(5); +// } +// catch (InterruptedException e) +// { +// e.printStackTrace(); +// } +// +// int length = Integer.parseInt(line, 16); +// for (int i = 0; i < length; ++i) +// { +// char c = (char)reader.read(); +// body.append(c); +// } +// line = reader.readLine(); +// assertEquals("", line); +// } +// } +// else throw new IllegalStateException(); +// +// return new Response(code, headers, body.toString().trim()); +// } +// +// protected Socket newSocket() throws IOException +// { +// Socket socket = new Socket("localhost", ((Connector.NetConnector)proxyConnector).getLocalPort()); +// socket.setSoTimeout(10000); +// return socket; +// } +// +// protected class Response +// { +// private final String code; +// private final Map headers; +// private final String body; +// +// private Response(String code, Map headers, String body) +// { +// this.code = code; +// this.headers = headers; +// this.body = body; +// } +// +// public String getCode() +// { +// return code; +// } +// +// public Map getHeaders() +// { +// return headers; +// } +// +// public String getBody() +// { +// return body; +// } +// +// @Override +// public String toString() +// { +// StringBuilder builder = new StringBuilder(); +// builder.append(code).append("\r\n"); +// for (Map.Entry entry : headers.entrySet()) +// builder.append(entry.getKey()).append(": ").append(entry.getValue()).append("\r\n"); +// builder.append("\r\n"); +// builder.append(body); +// return builder.toString(); +// } +// } } diff --git a/jetty-proxy/src/test/java/org/eclipse/jetty/proxy/AsyncProxyServer.java b/jetty-proxy/src/test/java/org/eclipse/jetty/proxy/AsyncProxyServer.java deleted file mode 100644 index 977a7c08ca8..00000000000 --- a/jetty-proxy/src/test/java/org/eclipse/jetty/proxy/AsyncProxyServer.java +++ /dev/null @@ -1,53 +0,0 @@ -// -// ======================================================================== -// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd. -// ------------------------------------------------------------------------ -// All rights reserved. This program and the accompanying materials -// are made available under the terms of the Eclipse Public License v1.0 -// and Apache License v2.0 which accompanies this distribution. -// -// The Eclipse Public License is available at -// http://www.eclipse.org/legal/epl-v10.html -// -// The Apache License v2.0 is available at -// http://www.opensource.org/licenses/apache2.0.php -// -// You may elect to redistribute this code under either of these licenses. -// ======================================================================== -// - -package org.eclipse.jetty.proxy; - -import java.util.EnumSet; - -import javax.servlet.DispatcherType; - -import org.eclipse.jetty.server.Connector; -import org.eclipse.jetty.server.Server; -import org.eclipse.jetty.server.nio.SelectChannelConnector; -import org.eclipse.jetty.servlet.ServletHandler; -import org.eclipse.jetty.servlet.ServletHolder; - -public class AsyncProxyServer -{ - public static void main(String[] args) - throws Exception - { - Server server = new Server(); - Connector connector=new SelectChannelConnector(); - connector.setPort(8888); - server.setConnectors(new Connector[]{connector}); - - ServletHandler handler=new ServletHandler(); - server.setHandler(handler); - - //FilterHolder gzip = handler.addFilterWithMapping("org.eclipse.jetty.servlet.GzipFilter","/*",EnumSet.of(DispatcherType.REQUEST,DispatcherType.ASYNC)); - //gzip.setAsyncSupported(true); - //gzip.setInitParameter("minGzipSize","256"); - ServletHolder proxy = handler.addServletWithMapping("org.eclipse.jetty.servlets.ProxyServlet","/"); - proxy.setAsyncSupported(true); - - server.start(); - server.join(); - } -} diff --git a/jetty-proxy/src/test/java/org/eclipse/jetty/proxy/BalancerServletTest.java b/jetty-proxy/src/test/java/org/eclipse/jetty/proxy/BalancerServletTest.java index 03e83567fb8..1fcb4fe5d50 100644 --- a/jetty-proxy/src/test/java/org/eclipse/jetty/proxy/BalancerServletTest.java +++ b/jetty-proxy/src/test/java/org/eclipse/jetty/proxy/BalancerServletTest.java @@ -16,69 +16,148 @@ // ======================================================================== // -package org.eclipse.jetty.servlets; - -import static org.junit.Assert.*; +package org.eclipse.jetty.proxy; import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStreamReader; - +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; import javax.servlet.ServletException; 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.server.NetworkConnector; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.server.session.HashSessionIdManager; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.servlet.ServletHolder; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; import org.junit.Test; -/** - * - */ -public class BalancerServletTest extends AbstractBalancerServletTest +public class BalancerServletTest { + private static final String CONTEXT_PATH = "/context"; + private static final String SERVLET_PATH = "/mapping"; + + private boolean stickySessions; + private Server server1; + private Server server2; + private Server balancer; + private HttpClient client; + + @Before + public void prepare() throws Exception + { + client = new HttpClient(); + client.start(); + } + + @After + public void dispose() throws Exception + { + server1.stop(); + server2.stop(); + balancer.stop(); + client.stop(); + } + + protected void startBalancer(Class servletClass) throws Exception + { + server1 = createServer(new ServletHolder(servletClass), "node1"); + server1.start(); + + server2 = createServer(new ServletHolder(servletClass), "node2"); + server2.start(); + + ServletHolder balancerServletHolder = new ServletHolder(BalancerServlet.class); + balancerServletHolder.setInitParameter("stickySessions", String.valueOf(stickySessions)); + balancerServletHolder.setInitParameter("proxyPassReverse", "true"); + balancerServletHolder.setInitParameter("balancerMember." + "node1" + ".proxyTo", "http://localhost:" + getServerPort(server1)); + balancerServletHolder.setInitParameter("balancerMember." + "node2" + ".proxyTo", "http://localhost:" + getServerPort(server2)); + + balancer = createServer(balancerServletHolder, null); + balancer.start(); + } + + private Server createServer(ServletHolder servletHolder, String nodeName) + { + Server server = new Server(); + ServerConnector connector = new ServerConnector(server); + server.addConnector(connector); + + ServletContextHandler context = new ServletContextHandler(server, CONTEXT_PATH, ServletContextHandler.SESSIONS); + context.addServlet(servletHolder, SERVLET_PATH + "/*"); + + if (nodeName != null) + { + HashSessionIdManager sessionIdManager = new HashSessionIdManager(); + sessionIdManager.setWorkerName(nodeName); + server.setSessionIdManager(sessionIdManager); + } + + return server; + } + + private int getServerPort(Server server) + { + return ((NetworkConnector)server.getConnectors()[0]).getLocalPort(); + } + + protected byte[] sendRequestToBalancer(String path) throws Exception + { + ContentResponse response = client.newRequest("localhost", getServerPort(balancer)) + .path(CONTEXT_PATH + SERVLET_PATH + path) + .send() + .get(5, TimeUnit.SECONDS); + return response.getContent(); + } @Test public void testRoundRobinBalancer() throws Exception { - setStickySessions(false); + stickySessions = false; startBalancer(CounterServlet.class); - for (int i = 0; i < 10; i++) { - byte[] responseBytes = sendRequestToBalancer("/"); + byte[] responseBytes = sendRequestToBalancer("/roundRobin"); String returnedCounter = readFirstLine(responseBytes); - // RR : response should increment every other request + // Counter should increment every other request String expectedCounter = String.valueOf(i / 2); - assertEquals(expectedCounter,returnedCounter); + Assert.assertEquals(expectedCounter, returnedCounter); } } @Test public void testStickySessionsBalancer() throws Exception { - setStickySessions(true); + stickySessions = true; startBalancer(CounterServlet.class); - for (int i = 0; i < 10; i++) { - byte[] responseBytes = sendRequestToBalancer("/"); + byte[] responseBytes = sendRequestToBalancer("/stickySessions"); String returnedCounter = readFirstLine(responseBytes); - // RR : response should increment on each request + // Counter should increment every request String expectedCounter = String.valueOf(i); - assertEquals(expectedCounter,returnedCounter); + Assert.assertEquals(expectedCounter, returnedCounter); } } @Test public void testProxyPassReverse() throws Exception { - setStickySessions(false); + stickySessions = false; startBalancer(RelocationServlet.class); - - byte[] responseBytes = sendRequestToBalancer("index.html"); + byte[] responseBytes = sendRequestToBalancer("/index.html"); String msg = readFirstLine(responseBytes); - assertEquals("success",msg); + Assert.assertEquals("success", msg); } private String readFirstLine(byte[] responseBytes) throws IOException @@ -87,17 +166,9 @@ public class BalancerServletTest extends AbstractBalancerServletTest return reader.readLine(); } - @SuppressWarnings("serial") public static final class CounterServlet extends HttpServlet { - - private int counter; - - @Override - public void init() throws ServletException - { - counter = 0; - } + private final AtomicInteger counter = new AtomicInteger(); @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException @@ -105,11 +176,10 @@ public class BalancerServletTest extends AbstractBalancerServletTest // Force session creation req.getSession(); resp.setContentType("text/plain"); - resp.getWriter().println(counter++); + resp.getWriter().print(counter.getAndIncrement()); } } - @SuppressWarnings("serial") public static final class RelocationServlet extends HttpServlet { @Override @@ -117,19 +187,14 @@ public class BalancerServletTest extends AbstractBalancerServletTest { if (req.getRequestURI().endsWith("/index.html")) { - resp.sendRedirect("http://localhost:" + req.getLocalPort() + req.getContextPath() + req.getServletPath() + "/other.html?secret=pipo%20molo"); - return; - } - resp.setContentType("text/plain"); - if ("pipo molo".equals(req.getParameter("secret"))) - { - resp.getWriter().println("success"); + resp.sendRedirect("http://localhost:" + req.getLocalPort() + req.getContextPath() + req.getServletPath() + "/other.html?secret=pipo+molo"); } else { - resp.getWriter().println("failure"); + resp.setContentType("text/plain"); + if ("pipo molo".equals(req.getParameter("secret"))) + resp.getWriter().println("success"); } } } - } diff --git a/jetty-proxy/src/test/java/org/eclipse/jetty/proxy/ConnectHandlerSSLTest.java b/jetty-proxy/src/test/java/org/eclipse/jetty/proxy/ConnectHandlerSSLTest.java index fe97b8fb06e..d6b8aaf0d10 100644 --- a/jetty-proxy/src/test/java/org/eclipse/jetty/proxy/ConnectHandlerSSLTest.java +++ b/jetty-proxy/src/test/java/org/eclipse/jetty/proxy/ConnectHandlerSSLTest.java @@ -16,40 +16,9 @@ // ======================================================================== // -package org.eclipse.jetty.server.handler; +package org.eclipse.jetty.proxy; -import java.io.BufferedReader; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.OutputStream; -import java.net.Socket; -import java.security.SecureRandom; -import java.security.cert.CertificateException; -import java.security.cert.X509Certificate; -import javax.net.ssl.SSLContext; -import javax.net.ssl.SSLSocket; -import javax.net.ssl.SSLSocketFactory; -import javax.net.ssl.TrustManager; -import javax.net.ssl.X509TrustManager; -import javax.servlet.ServletException; -import javax.servlet.ServletOutputStream; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import org.eclipse.jetty.server.Connector; -import org.eclipse.jetty.server.Request; -import org.eclipse.jetty.server.handler.AbstractHandler; -import org.eclipse.jetty.server.ssl.SslSelectChannelConnector; -import org.eclipse.jetty.toolchain.test.MavenTestingUtils; -import org.eclipse.jetty.util.ssl.SslContextFactory; -import org.junit.BeforeClass; import org.junit.Ignore; -import org.junit.Test; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; /** * @version $Revision$ $Date$ @@ -57,191 +26,191 @@ import static org.junit.Assert.assertFalse; @Ignore public class ConnectHandlerSSLTest extends AbstractConnectHandlerTest { - @BeforeClass - public static void init() throws Exception - { - SslSelectChannelConnector connector = new SslSelectChannelConnector(); - connector.setMaxIdleTime(3600000); // TODO remove - - String keyStorePath = MavenTestingUtils.getTestResourceFile("keystore").getAbsolutePath(); - SslContextFactory cf = connector.getSslContextFactory(); - cf.setKeyStorePath(keyStorePath); - cf.setKeyStorePassword("storepwd"); - cf.setKeyManagerPassword("keypwd"); - - startServer(connector, new ServerHandler()); - startProxy(); - } - - @Test - public void testGETRequest() throws Exception - { - String hostPort = "localhost:" + ((Connector.NetConnector)serverConnector).getLocalPort(); - String request = "" + - "CONNECT " + hostPort + " HTTP/1.1\r\n" + - "Host: " + hostPort + "\r\n" + - "\r\n"; - Socket socket = newSocket(); - socket.setSoTimeout(3600000); // TODO remove - try - { - OutputStream output = socket.getOutputStream(); - BufferedReader input = new BufferedReader(new InputStreamReader(socket.getInputStream())); - - output.write(request.getBytes("UTF-8")); - output.flush(); - - // Expect 200 OK from the CONNECT request - Response response = readResponse(input); - System.err.println(response); - assertEquals("200", response.getCode()); - - // Be sure the buffered input does not have anything buffered - assertFalse(input.ready()); - - // Upgrade the socket to SSL - SSLSocket sslSocket = wrapSocket(socket); - try - { - output = sslSocket.getOutputStream(); - input = new BufferedReader(new InputStreamReader(sslSocket.getInputStream())); - - request = - "GET /echo HTTP/1.1\r\n" + - "Host: " + hostPort + "\r\n" + - "\r\n"; - output.write(request.getBytes("UTF-8")); - output.flush(); - - response = readResponse(input); - assertEquals("200", response.getCode()); - assertEquals("GET /echo", response.getBody()); - } - finally - { - sslSocket.close(); - } - } - finally - { - socket.close(); - } - } - - @Test - public void testPOSTRequests() throws Exception - { - String hostPort = "localhost:" + ((Connector.NetConnector)serverConnector).getLocalPort(); - String request = "" + - "CONNECT " + hostPort + " HTTP/1.1\r\n" + - "Host: " + hostPort + "\r\n" + - "\r\n"; - Socket socket = newSocket(); - try - { - OutputStream output = socket.getOutputStream(); - BufferedReader input = new BufferedReader(new InputStreamReader(socket.getInputStream())); - - output.write(request.getBytes("UTF-8")); - output.flush(); - - // Expect 200 OK from the CONNECT request - Response response = readResponse(input); - assertEquals("200", response.getCode()); - - // Be sure the buffered input does not have anything buffered - assertFalse(input.ready()); - - // Upgrade the socket to SSL - SSLSocket sslSocket = wrapSocket(socket); - try - { - output = sslSocket.getOutputStream(); - input = new BufferedReader(new InputStreamReader(sslSocket.getInputStream())); - - for (int i = 0; i < 10; ++i) - { - request = "" + - "POST /echo?param=" + i + " HTTP/1.1\r\n" + - "Host: " + hostPort + "\r\n" + - "Content-Length: 5\r\n" + - "\r\n" + - "HELLO"; - output.write(request.getBytes("UTF-8")); - output.flush(); - - response = readResponse(input); - assertEquals("200", response.getCode()); - assertEquals("POST /echo?param=" + i + "\r\nHELLO", response.getBody()); - } - } - finally - { - sslSocket.close(); - } - } - finally - { - socket.close(); - } - } - - private SSLSocket wrapSocket(Socket socket) throws Exception - { - SSLContext sslContext = SSLContext.getInstance("SSLv3"); - sslContext.init(null, new TrustManager[]{new AlwaysTrustManager()}, new SecureRandom()); - SSLSocketFactory socketFactory = sslContext.getSocketFactory(); - SSLSocket sslSocket = (SSLSocket)socketFactory.createSocket(socket, socket.getInetAddress().getHostAddress(), socket.getPort(), true); - sslSocket.setUseClientMode(true); - sslSocket.startHandshake(); - return sslSocket; - } - - private class AlwaysTrustManager implements X509TrustManager - { - public void checkClientTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException - { - } - - public void checkServerTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException - { - } - - public X509Certificate[] getAcceptedIssuers() - { - return new X509Certificate[]{}; - } - } - - private static class ServerHandler extends AbstractHandler - { - public void handle(String target, Request request, HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws IOException, ServletException - { - request.setHandled(true); - - String uri = httpRequest.getRequestURI(); - if ("/echo".equals(uri)) - { - StringBuilder builder = new StringBuilder(); - builder.append(httpRequest.getMethod()).append(" ").append(uri); - if (httpRequest.getQueryString() != null) - builder.append("?").append(httpRequest.getQueryString()); - - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - InputStream input = httpRequest.getInputStream(); - int read = -1; - while ((read = input.read()) >= 0) - baos.write(read); - baos.close(); - - ServletOutputStream output = httpResponse.getOutputStream(); - output.println(builder.toString()); - output.write(baos.toByteArray()); - } - else - { - throw new ServletException(); - } - } - } +// @BeforeClass +// public static void init() throws Exception +// { +// SslSelectChannelConnector connector = new SslSelectChannelConnector(); +// connector.setMaxIdleTime(3600000); // TODO remove +// +// String keyStorePath = MavenTestingUtils.getTestResourceFile("keystore").getAbsolutePath(); +// SslContextFactory cf = connector.getSslContextFactory(); +// cf.setKeyStorePath(keyStorePath); +// cf.setKeyStorePassword("storepwd"); +// cf.setKeyManagerPassword("keypwd"); +// +// startServer(connector, new ServerHandler()); +// startProxy(); +// } +// +// @Test +// public void testGETRequest() throws Exception +// { +// String hostPort = "localhost:" + ((Connector.NetConnector)serverConnector).getLocalPort(); +// String request = "" + +// "CONNECT " + hostPort + " HTTP/1.1\r\n" + +// "Host: " + hostPort + "\r\n" + +// "\r\n"; +// Socket socket = newSocket(); +// socket.setSoTimeout(3600000); // TODO remove +// try +// { +// OutputStream output = socket.getOutputStream(); +// BufferedReader input = new BufferedReader(new InputStreamReader(socket.getInputStream())); +// +// output.write(request.getBytes("UTF-8")); +// output.flush(); +// +// // Expect 200 OK from the CONNECT request +// Response response = readResponse(input); +// System.err.println(response); +// assertEquals("200", response.getCode()); +// +// // Be sure the buffered input does not have anything buffered +// assertFalse(input.ready()); +// +// // Upgrade the socket to SSL +// SSLSocket sslSocket = wrapSocket(socket); +// try +// { +// output = sslSocket.getOutputStream(); +// input = new BufferedReader(new InputStreamReader(sslSocket.getInputStream())); +// +// request = +// "GET /echo HTTP/1.1\r\n" + +// "Host: " + hostPort + "\r\n" + +// "\r\n"; +// output.write(request.getBytes("UTF-8")); +// output.flush(); +// +// response = readResponse(input); +// assertEquals("200", response.getCode()); +// assertEquals("GET /echo", response.getBody()); +// } +// finally +// { +// sslSocket.close(); +// } +// } +// finally +// { +// socket.close(); +// } +// } +// +// @Test +// public void testPOSTRequests() throws Exception +// { +// String hostPort = "localhost:" + ((Connector.NetConnector)serverConnector).getLocalPort(); +// String request = "" + +// "CONNECT " + hostPort + " HTTP/1.1\r\n" + +// "Host: " + hostPort + "\r\n" + +// "\r\n"; +// Socket socket = newSocket(); +// try +// { +// OutputStream output = socket.getOutputStream(); +// BufferedReader input = new BufferedReader(new InputStreamReader(socket.getInputStream())); +// +// output.write(request.getBytes("UTF-8")); +// output.flush(); +// +// // Expect 200 OK from the CONNECT request +// Response response = readResponse(input); +// assertEquals("200", response.getCode()); +// +// // Be sure the buffered input does not have anything buffered +// assertFalse(input.ready()); +// +// // Upgrade the socket to SSL +// SSLSocket sslSocket = wrapSocket(socket); +// try +// { +// output = sslSocket.getOutputStream(); +// input = new BufferedReader(new InputStreamReader(sslSocket.getInputStream())); +// +// for (int i = 0; i < 10; ++i) +// { +// request = "" + +// "POST /echo?param=" + i + " HTTP/1.1\r\n" + +// "Host: " + hostPort + "\r\n" + +// "Content-Length: 5\r\n" + +// "\r\n" + +// "HELLO"; +// output.write(request.getBytes("UTF-8")); +// output.flush(); +// +// response = readResponse(input); +// assertEquals("200", response.getCode()); +// assertEquals("POST /echo?param=" + i + "\r\nHELLO", response.getBody()); +// } +// } +// finally +// { +// sslSocket.close(); +// } +// } +// finally +// { +// socket.close(); +// } +// } +// +// private SSLSocket wrapSocket(Socket socket) throws Exception +// { +// SSLContext sslContext = SSLContext.getInstance("SSLv3"); +// sslContext.init(null, new TrustManager[]{new AlwaysTrustManager()}, new SecureRandom()); +// SSLSocketFactory socketFactory = sslContext.getSocketFactory(); +// SSLSocket sslSocket = (SSLSocket)socketFactory.createSocket(socket, socket.getInetAddress().getHostAddress(), socket.getPort(), true); +// sslSocket.setUseClientMode(true); +// sslSocket.startHandshake(); +// return sslSocket; +// } +// +// private class AlwaysTrustManager implements X509TrustManager +// { +// public void checkClientTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException +// { +// } +// +// public void checkServerTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException +// { +// } +// +// public X509Certificate[] getAcceptedIssuers() +// { +// return new X509Certificate[]{}; +// } +// } +// +// private static class ServerHandler extends AbstractHandler +// { +// public void handle(String target, Request request, HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws IOException, ServletException +// { +// request.setHandled(true); +// +// String uri = httpRequest.getRequestURI(); +// if ("/echo".equals(uri)) +// { +// StringBuilder builder = new StringBuilder(); +// builder.append(httpRequest.getMethod()).append(" ").append(uri); +// if (httpRequest.getQueryString() != null) +// builder.append("?").append(httpRequest.getQueryString()); +// +// ByteArrayOutputStream baos = new ByteArrayOutputStream(); +// InputStream input = httpRequest.getInputStream(); +// int read = -1; +// while ((read = input.read()) >= 0) +// baos.write(read); +// baos.close(); +// +// ServletOutputStream output = httpResponse.getOutputStream(); +// output.println(builder.toString()); +// output.write(baos.toByteArray()); +// } +// else +// { +// throw new ServletException(); +// } +// } +// } } diff --git a/jetty-proxy/src/test/java/org/eclipse/jetty/proxy/ConnectHandlerTest.java b/jetty-proxy/src/test/java/org/eclipse/jetty/proxy/ConnectHandlerTest.java index afc95212e1c..0f2bb72df0c 100644 --- a/jetty-proxy/src/test/java/org/eclipse/jetty/proxy/ConnectHandlerTest.java +++ b/jetty-proxy/src/test/java/org/eclipse/jetty/proxy/ConnectHandlerTest.java @@ -16,670 +16,636 @@ // ======================================================================== // -package org.eclipse.jetty.server.handler; +package org.eclipse.jetty.proxy; -import static org.junit.Assert.*; -import static org.junit.Assume.*; - -import java.io.BufferedReader; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.OutputStream; -import java.net.InetAddress; -import java.net.Socket; -import java.nio.ByteBuffer; -import java.net.UnknownHostException; -import java.nio.channels.SocketChannel; -import java.util.concurrent.ConcurrentMap; - -import javax.servlet.ServletException; -import javax.servlet.ServletOutputStream; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import org.eclipse.jetty.io.EndPoint; -import org.eclipse.jetty.proxy.ConnectHandler; -import org.eclipse.jetty.server.Connector; -import org.eclipse.jetty.server.Request; -import org.eclipse.jetty.server.SelectChannelConnector; -import org.eclipse.jetty.server.handler.AbstractHandler; -import org.eclipse.jetty.toolchain.test.OS; -import org.junit.Assert; -import org.junit.BeforeClass; import org.junit.Ignore; -import org.junit.Test; -/** - * @version $Revision$ $Date$ - */ @Ignore public class ConnectHandlerTest extends AbstractConnectHandlerTest { - @BeforeClass - public static void init() throws Exception - { - startServer(new SelectChannelConnector(), new ServerHandler()); - startProxy(); - } - - @Test - public void testCONNECT() throws Exception - { - String hostPort = "localhost:" + ((Connector.NetConnector)serverConnector).getLocalPort(); - String request = "" + - "CONNECT " + hostPort + " HTTP/1.1\r\n" + - "Host: " + hostPort + "\r\n" + - "\r\n"; - Socket socket = newSocket(); - try - { - OutputStream output = socket.getOutputStream(); - BufferedReader input = new BufferedReader(new InputStreamReader(socket.getInputStream())); - - output.write(request.getBytes("UTF-8")); - output.flush(); - - // Expect 200 OK from the CONNECT request - Response response = readResponse(input); - assertEquals("200", response.getCode()); - } - finally - { - socket.close(); - } - } - - @Test - public void testCONNECTAndGET() throws Exception - { - String hostPort = "localhost:" + ((Connector.NetConnector)serverConnector).getLocalPort(); - String request = "" + - "CONNECT " + hostPort + " HTTP/1.1\r\n" + - "Host: " + hostPort + "\r\n" + - "\r\n"; - Socket socket = newSocket(); - socket.setSoTimeout(30000); - try - { - OutputStream output = socket.getOutputStream(); - BufferedReader input = new BufferedReader(new InputStreamReader(socket.getInputStream())); - - output.write(request.getBytes("UTF-8")); - output.flush(); - - // Expect 200 OK from the CONNECT request - Response response = readResponse(input); - assertEquals("200", response.getCode()); - - request = "" + - "GET /echo" + " HTTP/1.1\r\n" + - "Host: " + hostPort + "\r\n" + - "\r\n"; - output.write(request.getBytes("UTF-8")); - output.flush(); - - response = readResponse(input); - assertEquals("200", response.getCode()); - assertEquals("GET /echo", response.getBody()); - } - finally - { - socket.close(); - } - } - - - @Test - public void testCONNECTBadHostPort() throws Exception - { - String invalidHostname = "AMAZEBALLS_BADHOST.webtide.com"; - - try - { - InetAddress addr = InetAddress.getByName(invalidHostname); - StringBuilder err = new StringBuilder(); - err.append("DNS Hijacking detected: "); - err.append(invalidHostname).append(" should have not returned a valid IP address ["); - err.append(addr.getHostAddress()).append("]. "); - err.append("Fix your DNS provider to have this test pass."); - err.append("\nFor more info see https://en.wikipedia.org/wiki/DNS_hijacking"); - Assert.assertNull(err.toString(), addr); - } - catch (UnknownHostException e) - { - // expected path - } - - String hostPort = String.format("%s:%d",invalidHostname,serverConnector.getLocalPort()); - String request = "" + - "CONNECT " + hostPort + " HTTP/1.1\r\n" + - "Host: " + hostPort + "\r\n" + - "\r\n"; - Socket socket = newSocket(); - socket.setSoTimeout(30000); - try - { - OutputStream output = socket.getOutputStream(); - BufferedReader input = new BufferedReader(new InputStreamReader(socket.getInputStream())); - - output.write(request.getBytes("UTF-8")); - output.flush(); - - // Expect 500 OK from the CONNECT request - Response response = readResponse(input); - assertEquals("Response Code", "500", response.getCode()); - } - finally - { - socket.close(); - } - } - - @Test - public void testCONNECT10AndGET() throws Exception - { - String hostPort = "localhost:" + ((Connector.NetConnector)serverConnector).getLocalPort(); - String request = "" + - "CONNECT " + hostPort + " HTTP/1.0\r\n" + - "Host: " + hostPort + "\r\n" + - "\r\n"; - Socket socket = newSocket(); - try - { - OutputStream output = socket.getOutputStream(); - BufferedReader input = new BufferedReader(new InputStreamReader(socket.getInputStream())); - - output.write(request.getBytes("UTF-8")); - output.flush(); - - // Expect 200 OK from the CONNECT request - Response response = readResponse(input); - assertEquals("200", response.getCode()); - - request = "" + - "GET /echo" + " HTTP/1.1\r\n" + - "Host: " + hostPort + "\r\n" + - "\r\n"; - output.write(request.getBytes("UTF-8")); - output.flush(); - - response = readResponse(input); - assertEquals("200", response.getCode()); - assertEquals("GET /echo", response.getBody()); - } - finally - { - socket.close(); - } - } - - @Test - public void testCONNECTAndGETPipelined() throws Exception - { - String hostPort = "localhost:" + ((Connector.NetConnector)serverConnector).getLocalPort(); - String request = "" + - "CONNECT " + hostPort + " HTTP/1.1\r\n" + - "Host: " + hostPort + "\r\n" + - "\r\n" + - "GET /echo" + " HTTP/1.1\r\n" + - "Host: " + hostPort + "\r\n" + - "\r\n"; - Socket socket = newSocket(); - try - { - OutputStream output = socket.getOutputStream(); - BufferedReader input = new BufferedReader(new InputStreamReader(socket.getInputStream())); - - output.write(request.getBytes("UTF-8")); - output.flush(); - - // Expect 200 OK from the CONNECT request - Response response = readResponse(input); - assertEquals("200", response.getCode()); - - // The pipelined request must have gone up to the server as is - response = readResponse(input); - assertEquals("200", response.getCode()); - assertEquals("GET /echo", response.getBody()); - } - finally - { - socket.close(); - } - } - - @Test - public void testCONNECTAndMultipleGETs() throws Exception - { - String hostPort = "localhost:" + ((Connector.NetConnector)serverConnector).getLocalPort(); - String request = "" + - "CONNECT " + hostPort + " HTTP/1.1\r\n" + - "Host: " + hostPort + "\r\n" + - "\r\n"; - Socket socket = newSocket(); - try - { - OutputStream output = socket.getOutputStream(); - BufferedReader input = new BufferedReader(new InputStreamReader(socket.getInputStream())); - - output.write(request.getBytes("UTF-8")); - output.flush(); - - // Expect 200 OK from the CONNECT request - Response response = readResponse(input); - assertEquals("200", response.getCode()); - - for (int i = 0; i < 10; ++i) - { - request = "" + - "GET /echo" + " HTTP/1.1\r\n" + - "Host: " + hostPort + "\r\n" + - "\r\n"; - output.write(request.getBytes("UTF-8")); - output.flush(); - - response = readResponse(input); - assertEquals("200", response.getCode()); - assertEquals("GET /echo", response.getBody()); - } - } - finally - { - socket.close(); - } - } - - @Test - public void testCONNECTAndGETServerStop() throws Exception - { - String hostPort = "localhost:" + ((Connector.NetConnector)serverConnector).getLocalPort(); - String request = "" + - "CONNECT " + hostPort + " HTTP/1.1\r\n" + - "Host: " + hostPort + "\r\n" + - "\r\n"; - Socket socket = newSocket(); - try - { - OutputStream output = socket.getOutputStream(); - BufferedReader input = new BufferedReader(new InputStreamReader(socket.getInputStream())); - - output.write(request.getBytes("UTF-8")); - output.flush(); - - // Expect 200 OK from the CONNECT request - Response response = readResponse(input); - assertEquals("200", response.getCode()); - - request = "" + - "GET /echo HTTP/1.1\r\n" + - "Host: " + hostPort + "\r\n" + - "\r\n"; - output.write(request.getBytes("UTF-8")); - output.flush(); - - response = readResponse(input); - assertEquals("200", response.getCode()); - assertEquals("GET /echo", response.getBody()); - - // Idle server is shut down - stopServer(); - - int read = input.read(); - assertEquals(-1, read); - } - finally - { - socket.close(); - // Restart the server for the next test - server.start(); - } - } - - @Test - public void testCONNECTAndGETAndServerSideClose() throws Exception - { - String hostPort = "localhost:" + ((Connector.NetConnector)serverConnector).getLocalPort(); - String request = "" + - "CONNECT " + hostPort + " HTTP/1.1\r\n" + - "Host: " + hostPort + "\r\n" + - "\r\n"; - Socket socket = newSocket(); - try - { - OutputStream output = socket.getOutputStream(); - BufferedReader input = new BufferedReader(new InputStreamReader(socket.getInputStream())); - - output.write(request.getBytes("UTF-8")); - output.flush(); - - // Expect 200 OK from the CONNECT request - Response response = readResponse(input); - assertEquals("200", response.getCode()); - - request = "" + - "GET /close HTTP/1.1\r\n" + - "Host: " + hostPort + "\r\n" + - "\r\n"; - output.write(request.getBytes("UTF-8")); - output.flush(); - - int read = input.read(); - assertEquals(-1, read); - } - finally - { - socket.close(); - } - } - - @Test - public void testCONNECTAndPOSTAndGET() throws Exception - { - String hostPort = "localhost:" + ((Connector.NetConnector)serverConnector).getLocalPort(); - String request = "" + - "CONNECT " + hostPort + " HTTP/1.1\r\n" + - "Host: " + hostPort + "\r\n" + - "\r\n"; - Socket socket = newSocket(); - try - { - OutputStream output = socket.getOutputStream(); - BufferedReader input = new BufferedReader(new InputStreamReader(socket.getInputStream())); - - output.write(request.getBytes("UTF-8")); - output.flush(); - - // Expect 200 OK from the CONNECT request - Response response = readResponse(input); - assertEquals("200", response.getCode()); - - request = "" + - "POST /echo HTTP/1.1\r\n" + - "Host: " + hostPort + "\r\n" + - "Content-Length: 5\r\n" + - "\r\n" + - "HELLO"; - output.write(request.getBytes("UTF-8")); - output.flush(); - - response = readResponse(input); - assertEquals("200", response.getCode()); - assertEquals("POST /echo\r\nHELLO", response.getBody()); - - request = "" + - "GET /echo" + " HTTP/1.1\r\n" + - "Host: " + hostPort + "\r\n" + - "\r\n"; - output.write(request.getBytes("UTF-8")); - output.flush(); - - response = readResponse(input); - assertEquals("200", response.getCode()); - assertEquals("GET /echo", response.getBody()); - } - finally - { - socket.close(); - } - } - - @Test - public void testCONNECTAndPOSTWithBigBody() throws Exception - { - String hostPort = "localhost:" + ((Connector.NetConnector)serverConnector).getLocalPort(); - // fails under windows and occasionally on mac due to OOME - boolean stress = Boolean.getBoolean( "STRESS" ); - - if (!stress) - { - return; - } - - // Log.getLogger(ConnectHandler.class).setDebugEnabled(true); - String request = "" + - "CONNECT " + hostPort + " HTTP/1.1\r\n" + - "Host: " + hostPort + "\r\n" + - "\r\n"; - Socket socket = newSocket(); - try - { - OutputStream output = socket.getOutputStream(); - BufferedReader input = new BufferedReader(new InputStreamReader(socket.getInputStream())); - - output.write(request.getBytes("UTF-8")); - output.flush(); - - // Expect 200 OK from the CONNECT request - Response response = readResponse(input); - assertEquals("200", response.getCode()); - - StringBuilder body = new StringBuilder(); - String chunk = "0123456789ABCDEF"; - for (int i = 0; i < 1024 * 1024; ++i) - body.append(chunk); - - request = "" + - "POST /echo HTTP/1.1\r\n" + - "Host: " + hostPort + "\r\n" + - "Content-Length: " + body.length() + "\r\n" + - "\r\n" + - body; - output.write(request.getBytes("UTF-8")); - output.flush(); - - response = readResponse(input); - assertEquals("200", response.getCode()); - assertEquals("POST /echo\r\n" + body, response.getBody()); - } - finally - { - socket.close(); - } - } - - @Test - public void testCONNECTAndPOSTWithContext() throws Exception - { - final String contextKey = "contextKey"; - final String contextValue = "contextValue"; - - // Replace the default ProxyHandler with a subclass to test context information passing - stopProxy(); - proxy.setHandler(new ConnectHandler() - { - @Override - protected boolean handleAuthentication(HttpServletRequest request, HttpServletResponse response, String address) throws ServletException, IOException - { - request.setAttribute(contextKey, contextValue); - return super.handleAuthentication(request, response, address); - } - - @Override - protected SocketChannel connect(HttpServletRequest request, String host, int port) throws IOException - { - assertEquals(contextValue, request.getAttribute(contextKey)); - return super.connect(request, host, port); - } - - @Override - protected void prepareContext(HttpServletRequest request, ConcurrentMap context) - { - // Transfer data from the HTTP request to the connection context - assertEquals(contextValue, request.getAttribute(contextKey)); - context.put(contextKey, request.getAttribute(contextKey)); - } - - @Override - protected int read(EndPoint endPoint, ByteBuffer buffer, ConcurrentMap context) throws IOException - { - assertEquals(contextValue, context.get(contextKey)); - return super.read(endPoint, buffer, context); - } - - @Override - protected int write(EndPoint endPoint, ByteBuffer buffer, ConcurrentMap context) throws IOException - { - assertEquals(contextValue, context.get(contextKey)); - return super.write(endPoint, buffer, context); - } - }); - proxy.start(); - - String hostPort = "localhost:" + ((Connector.NetConnector)serverConnector).getLocalPort(); - String request = "" + - "CONNECT " + hostPort + " HTTP/1.1\r\n" + - "Host: " + hostPort + "\r\n" + - "\r\n"; - Socket socket = newSocket(); - try - { - OutputStream output = socket.getOutputStream(); - BufferedReader input = new BufferedReader(new InputStreamReader(socket.getInputStream())); - - output.write(request.getBytes("UTF-8")); - output.flush(); - - // Expect 200 OK from the CONNECT request - Response response = readResponse(input); - assertEquals("200", response.getCode()); - - String body = "0123456789ABCDEF"; - request = "" + - "POST /echo HTTP/1.1\r\n" + - "Host: " + hostPort + "\r\n" + - "Content-Length: " + body.length() + "\r\n" + - "\r\n" + - body; - output.write(request.getBytes("UTF-8")); - output.flush(); - - response = readResponse(input); - assertEquals("200", response.getCode()); - assertEquals("POST /echo\r\n" + body, response.getBody()); - } - finally - { - socket.close(); - } - } - - @Test - public void testCONNECTAndGETPipelinedAndOutputShutdown() throws Exception - { - // TODO needs to be further investigated - assumeTrue(!OS.IS_OSX); - - String hostPort = "localhost:" + ((Connector.NetConnector)serverConnector).getLocalPort(); - String request = "" + - "CONNECT " + hostPort + " HTTP/1.1\r\n" + - "Host: " + hostPort + "\r\n" + - "\r\n" + - "GET /echo" + " HTTP/1.1\r\n" + - "Host: " + hostPort + "\r\n" + - "\r\n"; - Socket socket = newSocket(); - try - { - OutputStream output = socket.getOutputStream(); - BufferedReader input = new BufferedReader(new InputStreamReader(socket.getInputStream())); - - output.write(request.getBytes("UTF-8")); - output.flush(); - socket.shutdownOutput(); - - // Expect 200 OK from the CONNECT request - Response response = readResponse(input); - assertEquals("200", response.getCode()); - - // The pipelined request must have gone up to the server as is - response = readResponse(input); - assertEquals("200", response.getCode()); - assertEquals("GET /echo", response.getBody()); - } - finally - { - socket.close(); - } - } - - @Test - public void testCONNECTAndGETAndOutputShutdown() throws Exception - { - // TODO needs to be further investigated - assumeTrue(!OS.IS_OSX); - - String hostPort = "localhost:" + ((Connector.NetConnector)serverConnector).getLocalPort(); - String request = "" + - "CONNECT " + hostPort + " HTTP/1.1\r\n" + - "Host: " + hostPort + "\r\n" + - "\r\n"; - Socket socket = newSocket(); - try - { - OutputStream output = socket.getOutputStream(); - BufferedReader input = new BufferedReader(new InputStreamReader(socket.getInputStream())); - - output.write(request.getBytes("UTF-8")); - output.flush(); - - // Expect 200 OK from the CONNECT request - Response response = readResponse(input); - assertEquals("200", response.getCode()); - - request = "" + - "GET /echo" + " HTTP/1.1\r\n" + - "Host: " + hostPort + "\r\n" + - "\r\n"; - output.write(request.getBytes("UTF-8")); - output.flush(); - socket.shutdownOutput(); - - // The pipelined request must have gone up to the server as is - response = readResponse(input); - assertEquals("200", response.getCode()); - assertEquals("GET /echo", response.getBody()); - } - finally - { - socket.close(); - } - } - - private static class ServerHandler extends AbstractHandler - { - public void handle(String target, Request request, HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws IOException, ServletException - { - request.setHandled(true); - - String uri = httpRequest.getRequestURI(); - if ("/echo".equals(uri)) - { - StringBuilder builder = new StringBuilder(); - builder.append(httpRequest.getMethod()).append(" ").append(uri); - if (httpRequest.getQueryString() != null) - builder.append("?").append(httpRequest.getQueryString()); - - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - InputStream input = httpRequest.getInputStream(); - int read = -1; - while ((read = input.read()) >= 0) - baos.write(read); - baos.close(); - - ServletOutputStream output = httpResponse.getOutputStream(); - output.println(builder.toString()); - output.write(baos.toByteArray()); - } - else if ("/close".equals(uri)) - { - request.getHttpChannel().getConnection().getEndPoint().close(); - } - else - { - throw new ServletException(); - } - } - } +// @BeforeClass +// public static void init() throws Exception +// { +// startServer(new SelectChannelConnector(), new ServerHandler()); +// startProxy(); +// } +// +// @Test +// public void testCONNECT() throws Exception +// { +// String hostPort = "localhost:" + ((Connector.NetConnector)serverConnector).getLocalPort(); +// String request = "" + +// "CONNECT " + hostPort + " HTTP/1.1\r\n" + +// "Host: " + hostPort + "\r\n" + +// "\r\n"; +// Socket socket = newSocket(); +// try +// { +// OutputStream output = socket.getOutputStream(); +// BufferedReader input = new BufferedReader(new InputStreamReader(socket.getInputStream())); +// +// output.write(request.getBytes("UTF-8")); +// output.flush(); +// +// // Expect 200 OK from the CONNECT request +// Response response = readResponse(input); +// assertEquals("200", response.getCode()); +// } +// finally +// { +// socket.close(); +// } +// } +// +// @Test +// public void testCONNECTAndGET() throws Exception +// { +// String hostPort = "localhost:" + ((Connector.NetConnector)serverConnector).getLocalPort(); +// String request = "" + +// "CONNECT " + hostPort + " HTTP/1.1\r\n" + +// "Host: " + hostPort + "\r\n" + +// "\r\n"; +// Socket socket = newSocket(); +// socket.setSoTimeout(30000); +// try +// { +// OutputStream output = socket.getOutputStream(); +// BufferedReader input = new BufferedReader(new InputStreamReader(socket.getInputStream())); +// +// output.write(request.getBytes("UTF-8")); +// output.flush(); +// +// // Expect 200 OK from the CONNECT request +// Response response = readResponse(input); +// assertEquals("200", response.getCode()); +// +// request = "" + +// "GET /echo" + " HTTP/1.1\r\n" + +// "Host: " + hostPort + "\r\n" + +// "\r\n"; +// output.write(request.getBytes("UTF-8")); +// output.flush(); +// +// response = readResponse(input); +// assertEquals("200", response.getCode()); +// assertEquals("GET /echo", response.getBody()); +// } +// finally +// { +// socket.close(); +// } +// } +// +// +// @Test +// public void testCONNECTBadHostPort() throws Exception +// { +// String invalidHostname = "AMAZEBALLS_BADHOST.webtide.com"; +// +// try +// { +// InetAddress addr = InetAddress.getByName(invalidHostname); +// StringBuilder err = new StringBuilder(); +// err.append("DNS Hijacking detected: "); +// err.append(invalidHostname).append(" should have not returned a valid IP address ["); +// err.append(addr.getHostAddress()).append("]. "); +// err.append("Fix your DNS provider to have this test pass."); +// err.append("\nFor more info see https://en.wikipedia.org/wiki/DNS_hijacking"); +// Assert.assertNull(err.toString(), addr); +// } +// catch (UnknownHostException e) +// { +// // expected path +// } +// +// String hostPort = String.format("%s:%d",invalidHostname,serverConnector.getLocalPort()); +// String request = "" + +// "CONNECT " + hostPort + " HTTP/1.1\r\n" + +// "Host: " + hostPort + "\r\n" + +// "\r\n"; +// Socket socket = newSocket(); +// socket.setSoTimeout(30000); +// try +// { +// OutputStream output = socket.getOutputStream(); +// BufferedReader input = new BufferedReader(new InputStreamReader(socket.getInputStream())); +// +// output.write(request.getBytes("UTF-8")); +// output.flush(); +// +// // Expect 500 OK from the CONNECT request +// Response response = readResponse(input); +// assertEquals("Response Code", "500", response.getCode()); +// } +// finally +// { +// socket.close(); +// } +// } +// +// @Test +// public void testCONNECT10AndGET() throws Exception +// { +// String hostPort = "localhost:" + ((Connector.NetConnector)serverConnector).getLocalPort(); +// String request = "" + +// "CONNECT " + hostPort + " HTTP/1.0\r\n" + +// "Host: " + hostPort + "\r\n" + +// "\r\n"; +// Socket socket = newSocket(); +// try +// { +// OutputStream output = socket.getOutputStream(); +// BufferedReader input = new BufferedReader(new InputStreamReader(socket.getInputStream())); +// +// output.write(request.getBytes("UTF-8")); +// output.flush(); +// +// // Expect 200 OK from the CONNECT request +// Response response = readResponse(input); +// assertEquals("200", response.getCode()); +// +// request = "" + +// "GET /echo" + " HTTP/1.1\r\n" + +// "Host: " + hostPort + "\r\n" + +// "\r\n"; +// output.write(request.getBytes("UTF-8")); +// output.flush(); +// +// response = readResponse(input); +// assertEquals("200", response.getCode()); +// assertEquals("GET /echo", response.getBody()); +// } +// finally +// { +// socket.close(); +// } +// } +// +// @Test +// public void testCONNECTAndGETPipelined() throws Exception +// { +// String hostPort = "localhost:" + ((Connector.NetConnector)serverConnector).getLocalPort(); +// String request = "" + +// "CONNECT " + hostPort + " HTTP/1.1\r\n" + +// "Host: " + hostPort + "\r\n" + +// "\r\n" + +// "GET /echo" + " HTTP/1.1\r\n" + +// "Host: " + hostPort + "\r\n" + +// "\r\n"; +// Socket socket = newSocket(); +// try +// { +// OutputStream output = socket.getOutputStream(); +// BufferedReader input = new BufferedReader(new InputStreamReader(socket.getInputStream())); +// +// output.write(request.getBytes("UTF-8")); +// output.flush(); +// +// // Expect 200 OK from the CONNECT request +// Response response = readResponse(input); +// assertEquals("200", response.getCode()); +// +// // The pipelined request must have gone up to the server as is +// response = readResponse(input); +// assertEquals("200", response.getCode()); +// assertEquals("GET /echo", response.getBody()); +// } +// finally +// { +// socket.close(); +// } +// } +// +// @Test +// public void testCONNECTAndMultipleGETs() throws Exception +// { +// String hostPort = "localhost:" + ((Connector.NetConnector)serverConnector).getLocalPort(); +// String request = "" + +// "CONNECT " + hostPort + " HTTP/1.1\r\n" + +// "Host: " + hostPort + "\r\n" + +// "\r\n"; +// Socket socket = newSocket(); +// try +// { +// OutputStream output = socket.getOutputStream(); +// BufferedReader input = new BufferedReader(new InputStreamReader(socket.getInputStream())); +// +// output.write(request.getBytes("UTF-8")); +// output.flush(); +// +// // Expect 200 OK from the CONNECT request +// Response response = readResponse(input); +// assertEquals("200", response.getCode()); +// +// for (int i = 0; i < 10; ++i) +// { +// request = "" + +// "GET /echo" + " HTTP/1.1\r\n" + +// "Host: " + hostPort + "\r\n" + +// "\r\n"; +// output.write(request.getBytes("UTF-8")); +// output.flush(); +// +// response = readResponse(input); +// assertEquals("200", response.getCode()); +// assertEquals("GET /echo", response.getBody()); +// } +// } +// finally +// { +// socket.close(); +// } +// } +// +// @Test +// public void testCONNECTAndGETServerStop() throws Exception +// { +// String hostPort = "localhost:" + ((Connector.NetConnector)serverConnector).getLocalPort(); +// String request = "" + +// "CONNECT " + hostPort + " HTTP/1.1\r\n" + +// "Host: " + hostPort + "\r\n" + +// "\r\n"; +// Socket socket = newSocket(); +// try +// { +// OutputStream output = socket.getOutputStream(); +// BufferedReader input = new BufferedReader(new InputStreamReader(socket.getInputStream())); +// +// output.write(request.getBytes("UTF-8")); +// output.flush(); +// +// // Expect 200 OK from the CONNECT request +// Response response = readResponse(input); +// assertEquals("200", response.getCode()); +// +// request = "" + +// "GET /echo HTTP/1.1\r\n" + +// "Host: " + hostPort + "\r\n" + +// "\r\n"; +// output.write(request.getBytes("UTF-8")); +// output.flush(); +// +// response = readResponse(input); +// assertEquals("200", response.getCode()); +// assertEquals("GET /echo", response.getBody()); +// +// // Idle server is shut down +// stopServer(); +// +// int read = input.read(); +// assertEquals(-1, read); +// } +// finally +// { +// socket.close(); +// // Restart the server for the next test +// server.start(); +// } +// } +// +// @Test +// public void testCONNECTAndGETAndServerSideClose() throws Exception +// { +// String hostPort = "localhost:" + ((Connector.NetConnector)serverConnector).getLocalPort(); +// String request = "" + +// "CONNECT " + hostPort + " HTTP/1.1\r\n" + +// "Host: " + hostPort + "\r\n" + +// "\r\n"; +// Socket socket = newSocket(); +// try +// { +// OutputStream output = socket.getOutputStream(); +// BufferedReader input = new BufferedReader(new InputStreamReader(socket.getInputStream())); +// +// output.write(request.getBytes("UTF-8")); +// output.flush(); +// +// // Expect 200 OK from the CONNECT request +// Response response = readResponse(input); +// assertEquals("200", response.getCode()); +// +// request = "" + +// "GET /close HTTP/1.1\r\n" + +// "Host: " + hostPort + "\r\n" + +// "\r\n"; +// output.write(request.getBytes("UTF-8")); +// output.flush(); +// +// int read = input.read(); +// assertEquals(-1, read); +// } +// finally +// { +// socket.close(); +// } +// } +// +// @Test +// public void testCONNECTAndPOSTAndGET() throws Exception +// { +// String hostPort = "localhost:" + ((Connector.NetConnector)serverConnector).getLocalPort(); +// String request = "" + +// "CONNECT " + hostPort + " HTTP/1.1\r\n" + +// "Host: " + hostPort + "\r\n" + +// "\r\n"; +// Socket socket = newSocket(); +// try +// { +// OutputStream output = socket.getOutputStream(); +// BufferedReader input = new BufferedReader(new InputStreamReader(socket.getInputStream())); +// +// output.write(request.getBytes("UTF-8")); +// output.flush(); +// +// // Expect 200 OK from the CONNECT request +// Response response = readResponse(input); +// assertEquals("200", response.getCode()); +// +// request = "" + +// "POST /echo HTTP/1.1\r\n" + +// "Host: " + hostPort + "\r\n" + +// "Content-Length: 5\r\n" + +// "\r\n" + +// "HELLO"; +// output.write(request.getBytes("UTF-8")); +// output.flush(); +// +// response = readResponse(input); +// assertEquals("200", response.getCode()); +// assertEquals("POST /echo\r\nHELLO", response.getBody()); +// +// request = "" + +// "GET /echo" + " HTTP/1.1\r\n" + +// "Host: " + hostPort + "\r\n" + +// "\r\n"; +// output.write(request.getBytes("UTF-8")); +// output.flush(); +// +// response = readResponse(input); +// assertEquals("200", response.getCode()); +// assertEquals("GET /echo", response.getBody()); +// } +// finally +// { +// socket.close(); +// } +// } +// +// @Test +// public void testCONNECTAndPOSTWithBigBody() throws Exception +// { +// String hostPort = "localhost:" + ((Connector.NetConnector)serverConnector).getLocalPort(); +// // fails under windows and occasionally on mac due to OOME +// boolean stress = Boolean.getBoolean( "STRESS" ); +// +// if (!stress) +// { +// return; +// } +// +// // Log.getLogger(ConnectHandler.class).setDebugEnabled(true); +// String request = "" + +// "CONNECT " + hostPort + " HTTP/1.1\r\n" + +// "Host: " + hostPort + "\r\n" + +// "\r\n"; +// Socket socket = newSocket(); +// try +// { +// OutputStream output = socket.getOutputStream(); +// BufferedReader input = new BufferedReader(new InputStreamReader(socket.getInputStream())); +// +// output.write(request.getBytes("UTF-8")); +// output.flush(); +// +// // Expect 200 OK from the CONNECT request +// Response response = readResponse(input); +// assertEquals("200", response.getCode()); +// +// StringBuilder body = new StringBuilder(); +// String chunk = "0123456789ABCDEF"; +// for (int i = 0; i < 1024 * 1024; ++i) +// body.append(chunk); +// +// request = "" + +// "POST /echo HTTP/1.1\r\n" + +// "Host: " + hostPort + "\r\n" + +// "Content-Length: " + body.length() + "\r\n" + +// "\r\n" + +// body; +// output.write(request.getBytes("UTF-8")); +// output.flush(); +// +// response = readResponse(input); +// assertEquals("200", response.getCode()); +// assertEquals("POST /echo\r\n" + body, response.getBody()); +// } +// finally +// { +// socket.close(); +// } +// } +// +// @Test +// public void testCONNECTAndPOSTWithContext() throws Exception +// { +// final String contextKey = "contextKey"; +// final String contextValue = "contextValue"; +// +// // Replace the default ProxyHandler with a subclass to test context information passing +// stopProxy(); +// proxy.setHandler(new ConnectHandler() +// { +// @Override +// protected boolean handleAuthentication(HttpServletRequest request, HttpServletResponse response, String address) throws ServletException, IOException +// { +// request.setAttribute(contextKey, contextValue); +// return super.handleAuthentication(request, response, address); +// } +// +// @Override +// protected SocketChannel connect(HttpServletRequest request, String host, int port) throws IOException +// { +// assertEquals(contextValue, request.getAttribute(contextKey)); +// return super.connect(request, host, port); +// } +// +// @Override +// protected void prepareContext(HttpServletRequest request, ConcurrentMap context) +// { +// // Transfer data from the HTTP request to the connection context +// assertEquals(contextValue, request.getAttribute(contextKey)); +// context.put(contextKey, request.getAttribute(contextKey)); +// } +// +// @Override +// protected int read(EndPoint endPoint, ByteBuffer buffer, ConcurrentMap context) throws IOException +// { +// assertEquals(contextValue, context.get(contextKey)); +// return super.read(endPoint, buffer, context); +// } +// +// @Override +// protected int write(EndPoint endPoint, ByteBuffer buffer, ConcurrentMap context) throws IOException +// { +// assertEquals(contextValue, context.get(contextKey)); +// return super.write(endPoint, buffer, context); +// } +// }); +// proxy.start(); +// +// String hostPort = "localhost:" + ((Connector.NetConnector)serverConnector).getLocalPort(); +// String request = "" + +// "CONNECT " + hostPort + " HTTP/1.1\r\n" + +// "Host: " + hostPort + "\r\n" + +// "\r\n"; +// Socket socket = newSocket(); +// try +// { +// OutputStream output = socket.getOutputStream(); +// BufferedReader input = new BufferedReader(new InputStreamReader(socket.getInputStream())); +// +// output.write(request.getBytes("UTF-8")); +// output.flush(); +// +// // Expect 200 OK from the CONNECT request +// Response response = readResponse(input); +// assertEquals("200", response.getCode()); +// +// String body = "0123456789ABCDEF"; +// request = "" + +// "POST /echo HTTP/1.1\r\n" + +// "Host: " + hostPort + "\r\n" + +// "Content-Length: " + body.length() + "\r\n" + +// "\r\n" + +// body; +// output.write(request.getBytes("UTF-8")); +// output.flush(); +// +// response = readResponse(input); +// assertEquals("200", response.getCode()); +// assertEquals("POST /echo\r\n" + body, response.getBody()); +// } +// finally +// { +// socket.close(); +// } +// } +// +// @Test +// public void testCONNECTAndGETPipelinedAndOutputShutdown() throws Exception +// { +// // TODO needs to be further investigated +// assumeTrue(!OS.IS_OSX); +// +// String hostPort = "localhost:" + ((Connector.NetConnector)serverConnector).getLocalPort(); +// String request = "" + +// "CONNECT " + hostPort + " HTTP/1.1\r\n" + +// "Host: " + hostPort + "\r\n" + +// "\r\n" + +// "GET /echo" + " HTTP/1.1\r\n" + +// "Host: " + hostPort + "\r\n" + +// "\r\n"; +// Socket socket = newSocket(); +// try +// { +// OutputStream output = socket.getOutputStream(); +// BufferedReader input = new BufferedReader(new InputStreamReader(socket.getInputStream())); +// +// output.write(request.getBytes("UTF-8")); +// output.flush(); +// socket.shutdownOutput(); +// +// // Expect 200 OK from the CONNECT request +// Response response = readResponse(input); +// assertEquals("200", response.getCode()); +// +// // The pipelined request must have gone up to the server as is +// response = readResponse(input); +// assertEquals("200", response.getCode()); +// assertEquals("GET /echo", response.getBody()); +// } +// finally +// { +// socket.close(); +// } +// } +// +// @Test +// public void testCONNECTAndGETAndOutputShutdown() throws Exception +// { +// // TODO needs to be further investigated +// assumeTrue(!OS.IS_OSX); +// +// String hostPort = "localhost:" + ((Connector.NetConnector)serverConnector).getLocalPort(); +// String request = "" + +// "CONNECT " + hostPort + " HTTP/1.1\r\n" + +// "Host: " + hostPort + "\r\n" + +// "\r\n"; +// Socket socket = newSocket(); +// try +// { +// OutputStream output = socket.getOutputStream(); +// BufferedReader input = new BufferedReader(new InputStreamReader(socket.getInputStream())); +// +// output.write(request.getBytes("UTF-8")); +// output.flush(); +// +// // Expect 200 OK from the CONNECT request +// Response response = readResponse(input); +// assertEquals("200", response.getCode()); +// +// request = "" + +// "GET /echo" + " HTTP/1.1\r\n" + +// "Host: " + hostPort + "\r\n" + +// "\r\n"; +// output.write(request.getBytes("UTF-8")); +// output.flush(); +// socket.shutdownOutput(); +// +// // The pipelined request must have gone up to the server as is +// response = readResponse(input); +// assertEquals("200", response.getCode()); +// assertEquals("GET /echo", response.getBody()); +// } +// finally +// { +// socket.close(); +// } +// } +// +// private static class ServerHandler extends AbstractHandler +// { +// public void handle(String target, Request request, HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws IOException, ServletException +// { +// request.setHandled(true); +// +// String uri = httpRequest.getRequestURI(); +// if ("/echo".equals(uri)) +// { +// StringBuilder builder = new StringBuilder(); +// builder.append(httpRequest.getMethod()).append(" ").append(uri); +// if (httpRequest.getQueryString() != null) +// builder.append("?").append(httpRequest.getQueryString()); +// +// ByteArrayOutputStream baos = new ByteArrayOutputStream(); +// InputStream input = httpRequest.getInputStream(); +// int read = -1; +// while ((read = input.read()) >= 0) +// baos.write(read); +// baos.close(); +// +// ServletOutputStream output = httpResponse.getOutputStream(); +// output.println(builder.toString()); +// output.write(baos.toByteArray()); +// } +// else if ("/close".equals(uri)) +// { +// request.getHttpChannel().getConnection().getEndPoint().close(); +// } +// else +// { +// throw new ServletException(); +// } +// } +// } } diff --git a/jetty-proxy/src/test/java/org/eclipse/jetty/proxy/ConnectHandlerUnitTest.java b/jetty-proxy/src/test/java/org/eclipse/jetty/proxy/ConnectHandlerUnitTest.java index e715b597dec..3bc16a53d41 100644 --- a/jetty-proxy/src/test/java/org/eclipse/jetty/proxy/ConnectHandlerUnitTest.java +++ b/jetty-proxy/src/test/java/org/eclipse/jetty/proxy/ConnectHandlerUnitTest.java @@ -18,60 +18,44 @@ package org.eclipse.jetty.proxy; -import static org.mockito.Mockito.when; -import static org.mockito.Matchers.*; -import static org.junit.Assert.*; -import static org.hamcrest.Matchers.*; - -import java.io.IOException; - -import org.eclipse.jetty.io.EndPoint; -import org.junit.Ignore; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.invocation.InvocationOnMock; -import org.mockito.runners.MockitoJUnitRunner; -import org.mockito.stubbing.Answer; - /* ------------------------------------------------------------ */ /** */ -@RunWith(MockitoJUnitRunner.class) +//@RunWith(MockitoJUnitRunner.class) public class ConnectHandlerUnitTest { - @Mock - private EndPoint endPoint; - - // TODO update for jetty-9 - @Test - @Ignore - public void testPartialWritesWithNonFullBuffer() throws IOException - { - /* - ConnectHandler connectHandler = new ConnectHandler(); - final byte[] bytes = "foo bar".getBytes(); - Buffer buffer = new ByteArrayBuffer(bytes.length * 2); - buffer.put(bytes); - when(endPoint.flush(buffer)).thenAnswer(new Answer() - { - public Object answer(InvocationOnMock invocation) - { - Object[] args = invocation.getArguments(); - Buffer buffer = (Buffer)args[0]; - int skip = bytes.length/2; - buffer.skip(skip); - return skip; - } - }); - when(endPoint.blockWritable(anyInt())).thenReturn(true); - - // method to test - connectHandler.write(endPoint,buffer,null); - - assertThat(buffer.length(),is(0)); - */ - } - - +// @Mock +// private EndPoint endPoint; +// +// // TODO update for jetty-9 +// @Test +// @Ignore +// public void testPartialWritesWithNonFullBuffer() throws IOException +// { +// /* +// ConnectHandler connectHandler = new ConnectHandler(); +// final byte[] bytes = "foo bar".getBytes(); +// Buffer buffer = new ByteArrayBuffer(bytes.length * 2); +// buffer.put(bytes); +// when(endPoint.flush(buffer)).thenAnswer(new Answer() +// { +// public Object answer(InvocationOnMock invocation) +// { +// Object[] args = invocation.getArguments(); +// Buffer buffer = (Buffer)args[0]; +// int skip = bytes.length/2; +// buffer.skip(skip); +// return skip; +// } +// }); +// when(endPoint.blockWritable(anyInt())).thenReturn(true); +// +// // method to test +// connectHandler.write(endPoint,buffer,null); +// +// assertThat(buffer.length(),is(0)); +// */ +// } +// +// } diff --git a/jetty-proxy/src/main/java/org/eclipse/jetty/proxy/ProxyServer.java b/jetty-proxy/src/test/java/org/eclipse/jetty/proxy/ProxyServer.java similarity index 71% rename from jetty-proxy/src/main/java/org/eclipse/jetty/proxy/ProxyServer.java rename to jetty-proxy/src/test/java/org/eclipse/jetty/proxy/ProxyServer.java index a804ec21d88..58430a56810 100644 --- a/jetty-proxy/src/main/java/org/eclipse/jetty/proxy/ProxyServer.java +++ b/jetty-proxy/src/test/java/org/eclipse/jetty/proxy/ProxyServer.java @@ -19,20 +19,18 @@ package org.eclipse.jetty.proxy; import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; import org.eclipse.jetty.server.handler.HandlerCollection; -import org.eclipse.jetty.proxy.ConnectHandler; -import org.eclipse.jetty.server.SelectChannelConnector; import org.eclipse.jetty.servlet.ServletContextHandler; import org.eclipse.jetty.servlet.ServletHolder; -import org.eclipse.jetty.proxy.ProxyServlet; public class ProxyServer { public static void main(String[] args) throws Exception { Server server = new Server(); - SelectChannelConnector connector = new SelectChannelConnector(server); - connector.setPort(8888); + ServerConnector connector = new ServerConnector(server); + connector.setPort(8080); server.addConnector(connector); HandlerCollection handlers = new HandlerCollection(); @@ -41,16 +39,16 @@ public class ProxyServer // Setup proxy servlet ServletContextHandler context = new ServletContextHandler(handlers, "/", ServletContextHandler.SESSIONS); ServletHolder proxyServlet = new ServletHolder(ProxyServlet.class); - proxyServlet.setInitParameter("whiteList", "google.com, www.eclipse.org, localhost"); - proxyServlet.setInitParameter("blackList", "google.com/calendar/*, www.eclipse.org/committers/"); +// proxyServlet.setInitParameter("whiteList", "google.com, www.eclipse.org, localhost"); +// proxyServlet.setInitParameter("blackList", "google.com/calendar/*, www.eclipse.org/committers/"); context.addServlet(proxyServlet, "/*"); - - + + // Setup proxy handler to handle CONNECT methods - ConnectHandler proxy = new ConnectHandler(); - proxy.setWhite(new String[]{"mail.google.com"}); - proxy.addWhite("www.google.com"); - handlers.addHandler(proxy); +// ConnectHandler proxy = new ConnectHandler(); +// proxy.setWhite(new String[]{"mail.google.com"}); +// proxy.addWhitelistHost("www.google.com"); +// handlers.addHandler(proxy); server.start(); } diff --git a/jetty-proxy/src/test/java/org/eclipse/jetty/proxy/ProxyServletTest.java b/jetty-proxy/src/test/java/org/eclipse/jetty/proxy/ProxyServletTest.java index a74ad3d15b7..c53b32baff2 100644 --- a/jetty-proxy/src/test/java/org/eclipse/jetty/proxy/ProxyServletTest.java +++ b/jetty-proxy/src/test/java/org/eclipse/jetty/proxy/ProxyServletTest.java @@ -16,104 +16,510 @@ // ======================================================================== // -package org.eclipse.jetty.servlets; +package org.eclipse.jetty.proxy; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; +import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; import java.io.PrintWriter; -import java.net.MalformedURLException; +import java.net.ConnectException; +import java.nio.ByteBuffer; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import javax.servlet.AsyncContext; +import javax.servlet.AsyncEvent; +import javax.servlet.AsyncListener; import javax.servlet.ServletException; -import javax.servlet.ServletOutputStream; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import junit.framework.Assert; -import org.eclipse.jetty.client.ContentExchange; + import org.eclipse.jetty.client.HttpClient; -import org.eclipse.jetty.client.HttpExchange; -import org.eclipse.jetty.http.HttpStatus; -import org.eclipse.jetty.http.HttpURI; -import org.eclipse.jetty.io.Buffer; -import org.eclipse.jetty.proxy.ProxyServlet; -import org.eclipse.jetty.server.Connector; +import org.eclipse.jetty.client.HttpContentResponse; +import org.eclipse.jetty.client.api.ContentResponse; +import org.eclipse.jetty.client.api.ProxyConfiguration; +import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.client.api.Response; +import org.eclipse.jetty.client.api.Result; +import org.eclipse.jetty.client.util.BufferingResponseListener; +import org.eclipse.jetty.client.util.BytesContentProvider; +import org.eclipse.jetty.http.HttpMethod; import org.eclipse.jetty.server.Server; -import org.eclipse.jetty.server.handler.HandlerCollection; -import org.eclipse.jetty.server.nio.SelectChannelConnector; +import org.eclipse.jetty.server.ServerConnector; import org.eclipse.jetty.servlet.ServletContextHandler; import org.eclipse.jetty.servlet.ServletHolder; +import org.eclipse.jetty.toolchain.test.AdvancedRunner; import org.eclipse.jetty.toolchain.test.MavenTestingUtils; +import org.eclipse.jetty.toolchain.test.TestTracker; +import org.eclipse.jetty.toolchain.test.annotation.Slow; +import org.eclipse.jetty.util.IO; +import org.hamcrest.Matchers; import org.junit.After; +import org.junit.Assert; +import org.junit.Rule; import org.junit.Test; +import org.junit.runner.RunWith; -import static org.junit.Assert.*; -import static org.hamcrest.Matchers.*; +import static java.nio.file.StandardOpenOption.CREATE; +@RunWith(AdvancedRunner.class) public class ProxyServletTest { - private Server _server; - private Connector _connector; - private HttpClient _client; + private static final String PROXIED_HEADER = "X-Proxied"; + @Rule + public final TestTracker tracker = new TestTracker(); + private HttpClient client; + private Server proxy; + private ServerConnector proxyConnector; + private ProxyServlet proxyServlet; + private Server server; + private ServerConnector serverConnector; - public void init(HttpServlet servlet) throws Exception + private void prepareProxy(ProxyServlet proxyServlet) throws Exception { - _server = new Server(); + proxy = new Server(); + proxyConnector = new ServerConnector(proxy); + proxy.addConnector(proxyConnector); - _connector = new SelectChannelConnector(); - _server.addConnector(_connector); - - HandlerCollection handlers = new HandlerCollection(); - _server.setHandler(handlers); - - ServletContextHandler proxyCtx = new ServletContextHandler(handlers, "/proxy", ServletContextHandler.NO_SESSIONS); - ServletHolder proxyServletHolder = new ServletHolder(new ProxyServlet() - { - @Override - protected HttpURI proxyHttpURI(String scheme, String serverName, int serverPort, String uri) throws MalformedURLException - { - // Proxies any call to "/proxy" to "/" - return new HttpURI(scheme + "://" + serverName + ":" + serverPort + uri.substring("/proxy".length())); - } - }); - proxyServletHolder.setInitParameter("timeout", String.valueOf(5 * 60 * 1000L)); + ServletContextHandler proxyCtx = new ServletContextHandler(proxy, "/", true, false); + this.proxyServlet = proxyServlet; + ServletHolder proxyServletHolder = new ServletHolder(proxyServlet); proxyCtx.addServlet(proxyServletHolder, "/*"); - ServletContextHandler appCtx = new ServletContextHandler(handlers, "/", ServletContextHandler.SESSIONS); + proxy.start(); + + client = new HttpClient(); + client.setProxyConfiguration(new ProxyConfiguration("localhost", proxyConnector.getLocalPort())); + client.start(); + } + + private void prepareServer(HttpServlet servlet) throws Exception + { + server = new Server(); + serverConnector = new ServerConnector(server); + server.addConnector(serverConnector); + + ServletContextHandler appCtx = new ServletContextHandler(server, "/", true, false); ServletHolder appServletHolder = new ServletHolder(servlet); appCtx.addServlet(appServletHolder, "/*"); - handlers.addHandler(proxyCtx); - handlers.addHandler(appCtx); - - _server.start(); - - _client = new HttpClient(); - _client.start(); + server.start(); } @After - public void destroy() throws Exception + public void disposeProxy() throws Exception { - if (_client != null) - _client.stop(); + client.stop(); + proxy.stop(); + } - if (_server != null) + @After + public void disposeServer() throws Exception + { + server.stop(); + } + + @Test + public void testProxyDown() throws Exception + { + prepareProxy(new ProxyServlet()); + prepareServer(new EmptyHttpServlet()); + + // Shutdown the proxy + proxy.stop(); + + try { - _server.stop(); - _server.join(); + client.newRequest("localhost", serverConnector.getLocalPort()) + .send() + .get(5, TimeUnit.SECONDS); + Assert.fail(); + } + catch (ExecutionException x) + { + Assert.assertThat(x.getCause(), Matchers.instanceOf(ConnectException.class)); } } @Test - public void testXForwardedHostHeader() throws Exception + public void testServerDown() throws Exception { - init(new HttpServlet() - { - private static final long serialVersionUID = 1L; + prepareProxy(new ProxyServlet()); + prepareServer(new EmptyHttpServlet()); + // Shutdown the server + int serverPort = serverConnector.getLocalPort(); + server.stop(); + + ContentResponse response = client.newRequest("localhost", serverPort) + .send() + .get(5, TimeUnit.SECONDS); + + Assert.assertEquals(502, response.getStatus()); + } + + @Test + public void testServerException() throws Exception + { + prepareProxy(new ProxyServlet()); + prepareServer(new HttpServlet() + { + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException + { + throw new ServletException(); + } + }); + + ContentResponse response = client.newRequest("localhost", serverConnector.getLocalPort()) + .send() + .get(5, TimeUnit.SECONDS); + + Assert.assertEquals(500, response.getStatus()); + } + + @Test + public void testProxyWithoutContent() throws Exception + { + prepareProxy(new ProxyServlet()); + prepareServer(new HttpServlet() + { + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException + { + if (req.getHeader("Via") != null) + resp.addHeader(PROXIED_HEADER, "true"); + } + }); + + ContentResponse response = client.newRequest("localhost", serverConnector.getLocalPort()) + .send() + .get(5, TimeUnit.SECONDS); + + Assert.assertEquals(200, response.getStatus()); + Assert.assertTrue(response.getHeaders().containsKey(PROXIED_HEADER)); + } + + @Test + public void testProxyWithResponseContent() throws Exception + { + prepareProxy(new ProxyServlet()); + final byte[] content = new byte[1024]; + Arrays.fill(content, (byte)'A'); + prepareServer(new HttpServlet() + { + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException + { + if (req.getHeader("Via") != null) + resp.addHeader(PROXIED_HEADER, "true"); + resp.getOutputStream().write(content); + } + }); + + // Request is for the target server + ContentResponse response = client.newRequest("localhost", serverConnector.getLocalPort()) + .send() + .get(5, TimeUnit.SECONDS); + + Assert.assertEquals(200, response.getStatus()); + Assert.assertTrue(response.getHeaders().containsKey(PROXIED_HEADER)); + Assert.assertArrayEquals(content, response.getContent()); + } + + @Test + public void testProxyWithRequestContentAndResponseContent() throws Exception + { + prepareProxy(new ProxyServlet()); + prepareServer(new HttpServlet() + { + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException + { + if (req.getHeader("Via") != null) + resp.addHeader(PROXIED_HEADER, "true"); + IO.copy(req.getInputStream(), resp.getOutputStream()); + } + }); + + byte[] content = new byte[1024]; + Arrays.fill(content, (byte)'A'); + ContentResponse response = client.newRequest("localhost", serverConnector.getLocalPort()) + .method(HttpMethod.POST) + .content(new BytesContentProvider(content)) + .send() + .get(5, TimeUnit.SECONDS); + + Assert.assertEquals(200, response.getStatus()); + Assert.assertTrue(response.getHeaders().containsKey(PROXIED_HEADER)); + Assert.assertArrayEquals(content, response.getContent()); + } + + @Test + public void testProxyWithBigRequestContentIgnored() throws Exception + { + prepareProxy(new ProxyServlet()); + prepareServer(new HttpServlet() + { + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException + { + if (req.getHeader("Via") != null) + resp.addHeader(PROXIED_HEADER, "true"); + } + }); + + byte[] content = new byte[128 * 1024]; + ContentResponse response = client.newRequest("localhost", serverConnector.getLocalPort()) + .method(HttpMethod.POST) + .content(new BytesContentProvider(content)) + .send() + .get(5, TimeUnit.SECONDS); + + Assert.assertEquals(200, response.getStatus()); + Assert.assertTrue(response.getHeaders().containsKey(PROXIED_HEADER)); + } + + @Test + public void testProxyWithBigRequestContentConsumed() throws Exception + { + prepareProxy(new ProxyServlet()); + prepareServer(new HttpServlet() + { + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException + { + if (req.getHeader("Via") != null) + resp.addHeader(PROXIED_HEADER, "true"); + InputStream input = req.getInputStream(); + while (true) + if (input.read() < 0) + break; + } + }); + + byte[] content = new byte[128 * 1024]; + ContentResponse response = client.newRequest("localhost", serverConnector.getLocalPort()) + .method(HttpMethod.POST) + .content(new BytesContentProvider(content)) + .send() + .get(5, TimeUnit.SECONDS); + + Assert.assertEquals(200, response.getStatus()); + Assert.assertTrue(response.getHeaders().containsKey(PROXIED_HEADER)); + } + + @Slow + @Test + public void testProxyWithBigResponseContentWithSlowReader() throws Exception + { + prepareProxy(new ProxyServlet()); + + // Create a 6 MiB file + final int length = 6 * 1024; + Path targetTestsDir = MavenTestingUtils.getTargetTestingDir().toPath(); + Files.createDirectories(targetTestsDir); + final Path temp = Files.createTempFile(targetTestsDir, "test_", null); + byte[] kb = new byte[1024]; + Arrays.fill(kb, (byte)'X'); + try (OutputStream output = Files.newOutputStream(temp, CREATE)) + { + for (int i = 0; i < length; ++i) + output.write(kb); + } + + prepareServer(new HttpServlet() + { + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException + { + try (InputStream input = Files.newInputStream(temp)) + { + IO.copy(input, response.getOutputStream()); + } + } + }); + + Request request = client.newRequest("localhost", serverConnector.getLocalPort()).path("/proxy/test"); + final CountDownLatch latch = new CountDownLatch(1); + request.send(new BufferingResponseListener(2 * length * 1024) + { + @Override + public void onContent(Response response, ByteBuffer content) + { + try + { + // Slow down the reader + TimeUnit.MILLISECONDS.sleep(5); + super.onContent(response, content); + } + catch (InterruptedException x) + { + response.abort(x); + } + } + + @Override + public void onComplete(Result result) + { + Assert.assertFalse(result.isFailed()); + Assert.assertEquals(200, result.getResponse().getStatus()); + Assert.assertEquals(length * 1024, getContent().length); + latch.countDown(); + } + }); + Assert.assertTrue(latch.await(30, TimeUnit.SECONDS)); + } + + @Test + public void testProxyWithQueryString() throws Exception + { + prepareProxy(new ProxyServlet()); + String query = "a=1&b=%E2%82%AC"; + prepareServer(new HttpServlet() + { + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException + { + resp.getOutputStream().print(req.getQueryString()); + } + }); + + ContentResponse response = client.newRequest("http://localhost:" + serverConnector.getLocalPort() + "/?" + query) + .send() + .get(5, TimeUnit.SECONDS); + Assert.assertEquals(200, response.getStatus()); + Assert.assertEquals(query, response.getContentAsString()); + } + + @Slow + @Test + public void testProxyLongPoll() throws Exception + { + prepareProxy(new ProxyServlet()); + final long timeout = 1000; + prepareServer(new HttpServlet() + { + @Override + protected void doGet(final HttpServletRequest request, final HttpServletResponse response) throws ServletException, IOException + { + if (!request.isAsyncStarted()) + { + final AsyncContext asyncContext = request.startAsync(); + asyncContext.setTimeout(timeout); + asyncContext.addListener(new AsyncListener() + { + @Override + public void onComplete(AsyncEvent event) throws IOException + { + } + + @Override + public void onTimeout(AsyncEvent event) throws IOException + { + if (request.getHeader("Via") != null) + response.addHeader(PROXIED_HEADER, "true"); + asyncContext.complete(); + } + + @Override + public void onError(AsyncEvent event) throws IOException + { + } + + @Override + public void onStartAsync(AsyncEvent event) throws IOException + { + } + }); + } + } + }); + + Response response = client.newRequest("localhost", serverConnector.getLocalPort()) + .send() + .get(2 * timeout, TimeUnit.MILLISECONDS); + Assert.assertEquals(200, response.getStatus()); + Assert.assertTrue(response.getHeaders().containsKey(PROXIED_HEADER)); + } + + @Slow + @Test + public void testProxyRequestExpired() throws Exception + { + prepareProxy(new ProxyServlet()); + final long timeout = 1000; + proxyServlet.setTimeout(timeout); + prepareServer(new HttpServlet() + { + @Override + protected void doGet(final HttpServletRequest request, final HttpServletResponse response) throws ServletException, IOException + { + if (request.getHeader("Via") != null) + response.addHeader(PROXIED_HEADER, "true"); + try + { + TimeUnit.MILLISECONDS.sleep(2 * timeout); + } + catch (InterruptedException x) + { + throw new ServletException(x); + } + } + }); + + Response response = client.newRequest("localhost", serverConnector.getLocalPort()) + .send() + .get(3 * timeout, TimeUnit.MILLISECONDS); + Assert.assertEquals(504, response.getStatus()); + Assert.assertFalse(response.getHeaders().containsKey(PROXIED_HEADER)); + } + + @Slow + @Test(expected = TimeoutException.class) + public void testClientRequestExpired() throws Exception + { + prepareProxy(new ProxyServlet()); + final long timeout = 1000; + proxyServlet.setTimeout(3 * timeout); + prepareServer(new HttpServlet() + { + @Override + protected void doGet(final HttpServletRequest request, final HttpServletResponse response) throws ServletException, IOException + { + if (request.getHeader("Via") != null) + response.addHeader(PROXIED_HEADER, "true"); + try + { + TimeUnit.MILLISECONDS.sleep(2 * timeout); + } + catch (InterruptedException x) + { + throw new ServletException(x); + } + } + }); + + client.newRequest("localhost", serverConnector.getLocalPort()) + .send() + .get(timeout, TimeUnit.MILLISECONDS); + Assert.fail(); + } + + @Test + public void testProxyXForwardedHostHeaderIsPresent() throws Exception + { + prepareProxy(new ProxyServlet()); + prepareServer(new HttpServlet() + { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { @@ -123,72 +529,231 @@ public class ProxyServletTest } }); - String url = "http://localhost:" + _connector.getLocalPort() + "/proxy/test"; - ContentExchange exchange = new ContentExchange(); - exchange.setURL(url); - _client.send(exchange); - exchange.waitForDone(); - assertThat("Response expected to contain content of X-Forwarded-Host Header from the request",exchange.getResponseContent(),equalTo("localhost:" - + _connector.getLocalPort())); + ContentResponse response = client.GET("http://localhost:" + serverConnector.getLocalPort()).get(5, TimeUnit.SECONDS); + Assert.assertThat("Response expected to contain content of X-Forwarded-Host Header from the request", + response.getContentAsString(), + Matchers.equalTo("localhost:" + serverConnector.getLocalPort())); } @Test - public void testBigDownloadWithSlowReader() throws Exception + public void testProxyWhitelist() throws Exception { - // Create a 6 MiB file - final File file = File.createTempFile("test_", null, MavenTestingUtils.getTargetTestingDir()); - file.deleteOnExit(); - FileOutputStream fos = new FileOutputStream(file); - byte[] buffer = new byte[1024]; - Arrays.fill(buffer, (byte)'X'); - for (int i = 0; i < 6 * 1024; ++i) - fos.write(buffer); - fos.close(); + prepareProxy(new ProxyServlet()); + prepareServer(new EmptyHttpServlet()); + int port = serverConnector.getLocalPort(); + proxyServlet.getWhitelistHosts().add("127.0.0.1:" + port); - init(new HttpServlet() + // Try with the wrong host + ContentResponse response = client.newRequest("localhost", port) + .send() + .get(5, TimeUnit.SECONDS); + Assert.assertEquals(403, response.getStatus()); + + // Try again with the right host + response = client.newRequest("127.0.0.1", port) + .send() + .get(5, TimeUnit.SECONDS); + Assert.assertEquals(200, response.getStatus()); + } + + @Test + public void testProxyBlacklist() throws Exception + { + prepareProxy(new ProxyServlet()); + prepareServer(new EmptyHttpServlet()); + int port = serverConnector.getLocalPort(); + proxyServlet.getBlacklistHosts().add("localhost:" + port); + + // Try with the wrong host + ContentResponse response = client.newRequest("localhost", port) + .send() + .get(5, TimeUnit.SECONDS); + Assert.assertEquals(403, response.getStatus()); + + // Try again with the right host + response = client.newRequest("127.0.0.1", port) + .send() + .get(5, TimeUnit.SECONDS); + Assert.assertEquals(200, response.getStatus()); + } + + @Test + public void testClientExcludedHosts() throws Exception + { + prepareProxy(new ProxyServlet()); + prepareServer(new HttpServlet() { - private static final long serialVersionUID = 1L; - @Override - protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { - FileInputStream fis = new FileInputStream(file); - ServletOutputStream output = response.getOutputStream(); - byte[] buffer = new byte[1024]; - int read; - while ((read = fis.read(buffer)) >= 0) - output.write(buffer, 0, read); - fis.close(); + if (req.getHeader("Via") != null) + resp.addHeader(PROXIED_HEADER, "true"); + } + }); + int port = serverConnector.getLocalPort(); + client.getProxyConfiguration().getExcludedHosts().add("127.0.0.1:" + port); + + // Try with a proxied host + ContentResponse response = client.newRequest("localhost", port) + .send() + .get(5, TimeUnit.SECONDS); + Assert.assertEquals(200, response.getStatus()); + Assert.assertTrue(response.getHeaders().containsKey(PROXIED_HEADER)); + + // Try again with an excluded host + response = client.newRequest("127.0.0.1", port) + .send() + .get(5, TimeUnit.SECONDS); + Assert.assertEquals(200, response.getStatus()); + Assert.assertFalse(response.getHeaders().containsKey(PROXIED_HEADER)); + } + + @Test + public void testTransparentProxy() throws Exception + { + final String target = "/test"; + prepareServer(new HttpServlet() + { + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException + { + if (req.getHeader("Via") != null) + resp.addHeader(PROXIED_HEADER, "true"); + resp.setStatus(target.equals(req.getRequestURI()) ? 200 : 404); } }); - String url = "http://localhost:" + _connector.getLocalPort() + "/proxy/test"; - ContentExchange exchange = new ContentExchange(true) + String proxyTo = "http://localhost:" + serverConnector.getLocalPort(); + String prefix = "/proxy"; + ProxyServlet.Transparent proxyServlet = new ProxyServlet.Transparent(proxyTo, prefix); + prepareProxy(proxyServlet); + + // Make the request to the proxy, it should transparently forward to the server + ContentResponse response = client.newRequest("localhost", proxyConnector.getLocalPort()) + .path(prefix + target) + .send() + .get(5, TimeUnit.SECONDS); + Assert.assertEquals(200, response.getStatus()); + Assert.assertTrue(response.getHeaders().containsKey(PROXIED_HEADER)); + } + + @Test + public void testCachingProxy() throws Exception + { + final byte[] content = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0xA, 0xB, 0xC, 0xD, 0xE, 0xF}; + prepareServer(new HttpServlet() { @Override - protected void onResponseContent(Buffer content) throws IOException + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { - try + if (req.getHeader("Via") != null) + resp.addHeader(PROXIED_HEADER, "true"); + resp.getOutputStream().write(content); + } + }); + + // Don't do this at home: this example is not concurrent, not complete, + // it is only used for this test and to verify that ProxyServlet can be + // subclassed enough to write your own caching servlet + final String cacheHeader = "X-Cached"; + ProxyServlet proxyServlet = new ProxyServlet() + { + private Map cache = new HashMap<>(); + private Map temp = new HashMap<>(); + + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException + { + ContentResponse cachedResponse = cache.get(request.getRequestURI()); + if (cachedResponse != null) { - // Slow down the reader - TimeUnit.MILLISECONDS.sleep(10); - super.onResponseContent(content); + response.setStatus(cachedResponse.getStatus()); + // Should copy headers too, but keep it simple + response.addHeader(cacheHeader, "true"); + response.getOutputStream().write(cachedResponse.getContent()); } - catch (InterruptedException x) + else { - throw (IOException)new IOException().initCause(x); + super.service(request, response); } } + + @Override + protected void onResponseContent(HttpServletRequest request, HttpServletResponse response, Response proxyResponse, byte[] buffer, int offset, int length) throws IOException + { + // Accumulate the response content + ByteArrayOutputStream baos = temp.get(request.getRequestURI()); + if (baos == null) + { + baos = new ByteArrayOutputStream(); + temp.put(request.getRequestURI(), baos); + } + baos.write(buffer, offset, length); + super.onResponseContent(request, response, proxyResponse, buffer, offset, length); + } + + @Override + protected void onResponseSuccess(HttpServletRequest request, HttpServletResponse response, Response proxyResponse) + { + byte[] content = temp.remove(request.getRequestURI()).toByteArray(); + ContentResponse cached = new HttpContentResponse(proxyResponse, content, null); + cache.put(request.getRequestURI(), cached); + super.onResponseSuccess(request, response, proxyResponse); + } }; - exchange.setURL(url); - long start = System.nanoTime(); - _client.send(exchange); - Assert.assertEquals(HttpExchange.STATUS_COMPLETED, exchange.waitForDone()); - long elapsed = System.nanoTime() - start; - Assert.assertEquals(HttpStatus.OK_200, exchange.getResponseStatus()); - Assert.assertEquals(file.length(), exchange.getResponseContentBytes().length); - long millis = TimeUnit.NANOSECONDS.toMillis(elapsed); - long rate = file.length() / 1024 * 1000 / millis; - System.out.printf("download rate = %d KiB/s%n", rate); + prepareProxy(proxyServlet); + + // First request + ContentResponse response = client.newRequest("localhost", serverConnector.getLocalPort()) + .send() + .get(5, TimeUnit.SECONDS); + Assert.assertEquals(200, response.getStatus()); + Assert.assertTrue(response.getHeaders().containsKey(PROXIED_HEADER)); + Assert.assertArrayEquals(content, response.getContent()); + + // Second request should be cached + response = client.newRequest("localhost", serverConnector.getLocalPort()) + .send() + .get(5, TimeUnit.SECONDS); + Assert.assertEquals(200, response.getStatus()); + Assert.assertTrue(response.getHeaders().containsKey(cacheHeader)); + Assert.assertArrayEquals(content, response.getContent()); + } + + @Test + public void testRedirectsAreProxied() throws Exception + { + prepareProxy(new ProxyServlet()); + prepareServer(new HttpServlet() + { + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException + { + if (req.getHeader("Via") != null) + resp.addHeader(PROXIED_HEADER, "true"); + resp.sendRedirect("/"); + } + }); + + client.setFollowRedirects(false); + + ContentResponse response = client.newRequest("localhost", serverConnector.getLocalPort()) + .send() + .get(5, TimeUnit.SECONDS); + Assert.assertEquals(302, response.getStatus()); + Assert.assertTrue(response.getHeaders().containsKey(PROXIED_HEADER)); + } + + private static class EmptyHttpServlet extends HttpServlet + { + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException + { + } + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException + { + } } } diff --git a/jetty-proxy/src/test/resources/jetty-logging.properties b/jetty-proxy/src/test/resources/jetty-logging.properties new file mode 100644 index 00000000000..e0c669f1be3 --- /dev/null +++ b/jetty-proxy/src/test/resources/jetty-logging.properties @@ -0,0 +1,4 @@ +org.eclipse.jetty.util.log.class=org.eclipse.jetty.util.log.StdErrLog +#org.eclipse.jetty.LEVEL=DEBUG +#org.eclipse.jetty.client.LEVEL=DEBUG +#org.eclipse.jetty.proxy.LEVEL=DEBUG diff --git a/pom.xml b/pom.xml index 28c77397899..2ba11ed0dfe 100644 --- a/pom.xml +++ b/pom.xml @@ -413,6 +413,7 @@ jetty-nosql tests/test-sessions tests/test-webapps + jetty-proxy