зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1286281 - Part 3: WebSocket server for WebSocket remote debugger transport r=jryans
MozReview-Commit-ID: DFcPTayju7m
This commit is contained in:
Родитель
e8b9017f0d
Коммит
06b68bac22
|
@ -114,6 +114,7 @@ devtools/server/*.jsm
|
|||
!devtools/server/child.js
|
||||
!devtools/server/css-logic.js
|
||||
!devtools/server/main.js
|
||||
!devtools/server/websocket-server.js
|
||||
devtools/server/actors/**
|
||||
!devtools/server/actors/inspector.js
|
||||
!devtools/server/actors/highlighters/eye-dropper.js
|
||||
|
|
|
@ -22,6 +22,7 @@
|
|||
"setTimeout": true,
|
||||
"uneval": true,
|
||||
"URL": true,
|
||||
"WebSocket": true,
|
||||
"XMLHttpRequest": true,
|
||||
"_Iterator": true,
|
||||
},
|
||||
|
|
|
@ -37,5 +37,6 @@ DevToolsModules(
|
|||
'main.js',
|
||||
'primitive.js',
|
||||
'service-worker-child.js',
|
||||
'websocket-server.js',
|
||||
'worker.js'
|
||||
)
|
||||
|
|
|
@ -0,0 +1,221 @@
|
|||
/* 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";
|
||||
|
||||
const { Cc, CC } = require("chrome");
|
||||
const Promise = require("promise");
|
||||
const { Task } = require("devtools/shared/task");
|
||||
const { executeSoon } = require("devtools/shared/DevToolsUtils");
|
||||
const { delimitedRead } = require("devtools/shared/transport/stream-utils");
|
||||
const CryptoHash = CC("@mozilla.org/security/hash;1", "nsICryptoHash", "initWithString");
|
||||
const threadManager = Cc["@mozilla.org/thread-manager;1"].getService();
|
||||
|
||||
// Limit the header size to put an upper bound on allocated memory
|
||||
const HEADER_MAX_LEN = 8000;
|
||||
|
||||
/**
|
||||
* Read a line from async input stream and return promise that resolves to the line once
|
||||
* it has been read. If the line is longer than HEADER_MAX_LEN, will throw error.
|
||||
*/
|
||||
function readLine(input) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let line = "";
|
||||
let wait = () => {
|
||||
input.asyncWait(stream => {
|
||||
try {
|
||||
let amountToRead = HEADER_MAX_LEN - line.length;
|
||||
line += delimitedRead(input, "\n", amountToRead);
|
||||
|
||||
if (line.endsWith("\n")) {
|
||||
resolve(line.trimRight());
|
||||
return;
|
||||
}
|
||||
|
||||
if (line.length >= HEADER_MAX_LEN) {
|
||||
throw new Error(
|
||||
`Failed to read HTTP header longer than ${HEADER_MAX_LEN} bytes`);
|
||||
}
|
||||
|
||||
wait();
|
||||
} catch (ex) {
|
||||
reject(ex);
|
||||
}
|
||||
}, 0, 0, threadManager.currentThread);
|
||||
};
|
||||
|
||||
wait();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) => {
|
||||
let wait = () => {
|
||||
if (data.length === 0) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
output.asyncWait(stream => {
|
||||
try {
|
||||
let written = output.write(data, data.length);
|
||||
data = data.slice(written);
|
||||
wait();
|
||||
} catch (ex) {
|
||||
reject(ex);
|
||||
}
|
||||
}, 0, 0, threadManager.currentThread);
|
||||
};
|
||||
|
||||
wait();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Read HTTP request from async input stream.
|
||||
* @return Request line (string) and Map of header names and values.
|
||||
*/
|
||||
const readHttpRequest = Task.async(function* (input) {
|
||||
let requestLine = "";
|
||||
let headers = new Map();
|
||||
|
||||
while (true) {
|
||||
let line = yield readLine(input);
|
||||
if (line.length == 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (!requestLine) {
|
||||
requestLine = line;
|
||||
} else {
|
||||
let colon = line.indexOf(":");
|
||||
if (colon == -1) {
|
||||
throw new Error(`Malformed HTTP header: ${line}`);
|
||||
}
|
||||
|
||||
let name = line.slice(0, colon).toLowerCase();
|
||||
let value = line.slice(colon + 1).trim();
|
||||
headers.set(name, value);
|
||||
}
|
||||
}
|
||||
|
||||
return { requestLine, headers };
|
||||
});
|
||||
|
||||
/**
|
||||
* Write HTTP response (array of strings) to async output stream.
|
||||
*/
|
||||
function writeHttpResponse(output, response) {
|
||||
let responseString = response.join("\r\n") + "\r\n\r\n";
|
||||
return writeString(output, responseString);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the WebSocket handshake headers and return the key to be sent in
|
||||
* Sec-WebSocket-Accept response header.
|
||||
*/
|
||||
function processRequest({ requestLine, headers }) {
|
||||
let [ method, path ] = requestLine.split(" ");
|
||||
if (method !== "GET") {
|
||||
throw new Error("The handshake request must use GET method");
|
||||
}
|
||||
|
||||
if (path !== "/") {
|
||||
throw new Error("The handshake request has unknown path");
|
||||
}
|
||||
|
||||
let upgrade = headers.get("upgrade");
|
||||
if (!upgrade || upgrade !== "websocket") {
|
||||
throw new Error("The handshake request has incorrect Upgrade header");
|
||||
}
|
||||
|
||||
let connection = headers.get("connection");
|
||||
if (!connection || !connection.split(",").map(t => t.trim()).includes("Upgrade")) {
|
||||
throw new Error("The handshake request has incorrect Connection header");
|
||||
}
|
||||
|
||||
let 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
|
||||
let 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) {
|
||||
let str = key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
|
||||
|
||||
let data = Array.from(str, ch => ch.charCodeAt(0));
|
||||
let 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.
|
||||
*/
|
||||
const serverHandshake = Task.async(function* (input, output) {
|
||||
// Read the request
|
||||
let request = yield readHttpRequest(input);
|
||||
|
||||
try {
|
||||
// Check and extract info from the request
|
||||
let { acceptKey } = processRequest(request);
|
||||
|
||||
// Send response headers
|
||||
yield 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
|
||||
yield writeHttpResponse(output, [ "HTTP/1.1 400 Bad Request" ]);
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Accept an incoming WebSocket server connection.
|
||||
* Takes an established nsISocketTransport in the parameters.
|
||||
* Performs the WebSocket handshake and waits for the WebSocket to open.
|
||||
* Returns Promise with a WebSocket ready to send and receive messages.
|
||||
*/
|
||||
const accept = Task.async(function* (transport, input, output) {
|
||||
yield serverHandshake(input, output);
|
||||
|
||||
let transportProvider = {
|
||||
setListener(upgradeListener) {
|
||||
// The onTransportAvailable callback shouldn't be called synchronously.
|
||||
executeSoon(() => {
|
||||
upgradeListener.onTransportAvailable(transport, input, output);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let socket = WebSocket.createServerWebSocket(null, [], transportProvider, "");
|
||||
socket.addEventListener("close", () => {
|
||||
input.close();
|
||||
output.close();
|
||||
});
|
||||
|
||||
socket.onopen = () => resolve(socket);
|
||||
socket.onerror = err => reject(err);
|
||||
});
|
||||
});
|
||||
|
||||
exports.accept = accept;
|
|
@ -293,3 +293,6 @@ defineLazyGetter(globals, "CSSRule", () => Ci.nsIDOMCSSRule);
|
|||
defineLazyGetter(globals, "DOMParser", () => {
|
||||
return CC("@mozilla.org/xmlextras/domparser;1", "nsIDOMParser");
|
||||
});
|
||||
defineLazyGetter(globals, "WebSocket", () => {
|
||||
return Services.appShell.hiddenDOMWindow.WebSocket;
|
||||
});
|
||||
|
|
Загрузка…
Ссылка в новой задаче