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"); pub fn routeRequest(api_source: anytype, req: *http.Request, res: *http.Response, alloc: std.mem.Allocator) void { // TODO: hashmaps? 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 {}; } fn routeRequestInternal(api_source: anytype, req: *http.Request, res: *Response, alloc: std.mem.Allocator) bool { inline for (routes) |route| { if (Context(route).matchAndHandle(api_source, req, res, alloc)) return true; } return false; } 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; fn parseRouteArgs(comptime route: []const u8, comptime Args: type, path: []const u8) !Args { var args: Args = undefined; var path_iter = util.PathIter.from(path); comptime var route_iter = util.PathIter.from(route); inline while (comptime route_iter.next()) |route_segment| { const path_segment = path_iter.next() orelse return error.RouteMismatch; if (route_segment.len > 0 and route_segment[0] == ':') { const A = @TypeOf(@field(args, route_segment[1..])); @field(args, route_segment[1..]) = try parseRouteArg(A, path_segment); } else { if (!std.ascii.eqlIgnoreCase(route_segment, path_segment)) return error.RouteMismatch; } } if (path_iter.next() != null) return error.RouteMismatch; return args; } fn parseRouteArg(comptime T: type, segment: []const u8) !T { if (T == []const u8) return segment; if (comptime std.meta.trait.isContainer(T) and std.meta.trait.hasFn("parse")(T)) return T.parse(segment); @compileError("Unsupported Type " ++ @typeName(T)); } const BaseContentType = enum { json, url_encoded, octet_stream, other, }; fn parseBody(comptime T: type, content_type: BaseContentType, reader: anytype, alloc: std.mem.Allocator) !T { const buf = try reader.readAllAlloc(alloc, 1 << 16); defer alloc.free(buf); switch (content_type) { .octet_stream, .json => { const body = try json_utils.parse(T, buf, alloc); defer json_utils.parseFree(body, alloc); return try util.deepClone(alloc, body); }, .url_encoded => return query_utils.parseQuery(alloc, T, buf) catch |err| switch (err) { error.NoQuery => error.NoBody, else => err, }, else => return error.UnsupportedMediaType, } } fn matchContentType(hdr: ?[]const u8) ?BaseContentType { if (hdr) |h| { if (std.ascii.eqlIgnoreCase(h, "application/x-www-form-urlencoded")) return .url_encoded; if (std.ascii.eqlIgnoreCase(h, "application/json")) return .json; if (std.ascii.eqlIgnoreCase(h, "application/octet-stream")) return .octet_stream; return .other; } return null; } pub const AllocationStrategy = enum { arena, normal, }; pub fn Context(comptime Route: type) type { return struct { const Self = @This(); pub const Args = if (@hasDecl(Route, "Args")) Route.Args else void; // TODO: if controller does not provide a body type, maybe we should // leave it as a simple reader instead of void pub const Body = if (@hasDecl(Route, "Body")) Route.Body else void; // TODO: if controller does not provide a query type, maybe we should // leave it as a simple string instead of void pub const Query = if (@hasDecl(Route, "Query")) Route.Query else void; const allocation_strategy: AllocationStrategy = if (@hasDecl(Route, "allocation_strategy")) Route.AllocationStrategy else .arena; base_request: *http.Request, allocator: std.mem.Allocator, method: http.Method, uri: []const u8, headers: http.Fields, args: Args, body: Body, query: Query, // TODO body_buf: ?[]const u8 = null, pub fn matchAndHandle(api_source: *api.ApiSource, req: *http.Request, res: *Response, alloc: std.mem.Allocator) bool { if (req.method != Route.method) return false; var path = std.mem.sliceTo(std.mem.sliceTo(req.uri, '#'), '?'); var args = parseRouteArgs(Route.path, Args, path) catch return false; std.log.debug("Matched route {s}", .{Route.path}); handle(api_source, req, res, alloc, args) catch |err| { std.log.err("{}", .{err}); if (!res.opened) res.err(.internal_server_error, "", {}) catch {}; }; return true; } fn handle( api_source: *api.ApiSource, req: *http.Request, res: *Response, base_allocator: std.mem.Allocator, args: Args, ) !void { const base_content_type = matchContentType(req.headers.get("Content-Type")); var arena = if (allocation_strategy == .arena) std.heap.ArenaAllocator.init(base_allocator) else {}; const alloc = if (allocation_strategy == .arena) arena.allocator() else base_allocator; const body = if (Body != void) blk: { var stream = req.body orelse return error.NoBody; break :blk try parseBody(Body, base_content_type orelse .json, stream.reader(), alloc); } else {}; defer if (Body != void) util.deepFree(alloc, body); const query = if (Query != void) blk: { const path = std.mem.sliceTo(req.uri, '?'); const q = req.uri[path.len..]; break :blk try query_utils.parseQuery(alloc, Query, q); }; defer if (Query != void) util.deepFree(alloc, query); var api_conn = 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| break :conn 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| { break :conn try api_source.connectToken(host, token_hdr, alloc); } } else return error.InvalidCookie; } break :conn try api_source.connectUnauthorized(host, alloc); }; defer api_conn.close(); const self = Self{ .allocator = alloc, .base_request = req, .method = req.method, .uri = req.uri, .headers = req.headers, .args = args, .body = body, .query = query, }; try Route.handler(self, res, &api_conn); } fn errorHandler(response: *Response, status: http.Status, err: anytype) void { std.log.err("Error occured on handler {s} {s}", .{ @tagName(Route.method), Route.path }); std.log.err("{}", .{err}); const result = if (builtin.mode == .Debug) response.err(status, @errorName(err), {}) else response.status(status); _ = result catch |err2| { std.log.err("Error printing response: {}", .{err2}); }; } }; } 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}, ); } };