diff --git a/src/display/fetch_stream.js b/src/display/fetch_stream.js index 1d025ce8bee68..24604e43b68df 100644 --- a/src/display/fetch_stream.js +++ b/src/display/fetch_stream.js @@ -18,6 +18,7 @@ import { createHeaders, createResponseStatusError, extractFilenameFromHeader, + getResponseOrigin, validateRangeRequestCapabilities, validateResponseStatus, } from "./network_utils.js"; @@ -52,6 +53,8 @@ function getArrayBuffer(val) { /** @implements {IPDFStream} */ class PDFFetchStream { + _responseOrigin = null; + constructor(source) { this.source = source; this.isHttp = /^https?:/i.test(source.url); @@ -121,6 +124,8 @@ class PDFFetchStreamReader { createFetchOptions(headers, this._withCredentials, this._abortController) ) .then(response => { + stream._responseOrigin = getResponseOrigin(response.url); + if (!validateResponseStatus(response.status)) { throw createResponseStatusError(response.status, url); } @@ -217,6 +222,13 @@ class PDFFetchStreamRangeReader { createFetchOptions(headers, this._withCredentials, this._abortController) ) .then(response => { + const responseOrigin = getResponseOrigin(response.url); + + if (responseOrigin !== stream._responseOrigin) { + throw new Error( + `Expected range response-origin "${responseOrigin}" to match "${stream._responseOrigin}".` + ); + } if (!validateResponseStatus(response.status)) { throw createResponseStatusError(response.status, url); } diff --git a/src/display/network.js b/src/display/network.js index 9bf7aec9610db..3beb3257bd546 100644 --- a/src/display/network.js +++ b/src/display/network.js @@ -18,6 +18,7 @@ import { createHeaders, createResponseStatusError, extractFilenameFromHeader, + getResponseOrigin, validateRangeRequestCapabilities, } from "./network_utils.js"; @@ -39,6 +40,8 @@ function getArrayBuffer(xhr) { } class NetworkManager { + _responseOrigin = null; + constructor({ url, httpHeaders, withCredentials }) { this.url = url; this.isHttp = /^https?:/i.test(url); @@ -273,6 +276,10 @@ class PDFNetworkStreamFullRequestReader { const fullRequestXhrId = this._fullRequestId; const fullRequestXhr = this._manager.getRequestXhr(fullRequestXhrId); + this._manager._responseOrigin = getResponseOrigin( + fullRequestXhr.responseURL + ); + const rawResponseHeaders = fullRequestXhr.getAllResponseHeaders(); const responseHeaders = new Headers( rawResponseHeaders @@ -370,6 +377,8 @@ class PDFNetworkStreamFullRequestReader { } async read() { + await this._headersCapability.promise; + if (this._storedError) { throw this._storedError; } @@ -405,6 +414,7 @@ class PDFNetworkStreamRangeRequestReader { this._manager = manager; const args = { + onHeadersReceived: this._onHeadersReceived.bind(this), onDone: this._onDone.bind(this), onError: this._onError.bind(this), onProgress: this._onProgress.bind(this), @@ -420,6 +430,19 @@ class PDFNetworkStreamRangeRequestReader { this.onClosed = null; } + _onHeadersReceived() { + const responseOrigin = getResponseOrigin( + this._manager.getRequestXhr(this._requestId)?.responseURL + ); + + if (responseOrigin !== this._manager._responseOrigin) { + this._storedError = new Error( + `Expected range response-origin "${responseOrigin}" to match "${this._manager._responseOrigin}".` + ); + this._onError(0); + } + } + _close() { this.onClosed?.(this); } @@ -441,7 +464,7 @@ class PDFNetworkStreamRangeRequestReader { } _onError(status) { - this._storedError = createResponseStatusError(status, this._url); + this._storedError ??= createResponseStatusError(status, this._url); for (const requestCapability of this._requests) { requestCapability.reject(this._storedError); } diff --git a/src/display/network_utils.js b/src/display/network_utils.js index 80f89588566c4..98ba103fdba43 100644 --- a/src/display/network_utils.js +++ b/src/display/network_utils.js @@ -36,6 +36,16 @@ function createHeaders(isHttp, httpHeaders) { return headers; } +function getResponseOrigin(url) { + try { + return new URL(url).origin; + } catch { + // `new URL()` will throw on incorrect data. + } + // Notably, null is distinct from "null" string (e.g. from file:-URLs). + return null; +} + function validateRangeRequestCapabilities({ responseHeaders, isHttp, @@ -116,6 +126,7 @@ export { createHeaders, createResponseStatusError, extractFilenameFromHeader, + getResponseOrigin, validateRangeRequestCapabilities, validateResponseStatus, }; diff --git a/src/interfaces.js b/src/interfaces.js index e0ecb54746c57..15493bdb819f2 100644 --- a/src/interfaces.js +++ b/src/interfaces.js @@ -30,6 +30,10 @@ class IPDFStream { /** * Gets a reader for the range of the PDF data. + * + * NOTE: Currently this method is only expected to be invoked *after* + * the `IPDFStreamReader.prototype.headersReady` promise has resolved. + * * @param {number} begin - the start offset of the data. * @param {number} end - the end offset of the data. * @returns {IPDFStreamRangeReader} diff --git a/test/unit/fetch_stream_spec.js b/test/unit/fetch_stream_spec.js index 75bda5d2caf4f..3fbe1efc10ef4 100644 --- a/test/unit/fetch_stream_spec.js +++ b/test/unit/fetch_stream_spec.js @@ -54,7 +54,7 @@ describe("fetch_stream", function () { const fullReader = stream.getFullReader(); let isStreamingSupported, isRangeSupported; - const promise = fullReader.headersReady.then(function () { + await fullReader.headersReady.then(function () { isStreamingSupported = fullReader.isStreamingSupported; isRangeSupported = fullReader.isRangeSupported; }); @@ -71,7 +71,7 @@ describe("fetch_stream", function () { }); }; - await Promise.all([read(), promise]); + await read(); expect(len).toEqual(pdfLength); expect(isStreamingSupported).toEqual(true); @@ -90,7 +90,7 @@ describe("fetch_stream", function () { const fullReader = stream.getFullReader(); let isStreamingSupported, isRangeSupported, fullReaderCancelled; - const promise = fullReader.headersReady.then(function () { + await fullReader.headersReady.then(function () { isStreamingSupported = fullReader.isStreamingSupported; isRangeSupported = fullReader.isRangeSupported; // We shall be able to close full reader without any issue. @@ -121,7 +121,6 @@ describe("fetch_stream", function () { await Promise.all([ read(rangeReader1, result1), read(rangeReader2, result2), - promise, ]); expect(isStreamingSupported).toEqual(true); diff --git a/test/unit/network_spec.js b/test/unit/network_spec.js index e8b4b9f4c8e1e..9a55b4771ff0e 100644 --- a/test/unit/network_spec.js +++ b/test/unit/network_spec.js @@ -31,7 +31,7 @@ describe("network", function () { const fullReader = stream.getFullReader(); let isStreamingSupported, isRangeSupported; - const promise = fullReader.headersReady.then(function () { + await fullReader.headersReady.then(function () { isStreamingSupported = fullReader.isStreamingSupported; isRangeSupported = fullReader.isRangeSupported; }); @@ -49,7 +49,7 @@ describe("network", function () { }); }; - await Promise.all([read(), promise]); + await read(); expect(len).toEqual(pdf1Length); expect(count).toEqual(1); @@ -72,7 +72,7 @@ describe("network", function () { const fullReader = stream.getFullReader(); let isStreamingSupported, isRangeSupported, fullReaderCancelled; - const promise = fullReader.headersReady.then(function () { + await fullReader.headersReady.then(function () { isStreamingSupported = fullReader.isStreamingSupported; isRangeSupported = fullReader.isRangeSupported; // we shall be able to close the full reader without issues @@ -107,7 +107,6 @@ describe("network", function () { await Promise.all([ read(range1Reader, result1), read(range2Reader, result2), - promise, ]); expect(result1.value).toEqual(rangeSize);