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);
+      }
+    }
+  }
+}