summaryrefslogtreecommitdiffstats
path: root/remote/cdp/JSONHandler.sys.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'remote/cdp/JSONHandler.sys.mjs')
-rw-r--r--remote/cdp/JSONHandler.sys.mjs266
1 files changed, 266 insertions, 0 deletions
diff --git a/remote/cdp/JSONHandler.sys.mjs b/remote/cdp/JSONHandler.sys.mjs
new file mode 100644
index 0000000000..5cb81d6a9a
--- /dev/null
+++ b/remote/cdp/JSONHandler.sys.mjs
@@ -0,0 +1,266 @@
+/* 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/. */
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ Log: "chrome://remote/content/shared/Log.sys.mjs",
+ HTTP_404: "chrome://remote/content/server/httpd.sys.mjs",
+ HTTP_405: "chrome://remote/content/server/httpd.sys.mjs",
+ HTTP_500: "chrome://remote/content/server/httpd.sys.mjs",
+ Protocol: "chrome://remote/content/cdp/Protocol.sys.mjs",
+ RemoteAgentError: "chrome://remote/content/cdp/Error.sys.mjs",
+ TabManager: "chrome://remote/content/shared/TabManager.sys.mjs",
+});
+
+export class JSONHandler {
+ constructor(cdp) {
+ this.cdp = cdp;
+ this.routes = {
+ "/json/version": {
+ handler: this.getVersion.bind(this),
+ },
+
+ "/json/protocol": {
+ handler: this.getProtocol.bind(this),
+ },
+
+ "/json/list": {
+ handler: this.getTargetList.bind(this),
+ },
+
+ "/json": {
+ handler: this.getTargetList.bind(this),
+ },
+
+ // PUT only - /json/new?{url}
+ "/json/new": {
+ handler: this.newTarget.bind(this),
+ method: "PUT",
+ },
+
+ // /json/activate/{targetId}
+ "/json/activate": {
+ handler: this.activateTarget.bind(this),
+ parameter: true,
+ },
+
+ // /json/close/{targetId}
+ "/json/close": {
+ handler: this.closeTarget.bind(this),
+ parameter: true,
+ },
+ };
+ }
+
+ getVersion() {
+ const mainProcessTarget = this.cdp.targetList.getMainProcessTarget();
+
+ const { userAgent } = Cc[
+ "@mozilla.org/network/protocol;1?name=http"
+ ].getService(Ci.nsIHttpProtocolHandler);
+
+ return {
+ body: {
+ Browser: `${Services.appinfo.name}/${Services.appinfo.version}`,
+ "Protocol-Version": "1.3",
+ "User-Agent": userAgent,
+ "V8-Version": "1.0",
+ "WebKit-Version": "1.0",
+ webSocketDebuggerUrl: mainProcessTarget.toJSON().webSocketDebuggerUrl,
+ },
+ };
+ }
+
+ getProtocol() {
+ return { body: lazy.Protocol.Description };
+ }
+
+ getTargetList() {
+ return { body: [...this.cdp.targetList].filter(x => x.type !== "browser") };
+ }
+
+ /** HTTP copy of Target.createTarget() */
+ async newTarget(url) {
+ const onTarget = this.cdp.targetList.once("target-created");
+
+ // Open new tab
+ const tab = await lazy.TabManager.addTab({
+ focus: true,
+ });
+
+ // Get the newly created target
+ const target = await onTarget;
+ if (tab.linkedBrowser != target.browser) {
+ throw new Error(
+ "Unexpected tab opened: " + tab.linkedBrowser.currentURI.spec
+ );
+ }
+
+ const returnJson = target.toJSON();
+
+ // Load URL if given, otherwise stay on about:blank
+ if (url) {
+ let validURL;
+ try {
+ validURL = Services.io.newURI(url);
+ } catch {
+ // If we failed to parse given URL, return now since we already loaded about:blank
+ return { body: returnJson };
+ }
+
+ target.browsingContext.loadURI(validURL, {
+ triggeringPrincipal:
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ });
+
+ // Force the URL in the returned target JSON to match given
+ // even if loading/will fail (matches Chromium behavior)
+ returnJson.url = url;
+ }
+
+ return { body: returnJson };
+ }
+
+ /** HTTP copy of Target.activateTarget() */
+ async activateTarget(targetId) {
+ // Try to get the target from given id
+ const target = this.cdp.targetList.getById(targetId);
+
+ if (!target) {
+ return {
+ status: lazy.HTTP_404,
+ body: `No such target id: ${targetId}`,
+ json: false,
+ };
+ }
+
+ // Select the tab (this endpoint does not show the window)
+ await lazy.TabManager.selectTab(target.tab);
+
+ return { body: "Target activated", json: false };
+ }
+
+ /** HTTP copy of Target.closeTarget() */
+ async closeTarget(targetId) {
+ // Try to get the target from given id
+ const target = this.cdp.targetList.getById(targetId);
+
+ if (!target) {
+ return {
+ status: lazy.HTTP_404,
+ body: `No such target id: ${targetId}`,
+ json: false,
+ };
+ }
+
+ // Remove the tab
+ await lazy.TabManager.removeTab(target.tab);
+
+ return { body: "Target is closing", json: false };
+ }
+
+ // nsIHttpRequestHandler
+
+ async handle(request, response) {
+ // Mark request as async so we can execute async routes and return values
+ response.processAsync();
+
+ // Run a provided route (function) with an argument
+ const runRoute = async (route, data) => {
+ try {
+ // Run the route to get data to return
+ const {
+ status = { code: 200, description: "OK" },
+ json = true,
+ body,
+ } = await route(data);
+
+ // Stringify into returnable JSON if wanted
+ const payload = json
+ ? JSON.stringify(body, null, lazy.Log.verbose ? "\t" : null)
+ : body;
+
+ // Handle HTTP response
+ response.setStatusLine(
+ request.httpVersion,
+ status.code,
+ status.description
+ );
+ response.setHeader("Content-Type", "application/json");
+ response.setHeader("Content-Security-Policy", "frame-ancestors 'none'");
+ response.write(payload);
+ } catch (e) {
+ new lazy.RemoteAgentError(e).notify();
+
+ // Mark as 500 as an error has occured internally
+ response.setStatusLine(
+ request.httpVersion,
+ lazy.HTTP_500.code,
+ lazy.HTTP_500.description
+ );
+ }
+ };
+
+ // Trim trailing slashes to conform with expected routes
+ const path = request.path.replace(/\/+$/, "");
+
+ let route;
+ for (const _route in this.routes) {
+ // Prefixed/parameter route (/path/{parameter})
+ if (path.startsWith(_route + "/") && this.routes[_route].parameter) {
+ route = _route;
+ break;
+ }
+
+ // Regular route (/path/example)
+ if (path === _route) {
+ route = _route;
+ break;
+ }
+ }
+
+ if (!route) {
+ // Route does not exist
+ response.setStatusLine(
+ request.httpVersion,
+ lazy.HTTP_404.code,
+ lazy.HTTP_404.description
+ );
+ response.write("Unknown command: " + path.replace("/json/", ""));
+
+ return response.finish();
+ }
+
+ const { handler, method, parameter } = this.routes[route];
+
+ // If only one valid method for route, check method matches
+ if (method && request.method !== method) {
+ response.setStatusLine(
+ request.httpVersion,
+ lazy.HTTP_405.code,
+ lazy.HTTP_405.description
+ );
+ response.write(
+ `Using unsafe HTTP verb ${request.method} to invoke ${route}. This action supports only PUT verb.`
+ );
+ return response.finish();
+ }
+
+ if (parameter) {
+ await runRoute(handler, path.split("/").pop());
+ } else {
+ await runRoute(handler, request.queryString);
+ }
+
+ // Send response
+ return response.finish();
+ }
+
+ // XPCOM
+
+ get QueryInterface() {
+ return ChromeUtils.generateQI(["nsIHttpRequestHandler"]);
+ }
+}