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"); pub const auth = @import("./controllers/api/auth.zig"); pub const communities = @import("./controllers/api/communities.zig"); pub const invites = @import("./controllers/api/invites.zig"); pub const users = @import("./controllers/api/users.zig"); pub const follows = @import("./controllers/api/users/follows.zig"); pub const notes = @import("./controllers/api/notes.zig"); pub const streaming = @import("./controllers/api/streaming.zig"); pub const timelines = @import("./controllers/api/timelines.zig"); const web = @import("./controllers/web.zig"); const mdw = http.middleware; const router = mdw.Router(&.{}); 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)); fn InjectApiConn(comptime ApiSource: type) type { return struct { api_source: ApiSource, fn getApiConn(self: @This(), alloc: std.mem.Allocator, req: anytype) !ApiSource.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 self.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 self.api_source.connectToken(host, token_hdr, alloc); } } else return error.InvalidCookie; } return try self.api_source.connectUnauthorized(host, alloc); } fn handle(self: @This(), req: anytype, res: anytype, ctx: anytype, next: anytype) !void { var api_conn = try self.getApiConn(ctx.allocator, req); defer api_conn.close(); return next.handle( req, res, mdw.injectContext(.{ .api_conn = &api_conn }), {}, ); } }; } pub fn EndpointRequest(comptime Endpoint: type) type { return struct { pub const Args = if (@hasDecl(Endpoint, "Args")) Endpoint.Args else void; pub const Body = if (@hasDecl(Endpoint, "Body")) Endpoint.Body else void; pub 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(Args){}; const body_middleware = if (Body == void) mdw.injectContext(.{ .body = {} }) else mdw.ParseBody(Body){}; const query_middleware = if (Query == void) mdw.injectContext(.{ .query = {} }) else mdw.ParseQueryParams(Query){}; }; } fn CallApiEndpoint(comptime Endpoint: type) type { return struct { 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, }; 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, api_source: anytype, ) 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 InjectApiConn(@TypeOf(api_source)), 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 InjectApiConn(@TypeOf(api_source)){ .api_source = api_source }, CallApiEndpoint(Endpoint){}, }); } pub fn routeRequest(api_source: anytype, req: *http.Request, res: *http.Response, alloc: std.mem.Allocator) void { // TODO: hashmaps? _ = .{ api_source, req, res, alloc }; unreachable; //var response = Response{ .headers = http.Fields.init(alloc), .res = res }; //defer response.headers.deinit(); //const found = routeRequestInternal(api_source, req, &response, alloc); //if (!found) response.status(.not_found) catch {}; } const routes = .{ auth.login, auth.verify_login, communities.create, communities.query, invites.create, users.create, notes.create, notes.get, streaming.streaming, timelines.global, timelines.local, timelines.home, follows.create, follows.delete, follows.query_followers, follows.query_following, } ++ web.routes; 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}, ); } };