From 35144b195ee6b99c46026f4508085cd1f9d47baa Mon Sep 17 00:00:00 2001 From: Rene Schallner Date: Sat, 20 May 2023 03:16:02 +0200 Subject: [PATCH] zap.Middleware.EndpointHandler, r.get/setContext() --- README.md | 2 + build.zig | 1 + build.zig.zon | 2 +- examples/middleware/middleware.zig | 2 + .../middleware_with_endpoint.zig | 246 ++++++++++++++++++ src/fio.zig | 4 +- src/middleware.zig | 36 +++ src/zap.zig | 66 +++++ targets.txt | 1 + 9 files changed, 358 insertions(+), 2 deletions(-) create mode 100644 examples/middleware_with_endpoint/middleware_with_endpoint.zig diff --git a/README.md b/README.md index 50f985b..532186c 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,8 @@ Here's what works: request handlers in middleware style. Provide custom context structs, totally type-safe, using **[ZIG-CEPTION](doc/zig-ception.md)**. If you come from GO this might appeal to you. +- **[MIDDLEWARE with endpoint support](examples/middleware_with_endpoint/middleware_with_endpoint.zig)**: Same as the example above, but this time we use an endpoint at the end of the chain, by wrapping it via `zap.Middleware.EndpointHandler`. Mixing endpoints in your middleware chain allows for usage of Zap's authenticated endpoints and your custom endpoints. Since Endpoints use a simpler API, you have to use `r.setUserContext()` and `r.getUserContext()` with the request if you want to access the middleware context from a wrapped endpoint. Since this mechanism uses an `*anyopaque` pointer underneath (to not break the Endpoint API), it is less type-safe than `zap.Middleware`'s use of contexts. +- **Per Request Contexts** : With the introduction of `setContext()` and `getContext()`, you can, of course use those two in projects that don't use `zap.SimpleEndpoint` or `zap.Middleware`, too, if you really, really, absolutely don't find another way to solve your context problem. **We recommend using a `zap.SimpleEndpoint`** inside of a struct that can provide all the context you need **instead**. You get access to your struct in the callbacks via the `@fieldParentPtr()` trick that is used extensively in Zap's examples, like the [endpoint example](examples/endpoint/endpoint.zig). I'll continue wrapping more of facil.io's functionality and adding stuff to zap diff --git a/build.zig b/build.zig index 311d287..7e7e033 100644 --- a/build.zig +++ b/build.zig @@ -56,6 +56,7 @@ pub fn build(b: *std.build.Builder) !void { .{ .name = "userpass_session", .src = "examples/userpass_session_auth/userpass_session_auth.zig" }, .{ .name = "sendfile", .src = "examples/sendfile/sendfile.zig" }, .{ .name = "middleware", .src = "examples/middleware/middleware.zig" }, + .{ .name = "middleware_with_endpoint", .src = "examples/middleware_with_endpoint/middleware_with_endpoint.zig" }, }) |excfg| { const ex_name = excfg.name; const ex_src = excfg.src; diff --git a/build.zig.zon b/build.zig.zon index ba26f6e..f0a9b56 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -1,6 +1,6 @@ .{ .name = "zap", - .version = "0.0.22", + .version = "0.0.23", .dependencies = .{ .@"facil.io" = .{ diff --git a/examples/middleware/middleware.zig b/examples/middleware/middleware.zig index 85f0878..72d4d1b 100644 --- a/examples/middleware/middleware.zig +++ b/examples/middleware/middleware.zig @@ -164,6 +164,7 @@ const HtmlMiddleWare = struct { if (context.session) |session| { sessionFound = true; + std.debug.assert(r.isFinished() == false); const message = std.fmt.bufPrint(&buf, "User: {s} / {s}, Session: {s} / {s}", .{ user.name, user.email, @@ -172,6 +173,7 @@ const HtmlMiddleWare = struct { }) catch unreachable; r.setContentType(.TEXT) catch unreachable; r.sendBody(message) catch unreachable; + std.debug.assert(r.isFinished() == true); return true; } } diff --git a/examples/middleware_with_endpoint/middleware_with_endpoint.zig b/examples/middleware_with_endpoint/middleware_with_endpoint.zig new file mode 100644 index 0000000..f22397f --- /dev/null +++ b/examples/middleware_with_endpoint/middleware_with_endpoint.zig @@ -0,0 +1,246 @@ +const std = @import("std"); +const zap = @import("zap"); + +// just a way to share our allocator via callback +const SharedAllocator = struct { + // static + var allocator: std.mem.Allocator = undefined; + + const Self = @This(); + + // just a convenience function + pub fn init(a: std.mem.Allocator) void { + allocator = a; + } + + // static function we can pass to the listener later + pub fn getAllocator() std.mem.Allocator { + return allocator; + } +}; + +// create a combined context struct +const Context = zap.Middleware.MixContexts(.{ + .{ .name = "?user", .type = UserMiddleWare.User }, + .{ .name = "?session", .type = SessionMiddleWare.Session }, +}); + +// we create a Handler type based on our Context +const Handler = zap.Middleware.Handler(Context); + +// +// Example user middleware: puts user info into the context +// +const UserMiddleWare = struct { + handler: Handler, + + const Self = @This(); + + // Just some arbitrary struct we want in the per-request context + // This is so that it can be constructed via .{} + // as we can't expect the listener to know how to initialize our context structs + const User = struct { + name: []const u8 = undefined, + email: []const u8 = undefined, + }; + + pub fn init(other: ?*Handler) Self { + return .{ + .handler = Handler.init(onRequest, other), + }; + } + + // we need the handler as a common interface to chain stuff + pub fn getHandler(self: *Self) *Handler { + return &self.handler; + } + + // note that the first parameter is of type *Handler, not *Self !!! + pub fn onRequest(handler: *Handler, r: zap.SimpleRequest, context: *Context) bool { + + // this is how we would get our self pointer + var self = @fieldParentPtr(Self, "handler", handler); + _ = self; + + // do our work: fill in the user field of the context + context.user = User{ + .name = "renerocksai", + .email = "supa@secret.org", + }; + + std.debug.print("\n\nUser Middleware: set user in context {any}\n\n", .{context.user}); + + // continue in the chain + return handler.handleOther(r, context); + } +}; + +// +// Example session middleware: puts session info into the context +// +const SessionMiddleWare = struct { + handler: Handler, + + const Self = @This(); + + // Just some arbitrary struct we want in the per-request context + // note: it MUST have all default values!!! + const Session = struct { + info: []const u8 = undefined, + token: []const u8 = undefined, + }; + + pub fn init(other: ?*Handler) Self { + return .{ + .handler = Handler.init(onRequest, other), + }; + } + + // we need the handler as a common interface to chain stuff + pub fn getHandler(self: *Self) *Handler { + return &self.handler; + } + + // note that the first parameter is of type *Handler, not *Self !!! + pub fn onRequest(handler: *Handler, r: zap.SimpleRequest, context: *Context) bool { + // this is how we would get our self pointer + var self = @fieldParentPtr(Self, "handler", handler); + _ = self; + + context.session = Session{ + .info = "secret session", + .token = "rot47-asdlkfjsaklfdj", + }; + + std.debug.print("\n\nSessionMiddleware: set session in context {any}\n\n", .{context.session}); + + // continue in the chain + return handler.handleOther(r, context); + } +}; + +// +// !!!! ENDPOINT !!! +// +// We define an endpoint as we usually would. +// NO ROUTING IS PERFORMED +// as we are just going to wrap it in a bunch of Middleware components +// and therefore NOT using an endpoint listener that would the routing for us +// +// Hence, the endpoint should check r.path in its on_request to check wether +// it is adressed or not. +// +// N.B. the EndpointHandler checks if the endpoint turned the request into +// "finished" state, e.g. by sending anything. If the endpoint didn't finish the +// request, the EndpointHandler will pass the request on to the next handler in +// the chain if there is one. See also the EndpointHandler's `breakOnFinish` +// parameter. +// +const HtmlEndpoint = struct { + endpoint: zap.SimpleEndpoint = undefined, + const Self = @This(); + + pub fn init() Self { + return .{ + .endpoint = zap.SimpleEndpoint.init(.{ + .path = "/doesn'tmatter", + .get = get, + }), + }; + } + + pub fn getEndpoint(self: *Self) *zap.SimpleEndpoint { + return &self.endpoint; + } + + pub fn get(ep: *zap.SimpleEndpoint, r: zap.SimpleRequest) void { + var self = @fieldParentPtr(Self, "endpoint", ep); + _ = self; + + var buf: [1024]u8 = undefined; + var userFound: bool = false; + var sessionFound: bool = false; + + // the EndpointHandler set this for us! + // we got middleware context!!! + const maybe_context: ?*Context = r.getUserContext(Context); + if (maybe_context) |context| { + std.debug.print("\n\nHtmlEndpoint: handling request with context: {any}\n\n", .{context}); + + if (context.user) |user| { + userFound = true; + if (context.session) |session| { + sessionFound = true; + + std.debug.assert(r.isFinished() == false); + const message = std.fmt.bufPrint(&buf, "User: {s} / {s}, Session: {s} / {s}", .{ + user.name, + user.email, + session.info, + session.token, + }) catch unreachable; + r.setContentType(.TEXT) catch unreachable; + r.sendBody(message) catch unreachable; + std.debug.assert(r.isFinished() == true); + return; + } + } + } + + const message = std.fmt.bufPrint(&buf, "User info found: {}, session info found: {}", .{ userFound, sessionFound }) catch unreachable; + + r.setContentType(.TEXT) catch unreachable; + r.sendBody(message) catch unreachable; + return; + } +}; + +pub fn main() !void { + var gpa = std.heap.GeneralPurposeAllocator(.{ + .thread_safe = true, + }){}; + var allocator = gpa.allocator(); + SharedAllocator.init(allocator); + + // create the endpoint + var htmlEndpoint = HtmlEndpoint.init(); + + // we wrap the endpoint with a middleware handler + var htmlHandler = zap.Middleware.EndpointHandler(Handler, Context).init( + htmlEndpoint.getEndpoint(), // the endpoint + null, // no other handler (we are the last in the chain) + true, // break on finish. See EndpointHandler for this. Not applicable here. + ); + + // we wrap it in the session Middleware component + var sessionHandler = SessionMiddleWare.init(htmlHandler.getHandler()); + + // we wrap that in the user Middleware component + var userHandler = UserMiddleWare.init(sessionHandler.getHandler()); + + // we create a listener with our combined context + // and pass it the initial handler: the user handler + var listener = try zap.Middleware.Listener(Context).init( + .{ + .on_request = null, // must be null + .port = 3000, + .log = true, + .max_clients = 100000, + }, + userHandler.getHandler(), + SharedAllocator.getAllocator, + ); + zap.enableDebugLog(); + listener.listen() catch |err| { + std.debug.print("\nLISTEN ERROR: {any}\n", .{err}); + return; + }; + + std.debug.print("Visit me on http://127.0.0.1:3000\n", .{}); + + // start worker threads + zap.start(.{ + .threads = 2, + .workers = 1, + }); +} diff --git a/src/fio.zig b/src/fio.zig index 22abfa3..cfacea5 100644 --- a/src/fio.zig +++ b/src/fio.zig @@ -66,7 +66,9 @@ pub const http_s = extern struct { params: FIOBJ, body: FIOBJ, udata: ?*anyopaque, -}; // zig-cache/i/e0c8a6e617497ade13de512cbe191f23/include/http.h:153:12: warning: struct demoted to opaque type - has bitfield +}; + +// zig-cache/i/e0c8a6e617497ade13de512cbe191f23/include/http.h:153:12: warning: struct demoted to opaque type - has bitfield // typedef struct { // /** The cookie's name (Symbol). */ diff --git a/src/middleware.zig b/src/middleware.zig index 9e54b1c..81c498e 100644 --- a/src/middleware.zig +++ b/src/middleware.zig @@ -80,6 +80,42 @@ pub fn Handler(comptime ContextType: anytype) type { }; } +/// A convenience handler for artibrary zap.SimpleEndpoint +pub fn EndpointHandler(comptime HandlerType: anytype, comptime ContextType: anytype) type { + return struct { + handler: HandlerType, + endpoint: *zap.SimpleEndpoint, + breakOnFinish: bool, + + const Self = @This(); + + pub fn init(endpoint: *zap.SimpleEndpoint, other: ?*HandlerType, breakOnFinish: bool) Self { + return .{ + .handler = HandlerType.init(onRequest, other), + .endpoint = endpoint, + .breakOnFinish = breakOnFinish, + }; + } + + // we need the handler as a common interface to chain stuff + pub fn getHandler(self: *Self) *HandlerType { + return &self.handler; + } + + pub fn onRequest(handler: *HandlerType, r: zap.SimpleRequest, context: *ContextType) bool { + var self = @fieldParentPtr(Self, "handler", handler); + r.setUserContext(context); + self.endpoint.onRequest(r); + + // if the request was handled by the endpoint, we may break the chain here + if (r.isFinished() and self.breakOnFinish) { + return true; + } + return self.handler.handleOther(r, context); + } + }; +} + pub const Error = error{ InitOnRequestIsNotNull, }; diff --git a/src/zap.zig b/src/zap.zig index 4d33107..d109020 100644 --- a/src/zap.zig +++ b/src/zap.zig @@ -69,8 +69,48 @@ pub const SimpleRequest = struct { method: ?[]const u8, h: [*c]fio.http_s, + /// NEVER touch this field!!!! + /// if you absolutely MUST, then you may provide context here + /// via setUserContext and getUserContext + _user_context: *UserContext, + /// NEVER touch this field!!!! + /// use markAsFinished() and isFinished() instead + /// this is a hack: the listener will put a pointer to this into the udata + /// field of `h`. So copies of the SimpleRequest will all have way to the + /// same instance of this field. + _is_finished_request_global: bool, + /// NEVER touch this field!!!! + /// this is part of the hack. + _is_finished: *bool = undefined, + + const UserContext = struct { + user_context: ?*anyopaque = null, + }; + const Self = @This(); + pub fn markAsFinished(self: *const Self, finished: bool) void { + // we might be a copy + self._is_finished.* = finished; + } + + pub fn isFinished(self: *const Self) bool { + // we might be a copy + return self._is_finished.*; + } + + pub fn setUserContext(self: *const Self, context: *anyopaque) void { + self._user_context.*.user_context = context; + } + + pub fn getUserContext(self: *const Self, comptime Context: type) ?*Context { + if (self._user_context.*.user_context) |ptr| { + return @ptrCast(*Context, @alignCast(@alignOf(*Context), ptr)); + } else { + return null; + } + } + pub fn sendBody(self: *const Self, body: []const u8) HttpError!void { const ret = fio.http_send_body(self.h, @intToPtr( *anyopaque, @@ -78,6 +118,7 @@ pub const SimpleRequest = struct { ), body.len); debug("SimpleRequest.sendBody(): ret = {}\n", .{ret}); if (ret == -1) return error.HttpSendBody; + self.markAsFinished(true); } pub fn sendJson(self: *const Self, json: []const u8) HttpError!void { @@ -86,6 +127,7 @@ pub const SimpleRequest = struct { *anyopaque, @ptrToInt(json.ptr), ), json.len) != 0) return error.HttpSendBody; + self.markAsFinished(true); } else |err| return err; } @@ -104,6 +146,7 @@ pub const SimpleRequest = struct { self.setStatus(if (code) |status| status else .found); try self.setHeader("Location", path); try self.sendBody("moved"); + self.markAsFinished(true); } /// shows how to use the logger @@ -187,6 +230,7 @@ pub const SimpleRequest = struct { pub fn sendFile(self: *const Self, file_path: []const u8) !void { if (fio.http_sendfile2(self.h, util.toCharPtr(file_path), file_path.len, null, 0) != 0) return error.SendFile; + self.markAsFinished(true); } /// Attempts to decode the request's body. @@ -575,7 +619,15 @@ pub const SimpleHttpListener = struct { .body = util.fio2str(r.*.body), .method = util.fio2str(r.*.method), .h = r, + ._is_finished_request_global = false, + ._user_context = undefined, }; + req._is_finished = &req._is_finished_request_global; + + var user_context: SimpleRequest.UserContext = .{}; + req._user_context = &user_context; + + req.markAsFinished(false); std.debug.assert(l.settings.on_request != null); if (l.settings.on_request) |on_request| { // l.settings.on_request.?(req); @@ -592,7 +644,14 @@ pub const SimpleHttpListener = struct { .body = util.fio2str(r.*.body), .method = util.fio2str(r.*.method), .h = r, + ._is_finished_request_global = false, + ._user_context = undefined, }; + req._is_finished = &req._is_finished_request_global; + + var user_context: SimpleRequest.UserContext = .{}; + req._user_context = &user_context; + l.settings.on_response.?(req); } } @@ -605,8 +664,15 @@ pub const SimpleHttpListener = struct { .body = util.fio2str(r.*.body), .method = util.fio2str(r.*.method), .h = r, + ._is_finished_request_global = false, + ._user_context = undefined, }; var zigtarget: []u8 = target[0..target_len]; + req._is_finished = &req._is_finished_request_global; + + var user_context: SimpleRequest.UserContext = .{}; + req._user_context = &user_context; + l.settings.on_upgrade.?(req, zigtarget); } } diff --git a/targets.txt b/targets.txt index 42fe186..6330cf8 100644 --- a/targets.txt +++ b/targets.txt @@ -15,3 +15,4 @@ websockets userpass_session sendfile middleware +middleware_with_endpoint