Files
Silaghi fb547655b0 Revert "Bug 2066063 - [devtools] Redirect invalid path to 404 for local mode requests. r=devtools-reviewers,nchevobbe" for causing devtools and TV failures
This reverts commit d27eb6ae99.

Revert "Bug 2066063 - [devtools] Ignore local mode request with custom ports. r=devtools-reviewers,jdescottes"

This reverts commit 2d7e2b58b1.
2026-09-11 05:23:06 +00:00

213 lines
6.8 KiB
JavaScript

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/**
* Implements the DevTools "Local Mode" backend, which redirects requests
* to local files defined by the Web Developer.
*/
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
FileUtils: "resource://gre/modules/FileUtils.sys.mjs",
NetworkOverride:
"resource://devtools/shared/network-observer/NetworkOverride.sys.mjs",
});
const xmlSerializer = new XMLSerializer();
export const NetworkLocalMode = {
/**
* Intercept a early channel that hasn't established a remote connection yet,
* which matches a "Local Mode" origin and should be served from a local file
* located in the folder passed as argument.
*
* @param {nsIHttpChannel} channel
* @param {string} localFolderPath
*/
interceptChannelWithPath(channel, localFolderPath) {
if (channel.URI.spec.includes("%00")) {
overrideChannelIntoCustomCode(channel, 400, "400 Bad request");
return;
}
const path = decodeURI(channel.URI.filePath).replace(/^\/+/, "");
// On Windows, replace all URI's '/' path separators with '\'
let systemPath = path;
if (Services.appinfo.OS === "WINNT") {
systemPath = systemPath.replace(/\//g, "\\");
}
const overridePath = PathUtils.joinRelative(localFolderPath, systemPath);
let file = new lazy.FileUtils.File(overridePath);
if (!file.exists()) {
// Create a 404 response to avoid leaving any request matching the host
// to fallback to any potential existing http server.
overrideChannelInto404(channel);
return;
}
if (file.isFile()) {
// This is the typical codepath redirecting to a existing local file
lazy.NetworkOverride.overrideChannelWithFilePath(channel, file.path);
return;
}
if (file.isDirectory()) {
// Before showing a directory listing, try to match a valid index HTML file
file.append("index.html");
if (file.exists() && file.isFile()) {
// This is the typical codepath redirecting to a existing local file
lazy.NetworkOverride.overrideChannelWithFilePath(channel, file.path);
return;
}
file = file.parent;
file.append("index.htm");
if (file.exists() && file.isFile()) {
// This is the typical codepath redirecting to a existing local file
lazy.NetworkOverride.overrideChannelWithFilePath(channel, file.path);
return;
}
file = file.parent;
// When we are trying to load a directory, return a file listing instead of 404
// as that's really handy and behaves like a typical development http server.
overrideChannelWithDirectoryListing(channel, path, file);
return;
}
// In case it is neither a file, nor a directory?
overrideChannelInto404(channel);
},
};
/**
* Make it so that an in-flight channel that just started
* is converted into a 404 response for when there is no matching local file
* for that request.
*
* @param {nsIHttpChannel} channel
*/
function overrideChannelInto404(channel) {
overrideChannelIntoCustomCode(
channel,
404,
"404 Not Found",
`No local file for: ${channel.URI.filePath}`
);
}
/**
* Make it so that an in-flight channel that just started
* is converted into a custom response with a custom HTTP status code.
*
* @param {nsIHttpChannel} channel
* @param {number} status
* The returned status code
* @param {string} statusText
* The returned status text
* @param {undefined|string} message
* The message displayed in the returned HTML page. Will default to status text if omitted.
*/
function overrideChannelIntoCustomCode(
channel,
status,
statusText,
message = statusText
) {
const replacedHttpResponse = Cc[
"@mozilla.org/network/replaced-http-response;1"
].createInstance(Ci.nsIReplacedHttpResponse);
replacedHttpResponse.responseStatus = status;
replacedHttpResponse.responseStatusText = statusText;
// We don't have access to a document, and we can't create a DocumentFragment from here,
// so use DOMParser to create a new document that we can use to build our listing page.
const doc = new DOMParser().parseFromString(
"<!DOCTYPE html><html/>",
"text/html"
);
// Compute a simple, but meaningful HTML file for the error
const h1 = doc.createElement("h1");
h1.append("DevTools Local Mode");
const p = doc.createElement("p");
p.textContent = message;
doc.body.append(h1, p);
const serializedDoc = xmlSerializer.serializeToString(doc);
replacedHttpResponse.responseBody = serializedDoc;
channel
.QueryInterface(Ci.nsIHttpChannelInternal)
.setResponseOverride(replacedHttpResponse);
}
/**
* Replace the content of an in-flight channel that just started
* that matches a local mode folder into an HTML page listing of
* the available files in that folder.
*
* @param {nsIHttpChannel} channel
* @param {string} folderPath
* @param {nsILocalFile} file
*/
async function overrideChannelWithDirectoryListing(channel, folderPath, file) {
const replacedHttpResponse = Cc[
"@mozilla.org/network/replaced-http-response;1"
].createInstance(Ci.nsIReplacedHttpResponse);
replacedHttpResponse.responseStatus = 200;
replacedHttpResponse.responseStatusText = "OK";
replacedHttpResponse.setResponseHeader("Content-Type", "text/html", false);
// `setResponseOverride` has to be called synchronously after before-connect event
// to really intercept the request.
// Workaround this by suspending the request until we asynchronously retrieve
// the directory listing content
channel.suspend();
// We don't have access to a document, and we can't create a DocumentFragment from here,
// so use DOMParser to create a new document that we can use to build our listing page.
const doc = new DOMParser().parseFromString(
"<!DOCTYPE html><html/>",
"text/html"
);
// Compute a simple, but meaningful HTML file for the listing
const h2 = doc.createElement("h2");
h2.append(PathUtils.filename(file.path));
const ul = doc.createElement("ul");
const children = await IOUtils.getChildren(file.path);
for (const childPath of children.sort()) {
const filename = PathUtils.filename(childPath);
const absolutePath =
"/" +
folderPath +
(!folderPath || folderPath.endsWith("/") ? "" : "/") +
filename;
const li = doc.createElement("li");
const a = doc.createElement("a");
a.href = absolutePath;
a.append(filename);
li.append(a);
ul.append(li);
}
doc.body.append(h2, ul);
const serializedDoc = xmlSerializer.serializeToString(doc);
replacedHttpResponse.responseBody = serializedDoc;
channel
.QueryInterface(Ci.nsIHttpChannelInternal)
.setResponseOverride(replacedHttpResponse);
channel.resume();
}