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 = ["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",
24 XPCOMUtils.defineLazyGetter(this, "CryptoHash", () => {
25 return CC("@mozilla.org/security/hash;1", "nsICryptoHash", "initWithString");
28 XPCOMUtils.defineLazyGetter(this, "threadManager", () => {
29 return Cc["@mozilla.org/thread-manager;1"].getService();
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;
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.
51 function writeString(output, data) {
52 return new Promise((resolve, reject) => {
54 if (data.length === 0) {
62 const written = output.write(data, data.length);
63 data = data.slice(written);
71 threadManager.currentThread
80 * Write HTTP response with headers (array of strings) and body
81 * to async output stream.
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);
91 * Check if the provided URI's host is an IP address.
97 function isIPAddress(uri) {
99 // getBaseDomain throws an explicit error if the uri host is an IP address.
100 Services.eTLD.getBaseDomain(uri);
102 return e.result == Cr.NS_ERROR_HOST_IS_IP_ADDRESS;
108 * Process the WebSocket handshake headers and return the key to be sent in
109 * Sec-WebSocket-Accept response header.
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) {
118 `The handshake request has incorrect Origin header ${origin}`
122 const hostHeader = headers.get("host");
124 let hostUri, host, port;
126 // Might throw both when calling newURI or when accessing the host/port.
127 hostUri = Services.io.newURI(`https://${hostHeader}`);
128 ({ host, port } = hostUri);
131 `The handshake request Host header must be a well-formed host: ${hostHeader}`
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) {
140 `The handshake request has incorrect Host header ${hostHeader}`
144 const method = requestLine.split(" ")[0];
145 if (method !== "GET") {
146 throw new Error("The handshake request must use GET method");
149 const upgrade = headers.get("upgrade");
150 if (!upgrade || upgrade.toLowerCase() !== "websocket") {
152 `The handshake request has incorrect Upgrade header: ${upgrade}`
156 const connection = headers.get("connection");
161 .map(t => t.trim().toLowerCase())
164 throw new Error("The handshake request has incorrect Connection header");
167 const version = headers.get("sec-websocket-version");
168 if (!version || version !== "13") {
170 "The handshake request must have Sec-WebSocket-Version: 13"
174 // Compute the accept key
175 const key = headers.get("sec-websocket-key");
178 "The handshake request must have a Sec-WebSocket-Key header"
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.
197 async function serverHandshake(request, output) {
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",
206 "Upgrade: websocket",
207 "Connection: Upgrade",
208 `Sec-WebSocket-Accept: ${acceptKey}`,
211 // Send error response in case of error
212 await writeHttpResponse(
215 "HTTP/1.1 400 Bad Request",
217 "Content-Type: text/plain",
226 async function createWebSocket(transport, input, output) {
227 const transportProvider = {
228 setListener(upgradeListener) {
229 // onTransportAvailable callback shouldn't be called synchronously
231 upgradeListener.onTransportAvailable(transport, input, output);
236 return new Promise((resolve, reject) => {
237 const socket = WebSocket.createServerWebSocket(
243 socket.addEventListener("close", () => {
248 socket.onopen = () => resolve(socket);
249 socket.onerror = err => reject(err);
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"));
264 const convertedRequest = {
265 requestLine: `${request.method} ${request.path}`,
268 await serverHandshake(convertedRequest, output);
270 return createWebSocket(transport, input, output);
273 const WebSocketHandshake = { upgrade };