Skip to content

Commit

Permalink
Merge pull request #120 from jetzig-framework/anti-csrf
Browse files Browse the repository at this point in the history
Closes #108: Anti-CSRF middleware
  • Loading branch information
bobf authored Nov 23, 2024
2 parents b1becfd + 6e6f1be commit 130a7c8
Show file tree
Hide file tree
Showing 28 changed files with 696 additions and 138 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ jobs:
- name: Run App Tests
run: |
cd demo
zig build jetzig:test
zig build -Denvironment=testing jetzig:test
- name: Build artifacts
if: ${{ matrix.os == 'ubuntu-latest' }}
Expand Down
21 changes: 18 additions & 3 deletions build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ pub fn build(b: *std.Build) !void {
main_tests.root_module.addImport("smtp", smtp_client_dep.module("smtp_client"));
const test_build_options = b.addOptions();
test_build_options.addOption(Environment, "environment", .testing);
test_build_options.addOption(bool, "build_static", true);
const run_main_tests = b.addRunArtifact(main_tests);
main_tests.root_module.addOptions("build_options", test_build_options);

Expand Down Expand Up @@ -137,6 +138,11 @@ pub fn jetzigInit(b: *std.Build, exe: *std.Build.Step.Compile, options: JetzigIn
"environment",
"Jetzig server environment.",
) 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(
"jetzig",
Expand Down Expand Up @@ -164,6 +170,7 @@ pub fn jetzigInit(b: *std.Build, exe: *std.Build.Step.Compile, options: JetzigIn

const build_options = b.addOptions();
build_options.addOption(Environment, "environment", environment);
build_options.addOption(bool, "build_static", build_static);
jetzig_module.addOptions("build_options", build_options);

exe.root_module.addImport("jetzig", jetzig_module);
Expand Down Expand Up @@ -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);

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 });
exe_static_routes.root_module.addImport("markdown_fragments", markdown_fragments_module);

const run_static_routes_cmd = b.addRunArtifact(exe_static_routes);
const static_outputs_path = run_static_routes_cmd.addOutputFileArg("static.zig");
const static_module = b.createModule(.{ .root_source_file = static_outputs_path });
exe.root_module.addImport("static", static_module);
const static_module = if (build_static)
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);

const run_tests_file_cmd = b.addRunArtifact(exe_routes_file);
Expand Down
4 changes: 2 additions & 2 deletions build.zig.zon
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
.hash = "1220d0e8734628fd910a73146e804d10a3269e3e7d065de6bb0e3e88d5ba234eb163",
},
.zmpl = .{
.url = "https://github.com/jetzig-framework/zmpl/archive/25b91d030b992631d319adde1cf01baecd9f3934.tar.gz",
.hash = "12208dd5a4bf0c6c7efc4e9f37a5d8ed80d6004d5680176d1fc2114bfa593e927baf",
.url = "https://github.com/jetzig-framework/zmpl/archive/af75c8b842c3957eb97b4fc4bc49c7b2243968fa.tar.gz",
.hash = "1220ecac93d295dafd2f034a86f0979f6108d40e5ea1a39e3a2b9977c35147cac684",
},
.jetkv = .{
.url = "https://github.com/jetzig-framework/jetkv/archive/2b1130a48979ea2871c8cf6ca89c38b1e7062839.tar.gz",
Expand Down
77 changes: 77 additions & 0 deletions demo/src/app/views/anti_csrf.zig
Original file line number Diff line number Diff line change
@@ -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 changes: 8 additions & 0 deletions demo/src/app/views/anti_csrf/index.zmpl
Original file line number Diff line number Diff line change
@@ -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 changes: 5 additions & 0 deletions demo/src/app/views/anti_csrf/post.zmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<h1>Spam Submitted Successfully</h1>

<h2>Spam:</h2>

<div>{{$.spam}}</div>
17 changes: 11 additions & 6 deletions demo/src/main.zig
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ pub const jetzig_options = struct {
/// Middleware chain. Add any custom middleware here, or use middleware provided in
/// `jetzig.middleware` (e.g. `jetzig.middleware.HtmxMiddleware`).
pub const middleware: []const type = &.{
// jetzig.middleware.AuthMiddleware,
// jetzig.middleware.AntiCsrfMiddleware,
// jetzig.middleware.HtmxMiddleware,
// jetzig.middleware.CompressionMiddleware,
// @import("app/middleware/DemoMiddleware.zig"),
Expand Down Expand Up @@ -79,13 +81,16 @@ pub const jetzig_options = struct {
pub const Schema = @import("Schema");

/// HTTP cookie configuration
pub const cookies: jetzig.http.Cookies.CookieOptions = .{
.domain = switch (jetzig.environment) {
.development => "localhost",
.testing => "localhost",
.production => "www.example.com",
pub const cookies: jetzig.http.Cookies.CookieOptions = switch (jetzig.environment) {
.development, .testing => .{
.domain = "localhost",
.path = "/",
},
.production => .{
.same_site = true,
.secure = true,
.http_only = true,
},
.path = "/",
};

/// Key-value store options. Set backend to `.file` to use a file-based store.
Expand Down
4 changes: 3 additions & 1 deletion src/Routes.zig
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,8 @@ fn writeRoute(self: *Routes, writer: std.ArrayList(u8).Writer, route: Function)
\\ .static = {4s},
\\ .uri_path = "{5s}",
\\ .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,
\\ .json_params = &[_][]const u8 {{ {8s} }},
\\ .formats = if (@hasDecl(@import("{7s}"), "formats")) @import("{7s}").formats else null,
Expand Down Expand Up @@ -389,7 +391,7 @@ fn generateRoutesForView(self: *Routes, dir: std.fs.Dir, path: []const u8) !Rout

for (capture.args, 0..) |arg, arg_index| {
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;
try static_routes.append(capture.*);
} else if (std.mem.eql(u8, try arg.typeBasename(), "Request")) {
Expand Down
4 changes: 2 additions & 2 deletions src/commands/routes.zig
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ pub fn main() !void {

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;

inline for (routes.routes) |route| max_uri_path_len = @max(route.uri_path.len + 5, max_uri_path_len);
Expand Down Expand Up @@ -44,7 +44,7 @@ pub fn main() !void {
}

var jetzig_app = jetzig.App{
.environment = environment,
.env = environment,
.allocator = allocator,
.custom_routes = std.ArrayList(jetzig.views.Route).init(allocator),
.initHook = initHook,
Expand Down
11 changes: 8 additions & 3 deletions src/compile_static_routes.zig
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ fn renderMarkdown(

if (zmpl.findPrefixed("views", prefixed_name)) |layout| {
view.data.content = .{ .data = content };
return try layout.render(view.data);
return try layout.render(view.data, jetzig.TemplateContext, .{}, .{});
} else {
std.debug.print("Unknown layout: {s}\n", .{layout_name});
return content;
Expand All @@ -170,13 +170,18 @@ fn renderZmplTemplate(
defer allocator.free(prefixed_name);

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 {
std.debug.print("Unknown layout: {s}\n", .{layout_name});
return try allocator.dupe(u8, "");
}
} else {
return try template.render(view.data);
return try template.render(view.data, jetzig.TemplateContext, .{}, .{});
}
} else return null;
}
Expand Down
10 changes: 9 additions & 1 deletion src/jetzig.zig
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,15 @@ pub const database = @import("jetzig/database.zig");
pub const testing = @import("jetzig/testing.zig");
pub const config = @import("jetzig/config.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 Time = jetcommon.types.Time;
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 environment = std.enums.nameCast(Environment.EnvironmentName, build_options.environment);

Expand All @@ -46,6 +50,9 @@ pub const Request = http.Request;
/// requests.
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`,
/// `Array`, `String`, `Integer`, `Float`, `Boolean`, and `NullType`.
pub const Data = data.Data;
Expand Down Expand Up @@ -78,7 +85,8 @@ pub const Logger = loggers.Logger;

pub const root = @import("root");
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;

Expand Down
6 changes: 4 additions & 2 deletions src/jetzig/App.zig
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ pub fn deinit(self: *const App) void {
@constCast(self).custom_routes.deinit();
}

// Not used yet, but allows us to add new options to `start()` without breaking
// backward-compatibility.
/// Specify a global value accessible as `request.server.global`.
/// Must specify type by defining `pub const Global` in your app's `src/main.zig`.
const AppOptions = struct {
global: *anyopaque = undefined,
};
Expand Down Expand Up @@ -228,6 +228,8 @@ pub fn createRoutes(
.template = const_route.template,
.json_params = const_route.json_params,
.formats = const_route.formats,
.before_callbacks = const_route.before_callbacks,
.after_callbacks = const_route.after_callbacks,
};

try var_route.initParams(allocator);
Expand Down
25 changes: 25 additions & 0 deletions src/jetzig/TemplateContext.zig
Original file line number Diff line number Diff line change
@@ -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;
}
Loading

0 comments on commit 130a7c8

Please sign in to comment.