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 #12744 and PR #19028
  • Loading branch information
Rob--W committed Nov 23, 2024
1 parent 07765e9 commit 914df5d
Show file tree
Hide file tree
Showing 4 changed files with 153 additions and 12 deletions.
12 changes: 5 additions & 7 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 @@ -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
86 changes: 86 additions & 0 deletions test/unit/network_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,13 @@
*/

import { AbortException } from "../../src/shared/util.js";
import { getCrossOriginHostname } from "./test_utils.js";
import { PDFFetchStream } from "../../src/display/fetch_stream.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 @@ -116,4 +120,86 @@ describe("network", function () {
expect(isRangeSupported).toEqual(true);
expect(fullReaderCancelled).toEqual(true);
});

// Common tests to verify behavior across implementations of the IPDFStream
// interface (PDFNetworkStream / PDFFetchStream).
function describeRedirectTests(PDFStreamClass) {
async function simulateFetchFullAndSomeRange(url, readMoreThanOnce) {
const rangeSize = 32768;
const stream = new PDFStreamClass({
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:.*"\.$/
);
});
}
describe("Redirects", function () {
describe("PDFFetchStream", function () {
describeRedirectTests(PDFFetchStream);
});
describe("PDFNetworkStream", function () {
describeRedirectTests(PDFNetworkStream);
});
});
});
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 914df5d

Please sign in to comment.