From d8b8ffd1d40775235394c42a9706098992b839f2 Mon Sep 17 00:00:00 2001 From: Bob Farrell Date: Mon, 6 May 2024 12:55:51 +0100 Subject: [PATCH 1/6] WIP --- build.zig | 2 + build.zig.zon | 4 + demo/src/app/views/basic.zig | 7 ++ demo/src/app/views/basic/index.zmpl | 3 + demo/src/main.zig | 2 +- src/jetzig.zig | 1 + src/jetzig/App.zig | 4 +- src/jetzig/HttpzServer.zig | 58 +++++++++++++++ src/jetzig/http/Request.zig | 60 +++++++++------ src/jetzig/http/Server.zig | 111 ++++++++++++++++++---------- src/jetzig/http/status_codes.zig | 6 ++ 11 files changed, 193 insertions(+), 65 deletions(-) create mode 100644 demo/src/app/views/basic.zig create mode 100644 demo/src/app/views/basic/index.zmpl create mode 100644 src/jetzig/HttpzServer.zig diff --git a/build.zig b/build.zig index 19456fa..d1a7486 100644 --- a/build.zig +++ b/build.zig @@ -57,6 +57,7 @@ pub fn build(b: *std.Build) !void { const jetkv_dep = b.dependency("jetkv", .{ .target = target, .optimize = optimize }); const zmd_dep = b.dependency("zmd", .{ .target = target, .optimize = optimize }); + const httpz_dep = b.dependency("httpz", .{ .target = target, .optimize = optimize }); // This is the way to make it look nice in the zig build script // If we would do it the other way around, we would have to do @@ -75,6 +76,7 @@ pub fn build(b: *std.Build) !void { jetzig_module.addImport("zmd", zmd_dep.module("zmd")); jetzig_module.addImport("jetkv", jetkv_dep.module("jetkv")); jetzig_module.addImport("smtp", smtp_client_dep.module("smtp_client")); + jetzig_module.addImport("httpz", httpz_dep.module("httpz")); const main_tests = b.addTest(.{ .root_source_file = .{ .path = "src/tests.zig" }, diff --git a/build.zig.zon b/build.zig.zon index 8917be1..7088855 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -22,6 +22,10 @@ .url = "https://github.com/karlseguin/smtp_client.zig/archive/e79e411862d4f4d41657bf41efb884efca3d67dd.tar.gz", .hash = "12209907c69891a38e6923308930ac43bfb40135bc609ea370b5759fc2e1c4f57284", }, + .httpz = .{ + .url = "https://github.com/karlseguin/http.zig/archive/bf748c6508090464213d3088a77d51faf9cd0876.tar.gz", + .hash = "12209b7426293ebe5075b930ae6029c009bfb6deb7ff92b9d69e28463abd14ad03da", + }, }, .paths = .{ diff --git a/demo/src/app/views/basic.zig b/demo/src/app/views/basic.zig new file mode 100644 index 0000000..4fac587 --- /dev/null +++ b/demo/src/app/views/basic.zig @@ -0,0 +1,7 @@ +const std = @import("std"); +const jetzig = @import("jetzig"); + +pub fn index(request: *jetzig.Request, data: *jetzig.Data) !jetzig.View { + _ = data; + return request.render(.ok); +} diff --git a/demo/src/app/views/basic/index.zmpl b/demo/src/app/views/basic/index.zmpl new file mode 100644 index 0000000..76457d0 --- /dev/null +++ b/demo/src/app/views/basic/index.zmpl @@ -0,0 +1,3 @@ +
+ Content goes here +
diff --git a/demo/src/main.zig b/demo/src/main.zig index 08213d1..691477e 100644 --- a/demo/src/main.zig +++ b/demo/src/main.zig @@ -15,7 +15,7 @@ pub const jetzig_options = struct { jetzig.middleware.HtmxMiddleware, // Demo middleware included with new projects. Remove once you are familiar with Jetzig's // middleware system. - @import("app/middleware/DemoMiddleware.zig"), + // @import("app/middleware/DemoMiddleware.zig"), }; // Maximum bytes to allow in request body. diff --git a/src/jetzig.zig b/src/jetzig.zig index 4d18546..3a72e6b 100644 --- a/src/jetzig.zig +++ b/src/jetzig.zig @@ -16,6 +16,7 @@ pub const markdown = @import("jetzig/markdown.zig"); pub const jobs = @import("jetzig/jobs.zig"); pub const mail = @import("jetzig/mail.zig"); pub const kv = @import("jetzig/kv.zig"); +pub const HttpzServer = @import("jetzig/HttpzServer.zig"); /// The primary interface for a Jetzig application. Create an `App` in your application's /// `src/main.zig` and call `start` to launch the application. diff --git a/src/jetzig/App.zig b/src/jetzig/App.zig index 59af1df..ed0c9a0 100644 --- a/src/jetzig/App.zig +++ b/src/jetzig/App.zig @@ -94,6 +94,9 @@ pub fn start(self: App, routes_module: type, options: AppOptions) !void { std.process.exit(0); } + // var httpz_server = try jetzig.HttpzServer.init(self.allocator); + // defer httpz_server.deinit(); + var server = jetzig.http.Server.init( self.allocator, server_options, @@ -105,7 +108,6 @@ pub fn start(self: App, routes_module: type, options: AppOptions) !void { &job_queue, &cache, ); - defer server.deinit(); var mutex = std.Thread.Mutex{}; var worker_pool = jetzig.jobs.Pool.init( diff --git a/src/jetzig/HttpzServer.zig b/src/jetzig/HttpzServer.zig new file mode 100644 index 0000000..40184e3 --- /dev/null +++ b/src/jetzig/HttpzServer.zig @@ -0,0 +1,58 @@ +const std = @import("std"); + +const httpz = @import("httpz"); + +const jetzig = @import("../jetzig.zig"); + +allocator: std.mem.Allocator, +server: httpz.ServerCtx(void, void), + +const HttpzServer = @This(); + +const DispatcherFn = *const fn (*jetzig.Server, *httpz.Request, *httpz.Response) anyerror!void; + +pub fn init(allocator: std.mem.Allocator, dispatcherFn: DispatcherFn) !HttpzServer { + return .{ + .allocator = allocator, + .dispatcherFn = dispatcherFn, + .server = try httpz.Server().init(allocator, .{ .port = 8080 }), + }; +} + +pub fn deinit(self: *HttpzServer) void { + self.server.deinit(); +} + +pub fn configure(self: *HttpzServer, dispatcherFn: DispatcherFn) void { + // Bypass router. + self.server.notFound(dispatcherFn); +} + +// var server = ; +// +// // set a global dispatch for any routes defined from this point on +// server.dispatcher(mainDispatcher); +// +// // set a dispatcher for this route +// // note the use of "deleteC" the "C" is for Configuration and is used +// // since Zig doesn't have overloading or optional parameters. +// server.router().deleteC("/v1/session", logout, .{.dispatcher = loggedIn}) +// ... +// +// fn mainDispatcher(action: httpz.Action(void), req: *httpz.Request, res: *httpz.Response) !void { +// res.header("cors", "isslow"); +// return action(req, res); +// } +// +// fn loggedIn(action: httpz.Action(void), req: *httpz.Request, res: *httpz.Response) !void { +// if (req.header("authorization")) |_auth| { +// // TODO: make sure "auth" is valid! +// return mainDispatcher(action, req, res); +// } +// res.status = 401; +// res.body = "Not authorized"; +// } +// +// fn logout(req: *httpz.Request, res: *httpz.Response) !void { +// ... +// } diff --git a/src/jetzig/http/Request.zig b/src/jetzig/http/Request.zig index e260a06..b18c5a5 100644 --- a/src/jetzig/http/Request.zig +++ b/src/jetzig/http/Request.zig @@ -1,5 +1,7 @@ const std = @import("std"); +const httpz = @import("httpz"); + const jetzig = @import("../../jetzig.zig"); const Request = @This(); @@ -14,7 +16,8 @@ path: jetzig.http.Path, method: Method, headers: jetzig.http.Headers, server: *jetzig.http.Server, -std_http_request: std.http.Server.Request, +httpz_request: *httpz.Request, +httpz_response: *httpz.Response, response: *jetzig.http.Response, status_code: jetzig.http.status_codes.StatusCode = .not_found, response_data: *jetzig.data.Data, @@ -90,20 +93,18 @@ pub fn init( allocator: std.mem.Allocator, server: *jetzig.http.Server, start_time: i128, - std_http_request: std.http.Server.Request, + httpz_request: *httpz.Request, + httpz_response: *httpz.Response, response: *jetzig.http.Response, ) !Request { - const method = switch (std_http_request.head.method) { + const method = switch (httpz_request.method) { .DELETE => Method.DELETE, .GET => Method.GET, .PATCH => Method.PATCH, .POST => Method.POST, .HEAD => Method.HEAD, .PUT => Method.PUT, - .CONNECT => Method.CONNECT, .OPTIONS => Method.OPTIONS, - .TRACE => Method.TRACE, - _ => return error.JetzigUnsupportedHttpMethod, }; const response_data = try allocator.create(jetzig.data.Data); @@ -111,13 +112,14 @@ pub fn init( return .{ .allocator = allocator, - .path = jetzig.http.Path.init(std_http_request.head.target), + .path = jetzig.http.Path.init(httpz_request.url.raw), .method = method, .headers = jetzig.http.Headers.init(allocator), .server = server, .response = response, .response_data = response_data, - .std_http_request = std_http_request, + .httpz_request = httpz_request, + .httpz_response = httpz_response, .start_time = start_time, .store = .{ .store = server.store, .allocator = allocator }, .cache = .{ .store = server.cache, .allocator = allocator }, @@ -134,12 +136,14 @@ pub fn deinit(self: *Request) void { /// Process request, read body if present, parse headers (TODO) pub fn process(self: *Request) !void { - var headers_it = self.std_http_request.iterateHeaders(); var cookie: ?[]const u8 = null; - while (headers_it.next()) |header| { - try self.headers.append(header.name, header.value); - if (std.mem.eql(u8, header.name, "Cookie")) cookie = header.value; + var header_index: usize = 0; + while (header_index < self.httpz_request.headers.len) : (header_index += 1) { + const name = self.httpz_request.headers.keys[header_index]; + const value = self.httpz_request.headers.values[header_index]; + try self.headers.append(name, value); + if (std.mem.eql(u8, name, "Cookie")) cookie = value; } self.cookies = try self.allocator.create(jetzig.http.Cookies); @@ -161,8 +165,7 @@ pub fn process(self: *Request) !void { } }; - const reader = try self.std_http_request.reader(); - self.body = try reader.readAllAlloc(self.allocator, jetzig.config.get(usize, "max_bytes_request_body")); + self.body = self.httpz_request.body() orelse ""; self.processed = true; } @@ -179,16 +182,25 @@ pub fn respond(self: *Request) !void { var std_response_headers = try self.response.headers.stdHeaders(); defer std_response_headers.deinit(self.allocator); - try self.std_http_request.respond( - self.response.content, - .{ - .keep_alive = false, - .status = switch (self.response.status_code) { - inline else => |tag| @field(std.http.Status, @tagName(tag)), - }, - .extra_headers = std_response_headers.items, - }, - ); + for (self.response.headers.headers.items) |header| { + self.httpz_response.header(header.name, header.value); + } + + const status = jetzig.http.status_codes.get(self.response.status_code); + self.httpz_response.status = try status.getCodeInt(); + self.httpz_response.body = self.response.content; + try self.httpz_response.write(); + + // try self.httpz_response.respond( + // self.response.content, + // .{ + // .keep_alive = false, + // .status = switch (self.response.status_code) { + // inline else => |tag| @field(std.http.Status, @tagName(tag)), + // }, + // .extra_headers = std_response_headers.items, + // }, + // ); } /// Render a response. This function can only be called once per request (repeat calls will diff --git a/src/jetzig/http/Server.zig b/src/jetzig/http/Server.zig index 9434bd6..325796b 100644 --- a/src/jetzig/http/Server.zig +++ b/src/jetzig/http/Server.zig @@ -3,6 +3,7 @@ const std = @import("std"); const jetzig = @import("../../jetzig.zig"); const zmpl = @import("zmpl"); const zmd = @import("zmd"); +const httpz = @import("httpz"); pub const ServerOptions = struct { logger: jetzig.loggers.Logger, @@ -60,52 +61,58 @@ pub fn deinit(self: *Server) void { } pub fn listen(self: *Server) !void { - const address = try std.net.Address.parseIp(self.options.bind, self.options.port); - self.std_net_server = try address.listen(.{ .reuse_port = true }); + var httpz_server = try httpz.ServerCtx(*jetzig.http.Server, *jetzig.http.Server).init( + self.allocator, + .{ + .port = self.options.port, + .address = self.options.bind, + .workers = .{ .count = 8 }, + .thread_pool = .{ .count = 8 }, + }, + self, + ); + defer httpz_server.deinit(); + httpz_server.notFound(jetzig.http.Server.dispatcherFn); + // httpz_server.dispatcher(jetzig.http.Server.dispatcherFn); - self.initialized = true; - - try self.logger.INFO("Listening on http://{s}:{} [{s}]", .{ - self.options.bind, - self.options.port, - @tagName(self.options.environment), - }); - try self.processRequests(); + return try httpz_server.listen(); + // const address = try std.net.Address.parseIp(self.options.bind, self.options.port); + // self.std_net_server = try address.listen(.{ .reuse_port = true }); + // + // self.initialized = true; + // + // try self.logger.INFO("Listening on http://{s}:{} [{s}]", .{ + // self.options.bind, + // self.options.port, + // @tagName(self.options.environment), + // }); + // try self.processRequests(); } -fn processRequests(self: *Server) !void { - // TODO: Keepalive - while (true) { - var arena = std.heap.ArenaAllocator.init(self.allocator); - errdefer arena.deinit(); - const allocator = arena.allocator(); - - const connection = try self.std_net_server.accept(); - - var buf: [jetzig.config.get(usize, "http_buffer_size")]u8 = undefined; - var std_http_server = std.http.Server.init(connection, &buf); - errdefer std_http_server.connection.stream.close(); - - self.processNextRequest(allocator, &std_http_server) catch |err| { - if (isBadHttpError(err)) { - std_http_server.connection.stream.close(); - continue; - } else return err; - }; - - std_http_server.connection.stream.close(); - arena.deinit(); - } +pub fn dispatcherFn(self: *Server, request: *httpz.Request, response: *httpz.Response) !void { + var arena = std.heap.ArenaAllocator.init(self.allocator); + defer arena.deinit(); + const allocator = arena.allocator(); + try self.processNextRequest(allocator, request, response); } -fn processNextRequest(self: *Server, allocator: std.mem.Allocator, std_http_server: *std.http.Server) !void { +fn processNextRequest( + self: *Server, + allocator: std.mem.Allocator, + httpz_request: *httpz.Request, + httpz_response: *httpz.Response, +) !void { const start_time = std.time.nanoTimestamp(); - const std_http_request = try std_http_server.receiveHead(); - if (std_http_server.state == .receiving_head) return error.JetzigParseHeadError; - var response = try jetzig.http.Response.init(allocator); - var request = try jetzig.http.Request.init(allocator, self, start_time, std_http_request, &response); + var request = try jetzig.http.Request.init( + allocator, + self, + start_time, + httpz_request, + httpz_response, + &response, + ); try request.process(); @@ -121,9 +128,35 @@ fn processNextRequest(self: *Server, allocator: std.mem.Allocator, std_http_serv try jetzig.http.middleware.afterResponse(&middleware_data, &request); jetzig.http.middleware.deinit(&middleware_data, &request); - try self.logger.logRequest(&request); + // try self.logger.logRequest(&request); } +// fn processNextRequest(self: *Server, allocator: std.mem.Allocator, std_http_server: *std.http.Server) !void { +// const start_time = std.time.nanoTimestamp(); +// +// const std_http_request = try std_http_server.receiveHead(); +// if (std_http_server.state == .receiving_head) return error.JetzigParseHeadError; +// +// var response = try jetzig.http.Response.init(allocator); +// var request = try jetzig.http.Request.init(allocator, self, start_time, std_http_request, &response); +// +// try request.process(); +// +// var middleware_data = try jetzig.http.middleware.afterRequest(&request); +// +// try self.renderResponse(&request); +// try request.response.headers.append("content-type", response.content_type); +// +// try jetzig.http.middleware.beforeResponse(&middleware_data, &request); +// +// try request.respond(); +// +// try jetzig.http.middleware.afterResponse(&middleware_data, &request); +// jetzig.http.middleware.deinit(&middleware_data, &request); +// +// try self.logger.logRequest(&request); +// } +// fn renderResponse(self: *Server, request: *jetzig.http.Request) !void { const static_resource = self.matchStaticResource(request) catch |err| { if (isUnhandledError(err)) return err; diff --git a/src/jetzig/http/status_codes.zig b/src/jetzig/http/status_codes.zig index 611cc08..2987a8d 100644 --- a/src/jetzig/http/status_codes.zig +++ b/src/jetzig/http/status_codes.zig @@ -178,6 +178,12 @@ pub const TaggedStatusCode = union(StatusCode) { }; } + pub fn getCodeInt(self: Self) !u16 { + return switch (self) { + inline else => |capture| try std.fmt.parseInt(u16, capture.code, 10), + }; + } + pub fn getMessage(self: Self) []const u8 { return switch (self) { inline else => |capture| capture.message, From c84d2fcd71cee43246199f336ee9b28eca6e9c5b Mon Sep 17 00:00:00 2001 From: Bob Farrell Date: Mon, 6 May 2024 13:58:30 +0100 Subject: [PATCH 2/6] WIP --- src/jetzig/App.zig | 3 --- src/jetzig/http/Request.zig | 20 ++++++-------------- src/jetzig/http/Server.zig | 30 ++---------------------------- 3 files changed, 8 insertions(+), 45 deletions(-) diff --git a/src/jetzig/App.zig b/src/jetzig/App.zig index ed0c9a0..13a7340 100644 --- a/src/jetzig/App.zig +++ b/src/jetzig/App.zig @@ -94,9 +94,6 @@ pub fn start(self: App, routes_module: type, options: AppOptions) !void { std.process.exit(0); } - // var httpz_server = try jetzig.HttpzServer.init(self.allocator); - // defer httpz_server.deinit(); - var server = jetzig.http.Server.init( self.allocator, server_options, diff --git a/src/jetzig/http/Request.zig b/src/jetzig/http/Request.zig index b18c5a5..7ff4428 100644 --- a/src/jetzig/http/Request.zig +++ b/src/jetzig/http/Request.zig @@ -183,24 +183,16 @@ pub fn respond(self: *Request) !void { defer std_response_headers.deinit(self.allocator); for (self.response.headers.headers.items) |header| { - self.httpz_response.header(header.name, header.value); + self.httpz_response.header( + try self.httpz_response.arena.dupe(u8, header.name), + try self.httpz_response.arena.dupe(u8, header.value), + ); } const status = jetzig.http.status_codes.get(self.response.status_code); self.httpz_response.status = try status.getCodeInt(); - self.httpz_response.body = self.response.content; - try self.httpz_response.write(); - - // try self.httpz_response.respond( - // self.response.content, - // .{ - // .keep_alive = false, - // .status = switch (self.response.status_code) { - // inline else => |tag| @field(std.http.Status, @tagName(tag)), - // }, - // .extra_headers = std_response_headers.items, - // }, - // ); + self.httpz_response.body = try self.httpz_response.arena.dupe(u8, self.response.content); + // try self.httpz_response.write(); } /// Render a response. This function can only be called once per request (repeat calls will diff --git a/src/jetzig/http/Server.zig b/src/jetzig/http/Server.zig index 325796b..2f5b236 100644 --- a/src/jetzig/http/Server.zig +++ b/src/jetzig/http/Server.zig @@ -66,8 +66,8 @@ pub fn listen(self: *Server) !void { .{ .port = self.options.port, .address = self.options.bind, - .workers = .{ .count = 8 }, - .thread_pool = .{ .count = 8 }, + .workers = .{ .count = 20 }, + .thread_pool = .{ .count = 1 }, }, self, ); @@ -131,32 +131,6 @@ fn processNextRequest( // try self.logger.logRequest(&request); } -// fn processNextRequest(self: *Server, allocator: std.mem.Allocator, std_http_server: *std.http.Server) !void { -// const start_time = std.time.nanoTimestamp(); -// -// const std_http_request = try std_http_server.receiveHead(); -// if (std_http_server.state == .receiving_head) return error.JetzigParseHeadError; -// -// var response = try jetzig.http.Response.init(allocator); -// var request = try jetzig.http.Request.init(allocator, self, start_time, std_http_request, &response); -// -// try request.process(); -// -// var middleware_data = try jetzig.http.middleware.afterRequest(&request); -// -// try self.renderResponse(&request); -// try request.response.headers.append("content-type", response.content_type); -// -// try jetzig.http.middleware.beforeResponse(&middleware_data, &request); -// -// try request.respond(); -// -// try jetzig.http.middleware.afterResponse(&middleware_data, &request); -// jetzig.http.middleware.deinit(&middleware_data, &request); -// -// try self.logger.logRequest(&request); -// } -// fn renderResponse(self: *Server, request: *jetzig.http.Request) !void { const static_resource = self.matchStaticResource(request) catch |err| { if (isUnhandledError(err)) return err; From 458ea252e7bb40326a682fe7a00d6927a41ffe64 Mon Sep 17 00:00:00 2001 From: Bob Farrell Date: Tue, 7 May 2024 20:21:40 +0100 Subject: [PATCH 3/6] WIP --- build.zig.zon | 8 ++--- src/jetzig.zig | 1 - src/jetzig/HttpzServer.zig | 58 -------------------------------- src/jetzig/http/Request.zig | 29 ++++++++++------ src/jetzig/http/Server.zig | 66 +++++++++++++++++++++++-------------- 5 files changed, 64 insertions(+), 98 deletions(-) delete mode 100644 src/jetzig/HttpzServer.zig diff --git a/build.zig.zon b/build.zig.zon index 7088855..1435df6 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -19,12 +19,12 @@ .hash = "12208a1de366740d11de525db7289345949f5fd46527db3f89eecc7bb49b012c0732", }, .smtp_client = .{ - .url = "https://github.com/karlseguin/smtp_client.zig/archive/e79e411862d4f4d41657bf41efb884efca3d67dd.tar.gz", - .hash = "12209907c69891a38e6923308930ac43bfb40135bc609ea370b5759fc2e1c4f57284", + .url = "https://github.com/karlseguin/smtp_client.zig/archive/964152ad4e19dc1d22f6def6f659c86df60e7832.tar.gz", + .hash = "1220d4f1c2472769b0d689ea878f41f0a66cb07f28569a138aea2c0a648a5c90dd4e", }, .httpz = .{ - .url = "https://github.com/karlseguin/http.zig/archive/bf748c6508090464213d3088a77d51faf9cd0876.tar.gz", - .hash = "12209b7426293ebe5075b930ae6029c009bfb6deb7ff92b9d69e28463abd14ad03da", + .url = "https://github.com/karlseguin/http.zig/archive/0d4a5cd520a54eaf800438e0b9093c77c90dcf11.tar.gz", + .hash = "12209b8216a80f21be12d43e588811150bdbbb53d35eac6a2a61c460f197350e19ad", }, }, diff --git a/src/jetzig.zig b/src/jetzig.zig index 3a72e6b..4d18546 100644 --- a/src/jetzig.zig +++ b/src/jetzig.zig @@ -16,7 +16,6 @@ pub const markdown = @import("jetzig/markdown.zig"); pub const jobs = @import("jetzig/jobs.zig"); pub const mail = @import("jetzig/mail.zig"); pub const kv = @import("jetzig/kv.zig"); -pub const HttpzServer = @import("jetzig/HttpzServer.zig"); /// The primary interface for a Jetzig application. Create an `App` in your application's /// `src/main.zig` and call `start` to launch the application. diff --git a/src/jetzig/HttpzServer.zig b/src/jetzig/HttpzServer.zig deleted file mode 100644 index 40184e3..0000000 --- a/src/jetzig/HttpzServer.zig +++ /dev/null @@ -1,58 +0,0 @@ -const std = @import("std"); - -const httpz = @import("httpz"); - -const jetzig = @import("../jetzig.zig"); - -allocator: std.mem.Allocator, -server: httpz.ServerCtx(void, void), - -const HttpzServer = @This(); - -const DispatcherFn = *const fn (*jetzig.Server, *httpz.Request, *httpz.Response) anyerror!void; - -pub fn init(allocator: std.mem.Allocator, dispatcherFn: DispatcherFn) !HttpzServer { - return .{ - .allocator = allocator, - .dispatcherFn = dispatcherFn, - .server = try httpz.Server().init(allocator, .{ .port = 8080 }), - }; -} - -pub fn deinit(self: *HttpzServer) void { - self.server.deinit(); -} - -pub fn configure(self: *HttpzServer, dispatcherFn: DispatcherFn) void { - // Bypass router. - self.server.notFound(dispatcherFn); -} - -// var server = ; -// -// // set a global dispatch for any routes defined from this point on -// server.dispatcher(mainDispatcher); -// -// // set a dispatcher for this route -// // note the use of "deleteC" the "C" is for Configuration and is used -// // since Zig doesn't have overloading or optional parameters. -// server.router().deleteC("/v1/session", logout, .{.dispatcher = loggedIn}) -// ... -// -// fn mainDispatcher(action: httpz.Action(void), req: *httpz.Request, res: *httpz.Response) !void { -// res.header("cors", "isslow"); -// return action(req, res); -// } -// -// fn loggedIn(action: httpz.Action(void), req: *httpz.Request, res: *httpz.Response) !void { -// if (req.header("authorization")) |_auth| { -// // TODO: make sure "auth" is valid! -// return mainDispatcher(action, req, res); -// } -// res.status = 401; -// res.body = "Not authorized"; -// } -// -// fn logout(req: *httpz.Request, res: *httpz.Response) !void { -// ... -// } diff --git a/src/jetzig/http/Request.zig b/src/jetzig/http/Request.zig index 7ff4428..abd6284 100644 --- a/src/jetzig/http/Request.zig +++ b/src/jetzig/http/Request.zig @@ -169,8 +169,23 @@ pub fn process(self: *Request) !void { self.processed = true; } +pub const CallbackState = struct { + arena: *std.heap.ArenaAllocator, + allocator: std.mem.Allocator, +}; + +pub fn responseCompleteCallback(ptr: *anyopaque) void { + var state: *CallbackState = @ptrCast(@alignCast(ptr)); + state.arena.deinit(); + state.allocator.destroy(state.arena); + state.allocator.destroy(state); +} + /// Set response headers, write response payload, and finalize the response. -pub fn respond(self: *Request) !void { +pub fn respond( + self: *Request, + state: *CallbackState, +) !void { if (!self.processed) unreachable; var cookie_it = self.cookies.headerIterator(); @@ -179,20 +194,14 @@ pub fn respond(self: *Request) !void { try self.response.headers.append("Set-Cookie", header); } - var std_response_headers = try self.response.headers.stdHeaders(); - defer std_response_headers.deinit(self.allocator); - for (self.response.headers.headers.items) |header| { - self.httpz_response.header( - try self.httpz_response.arena.dupe(u8, header.name), - try self.httpz_response.arena.dupe(u8, header.value), - ); + self.httpz_response.header(header.name, header.value); } const status = jetzig.http.status_codes.get(self.response.status_code); self.httpz_response.status = try status.getCodeInt(); - self.httpz_response.body = try self.httpz_response.arena.dupe(u8, self.response.content); - // try self.httpz_response.write(); + self.httpz_response.body = self.response.content; + self.httpz_response.callback(responseCompleteCallback, @ptrCast(state)); } /// Render a response. This function can only be called once per request (repeat calls will diff --git a/src/jetzig/http/Server.zig b/src/jetzig/http/Server.zig index 2f5b236..9ea6dcb 100644 --- a/src/jetzig/http/Server.zig +++ b/src/jetzig/http/Server.zig @@ -60,48 +60,64 @@ pub fn deinit(self: *Server) void { self.allocator.free(self.options.bind); } +const Dispatcher = struct { + server: *Server, + + pub fn handle(self: Dispatcher, request: *httpz.Request, response: *httpz.Response) void { + self.server.processNextRequest(request, response) catch |err| { + self.server.errorHandlerFn(request, response, err); + }; + } +}; + pub fn listen(self: *Server) !void { - var httpz_server = try httpz.ServerCtx(*jetzig.http.Server, *jetzig.http.Server).init( + var httpz_server = try httpz.ServerCtx(Dispatcher, Dispatcher).init( self.allocator, .{ .port = self.options.port, .address = self.options.bind, - .workers = .{ .count = 20 }, - .thread_pool = .{ .count = 1 }, + .thread_pool = .{ .count = @intCast(try std.Thread.getCpuCount()) }, }, - self, + Dispatcher{ .server = self }, ); defer httpz_server.deinit(); - httpz_server.notFound(jetzig.http.Server.dispatcherFn); - // httpz_server.dispatcher(jetzig.http.Server.dispatcherFn); + + try self.logger.INFO("Listening on http://{s}:{} [{s}]", .{ + self.options.bind, + self.options.port, + @tagName(self.options.environment), + }); + + self.initialized = true; return try httpz_server.listen(); - // const address = try std.net.Address.parseIp(self.options.bind, self.options.port); - // self.std_net_server = try address.listen(.{ .reuse_port = true }); - // - // self.initialized = true; - // - // try self.logger.INFO("Listening on http://{s}:{} [{s}]", .{ - // self.options.bind, - // self.options.port, - // @tagName(self.options.environment), - // }); - // try self.processRequests(); } -pub fn dispatcherFn(self: *Server, request: *httpz.Request, response: *httpz.Response) !void { - var arena = std.heap.ArenaAllocator.init(self.allocator); - defer arena.deinit(); - const allocator = arena.allocator(); - try self.processNextRequest(allocator, request, response); +pub fn errorHandlerFn(self: *Server, request: *httpz.Request, response: *httpz.Response, err: anyerror) void { + if (isBadHttpError(err)) return; + + self.logger.ERROR("Encountered error: {s} {s}", .{ @errorName(err), request.url.raw }) catch {}; + response.body = "500 Internal Server Error"; } fn processNextRequest( self: *Server, - allocator: std.mem.Allocator, httpz_request: *httpz.Request, httpz_response: *httpz.Response, ) !void { + const state = try self.allocator.create(jetzig.http.Request.CallbackState); + const arena = try self.allocator.create(std.heap.ArenaAllocator); + arena.* = std.heap.ArenaAllocator.init(self.allocator); + state.* = .{ + .arena = arena, + .allocator = self.allocator, + }; + + // Regular arena deinit occurs in jetzig.http.Request.responseCompletCallback + errdefer state.arena.deinit(); + + const allocator = state.arena.allocator(); + const start_time = std.time.nanoTimestamp(); var response = try jetzig.http.Response.init(allocator); @@ -123,12 +139,12 @@ fn processNextRequest( try jetzig.http.middleware.beforeResponse(&middleware_data, &request); - try request.respond(); + try request.respond(state); try jetzig.http.middleware.afterResponse(&middleware_data, &request); jetzig.http.middleware.deinit(&middleware_data, &request); - // try self.logger.logRequest(&request); + try self.logger.logRequest(&request); } fn renderResponse(self: *Server, request: *jetzig.http.Request) !void { From 6e4048a3182e66234c5ab72afffd996c0165eb7b Mon Sep 17 00:00:00 2001 From: Bob Farrell Date: Tue, 7 May 2024 21:11:54 +0100 Subject: [PATCH 4/6] WIP --- src/jetzig/http.zig | 6 +- src/jetzig/http/RequestWindows.zig | 546 +++++++++++++++++++++++++++++ src/jetzig/http/Server.zig | 70 ++++ 3 files changed, 621 insertions(+), 1 deletion(-) create mode 100644 src/jetzig/http/RequestWindows.zig diff --git a/src/jetzig/http.zig b/src/jetzig/http.zig index 5151d26..f558717 100644 --- a/src/jetzig/http.zig +++ b/src/jetzig/http.zig @@ -1,7 +1,11 @@ const std = @import("std"); +const builtin = @import("builtin"); pub const Server = @import("http/Server.zig"); -pub const Request = @import("http/Request.zig"); +pub const Request = if (builtin.os.tag == .windows) + @import("http/RequestWindows.zig") +else + @import("http/Request.zig"); pub const StaticRequest = @import("http/StaticRequest.zig"); pub const Response = @import("http/Response.zig"); pub const Session = @import("http/Session.zig"); diff --git a/src/jetzig/http/RequestWindows.zig b/src/jetzig/http/RequestWindows.zig new file mode 100644 index 0000000..e260a06 --- /dev/null +++ b/src/jetzig/http/RequestWindows.zig @@ -0,0 +1,546 @@ +const std = @import("std"); + +const jetzig = @import("../../jetzig.zig"); + +const Request = @This(); +const default_content_type = "text/html"; + +pub const Method = enum { DELETE, GET, PATCH, POST, HEAD, PUT, CONNECT, OPTIONS, TRACE }; +pub const Modifier = enum { edit, new }; +pub const Format = enum { HTML, JSON, UNKNOWN }; + +allocator: std.mem.Allocator, +path: jetzig.http.Path, +method: Method, +headers: jetzig.http.Headers, +server: *jetzig.http.Server, +std_http_request: std.http.Server.Request, +response: *jetzig.http.Response, +status_code: jetzig.http.status_codes.StatusCode = .not_found, +response_data: *jetzig.data.Data, +query_params: ?*jetzig.http.Query = null, +query_body: ?*jetzig.http.Query = null, +cookies: *jetzig.http.Cookies = undefined, +session: *jetzig.http.Session = undefined, +body: []const u8 = undefined, +processed: bool = false, +layout: ?[]const u8 = null, +layout_disabled: bool = false, +rendered: bool = false, +redirected: bool = false, +rendered_multiple: bool = false, +rendered_view: ?jetzig.views.View = null, +start_time: i128, +store: RequestStore, +cache: RequestStore, + +/// Wrapper for KV store that uses the request's arena allocator for fetching values. +pub const RequestStore = struct { + allocator: std.mem.Allocator, + store: *jetzig.kv.Store, + + /// Put a String or into the key-value store. + pub fn get(self: RequestStore, key: []const u8) !?*jetzig.data.Value { + return try self.store.get(try self.data(), key); + } + + /// Get a String from the store. + pub fn put(self: RequestStore, key: []const u8, value: *jetzig.data.Value) !void { + try self.store.put(key, value); + } + + /// Remove a String to from the key-value store and return it if found. + pub fn fetchRemove(self: RequestStore, key: []const u8) !?*jetzig.data.Value { + return try self.store.fetchRemove(try self.data(), key); + } + + /// Remove a String to from the key-value store. + pub fn remove(self: RequestStore, key: []const u8) !void { + try self.store.remove(key); + } + + /// Append a Value to the end of an Array in the key-value store. + pub fn append(self: RequestStore, key: []const u8, value: *jetzig.data.Value) !void { + try self.store.append(key, value); + } + + /// Prepend a Value to the start of an Array in the key-value store. + pub fn prepend(self: RequestStore, key: []const u8, value: *jetzig.data.Value) !void { + try self.store.prepend(key, value); + } + + /// Pop a String from an Array in the key-value store. + pub fn pop(self: RequestStore, key: []const u8) !?*jetzig.data.Value { + return try self.store.pop(try self.data(), key); + } + + /// Left-pop a String from an Array in the key-value store. + pub fn popFirst(self: RequestStore, key: []const u8) !?*jetzig.data.Value { + return try self.store.popFirst(try self.data(), key); + } + + fn data(self: RequestStore) !*jetzig.data.Data { + const arena_data = try self.allocator.create(jetzig.data.Data); + arena_data.* = jetzig.data.Data.init(self.allocator); + return arena_data; + } +}; + +pub fn init( + allocator: std.mem.Allocator, + server: *jetzig.http.Server, + start_time: i128, + std_http_request: std.http.Server.Request, + response: *jetzig.http.Response, +) !Request { + const method = switch (std_http_request.head.method) { + .DELETE => Method.DELETE, + .GET => Method.GET, + .PATCH => Method.PATCH, + .POST => Method.POST, + .HEAD => Method.HEAD, + .PUT => Method.PUT, + .CONNECT => Method.CONNECT, + .OPTIONS => Method.OPTIONS, + .TRACE => Method.TRACE, + _ => return error.JetzigUnsupportedHttpMethod, + }; + + const response_data = try allocator.create(jetzig.data.Data); + response_data.* = jetzig.data.Data.init(allocator); + + return .{ + .allocator = allocator, + .path = jetzig.http.Path.init(std_http_request.head.target), + .method = method, + .headers = jetzig.http.Headers.init(allocator), + .server = server, + .response = response, + .response_data = response_data, + .std_http_request = std_http_request, + .start_time = start_time, + .store = .{ .store = server.store, .allocator = allocator }, + .cache = .{ .store = server.cache, .allocator = allocator }, + }; +} + +pub fn deinit(self: *Request) void { + self.session.deinit(); + self.cookies.deinit(); + self.allocator.destroy(self.cookies); + self.allocator.destroy(self.session); + if (self.processed) self.allocator.free(self.body); +} + +/// Process request, read body if present, parse headers (TODO) +pub fn process(self: *Request) !void { + var headers_it = self.std_http_request.iterateHeaders(); + var cookie: ?[]const u8 = null; + + while (headers_it.next()) |header| { + try self.headers.append(header.name, header.value); + if (std.mem.eql(u8, header.name, "Cookie")) cookie = header.value; + } + + self.cookies = try self.allocator.create(jetzig.http.Cookies); + self.cookies.* = jetzig.http.Cookies.init( + self.allocator, + cookie orelse "", + ); + try self.cookies.parse(); + + self.session = try self.allocator.create(jetzig.http.Session); + self.session.* = jetzig.http.Session.init(self.allocator, self.cookies, self.server.options.secret); + self.session.parse() catch |err| { + switch (err) { + error.JetzigInvalidSessionCookie => { + try self.server.logger.DEBUG("Invalid session cookie detected. Resetting session.", .{}); + try self.session.reset(); + }, + else => return err, + } + }; + + const reader = try self.std_http_request.reader(); + self.body = try reader.readAllAlloc(self.allocator, jetzig.config.get(usize, "max_bytes_request_body")); + self.processed = true; +} + +/// Set response headers, write response payload, and finalize the response. +pub fn respond(self: *Request) !void { + if (!self.processed) unreachable; + + var cookie_it = self.cookies.headerIterator(); + while (try cookie_it.next()) |header| { + // FIXME: Skip setting cookies that are already present ? + try self.response.headers.append("Set-Cookie", header); + } + + var std_response_headers = try self.response.headers.stdHeaders(); + defer std_response_headers.deinit(self.allocator); + + try self.std_http_request.respond( + self.response.content, + .{ + .keep_alive = false, + .status = switch (self.response.status_code) { + inline else => |tag| @field(std.http.Status, @tagName(tag)), + }, + .extra_headers = std_response_headers.items, + }, + ); +} + +/// Render a response. This function can only be called once per request (repeat calls will +/// trigger an error). +pub fn render(self: *Request, status_code: jetzig.http.status_codes.StatusCode) jetzig.views.View { + if (self.rendered) self.rendered_multiple = true; + + self.rendered = true; + self.rendered_view = .{ .data = self.response_data, .status_code = status_code }; + return self.rendered_view.?; +} + +/// Issue a redirect to a new location. +/// ```zig +/// return request.redirect("https://www.example.com/", .moved_permanently); +/// ``` +/// ```zig +/// return request.redirect("https://www.example.com/", .found); +/// ``` +/// The second argument must be `moved_permanently` or `found`. +pub fn redirect( + self: *Request, + location: []const u8, + redirect_status: enum { moved_permanently, found }, +) jetzig.views.View { + if (self.rendered) self.rendered_multiple = true; + + self.rendered = true; + self.redirected = true; + + const status_code = switch (redirect_status) { + .moved_permanently => jetzig.http.status_codes.StatusCode.moved_permanently, + .found => jetzig.http.status_codes.StatusCode.found, + }; + + self.response_data.reset(); + + self.response.headers.remove("Location"); + self.response.headers.append("Location", location) catch @panic("OOM"); + + self.rendered_view = .{ .data = self.response_data, .status_code = status_code }; + return self.rendered_view.?; +} + +/// Infer the current format (JSON or HTML) from the request in this order: +/// * Extension (path ends in `.json` or `.html`) +/// * `Accept` header (`application/json` or `text/html`) +/// * `Content-Type` header (`application/json` or `text/html`) +/// * Fall back to default: HTML +pub fn requestFormat(self: *const Request) jetzig.http.Request.Format { + return self.extensionFormat() orelse + self.acceptHeaderFormat() orelse + self.contentTypeHeaderFormat() orelse + .UNKNOWN; +} + +/// Set the layout for the current request/response. Use this to override a `pub const layout` +/// declaration in a view, either in middleware or in a view function itself. +pub fn setLayout(self: *Request, layout: ?[]const u8) void { + if (layout) |layout_name| { + self.layout = layout_name; + self.layout_disabled = false; + } else { + self.layout_disabled = true; + } +} + +/// Derive a layout name from the current request if defined, otherwise from the route (if +/// defined). +pub fn getLayout(self: *Request, route: *jetzig.views.Route) ?[]const u8 { + if (self.layout_disabled) return null; + if (self.layout) |capture| return capture; + if (route.layout) |capture| return capture; + + return null; +} + +/// Shortcut for `request.headers.getFirstValue`. Returns the first matching value for a given +/// header name or `null` if not found. Header names are case-insensitive. +pub fn getHeader(self: *const Request, key: []const u8) ?[]const u8 { + return self.headers.getFirstValue(key); +} + +/// Return a `Value` representing request parameters. Parameters are normalized, meaning that +/// both the JSON request body and query parameters are accessed via the same interface. +/// Note that query parameters are supported for JSON requests if no request body is present, +/// otherwise the parsed JSON request body will take precedence and query parameters will be +/// ignored. +pub fn params(self: *Request) !*jetzig.data.Value { + if (!self.processed) unreachable; + + switch (self.requestFormat()) { + .JSON => { + if (self.body.len == 0) return self.queryParams(); + + var data = try self.allocator.create(jetzig.data.Data); + data.* = jetzig.data.Data.init(self.allocator); + data.fromJson(self.body) catch |err| { + switch (err) { + error.SyntaxError, error.UnexpectedEndOfInput => return error.JetzigBodyParseError, + else => return err, + } + }; + return data.value.?; + }, + .HTML, .UNKNOWN => return self.parseQuery(), + } +} + +/// Return a `*Value` representing request parameters. This function **always** returns the +/// parsed query string and never the request body. +pub fn queryParams(self: *Request) !*jetzig.data.Value { + if (self.query_params) |parsed| return parsed.data.value.?; + + const data = try self.allocator.create(jetzig.data.Data); + data.* = jetzig.data.Data.init(self.allocator); + self.query_params = try self.allocator.create(jetzig.http.Query); + self.query_params.?.* = jetzig.http.Query.init( + self.allocator, + self.path.query orelse "", + data, + ); + try self.query_params.?.parse(); + return self.query_params.?.data.value.?; +} + +// Parses request body as params if present, otherwise delegates to `queryParams`. +fn parseQuery(self: *Request) !*jetzig.data.Value { + if (self.body.len == 0) return try self.queryParams(); + if (self.query_body) |parsed| return parsed.data.value.?; + + const data = try self.allocator.create(jetzig.data.Data); + data.* = jetzig.data.Data.init(self.allocator); + self.query_body = try self.allocator.create(jetzig.http.Query); + self.query_body.?.* = jetzig.http.Query.init( + self.allocator, + self.body, + data, + ); + try self.query_body.?.parse(); + return self.query_body.?.data.value.?; +} + +/// Creates a new Job. Receives a job name which must resolve to `src/app/jobs/.zig` +/// Call `Job.put(...)` to set job params. +/// Call `Job.background()` to run the job outside of the request/response flow. +/// e.g.: +/// ``` +/// pub fn post(request: *jetzig.Request, data: *jetzig.Data) !jetzig.View { +/// var job = try request.job("foo"); // Will invoke `process()` in `src/app/jobs/foo.zig` +/// try job.put("foo", data.string("bar")); +/// try job.background(); // Job added to queue and processed by job worker. +/// return request.render(.ok); +/// } +/// ``` +pub fn job(self: *Request, job_name: []const u8) !*jetzig.Job { + const background_job = try self.allocator.create(jetzig.Job); + background_job.* = jetzig.Job.init( + self.allocator, + self.server.store, + self.server.job_queue, + self.server.cache, + self.server.logger, + self.server.job_definitions, + job_name, + ); + return background_job; +} + +const RequestMail = struct { + request: *Request, + mail_params: jetzig.mail.MailParams, + name: []const u8, + + // Will allow scheduling when strategy is `.later` (e.g.). + const DeliveryOptions = struct {}; + + pub fn deliver(self: RequestMail, strategy: enum { background, now }, options: DeliveryOptions) !void { + _ = options; + var mail_job = try self.request.job("__jetzig_mail"); + + try mail_job.params.put("mailer_name", mail_job.data.string(self.name)); + + const from = if (self.mail_params.from) |from| mail_job.data.string(from) else null; + try mail_job.params.put("from", from); + + var to_array = try mail_job.data.array(); + if (self.mail_params.to) |capture| { + for (capture) |to| try to_array.append(mail_job.data.string(to)); + } + try mail_job.params.put("to", to_array); + + const subject = if (self.mail_params.subject) |subject| mail_job.data.string(subject) else null; + try mail_job.params.put("subject", subject); + + const html = if (self.mail_params.html) |html| mail_job.data.string(html) else null; + try mail_job.params.put("html", html); + + const text = if (self.mail_params.text) |text| mail_job.data.string(text) else null; + try mail_job.params.put("text", text); + + if (self.request.response_data.value) |value| try mail_job.params.put( + "params", + if (strategy == .now) try value.clone(self.request.allocator) else value, + ); + + switch (strategy) { + .background => try mail_job.schedule(), + .now => try mail_job.definition.?.runFn( + self.request.allocator, + mail_job.params, + jetzig.jobs.JobEnv{ + .environment = self.request.server.options.environment, + .logger = self.request.server.logger, + .routes = self.request.server.routes, + .mailers = self.request.server.mailer_definitions, + .jobs = self.request.server.job_definitions, + .store = self.request.server.store, + .cache = self.request.server.cache, + .mutex = undefined, + }, + ), + } + } +}; + +pub fn mail(self: *Request, name: []const u8, mail_params: jetzig.mail.MailParams) RequestMail { + return .{ + .request = self, + .name = name, + .mail_params = mail_params, + }; +} + +fn extensionFormat(self: *const Request) ?jetzig.http.Request.Format { + const extension = self.path.extension orelse return null; + if (std.mem.eql(u8, extension, ".html")) { + return .HTML; + } else if (std.mem.eql(u8, extension, ".json")) { + return .JSON; + } else { + return null; + } +} + +pub fn acceptHeaderFormat(self: *const Request) ?jetzig.http.Request.Format { + const acceptHeader = self.getHeader("Accept"); + + if (acceptHeader) |item| { + if (std.mem.eql(u8, item, "text/html")) return .HTML; + if (std.mem.eql(u8, item, "application/json")) return .JSON; + } + + return null; +} + +pub fn contentTypeHeaderFormat(self: *const Request) ?jetzig.http.Request.Format { + const acceptHeader = self.getHeader("content-type"); + + if (acceptHeader) |item| { + if (std.mem.eql(u8, item, "text/html")) return .HTML; + if (std.mem.eql(u8, item, "application/json")) return .JSON; + } + + return null; +} + +pub fn hash(self: *Request) ![]const u8 { + return try std.fmt.allocPrint( + self.allocator, + "{s}-{s}-{s}", + .{ @tagName(self.method), self.path, @tagName(self.requestFormat()) }, + ); +} + +pub fn fmtMethod(self: *const Request, colorized: bool) []const u8 { + if (!colorized) return @tagName(self.method); + + return switch (self.method) { + .GET => jetzig.colors.cyan("GET"), + .PUT => jetzig.colors.yellow("PUT"), + .PATCH => jetzig.colors.yellow("PATCH"), + .HEAD => jetzig.colors.white("HEAD"), + .POST => jetzig.colors.green("POST"), + .DELETE => jetzig.colors.red("DELETE"), + inline else => |method| jetzig.colors.white(@tagName(method)), + }; +} + +/// Format a status code appropriately for the current request format. +/// e.g. `.HTML` => `404 Not Found` +/// `.JSON` => `{ "message": "Not Found", "status": "404" }` +pub fn formatStatus(self: *const Request, status_code: jetzig.http.StatusCode) ![]const u8 { + const status = jetzig.http.status_codes.get(status_code); + + return switch (self.requestFormat()) { + .JSON => try std.json.stringifyAlloc(self.allocator, .{ + .@"error" = .{ + .message = status.getMessage(), + .code = status.getCode(), + }, + }, .{}), + .HTML, .UNKNOWN => status.getFormatted(.{ .linebreak = true }), + }; +} + +pub fn setResponse( + self: *Request, + rendered_view: jetzig.http.Server.RenderedView, + options: struct { content_type: ?[]const u8 = null }, +) void { + self.response.content = rendered_view.content; + self.response.status_code = rendered_view.view.status_code; + self.response.content_type = options.content_type orelse switch (self.requestFormat()) { + .HTML, .UNKNOWN => "text/html", + .JSON => "application/json", + }; +} + +// Determine if a given route matches the current request. +pub fn match(self: *Request, route: jetzig.views.Route) !bool { + return switch (self.method) { + .GET => switch (route.action) { + .index => self.isMatch(.exact, route), + .get => self.isMatch(.resource_id, route), + else => false, + }, + .POST => switch (route.action) { + .post => self.isMatch(.exact, route), + else => false, + }, + .PUT => switch (route.action) { + .put => self.isMatch(.resource_id, route), + else => false, + }, + .PATCH => switch (route.action) { + .patch => self.isMatch(.resource_id, route), + else => false, + }, + .DELETE => switch (route.action) { + .delete => self.isMatch(.resource_id, route), + else => false, + }, + .HEAD, .CONNECT, .OPTIONS, .TRACE => false, + }; +} + +fn isMatch(self: *Request, match_type: enum { exact, resource_id }, route: jetzig.views.Route) bool { + const path = switch (match_type) { + .exact => self.path.base_path, + .resource_id => self.path.directory, + }; + + return std.mem.eql(u8, path, route.uri_path); +} diff --git a/src/jetzig/http/Server.zig b/src/jetzig/http/Server.zig index 9ea6dcb..242d975 100644 --- a/src/jetzig/http/Server.zig +++ b/src/jetzig/http/Server.zig @@ -1,4 +1,5 @@ const std = @import("std"); +const builtin = @import("builtin"); const jetzig = @import("../../jetzig.zig"); const zmpl = @import("zmpl"); @@ -71,6 +72,8 @@ const Dispatcher = struct { }; pub fn listen(self: *Server) !void { + if (builtin.os.tag == .windows) return try self.listenWindows(); + var httpz_server = try httpz.ServerCtx(Dispatcher, Dispatcher).init( self.allocator, .{ @@ -100,6 +103,73 @@ pub fn errorHandlerFn(self: *Server, request: *httpz.Request, response: *httpz.R response.body = "500 Internal Server Error"; } +// TODO: http.zig windows +pub fn listenWindows(self: *Server) !void { + const address = try std.net.Address.parseIp(self.options.bind, self.options.port); + self.std_net_server = try address.listen(.{ .reuse_port = true }); + + self.initialized = true; + + try self.logger.INFO("Listening on http://{s}:{} [{s}]", .{ + self.options.bind, + self.options.port, + @tagName(self.options.environment), + }); + try self.processRequestsWindows(); +} + +// TODO: http.zig windows +fn processRequestsWindows(self: *Server) !void { + while (true) { + var arena = std.heap.ArenaAllocator.init(self.allocator); + errdefer arena.deinit(); + const allocator = arena.allocator(); + + const connection = try self.std_net_server.accept(); + + var buf: [jetzig.config.get(usize, "http_buffer_size")]u8 = undefined; + var std_http_server = std.http.Server.init(connection, &buf); + errdefer std_http_server.connection.stream.close(); + + self.processNextRequestWindows(allocator, &std_http_server) catch |err| { + if (isBadHttpError(err)) { + std_http_server.connection.stream.close(); + continue; + } else return err; + }; + + std_http_server.connection.stream.close(); + arena.deinit(); + } +} + +// TODO: http.zig windows +fn processNextRequestWindows(self: *Server, allocator: std.mem.Allocator, std_http_server: *std.http.Server) !void { + const start_time = std.time.nanoTimestamp(); + + const std_http_request = try std_http_server.receiveHead(); + if (std_http_server.state == .receiving_head) return error.JetzigParseHeadError; + + var response = try jetzig.http.Response.init(allocator); + var request = try jetzig.http.Request.init(allocator, self, start_time, std_http_request, &response); + + try request.process(); + + var middleware_data = try jetzig.http.middleware.afterRequest(&request); + + try self.renderResponse(&request); + try request.response.headers.append("content-type", response.content_type); + + try jetzig.http.middleware.beforeResponse(&middleware_data, &request); + + try request.respond(); + + try jetzig.http.middleware.afterResponse(&middleware_data, &request); + jetzig.http.middleware.deinit(&middleware_data, &request); + + try self.logger.logRequest(&request); +} + fn processNextRequest( self: *Server, httpz_request: *httpz.Request, From 1693f24ce27be8d377640a7d00c4e92f3255d8e1 Mon Sep 17 00:00:00 2001 From: Bob Farrell Date: Tue, 7 May 2024 21:26:25 +0100 Subject: [PATCH 5/6] WIP --- src/jetzig/http.zig | 2 +- src/jetzig/http/Server.zig | 75 ++----------------- src/jetzig/windows.zig | 67 +++++++++++++++++ .../Request.zig} | 0 4 files changed, 73 insertions(+), 71 deletions(-) create mode 100644 src/jetzig/windows.zig rename src/jetzig/{http/RequestWindows.zig => windows/Request.zig} (100%) diff --git a/src/jetzig/http.zig b/src/jetzig/http.zig index f558717..4786f20 100644 --- a/src/jetzig/http.zig +++ b/src/jetzig/http.zig @@ -3,7 +3,7 @@ const builtin = @import("builtin"); pub const Server = @import("http/Server.zig"); pub const Request = if (builtin.os.tag == .windows) - @import("http/RequestWindows.zig") + @import("windows/Request.zig") else @import("http/Request.zig"); pub const StaticRequest = @import("http/StaticRequest.zig"); diff --git a/src/jetzig/http/Server.zig b/src/jetzig/http/Server.zig index 242d975..b8a237f 100644 --- a/src/jetzig/http/Server.zig +++ b/src/jetzig/http/Server.zig @@ -72,7 +72,7 @@ const Dispatcher = struct { }; pub fn listen(self: *Server) !void { - if (builtin.os.tag == .windows) return try self.listenWindows(); + if (builtin.os.tag == .windows) return try @import("../windows.zig").listen(self); var httpz_server = try httpz.ServerCtx(Dispatcher, Dispatcher).init( self.allocator, @@ -103,73 +103,6 @@ pub fn errorHandlerFn(self: *Server, request: *httpz.Request, response: *httpz.R response.body = "500 Internal Server Error"; } -// TODO: http.zig windows -pub fn listenWindows(self: *Server) !void { - const address = try std.net.Address.parseIp(self.options.bind, self.options.port); - self.std_net_server = try address.listen(.{ .reuse_port = true }); - - self.initialized = true; - - try self.logger.INFO("Listening on http://{s}:{} [{s}]", .{ - self.options.bind, - self.options.port, - @tagName(self.options.environment), - }); - try self.processRequestsWindows(); -} - -// TODO: http.zig windows -fn processRequestsWindows(self: *Server) !void { - while (true) { - var arena = std.heap.ArenaAllocator.init(self.allocator); - errdefer arena.deinit(); - const allocator = arena.allocator(); - - const connection = try self.std_net_server.accept(); - - var buf: [jetzig.config.get(usize, "http_buffer_size")]u8 = undefined; - var std_http_server = std.http.Server.init(connection, &buf); - errdefer std_http_server.connection.stream.close(); - - self.processNextRequestWindows(allocator, &std_http_server) catch |err| { - if (isBadHttpError(err)) { - std_http_server.connection.stream.close(); - continue; - } else return err; - }; - - std_http_server.connection.stream.close(); - arena.deinit(); - } -} - -// TODO: http.zig windows -fn processNextRequestWindows(self: *Server, allocator: std.mem.Allocator, std_http_server: *std.http.Server) !void { - const start_time = std.time.nanoTimestamp(); - - const std_http_request = try std_http_server.receiveHead(); - if (std_http_server.state == .receiving_head) return error.JetzigParseHeadError; - - var response = try jetzig.http.Response.init(allocator); - var request = try jetzig.http.Request.init(allocator, self, start_time, std_http_request, &response); - - try request.process(); - - var middleware_data = try jetzig.http.middleware.afterRequest(&request); - - try self.renderResponse(&request); - try request.response.headers.append("content-type", response.content_type); - - try jetzig.http.middleware.beforeResponse(&middleware_data, &request); - - try request.respond(); - - try jetzig.http.middleware.afterResponse(&middleware_data, &request); - jetzig.http.middleware.deinit(&middleware_data, &request); - - try self.logger.logRequest(&request); -} - fn processNextRequest( self: *Server, httpz_request: *httpz.Request, @@ -217,7 +150,8 @@ fn processNextRequest( try self.logger.logRequest(&request); } -fn renderResponse(self: *Server, request: *jetzig.http.Request) !void { +// TODO: Make private when http.zig Windows is working +pub fn renderResponse(self: *Server, request: *jetzig.http.Request) !void { const static_resource = self.matchStaticResource(request) catch |err| { if (isUnhandledError(err)) return err; @@ -398,7 +332,8 @@ fn isUnhandledError(err: anyerror) bool { }; } -fn isBadHttpError(err: anyerror) bool { +// TODO: Make private when http.zig Windows is working +pub fn isBadHttpError(err: anyerror) bool { return switch (err) { error.JetzigParseHeadError, error.UnknownHttpMethod, diff --git a/src/jetzig/windows.zig b/src/jetzig/windows.zig new file mode 100644 index 0000000..37085d3 --- /dev/null +++ b/src/jetzig/windows.zig @@ -0,0 +1,67 @@ +const std = @import("std"); +const Server = @import("http/Server.zig"); +const jetzig = @import("../jetzig.zig"); + +pub fn listen(self: *Server) !void { + const address = try std.net.Address.parseIp(self.options.bind, self.options.port); + self.std_net_server = try address.listen(.{ .reuse_port = true }); + + self.initialized = true; + + try self.logger.INFO("Listening on http://{s}:{} [{s}]", .{ + self.options.bind, + self.options.port, + @tagName(self.options.environment), + }); + try processRequests(self); +} + +fn processRequests(self: *Server) !void { + while (true) { + var arena = std.heap.ArenaAllocator.init(self.allocator); + errdefer arena.deinit(); + const allocator = arena.allocator(); + + const connection = try self.std_net_server.accept(); + + var buf: [jetzig.config.get(usize, "http_buffer_size")]u8 = undefined; + var std_http_server = std.http.Server.init(connection, &buf); + errdefer std_http_server.connection.stream.close(); + + processNextRequest(self, allocator, &std_http_server) catch |err| { + if (Server.isBadHttpError(err)) { + std_http_server.connection.stream.close(); + continue; + } else return err; + }; + + std_http_server.connection.stream.close(); + arena.deinit(); + } +} + +fn processNextRequest(self: *Server, allocator: std.mem.Allocator, std_http_server: *std.http.Server) !void { + const start_time = std.time.nanoTimestamp(); + + const std_http_request = try std_http_server.receiveHead(); + if (std_http_server.state == .receiving_head) return error.JetzigParseHeadError; + + var response = try jetzig.http.Response.init(allocator); + var request = try jetzig.http.Request.init(allocator, self, start_time, std_http_request, &response); + + try request.process(); + + var middleware_data = try jetzig.http.middleware.afterRequest(&request); + + try self.renderResponse(&request); + try request.response.headers.append("content-type", response.content_type); + + try jetzig.http.middleware.beforeResponse(&middleware_data, &request); + + try request.respond(); + + try jetzig.http.middleware.afterResponse(&middleware_data, &request); + jetzig.http.middleware.deinit(&middleware_data, &request); + + try self.logger.logRequest(&request); +} diff --git a/src/jetzig/http/RequestWindows.zig b/src/jetzig/windows/Request.zig similarity index 100% rename from src/jetzig/http/RequestWindows.zig rename to src/jetzig/windows/Request.zig From 35c5f26de9ba8bea825b3a82b86f2f1c18b26b25 Mon Sep 17 00:00:00 2001 From: Bob Farrell Date: Wed, 8 May 2024 17:53:48 +0100 Subject: [PATCH 6/6] WIP --- build.zig.zon | 4 +- src/jetzig/http.zig | 5 +- src/jetzig/http/Server.zig | 8 +- src/jetzig/windows/Request.zig | 546 --------------------------------- 4 files changed, 5 insertions(+), 558 deletions(-) delete mode 100644 src/jetzig/windows/Request.zig diff --git a/build.zig.zon b/build.zig.zon index 1435df6..8bef8ae 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -23,8 +23,8 @@ .hash = "1220d4f1c2472769b0d689ea878f41f0a66cb07f28569a138aea2c0a648a5c90dd4e", }, .httpz = .{ - .url = "https://github.com/karlseguin/http.zig/archive/0d4a5cd520a54eaf800438e0b9093c77c90dcf11.tar.gz", - .hash = "12209b8216a80f21be12d43e588811150bdbbb53d35eac6a2a61c460f197350e19ad", + .url = "https://github.com/karlseguin/http.zig/archive/206a34c0ee35a07b89d000f630b2f1e0f7c98119.tar.gz", + .hash = "1220768b5925b4e13f73c036f1ca18b4a7d987ffaf5e825af6443d5d4ed8e37e7dfd", }, }, diff --git a/src/jetzig/http.zig b/src/jetzig/http.zig index 4786f20..e35f325 100644 --- a/src/jetzig/http.zig +++ b/src/jetzig/http.zig @@ -2,10 +2,7 @@ const std = @import("std"); const builtin = @import("builtin"); pub const Server = @import("http/Server.zig"); -pub const Request = if (builtin.os.tag == .windows) - @import("windows/Request.zig") -else - @import("http/Request.zig"); +pub const Request = @import("http/Request.zig"); pub const StaticRequest = @import("http/StaticRequest.zig"); pub const Response = @import("http/Response.zig"); pub const Session = @import("http/Session.zig"); diff --git a/src/jetzig/http/Server.zig b/src/jetzig/http/Server.zig index b8a237f..eb6c470 100644 --- a/src/jetzig/http/Server.zig +++ b/src/jetzig/http/Server.zig @@ -72,8 +72,6 @@ const Dispatcher = struct { }; pub fn listen(self: *Server) !void { - if (builtin.os.tag == .windows) return try @import("../windows.zig").listen(self); - var httpz_server = try httpz.ServerCtx(Dispatcher, Dispatcher).init( self.allocator, .{ @@ -150,8 +148,7 @@ fn processNextRequest( try self.logger.logRequest(&request); } -// TODO: Make private when http.zig Windows is working -pub fn renderResponse(self: *Server, request: *jetzig.http.Request) !void { +fn renderResponse(self: *Server, request: *jetzig.http.Request) !void { const static_resource = self.matchStaticResource(request) catch |err| { if (isUnhandledError(err)) return err; @@ -332,8 +329,7 @@ fn isUnhandledError(err: anyerror) bool { }; } -// TODO: Make private when http.zig Windows is working -pub fn isBadHttpError(err: anyerror) bool { +fn isBadHttpError(err: anyerror) bool { return switch (err) { error.JetzigParseHeadError, error.UnknownHttpMethod, diff --git a/src/jetzig/windows/Request.zig b/src/jetzig/windows/Request.zig deleted file mode 100644 index e260a06..0000000 --- a/src/jetzig/windows/Request.zig +++ /dev/null @@ -1,546 +0,0 @@ -const std = @import("std"); - -const jetzig = @import("../../jetzig.zig"); - -const Request = @This(); -const default_content_type = "text/html"; - -pub const Method = enum { DELETE, GET, PATCH, POST, HEAD, PUT, CONNECT, OPTIONS, TRACE }; -pub const Modifier = enum { edit, new }; -pub const Format = enum { HTML, JSON, UNKNOWN }; - -allocator: std.mem.Allocator, -path: jetzig.http.Path, -method: Method, -headers: jetzig.http.Headers, -server: *jetzig.http.Server, -std_http_request: std.http.Server.Request, -response: *jetzig.http.Response, -status_code: jetzig.http.status_codes.StatusCode = .not_found, -response_data: *jetzig.data.Data, -query_params: ?*jetzig.http.Query = null, -query_body: ?*jetzig.http.Query = null, -cookies: *jetzig.http.Cookies = undefined, -session: *jetzig.http.Session = undefined, -body: []const u8 = undefined, -processed: bool = false, -layout: ?[]const u8 = null, -layout_disabled: bool = false, -rendered: bool = false, -redirected: bool = false, -rendered_multiple: bool = false, -rendered_view: ?jetzig.views.View = null, -start_time: i128, -store: RequestStore, -cache: RequestStore, - -/// Wrapper for KV store that uses the request's arena allocator for fetching values. -pub const RequestStore = struct { - allocator: std.mem.Allocator, - store: *jetzig.kv.Store, - - /// Put a String or into the key-value store. - pub fn get(self: RequestStore, key: []const u8) !?*jetzig.data.Value { - return try self.store.get(try self.data(), key); - } - - /// Get a String from the store. - pub fn put(self: RequestStore, key: []const u8, value: *jetzig.data.Value) !void { - try self.store.put(key, value); - } - - /// Remove a String to from the key-value store and return it if found. - pub fn fetchRemove(self: RequestStore, key: []const u8) !?*jetzig.data.Value { - return try self.store.fetchRemove(try self.data(), key); - } - - /// Remove a String to from the key-value store. - pub fn remove(self: RequestStore, key: []const u8) !void { - try self.store.remove(key); - } - - /// Append a Value to the end of an Array in the key-value store. - pub fn append(self: RequestStore, key: []const u8, value: *jetzig.data.Value) !void { - try self.store.append(key, value); - } - - /// Prepend a Value to the start of an Array in the key-value store. - pub fn prepend(self: RequestStore, key: []const u8, value: *jetzig.data.Value) !void { - try self.store.prepend(key, value); - } - - /// Pop a String from an Array in the key-value store. - pub fn pop(self: RequestStore, key: []const u8) !?*jetzig.data.Value { - return try self.store.pop(try self.data(), key); - } - - /// Left-pop a String from an Array in the key-value store. - pub fn popFirst(self: RequestStore, key: []const u8) !?*jetzig.data.Value { - return try self.store.popFirst(try self.data(), key); - } - - fn data(self: RequestStore) !*jetzig.data.Data { - const arena_data = try self.allocator.create(jetzig.data.Data); - arena_data.* = jetzig.data.Data.init(self.allocator); - return arena_data; - } -}; - -pub fn init( - allocator: std.mem.Allocator, - server: *jetzig.http.Server, - start_time: i128, - std_http_request: std.http.Server.Request, - response: *jetzig.http.Response, -) !Request { - const method = switch (std_http_request.head.method) { - .DELETE => Method.DELETE, - .GET => Method.GET, - .PATCH => Method.PATCH, - .POST => Method.POST, - .HEAD => Method.HEAD, - .PUT => Method.PUT, - .CONNECT => Method.CONNECT, - .OPTIONS => Method.OPTIONS, - .TRACE => Method.TRACE, - _ => return error.JetzigUnsupportedHttpMethod, - }; - - const response_data = try allocator.create(jetzig.data.Data); - response_data.* = jetzig.data.Data.init(allocator); - - return .{ - .allocator = allocator, - .path = jetzig.http.Path.init(std_http_request.head.target), - .method = method, - .headers = jetzig.http.Headers.init(allocator), - .server = server, - .response = response, - .response_data = response_data, - .std_http_request = std_http_request, - .start_time = start_time, - .store = .{ .store = server.store, .allocator = allocator }, - .cache = .{ .store = server.cache, .allocator = allocator }, - }; -} - -pub fn deinit(self: *Request) void { - self.session.deinit(); - self.cookies.deinit(); - self.allocator.destroy(self.cookies); - self.allocator.destroy(self.session); - if (self.processed) self.allocator.free(self.body); -} - -/// Process request, read body if present, parse headers (TODO) -pub fn process(self: *Request) !void { - var headers_it = self.std_http_request.iterateHeaders(); - var cookie: ?[]const u8 = null; - - while (headers_it.next()) |header| { - try self.headers.append(header.name, header.value); - if (std.mem.eql(u8, header.name, "Cookie")) cookie = header.value; - } - - self.cookies = try self.allocator.create(jetzig.http.Cookies); - self.cookies.* = jetzig.http.Cookies.init( - self.allocator, - cookie orelse "", - ); - try self.cookies.parse(); - - self.session = try self.allocator.create(jetzig.http.Session); - self.session.* = jetzig.http.Session.init(self.allocator, self.cookies, self.server.options.secret); - self.session.parse() catch |err| { - switch (err) { - error.JetzigInvalidSessionCookie => { - try self.server.logger.DEBUG("Invalid session cookie detected. Resetting session.", .{}); - try self.session.reset(); - }, - else => return err, - } - }; - - const reader = try self.std_http_request.reader(); - self.body = try reader.readAllAlloc(self.allocator, jetzig.config.get(usize, "max_bytes_request_body")); - self.processed = true; -} - -/// Set response headers, write response payload, and finalize the response. -pub fn respond(self: *Request) !void { - if (!self.processed) unreachable; - - var cookie_it = self.cookies.headerIterator(); - while (try cookie_it.next()) |header| { - // FIXME: Skip setting cookies that are already present ? - try self.response.headers.append("Set-Cookie", header); - } - - var std_response_headers = try self.response.headers.stdHeaders(); - defer std_response_headers.deinit(self.allocator); - - try self.std_http_request.respond( - self.response.content, - .{ - .keep_alive = false, - .status = switch (self.response.status_code) { - inline else => |tag| @field(std.http.Status, @tagName(tag)), - }, - .extra_headers = std_response_headers.items, - }, - ); -} - -/// Render a response. This function can only be called once per request (repeat calls will -/// trigger an error). -pub fn render(self: *Request, status_code: jetzig.http.status_codes.StatusCode) jetzig.views.View { - if (self.rendered) self.rendered_multiple = true; - - self.rendered = true; - self.rendered_view = .{ .data = self.response_data, .status_code = status_code }; - return self.rendered_view.?; -} - -/// Issue a redirect to a new location. -/// ```zig -/// return request.redirect("https://www.example.com/", .moved_permanently); -/// ``` -/// ```zig -/// return request.redirect("https://www.example.com/", .found); -/// ``` -/// The second argument must be `moved_permanently` or `found`. -pub fn redirect( - self: *Request, - location: []const u8, - redirect_status: enum { moved_permanently, found }, -) jetzig.views.View { - if (self.rendered) self.rendered_multiple = true; - - self.rendered = true; - self.redirected = true; - - const status_code = switch (redirect_status) { - .moved_permanently => jetzig.http.status_codes.StatusCode.moved_permanently, - .found => jetzig.http.status_codes.StatusCode.found, - }; - - self.response_data.reset(); - - self.response.headers.remove("Location"); - self.response.headers.append("Location", location) catch @panic("OOM"); - - self.rendered_view = .{ .data = self.response_data, .status_code = status_code }; - return self.rendered_view.?; -} - -/// Infer the current format (JSON or HTML) from the request in this order: -/// * Extension (path ends in `.json` or `.html`) -/// * `Accept` header (`application/json` or `text/html`) -/// * `Content-Type` header (`application/json` or `text/html`) -/// * Fall back to default: HTML -pub fn requestFormat(self: *const Request) jetzig.http.Request.Format { - return self.extensionFormat() orelse - self.acceptHeaderFormat() orelse - self.contentTypeHeaderFormat() orelse - .UNKNOWN; -} - -/// Set the layout for the current request/response. Use this to override a `pub const layout` -/// declaration in a view, either in middleware or in a view function itself. -pub fn setLayout(self: *Request, layout: ?[]const u8) void { - if (layout) |layout_name| { - self.layout = layout_name; - self.layout_disabled = false; - } else { - self.layout_disabled = true; - } -} - -/// Derive a layout name from the current request if defined, otherwise from the route (if -/// defined). -pub fn getLayout(self: *Request, route: *jetzig.views.Route) ?[]const u8 { - if (self.layout_disabled) return null; - if (self.layout) |capture| return capture; - if (route.layout) |capture| return capture; - - return null; -} - -/// Shortcut for `request.headers.getFirstValue`. Returns the first matching value for a given -/// header name or `null` if not found. Header names are case-insensitive. -pub fn getHeader(self: *const Request, key: []const u8) ?[]const u8 { - return self.headers.getFirstValue(key); -} - -/// Return a `Value` representing request parameters. Parameters are normalized, meaning that -/// both the JSON request body and query parameters are accessed via the same interface. -/// Note that query parameters are supported for JSON requests if no request body is present, -/// otherwise the parsed JSON request body will take precedence and query parameters will be -/// ignored. -pub fn params(self: *Request) !*jetzig.data.Value { - if (!self.processed) unreachable; - - switch (self.requestFormat()) { - .JSON => { - if (self.body.len == 0) return self.queryParams(); - - var data = try self.allocator.create(jetzig.data.Data); - data.* = jetzig.data.Data.init(self.allocator); - data.fromJson(self.body) catch |err| { - switch (err) { - error.SyntaxError, error.UnexpectedEndOfInput => return error.JetzigBodyParseError, - else => return err, - } - }; - return data.value.?; - }, - .HTML, .UNKNOWN => return self.parseQuery(), - } -} - -/// Return a `*Value` representing request parameters. This function **always** returns the -/// parsed query string and never the request body. -pub fn queryParams(self: *Request) !*jetzig.data.Value { - if (self.query_params) |parsed| return parsed.data.value.?; - - const data = try self.allocator.create(jetzig.data.Data); - data.* = jetzig.data.Data.init(self.allocator); - self.query_params = try self.allocator.create(jetzig.http.Query); - self.query_params.?.* = jetzig.http.Query.init( - self.allocator, - self.path.query orelse "", - data, - ); - try self.query_params.?.parse(); - return self.query_params.?.data.value.?; -} - -// Parses request body as params if present, otherwise delegates to `queryParams`. -fn parseQuery(self: *Request) !*jetzig.data.Value { - if (self.body.len == 0) return try self.queryParams(); - if (self.query_body) |parsed| return parsed.data.value.?; - - const data = try self.allocator.create(jetzig.data.Data); - data.* = jetzig.data.Data.init(self.allocator); - self.query_body = try self.allocator.create(jetzig.http.Query); - self.query_body.?.* = jetzig.http.Query.init( - self.allocator, - self.body, - data, - ); - try self.query_body.?.parse(); - return self.query_body.?.data.value.?; -} - -/// Creates a new Job. Receives a job name which must resolve to `src/app/jobs/.zig` -/// Call `Job.put(...)` to set job params. -/// Call `Job.background()` to run the job outside of the request/response flow. -/// e.g.: -/// ``` -/// pub fn post(request: *jetzig.Request, data: *jetzig.Data) !jetzig.View { -/// var job = try request.job("foo"); // Will invoke `process()` in `src/app/jobs/foo.zig` -/// try job.put("foo", data.string("bar")); -/// try job.background(); // Job added to queue and processed by job worker. -/// return request.render(.ok); -/// } -/// ``` -pub fn job(self: *Request, job_name: []const u8) !*jetzig.Job { - const background_job = try self.allocator.create(jetzig.Job); - background_job.* = jetzig.Job.init( - self.allocator, - self.server.store, - self.server.job_queue, - self.server.cache, - self.server.logger, - self.server.job_definitions, - job_name, - ); - return background_job; -} - -const RequestMail = struct { - request: *Request, - mail_params: jetzig.mail.MailParams, - name: []const u8, - - // Will allow scheduling when strategy is `.later` (e.g.). - const DeliveryOptions = struct {}; - - pub fn deliver(self: RequestMail, strategy: enum { background, now }, options: DeliveryOptions) !void { - _ = options; - var mail_job = try self.request.job("__jetzig_mail"); - - try mail_job.params.put("mailer_name", mail_job.data.string(self.name)); - - const from = if (self.mail_params.from) |from| mail_job.data.string(from) else null; - try mail_job.params.put("from", from); - - var to_array = try mail_job.data.array(); - if (self.mail_params.to) |capture| { - for (capture) |to| try to_array.append(mail_job.data.string(to)); - } - try mail_job.params.put("to", to_array); - - const subject = if (self.mail_params.subject) |subject| mail_job.data.string(subject) else null; - try mail_job.params.put("subject", subject); - - const html = if (self.mail_params.html) |html| mail_job.data.string(html) else null; - try mail_job.params.put("html", html); - - const text = if (self.mail_params.text) |text| mail_job.data.string(text) else null; - try mail_job.params.put("text", text); - - if (self.request.response_data.value) |value| try mail_job.params.put( - "params", - if (strategy == .now) try value.clone(self.request.allocator) else value, - ); - - switch (strategy) { - .background => try mail_job.schedule(), - .now => try mail_job.definition.?.runFn( - self.request.allocator, - mail_job.params, - jetzig.jobs.JobEnv{ - .environment = self.request.server.options.environment, - .logger = self.request.server.logger, - .routes = self.request.server.routes, - .mailers = self.request.server.mailer_definitions, - .jobs = self.request.server.job_definitions, - .store = self.request.server.store, - .cache = self.request.server.cache, - .mutex = undefined, - }, - ), - } - } -}; - -pub fn mail(self: *Request, name: []const u8, mail_params: jetzig.mail.MailParams) RequestMail { - return .{ - .request = self, - .name = name, - .mail_params = mail_params, - }; -} - -fn extensionFormat(self: *const Request) ?jetzig.http.Request.Format { - const extension = self.path.extension orelse return null; - if (std.mem.eql(u8, extension, ".html")) { - return .HTML; - } else if (std.mem.eql(u8, extension, ".json")) { - return .JSON; - } else { - return null; - } -} - -pub fn acceptHeaderFormat(self: *const Request) ?jetzig.http.Request.Format { - const acceptHeader = self.getHeader("Accept"); - - if (acceptHeader) |item| { - if (std.mem.eql(u8, item, "text/html")) return .HTML; - if (std.mem.eql(u8, item, "application/json")) return .JSON; - } - - return null; -} - -pub fn contentTypeHeaderFormat(self: *const Request) ?jetzig.http.Request.Format { - const acceptHeader = self.getHeader("content-type"); - - if (acceptHeader) |item| { - if (std.mem.eql(u8, item, "text/html")) return .HTML; - if (std.mem.eql(u8, item, "application/json")) return .JSON; - } - - return null; -} - -pub fn hash(self: *Request) ![]const u8 { - return try std.fmt.allocPrint( - self.allocator, - "{s}-{s}-{s}", - .{ @tagName(self.method), self.path, @tagName(self.requestFormat()) }, - ); -} - -pub fn fmtMethod(self: *const Request, colorized: bool) []const u8 { - if (!colorized) return @tagName(self.method); - - return switch (self.method) { - .GET => jetzig.colors.cyan("GET"), - .PUT => jetzig.colors.yellow("PUT"), - .PATCH => jetzig.colors.yellow("PATCH"), - .HEAD => jetzig.colors.white("HEAD"), - .POST => jetzig.colors.green("POST"), - .DELETE => jetzig.colors.red("DELETE"), - inline else => |method| jetzig.colors.white(@tagName(method)), - }; -} - -/// Format a status code appropriately for the current request format. -/// e.g. `.HTML` => `404 Not Found` -/// `.JSON` => `{ "message": "Not Found", "status": "404" }` -pub fn formatStatus(self: *const Request, status_code: jetzig.http.StatusCode) ![]const u8 { - const status = jetzig.http.status_codes.get(status_code); - - return switch (self.requestFormat()) { - .JSON => try std.json.stringifyAlloc(self.allocator, .{ - .@"error" = .{ - .message = status.getMessage(), - .code = status.getCode(), - }, - }, .{}), - .HTML, .UNKNOWN => status.getFormatted(.{ .linebreak = true }), - }; -} - -pub fn setResponse( - self: *Request, - rendered_view: jetzig.http.Server.RenderedView, - options: struct { content_type: ?[]const u8 = null }, -) void { - self.response.content = rendered_view.content; - self.response.status_code = rendered_view.view.status_code; - self.response.content_type = options.content_type orelse switch (self.requestFormat()) { - .HTML, .UNKNOWN => "text/html", - .JSON => "application/json", - }; -} - -// Determine if a given route matches the current request. -pub fn match(self: *Request, route: jetzig.views.Route) !bool { - return switch (self.method) { - .GET => switch (route.action) { - .index => self.isMatch(.exact, route), - .get => self.isMatch(.resource_id, route), - else => false, - }, - .POST => switch (route.action) { - .post => self.isMatch(.exact, route), - else => false, - }, - .PUT => switch (route.action) { - .put => self.isMatch(.resource_id, route), - else => false, - }, - .PATCH => switch (route.action) { - .patch => self.isMatch(.resource_id, route), - else => false, - }, - .DELETE => switch (route.action) { - .delete => self.isMatch(.resource_id, route), - else => false, - }, - .HEAD, .CONNECT, .OPTIONS, .TRACE => false, - }; -} - -fn isMatch(self: *Request, match_type: enum { exact, resource_id }, route: jetzig.views.Route) bool { - const path = switch (match_type) { - .exact => self.path.base_path, - .resource_id => self.path.directory, - }; - - return std.mem.eql(u8, path, route.uri_path); -}