const std = @import("std"); const httpz = @import("httpz"); const helper = @import("helper.zig"); const Makko = @import("Makko.zig"); const QR = @import("qrcode").QR; const Log = @import("Log.zig"); const getLanIP = @import("getLanIP.zig").getLanIP; const Handle = struct { changed: std.atomic.Value(i16), log: Log, directory: std.fs.Dir, port: u16, pub fn update(self: *Handle) void { self.log.header("Reloading"); _ = self.changed.fetchAdd(1, .monotonic); } pub fn dispatch( self: *Handle, action: httpz.Action(*Handle), req: *httpz.Request, res: *httpz.Response, ) !void { if (std.mem.startsWith(u8, req.url.path, "/.makko")) return action(self, req, res); const args = .{ @tagName(req.method), req.url.path }; if (self.log.colors) self.log.raw("[{s}] \x1b[2m{s}\x1b[0m\n", args) else self.log.raw("[{s}] {s}\n", args); action(self, req, res) catch |err| { const writer = res.writer(); try writer.print( @embedFile("gui/error.html"), .{ 500, req.url.path, err }, ); const query = try req.query(); const enable_banner = query.get("makko-disable-banner") == null; if (enable_banner) try writer.writeAll(@embedFile("gui/banner.html")); res.content_type = .HTML; res.status = 500; }; } }; pub const Server = httpz.Server(*Handle); pub fn init( makko: *Makko, public: bool, port: u16, ) !Server { const address = if (public) "0.0.0.0" else "127.0.0.1"; const printable = if (public) blk: { var buffer: [32]u8 = undefined; if (getLanIP(&buffer)) |lan_ip| { try showQR(makko.allocator, lan_ip, port); break :blk lan_ip; } makko.log.err("Could not fetch local IP! No QR, sorry.", .{}); break :blk "0.0.0.0"; } else "127.0.0.1"; var handler: Handle = .{ .changed = .{ .raw = 0 }, .directory = makko.paths.output, .log = makko.log, .port = port, }; var server = try Server.init( makko.allocator, .{ .port = port, .address = address, }, &handler, ); var router = try server.router(.{}); router.get("/*", static, .{}); router.get("/", redirect, .{}); router.get("/.makko/state", state, .{}); router.get("/.makko/", makko_ui, .{}); handler.log.info("Serving at http://{s}:{}/\n", .{ printable, port }); return server; } // For rendering our QR code fn showQR(allocator: std.mem.Allocator, address: []const u8, port: u16) !void { const whole = try std.fmt.allocPrint( allocator, "http://{s}:{}", .{ address, port }, ); defer allocator.free(whole); var qr = try QR.fromText(allocator, 2, .low, whole); defer qr.deinit(); const stdout = std.io.getStdOut(); const writer = stdout.writer(); try writer.writeByte(' '); // small left margin // We iterate through each pixel of the QR code and // we render it using ASCII codes. var y: usize = 0; while (y < qr.size) : (y += 2) { try writer.writeAll("\n "); for (0..qr.size) |x| { const top = qr.getModule(@intCast(x), @intCast(y)); const bottom = qr.getModule(@intCast(x), @intCast(y + 1)); const char = if (top and bottom) "█" else if (top and !bottom) "▀" else if (!top and bottom) "▄" else " "; try writer.writeAll(char); } } try writer.writeAll("\n\n"); } // in case we want to access either /directory/ or just /, we redirect to // /directory/index.html and /index.html fn redirect(_: *Handle, req: *httpz.Request, res: *httpz.Response) !void { const query = try req.query(); const enable_banner = query.get("makko-disable-banner") == null; const alloc = try std.mem.concat(res.arena, u8, &.{ req.url.path, "index.html", if (!enable_banner) "?makko-disable-banner=1" else "", }); res.headers.add("Location", alloc); res.status = 308; return; } fn makko_ui(handle: *Handle, req: *httpz.Request, res: *httpz.Response) !void { const addr = req.conn.address.in.sa.addr; // checks if NOT accessing from eiher 127.0.0.1 or 0.0.0.0 const is_local = addr == 0x0100007f or addr == 0; if (!is_local) { const new_addr = try std.fmt.allocPrint(res.arena, "http://localhost:{}/.makko", .{handle.port}); res.headers.add("Location", new_addr); res.status = 308; return; } const writer = res.writer(); try writer.writeAll(@embedFile("gui/shell.html")); res.content_type = .HTML; res.status = 200; } fn state(handle: *Handle, _: *httpz.Request, res: *httpz.Response) !void { try res.writer().print("{}", .{handle.changed.raw}); res.status = 200; res.content_type = .TEXT; } fn static(handle: *Handle, req: *httpz.Request, res: *httpz.Response) !void { const query = try req.query(); const enable_banner = query.get("makko-disable-banner") == null; const path = req.url.path[1..]; // If path is a directory, attempt to redirect to $(path)/index.html if (path[path.len - 1] == '/') return try redirect(handle, req, res); const output = handle.directory; if (!helper.exists(output, path)) { const writer = res.writer(); try writer.print( @embedFile("gui/error.html"), .{ 404, path, error.FileNotFound }, ); if (enable_banner) try writer.writeAll(@embedFile("gui/banner.html")); res.content_type = .HTML; res.status = 404; return; } // open the file! const file = try output.openFile(path, .{}); defer file.close(); res.content_type = httpz.ContentType.forFile(path); const writer = res.writer(); const size = try file.getEndPos(); // we read the file up and write as we read var file_buf: [4096]u8 = undefined; var remaining = size; while (remaining > 0) { const len = try file.read(&file_buf); if (len == 0) break; remaining -= len; try writer.writeAll(file_buf[0..len]); } // we inject the banner if we're dealing with HTML if (enable_banner and res.content_type.? == .HTML) try writer.writeAll(@embedFile("gui/banner.html")); try res.write(); res.status = 200; }