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/auth.zig"); pub const communities = @import("./controllers/communities.zig"); pub const invites = @import("./controllers/invites.zig"); pub const users = @import("./controllers/users.zig"); pub const notes = @import("./controllers/notes.zig"); pub const streaming = @import("./controllers/streaming.zig"); pub const timelines = @import("./controllers/timelines.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, }; 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; 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, fn parseArgs(path: []const u8) ?Args { var args: Args = undefined; var path_iter = util.PathIter.from(path); comptime var route_iter = util.PathIter.from(Route.path); inline while (comptime route_iter.next()) |route_segment| { const path_segment = path_iter.next() orelse return null; if (route_segment[0] == ':') { const A = @TypeOf(@field(args, route_segment[1..])); @field(args, route_segment[1..]) = parseArg(A, path_segment) catch return null; } else { if (!std.ascii.eqlIgnoreCase(route_segment, path_segment)) return null; } } return args; } fn parseArg(comptime T: type, segment: []const u8) !T { if (T == []const u8) return segment; if (comptime std.meta.trait.hasFn("parse")(T)) return T.parse(segment); @compileError("Unsupported Type " ++ @typeName(T)); } 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: Args = parseArgs(path) orelse return false; var self = Self{ .allocator = alloc, .base_request = req, .method = req.method, .uri = req.uri, .headers = req.headers, .args = args, .body = undefined, .query = undefined, }; std.log.debug("Matched route {s}", .{path}); self.prepareAndHandle(api_source, req, res); return true; } 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}); }; } fn prepareAndHandle(self: *Self, api_source: anytype, req: *http.Request, response: *Response) void { self.parseBody(req) catch |err| return errorHandler(response, .bad_request, err); defer self.freeBody(); self.parseQuery() catch |err| return errorHandler(response, .bad_request, err); var api_conn = self.getApiConn(api_source) catch |err| return errorHandler(response, .internal_server_error, err); defer api_conn.close(); self.handle(response, &api_conn); } fn parseBody(self: *Self, req: *http.Request) !void { if (Body != void) { var stream = req.body orelse return error.NoBody; const body = try stream.reader().readAllAlloc(self.allocator, 1 << 16); errdefer self.allocator.free(body); self.body = try json_utils.parse(Body, body, self.allocator); self.body_buf = body; } } fn freeBody(self: *Self) void { if (Body != void) { json_utils.parseFree(self.body, self.allocator); self.allocator.free(self.body_buf.?); } } fn parseQuery(self: *Self) !void { if (Query != void) { const path = std.mem.sliceTo(self.uri, '?'); const q = std.mem.sliceTo(self.uri[path.len..], '#'); self.query = try query_utils.parseQuery(Query, q); } } fn handle(self: Self, response: *Response, api_conn: anytype) void { Route.handler(self, response, api_conn) catch |err| switch (err) { else => { std.log.err("{}", .{err}); if (!response.opened) response.err(.internal_server_error, "", {}) catch {}; }, }; } fn getApiConn(self: *Self, api_source: anytype) !api.ApiSource.Conn { const host = self.headers.get("Host") orelse return error.NoHost; const auth_header = self.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, self.allocator); return try api_source.connectUnauthorized(host, self.allocator); } }; } 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 { std.debug.assert(!self.opened); self.opened = true; var stream = try self.res.open(status_code, &self.headers); 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 { std.debug.assert(!self.opened); self.opened = true; try self.headers.put("Content-Type", "application/json"); var stream = try self.res.open(status_code, &self.headers); defer stream.close(); const writer = stream.writer(); try std.json.stringify(response_body, json_options, writer); try stream.finish(); } /// 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 { self.opened = true; return self.res; } }; 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}, ); } };