diff options
Diffstat (limited to 'src/routes')
| -rw-r--r-- | src/routes/access.zig | 6 | ||||
| -rw-r--r-- | src/routes/api/auth/first-login.zig | 5 | ||||
| -rw-r--r-- | src/routes/api/auth/login.zig | 18 | ||||
| -rw-r--r-- | src/routes/api/auth/root.zig | 2 | ||||
| -rw-r--r-- | src/routes/api/root.zig | 1 | ||||
| -rw-r--r-- | src/routes/api/session/current.zig | 23 | ||||
| -rw-r--r-- | src/routes/api/session/drop.zig | 8 | ||||
| -rw-r--r-- | src/routes/api/session/renew.zig | 8 | ||||
| -rw-r--r-- | src/routes/api/session/root.zig | 5 | ||||
| -rw-r--r-- | src/routes/context.zig | 2 | ||||
| -rw-r--r-- | src/routes/handler-info.zig | 201 | ||||
| -rw-r--r-- | src/routes/root.zig | 45 | ||||
| -rw-r--r-- | src/routes/static.zig (renamed from src/routes/fallback.zig) | 5 |
13 files changed, 251 insertions, 78 deletions
diff --git a/src/routes/access.zig b/src/routes/access.zig new file mode 100644 index 0000000..39872b8 --- /dev/null +++ b/src/routes/access.zig @@ -0,0 +1,6 @@ + +pub const Access = enum { + everyone, + users, + admins, +}; diff --git a/src/routes/api/auth/first-login.zig b/src/routes/api/auth/first-login.zig index 2fb8c02..34f04d8 100644 --- a/src/routes/api/auth/first-login.zig +++ b/src/routes/api/auth/first-login.zig @@ -1,14 +1,13 @@ const std = @import("std"); const Context = @import("../../context.zig"); -pub const needs_auth = true; -pub const method = .POST; +pub const access = .everyone; const Result = struct { is_first: bool, }; -pub fn handler(ctx: *Context) anyerror!Result { +pub fn post(ctx: *Context) anyerror!Result { _ = ctx; return .{ .is_first = false }; } diff --git a/src/routes/api/auth/login.zig b/src/routes/api/auth/login.zig index c3f2bef..76efcf4 100644 --- a/src/routes/api/auth/login.zig +++ b/src/routes/api/auth/login.zig @@ -3,9 +3,6 @@ const std = @import("std"); const Context = @import("../../context.zig"); const Storage = @import("../../../storage/root.zig"); -pub const needs_auth = false; -pub const method = .POST; - const Body = struct { user: []const u8, password: []const u8, @@ -15,11 +12,22 @@ const Result = struct { success: bool, }; -pub fn handler(ctx: *Context, body: Body) anyerror!Result { +pub const access = .everyone; + +pub fn post(ctx: *Context, body: Body) anyerror!Result { var user = Storage.User.open(ctx.storage, body.user, ctx.allocator) catch return .{ .success = false }; defer user.deinit(); - return .{ .success = user.check_password(body.password) }; + const result = Result { + .success = user.check_password(body.password) + }; + + if (result.success) { + const session = try ctx.storage.sessions.add(ctx.storage, user.info); + ctx.response.headers.fingerprint = session.fingerprint; + } + + return result; } diff --git a/src/routes/api/auth/root.zig b/src/routes/api/auth/root.zig index 785271e..5f45891 100644 --- a/src/routes/api/auth/root.zig +++ b/src/routes/api/auth/root.zig @@ -1,4 +1,4 @@ const HandlerInfo = @import("../../handler-info.zig"); pub const login: HandlerInfo = .from_type(@import("login.zig")); -pub const first_login: HandlerInfo = .from_type(@import("first-login.zig")); +pub const @"first-login": HandlerInfo = .from_type(@import("first-login.zig")); diff --git a/src/routes/api/root.zig b/src/routes/api/root.zig index 19c4074..e3c5d6f 100644 --- a/src/routes/api/root.zig +++ b/src/routes/api/root.zig @@ -1 +1,2 @@ pub const auth = @import("auth/root.zig"); +pub const session = @import("session/root.zig"); diff --git a/src/routes/api/session/current.zig b/src/routes/api/session/current.zig new file mode 100644 index 0000000..31c8476 --- /dev/null +++ b/src/routes/api/session/current.zig @@ -0,0 +1,23 @@ +const std = @import("std"); +const Context = @import("../../context.zig"); + +pub const access = .users; + +const Result = struct { + name: []const u8, + full_name: []const u8, + birthday: []const u8, +}; + +pub fn get(ctx: *Context) !Result { + const session = ctx.storage.sessions.get( + ctx.storage, + ctx.fingerprint, + ) orelse return error.UserDoesNotExist; + + return .{ + .name = session.info.name, + .full_name = session.info.full_name, + .birthday = session.info.birthday, + }; +} diff --git a/src/routes/api/session/drop.zig b/src/routes/api/session/drop.zig new file mode 100644 index 0000000..eacf8ce --- /dev/null +++ b/src/routes/api/session/drop.zig @@ -0,0 +1,8 @@ +const Context = @import("../../context.zig"); + +pub const access = .users; + +pub fn get(ctx: *Context) !void { + ctx.storage.sessions.remove(ctx.storage, ctx.fingerprint); + ctx.response.headers.fingerprint = ""; +} diff --git a/src/routes/api/session/renew.zig b/src/routes/api/session/renew.zig new file mode 100644 index 0000000..7dfa491 --- /dev/null +++ b/src/routes/api/session/renew.zig @@ -0,0 +1,8 @@ +const Context = @import("../../context.zig"); + +pub const access = .users; + +pub fn get(ctx: *Context) !void { + const new = try ctx.storage.sessions.renew(ctx.storage, ctx.fingerprint); + ctx.response.headers.fingerprint = new.fingerprint; +} diff --git a/src/routes/api/session/root.zig b/src/routes/api/session/root.zig new file mode 100644 index 0000000..ea155e5 --- /dev/null +++ b/src/routes/api/session/root.zig @@ -0,0 +1,5 @@ +const HandlerInfo = @import("../../handler-info.zig"); + +pub const current: HandlerInfo = .from_type(@import("current.zig")); +pub const drop: HandlerInfo = .from_type(@import("renew.zig")); +pub const renew: HandlerInfo = .from_type(@import("drop.zig")); diff --git a/src/routes/context.zig b/src/routes/context.zig index bc3e710..7c0ca4e 100644 --- a/src/routes/context.zig +++ b/src/routes/context.zig @@ -6,9 +6,11 @@ const Self = @This(); allocator: std.mem.Allocator, request: *std.http.Server.Request, storage: *Storage, +fingerprint: []const u8, user: ?[]const u8 = null, response: struct { headers: struct { content_type: []const u8 = "application/json", + fingerprint: ?[]const u8 = null, } = .{}, } = .{}, diff --git a/src/routes/handler-info.zig b/src/routes/handler-info.zig index bac7612..7b744d2 100644 --- a/src/routes/handler-info.zig +++ b/src/routes/handler-info.zig @@ -2,24 +2,65 @@ const std = @import("std"); const Context = @import("context.zig"); const Storage = @import("../storage/root.zig"); +const Access = @import("access.zig").Access; + const log = std.log.scoped(.handler_info); -handler: *const fn (*Context) anyerror![]const u8, -needs_auth: bool, -method: std.http.Method, +const Self = @This(); + +const Handler = *const fn (*Context) anyerror![]const u8; + +get: ?Handler, +head: ?Handler, +post: ?Handler, +put: ?Handler, +delete: ?Handler, +connect: ?Handler, +options: ?Handler, +trace: ?Handler, +patch: ?Handler, +access: Access, + +inline fn handler_from_method(self: *const Self, method: std.http.Method) ?Handler { + return switch (method) { + .GET => self.get, + .HEAD => self.head, + .POST => self.post, + .PUT => self.put, + .DELETE => self.delete, + .CONNECT => self.connect, + .OPTIONS => self.options, + .TRACE => self.trace, + .PATCH => self.patch, + }; +} + +inline fn get_fingerprint(cookie: []const u8) []const u8 { + const start = std.mem.indexOf(u8, cookie, "fingerprint=") orelse return ""; + const end = std.mem.indexOf(u8, cookie[start + 12..], " ") orelse return cookie[start + 12..]; + return cookie[start..end + 1]; +} pub fn handle( - self: *const @This(), + self: *const Self, request: *std.http.Server.Request, storage: *Storage, allocator: std.mem.Allocator, ) !void { - if (request.head.method != self.method) { - return request.respond( - "{ \"error\": \"Bad Request\" }", - .{ .status = .bad_request } - ); - } + const handler = self.handler_from_method(request.head.method) orelse return request.respond( + "{ \"error\": \"Bad Request\" }", + .{ .status = .bad_request } + ); + + const cookie = cookie: { + var iterator = request.iterateHeaders(); + while (iterator.next()) |header| { + if (std.ascii.eqlIgnoreCase(header.name, "cookie")) { + break :cookie header.value; + } + } + break :cookie ""; + }; var arena = std.heap.ArenaAllocator.init(allocator); defer arena.deinit(); @@ -27,9 +68,28 @@ pub fn handle( .request = request, .storage = storage, .allocator = arena.allocator(), + .fingerprint = get_fingerprint(cookie), }; - const response = self.handler(&context) catch |err| { + const allowed = switch (self.access) { + .everyone => true, + .users => storage.sessions.get(storage, context.fingerprint) != null, + .admins => admin: { + if (storage.sessions.get(storage, context.fingerprint)) |session| { + break :admin session.info.is_admin; + } + break :admin false; + }, + }; + + if (!allowed) { + return request.respond( + "{ \"error\": \"Forbidden\" }", + .{ .status = .forbidden } + ); + } + + const response = handler(&context) catch |err| { const response, const status_code: std.http.Status = switch (err) { error.BadRequest => .{ "{ \"error\": \"Bad Request\" }", .bad_request }, error.Unauthorized => .{ "{ \"error\": \"Unauthorized\" }", .unauthorized }, @@ -44,70 +104,91 @@ pub fn handle( return request.respond(response, .{ .status = status_code }); }; + var headers: std.ArrayList(std.http.Header) = .empty; + defer headers.deinit(allocator); + + try headers.append(allocator, .{ + .name = "Content-Type", + .value = context.response.headers.content_type + }); + + if (context.response.headers.fingerprint) |auth_token| { + var value = std.Io.Writer.Allocating.init(arena.allocator()); + + try value.writer.print("fingerprint={s}; Secure; Path=/", .{auth_token}); + + try headers.append(allocator, .{ + .name = "Set-Cookie", + .value = value.written(), + }); + } + try request.respond(response, .{ - .extra_headers = &.{ - .{ - .name = "Content-Type", - .value = context.response.headers.content_type - }, - }, + .extra_headers = headers.items }); } -pub fn from_type(T: type) @This() { - const info = @typeInfo(@TypeOf(T.handler)); +fn HandlerWrapper(T: type, name: []const u8) type { + const info = @typeInfo(@TypeOf(@field(T, name))); const return_type = info.@"fn".return_type orelse void; const payload_type = @typeInfo(return_type).error_union.payload; - const Handler = type: { - break :type struct { - pub fn call(ctx: *Context) anyerror![]const u8 { - const args = args: { - const tuple = std.meta.fields(std.meta.ArgsTuple(@TypeOf(T.handler))); + return struct { + pub fn call(ctx: *Context) anyerror![]const u8 { + const args = args: { + const tuple = std.meta.fields(std.meta.ArgsTuple(@TypeOf(@field(T, name)))); - if (tuple.len == 1) { - break :args .{ ctx }; - } else if (tuple.len == 2) { - const Body = tuple[1].@"type"; + if (tuple.len == 1) { + break :args .{ ctx }; + } else if (tuple.len == 2) { + const Body = tuple[1].@"type"; - var writer = std.Io.Writer.Allocating.init(ctx.allocator); - const interface = &writer.writer; + var writer = std.Io.Writer.Allocating.init(ctx.allocator); + const interface = &writer.writer; - var buffer: [1024]u8 = undefined; - const reader = try ctx.request.readerExpectContinue(&buffer); + var buffer: [1024]u8 = undefined; + const reader = try ctx.request.readerExpectContinue(&buffer); - try reader.streamExact64(interface, ctx.request.head.content_length orelse 0); + try reader.streamExact64(interface, ctx.request.head.content_length orelse 0); - const body = std.json.parseFromSliceLeaky( - Body, - ctx.allocator, - writer.written(), - .{} - ) catch return error.BadRequest; - break :args .{ ctx, body }; - } else { - @compileError("invalid amount of arguments for request function"); - } - }; - - if (payload_type == []const u8) { - return @call(.auto, T.handler, args); - } else if (payload_type == void) { - try @call(.auto, T.handler, args); - return ""; - } else { - var writer = std.Io.Writer.Allocating.init(ctx.allocator); - var stringify = std.json.Stringify { .writer = &writer.writer }; - try stringify.write(try @call(.auto, T.handler, args)); - return writer.written(); - } + const body = std.json.parseFromSliceLeaky( + Body, + ctx.allocator, + writer.written(), + .{} + ) catch return error.BadRequest; + break :args .{ ctx, body }; + } else { + @compileError("invalid amount of arguments for request function"); } }; + + if (payload_type == []const u8) { + return @call(.auto, @field(T, name), args); + } else if (payload_type == void) { + try @call(.auto, @field(T, name), args); + return ""; + } else { + var writer = std.Io.Writer.Allocating.init(ctx.allocator); + var stringify = std.json.Stringify { .writer = &writer.writer }; + try stringify.write(try @call(.auto, @field(T, name), args)); + return writer.written(); + } + } }; +} - return @This() { - .handler = Handler.call, - .needs_auth = T.needs_auth, - .method = T.method, +pub fn from_type(T: type) @This() { + return Self { + .get = if (@hasDecl(T, "get")) HandlerWrapper(T, "get").call else null, + .head = if (@hasDecl(T, "head")) HandlerWrapper(T, "head").call else null, + .post = if (@hasDecl(T, "post")) HandlerWrapper(T, "post").call else null, + .put = if (@hasDecl(T, "put")) HandlerWrapper(T, "put").call else null, + .delete = if (@hasDecl(T, "delete")) HandlerWrapper(T, "delete").call else null, + .connect = if (@hasDecl(T, "connect")) HandlerWrapper(T, "connect").call else null, + .options = if (@hasDecl(T, "options")) HandlerWrapper(T, "options").call else null, + .trace = if (@hasDecl(T, "trace")) HandlerWrapper(T, "trace").call else null, + .patch = if (@hasDecl(T, "patch")) HandlerWrapper(T, "patch").call else null, + .access = T.access, }; } diff --git a/src/routes/root.zig b/src/routes/root.zig index dbfce32..878b9f2 100644 --- a/src/routes/root.zig +++ b/src/routes/root.zig @@ -4,14 +4,47 @@ pub const HandlerInfo = @import("handler-info.zig"); pub const Context = @import("context.zig"); pub const api = @import("api/root.zig"); -pub const fallback: HandlerInfo = .from_type(@import("fallback.zig")); +pub const static: HandlerInfo = .from_type(@import("static.zig")); -pub const handlers = std.StaticStringMap(HandlerInfo).initComptime(.{ - .{ "", fallback }, - .{ "/api/auth/login", api.auth.login }, - .{ "/api/auth/first-login", api.auth.first_login }, -}); +const routes = (Routes {}) + .with("", static) + .with_module("/api", api); + +pub const handlers = std.StaticStringMap(HandlerInfo).initComptime(routes.items); pub fn get(path: []const u8) HandlerInfo { return (handlers.getLongestPrefix(std.mem.trimEnd(u8, path, "/")) orelse unreachable).value; } + +const Routes = struct { + const Self = @This(); + + const Route = struct{ []const u8, HandlerInfo }; + + items: []const Route = &[0]Route{}, + + pub fn with(self: Self, comptime route: []const u8, handler: HandlerInfo) Self { + var next = self; + const tail: []const Route = &[1]Route { .{ route, handler } }; + next.items = self.items ++ tail; + return next; + } + + pub fn with_module(self: Self, comptime prefix: []const u8, module: type) Self { + var next = self; + + if (@typeInfo(module) != .@"struct") return self; + + inline for (@typeInfo(module).@"struct".decls) |decl| { + const field = @field(module, decl.name); + const route = prefix ++ "/" ++ decl.name; + switch (@TypeOf(field)) { + type => next = next.with_module(route, field), + HandlerInfo => next = next.with(route, field), + else => {}, + } + } + + return next; + } +}; diff --git a/src/routes/fallback.zig b/src/routes/static.zig index 25e1927..8719cc9 100644 --- a/src/routes/fallback.zig +++ b/src/routes/static.zig @@ -4,10 +4,9 @@ const Context = @import("context.zig"); const log = std.log.scoped(.fallback); -pub const needs_auth = false; -pub const method = .GET; +pub const access = .everyone; -pub fn handler(ctx: *Context) anyerror![]const u8 { +pub fn get(ctx: *Context) anyerror![]const u8 { var static = try std.fs.cwd().openDir("static", .{}); defer static.close(); |