summaryrefslogtreecommitdiffstats
path: root/remote/server
diff options
context:
space:
mode:
Diffstat (limited to 'remote/server')
-rw-r--r--remote/server/README8
-rw-r--r--remote/server/WebSocketHandshake.jsm198
-rw-r--r--remote/server/WebSocketTransport.jsm88
3 files changed, 294 insertions, 0 deletions
diff --git a/remote/server/README b/remote/server/README
new file mode 100644
index 0000000000..00184130d3
--- /dev/null
+++ b/remote/server/README
@@ -0,0 +1,8 @@
+These files provide functionality for serving and responding to HTTP
+requests, and handling WebSocket connections. For this we rely on
+httpd.js and the chrome-only WebSocket.createServerWebSocket function.
+
+Generally speaking, this is all held together with a piece of string.
+It is a known problem that we do not have a high-quality HTTPD
+implementation in central, and we’d like to move away from using
+any of this code.
diff --git a/remote/server/WebSocketHandshake.jsm b/remote/server/WebSocketHandshake.jsm
new file mode 100644
index 0000000000..3037738c66
--- /dev/null
+++ b/remote/server/WebSocketHandshake.jsm
@@ -0,0 +1,198 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+var EXPORTED_SYMBOLS = ["WebSocketHandshake"];
+
+// This file is an XPCOM service-ified copy of ../devtools/server/socket/websocket-server.js.
+
+const CC = Components.Constructor;
+
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+const { XPCOMUtils } = ChromeUtils.import(
+ "resource://gre/modules/XPCOMUtils.jsm"
+);
+
+const { executeSoon } = ChromeUtils.import("chrome://remote/content/Sync.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "WebSocket", () => {
+ return Services.appShell.hiddenDOMWindow.WebSocket;
+});
+
+const CryptoHash = CC(
+ "@mozilla.org/security/hash;1",
+ "nsICryptoHash",
+ "initWithString"
+);
+const threadManager = Cc["@mozilla.org/thread-manager;1"].getService();
+
+// TODO(ato): Merge this with httpd.js so that we can respond to both HTTP/1.1
+// as well as WebSocket requests on the same server.
+
+/**
+ * Write a string of bytes to async output stream
+ * and return promise that resolves once all data has been written.
+ * Doesn't do any UTF-16/UTF-8 conversion.
+ * The string is treated as an array of bytes.
+ */
+function writeString(output, data) {
+ return new Promise((resolve, reject) => {
+ const wait = () => {
+ if (data.length === 0) {
+ resolve();
+ return;
+ }
+
+ output.asyncWait(
+ stream => {
+ try {
+ const written = output.write(data, data.length);
+ data = data.slice(written);
+ wait();
+ } catch (ex) {
+ reject(ex);
+ }
+ },
+ 0,
+ 0,
+ threadManager.currentThread
+ );
+ };
+
+ wait();
+ });
+}
+
+/** Write HTTP response (array of strings) to async output stream. */
+function writeHttpResponse(output, response) {
+ const s = response.join("\r\n") + "\r\n\r\n";
+ return writeString(output, s);
+}
+
+/**
+ * Process the WebSocket handshake headers and return the key to be sent in
+ * Sec-WebSocket-Accept response header.
+ */
+function processRequest({ requestLine, headers }) {
+ const method = requestLine.split(" ")[0];
+ if (method !== "GET") {
+ throw new Error("The handshake request must use GET method");
+ }
+
+ const upgrade = headers.get("upgrade");
+ if (!upgrade || upgrade.toLowerCase() !== "websocket") {
+ throw new Error(
+ `The handshake request has incorrect Upgrade header: ${upgrade}`
+ );
+ }
+
+ const connection = headers.get("connection");
+ if (
+ !connection ||
+ !connection
+ .split(",")
+ .map(t => t.trim())
+ .includes("Upgrade")
+ ) {
+ throw new Error("The handshake request has incorrect Connection header");
+ }
+
+ const version = headers.get("sec-websocket-version");
+ if (!version || version !== "13") {
+ throw new Error(
+ "The handshake request must have Sec-WebSocket-Version: 13"
+ );
+ }
+
+ // Compute the accept key
+ const key = headers.get("sec-websocket-key");
+ if (!key) {
+ throw new Error(
+ "The handshake request must have a Sec-WebSocket-Key header"
+ );
+ }
+
+ return { acceptKey: computeKey(key) };
+}
+
+function computeKey(key) {
+ const str = `${key}258EAFA5-E914-47DA-95CA-C5AB0DC85B11`;
+ const data = Array.from(str, ch => ch.charCodeAt(0));
+ const hash = new CryptoHash("sha1");
+ hash.update(data, data.length);
+ return hash.finish(true);
+}
+
+/**
+ * Perform the server part of a WebSocket opening handshake
+ * on an incoming connection.
+ */
+async function serverHandshake(request, output) {
+ try {
+ // Check and extract info from the request
+ const { acceptKey } = processRequest(request);
+
+ // Send response headers
+ await writeHttpResponse(output, [
+ "HTTP/1.1 101 Switching Protocols",
+ "Upgrade: websocket",
+ "Connection: Upgrade",
+ `Sec-WebSocket-Accept: ${acceptKey}`,
+ ]);
+ } catch (error) {
+ // Send error response in case of error
+ await writeHttpResponse(output, ["HTTP/1.1 400 Bad Request"]);
+ throw error;
+ }
+}
+
+async function createWebSocket(transport, input, output) {
+ const transportProvider = {
+ setListener(upgradeListener) {
+ // onTransportAvailable callback shouldn't be called synchronously
+ executeSoon(() => {
+ upgradeListener.onTransportAvailable(transport, input, output);
+ });
+ },
+ };
+
+ return new Promise((resolve, reject) => {
+ const socket = WebSocket.createServerWebSocket(
+ null,
+ [],
+ transportProvider,
+ ""
+ );
+ socket.addEventListener("close", () => {
+ input.close();
+ output.close();
+ });
+
+ socket.onopen = () => resolve(socket);
+ socket.onerror = err => reject(err);
+ });
+}
+
+/** Upgrade an existing HTTP request from httpd.js to WebSocket. */
+async function upgrade(request, response) {
+ // handle response manually, allowing us to send arbitrary data
+ response._powerSeized = true;
+
+ const { transport, input, output } = response._connection;
+
+ const headers = new Map();
+ for (let [key, values] of Object.entries(request._headers._headers)) {
+ headers.set(key, values.join("\n"));
+ }
+ const convertedRequest = {
+ requestLine: `${request.method} ${request.path}`,
+ headers,
+ };
+ await serverHandshake(convertedRequest, output);
+
+ return createWebSocket(transport, input, output);
+}
+
+const WebSocketHandshake = { upgrade };
diff --git a/remote/server/WebSocketTransport.jsm b/remote/server/WebSocketTransport.jsm
new file mode 100644
index 0000000000..611000f07b
--- /dev/null
+++ b/remote/server/WebSocketTransport.jsm
@@ -0,0 +1,88 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// This is an XPCOM service-ified copy of ../devtools/shared/transport/websocket-transport.js.
+
+"use strict";
+
+var EXPORTED_SYMBOLS = ["WebSocketTransport"];
+
+const { EventEmitter } = ChromeUtils.import(
+ "resource://gre/modules/EventEmitter.jsm"
+);
+
+function WebSocketTransport(socket) {
+ EventEmitter.decorate(this);
+
+ this.active = false;
+ this.hooks = null;
+ this.socket = socket;
+}
+
+WebSocketTransport.prototype = {
+ ready() {
+ if (this.active) {
+ return;
+ }
+
+ this.socket.addEventListener("message", this);
+ this.socket.addEventListener("close", this);
+
+ this.active = true;
+ },
+
+ send(object) {
+ this.emit("send", object);
+ if (this.socket) {
+ this.socket.send(JSON.stringify(object));
+ }
+ },
+
+ startBulkSend() {
+ throw new Error("Bulk send is not supported by WebSocket transport");
+ },
+
+ close() {
+ if (!this.socket) {
+ return;
+ }
+ this.emit("close");
+ this.active = false;
+
+ this.socket.removeEventListener("message", this);
+ this.socket.removeEventListener("close", this);
+ this.socket.close();
+ this.socket = null;
+
+ if (this.hooks) {
+ this.hooks.onClosed();
+ this.hooks = null;
+ }
+ },
+
+ handleEvent(event) {
+ switch (event.type) {
+ case "message":
+ this.onMessage(event);
+ break;
+ case "close":
+ this.close();
+ break;
+ }
+ },
+
+ onMessage({ data }) {
+ if (typeof data !== "string") {
+ throw new Error(
+ "Binary messages are not supported by WebSocket transport"
+ );
+ }
+
+ const object = JSON.parse(data);
+ this.emit("packet", object);
+ if (this.hooks) {
+ this.hooks.onPacket(object);
+ }
+ },
+};