Bug 1550804 - Add color scheme simulation to the inspector. r=pbro
[gecko.git] / remote / server / WebSocketHandshake.jsm
blobbda9a1fda83124aa5d8042e107000841521373e9
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 = ["WebSocketHandshake"];
9 // This file is an XPCOM service-ified copy of ../devtools/server/socket/websocket-server.js.
11 const CC = Components.Constructor;
13 const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
14 const { XPCOMUtils } = ChromeUtils.import(
15   "resource://gre/modules/XPCOMUtils.jsm"
18 XPCOMUtils.defineLazyGetter(this, "WebSocket", () => {
19   return Services.appShell.hiddenDOMWindow.WebSocket;
20 });
22 const CryptoHash = CC(
23   "@mozilla.org/security/hash;1",
24   "nsICryptoHash",
25   "initWithString"
27 const threadManager = Cc["@mozilla.org/thread-manager;1"].getService();
29 // TODO(ato): Merge this with httpd.js so that we can respond to both HTTP/1.1
30 // as well as WebSocket requests on the same server.
32 /**
33  * Write a string of bytes to async output stream
34  * and return promise that resolves once all data has been written.
35  * Doesn't do any UTF-16/UTF-8 conversion.
36  * The string is treated as an array of bytes.
37  */
38 function writeString(output, data) {
39   return new Promise((resolve, reject) => {
40     const wait = () => {
41       if (data.length === 0) {
42         resolve();
43         return;
44       }
46       output.asyncWait(
47         stream => {
48           try {
49             const written = output.write(data, data.length);
50             data = data.slice(written);
51             wait();
52           } catch (ex) {
53             reject(ex);
54           }
55         },
56         0,
57         0,
58         threadManager.currentThread
59       );
60     };
62     wait();
63   });
66 /** Write HTTP response (array of strings) to async output stream. */
67 function writeHttpResponse(output, response) {
68   const s = response.join("\r\n") + "\r\n\r\n";
69   return writeString(output, s);
72 /**
73  * Process the WebSocket handshake headers and return the key to be sent in
74  * Sec-WebSocket-Accept response header.
75  */
76 function processRequest({ requestLine, headers }) {
77   const method = requestLine.split(" ")[0];
78   if (method !== "GET") {
79     throw new Error("The handshake request must use GET method");
80   }
82   const upgrade = headers.get("upgrade");
83   if (!upgrade || upgrade !== "websocket") {
84     throw new Error("The handshake request has incorrect Upgrade header");
85   }
87   const connection = headers.get("connection");
88   if (
89     !connection ||
90     !connection
91       .split(",")
92       .map(t => t.trim())
93       .includes("Upgrade")
94   ) {
95     throw new Error("The handshake request has incorrect Connection header");
96   }
98   const version = headers.get("sec-websocket-version");
99   if (!version || version !== "13") {
100     throw new Error(
101       "The handshake request must have Sec-WebSocket-Version: 13"
102     );
103   }
105   // Compute the accept key
106   const key = headers.get("sec-websocket-key");
107   if (!key) {
108     throw new Error(
109       "The handshake request must have a Sec-WebSocket-Key header"
110     );
111   }
113   return { acceptKey: computeKey(key) };
116 function computeKey(key) {
117   const str = `${key}258EAFA5-E914-47DA-95CA-C5AB0DC85B11`;
118   const data = Array.from(str, ch => ch.charCodeAt(0));
119   const hash = new CryptoHash("sha1");
120   hash.update(data, data.length);
121   return hash.finish(true);
125  * Perform the server part of a WebSocket opening handshake
126  * on an incoming connection.
127  */
128 async function serverHandshake(request, output) {
129   try {
130     // Check and extract info from the request
131     const { acceptKey } = processRequest(request);
133     // Send response headers
134     await writeHttpResponse(output, [
135       "HTTP/1.1 101 Switching Protocols",
136       "Upgrade: websocket",
137       "Connection: Upgrade",
138       `Sec-WebSocket-Accept: ${acceptKey}`,
139     ]);
140   } catch (error) {
141     // Send error response in case of error
142     await writeHttpResponse(output, ["HTTP/1.1 400 Bad Request"]);
143     throw error;
144   }
147 async function createWebSocket(transport, input, output) {
148   const transportProvider = {
149     setListener(upgradeListener) {
150       // onTransportAvailable callback shouldn't be called synchronously
151       Services.tm.dispatchToMainThread(() => {
152         upgradeListener.onTransportAvailable(transport, input, output);
153       });
154     },
155   };
157   return new Promise((resolve, reject) => {
158     const socket = WebSocket.createServerWebSocket(
159       null,
160       [],
161       transportProvider,
162       ""
163     );
164     socket.addEventListener("close", () => {
165       input.close();
166       output.close();
167     });
169     socket.onopen = () => resolve(socket);
170     socket.onerror = err => reject(err);
171   });
174 /** Upgrade an existing HTTP request from httpd.js to WebSocket. */
175 async function upgrade(request, response) {
176   // handle response manually, allowing us to send arbitrary data
177   response._powerSeized = true;
179   const { transport, input, output } = response._connection;
181   const headers = new Map();
182   for (let [key, values] of Object.entries(request._headers._headers)) {
183     headers.set(key, values.join("\n"));
184   }
185   const convertedRequest = {
186     requestLine: `${request.method} ${request.path}`,
187     headers,
188   };
189   await serverHandshake(convertedRequest, output);
191   return createWebSocket(transport, input, output);
194 const WebSocketHandshake = { upgrade };