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/. */
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;
22 const CryptoHash = CC(
23 "@mozilla.org/security/hash;1",
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.
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.
38 function writeString(output, data) {
39 return new Promise((resolve, reject) => {
41 if (data.length === 0) {
49 const written = output.write(data, data.length);
50 data = data.slice(written);
58 threadManager.currentThread
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);
73 * Process the WebSocket handshake headers and return the key to be sent in
74 * Sec-WebSocket-Accept response header.
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");
82 const upgrade = headers.get("upgrade");
83 if (!upgrade || upgrade !== "websocket") {
84 throw new Error("The handshake request has incorrect Upgrade header");
87 const connection = headers.get("connection");
95 throw new Error("The handshake request has incorrect Connection header");
98 const version = headers.get("sec-websocket-version");
99 if (!version || version !== "13") {
101 "The handshake request must have Sec-WebSocket-Version: 13"
105 // Compute the accept key
106 const key = headers.get("sec-websocket-key");
109 "The handshake request must have a Sec-WebSocket-Key header"
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.
128 async function serverHandshake(request, output) {
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}`,
141 // Send error response in case of error
142 await writeHttpResponse(output, ["HTTP/1.1 400 Bad Request"]);
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);
157 return new Promise((resolve, reject) => {
158 const socket = WebSocket.createServerWebSocket(
164 socket.addEventListener("close", () => {
169 socket.onopen = () => resolve(socket);
170 socket.onerror = err => reject(err);
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"));
185 const convertedRequest = {
186 requestLine: `${request.method} ${request.path}`,
189 await serverHandshake(convertedRequest, output);
191 return createWebSocket(transport, input, output);
194 const WebSocketHandshake = { upgrade };