import * as core from './core.js'; let gHandlers = []; let gSocketHandlers = []; let gBadRequests = {}; const kRequestTimeout = 15000; const kStallTimeout = 60000; function logError(error) { print("ERROR " + error); if (error.stackTrace) { print(error.stackTrace); } } function addHandler(handler) { let added = false; for (let 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; let 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) { let matchedHandler = null; for (let name in gHandlers) { let 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) { let matchedHandler = null; for (let name in gSocketHandlers) { let 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) { let kStatusText = { 101: "Switching Protocols", 200: 'OK', 303: 'See other', 304: 'Not Modified', 400: 'Bad Request', 401: 'Unauthorized', 403: 'Forbidden', 404: 'File not found', 500: 'Internal server error', }; let _started = false; let _finished = false; let _keepAlive = false; let _chunked = false; return { writeHead: function(status) { if (_started) { throw new Error("Response.writeHead called multiple times."); } let reason; let headers; if (arguments.length == 3) { reason = arguments[1]; headers = arguments[2]; } else { reason = kStatusText[status]; headers = arguments[1]; } let lowerHeaders = {}; let requestVersion = request.version.split("/")[1].split("."); let responseVersion = (requestVersion[0] >= 1 && requestVersion[0] >= 1) ? "1.1" : "1.0"; let headerString = "HTTP/" + responseVersion + " " + status + " " + reason + "\r\n"; for (let 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) { let handler = findHandler(request); print(request.client.peerName + " - - [" + new Date() + "] " + request.method + " " + request.uri + " " + request.version + " \"" + request.headers["user-agent"] + "\""); if (handler) { try { Promise.resolve(handler.invoke(request, response)).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) { let buffer = new Uint8Array(0); let frame; let frameOpCode = 0x0; let 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); } let fin = true; let packet = [(fin ? (1 << 7) : 0) | (opCode & 0xf)]; let 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 { let high = 0; //(message.length / (1 ** 32)) & 0xffffffff; let 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); } let 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) { let newBuffer = new Uint8Array(buffer.length + data.length); newBuffer.set(buffer, 0); newBuffer.set(data, buffer.length); buffer = newBuffer; while (buffer.length >= 2) { let bits0 = buffer[0]; let bits1 = buffer[1]; if (bits1 & (1 << 7) == 0) { // Unmasked message. client.close(); } let opCode = bits0 & 0xf; let fin = bits0 & (1 << 7); let payloadLength = bits1 & 0x7f; let maskStart = 2; if (payloadLength == 126) { payloadLength = 0; for (let i = 0; i < 2; i++) { payloadLength <<= 8; payloadLength |= buffer[2 + i]; } maskStart = 4; } else if (payloadLength == 127) { payloadLength = 0; for (let i = 0; i < 8; i++) { payloadLength <<= 8; payloadLength |= buffer[2 + i]; } maskStart = 10; } let havePayload = buffer.length >= payloadLength + 2 + 4; if (havePayload) { let mask = buffer[maskStart + 0] | buffer[maskStart + 1] << 8 | buffer[maskStart + 2] << 16 | buffer[maskStart + 3] << 24; let dataStart = maskStart + 4; let payload = buffer.slice(dataStart, dataStart + payloadLength); let decoded = maskBytes(payload, mask); buffer = buffer.slice(dataStart + payloadLength); if (frame) { let newBuffer = new Uint8Array(frame.length + decoded.length); newBuffer.set(frame, 0); newBuffer.set(decoded, frame.length); frame = newBuffer; } else { frame = decoded; } if (opCode) { frameOpCode = opCode; } if (fin) { if (response.onMessage) { response.onMessage({ data: frameOpCode == 0x1 ? utf8Decode(frame) : frame, opCode: frameOpCode, }); } frame = undefined; } } 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) { let kMagic = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; let kAlphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; return base64Encode(sha1Digest(key + kMagic)); } function badRequest(client, reason) { let now = new Date(); let count = 0; let 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) { let old = gBadRequests[client.peerName]; if (old) { let 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'; let inputBuffer = new Uint8Array(0); let request; let headers = {}; let parsing_header = true; let bodyToRead = -1; let body; let requestCount = -1; let readCount = 0; let 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 { let 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() { request = undefined; headers = {}; parsing_header = true; bodyToRead = -1; body = undefined; client.info = 'reset'; resetTimeout(++requestCount); } function finish() { client.info = 'finishing'; let requestObject = new Request(request[0], request[1], request[2], headers, body, client); let response = new Response(requestObject, client); try { handleRequest(requestObject, response) if (client.isConnected) { reset(); } } catch (error) { response.reportError(error); client.close(); } } client.onError(function(error) { logError(client.peerName + " - - [" + new Date() + "] " + error); }); client.read(function(data) { readCount++; if (data) { if (bodyToRead != -1 && !isWebsocket) { resetTimeout(requestCount); } let newBuffer = new Uint8Array(inputBuffer.length + data.length); newBuffer.set(inputBuffer, 0); newBuffer.set(data, inputBuffer.length); inputBuffer = newBuffer; if (parsing_header) { let result = parseHttpRequest(inputBuffer, inputBuffer.length - data.length); if (result) { if (typeof result === 'number') { if (result == -2) { /* More. */ } else { badRequest(client, 'Bad request.'); return; } } else if (typeof result === 'object') { request = [ result.method, result.path, `HTTP/1.${result.minor_version}`, ]; headers = Object.fromEntries(Object.entries(result.headers).map(x => [x[0].toLowerCase(), x[1]])); parsing_header = false; inputBuffer = inputBuffer.slice(result.bytes_parsed); if (!client.tls && tildefriends.https_port && core.globalSettings.http_redirect && !result.path.startsWith('/.well-known/')) { let requestObject = new Request(request[0], request[1], request[2], headers, body, client); let response = new Response(requestObject, client); response.writeHead(303, {"Location": `${core.globalSettings.http_redirect}${result.path}`, "Content-Length": "0"}); response.end(); return; } if (headers["content-length"] != undefined) { bodyToRead = parseInt(headers["content-length"]); if (bodyToRead > 16 * 1024 * 1024) { badRequest(client, 'Request too large: ' + bodyToRead + '.'); return; } body = new Uint8Array(bodyToRead); client.info = 'waiting for body'; resetTimeout(requestCount); } 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'; let requestObject = new Request(request[0], request[1], request[2], headers, body, client); let response = new Response(requestObject, client); handleWebSocketRequest(requestObject, response, client); /* Prevent the timeout from disconnecting us. */ requestCount++; } else { finish(); } } } } if (!parsing_header && inputBuffer.length) { let offset = body.length - bodyToRead; let length = Math.min(inputBuffer.length, body.length - offset); if (inputBuffer.length > body.length - offset) { body.set(inputBuffer.slice(0, length), offset); inputBuffer = inputBuffer.slice(length); } else { body.set(inputBuffer, offset); inputBuffer = inputBuffer.slice(inputBuffer.length); } bodyToRead -= length; if (bodyToRead <= 0) { finish(); } } } else { client.info = 'EOF'; client.close(); } }); } let kBacklog = 8; let kHost = '::'; let socket = new Socket(); socket.bind(kHost, tildefriends.http_port).then(function(port) { print("bound to", port); print("checking", tildefriends.args.out_http_port_file); if (tildefriends.args.out_http_port_file) { print("going to write the file"); File.writeFile(tildefriends.args.out_http_port_file, port.toString() + '\n').then(function(r) { print("wrote port file", tildefriends.args.out_http_port_file, r); }).catch(function() { print("failed to write port file"); }); } let listenResult = socket.listen(kBacklog, async function() { try { let client = await socket.accept(); client.noDelay = true; handleConnection(client); } catch (error) { logError("[" + new Date() + "] accept error " + error); } }); }).catch(function(error) { logError("[" + new Date() + "] bind error " + error); }); if (tildefriends.https_port) { let tls = {}; let secureSocket = new Socket(); secureSocket.bind(kHost, tildefriends.https_port).then(function() { return secureSocket.listen(kBacklog, async function() { try { let client = await secureSocket.accept(); client.noDelay = true; client.tls = true; const kCertificatePath = "data/httpd/certificate.pem"; const kPrivateKeyPath = "data/httpd/privatekey.pem"; let 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); let privateKey = utf8Decode(await File.readFile(kPrivateKeyPath)); let 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 };