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 ab074d21b01..4e1acf4d039 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; @@ -44,6 +43,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; @@ -127,6 +127,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() { @@ -351,7 +352,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.getConnectAddress()); Future result = new ConnectionCallback(destination, callback); selectorManager.connect(channel, result); @@ -596,6 +597,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..650f5d427a2 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,13 +364,13 @@ public class HttpConnection extends AbstractConnection implements Connection } } - public boolean abort(HttpExchange exchange, String reason) + public boolean abort(HttpExchange exchange, Throwable cause) { // We want the return value to be that of the response // because if the response has already successfully // arrived then we failed to abort the exchange - sender.abort(exchange, reason); - return receiver.abort(exchange, reason); + sender.abort(exchange, cause); + return receiver.abort(exchange, cause); } public void proceed(boolean proceed) 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..d6f09491387 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,9 +77,9 @@ public class HttpContentResponse implements ContentResponse } @Override - public boolean abort(String reason) + public boolean abort(Throwable cause) { - return response.abort(reason); + return response.abort(cause); } @Override 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..43b4a4e564e 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,10 +98,10 @@ public class HttpConversation implements Attributes attributes.clear(); } - public boolean abort(String reason) + public boolean abort(Throwable cause) { HttpExchange exchange = exchanges.peekLast(); - return exchange != null && exchange.abort(reason); + return exchange != null && exchange.abort(cause); } @Override diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpCookieStore.java b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpCookieStore.java index c30ec560fb0..43ef9b82a65 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpCookieStore.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpCookieStore.java @@ -41,23 +41,27 @@ public class HttpCookieStore implements CookieStore String host = destination.getHost(); int port = destination.getPort(); - String key = host + ":" + port + path; + String key = host + ":" + port; - // First lookup: direct hit - Queue cookies = allCookies.get(key); + // Root path lookup + Queue cookies = allCookies.get(key + "/"); if (cookies != null) accumulateCookies(destination, cookies, result); - // Second lookup: root path - if (!"/".equals(path)) + // Path lookup + String[] split = path.split("/"); + for (int i = 1; i < split.length; i++) { - key = host + ":" + port + "/"; + String segment = split[i]; + key += "/" + segment; cookies = allCookies.get(key); if (cookies != null) accumulateCookies(destination, cookies, result); + if (segment.length() > 0) + key += "/"; } - // Third lookup: parent domains + // Domain lookup int domains = host.split("\\.").length - 1; for (int i = 2; i <= domains; ++i) { 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 489826a477b..1fbad3653b0 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 getConnectAddress() + { + 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.isAborted()) + Throwable cause = request.getAbortCause(); + if (cause != null) { - abort(request, listeners, "Aborted"); + abort(request, listeners, cause); 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 cause) { for (RequestContext requestContext : requests) { @@ -343,7 +368,7 @@ public class HttpDestination implements Destination, AutoCloseable, Dumpable if (requests.remove(requestContext)) { // We were able to remove the pair, so it won't be processed - abort(request, requestContext.listeners, reason); + abort(request, requestContext.listeners, cause); LOG.debug("Aborted {} while queued", request); return true; } @@ -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 cause) { 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, cause); + responseNotifier.notifyComplete(listeners, new Result(request, cause, response, cause)); } @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..c27779090fd 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,10 +193,10 @@ public class HttpExchange return new AtomicMarkableReference<>(result, modified); } - public boolean abort(String reason) + public boolean abort(Throwable cause) { - LOG.debug("Aborting {} reason {}", this, reason); - boolean aborted = connection.abort(this, reason); + LOG.debug("Aborting {} reason {}", this, cause); + boolean aborted = connection.abort(this, cause); LOG.debug("Aborted {}: {}", this, aborted); return aborted; } 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 e8cdec6d1db..1f2b8c6a3b0 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 @@ -387,9 +387,9 @@ public class HttpReceiver implements HttpParser.ResponseHandler fail(new TimeoutException()); } - public boolean abort(HttpExchange exchange, String reason) + public boolean abort(HttpExchange exchange, Throwable cause) { - return fail(new HttpResponseException(reason == null ? "Response aborted" : reason, exchange.getResponse())); + return fail(cause); } 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 537c69966b3..815fc5aa2fa 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,17 +404,17 @@ public class HttpRequest implements Request } @Override - public boolean abort(String reason) + public boolean abort(Throwable cause) { - aborted = true; - if (client.provideDestination(getScheme(), getHost(), getPort()).abort(this, reason)) + aborted = Objects.requireNonNull(cause); + if (client.provideDestination(getScheme(), getHost(), getPort()).abort(this, cause)) return true; HttpConversation conversation = client.getConversation(getConversationID(), false); - return conversation != null && conversation.abort(reason); + return conversation != null && conversation.abort(cause); } @Override - public boolean isAborted() + public Throwable getAbortCause() { 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..1ebdb8e6bfd 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,9 +98,9 @@ public class HttpResponse implements Response } @Override - public boolean abort(String reason) + public boolean abort(Throwable cause) { - return request.abort(reason); + return request.abort(cause); } @Override 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 dabd10cbc12..6c0de4af979 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.isAborted()) + Throwable cause = request.getAbortCause(); + if (cause != null) { - exchange.abort(null); + exchange.abort(cause); } 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.isAborted()) + if (result == null && notCommitted && request.getAbortCause() == 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 cause) { 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(cause); } 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 c53b29e4be8..d263ed10afb 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.isAborted()) - redirect.abort(null); + Throwable cause = request.getAbortCause(); + if (cause != null) + redirect.abort(cause); } }); 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 1d36b9e8894..4fc95618a7d 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 cause the abort cause, must not be null * @return whether the abort succeeded */ - boolean abort(String reason); + boolean abort(Throwable cause); /** - * @return whether {@link #abort(String)} was called + * @return the abort cause passed to {@link #abort(Throwable)}, + * or null if this request has not been aborted */ - boolean isAborted(); + Throwable getAbortCause(); 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..5d42554099f 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 @@ -71,10 +71,10 @@ public interface Response /** * Attempts to abort the receive of this response. * - * @param reason the abort reason + * @param cause the abort cause, must not be null * @return whether the abort succeeded */ - boolean abort(String reason); + boolean abort(Throwable cause); public interface ResponseListener extends EventListener { diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/util/BasicAuthentication.java b/jetty-client/src/main/java/org/eclipse/jetty/client/util/BasicAuthentication.java index 1df75c3953e..bfcd825f12c 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/util/BasicAuthentication.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/util/BasicAuthentication.java @@ -18,9 +18,6 @@ package org.eclipse.jetty.client.util; -import java.io.UnsupportedEncodingException; -import java.nio.charset.UnsupportedCharsetException; - import org.eclipse.jetty.client.api.Authentication; import org.eclipse.jetty.client.api.ContentResponse; import org.eclipse.jetty.client.api.Request; @@ -60,15 +57,8 @@ public class BasicAuthentication implements Authentication public Result authenticate(Request request, ContentResponse response, String wwwAuthenticate, Attributes context) { String encoding = StringUtil.__ISO_8859_1; - try - { - String value = "Basic " + B64Code.encode(user + ":" + password, encoding); - return new BasicResult(request.getURI(), value); - } - catch (UnsupportedEncodingException x) - { - throw new UnsupportedCharsetException(encoding); - } + String value = "Basic " + B64Code.encode(user + ":" + password, encoding); + return new BasicResult(request.getURI(), value); } private static class BasicResult implements Result 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 cecd461cfea..5880606f510 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 @@ -64,7 +64,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/HttpClientContinueTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientContinueTest.java index bf787a94c77..18907d93447 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientContinueTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientContinueTest.java @@ -408,7 +408,7 @@ public class HttpClientContinueTest extends AbstractHttpClientServerTest @Override public void onBegin(Response response) { - response.abort(null); + response.abort(new Exception()); } @Override 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 ba3f6488557..7cb514f4c24 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.isAborted()); + Assert.assertNull(request.getAbortCause()); } @Slow @@ -208,7 +208,7 @@ public class HttpClientTimeoutTest extends AbstractHttpClientServerTest TimeUnit.MILLISECONDS.sleep(2 * timeout); - Assert.assertFalse(request.isAborted()); + Assert.assertNull(request.getAbortCause()); } } diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpCookieStoreTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpCookieStoreTest.java index 804a7656bda..846d2c2a3d9 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpCookieStoreTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpCookieStoreTest.java @@ -100,6 +100,21 @@ public class HttpCookieStoreTest Assert.assertEquals("1", cookie.getValue()); } + @Test + public void testCookieStoredWithPathIsRetrievedWithChildPath() throws Exception + { + CookieStore cookies = new HttpCookieStore(); + Destination destination = new HttpDestination(client, "http", "localhost", 80); + Assert.assertTrue(cookies.addCookie(destination, new HttpCookie("a", "1", null, "/path"))); + + List result = cookies.findCookies(destination, "/path/child"); + Assert.assertNotNull(result); + Assert.assertEquals(1, result.size()); + HttpCookie cookie = result.get(0); + Assert.assertEquals("a", cookie.getName()); + Assert.assertEquals("1", cookie.getValue()); + } + @Test public void testCookieStoredWithParentDomainIsRetrievedWithChildDomain() throws Exception { @@ -118,6 +133,19 @@ public class HttpCookieStoreTest Assert.assertEquals(2, result.size()); } + @Test + public void testCookieStoredWithChildDomainIsNotRetrievedWithParentDomain() throws Exception + { + CookieStore cookies = new HttpCookieStore(); + Destination childDestination = new HttpDestination(client, "http", "child.localhost.org", 80); + Assert.assertTrue(cookies.addCookie(childDestination, new HttpCookie("b", "2", null, "/"))); + + Destination parentDestination = new HttpDestination(client, "http", "localhost.org", 80); + List result = cookies.findCookies(parentDestination, "/path"); + Assert.assertNotNull(result); + Assert.assertEquals(0, result.size()); + } + @Test public void testExpiredCookieIsNotRetrieved() throws Exception { diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpRequestAbortTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpRequestAbortTest.java index 337db292b2f..350cfd1466c 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpRequestAbortTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpRequestAbortTest.java @@ -33,7 +33,6 @@ import javax.servlet.http.HttpServletResponse; import org.eclipse.jetty.client.api.ContentResponse; 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.ByteBufferContentProvider; import org.eclipse.jetty.server.HttpChannel; @@ -42,7 +41,6 @@ import org.eclipse.jetty.toolchain.test.annotation.Slow; import org.eclipse.jetty.util.IO; import org.eclipse.jetty.util.log.StdErrLog; import org.eclipse.jetty.util.ssl.SslContextFactory; -import org.hamcrest.Matchers; import org.junit.Assert; import org.junit.Test; @@ -58,6 +56,7 @@ public class HttpRequestAbortTest extends AbstractHttpClientServerTest { start(new EmptyServerHandler()); + final Throwable cause = new Exception(); final AtomicBoolean begin = new AtomicBoolean(); try { @@ -68,7 +67,7 @@ public class HttpRequestAbortTest extends AbstractHttpClientServerTest @Override public void onQueued(Request request) { - request.abort(null); + request.abort(cause); } @Override @@ -82,7 +81,7 @@ public class HttpRequestAbortTest extends AbstractHttpClientServerTest } catch (ExecutionException x) { - Assert.assertThat(x.getCause(), Matchers.instanceOf(HttpResponseException.class)); + Assert.assertSame(cause, x.getCause()); Assert.assertFalse(begin.get()); } } @@ -93,6 +92,7 @@ public class HttpRequestAbortTest extends AbstractHttpClientServerTest { start(new EmptyServerHandler()); + final Throwable cause = new Exception(); final CountDownLatch aborted = new CountDownLatch(1); final CountDownLatch headers = new CountDownLatch(1); try @@ -104,7 +104,7 @@ public class HttpRequestAbortTest extends AbstractHttpClientServerTest @Override public void onBegin(Request request) { - if (request.abort(null)) + if (request.abort(cause)) aborted.countDown(); } @@ -119,7 +119,7 @@ public class HttpRequestAbortTest extends AbstractHttpClientServerTest } catch (ExecutionException x) { - Assert.assertThat(x.getCause(), Matchers.instanceOf(HttpResponseException.class)); + Assert.assertSame(cause, x.getCause()); Assert.assertTrue(aborted.await(5, TimeUnit.SECONDS)); Assert.assertFalse(headers.await(1, TimeUnit.SECONDS)); } @@ -134,6 +134,7 @@ public class HttpRequestAbortTest extends AbstractHttpClientServerTest // A) the request is failed before the response arrived, then we get an ExecutionException // B) the request is failed after the response arrived, we get the 200 OK + final Throwable cause = new Exception(); final CountDownLatch aborted = new CountDownLatch(1); try { @@ -144,7 +145,7 @@ public class HttpRequestAbortTest extends AbstractHttpClientServerTest @Override public void onHeaders(Request request) { - if (request.abort(null)) + if (request.abort(cause)) aborted.countDown(); } }) @@ -154,7 +155,7 @@ public class HttpRequestAbortTest extends AbstractHttpClientServerTest } catch (ExecutionException x) { - Assert.assertThat(x.getCause(), Matchers.instanceOf(HttpResponseException.class)); + Assert.assertSame(cause, x.getCause()); Assert.assertTrue(aborted.await(5, TimeUnit.SECONDS)); } } @@ -181,12 +182,8 @@ public class HttpRequestAbortTest extends AbstractHttpClientServerTest } }); - // Test can behave in 3 ways: - // A) non-SSL, if the request is failed before the response arrived, then we get an ExecutionException - // B) non-SSL, if the request is failed after the response arrived, then we get the 500 - // C) SSL, the server tries to write the 500, but the connection is already closed, the client - // reads -1 with a pending exchange and fails the response with an EOFException StdErrLog.getLogger(HttpChannel.class).setHideStacks(true); + final Throwable cause = new Exception(); try { client.newRequest("localhost", connector.getLocalPort()) @@ -196,7 +193,7 @@ public class HttpRequestAbortTest extends AbstractHttpClientServerTest @Override public void onHeaders(Request request) { - request.abort(null); + request.abort(cause); } }) .content(new ByteBufferContentProvider(ByteBuffer.wrap(new byte[]{0}), ByteBuffer.wrap(new byte[]{1})) @@ -212,24 +209,15 @@ public class HttpRequestAbortTest extends AbstractHttpClientServerTest } catch (ExecutionException x) { - Throwable cause = x.getCause(); - if (cause instanceof EOFException) + Throwable abort = x.getCause(); + if (abort instanceof EOFException) { - // Server closed abruptly, behavior C + // Server closed abruptly + System.err.println("C"); } - else if (cause instanceof HttpRequestException) + else if (abort == cause) { - // Request failed, behavior B - HttpRequestException xx = (HttpRequestException)cause; - Request request = xx.getRequest(); - Assert.assertNotNull(request); - } - else if (cause instanceof HttpResponseException) - { - // Response failed, behavior A - HttpResponseException xx = (HttpResponseException)cause; - Response response = xx.getResponse(); - Assert.assertNotNull(response); + // Expected } else { @@ -270,7 +258,8 @@ public class HttpRequestAbortTest extends AbstractHttpClientServerTest TimeUnit.MILLISECONDS.sleep(delay); - request.abort(null); + Throwable cause = new Exception(); + request.abort(cause); try { @@ -278,7 +267,7 @@ public class HttpRequestAbortTest extends AbstractHttpClientServerTest } catch (ExecutionException x) { - Assert.assertThat(x.getCause(), Matchers.instanceOf(HttpResponseException.class)); + Assert.assertSame(cause, x.getCause()); } } @@ -296,6 +285,7 @@ public class HttpRequestAbortTest extends AbstractHttpClientServerTest } }); + final Throwable cause = new Exception(); client.getProtocolHandlers().clear(); client.getProtocolHandlers().add(new RedirectProtocolHandler(client) { @@ -304,7 +294,7 @@ public class HttpRequestAbortTest extends AbstractHttpClientServerTest { // Abort the request after the 3xx response but before issuing the next request if (!result.isFailed()) - result.getRequest().abort(null); + result.getRequest().abort(cause); super.onComplete(result); } }); @@ -320,7 +310,7 @@ public class HttpRequestAbortTest extends AbstractHttpClientServerTest } catch (ExecutionException x) { - Assert.assertThat(x.getCause(), Matchers.instanceOf(HttpResponseException.class)); + Assert.assertSame(cause, x.getCause()); } } } diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpResponseAbortTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpResponseAbortTest.java index 74efb0cd14c..ec97ac48528 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpResponseAbortTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpResponseAbortTest.java @@ -60,7 +60,7 @@ public class HttpResponseAbortTest extends AbstractHttpClientServerTest @Override public void onBegin(Response response) { - response.abort(null); + response.abort(new Exception()); } }) .send(new Response.CompleteListener() @@ -88,7 +88,7 @@ public class HttpResponseAbortTest extends AbstractHttpClientServerTest @Override public void onHeaders(Response response) { - response.abort(null); + response.abort(new Exception()); } }) .send(new Response.CompleteListener() @@ -136,7 +136,7 @@ public class HttpResponseAbortTest extends AbstractHttpClientServerTest @Override public void onContent(Response response, ByteBuffer content) { - response.abort(null); + response.abort(new Exception()); } }) .send(new Response.CompleteListener() diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/api/Usage.java b/jetty-client/src/test/java/org/eclipse/jetty/client/api/Usage.java index b53d51bf6c5..5a674a30537 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/api/Usage.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/api/Usage.java @@ -191,7 +191,7 @@ public class Usage } else { - response.abort(null); + response.abort(new Exception()); } } 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..cf64c1c1c41 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,160 +16,168 @@ // ======================================================================== // -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.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.Executor; +import javax.servlet.AsyncContext; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.http.HttpHeaderValue; import org.eclipse.jetty.http.HttpMethod; -import org.eclipse.jetty.io.AsyncConnection; +import org.eclipse.jetty.io.ByteBufferPool; +import org.eclipse.jetty.io.Connection; import org.eclipse.jetty.io.EndPoint; +import org.eclipse.jetty.io.MappedByteBufferPool; +import org.eclipse.jetty.io.SelectChannelEndPoint; +import org.eclipse.jetty.io.SelectorManager; import org.eclipse.jetty.server.Handler; +import org.eclipse.jetty.server.HttpConnection; 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.BufferUtil; +import org.eclipse.jetty.util.Callback; +import org.eclipse.jetty.util.TypeUtil; 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; +import org.eclipse.jetty.util.thread.TimerScheduler; /** - *

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(); + protected static final Logger LOG = Log.getLogger(ConnectHandler.class); + + private final Set whiteList = new HashSet<>(); + private final Set blackList = new HashSet<>(); + private Executor executor; + private Scheduler scheduler; + private ByteBufferPool bufferPool; + private SelectorManager selector; + private long connectTimeout = 15000; + private long idleTimeout = 30000; + private int bufferSize = 4096; 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) + public Executor getExecutor() { - setHandler(handler); - set(white, _white); - set(black, _black); + return executor; + } + + public void setExecutor(Executor executor) + { + this.executor = executor; + } + + public Scheduler getScheduler() + { + return scheduler; + } + + public void setScheduler(Scheduler scheduler) + { + this.scheduler = scheduler; + } + + public ByteBufferPool getByteBufferPool() + { + return bufferPool; + } + + public void setByteBufferPool(ByteBufferPool bufferPool) + { + this.bufferPool = bufferPool; } /** * @return the timeout, in milliseconds, to connect to the remote server */ - public int getConnectTimeout() + public long getConnectTimeout() { - return _connectTimeout; + return connectTimeout; } /** * @param connectTimeout the timeout, in milliseconds, to connect to the remote server */ - public void setConnectTimeout(int connectTimeout) + public void setConnectTimeout(long connectTimeout) { - _connectTimeout = connectTimeout; + this.connectTimeout = connectTimeout; } /** - * @return the timeout, in milliseconds, to write data to a peer + * @return the idle timeout, in milliseconds */ - public int getWriteTimeout() + public long 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(long idleTimeout) { - _writeTimeout = writeTimeout; + this.idleTimeout = idleTimeout; } - @Override - public void setServer(Server server) + public int getBufferSize() { - 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 bufferSize; } - /** - * @return the thread pool - */ - public ThreadPool getThreadPool() + public void setBufferSize(int bufferSize) { - 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; + this.bufferSize = bufferSize; } @Override protected void doStart() throws Exception { - super.doStart(); - - if (_threadPool == null) + if (executor == null) { - _threadPool = getServer().getThreadPool(); - _privateThreadPool = false; + setExecutor(getServer().getThreadPool()); } - if (_threadPool instanceof LifeCycle && !((LifeCycle)_threadPool).isRunning()) - ((LifeCycle)_threadPool).start(); - - // TODO _selectorManager.start(); + if (scheduler == null) + { + setScheduler(new TimerScheduler()); + addBean(getScheduler()); + } + if (bufferPool == null) + { + setByteBufferPool(new MappedByteBufferPool()); + addBean(getByteBufferPool()); + } + addBean(selector = newSelectorManager()); + selector.setConnectTimeout(getConnectTimeout()); + super.doStart(); } - @Override - protected void doStop() throws Exception + protected SelectorManager newSelectorManager() { - // TODO _selectorManager.stop(); - - ThreadPool threadPool = _threadPool; - if (_privateThreadPool && _threadPool != null && threadPool instanceof LifeCycle) - ((LifeCycle)threadPool).stop(); - - super.doStop(); + return new Manager(getExecutor(), getScheduler(), 1); } @Override @@ -177,15 +185,17 @@ public class ConnectHandler extends HandlerWrapper { if (HttpMethod.CONNECT.is(request.getMethod())) { - LOG.debug("CONNECT request for {}", request.getRequestURI()); + String serverAddress = request.getRequestURI(); + LOG.debug("CONNECT request for {}", serverAddress); try { - handleConnect(baseRequest, request, response, request.getRequestURI()); + handleConnect(baseRequest, request, response, serverAddress); } - catch(Exception e) + catch (Exception x) { - LOG.warn("ConnectHandler "+baseRequest.getUri()+" "+ e); - LOG.debug(e); + // TODO + LOG.warn("ConnectHandler " + baseRequest.getUri() + " " + x); + LOG.debug(x); } } else @@ -196,125 +206,118 @@ 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) { - boolean proceed = handleAuthentication(request, response, serverAddress); - if (!proceed) - return; - - String host = serverAddress; - int port = 80; - int colon = serverAddress.indexOf(':'); - if (colon > 0) - { - host = serverAddress.substring(0, colon); - port = Integer.parseInt(serverAddress.substring(colon + 1)); - } - - if (!validateDestination(host)) - { - LOG.info("ProxyHandler: Forbidden destination " + host); - response.setStatus(HttpServletResponse.SC_FORBIDDEN); - baseRequest.setHandled(true); - return; - } - - SocketChannel channel; - + jettyRequest.setHandled(true); try { - channel = connectToServer(request,host,port); - } - catch (SocketException se) - { - LOG.info("ConnectHandler: SocketException " + se.getMessage()); - response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); - baseRequest.setHandled(true); - return; - } - catch (SocketTimeoutException ste) - { - LOG.info("ConnectHandler: SocketTimeoutException" + ste.getMessage()); - response.setStatus(HttpServletResponse.SC_GATEWAY_TIMEOUT); - baseRequest.setHandled(true); - return; - } - catch (IOException ioe) - { - LOG.info("ConnectHandler: IOException" + ioe.getMessage()); - response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); - baseRequest.setHandled(true); - return; - } - - // Transfer unread data from old connection to new connection - // We need to copy the data to avoid races: - // 1. when this unread data is written and the server replies before the clientToProxy - // connection is installed (it is only installed after returning from this method) - // 2. when the client sends data before this unread data has been written. - - /* TODO - AbstractHttpConnection httpConnection = AbstractHttpConnection.getCurrentHttpChannel(); - ByteBuffer headerBuffer = ((HttpParser)httpConnection.getParser()).getHeaderBuffer(); - ByteBuffer bodyBuffer = ((HttpParser)httpConnection.getParser()).getBodyBuffer(); - int length = headerBuffer == null ? 0 : headerBuffer.length(); - length += bodyBuffer == null ? 0 : bodyBuffer.length(); - IndirectNIOBuffer buffer = null; - if (length > 0) - { - buffer = new IndirectNIOBuffer(length); - if (headerBuffer != null) + boolean proceed = handleAuthentication(request, response, serverAddress); + if (!proceed) { - buffer.put(headerBuffer); - headerBuffer.clear(); + LOG.debug("Missing proxy authentication"); + sendConnectResponse(request, response, HttpServletResponse.SC_PROXY_AUTHENTICATION_REQUIRED); + return; } - if (bodyBuffer != null) + + String host = serverAddress; + int port = 80; + int colon = serverAddress.indexOf(':'); + if (colon > 0) { - buffer.put(bodyBuffer); - bodyBuffer.clear(); + host = serverAddress.substring(0, colon); + port = Integer.parseInt(serverAddress.substring(colon + 1)); } + + if (!validateDestination(host, port)) + { + LOG.debug("Destination {}:{} forbidden", host, port); + sendConnectResponse(request, response, HttpServletResponse.SC_FORBIDDEN); + return; + } + + SocketChannel channel = SocketChannel.open(); + channel.socket().setTcpNoDelay(true); + channel.configureBlocking(false); + InetSocketAddress address = new InetSocketAddress(host, port); + channel.connect(address); + + AsyncContext asyncContext = request.startAsync(); + asyncContext.setTimeout(0); + + LOG.debug("Connecting to {}", address); + ConnectContext connectContext = new ConnectContext(request, response, asyncContext, HttpConnection.getCurrentConnection()); + selector.connect(channel, connectContext); + } + catch (Exception x) + { + onConnectFailure(request, response, null, x); + } + } + + protected void onConnectSuccess(ConnectContext connectContext, UpstreamConnection upstreamConnection) + { + HttpConnection httpConnection = connectContext.getHttpConnection(); + ByteBuffer requestBuffer = httpConnection.getRequestBuffer(); + ByteBuffer buffer = BufferUtil.EMPTY_BUFFER; + int remaining = requestBuffer.remaining(); + if (remaining > 0) + { + buffer = bufferPool.acquire(remaining, requestBuffer.isDirect()); + BufferUtil.flipToFill(buffer); + buffer.put(requestBuffer); + buffer.flip(); } - ConcurrentMap context = new ConcurrentHashMap(); + ConcurrentMap context = connectContext.getContext(); + HttpServletRequest request = connectContext.getRequest(); prepareContext(request, context); - ClientToProxyConnection clientToProxy = prepareConnections(context, channel, buffer); + EndPoint downstreamEndPoint = httpConnection.getEndPoint(); + DownstreamConnection downstreamConnection = newDownstreamConnection(downstreamEndPoint, context, buffer); + downstreamConnection.setInputBufferSize(getBufferSize()); - // CONNECT expects a 200 response - response.setStatus(HttpServletResponse.SC_OK); + upstreamConnection.setConnection(downstreamConnection); + downstreamConnection.setConnection(upstreamConnection); + LOG.debug("Connection setup completed: {}<->{}", downstreamConnection, upstreamConnection); - // Prevent close - baseRequest.getConnection().getGenerator().setPersistent(true); + HttpServletResponse response = connectContext.getResponse(); + sendConnectResponse(request, response, HttpServletResponse.SC_OK); - // Close to force last flush it so that the client receives it - response.getOutputStream().close(); - - upgradeConnection(request, response, clientToProxy); - */ + upgradeConnection(request, response, downstreamConnection); + connectContext.getAsyncContext().complete(); } - /* TODO - private ClientToProxyConnection prepareConnections(ConcurrentMap context, SocketChannel channel, ByteBuffer buffer) + protected void onConnectFailure(HttpServletRequest request, HttpServletResponse response, AsyncContext asyncContext, Throwable failure) { - AbstractHttpConnection httpConnection = AbstractHttpConnection.getCurrentHttpChannel(); - ProxyToServerConnection proxyToServer = newProxyToServerConnection(context, buffer); - ClientToProxyConnection clientToProxy = newClientToProxyConnection(context, channel, httpConnection.getEndPoint(), httpConnection.getTimeStamp()); - clientToProxy.setConnection(proxyToServer); - proxyToServer.setConnection(clientToProxy); - return clientToProxy; - return null; + LOG.debug("CONNECT failed", failure); + sendConnectResponse(request, response, HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + if (asyncContext != null) + asyncContext.complete(); + } + + private void sendConnectResponse(HttpServletRequest request, HttpServletResponse response, int statusCode) + { + try + { + response.setStatus(statusCode); + if (statusCode != HttpServletResponse.SC_OK) + response.setHeader(HttpHeader.CONNECTION.asString(), HttpHeaderValue.CLOSE.asString()); + response.getOutputStream().close(); + LOG.debug("CONNECT response sent {} {}", request.getProtocol(), response.getStatus()); + } + catch (IOException x) + { + // TODO: nothing we can do, close the connection + } } - */ /** *

Handles the authentication before setting up the tunnel to the remote server.

@@ -324,97 +327,35 @@ public class ConnectHandler extends HandlerWrapper * @param response the HTTP response * @param address the address of the remote server in the form {@code host:port}. * @return true to allow to connect to the remote host, false otherwise - * @throws ServletException to report a server error to the caller - * @throws IOException to report a server error to the caller */ - protected boolean handleAuthentication(HttpServletRequest request, HttpServletResponse response, String address) throws ServletException, IOException + protected boolean handleAuthentication(HttpServletRequest request, HttpServletResponse response, String address) { return true; } - /* TODO - protected ClientToProxyConnection newClientToProxyConnection(ConcurrentMap context, SocketChannel channel, EndPoint endPoint, long timeStamp) + protected DownstreamConnection newDownstreamConnection(EndPoint endPoint, ConcurrentMap context, ByteBuffer buffer) { - return new ClientToProxyConnection(context, channel, endPoint, timeStamp); + return new DownstreamConnection(endPoint, getExecutor(), getByteBufferPool(), context, this, buffer); } - protected ProxyToServerConnection newProxyToServerConnection(ConcurrentMap context, ByteBuffer buffer) + protected UpstreamConnection newUpstreamConnection(EndPoint endPoint, ConnectContext connectContext) { - return new ProxyToServerConnection(context, buffer); - } - */ - - // may return null - private SocketChannel connectToServer(HttpServletRequest request, String host, int port) throws IOException - { - SocketChannel channel = connect(request, host, port); - channel.configureBlocking(false); - return channel; - } - - /** - *

Establishes a connection to the remote server.

- * - * @param request the HTTP request that initiated the tunnel - * @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); - } - - try - { - // Connect to remote server - LOG.debug("Establishing connection to {}:{}", host, port); - channel.socket().setTcpNoDelay(true); - channel.socket().connect(new InetSocketAddress(host, port), getConnectTimeout()); - LOG.debug("Established connection to {}:{}", host, port); - return channel; - } - catch (IOException x) - { - LOG.debug("Failed to establish connection to " + host + ":" + port, x); - try - { - channel.close(); - } - catch (IOException xx) - { - LOG.ignore(xx); - } - throw x; - } + return new UpstreamConnection(endPoint, getExecutor(), getByteBufferPool(), this, connectContext); } protected void prepareContext(HttpServletRequest request, ConcurrentMap context) { } - private void upgradeConnection(HttpServletRequest request, HttpServletResponse response, AsyncConnection connection) throws IOException + private void upgradeConnection(HttpServletRequest request, HttpServletResponse response, Connection connection) { // Set the new connection as request attribute and change the status to 101 // so that Jetty understands that it has to upgrade the connection - request.setAttribute("org.eclipse.jetty.io.Connection", connection); + request.setAttribute(HttpConnection.UPGRADE_CONNECTION_ATTRIBUTE, connection); response.setStatus(HttpServletResponse.SC_SWITCHING_PROTOCOLS); LOG.debug("Upgraded connection to {}", connection); } - /* TODO - private void register(SocketChannel channel, ProxyToServerConnection proxyToServer) throws IOException - { - _selectorManager.register(channel, proxyToServer); - proxyToServer.waitReady(_connectTimeout); - } - */ - /** *

Reads (with non-blocking semantic) into the given {@code buffer} from the given {@code endPoint}.

* @@ -431,581 +372,54 @@ public class ConnectHandler extends HandlerWrapper } /** - *

Writes (with blocking semantic) the given buffer of data onto the given endPoint.

+ *

Writes (with non-blocking semantic) the given buffer of data onto the given endPoint.

* * @param endPoint the endPoint to write to * @param buffer the buffer to write * @param context the context information related to the connection - * @throws IOException if the buffer cannot be written - * @return the number of bytes written + * @param callback the completion callback to invoke */ - protected int write(EndPoint endPoint, ByteBuffer buffer, ConcurrentMap context) throws IOException + protected void write(EndPoint endPoint, ByteBuffer buffer, ConcurrentMap context, Callback callback) { - /* TODO - if (buffer == null) - return 0; - - int length = buffer.length(); - final StringBuilder debug = LOG.isDebugEnabled()?new StringBuilder():null; - int flushed = endPoint.flush(buffer); - if (debug!=null) - debug.append(flushed); - - // Loop until all written - while (buffer.length()>0 && !endPoint.isOutputShutdown()) - { - if (!endPoint.isBlocking()) - { - boolean ready = endPoint.blockWritable(getWriteTimeout()); - if (!ready) - throw new IOException("Write timeout"); - } - flushed = endPoint.flush(buffer); - if (debug!=null) - debug.append("+").append(flushed); - } - - LOG.debug("Written {}/{} bytes {}", debug, length, endPoint); - buffer.compact(); - return length; - */ - return -1; + endPoint.write(null, callback, buffer); } - /* TODO - private class Manager extends SelectorManager + public Set getWhiteListHosts() { - @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(_writeTimeout); - 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) - { - } + return whiteList; } - - - public class ProxyToServerConnection implements AsyncConnection + public Set getBlackListHosts() { - private final CountDownLatch _ready = new CountDownLatch(1); - private final ByteBuffer _buffer = new IndirectNIOBuffer(4096); - private final ConcurrentMap _context; - private volatile ByteBuffer _data; - private volatile ClientToProxyConnection _toClient; - private volatile long _timestamp; - private volatile AsyncEndPoint _endPoint; - - public ProxyToServerConnection(ConcurrentMap context, ByteBuffer data) - { - _context = context; - _data = data; - } - - @Override - public String toString() - { - StringBuilder builder = new StringBuilder("ProxyToServer"); - builder.append("(:").write(_endPoint.getLocalPort()); - builder.append("<=>:").write(_endPoint.getRemotePort()); - return builder.append(")").toString(); - } - - public AsyncConnection handle() throws IOException - { - LOG.debug("{}: begin reading from server", this); - try - { - writeData(); - - while (true) - { - int read = read(_endPoint, _buffer, _context); - - if (read == -1) - { - LOG.debug("{}: server closed connection {}", this, _endPoint); - - if (_endPoint.isOutputShutdown() || !_endPoint.isOpen()) - closeClient(); - else - _toClient.shutdownOutput(); - - break; - } - - if (read == 0) - break; - - LOG.debug("{}: read from server {} bytes {}", this, read, _endPoint); - int written = write(_toClient._endPoint, _buffer, _context); - LOG.debug("{}: written to {} {} bytes", this, _toClient, written); - } - return this; - } - catch (ClosedChannelException x) - { - LOG.debug(x); - throw x; - } - catch (IOException x) - { - LOG.warn(this + ": unexpected exception", x); - close(); - throw x; - } - catch (RuntimeException x) - { - LOG.warn(this + ": unexpected exception", x); - close(); - throw x; - } - finally - { - LOG.debug("{}: end reading from server", this); - } - } - - public void onInputShutdown() throws IOException - { - // TODO - } - - private void writeData() throws IOException - { - // This method is called from handle() and closeServer() - // which may happen concurrently (e.g. a client closing - // while reading from the server), so needs synchronization - synchronized (this) - { - if (_data != null) - { - try - { - int written = write(_endPoint, _data, _context); - LOG.debug("{}: written to server {} bytes", this, written); - } - finally - { - // Attempt once to write the data; if the write fails (for example - // because the connection is already closed), clear the data and - // give up to avoid to continue to write data to a closed connection - _data = null; - } - } - } - } - - public void setConnection(ClientToProxyConnection connection) - { - _toClient = connection; - } - - public long getTimeStamp() - { - return _timestamp; - } - - public void setTimeStamp(long timestamp) - { - _timestamp = timestamp; - } - - public void setEndPoint(AsyncEndPoint endpoint) - { - _endPoint = endpoint; - } - - public boolean isIdle() - { - return false; - } - - public boolean isSuspended() - { - return false; - } - - public void onClose() - { - } - - public void ready() - { - _ready.countDown(); - } - - public void waitReady(long timeout) throws IOException - { - try - { - _ready.await(timeout, TimeUnit.MILLISECONDS); - } - catch (final InterruptedException x) - { - throw new IOException() - {{ - initCause(x); - }}; - } - } - - public void closeClient() throws IOException - { - _toClient.closeClient(); - } - - public void closeServer() throws IOException - { - _endPoint.close(); - } - - public void close() - { - try - { - closeClient(); - } - catch (IOException x) - { - LOG.debug(this + ": unexpected exception closing the client", x); - } - - try - { - closeServer(); - } - catch (IOException x) - { - LOG.debug(this + ": unexpected exception closing the server", x); - } - } - - public void shutdownOutput() throws IOException - { - writeData(); - _endPoint.shutdownOutput(); - } - - public void onIdleExpired(long idleForMs) - { - try - { - shutdownOutput(); - } - catch(Exception e) - { - LOG.debug(e); - close(); - } - } - } - - public class ClientToProxyConnection implements AsyncConnection - { - private final ByteBuffer _buffer = new IndirectNIOBuffer(4096); - private final ConcurrentMap _context; - private final SocketChannel _channel; - private final EndPoint _endPoint; - private final long _timestamp; - private volatile ProxyToServerConnection _toServer; - private boolean _firstTime = true; - - public ClientToProxyConnection(ConcurrentMap context, SocketChannel channel, EndPoint endPoint, long timestamp) - { - _context = context; - _channel = channel; - _endPoint = endPoint; - _timestamp = timestamp; - } - - @Override - public String toString() - { - StringBuilder builder = new StringBuilder("ClientToProxy"); - builder.append("(:").write(_endPoint.getLocalPort()); - builder.append("<=>:").write(_endPoint.getRemotePort()); - return builder.append(")").toString(); - } - - public AsyncConnection handle() throws IOException - { - LOG.debug("{}: begin reading from client", this); - try - { - if (_firstTime) - { - _firstTime = false; - register(_channel, _toServer); - LOG.debug("{}: registered channel {} with connection {}", this, _channel, _toServer); - } - - while (true) - { - int read = read(_endPoint, _buffer, _context); - - if (read == -1) - { - LOG.debug("{}: client closed connection {}", this, _endPoint); - - if (_endPoint.isOutputShutdown() || !_endPoint.isOpen()) - closeServer(); - else - _toServer.shutdownOutput(); - - break; - } - - if (read == 0) - break; - - LOG.debug("{}: read from client {} bytes {}", this, read, _endPoint); - int written = write(_toServer._endPoint, _buffer, _context); - LOG.debug("{}: written to {} {} bytes", this, _toServer, written); - } - return this; - } - catch (ClosedChannelException x) - { - LOG.debug(x); - closeServer(); - throw x; - } - catch (IOException x) - { - LOG.warn(this + ": unexpected exception", x); - close(); - throw x; - } - catch (RuntimeException x) - { - LOG.warn(this + ": unexpected exception", x); - close(); - throw x; - } - finally - { - LOG.debug("{}: end reading from client", this); - } - } - - public void onInputShutdown() throws IOException - { - // TODO - } - - public long getTimeStamp() - { - return _timestamp; - } - - public boolean isIdle() - { - return false; - } - - public boolean isSuspended() - { - return false; - } - - public void onClose() - { - } - - public void setConnection(ProxyToServerConnection connection) - { - _toServer = connection; - } - - public void closeClient() throws IOException - { - _endPoint.close(); - } - - public void closeServer() throws IOException - { - _toServer.closeServer(); - } - - public void close() - { - try - { - closeClient(); - } - catch (IOException x) - { - LOG.debug(this + ": unexpected exception closing the client", x); - } - - try - { - closeServer(); - } - catch (IOException x) - { - LOG.debug(this + ": unexpected exception closing the server", x); - } - } - - public void shutdownOutput() throws IOException - { - _endPoint.shutdownOutput(); - } - - public void onIdleExpired(long idleForMs) - { - try - { - shutdownOutput(); - } - catch(Exception e) - { - LOG.debug(e); - close(); - } - } + return blackList; } /** - * Add a whitelist entry to an existing handler configuration + * Checks the given {@code host} and {@code port} against whitelist and blacklist. * - * @param entry new whitelist entry + * @param host the host to check + * @param port the port to check + * @return true if it is allowed to connect to the given host and port */ - public void addWhite(String entry) + public boolean validateDestination(String host, int port) { - add(entry, _white); - } - - /** - * Add a blacklist entry to an existing handler configuration - * - * @param entry new blacklist entry - */ - public void addBlack(String entry) - { - add(entry, _black); - } - - /** - * Re-initialize the whitelist of existing handler object - * - * @param entries array of whitelist entries - */ - public void setWhite(String[] entries) - { - set(entries, _white); - } - - /** - * Re-initialize the blacklist of existing handler object - * - * @param entries array of blacklist entries - */ - public void setBlack(String[] entries) - { - set(entries, _black); - } - - /** - * Helper method to process a list of new entries and replace - * the content of the specified host map - * - * @param entries new entries - * @param hostMap target host map - */ - protected void set(String[] entries, HostMap hostMap) - { - hostMap.clear(); - - if (entries != null && entries.length > 0) + String hostPort = host + ":" + port; + if (!whiteList.isEmpty()) { - for (String addrPath : entries) - { - add(addrPath, hostMap); - } - } - } - - /** - * Helper method to process the new entry and add it to - * the specified host map. - * - * @param entry new entry - * @param hostMap target host map - */ - private void add(String entry, HostMap hostMap) - { - if (entry != null && entry.length() > 0) - { - entry = entry.trim(); - if (hostMap.get(entry) == null) - { - hostMap.put(entry, entry); - } - } - } - - /** - * Check the request hostname against white- and blacklist. - * - * @param host hostname to check - * @return true if hostname is allowed to be proxied - */ - public boolean validateDestination(String host) - { - if (_white.size() > 0) - { - Object whiteObj = _white.getLazyMatches(host); - if (whiteObj == null) + if (!whiteList.contains(hostPort)) { + LOG.debug("Host {}:{} not whitelisted", host, port); return false; } } - - if (_black.size() > 0) + if (!blackList.isEmpty()) { - Object blackObj = _black.getLazyMatches(host); - if (blackObj != null) + if (blackList.contains(hostPort)) { + LOG.debug("Host {}:{} blacklisted", host, port); return false; } } - return true; } @@ -1013,11 +427,80 @@ public class ConnectHandler extends HandlerWrapper public void dump(Appendable out, String indent) throws IOException { dumpThis(out); - /* TODO - if (_privateThreadPool) - dump(out, indent, Arrays.asList(_threadPool, _selectorManager), TypeUtil.asList(getHandlers()), getBeans()); - else - dump(out, indent, Arrays.asList(_selectorManager), TypeUtil.asList(getHandlers()), getBeans()); - */ + dump(out, indent, getBeans(), TypeUtil.asList(getHandlers())); + } + + protected class Manager extends SelectorManager + { + + private Manager(Executor executor, Scheduler scheduler, int selectors) + { + super(executor, scheduler, selectors); + } + + @Override + protected EndPoint newEndPoint(SocketChannel channel, ManagedSelector selector, SelectionKey selectionKey) throws IOException + { + return new SelectChannelEndPoint(channel, selector, selectionKey, getScheduler(), getIdleTimeout()); + } + + @Override + public Connection newConnection(SocketChannel channel, EndPoint endpoint, Object attachment) throws IOException + { + ConnectHandler.LOG.debug("Connected to {}", channel.getRemoteAddress()); + ConnectContext connectContext = (ConnectContext)attachment; + UpstreamConnection connection = newUpstreamConnection(endpoint, connectContext); + connection.setInputBufferSize(getBufferSize()); + return connection; + } + + @Override + protected void connectionFailed(SocketChannel channel, Throwable ex, Object attachment) + { + ConnectContext connectContext = (ConnectContext)attachment; + onConnectFailure(connectContext.request, connectContext.response, connectContext.asyncContext, ex); + } + } + + protected static class ConnectContext + { + private final ConcurrentMap context = new ConcurrentHashMap<>(); + private final HttpServletRequest request; + private final HttpServletResponse response; + private final AsyncContext asyncContext; + private final HttpConnection httpConnection; + + public ConnectContext(HttpServletRequest request, HttpServletResponse response, AsyncContext asyncContext, HttpConnection httpConnection) + { + this.request = request; + this.response = response; + this.asyncContext = asyncContext; + this.httpConnection = httpConnection; + } + + public ConcurrentMap getContext() + { + return context; + } + + public HttpServletRequest getRequest() + { + return request; + } + + public HttpServletResponse getResponse() + { + return response; + } + + public AsyncContext getAsyncContext() + { + return asyncContext; + } + + public HttpConnection getHttpConnection() + { + return httpConnection; + } } } diff --git a/jetty-proxy/src/main/java/org/eclipse/jetty/proxy/DownstreamConnection.java b/jetty-proxy/src/main/java/org/eclipse/jetty/proxy/DownstreamConnection.java new file mode 100644 index 00000000000..24a241f183b --- /dev/null +++ b/jetty-proxy/src/main/java/org/eclipse/jetty/proxy/DownstreamConnection.java @@ -0,0 +1,62 @@ +// +// ======================================================================== +// 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.nio.ByteBuffer; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.Executor; + +import org.eclipse.jetty.io.ByteBufferPool; +import org.eclipse.jetty.io.EndPoint; +import org.eclipse.jetty.util.Callback; + +public class DownstreamConnection extends ProxyConnection +{ + private final ByteBuffer buffer; + + public DownstreamConnection(EndPoint endPoint, Executor executor, ByteBufferPool bufferPool, ConcurrentMap context, ConnectHandler connectHandler, ByteBuffer buffer) + { + super(endPoint, executor, bufferPool, context, connectHandler); + this.buffer = buffer; + } + + @Override + public void onOpen() + { + super.onOpen(); + final int remaining = buffer.remaining(); + write(buffer, new Callback() + { + @Override + public void completed(Void context) + { + LOG.debug("{} wrote initial {} bytes to server", DownstreamConnection.this, remaining); + fillInterested(); + } + + @Override + public void failed(Void context, Throwable x) + { + LOG.debug(this + " failed to write initial " + remaining + " bytes to server", x); + close(); + getConnection().close(); + } + }); + } +} diff --git a/jetty-proxy/src/main/java/org/eclipse/jetty/proxy/ProxyConnection.java b/jetty-proxy/src/main/java/org/eclipse/jetty/proxy/ProxyConnection.java new file mode 100644 index 00000000000..67a1037748a --- /dev/null +++ b/jetty-proxy/src/main/java/org/eclipse/jetty/proxy/ProxyConnection.java @@ -0,0 +1,171 @@ +// +// ======================================================================== +// 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.io.IOException; +import java.nio.ByteBuffer; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.Executor; + +import org.eclipse.jetty.io.AbstractConnection; +import org.eclipse.jetty.io.ByteBufferPool; +import org.eclipse.jetty.io.Connection; +import org.eclipse.jetty.io.EndPoint; +import org.eclipse.jetty.util.Callback; +import org.eclipse.jetty.util.ForkInvoker; +import org.eclipse.jetty.util.log.Logger; + +public abstract class ProxyConnection extends AbstractConnection +{ + protected static final Logger LOG = ConnectHandler.LOG; + private final ForkInvoker invoker = new ProxyForkInvoker(); + private final ByteBufferPool bufferPool; + private final ConcurrentMap context; + private final ConnectHandler connectHandler; + private Connection connection; + + protected ProxyConnection(EndPoint endp, Executor executor, ByteBufferPool bufferPool, ConcurrentMap context, ConnectHandler connectHandler) + { + super(endp, executor); + this.bufferPool = bufferPool; + this.context = context; + this.connectHandler = connectHandler; + } + + public ByteBufferPool getByteBufferPool() + { + return bufferPool; + } + + public ConcurrentMap getContext() + { + return context; + } + + public ConnectHandler getConnectHandler() + { + return connectHandler; + } + + public Connection getConnection() + { + return connection; + } + + public void setConnection(Connection connection) + { + this.connection = connection; + } + + @Override + public void onFillable() + { + ByteBuffer buffer = getByteBufferPool().acquire(getInputBufferSize(), true); + fill(buffer); + } + + private void fill(final ByteBuffer buffer) + { + try + { + final int filled = connectHandler.read(getEndPoint(), buffer, getContext()); + LOG.debug("{} filled {} bytes", this, filled); + if (filled > 0) + { + write(buffer, new Callback() + { + @Override + public void completed(Void context) + { + LOG.debug("{} wrote {} bytes", this, filled); + buffer.clear(); + invoker.invoke(buffer); + } + + @Override + public void failed(Void context, Throwable x) + { + LOG.debug(this + " failed to write " + filled + " bytes", x); + bufferPool.release(buffer); + connection.close(); + } + }); + } + else if (filled == 0) + { + bufferPool.release(buffer); + fillInterested(); + } + else + { + bufferPool.release(buffer); + connection.getEndPoint().shutdownOutput(); + } + } + catch (IOException x) + { + LOG.debug(this + " could not fill", x); + bufferPool.release(buffer); + close(); + connection.close(); + } + } + + protected void write(ByteBuffer buffer, Callback callback) + { + LOG.debug("{} writing {} bytes", this, buffer.remaining()); + connectHandler.write(getConnection().getEndPoint(), buffer, context, callback); + } + + @Override + public String toString() + { + return String.format("%s[l:%d<=>r:%d]", + super.toString(), + getEndPoint().getLocalAddress().getPort(), + getEndPoint().getRemoteAddress().getPort()); + } + + private class ProxyForkInvoker extends ForkInvoker + { + private ProxyForkInvoker() + { + super(4); + } + + @Override + public void fork(final ByteBuffer buffer) + { + getExecutor().execute(new Runnable() + { + @Override + public void run() + { + call(buffer); + } + }); + } + + @Override + public void call(ByteBuffer buffer) + { + fill(buffer); + } + } +} 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 f6bb52fc37d..b5656586148 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,134 +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.Iterator; import java.util.Locale; -import java.util.Map; -import java.util.StringTokenizer; - -import javax.servlet.Servlet; +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) { @@ -154,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 @@ -166,737 +188,522 @@ 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 given {@code host} and {@code port} against whitelist and blacklist. * - * @param list - * comma-separated list of new entries - * @param hostMap - * target host map + * @param host the host to check + * @param port the port to check + * @return true if it is allowed to be proxy to the given host and port */ - private void parseList(String list, HostMap hostMap) + public boolean validateDestination(String host, int port) { - if (list != null && list.length() > 0) + String hostPort = host + ":" + port; + if (!_whiteList.isEmpty()) { - int idx; - String entry; - - StringTokenizer entries = new StringTokenizer(list,","); - while (entries.hasMoreTokens()) + if (!_whiteList.contains(hostPort)) { - 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 - * @return true if request is allowed to be proxied - */ - public boolean validateDestination(String host, String path) - { - if (_white.size() > 0) - { - boolean match = false; - - Object whiteObj = _white.getLazyMatches(host); - if (whiteObj != null) - { - 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(Locale.ENGLISH); - 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(Locale.ENGLISH); - 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(Locale.ENGLISH); - - 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/main/java/org/eclipse/jetty/proxy/UpstreamConnection.java b/jetty-proxy/src/main/java/org/eclipse/jetty/proxy/UpstreamConnection.java new file mode 100644 index 00000000000..43717e40e7d --- /dev/null +++ b/jetty-proxy/src/main/java/org/eclipse/jetty/proxy/UpstreamConnection.java @@ -0,0 +1,43 @@ +// +// ======================================================================== +// 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.concurrent.Executor; + +import org.eclipse.jetty.io.ByteBufferPool; +import org.eclipse.jetty.io.EndPoint; + +public class UpstreamConnection extends ProxyConnection +{ + private ConnectHandler.ConnectContext connectContext; + + public UpstreamConnection(EndPoint endPoint, Executor executor, ByteBufferPool bufferPool, ConnectHandler connectHandler, ConnectHandler.ConnectContext connectContext) + { + super(endPoint, executor, bufferPool, connectContext.getContext(), connectHandler); + this.connectContext = connectContext; + } + + @Override + public void onOpen() + { + super.onOpen(); + getConnectHandler().onConnectSuccess(connectContext, this); + fillInterested(); + } +} 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 037dd025f5c..57297e3b73f 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,203 +16,64 @@ // ======================================================================== // -package org.eclipse.jetty.server.handler; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; +package org.eclipse.jetty.proxy; 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.Locale; -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.NetworkConnector; import org.eclipse.jetty.server.Server; -import org.junit.AfterClass; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.toolchain.test.http.SimpleHttpParser; +import org.eclipse.jetty.toolchain.test.http.SimpleHttpResponse; +import org.junit.After; -/** - * @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 Server server; + protected ServerConnector serverConnector; + protected Server proxy; + protected Connector proxyConnector; + protected ConnectHandler connectHandler; - 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 + protected void prepareProxy() throws Exception { proxy = new Server(); - proxyConnector = new SelectChannelConnector(); + proxyConnector = new ServerConnector(proxy); proxy.addConnector(proxyConnector); - proxy.setHandler(new ConnectHandler()); + connectHandler = new ConnectHandler(); + proxy.setHandler(connectHandler); proxy.start(); } - @AfterClass - public static void stop() throws Exception + @After + public void dispose() throws Exception { - stopProxy(); - stopServer(); + disposeServer(); + disposeProxy(); } - protected static void stopServer() throws Exception + protected void disposeServer() throws Exception { server.stop(); - server.join(); } - protected static void stopProxy() throws Exception + protected void disposeProxy() throws Exception { proxy.stop(); - proxy.join(); } - protected Response readResponse(BufferedReader reader) throws IOException + protected SimpleHttpResponse 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(Locale.ENGLISH), headerValue.toLowerCase(Locale.ENGLISH)); - } - - 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()); + return new SimpleHttpParser().readResponse(reader); } protected Socket newSocket() throws IOException { - Socket socket = new Socket("localhost", ((Connector.NetConnector)proxyConnector).getLocalPort()); - socket.setSoTimeout(10000); + Socket socket = new Socket("localhost", ((NetworkConnector)proxyConnector).getLocalPort()); + socket.setSoTimeout(5000); 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..53e9079da42 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,7 +16,7 @@ // ======================================================================== // -package org.eclipse.jetty.server.handler; +package org.eclipse.jetty.proxy; import java.io.BufferedReader; import java.io.ByteArrayOutputStream; @@ -25,65 +25,56 @@ 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.Server; +import org.eclipse.jetty.server.ServerConnector; 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.toolchain.test.http.SimpleHttpResponse; import org.eclipse.jetty.util.ssl.SslContextFactory; -import org.junit.BeforeClass; -import org.junit.Ignore; +import org.junit.Assert; +import org.junit.Before; import org.junit.Test; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; - -/** - * @version $Revision$ $Date$ - */ -@Ignore public class ConnectHandlerSSLTest extends AbstractConnectHandlerTest { - @BeforeClass - public static void init() throws Exception + private SslContextFactory sslContextFactory; + + @Before + public void prepare() 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(); + sslContextFactory = new SslContextFactory(); + String keyStorePath = MavenTestingUtils.getTestResourceFile("keystore.jks").getAbsolutePath(); + sslContextFactory.setKeyStorePath(keyStorePath); + sslContextFactory.setKeyStorePassword("storepwd"); + String trustStorePath = MavenTestingUtils.getTestResourceFile("truststore.jks").getAbsolutePath(); + sslContextFactory.setTrustStorePath(trustStorePath); + sslContextFactory.setTrustStorePassword("storepwd"); + server = new Server(); + serverConnector = new ServerConnector(server, sslContextFactory); + server.addConnector(serverConnector); + server.setHandler(new ServerHandler()); + server.start(); + prepareProxy(); } @Test public void testGETRequest() throws Exception { - String hostPort = "localhost:" + ((Connector.NetConnector)serverConnector).getLocalPort(); + String hostPort = "localhost:" + 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 + try (Socket socket = newSocket()) { OutputStream output = socket.getOutputStream(); BufferedReader input = new BufferedReader(new InputStreamReader(socket.getInputStream())); @@ -92,52 +83,41 @@ public class ConnectHandlerSSLTest extends AbstractConnectHandlerTest output.flush(); // Expect 200 OK from the CONNECT request - Response response = readResponse(input); - System.err.println(response); - assertEquals("200", response.getCode()); + SimpleHttpResponse response = readResponse(input); + Assert.assertEquals("200", response.getCode()); // Be sure the buffered input does not have anything buffered - assertFalse(input.ready()); + Assert.assertFalse(input.ready()); // Upgrade the socket to SSL - SSLSocket sslSocket = wrapSocket(socket); - try + try (SSLSocket sslSocket = wrapSocket(socket)) { output = sslSocket.getOutputStream(); input = new BufferedReader(new InputStreamReader(sslSocket.getInputStream())); request = "GET /echo HTTP/1.1\r\n" + - "Host: " + hostPort + "\r\n" + - "\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()); + Assert.assertEquals("200", response.getCode()); + Assert.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 hostPort = "localhost:" + serverConnector.getLocalPort(); String request = "" + "CONNECT " + hostPort + " HTTP/1.1\r\n" + "Host: " + hostPort + "\r\n" + "\r\n"; - Socket socket = newSocket(); - try + try (Socket socket = newSocket()) { OutputStream output = socket.getOutputStream(); BufferedReader input = new BufferedReader(new InputStreamReader(socket.getInputStream())); @@ -146,15 +126,14 @@ public class ConnectHandlerSSLTest extends AbstractConnectHandlerTest output.flush(); // Expect 200 OK from the CONNECT request - Response response = readResponse(input); - assertEquals("200", response.getCode()); + SimpleHttpResponse response = readResponse(input); + Assert.assertEquals("200", response.getCode()); // Be sure the buffered input does not have anything buffered - assertFalse(input.ready()); + Assert.assertFalse(input.ready()); // Upgrade the socket to SSL - SSLSocket sslSocket = wrapSocket(socket); - try + try (SSLSocket sslSocket = wrapSocket(socket)) { output = sslSocket.getOutputStream(); input = new BufferedReader(new InputStreamReader(sslSocket.getInputStream())); @@ -171,25 +150,16 @@ public class ConnectHandlerSSLTest extends AbstractConnectHandlerTest output.flush(); response = readResponse(input); - assertEquals("200", response.getCode()); - assertEquals("POST /echo?param=" + i + "\r\nHELLO", response.getBody()); + Assert.assertEquals("200", response.getCode()); + Assert.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()); + SSLContext sslContext = sslContextFactory.getSslContext(); SSLSocketFactory socketFactory = sslContext.getSocketFactory(); SSLSocket sslSocket = (SSLSocket)socketFactory.createSocket(socket, socket.getInetAddress().getHostAddress(), socket.getPort(), true); sslSocket.setUseClientMode(true); @@ -197,22 +167,6 @@ public class ConnectHandlerSSLTest extends AbstractConnectHandlerTest 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 @@ -229,7 +183,7 @@ public class ConnectHandlerSSLTest extends AbstractConnectHandlerTest ByteArrayOutputStream baos = new ByteArrayOutputStream(); InputStream input = httpRequest.getInputStream(); - int read = -1; + int read; while ((read = input.read()) >= 0) baos.write(read); baos.close(); 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..0fcd031115c 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,10 +16,7 @@ // ======================================================================== // -package org.eclipse.jetty.server.handler; - -import static org.junit.Assert.*; -import static org.junit.Assume.*; +package org.eclipse.jetty.proxy; import java.io.BufferedReader; import java.io.ByteArrayOutputStream; @@ -29,51 +26,49 @@ 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.nio.ByteBuffer; +import java.util.Locale; 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.Server; +import org.eclipse.jetty.server.ServerConnector; import org.eclipse.jetty.server.handler.AbstractHandler; -import org.eclipse.jetty.toolchain.test.OS; +import org.eclipse.jetty.toolchain.test.http.SimpleHttpResponse; +import org.eclipse.jetty.util.B64Code; +import org.eclipse.jetty.util.Callback; import org.junit.Assert; -import org.junit.BeforeClass; -import org.junit.Ignore; +import org.junit.Before; import org.junit.Test; -/** - * @version $Revision$ $Date$ - */ -@Ignore public class ConnectHandlerTest extends AbstractConnectHandlerTest { - @BeforeClass - public static void init() throws Exception + @Before + public void prepare() throws Exception { - startServer(new SelectChannelConnector(), new ServerHandler()); - startProxy(); + server = new Server(); + serverConnector = new ServerConnector(server); + server.addConnector(serverConnector); + server.setHandler(new ServerHandler()); + server.start(); + prepareProxy(); } @Test public void testCONNECT() throws Exception { - String hostPort = "localhost:" + ((Connector.NetConnector)serverConnector).getLocalPort(); + String hostPort = "localhost:" + serverConnector.getLocalPort(); String request = "" + "CONNECT " + hostPort + " HTTP/1.1\r\n" + "Host: " + hostPort + "\r\n" + "\r\n"; - Socket socket = newSocket(); - try + try (Socket socket = newSocket()) { OutputStream output = socket.getOutputStream(); BufferedReader input = new BufferedReader(new InputStreamReader(socket.getInputStream())); @@ -82,26 +77,20 @@ public class ConnectHandlerTest extends AbstractConnectHandlerTest output.flush(); // Expect 200 OK from the CONNECT request - Response response = readResponse(input); - assertEquals("200", response.getCode()); - } - finally - { - socket.close(); + SimpleHttpResponse response = readResponse(input); + Assert.assertEquals("200", response.getCode()); } } @Test public void testCONNECTAndGET() throws Exception { - String hostPort = "localhost:" + ((Connector.NetConnector)serverConnector).getLocalPort(); + String hostPort = "localhost:" + serverConnector.getLocalPort(); String request = "" + "CONNECT " + hostPort + " HTTP/1.1\r\n" + "Host: " + hostPort + "\r\n" + "\r\n"; - Socket socket = newSocket(); - socket.setSoTimeout(30000); - try + try (Socket socket = newSocket()) { OutputStream output = socket.getOutputStream(); BufferedReader input = new BufferedReader(new InputStreamReader(socket.getInputStream())); @@ -110,8 +99,8 @@ public class ConnectHandlerTest extends AbstractConnectHandlerTest output.flush(); // Expect 200 OK from the CONNECT request - Response response = readResponse(input); - assertEquals("200", response.getCode()); + SimpleHttpResponse response = readResponse(input); + Assert.assertEquals("200", response.getCode()); request = "" + "GET /echo" + " HTTP/1.1\r\n" + @@ -121,38 +110,229 @@ public class ConnectHandlerTest extends AbstractConnectHandlerTest output.flush(); response = readResponse(input); - assertEquals("200", response.getCode()); - assertEquals("GET /echo", response.getBody()); - } - finally - { - socket.close(); + Assert.assertEquals("200", response.getCode()); + Assert.assertEquals("GET /echo", response.getBody()); + } + } + + @Test + public void testProxyWhiteList() throws Exception + { + int port = serverConnector.getLocalPort(); + String hostPort = "127.0.0.1:" + port; + connectHandler.getWhiteListHosts().add(hostPort); + + // Try with the wrong host + String request = "" + + "CONNECT localhost:" + port + " HTTP/1.1\r\n" + + "Host: localhost:" + port + "\r\n" + + "\r\n"; + try (Socket socket = newSocket()) + { + OutputStream output = socket.getOutputStream(); + BufferedReader input = new BufferedReader(new InputStreamReader(socket.getInputStream())); + + output.write(request.getBytes("UTF-8")); + output.flush(); + + // Expect 403 from the CONNECT request + SimpleHttpResponse response = readResponse(input); + Assert.assertEquals("403", response.getCode()); + + // Socket should be closed + Assert.assertEquals(-1, input.read()); + } + + // Try again with the right host + request = "" + + "CONNECT " + hostPort + " HTTP/1.1\r\n" + + "Host: " + hostPort + "\r\n" + + "\r\n"; + try (Socket socket = newSocket()) + { + OutputStream output = socket.getOutputStream(); + BufferedReader input = new BufferedReader(new InputStreamReader(socket.getInputStream())); + + output.write(request.getBytes("UTF-8")); + output.flush(); + + // Expect 200 from the CONNECT request + SimpleHttpResponse response = readResponse(input); + Assert.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); + Assert.assertEquals("200", response.getCode()); + Assert.assertEquals("GET /echo", response.getBody()); + } + } + + @Test + public void testProxyBlackList() throws Exception + { + int port = serverConnector.getLocalPort(); + String hostPort = "localhost:" + port; + connectHandler.getBlackListHosts().add(hostPort); + + // Try with the wrong host + String request = "" + + "CONNECT " + hostPort + " HTTP/1.1\r\n" + + "Host: " + hostPort + "\r\n" + + "\r\n"; + try (Socket socket = newSocket()) + { + OutputStream output = socket.getOutputStream(); + BufferedReader input = new BufferedReader(new InputStreamReader(socket.getInputStream())); + + output.write(request.getBytes("UTF-8")); + output.flush(); + + // Expect 403 from the CONNECT request + SimpleHttpResponse response = readResponse(input); + Assert.assertEquals("403", response.getCode()); + + // Socket should be closed + Assert.assertEquals(-1, input.read()); + } + + // Try again with the right host + request = "" + + "CONNECT 127.0.0.1:" + port + " HTTP/1.1\r\n" + + "Host: 127.0.0.1:" + port + "\r\n" + + "\r\n"; + try (Socket socket = newSocket()) + { + OutputStream output = socket.getOutputStream(); + BufferedReader input = new BufferedReader(new InputStreamReader(socket.getInputStream())); + + output.write(request.getBytes("UTF-8")); + output.flush(); + + // Expect 200 from the CONNECT request + SimpleHttpResponse response = readResponse(input); + Assert.assertEquals("200", response.getCode()); + + request = "" + + "GET /echo" + " HTTP/1.1\r\n" + + "Host: 127.0.0.1:" + port + "\r\n" + + "\r\n"; + output.write(request.getBytes("UTF-8")); + output.flush(); + + response = readResponse(input); + Assert.assertEquals("200", response.getCode()); + Assert.assertEquals("GET /echo", response.getBody()); + } + } + + @Test + public void testProxyAuthentication() throws Exception + { + disposeProxy(); + connectHandler = new ConnectHandler() + { + @Override + protected boolean handleAuthentication(HttpServletRequest request, HttpServletResponse response, String address) + { + String proxyAuthorization = request.getHeader("Proxy-Authorization"); + if (proxyAuthorization == null) + { + response.setHeader("Proxy-Authenticate", "Basic realm=\"test\""); + return false; + } + String b64 = proxyAuthorization.substring("Basic ".length()); + String credentials = B64Code.decode(b64, "UTF-8"); + return "test:test".equals(credentials); + } + }; + proxy.setHandler(connectHandler); + proxy.start(); + + int port = serverConnector.getLocalPort(); + String hostPort = "localhost:" + port; + + // Try without authentication + String request = "" + + "CONNECT " + hostPort + " HTTP/1.1\r\n" + + "Host: " + hostPort + "\r\n" + + "\r\n"; + try (Socket socket = newSocket()) + { + OutputStream output = socket.getOutputStream(); + BufferedReader input = new BufferedReader(new InputStreamReader(socket.getInputStream())); + + output.write(request.getBytes("UTF-8")); + output.flush(); + + // Expect 407 from the CONNECT request + SimpleHttpResponse response = readResponse(input); + Assert.assertEquals("407", response.getCode()); + Assert.assertTrue(response.getHeaders().containsKey("Proxy-Authenticate".toLowerCase(Locale.ENGLISH))); + + // Socket should be closed + Assert.assertEquals(-1, input.read()); + } + + // Try with authentication + String credentials = "Basic " + B64Code.encode("test:test"); + request = "" + + "CONNECT " + hostPort + " HTTP/1.1\r\n" + + "Host: " + hostPort + "\r\n" + + "Proxy-Authorization: " + credentials + "\r\n" + + "\r\n"; + try (Socket socket = newSocket()) + { + OutputStream output = socket.getOutputStream(); + BufferedReader input = new BufferedReader(new InputStreamReader(socket.getInputStream())); + + output.write(request.getBytes("UTF-8")); + output.flush(); + + // Expect 200 from the CONNECT request + SimpleHttpResponse response = readResponse(input); + Assert.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); + Assert.assertEquals("200", response.getCode()); + Assert.assertEquals("GET /echo", response.getBody()); } } - @Test public void testCONNECTBadHostPort() throws Exception { - String invalidHostname = "AMAZEBALLS_BADHOST.webtide.com"; - + String invalidHostname = "badHost.webtide.com"; + try { - InetAddress addr = InetAddress.getByName(invalidHostname); + InetAddress address = 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(address.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); + Assert.assertNull(err.toString(), address); } catch (UnknownHostException e) { // expected path } - - String hostPort = String.format("%s:%d",invalidHostname,serverConnector.getLocalPort()); + + String hostPort = String.format("%s:%d", invalidHostname, serverConnector.getLocalPort()); String request = "" + "CONNECT " + hostPort + " HTTP/1.1\r\n" + "Host: " + hostPort + "\r\n" + @@ -168,25 +348,24 @@ public class ConnectHandlerTest extends AbstractConnectHandlerTest output.flush(); // Expect 500 OK from the CONNECT request - Response response = readResponse(input); - assertEquals("Response Code", "500", response.getCode()); + SimpleHttpResponse response = readResponse(input); + Assert.assertEquals("Response Code", "500", response.getCode()); } finally { socket.close(); } } - + @Test public void testCONNECT10AndGET() throws Exception { - String hostPort = "localhost:" + ((Connector.NetConnector)serverConnector).getLocalPort(); + String hostPort = "localhost:" + serverConnector.getLocalPort(); String request = "" + "CONNECT " + hostPort + " HTTP/1.0\r\n" + "Host: " + hostPort + "\r\n" + "\r\n"; - Socket socket = newSocket(); - try + try (Socket socket = newSocket()) { OutputStream output = socket.getOutputStream(); BufferedReader input = new BufferedReader(new InputStreamReader(socket.getInputStream())); @@ -195,8 +374,8 @@ public class ConnectHandlerTest extends AbstractConnectHandlerTest output.flush(); // Expect 200 OK from the CONNECT request - Response response = readResponse(input); - assertEquals("200", response.getCode()); + SimpleHttpResponse response = readResponse(input); + Assert.assertEquals("200", response.getCode()); request = "" + "GET /echo" + " HTTP/1.1\r\n" + @@ -206,19 +385,15 @@ public class ConnectHandlerTest extends AbstractConnectHandlerTest output.flush(); response = readResponse(input); - assertEquals("200", response.getCode()); - assertEquals("GET /echo", response.getBody()); - } - finally - { - socket.close(); + Assert.assertEquals("200", response.getCode()); + Assert.assertEquals("GET /echo", response.getBody()); } } @Test public void testCONNECTAndGETPipelined() throws Exception { - String hostPort = "localhost:" + ((Connector.NetConnector)serverConnector).getLocalPort(); + String hostPort = "localhost:" + serverConnector.getLocalPort(); String request = "" + "CONNECT " + hostPort + " HTTP/1.1\r\n" + "Host: " + hostPort + "\r\n" + @@ -226,8 +401,7 @@ public class ConnectHandlerTest extends AbstractConnectHandlerTest "GET /echo" + " HTTP/1.1\r\n" + "Host: " + hostPort + "\r\n" + "\r\n"; - Socket socket = newSocket(); - try + try (Socket socket = newSocket()) { OutputStream output = socket.getOutputStream(); BufferedReader input = new BufferedReader(new InputStreamReader(socket.getInputStream())); @@ -236,30 +410,25 @@ public class ConnectHandlerTest extends AbstractConnectHandlerTest output.flush(); // Expect 200 OK from the CONNECT request - Response response = readResponse(input); - assertEquals("200", response.getCode()); + SimpleHttpResponse response = readResponse(input); + Assert.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(); + Assert.assertEquals("200", response.getCode()); + Assert.assertEquals("GET /echo", response.getBody()); } } @Test public void testCONNECTAndMultipleGETs() throws Exception { - String hostPort = "localhost:" + ((Connector.NetConnector)serverConnector).getLocalPort(); + String hostPort = "localhost:" + serverConnector.getLocalPort(); String request = "" + "CONNECT " + hostPort + " HTTP/1.1\r\n" + "Host: " + hostPort + "\r\n" + "\r\n"; - Socket socket = newSocket(); - try + try (Socket socket = newSocket()) { OutputStream output = socket.getOutputStream(); BufferedReader input = new BufferedReader(new InputStreamReader(socket.getInputStream())); @@ -268,8 +437,8 @@ public class ConnectHandlerTest extends AbstractConnectHandlerTest output.flush(); // Expect 200 OK from the CONNECT request - Response response = readResponse(input); - assertEquals("200", response.getCode()); + SimpleHttpResponse response = readResponse(input); + Assert.assertEquals("200", response.getCode()); for (int i = 0; i < 10; ++i) { @@ -281,26 +450,21 @@ public class ConnectHandlerTest extends AbstractConnectHandlerTest output.flush(); response = readResponse(input); - assertEquals("200", response.getCode()); - assertEquals("GET /echo", response.getBody()); + Assert.assertEquals("200", response.getCode()); + Assert.assertEquals("GET /echo", response.getBody()); } } - finally - { - socket.close(); - } } @Test public void testCONNECTAndGETServerStop() throws Exception { - String hostPort = "localhost:" + ((Connector.NetConnector)serverConnector).getLocalPort(); + String hostPort = "localhost:" + serverConnector.getLocalPort(); String request = "" + "CONNECT " + hostPort + " HTTP/1.1\r\n" + "Host: " + hostPort + "\r\n" + "\r\n"; - Socket socket = newSocket(); - try + try (Socket socket = newSocket()) { OutputStream output = socket.getOutputStream(); BufferedReader input = new BufferedReader(new InputStreamReader(socket.getInputStream())); @@ -309,8 +473,8 @@ public class ConnectHandlerTest extends AbstractConnectHandlerTest output.flush(); // Expect 200 OK from the CONNECT request - Response response = readResponse(input); - assertEquals("200", response.getCode()); + SimpleHttpResponse response = readResponse(input); + Assert.assertEquals("200", response.getCode()); request = "" + "GET /echo HTTP/1.1\r\n" + @@ -320,33 +484,26 @@ public class ConnectHandlerTest extends AbstractConnectHandlerTest output.flush(); response = readResponse(input); - assertEquals("200", response.getCode()); - assertEquals("GET /echo", response.getBody()); + Assert.assertEquals("200", response.getCode()); + Assert.assertEquals("GET /echo", response.getBody()); // Idle server is shut down - stopServer(); + disposeServer(); int read = input.read(); - assertEquals(-1, read); - } - finally - { - socket.close(); - // Restart the server for the next test - server.start(); + Assert.assertEquals(-1, read); } } @Test public void testCONNECTAndGETAndServerSideClose() throws Exception { - String hostPort = "localhost:" + ((Connector.NetConnector)serverConnector).getLocalPort(); + String hostPort = "localhost:" + serverConnector.getLocalPort(); String request = "" + "CONNECT " + hostPort + " HTTP/1.1\r\n" + "Host: " + hostPort + "\r\n" + "\r\n"; - Socket socket = newSocket(); - try + try (Socket socket = newSocket()) { OutputStream output = socket.getOutputStream(); BufferedReader input = new BufferedReader(new InputStreamReader(socket.getInputStream())); @@ -355,8 +512,8 @@ public class ConnectHandlerTest extends AbstractConnectHandlerTest output.flush(); // Expect 200 OK from the CONNECT request - Response response = readResponse(input); - assertEquals("200", response.getCode()); + SimpleHttpResponse response = readResponse(input); + Assert.assertEquals("200", response.getCode()); request = "" + "GET /close HTTP/1.1\r\n" + @@ -366,24 +523,19 @@ public class ConnectHandlerTest extends AbstractConnectHandlerTest output.flush(); int read = input.read(); - assertEquals(-1, read); - } - finally - { - socket.close(); + Assert.assertEquals(-1, read); } } @Test public void testCONNECTAndPOSTAndGET() throws Exception { - String hostPort = "localhost:" + ((Connector.NetConnector)serverConnector).getLocalPort(); + String hostPort = "localhost:" + serverConnector.getLocalPort(); String request = "" + "CONNECT " + hostPort + " HTTP/1.1\r\n" + "Host: " + hostPort + "\r\n" + "\r\n"; - Socket socket = newSocket(); - try + try (Socket socket = newSocket()) { OutputStream output = socket.getOutputStream(); BufferedReader input = new BufferedReader(new InputStreamReader(socket.getInputStream())); @@ -392,8 +544,8 @@ public class ConnectHandlerTest extends AbstractConnectHandlerTest output.flush(); // Expect 200 OK from the CONNECT request - Response response = readResponse(input); - assertEquals("200", response.getCode()); + SimpleHttpResponse response = readResponse(input); + Assert.assertEquals("200", response.getCode()); request = "" + "POST /echo HTTP/1.1\r\n" + @@ -405,8 +557,8 @@ public class ConnectHandlerTest extends AbstractConnectHandlerTest output.flush(); response = readResponse(input); - assertEquals("200", response.getCode()); - assertEquals("POST /echo\r\nHELLO", response.getBody()); + Assert.assertEquals("200", response.getCode()); + Assert.assertEquals("POST /echo\r\nHELLO", response.getBody()); request = "" + "GET /echo" + " HTTP/1.1\r\n" + @@ -416,34 +568,21 @@ public class ConnectHandlerTest extends AbstractConnectHandlerTest output.flush(); response = readResponse(input); - assertEquals("200", response.getCode()); - assertEquals("GET /echo", response.getBody()); - } - finally - { - socket.close(); + Assert.assertEquals("200", response.getCode()); + Assert.assertEquals("GET /echo", response.getBody()); } } @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 hostPort = "localhost:" + serverConnector.getLocalPort(); + String request = "" + "CONNECT " + hostPort + " HTTP/1.1\r\n" + "Host: " + hostPort + "\r\n" + "\r\n"; - Socket socket = newSocket(); - try + try (Socket socket = newSocket()) { OutputStream output = socket.getOutputStream(); BufferedReader input = new BufferedReader(new InputStreamReader(socket.getInputStream())); @@ -452,8 +591,8 @@ public class ConnectHandlerTest extends AbstractConnectHandlerTest output.flush(); // Expect 200 OK from the CONNECT request - Response response = readResponse(input); - assertEquals("200", response.getCode()); + SimpleHttpResponse response = readResponse(input); + Assert.assertEquals("200", response.getCode()); StringBuilder body = new StringBuilder(); String chunk = "0123456789ABCDEF"; @@ -470,12 +609,8 @@ public class ConnectHandlerTest extends AbstractConnectHandlerTest output.flush(); response = readResponse(input); - assertEquals("200", response.getCode()); - assertEquals("POST /echo\r\n" + body, response.getBody()); - } - finally - { - socket.close(); + Assert.assertEquals("200", response.getCode()); + Assert.assertEquals("POST /echo\r\n" + body, response.getBody()); } } @@ -486,54 +621,46 @@ public class ConnectHandlerTest extends AbstractConnectHandlerTest final String contextValue = "contextValue"; // Replace the default ProxyHandler with a subclass to test context information passing - stopProxy(); + disposeProxy(); proxy.setHandler(new ConnectHandler() { @Override - protected boolean handleAuthentication(HttpServletRequest request, HttpServletResponse response, String address) throws ServletException, IOException + protected boolean handleAuthentication(HttpServletRequest request, HttpServletResponse response, String address) { 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)); + Assert.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)); + Assert.assertEquals(contextValue, context.get(contextKey)); return super.read(endPoint, buffer, context); } @Override - protected int write(EndPoint endPoint, ByteBuffer buffer, ConcurrentMap context) throws IOException + protected void write(EndPoint endPoint, ByteBuffer buffer, ConcurrentMap context, Callback callback) { - assertEquals(contextValue, context.get(contextKey)); - return super.write(endPoint, buffer, context); + Assert.assertEquals(contextValue, context.get(contextKey)); + super.write(endPoint, buffer, context, callback); } }); proxy.start(); - String hostPort = "localhost:" + ((Connector.NetConnector)serverConnector).getLocalPort(); + String hostPort = "localhost:" + serverConnector.getLocalPort(); String request = "" + "CONNECT " + hostPort + " HTTP/1.1\r\n" + "Host: " + hostPort + "\r\n" + "\r\n"; - Socket socket = newSocket(); - try + try (Socket socket = newSocket()) { OutputStream output = socket.getOutputStream(); BufferedReader input = new BufferedReader(new InputStreamReader(socket.getInputStream())); @@ -542,8 +669,8 @@ public class ConnectHandlerTest extends AbstractConnectHandlerTest output.flush(); // Expect 200 OK from the CONNECT request - Response response = readResponse(input); - assertEquals("200", response.getCode()); + SimpleHttpResponse response = readResponse(input); + Assert.assertEquals("200", response.getCode()); String body = "0123456789ABCDEF"; request = "" + @@ -556,22 +683,15 @@ public class ConnectHandlerTest extends AbstractConnectHandlerTest output.flush(); response = readResponse(input); - assertEquals("200", response.getCode()); - assertEquals("POST /echo\r\n" + body, response.getBody()); - } - finally - { - socket.close(); + Assert.assertEquals("200", response.getCode()); + Assert.assertEquals("POST /echo\r\n" + body, response.getBody()); } } @Test public void testCONNECTAndGETPipelinedAndOutputShutdown() throws Exception { - // TODO needs to be further investigated - assumeTrue(!OS.IS_OSX); - - String hostPort = "localhost:" + ((Connector.NetConnector)serverConnector).getLocalPort(); + String hostPort = "localhost:" + serverConnector.getLocalPort(); String request = "" + "CONNECT " + hostPort + " HTTP/1.1\r\n" + "Host: " + hostPort + "\r\n" + @@ -579,8 +699,7 @@ public class ConnectHandlerTest extends AbstractConnectHandlerTest "GET /echo" + " HTTP/1.1\r\n" + "Host: " + hostPort + "\r\n" + "\r\n"; - Socket socket = newSocket(); - try + try (Socket socket = newSocket()) { OutputStream output = socket.getOutputStream(); BufferedReader input = new BufferedReader(new InputStreamReader(socket.getInputStream())); @@ -590,33 +709,25 @@ public class ConnectHandlerTest extends AbstractConnectHandlerTest socket.shutdownOutput(); // Expect 200 OK from the CONNECT request - Response response = readResponse(input); - assertEquals("200", response.getCode()); + SimpleHttpResponse response = readResponse(input); + Assert.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(); + Assert.assertEquals("200", response.getCode()); + Assert.assertEquals("GET /echo", response.getBody()); } } @Test public void testCONNECTAndGETAndOutputShutdown() throws Exception { - // TODO needs to be further investigated - assumeTrue(!OS.IS_OSX); - - String hostPort = "localhost:" + ((Connector.NetConnector)serverConnector).getLocalPort(); + String hostPort = "localhost:" + serverConnector.getLocalPort(); String request = "" + "CONNECT " + hostPort + " HTTP/1.1\r\n" + "Host: " + hostPort + "\r\n" + "\r\n"; - Socket socket = newSocket(); - try + try (Socket socket = newSocket()) { OutputStream output = socket.getOutputStream(); BufferedReader input = new BufferedReader(new InputStreamReader(socket.getInputStream())); @@ -625,8 +736,8 @@ public class ConnectHandlerTest extends AbstractConnectHandlerTest output.flush(); // Expect 200 OK from the CONNECT request - Response response = readResponse(input); - assertEquals("200", response.getCode()); + SimpleHttpResponse response = readResponse(input); + Assert.assertEquals("200", response.getCode()); request = "" + "GET /echo" + " HTTP/1.1\r\n" + @@ -638,12 +749,8 @@ public class ConnectHandlerTest extends AbstractConnectHandlerTest // 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(); + Assert.assertEquals("200", response.getCode()); + Assert.assertEquals("GET /echo", response.getBody()); } } @@ -654,31 +761,36 @@ public class ConnectHandlerTest extends AbstractConnectHandlerTest request.setHandled(true); String uri = httpRequest.getRequestURI(); - if ("/echo".equals(uri)) + switch (uri) { - StringBuilder builder = new StringBuilder(); - builder.append(httpRequest.getMethod()).append(" ").append(uri); - if (httpRequest.getQueryString() != null) - builder.append("?").append(httpRequest.getQueryString()); + case "/echo": + { + 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(); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + InputStream input = httpRequest.getInputStream(); + int read; + 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(); + ServletOutputStream output = httpResponse.getOutputStream(); + output.println(builder.toString()); + output.write(baos.toByteArray()); + break; + } + case "/close": + { + request.getHttpChannel().getEndPoint().close(); + break; + } + default: + { + 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 deleted file mode 100644 index e715b597dec..00000000000 --- a/jetty-proxy/src/test/java/org/eclipse/jetty/proxy/ConnectHandlerUnitTest.java +++ /dev/null @@ -1,77 +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 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) -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)); - */ - } - - -} 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..2a9a47684ba 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,233 @@ 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)); + } + + // TODO: test proxy authentication + + 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/jetty-proxy/src/test/resources/keystore.jks b/jetty-proxy/src/test/resources/keystore.jks new file mode 100644 index 00000000000..428ba54776e Binary files /dev/null and b/jetty-proxy/src/test/resources/keystore.jks differ diff --git a/jetty-proxy/src/test/resources/truststore.jks b/jetty-proxy/src/test/resources/truststore.jks new file mode 100644 index 00000000000..839cb8c3515 Binary files /dev/null and b/jetty-proxy/src/test/resources/truststore.jks differ diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConnection.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConnection.java index 3381c90fb9d..06fe0beb09d 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConnection.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConnection.java @@ -25,12 +25,13 @@ import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.TimeoutException; import org.eclipse.jetty.http.HttpGenerator; +import org.eclipse.jetty.http.HttpGenerator.ResponseInfo; import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpHeaderValue; +import org.eclipse.jetty.http.HttpMethod; import org.eclipse.jetty.http.HttpParser; import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.http.HttpVersion; -import org.eclipse.jetty.http.HttpGenerator.ResponseInfo; import org.eclipse.jetty.io.AbstractConnection; import org.eclipse.jetty.io.ByteBufferPool; import org.eclipse.jetty.io.Connection; @@ -61,7 +62,7 @@ public class HttpConnection extends AbstractConnection implements Runnable, Http private volatile ByteBuffer _chunk = null; private BlockingCallback _readBlocker = new BlockingCallback(); private BlockingCallback _writeBlocker = new BlockingCallback(); - + // TODO get rid of this private final Runnable _channelRunner = new Runnable() { @@ -77,7 +78,7 @@ public class HttpConnection extends AbstractConnection implements Runnable, Http { setCurrentConnection(null); } - + } }; @@ -410,7 +411,7 @@ public class HttpConnection extends AbstractConnection implements Runnable, Http } } } - + @Override public void send(ResponseInfo info, ByteBuffer content, boolean lastContent, C context, Callback callback) { @@ -514,6 +515,10 @@ public class HttpConnection extends AbstractConnection implements Runnable, Http } } + public ByteBuffer getRequestBuffer() + { + return _requestBuffer; + } private class Input extends ByteBufferHttpInput { @@ -543,7 +548,7 @@ public class HttpConnection extends AbstractConnection implements Runnable, Http // Do we have content ready to parse? if (BufferUtil.isEmpty(_requestBuffer)) - { + { // If no more input if (getEndPoint().isInputShutdown()) { @@ -642,24 +647,32 @@ public class HttpConnection extends AbstractConnection implements Runnable, Http switch (version) { case HTTP_0_9: + { persistent = false; break; - + } case HTTP_1_0: + { persistent = getRequest().getHttpFields().contains(HttpHeader.CONNECTION, HttpHeaderValue.KEEP_ALIVE.asString()); + if (!persistent) + persistent = HttpMethod.CONNECT.is(getRequest().getMethod()); if (persistent) getResponse().getHttpFields().add(HttpHeader.CONNECTION, HttpHeaderValue.KEEP_ALIVE); break; - + } case HTTP_1_1: + { persistent = !getRequest().getHttpFields().contains(HttpHeader.CONNECTION, HttpHeaderValue.CLOSE.asString()); - + if (!persistent) + persistent = HttpMethod.CONNECT.is(getRequest().getMethod()); if (!persistent) getResponse().getHttpFields().add(HttpHeader.CONNECTION, HttpHeaderValue.CLOSE); - break; + } default: + { throw new IllegalStateException(); + } } if (!persistent) diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/B64Code.java b/jetty-util/src/main/java/org/eclipse/jetty/util/B64Code.java index d9ca17c84ac..5a8ffdb2827 100644 --- a/jetty-util/src/main/java/org/eclipse/jetty/util/B64Code.java +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/B64Code.java @@ -20,22 +20,20 @@ package org.eclipse.jetty.util; import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.io.UnsupportedEncodingException; +import java.nio.charset.Charset; -/* ------------------------------------------------------------ */ /** Fast B64 Encoder/Decoder as described in RFC 1421. *

Does not insert or interpret whitespace as described in RFC * 1521. If you require this you must pre/post process your data. *

Note that in a web context the usual case is to not want * linebreaks or other white space in the encoded output. - * + * */ public class B64Code { - // ------------------------------------------------------------------ - static final char __pad='='; - static final char[] __rfc1421alphabet= + private static final char __pad='='; + private static final char[] __rfc1421alphabet= { 'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P', 'Q','R','S','T','U','V','W','X','Y','Z','a','b','c','d','e','f', @@ -43,8 +41,7 @@ public class B64Code 'w','x','y','z','0','1','2','3','4','5','6','7','8','9','+','/' }; - static final byte[] __rfc1421nibbles; - + private static final byte[] __rfc1421nibbles; static { __rfc1421nibbles=new byte[256]; @@ -55,26 +52,21 @@ public class B64Code __rfc1421nibbles[(byte)__pad]=0; } - // ------------------------------------------------------------------ + private B64Code() + { + } + /** * Base 64 encode as described in RFC 1421. *

Does not insert whitespace as described in RFC 1521. * @param s String to encode. * @return String containing the encoded form of the input. */ - static public String encode(String s) + public static String encode(String s) { - try - { - return encode(s,null); - } - catch (UnsupportedEncodingException e) - { - throw new IllegalArgumentException(e.toString()); - } + return encode(s,null); } - // ------------------------------------------------------------------ /** * Base 64 encode as described in RFC 1421. *

Does not insert whitespace as described in RFC 1521. @@ -83,19 +75,16 @@ public class B64Code * the character encoding of the provided input String. * @return String containing the encoded form of the input. */ - static public String encode(String s,String charEncoding) - throws UnsupportedEncodingException + public static String encode(String s,String charEncoding) { byte[] bytes; if (charEncoding==null) - bytes=s.getBytes(StringUtil.__ISO_8859_1); + bytes=s.getBytes(Charset.forName(StringUtil.__ISO_8859_1)); else - bytes=s.getBytes(charEncoding); - + bytes=s.getBytes(Charset.forName(charEncoding)); return new String(encode(bytes)); } - - // ------------------------------------------------------------------ + /** * Fast Base 64 encode as described in RFC 1421. *

Does not insert whitespace as described in RFC 1521. @@ -103,7 +92,7 @@ public class B64Code * @param b byte array to encode. * @return char array containing the encoded form of the input. */ - static public char[] encode(byte[] b) + public static char[] encode(byte[] b) { if (b==null) return null; @@ -123,7 +112,7 @@ public class B64Code c[ci++]=__rfc1421alphabet[(b0>>>2)&0x3f]; c[ci++]=__rfc1421alphabet[(b0<<4)&0x3f|(b1>>>4)&0x0f]; c[ci++]=__rfc1421alphabet[(b1<<2)&0x3f|(b2>>>6)&0x03]; - c[ci++]=__rfc1421alphabet[b2&077]; + c[ci++]=__rfc1421alphabet[b2&0x3f]; } if (bLen!=bi) @@ -154,8 +143,7 @@ public class B64Code return c; } - - // ------------------------------------------------------------------ + /** * Fast Base 64 encode as described in RFC 1421 and RFC2045 *

Does not insert whitespace as described in RFC 1521, unless rfc2045 is passed as true. @@ -164,7 +152,7 @@ public class B64Code * @param rfc2045 If true, break lines at 76 characters with CRLF * @return char array containing the encoded form of the input. */ - static public char[] encode(byte[] b, boolean rfc2045) + public static char[] encode(byte[] b, boolean rfc2045) { if (b==null) return null; @@ -188,7 +176,7 @@ public class B64Code c[ci++]=__rfc1421alphabet[(b0>>>2)&0x3f]; c[ci++]=__rfc1421alphabet[(b0<<4)&0x3f|(b1>>>4)&0x0f]; c[ci++]=__rfc1421alphabet[(b1<<2)&0x3f|(b2>>>6)&0x03]; - c[ci++]=__rfc1421alphabet[b2&077]; + c[ci++]=__rfc1421alphabet[b2&0x3f]; l+=4; if (l%76==0) { @@ -228,7 +216,6 @@ public class B64Code return c; } - // ------------------------------------------------------------------ /** * Base 64 decode as described in RFC 2045. *

Unlike {@link #decode(char[])}, extra whitespace is ignored. @@ -236,24 +223,22 @@ public class B64Code * @param charEncoding String representing the character encoding * used to map the decoded bytes into a String. * @return String decoded byte array. - * @throws UnsupportedEncodingException if the encoding is not supported + * @throws UnsupportedCharsetException if the encoding is not supported * @throws IllegalArgumentException if the input is not a valid * B64 encoding. */ - static public String decode(String encoded,String charEncoding) - throws UnsupportedEncodingException + public static String decode(String encoded,String charEncoding) { byte[] decoded=decode(encoded); if (charEncoding==null) return new String(decoded); - return new String(decoded,charEncoding); + return new String(decoded,Charset.forName(charEncoding)); } - /* ------------------------------------------------------------ */ /** * Fast Base 64 decode as described in RFC 1421. - * - *

Unlike other decode methods, this does not attempt to + * + *

Unlike other decode methods, this does not attempt to * cope with extra whitespace as described in RFC 1521/2045. *

Avoids creating extra copies of the input/output. *

Note this code has been flattened for performance. @@ -262,7 +247,7 @@ public class B64Code * @throws IllegalArgumentException if the input is not a valid * B64 encoding. */ - static public byte[] decode(char[] b) + public static byte[] decode(char[] b) { if (b==null) return null; @@ -336,8 +321,7 @@ public class B64Code return r; } - - /* ------------------------------------------------------------ */ + /** * Base 64 decode as described in RFC 2045. *

Unlike {@link #decode(char[])}, extra whitespace is ignored. @@ -346,11 +330,11 @@ public class B64Code * @throws IllegalArgumentException if the input is not a valid * B64 encoding. */ - static public byte[] decode(String encoded) + public static byte[] decode(String encoded) { if (encoded==null) return null; - + int ci=0; byte nibbles[] = new byte[4]; int s=0; @@ -362,7 +346,7 @@ public class B64Code if (c==__pad) break; - + if (Character.isWhitespace(c)) continue; @@ -392,8 +376,7 @@ public class B64Code return bout.toByteArray(); } - - /* ------------------------------------------------------------ */ + public static void encode(int value,Appendable buf) throws IOException { buf.append(__rfc1421alphabet[0x3f&((0xFC000000&value)>>26)]); @@ -404,8 +387,7 @@ public class B64Code buf.append(__rfc1421alphabet[0x3f&((0x00000003&value)<<4)]); buf.append('='); } - - /* ------------------------------------------------------------ */ + public static void encode(long lvalue,Appendable buf) throws IOException { int value=(int)(0xFFFFFFFC&(lvalue>>32)); @@ -414,9 +396,9 @@ public class B64Code buf.append(__rfc1421alphabet[0x3f&((0x000FC000&value)>>14)]); buf.append(__rfc1421alphabet[0x3f&((0x00003F00&value)>>8)]); buf.append(__rfc1421alphabet[0x3f&((0x000000FC&value)>>2)]); - + buf.append(__rfc1421alphabet[0x3f&((0x00000003&value)<<4) + (0xf&(int)(lvalue>>28))]); - + value=0x0FFFFFFF&(int)lvalue; buf.append(__rfc1421alphabet[0x3f&((0x0FC00000&value)>>22)]); buf.append(__rfc1421alphabet[0x3f&((0x003F0000&value)>>16)]); diff --git a/pom.xml b/pom.xml index 7690d7cb150..1c4373fdf45 100644 --- a/pom.xml +++ b/pom.xml @@ -413,6 +413,7 @@ examples/async-rest jetty-rewrite jetty-nosql + jetty-proxy tests @@ -421,7 +422,6 @@ jetty-runner jetty-rhttp - jetty-proxy jetty-monitor jetty-nested jetty-overlay-deployer @@ -578,7 +578,9 @@ release - aggregates/jetty-all +