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 const CC = Components.Constructor;
11 ChromeUtils.defineESModuleGetters(lazy, {
12 executeSoon: "chrome://remote/content/shared/Sync.sys.mjs",
13 Log: "chrome://remote/content/shared/Log.sys.mjs",
14 RemoteAgent: "chrome://remote/content/components/RemoteAgent.sys.mjs",
17 ChromeUtils.defineLazyGetter(lazy, "logger", () => lazy.Log.get());
19 ChromeUtils.defineLazyGetter(lazy, "CryptoHash", () => {
20 return CC("@mozilla.org/security/hash;1", "nsICryptoHash", "initWithString");
23 ChromeUtils.defineLazyGetter(lazy, "threadManager", () => {
24 return Cc["@mozilla.org/thread-manager;1"].getService();
28 * Allowed origins are exposed through 2 separate getters because while most
29 * of the values should be valid URIs, `null` is also a valid origin and cannot
30 * be converted to a URI. Call sites interested in checking for null should use
31 * `allowedOrigins`, those interested in URIs should use `allowedOriginURIs`.
33 ChromeUtils.defineLazyGetter(lazy, "allowedOrigins", () =>
34 lazy.RemoteAgent.allowOrigins !== null ? lazy.RemoteAgent.allowOrigins : []
37 ChromeUtils.defineLazyGetter(lazy, "allowedOriginURIs", () => {
38 return lazy.allowedOrigins
41 const originURI = Services.io.newURI(origin);
42 // Make sure to read host/port/scheme as those getters could throw for
47 scheme: originURI.scheme,
53 .filter(uri => uri !== null);
57 * Write a string of bytes to async output stream
58 * and return promise that resolves once all data has been written.
59 * Doesn't do any UTF-16/UTF-8 conversion.
60 * The string is treated as an array of bytes.
62 function writeString(output, data) {
63 return new Promise((resolve, reject) => {
65 if (data.length === 0) {
73 const written = output.write(data, data.length);
74 data = data.slice(written);
82 lazy.threadManager.currentThread
91 * Write HTTP response with headers (array of strings) and body
92 * to async output stream.
94 function writeHttpResponse(output, headers, body = "") {
95 headers.push(`Content-Length: ${body.length}`);
97 const s = headers.join("\r\n") + `\r\n\r\n${body}`;
98 return writeString(output, s);
102 * Check if the provided URI's host is an IP address.
104 * @param {nsIURI} uri
108 function isIPAddress(uri) {
110 // getBaseDomain throws an explicit error if the uri host is an IP address.
111 Services.eTLD.getBaseDomain(uri);
113 return e.result == Cr.NS_ERROR_HOST_IS_IP_ADDRESS;
118 function isHostValid(hostHeader) {
120 // Might throw both when calling newURI or when accessing the host/port.
121 const hostUri = Services.io.newURI(`https://${hostHeader}`);
122 const { host, port } = hostUri;
123 const isHostnameValid =
124 isIPAddress(hostUri) || lazy.RemoteAgent.allowHosts.includes(host);
125 // For nsIURI a port value of -1 corresponds to the protocol's default port.
126 const isPortValid = [-1, lazy.RemoteAgent.port].includes(port);
127 return isHostnameValid && isPortValid;
133 function isOriginValid(originHeader) {
134 if (originHeader === undefined) {
135 // Always accept no origin header.
139 // Special case "null" origins, used for privacy sensitive or opaque origins.
140 if (originHeader === "null") {
141 return lazy.allowedOrigins.includes("null");
145 // Extract the host, port and scheme from the provided origin header.
146 const { host, port, scheme } = Services.io.newURI(originHeader);
147 // Check if any allowed origin matches the provided host, port and scheme.
148 return lazy.allowedOriginURIs.some(
149 uri => uri.host === host && uri.port === port && uri.scheme === scheme
152 // Reject invalid origin headers
158 * Process the WebSocket handshake headers and return the key to be sent in
159 * Sec-WebSocket-Accept response header.
161 function processRequest({ requestLine, headers }) {
162 if (!isOriginValid(headers.get("origin"))) {
164 `Incorrect Origin header, allowed origins: [${lazy.allowedOrigins}]`
167 `The handshake request has incorrect Origin header ${headers.get(
173 if (!isHostValid(headers.get("host"))) {
175 `Incorrect Host header, allowed hosts: [${lazy.RemoteAgent.allowHosts}]`
178 `The handshake request has incorrect Host header ${headers.get("host")}`
182 const method = requestLine.split(" ")[0];
183 if (method !== "GET") {
184 throw new Error("The handshake request must use GET method");
187 const upgrade = headers.get("upgrade");
188 if (!upgrade || upgrade.toLowerCase() !== "websocket") {
190 `The handshake request has incorrect Upgrade header: ${upgrade}`
194 const connection = headers.get("connection");
199 .map(t => t.trim().toLowerCase())
202 throw new Error("The handshake request has incorrect Connection header");
205 const version = headers.get("sec-websocket-version");
206 if (!version || version !== "13") {
208 "The handshake request must have Sec-WebSocket-Version: 13"
212 // Compute the accept key
213 const key = headers.get("sec-websocket-key");
216 "The handshake request must have a Sec-WebSocket-Key header"
220 return { acceptKey: computeKey(key) };
223 function computeKey(key) {
224 const str = `${key}258EAFA5-E914-47DA-95CA-C5AB0DC85B11`;
225 const data = Array.from(str, ch => ch.charCodeAt(0));
226 const hash = new lazy.CryptoHash("sha1");
227 hash.update(data, data.length);
228 return hash.finish(true);
232 * Perform the server part of a WebSocket opening handshake
233 * on an incoming connection.
235 async function serverHandshake(request, output) {
237 // Check and extract info from the request
238 const { acceptKey } = processRequest(request);
240 // Send response headers
241 await writeHttpResponse(output, [
242 "HTTP/1.1 101 Switching Protocols",
244 "Upgrade: websocket",
245 "Connection: Upgrade",
246 `Sec-WebSocket-Accept: ${acceptKey}`,
249 // Send error response in case of error
250 await writeHttpResponse(
253 "HTTP/1.1 400 Bad Request",
255 "Content-Type: text/plain",
264 async function createWebSocket(transport, input, output) {
265 const transportProvider = {
266 setListener(upgradeListener) {
267 // onTransportAvailable callback shouldn't be called synchronously
268 lazy.executeSoon(() => {
269 upgradeListener.onTransportAvailable(transport, input, output);
274 return new Promise((resolve, reject) => {
275 const socket = WebSocket.createServerWebSocket(
281 socket.addEventListener("close", () => {
286 socket.onopen = () => resolve(socket);
287 socket.onerror = err => reject(err);
291 /** Upgrade an existing HTTP request from httpd.js to WebSocket. */
292 async function upgrade(request, response) {
293 // handle response manually, allowing us to send arbitrary data
294 response._powerSeized = true;
296 const { transport, input, output } = response._connection;
299 `Perform WebSocket upgrade for incoming connection from ${transport.host}:${transport.port}`
302 const headers = new Map();
303 for (let [key, values] of Object.entries(request._headers._headers)) {
304 headers.set(key, values.join("\n"));
306 const convertedRequest = {
307 requestLine: `${request.method} ${request.path}`,
310 await serverHandshake(convertedRequest, output);
312 return createWebSocket(transport, input, output);
315 export const WebSocketHandshake = { upgrade };