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 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
9 const CC = Components.Constructor;
13 ChromeUtils.defineESModuleGetters(lazy, {
14 executeSoon: "chrome://remote/content/shared/Sync.sys.mjs",
15 Log: "chrome://remote/content/shared/Log.sys.mjs",
16 RemoteAgent: "chrome://remote/content/components/RemoteAgent.sys.mjs",
19 XPCOMUtils.defineLazyGetter(lazy, "logger", () => lazy.Log.get());
21 XPCOMUtils.defineLazyGetter(lazy, "CryptoHash", () => {
22 return CC("@mozilla.org/security/hash;1", "nsICryptoHash", "initWithString");
25 XPCOMUtils.defineLazyGetter(lazy, "threadManager", () => {
26 return Cc["@mozilla.org/thread-manager;1"].getService();
30 * Allowed origins are exposed through 2 separate getters because while most
31 * of the values should be valid URIs, `null` is also a valid origin and cannot
32 * be converted to a URI. Call sites interested in checking for null should use
33 * `allowedOrigins`, those interested in URIs should use `allowedOriginURIs`.
35 XPCOMUtils.defineLazyGetter(lazy, "allowedOrigins", () =>
36 lazy.RemoteAgent.allowOrigins !== null ? lazy.RemoteAgent.allowOrigins : []
39 XPCOMUtils.defineLazyGetter(lazy, "allowedOriginURIs", () => {
40 return lazy.allowedOrigins
43 const originURI = Services.io.newURI(origin);
44 // Make sure to read host/port/scheme as those getters could throw for
49 scheme: originURI.scheme,
55 .filter(uri => uri !== null);
59 * Write a string of bytes to async output stream
60 * and return promise that resolves once all data has been written.
61 * Doesn't do any UTF-16/UTF-8 conversion.
62 * The string is treated as an array of bytes.
64 function writeString(output, data) {
65 return new Promise((resolve, reject) => {
67 if (data.length === 0) {
75 const written = output.write(data, data.length);
76 data = data.slice(written);
84 lazy.threadManager.currentThread
93 * Write HTTP response with headers (array of strings) and body
94 * to async output stream.
96 function writeHttpResponse(output, headers, body = "") {
97 headers.push(`Content-Length: ${body.length}`);
99 const s = headers.join("\r\n") + `\r\n\r\n${body}`;
100 return writeString(output, s);
104 * Check if the provided URI's host is an IP address.
106 * @param {nsIURI} uri
110 function isIPAddress(uri) {
112 // getBaseDomain throws an explicit error if the uri host is an IP address.
113 Services.eTLD.getBaseDomain(uri);
115 return e.result == Cr.NS_ERROR_HOST_IS_IP_ADDRESS;
120 function isHostValid(hostHeader) {
122 // Might throw both when calling newURI or when accessing the host/port.
123 const hostUri = Services.io.newURI(`https://${hostHeader}`);
124 const { host, port } = hostUri;
125 const isHostnameValid =
126 isIPAddress(hostUri) || lazy.RemoteAgent.allowHosts.includes(host);
127 // For nsIURI a port value of -1 corresponds to the protocol's default port.
128 const isPortValid = [-1, lazy.RemoteAgent.port].includes(port);
129 return isHostnameValid && isPortValid;
135 function isOriginValid(originHeader) {
136 if (originHeader === undefined) {
137 // Always accept no origin header.
141 // Special case "null" origins, used for privacy sensitive or opaque origins.
142 if (originHeader === "null") {
143 return lazy.allowedOrigins.includes("null");
147 // Extract the host, port and scheme from the provided origin header.
148 const { host, port, scheme } = Services.io.newURI(originHeader);
149 // Check if any allowed origin matches the provided host, port and scheme.
150 return lazy.allowedOriginURIs.some(
151 uri => uri.host === host && uri.port === port && uri.scheme === scheme
154 // Reject invalid origin headers
160 * Process the WebSocket handshake headers and return the key to be sent in
161 * Sec-WebSocket-Accept response header.
163 function processRequest({ requestLine, headers }) {
164 if (!isOriginValid(headers.get("origin"))) {
166 `Incorrect Origin header, allowed origins: [${lazy.allowedOrigins}]`
169 `The handshake request has incorrect Origin header ${headers.get(
175 if (!isHostValid(headers.get("host"))) {
177 `Incorrect Host header, allowed hosts: [${lazy.RemoteAgent.allowHosts}]`
180 `The handshake request has incorrect Host header ${headers.get("host")}`
184 const method = requestLine.split(" ")[0];
185 if (method !== "GET") {
186 throw new Error("The handshake request must use GET method");
189 const upgrade = headers.get("upgrade");
190 if (!upgrade || upgrade.toLowerCase() !== "websocket") {
192 `The handshake request has incorrect Upgrade header: ${upgrade}`
196 const connection = headers.get("connection");
201 .map(t => t.trim().toLowerCase())
204 throw new Error("The handshake request has incorrect Connection header");
207 const version = headers.get("sec-websocket-version");
208 if (!version || version !== "13") {
210 "The handshake request must have Sec-WebSocket-Version: 13"
214 // Compute the accept key
215 const key = headers.get("sec-websocket-key");
218 "The handshake request must have a Sec-WebSocket-Key header"
222 return { acceptKey: computeKey(key) };
225 function computeKey(key) {
226 const str = `${key}258EAFA5-E914-47DA-95CA-C5AB0DC85B11`;
227 const data = Array.from(str, ch => ch.charCodeAt(0));
228 const hash = new lazy.CryptoHash("sha1");
229 hash.update(data, data.length);
230 return hash.finish(true);
234 * Perform the server part of a WebSocket opening handshake
235 * on an incoming connection.
237 async function serverHandshake(request, output) {
239 // Check and extract info from the request
240 const { acceptKey } = processRequest(request);
242 // Send response headers
243 await writeHttpResponse(output, [
244 "HTTP/1.1 101 Switching Protocols",
246 "Upgrade: websocket",
247 "Connection: Upgrade",
248 `Sec-WebSocket-Accept: ${acceptKey}`,
251 // Send error response in case of error
252 await writeHttpResponse(
255 "HTTP/1.1 400 Bad Request",
257 "Content-Type: text/plain",
266 async function createWebSocket(transport, input, output) {
267 const transportProvider = {
268 setListener(upgradeListener) {
269 // onTransportAvailable callback shouldn't be called synchronously
270 lazy.executeSoon(() => {
271 upgradeListener.onTransportAvailable(transport, input, output);
276 return new Promise((resolve, reject) => {
277 const socket = WebSocket.createServerWebSocket(
283 socket.addEventListener("close", () => {
288 socket.onopen = () => resolve(socket);
289 socket.onerror = err => reject(err);
293 /** Upgrade an existing HTTP request from httpd.js to WebSocket. */
294 async function upgrade(request, response) {
295 // handle response manually, allowing us to send arbitrary data
296 response._powerSeized = true;
298 const { transport, input, output } = response._connection;
301 `Perform WebSocket upgrade for incoming connection from ${transport.host}:${transport.port}`
304 const headers = new Map();
305 for (let [key, values] of Object.entries(request._headers._headers)) {
306 headers.set(key, values.join("\n"));
308 const convertedRequest = {
309 requestLine: `${request.method} ${request.path}`,
312 await serverHandshake(convertedRequest, output);
314 return createWebSocket(transport, input, output);
317 export const WebSocketHandshake = { upgrade };