281 lines
8.5 KiB
Zig
281 lines
8.5 KiB
Zig
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 web_endpoints = @import("./controllers/web.zig").routes;
|
|
const api_endpoints = @import("./controllers/api.zig").routes;
|
|
|
|
const mdw = http.middleware;
|
|
|
|
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));
|
|
|
|
const inject_api_conn = struct {
|
|
fn getApiConn(alloc: std.mem.Allocator, api_source: anytype, req: anytype) !@TypeOf(api_source.*).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 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;
|
|
}
|
|
|
|
return try api_source.connectUnauthorized(host, alloc);
|
|
}
|
|
|
|
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();
|
|
|
|
return mdw.injectContextValue("api_conn", &api_conn).handle(
|
|
req,
|
|
res,
|
|
ctx,
|
|
next,
|
|
);
|
|
}
|
|
}{};
|
|
|
|
pub fn EndpointRequest(comptime Endpoint: type) type {
|
|
return struct {
|
|
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;
|
|
|
|
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(Endpoint.path, Args){};
|
|
|
|
const body_middleware = //if (Body == void)
|
|
//mdw.injectContext(.{ .body = {} })
|
|
//else
|
|
mdw.ParseBody(Body){};
|
|
|
|
const query_middleware = //if (Query == void)
|
|
//mdw.injectContext(.{ .query_params = {} })
|
|
//else
|
|
mdw.ParseQueryParams(Query){};
|
|
};
|
|
}
|
|
|
|
fn CallApiEndpoint(comptime Endpoint: type) type {
|
|
return struct {
|
|
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,
|
|
.query = ctx.query_params,
|
|
};
|
|
|
|
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);
|
|
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
|
|
@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
|
|
inject_api_conn,
|
|
CallApiEndpoint(Endpoint){},
|
|
});
|
|
}
|
|
|
|
const api_router = mdw.apply(.{
|
|
mdw.mount("/api/v0/"),
|
|
mdw.router(api_endpoints),
|
|
});
|
|
|
|
pub const router = mdw.apply(.{
|
|
mdw.split_uri,
|
|
mdw.catchErrors(mdw.default_error_handler),
|
|
mdw.router(.{api_router} ++ web_endpoints),
|
|
});
|
|
|
|
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(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, null, "", results.next_page, "next");
|
|
try link_writer.writeByte(',');
|
|
try writeLink(link_writer, null, "", 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 {
|
|
if (community) |c| {
|
|
try std.fmt.format(
|
|
writer,
|
|
"<{s}://{s}/{s}?{}>; rel=\"{s}\"",
|
|
.{ @tagName(c.scheme), c.host, path, http.urlencode.encodeStruct(params), rel },
|
|
);
|
|
} else {
|
|
try std.fmt.format(
|
|
writer,
|
|
"<{s}?{}>; rel=\"{s}\"",
|
|
.{ path, http.urlencode.encodeStruct(params), rel },
|
|
);
|
|
}
|
|
// TODO: percent-encode
|
|
}
|
|
};
|