fediglam/src/main/controllers.zig

363 lines
12 KiB
Zig
Raw Permalink 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-07-13 07:57:21 +00:00
2022-11-26 01:43:30 +00:00
const web_endpoints = @import("./controllers/web.zig").routes;
const api_endpoints = @import("./controllers/api.zig").routes;
2022-07-22 06:53:05 +00:00
2022-11-24 04:51:30 +00:00
const mdw = http.middleware;
const not_found = struct {
pub fn handler(
_: @This(),
_: anytype,
res: anytype,
ctx: anytype,
) !void {
2022-11-24 04:51:30 +00:00
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));
2022-11-24 11:50:25 +00:00
const inject_api_conn = struct {
2022-11-26 01:43:30 +00:00
fn getApiConn(alloc: std.mem.Allocator, api_source: anytype, req: anytype) !@TypeOf(api_source.*).Conn {
2022-11-24 11:50:25 +00:00
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;
}
2022-11-24 11:50:25 +00:00
return try api_source.connectUnauthorized(host, alloc);
}
2022-11-24 11:50:25 +00:00
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();
2022-11-27 01:33:46 +00:00
return mdw.injectContextValue("api_conn", &api_conn).handle(
2022-11-24 11:50:25 +00:00
req,
res,
2022-11-26 01:43:30 +00:00
ctx,
next,
2022-11-24 11:50:25 +00:00
);
}
2022-11-26 01:43:30 +00:00
}{};
pub fn EndpointRequest(comptime Endpoint: type) type {
return struct {
2022-11-26 01:43:30 +00:00
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;
2022-12-06 09:48:12 +00:00
const body_options = .{
.allow_unknown_fields = if (@hasDecl(Endpoint, "allow_unknown_fields_in_body"))
Endpoint.allow_unknown_fields_in_body
else
false,
};
const union_tag_from_query_param = if (@hasDecl(Endpoint, "body_tag_from_query_param")) blk: {
if (!std.meta.trait.is(.Union)(Body)) @compileError("body_tag_from_query_param only valid if body is a union");
break :blk @as(?[]const u8, Endpoint.body_tag_from_query_param);
} else null;
allocator: std.mem.Allocator,
method: http.Method,
uri: []const u8,
headers: http.Fields,
args: Args,
body: Body,
query: Query,
2022-12-14 10:57:53 +00:00
mount_path: []const u8,
2022-11-27 01:33:46 +00:00
const args_middleware = //if (Args == void)
//mdw.injectContext(.{ .args = {} })
//else
2022-11-26 01:43:30 +00:00
mdw.ParsePathArgs(Endpoint.path, Args){};
const body_middleware = if (union_tag_from_query_param) |param|
ParseBodyWithQueryType(Body, param, body_options){}
else
2022-12-06 09:48:12 +00:00
mdw.ParseBody(Body, body_options){};
2022-11-27 01:33:46 +00:00
const query_middleware = //if (Query == void)
//mdw.injectContext(.{ .query_params = {} })
//else
mdw.ParseQueryParams(Query){};
};
}
/// Gets a tag from the query param with the given name, then treats the request body
/// as the respective union type
fn ParseBodyWithQueryType(comptime Union: type, comptime query_param_name: []const u8, comptime options: anytype) type {
return struct {
pub fn handle(_: @This(), req: anytype, res: anytype, ctx: anytype, next: anytype) !void {
const Tag = std.meta.Tag(Union);
const Param = @Type(.{ .Struct = .{
.fields = &.{.{
.name = query_param_name,
.field_type = Tag,
.default_value = null,
.is_comptime = false,
.alignment = if (@sizeOf(Tag) == 0) 0 else @alignOf(Tag),
}},
.decls = &.{},
.layout = .Auto,
.is_tuple = false,
} });
const param = try http.urlencode.parse(ctx.allocator, true, Param, ctx.query_string);
var result: ?Union = null;
const content_type = req.headers.get("Content-Type");
inline for (comptime std.meta.tags(Tag)) |tag| {
if (@field(param, query_param_name) == tag) {
std.debug.assert(result == null);
const P = std.meta.TagPayload(Union, tag);
std.log.debug("Deserializing to type {}", .{P});
var stream = req.body orelse return error.NoBody;
result = @unionInit(Union, @tagName(tag), try mdw.parseBodyFromReader(
P,
options,
content_type,
stream.reader(),
ctx.allocator,
));
}
}
return mdw.injectContextValue("body", result.?).handle(
req,
res,
ctx,
next,
);
}
};
}
fn CallApiEndpoint(comptime Endpoint: type) type {
return struct {
2022-11-26 01:43:30 +00:00
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,
2022-11-26 01:43:30 +00:00
.query = ctx.query_params,
2022-12-14 10:57:53 +00:00
.mount_path = if (@hasField(@TypeOf(ctx), "mounted_at")) ctx.mounted_at else "",
};
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);
2022-11-26 01:43:30 +00:00
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
2022-11-24 11:50:25 +00:00
@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
2022-11-24 11:50:25 +00:00
inject_api_conn,
CallApiEndpoint(Endpoint){},
});
2022-11-24 04:51:30 +00:00
}
2022-11-26 01:43:30 +00:00
const api_router = mdw.apply(.{
mdw.mount("/api/v0/"),
mdw.router(api_endpoints),
});
pub const router = mdw.apply(.{
mdw.split_uri,
2022-11-27 01:52:30 +00:00
mdw.catchErrors(mdw.default_error_handler),
2022-11-27 01:33:46 +00:00
mdw.router(.{api_router} ++ web_endpoints),
2022-11-26 01:43:30 +00:00
});
2022-10-16 12:48:12 +00:00
pub const AllocationStrategy = enum {
arena,
normal,
};
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
2022-12-09 12:31:23 +00:00
pub fn template(self: *Self, status_code: http.Status, srv: anytype, comptime templ: []const u8, data: anytype) !void {
2022-11-18 04:11:04 +00:00
try self.headers.put("Content-Type", "text/html");
2023-01-04 19:03:23 +00:00
const user = if (srv.context.userId()) |uid| try srv.getActor(uid) else null;
2022-12-13 10:28:25 +00:00
defer util.deepFree(srv.allocator, user);
2022-11-18 04:11:04 +00:00
var stream = try self.open(status_code);
defer stream.close();
const writer = stream.writer();
2022-12-09 12:31:23 +00:00
try @import("template").execute(
writer,
.{
._page = templ,
2022-12-12 04:23:20 +00:00
.@"mini-user" = @embedFile("./controllers/web/_components/mini-user.tmpl.html"),
2022-12-09 12:31:23 +00:00
},
@embedFile("./controllers/web/_format.tmpl.html"),
data,
.{
2023-01-02 00:01:58 +00:00
.community = srv.context.community,
2022-12-13 10:28:25 +00:00
.user = user,
2022-12-09 12:31:23 +00:00
},
);
2022-11-18 04:11:04 +00:00
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 {
2022-11-27 09:59:37 +00:00
pub fn paginate(results: anytype, res: *Response, alloc: std.mem.Allocator) !void {
2022-11-14 08:25:52 +00:00
var link = std.ArrayList(u8).init(alloc);
const link_writer = link.writer();
defer link.deinit();
2022-11-27 09:59:37 +00:00
try writeLink(link_writer, null, "", results.next_page, "next");
2022-11-14 08:25:52 +00:00
try link_writer.writeByte(',');
2022-11-27 09:59:37 +00:00
try writeLink(link_writer, null, "", results.prev_page, "prev");
2022-11-14 08:25:52 +00:00
try res.headers.put("Link", link.items);
try res.json(.ok, results.items);
}
fn writeLink(
writer: anytype,
2022-11-27 09:59:37 +00:00
community: ?api.Community,
2022-11-14 08:25:52 +00:00
path: []const u8,
params: anytype,
rel: []const u8,
) !void {
2022-11-27 09:59:37 +00:00
if (community) |c| {
try std.fmt.format(
writer,
"<{s}://{s}/{s}?{}>; rel=\"{s}\"",
2022-12-02 04:41:52 +00:00
.{ @tagName(c.scheme), c.host, path, http.urlencode.encodeStruct(params), rel },
2022-11-27 09:59:37 +00:00
);
} else {
try std.fmt.format(
writer,
"<{s}?{}>; rel=\"{s}\"",
2022-12-02 04:41:52 +00:00
.{ path, http.urlencode.encodeStruct(params), rel },
2022-11-27 09:59:37 +00:00
);
}
2022-11-14 08:25:52 +00:00
// TODO: percent-encode
}
};