import * as sha1 from './sha1.js'; "use strict"; var gHandlers = []; var gSocketHandlers = []; var gBadRequests = {}; const kRequestTimeout = 15000; const kStallTimeout = 60000; function logError(error) { print("ERROR " + error); if (error.stackTrace) { print(error.stackTrace); } } 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, tls: client.tls}; 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).catch(function() {}); }, 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").catch(function() {}); } else { client.write(data).catch(function() {}); } } else if (_chunked) { client.write("0\r\n\r\n").catch(function() {}); } _finished = true; if (!_keepAlive) { client.shutdown().catch(function() {}); } }, 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").catch(function() {}); } if (!_finished) { client.write("500 Internal Server Error\r\n\r\n" + error?.stackTrace).catch(function() {}); } 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); request.client.close(); }); } } catch (error) { response.reportError(error); request.client.close(); } } 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 = utf8Encode(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 { var high = 0; //(message.length / (1 ** 32)) & 0xffffffff; var low = message.length & 0xffffffff; packet.push((mask ? (1 << 7) : 0) | 127); packet.push((high >> 24) & 0xff); packet.push((high >> 16) & 0xff); packet.push((high >> 8) & 0xff); packet.push((high >> 0) & 0xff); packet.push((low >> 24) & 0xff); packet.push((low >> 16) & 0xff); packet.push((low >> 8) & 0xff); packet.push(low & 0xff); } var array = new Uint8Array(packet.length + message.length); array.set(packet, 0); array.set(message, packet.length); try { return client.write(array); } catch (error) { client.close(); throw error; } } response.onMessage = null; let extra_headers = 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; while (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 ? utf8Decode(frame) : frame, opCode: frameOpCode, }); } frame = new Uint8Array(0); } } else { break; } } } else { response.onClose(); client.close(); } }); client.onError(function(error) { logError(client.peerName + " - - [" + new Date() + "] " + error); response.onError(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, Object.assign({}, headers, extra_headers)); } function webSocketAcceptResponse(key) { var kMagic = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; var kAlphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; var hex = 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 badRequest(client, reason) { var now = new Date(); var count = 0; var old = gBadRequests[client.peerName]; if (!old) { gBadRequests[client.peerName] = { expire: new Date(now.getTime() + 1 * 60 * 1000), count: 1, reason: reason, }; count = 1; } else { old.count++; old.reason = reason; count = old.count; } new Response({version: '1.0'}, client).reportError(reason + ': ' + count); client.close(); } function allowRequest(client) { var old = gBadRequests[client.peerName]; if (old) { var now = new Date(); if (old.expire < now) { delete gBadRequests[client.peerName]; return true; } else { return old.count < 3; } } else { return true; } } function handleConnection(client) { if (!allowRequest(client)) { print('Rejecting client for too many bad requests: ', client.peerName, gBadRequests[client.peerName].reason); client.info = 'rejected'; client.close(); return; } client.info = 'accepted'; var inputBuffer = new Uint8Array(0); var request; var headers = {}; var lineByLine = true; var bodyToRead = -1; var body; var requestCount = -1; var readCount = 0; var isWebsocket = false; function resetTimeout(requestIndex) { if (isWebsocket) { return; } if (bodyToRead == -1) { setTimeout(function() { if (requestCount == requestIndex) { client.info = 'timed out'; if (requestCount == 0) { badRequest(client, 'Timed out waiting for request.'); } else { client.close(); } } }, kRequestTimeout); } else { var lastReadCount = readCount; setTimeout(function() { if (readCount == lastReadCount) { client.info = 'stalled'; if (requestCount == 0) { badRequest(client, 'Request stalled.'); } else { client.close(); } } }, kStallTimeout); } } resetTimeout(++requestCount); function reset() { inputBuffer = new Uint8Array(0); request = undefined; headers = {}; lineByLine = true; bodyToRead = -1; body = undefined; client.info = 'reset'; resetTimeout(++requestCount); } function finish() { client.info = 'finishing'; 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); client.close(); } } function handleLine(line, length) { if (bodyToRead == -1) { line = utf8Decode(line); if (!request) { if (!line) { badRequest(client, 'Empty request.'); return false; } request = line.split(' '); if (request.length != 3 || !request[2].startsWith('HTTP/1.')) { badRequest(client, 'Bad request.'); request = null; return false; } 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; if (bodyToRead > 16 * 1024 * 1024) { badRequest(client, 'Request too large: ' + bodyToRead + '.'); return false; } body = new Uint8Array(bodyToRead); client.info = 'waiting for body'; resetTimeout(requestCount); return true; } else if (headers["connection"] && headers["connection"].toLowerCase().split(",").map(x => x.trim()).indexOf("upgrade") != -1 && headers["upgrade"] && headers["upgrade"].toLowerCase() == "websocket") { isWebsocket = true; client.info = 'websocket'; var requestObject = new Request(request[0], request[1], request[2], headers, body, client); var response = new Response(requestObject, client); handleWebSocketRequest(requestObject, response, client); /* Prevent the timeout from disconnecting us. */ requestCount++; return false; } else { finish(); return false; } } } else { var offset = body.length - bodyToRead; if (line.length > body.length - offset) { line = line.slice(0, body.length - offset); } body.set(line, offset); bodyToRead -= line.length; if (bodyToRead <= 0) { finish(); } } } client.noDelay = true; client.onError(function(error) { logError(client.peerName + " - - [" + new Date() + "] " + error); }); client.read(function(data) { readCount++; if (data) { if (bodyToRead != -1 && !isWebsocket) { resetTimeout(requestCount); } const kMaxLineLength = 4096; 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 > kMaxLineLength || end == -1 && inputBuffer.length > kMaxLineLength) { badRequest(client, 'Request too long.'); return; } 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); } } } else { client.info = 'EOF'; client.close(); } }); } var kBacklog = 8; var kHost = "0.0.0.0" var socket = new Socket(); socket.bind(kHost, tildefriends.http_port).then(function() { var listenResult = socket.listen(kBacklog, function() { socket.accept().then(handleConnection).catch(function(error) { logError("[" + new Date() + "] accept error " + error); }); }); }).catch(function(error) { logError("[" + new Date() + "] bind error " + error); }); if (tildefriends.https_port) { var tls = {}; var secureSocket = new Socket(); secureSocket.bind(kHost, tildefriends.https_port).then(function() { return secureSocket.listen(kBacklog, async function() { try { var client = await secureSocket.accept(); client.tls = true; const kCertificatePath = "data/httpd/certificate.pem"; const kPrivateKeyPath = "data/httpd/privatekey.pem"; var stat = await Promise.all([ await File.stat(kCertificatePath), await File.stat(kPrivateKeyPath), ]); if (!tls.context || tls.certStat.mtime != stat[0].mtime || tls.certStat.size != stat[0].size || tls.keyStat.mtime != stat[1].mtime || tls.keyStat.size != stat[1].size) { print("Reloading " + kCertificatePath + " and " + kPrivateKeyPath); var privateKey = utf8Decode(await File.readFile(kPrivateKeyPath)); var certificate = utf8Decode(await File.readFile(kCertificatePath)); tls.context = new TlsContext(); tls.context.setPrivateKey(privateKey); tls.context.setCertificate(certificate); tls.certStat = stat[0]; tls.keyStat = stat[1]; } let result = client.startTls(tls.context); handleConnection(client); return result; } catch (error) { logError("[" + new Date() + "] [" + client.peerName + "] " + error); } }); }).catch(function(error) { logError("[" + new Date() + "] bind error " + error); }); } export { all, registerSocketHandler };