Skip to content

Commit

Permalink
Add test cases for redirected responses
Browse files Browse the repository at this point in the history
Regression tests for issue mozilla#12744 and PR mozilla#19028
  • Loading branch information
Rob--W committed Nov 20, 2024
1 parent 0fa792f commit 47eb3d1
Show file tree
Hide file tree
Showing 4 changed files with 144 additions and 14 deletions.
16 changes: 7 additions & 9 deletions test/unit/api_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {
CMAP_URL,
createTemporaryNodeServer,
DefaultFileReaderFactory,
getCrossOriginHostname,
TEST_PDFS_PATH,
} from "./test_utils.js";
import {
Expand Down Expand Up @@ -2438,12 +2439,12 @@ describe("api", function () {
const manifesto = `
The Mozilla Manifesto Addendum
Pledge for a Healthy Internet
The open, global internet is the most powerful communication and collaboration resource we have ever seen.
It embodies some of our deepest hopes for human progress.
It enables new opportunities for learning, building a sense of shared humanity, and solving the pressing problems
facing people everywhere.
Over the last decade we have seen this promise fulfilled in many ways.
We have also seen the power of the internet used to magnify divisiveness,
incite violence, promote hatred, and intentionally manipulate fact and reality.
Expand Down Expand Up @@ -2989,17 +2990,14 @@ describe("api", function () {
let loadingTask;
function _checkCanLoad(expectSuccess, filename, options) {
if (isNodeJS) {
// On Node.js, we only support loading file:-URLs.
// Moreover, Node.js does not enforce the Same-origin policy, so
// CORS cannot be tested on Node.js.
pending("Cannot simulate cross-origin requests in Node.js");
}
const params = buildGetDocumentParams(filename, options);
const url = new URL(params.url);
if (url.hostname === "localhost") {
url.hostname = "127.0.0.1";
} else if (params.url.hostname === "127.0.0.1") {
url.hostname = "localhost";
} else {
pending("Can only run cross-origin test on localhost!");
}
url.hostname = getCrossOriginHostname(url.hostname);
params.url = url.href;
loadingTask = getDocument(params);
return loadingTask.promise
Expand Down
75 changes: 75 additions & 0 deletions test/unit/network_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,12 @@
*/

import { AbortException } from "../../src/shared/util.js";
import { getCrossOriginHostname } from "./test_utils.js";
import { PDFNetworkStream } from "../../src/display/network.js";

describe("network", function () {
const basicApiUrl = new URL("../pdfs/basicapi.pdf", window.location).href;
const basicApiFileLength = 105779; //
const pdf1 = new URL("../pdfs/tracemonkey.pdf", window.location).href;
const pdf1Length = 1016315;

Expand Down Expand Up @@ -115,4 +118,76 @@ describe("network", function () {
expect(isRangeSupported).toEqual(true);
expect(fullReaderCancelled).toEqual(true);
});

describe("Redirects", function () {
async function simulateFetchFullAndSomeRange(url, readMoreThanOnce) {
const rangeSize = 32768;
const stream = new PDFNetworkStream({
url,
length: basicApiFileLength,
rangeChunkSize: rangeSize,
disableStream: true,
disableRange: false,
});

const fullReader = stream.getFullReader();

await fullReader.headersReady;
// Sanity check: We can only test range requests if supported:
expect(fullReader.isRangeSupported).toEqual(false);

fullReader.cancel(new AbortException("Don't need fullReader."));

const rangeReader = stream.getRangeReader(
basicApiFileLength - rangeSize,
basicApiFileLength
);
// May throw or not throw - the caller will verify it:
await rangeReader.read();
if (readMoreThanOnce) {
await rangeReader.read();
}
rangeReader.cancel(new AbortException("Don't need rangeReader"));
}
function getCrossOriginUrlWithRedirects({ redirectIfRange = false }) {
const url = new URL(basicApiUrl);
// The responses are going to be cross-origin, and therefore we need CORS
// to read it. This option depends on crossOriginHandler in webserver.mjs.
url.searchParams.set("cors", "withoutCredentials");

// This redirect options depend on redirectHandler in webserver.mjs.

// We will change the host to a cross-origin domain so that the initial
// request will be cross-origin. Set "redirectToHost" to the original host
// to force a cross-origin redirect (relative to the initial URL).
url.searchParams.set("redirectToHost", url.hostname);
url.hostname = getCrossOriginHostname(url.hostname);
if (redirectIfRange) {
url.searchParams.set("redirectIfRange", "1");
}
return url.href;
}
it("redirects allowed if all responses are same-origin", async function () {
const pdfUrl = getCrossOriginUrlWithRedirects({ redirectIfRange: false });
await expectAsync(simulateFetchFullAndSomeRange(pdfUrl)).toBeResolved();
});

it("redirects blocked if any response is cross-origin, read once", async function () {
const pdfUrl = getCrossOriginUrlWithRedirects({ redirectIfRange: true });
await expectAsync(
simulateFetchFullAndSomeRange(pdfUrl)
).toBeRejectedWithError(
/^Expected range response-origin "http:.*" to match "http:.*"\.$/
);
});

it("redirects blocked if any response is cross-origin, read again", async function () {
const pdfUrl = getCrossOriginUrlWithRedirects({ redirectIfRange: true });
await expectAsync(
simulateFetchFullAndSomeRange(pdfUrl, true)
).toBeRejectedWithError(
/^Expected range response-origin "http:.*" to match "http:.*"\.$/
);
});
});
});
17 changes: 17 additions & 0 deletions test/unit/test_utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,22 @@ function buildGetDocumentParams(filename, options) {
return params;
}

function getCrossOriginHostname(hostname) {
if (hostname === "localhost") {
// Note: This does not work if localhost is listening on IPv6 only.
// As a work-around, visit the IPv6 version at:
// http://[::1]:8888/test/unit/unit_test.html?spec=Cross-origin
return "127.0.0.1";
}

if (hostname === "127.0.0.1" || hostname === "[::1]") {
return "localhost";
}

// FQDN are cross-origin and browsers usually resolve them to the same server.
return hostname.endsWith(".") ? hostname.slice(0, -1) : hostname + ".";
}

class XRefMock {
constructor(array) {
this._map = Object.create(null);
Expand Down Expand Up @@ -174,6 +190,7 @@ export {
createIdFactory,
createTemporaryNodeServer,
DefaultFileReaderFactory,
getCrossOriginHostname,
STANDARD_FONT_DATA_URL,
TEST_PDFS_PATH,
XRefMock,
Expand Down
50 changes: 45 additions & 5 deletions test/webserver.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ class WebServer {
this.cacheExpirationTime = cacheExpirationTime || 0;
this.disableRangeRequests = false;
this.hooks = {
GET: [crossOriginHandler],
GET: [crossOriginHandler, redirectHandler],
POST: [],
};
}
Expand Down Expand Up @@ -308,6 +308,11 @@ class WebServer {
}

#serveFileRange(response, fileURL, fileSize, start, end) {
if (end > fileSize || start > end) {
response.writeHead(416);
response.end();
return;
}
const stream = fs.createReadStream(fileURL, {
flags: "rs",
start,
Expand Down Expand Up @@ -336,18 +341,53 @@ class WebServer {
}

// This supports the "Cross-origin" test in test/unit/api_spec.js
// It is here instead of test.js so that when the test will still complete as
// and "Redirects" in test/unit/network_spec.js
// It is here instead of test.mjs so that when the test will still complete as
// expected if the user does "gulp server" and then visits
// http://localhost:8888/test/unit/unit_test.html?spec=Cross-origin
function crossOriginHandler(url, request, response) {
if (url.pathname === "/test/pdfs/basicapi.pdf") {
if (!url.searchParams.has("cors") || !request.headers.origin) {
return;
}
response.setHeader("Access-Control-Allow-Origin", request.headers.origin);
if (url.searchParams.get("cors") === "withCredentials") {
response.setHeader("Access-Control-Allow-Origin", request.headers.origin);
response.setHeader("Access-Control-Allow-Credentials", "true");
} else if (url.searchParams.get("cors") === "withoutCredentials") {
response.setHeader("Access-Control-Allow-Origin", request.headers.origin);
} // withoutCredentials does not include Access-Control-Allow-Credentials.
response.setHeader("Access-Control-Expose-Headers", "Content-Range");
}
}

// This supports the "Redirects" test in test/unit/network_spec.js
// It is here instead of test.mjs so that when the test will still complete as
// expected if the user does "gulp server" and then visits
// http://localhost:8888/test/unit/unit_test.html?spec=Redirects
function redirectHandler(url, request, response) {
const redirectToHost = url.searchParams.get("redirectToHost");
if (redirectToHost) {
if (url.searchParams.get("redirectIfRange") && !request.headers.range) {
return false;
}
try {
const newURL = new URL(url);
newURL.hostname = redirectToHost;
// Delete test-only query parameters to avoid infinite redirects.
newURL.searchParams.delete("redirectToHost");
newURL.searchParams.delete("redirectIfRange");
if (newURL.hostname !== redirectToHost) {
throw new Error(`Invalid hostname: ${redirectToHost}`);
}
response.setHeader("Location", newURL.href);
} catch {
response.writeHead(500);
response.end();
return true;
}
response.writeHead(302);
response.end();
return true;
}
return false;
}

export { WebServer };

0 comments on commit 47eb3d1

Please sign in to comment.