Bug 1747279 [wpt PR 32174] - Make Transform::TransformRRectF tolerate values within...
[gecko.git] / remote / server / WebSocketHandshake.jsm
blobcaf38e1d23a7de110a1b0268d170e90e96e90643
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 "use strict";
7 var EXPORTED_SYMBOLS = ["allowNullOrigin", "WebSocketHandshake"];
9 // This file is an XPCOM service-ified copy of ../devtools/server/socket/websocket-server.js.
11 const CC = Components.Constructor;
13 const { XPCOMUtils } = ChromeUtils.import(
14   "resource://gre/modules/XPCOMUtils.jsm"
17 XPCOMUtils.defineLazyModuleGetters(this, {
18   Services: "resource://gre/modules/Services.jsm",
20   executeSoon: "chrome://remote/content/shared/Sync.jsm",
21   RemoteAgent: "chrome://remote/content/components/RemoteAgent.jsm",
22 });
24 XPCOMUtils.defineLazyGetter(this, "CryptoHash", () => {
25   return CC("@mozilla.org/security/hash;1", "nsICryptoHash", "initWithString");
26 });
28 XPCOMUtils.defineLazyGetter(this, "threadManager", () => {
29   return Cc["@mozilla.org/thread-manager;1"].getService();
30 });
32 // TODO(ato): Merge this with httpd.js so that we can respond to both HTTP/1.1
33 // as well as WebSocket requests on the same server.
35 // Well-known localhost loopback addresses.
36 const LOOPBACKS = ["localhost", "127.0.0.1", "[::1]"];
38 // This should only be used by the CDP browser mochitests which create a
39 // websocket handshake with a non-null origin.
40 let nullOriginAllowed = false;
41 function allowNullOrigin(allowed) {
42   nullOriginAllowed = allowed;
45 /**
46  * Write a string of bytes to async output stream
47  * and return promise that resolves once all data has been written.
48  * Doesn't do any UTF-16/UTF-8 conversion.
49  * The string is treated as an array of bytes.
50  */
51 function writeString(output, data) {
52   return new Promise((resolve, reject) => {
53     const wait = () => {
54       if (data.length === 0) {
55         resolve();
56         return;
57       }
59       output.asyncWait(
60         stream => {
61           try {
62             const written = output.write(data, data.length);
63             data = data.slice(written);
64             wait();
65           } catch (ex) {
66             reject(ex);
67           }
68         },
69         0,
70         0,
71         threadManager.currentThread
72       );
73     };
75     wait();
76   });
79 /**
80  * Write HTTP response with headers (array of strings) and body
81  * to async output stream.
82  */
83 function writeHttpResponse(output, headers, body = "") {
84   headers.push(`Content-Length: ${body.length}`);
86   const s = headers.join("\r\n") + `\r\n\r\n${body}`;
87   return writeString(output, s);
90 /**
91  * Check if the provided URI's host is an IP address.
92  *
93  * @param {nsIURI} uri
94  *     The URI to check.
95  * @return {boolean}
96  */
97 function isIPAddress(uri) {
98   try {
99     // getBaseDomain throws an explicit error if the uri host is an IP address.
100     Services.eTLD.getBaseDomain(uri);
101   } catch (e) {
102     return e.result == Cr.NS_ERROR_HOST_IS_IP_ADDRESS;
103   }
104   return false;
108  * Process the WebSocket handshake headers and return the key to be sent in
109  * Sec-WebSocket-Accept response header.
110  */
111 function processRequest({ requestLine, headers }) {
112   const origin = headers.get("origin");
114   // A "null" origin is exceptionally allowed in browser mochitests.
115   const isTestOrigin = origin === "null" && nullOriginAllowed;
116   if (headers.has("origin") && !isTestOrigin) {
117     throw new Error(
118       `The handshake request has incorrect Origin header ${origin}`
119     );
120   }
122   const hostHeader = headers.get("host");
124   let hostUri, host, port;
125   try {
126     // Might throw both when calling newURI or when accessing the host/port.
127     hostUri = Services.io.newURI(`https://${hostHeader}`);
128     ({ host, port } = hostUri);
129   } catch (e) {
130     throw new Error(
131       `The handshake request Host header must be a well-formed host: ${hostHeader}`
132     );
133   }
135   const isHostnameValid = LOOPBACKS.includes(host) || isIPAddress(hostUri);
136   // For nsIURI a port value of -1 corresponds to the protocol's default port.
137   const isPortValid = port === -1 || port == RemoteAgent.port;
138   if (!isHostnameValid || !isPortValid) {
139     throw new Error(
140       `The handshake request has incorrect Host header ${hostHeader}`
141     );
142   }
144   const method = requestLine.split(" ")[0];
145   if (method !== "GET") {
146     throw new Error("The handshake request must use GET method");
147   }
149   const upgrade = headers.get("upgrade");
150   if (!upgrade || upgrade.toLowerCase() !== "websocket") {
151     throw new Error(
152       `The handshake request has incorrect Upgrade header: ${upgrade}`
153     );
154   }
156   const connection = headers.get("connection");
157   if (
158     !connection ||
159     !connection
160       .split(",")
161       .map(t => t.trim().toLowerCase())
162       .includes("upgrade")
163   ) {
164     throw new Error("The handshake request has incorrect Connection header");
165   }
167   const version = headers.get("sec-websocket-version");
168   if (!version || version !== "13") {
169     throw new Error(
170       "The handshake request must have Sec-WebSocket-Version: 13"
171     );
172   }
174   // Compute the accept key
175   const key = headers.get("sec-websocket-key");
176   if (!key) {
177     throw new Error(
178       "The handshake request must have a Sec-WebSocket-Key header"
179     );
180   }
182   return { acceptKey: computeKey(key) };
185 function computeKey(key) {
186   const str = `${key}258EAFA5-E914-47DA-95CA-C5AB0DC85B11`;
187   const data = Array.from(str, ch => ch.charCodeAt(0));
188   const hash = new CryptoHash("sha1");
189   hash.update(data, data.length);
190   return hash.finish(true);
194  * Perform the server part of a WebSocket opening handshake
195  * on an incoming connection.
196  */
197 async function serverHandshake(request, output) {
198   try {
199     // Check and extract info from the request
200     const { acceptKey } = processRequest(request);
202     // Send response headers
203     await writeHttpResponse(output, [
204       "HTTP/1.1 101 Switching Protocols",
205       "Server: httpd.js",
206       "Upgrade: websocket",
207       "Connection: Upgrade",
208       `Sec-WebSocket-Accept: ${acceptKey}`,
209     ]);
210   } catch (error) {
211     // Send error response in case of error
212     await writeHttpResponse(
213       output,
214       [
215         "HTTP/1.1 400 Bad Request",
216         "Server: httpd.js",
217         "Content-Type: text/plain",
218       ],
219       error.message
220     );
222     throw error;
223   }
226 async function createWebSocket(transport, input, output) {
227   const transportProvider = {
228     setListener(upgradeListener) {
229       // onTransportAvailable callback shouldn't be called synchronously
230       executeSoon(() => {
231         upgradeListener.onTransportAvailable(transport, input, output);
232       });
233     },
234   };
236   return new Promise((resolve, reject) => {
237     const socket = WebSocket.createServerWebSocket(
238       null,
239       [],
240       transportProvider,
241       ""
242     );
243     socket.addEventListener("close", () => {
244       input.close();
245       output.close();
246     });
248     socket.onopen = () => resolve(socket);
249     socket.onerror = err => reject(err);
250   });
253 /** Upgrade an existing HTTP request from httpd.js to WebSocket. */
254 async function upgrade(request, response) {
255   // handle response manually, allowing us to send arbitrary data
256   response._powerSeized = true;
258   const { transport, input, output } = response._connection;
260   const headers = new Map();
261   for (let [key, values] of Object.entries(request._headers._headers)) {
262     headers.set(key, values.join("\n"));
263   }
264   const convertedRequest = {
265     requestLine: `${request.method} ${request.path}`,
266     headers,
267   };
268   await serverHandshake(convertedRequest, output);
270   return createWebSocket(transport, input, output);
273 const WebSocketHandshake = { upgrade };