mirror of
https://github.com/jetzig-framework/jetzig.git
synced 2025-07-01 05:26:07 +00:00
Closes #108: Anti-CSRF middleware
Add to middleware in app's `src/main.zig`: ```zig pub const jetzig_options = struct { pub const middleware: []const type = &.{ jetzig.middleware.AntiCsrfMiddleware, }; }; ``` CSRF token available in Zmpl templates: ``` {{context.authenticityToken()}} ``` or render a hidden form element: ``` {{context.authenticityFormElement()}} ``` The following HTML requests are rejected (403 Forbidden) if the submitted query param does not match the value stored in the encrypted session (added automatically when the token is generated for a template value): * POST * PUT * PATCH * DELETE JSON requests are not impacted - users should either disable JSON endpoints or implement a different authentication method to protect them.
This commit is contained in:
parent
d887cd5bd8
commit
6e6f1bec1b
2
.github/workflows/CI.yml
vendored
2
.github/workflows/CI.yml
vendored
@ -45,7 +45,7 @@ jobs:
|
|||||||
- name: Run App Tests
|
- name: Run App Tests
|
||||||
run: |
|
run: |
|
||||||
cd demo
|
cd demo
|
||||||
zig build jetzig:test
|
zig build -Denvironment=testing jetzig:test
|
||||||
|
|
||||||
- name: Build artifacts
|
- name: Build artifacts
|
||||||
if: ${{ matrix.os == 'ubuntu-latest' }}
|
if: ${{ matrix.os == 'ubuntu-latest' }}
|
||||||
|
21
build.zig
21
build.zig
@ -109,6 +109,7 @@ pub fn build(b: *std.Build) !void {
|
|||||||
main_tests.root_module.addImport("smtp", smtp_client_dep.module("smtp_client"));
|
main_tests.root_module.addImport("smtp", smtp_client_dep.module("smtp_client"));
|
||||||
const test_build_options = b.addOptions();
|
const test_build_options = b.addOptions();
|
||||||
test_build_options.addOption(Environment, "environment", .testing);
|
test_build_options.addOption(Environment, "environment", .testing);
|
||||||
|
test_build_options.addOption(bool, "build_static", true);
|
||||||
const run_main_tests = b.addRunArtifact(main_tests);
|
const run_main_tests = b.addRunArtifact(main_tests);
|
||||||
main_tests.root_module.addOptions("build_options", test_build_options);
|
main_tests.root_module.addOptions("build_options", test_build_options);
|
||||||
|
|
||||||
@ -137,6 +138,11 @@ pub fn jetzigInit(b: *std.Build, exe: *std.Build.Step.Compile, options: JetzigIn
|
|||||||
"environment",
|
"environment",
|
||||||
"Jetzig server environment.",
|
"Jetzig server environment.",
|
||||||
) orelse .development;
|
) orelse .development;
|
||||||
|
const build_static = b.option(
|
||||||
|
bool,
|
||||||
|
"build_static",
|
||||||
|
"Pre-render static routes. [default: false in development, true in testing/production]",
|
||||||
|
) orelse (environment != .development);
|
||||||
|
|
||||||
const jetzig_dep = b.dependency(
|
const jetzig_dep = b.dependency(
|
||||||
"jetzig",
|
"jetzig",
|
||||||
@ -164,6 +170,7 @@ pub fn jetzigInit(b: *std.Build, exe: *std.Build.Step.Compile, options: JetzigIn
|
|||||||
|
|
||||||
const build_options = b.addOptions();
|
const build_options = b.addOptions();
|
||||||
build_options.addOption(Environment, "environment", environment);
|
build_options.addOption(Environment, "environment", environment);
|
||||||
|
build_options.addOption(bool, "build_static", build_static);
|
||||||
jetzig_module.addOptions("build_options", build_options);
|
jetzig_module.addOptions("build_options", build_options);
|
||||||
|
|
||||||
exe.root_module.addImport("jetzig", jetzig_module);
|
exe.root_module.addImport("jetzig", jetzig_module);
|
||||||
@ -253,15 +260,23 @@ pub fn jetzigInit(b: *std.Build, exe: *std.Build.Step.Compile, options: JetzigIn
|
|||||||
exe_static_routes.root_module.addImport("zmpl", zmpl_module);
|
exe_static_routes.root_module.addImport("zmpl", zmpl_module);
|
||||||
|
|
||||||
const markdown_fragments_write_files = b.addWriteFiles();
|
const markdown_fragments_write_files = b.addWriteFiles();
|
||||||
const path = markdown_fragments_write_files.add("markdown_fragments.zig", try generateMarkdownFragments(b));
|
const path = markdown_fragments_write_files.add(
|
||||||
|
"markdown_fragments.zig",
|
||||||
|
try generateMarkdownFragments(b),
|
||||||
|
);
|
||||||
const markdown_fragments_module = b.createModule(.{ .root_source_file = path });
|
const markdown_fragments_module = b.createModule(.{ .root_source_file = path });
|
||||||
exe_static_routes.root_module.addImport("markdown_fragments", markdown_fragments_module);
|
exe_static_routes.root_module.addImport("markdown_fragments", markdown_fragments_module);
|
||||||
|
|
||||||
const run_static_routes_cmd = b.addRunArtifact(exe_static_routes);
|
const run_static_routes_cmd = b.addRunArtifact(exe_static_routes);
|
||||||
const static_outputs_path = run_static_routes_cmd.addOutputFileArg("static.zig");
|
const static_outputs_path = run_static_routes_cmd.addOutputFileArg("static.zig");
|
||||||
const static_module = b.createModule(.{ .root_source_file = static_outputs_path });
|
const static_module = if (build_static)
|
||||||
exe.root_module.addImport("static", static_module);
|
b.createModule(.{ .root_source_file = static_outputs_path })
|
||||||
|
else
|
||||||
|
b.createModule(.{
|
||||||
|
.root_source_file = jetzig_dep.builder.path("src/jetzig/development_static.zig"),
|
||||||
|
});
|
||||||
|
|
||||||
|
exe.root_module.addImport("static", static_module);
|
||||||
run_static_routes_cmd.expectExitCode(0);
|
run_static_routes_cmd.expectExitCode(0);
|
||||||
|
|
||||||
const run_tests_file_cmd = b.addRunArtifact(exe_routes_file);
|
const run_tests_file_cmd = b.addRunArtifact(exe_routes_file);
|
||||||
|
@ -7,8 +7,8 @@
|
|||||||
.hash = "1220d0e8734628fd910a73146e804d10a3269e3e7d065de6bb0e3e88d5ba234eb163",
|
.hash = "1220d0e8734628fd910a73146e804d10a3269e3e7d065de6bb0e3e88d5ba234eb163",
|
||||||
},
|
},
|
||||||
.zmpl = .{
|
.zmpl = .{
|
||||||
.url = "https://github.com/jetzig-framework/zmpl/archive/25b91d030b992631d319adde1cf01baecd9f3934.tar.gz",
|
.url = "https://github.com/jetzig-framework/zmpl/archive/af75c8b842c3957eb97b4fc4bc49c7b2243968fa.tar.gz",
|
||||||
.hash = "12208dd5a4bf0c6c7efc4e9f37a5d8ed80d6004d5680176d1fc2114bfa593e927baf",
|
.hash = "1220ecac93d295dafd2f034a86f0979f6108d40e5ea1a39e3a2b9977c35147cac684",
|
||||||
},
|
},
|
||||||
.jetkv = .{
|
.jetkv = .{
|
||||||
.url = "https://github.com/jetzig-framework/jetkv/archive/2b1130a48979ea2871c8cf6ca89c38b1e7062839.tar.gz",
|
.url = "https://github.com/jetzig-framework/jetkv/archive/2b1130a48979ea2871c8cf6ca89c38b1e7062839.tar.gz",
|
||||||
|
77
demo/src/app/views/anti_csrf.zig
Normal file
77
demo/src/app/views/anti_csrf.zig
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const jetzig = @import("jetzig");
|
||||||
|
|
||||||
|
pub const layout = "application";
|
||||||
|
|
||||||
|
pub const actions = .{
|
||||||
|
.before = .{jetzig.middleware.AntiCsrfMiddleware},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn post(request: *jetzig.Request) !jetzig.View {
|
||||||
|
var root = try request.data(.object);
|
||||||
|
|
||||||
|
const Params = struct { spam: []const u8 };
|
||||||
|
const params = try request.expectParams(Params) orelse {
|
||||||
|
return request.fail(.unprocessable_entity);
|
||||||
|
};
|
||||||
|
|
||||||
|
try root.put("spam", params.spam);
|
||||||
|
|
||||||
|
return request.render(.created);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn index(request: *jetzig.Request) !jetzig.View {
|
||||||
|
return request.render(.ok);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "post with missing token" {
|
||||||
|
var app = try jetzig.testing.app(std.testing.allocator, @import("routes"));
|
||||||
|
defer app.deinit();
|
||||||
|
|
||||||
|
const response = try app.request(.POST, "/anti_csrf", .{});
|
||||||
|
try response.expectStatus(.forbidden);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "post with invalid token" {
|
||||||
|
var app = try jetzig.testing.app(std.testing.allocator, @import("routes"));
|
||||||
|
defer app.deinit();
|
||||||
|
|
||||||
|
const response = try app.request(.POST, "/anti_csrf", .{});
|
||||||
|
try response.expectStatus(.forbidden);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "post with valid token but missing expected params" {
|
||||||
|
var app = try jetzig.testing.app(std.testing.allocator, @import("routes"));
|
||||||
|
defer app.deinit();
|
||||||
|
|
||||||
|
_ = try app.request(.GET, "/anti_csrf", .{});
|
||||||
|
const token = app.session.getT(.string, jetzig.authenticity_token_name).?;
|
||||||
|
const response = try app.request(
|
||||||
|
.POST,
|
||||||
|
"/anti_csrf",
|
||||||
|
.{ .params = .{ ._jetzig_authenticity_token = token } },
|
||||||
|
);
|
||||||
|
try response.expectStatus(.unprocessable_entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "post with valid token and expected params" {
|
||||||
|
var app = try jetzig.testing.app(std.testing.allocator, @import("routes"));
|
||||||
|
defer app.deinit();
|
||||||
|
|
||||||
|
_ = try app.request(.GET, "/anti_csrf", .{});
|
||||||
|
const token = app.session.getT(.string, jetzig.authenticity_token_name).?;
|
||||||
|
const response = try app.request(
|
||||||
|
.POST,
|
||||||
|
"/anti_csrf",
|
||||||
|
.{ .params = .{ ._jetzig_authenticity_token = token, .spam = "Spam" } },
|
||||||
|
);
|
||||||
|
try response.expectStatus(.created);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "index" {
|
||||||
|
var app = try jetzig.testing.app(std.testing.allocator, @import("routes"));
|
||||||
|
defer app.deinit();
|
||||||
|
|
||||||
|
const response = try app.request(.GET, "/anti_csrf", .{});
|
||||||
|
try response.expectStatus(.ok);
|
||||||
|
}
|
8
demo/src/app/views/anti_csrf/index.zmpl
Normal file
8
demo/src/app/views/anti_csrf/index.zmpl
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<form action="/anti_csrf" method="POST">
|
||||||
|
{{context.authenticityFormElement()}}
|
||||||
|
|
||||||
|
<label>Enter spam here:</label>
|
||||||
|
<input type="text" name="spam" />
|
||||||
|
|
||||||
|
<input type="submit" value="Submit Spam" />
|
||||||
|
</form>
|
5
demo/src/app/views/anti_csrf/post.zmpl
Normal file
5
demo/src/app/views/anti_csrf/post.zmpl
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<h1>Spam Submitted Successfully</h1>
|
||||||
|
|
||||||
|
<h2>Spam:</h2>
|
||||||
|
|
||||||
|
<div>{{$.spam}}</div>
|
@ -12,6 +12,8 @@ pub const jetzig_options = struct {
|
|||||||
/// Middleware chain. Add any custom middleware here, or use middleware provided in
|
/// Middleware chain. Add any custom middleware here, or use middleware provided in
|
||||||
/// `jetzig.middleware` (e.g. `jetzig.middleware.HtmxMiddleware`).
|
/// `jetzig.middleware` (e.g. `jetzig.middleware.HtmxMiddleware`).
|
||||||
pub const middleware: []const type = &.{
|
pub const middleware: []const type = &.{
|
||||||
|
// jetzig.middleware.AuthMiddleware,
|
||||||
|
// jetzig.middleware.AntiCsrfMiddleware,
|
||||||
// jetzig.middleware.HtmxMiddleware,
|
// jetzig.middleware.HtmxMiddleware,
|
||||||
// jetzig.middleware.CompressionMiddleware,
|
// jetzig.middleware.CompressionMiddleware,
|
||||||
// @import("app/middleware/DemoMiddleware.zig"),
|
// @import("app/middleware/DemoMiddleware.zig"),
|
||||||
@ -79,13 +81,16 @@ pub const jetzig_options = struct {
|
|||||||
pub const Schema = @import("Schema");
|
pub const Schema = @import("Schema");
|
||||||
|
|
||||||
/// HTTP cookie configuration
|
/// HTTP cookie configuration
|
||||||
pub const cookies: jetzig.http.Cookies.CookieOptions = .{
|
pub const cookies: jetzig.http.Cookies.CookieOptions = switch (jetzig.environment) {
|
||||||
.domain = switch (jetzig.environment) {
|
.development, .testing => .{
|
||||||
.development => "localhost",
|
.domain = "localhost",
|
||||||
.testing => "localhost",
|
.path = "/",
|
||||||
.production => "www.example.com",
|
},
|
||||||
|
.production => .{
|
||||||
|
.same_site = true,
|
||||||
|
.secure = true,
|
||||||
|
.http_only = true,
|
||||||
},
|
},
|
||||||
.path = "/",
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Key-value store options. Set backend to `.file` to use a file-based store.
|
/// Key-value store options. Set backend to `.file` to use a file-based store.
|
||||||
|
@ -280,6 +280,8 @@ fn writeRoute(self: *Routes, writer: std.ArrayList(u8).Writer, route: Function)
|
|||||||
\\ .static = {4s},
|
\\ .static = {4s},
|
||||||
\\ .uri_path = "{5s}",
|
\\ .uri_path = "{5s}",
|
||||||
\\ .template = "{6s}",
|
\\ .template = "{6s}",
|
||||||
|
\\ .before_callbacks = jetzig.callbacks.beforeCallbacks(@import("{7s}")),
|
||||||
|
\\ .after_callbacks = jetzig.callbacks.afterCallbacks(@import("{7s}")),
|
||||||
\\ .layout = if (@hasDecl(@import("{7s}"), "layout")) @import("{7s}").layout else null,
|
\\ .layout = if (@hasDecl(@import("{7s}"), "layout")) @import("{7s}").layout else null,
|
||||||
\\ .json_params = &[_][]const u8 {{ {8s} }},
|
\\ .json_params = &[_][]const u8 {{ {8s} }},
|
||||||
\\ .formats = if (@hasDecl(@import("{7s}"), "formats")) @import("{7s}").formats else null,
|
\\ .formats = if (@hasDecl(@import("{7s}"), "formats")) @import("{7s}").formats else null,
|
||||||
@ -389,7 +391,7 @@ fn generateRoutesForView(self: *Routes, dir: std.fs.Dir, path: []const u8) !Rout
|
|||||||
|
|
||||||
for (capture.args, 0..) |arg, arg_index| {
|
for (capture.args, 0..) |arg, arg_index| {
|
||||||
if (std.mem.eql(u8, try arg.typeBasename(), "StaticRequest")) {
|
if (std.mem.eql(u8, try arg.typeBasename(), "StaticRequest")) {
|
||||||
capture.static = true;
|
capture.static = jetzig.build_options.build_static;
|
||||||
capture.legacy = arg_index + 1 < capture.args.len;
|
capture.legacy = arg_index + 1 < capture.args.len;
|
||||||
try static_routes.append(capture.*);
|
try static_routes.append(capture.*);
|
||||||
} else if (std.mem.eql(u8, try arg.typeBasename(), "Request")) {
|
} else if (std.mem.eql(u8, try arg.typeBasename(), "Request")) {
|
||||||
|
@ -13,7 +13,7 @@ pub fn main() !void {
|
|||||||
|
|
||||||
log("Jetzig Routes:", .{});
|
log("Jetzig Routes:", .{});
|
||||||
|
|
||||||
const environment = jetzig.Environment.init(allocator, .{ .silent = true });
|
const environment = try jetzig.Environment.init(allocator, .{ .silent = true });
|
||||||
const initHook: ?*const fn (*jetzig.App) anyerror!void = if (@hasDecl(app, "init")) app.init else null;
|
const initHook: ?*const fn (*jetzig.App) anyerror!void = if (@hasDecl(app, "init")) app.init else null;
|
||||||
|
|
||||||
inline for (routes.routes) |route| max_uri_path_len = @max(route.uri_path.len + 5, max_uri_path_len);
|
inline for (routes.routes) |route| max_uri_path_len = @max(route.uri_path.len + 5, max_uri_path_len);
|
||||||
@ -44,7 +44,7 @@ pub fn main() !void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var jetzig_app = jetzig.App{
|
var jetzig_app = jetzig.App{
|
||||||
.environment = environment,
|
.env = environment,
|
||||||
.allocator = allocator,
|
.allocator = allocator,
|
||||||
.custom_routes = std.ArrayList(jetzig.views.Route).init(allocator),
|
.custom_routes = std.ArrayList(jetzig.views.Route).init(allocator),
|
||||||
.initHook = initHook,
|
.initHook = initHook,
|
||||||
|
@ -147,7 +147,7 @@ fn renderMarkdown(
|
|||||||
|
|
||||||
if (zmpl.findPrefixed("views", prefixed_name)) |layout| {
|
if (zmpl.findPrefixed("views", prefixed_name)) |layout| {
|
||||||
view.data.content = .{ .data = content };
|
view.data.content = .{ .data = content };
|
||||||
return try layout.render(view.data);
|
return try layout.render(view.data, jetzig.TemplateContext, .{}, .{});
|
||||||
} else {
|
} else {
|
||||||
std.debug.print("Unknown layout: {s}\n", .{layout_name});
|
std.debug.print("Unknown layout: {s}\n", .{layout_name});
|
||||||
return content;
|
return content;
|
||||||
@ -170,13 +170,18 @@ fn renderZmplTemplate(
|
|||||||
defer allocator.free(prefixed_name);
|
defer allocator.free(prefixed_name);
|
||||||
|
|
||||||
if (zmpl.findPrefixed("views", prefixed_name)) |layout| {
|
if (zmpl.findPrefixed("views", prefixed_name)) |layout| {
|
||||||
return try template.renderWithOptions(view.data, .{ .layout = layout });
|
return try template.render(
|
||||||
|
view.data,
|
||||||
|
jetzig.TemplateContext,
|
||||||
|
.{},
|
||||||
|
.{ .layout = layout },
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
std.debug.print("Unknown layout: {s}\n", .{layout_name});
|
std.debug.print("Unknown layout: {s}\n", .{layout_name});
|
||||||
return try allocator.dupe(u8, "");
|
return try allocator.dupe(u8, "");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return try template.render(view.data);
|
return try template.render(view.data, jetzig.TemplateContext, .{}, .{});
|
||||||
}
|
}
|
||||||
} else return null;
|
} else return null;
|
||||||
}
|
}
|
||||||
|
@ -22,11 +22,15 @@ pub const database = @import("jetzig/database.zig");
|
|||||||
pub const testing = @import("jetzig/testing.zig");
|
pub const testing = @import("jetzig/testing.zig");
|
||||||
pub const config = @import("jetzig/config.zig");
|
pub const config = @import("jetzig/config.zig");
|
||||||
pub const auth = @import("jetzig/auth.zig");
|
pub const auth = @import("jetzig/auth.zig");
|
||||||
|
pub const callbacks = @import("jetzig/callbacks.zig");
|
||||||
|
pub const TemplateContext = @import("jetzig/TemplateContext.zig");
|
||||||
|
|
||||||
pub const DateTime = jetcommon.types.DateTime;
|
pub const DateTime = jetcommon.types.DateTime;
|
||||||
pub const Time = jetcommon.types.Time;
|
pub const Time = jetcommon.types.Time;
|
||||||
pub const Date = jetcommon.types.Date;
|
pub const Date = jetcommon.types.Date;
|
||||||
|
|
||||||
|
pub const authenticity_token_name = config.get([]const u8, "authenticity_token_name");
|
||||||
|
|
||||||
pub const build_options = @import("build_options");
|
pub const build_options = @import("build_options");
|
||||||
pub const environment = std.enums.nameCast(Environment.EnvironmentName, build_options.environment);
|
pub const environment = std.enums.nameCast(Environment.EnvironmentName, build_options.environment);
|
||||||
|
|
||||||
@ -46,6 +50,9 @@ pub const Request = http.Request;
|
|||||||
/// requests.
|
/// requests.
|
||||||
pub const StaticRequest = http.StaticRequest;
|
pub const StaticRequest = http.StaticRequest;
|
||||||
|
|
||||||
|
/// An HTTP response generated during request processing.
|
||||||
|
pub const Response = http.Response;
|
||||||
|
|
||||||
/// Generic, JSON-compatible data type. Provides `Value` which in turn provides `Object`,
|
/// Generic, JSON-compatible data type. Provides `Value` which in turn provides `Object`,
|
||||||
/// `Array`, `String`, `Integer`, `Float`, `Boolean`, and `NullType`.
|
/// `Array`, `String`, `Integer`, `Float`, `Boolean`, and `NullType`.
|
||||||
pub const Data = data.Data;
|
pub const Data = data.Data;
|
||||||
@ -78,7 +85,8 @@ pub const Logger = loggers.Logger;
|
|||||||
|
|
||||||
pub const root = @import("root");
|
pub const root = @import("root");
|
||||||
pub const Global = if (@hasDecl(root, "Global")) root.Global else DefaultGlobal;
|
pub const Global = if (@hasDecl(root, "Global")) root.Global else DefaultGlobal;
|
||||||
pub const DefaultGlobal = struct { __jetzig_default: bool };
|
pub const DefaultGlobal = struct { comptime __jetzig_default: bool = true };
|
||||||
|
pub const default_global = DefaultGlobal{};
|
||||||
|
|
||||||
pub const initHook: ?*const fn (*App) anyerror!void = if (@hasDecl(root, "init")) root.init else null;
|
pub const initHook: ?*const fn (*App) anyerror!void = if (@hasDecl(root, "init")) root.init else null;
|
||||||
|
|
||||||
|
@ -17,8 +17,8 @@ pub fn deinit(self: *const App) void {
|
|||||||
@constCast(self).custom_routes.deinit();
|
@constCast(self).custom_routes.deinit();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Not used yet, but allows us to add new options to `start()` without breaking
|
/// Specify a global value accessible as `request.server.global`.
|
||||||
// backward-compatibility.
|
/// Must specify type by defining `pub const Global` in your app's `src/main.zig`.
|
||||||
const AppOptions = struct {
|
const AppOptions = struct {
|
||||||
global: *anyopaque = undefined,
|
global: *anyopaque = undefined,
|
||||||
};
|
};
|
||||||
@ -228,6 +228,8 @@ pub fn createRoutes(
|
|||||||
.template = const_route.template,
|
.template = const_route.template,
|
||||||
.json_params = const_route.json_params,
|
.json_params = const_route.json_params,
|
||||||
.formats = const_route.formats,
|
.formats = const_route.formats,
|
||||||
|
.before_callbacks = const_route.before_callbacks,
|
||||||
|
.after_callbacks = const_route.after_callbacks,
|
||||||
};
|
};
|
||||||
|
|
||||||
try var_route.initParams(allocator);
|
try var_route.initParams(allocator);
|
||||||
|
25
src/jetzig/TemplateContext.zig
Normal file
25
src/jetzig/TemplateContext.zig
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
pub const http = @import("http.zig");
|
||||||
|
pub const config = @import("config.zig");
|
||||||
|
|
||||||
|
/// Context available in every Zmpl template as `context`.
|
||||||
|
pub const TemplateContext = @This();
|
||||||
|
|
||||||
|
request: ?*http.Request = null,
|
||||||
|
|
||||||
|
pub fn authenticityToken(self: TemplateContext) !?[]const u8 {
|
||||||
|
return if (self.request) |request|
|
||||||
|
try request.authenticityToken()
|
||||||
|
else
|
||||||
|
null;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn authenticityFormElement(self: TemplateContext) !?[]const u8 {
|
||||||
|
return if (self.request) |request| blk: {
|
||||||
|
const token = try request.authenticityToken();
|
||||||
|
break :blk try std.fmt.allocPrint(request.allocator,
|
||||||
|
\\<input type="hidden" name="{s}" value="{s}" />
|
||||||
|
, .{ config.get([]const u8, "authenticity_token_name"), token });
|
||||||
|
} else null;
|
||||||
|
}
|
112
src/jetzig/callbacks.zig
Normal file
112
src/jetzig/callbacks.zig
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
const jetzig = @import("../jetzig.zig");
|
||||||
|
|
||||||
|
pub const BeforeCallback = *const fn (
|
||||||
|
*jetzig.http.Request,
|
||||||
|
jetzig.views.Route,
|
||||||
|
) anyerror!void;
|
||||||
|
|
||||||
|
pub const AfterCallback = *const fn (
|
||||||
|
*jetzig.http.Request,
|
||||||
|
*jetzig.http.Response,
|
||||||
|
jetzig.views.Route,
|
||||||
|
) anyerror!void;
|
||||||
|
|
||||||
|
pub const Context = enum { before, after };
|
||||||
|
|
||||||
|
pub fn beforeCallbacks(view: type) []const BeforeCallback {
|
||||||
|
comptime {
|
||||||
|
return buildCallbacks(.before, view);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn afterCallbacks(view: type) []const AfterCallback {
|
||||||
|
comptime {
|
||||||
|
return buildCallbacks(.after, view);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn buildCallbacks(comptime context: Context, view: type) switch (context) {
|
||||||
|
.before => []const BeforeCallback,
|
||||||
|
.after => []const AfterCallback,
|
||||||
|
} {
|
||||||
|
comptime {
|
||||||
|
if (!@hasDecl(view, "actions")) return &.{};
|
||||||
|
if (!@hasField(@TypeOf(view.actions), @tagName(context))) return &.{};
|
||||||
|
|
||||||
|
var size: usize = 0;
|
||||||
|
for (@field(view.actions, @tagName(context))) |module| {
|
||||||
|
if (isCallback(context, module)) {
|
||||||
|
size += 1;
|
||||||
|
} else {
|
||||||
|
@compileError(std.fmt.comptimePrint(
|
||||||
|
"`{0s}` callbacks must be either a function `{1s}` or a type that defines " ++
|
||||||
|
"`pub const {0s}Render`. Found: `{2s}`",
|
||||||
|
.{
|
||||||
|
@tagName(context),
|
||||||
|
switch (context) {
|
||||||
|
.before => @typeName(BeforeCallback),
|
||||||
|
.after => @typeName(AfterCallback),
|
||||||
|
},
|
||||||
|
if (@TypeOf(module) == type)
|
||||||
|
@typeName(module)
|
||||||
|
else
|
||||||
|
@typeName(@TypeOf(&module)),
|
||||||
|
},
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var callbacks: [size]switch (context) {
|
||||||
|
.before => BeforeCallback,
|
||||||
|
.after => AfterCallback,
|
||||||
|
} = undefined;
|
||||||
|
var index: usize = 0;
|
||||||
|
for (@field(view.actions, @tagName(context))) |module| {
|
||||||
|
if (!isCallback(context, module)) continue;
|
||||||
|
|
||||||
|
callbacks[index] = if (@TypeOf(module) == type)
|
||||||
|
@field(module, @tagName(context) ++ "Render")
|
||||||
|
else
|
||||||
|
&module;
|
||||||
|
|
||||||
|
index += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const final = callbacks;
|
||||||
|
return &final;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn isCallback(comptime context: Context, comptime module: anytype) bool {
|
||||||
|
comptime {
|
||||||
|
if (@typeInfo(@TypeOf(module)) == .@"fn") {
|
||||||
|
const expected = switch (context) {
|
||||||
|
.before => BeforeCallback,
|
||||||
|
.after => AfterCallback,
|
||||||
|
};
|
||||||
|
|
||||||
|
const info = @typeInfo(@TypeOf(module)).@"fn";
|
||||||
|
|
||||||
|
const actual_params = info.params;
|
||||||
|
const expected_params = @typeInfo(@typeInfo(expected).pointer.child).@"fn".params;
|
||||||
|
|
||||||
|
if (actual_params.len != expected_params.len) return false;
|
||||||
|
|
||||||
|
for (actual_params, expected_params) |actual_param, expected_param| {
|
||||||
|
if (actual_param.type != expected_param.type) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (@typeInfo(info.return_type.?) != .error_union) return false;
|
||||||
|
if (@typeInfo(info.return_type.?).error_union.payload != void) return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return if (@TypeOf(module) == type and @hasDecl(module, @tagName(context) ++ "Render"))
|
||||||
|
true
|
||||||
|
else
|
||||||
|
false;
|
||||||
|
}
|
||||||
|
}
|
@ -17,6 +17,9 @@ pub const jobs = @import("jobs.zig");
|
|||||||
pub const mail = @import("mail.zig");
|
pub const mail = @import("mail.zig");
|
||||||
pub const kv = @import("kv.zig");
|
pub const kv = @import("kv.zig");
|
||||||
pub const db = @import("database.zig");
|
pub const db = @import("database.zig");
|
||||||
|
pub const Environment = @import("Environment.zig");
|
||||||
|
pub const environment = std.enums.nameCast(Environment.EnvironmentName, build_options.environment);
|
||||||
|
pub const build_options = @import("build_options");
|
||||||
|
|
||||||
const root = @import("root");
|
const root = @import("root");
|
||||||
|
|
||||||
@ -149,11 +152,26 @@ pub const smtp: mail.SMTPConfig = .{
|
|||||||
};
|
};
|
||||||
|
|
||||||
/// HTTP cookie configuration
|
/// HTTP cookie configuration
|
||||||
pub const cookies: http.Cookies.CookieOptions = .{
|
pub const cookies: http.Cookies.CookieOptions = switch (environment) {
|
||||||
.domain = "localhost",
|
.development, .testing => .{
|
||||||
.path = "/",
|
.domain = "localhost",
|
||||||
|
.path = "/",
|
||||||
|
},
|
||||||
|
.production => .{
|
||||||
|
.secure = true,
|
||||||
|
.http_only = true,
|
||||||
|
.same_site = true,
|
||||||
|
.path = "/",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// Override the default anti-CSRF authenticity token name that is stored in the encrypted
|
||||||
|
/// session. This value is also used by `context.authenticityFormElement()` to render an HTML
|
||||||
|
/// element: the element's `name` attribute is set to this value.
|
||||||
|
pub const authenticity_token_name: []const u8 = "_jetzig_authenticity_token";
|
||||||
|
|
||||||
|
/// When using `AuthMiddleware`, set this value to override the default JetQuery model name that
|
||||||
|
/// maps the users table.
|
||||||
pub const auth: @import("auth.zig").AuthOptions = .{
|
pub const auth: @import("auth.zig").AuthOptions = .{
|
||||||
.user_model = "User",
|
.user_model = "User",
|
||||||
};
|
};
|
||||||
|
12
src/jetzig/development_static.zig
Normal file
12
src/jetzig/development_static.zig
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
pub const compiled = [_]Compiled{};
|
||||||
|
|
||||||
|
const StaticOutput = struct {
|
||||||
|
json: ?[]const u8 = null,
|
||||||
|
html: ?[]const u8 = null,
|
||||||
|
params: ?[]const u8,
|
||||||
|
};
|
||||||
|
|
||||||
|
const Compiled = struct {
|
||||||
|
route_id: []const u8,
|
||||||
|
output: StaticOutput,
|
||||||
|
};
|
@ -1,9 +1,14 @@
|
|||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const builtin = @import("builtin");
|
const builtin = @import("builtin");
|
||||||
|
|
||||||
|
pub const build_options = @import("build_options");
|
||||||
|
|
||||||
pub const Server = @import("http/Server.zig");
|
pub const Server = @import("http/Server.zig");
|
||||||
pub const Request = @import("http/Request.zig");
|
pub const Request = @import("http/Request.zig");
|
||||||
pub const StaticRequest = @import("http/StaticRequest.zig");
|
pub const StaticRequest = if (build_options.environment == .development)
|
||||||
|
Request
|
||||||
|
else
|
||||||
|
@import("http/StaticRequest.zig");
|
||||||
pub const Response = @import("http/Response.zig");
|
pub const Response = @import("http/Response.zig");
|
||||||
pub const Session = @import("http/Session.zig");
|
pub const Session = @import("http/Session.zig");
|
||||||
pub const Cookies = @import("http/Cookies.zig");
|
pub const Cookies = @import("http/Cookies.zig");
|
||||||
|
@ -8,18 +8,18 @@ cookies: std.StringArrayHashMap(*Cookie),
|
|||||||
modified: bool = false,
|
modified: bool = false,
|
||||||
arena: std.heap.ArenaAllocator,
|
arena: std.heap.ArenaAllocator,
|
||||||
|
|
||||||
const Self = @This();
|
const Cookies = @This();
|
||||||
|
|
||||||
const SameSite = enum { strict, lax, none };
|
const SameSite = enum { strict, lax, none };
|
||||||
pub const CookieOptions = struct {
|
pub const CookieOptions = struct {
|
||||||
domain: []const u8 = "localhost",
|
domain: ?[]const u8 = "localhost",
|
||||||
path: []const u8 = "/",
|
path: []const u8 = "/",
|
||||||
same_site: ?SameSite = null,
|
|
||||||
secure: bool = false,
|
secure: bool = false,
|
||||||
expires: ?i64 = null, // if used, set to time in seconds to be added to std.time.timestamp()
|
|
||||||
http_only: bool = false,
|
http_only: bool = false,
|
||||||
max_age: ?i64 = null,
|
|
||||||
partitioned: bool = false,
|
partitioned: bool = false,
|
||||||
|
same_site: ?SameSite = null,
|
||||||
|
expires: ?i64 = null, // if used, set to time in seconds to be added to std.time.timestamp()
|
||||||
|
max_age: ?i64 = null,
|
||||||
};
|
};
|
||||||
|
|
||||||
const cookie_options = jetzig.config.get(CookieOptions, "cookies");
|
const cookie_options = jetzig.config.get(CookieOptions, "cookies");
|
||||||
@ -27,43 +27,50 @@ const cookie_options = jetzig.config.get(CookieOptions, "cookies");
|
|||||||
pub const Cookie = struct {
|
pub const Cookie = struct {
|
||||||
name: []const u8,
|
name: []const u8,
|
||||||
value: []const u8,
|
value: []const u8,
|
||||||
domain: ?[]const u8 = null,
|
secure: bool = cookie_options.secure,
|
||||||
path: ?[]const u8 = null,
|
http_only: bool = cookie_options.http_only,
|
||||||
same_site: ?SameSite = null,
|
partitioned: bool = cookie_options.partitioned,
|
||||||
secure: ?bool = null,
|
domain: ?[]const u8 = cookie_options.domain,
|
||||||
expires: ?i64 = null, // if used, set to time in seconds to be added to std.time.timestamp()
|
path: ?[]const u8 = cookie_options.path,
|
||||||
http_only: ?bool = null,
|
same_site: ?SameSite = cookie_options.same_site,
|
||||||
max_age: ?i64 = null,
|
// if used, set to time in seconds to be added to std.time.timestamp()
|
||||||
partitioned: ?bool = null,
|
expires: ?i64 = cookie_options.expires,
|
||||||
|
max_age: ?i64 = cookie_options.max_age,
|
||||||
|
|
||||||
/// Build a cookie string.
|
/// Build a cookie string.
|
||||||
pub fn bufPrint(self: Cookie, buf: *[4096]u8) ![]const u8 {
|
pub fn bufPrint(self: Cookie, buf: *[4096]u8) ![]const u8 {
|
||||||
var options = cookie_options;
|
|
||||||
inline for (std.meta.fields(CookieOptions)) |field| {
|
|
||||||
@field(options, field.name) = @field(self, field.name) orelse @field(cookie_options, field.name);
|
|
||||||
}
|
|
||||||
|
|
||||||
// secure is required if samesite is set to none
|
|
||||||
const require_secure = if (options.same_site) |same_site| same_site == .none else false;
|
|
||||||
|
|
||||||
var stream = std.io.fixedBufferStream(buf);
|
var stream = std.io.fixedBufferStream(buf);
|
||||||
const writer = stream.writer();
|
const writer = stream.writer();
|
||||||
|
try writer.print("{}", .{self});
|
||||||
|
return stream.getWritten();
|
||||||
|
}
|
||||||
|
|
||||||
try writer.print("{s}={s}; path={s}; domain={s};", .{
|
/// Build a cookie string.
|
||||||
|
pub fn format(self: Cookie, _: anytype, _: anytype, writer: anytype) !void {
|
||||||
|
// secure is required if samesite is set to none
|
||||||
|
const require_secure = if (self.same_site) |same_site| same_site == .none else false;
|
||||||
|
|
||||||
|
try writer.print("{s}={s}; path={s};", .{
|
||||||
self.name,
|
self.name,
|
||||||
self.value,
|
self.value,
|
||||||
options.path,
|
self.path orelse "/",
|
||||||
options.domain,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (options.same_site) |same_site| try writer.print(" SameSite={s};", .{@tagName(same_site)});
|
if (self.domain) |domain| try writer.print(" domain={s};", .{domain});
|
||||||
if (options.secure or require_secure) try writer.writeAll(" Secure;");
|
if (self.same_site) |same_site| try writer.print(
|
||||||
if (options.expires) |expires| try writer.print(" Expires={d};", .{std.time.timestamp() + expires});
|
" SameSite={s};",
|
||||||
if (options.max_age) |max_age| try writer.print(" Max-Age={d};", .{max_age});
|
.{@tagName(same_site)},
|
||||||
if (options.http_only) try writer.writeAll(" HttpOnly;");
|
);
|
||||||
if (options.partitioned) try writer.writeAll(" Partitioned;");
|
if (self.secure or require_secure) try writer.writeAll(" Secure;");
|
||||||
|
if (self.expires) |expires| {
|
||||||
return stream.getWritten();
|
const seconds = std.time.timestamp() + expires;
|
||||||
|
const timestamp = try jetzig.jetcommon.DateTime.fromUnix(seconds, .seconds);
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#expiresdate
|
||||||
|
try timestamp.strftime(writer, " Expires=%a, %d %h %Y %H:%M:%S GMT;");
|
||||||
|
}
|
||||||
|
if (self.max_age) |max_age| try writer.print(" Max-Age={d};", .{max_age});
|
||||||
|
if (self.http_only) try writer.writeAll(" HttpOnly;");
|
||||||
|
if (self.partitioned) try writer.writeAll(" Partitioned;");
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn applyFlag(self: *Cookie, allocator: std.mem.Allocator, flag: Flag) !void {
|
pub fn applyFlag(self: *Cookie, allocator: std.mem.Allocator, flag: Flag) !void {
|
||||||
@ -80,7 +87,7 @@ pub const Cookie = struct {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn init(allocator: std.mem.Allocator, cookie_string: []const u8) Self {
|
pub fn init(allocator: std.mem.Allocator, cookie_string: []const u8) Cookies {
|
||||||
return .{
|
return .{
|
||||||
.allocator = allocator,
|
.allocator = allocator,
|
||||||
.cookie_string = cookie_string,
|
.cookie_string = cookie_string,
|
||||||
@ -89,7 +96,7 @@ pub fn init(allocator: std.mem.Allocator, cookie_string: []const u8) Self {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(self: *Self) void {
|
pub fn deinit(self: *Cookies) void {
|
||||||
var it = self.cookies.iterator();
|
var it = self.cookies.iterator();
|
||||||
while (it.next()) |item| {
|
while (it.next()) |item| {
|
||||||
self.allocator.free(item.key_ptr.*);
|
self.allocator.free(item.key_ptr.*);
|
||||||
@ -100,11 +107,11 @@ pub fn deinit(self: *Self) void {
|
|||||||
self.arena.deinit();
|
self.arena.deinit();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get(self: *Self, key: []const u8) ?*Cookie {
|
pub fn get(self: *Cookies, key: []const u8) ?*Cookie {
|
||||||
return self.cookies.get(key);
|
return self.cookies.get(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn put(self: *Self, cookie: Cookie) !void {
|
pub fn put(self: *Cookies, cookie: Cookie) !void {
|
||||||
self.modified = true;
|
self.modified = true;
|
||||||
|
|
||||||
if (self.cookies.fetchSwapRemove(cookie.name)) |entry| {
|
if (self.cookies.fetchSwapRemove(cookie.name)) |entry| {
|
||||||
@ -125,8 +132,12 @@ pub const HeaderIterator = struct {
|
|||||||
cookies_iterator: std.StringArrayHashMap(*Cookie).Iterator,
|
cookies_iterator: std.StringArrayHashMap(*Cookie).Iterator,
|
||||||
buf: *[4096]u8,
|
buf: *[4096]u8,
|
||||||
|
|
||||||
pub fn init(allocator: std.mem.Allocator, cookies: *Self, buf: *[4096]u8) HeaderIterator {
|
pub fn init(allocator: std.mem.Allocator, cookies: *Cookies, buf: *[4096]u8) HeaderIterator {
|
||||||
return .{ .allocator = allocator, .cookies_iterator = cookies.cookies.iterator(), .buf = buf };
|
return .{
|
||||||
|
.allocator = allocator,
|
||||||
|
.cookies_iterator = cookies.cookies.iterator(),
|
||||||
|
.buf = buf,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn next(self: *HeaderIterator) !?[]const u8 {
|
pub fn next(self: *HeaderIterator) !?[]const u8 {
|
||||||
@ -139,14 +150,14 @@ pub const HeaderIterator = struct {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn headerIterator(self: *Self, buf: *[4096]u8) HeaderIterator {
|
pub fn headerIterator(self: *Cookies, buf: *[4096]u8) HeaderIterator {
|
||||||
return HeaderIterator.init(self.allocator, self, buf);
|
return HeaderIterator.init(self.allocator, self, buf);
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://datatracker.ietf.org/doc/html/rfc6265#section-4.2.1
|
// https://datatracker.ietf.org/doc/html/rfc6265#section-4.2.1
|
||||||
// cookie-header = "Cookie:" OWS cookie-string OWS
|
// cookie-header = "Cookie:" OWS cookie-string OWS
|
||||||
// cookie-string = cookie-pair *( ";" SP cookie-pair )
|
// cookie-string = cookie-pair *( ";" SP cookie-pair )
|
||||||
pub fn parse(self: *Self) !void {
|
pub fn parse(self: *Cookies) !void {
|
||||||
var key_buf = std.ArrayList(u8).init(self.allocator);
|
var key_buf = std.ArrayList(u8).init(self.allocator);
|
||||||
var value_buf = std.ArrayList(u8).init(self.allocator);
|
var value_buf = std.ArrayList(u8).init(self.allocator);
|
||||||
var key_terminated = false;
|
var key_terminated = false;
|
||||||
@ -202,6 +213,13 @@ pub fn parse(self: *Self) !void {
|
|||||||
for (cookie_buf.items) |cookie| try self.put(cookie);
|
for (cookie_buf.items) |cookie| try self.put(cookie);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn format(self: Cookies, _: anytype, _: anytype, writer: anytype) !void {
|
||||||
|
var it = self.cookies.iterator();
|
||||||
|
while (it.next()) |entry| {
|
||||||
|
try writer.print("{}; ", .{entry.value_ptr.*});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const Flag = union(enum) {
|
const Flag = union(enum) {
|
||||||
domain: []const u8,
|
domain: []const u8,
|
||||||
path: []const u8,
|
path: []const u8,
|
||||||
@ -250,7 +268,7 @@ fn parseFlag(key: []const u8, value: []const u8) ?Flag {
|
|||||||
|
|
||||||
test "basic cookie string" {
|
test "basic cookie string" {
|
||||||
const allocator = std.testing.allocator;
|
const allocator = std.testing.allocator;
|
||||||
var cookies = Self.init(allocator, "foo=bar; baz=qux;");
|
var cookies = Cookies.init(allocator, "foo=bar; baz=qux;");
|
||||||
defer cookies.deinit();
|
defer cookies.deinit();
|
||||||
try cookies.parse();
|
try cookies.parse();
|
||||||
try std.testing.expectEqualStrings("bar", cookies.get("foo").?.value);
|
try std.testing.expectEqualStrings("bar", cookies.get("foo").?.value);
|
||||||
@ -259,14 +277,14 @@ test "basic cookie string" {
|
|||||||
|
|
||||||
test "empty cookie string" {
|
test "empty cookie string" {
|
||||||
const allocator = std.testing.allocator;
|
const allocator = std.testing.allocator;
|
||||||
var cookies = Self.init(allocator, "");
|
var cookies = Cookies.init(allocator, "");
|
||||||
defer cookies.deinit();
|
defer cookies.deinit();
|
||||||
try cookies.parse();
|
try cookies.parse();
|
||||||
}
|
}
|
||||||
|
|
||||||
test "cookie string with irregular spaces" {
|
test "cookie string with irregular spaces" {
|
||||||
const allocator = std.testing.allocator;
|
const allocator = std.testing.allocator;
|
||||||
var cookies = Self.init(allocator, "foo= bar; baz= qux;");
|
var cookies = Cookies.init(allocator, "foo= bar; baz= qux;");
|
||||||
defer cookies.deinit();
|
defer cookies.deinit();
|
||||||
try cookies.parse();
|
try cookies.parse();
|
||||||
try std.testing.expectEqualStrings("bar", cookies.get("foo").?.value);
|
try std.testing.expectEqualStrings("bar", cookies.get("foo").?.value);
|
||||||
@ -280,7 +298,7 @@ test "headerIterator" {
|
|||||||
|
|
||||||
const writer = buf.writer();
|
const writer = buf.writer();
|
||||||
|
|
||||||
var cookies = Self.init(allocator, "foo=bar; baz=qux;");
|
var cookies = Cookies.init(allocator, "foo=bar; baz=qux;");
|
||||||
defer cookies.deinit();
|
defer cookies.deinit();
|
||||||
try cookies.parse();
|
try cookies.parse();
|
||||||
|
|
||||||
@ -300,7 +318,7 @@ test "headerIterator" {
|
|||||||
|
|
||||||
test "modified" {
|
test "modified" {
|
||||||
const allocator = std.testing.allocator;
|
const allocator = std.testing.allocator;
|
||||||
var cookies = Self.init(allocator, "foo=bar; baz=qux;");
|
var cookies = Cookies.init(allocator, "foo=bar; baz=qux;");
|
||||||
defer cookies.deinit();
|
defer cookies.deinit();
|
||||||
|
|
||||||
try cookies.parse();
|
try cookies.parse();
|
||||||
@ -312,7 +330,7 @@ test "modified" {
|
|||||||
|
|
||||||
test "domain=example.com" {
|
test "domain=example.com" {
|
||||||
const allocator = std.testing.allocator;
|
const allocator = std.testing.allocator;
|
||||||
var cookies = Self.init(allocator, "foo=bar; baz=qux; Domain=example.com;");
|
var cookies = Cookies.init(allocator, "foo=bar; baz=qux; Domain=example.com;");
|
||||||
defer cookies.deinit();
|
defer cookies.deinit();
|
||||||
|
|
||||||
try cookies.parse();
|
try cookies.parse();
|
||||||
@ -322,7 +340,7 @@ test "domain=example.com" {
|
|||||||
|
|
||||||
test "path=/example_path" {
|
test "path=/example_path" {
|
||||||
const allocator = std.testing.allocator;
|
const allocator = std.testing.allocator;
|
||||||
var cookies = Self.init(allocator, "foo=bar; baz=qux; Path=/example_path;");
|
var cookies = Cookies.init(allocator, "foo=bar; baz=qux; Path=/example_path;");
|
||||||
defer cookies.deinit();
|
defer cookies.deinit();
|
||||||
|
|
||||||
try cookies.parse();
|
try cookies.parse();
|
||||||
@ -332,7 +350,7 @@ test "path=/example_path" {
|
|||||||
|
|
||||||
test "SameSite=lax" {
|
test "SameSite=lax" {
|
||||||
const allocator = std.testing.allocator;
|
const allocator = std.testing.allocator;
|
||||||
var cookies = Self.init(allocator, "foo=bar; baz=qux; SameSite=lax;");
|
var cookies = Cookies.init(allocator, "foo=bar; baz=qux; SameSite=lax;");
|
||||||
defer cookies.deinit();
|
defer cookies.deinit();
|
||||||
|
|
||||||
try cookies.parse();
|
try cookies.parse();
|
||||||
@ -342,7 +360,7 @@ test "SameSite=lax" {
|
|||||||
|
|
||||||
test "SameSite=none" {
|
test "SameSite=none" {
|
||||||
const allocator = std.testing.allocator;
|
const allocator = std.testing.allocator;
|
||||||
var cookies = Self.init(allocator, "foo=bar; baz=qux; SameSite=none;");
|
var cookies = Cookies.init(allocator, "foo=bar; baz=qux; SameSite=none;");
|
||||||
defer cookies.deinit();
|
defer cookies.deinit();
|
||||||
|
|
||||||
try cookies.parse();
|
try cookies.parse();
|
||||||
@ -352,7 +370,7 @@ test "SameSite=none" {
|
|||||||
|
|
||||||
test "SameSite=strict" {
|
test "SameSite=strict" {
|
||||||
const allocator = std.testing.allocator;
|
const allocator = std.testing.allocator;
|
||||||
var cookies = Self.init(allocator, "foo=bar; baz=qux; SameSite=strict;");
|
var cookies = Cookies.init(allocator, "foo=bar; baz=qux; SameSite=strict;");
|
||||||
defer cookies.deinit();
|
defer cookies.deinit();
|
||||||
|
|
||||||
try cookies.parse();
|
try cookies.parse();
|
||||||
@ -362,27 +380,27 @@ test "SameSite=strict" {
|
|||||||
|
|
||||||
test "Secure" {
|
test "Secure" {
|
||||||
const allocator = std.testing.allocator;
|
const allocator = std.testing.allocator;
|
||||||
var cookies = Self.init(allocator, "foo=bar; baz=qux; Secure;");
|
var cookies = Cookies.init(allocator, "foo=bar; baz=qux; Secure;");
|
||||||
defer cookies.deinit();
|
defer cookies.deinit();
|
||||||
|
|
||||||
try cookies.parse();
|
try cookies.parse();
|
||||||
const cookie = cookies.get("foo").?;
|
const cookie = cookies.get("foo").?;
|
||||||
try std.testing.expect(cookie.secure.?);
|
try std.testing.expect(cookie.secure);
|
||||||
}
|
}
|
||||||
|
|
||||||
test "Partitioned" {
|
test "Partitioned" {
|
||||||
const allocator = std.testing.allocator;
|
const allocator = std.testing.allocator;
|
||||||
var cookies = Self.init(allocator, "foo=bar; baz=qux; Partitioned;");
|
var cookies = Cookies.init(allocator, "foo=bar; baz=qux; Partitioned;");
|
||||||
defer cookies.deinit();
|
defer cookies.deinit();
|
||||||
|
|
||||||
try cookies.parse();
|
try cookies.parse();
|
||||||
const cookie = cookies.get("foo").?;
|
const cookie = cookies.get("foo").?;
|
||||||
try std.testing.expect(cookie.partitioned.?);
|
try std.testing.expect(cookie.partitioned);
|
||||||
}
|
}
|
||||||
|
|
||||||
test "Max-Age" {
|
test "Max-Age" {
|
||||||
const allocator = std.testing.allocator;
|
const allocator = std.testing.allocator;
|
||||||
var cookies = Self.init(allocator, "foo=bar; baz=qux; Max-Age=123123123;");
|
var cookies = Cookies.init(allocator, "foo=bar; baz=qux; Max-Age=123123123;");
|
||||||
defer cookies.deinit();
|
defer cookies.deinit();
|
||||||
|
|
||||||
try cookies.parse();
|
try cookies.parse();
|
||||||
@ -392,7 +410,7 @@ test "Max-Age" {
|
|||||||
|
|
||||||
test "Expires" {
|
test "Expires" {
|
||||||
const allocator = std.testing.allocator;
|
const allocator = std.testing.allocator;
|
||||||
var cookies = Self.init(allocator, "foo=bar; baz=qux; Expires=123123123;");
|
var cookies = Cookies.init(allocator, "foo=bar; baz=qux; Expires=123123123;");
|
||||||
defer cookies.deinit();
|
defer cookies.deinit();
|
||||||
|
|
||||||
try cookies.parse();
|
try cookies.parse();
|
||||||
@ -402,17 +420,17 @@ test "Expires" {
|
|||||||
|
|
||||||
test "default flags" {
|
test "default flags" {
|
||||||
const allocator = std.testing.allocator;
|
const allocator = std.testing.allocator;
|
||||||
var cookies = Self.init(allocator, "foo=bar; baz=qux;");
|
var cookies = Cookies.init(allocator, "foo=bar; baz=qux;");
|
||||||
defer cookies.deinit();
|
defer cookies.deinit();
|
||||||
|
|
||||||
try cookies.parse();
|
try cookies.parse();
|
||||||
const cookie = cookies.get("foo").?;
|
const cookie = cookies.get("foo").?;
|
||||||
try std.testing.expect(cookie.domain == null);
|
try std.testing.expect(cookie.secure == false);
|
||||||
try std.testing.expect(cookie.path == null);
|
try std.testing.expect(cookie.partitioned == false);
|
||||||
|
try std.testing.expect(cookie.http_only == false);
|
||||||
try std.testing.expect(cookie.same_site == null);
|
try std.testing.expect(cookie.same_site == null);
|
||||||
try std.testing.expect(cookie.secure == null);
|
try std.testing.expectEqualStrings(cookie.domain.?, "localhost");
|
||||||
|
try std.testing.expectEqualStrings(cookie.path.?, "/");
|
||||||
try std.testing.expect(cookie.expires == null);
|
try std.testing.expect(cookie.expires == null);
|
||||||
try std.testing.expect(cookie.http_only == null);
|
|
||||||
try std.testing.expect(cookie.max_age == null);
|
try std.testing.expect(cookie.max_age == null);
|
||||||
try std.testing.expect(cookie.partitioned == null);
|
|
||||||
}
|
}
|
||||||
|
@ -46,10 +46,9 @@ pub fn getAll(self: Headers, name: []const u8) []const []const u8 {
|
|||||||
var headers = std.ArrayList([]const u8).init(self.allocator);
|
var headers = std.ArrayList([]const u8).init(self.allocator);
|
||||||
|
|
||||||
for (self.httpz_headers.keys, 0..) |key, index| {
|
for (self.httpz_headers.keys, 0..) |key, index| {
|
||||||
var buf: [max_bytes_header_name]u8 = undefined;
|
if (std.ascii.eqlIgnoreCase(name, key)) {
|
||||||
const lower = std.ascii.lowerString(&buf, name);
|
headers.append(self.httpz_headers.values[index]) catch @panic("OOM");
|
||||||
|
}
|
||||||
if (std.mem.eql(u8, lower, key)) headers.append(self.httpz_headers.values[index]) catch @panic("OOM");
|
|
||||||
}
|
}
|
||||||
return headers.toOwnedSlice() catch @panic("OOM");
|
return headers.toOwnedSlice() catch @panic("OOM");
|
||||||
}
|
}
|
||||||
|
@ -10,6 +10,7 @@ const default_content_type = "text/html";
|
|||||||
pub const Method = enum { DELETE, GET, PATCH, POST, HEAD, PUT, CONNECT, OPTIONS, TRACE };
|
pub const Method = enum { DELETE, GET, PATCH, POST, HEAD, PUT, CONNECT, OPTIONS, TRACE };
|
||||||
pub const Modifier = enum { edit, new };
|
pub const Modifier = enum { edit, new };
|
||||||
pub const Format = enum { HTML, JSON, UNKNOWN };
|
pub const Format = enum { HTML, JSON, UNKNOWN };
|
||||||
|
pub const Protocol = enum { http, https };
|
||||||
|
|
||||||
allocator: std.mem.Allocator,
|
allocator: std.mem.Allocator,
|
||||||
path: jetzig.http.Path,
|
path: jetzig.http.Path,
|
||||||
@ -34,6 +35,8 @@ response_started: bool = false,
|
|||||||
dynamic_assigned_template: ?[]const u8 = null,
|
dynamic_assigned_template: ?[]const u8 = null,
|
||||||
layout: ?[]const u8 = null,
|
layout: ?[]const u8 = null,
|
||||||
layout_disabled: bool = false,
|
layout_disabled: bool = false,
|
||||||
|
// TODO: Squash rendered/redirected/failed into
|
||||||
|
// `state: enum { initial, rendered, redirected, failed }`
|
||||||
rendered: bool = false,
|
rendered: bool = false,
|
||||||
redirected: bool = false,
|
redirected: bool = false,
|
||||||
failed: bool = false,
|
failed: bool = false,
|
||||||
@ -249,7 +252,10 @@ pub fn middleware(
|
|||||||
unreachable;
|
unreachable;
|
||||||
}
|
}
|
||||||
|
|
||||||
const RedirectState = struct { location: []const u8, status_code: jetzig.http.status_codes.StatusCode };
|
const RedirectState = struct {
|
||||||
|
location: []const u8,
|
||||||
|
status_code: jetzig.http.status_codes.StatusCode,
|
||||||
|
};
|
||||||
|
|
||||||
pub fn renderRedirect(self: *Request, state: RedirectState) !void {
|
pub fn renderRedirect(self: *Request, state: RedirectState) !void {
|
||||||
self.response_data.reset();
|
self.response_data.reset();
|
||||||
@ -275,7 +281,12 @@ pub fn renderRedirect(self: *Request, state: RedirectState) !void {
|
|||||||
.HTML, .UNKNOWN => if (maybe_template) |template| blk: {
|
.HTML, .UNKNOWN => if (maybe_template) |template| blk: {
|
||||||
try view.data.addConst("jetzig_view", view.data.string("internal"));
|
try view.data.addConst("jetzig_view", view.data.string("internal"));
|
||||||
try view.data.addConst("jetzig_action", view.data.string(@tagName(state.status_code)));
|
try view.data.addConst("jetzig_action", view.data.string(@tagName(state.status_code)));
|
||||||
break :blk try template.render(self.response_data);
|
break :blk try template.render(
|
||||||
|
self.response_data,
|
||||||
|
jetzig.TemplateContext,
|
||||||
|
.{ .request = self },
|
||||||
|
.{},
|
||||||
|
);
|
||||||
} else try std.fmt.allocPrint(self.allocator, "Redirecting to {s}", .{state.location}),
|
} else try std.fmt.allocPrint(self.allocator, "Redirecting to {s}", .{state.location}),
|
||||||
.JSON => blk: {
|
.JSON => blk: {
|
||||||
break :blk try std.json.stringifyAlloc(
|
break :blk try std.json.stringifyAlloc(
|
||||||
@ -486,6 +497,31 @@ pub fn session(self: *Request) !*jetzig.http.Session {
|
|||||||
return local_session;
|
return local_session;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Return the anti-CSRF token cookie value. If no cookie exist, create it.
|
||||||
|
pub fn authenticityToken(self: *Request) ![]const u8 {
|
||||||
|
var local_session = try self.session();
|
||||||
|
|
||||||
|
return local_session.getT(.string, jetzig.authenticity_token_name) orelse blk: {
|
||||||
|
const token = try jetzig.util.generateSecret(self.allocator, 32);
|
||||||
|
try local_session.put(jetzig.authenticity_token_name, token);
|
||||||
|
break :blk local_session.getT(.string, jetzig.authenticity_token_name).?;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn resourceId(self: *const Request) ![]const u8 {
|
||||||
|
return self.path.resource_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return the protocol used to serve the current request by detecting the `X-Forwarded-Proto`
|
||||||
|
/// header.
|
||||||
|
pub fn protocol(self: *const Request) Protocol {
|
||||||
|
return if (self.headers.get("x-forwarded-proto")) |x_forwarded_proto|
|
||||||
|
if (std.ascii.eqlIgnoreCase(x_forwarded_proto, "https")) .https else .http
|
||||||
|
else
|
||||||
|
// TODO: Extend login when we support serving HTTPS directly.
|
||||||
|
.http;
|
||||||
|
}
|
||||||
|
|
||||||
/// Create a new Job. Receives a job name which must resolve to `src/app/jobs/<name>.zig`
|
/// Create a new Job. Receives a job name which must resolve to `src/app/jobs/<name>.zig`
|
||||||
/// Call `Job.put(...)` to set job params.
|
/// Call `Job.put(...)` to set job params.
|
||||||
/// Call `Job.background()` to run the job outside of the request/response flow.
|
/// Call `Job.background()` to run the job outside of the request/response flow.
|
||||||
|
@ -14,7 +14,6 @@ custom_routes: []jetzig.views.Route,
|
|||||||
job_definitions: []const jetzig.JobDefinition,
|
job_definitions: []const jetzig.JobDefinition,
|
||||||
mailer_definitions: []const jetzig.MailerDefinition,
|
mailer_definitions: []const jetzig.MailerDefinition,
|
||||||
mime_map: *jetzig.http.mime.MimeMap,
|
mime_map: *jetzig.http.mime.MimeMap,
|
||||||
std_net_server: std.net.Server = undefined,
|
|
||||||
initialized: bool = false,
|
initialized: bool = false,
|
||||||
store: *jetzig.kv.Store,
|
store: *jetzig.kv.Store,
|
||||||
job_queue: *jetzig.kv.Store,
|
job_queue: *jetzig.kv.Store,
|
||||||
@ -57,7 +56,6 @@ pub fn init(
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(self: *Server) void {
|
pub fn deinit(self: *Server) void {
|
||||||
if (self.initialized) self.std_net_server.deinit();
|
|
||||||
self.allocator.free(self.env.secret);
|
self.allocator.free(self.env.secret);
|
||||||
self.allocator.free(self.env.bind);
|
self.allocator.free(self.env.bind);
|
||||||
}
|
}
|
||||||
@ -187,21 +185,45 @@ fn renderResponse(self: *Server, request: *jetzig.http.Request) !void {
|
|||||||
} else unreachable; // In future a MiddlewareRoute might provide a render function etc.
|
} else unreachable; // In future a MiddlewareRoute might provide a render function etc.
|
||||||
}
|
}
|
||||||
|
|
||||||
const route = self.matchCustomRoute(request) orelse try self.matchRoute(request, false);
|
const maybe_route = self.matchCustomRoute(request) orelse try self.matchRoute(request, false);
|
||||||
|
|
||||||
if (route) |capture| {
|
if (maybe_route) |route| {
|
||||||
if (!capture.validateFormat(request)) {
|
if (!route.validateFormat(request)) {
|
||||||
return request.setResponse(try self.renderNotFound(request), .{});
|
return request.setResponse(try self.renderNotFound(request), .{});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (request.requestFormat()) {
|
if (maybe_route) |route| {
|
||||||
.HTML => try self.renderHTML(request, route),
|
for (route.before_callbacks) |callback| {
|
||||||
.JSON => try self.renderJSON(request, route),
|
try callback(request, route);
|
||||||
.UNKNOWN => try self.renderHTML(request, route),
|
if (request.rendered_view) |view| {
|
||||||
|
if (request.failed) {
|
||||||
|
request.setResponse(try self.renderError(request, view.status_code), .{});
|
||||||
|
} else if (request.rendered) {
|
||||||
|
// TODO: Allow callbacks to set content
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (request.redirect_state) |state| {
|
||||||
|
try request.renderRedirect(state);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (request.redirect_state) |state| return try request.renderRedirect(state);
|
switch (request.requestFormat()) {
|
||||||
|
.HTML => try self.renderHTML(request, maybe_route),
|
||||||
|
.JSON => try self.renderJSON(request, maybe_route),
|
||||||
|
.UNKNOWN => try self.renderHTML(request, maybe_route),
|
||||||
|
}
|
||||||
|
|
||||||
|
if (maybe_route) |route| {
|
||||||
|
for (route.after_callbacks) |callback| {
|
||||||
|
try callback(request, request.response, route);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.redirect_state) |state| try request.renderRedirect(state);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn renderStatic(resource: StaticResource, request: *jetzig.http.Request) !void {
|
fn renderStatic(resource: StaticResource, request: *jetzig.http.Request) !void {
|
||||||
@ -355,6 +377,8 @@ fn renderTemplateWithLayout(
|
|||||||
) ![]const u8 {
|
) ![]const u8 {
|
||||||
try addTemplateConstants(view, route);
|
try addTemplateConstants(view, route);
|
||||||
|
|
||||||
|
const template_context = jetzig.TemplateContext{ .request = request };
|
||||||
|
|
||||||
if (request.getLayout(route)) |layout_name| {
|
if (request.getLayout(route)) |layout_name| {
|
||||||
// TODO: Allow user to configure layouts directory other than src/app/views/layouts/
|
// TODO: Allow user to configure layouts directory other than src/app/views/layouts/
|
||||||
const prefixed_name = try std.mem.concat(
|
const prefixed_name = try std.mem.concat(
|
||||||
@ -365,23 +389,37 @@ fn renderTemplateWithLayout(
|
|||||||
defer self.allocator.free(prefixed_name);
|
defer self.allocator.free(prefixed_name);
|
||||||
|
|
||||||
if (zmpl.findPrefixed("views", prefixed_name)) |layout| {
|
if (zmpl.findPrefixed("views", prefixed_name)) |layout| {
|
||||||
return try template.renderWithOptions(view.data, .{ .layout = layout });
|
return try template.render(
|
||||||
|
view.data,
|
||||||
|
jetzig.TemplateContext,
|
||||||
|
template_context,
|
||||||
|
.{ .layout = layout },
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
try self.logger.WARN("Unknown layout: {s}", .{layout_name});
|
try self.logger.WARN("Unknown layout: {s}", .{layout_name});
|
||||||
return try template.render(view.data);
|
return try template.render(
|
||||||
|
view.data,
|
||||||
|
jetzig.TemplateContext,
|
||||||
|
template_context,
|
||||||
|
.{},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else return try template.render(view.data);
|
} else return try template.render(
|
||||||
|
view.data,
|
||||||
|
jetzig.TemplateContext,
|
||||||
|
template_context,
|
||||||
|
.{},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn addTemplateConstants(view: jetzig.views.View, route: jetzig.views.Route) !void {
|
fn addTemplateConstants(view: jetzig.views.View, route: jetzig.views.Route) !void {
|
||||||
try view.data.addConst("jetzig_view", view.data.string(route.view_name));
|
|
||||||
|
|
||||||
const action = switch (route.action) {
|
const action = switch (route.action) {
|
||||||
.custom => route.name,
|
.custom => route.name,
|
||||||
else => |tag| @tagName(tag),
|
else => |tag| @tagName(tag),
|
||||||
};
|
};
|
||||||
|
|
||||||
try view.data.addConst("jetzig_action", view.data.string(action));
|
try view.data.addConst("jetzig_action", view.data.string(action));
|
||||||
|
try view.data.addConst("jetzig_view", view.data.string(route.view_name));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn isBadRequest(err: anyerror) bool {
|
fn isBadRequest(err: anyerror) bool {
|
||||||
@ -481,7 +519,15 @@ fn renderErrorView(
|
|||||||
.HTML, .UNKNOWN => {
|
.HTML, .UNKNOWN => {
|
||||||
if (zmpl.findPrefixed("views", route.template)) |template| {
|
if (zmpl.findPrefixed("views", route.template)) |template| {
|
||||||
try addTemplateConstants(view, route.*);
|
try addTemplateConstants(view, route.*);
|
||||||
return .{ .view = view, .content = try template.render(request.response_data) };
|
return .{
|
||||||
|
.view = view,
|
||||||
|
.content = try template.render(
|
||||||
|
request.response_data,
|
||||||
|
jetzig.TemplateContext,
|
||||||
|
.{ .request = request },
|
||||||
|
.{},
|
||||||
|
),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
.JSON => return .{ .view = view, .content = try request.response_data.toJson() },
|
.JSON => return .{ .view = view, .content = try request.response_data.toJson() },
|
||||||
@ -573,19 +619,26 @@ fn matchMiddlewareRoute(request: *const jetzig.http.Request) ?jetzig.middleware.
|
|||||||
fn matchRoute(self: *Server, request: *jetzig.http.Request, static: bool) !?jetzig.views.Route {
|
fn matchRoute(self: *Server, request: *jetzig.http.Request, static: bool) !?jetzig.views.Route {
|
||||||
for (self.routes) |route| {
|
for (self.routes) |route| {
|
||||||
// .index routes always take precedence.
|
// .index routes always take precedence.
|
||||||
if (route.static == static and route.action == .index and try request.match(route.*)) {
|
if (route.action == .index and try request.match(route.*)) {
|
||||||
return route.*;
|
if (!jetzig.build_options.build_static) return route.*;
|
||||||
|
if (route.static == static) return route.*;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (self.routes) |route| {
|
for (self.routes) |route| {
|
||||||
if (route.static == static and try request.match(route.*)) return route.*;
|
if (try request.match(route.*)) {
|
||||||
|
if (!jetzig.build_options.build_static) return route.*;
|
||||||
|
if (route.static == static) return route.*;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const StaticResource = struct { content: []const u8, mime_type: []const u8 = "application/octet-stream" };
|
const StaticResource = struct {
|
||||||
|
content: []const u8,
|
||||||
|
mime_type: []const u8 = "application/octet-stream",
|
||||||
|
};
|
||||||
|
|
||||||
fn matchStaticResource(self: *Server, request: *jetzig.http.Request) !?StaticResource {
|
fn matchStaticResource(self: *Server, request: *jetzig.http.Request) !?StaticResource {
|
||||||
// TODO: Map public and static routes at launch to avoid accessing the file system when
|
// TODO: Map public and static routes at launch to avoid accessing the file system when
|
||||||
@ -682,7 +735,7 @@ fn matchStaticContent(self: *Server, request: *jetzig.http.Request) !?[]const u8
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn decodeStaticParams(self: *Server) !void {
|
pub fn decodeStaticParams(self: *Server) !void {
|
||||||
if (!@hasDecl(jetzig.root, "static")) return;
|
if (comptime !@hasDecl(jetzig.root, "static")) return;
|
||||||
|
|
||||||
// Store decoded static params (i.e. declared in views) for faster comparison at request time.
|
// Store decoded static params (i.e. declared in views) for faster comparison at request time.
|
||||||
var decoded = std.ArrayList(*jetzig.data.Value).init(self.allocator);
|
var decoded = std.ArrayList(*jetzig.data.Value).init(self.allocator);
|
||||||
|
@ -134,7 +134,7 @@ fn defaultHtml(
|
|||||||
try data.addConst("jetzig_view", data.string(""));
|
try data.addConst("jetzig_view", data.string(""));
|
||||||
try data.addConst("jetzig_action", data.string(""));
|
try data.addConst("jetzig_action", data.string(""));
|
||||||
return if (jetzig.zmpl.findPrefixed("mailers", mailer.html_template)) |template|
|
return if (jetzig.zmpl.findPrefixed("mailers", mailer.html_template)) |template|
|
||||||
try template.render(&data)
|
try template.render(&data, jetzig.TemplateContext, .{}, .{})
|
||||||
else
|
else
|
||||||
null;
|
null;
|
||||||
}
|
}
|
||||||
@ -152,7 +152,7 @@ fn defaultText(
|
|||||||
try data.addConst("jetzig_view", data.string(""));
|
try data.addConst("jetzig_view", data.string(""));
|
||||||
try data.addConst("jetzig_action", data.string(""));
|
try data.addConst("jetzig_action", data.string(""));
|
||||||
return if (jetzig.zmpl.findPrefixed("mailers", mailer.text_template)) |template|
|
return if (jetzig.zmpl.findPrefixed("mailers", mailer.text_template)) |template|
|
||||||
try template.render(&data)
|
try template.render(&data, jetzig.TemplateContext, .{}, .{})
|
||||||
else
|
else
|
||||||
null;
|
null;
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ const jetzig = @import("../jetzig.zig");
|
|||||||
pub const HtmxMiddleware = @import("middleware/HtmxMiddleware.zig");
|
pub const HtmxMiddleware = @import("middleware/HtmxMiddleware.zig");
|
||||||
pub const CompressionMiddleware = @import("middleware/CompressionMiddleware.zig");
|
pub const CompressionMiddleware = @import("middleware/CompressionMiddleware.zig");
|
||||||
pub const AuthMiddleware = @import("middleware/AuthMiddleware.zig");
|
pub const AuthMiddleware = @import("middleware/AuthMiddleware.zig");
|
||||||
|
pub const AntiCsrfMiddleware = @import("middleware/AntiCsrfMiddleware.zig");
|
||||||
|
|
||||||
const RouteOptions = struct {
|
const RouteOptions = struct {
|
||||||
content: ?[]const u8 = null,
|
content: ?[]const u8 = null,
|
||||||
|
73
src/jetzig/middleware/AntiCsrfMiddleware.zig
Normal file
73
src/jetzig/middleware/AntiCsrfMiddleware.zig
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const jetzig = @import("../../jetzig.zig");
|
||||||
|
|
||||||
|
pub const middleware_name = "anti_csrf";
|
||||||
|
|
||||||
|
const TokenParams = @Type(.{
|
||||||
|
.@"struct" = .{
|
||||||
|
.layout = .auto,
|
||||||
|
.is_tuple = false,
|
||||||
|
.decls = &.{},
|
||||||
|
.fields = &.{.{
|
||||||
|
.name = jetzig.authenticity_token_name ++ "",
|
||||||
|
.type = []const u8,
|
||||||
|
.is_comptime = false,
|
||||||
|
.default_value = null,
|
||||||
|
.alignment = @alignOf([]const u8),
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
pub fn afterRequest(request: *jetzig.http.Request) !void {
|
||||||
|
try verifyCsrfToken(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn beforeRender(request: *jetzig.http.Request, route: jetzig.views.Route) !void {
|
||||||
|
_ = route;
|
||||||
|
try verifyCsrfToken(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn logFailure(request: *jetzig.http.Request) !void {
|
||||||
|
_ = request.fail(.forbidden);
|
||||||
|
try request.server.logger.DEBUG("Anti-CSRF token validation failed. Request aborted.", .{});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn verifyCsrfToken(request: *jetzig.http.Request) !void {
|
||||||
|
switch (request.method) {
|
||||||
|
.DELETE, .PATCH, .PUT, .POST => {},
|
||||||
|
else => return,
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (request.requestFormat()) {
|
||||||
|
.HTML, .UNKNOWN => {},
|
||||||
|
// We do not authenticate JSON requests. Users must implement their own authentication
|
||||||
|
// system or disable JSON endpoints that should be protected.
|
||||||
|
.JSON => return,
|
||||||
|
}
|
||||||
|
|
||||||
|
const session = try request.session();
|
||||||
|
|
||||||
|
if (session.getT(.string, jetzig.authenticity_token_name)) |token| {
|
||||||
|
const params = try request.expectParams(TokenParams) orelse {
|
||||||
|
return logFailure(request);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (token.len != 32 or @field(params, jetzig.authenticity_token_name).len != 32) {
|
||||||
|
return try logFailure(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
var actual: [32]u8 = undefined;
|
||||||
|
var expected: [32]u8 = undefined;
|
||||||
|
|
||||||
|
@memcpy(&actual, token[0..32]);
|
||||||
|
@memcpy(&expected, @field(params, jetzig.authenticity_token_name)[0..32]);
|
||||||
|
|
||||||
|
const valid = std.crypto.timing_safe.eql([32]u8, expected, actual);
|
||||||
|
|
||||||
|
if (!valid) {
|
||||||
|
return try logFailure(request);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return try logFailure(request);
|
||||||
|
}
|
||||||
|
}
|
@ -11,11 +11,11 @@ const user_model = jetzig.config.get(jetzig.auth.AuthOptions, "auth").user_model
|
|||||||
/// they can also be modified.
|
/// they can also be modified.
|
||||||
user: ?@TypeOf(jetzig.database.Query(user_model).find(0)).ResultType,
|
user: ?@TypeOf(jetzig.database.Query(user_model).find(0)).ResultType,
|
||||||
|
|
||||||
const Self = @This();
|
const AuthMiddleware = @This();
|
||||||
|
|
||||||
/// Initialize middleware.
|
/// Initialize middleware.
|
||||||
pub fn init(request: *jetzig.http.Request) !*Self {
|
pub fn init(request: *jetzig.http.Request) !*AuthMiddleware {
|
||||||
const middleware = try request.allocator.create(Self);
|
const middleware = try request.allocator.create(AuthMiddleware);
|
||||||
middleware.* = .{ .user = null };
|
middleware.* = .{ .user = null };
|
||||||
return middleware;
|
return middleware;
|
||||||
}
|
}
|
||||||
@ -31,8 +31,11 @@ const map = std.StaticStringMap(void).initComptime(.{
|
|||||||
///
|
///
|
||||||
/// User ID is accessible from a request:
|
/// User ID is accessible from a request:
|
||||||
/// ```zig
|
/// ```zig
|
||||||
///
|
/// if (request.middleware(.auth).user) |user| {
|
||||||
pub fn afterRequest(self: *Self, request: *jetzig.http.Request) !void {
|
/// try request.server.log(.DEBUG, "{}", .{user.id});
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
pub fn afterRequest(self: *AuthMiddleware, request: *jetzig.http.Request) !void {
|
||||||
if (request.path.extension) |extension| {
|
if (request.path.extension) |extension| {
|
||||||
if (map.get(extension) == null) return;
|
if (map.get(extension) == null) return;
|
||||||
}
|
}
|
||||||
@ -47,6 +50,6 @@ pub fn afterRequest(self: *Self, request: *jetzig.http.Request) !void {
|
|||||||
/// Invoked after `afterRequest` is called, use this function to do any clean-up.
|
/// Invoked after `afterRequest` is called, use this function to do any clean-up.
|
||||||
/// Note that `request.allocator` is an arena allocator, so any allocations are automatically
|
/// Note that `request.allocator` is an arena allocator, so any allocations are automatically
|
||||||
/// done before the next request starts processing.
|
/// done before the next request starts processing.
|
||||||
pub fn deinit(self: *Self, request: *jetzig.http.Request) void {
|
pub fn deinit(self: *AuthMiddleware, request: *jetzig.http.Request) void {
|
||||||
request.allocator.destroy(self);
|
request.allocator.destroy(self);
|
||||||
}
|
}
|
||||||
|
@ -15,6 +15,8 @@ multipart_boundary: ?[]const u8 = null,
|
|||||||
logger: jetzig.loggers.Logger,
|
logger: jetzig.loggers.Logger,
|
||||||
server: Server,
|
server: Server,
|
||||||
repo: *jetzig.database.Repo,
|
repo: *jetzig.database.Repo,
|
||||||
|
cookies: *jetzig.http.Cookies,
|
||||||
|
session: *jetzig.http.Session,
|
||||||
|
|
||||||
const Server = struct { logger: jetzig.loggers.Logger };
|
const Server = struct { logger: jetzig.loggers.Logger };
|
||||||
|
|
||||||
@ -47,6 +49,13 @@ pub fn init(allocator: std.mem.Allocator, routes_module: type) !App {
|
|||||||
const app = try alloc.create(App);
|
const app = try alloc.create(App);
|
||||||
const repo = try alloc.create(jetzig.database.Repo);
|
const repo = try alloc.create(jetzig.database.Repo);
|
||||||
|
|
||||||
|
const cookies = try alloc.create(jetzig.http.Cookies);
|
||||||
|
cookies.* = jetzig.http.Cookies.init(alloc, "");
|
||||||
|
try cookies.parse();
|
||||||
|
|
||||||
|
const session = try alloc.create(jetzig.http.Session);
|
||||||
|
session.* = jetzig.http.Session.init(alloc, cookies, jetzig.testing.secret);
|
||||||
|
|
||||||
app.* = App{
|
app.* = App{
|
||||||
.arena = arena,
|
.arena = arena,
|
||||||
.allocator = allocator,
|
.allocator = allocator,
|
||||||
@ -57,6 +66,8 @@ pub fn init(allocator: std.mem.Allocator, routes_module: type) !App {
|
|||||||
.logger = logger,
|
.logger = logger,
|
||||||
.server = .{ .logger = logger },
|
.server = .{ .logger = logger },
|
||||||
.repo = repo,
|
.repo = repo,
|
||||||
|
.cookies = cookies,
|
||||||
|
.session = session,
|
||||||
};
|
};
|
||||||
|
|
||||||
repo.* = try jetzig.database.repo(alloc, app.*);
|
repo.* = try jetzig.database.repo(alloc, app.*);
|
||||||
@ -149,30 +160,61 @@ pub fn request(
|
|||||||
try server.decodeStaticParams();
|
try server.decodeStaticParams();
|
||||||
|
|
||||||
var buf: [1024]u8 = undefined;
|
var buf: [1024]u8 = undefined;
|
||||||
var httpz_request = try stubbedRequest(allocator, &buf, method, path, self.multipart_boundary, options);
|
var httpz_request = try stubbedRequest(
|
||||||
|
allocator,
|
||||||
|
&buf,
|
||||||
|
method,
|
||||||
|
path,
|
||||||
|
self.multipart_boundary,
|
||||||
|
options,
|
||||||
|
self.cookies,
|
||||||
|
);
|
||||||
var httpz_response = try stubbedResponse(allocator);
|
var httpz_response = try stubbedResponse(allocator);
|
||||||
|
|
||||||
try server.processNextRequest(&httpz_request, &httpz_response);
|
try server.processNextRequest(&httpz_request, &httpz_response);
|
||||||
var headers = std.ArrayList(jetzig.testing.TestResponse.Header).init(self.arena.allocator());
|
|
||||||
for (0..httpz_response.headers.len) |index| {
|
{
|
||||||
try headers.append(.{
|
const cookies = try allocator.create(jetzig.http.Cookies);
|
||||||
.name = try self.arena.allocator().dupe(u8, httpz_response.headers.keys[index]),
|
cookies.* = jetzig.http.Cookies.init(allocator, "");
|
||||||
.value = try self.arena.allocator().dupe(u8, httpz_response.headers.values[index]),
|
try cookies.parse();
|
||||||
});
|
self.cookies = cookies;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var headers = std.ArrayList(jetzig.testing.TestResponse.Header).init(allocator);
|
||||||
|
for (0..httpz_response.headers.len) |index| {
|
||||||
|
const key = httpz_response.headers.keys[index];
|
||||||
|
const value = httpz_response.headers.values[index];
|
||||||
|
|
||||||
|
try headers.append(.{
|
||||||
|
.name = try allocator.dupe(u8, key),
|
||||||
|
.value = try allocator.dupe(u8, httpz_response.headers.values[index]),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (std.ascii.eqlIgnoreCase(key, "set-cookie")) {
|
||||||
|
// FIXME: We only expect one set-cookie header at the moment.
|
||||||
|
const cookies = try allocator.create(jetzig.http.Cookies);
|
||||||
|
cookies.* = jetzig.http.Cookies.init(allocator, value);
|
||||||
|
self.cookies = cookies;
|
||||||
|
try self.cookies.parse();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var data = jetzig.data.Data.init(allocator);
|
var data = jetzig.data.Data.init(allocator);
|
||||||
defer data.deinit();
|
defer data.deinit();
|
||||||
|
|
||||||
var jobs = std.ArrayList(jetzig.testing.TestResponse.Job).init(self.arena.allocator());
|
var jobs = std.ArrayList(jetzig.testing.TestResponse.Job).init(allocator);
|
||||||
while (try self.job_queue.popFirst(&data, "__jetzig_jobs")) |value| {
|
while (try self.job_queue.popFirst(&data, "__jetzig_jobs")) |value| {
|
||||||
if (value.getT(.string, "__jetzig_job_name")) |job_name| try jobs.append(.{
|
if (value.getT(.string, "__jetzig_job_name")) |job_name| try jobs.append(.{
|
||||||
.name = try self.arena.allocator().dupe(u8, job_name),
|
.name = try allocator.dupe(u8, job_name),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try self.initSession();
|
||||||
|
|
||||||
return .{
|
return .{
|
||||||
.allocator = self.arena.allocator(),
|
.allocator = allocator,
|
||||||
.status = httpz_response.status,
|
.status = httpz_response.status,
|
||||||
.body = try self.arena.allocator().dupe(u8, httpz_response.body),
|
.body = try allocator.dupe(u8, httpz_response.body),
|
||||||
.headers = try headers.toOwnedSlice(),
|
.headers = try headers.toOwnedSlice(),
|
||||||
.jobs = try jobs.toOwnedSlice(),
|
.jobs = try jobs.toOwnedSlice(),
|
||||||
};
|
};
|
||||||
@ -189,6 +231,16 @@ pub fn params(self: App, args: anytype) []Param {
|
|||||||
return array.toOwnedSlice() catch @panic("OOM");
|
return array.toOwnedSlice() catch @panic("OOM");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn initSession(self: *App) !void {
|
||||||
|
const allocator = self.arena.allocator();
|
||||||
|
|
||||||
|
var local_session = try allocator.create(jetzig.http.Session);
|
||||||
|
local_session.* = jetzig.http.Session.init(allocator, self.cookies, jetzig.testing.secret);
|
||||||
|
try local_session.parse();
|
||||||
|
|
||||||
|
self.session = local_session;
|
||||||
|
}
|
||||||
|
|
||||||
/// Encode an arbitrary struct to a JSON string for use as a request body.
|
/// Encode an arbitrary struct to a JSON string for use as a request body.
|
||||||
pub fn json(self: App, args: anytype) []const u8 {
|
pub fn json(self: App, args: anytype) []const u8 {
|
||||||
const allocator = self.arena.allocator();
|
const allocator = self.arena.allocator();
|
||||||
@ -245,15 +297,29 @@ fn stubbedRequest(
|
|||||||
comptime path: []const u8,
|
comptime path: []const u8,
|
||||||
multipart_boundary: ?[]const u8,
|
multipart_boundary: ?[]const u8,
|
||||||
options: RequestOptions,
|
options: RequestOptions,
|
||||||
|
maybe_cookies: ?*const jetzig.http.Cookies,
|
||||||
) !httpz.Request {
|
) !httpz.Request {
|
||||||
// TODO: Use httpz.testing
|
// TODO: Use httpz.testing
|
||||||
var request_headers = try keyValue(allocator, 32);
|
var request_headers = try keyValue(allocator, 32);
|
||||||
for (options.headers) |header| request_headers.add(header.name, header.value);
|
for (options.headers) |header| request_headers.add(header.name, header.value);
|
||||||
|
|
||||||
|
if (maybe_cookies) |cookies| {
|
||||||
|
var cookie_buf = std.ArrayList(u8).init(allocator);
|
||||||
|
const cookie_writer = cookie_buf.writer();
|
||||||
|
try cookie_writer.print("{}", .{cookies});
|
||||||
|
const cookie = try cookie_buf.toOwnedSlice();
|
||||||
|
request_headers.add("cookie", cookie);
|
||||||
|
}
|
||||||
|
|
||||||
if (options.json != null) {
|
if (options.json != null) {
|
||||||
request_headers.add("accept", "application/json");
|
request_headers.add("accept", "application/json");
|
||||||
request_headers.add("content-type", "application/json");
|
request_headers.add("content-type", "application/json");
|
||||||
} else if (multipart_boundary) |boundary| {
|
} else if (multipart_boundary) |boundary| {
|
||||||
const header = try std.mem.concat(allocator, u8, &.{ "multipart/form-data; boundary=", boundary });
|
const header = try std.mem.concat(
|
||||||
|
allocator,
|
||||||
|
u8,
|
||||||
|
&.{ "multipart/form-data; boundary=", boundary },
|
||||||
|
);
|
||||||
request_headers.add("content-type", header);
|
request_headers.add("content-type", header);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -358,7 +424,10 @@ fn buildOptions(allocator: std.mem.Allocator, app: *const App, args: anytype) !R
|
|||||||
}
|
}
|
||||||
|
|
||||||
return .{
|
return .{
|
||||||
.headers = if (@hasField(@TypeOf(args), "headers")) try buildHeaders(allocator, args.headers) else &.{},
|
.headers = if (@hasField(@TypeOf(args), "headers"))
|
||||||
|
try buildHeaders(allocator, args.headers)
|
||||||
|
else
|
||||||
|
&.{},
|
||||||
.json = if (@hasField(@TypeOf(args), "json")) app.json(args.json) else null,
|
.json = if (@hasField(@TypeOf(args), "json")) app.json(args.json) else null,
|
||||||
.params = if (@hasField(@TypeOf(args), "params")) app.params(args.params) else null,
|
.params = if (@hasField(@TypeOf(args), "params")) app.params(args.params) else null,
|
||||||
.body = if (@hasField(@TypeOf(args), "body")) args.body else null,
|
.body = if (@hasField(@TypeOf(args), "body")) args.body else null,
|
||||||
@ -368,7 +437,12 @@ fn buildOptions(allocator: std.mem.Allocator, app: *const App, args: anytype) !R
|
|||||||
fn buildHeaders(allocator: std.mem.Allocator, args: anytype) ![]const jetzig.testing.TestResponse.Header {
|
fn buildHeaders(allocator: std.mem.Allocator, args: anytype) ![]const jetzig.testing.TestResponse.Header {
|
||||||
var headers = std.ArrayList(jetzig.testing.TestResponse.Header).init(allocator);
|
var headers = std.ArrayList(jetzig.testing.TestResponse.Header).init(allocator);
|
||||||
inline for (std.meta.fields(@TypeOf(args))) |field| {
|
inline for (std.meta.fields(@TypeOf(args))) |field| {
|
||||||
try headers.append(jetzig.testing.TestResponse.Header{ .name = field.name, .value = @field(args, field.name) });
|
try headers.append(
|
||||||
|
jetzig.testing.TestResponse.Header{
|
||||||
|
.name = field.name,
|
||||||
|
.value = @field(args, field.name),
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return try headers.toOwnedSlice();
|
return try headers.toOwnedSlice();
|
||||||
}
|
}
|
||||||
|
@ -67,25 +67,25 @@ pub inline fn unquote(input: []const u8) []const u8 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Generate a secure random string of `len` characters (for cryptographic purposes).
|
/// Generate a secure random string of `len` characters (for cryptographic purposes).
|
||||||
pub fn generateSecret(allocator: std.mem.Allocator, comptime len: u10) ![]const u8 {
|
pub fn generateSecret(allocator: std.mem.Allocator, len: u10) ![]const u8 {
|
||||||
const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
||||||
var secret: [len]u8 = undefined;
|
const secret = try allocator.alloc(u8, len);
|
||||||
|
|
||||||
for (0..len) |index| {
|
for (0..len) |index| {
|
||||||
secret[index] = chars[std.crypto.random.intRangeAtMost(u8, 0, chars.len)];
|
secret[index] = chars[std.crypto.random.intRangeAtMost(u8, 0, chars.len - 1)];
|
||||||
}
|
}
|
||||||
|
|
||||||
return try allocator.dupe(u8, &secret);
|
return secret;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn generateRandomString(buf: []u8) []const u8 {
|
pub fn generateRandomString(buf: []u8) []const u8 {
|
||||||
const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
||||||
|
|
||||||
for (0..buf.len) |index| {
|
for (0..buf.len) |index| {
|
||||||
buf[index] = chars[std.crypto.random.intRangeAtMost(u8, 0, chars.len)];
|
buf[index] = chars[std.crypto.random.intRangeAtMost(u8, 0, chars.len - 1)];
|
||||||
}
|
}
|
||||||
|
|
||||||
return buf;
|
return buf[0..];
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Calculate a duration from a given start time (in nanoseconds) to the current time.
|
/// Calculate a duration from a given start time (in nanoseconds) to the current time.
|
||||||
|
@ -56,6 +56,8 @@ json_params: []const []const u8,
|
|||||||
params: std.ArrayList(*jetzig.data.Data) = undefined,
|
params: std.ArrayList(*jetzig.data.Data) = undefined,
|
||||||
id: []const u8,
|
id: []const u8,
|
||||||
formats: ?Formats = null,
|
formats: ?Formats = null,
|
||||||
|
before_callbacks: []const jetzig.callbacks.BeforeCallback = &.{},
|
||||||
|
after_callbacks: []const jetzig.callbacks.AfterCallback = &.{},
|
||||||
|
|
||||||
/// Initializes a route's static params on server launch. Converts static params (JSON strings)
|
/// Initializes a route's static params on server launch. Converts static params (JSON strings)
|
||||||
/// to `jetzig.data.Data` values. Memory is owned by caller (`App.start()`).
|
/// to `jetzig.data.Data` values. Memory is owned by caller (`App.start()`).
|
||||||
|
Loading…
x
Reference in New Issue
Block a user