code: // Licensed to the Software Freedom Conservancy (SFC) under one
  // or more contributor license agreements.  See the NOTICE file
  // distributed with this work for additional information
  // regarding copyright ownership.  The SFC licenses this file
  // to you 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 org.openqa.selenium;
  
  import static com.google.common.net.HttpHeaders.REFERER;
  import static java.nio.charset.StandardCharsets.US_ASCII;
  import static java.nio.charset.StandardCharsets.UTF_8;
  import static org.assertj.core.api.Assertions.assertThat;
  import static org.assertj.core.api.Assumptions.assumeThat;
  import static org.openqa.selenium.build.InProject.locate;
  import static org.openqa.selenium.remote.CapabilityType.PROXY;
  import static org.openqa.selenium.support.ui.ExpectedConditions.presenceOfElementLocated;
  import static org.openqa.selenium.support.ui.ExpectedConditions.titleIs;
  import static org.openqa.selenium.testing.Safely.safelyCall;
  
  import com.google.common.net.HostAndPort;
  import java.io.IOException;
  import java.io.UncheckedIOException;
  import java.io.UnsupportedEncodingException;
  import java.net.URI;
  import java.net.URLEncoder;
  import java.nio.file.Files;
  import java.time.Duration;
  import java.util.List;
  import java.util.Objects;
  import java.util.concurrent.CopyOnWriteArrayList;
  import java.util.stream.Collectors;
  import org.junit.jupiter.api.AfterEach;
  import org.junit.jupiter.api.BeforeAll;
  import org.junit.jupiter.api.BeforeEach;
  import org.junit.jupiter.api.Test;
  import org.junit.jupiter.api.extension.RegisterExtension;
  import org.openqa.selenium.environment.webserver.AppServer;
  import org.openqa.selenium.environment.webserver.NettyAppServer;
  import org.openqa.selenium.remote.http.Contents;
  import org.openqa.selenium.remote.http.HttpHandler;
  import org.openqa.selenium.remote.http.HttpRequest;
  import org.openqa.selenium.remote.http.HttpResponse;
  import org.openqa.selenium.support.ui.WebDriverWait;
  import org.openqa.selenium.testing.SeleniumExtension;
  
  /**
   * Tests that "Referer" headers are generated as expected under various conditions. Each test will
   * perform the following steps in the browser:
   *
   * <ol>
   *   <li>navigate to page 1
   *   <li>click a link to page 2
   *   <li>click another link to page 3
   *   <li>click a link to go back to page 2
   *   <li>click a link to go forward to page 3
   * </ol>
   *
   * <p>After performing the steps above, the test will check that the test server(s) recorded the
   * expected HTTP requests. For each step, the tests expect:
   *
   * <ol>
   *   <li>a request for page1; no Referer header
   *   <li>a request for page2; Referer: $absolute-url-for-page1
   *   <li>a request for page3; Referer: $absolute-url-for-page2
   *   <li>no request
   *   <li>no request
   * </ol>
   *
   * <p>Note: depending on the condition under test, the various pages may or may not be served by the
   * same server.
   */
  class ReferrerTest {
  
    @RegisterExtension static SeleniumExtension seleniumExtension = new SeleniumExtension();
  
    private static final String PAGE_1 = "/page1.html";
    private static final String PAGE_2 = "/page2.html";
    private static final String PAGE_3 = "/page3.html";
    private static String page1;
    private static String page2;
    private static String page3;
    private TestServer server1;
    private TestServer server2;
    private ProxyServer proxyServer;
  
    @BeforeAll
    public static void shouldTestBeRunAtAll() {
      assumeThat(Boolean.getBoolean("selenium.skiptest")).isFalse();
    }
  
    @BeforeAll
    public static void readContents() throws IOException {
      page1 = new String(Files.readAllBytes(locate("common/src/web/proxy" + PAGE_1)));
      page2 = new String(Files.readAllBytes(locate("common/src/web/proxy" + PAGE_2)));
      page3 = new String(Files.readAllBytes(locate("common/src/web/proxy/page3.html")));
    }
  
    @BeforeEach
    public void startServers() {
      server1 = new TestServer();
      server2 = new TestServer();
      proxyServer = new ProxyServer();
    }
  
    @AfterEach
    public void stopServers() {
      safelyCall(() -> proxyServer.stop());
      safelyCall(() -> server1.stop());
      safelyCall(() -> server2.stop());
    }
  
    private WebDriver createDriver(String pacUrl) {
      Proxy proxy = new Proxy();
      proxy.setProxyAutoconfigUrl(pacUrl);
  
      Capabilities caps = new ImmutableCapabilities(PROXY, proxy);
  
      return seleniumExtension.createNewDriver(caps);
    }
  
    /**
     * Tests navigation when all of the files are hosted on the same domain and the browser does not
     * have a proxy configured.
     */
    @Test
    void basicHistoryNavigationWithoutAProxy() {
      String page1Url = server1.whereIs(PAGE_1 + "?next=" + encode(server1.whereIs(PAGE_2)));
      String page2Url = server1.whereIs(PAGE_2 + "?next=" + encode(server1.whereIs(PAGE_3)));
  
      performNavigation(seleniumExtension.getDriver(), page1Url);
  
      assertThat(server1.getRequests())
          .containsExactly(
              new ExpectedRequest(PAGE_1, null),
              new ExpectedRequest(PAGE_2, page1Url),
              new ExpectedRequest(PAGE_3, page2Url));
    }
  
    /**
     * Tests navigation when all of the files are hosted on the same domain and the browser is
     * configured to use a proxy that permits direct access to that domain.
     */
    @Test
    void basicHistoryNavigationWithADirectProxy() {
      proxyServer.setPacFileContents("function FindProxyForURL(url, host) { return 'DIRECT'; }");
      WebDriver driver = createDriver(proxyServer.whereIs("/pac.js"));
  
      String page1Url = server1.whereIs(PAGE_1) + "?next=" + encode(server1.whereIs(PAGE_2));
      String page2Url = server1.whereIs(PAGE_2) + "?next=" + encode(server1.whereIs(PAGE_3));
  
      performNavigation(driver, page1Url);
  
      assertThat(server1.getRequests())
          .containsExactly(
              new ExpectedRequest(PAGE_1, null),
              new ExpectedRequest(PAGE_2, page1Url),
              new ExpectedRequest(PAGE_3, page2Url));
    }
  
    private static String encode(String url) {
      try {
        return URLEncoder.encode(url, UTF_8.name());
      } catch (UnsupportedEncodingException e) {
        throw new RuntimeException("UTF-8 should always be supported!", e);
      }
    }
  
    private void performNavigation(WebDriver driver, String firstUrl) {
      WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(5));
  
      driver.get(firstUrl);
      wait.until(titleIs("Page 1"));
      wait.until(presenceOfElementLocated(By.id("next"))).click();
  
      wait.until(titleIs("Page 2"));
      wait.until(presenceOfElementLocated(By.id("next"))).click();
  
      wait.until(titleIs("Page 3"));
      wait.until(presenceOfElementLocated(By.id("back"))).click();
  
      wait.until(titleIs("Page 2"));
      wait.until(presenceOfElementLocated(By.id("forward"))).click();
  
      wait.until(titleIs("Page 3"));
    }
  
    private static class ExpectedRequest {
      private final String uri;
      private final String referer;
  
      public ExpectedRequest(HttpRequest request) {
        this(request.getUri(), request.getHeader(REFERER));
      }
  
      public ExpectedRequest(String uri, String referer) {
        this.uri = uri;
        this.referer = referer;
      }
  
      @Override
      public String toString() {
        return "ExpectedRequest{" + "uri='" + uri + '\'' + ", referer='" + referer + '\'' + '}';
      }
  
      @Override
      public boolean equals(Object o) {
        if (!(o instanceof ExpectedRequest)) {
          return false;
        }
        ExpectedRequest that = (ExpectedRequest) o;
        return this.uri.equals(that.uri) && Objects.equals(this.referer, that.referer);
      }
  
      @Override
      public int hashCode() {
        return Objects.hash(uri, referer);
      }
    }
  
    private static class RecordingHandler implements HttpHandler {
  
      private final List<ExpectedRequest> requests = new CopyOnWriteArrayList<>();
  
      @Override
      public HttpResponse execute(HttpRequest req) throws UncheckedIOException {
        if (req.getUri().endsWith("/favicon.ico")) {
          return new HttpResponse().setStatus(204);
        }
  
        // Don't record / requests so we can poll the server for availability in start().
        if (!"/".equals(req.getUri())) {
          requests.add(new ExpectedRequest(req));
        }
  
        String responseHtml;
        if (req.getUri().contains(PAGE_1)) {
          responseHtml = page1;
        } else if (req.getUri().contains(PAGE_2)) {
          responseHtml = page2;
        } else {
          responseHtml = page3;
        }
  
        return new HttpResponse()
            .setHeader("Content-Type", "text/html; charset=utf-8")
            .setContent(Contents.utf8String(responseHtml));
      }
  
      public List<ExpectedRequest> getRequests() {
        return requests.stream().collect(Collectors.toList());
      }
    }
  
    private static class TestServer {
      private final AppServer server;
      private final RecordingHandler handler = new RecordingHandler();
  
      public TestServer() {
        server = new NettyAppServer(handler);
        server.start();
      }
  
      public String whereIs(String relativeUrl) {
        return server.whereIs(relativeUrl);
      }
  
      public void stop() {
        server.stop();
      }
  
      public List<ExpectedRequest> getRequests() {
        return handler.getRequests();
      }
  
      public HostAndPort getHostAndPort() {
        URI uri = URI.create(server.whereIs("/"));
        return HostAndPort.fromParts(uri.getHost(), uri.getPort());
      }
    }
  
    private static class ProxyServer {
      private final AppServer server;
      private final RecordingHandler handler = new RecordingHandler();
      private String pacFileContents;
  
      public ProxyServer() {
        server =
            new NettyAppServer(
                req -> {
                  if (pacFileContents != null && req.getUri().endsWith("/pac.js")) {
                    return new HttpResponse()
                        .setHeader(
                            "Content-Type", "application/x-javascript-config; charset=us-ascii")
                        .setContent(Contents.bytes(pacFileContents.getBytes(US_ASCII)));
                  }
                  return handler.execute(req);
                });
        server.start();
      }
  
      public void setPacFileContents(String pacFileContents) {
        this.pacFileContents = pacFileContents;
      }
  
      public String whereIs(String relativeUrl) {
        return server.whereIs(relativeUrl);
      }
  
      public List<ExpectedRequest> getRequests() {
        return handler.getRequests();
      }
  
      public void stop() {
        server.stop();
      }
  
      public HostAndPort getHostAndPort() {
        URI uri = URI.create(server.whereIs("/"));
        return HostAndPort.fromParts(uri.getHost(), uri.getPort());
      }
    }
  }
  