fediglam/src/main/controllers.zig

398 lines
13 KiB
Zig
Raw Normal View History

2022-07-13 07:57:21 +00:00
const std = @import("std");
const root = @import("root");
const builtin = @import("builtin");
const http = @import("http");
2022-10-08 20:47:54 +00:00
const api = @import("api");
2022-10-08 07:51:22 +00:00
const util = @import("util");
2022-10-09 09:05:01 +00:00
const query_utils = @import("./query.zig");
2022-10-10 02:06:11 +00:00
const json_utils = @import("./json.zig");
2022-07-13 07:57:21 +00:00
2022-11-15 05:38:08 +00:00
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");
2022-11-18 04:11:04 +00:00
const web = @import("./controllers/web.zig");
2022-07-22 06:53:05 +00:00
2022-11-24 04:51:30 +00:00
const mdw = http.middleware;
const router = Router(&.{});
const not_found = struct {
pub fn handler(self: @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 ApiCall(comptime Route: type) type {
return mdw.
}
2022-11-07 07:38:21 +00:00
pub fn routeRequest(api_source: anytype, req: *http.Request, res: *http.Response, alloc: std.mem.Allocator) void {
2022-10-10 02:06:11 +00:00
// TODO: hashmaps?
2022-11-24 04:51:30 +00:00
base_handler
//var response = Response{ .headers = http.Fields.init(alloc), .res = res };
//defer response.headers.deinit();
2022-10-16 12:48:12 +00:00
2022-11-24 04:51:30 +00:00
//const found = routeRequestInternal(api_source, req, &response, alloc);
2022-10-16 12:48:12 +00:00
2022-11-24 04:51:30 +00:00
//if (!found) response.status(.not_found) catch {};
2022-10-16 12:48:12 +00:00
}
2022-11-07 07:38:21 +00:00
fn routeRequestInternal(api_source: anytype, req: *http.Request, res: *Response, alloc: std.mem.Allocator) bool {
2022-10-10 02:06:11 +00:00
inline for (routes) |route| {
2022-10-16 12:48:12 +00:00
if (Context(route).matchAndHandle(api_source, req, res, alloc)) return true;
2022-10-10 02:06:11 +00:00
}
2022-10-16 12:48:12 +00:00
return false;
2022-10-11 03:28:23 +00:00
}
2022-07-13 07:57:21 +00:00
2022-10-11 03:28:23 +00:00
const routes = .{
auth.login,
auth.verify_login,
communities.create,
2022-10-11 04:49:36 +00:00
communities.query,
2022-10-11 03:28:23 +00:00
invites.create,
users.create,
notes.create,
2022-10-11 04:49:36 +00:00
notes.get,
2022-10-16 12:48:12 +00:00
streaming.streaming,
2022-11-12 12:39:49 +00:00
timelines.global,
2022-11-12 13:23:55 +00:00
timelines.local,
2022-11-14 23:00:01 +00:00
timelines.home,
2022-11-14 09:03:11 +00:00
follows.create,
2022-11-19 11:33:35 +00:00
follows.delete,
2022-11-14 09:03:11 +00:00
follows.query_followers,
follows.query_following,
2022-11-18 04:11:04 +00:00
} ++ web.routes;
2022-07-13 07:57:21 +00:00
2022-11-15 07:22:40 +00:00
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 {
2022-11-15 07:22:40 +00:00
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;
2022-11-15 07:22:40 +00:00
}
pub const AllocationStrategy = enum {
arena,
normal,
};
2022-10-09 09:05:01 +00:00
pub fn Context(comptime Route: type) type {
return struct {
const Self = @This();
pub const Args = if (@hasDecl(Route, "Args")) Route.Args else void;
2022-10-10 02:31:15 +00:00
// TODO: if controller does not provide a body type, maybe we should
// leave it as a simple reader instead of void
2022-10-09 09:05:01 +00:00
pub const Body = if (@hasDecl(Route, "Body")) Route.Body else void;
2022-10-10 02:31:15 +00:00
// TODO: if controller does not provide a query type, maybe we should
// leave it as a simple string instead of void
2022-10-09 09:05:01 +00:00
pub const Query = if (@hasDecl(Route, "Query")) Route.Query else void;
const allocation_strategy: AllocationStrategy = if (@hasDecl(Route, "allocation_strategy"))
Route.AllocationStrategy
else
.arena;
2022-11-07 07:38:21 +00:00
base_request: *http.Request,
2022-10-16 12:48:12 +00:00
2022-10-09 09:05:01 +00:00
allocator: std.mem.Allocator,
method: http.Method,
2022-10-13 09:23:57 +00:00
uri: []const u8,
2022-11-05 07:26:53 +00:00
headers: http.Fields,
2022-10-09 09:05:01 +00:00
args: Args,
body: Body,
query: Query,
2022-11-07 07:38:21 +00:00
// TODO
body_buf: ?[]const u8 = null,
2022-11-15 07:22:40 +00:00
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;
2022-07-13 07:57:21 +00:00
2022-11-15 07:22:40 +00:00
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 {};
};
2022-10-11 04:49:36 +00:00
2022-11-15 07:22:40 +00:00
return true;
2022-10-11 04:49:36 +00:00
}
2022-11-15 07:22:40 +00:00
fn handle(
api_source: *api.ApiSource,
req: *http.Request,
res: *Response,
base_allocator: std.mem.Allocator,
2022-11-15 07:22:40 +00:00
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;
2022-11-15 07:22:40 +00:00
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);
2022-11-15 07:22:40 +00:00
} 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);
2022-11-15 07:22:40 +00:00
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);
2022-07-13 07:57:21 +00:00
2022-11-18 06:51:51 +00:00
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);
}
2022-11-18 11:37:42 +00:00
} else return error.InvalidCookie;
2022-11-18 06:51:51 +00:00
}
2022-11-15 07:22:40 +00:00
break :conn try api_source.connectUnauthorized(host, alloc);
};
defer api_conn.close();
const self = Self{
2022-10-09 09:05:01 +00:00
.allocator = alloc,
2022-10-16 12:48:12 +00:00
.base_request = req,
2022-10-09 09:05:01 +00:00
.method = req.method,
2022-10-13 09:23:57 +00:00
.uri = req.uri,
2022-10-09 09:05:01 +00:00
.headers = req.headers,
.args = args,
2022-11-15 07:22:40 +00:00
.body = body,
.query = query,
2022-10-09 09:05:01 +00:00
};
2022-11-15 07:22:40 +00:00
try Route.handler(self, res, &api_conn);
2022-10-08 07:51:22 +00:00
}
2022-10-09 09:05:01 +00:00
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});
};
2022-10-09 09:05:01 +00:00
}
};
}
2022-07-18 06:11:42 +00:00
2022-10-09 09:05:01 +00:00
pub const Response = struct {
const Self = @This();
2022-11-05 07:26:53 +00:00
headers: http.Fields,
2022-10-13 09:23:57 +00:00
res: *http.Response,
opened: bool = false,
2022-07-18 06:11:42 +00:00
2022-10-16 12:48:12 +00:00
/// Write a response with no body, only a given status
2022-10-11 03:28:23 +00:00
pub fn status(self: *Self, status_code: http.Status) !void {
2022-11-15 05:38:08 +00:00
var stream = try self.open(status_code);
2022-10-09 09:05:01 +00:00
defer stream.close();
try stream.finish();
2022-07-18 06:11:42 +00:00
}
2022-07-13 07:57:21 +00:00
2022-10-16 12:48:12 +00:00
/// Write a request body as json
2022-10-11 03:28:23 +00:00
pub fn json(self: *Self, status_code: http.Status, response_body: anytype) !void {
2022-10-09 09:05:01 +00:00
try self.headers.put("Content-Type", "application/json");
2022-07-13 07:57:21 +00:00
2022-11-15 05:38:08 +00:00
var stream = try self.open(status_code);
2022-10-09 09:05:01 +00:00
defer stream.close();
2022-07-13 07:57:21 +00:00
2022-10-09 09:05:01 +00:00
const writer = stream.writer();
try std.json.stringify(response_body, json_options, writer);
2022-08-02 06:24:16 +00:00
2022-10-09 09:05:01 +00:00
try stream.finish();
}
2022-10-11 03:28:23 +00:00
2022-11-18 02:42:23 +00:00
pub fn open(self: *Self, status_code: http.Status) !http.Response.ResponseStream {
2022-11-15 05:38:08 +00:00
std.debug.assert(!self.opened);
self.opened = true;
return try self.res.open(status_code, &self.headers);
}
2022-10-16 12:48:12 +00:00
/// Prints the given error as json
2022-10-11 03:28:23 +00:00
pub fn err(self: *Self, status_code: http.Status, message: []const u8, details: anytype) !void {
return self.json(status_code, .{
.message = message,
.details = details,
});
}
2022-10-16 12:48:12 +00:00
/// Signals that the HTTP connection should be hijacked without writing a
/// response beforehand.
pub fn hijack(self: *Self) *http.Response {
2022-11-15 05:38:08 +00:00
std.debug.assert(!self.opened);
2022-10-16 12:48:12 +00:00
self.opened = true;
return self.res;
}
2022-11-18 04:11:04 +00:00
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();
}
2022-10-09 09:05:01 +00:00
};
const json_options = if (builtin.mode == .Debug)
.{
.whitespace = .{
.indent = .{ .Space = 2 },
.separator = true,
},
.string = .{ .String = .{} },
} else .{
.whitespace = .{
.indent = .None,
.separator = false,
},
.string = .{ .String = .{} },
};
2022-11-14 08:25:52 +00:00
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},
);
}
};