Something went wrong. Try again.
🦦 a tiny cute lua web framework
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195const 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, });}