Something went wrong. Try again.
🦦 a tiny cute lua web framework
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378const std = @import("std");const Io = std.Io;const http = std.http;const Request = http.Server.Request;const Status = http.Status;
const fern = @import("fern");
const handler = @import("handler.zig");const Connection = handler.Connection;const maivi = @import("root.zig");
pub fn requestHeader(request: Request, name: []const u8) ?http.Header { var it = request.iterateHeaders(); while (it.next()) |header| if (std.mem.eql(u8, header.name, name)) return header; return null;}
const SERVER_HEADER: http.Header = .{ .name = "server", .value = "maivi",};
pub fn respond(request: *Request, buf: []u8, opts: struct { content_length: ?u64 = null, content_type: ContentType = .octetstream, status: http.Status = .ok, reason: ?[]const u8 = null, headers: []const http.Header = &.{},}) !http.BodyWriter { const headers = try std.mem.concat(maivi.allocator, http.Header, &.{ &.{SERVER_HEADER}, &.{opts.content_type.header()}, opts.headers, }); defer maivi.allocator.free(headers);
return try request.respondStreaming(buf, .{ .content_length = opts.content_length, .respond_options = .{ .status = opts.status, .reason = opts.reason, .extra_headers = headers, }, });}
// u usually want to use sendfile(2) to directly stream other FDs// luckily zig std is awesome and already implements everything// b.writer.sendFileAll(file_reader: *Reader, .unlimited);// later on u probably wanna make it chunked too, see// std.http.BodyWriter.chunkedSendFile// for reference :3
pub fn sendfile(conn: *Connection, dir: std.Io.Dir, path: []const u8) !void { const request = conn.request;
const sub_path = std.mem.cutPrefix(u8, path, "/") orelse path; const file = try dir.openFile(conn.io, sub_path, .{}); defer file.close(conn.io);
const stat = try file.stat(conn.io); if (stat.kind == .directory) return error.IsDirectory; if (stat.kind != .file) return error.NonFile; const size = stat.size;
const content_type = content_type_for_file(path) orelse .txt;
var wbuf: [0x0]u8 = undefined; var b = try maivi.helpers.respond(request, &wbuf, .{ .content_length = size, .content_type = content_type, }); try b.flush();
var remaining = size; var offset: i64 = 0; while (remaining > 0) { const count: usize = @intCast(@min(remaining, 0x7ffff000)); // ~2GiB const ret = std.c.sendfile(conn.handle, file.handle, &offset, count); if (ret == -1) { const errno = std.c.errno(ret); if (errno == .AGAIN or errno == .INTR) continue; return error.SendFileFailed; } else if (ret == 0) return error.UnexpectedEndOfFile; remaining -= @intCast(ret); }
b.state = .end;}
/// NOTE: returns allocated memory that can optionally be freed by the callerpub fn redirect(arena: std.mem.Allocator, request: *Request, uri: []const u8, code: ?Status) ![]http.Header { const headers = try std.mem.concat(arena, http.Header, &.{ &.{SERVER_HEADER}, &.{.{ .name = "location", .value = uri }}, });
try request.respond(uri, .{ .status = code orelse Status.found, .extra_headers = headers, });
return headers;}
pub fn statusFromErr(err: anyerror) ?Status { return switch (err) { error.FileNotFound => Status.not_found, error.AccessDenied => Status.forbidden, else => null, };}
pub fn content_type_for_file(path: []const u8) ?ContentType { return .forExtension(std.fs.path.extension(path));}
pub const ContentType = enum { @"3g2", @"3gp", @"7z", aac, abw, apng, arc, avi, avif, azw, bin, bmp, bz, bz2, cda, csh, css, csv, doc, docx, eot, epub, gif, gz, htm, html, ico, ics, jar, jpeg, jpg, js, json, jsonld, m4a, md, midi, mid, mjs, mp3, mp4, mpeg, mpkg, octetstream, odp, ods, odt, oga, ogv, ogx, opus, otf, pdf, php, png, ppt, pptx, rar, rtf, sh, svg, tar, tiff, tif, tkt, ts, ttf, txt, vsd, wasm, wav, weba, webm, webmanifest, webp, woff, woff2, xhtml, xls, xlsx, xml, xul, zip,
pub fn header(self: ContentType) http.Header { return .{ .name = "content-type", .value = self.toHeader(), }; }
fn toHeader(self: ContentType) []const u8 { return switch (self) { .@"3g2" => "audio/3gpp2", .@"3gp" => "audio/3gpp", .@"7z" => "application/x-7z-compressed", .aac => "audio/aac", .abw => "application/x-abiword", .apng => "image/apng", .arc => "application/x-freearc", .avi => "video/x-msvideo", .avif => "image/avif", .azw => "application/vnd.amazon.ebook", .bin => "application/octet-stream", .bmp => "image/bmp", .bz => "application/x-bzip", .bz2 => "application/x-bzip2", .cda => "application/x-cdf", .csh => "application/x-csh", .css => "text/css", .csv => "text/csv", .doc => "application/msword", .docx => "application/vnd.openxmlformats-officedocument.wordprocessingml.document", .eot => "application/vnd.ms-fontobject", .epub => "application/epub+zip", .gif => "image/gif", .gz => "application/x-gzip", .htm, .html => "text/html", .ico => "image/vnd.microsoft.icon", .ics => "text/calendar", .jar => "application/java-archive", .jpeg, .jpg => "image/jpeg", .js => "text/javascript", .json => "application/json", .jsonld => "application/ld+json", .m4a => "audio/mp4", .md => "text/markdown", .mid, .midi => "audio/x-midi", .mjs => "text/javascript", .mp3 => "audio/mpeg", .mp4 => "video/mp4", .mpeg => "video/mpeg", .mpkg => "application/vnd.apple.installer+xml", .octetstream => "application/octet-stream", .odp => "application/vnd.oasis.opendocument.presentation", .ods => "application/vnd.oasis.opendocument.spreadsheet", .odt => "application/vnd.oasis.opendocument.text", .oga => "audio/ogg", .ogv => "video/ogg", .ogx => "application/ogg", .opus => "audio/ogg", .otf => "font/otf", .pdf => "application/pdf", .php => "application/x-httpd-php", .png => "image/png", .ppt => "application/vnd.ms-powerpoint", .pptx => "application/vnd.openxmlformats-officedocument.presentationml.presentation", .rar => "application/vnd.rar", .rtf => "application/rtf", .sh => "application/x-sh", .svg => "image/svg+xml", .tar => "application/x-tar", .tif, .tiff => "image/tiff", .tkt => "text/trinket", .ts => "video/mp2t", .ttf => "font/ttf", .txt => "text/plain", .vsd => "application/vnd.visio", .wasm => "text/wasm", .wav => "audio/wav", .weba => "audio/webm", .webm => "video/webm", .webmanifest => "application/manifest+json", .webp => "image/webp", .woff => "font/woff", .woff2 => "font/woff2", .xhtml => "application/xhtml+xml", .xls => "application/vnd.ms-excel", .xlsx => "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", .xml => "application/xml", .xul => "application/vnd.mozilla.xul+xml", .zip => "application/x-zip-compressed", }; }
pub fn forExtension(ext: []const u8) ?ContentType { if (ext.len == 0) return null; const temp = if (ext[0] == '.') ext[1..] else ext; if (temp.len == 0) return null; if (temp.len > 5) return null;
var normalized: [5]u8 = undefined; for (temp, 0..) |c, i| { normalized[i] = std.ascii.toLower(c); }
return ContentType.map.get(normalized[0..temp.len]); }
const map = std.StaticStringMap(@This()).initComptime(.{ .{ "css", .css }, .{ "csv", .csv }, .{ "eot", .eot }, .{ "gif", .gif }, .{ "gz", .gz }, .{ "htm", .html }, .{ "html", .html }, .{ "ico", .ico }, .{ "jpeg", .jpg }, .{ "jpg", .jpg }, .{ "js", .js }, .{ "json", .json }, .{ "otf", .otf }, .{ "pdf", .pdf }, .{ "png", .png }, .{ "svg", .svg }, .{ "tar", .tar }, .{ "ttf", .ttf }, .{ "txt", .txt }, .{ "wasm", .wasm }, .{ "webp", .webp }, .{ "woff", .woff }, .{ "woff2", .woff2 }, .{ "xml", .xml }, });};
pub fn read_file(io: Io, allocator: std.mem.Allocator, dir: std.Io.Dir, path: []const u8) ![]const u8 { fern.debug().ctx("helpers.read_file").str("path", path).log();
const sub_path = std.mem.cutPrefix(u8, path, "/") orelse path; const file = try dir.openFile(io, sub_path, .{}); defer file.close(io);
const stat = try file.stat(io); if (stat.kind == .directory) return error.IsDirectory; if (stat.kind != .file) return error.NonFile; const size = stat.size;
var buf: [0x400]u8 = undefined; var file_reader = file.reader(io, &buf); const reader = &file_reader.interface; const contents = try reader.readAlloc(allocator, size);
return contents;}
const SanitizationError = error{NoTrailingSlashes} || std.mem.Allocator.Error;pub fn sanitize(arena: std.mem.Allocator, path: []const u8) ![]u8 { const buf = try arena.dupe(u8, path); const decoded = std.Uri.percentDecodeInPlace(buf);
// redirect: no-trailing-slashes if (path.len > 1 and std.mem.endsWith(u8, path, "/")) return error.TrailingSlash;
// replace double slash (//) with single slash (/) return decoded[0..std.mem.collapseRepeatsLen(u8, decoded, '/')];}
pub fn try_path(conn: *Connection, dir: std.Io.Dir, path: ?[]const u8) !bool { sendfile(conn, dir, path orelse conn.path) catch |err| switch (err) { error.FileNotFound => return false, else => return err, }; return true;}