const std = @import("std"); const root = @import("root"); const builtin = @import("builtin"); const http = @import("http"); const api = @import("api"); const util = @import("util"); const web_endpoints = @import("./controllers/web.zig").routes; const api_endpoints = @import("./controllers/api.zig").routes; const mdw = http.middleware; const not_found = struct { pub fn handler( _: @This(), _: anytype, res: anytype, ctx: anytype, ) !void { var headers = http.Fields.init(ctx.allocator); defer headers.deinit(); var stream = try res.open(.not_found, &headers); defer stream.close(); try stream.finish(); } }; const base_handler = mdw.SplitUri(mdw.CatchErrors(not_found, mdw.DefaultErrorHandler)); const inject_api_conn = struct { fn getApiConn(alloc: std.mem.Allocator, api_source: anytype, req: anytype) !@TypeOf(api_source.*).Conn { const host = req.headers.get("Host") orelse return error.NoHost; const auth_header = req.headers.get("Authorization"); const token = if (auth_header) |header| blk: { const prefix = "bearer "; if (header.len < prefix.len) break :blk null; if (!std.ascii.eqlIgnoreCase(prefix, header[0..prefix.len])) break :blk null; break :blk header[prefix.len..]; } else null; if (token) |t| return try api_source.connectToken(host, t, alloc); if (req.headers.getCookie("active_account") catch return error.BadRequest) |account| { if (account.len + ("token.").len <= 64) { var buf: [64]u8 = undefined; const cookie_name = std.fmt.bufPrint(&buf, "token.{s}", .{account}) catch unreachable; if (try req.headers.getCookie(cookie_name)) |token_hdr| { return try api_source.connectToken(host, token_hdr, alloc); } } else return error.InvalidCookie; } return try api_source.connectUnauthorized(host, alloc); } pub fn handle(_: @This(), req: anytype, res: anytype, ctx: anytype, next: anytype) !void { var api_conn = try getApiConn(ctx.allocator, ctx.api_source, req); defer api_conn.close(); return mdw.injectContextValue("api_conn", &api_conn).handle( req, res, ctx, next, ); } }{}; pub fn EndpointRequest(comptime Endpoint: type) type { return struct { const Args = if (@hasDecl(Endpoint, "Args")) Endpoint.Args else void; const Body = if (@hasDecl(Endpoint, "Body")) Endpoint.Body else void; const Query = if (@hasDecl(Endpoint, "Query")) Endpoint.Query else void; const body_options = .{ .allow_unknown_fields = if (@hasDecl(Endpoint, "allow_unknown_fields_in_body")) Endpoint.allow_unknown_fields_in_body else false, }; const union_tag_from_query_param = if (@hasDecl(Endpoint, "body_tag_from_query_param")) blk: { if (!std.meta.trait.is(.Union)(Body)) @compileError("body_tag_from_query_param only valid if body is a union"); break :blk @as(?[]const u8, Endpoint.body_tag_from_query_param); } else null; allocator: std.mem.Allocator, method: http.Method, uri: []const u8, headers: http.Fields, args: Args, body: Body, query: Query, mount_path: []const u8, const args_middleware = //if (Args == void) //mdw.injectContext(.{ .args = {} }) //else mdw.ParsePathArgs(Endpoint.path, Args){}; const body_middleware = if (union_tag_from_query_param) |param| ParseBodyWithQueryType(Body, param, body_options){} else mdw.ParseBody(Body, body_options){}; const query_middleware = //if (Query == void) //mdw.injectContext(.{ .query_params = {} }) //else mdw.ParseQueryParams(Query){}; }; } /// Gets a tag from the query param with the given name, then treats the request body /// as the respective union type fn ParseBodyWithQueryType(comptime Union: type, comptime query_param_name: []const u8, comptime options: anytype) type { return struct { pub fn handle(_: @This(), req: anytype, res: anytype, ctx: anytype, next: anytype) !void { const Tag = std.meta.Tag(Union); const Param = @Type(.{ .Struct = .{ .fields = &.{.{ .name = query_param_name, .field_type = Tag, .default_value = null, .is_comptime = false, .alignment = if (@sizeOf(Tag) == 0) 0 else @alignOf(Tag), }}, .decls = &.{}, .layout = .Auto, .is_tuple = false, } }); const param = try http.urlencode.parse(ctx.allocator, true, Param, ctx.query_string); var result: ?Union = null; const content_type = req.headers.get("Content-Type"); inline for (comptime std.meta.tags(Tag)) |tag| { if (@field(param, query_param_name) == tag) { std.debug.assert(result == null); const P = std.meta.TagPayload(Union, tag); std.log.debug("Deserializing to type {}", .{P}); var stream = req.body orelse return error.NoBody; result = @unionInit(Union, @tagName(tag), try mdw.parseBodyFromReader( P, options, content_type, stream.reader(), ctx.allocator, )); } } return mdw.injectContextValue("body", result.?).handle( req, res, ctx, next, ); } }; } fn CallApiEndpoint(comptime Endpoint: type) type { return struct { pub fn handle(_: @This(), req: anytype, res: anytype, ctx: anytype, _: void) !void { const request = EndpointRequest(Endpoint){ .allocator = ctx.allocator, .method = req.method, .uri = req.uri, .headers = req.headers, .args = ctx.args, .body = ctx.body, .query = ctx.query_params, .mount_path = if (@hasField(@TypeOf(ctx), "mounted_at")) ctx.mounted_at else "", }; var response = Response{ .headers = http.Fields.init(ctx.allocator), .res = res }; defer response.headers.deinit(); return Endpoint.handler(request, &response, ctx.api_conn); } }; } pub fn apiEndpoint( comptime Endpoint: type, ) return_type: { const RequestType = EndpointRequest(Endpoint); break :return_type mdw.Apply(std.meta.Tuple(&.{ mdw.Route, @TypeOf(RequestType.args_middleware), @TypeOf(RequestType.query_middleware), @TypeOf(RequestType.body_middleware), // TODO: allocation strategy @TypeOf(inject_api_conn), CallApiEndpoint(Endpoint), })); } { const RequestType = EndpointRequest(Endpoint); return mdw.apply(.{ mdw.Route{ .desc = .{ .path = Endpoint.path, .method = Endpoint.method } }, RequestType.args_middleware, RequestType.query_middleware, RequestType.body_middleware, // TODO: allocation strategy inject_api_conn, CallApiEndpoint(Endpoint){}, }); } const api_router = mdw.apply(.{ mdw.mount("/api/v0/"), mdw.router(api_endpoints), }); pub const router = mdw.apply(.{ mdw.split_uri, mdw.catchErrors(mdw.default_error_handler), mdw.router(.{api_router} ++ web_endpoints), }); pub const AllocationStrategy = enum { arena, normal, }; pub const Response = struct { const Self = @This(); headers: http.Fields, res: *http.Response, opened: bool = false, /// Write a response with no body, only a given status pub fn status(self: *Self, status_code: http.Status) !void { var stream = try self.open(status_code); defer stream.close(); try stream.finish(); } /// Write a request body as json pub fn json(self: *Self, status_code: http.Status, response_body: anytype) !void { try self.headers.put("Content-Type", "application/json"); var stream = try self.open(status_code); defer stream.close(); const writer = stream.writer(); try std.json.stringify(response_body, json_options, writer); try stream.finish(); } pub fn open(self: *Self, status_code: http.Status) !http.Response.ResponseStream { std.debug.assert(!self.opened); self.opened = true; return try self.res.open(status_code, &self.headers); } /// Prints the given error as json pub fn err(self: *Self, status_code: http.Status, message: []const u8, details: anytype) !void { return self.json(status_code, .{ .message = message, .details = details, }); } /// Signals that the HTTP connection should be hijacked without writing a /// response beforehand. pub fn hijack(self: *Self) *http.Response { std.debug.assert(!self.opened); self.opened = true; return self.res; } pub fn template(self: *Self, status_code: http.Status, srv: anytype, comptime templ: []const u8, data: anytype) !void { try self.headers.put("Content-Type", "text/html"); const user = if (srv.context.userId()) |uid| try srv.getActor(uid) else null; defer util.deepFree(srv.allocator, user); var stream = try self.open(status_code); defer stream.close(); const writer = stream.writer(); try @import("template").execute( writer, .{ ._page = templ, .@"mini-user" = @embedFile("./controllers/web/_components/mini-user.tmpl.html"), }, @embedFile("./controllers/web/_format.tmpl.html"), data, .{ .community = srv.context.community, .user = user, }, ); try stream.finish(); } }; const json_options = if (builtin.mode == .Debug) .{ .whitespace = .{ .indent = .{ .Space = 2 }, .separator = true, }, .string = .{ .String = .{} }, } else .{ .whitespace = .{ .indent = .None, .separator = false, }, .string = .{ .String = .{} }, }; pub const helpers = struct { pub fn paginate(results: anytype, res: *Response, alloc: std.mem.Allocator) !void { var link = std.ArrayList(u8).init(alloc); const link_writer = link.writer(); defer link.deinit(); try writeLink(link_writer, null, "", results.next_page, "next"); try link_writer.writeByte(','); try writeLink(link_writer, null, "", results.prev_page, "prev"); try res.headers.put("Link", link.items); try res.json(.ok, results.items); } fn writeLink( writer: anytype, community: ?api.Community, path: []const u8, params: anytype, rel: []const u8, ) !void { if (community) |c| { try std.fmt.format( writer, "<{s}://{s}/{s}?{}>; rel=\"{s}\"", .{ @tagName(c.scheme), c.host, path, http.urlencode.encodeStruct(params), rel }, ); } else { try std.fmt.format( writer, "<{s}?{}>; rel=\"{s}\"", .{ path, http.urlencode.encodeStruct(params), rel }, ); } // TODO: percent-encode } };