const std = @import("std"); const memora = @import("memora"); const Context = memora.Context; const Storage = memora.Storage; const config = @import("config"); const Access = @import("access.zig").Access; const log = std.log.scoped(.handler_info); const Self = @This(); const Handler = *const fn (*Context) anyerror!memora.Stream; get: ?Handler, head: ?Handler, post: ?Handler, put: ?Handler, delete: ?Handler, connect: ?Handler, options: ?Handler, trace: ?Handler, patch: ?Handler, access: Access, inline fn handler_from_method(self: *const Self, method: std.http.Method) ?Handler { return switch (method) { .GET => self.get, .HEAD => self.head, .POST => self.post, .PUT => self.put, .DELETE => self.delete, .CONNECT => self.connect, .OPTIONS => self.options, .TRACE => self.trace, .PATCH => self.patch, }; } inline fn get_fingerprint(cookie: []const u8) []const u8 { const start = std.mem.indexOf(u8, cookie, "fingerprint=") orelse return ""; const end = std.mem.indexOf(u8, cookie[start + 12..], " ") orelse return cookie[start + 12..]; return cookie[start..end + 1]; } pub fn handle( self: *const Self, request: *std.http.Server.Request, storage: *Storage, allocator: std.mem.Allocator, ) !void { const target = try allocator.dupe(u8, request.head.target); defer allocator.free(target); const handler = self.handler_from_method(request.head.method) orelse return request.respond( "{ \"error\": \"Bad Request\" }", .{ .status = .bad_request } ); const cookie = cookie: { var iterator = request.iterateHeaders(); while (iterator.next()) |header| { if (std.ascii.eqlIgnoreCase(header.name, "cookie")) { break :cookie header.value; } } break :cookie ""; }; var arena = std.heap.ArenaAllocator.init(allocator); defer arena.deinit(); var context: Context = .{ .request = request, .storage = storage, .allocator = arena.allocator(), .fingerprint = get_fingerprint(cookie), }; if (!comptime config.disable_auth) { const allowed = switch (self.access) { .everyone => true, .users => storage.sessions.get(storage, context.fingerprint) != null, .admins => admin: { if (storage.sessions.get(storage, context.fingerprint)) |session| { break :admin session.info.is_admin; } break :admin false; }, }; if (!allowed) { return request.respond( "{ \"error\": \"Forbidden\" }", .{ .status = .forbidden } ); } } var stream = handler(&context) catch |err| { const response, const status_code: std.http.Status = switch (err) { error.BadRequest => .{ "{ \"error\": \"Bad Request\" }", .bad_request }, error.Unauthorized => .{ "{ \"error\": \"Unauthorized\" }", .unauthorized }, error.Forbidden => .{ "{ \"error\": \"Forbidden\" }", .forbidden }, error.NotFound => .{ "{ \"error\": \"Not Found\" }", .not_found }, else => blk: { log.err("handler for '{s}' returned {}", .{target, err}); break :blk .{ "{ \"error\": \"Internal Server Error\" }", .internal_server_error }; }, }; return request.respond(response, .{ .status = status_code }); }; defer stream.close(); var headers: std.ArrayList(std.http.Header) = .empty; defer headers.deinit(allocator); try headers.append(allocator, .{ .name = "Content-Type", .value = context.response.headers.content_type }); try headers.append(allocator, .{ .name = "Service-Worker-Allowed", .value = "/", }); if (context.response.headers.fingerprint) |auth_token| { var value = std.Io.Writer.Allocating.init(arena.allocator()); try value.writer.print("fingerprint={s}; Secure; Path=/", .{auth_token}); try headers.append(allocator, .{ .name = "Set-Cookie", .value = value.written(), }); } var read_buffer: [1024]u8 = undefined; var reader = stream.reader(&read_buffer); var write_buffer: [1024]u8 = undefined; var body_writer = try request.respondStreaming(&write_buffer, .{ .respond_options = .{ .extra_headers = headers.items, .transfer_encoding = .chunked, }, }); _ = try reader.streamRemaining(&body_writer.writer); try body_writer.end(); } fn HandlerWrapper(T: type, name: []const u8) type { const info = @typeInfo(@TypeOf(@field(T, name))); const return_type = info.@"fn".return_type orelse void; const payload_type = @typeInfo(return_type).error_union.payload; return struct { pub fn call(ctx: *Context) anyerror!memora.Stream { const args = args: { const tuple = std.meta.fields(std.meta.ArgsTuple(@TypeOf(@field(T, name)))); if (tuple.len == 1) { break :args .{ ctx }; } else if (tuple.len == 2) { const Body = tuple[1].@"type"; var writer = std.Io.Writer.Allocating.init(ctx.allocator); const interface = &writer.writer; var buffer: [1024]u8 = undefined; const reader = try ctx.request.readerExpectContinue(&buffer); try reader.streamExact64(interface, ctx.request.head.content_length orelse 0); const body = std.json.parseFromSliceLeaky( Body, ctx.allocator, writer.written(), .{}, ) catch |err| { log.warn("failed to parse JSON {}", .{err}); return error.BadRequest; }; break :args .{ ctx, body }; } else { @compileError("invalid amount of arguments for request function"); } }; if (payload_type == memora.Stream) { return @call(.auto, @field(T, name), args); } else if (payload_type == void) { try @call(.auto, @field(T, name), args); return memora.Stream.from_buffer("{}"); } else { var writer = std.Io.Writer.Allocating.init(ctx.allocator); var stringify = std.json.Stringify { .writer = &writer.writer }; try stringify.write(try @call(.auto, @field(T, name), args)); return memora.Stream.from_buffer(writer.written()); } } }; } pub fn from_type(T: type) @This() { return Self { .get = if (@hasDecl(T, "get")) HandlerWrapper(T, "get").call else null, .head = if (@hasDecl(T, "head")) HandlerWrapper(T, "head").call else null, .post = if (@hasDecl(T, "post")) HandlerWrapper(T, "post").call else null, .put = if (@hasDecl(T, "put")) HandlerWrapper(T, "put").call else null, .delete = if (@hasDecl(T, "delete")) HandlerWrapper(T, "delete").call else null, .connect = if (@hasDecl(T, "connect")) HandlerWrapper(T, "connect").call else null, .options = if (@hasDecl(T, "options")) HandlerWrapper(T, "options").call else null, .trace = if (@hasDecl(T, "trace")) HandlerWrapper(T, "trace").call else null, .patch = if (@hasDecl(T, "patch")) HandlerWrapper(T, "patch").call else null, .access = T.access, }; }