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");
|
|
|
|
|
|
|
|
const web = struct {
|
|
|
|
const index = @import("./controllers/web/index.zig");
|
|
|
|
};
|
2022-07-22 06:53:05 +00:00
|
|
|
|
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-05 07:26:53 +00:00
|
|
|
var response = Response{ .headers = http.Fields.init(alloc), .res = res };
|
2022-10-13 09:23:57 +00:00
|
|
|
defer response.headers.deinit();
|
2022-10-16 12:48:12 +00:00
|
|
|
|
|
|
|
const found = routeRequestInternal(api_source, req, &response, alloc);
|
|
|
|
|
|
|
|
if (!found) response.status(.not_found) catch {};
|
|
|
|
}
|
|
|
|
|
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,
|
|
|
|
follows.query_followers,
|
|
|
|
follows.query_following,
|
2022-11-15 05:38:08 +00:00
|
|
|
|
|
|
|
web.index,
|
2022-10-09 09:05:01 +00:00
|
|
|
};
|
2022-07-13 07:57:21 +00:00
|
|
|
|
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;
|
|
|
|
|
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-10-09 09:05:01 +00:00
|
|
|
fn parseArgs(path: []const u8) ?Args {
|
2022-10-11 03:28:23 +00:00
|
|
|
var args: Args = undefined;
|
2022-10-09 09:05:01 +00:00
|
|
|
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;
|
2022-11-15 05:38:08 +00:00
|
|
|
if (route_segment.len > 0 and route_segment[0] == ':') {
|
2022-10-11 04:49:36 +00:00
|
|
|
const A = @TypeOf(@field(args, route_segment[1..]));
|
|
|
|
@field(args, route_segment[1..]) = parseArg(A, path_segment) catch return null;
|
2022-10-09 09:05:01 +00:00
|
|
|
} else {
|
|
|
|
if (!std.ascii.eqlIgnoreCase(route_segment, path_segment)) return null;
|
|
|
|
}
|
|
|
|
}
|
2022-07-21 03:39:43 +00:00
|
|
|
|
2022-11-14 09:03:11 +00:00
|
|
|
if (path_iter.next() != null) return null;
|
|
|
|
|
2022-10-09 09:05:01 +00:00
|
|
|
return args;
|
|
|
|
}
|
2022-07-13 07:57:21 +00:00
|
|
|
|
2022-10-11 04:49:36 +00:00
|
|
|
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));
|
|
|
|
}
|
|
|
|
|
2022-11-07 07:38:21 +00:00
|
|
|
pub fn matchAndHandle(api_source: *api.ApiSource, req: *http.Request, res: *Response, alloc: std.mem.Allocator) bool {
|
2022-10-09 09:05:01 +00:00
|
|
|
if (req.method != Route.method) return false;
|
2022-10-13 09:23:57 +00:00
|
|
|
var path = std.mem.sliceTo(std.mem.sliceTo(req.uri, '#'), '?');
|
2022-10-11 03:28:23 +00:00
|
|
|
var args: Args = parseArgs(path) orelse return false;
|
2022-07-13 07:57:21 +00:00
|
|
|
|
2022-10-09 09:05:01 +00:00
|
|
|
var self = Self{
|
|
|
|
.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,
|
|
|
|
.body = undefined,
|
|
|
|
.query = undefined,
|
|
|
|
};
|
|
|
|
|
2022-11-07 07:38:21 +00:00
|
|
|
std.log.debug("Matched route {s}", .{path});
|
|
|
|
|
2022-10-13 09:23:57 +00:00
|
|
|
self.prepareAndHandle(api_source, req, res);
|
2022-10-09 09:05:01 +00:00
|
|
|
|
|
|
|
return true;
|
2022-10-08 07:51:22 +00:00
|
|
|
}
|
2022-10-09 09:05:01 +00:00
|
|
|
|
2022-10-12 02:19:34 +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-11-07 07:38:21 +00:00
|
|
|
fn prepareAndHandle(self: *Self, api_source: anytype, req: *http.Request, response: *Response) void {
|
2022-10-12 02:19:34 +00:00
|
|
|
self.parseBody(req) catch |err| return errorHandler(response, .bad_request, err);
|
2022-10-09 09:05:01 +00:00
|
|
|
defer self.freeBody();
|
|
|
|
|
2022-10-12 02:19:34 +00:00
|
|
|
self.parseQuery() catch |err| return errorHandler(response, .bad_request, err);
|
2022-10-09 09:05:01 +00:00
|
|
|
|
2022-10-12 02:19:34 +00:00
|
|
|
var api_conn = self.getApiConn(api_source) catch |err| return errorHandler(response, .internal_server_error, err);
|
2022-10-09 09:05:01 +00:00
|
|
|
defer api_conn.close();
|
|
|
|
|
2022-10-11 03:28:23 +00:00
|
|
|
self.handle(response, &api_conn);
|
2022-10-09 09:05:01 +00:00
|
|
|
}
|
|
|
|
|
2022-11-07 07:38:21 +00:00
|
|
|
fn parseBody(self: *Self, req: *http.Request) !void {
|
2022-10-09 09:05:01 +00:00
|
|
|
if (Body != void) {
|
2022-11-07 07:38:21 +00:00
|
|
|
var stream = req.body orelse return error.NoBody;
|
|
|
|
const body = try stream.reader().readAllAlloc(self.allocator, 1 << 16);
|
|
|
|
errdefer self.allocator.free(body);
|
2022-10-10 02:06:11 +00:00
|
|
|
self.body = try json_utils.parse(Body, body, self.allocator);
|
2022-11-07 07:38:21 +00:00
|
|
|
self.body_buf = body;
|
2022-10-08 07:51:22 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-10-09 09:05:01 +00:00
|
|
|
fn freeBody(self: *Self) void {
|
|
|
|
if (Body != void) {
|
2022-10-10 02:06:11 +00:00
|
|
|
json_utils.parseFree(self.body, self.allocator);
|
2022-11-07 07:38:21 +00:00
|
|
|
self.allocator.free(self.body_buf.?);
|
2022-10-09 09:05:01 +00:00
|
|
|
}
|
|
|
|
}
|
2022-10-08 07:51:22 +00:00
|
|
|
|
2022-10-09 09:05:01 +00:00
|
|
|
fn parseQuery(self: *Self) !void {
|
|
|
|
if (Query != void) {
|
2022-10-13 09:23:57 +00:00
|
|
|
const path = std.mem.sliceTo(self.uri, '?');
|
|
|
|
const q = std.mem.sliceTo(self.uri[path.len..], '#');
|
2022-10-08 07:51:22 +00:00
|
|
|
|
2022-10-09 09:05:01 +00:00
|
|
|
self.query = try query_utils.parseQuery(Query, q);
|
2022-10-08 07:51:22 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-10-09 09:05:01 +00:00
|
|
|
fn handle(self: Self, response: *Response, api_conn: anytype) void {
|
2022-10-13 09:23:57 +00:00
|
|
|
Route.handler(self, response, api_conn) catch |err| switch (err) {
|
|
|
|
else => {
|
|
|
|
std.log.err("{}", .{err});
|
2022-10-16 12:48:12 +00:00
|
|
|
if (!response.opened) response.err(.internal_server_error, "", {}) catch {};
|
2022-10-13 09:23:57 +00:00
|
|
|
},
|
|
|
|
};
|
2022-10-09 09:05:01 +00:00
|
|
|
}
|
2022-07-18 06:11:42 +00:00
|
|
|
|
2022-10-09 09:05:01 +00:00
|
|
|
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;
|
2022-09-05 08:52:49 +00:00
|
|
|
|
2022-10-09 09:05:01 +00:00
|
|
|
if (token) |t| return try api_source.connectToken(host, t, self.allocator);
|
2022-07-26 02:07:05 +00:00
|
|
|
|
2022-10-09 09:05:01 +00:00
|
|
|
return try api_source.connectUnauthorized(host, self.allocator);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
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-15 05:38:08 +00:00
|
|
|
pub fn open(self: *Self, status_code: http.Status) !http.Response.Stream {
|
|
|
|
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-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},
|
|
|
|
);
|
|
|
|
}
|
|
|
|
};
|