Bug 1869043 add a main thread record of track audio outputs r=padenot
[gecko.git] / remote / server / WebSocketHandshake.sys.mjs
blobf137b484aeeed96eff0d7a51acf7bd2827ca0b83
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 // This file is an XPCOM service-ified copy of ../devtools/server/socket/websocket-server.js.
7 const CC = Components.Constructor;
9 const lazy = {};
11 ChromeUtils.defineESModuleGetters(lazy, {
12   executeSoon: "chrome://remote/content/shared/Sync.sys.mjs",
13   Log: "chrome://remote/content/shared/Log.sys.mjs",
14   RemoteAgent: "chrome://remote/content/components/RemoteAgent.sys.mjs",
15 });
17 ChromeUtils.defineLazyGetter(lazy, "logger", () => lazy.Log.get());
19 ChromeUtils.defineLazyGetter(lazy, "CryptoHash", () => {
20   return CC("@mozilla.org/security/hash;1", "nsICryptoHash", "initWithString");
21 });
23 ChromeUtils.defineLazyGetter(lazy, "threadManager", () => {
24   return Cc["@mozilla.org/thread-manager;1"].getService();
25 });
27 /**
28  * Allowed origins are exposed through 2 separate getters because while most
29  * of the values should be valid URIs, `null` is also a valid origin and cannot
30  * be converted to a URI. Call sites interested in checking for null should use
31  * `allowedOrigins`, those interested in URIs should use `allowedOriginURIs`.
32  */
33 ChromeUtils.defineLazyGetter(lazy, "allowedOrigins", () =>
34   lazy.RemoteAgent.allowOrigins !== null ? lazy.RemoteAgent.allowOrigins : []
37 ChromeUtils.defineLazyGetter(lazy, "allowedOriginURIs", () => {
38   return lazy.allowedOrigins
39     .map(origin => {
40       try {
41         const originURI = Services.io.newURI(origin);
42         // Make sure to read host/port/scheme as those getters could throw for
43         // invalid URIs.
44         return {
45           host: originURI.host,
46           port: originURI.port,
47           scheme: originURI.scheme,
48         };
49       } catch (e) {
50         return null;
51       }
52     })
53     .filter(uri => uri !== null);
54 });
56 /**
57  * Write a string of bytes to async output stream
58  * and return promise that resolves once all data has been written.
59  * Doesn't do any UTF-16/UTF-8 conversion.
60  * The string is treated as an array of bytes.
61  */
62 function writeString(output, data) {
63   return new Promise((resolve, reject) => {
64     const wait = () => {
65       if (data.length === 0) {
66         resolve();
67         return;
68       }
70       output.asyncWait(
71         stream => {
72           try {
73             const written = output.write(data, data.length);
74             data = data.slice(written);
75             wait();
76           } catch (ex) {
77             reject(ex);
78           }
79         },
80         0,
81         0,
82         lazy.threadManager.currentThread
83       );
84     };
86     wait();
87   });
90 /**
91  * Write HTTP response with headers (array of strings) and body
92  * to async output stream.
93  */
94 function writeHttpResponse(output, headers, body = "") {
95   headers.push(`Content-Length: ${body.length}`);
97   const s = headers.join("\r\n") + `\r\n\r\n${body}`;
98   return writeString(output, s);
102  * Check if the provided URI's host is an IP address.
104  * @param {nsIURI} uri
105  *     The URI to check.
106  * @returns {boolean}
107  */
108 function isIPAddress(uri) {
109   try {
110     // getBaseDomain throws an explicit error if the uri host is an IP address.
111     Services.eTLD.getBaseDomain(uri);
112   } catch (e) {
113     return e.result == Cr.NS_ERROR_HOST_IS_IP_ADDRESS;
114   }
115   return false;
118 function isHostValid(hostHeader) {
119   try {
120     // Might throw both when calling newURI or when accessing the host/port.
121     const hostUri = Services.io.newURI(`https://${hostHeader}`);
122     const { host, port } = hostUri;
123     const isHostnameValid =
124       isIPAddress(hostUri) || lazy.RemoteAgent.allowHosts.includes(host);
125     // For nsIURI a port value of -1 corresponds to the protocol's default port.
126     const isPortValid = [-1, lazy.RemoteAgent.port].includes(port);
127     return isHostnameValid && isPortValid;
128   } catch (e) {
129     return false;
130   }
133 function isOriginValid(originHeader) {
134   if (originHeader === undefined) {
135     // Always accept no origin header.
136     return true;
137   }
139   // Special case "null" origins, used for privacy sensitive or opaque origins.
140   if (originHeader === "null") {
141     return lazy.allowedOrigins.includes("null");
142   }
144   try {
145     // Extract the host, port and scheme from the provided origin header.
146     const { host, port, scheme } = Services.io.newURI(originHeader);
147     // Check if any allowed origin matches the provided host, port and scheme.
148     return lazy.allowedOriginURIs.some(
149       uri => uri.host === host && uri.port === port && uri.scheme === scheme
150     );
151   } catch (e) {
152     // Reject invalid origin headers
153     return false;
154   }
158  * Process the WebSocket handshake headers and return the key to be sent in
159  * Sec-WebSocket-Accept response header.
160  */
161 function processRequest({ requestLine, headers }) {
162   if (!isOriginValid(headers.get("origin"))) {
163     lazy.logger.debug(
164       `Incorrect Origin header, allowed origins: [${lazy.allowedOrigins}]`
165     );
166     throw new Error(
167       `The handshake request has incorrect Origin header ${headers.get(
168         "origin"
169       )}`
170     );
171   }
173   if (!isHostValid(headers.get("host"))) {
174     lazy.logger.debug(
175       `Incorrect Host header, allowed hosts: [${lazy.RemoteAgent.allowHosts}]`
176     );
177     throw new Error(
178       `The handshake request has incorrect Host header ${headers.get("host")}`
179     );
180   }
182   const method = requestLine.split(" ")[0];
183   if (method !== "GET") {
184     throw new Error("The handshake request must use GET method");
185   }
187   const upgrade = headers.get("upgrade");
188   if (!upgrade || upgrade.toLowerCase() !== "websocket") {
189     throw new Error(
190       `The handshake request has incorrect Upgrade header: ${upgrade}`
191     );
192   }
194   const connection = headers.get("connection");
195   if (
196     !connection ||
197     !connection
198       .split(",")
199       .map(t => t.trim().toLowerCase())
200       .includes("upgrade")
201   ) {
202     throw new Error("The handshake request has incorrect Connection header");
203   }
205   const version = headers.get("sec-websocket-version");
206   if (!version || version !== "13") {
207     throw new Error(
208       "The handshake request must have Sec-WebSocket-Version: 13"
209     );
210   }
212   // Compute the accept key
213   const key = headers.get("sec-websocket-key");
214   if (!key) {
215     throw new Error(
216       "The handshake request must have a Sec-WebSocket-Key header"
217     );
218   }
220   return { acceptKey: computeKey(key) };
223 function computeKey(key) {
224   const str = `${key}258EAFA5-E914-47DA-95CA-C5AB0DC85B11`;
225   const data = Array.from(str, ch => ch.charCodeAt(0));
226   const hash = new lazy.CryptoHash("sha1");
227   hash.update(data, data.length);
228   return hash.finish(true);
232  * Perform the server part of a WebSocket opening handshake
233  * on an incoming connection.
234  */
235 async function serverHandshake(request, output) {
236   try {
237     // Check and extract info from the request
238     const { acceptKey } = processRequest(request);
240     // Send response headers
241     await writeHttpResponse(output, [
242       "HTTP/1.1 101 Switching Protocols",
243       "Server: httpd.js",
244       "Upgrade: websocket",
245       "Connection: Upgrade",
246       `Sec-WebSocket-Accept: ${acceptKey}`,
247     ]);
248   } catch (error) {
249     // Send error response in case of error
250     await writeHttpResponse(
251       output,
252       [
253         "HTTP/1.1 400 Bad Request",
254         "Server: httpd.js",
255         "Content-Type: text/plain",
256       ],
257       error.message
258     );
260     throw error;
261   }
264 async function createWebSocket(transport, input, output) {
265   const transportProvider = {
266     setListener(upgradeListener) {
267       // onTransportAvailable callback shouldn't be called synchronously
268       lazy.executeSoon(() => {
269         upgradeListener.onTransportAvailable(transport, input, output);
270       });
271     },
272   };
274   return new Promise((resolve, reject) => {
275     const socket = WebSocket.createServerWebSocket(
276       null,
277       [],
278       transportProvider,
279       ""
280     );
281     socket.addEventListener("close", () => {
282       input.close();
283       output.close();
284     });
286     socket.onopen = () => resolve(socket);
287     socket.onerror = err => reject(err);
288   });
291 /** Upgrade an existing HTTP request from httpd.js to WebSocket. */
292 async function upgrade(request, response) {
293   // handle response manually, allowing us to send arbitrary data
294   response._powerSeized = true;
296   const { transport, input, output } = response._connection;
298   lazy.logger.info(
299     `Perform WebSocket upgrade for incoming connection from ${transport.host}:${transport.port}`
300   );
302   const headers = new Map();
303   for (let [key, values] of Object.entries(request._headers._headers)) {
304     headers.set(key, values.join("\n"));
305   }
306   const convertedRequest = {
307     requestLine: `${request.method} ${request.path}`,
308     headers,
309   };
310   await serverHandshake(convertedRequest, output);
312   return createWebSocket(transport, input, output);
315 export const WebSocketHandshake = { upgrade };