Provide tooling for running Jetzig in deployment

This commit is contained in:
Bob Farrell 2024-03-19 23:03:55 +00:00
parent adba4756c5
commit 87bcc4c9e0
22 changed files with 365 additions and 251 deletions

View File

@ -39,7 +39,7 @@ pub fn build(b: *std.Build) !void {
lib.root_module.addImport("zmpl", zmpl_dep.module("zmpl")); lib.root_module.addImport("zmpl", zmpl_dep.module("zmpl"));
jetzig_module.addImport("zmpl", zmpl_dep.module("zmpl")); jetzig_module.addImport("zmpl", zmpl_dep.module("zmpl"));
lib.root_module.addImport("args", zig_args_dep.module("args")); jetzig_module.addImport("args", zig_args_dep.module("args"));
// This is the way to make it look nice in the zig build script // This is the way to make it look nice in the zig build script
// If we would do it the other way around, we would have to do // If we would do it the other way around, we would have to do

View File

@ -7,8 +7,8 @@
.hash = "1220e5ede084ca6b94defd466a8f8779aab151d37bf688fefb928fded6f02cde4135", .hash = "1220e5ede084ca6b94defd466a8f8779aab151d37bf688fefb928fded6f02cde4135",
}, },
.args = .{ .args = .{
.url = "https://github.com/MasterQ32/zig-args/archive/89f18a104d9c13763b90e97d6b4ce133da8a3e2b.tar.gz", .url = "https://github.com/bobf/zig-args/archive/e827c93f00e8bd95bd4b970c59593f393a6b08d5.tar.gz",
.hash = "12203ded54c85878eea7f12744066dcb4397177395ac49a7b2aa365bf6047b623829", .hash = "12208a1de366740d11de525db7289345949f5fd46527db3f89eecc7bb49b012c0732",
}, },
}, },

View File

@ -5,8 +5,8 @@
.dependencies = .{ .dependencies = .{
.args = .{ .args = .{
.url = "https://github.com/MasterQ32/zig-args/archive/89f18a104d9c13763b90e97d6b4ce133da8a3e2b.tar.gz", .url = "https://github.com/bobf/zig-args/archive/e827c93f00e8bd95bd4b970c59593f393a6b08d5.tar.gz",
.hash = "12203ded54c85878eea7f12744066dcb4397177395ac49a7b2aa365bf6047b623829", .hash = "12208a1de366740d11de525db7289345949f5fd46527db3f89eecc7bb49b012c0732",
}, },
}, },
.paths = .{ .paths = .{

View File

@ -4,12 +4,13 @@ const view = @import("generate/view.zig");
const partial = @import("generate/partial.zig"); const partial = @import("generate/partial.zig");
const layout = @import("generate/layout.zig"); const layout = @import("generate/layout.zig");
const middleware = @import("generate/middleware.zig"); const middleware = @import("generate/middleware.zig");
const secret = @import("generate/secret.zig");
const util = @import("../util.zig"); const util = @import("../util.zig");
/// Command line options for the `generate` command. /// Command line options for the `generate` command.
pub const Options = struct { pub const Options = struct {
pub const meta = .{ pub const meta = .{
.usage_summary = "[view|partial|layout|middleware] [options]", .usage_summary = "[view|partial|layout|middleware|secret] [options]",
.full_text = .full_text =
\\Generates scaffolding for views, middleware, and other objects in future. \\Generates scaffolding for views, middleware, and other objects in future.
\\ \\
@ -44,7 +45,7 @@ pub fn run(
try args.printHelp(Options, "jetzig generate", writer); try args.printHelp(Options, "jetzig generate", writer);
return; return;
} }
var generate_type: ?enum { view, partial, layout, middleware } = null; var generate_type: ?enum { view, partial, layout, middleware, secret } = null;
var sub_args = std.ArrayList([]const u8).init(allocator); var sub_args = std.ArrayList([]const u8).init(allocator);
defer sub_args.deinit(); defer sub_args.deinit();
@ -57,6 +58,8 @@ pub fn run(
generate_type = .layout; generate_type = .layout;
} else if (generate_type == null and std.mem.eql(u8, arg, "middleware")) { } else if (generate_type == null and std.mem.eql(u8, arg, "middleware")) {
generate_type = .middleware; generate_type = .middleware;
} else if (generate_type == null and std.mem.eql(u8, arg, "secret")) {
generate_type = .secret;
} else if (generate_type == null) { } else if (generate_type == null) {
std.debug.print("Unknown generator command: {s}\n", .{arg}); std.debug.print("Unknown generator command: {s}\n", .{arg});
return error.JetzigCommandError; return error.JetzigCommandError;
@ -71,6 +74,7 @@ pub fn run(
.partial => partial.run(allocator, cwd, sub_args.items), .partial => partial.run(allocator, cwd, sub_args.items),
.layout => layout.run(allocator, cwd, sub_args.items), .layout => layout.run(allocator, cwd, sub_args.items),
.middleware => middleware.run(allocator, cwd, sub_args.items), .middleware => middleware.run(allocator, cwd, sub_args.items),
.secret => secret.run(allocator, cwd, sub_args.items),
}; };
} else { } else {
std.debug.print("Missing sub-command. Expected: [view|partial|layout|middleware]\n", .{}); std.debug.print("Missing sub-command. Expected: [view|partial|layout|middleware]\n", .{});

View File

@ -0,0 +1,16 @@
const std = @import("std");
/// Generate a secure random secret and output to stdout.
pub fn run(allocator: std.mem.Allocator, cwd: std.fs.Dir, args: [][]const u8) !void {
_ = allocator;
_ = args;
_ = cwd;
const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
var secret: [44]u8 = undefined;
for (0..44) |index| {
secret[index] = chars[std.crypto.random.intRangeAtMost(u8, 0, chars.len)];
}
std.debug.print("{s}\n", .{secret});
}

View File

@ -33,14 +33,17 @@ pub fn init(request: *jetzig.http.Request) !*Self {
/// Any calls to `request.render` or `request.redirect` will prevent further processing of the /// Any calls to `request.render` or `request.redirect` will prevent further processing of the
/// request, including any other middleware in the chain. /// request, including any other middleware in the chain.
pub fn afterRequest(self: *Self, request: *jetzig.http.Request) !void { pub fn afterRequest(self: *Self, request: *jetzig.http.Request) !void {
request.server.logger.debug("[DemoMiddleware:afterRequest] my_custom_value: {s}", .{self.my_custom_value}); try request.server.logger.DEBUG(
"[DemoMiddleware:afterRequest] my_custom_value: {s}",
.{self.my_custom_value},
);
self.my_custom_value = @tagName(request.method); self.my_custom_value = @tagName(request.method);
} }
/// Invoked immediately before the response renders to the client. /// Invoked immediately before the response renders to the client.
/// The response can be modified here if needed. /// The response can be modified here if needed.
pub fn beforeResponse(self: *Self, request: *jetzig.http.Request, response: *jetzig.http.Response) !void { pub fn beforeResponse(self: *Self, request: *jetzig.http.Request, response: *jetzig.http.Response) !void {
request.server.logger.debug( try request.server.logger.DEBUG(
"[DemoMiddleware:beforeResponse] my_custom_value: {s}, response status: {s}", "[DemoMiddleware:beforeResponse] my_custom_value: {s}, response status: {s}",
.{ self.my_custom_value, @tagName(response.status_code) }, .{ self.my_custom_value, @tagName(response.status_code) },
); );
@ -51,7 +54,7 @@ pub fn beforeResponse(self: *Self, request: *jetzig.http.Request, response: *jet
pub fn afterResponse(self: *Self, request: *jetzig.http.Request, response: *jetzig.http.Response) !void { pub fn afterResponse(self: *Self, request: *jetzig.http.Request, response: *jetzig.http.Response) !void {
_ = self; _ = self;
_ = response; _ = response;
request.server.logger.debug("[DemoMiddleware:afterResponse] response completed", .{}); try request.server.logger.DEBUG("[DemoMiddleware:afterResponse] response completed", .{});
} }
/// Invoked after `afterResponse` is called. Use this function to do any clean-up. /// Invoked after `afterResponse` is called. Use this function to do any clean-up.

View File

@ -5,16 +5,19 @@ pub const zmpl = @import("zmpl").zmpl;
pub const http = @import("jetzig/http.zig"); pub const http = @import("jetzig/http.zig");
pub const loggers = @import("jetzig/loggers.zig"); pub const loggers = @import("jetzig/loggers.zig");
pub const data = @import("jetzig/data.zig"); pub const data = @import("jetzig/data.zig");
pub const caches = @import("jetzig/caches.zig");
pub const views = @import("jetzig/views.zig"); pub const views = @import("jetzig/views.zig");
pub const colors = @import("jetzig/colors.zig"); pub const colors = @import("jetzig/colors.zig");
pub const middleware = @import("jetzig/middleware.zig"); pub const middleware = @import("jetzig/middleware.zig");
pub const util = @import("jetzig/util.zig"); pub const util = @import("jetzig/util.zig");
pub const types = @import("jetzig/types.zig");
/// The primary interface for a Jetzig application. Create an `App` in your application's /// The primary interface for a Jetzig application. Create an `App` in your application's
/// `src/main.zig` and call `start` to launch the application. /// `src/main.zig` and call `start` to launch the application.
pub const App = @import("jetzig/App.zig"); pub const App = @import("jetzig/App.zig");
/// Configuration options for the application server with command-line argument parsing.
pub const Environment = @import("jetzig/Environment.zig");
/// An HTTP request which is passed to (dynamic) view functions and provides access to params, /// An HTTP request which is passed to (dynamic) view functions and provides access to params,
/// headers, and functions to render a response. /// headers, and functions to render a response.
pub const Request = http.Request; pub const Request = http.Request;
@ -43,36 +46,11 @@ pub fn init(allocator: std.mem.Allocator) !App {
const args = try std.process.argsAlloc(allocator); const args = try std.process.argsAlloc(allocator);
defer std.process.argsFree(allocator, args); defer std.process.argsFree(allocator, args);
const host: []const u8 = if (args.len > 1) const environment = Environment.init(allocator);
try allocator.dupe(u8, args[1])
else
try allocator.dupe(u8, "127.0.0.1");
// TODO: Fix this up with proper arg parsing
const port: u16 = if (args.len > 2) try std.fmt.parseInt(u16, args[2], 10) else 8080;
const use_cache: bool = args.len > 3 and std.mem.eql(u8, args[3], "--cache");
const server_cache = switch (use_cache) {
true => caches.Cache{ .memory_cache = caches.MemoryCache.init(allocator) },
false => caches.Cache{ .null_cache = caches.NullCache.init(allocator) },
};
var logger = loggers.Logger{ .development_logger = loggers.DevelopmentLogger.init(allocator) };
const secret = try generateSecret(allocator);
logger.debug(
"Running in development mode, using auto-generated cookie encryption key:\n {s}",
.{secret},
);
const server_options = http.Server.ServerOptions{
.cache = server_cache,
.logger = logger,
.secret = secret,
};
return .{ return .{
.server_options = server_options, .server_options = try environment.getServerOptions(),
.allocator = allocator, .allocator = allocator,
.host = host,
.port = port,
}; };
} }
@ -154,36 +132,3 @@ pub fn route(comptime routes: anytype) []views.Route {
return &detected; return &detected;
} }
pub fn generateSecret(allocator: std.mem.Allocator) ![]const u8 {
const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
var secret: [64]u8 = undefined;
for (0..64) |index| {
secret[index] = chars[std.crypto.random.intRangeAtMost(u8, 0, chars.len)];
}
return try allocator.dupe(u8, &secret);
}
pub fn base64Encode(allocator: std.mem.Allocator, string: []const u8) ![]const u8 {
const encoder = std.base64.Base64Encoder.init(
std.base64.url_safe_no_pad.alphabet_chars,
std.base64.url_safe_no_pad.pad_char,
);
const size = encoder.calcSize(string.len);
const ptr = try allocator.alloc(u8, size);
_ = encoder.encode(ptr, string);
return ptr;
}
pub fn base64Decode(allocator: std.mem.Allocator, string: []const u8) ![]const u8 {
const decoder = std.base64.Base64Decoder.init(
std.base64.url_safe_no_pad.alphabet_chars,
std.base64.url_safe_no_pad.pad_char,
);
const size = try decoder.calcSizeForSlice(string);
const ptr = try allocator.alloc(u8, size);
try decoder.decode(ptr, string);
return ptr;
}

View File

@ -1,5 +1,7 @@
const std = @import("std"); const std = @import("std");
const args = @import("args");
const jetzig = @import("../jetzig.zig"); const jetzig = @import("../jetzig.zig");
const mime_types = @import("mime_types").mime_types; // Generated at build time. const mime_types = @import("mime_types").mime_types; // Generated at build time.
@ -7,8 +9,6 @@ const Self = @This();
server_options: jetzig.http.Server.ServerOptions, server_options: jetzig.http.Server.ServerOptions,
allocator: std.mem.Allocator, allocator: std.mem.Allocator,
host: []const u8,
port: u16,
pub fn deinit(self: Self) void { pub fn deinit(self: Self) void {
_ = self; _ = self;
@ -48,30 +48,40 @@ pub fn start(self: Self, comptime_routes: []jetzig.views.Route) !void {
self.allocator.destroy(route); self.allocator.destroy(route);
}; };
if (self.server_options.detach) {
const argv = try std.process.argsAlloc(self.allocator);
defer std.process.argsFree(self.allocator, argv);
var child_argv = std.ArrayList([]const u8).init(self.allocator);
for (argv) |arg| {
if (!std.mem.eql(u8, "-d", arg) and !std.mem.eql(u8, "--detach", arg)) {
try child_argv.append(arg);
}
}
var child = std.process.Child.init(child_argv.items, self.allocator);
try child.spawn();
std.debug.print("Spawned child process. PID: {}. Exiting.\n", .{child.id});
std.process.exit(0);
}
var server = jetzig.http.Server.init( var server = jetzig.http.Server.init(
self.allocator, self.allocator,
self.host,
self.port,
self.server_options, self.server_options,
routes.items, routes.items,
&mime_map, &mime_map,
); );
defer server.deinit(); defer server.deinit();
defer self.allocator.free(self.host);
defer self.allocator.free(server.options.secret);
server.listen() catch |err| { server.listen() catch |err| {
switch (err) { switch (err) {
error.AddressInUse => { error.AddressInUse => {
server.logger.debug( try server.logger.ERROR(
"Socket unavailable: {s}:{} - unable to start server.\n", "Socket unavailable: {s}:{} - unable to start server.\n",
.{ self.host, self.port }, .{ self.server_options.bind, self.server_options.port },
); );
return; return;
}, },
else => { else => {
server.logger.debug("Encountered error: {}\nExiting.\n", .{err}); try server.logger.ERROR("Encountered error: {}\nExiting.\n", .{err});
return err; return err;
}, },
} }

124
src/jetzig/Environment.zig Normal file
View File

@ -0,0 +1,124 @@
const std = @import("std");
const args = @import("args");
const jetzig = @import("../jetzig.zig");
const Environment = @This();
allocator: std.mem.Allocator,
const Options = struct {
help: bool = false,
bind: []const u8 = "127.0.0.1",
port: u16 = 8080,
environment: []const u8 = "development",
log: []const u8 = "-",
@"log-error": []const u8 = "-",
@"log-level": jetzig.loggers.LogLevel = .DEBUG,
detach: bool = false,
pub const shorthands = .{
.h = "help",
.b = "bind",
.p = "port",
.e = "environment",
.d = "detach",
};
pub const wrap_len = 80;
pub const meta = .{
.option_docs = .{
.bind = "IP address/hostname to bind to (default: 127.0.0.1)",
.port = "Port to listen on (default: 8080)",
.environment = "Load an environment configuration from src/app/environments/<environment>.zig",
.log = "Path to log file. Use '-' for stdout (default: -)",
.@"log-error" =
\\Optional path to separate error log file. Use '-' for stdout. If omitted, errors are logged to the location specified by the `log` option.
,
.@"log-level" =
\\Specify the minimum log level. Log events below the given level are ignored. Must be one of: TRACE, DEBUG, INFO, WARN, ERROR, FATAL (default: DEBUG)
,
.detach =
\\Run the server in the background. Must be used in conjunction with --log (default: false)
,
.help = "Print help and exit",
},
};
};
pub fn init(allocator: std.mem.Allocator) Environment {
return .{ .allocator = allocator };
}
/// Generate server initialization options using command line args with defaults.
pub fn getServerOptions(self: Environment) !jetzig.http.Server.ServerOptions {
const options = try args.parseForCurrentProcess(Options, self.allocator, .print);
if (options.options.help) {
const writer = std.io.getStdErr().writer();
try args.printHelp(Options, options.executable_name orelse "<app-name>", writer);
std.process.exit(0);
}
var logger = jetzig.loggers.Logger{
.development_logger = jetzig.loggers.DevelopmentLogger.init(
self.allocator,
try getLogFile(options.options.log),
),
};
if (options.options.detach and std.mem.eql(u8, options.options.log, "-")) {
try logger.ERROR("Must pass `--log` when using `--detach`.", .{});
std.process.exit(1);
}
// TODO: Generate nonce per session - do research to confirm correct best practice.
const secret_len = jetzig.http.Session.Cipher.key_length + jetzig.http.Session.Cipher.nonce_length;
const secret = try self.getSecret(&logger, secret_len);
if (secret.len != secret_len) {
try logger.ERROR("Expected secret length: {}, found: {}.", .{ secret_len, secret.len });
try logger.ERROR("Use `jetzig generate secret` to create a secure secret value.", .{});
std.process.exit(1);
}
return .{
.logger = logger,
.secret = secret,
.bind = try self.allocator.dupe(u8, options.options.bind),
.port = options.options.port,
.detach = options.options.detach,
};
}
fn getLogFile(path: []const u8) !std.fs.File {
if (std.mem.eql(u8, path, "-")) return std.io.getStdOut();
const file = try std.fs.createFileAbsolute(path, .{ .truncate = false });
try file.seekFromEnd(0);
return file;
}
fn getSecret(self: Environment, logger: *jetzig.loggers.Logger, comptime len: u10) ![]const u8 {
return std.process.getEnvVarOwned(self.allocator, "JETZIG_SECRET") catch |err| {
switch (err) {
error.EnvironmentVariableNotFound => {
// TODO: Make this a failure when running in non-development mode.
const secret = try jetzig.util.generateSecret(self.allocator, len);
try logger.WARN(
"Running in development mode, using auto-generated cookie encryption key: {s}",
.{secret},
);
try logger.WARN(
"Run `jetzig generate secret` and set `JETZIG_SECRET` to remove this warning.",
.{},
);
return secret;
},
else => return err,
}
};
}

View File

@ -1,30 +0,0 @@
const std = @import("std");
const http = @import("http.zig");
pub const Result = @import("caches/Result.zig");
pub const MemoryCache = @import("caches/MemoryCache.zig");
pub const NullCache = @import("caches/NullCache.zig");
pub const Cache = union(enum) {
memory_cache: MemoryCache,
null_cache: NullCache,
pub fn deinit(self: *Cache) void {
switch (self.*) {
inline else => |*case| case.deinit(),
}
}
pub fn get(self: *Cache, key: []const u8) ?Result {
return switch (self.*) {
inline else => |*case| case.get(key),
};
}
pub fn put(self: *Cache, key: []const u8, value: http.Response) !Result {
return switch (self.*) {
inline else => |*case| case.put(key, value),
};
}
};

View File

@ -1,5 +0,0 @@
const std = @import("std");
pub const Result = @import("Result.zig");
pub const MemoryCache = @import("MemoryCache.zig");
pub const NullCache = @import("NullCache.zig");

View File

@ -1,39 +0,0 @@
const std = @import("std");
const http = @import("../http.zig");
const Result = @import("Result.zig");
allocator: std.mem.Allocator,
cache: std.StringHashMap(http.Response),
const Self = @This();
pub fn init(allocator: std.mem.Allocator) Self {
const cache = std.StringHashMap(http.Response).init(allocator);
return .{ .allocator = allocator, .cache = cache };
}
pub fn deinit(self: *Self) void {
var iterator = self.cache.keyIterator();
while (iterator.next()) |key| {
self.allocator.free(key.*);
}
self.cache.deinit();
}
pub fn get(self: *Self, key: []const u8) ?Result {
if (self.cache.get(key)) |value| {
return Result.init(self.allocator, value, true);
} else {
return null;
}
}
pub fn put(self: *Self, key: []const u8, value: http.Response) !Result {
const key_dupe = try self.allocator.dupe(u8, key);
const value_dupe = try value.dupe();
try self.cache.put(key_dupe, value_dupe);
return Result.init(self.allocator, value_dupe, true);
}

View File

@ -1,27 +0,0 @@
const std = @import("std");
const http = @import("../http.zig");
const Result = @import("Result.zig");
const Self = @This();
allocator: std.mem.Allocator,
pub fn init(allocator: std.mem.Allocator) Self {
return Self{ .allocator = allocator };
}
pub fn deinit(self: *const Self) void {
_ = self;
}
pub fn get(self: *const Self, key: []const u8) ?Result {
_ = key;
_ = self;
return null;
}
pub fn put(self: *const Self, key: []const u8, value: http.Response) !Result {
_ = key;
return Result{ .value = value, .cached = false, .allocator = self.allocator };
}

View File

@ -1,17 +0,0 @@
const std = @import("std");
const Self = @This();
const jetzig = @import("../../jetzig.zig");
value: jetzig.http.Response,
cached: bool,
allocator: std.mem.Allocator,
pub fn init(allocator: std.mem.Allocator, value: jetzig.http.Response, cached: bool) Self {
return .{ .allocator = allocator, .cached = cached, .value = value };
}
pub fn deinit(self: *const Self) void {
if (!self.cached) self.value.deinit();
}

View File

@ -101,7 +101,7 @@ pub fn process(self: *Self) !void {
self.session.parse() catch |err| { self.session.parse() catch |err| {
switch (err) { switch (err) {
error.JetzigInvalidSessionCookie => { error.JetzigInvalidSessionCookie => {
self.server.logger.debug("Invalid session cookie detected. Resetting session.", .{}); try self.server.logger.DEBUG("Invalid session cookie detected. Resetting session.", .{});
try self.session.reset(); try self.session.reset();
}, },
else => return err, else => return err,
@ -310,7 +310,9 @@ pub fn hash(self: *Self) ![]const u8 {
); );
} }
pub fn fmtMethod(self: *Self) []const u8 { pub fn fmtMethod(self: *Self, colorized: bool) []const u8 {
if (!colorized) return @tagName(self.method);
return switch (self.method) { return switch (self.method) {
.GET => jetzig.colors.cyan("GET"), .GET => jetzig.colors.cyan("GET"),
.PUT => jetzig.colors.yellow("PUT"), .PUT => jetzig.colors.yellow("PUT"),

View File

@ -11,37 +11,32 @@ else
struct {}; struct {};
pub const ServerOptions = struct { pub const ServerOptions = struct {
cache: jetzig.caches.Cache,
logger: jetzig.loggers.Logger, logger: jetzig.loggers.Logger,
bind: []const u8,
port: u16,
secret: []const u8, secret: []const u8,
detach: bool,
}; };
allocator: std.mem.Allocator, allocator: std.mem.Allocator,
port: u16,
host: []const u8,
cache: jetzig.caches.Cache,
logger: jetzig.loggers.Logger, logger: jetzig.loggers.Logger,
options: ServerOptions, options: ServerOptions,
start_time: i128 = undefined, start_time: i128 = undefined,
routes: []*jetzig.views.Route, routes: []*jetzig.views.Route,
mime_map: *jetzig.http.mime.MimeMap, mime_map: *jetzig.http.mime.MimeMap,
std_net_server: std.net.Server = undefined, std_net_server: std.net.Server = undefined,
initialized: bool = false,
const Self = @This(); const Self = @This();
pub fn init( pub fn init(
allocator: std.mem.Allocator, allocator: std.mem.Allocator,
host: []const u8,
port: u16,
options: ServerOptions, options: ServerOptions,
routes: []*jetzig.views.Route, routes: []*jetzig.views.Route,
mime_map: *jetzig.http.mime.MimeMap, mime_map: *jetzig.http.mime.MimeMap,
) Self { ) Self {
return .{ return .{
.allocator = allocator, .allocator = allocator,
.host = host,
.port = port,
.cache = options.cache,
.logger = options.logger, .logger = options.logger,
.options = options, .options = options,
.routes = routes, .routes = routes,
@ -50,15 +45,18 @@ pub fn init(
} }
pub fn deinit(self: *Self) void { pub fn deinit(self: *Self) void {
self.std_net_server.deinit(); if (self.initialized) self.std_net_server.deinit();
self.allocator.free(self.options.secret);
self.allocator.free(self.options.bind);
} }
pub fn listen(self: *Self) !void { pub fn listen(self: *Self) !void {
const address = try std.net.Address.parseIp("127.0.0.1", 8080); const address = try std.net.Address.parseIp(self.options.bind, self.options.port);
self.std_net_server = try address.listen(.{ .reuse_port = true }); self.std_net_server = try address.listen(.{ .reuse_port = true });
const cache_status = if (self.options.cache == .null_cache) "disabled" else "enabled"; self.initialized = true;
self.logger.debug("Listening on http://{s}:{} [cache:{s}]", .{ self.host, self.port, cache_status });
try self.logger.INFO("Listening on http://{s}:{}", .{ self.options.bind, self.options.port });
try self.processRequests(); try self.processRequests();
} }
@ -77,7 +75,6 @@ fn processRequests(self: *Self) !void {
self.processNextRequest(allocator, &std_http_server) catch |err| { self.processNextRequest(allocator, &std_http_server) catch |err| {
if (isBadHttpError(err)) { if (isBadHttpError(err)) {
std.debug.print("Encountered HTTP error: {s}\n", .{@errorName(err)});
std_http_server.connection.stream.close(); std_http_server.connection.stream.close();
continue; continue;
} else return err; } else return err;
@ -113,7 +110,7 @@ fn processNextRequest(self: *Self, allocator: std.mem.Allocator, std_http_server
const log_message = try self.requestLogMessage(&request); const log_message = try self.requestLogMessage(&request);
defer self.allocator.free(log_message); defer self.allocator.free(log_message);
self.logger.debug("{s}", .{log_message}); try self.logger.INFO("{s}", .{log_message});
} }
fn renderResponse(self: *Self, request: *jetzig.http.Request) !void { fn renderResponse(self: *Self, request: *jetzig.http.Request) !void {
@ -210,7 +207,7 @@ fn renderView(
// `return request.render(.ok)`, but the actual rendered view is stored in // `return request.render(.ok)`, but the actual rendered view is stored in
// `request.rendered_view`. // `request.rendered_view`.
_ = route.render(route.*, request) catch |err| { _ = route.render(route.*, request) catch |err| {
self.logger.debug("Encountered error: {s}", .{@errorName(err)}); try self.logger.ERROR("Encountered error: {s}", .{@errorName(err)});
if (isUnhandledError(err)) return err; if (isUnhandledError(err)) return err;
if (isBadRequest(err)) return try self.renderBadRequest(request); if (isBadRequest(err)) return try self.renderBadRequest(request);
return try self.renderInternalServerError(request, err); return try self.renderInternalServerError(request, err);
@ -230,7 +227,7 @@ fn renderView(
return .{ .view = rendered_view, .content = "" }; return .{ .view = rendered_view, .content = "" };
} }
} else { } else {
self.logger.debug("`request.render` was not invoked. Rendering empty content.", .{}); try self.logger.WARN("`request.render` was not invoked. Rendering empty content.", .{});
request.response_data.reset(); request.response_data.reset();
return .{ return .{
.view = .{ .data = request.response_data, .status_code = .no_content }, .view = .{ .data = request.response_data, .status_code = .no_content },
@ -254,7 +251,7 @@ fn renderTemplateWithLayout(
if (zmpl.manifest.find(prefixed_name)) |layout| { if (zmpl.manifest.find(prefixed_name)) |layout| {
return try template.renderWithLayout(layout, view.data); return try template.renderWithLayout(layout, view.data);
} else { } else {
self.logger.debug("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);
} }
} else return try template.render(view.data); } else return try template.render(view.data);
@ -299,7 +296,7 @@ fn renderInternalServerError(self: *Self, request: *jetzig.http.Request, err: an
try object.put("error", request.response_data.string(@errorName(err))); try object.put("error", request.response_data.string(@errorName(err)));
const stack = @errorReturnTrace(); const stack = @errorReturnTrace();
if (stack) |capture| try self.logStackTrace(capture, request, object); if (stack) |capture| try self.logStackTrace(capture, request);
return .{ return .{
.view = jetzig.views.View{ .data = request.response_data, .status_code = .internal_server_error }, .view = jetzig.views.View{ .data = request.response_data, .status_code = .internal_server_error },
@ -324,16 +321,13 @@ fn logStackTrace(
self: *Self, self: *Self,
stack: *std.builtin.StackTrace, stack: *std.builtin.StackTrace,
request: *jetzig.http.Request, request: *jetzig.http.Request,
object: *jetzig.data.Value,
) !void { ) !void {
_ = self; try self.logger.ERROR("\nStack Trace:\n{}", .{stack});
std.debug.print("\nStack Trace:\n{}", .{stack}); var buf = std.ArrayList(u8).init(request.allocator);
var array = std.ArrayList(u8).init(request.allocator); defer buf.deinit();
const writer = array.writer(); const writer = buf.writer();
try stack.format("", .{}, writer); try stack.format("", .{}, writer);
// TODO: Generate an array of objects with stack trace in useful data structure instead of try self.logger.ERROR("{s}", .{buf.items});
// dumping the whole formatted backtrace as a JSON string:
try object.put("backtrace", request.response_data.string(array.items));
} }
fn requestLogMessage(self: *Self, request: *jetzig.http.Request) ![]const u8 { fn requestLogMessage(self: *Self, request: *jetzig.http.Request) ![]const u8 {
@ -345,13 +339,16 @@ fn requestLogMessage(self: *Self, request: *jetzig.http.Request) ![]const u8 {
), ),
}; };
const formatted_duration = try jetzig.colors.duration(self.allocator, self.duration()); const formatted_duration = if (self.logger.isColorized())
try jetzig.colors.duration(self.allocator, self.duration())
else
try std.fmt.allocPrint(self.allocator, "{}", .{self.duration()});
defer self.allocator.free(formatted_duration); defer self.allocator.free(formatted_duration);
return try std.fmt.allocPrint(self.allocator, "[{s}/{s}/{s}] {s}", .{ return try std.fmt.allocPrint(self.allocator, "[{s}/{s}/{s}] {s}", .{
formatted_duration, formatted_duration,
request.fmtMethod(), request.fmtMethod(self.logger.isColorized()),
status.format(), status.format(self.logger.isColorized()),
request.path.path, request.path.path,
}); });
} }

View File

@ -3,7 +3,7 @@ const std = @import("std");
const jetzig = @import("../../jetzig.zig"); const jetzig = @import("../../jetzig.zig");
pub const cookie_name = "_jetzig-session"; pub const cookie_name = "_jetzig-session";
const Cipher = std.crypto.aead.aes_gcm.Aes256Gcm; pub const Cipher = std.crypto.aead.aes_gcm.Aes256Gcm;
allocator: std.mem.Allocator, allocator: std.mem.Allocator,
encryption_key: ?[]const u8, encryption_key: ?[]const u8,
@ -99,7 +99,7 @@ fn save(self: *Self) !void {
} }
self.encrypted = try self.encrypt(json); self.encrypted = try self.encrypt(json);
const encoded = try jetzig.base64Encode(self.allocator, self.encrypted.?); const encoded = try jetzig.util.base64Encode(self.allocator, self.encrypted.?);
defer self.allocator.free(encoded); defer self.allocator.free(encoded);
if (self.cookie) |*ptr| self.allocator.free(ptr.*.value); if (self.cookie) |*ptr| self.allocator.free(ptr.*.value);
@ -113,7 +113,7 @@ fn save(self: *Self) !void {
fn parseSessionCookie(self: *Self, cookie_value: []const u8) !void { fn parseSessionCookie(self: *Self, cookie_value: []const u8) !void {
self.data = jetzig.data.Data.init(self.allocator); self.data = jetzig.data.Data.init(self.allocator);
const decoded = try jetzig.base64Decode(self.allocator, cookie_value); const decoded = try jetzig.util.base64Decode(self.allocator, cookie_value);
defer self.allocator.free(decoded); defer self.allocator.free(decoded);
const buf = self.decrypt(decoded) catch |err| { const buf = self.decrypt(decoded) catch |err| {

View File

@ -73,11 +73,12 @@ pub fn StatusCodeType(comptime code: []const u8, comptime message: []const u8) t
const Self = @This(); const Self = @This();
pub fn format(self: Self) []const u8 { pub fn format(self: Self, colorized: bool) []const u8 {
_ = self; _ = self;
const full_message = code ++ " " ++ message; const full_message = code ++ " " ++ message;
if (!colorized) return full_message;
if (std.mem.startsWith(u8, code, "2")) { if (std.mem.startsWith(u8, code, "2")) {
return jetzig.colors.green(full_message); return jetzig.colors.green(full_message);
} else if (std.mem.startsWith(u8, code, "3")) { } else if (std.mem.startsWith(u8, code, "3")) {
@ -158,9 +159,9 @@ pub const TaggedStatusCode = union(StatusCode) {
const Self = @This(); const Self = @This();
pub fn format(self: Self) []const u8 { pub fn format(self: Self, colorized: bool) []const u8 {
return switch (self) { return switch (self) {
inline else => |capture| capture.format(), inline else => |capture| capture.format(colorized),
}; };
} }
}; };

View File

@ -4,18 +4,56 @@ const Self = @This();
pub const DevelopmentLogger = @import("loggers/DevelopmentLogger.zig"); pub const DevelopmentLogger = @import("loggers/DevelopmentLogger.zig");
const LogLevel = enum { pub const LogLevel = enum { TRACE, DEBUG, INFO, WARN, ERROR, FATAL };
debug,
};
pub const Logger = union(enum) { pub const Logger = union(enum) {
development_logger: DevelopmentLogger, development_logger: DevelopmentLogger,
pub fn debug(self: *Logger, comptime message: []const u8, args: anytype) void { pub fn isColorized(self: Logger) bool {
switch (self) {
inline else => |logger| return logger.isColorized(),
}
}
/// Log a TRACE level message to the configured logger.
pub fn TRACE(self: *const Logger, comptime message: []const u8, args: anytype) !void {
switch (self.*) { switch (self.*) {
inline else => |*case| case.debug(message, args) catch |err| { inline else => |*logger| try logger.TRACE(message, args),
std.debug.print("{}\n", .{err}); }
}, }
/// Log a DEBUG level message to the configured logger.
pub fn DEBUG(self: *const Logger, comptime message: []const u8, args: anytype) !void {
switch (self.*) {
inline else => |*logger| try logger.DEBUG(message, args),
}
}
/// Log an INFO level message to the configured logger.
pub fn INFO(self: *const Logger, comptime message: []const u8, args: anytype) !void {
switch (self.*) {
inline else => |*logger| try logger.INFO(message, args),
}
}
/// Log a WARN level message to the configured logger.
pub fn WARN(self: *const Logger, comptime message: []const u8, args: anytype) !void {
switch (self.*) {
inline else => |*logger| try logger.WARN(message, args),
}
}
/// Log an ERROR level message to the configured logger.
pub fn ERROR(self: *const Logger, comptime message: []const u8, args: anytype) !void {
switch (self.*) {
inline else => |*logger| try logger.ERROR(message, args),
}
}
/// Log a FATAL level message to the configured logger.
pub fn FATAL(self: *const Logger, comptime message: []const u8, args: anytype) !void {
switch (self.*) {
inline else => |*logger| try logger.FATAL(message, args),
} }
} }
}; };

View File

@ -1,19 +1,74 @@
const std = @import("std"); const std = @import("std");
const jetzig = @import("../../jetzig.zig");
const Self = @This(); const Self = @This();
const Timestamp = @import("../types/Timestamp.zig");
const Timestamp = jetzig.types.Timestamp;
const LogLevel = jetzig.loggers.LogLevel;
allocator: std.mem.Allocator, allocator: std.mem.Allocator,
file: std.fs.File,
colorized: bool,
pub fn init(allocator: std.mem.Allocator) Self { pub fn init(allocator: std.mem.Allocator, file: std.fs.File) Self {
return .{ .allocator = allocator }; return .{ .allocator = allocator, .file = file, .colorized = file.isTty() };
} }
pub fn debug(self: *Self, comptime message: []const u8, args: anytype) !void { /// Return true if logger was initialized with colorization (i.e. if log file is a tty)
pub fn isColorized(self: Self) bool {
return self.colorized;
}
/// Log a TRACE level message to the configured logger.
pub fn TRACE(self: *const Self, comptime message: []const u8, args: anytype) !void {
try self.log(.DEBUG, message, args);
}
/// Log a DEBUG level message to the configured logger.
pub fn DEBUG(self: *const Self, comptime message: []const u8, args: anytype) !void {
try self.log(.DEBUG, message, args);
}
/// Log an INFO level message to the configured logger.
pub fn INFO(self: *const Self, comptime message: []const u8, args: anytype) !void {
try self.log(.INFO, message, args);
}
/// Log a WARN level message to the configured logger.
pub fn WARN(self: *const Self, comptime message: []const u8, args: anytype) !void {
try self.log(.WARN, message, args);
}
/// Log an ERROR level message to the configured logger.
pub fn ERROR(self: *const Self, comptime message: []const u8, args: anytype) !void {
try self.log(.ERROR, message, args);
}
/// Log a FATAL level message to the configured logger.
pub fn FATAL(self: *const Self, comptime message: []const u8, args: anytype) !void {
try self.log(.FATAL, message, args);
}
pub fn log(self: Self, comptime level: LogLevel, comptime message: []const u8, args: anytype) !void {
const output = try std.fmt.allocPrint(self.allocator, message, args); const output = try std.fmt.allocPrint(self.allocator, message, args);
defer self.allocator.free(output); defer self.allocator.free(output);
const timestamp = Timestamp.init(std.time.timestamp(), self.allocator); const timestamp = Timestamp.init(std.time.timestamp(), self.allocator);
const iso8601 = try timestamp.iso8601(); const iso8601 = try timestamp.iso8601();
defer self.allocator.free(iso8601); defer self.allocator.free(iso8601);
std.debug.print("[{s}] {s}\n", .{ iso8601, output }); const writer = self.file.writer();
const level_formatted = if (self.colorized) colorizedLogLevel(level) else @tagName(level);
try writer.print("{s: >5} [{s}] {s}\n", .{ level_formatted, iso8601, output });
if (!self.file.isTty()) try self.file.sync();
}
fn colorizedLogLevel(comptime level: LogLevel) []const u8 {
return switch (level) {
.TRACE => jetzig.colors.white(@tagName(level)),
.DEBUG => jetzig.colors.cyan(@tagName(level)),
.INFO => jetzig.colors.blue(@tagName(level) ++ " "),
.WARN => jetzig.colors.yellow(@tagName(level) ++ " "),
.ERROR => jetzig.colors.red(@tagName(level)),
.FATAL => jetzig.colors.red(@tagName(level)),
};
} }

View File

@ -16,7 +16,7 @@ pub fn init(request: *jetzig.http.Request) !*Self {
pub fn afterRequest(self: *Self, request: *jetzig.http.Request) !void { pub fn afterRequest(self: *Self, request: *jetzig.http.Request) !void {
_ = self; _ = self;
if (request.getHeader("HX-Target")) |target| { if (request.getHeader("HX-Target")) |target| {
request.server.logger.debug( try request.server.logger.DEBUG(
"[middleware-htmx] htmx request detected, disabling layout. (#{s})", "[middleware-htmx] htmx request detected, disabling layout. (#{s})",
.{target}, .{target},
); );

View File

@ -1,5 +1,6 @@
const std = @import("std"); const std = @import("std");
/// Compare two strings with case-insensitive matching.
pub fn equalStringsCaseInsensitive(expected: []const u8, actual: []const u8) bool { pub fn equalStringsCaseInsensitive(expected: []const u8, actual: []const u8) bool {
if (expected.len != actual.len) return false; if (expected.len != actual.len) return false;
for (expected, actual) |expected_char, actual_char| { for (expected, actual) |expected_char, actual_char| {
@ -7,3 +8,39 @@ pub fn equalStringsCaseInsensitive(expected: []const u8, actual: []const u8) boo
} }
return true; return true;
} }
/// Encode arbitrary input to Base64.
pub fn base64Encode(allocator: std.mem.Allocator, string: []const u8) ![]const u8 {
const encoder = std.base64.Base64Encoder.init(
std.base64.url_safe_no_pad.alphabet_chars,
std.base64.url_safe_no_pad.pad_char,
);
const size = encoder.calcSize(string.len);
const ptr = try allocator.alloc(u8, size);
_ = encoder.encode(ptr, string);
return ptr;
}
/// Decode arbitrary input from Base64.
pub fn base64Decode(allocator: std.mem.Allocator, string: []const u8) ![]const u8 {
const decoder = std.base64.Base64Decoder.init(
std.base64.url_safe_no_pad.alphabet_chars,
std.base64.url_safe_no_pad.pad_char,
);
const size = try decoder.calcSizeForSlice(string);
const ptr = try allocator.alloc(u8, size);
try decoder.decode(ptr, string);
return ptr;
}
/// Generate a secure random string of `len` characters (for cryptographic purposes).
pub fn generateSecret(allocator: std.mem.Allocator, comptime len: u10) ![]const u8 {
const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
var secret: [len]u8 = undefined;
for (0..len) |index| {
secret[index] = chars[std.crypto.random.intRangeAtMost(u8, 0, chars.len)];
}
return try allocator.dupe(u8, &secret);
}