Merge pull request #18657 from nicolo-ribaudo/url

Use the URL global instead of the deprecated url.parse
This commit is contained in:
Tim van der Meij 2024-08-29 20:50:43 +02:00 committed by GitHub
commit 5d94047dad
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 83 additions and 124 deletions

View file

@ -26,29 +26,20 @@ if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL")) {
); );
} }
const fileUriRegex = /^file:\/\/\/[a-zA-Z]:\//; const urlRegex = /^[a-z][a-z0-9\-+.]+:/i;
function parseUrl(sourceUrl) { function parseUrlOrPath(sourceUrl) {
if (urlRegex.test(sourceUrl)) {
return new URL(sourceUrl);
}
const url = NodePackages.get("url"); const url = NodePackages.get("url");
const parsedUrl = url.parse(sourceUrl); return new URL(url.pathToFileURL(sourceUrl));
if (parsedUrl.protocol === "file:" || parsedUrl.host) {
return parsedUrl;
}
// Prepending 'file:///' to Windows absolute path.
if (/^[a-z]:[/\\]/i.test(sourceUrl)) {
return url.parse(`file:///${sourceUrl}`);
}
// Changes protocol to 'file:' if url refers to filesystem.
if (!parsedUrl.host) {
parsedUrl.protocol = "file:";
}
return parsedUrl;
} }
class PDFNodeStream { class PDFNodeStream {
constructor(source) { constructor(source) {
this.source = source; this.source = source;
this.url = parseUrl(source.url); this.url = parseUrlOrPath(source.url);
this.isHttp = this.isHttp =
this.url.protocol === "http:" || this.url.protocol === "https:"; this.url.protocol === "http:" || this.url.protocol === "https:";
// Check if url refers to filesystem. // Check if url refers to filesystem.
@ -287,18 +278,6 @@ class BaseRangeReader {
} }
} }
function createRequestOptions(parsedUrl, headers) {
return {
protocol: parsedUrl.protocol,
auth: parsedUrl.auth,
host: parsedUrl.hostname,
port: parsedUrl.port,
path: parsedUrl.path,
method: "GET",
headers,
};
}
class PDFNodeStreamFullReader extends BaseFullReader { class PDFNodeStreamFullReader extends BaseFullReader {
constructor(stream) { constructor(stream) {
super(stream); super(stream);
@ -337,13 +316,15 @@ class PDFNodeStreamFullReader extends BaseFullReader {
if (this._url.protocol === "http:") { if (this._url.protocol === "http:") {
const http = NodePackages.get("http"); const http = NodePackages.get("http");
this._request = http.request( this._request = http.request(
createRequestOptions(this._url, stream.httpHeaders), this._url,
{ headers: stream.httpHeaders },
handleResponse handleResponse
); );
} else { } else {
const https = NodePackages.get("https"); const https = NodePackages.get("https");
this._request = https.request( this._request = https.request(
createRequestOptions(this._url, stream.httpHeaders), this._url,
{ headers: stream.httpHeaders },
handleResponse handleResponse
); );
} }
@ -386,13 +367,15 @@ class PDFNodeStreamRangeReader extends BaseRangeReader {
if (this._url.protocol === "http:") { if (this._url.protocol === "http:") {
const http = NodePackages.get("http"); const http = NodePackages.get("http");
this._request = http.request( this._request = http.request(
createRequestOptions(this._url, this._httpHeaders), this._url,
{ headers: this._httpHeaders },
handleResponse handleResponse
); );
} else { } else {
const https = NodePackages.get("https"); const https = NodePackages.get("https");
this._request = https.request( this._request = https.request(
createRequestOptions(this._url, this._httpHeaders), this._url,
{ headers: this._httpHeaders },
handleResponse handleResponse
); );
} }
@ -408,25 +391,18 @@ class PDFNodeStreamFsFullReader extends BaseFullReader {
constructor(stream) { constructor(stream) {
super(stream); super(stream);
let path = decodeURIComponent(this._url.path);
// Remove the extra slash to get right path from url like `file:///C:/`
if (fileUriRegex.test(this._url.href)) {
path = path.replace(/^\//, "");
}
const fs = NodePackages.get("fs"); const fs = NodePackages.get("fs");
fs.promises.lstat(path).then( fs.promises.lstat(this._url).then(
stat => { stat => {
// Setting right content length. // Setting right content length.
this._contentLength = stat.size; this._contentLength = stat.size;
this._setReadableStream(fs.createReadStream(path)); this._setReadableStream(fs.createReadStream(this._url));
this._headersCapability.resolve(); this._headersCapability.resolve();
}, },
error => { error => {
if (error.code === "ENOENT") { if (error.code === "ENOENT") {
error = new MissingPDFException(`Missing PDF "${path}".`); error = new MissingPDFException(`Missing PDF "${this._url}".`);
} }
this._storedError = error; this._storedError = error;
this._headersCapability.reject(error); this._headersCapability.reject(error);
@ -439,15 +415,10 @@ class PDFNodeStreamFsRangeReader extends BaseRangeReader {
constructor(stream, start, end) { constructor(stream, start, end) {
super(stream); super(stream);
let path = decodeURIComponent(this._url.path);
// Remove the extra slash to get right path from url like `file:///C:/`
if (fileUriRegex.test(this._url.href)) {
path = path.replace(/^\//, "");
}
const fs = NodePackages.get("fs"); const fs = NodePackages.get("fs");
this._setReadableStream(fs.createReadStream(path, { start, end: end - 1 })); this._setReadableStream(
fs.createReadStream(this._url, { start, end: end - 1 })
);
} }
} }

View file

@ -26,7 +26,6 @@ import path from "path";
import puppeteer from "puppeteer"; import puppeteer from "puppeteer";
import readline from "readline"; import readline from "readline";
import { translateFont } from "./font/ttxdriver.mjs"; import { translateFont } from "./font/ttxdriver.mjs";
import url from "url";
import { WebServer } from "./webserver.mjs"; import { WebServer } from "./webserver.mjs";
import yargs from "yargs"; import yargs from "yargs";
@ -670,8 +669,7 @@ function checkRefTestResults(browser, id, results) {
}); });
} }
function refTestPostHandler(req, res) { function refTestPostHandler(parsedUrl, req, res) {
var parsedUrl = url.parse(req.url, true);
var pathname = parsedUrl.pathname; var pathname = parsedUrl.pathname;
if ( if (
pathname !== "/tellMeToQuit" && pathname !== "/tellMeToQuit" &&
@ -691,7 +689,7 @@ function refTestPostHandler(req, res) {
var session; var session;
if (pathname === "/tellMeToQuit") { if (pathname === "/tellMeToQuit") {
session = getSession(parsedUrl.query.browser); session = getSession(parsedUrl.searchParams.get("browser"));
monitorBrowserTimeout(session, null); monitorBrowserTimeout(session, null);
closeSession(session.name); closeSession(session.name);
return; return;
@ -821,8 +819,7 @@ async function startIntegrationTest() {
await Promise.all(sessions.map(session => closeSession(session.name))); await Promise.all(sessions.map(session => closeSession(session.name)));
} }
function unitTestPostHandler(req, res) { function unitTestPostHandler(parsedUrl, req, res) {
var parsedUrl = url.parse(req.url);
var pathname = parsedUrl.pathname; var pathname = parsedUrl.pathname;
if ( if (
pathname !== "/tellMeToQuit" && pathname !== "/tellMeToQuit" &&

View file

@ -24,17 +24,13 @@ if (!isNodeJS) {
); );
} }
const path = await __non_webpack_import__("path");
const url = await __non_webpack_import__("url"); const url = await __non_webpack_import__("url");
describe("node_stream", function () { describe("node_stream", function () {
let tempServer = null; let tempServer = null;
const pdf = url.parse( const cwdURL = url.pathToFileURL(process.cwd()) + "/";
encodeURI( const pdf = new URL("./test/pdfs/tracemonkey.pdf", cwdURL).href;
"file://" + path.join(process.cwd(), "./test/pdfs/tracemonkey.pdf")
)
).href;
const pdfLength = 1016315; const pdfLength = 1016315;
beforeAll(function () { beforeAll(function () {

View file

@ -21,6 +21,7 @@ import fs from "fs";
import fsPromises from "fs/promises"; import fsPromises from "fs/promises";
import http from "http"; import http from "http";
import path from "path"; import path from "path";
import { pathToFileURL } from "url";
const MIME_TYPES = { const MIME_TYPES = {
".css": "text/css", ".css": "text/css",
@ -42,7 +43,8 @@ const DEFAULT_MIME_TYPE = "application/octet-stream";
class WebServer { class WebServer {
constructor({ root, host, port, cacheExpirationTime }) { constructor({ root, host, port, cacheExpirationTime }) {
this.root = root || "."; const cwdURL = pathToFileURL(process.cwd()) + "/";
this.rootURL = new URL(`${root || "."}/`, cwdURL);
this.host = host || "localhost"; this.host = host || "localhost";
this.port = port || 0; this.port = port || 0;
this.server = null; this.server = null;
@ -82,27 +84,10 @@ class WebServer {
} }
async #handler(request, response) { async #handler(request, response) {
// Validate and parse the request URL. // URLs are normalized and automatically disallow directory traversal
const url = request.url.replaceAll("//", "/"); // attacks. For example, http://HOST:PORT/../../../../../../../etc/passwd
const urlParts = /([^?]*)((?:\?(.*))?)/.exec(url); // is equivalent to http://HOST:PORT/etc/passwd.
let pathPart; const url = new URL(`http://${this.host}:${this.port}${request.url}`);
try {
// Guard against directory traversal attacks such as
// `/../../../../../../../etc/passwd`, which let you make GET requests
// for files outside of `this.root`.
pathPart = path.normalize(decodeURI(urlParts[1]));
// `path.normalize` returns a path on the basis of the current platform.
// Windows paths cause issues in `checkRequest` and underlying methods.
// Converting to a Unix path avoids platform checks in said functions.
pathPart = pathPart.replaceAll("\\", "/");
} catch {
// If the URI cannot be decoded, a `URIError` is thrown. This happens for
// malformed URIs such as `http://localhost:8888/%s%s` and should be
// handled as a bad request.
response.writeHead(400);
response.end("Bad request", "utf8");
return;
}
// Validate the request method and execute method hooks. // Validate the request method and execute method hooks.
const methodHooks = this.hooks[request.method]; const methodHooks = this.hooks[request.method];
@ -111,24 +96,34 @@ class WebServer {
response.end("Unsupported request method", "utf8"); response.end("Unsupported request method", "utf8");
return; return;
} }
const handled = methodHooks.some(hook => hook(request, response)); const handled = methodHooks.some(hook => hook(url, request, response));
if (handled) { if (handled) {
return; return;
} }
// Check the request and serve the file/folder contents. // Check the request and serve the file/folder contents.
if (pathPart === "/favicon.ico") { if (url.pathname === "/favicon.ico") {
pathPart = "test/resources/favicon.ico"; url.pathname = "/test/resources/favicon.ico";
} }
await this.#checkRequest(request, response, url, urlParts, pathPart); await this.#checkRequest(request, response, url);
} }
async #checkRequest(request, response, url, urlParts, pathPart) { async #checkRequest(request, response, url) {
const localURL = new URL(`.${url.pathname}`, this.rootURL);
// Check if the file/folder exists. // Check if the file/folder exists.
let filePath;
try { try {
filePath = await fsPromises.realpath(path.join(this.root, pathPart)); await fsPromises.realpath(localURL);
} catch { } catch (e) {
if (e instanceof URIError) {
// If the URI cannot be decoded, a `URIError` is thrown. This happens
// for malformed URIs such as `http://localhost:8888/%s%s` and should be
// handled as a bad request.
response.writeHead(400);
response.end("Bad request", "utf8");
return;
}
response.writeHead(404); response.writeHead(404);
response.end(); response.end();
if (this.verbose) { if (this.verbose) {
@ -140,7 +135,7 @@ class WebServer {
// Get the properties of the file/folder. // Get the properties of the file/folder.
let stats; let stats;
try { try {
stats = await fsPromises.stat(filePath); stats = await fsPromises.stat(localURL);
} catch { } catch {
response.writeHead(500); response.writeHead(500);
response.end(); response.end();
@ -150,15 +145,14 @@ class WebServer {
const isDir = stats.isDirectory(); const isDir = stats.isDirectory();
// If a folder is requested, serve the directory listing. // If a folder is requested, serve the directory listing.
if (isDir && !/\/$/.test(pathPart)) { if (isDir && !/\/$/.test(url.pathname)) {
response.setHeader("Location", `${pathPart}/${urlParts[2]}`); response.setHeader("Location", `${url.pathname}/${url.search}`);
response.writeHead(301); response.writeHead(301);
response.end("Redirected", "utf8"); response.end("Redirected", "utf8");
return; return;
} }
if (isDir) { if (isDir) {
const queryPart = urlParts[3]; await this.#serveDirectoryIndex(response, url, localURL);
await this.#serveDirectoryIndex(response, pathPart, queryPart, filePath);
return; return;
} }
@ -182,7 +176,7 @@ class WebServer {
} }
this.#serveFileRange( this.#serveFileRange(
response, response,
filePath, localURL,
fileSize, fileSize,
start, start,
isNaN(end) ? fileSize : end + 1 isNaN(end) ? fileSize : end + 1
@ -194,19 +188,19 @@ class WebServer {
if (this.verbose) { if (this.verbose) {
console.log(url); console.log(url);
} }
this.#serveFile(response, filePath, fileSize); this.#serveFile(response, localURL, fileSize);
} }
async #serveDirectoryIndex(response, pathPart, queryPart, directory) { async #serveDirectoryIndex(response, url, localUrl) {
response.setHeader("Content-Type", "text/html"); response.setHeader("Content-Type", "text/html");
response.writeHead(200); response.writeHead(200);
if (queryPart === "frame") { if (url.searchParams.has("frame")) {
response.end( response.end(
`<html> `<html>
<frameset cols=*,200> <frameset cols=*,200>
<frame name=pdf> <frame name=pdf>
<frame src="${encodeURI(pathPart)}?side"> <frame src="${url.pathname}?side">
</frameset> </frameset>
</html>`, </html>`,
"utf8" "utf8"
@ -216,7 +210,7 @@ class WebServer {
let files; let files;
try { try {
files = await fsPromises.readdir(directory); files = await fsPromises.readdir(localUrl);
} catch { } catch {
response.end(); response.end();
return; return;
@ -228,13 +222,13 @@ class WebServer {
<meta charset="utf-8"> <meta charset="utf-8">
</head> </head>
<body> <body>
<h1>Index of ${pathPart}</h1>` <h1>Index of ${url.pathname}</h1>`
); );
if (pathPart !== "/") { if (url.pathname !== "/") {
response.write('<a href="..">..</a><br>'); response.write('<a href="..">..</a><br>');
} }
const all = queryPart === "all"; const all = url.searchParams.has("all");
const escapeHTML = untrusted => const escapeHTML = untrusted =>
// Escape untrusted input so that it can safely be used in a HTML response // Escape untrusted input so that it can safely be used in a HTML response
// in HTML and in HTML attributes. // in HTML and in HTML attributes.
@ -247,13 +241,13 @@ class WebServer {
for (const file of files) { for (const file of files) {
let stat; let stat;
const item = pathPart + file; const item = url.pathname + file;
let href = ""; let href = "";
let label = ""; let label = "";
let extraAttributes = ""; let extraAttributes = "";
try { try {
stat = fs.statSync(path.join(directory, file)); stat = fs.statSync(new URL(file, localUrl));
} catch (ex) { } catch (ex) {
href = encodeURI(item); href = encodeURI(item);
label = `${file} (${ex})`; label = `${file} (${ex})`;
@ -284,7 +278,7 @@ class WebServer {
if (files.length === 0) { if (files.length === 0) {
response.write("<p>No files found</p>"); response.write("<p>No files found</p>");
} }
if (!all && queryPart !== "side") { if (!all && !url.searchParams.has("side")) {
response.write( response.write(
'<hr><p>(only PDF files are shown, <a href="?all">show all</a>)</p>' '<hr><p>(only PDF files are shown, <a href="?all">show all</a>)</p>'
); );
@ -292,8 +286,8 @@ class WebServer {
response.end("</body></html>"); response.end("</body></html>");
} }
#serveFile(response, filePath, fileSize) { #serveFile(response, fileURL, fileSize) {
const stream = fs.createReadStream(filePath, { flags: "rs" }); const stream = fs.createReadStream(fileURL, { flags: "rs" });
stream.on("error", error => { stream.on("error", error => {
response.writeHead(500); response.writeHead(500);
response.end(); response.end();
@ -302,7 +296,7 @@ class WebServer {
if (!this.disableRangeRequests) { if (!this.disableRangeRequests) {
response.setHeader("Accept-Ranges", "bytes"); response.setHeader("Accept-Ranges", "bytes");
} }
response.setHeader("Content-Type", this.#getContentType(filePath)); response.setHeader("Content-Type", this.#getContentType(fileURL));
response.setHeader("Content-Length", fileSize); response.setHeader("Content-Length", fileSize);
if (this.cacheExpirationTime > 0) { if (this.cacheExpirationTime > 0) {
const expireTime = new Date(); const expireTime = new Date();
@ -313,8 +307,8 @@ class WebServer {
stream.pipe(response); stream.pipe(response);
} }
#serveFileRange(response, filePath, fileSize, start, end) { #serveFileRange(response, fileURL, fileSize, start, end) {
const stream = fs.createReadStream(filePath, { const stream = fs.createReadStream(fileURL, {
flags: "rs", flags: "rs",
start, start,
end: end - 1, end: end - 1,
@ -325,7 +319,7 @@ class WebServer {
}); });
response.setHeader("Accept-Ranges", "bytes"); response.setHeader("Accept-Ranges", "bytes");
response.setHeader("Content-Type", this.#getContentType(filePath)); response.setHeader("Content-Type", this.#getContentType(fileURL));
response.setHeader("Content-Length", end - start); response.setHeader("Content-Length", end - start);
response.setHeader( response.setHeader(
"Content-Range", "Content-Range",
@ -335,8 +329,8 @@ class WebServer {
stream.pipe(response); stream.pipe(response);
} }
#getContentType(filePath) { #getContentType(fileURL) {
const extension = path.extname(filePath).toLowerCase(); const extension = path.extname(fileURL.pathname).toLowerCase();
return MIME_TYPES[extension] || DEFAULT_MIME_TYPE; return MIME_TYPES[extension] || DEFAULT_MIME_TYPE;
} }
} }
@ -345,13 +339,14 @@ class WebServer {
// It is here instead of test.js so that when the test will still complete as // It is here instead of test.js so that when the test will still complete as
// expected if the user does "gulp server" and then visits // expected if the user does "gulp server" and then visits
// http://localhost:8888/test/unit/unit_test.html?spec=Cross-origin // http://localhost:8888/test/unit/unit_test.html?spec=Cross-origin
function crossOriginHandler(request, response) { function crossOriginHandler(url, request, response) {
if (request.url === "/test/pdfs/basicapi.pdf?cors=withCredentials") { if (url.pathname === "/test/pdfs/basicapi.pdf") {
response.setHeader("Access-Control-Allow-Origin", request.headers.origin); if (url.searchParams.get("cors") === "withCredentials") {
response.setHeader("Access-Control-Allow-Credentials", "true"); response.setHeader("Access-Control-Allow-Origin", request.headers.origin);
} response.setHeader("Access-Control-Allow-Credentials", "true");
if (request.url === "/test/pdfs/basicapi.pdf?cors=withoutCredentials") { } else if (url.searchParams.get("cors") === "withoutCredentials") {
response.setHeader("Access-Control-Allow-Origin", request.headers.origin); response.setHeader("Access-Control-Allow-Origin", request.headers.origin);
}
} }
} }