websocket-server.js (6553B)
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/. */ 4 5 "use strict"; 6 7 const { executeSoon } = require("resource://devtools/shared/DevToolsUtils.js"); 8 const { 9 delimitedRead, 10 } = require("resource://devtools/shared/transport/stream-utils.js"); 11 const CryptoHash = Components.Constructor( 12 "@mozilla.org/security/hash;1", 13 "nsICryptoHash", 14 "initWithString" 15 ); 16 const threadManager = Cc["@mozilla.org/thread-manager;1"].getService(); 17 18 // Limit the header size to put an upper bound on allocated memory 19 const HEADER_MAX_LEN = 8000; 20 21 /** 22 * Read a line from async input stream and return promise that resolves to the line once 23 * it has been read. If the line is longer than HEADER_MAX_LEN, will throw error. 24 */ 25 function readLine(input) { 26 return new Promise((resolve, reject) => { 27 let line = ""; 28 const wait = () => { 29 input.asyncWait( 30 () => { 31 try { 32 const amountToRead = HEADER_MAX_LEN - line.length; 33 line += delimitedRead(input, "\n", amountToRead); 34 35 if (line.endsWith("\n")) { 36 resolve(line.trimRight()); 37 return; 38 } 39 40 if (line.length >= HEADER_MAX_LEN) { 41 throw new Error( 42 `Failed to read HTTP header longer than ${HEADER_MAX_LEN} bytes` 43 ); 44 } 45 46 wait(); 47 } catch (ex) { 48 reject(ex); 49 } 50 }, 51 0, 52 0, 53 threadManager.currentThread 54 ); 55 }; 56 57 wait(); 58 }); 59 } 60 61 /** 62 * Write a string of bytes to async output stream and return promise that resolves once 63 * all data has been written. Doesn't do any utf-16/utf-8 conversion - the string is 64 * treated as an array of bytes. 65 */ 66 function writeString(output, data) { 67 return new Promise((resolve, reject) => { 68 const wait = () => { 69 if (data.length === 0) { 70 resolve(); 71 return; 72 } 73 74 output.asyncWait( 75 () => { 76 try { 77 const written = output.write(data, data.length); 78 data = data.slice(written); 79 wait(); 80 } catch (ex) { 81 reject(ex); 82 } 83 }, 84 0, 85 0, 86 threadManager.currentThread 87 ); 88 }; 89 90 wait(); 91 }); 92 } 93 94 /** 95 * Read HTTP request from async input stream. 96 * 97 * @return Request line (string) and Map of header names and values. 98 */ 99 const readHttpRequest = async function (input) { 100 let requestLine = ""; 101 const headers = new Map(); 102 103 while (true) { 104 const line = await readLine(input); 105 if (!line.length) { 106 break; 107 } 108 109 if (!requestLine) { 110 requestLine = line; 111 } else { 112 const colon = line.indexOf(":"); 113 if (colon == -1) { 114 throw new Error(`Malformed HTTP header: ${line}`); 115 } 116 117 const name = line.slice(0, colon).toLowerCase(); 118 const value = line.slice(colon + 1).trim(); 119 headers.set(name, value); 120 } 121 } 122 123 return { requestLine, headers }; 124 }; 125 126 /** 127 * Write HTTP response (array of strings) to async output stream. 128 */ 129 function writeHttpResponse(output, response) { 130 const responseString = response.join("\r\n") + "\r\n\r\n"; 131 return writeString(output, responseString); 132 } 133 134 /** 135 * Process the WebSocket handshake headers and return the key to be sent in 136 * Sec-WebSocket-Accept response header. 137 */ 138 function processRequest({ requestLine, headers }) { 139 const [method, path] = requestLine.split(" "); 140 if (method !== "GET") { 141 throw new Error("The handshake request must use GET method"); 142 } 143 144 if (path !== "/") { 145 throw new Error("The handshake request has unknown path"); 146 } 147 148 const upgrade = headers.get("upgrade"); 149 if (!upgrade || upgrade !== "websocket") { 150 throw new Error("The handshake request has incorrect Upgrade header"); 151 } 152 153 const connection = headers.get("connection"); 154 if ( 155 !connection || 156 !connection 157 .split(",") 158 .map(t => t.trim()) 159 .includes("Upgrade") 160 ) { 161 throw new Error("The handshake request has incorrect Connection header"); 162 } 163 164 const version = headers.get("sec-websocket-version"); 165 if (!version || version !== "13") { 166 throw new Error( 167 "The handshake request must have Sec-WebSocket-Version: 13" 168 ); 169 } 170 171 // Compute the accept key 172 const key = headers.get("sec-websocket-key"); 173 if (!key) { 174 throw new Error( 175 "The handshake request must have a Sec-WebSocket-Key header" 176 ); 177 } 178 179 return { acceptKey: computeKey(key) }; 180 } 181 182 function computeKey(key) { 183 const str = key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; 184 185 const data = Array.from(str, ch => ch.charCodeAt(0)); 186 const hash = new CryptoHash("sha1"); 187 hash.update(data, data.length); 188 return hash.finish(true); 189 } 190 191 /** 192 * Perform the server part of a WebSocket opening handshake on an incoming connection. 193 */ 194 const serverHandshake = async function (input, output) { 195 // Read the request 196 const request = await readHttpRequest(input); 197 198 try { 199 // Check and extract info from the request 200 const { acceptKey } = processRequest(request); 201 202 // Send response headers 203 await writeHttpResponse(output, [ 204 "HTTP/1.1 101 Switching Protocols", 205 "Upgrade: websocket", 206 "Connection: Upgrade", 207 `Sec-WebSocket-Accept: ${acceptKey}`, 208 ]); 209 } catch (error) { 210 // Send error response in case of error 211 await writeHttpResponse(output, ["HTTP/1.1 400 Bad Request"]); 212 throw error; 213 } 214 }; 215 216 /** 217 * Accept an incoming WebSocket server connection. 218 * Takes an established nsISocketTransport in the parameters. 219 * Performs the WebSocket handshake and waits for the WebSocket to open. 220 * Returns Promise with a WebSocket ready to send and receive messages. 221 */ 222 const accept = async function (transport, input, output) { 223 await serverHandshake(input, output); 224 225 const transportProvider = { 226 setListener(upgradeListener) { 227 // The onTransportAvailable callback shouldn't be called synchronously. 228 executeSoon(() => { 229 upgradeListener.onTransportAvailable(transport, input, output); 230 }); 231 }, 232 }; 233 234 return new Promise((resolve, reject) => { 235 const socket = WebSocket.createServerWebSocket( 236 null, 237 [], 238 transportProvider, 239 "" 240 ); 241 socket.addEventListener("close", () => { 242 input.close(); 243 output.close(); 244 }); 245 246 socket.onopen = () => resolve(socket); 247 socket.onerror = err => reject(err); 248 }); 249 }; 250 251 exports.accept = accept;