blob: 6170237787e9f15fcf0a4a826e0273c0e7e2e0a9 [file]
// 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.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 java.io.IOException;
import java.io.UncheckedIOException;
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 org.jspecify.annotations.NullMarked;
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.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.JupiterTestBase;
import org.openqa.selenium.testing.NoDriverBeforeTest;
import org.openqa.selenium.testing.drivers.WebDriverBuilder;
/**
* 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 extends JupiterTestBase {
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 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() {
if (proxyServer != null) {
safelyCall(() -> proxyServer.stop());
}
if (server1 != null) {
safelyCall(() -> server1.stop());
}
if (server2 != null) {
safelyCall(() -> server2.stop());
}
}
private WebDriver createProxyDriver(String pacUrl) {
Proxy proxy = new Proxy();
proxy.setProxyAutoconfigUrl(pacUrl);
Capabilities caps = new ImmutableCapabilities(PROXY, proxy);
return new WebDriverBuilder().get(caps);
}
/**
* Tests navigation when all 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(driver, page1Url);
assertThat(server1.getRequests())
.containsExactly(
new ExpectedRequest(PAGE_1, null),
new ExpectedRequest(PAGE_2, page1Url),
new ExpectedRequest(PAGE_3, page2Url));
}
/**
* Tests navigation when all files are hosted on the same domain and the browser is configured to
* use a proxy that permits direct access to that domain.
*/
@Test
@NoDriverBeforeTest
void basicHistoryNavigationWithADirectProxy() {
proxyServer.setPacFileContents("function FindProxyForURL(url, host) { return 'DIRECT'; }");
localDriver = createProxyDriver(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(localDriver, 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) {
return URLEncoder.encode(url, UTF_8);
}
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);
}
}
@NullMarked
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;
}
}
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();
}
}
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 void stop() {
server.stop();
}
}
}