"use strict"; var gHandlers = []; var gSocketHandlers = []; function logError(error) { print("ERROR " + error); } function addHandler(handler) { var added = false; for (var i in gHandlers) { if (gHandlers[i].path == handler.path) { gHandlers[i] = handler; added = true; break; } } if (!added) { gHandlers.push(handler); added = true; } } function all(prefix, handler) { addHandler({ owner: this, path: prefix, invoke: handler, }); } function registerSocketHandler(prefix, handler) { gSocketHandlers.push({ owner: this, path: prefix, invoke: handler, }); } function Request(method, uri, version, headers, body, client) { this.method = method; var index = uri.indexOf("?"); if (index != -1) { this.uri = uri.slice(0, index); this.query = uri.slice(index + 1); } else { this.uri = uri; this.query = undefined; } this.version = version; this.headers = headers; this.client = {peerName: client.peerName}; this.body = body; return this; } function findHandler(request) { var matchedHandler = null; for (var name in gHandlers) { var handler = gHandlers[name]; if (request.uri == handler.path || request.uri.slice(0, handler.path.length + 1) == handler.path + '/') { matchedHandler = handler; break; } } return matchedHandler; } function findSocketHandler(request) { var matchedHandler = null; for (var name in gSocketHandlers) { var handler = gSocketHandlers[name]; if (request.uri == handler.path || request.uri.slice(0, handler.path.length + 1) == handler.path + '/') { matchedHandler = handler; break; } } return matchedHandler; } function Response(request, client) { var kStatusText = { 101: "Switching Protocols", 200: 'OK', 303: 'See other', 403: 'Forbidden', 404: 'File not found', 500: 'Internal server error', }; var _started = false; var _finished = false; var _keepAlive = false; var _chunked = false; return { writeHead: function(status) { if (_started) { throw new Error("Response.writeHead called multiple times."); } var reason; var headers; if (arguments.length == 3) { reason = arguments[1]; headers = arguments[2]; } else { reason = kStatusText[status]; headers = arguments[1]; } var lowerHeaders = {}; var requestVersion = request.version.split("/")[1].split("."); var responseVersion = (requestVersion[0] >= 1 && requestVersion[0] >= 1) ? "1.1" : "1.0"; var headerString = "HTTP/" + responseVersion + " " + status + " " + reason + "\r\n"; for (var i in headers) { headerString += i + ": " + headers[i] + "\r\n"; lowerHeaders[i.toLowerCase()] = headers[i]; } if ("connection" in lowerHeaders) { _keepAlive = lowerHeaders["connection"].toLowerCase() == "keep-alive"; } else { _keepAlive = ((request.version == "HTTP/1.0" && ("connection" in lowerHeaders && lowerHeaders["connection"].toLowerCase() == "keep-alive")) || (request.version == "HTTP/1.1" && (!("connection" in lowerHeaders) || lowerHeaders["connection"].toLowerCase() != "close"))); headerString += "Connection: " + (_keepAlive ? "keep-alive" : "close") + "\r\n"; } _chunked = _keepAlive && !("content-length" in lowerHeaders); if (_chunked) { headerString += "Transfer-Encoding: chunked\r\n"; } headerString += "\r\n"; _started = true; client.write(headerString); }, end: function(data) { if (_finished) { throw new Error("Response.end called multiple times."); } if (data) { if (_chunked) { client.write(data.length.toString(16) + "\r\n" + data + "\r\n" + "0\r\n\r\n"); } else { client.write(data); } } else if (_chunked) { client.write("0\r\n\r\n"); } _finished = true; if (!_keepAlive) { client.shutdown(); } }, reportError: function(error) { if (!_started) { client.write("HTTP/1.1 500 Internal Server Error\r\nContent-Type: text/plain; charset=utf-8\r\n\r\n"); } if (!_finished) { client.write("500 Internal Server Error\r\n\r\n" + error.stackTrace); client.shutdown(); } logError(client.peerName + " - - [" + new Date() + "] " + error); }, isConnected: function() { return client.isConnected; }, }; } function handleRequest(request, response) { var handler = findHandler(request); print(request.client.peerName + " - - [" + new Date() + "] " + request.method + " " + request.uri + " " + request.version + " \"" + request.headers["user-agent"] + "\""); if (handler) { try { var promise = handler.invoke(request, response); if (promise) { promise.catch(function(error) { response.reportError(error); }); } } catch (error) { print(error); response.reportError(error); } } else { response.writeHead(200, {"Content-Type": "text/plain; charset=utf-8"}); response.end("No handler found for request: " + request.uri); } } function handleWebSocketRequest(request, response, client) { var buffer = new Uint8Array(0); var frame = new Uint8Array(0); var frameOpCode = 0x0; var handler = findSocketHandler(request); if (!handler) { client.close(); return; } response.send = function(message, opCode) { if (opCode === undefined) { opCode = 0x2; } if (opCode == 0x1 && (typeof message == "string" || message instanceof String)) { message = new TextEncoder("UTF-8").encode(message); } var fin = true; var packet = [(fin ? (1 << 7) : 0) | (opCode & 0xf)]; var mask = false; if (message.length < 126) { packet.push((mask ? (1 << 7) : 0) | message.length); } else if (message.length < (1 << 16)) { packet.push((mask ? (1 << 7) : 0) | 126); packet.push((message.length >> 8) & 0xff); packet.push(message.length & 0xff); } else { packet.push((mask ? (1 << 7) : 0) | 127); packet.push((message.length >> 24) & 0xff); packet.push((message.length >> 16) & 0xff); packet.push((message.length >> 8) & 0xff); packet.push(message.length & 0xff); } var array = new Uint8Array(packet.length + message.length); array.set(packet, 0); array.set(message, packet.length); return client.write(array); } response.onMessage = null; handler.invoke(request, response); client.read(function(data) { if (data) { var newBuffer = new Uint8Array(buffer.length + data.length); newBuffer.set(buffer, 0); newBuffer.set(data, buffer.length); buffer = newBuffer; if (buffer.length >= 2) { var bits0 = buffer[0]; var bits1 = buffer[1]; if (bits1 & (1 << 7) == 0) { // Unmasked message. client.close(); } var opCode = bits0 & 0xf; var fin = bits0 & (1 << 7); var payloadLength = bits1 & 0x7f; var maskStart = 2; if (payloadLength == 126) { payloadLength = 0; for (var i = 0; i < 2; i++) { payloadLength <<= 8; payloadLength |= buffer[2 + i]; } maskStart = 4; } else if (payloadLength == 127) { payloadLength = 0; for (var i = 0; i < 8; i++) { payloadLength <<= 8; payloadLength |= buffer[2 + i]; } maskStart = 10; } var havePayload = buffer.length >= payloadLength + 2 + 4; if (havePayload) { var mask = buffer.slice(maskStart, maskStart + 4); var dataStart = maskStart + 4; var decoded = new Array(payloadLength); var payload = buffer.slice(dataStart, dataStart + payloadLength); buffer = buffer.slice(dataStart + payloadLength); for (var i = 0; i < payloadLength; i++) { decoded[i] = payload[i] ^ mask[i % 4]; } var newBuffer = new Uint8Array(frame.length + decoded.length); newBuffer.set(frame, 0); newBuffer.set(decoded, frame.length); frame = newBuffer; if (opCode) { frameOpCode = opCode; } if (fin) { if (response.onMessage) { response.onMessage({ data: frameOpCode == 0x1 ? new TextDecoder("UTF-8").decode(frame) : frame, opCode: frameOpCode, }); } frame = new Uint8Array(0); } } } } }); client.onError(function(error) { logError(client.peerName + " - - [" + new Date() + "] " + error); }); let headers = { "Upgrade": "websocket", "Connection": "Upgrade", "Sec-WebSocket-Accept": webSocketAcceptResponse(request.headers["sec-websocket-key"]), }; if (request.headers["sec-websocket-version"] != "13") { headers["Sec-WebSocket-Version"] = "13"; } response.writeHead(101, headers); } function webSocketAcceptResponse(key) { var kMagic = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; var kAlphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; var hex = require("sha1").hash(key + kMagic) var binary = ""; for (var i = 0; i < hex.length; i += 6) { var characters = hex.substring(i, i + 6); if (characters.length < 6) { characters += "0".repeat(6 - characters.length); } var value = parseInt(characters, 16); for (var bit = 0; bit < 8 * 3; bit += 6) { if (i * 8 / 2 + bit >= 8 * hex.length / 2) { binary += kAlphabet.charAt(64); } else { binary += kAlphabet.charAt((value >> (18 - bit)) & 63); } } } return binary; } function handleConnection(client) { var inputBuffer = new Uint8Array(0); var request; var headers = {}; var lineByLine = true; var bodyToRead = -1; var body; function reset() { inputBuffer = new Uint8Array(0); request = undefined; headers = {}; lineByLine = true; bodyToRead = -1; body = undefined; } function finish() { var requestObject = new Request(request[0], request[1], request[2], headers, body, client); var response = new Response(requestObject, client); try { handleRequest(requestObject, response) if (client.isConnected) { reset(); } } catch (error) { response.reportError(error); } } function handleLine(line, length) { if (bodyToRead == -1) { line = new TextDecoder("ASCII").decode(line); if (!request) { request = line.split(' '); return true; } else if (line) { var colon = line.indexOf(':'); var key = line.slice(0, colon).trim(); var value = line.slice(colon + 1).trim(); headers[key.toLowerCase()] = value; return true; } else { if (headers["content-length"] != undefined) { bodyToRead = parseInt(headers["content-length"]); lineByLine = false; body = ""; return true; } else if (headers["connection"] && headers["connection"].toLowerCase().split(",").map(x => x.trim()).indexOf("upgrade") != -1 && headers["upgrade"] && headers["upgrade"].toLowerCase() == "websocket") { var requestObject = new Request(request[0], request[1], request[2], headers, body, client); var response = new Response(requestObject, client); handleWebSocketRequest(requestObject, response, client); return false; } else { finish(); return false; } } } else { line = new TextDecoder("UTF-8").decode(line); body += line; bodyToRead -= length; if (bodyToRead <= 0) { finish(); } } } client.onError(function(error) { logError(client.peerName + " - - [" + new Date() + "] " + error); }); client.read(function(data) { if (data) { var newBuffer = new Uint8Array(inputBuffer.length + data.length); newBuffer.set(inputBuffer, 0); newBuffer.set(data, inputBuffer.length); inputBuffer = newBuffer; var newLine = '\n'.charCodeAt(0); var carriageReturn = '\r'.charCodeAt(0); var more = true; while (more) { if (lineByLine) { more = false; var end = inputBuffer.indexOf(newLine); var realEnd = end; if (end > 0 && inputBuffer[end - 1] == carriageReturn) { --end; } if (end != -1) { var line = inputBuffer.slice(0, end); inputBuffer = inputBuffer.slice(realEnd + 1); more = handleLine(line, realEnd + 1); } } else { more = handleLine(inputBuffer, inputBuffer.length); inputBuffer = new Uint8Array(0); } } } }); } var kBacklog = 8; var kHost = "0.0.0.0" var kHttpPort = gGlobalSettings.httpPort || 12345; var kHttpsPort = gGlobalSettings.httpsPort || 12346; var socket = new Socket(); socket.bind(kHost, kHttpPort).then(function() { var listenResult = socket.listen(kBacklog, function() { socket.accept().then(handleConnection).catch(function(error) { logError("[" + new Date() + "] " + error); }); }); }).catch(function(error) { logError("[" + new Date() + "] " + error); }); var privateKey = new TextDecoder("ASCII").decode(File.readFile("data/httpd/privatekey.pem")); var certificate = new TextDecoder("ASCII").decode(File.readFile("data/httpd/certificate.pem")); if (privateKey && certificate) { var tls = new TlsContext(); tls.setPrivateKey(privateKey); tls.setCertificate(certificate); var secureSocket = new Socket(); secureSocket.bind(kHost, kHttpsPort).then(function() { secureSocket.listen(kBacklog, function() { secureSocket.accept().then(function(client) { handleConnection(client); client.startTls(tls).catch(function(error) { logError("[" + new Date() + "] [" + client.peerName + "] " + error); }); }).catch(function(error) { logError("[" + new Date() + "] " + error); }); }); }); } exports.all = all; exports.registerSocketHandler = registerSocketHandler;