From b8ca82f0fd4b448d773f859593b1dd146c638878 Mon Sep 17 00:00:00 2001 From: renerocksai Date: Sun, 30 Mar 2025 13:09:36 +0200 Subject: [PATCH] zap.App.Create(Context).Endpoint.Authenticating -> zap.App is READY. --- build.zig | 3 +- examples/app/auth.zig | 119 +++++++++++++++++++++++++++ examples/app/{main.zig => basic.zig} | 0 src/App.zig | 82 +++++++++++++++++- 4 files changed, 201 insertions(+), 3 deletions(-) create mode 100644 examples/app/auth.zig rename examples/app/{main.zig => basic.zig} (100%) diff --git a/build.zig b/build.zig index 1629048..b13002a 100644 --- a/build.zig +++ b/build.zig @@ -50,7 +50,8 @@ pub fn build(b: *std.Build) !void { name: []const u8, src: []const u8, }{ - .{ .name = "app", .src = "examples/app/main.zig" }, + .{ .name = "app_basic", .src = "examples/app/basic.zig" }, + .{ .name = "app_auth", .src = "examples/app/auth.zig" }, .{ .name = "hello", .src = "examples/hello/hello.zig" }, .{ .name = "https", .src = "examples/https/https.zig" }, .{ .name = "hello2", .src = "examples/hello2/hello2.zig" }, diff --git a/examples/app/auth.zig b/examples/app/auth.zig new file mode 100644 index 0000000..ef73219 --- /dev/null +++ b/examples/app/auth.zig @@ -0,0 +1,119 @@ +const std = @import("std"); +const zap = @import("zap"); + +const Allocator = std.mem.Allocator; + +// The "Application Context" +const MyContext = struct { + bearer_token: []const u8, +}; + +// We reply with this +const HTTP_RESPONSE_TEMPLATE: []const u8 = + \\ + \\ {s} from ZAP on {s} (token {s} == {s} : {s})!!! + \\ + \\ +; + +// Our simple endpoint that will be wrapped by the authenticator +const MyEndpoint = struct { + // the slug + path: []const u8, + error_strategy: zap.Endpoint.ErrorStrategy = .log_to_response, + + fn get_bearer_token(r: zap.Request) []const u8 { + const auth_header = zap.Auth.extractAuthHeader(.Bearer, &r) orelse "Bearer (no token)"; + return auth_header[zap.Auth.AuthScheme.Bearer.str().len..]; + } + + // authenticated GET requests go here + // we use the endpoint, the context, the arena, and try + pub fn get(ep: *MyEndpoint, arena: Allocator, context: *MyContext, r: zap.Request) !void { + const used_token = get_bearer_token(r); + const response = try std.fmt.allocPrint( + arena, + HTTP_RESPONSE_TEMPLATE, + .{ "Hello", ep.path, used_token, context.bearer_token, "OK" }, + ); + r.setStatus(.ok); + try r.sendBody(response); + } + + // we also catch the unauthorized callback + // we use the endpoint, the context, the arena, and try + pub fn unauthorized(ep: *MyEndpoint, arena: Allocator, context: *MyContext, r: zap.Request) !void { + r.setStatus(.unauthorized); + const used_token = get_bearer_token(r); + const response = try std.fmt.allocPrint( + arena, + HTTP_RESPONSE_TEMPLATE, + .{ "UNAUTHORIZED", ep.path, used_token, context.bearer_token, "NOT OK" }, + ); + try r.sendBody(response); + } + + // not implemented, don't care + pub fn post(_: *MyEndpoint, _: Allocator, _: *MyContext, _: zap.Request) !void {} + pub fn put(_: *MyEndpoint, _: Allocator, _: *MyContext, _: zap.Request) !void {} + pub fn delete(_: *MyEndpoint, _: Allocator, _: *MyContext, _: zap.Request) !void {} + pub fn patch(_: *MyEndpoint, _: Allocator, _: *MyContext, _: zap.Request) !void {} + pub fn options(_: *MyEndpoint, _: Allocator, _: *MyContext, _: zap.Request) !void {} +}; + +pub fn main() !void { + var gpa: std.heap.GeneralPurposeAllocator(.{ + // just to be explicit + .thread_safe = true, + }) = .{}; + defer std.debug.print("\n\nLeaks detected: {}\n\n", .{gpa.deinit() != .ok}); + const allocator = gpa.allocator(); + + // our global app context + var my_context: MyContext = .{ .bearer_token = "ABCDEFG" }; // ABCDEFG is our Bearer token + + // our global app that holds the context + // App is the type + // app is the instance + const App = zap.App.Create(MyContext); + var app = try App.init(allocator, &my_context, .{}); + defer app.deinit(); + + // create mini endpoint + var ep: MyEndpoint = .{ + .path = "/test", + }; + + // create authenticator, use token from context + const Authenticator = zap.Auth.BearerSingle; // Simple Authenticator that uses a single bearer token + var authenticator = try Authenticator.init(allocator, my_context.bearer_token, null); + defer authenticator.deinit(); + + // create authenticating endpoint by combining endpoint and authenticator + const BearerAuthEndpoint = App.Endpoint.Authenticating(MyEndpoint, Authenticator); + var auth_ep = BearerAuthEndpoint.init(&ep, &authenticator); + + // make the authenticating endpoint known to the app + try app.register(&auth_ep); + + // listen + try app.listen(.{ + .interface = "0.0.0.0", + .port = 3000, + }); + std.debug.print( + \\ Run the following: + \\ + \\ curl http://localhost:3000/test -i -H "Authorization: Bearer ABCDEFG" -v + \\ curl http://localhost:3000/test -i -H "Authorization: Bearer invalid" -v + \\ + \\ and see what happens + \\ + , .{}); + + // start worker threads + zap.start(.{ + .threads = 2, + .workers = 1, + }); +} diff --git a/examples/app/main.zig b/examples/app/basic.zig similarity index 100% rename from examples/app/main.zig rename to examples/app/basic.zig diff --git a/src/App.zig b/src/App.zig index ad0740f..4699b77 100644 --- a/src/App.zig +++ b/src/App.zig @@ -14,10 +14,11 @@ const RwLock = Thread.RwLock; const zap = @import("zap.zig"); const Request = zap.Request; const HttpListener = zap.HttpListener; +const ErrorStrategy = zap.Endpoint.ErrorStrategy; pub const AppOpts = struct { /// ErrorStrategy for (optional) request handler if no endpoint matches - default_error_strategy: zap.Endpoint.ErrorStrategy = .log_to_console, + default_error_strategy: ErrorStrategy = .log_to_console, arena_retain_capacity: usize = 16 * 1024 * 1024, }; @@ -137,7 +138,7 @@ pub fn Create(comptime Context: type) type { } if (@hasField(T, "error_strategy")) { - if (@FieldType(T, "error_strategy") != zap.Endpoint.ErrorStrategy) { + if (@FieldType(T, "error_strategy") != ErrorStrategy) { @compileError(@typeName(@FieldType(T, "error_strategy")) ++ " has wrong type, expected: zap.Endpoint.ErrorStrategy"); } } else { @@ -164,6 +165,83 @@ pub fn Create(comptime Context: type) type { } } } + + /// Wrap an endpoint with an Authenticator + pub fn Authenticating(EndpointType: type, Authenticator: type) type { + return struct { + authenticator: *Authenticator, + ep: *EndpointType, + path: []const u8, + error_strategy: ErrorStrategy, + const AuthenticatingEndpoint = @This(); + + /// Init the authenticating endpoint. Pass in a pointer to the endpoint + /// you want to wrap, and the Authenticator that takes care of authenticating + /// requests. + pub fn init(e: *EndpointType, authenticator: *Authenticator) AuthenticatingEndpoint { + return .{ + .authenticator = authenticator, + .ep = e, + .path = e.path, + .error_strategy = e.error_strategy, + }; + } + + /// Authenticates GET requests using the Authenticator. + pub fn get(self: *AuthenticatingEndpoint, arena: Allocator, context: *Context, request: Request) anyerror!void { + try switch (self.authenticator.authenticateRequest(&request)) { + .AuthFailed => return self.ep.*.unauthorized(arena, context, request), + .AuthOK => self.ep.*.get(arena, context, request), + .Handled => {}, + }; + } + + /// Authenticates POST requests using the Authenticator. + pub fn post(self: *AuthenticatingEndpoint, arena: Allocator, context: *Context, request: Request) anyerror!void { + try switch (self.authenticator.authenticateRequest(&request)) { + .AuthFailed => return self.ep.*.unauthorized(arena, context, request), + .AuthOK => self.ep.*.post(arena, context, request), + .Handled => {}, + }; + } + + /// Authenticates PUT requests using the Authenticator. + pub fn put(self: *AuthenticatingEndpoint, arena: Allocator, context: *Context, request: zap.Request) anyerror!void { + try switch (self.authenticator.authenticateRequest(&request)) { + .AuthFailed => return self.ep.*.unauthorized(arena, context, request), + .AuthOK => self.ep.*.put(arena, context, request), + .Handled => {}, + }; + } + + /// Authenticates DELETE requests using the Authenticator. + pub fn delete(self: *AuthenticatingEndpoint, arena: Allocator, context: *Context, request: zap.Request) anyerror!void { + try switch (self.authenticator.authenticateRequest(&request)) { + .AuthFailed => return self.ep.*.unauthorized(arena, context, request), + .AuthOK => self.ep.*.delete(arena, context, request), + .Handled => {}, + }; + } + + /// Authenticates PATCH requests using the Authenticator. + pub fn patch(self: *AuthenticatingEndpoint, arena: Allocator, context: *Context, request: zap.Request) anyerror!void { + try switch (self.authenticator.authenticateRequest(&request)) { + .AuthFailed => return self.ep.*.unauthorized(arena, context, request), + .AuthOK => self.ep.*.patch(arena, context, request), + .Handled => {}, + }; + } + + /// Authenticates OPTIONS requests using the Authenticator. + pub fn options(self: *AuthenticatingEndpoint, arena: Allocator, context: *Context, request: zap.Request) anyerror!void { + try switch (self.authenticator.authenticateRequest(&request)) { + .AuthFailed => return self.ep.*.unauthorized(arena, context, request), + .AuthOK => self.ep.*.put(arena, context, request), + .Handled => {}, + }; + } + }; + } }; pub const ListenerSettings = struct {