@Test
  public void testClientExcludedHosts() throws Exception {
    prepareProxy();
    prepareServer(
        new HttpServlet() {
          @Override
          protected void doGet(HttpServletRequest req, HttpServletResponse resp)
              throws ServletException, IOException {
            if (req.getHeader("Via") != null) resp.addHeader(PROXIED_HEADER, "true");
          }
        });
    int port = serverConnector.getLocalPort();
    client
        .getProxyConfiguration()
        .getProxies()
        .get(0)
        .getExcludedAddresses()
        .add("127.0.0.1:" + port);

    // Try with a proxied host
    ContentResponse response =
        client.newRequest("localhost", port).timeout(5, TimeUnit.SECONDS).send();
    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).timeout(5, TimeUnit.SECONDS).send();
    Assert.assertEquals(200, response.getStatus());
    Assert.assertFalse(response.getHeaders().containsKey(PROXIED_HEADER));
  }
  /** @throws Exception */
  public void testSessionRenewal() throws Exception {
    String contextPath = "";
    String servletMapping = "/server";
    int maxInactive = 1;
    int scavengePeriod = 3;
    _server = createServer(0, maxInactive, scavengePeriod, SessionCache.NEVER_EVICT);
    WebAppContext context = _server.addWebAppContext(".", contextPath);
    context.setParentLoaderPriority(true);
    context.addServlet(TestServlet.class, servletMapping);
    TestHttpSessionIdListener testListener = new TestHttpSessionIdListener();
    context.addEventListener(testListener);

    HttpClient client = new HttpClient();
    try {
      _server.start();
      int port = _server.getPort();

      client.start();

      // make a request to create a session
      ContentResponse response =
          client.GET("http://localhost:" + port + contextPath + servletMapping + "?action=create");
      assertEquals(HttpServletResponse.SC_OK, response.getStatus());

      String sessionCookie = response.getHeaders().get("Set-Cookie");
      assertTrue(sessionCookie != null);
      assertFalse(testListener.isCalled());

      // make a request to change the sessionid
      Request request =
          client.newRequest(
              "http://localhost:" + port + contextPath + servletMapping + "?action=renew");
      request.header("Cookie", sessionCookie);
      ContentResponse renewResponse = request.send();

      assertEquals(HttpServletResponse.SC_OK, renewResponse.getStatus());
      String renewSessionCookie = renewResponse.getHeaders().get("Set-Cookie");
      assertNotNull(renewSessionCookie);
      assertNotSame(sessionCookie, renewSessionCookie);
      assertTrue(testListener.isCalled());

      assertTrue(
          verifyChange(
              context,
              AbstractTestServer.extractSessionId(sessionCookie),
              AbstractTestServer.extractSessionId(renewSessionCookie)));
    } finally {
      client.stop();
      _server.stop();
    }
  }
  @Test
  public void testProxyWithBigRequestContentConsumed() throws Exception {
    final byte[] content = new byte[128 * 1024];
    new Random().nextBytes(content);

    prepareProxy();
    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();
            int index = 0;
            while (true) {
              int value = input.read();
              if (value < 0) break;
              Assert.assertEquals(
                  "Content mismatch at index=" + index, content[index] & 0xFF, value);
              ++index;
            }
          }
        });

    ContentResponse response =
        client
            .newRequest("localhost", serverConnector.getLocalPort())
            .method(HttpMethod.POST)
            .content(new BytesContentProvider(content))
            .timeout(5, TimeUnit.SECONDS)
            .send();

    Assert.assertEquals(200, response.getStatus());
    Assert.assertTrue(response.getHeaders().containsKey(PROXIED_HEADER));
  }
  @Test
  public void testProxyWithRequestContentAndResponseContent() throws Exception {
    prepareProxy();
    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];
    new Random().nextBytes(content);
    ContentResponse response =
        client
            .newRequest("localhost", serverConnector.getLocalPort())
            .method(HttpMethod.POST)
            .content(new BytesContentProvider(content))
            .timeout(5, TimeUnit.SECONDS)
            .send();

    Assert.assertEquals(200, response.getStatus());
    Assert.assertTrue(response.getHeaders().containsKey(PROXIED_HEADER));
    Assert.assertArrayEquals(content, response.getContent());
  }
  @Test
  public void testResponseHeadersAreNotRemoved() throws Exception {
    prepareProxy();
    proxyContext.stop();
    final String headerName = "X-Test";
    final String headerValue = "test-value";
    proxyContext.addFilter(
        new FilterHolder(
            new Filter() {
              @Override
              public void init(FilterConfig filterConfig) throws ServletException {}

              @Override
              public void doFilter(
                  ServletRequest request, ServletResponse response, FilterChain chain)
                  throws IOException, ServletException {
                ((HttpServletResponse) response).addHeader(headerName, headerValue);
                chain.doFilter(request, response);
              }

              @Override
              public void destroy() {}
            }),
        "/*",
        EnumSet.of(DispatcherType.REQUEST));
    proxyContext.start();
    prepareServer(new EmptyHttpServlet());

    HttpClient client = prepareClient();
    ContentResponse response =
        client.newRequest("localhost", serverConnector.getLocalPort()).send();

    Assert.assertEquals(200, response.getStatus());
    Assert.assertEquals(headerValue, response.getHeaders().get(headerName));
  }
  @Override
  public ClientResponse apply(final ClientRequest jerseyRequest) throws ProcessingException {
    final Request jettyRequest = translateRequest(jerseyRequest);
    final Map<String, String> clientHeadersSnapshot =
        writeOutBoundHeaders(jerseyRequest.getHeaders(), jettyRequest);
    final ContentProvider entity = getBytesProvider(jerseyRequest);
    if (entity != null) {
      jettyRequest.content(entity);
    }

    try {
      final ContentResponse jettyResponse = jettyRequest.send();
      HeaderUtils.checkHeaderChanges(
          clientHeadersSnapshot,
          jerseyRequest.getHeaders(),
          JettyConnector.this.getClass().getName());

      final javax.ws.rs.core.Response.StatusType status =
          jettyResponse.getReason() == null
              ? Statuses.from(jettyResponse.getStatus())
              : Statuses.from(jettyResponse.getStatus(), jettyResponse.getReason());

      final ClientResponse jerseyResponse = new ClientResponse(status, jerseyRequest);
      processResponseHeaders(jettyResponse.getHeaders(), jerseyResponse);
      try {
        jerseyResponse.setEntityStream(new HttpClientResponseInputStream(jettyResponse));
      } catch (final IOException e) {
        LOGGER.log(Level.SEVERE, null, e);
      }

      return jerseyResponse;
    } catch (final Exception e) {
      throw new ProcessingException(e);
    }
  }
  @Test
  public void testGZIPContentIsProxied() throws Exception {
    final byte[] content = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
    prepareProxy();
    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.addHeader("Content-Encoding", "gzip");
            GZIPOutputStream gzipOutputStream = new GZIPOutputStream(resp.getOutputStream());
            gzipOutputStream.write(content);
            gzipOutputStream.close();
          }
        });

    ContentResponse response =
        client
            .newRequest("localhost", serverConnector.getLocalPort())
            .timeout(5, TimeUnit.SECONDS)
            .send();
    Assert.assertEquals(200, response.getStatus());
    Assert.assertTrue(response.getHeaders().containsKey(PROXIED_HEADER));
    Assert.assertArrayEquals(content, response.getContent());
  }
  @Test
  public void testTransparentProxyWithoutPrefix() 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);
          }
        });

    final String proxyTo = "http://localhost:" + serverConnector.getLocalPort();
    proxyServlet = new ProxyServlet.Transparent();
    Map<String, String> initParams = new HashMap<>();
    initParams.put("proxyTo", proxyTo);
    prepareProxy(initParams);

    // Make the request to the proxy, it should transparently forward to the server
    ContentResponse response =
        client
            .newRequest("localhost", proxyConnector.getLocalPort())
            .path(target)
            .timeout(5, TimeUnit.SECONDS)
            .send();
    Assert.assertEquals(200, response.getStatus());
    Assert.assertTrue(response.getHeaders().containsKey(PROXIED_HEADER));
  }
  @Test
  public void testSessionMigration() throws Exception {
    String contextPath = "";
    String servletMapping = "/server";
    AbstractTestServer server1 = createServer(0);
    server1.addContext(contextPath).addServlet(TestServlet.class, servletMapping);

    try {
      server1.start();
      int port1 = server1.getPort();

      AbstractTestServer server2 = createServer(0);
      server2.addContext(contextPath).addServlet(TestServlet.class, servletMapping);

      try {
        server2.start();
        int port2 = server2.getPort();

        HttpClient client = new HttpClient();
        client.start();
        try {
          // Perform one request to server1 to create a session
          int value = 1;
          Request request1 =
              client.POST(
                  "http://localhost:"
                      + port1
                      + contextPath
                      + servletMapping
                      + "?action=set&value="
                      + value);
          ContentResponse response1 = request1.send();
          assertEquals(HttpServletResponse.SC_OK, response1.getStatus());
          String sessionCookie = response1.getHeaders().get("Set-Cookie");
          assertTrue(sessionCookie != null);
          // Mangle the cookie, replacing Path with $Path, etc.
          sessionCookie = sessionCookie.replaceFirst("(\\W)(P|p)ath=", "$1\\$Path=");

          // Perform a request to server2 using the session cookie from the previous request
          // This should migrate the session from server1 to server2.
          Request request2 =
              client.newRequest(
                  "http://localhost:" + port2 + contextPath + servletMapping + "?action=get");
          request2.header("Cookie", sessionCookie);
          ContentResponse response2 = request2.send();
          assertEquals(HttpServletResponse.SC_OK, response2.getStatus());
          String response = response2.getContentAsString();
          assertEquals(response.trim(), String.valueOf(value));
        } finally {
          client.stop();
        }
      } finally {
        server2.stop();
      }
    } finally {
      server1.stop();
    }
  }
  @Test
  public void testAttributeNamesWithDots() throws Exception {
    String contextPath = "";
    String servletMapping = "/server";
    int maxInactivePeriod = 10000;
    int scavengePeriod = 20000;
    AbstractTestServer server1 = createServer(0, maxInactivePeriod, scavengePeriod);
    server1.addContext(contextPath).addServlet(TestServlet.class, servletMapping);
    server1.start();
    int port1 = server1.getPort();

    AbstractTestServer server2 = createServer(0, maxInactivePeriod, scavengePeriod);
    server2.addContext(contextPath).addServlet(TestServlet.class, servletMapping);
    server2.start();
    int port2 = server2.getPort();

    try {

      HttpClient client = new HttpClient();
      client.start();
      try {

        // Perform one request to server1 to create a session with attribute with dotted name
        ContentResponse response =
            client.GET("http://localhost:" + port1 + contextPath + servletMapping + "?action=init");

        assertEquals(HttpServletResponse.SC_OK, response.getStatus());

        String resp = response.getContentAsString();

        String[] sessionTestResponse = resp.split("/");
        assertEquals("a.b.c", sessionTestResponse[0]);

        String sessionCookie = response.getHeaders().get(HttpHeader.SET_COOKIE);

        assertTrue(sessionCookie != null);
        // Mangle the cookie, replacing Path with $Path, etc.
        sessionCookie = sessionCookie.replaceFirst("(\\W)(P|p)ath=", "$1\\$Path=");

        // Make a request to the 2nd server which will do a refresh, use TestServlet to ensure that
        // the
        // session attribute with dotted name is not removed
        Request request2 =
            client.newRequest(
                "http://localhost:" + port2 + contextPath + servletMapping + "?action=get");
        request2.header("Cookie", sessionCookie);
        ContentResponse response2 = request2.send();
        assertEquals(HttpServletResponse.SC_OK, response2.getStatus());

      } finally {
        client.stop();
      }
    } finally {
      server1.stop();
      server2.stop();
    }
  }
  /**
   * If nodeA creates a session, and just afterwards crashes, it is the only node that knows about
   * the session. We want to test that the session data is gone after scavenging.
   */
  @Test
  public void testOrphanedSession() throws Exception {
    // Disable scavenging for the first server, so that we simulate its "crash".
    String contextPath = "";
    String servletMapping = "/server";
    int inactivePeriod = 5;
    AbstractTestServer server1 = createServer(0, inactivePeriod, -1);
    server1.addContext(contextPath).addServlet(TestServlet.class, servletMapping);
    try {
      server1.start();
      int port1 = server1.getPort();
      int scavengePeriod = 2;
      AbstractTestServer server2 = createServer(0, inactivePeriod, scavengePeriod);
      server2.addContext(contextPath).addServlet(TestServlet.class, servletMapping);
      try {
        server2.start();
        int port2 = server2.getPort();
        HttpClient client = new HttpClient();
        client.start();
        try {
          // Connect to server1 to create a session and get its session cookie
          ContentResponse response1 =
              client.GET(
                  "http://localhost:" + port1 + contextPath + servletMapping + "?action=init");
          assertEquals(HttpServletResponse.SC_OK, response1.getStatus());
          String sessionCookie = response1.getHeaders().getStringField("Set-Cookie");
          assertTrue(sessionCookie != null);
          // Mangle the cookie, replacing Path with $Path, etc.
          sessionCookie = sessionCookie.replaceFirst("(\\W)(P|p)ath=", "$1\\$Path=");

          // Wait for the session to expire.
          // The first node does not do any scavenging, but the session
          // must be removed by scavenging done in the other node.
          Thread.sleep(TimeUnit.SECONDS.toMillis(inactivePeriod + 2L * scavengePeriod));

          // Perform one request to server2 to be sure that the session has been expired
          Request request =
              client.newRequest(
                  "http://localhost:" + port2 + contextPath + servletMapping + "?action=check");
          request.header("Cookie", sessionCookie);
          ContentResponse response2 = request.send();
          assertEquals(HttpServletResponse.SC_OK, response2.getStatus());
        } finally {
          client.stop();
        }
      } finally {
        server2.stop();
      }
    } finally {
      server1.stop();
    }
  }
  @Test
  @Ignore("failing because an http cookie with null value is coming over as \"null\"")
  public void testSessionCookie() throws Exception {
    String contextPath = "";
    String servletMapping = "/server";
    int scavengePeriod = 3;
    AbstractTestServer server = createServer(0, 1, scavengePeriod);
    ServletContextHandler context = server.addContext(contextPath);
    context.addServlet(TestServlet.class, servletMapping);

    try {
      server.start();
      int port = server.getPort();

      HttpClient client = new HttpClient();
      client.start();
      try {

        ContentResponse response =
            client.GET(
                "http://localhost:" + port + contextPath + servletMapping + "?action=create");
        assertEquals(HttpServletResponse.SC_OK, response.getStatus());

        String sessionCookie = response.getHeaders().get("Set-Cookie");
        assertTrue(sessionCookie != null);
        // Mangle the cookie, replacing Path with $Path, etc.
        // sessionCookie = sessionCookie.replaceFirst("(\\W)(P|p)ath=", "$1\\$Path=");

        // Let's wait for the scavenger to run, waiting 2.5 times the scavenger period
        // pause(scavengePeriod);
        Request request =
            client.newRequest(
                "http://localhost:" + port + contextPath + servletMapping + "?action=check-cookie");
        request.header("Cookie", sessionCookie);
        response = request.send();

        assertEquals(HttpServletResponse.SC_OK, response.getStatus());

        request =
            client.newRequest(
                "http://localhost:" + port + contextPath + servletMapping + "?action=null-cookie");
        request.header("Cookie", sessionCookie);
        response = request.send();
        assertEquals(HttpServletResponse.SC_OK, response.getStatus());
      } finally {
        client.stop();
      }
    } finally {
      server.stop();
    }
  }
  @Test
  public void testProxyWithoutContent() throws Exception {
    prepareProxy();
    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())
            .timeout(5, TimeUnit.SECONDS)
            .send();

    Assert.assertEquals("OK", response.getReason());
    Assert.assertEquals(200, response.getStatus());
    Assert.assertTrue(response.getHeaders().containsKey(PROXIED_HEADER));
  }
  @Test
  public void testTransparentProxyWithQueryWithSpaces() throws Exception {
    final String target = "/test";
    final String query = "a=1&b=2&c=1234%205678&d=hello+world";
    prepareServer(
        new HttpServlet() {
          @Override
          protected void doGet(HttpServletRequest req, HttpServletResponse resp)
              throws ServletException, IOException {
            if (req.getHeader("Via") != null) resp.addHeader(PROXIED_HEADER, "true");

            if (target.equals(req.getRequestURI())) {
              if (query.equals(req.getQueryString())) {
                resp.setStatus(200);
                return;
              }
            }
            resp.setStatus(404);
          }
        });

    String proxyTo = "http://localhost:" + serverConnector.getLocalPort();
    String prefix = "/proxy";
    proxyServlet = new ProxyServlet.Transparent();
    Map<String, String> params = new HashMap<>();
    params.put("proxyTo", proxyTo);
    params.put("prefix", prefix);
    prepareProxy(params);

    // Make the request to the proxy, it should transparently forward to the server
    ContentResponse response =
        client
            .newRequest("localhost", proxyConnector.getLocalPort())
            .path(prefix + target + "?" + query)
            .timeout(5, TimeUnit.SECONDS)
            .send();
    Assert.assertEquals(200, response.getStatus());
    Assert.assertTrue(response.getHeaders().containsKey(PROXIED_HEADER));
  }
  @Test
  public void testCookiesFromDifferentClientsAreNotMixed() throws Exception {
    final String name = "biscuit";
    prepareProxy();
    prepareServer(
        new HttpServlet() {
          @Override
          protected void doGet(HttpServletRequest req, HttpServletResponse resp)
              throws ServletException, IOException {
            if (req.getHeader("Via") != null) resp.addHeader(PROXIED_HEADER, "true");

            String value = req.getHeader(name);
            if (value != null) {
              Cookie cookie = new Cookie(name, value);
              cookie.setMaxAge(3600);
              resp.addCookie(cookie);
            } else {
              Cookie[] cookies = req.getCookies();
              Assert.assertEquals(1, cookies.length);
            }
          }
        });

    String value1 = "1";
    ContentResponse response1 =
        client
            .newRequest("localhost", serverConnector.getLocalPort())
            .header(name, value1)
            .timeout(5, TimeUnit.SECONDS)
            .send();
    Assert.assertEquals(200, response1.getStatus());
    Assert.assertTrue(response1.getHeaders().containsKey(PROXIED_HEADER));
    List<HttpCookie> cookies = client.getCookieStore().getCookies();
    Assert.assertEquals(1, cookies.size());
    Assert.assertEquals(name, cookies.get(0).getName());
    Assert.assertEquals(value1, cookies.get(0).getValue());

    HttpClient client2 = prepareClient();
    String value2 = "2";
    ContentResponse response2 =
        client2
            .newRequest("localhost", serverConnector.getLocalPort())
            .header(name, value2)
            .timeout(5, TimeUnit.SECONDS)
            .send();
    Assert.assertEquals(200, response2.getStatus());
    Assert.assertTrue(response2.getHeaders().containsKey(PROXIED_HEADER));
    cookies = client2.getCookieStore().getCookies();
    Assert.assertEquals(1, cookies.size());
    Assert.assertEquals(name, cookies.get(0).getName());
    Assert.assertEquals(value2, cookies.get(0).getValue());

    // Make a third request to be sure the proxy does not mix cookies
    ContentResponse response3 =
        client
            .newRequest("localhost", serverConnector.getLocalPort())
            .timeout(5, TimeUnit.SECONDS)
            .send();
    Assert.assertEquals(200, response3.getStatus());
    Assert.assertTrue(response3.getHeaders().containsKey(PROXIED_HEADER));
  }
  @Test
  public void testInvalidation() throws Exception {
    String contextPath = "";
    String servletMapping = "/server";
    AbstractTestServer server1 = createServer(0);
    server1.addContext(contextPath).addServlet(TestServlet.class, servletMapping);
    server1.start();
    int port1 = server1.getPort();
    System.err.println("Port1=" + port1);
    try {
      AbstractTestServer server2 = createServer(0);
      server2.addContext(contextPath).addServlet(TestServlet.class, servletMapping);
      server2.start();
      int port2 = server2.getPort();
      System.err.println("port2=" + port2);
      try {
        HttpClient client = new HttpClient();
        QueuedThreadPool executor = new QueuedThreadPool();
        client.setExecutor(executor);
        client.start();
        try {
          String[] urls = new String[2];
          urls[0] = "http://localhost:" + port1 + contextPath + servletMapping;
          urls[1] = "http://localhost:" + port2 + contextPath + servletMapping;

          // Create the session on node1
          ContentResponse response1 = client.GET(urls[0] + "?action=init");

          assertEquals(HttpServletResponse.SC_OK, response1.getStatus());
          String sessionCookie = response1.getHeaders().getStringField("Set-Cookie");
          assertTrue(sessionCookie != null);
          // Mangle the cookie, replacing Path with $Path, etc.
          sessionCookie = sessionCookie.replaceFirst("(\\W)(P|p)ath=", "$1\\$Path=");

          // Be sure the session is also present in node2

          Request request2 = client.newRequest(urls[1] + "?action=increment");
          request2.header("Cookie", sessionCookie);
          ContentResponse response2 = request2.send();
          assertEquals(HttpServletResponse.SC_OK, response2.getStatus());

          // Invalidate on node1
          Request request1 = client.newRequest(urls[0] + "?action=invalidate");
          request1.header("Cookie", sessionCookie);
          response1 = request1.send();
          assertEquals(HttpServletResponse.SC_OK, response1.getStatus());

          pause();

          // Be sure on node2 we don't see the session anymore
          request2 = client.newRequest(urls[1] + "?action=test");
          request2.header("Cookie", sessionCookie);
          response2 = request2.send();
          assertEquals(HttpServletResponse.SC_OK, response2.getStatus());
        } finally {
          client.stop();
        }
      } finally {
        server2.stop();
      }
    } finally {
      server1.stop();
    }
  }
  @Test
  public void testRemoveSession() throws Exception {
    String contextPath = "";
    String servletMapping = "/server";
    int scavengePeriod = 3;
    AbstractTestServer server = createServer(0, 1, scavengePeriod);
    ServletContextHandler context = server.addContext(contextPath);
    context.addServlet(TestServlet.class, servletMapping);
    TestEventListener testListener = new TestEventListener();
    context.getSessionHandler().addEventListener(testListener);
    AbstractSessionManager m =
        (AbstractSessionManager) context.getSessionHandler().getSessionManager();
    try {
      server.start();
      int port = server.getPort();

      HttpClient client = new HttpClient();
      client.start();
      try {
        ContentResponse response =
            client.GET(
                "http://localhost:" + port + contextPath + servletMapping + "?action=create");
        assertEquals(HttpServletResponse.SC_OK, response.getStatus());
        String sessionCookie = response.getHeaders().get("Set-Cookie");
        assertTrue(sessionCookie != null);
        // Mangle the cookie, replacing Path with $Path, etc.
        sessionCookie = sessionCookie.replaceFirst("(\\W)(P|p)ath=", "$1\\$Path=");
        // ensure sessionCreated listener is called
        assertTrue(testListener.isCreated());
        assertEquals(1, m.getSessions());
        assertEquals(1, m.getSessionsMax());
        assertEquals(1, m.getSessionsTotal());

        // now delete the session
        Request request =
            client.newRequest(
                "http://localhost:" + port + contextPath + servletMapping + "?action=delete");
        request.header("Cookie", sessionCookie);
        response = request.send();
        assertEquals(HttpServletResponse.SC_OK, response.getStatus());
        // ensure sessionDestroyed listener is called
        assertTrue(testListener.isDestroyed());
        assertEquals(0, m.getSessions());
        assertEquals(1, m.getSessionsMax());
        assertEquals(1, m.getSessionsTotal());

        // The session is not there anymore, even if we present an old cookie
        request =
            client.newRequest(
                "http://localhost:" + port + contextPath + servletMapping + "?action=check");
        request.header("Cookie", sessionCookie);
        response = request.send();
        assertEquals(HttpServletResponse.SC_OK, response.getStatus());
        assertEquals(0, m.getSessions());
        assertEquals(1, m.getSessionsMax());
        assertEquals(1, m.getSessionsTotal());
      } finally {
        client.stop();
      }
    } finally {
      server.stop();
    }
  }
  @Test
  public void testLastAccessTime() throws Exception {
    String contextPath = "";
    String servletMapping = "/server";
    int maxInactivePeriod = 8; // session will timeout after 8 seconds
    int scavengePeriod = 2; // scavenging occurs every 2 seconds
    AbstractTestServer server1 = createServer(0, maxInactivePeriod, scavengePeriod);
    TestServlet servlet1 = new TestServlet();
    ServletHolder holder1 = new ServletHolder(servlet1);
    ServletContextHandler context = server1.addContext(contextPath);
    TestSessionListener listener1 = new TestSessionListener();
    context.addEventListener(listener1);
    context.addServlet(holder1, servletMapping);
    server1.start();
    int port1 = server1.getPort();
    try {
      AbstractTestServer server2 = createServer(0, maxInactivePeriod, scavengePeriod);
      server2.addContext(contextPath).addServlet(TestServlet.class, servletMapping);
      server2.start();
      int port2 = server2.getPort();
      try {
        HttpClient client = new HttpClient();
        client.start();
        try {
          // Perform one request to server1 to create a session
          Future<ContentResponse> future =
              client.GET(
                  "http://localhost:" + port1 + contextPath + servletMapping + "?action=init");
          ContentResponse response1 = future.get();
          assertEquals(HttpServletResponse.SC_OK, response1.getStatus());
          assertEquals("test", response1.getContentAsString());
          String sessionCookie = response1.getHeaders().getStringField("Set-Cookie");
          assertTrue(sessionCookie != null);
          // Mangle the cookie, replacing Path with $Path, etc.
          sessionCookie = sessionCookie.replaceFirst("(\\W)(P|p)ath=", "$1\\$Path=");

          // Perform some request to server2 using the session cookie from the previous request
          // This should migrate the session from server1 to server2, and leave server1's
          // session in a very stale state, while server2 has a very fresh session.
          // We want to test that optimizations done to the saving of the shared lastAccessTime
          // do not break the correct working
          int requestInterval = 500;
          for (int i = 0; i < maxInactivePeriod * (1000 / requestInterval); ++i) {
            Request request =
                client.newRequest("http://localhost:" + port2 + contextPath + servletMapping);
            request.header("Cookie", sessionCookie);
            future = request.send();
            ContentResponse response2 = future.get();
            assertEquals(HttpServletResponse.SC_OK, response2.getStatus());
            assertEquals("test", response2.getContentAsString());

            String setCookie = response2.getHeaders().getStringField("Set-Cookie");
            if (setCookie != null)
              sessionCookie = setCookie.replaceFirst("(\\W)(P|p)ath=", "$1\\$Path=");

            Thread.sleep(requestInterval);
          }

          // At this point, session1 should be eligible for expiration.
          // Let's wait for the scavenger to run, waiting 2.5 times the scavenger period
          Thread.sleep(scavengePeriod * 2500L);

          // check that the session was not scavenged over on server1 by ensuring that the
          // SessionListener destroy method wasn't called
          assertTrue(listener1.destroyed == false);
        } finally {
          client.stop();
        }
      } finally {
        server2.stop();
      }
    } finally {
      server1.stop();
    }
  }
  @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 doGet(HttpServletRequest req, HttpServletResponse resp)
              throws ServletException, IOException {
            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 =
        new ProxyServlet() {
          private Map<String, ContentResponse> cache = new HashMap<>();
          private Map<String, ByteArrayOutputStream> temp = new HashMap<>();

          @Override
          protected void service(HttpServletRequest request, HttpServletResponse response)
              throws ServletException, IOException {
            ContentResponse cachedResponse = cache.get(request.getRequestURI());
            if (cachedResponse != null) {
              response.setStatus(cachedResponse.getStatus());
              // Should copy headers too, but keep it simple
              response.addHeader(cacheHeader, "true");
              response.getOutputStream().write(cachedResponse.getContent());
            } else {
              super.service(request, response);
            }
          }

          @Override
          protected void onResponseContent(
              HttpServletRequest request,
              HttpServletResponse response,
              Response proxyResponse,
              byte[] buffer,
              int offset,
              int length,
              Callback callback) {
            // 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, callback);
          }

          @Override
          protected void onResponseSuccess(
              HttpServletRequest request, HttpServletResponse response, Response proxyResponse) {
            byte[] content = temp.remove(request.getRequestURI()).toByteArray();
            ContentResponse cached = new HttpContentResponse(proxyResponse, content, null, null);
            cache.put(request.getRequestURI(), cached);
            super.onResponseSuccess(request, response, proxyResponse);
          }
        };
    prepareProxy();

    // First request
    ContentResponse response =
        client
            .newRequest("localhost", serverConnector.getLocalPort())
            .timeout(5, TimeUnit.SECONDS)
            .send();
    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())
            .timeout(5, TimeUnit.SECONDS)
            .send();
    Assert.assertEquals(200, response.getStatus());
    Assert.assertTrue(response.getHeaders().containsKey(cacheHeader));
    Assert.assertArrayEquals(content, response.getContent());
  }