Add an HTTP reverse proxy
diff --git a/src/com/google/enterprise/adaptor/ReverseProxyHandler.java b/src/com/google/enterprise/adaptor/ReverseProxyHandler.java
new file mode 100644
index 0000000..13c57fc
--- /dev/null
+++ b/src/com/google/enterprise/adaptor/ReverseProxyHandler.java
@@ -0,0 +1,266 @@
+// Copyright 2014 Google Inc. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.enterprise.adaptor;
+
+import com.google.common.collect.ImmutableSortedSet;
+import com.google.common.collect.Sets;
+
+import com.sun.net.httpserver.HttpExchange;
+import com.sun.net.httpserver.HttpHandler;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.Authenticator;
+import java.net.HttpURLConnection;
+import java.net.InetSocketAddress;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * An HTTP reverse proxy. This class uses HttpURLConnection internally, so the
+ * default {@link Authenticator} must be {@code null} for proper operation. The
+ * implementation is known to drop the response body for 401 responses to POST
+ * requests.
+ */
+class ReverseProxyHandler implements HttpHandler {
+ // The Hop-by-hop headers that are connection-local, as defined by RFC 2616
+ // Section 13.5.1. As mentioned in the section, additional headers can be
+ // specified in the Connection header.
+ private static final Set<String> PREDEFINED_HOP_BY_HOP_HEADERS
+ = ImmutableSortedSet.orderedBy(String.CASE_INSENSITIVE_ORDER).add(
+ "Connection",
+ "Host", // Because this is a reverse proxy.
+ "Keep-Alive",
+ "Proxy-Authenticate",
+ "Proxy-Authorization",
+ "TE",
+ "Trailers",
+ "Transfer-Encoding",
+ "Upgrade").build();
+
+ private static final Logger log
+ = Logger.getLogger(HttpExchanges.class.getName());
+
+ private final URI destinationBase;
+
+ public ReverseProxyHandler(URI destinationBase) {
+ if (destinationBase == null) {
+ throw new NullPointerException();
+ }
+ this.destinationBase = destinationBase;
+ }
+
+ @Override
+ public void handle(HttpExchange ex) throws IOException {
+ URI dest = computeProxyDestination(ex);
+
+ // Set up request
+ HttpURLConnection conn = (HttpURLConnection) dest.toURL().openConnection();
+ conn.setRequestMethod(ex.getRequestMethod());
+ conn.setAllowUserInteraction(false);
+ conn.setInstanceFollowRedirects(false);
+ // This adds Cache-Control: no-cache and Pragma: no-cache if those headers
+ // keys aren't already there. It is unfortunate that it modifies the
+ // request, however, the presense of these headers shouldn't have an impact
+ // because they are targeted to proxy servers, not the application server.
+ // See RFC 2616 Section 14.9.1 and 14.32.
+ conn.setUseCaches(false);
+ // HttpUrlConnection will add Accept and User-Agent headers if they aren't
+ // already there. Luckily they are almost always there.
+ copyRequestHeaders(ex, conn);
+ conn.addRequestProperty("X-Forwarded-For", getClientIp(ex));
+
+ // As defined in RFC 2616 Section 4.3
+ boolean hasRequestBody
+ = ex.getRequestHeaders().containsKey("Content-Length")
+ || ex.getRequestHeaders().containsKey("Transfer-Encoding");
+
+ conn.setDoOutput(hasRequestBody);
+ // Input is required for getResponseCode()
+ conn.setDoInput(true);
+
+ if (hasRequestBody) {
+ String strContentLength
+ = ex.getRequestHeaders().getFirst("Content-Length");
+ long contentLength = -1;
+ if (strContentLength != null) {
+ try {
+ contentLength = Long.parseLong(strContentLength);
+ } catch (NumberFormatException e) {
+ // Keep contentLength = -1
+ }
+ }
+ // Enable streaming mode on the connection to prevent entire request body
+ // from being buffered. Streaming mode also disables automatic
+ // authentication.
+ if (contentLength < 0) {
+ conn.setChunkedStreamingMode(-1);
+ } else {
+ // TODO(ejona): remove cast once we depend on Java 7.
+ conn.setFixedLengthStreamingMode((int) contentLength);
+ }
+ } else {
+ // It is essential that no authentication happens. Unfortunately for
+ // requests with no body (like GET), it seems impossible to detect if
+ // authentication could be or was performed and impossible to prevent it
+ // for a particular request.
+ //
+ // Thus, we disable authentication globally, which could easily break
+ // other code's assumptions. However, the hope is that the other code will
+ // begin breaking and we have a chance at discovering the incompatibility
+ // between the two pieces of code, instead of silently offering content to
+ // requesters that they are not authenticated for.
+ Authenticator.setDefault(null);
+ }
+
+ // Actually issue the request, and copy the data back
+ conn.connect();
+ if (hasRequestBody) {
+ try {
+ IOHelper.copyStream(ex.getRequestBody(), conn.getOutputStream());
+ ex.getRequestBody().close();
+ conn.getOutputStream().close();
+ } catch (IOException e) {
+ // TODO(ejona): determine if there is a more graceful way to clean up
+ conn.disconnect();
+ throw e;
+ }
+ }
+
+ InputStream inputStream = conn.getResponseCode() >= 400
+ ? conn.getErrorStream() : conn.getInputStream();
+ // InputStream is null when responseCode == 401 and doing streaming, because
+ // HttpURLConnection does not consider its state "connected". Unfortunately,
+ // that means that we will lose the content for 401 responses for POST.
+ if (conn.getResponseCode() == 401 && inputStream == null) {
+ inputStream = new ByteArrayInputStream(new byte[0]);
+ }
+
+ // As defined in RFC 2616 Section 4.3
+ boolean hasResponseBody = !("HEAD".equalsIgnoreCase(ex.getRequestMethod())
+ || conn.getResponseCode() / 100 == 1 // 1xx Informational
+ || conn.getResponseCode() == 204 // No Content
+ || conn.getResponseCode() == 304); // Not Modified
+
+ try {
+ copyResponseHeaders(conn, ex);
+
+ if (!hasResponseBody) {
+ ex.sendResponseHeaders(conn.getResponseCode(), -1);
+ } else {
+ int contentLength = conn.getContentLength();
+ if (contentLength <= 0) {
+ // If the content length was unknown (-1) or if it was zero, then we
+ // are forced to use chunked transfer encoding.
+ contentLength = 0;
+ }
+ ex.sendResponseHeaders(conn.getResponseCode(), contentLength);
+ IOHelper.copyStream(inputStream, ex.getResponseBody());
+ }
+ // Don't close in a finally because that would be a successful response.
+ // If there is an error we want the server to kill the connection, which
+ // informs the client that something went wrong.
+ ex.close();
+ } finally {
+ inputStream.close();
+ }
+ }
+
+ /**
+ * Compute the URI the proxy should send a request to, to proxy the provided
+ * request.
+ */
+ private URI computeProxyDestination(HttpExchange ex) {
+ URI req = HttpExchanges.getRequestUri(ex);
+ final String basePath = ex.getHttpContext().getPath();
+ if (!req.getPath().startsWith(basePath)) {
+ throw new AssertionError();
+ }
+ String lastPartOfPath = req.getPath().substring(basePath.length());
+ try {
+ return new URI(destinationBase.getScheme(),
+ destinationBase.getAuthority(),
+ destinationBase.getPath() + lastPartOfPath, req.getQuery(),
+ req.getFragment());
+ } catch (URISyntaxException e) {
+ throw new AssertionError(e);
+ }
+ }
+
+ private void copyRequestHeaders(HttpExchange from, HttpURLConnection to) {
+ Set<String> requestHopByHopHeaders
+ = getHopByHopHeaders(from.getRequestHeaders().get("Connection"));
+ for (Map.Entry<String, List<String>> me
+ : from.getRequestHeaders().entrySet()) {
+ if (requestHopByHopHeaders.contains(me.getKey())) {
+ continue;
+ }
+ for (String value : me.getValue()) {
+ to.addRequestProperty(me.getKey(), value);
+ }
+ }
+ }
+
+ private void copyResponseHeaders(HttpURLConnection from, HttpExchange to) {
+ Set<String> responseHopByHopHeaders
+ = getHopByHopHeaders(from.getHeaderFields().get("Connection"));
+ // HttpURLConnection.getHeaderFields() reverses the order of repeated
+ // headers' values, so we avoid it. Specifically,
+ // sun.net.www.MessageHeader.filterAndAddHeaders() loops through the
+ // headers in reverse order while building the map.
+ for (int i = 0;; i++) {
+ String key = from.getHeaderFieldKey(i);
+ String value = from.getHeaderField(i);
+ if (value == null) {
+ break; // Reached end
+ }
+ if (key == null) {
+ continue; // Value is the HTTP status line, which we want to skip
+ }
+ if (responseHopByHopHeaders.contains(key)) {
+ continue;
+ }
+ to.getResponseHeaders().add(key, value);
+ }
+ }
+
+ private String getClientIp(HttpExchange ex) {
+ InetSocketAddress clientAddr = ex.getRemoteAddress();
+ if (clientAddr.isUnresolved()) {
+ log.log(Level.WARNING, "Could not determine client's IP");
+ return "unknown";
+ }
+ return clientAddr.getAddress().getHostAddress();
+ }
+
+ private Set<String> getHopByHopHeaders(List<String> connectionValues) {
+ List<String> rawConnectionHeaders
+ = HttpExchanges.splitHeaderValues(connectionValues);
+ if (rawConnectionHeaders == null) {
+ rawConnectionHeaders = Collections.emptyList();
+ }
+ Set<String> connectionHeaders
+ = ImmutableSortedSet.orderedBy(String.CASE_INSENSITIVE_ORDER)
+ .addAll(rawConnectionHeaders).build();
+ return Sets.union(PREDEFINED_HOP_BY_HOP_HEADERS, connectionHeaders);
+ }
+}
diff --git a/test/com/google/enterprise/adaptor/MockHttpExchange.java b/test/com/google/enterprise/adaptor/MockHttpExchange.java
index f1139bd..f385aaf 100644
--- a/test/com/google/enterprise/adaptor/MockHttpExchange.java
+++ b/test/com/google/enterprise/adaptor/MockHttpExchange.java
@@ -24,6 +24,9 @@
* Mock {@link HttpExchange} for testing.
*/
public class MockHttpExchange extends HttpExchange {
+ public static final String HEADER_DATE_VALUE
+ = "Sun, 06 Nov 1994 08:49:37 GMT";
+
private final String method;
private final URI uri;
private final Map<String, Object> attributes = new HashMap<String, Object>();
@@ -40,13 +43,14 @@
= new ClosingFilterOutputStream(responseBodyOrig);
private int responseCode = -1;
private HttpContext httpContext;
+ private InetSocketAddress remoteAddress;
public MockHttpExchange(String method, String path,
HttpContext context) {
this(method, "localhost", path, context);
}
- private MockHttpExchange(String method, String host, String path,
+ public MockHttpExchange(String method, String host, String path,
HttpContext context) {
if (method == null || host == null || path == null) {
throw new NullPointerException();
@@ -63,6 +67,14 @@
}
this.httpContext = context;
getRequestHeaders().add("Host", host);
+
+ try {
+ remoteAddress = new InetSocketAddress(
+ InetAddress.getByAddress("remotehost", new byte[] {127, 0, 0, 3}),
+ 65000);
+ } catch (UnknownHostException ex) {
+ throw new AssertionError(ex);
+ }
}
@Override
@@ -110,13 +122,11 @@
@Override
public InetSocketAddress getRemoteAddress() {
- try {
- return new InetSocketAddress(
- InetAddress.getByAddress("remotehost", new byte[] {127, 0, 0, 3}),
- 65000);
- } catch (UnknownHostException ex) {
- throw new IllegalStateException(ex);
- }
+ return remoteAddress;
+ }
+
+ public void setRemoteAddress(InetSocketAddress remoteAddress) {
+ this.remoteAddress = remoteAddress;
}
@Override
@@ -162,7 +172,8 @@
if (responseCode != -1) {
throw new IllegalStateException();
}
- getResponseHeaders().add("Date", "Sun, 06 Nov 1994 08:49:37 GMT");
+ // The handler gets no choice of the date.
+ getResponseHeaders().set("Date", HEADER_DATE_VALUE);
responseCode = rCode;
// TODO(ejona): handle responseLengeth
}
@@ -191,6 +202,7 @@
public void setRequestBody(InputStream i) {
requestBodyOrig = i;
requestBody = requestBodyOrig;
+ getRequestHeaders().add("Transfer-Encoding", "chunked");
}
public byte[] getResponseBytes() {
diff --git a/test/com/google/enterprise/adaptor/ReverseProxyHandlerTest.java b/test/com/google/enterprise/adaptor/ReverseProxyHandlerTest.java
new file mode 100644
index 0000000..a68a256
--- /dev/null
+++ b/test/com/google/enterprise/adaptor/ReverseProxyHandlerTest.java
@@ -0,0 +1,305 @@
+// Copyright 2014 Google Inc. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.enterprise.adaptor;
+
+import static org.junit.Assert.*;
+
+import com.google.common.base.Objects;
+import com.sun.net.httpserver.Headers;
+import com.sun.net.httpserver.HttpExchange;
+import com.sun.net.httpserver.HttpHandler;
+import com.sun.net.httpserver.HttpServer;
+
+import org.junit.*;
+
+import java.io.*;
+import java.net.*;
+import java.nio.charset.Charset;
+import java.util.*;
+
+/** Unit tests for {@link ReverseProxyHandler}. */
+public class ReverseProxyHandlerTest {
+ private static final Charset charset = Charset.forName("UTF-8");
+
+ private HttpServer server;
+ private int port;
+ private HttpServerScope scope;
+ private HttpHandler handler;
+ private MockHttpContext context = new MockHttpContext("/proxy/");
+
+ @Before
+ public void startupServer() throws IOException {
+ server = HttpServer.create(new InetSocketAddress(0), 0);
+ port = server.getAddress().getPort();
+ server.start();
+ handler = new ReverseProxyHandler(
+ URI.create("http://localhost:" + port + "/"));
+ }
+
+ @After
+ public void shutdownServer() {
+ server.stop(0);
+ }
+
+ @Test
+ public void testGet() throws IOException {
+ byte[] response = "test response".getBytes(charset);
+ Headers goldenRequestHeaders = new Headers();
+ goldenRequestHeaders.add("Host", "localhost:" + port);
+ goldenRequestHeaders.add("X-Forwarded-For", "127.0.0.3");
+ goldenRequestHeaders.add("Accept", "text/html,text/plain,application/*");
+ goldenRequestHeaders.add("User-agent",
+ "gsa-crawler (Enterprise; E3-SOMETHING; nobody@google.com)");
+ // Added by HttpUrlConnection. Would prefer not to have them, but they are
+ // there.
+ goldenRequestHeaders.add("Cache-Control", "no-cache");
+ goldenRequestHeaders.add("Pragma", "no-cache");
+ // Connection-specific header by HttpUrlConnection. This is normal.
+ goldenRequestHeaders.add("Connection", "keep-alive");
+ Headers goldenResponseHeaders = new Headers();
+ goldenResponseHeaders.add("Date", MockHttpExchange.HEADER_DATE_VALUE);
+ goldenResponseHeaders.add("Example", "1");
+ goldenResponseHeaders.add("Example", "something2");
+ goldenResponseHeaders.add("Example", "3");
+ goldenResponseHeaders.add("Best-Header", "best_value");
+
+ Map<String, List<String>> responseHeaders
+ = new HashMap<String, List<String>>();
+ responseHeaders.put("Example", Arrays.asList("1", "something2", "3"));
+ responseHeaders.put("Best-Header", Arrays.asList("best_value"));
+ MockHttpHandler mockHandler
+ = new MockHttpHandler(200, response, responseHeaders);
+ server.createContext("/get", mockHandler);
+ MockHttpExchange ex = new MockHttpExchange("GET", "example.com",
+ "/proxy/get", context);
+ ex.getRequestHeaders().add("Accept", "text/html,text/plain,application/*");
+ ex.getRequestHeaders().add("User-agent",
+ "gsa-crawler (Enterprise; E3-SOMETHING; nobody@google.com)");
+ handler.handle(ex);
+
+ assertHeadersEquals(goldenRequestHeaders, mockHandler.getRequestHeaders());
+ assertEquals(200, ex.getResponseCode());
+ assertHeadersEquals(goldenResponseHeaders, ex.getResponseHeaders());
+ assertArrayEquals(response, ex.getResponseBytes());
+ }
+
+ @Test
+ public void testHead() throws IOException {
+ MockHttpHandler mockHandler = new MockHttpHandler(200, null);
+ server.createContext("/head", mockHandler);
+ MockHttpExchange ex = new MockHttpExchange("HEAD", "example.com",
+ "/proxy/head", context);
+ handler.handle(ex);
+
+ assertEquals(200, ex.getResponseCode());
+ assertArrayEquals(new byte[0], ex.getResponseBytes());
+ }
+
+ @Test
+ public void testPost() throws IOException {
+ byte[] request = "Are you still there?".getBytes(charset);
+ byte[] response = "Hello, world!".getBytes(charset);
+ MockHttpHandler mockHandler = new MockHttpHandler(200, response);
+ server.createContext("/post", mockHandler);
+ MockHttpExchange ex = new MockHttpExchange("POST", "example.com",
+ "/proxy/post", context);
+ ex.setRequestBody(request);
+ handler.handle(ex);
+
+ assertEquals(200, ex.getResponseCode());
+ assertArrayEquals(request, mockHandler.getRequestBytes());
+ assertArrayEquals(response, ex.getResponseBytes());
+ }
+
+ @Test
+ public void testProxyXForwardedFor() throws IOException {
+ MockHttpHandler mockHandler = new MockHttpHandler(200, null);
+ server.createContext("/get", mockHandler);
+ MockHttpExchange ex = new MockHttpExchange("HEAD", "example.com",
+ "/proxy/get", context);
+ ex.getRequestHeaders().add("X-Forwarded-For", "10.0.0.4");
+ handler.handle(ex);
+
+ assertEquals(200, ex.getResponseCode());
+ assertEquals(Arrays.asList("10.0.0.4", "127.0.0.3"),
+ mockHandler.getRequestHeaders().get("X-Forwarded-For"));
+ }
+
+ @Test
+ public void testIpv6Host() throws IOException {
+ MockHttpHandler mockHandler = new MockHttpHandler(200, null);
+ server.createContext("/get", mockHandler);
+ MockHttpExchange ex = new MockHttpExchange("HEAD", "example.com",
+ "/proxy/get", context);
+ ex.setRemoteAddress(new InetSocketAddress(
+ InetAddress.getByAddress("remotehost",
+ new byte[] {1, 2, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 15, 16}),
+ 65000));
+ handler.handle(ex);
+
+ assertEquals(200, ex.getResponseCode());
+ assertEquals(Arrays.asList("102:0:0:8:0:0:0:f10"),
+ mockHandler.getRequestHeaders().get("X-Forwarded-For"));
+ }
+
+ @Test
+ public void test500() throws IOException {
+ byte[] response = "error!".getBytes(charset);
+ MockHttpHandler mockHandler = new MockHttpHandler(500, response);
+ server.createContext("/get", mockHandler);
+ MockHttpExchange ex = new MockHttpExchange("GET", "example.com",
+ "/proxy/get", context);
+ handler.handle(ex);
+
+ assertEquals(500, ex.getResponseCode());
+ assertArrayEquals(response, ex.getResponseBytes());
+ }
+
+ @Test
+ public void test404() throws IOException {
+ byte[] response = "not there".getBytes(charset);
+ MockHttpHandler mockHandler = new MockHttpHandler(404, response);
+ server.createContext("/get", mockHandler);
+ MockHttpExchange ex = new MockHttpExchange("GET", "example.com",
+ "/proxy/get", context);
+ handler.handle(ex);
+
+ assertEquals(404, ex.getResponseCode());
+ assertArrayEquals(response, ex.getResponseBytes());
+ }
+
+ @Test
+ public void test401Post() throws IOException {
+ byte[] request = "Are you still there?".getBytes(charset);
+ byte[] response = "not authorized".getBytes(charset);
+ byte[] response2 = "authorized".getBytes(charset);
+ MockHttpHandler mockHandler = new MockHttpHandler(401, response);
+ MockHttpHandler mockHandler2 = new MockHttpHandler(200, response2);
+ HttpHandler authHandler = new AuthHandler(mockHandler, mockHandler2);
+ server.createContext("/post", authHandler);
+ MockHttpExchange ex = new MockHttpExchange("POST", "example.com",
+ "/proxy/post", context);
+ ex.setRequestBody(request);
+ Authenticator.setDefault(new Authenticator() {
+ @Override
+ protected PasswordAuthentication getPasswordAuthentication() {
+ return new PasswordAuthentication("user", "pass".toCharArray());
+ }
+ });
+ try {
+ handler.handle(ex);
+ } finally {
+ Authenticator.setDefault(null);
+ }
+
+ assertEquals(401, ex.getResponseCode());
+ // The response body is known-broken due to HttpURLConnection.
+ assertArrayEquals(new byte[0], ex.getResponseBytes());
+ // However, the headers are properly captured.
+ assertTrue(ex.getResponseHeaders().containsKey("Www-Authenticate"));
+ }
+
+ @Test
+ public void test401GetAuthenticator() throws IOException {
+ byte[] response = "not authorized".getBytes(charset);
+ byte[] response2 = "authorized".getBytes(charset);
+ MockHttpHandler mockHandler = new MockHttpHandler(401, response);
+ MockHttpHandler mockHandler2 = new MockHttpHandler(200, response2);
+ HttpHandler authHandler = new AuthHandler(mockHandler, mockHandler2);
+ server.createContext("/get", authHandler);
+ MockHttpExchange ex = new MockHttpExchange("GET", "example.com",
+ "/proxy/get", context);
+ Authenticator.setDefault(new Authenticator() {
+ @Override
+ protected PasswordAuthentication getPasswordAuthentication() {
+ return new PasswordAuthentication("user", "pass".toCharArray());
+ }
+ });
+ try {
+ handler.handle(ex);
+ } finally {
+ Authenticator.setDefault(null);
+ }
+
+ assertEquals(401, ex.getResponseCode());
+ assertArrayEquals(response, ex.getResponseBytes());
+ }
+
+ @Test
+ public void test401GetNoAuthenticator() throws IOException {
+ byte[] response = "not authorized".getBytes(charset);
+ byte[] response2 = "authorized".getBytes(charset);
+ MockHttpHandler mockHandler = new MockHttpHandler(401, response);
+ MockHttpHandler mockHandler2 = new MockHttpHandler(200, response2);
+ HttpHandler authHandler = new AuthHandler(mockHandler, mockHandler2);
+ server.createContext("/get", authHandler);
+ MockHttpExchange ex = new MockHttpExchange("GET", "example.com",
+ "/proxy/get", context);
+ Authenticator.setDefault(null);
+ handler.handle(ex);
+
+ assertEquals(401, ex.getResponseCode());
+ assertArrayEquals(response, ex.getResponseBytes());
+ }
+
+ @Test
+ public void testRedirect() throws IOException {
+ byte[] response = "test response".getBytes(charset);
+ Headers goldenResponseHeaders = new Headers();
+ goldenResponseHeaders.add("Date", MockHttpExchange.HEADER_DATE_VALUE);
+ goldenResponseHeaders.add("Location", "http://example.com");
+
+ Map<String, List<String>> responseHeaders
+ = new HashMap<String, List<String>>();
+ responseHeaders.put("Location", Arrays.asList("http://example.com"));
+ MockHttpHandler mockHandler
+ = new MockHttpHandler(307, response, responseHeaders);
+ server.createContext("/get", mockHandler);
+ MockHttpExchange ex = new MockHttpExchange("GET", "example.com",
+ "/proxy/get", context);
+ handler.handle(ex);
+
+ assertEquals(307, ex.getResponseCode());
+ assertHeadersEquals(goldenResponseHeaders, ex.getResponseHeaders());
+ assertArrayEquals(response, ex.getResponseBytes());
+ }
+
+ private static void assertHeadersEquals(Headers golden, Headers header) {
+ if (!Objects.equal(golden, header)) {
+ fail("expected:" + new TreeMap<String, List<String>>(golden)
+ + " but was:" + new TreeMap<String, List<String>>(header));
+ }
+ }
+
+ private static class AuthHandler implements HttpHandler {
+ private final HttpHandler needToAuth;
+ private final HttpHandler authed;
+
+ public AuthHandler(HttpHandler needToAuth, HttpHandler authed) {
+ this.needToAuth = needToAuth;
+ this.authed = authed;
+ }
+
+ @Override
+ public void handle(HttpExchange ex) throws IOException {
+ if (ex.getRequestHeaders().containsKey("Authorization")) {
+ authed.handle(ex);
+ } else {
+ ex.getResponseHeaders().add("WWW-Authenticate", "Basic realm=\"test\"");
+ needToAuth.handle(ex);
+ }
+ }
+ }
+}