const std = @import("std"); const Io = std.Io; const http = std.http; const Request = http.Server.Request; const fern = @import("fern"); const helpers = @import("helpers.zig"); const maivi = @import("root.zig"); const Lua = maivi.lua.Lua; pub fn task(io: Io, stream: Io.net.Stream) error{Canceled}!void { defer stream.close(io); var arena: std.heap.ArenaAllocator = .init(maivi.allocator); defer arena.deinit(); const allocator = arena.allocator(); var recv_buffer: [0x1000]u8 = undefined; var send_buffer: [0x1000]u8 = undefined; var connection_reader = stream.reader(io, &recv_buffer); var connection_writer = stream.writer(io, &send_buffer); var server: std.http.Server = .init(&connection_reader.interface, &connection_writer.interface); var request = server.receiveHead() catch return; const ret = blk: { const path = helpers.sanitize(allocator, request.head.target) catch |err| switch (err) { error.TrailingSlash => return { _ = helpers.redirect( allocator, &request, request.head.target[0 .. request.head.target.len - 1], .temporary_redirect, ) catch |e| break :blk e; }, else => break :blk err, }; var conn: Connection = .{ .request = &request, .io = io, .handle = stream.socket.handle, .path = path, .arena = allocator, .lua = maivi.lua.thread(), }; const timer = Io.Timestamp.now(io, .cpu_thread); defer { const elapsed = Io.Timestamp.durationTo(timer, Io.Timestamp.now(io, .cpu_thread)).toMilliseconds(); fern.debug().ctx("handler.dispatch") .msg("response") .str("path", conn.path) .int("elapsed", elapsed).log(); } dispatch(&conn) catch |e| break :blk e; }; ret catch |e| { fern.err().ctx("handler.task").err(e).log(); return error.Canceled; }; } pub const Connection = struct { request: *Request, io: Io, handle: Io.net.Socket.Handle, path: []const u8, arena: std.mem.Allocator, lua: *Lua, fallen: bool = false, ref: ?i32 = null, // reusable lua ref for hooks pub fn toLua(self: @This(), l: *Lua) !void { // { path = ..., req = ... } l.createTable(0, 2); { // path _ = l.pushString(self.path); l.setField(-2, "path"); } { // request ============================================================ // { headers = ..., head = ... } l.createTable(0, 2); { // headers l.newTable(); var it = self.request.iterateHeaders(); while (it.next()) |header| { _ = l.pushString(header.name); _ = l.pushString(header.value); l.setTable(-3); // header } l.setField(-2, "headers"); } { // head try l.pushAny(self.request.head); l.setField(-2, "head"); } l.setField(-2, "request"); } // metatable try maivi.lua.setMethodTable(l, MT); } const MT = struct { pub fn redirect(l: *Lua) !i32 { l.argCheck(l.isTable(1), 1, "ctx"); l.argCheck(l.isString(2), 2, "string"); l.argCheck(l.isNil(3) or l.isNumber(3), 3, "optional number"); // -1 => code, -2 => uri, -3 => ctx const conn = blk: { const ltype_ptr = l.getField(1, "ptr"); defer l.pop(1); if (ltype_ptr != .light_userdata) return error.InvalidPtr; break :blk try l.toUserdata(Connection, -1); }; const uri = try l.toString(2); const code: ?http.Status = if (l.getTop() == 3 and !l.isNil(3)) @enumFromInt(try l.toNumeric(u10, 3)) else null; _ = try helpers.redirect(conn.arena, conn.request, uri, code); conn.fallen = true; return 0; } }; }; pub fn dispatch(conn: *Connection) !void { const request = conn.request; var l = fern.debug().ctx("handler.dispatch") .msg("request") .fmt("method", "{}", .{request.head.method}) .str("path", conn.path); var it = request.iterateHeaders(); while (it.next()) |header| l = l.str(header.name, header.value); l.log(); maivi.handle(conn) catch |e| switch (e) { error.FileNotFound => try returnNotFound(conn), else => try returnInternalErr(request, e), }; } const NOTFOUND_MSG = "path not found :("; const INTERNALERR_MSG = "internal server error x_x"; fn returnNotFound(conn: *Connection) !void { withNotFound(conn, "/404.html") catch |err| switch (err) { error.IsDirectory => return withNotFound(conn, "/404/index.html") catch |e| switch (e) { error.FileNotFound => return try respondNotFound(conn), else => return try returnInternalErr(conn.request, err), }, error.FileNotFound => return try respondNotFound(conn), else => return try returnInternalErr(conn.request, err), }; } fn withNotFound(conn: *Connection, try_path: []const u8) !void { const dir = try Io.Dir.openDirAbsolute(conn.io, maivi.config.buildroot, .{}); defer dir.close(conn.io); try helpers.sendfile(conn, dir, try_path); } inline fn respondNotFound(conn: *Connection) !void { try conn.request.respond(NOTFOUND_MSG, .{ .status = .not_found, }); } fn returnInternalErr(request: *Request, err: anyerror) !void { fern.err().err(err).log(); const status = helpers.statusFromErr(err) orelse return err; try request.respond(INTERNALERR_MSG, .{ .status = status, }); }