diff --git a/core/core.js b/core/core.js index 3d595a0d..13b9563a 100644 --- a/core/core.js +++ b/core/core.js @@ -2,7 +2,6 @@ import * as app from './app.js'; import * as auth from './auth.js'; import * as form from './form.js'; import * as http from './http.js'; -import * as httpd from './httpd.js'; let gProcessIndex = 0; let gProcesses = {}; @@ -952,10 +951,9 @@ function stringResponse(response, data) { } loadSettings().then(function() { - let httpd_impl = (tildefriends.args.httpdc ? httpdc : httpd); - httpd_impl.all("/login", auth.handler); - httpd_impl.registerSocketHandler("/app/socket", app.socket); - httpd_impl.all("", function(request, response) { + httpd.all("/login", auth.handler); + httpd.registerSocketHandler("/app/socket", app.socket); + httpd.all("", function(request, response) { let match; if (request.uri === "/" || request.uri === "") { try { @@ -1005,9 +1003,17 @@ loadSettings().then(function() { return response.end(data); } }); - httpd_impl.start(tildefriends.http_port); + let port = httpd.start(tildefriends.http_port); + if (tildefriends.args.out_http_port_file) { + print("Writing the port file."); + File.writeFile(tildefriends.args.out_http_port_file, port.toString() + '\n').then(function(r) { + print("Wrote the port file:", tildefriends.args.out_http_port_file, r); + }).catch(function() { + print("Failed to write the port file."); + }); + } - if (tildefriends.args.httpdc && tildefriends.https_port) { + if (tildefriends.https_port) { async function start_tls() { const kCertificatePath = "data/httpd/certificate.pem"; const kPrivateKeyPath = "data/httpd/privatekey.pem"; @@ -1016,7 +1022,7 @@ loadSettings().then(function() { let context = new TlsContext(); context.setPrivateKey(privateKey); context.setCertificate(certificate); - httpd_impl.start(tildefriends.https_port, context); + httpd.start(tildefriends.https_port, context); } start_tls(); } diff --git a/core/httpd.js b/core/httpd.js deleted file mode 100644 index ab67ac0b..00000000 --- a/core/httpd.js +++ /dev/null @@ -1,598 +0,0 @@ -import * as core from './core.js'; - -let gHandlers = []; -let gSocketHandlers = []; -let gBadRequests = {}; - -const kRequestTimeout = 5000; -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"; - headers['Server'] = `Tilde Friends/${version().number}`; - 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; - } - - if (client) { - 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); - - if (client) { - 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"; - 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 readCount = 0; - let isWebsocket = false; - - client.setActivityTimeout(kRequestTimeout); - - function reset() { - request = undefined; - headers = {}; - parsing_header = true; - bodyToRead = -1; - body = undefined; - client.info = 'reset'; - client.setActivityTimeout(kRequestTimeout); - } - - 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) { - 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(parse=${result}, length=${inputBuffer.length - data.length}).`); - return; - } - } else if (typeof result === 'object') { - client.setActivityTimeout(kStallTimeout); - 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'; - } 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. */ - client.setActivityTimeout(); - } 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 = platform() == 'haiku' ? 'localhost' : '::'; - -function start() { - 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() + "] " + error); - } - }); - }).catch(function(error) { - logError("[" + new Date() + "] bind error " + error); - }); - } -} - -export { all, start, registerSocketHandler }; diff --git a/src/http.c b/src/http.c index e79179ca..f3c828df 100644 --- a/src/http.c +++ b/src/http.c @@ -544,7 +544,7 @@ static void _http_on_connection(uv_stream_t* stream, int status) http->connections[http->connections_count++] = connection; } -void tf_http_listen(tf_http_t* http, int port, tf_tls_context_t* tls) +int tf_http_listen(tf_http_t* http, int port, tf_tls_context_t* tls) { tf_http_listener_t* listener = tf_malloc(sizeof(tf_http_listener_t)); *listener = (tf_http_listener_t) { .http = http, .tls = tls, .tcp = { .data = listener } }; @@ -569,6 +569,15 @@ void tf_http_listen(tf_http_t* http, int port, tf_tls_context_t* tls) } } + int assigned_port = 0; + if (r == 0) + { + struct sockaddr_storage name = { 0 }; + int size = (int)sizeof(name); + r = uv_tcp_getsockname(&listener->tcp, (struct sockaddr*)&name, &size); + assigned_port = ntohs(((struct sockaddr_in*)&name)->sin_port); + } + if (r == 0) { r = uv_listen((uv_stream_t*)&listener->tcp, 16, _http_on_connection); @@ -583,6 +592,7 @@ void tf_http_listen(tf_http_t* http, int port, tf_tls_context_t* tls) http->listeners = tf_realloc(http->listeners, sizeof(tf_http_listener_t*) * (http->listeners_count + 1)); http->listeners[http->listeners_count++] = listener; } + return assigned_port; } void tf_http_add_handler(tf_http_t* http, const char* pattern, tf_http_callback_t* callback, void* user_data) diff --git a/src/http.h b/src/http.h index 3ae18989..62401636 100644 --- a/src/http.h +++ b/src/http.h @@ -36,7 +36,7 @@ typedef struct _tf_http_request_t typedef void (tf_http_callback_t)(tf_http_request_t* request); tf_http_t* tf_http_create(uv_loop_t* loop); -void tf_http_listen(tf_http_t* http, int port, tf_tls_context_t* tls); +int tf_http_listen(tf_http_t* http, int port, tf_tls_context_t* tls); void tf_http_add_handler(tf_http_t* http, const char* pattern, tf_http_callback_t* callback, void* user_data); void tf_http_respond(tf_http_request_t* request, int status, const char** headers, int headers_count, const void* body, size_t content_length); size_t tf_http_get_body(const tf_http_request_t* request, const void** out_data); diff --git a/src/httpd.js.c b/src/httpd.js.c index e4985aa5..85792a63 100644 --- a/src/httpd.js.c +++ b/src/httpd.js.c @@ -293,8 +293,8 @@ static JSValue _httpd_start(JSContext* context, JSValueConst this_val, int argc, int port = 0; JS_ToInt32(context, &port, argv[0]); tf_tls_context_t* tls = tf_tls_context_get(JS_DupValue(context, argv[1])); - tf_http_listen(http, port, tls); - return JS_UNDEFINED; + int assigned_port = tf_http_listen(http, port, tls); + return JS_NewInt32(context, assigned_port); } void _httpd_finalizer(JSRuntime* runtime, JSValue value) @@ -343,6 +343,6 @@ void tf_httpd_register(JSContext* context) JS_SetPropertyStr(context, httpd, "all", JS_NewCFunction(context, _httpd_all, "all", 2)); JS_SetPropertyStr(context, httpd, "registerSocketHandler", JS_NewCFunction(context, _httpd_register_socket_handler, "register_socket_handler", 2)); JS_SetPropertyStr(context, httpd, "start", JS_NewCFunction(context, _httpd_start, "start", 2)); - JS_SetPropertyStr(context, global, "httpdc", httpd); + JS_SetPropertyStr(context, global, "httpd", httpd); JS_FreeValue(context, global); }