no bug - Import translations from android-l10n r=release a=l10n CLOSED TREE
[gecko.git] / remote / cdp / JSONHandler.sys.mjs
blob5cb81d6a9a883d05efcbacca99566d1a9cbd986a
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2  * License, v. 2.0. If a copy of the MPL was not distributed with this
3  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 const lazy = {};
7 ChromeUtils.defineESModuleGetters(lazy, {
8   Log: "chrome://remote/content/shared/Log.sys.mjs",
9   HTTP_404: "chrome://remote/content/server/httpd.sys.mjs",
10   HTTP_405: "chrome://remote/content/server/httpd.sys.mjs",
11   HTTP_500: "chrome://remote/content/server/httpd.sys.mjs",
12   Protocol: "chrome://remote/content/cdp/Protocol.sys.mjs",
13   RemoteAgentError: "chrome://remote/content/cdp/Error.sys.mjs",
14   TabManager: "chrome://remote/content/shared/TabManager.sys.mjs",
15 });
17 export class JSONHandler {
18   constructor(cdp) {
19     this.cdp = cdp;
20     this.routes = {
21       "/json/version": {
22         handler: this.getVersion.bind(this),
23       },
25       "/json/protocol": {
26         handler: this.getProtocol.bind(this),
27       },
29       "/json/list": {
30         handler: this.getTargetList.bind(this),
31       },
33       "/json": {
34         handler: this.getTargetList.bind(this),
35       },
37       // PUT only - /json/new?{url}
38       "/json/new": {
39         handler: this.newTarget.bind(this),
40         method: "PUT",
41       },
43       // /json/activate/{targetId}
44       "/json/activate": {
45         handler: this.activateTarget.bind(this),
46         parameter: true,
47       },
49       // /json/close/{targetId}
50       "/json/close": {
51         handler: this.closeTarget.bind(this),
52         parameter: true,
53       },
54     };
55   }
57   getVersion() {
58     const mainProcessTarget = this.cdp.targetList.getMainProcessTarget();
60     const { userAgent } = Cc[
61       "@mozilla.org/network/protocol;1?name=http"
62     ].getService(Ci.nsIHttpProtocolHandler);
64     return {
65       body: {
66         Browser: `${Services.appinfo.name}/${Services.appinfo.version}`,
67         "Protocol-Version": "1.3",
68         "User-Agent": userAgent,
69         "V8-Version": "1.0",
70         "WebKit-Version": "1.0",
71         webSocketDebuggerUrl: mainProcessTarget.toJSON().webSocketDebuggerUrl,
72       },
73     };
74   }
76   getProtocol() {
77     return { body: lazy.Protocol.Description };
78   }
80   getTargetList() {
81     return { body: [...this.cdp.targetList].filter(x => x.type !== "browser") };
82   }
84   /** HTTP copy of Target.createTarget() */
85   async newTarget(url) {
86     const onTarget = this.cdp.targetList.once("target-created");
88     // Open new tab
89     const tab = await lazy.TabManager.addTab({
90       focus: true,
91     });
93     // Get the newly created target
94     const target = await onTarget;
95     if (tab.linkedBrowser != target.browser) {
96       throw new Error(
97         "Unexpected tab opened: " + tab.linkedBrowser.currentURI.spec
98       );
99     }
101     const returnJson = target.toJSON();
103     // Load URL if given, otherwise stay on about:blank
104     if (url) {
105       let validURL;
106       try {
107         validURL = Services.io.newURI(url);
108       } catch {
109         // If we failed to parse given URL, return now since we already loaded about:blank
110         return { body: returnJson };
111       }
113       target.browsingContext.loadURI(validURL, {
114         triggeringPrincipal:
115           Services.scriptSecurityManager.getSystemPrincipal(),
116       });
118       // Force the URL in the returned target JSON to match given
119       // even if loading/will fail (matches Chromium behavior)
120       returnJson.url = url;
121     }
123     return { body: returnJson };
124   }
126   /** HTTP copy of Target.activateTarget() */
127   async activateTarget(targetId) {
128     // Try to get the target from given id
129     const target = this.cdp.targetList.getById(targetId);
131     if (!target) {
132       return {
133         status: lazy.HTTP_404,
134         body: `No such target id: ${targetId}`,
135         json: false,
136       };
137     }
139     // Select the tab (this endpoint does not show the window)
140     await lazy.TabManager.selectTab(target.tab);
142     return { body: "Target activated", json: false };
143   }
145   /** HTTP copy of Target.closeTarget() */
146   async closeTarget(targetId) {
147     // Try to get the target from given id
148     const target = this.cdp.targetList.getById(targetId);
150     if (!target) {
151       return {
152         status: lazy.HTTP_404,
153         body: `No such target id: ${targetId}`,
154         json: false,
155       };
156     }
158     // Remove the tab
159     await lazy.TabManager.removeTab(target.tab);
161     return { body: "Target is closing", json: false };
162   }
164   // nsIHttpRequestHandler
166   async handle(request, response) {
167     // Mark request as async so we can execute async routes and return values
168     response.processAsync();
170     // Run a provided route (function) with an argument
171     const runRoute = async (route, data) => {
172       try {
173         // Run the route to get data to return
174         const {
175           status = { code: 200, description: "OK" },
176           json = true,
177           body,
178         } = await route(data);
180         // Stringify into returnable JSON if wanted
181         const payload = json
182           ? JSON.stringify(body, null, lazy.Log.verbose ? "\t" : null)
183           : body;
185         // Handle HTTP response
186         response.setStatusLine(
187           request.httpVersion,
188           status.code,
189           status.description
190         );
191         response.setHeader("Content-Type", "application/json");
192         response.setHeader("Content-Security-Policy", "frame-ancestors 'none'");
193         response.write(payload);
194       } catch (e) {
195         new lazy.RemoteAgentError(e).notify();
197         // Mark as 500 as an error has occured internally
198         response.setStatusLine(
199           request.httpVersion,
200           lazy.HTTP_500.code,
201           lazy.HTTP_500.description
202         );
203       }
204     };
206     // Trim trailing slashes to conform with expected routes
207     const path = request.path.replace(/\/+$/, "");
209     let route;
210     for (const _route in this.routes) {
211       // Prefixed/parameter route (/path/{parameter})
212       if (path.startsWith(_route + "/") && this.routes[_route].parameter) {
213         route = _route;
214         break;
215       }
217       // Regular route (/path/example)
218       if (path === _route) {
219         route = _route;
220         break;
221       }
222     }
224     if (!route) {
225       // Route does not exist
226       response.setStatusLine(
227         request.httpVersion,
228         lazy.HTTP_404.code,
229         lazy.HTTP_404.description
230       );
231       response.write("Unknown command: " + path.replace("/json/", ""));
233       return response.finish();
234     }
236     const { handler, method, parameter } = this.routes[route];
238     // If only one valid method for route, check method matches
239     if (method && request.method !== method) {
240       response.setStatusLine(
241         request.httpVersion,
242         lazy.HTTP_405.code,
243         lazy.HTTP_405.description
244       );
245       response.write(
246         `Using unsafe HTTP verb ${request.method} to invoke ${route}. This action supports only PUT verb.`
247       );
248       return response.finish();
249     }
251     if (parameter) {
252       await runRoute(handler, path.split("/").pop());
253     } else {
254       await runRoute(handler, request.queryString);
255     }
257     // Send response
258     return response.finish();
259   }
261   // XPCOM
263   get QueryInterface() {
264     return ChromeUtils.generateQI(["nsIHttpRequestHandler"]);
265   }