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 query_utils = @import("./query.zig"); const json_utils = @import("./json.zig"); 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.injectContext(.{ .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; allocator: std.mem.Allocator, method: http.Method, uri: []const u8, headers: http.Fields, args: Args, body: Body, query: Query, const args_middleware = if (Args == void) mdw.injectContext(.{ .args = {} }) else mdw.ParsePathArgs(Endpoint.path, Args){}; const body_middleware = if (Body == void) mdw.injectContext(.{ .body = {} }) else mdw.ParseBody(Body){}; const query_middleware = if (Query == void) mdw.injectContext(.{ .query_params = {} }) else mdw.ParseQueryParams(Query){}; }; } 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, }; 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), mdw.router(web_endpoints), //api_router, }); 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, comptime templ: []const u8, data: anytype) !void { try self.headers.put("Content-Type", "text/html"); var stream = try self.open(status_code); defer stream.close(); const writer = stream.writer(); try @import("template").execute(writer, templ, data); 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(community: api.Community, path: []const u8, 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, community, path, results.next_page, "next"); try link_writer.writeByte(','); try writeLink(link_writer, community, path, 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 { // TODO: percent-encode try std.fmt.format( writer, "<{s}://{s}/{s}?", .{ @tagName(community.scheme), community.host, path }, ); try query_utils.formatQuery(params, writer); try std.fmt.format( writer, ">; rel=\"{s}\"", .{rel}, ); } };