const 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 caller pub 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; }