From ec045cccd653cfb63c524beb91a6d119bf77ad40 Mon Sep 17 00:00:00 2001 From: Bob Farrell Date: Thu, 28 Mar 2024 22:37:59 +0000 Subject: [PATCH] Add environments, use pretty-printed JSON in development Fix secret generation - overallocate length to ensure we have enough bytes. Error if no secret provided in production mode. --- build.zig.zon | 4 ++-- cli/commands/generate/secret.zig | 4 ++-- src/GenerateRoutes.zig | 2 +- src/jetzig/Environment.zig | 29 +++++++++++++++++++---------- src/jetzig/http/Server.zig | 13 +++++++++++-- 5 files changed, 35 insertions(+), 17 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index 8a1cfbe..dbb8401 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -7,8 +7,8 @@ .hash = "1220bfc5c29bc930b5a524c210712ef65c6cde6770450899bef01164a3089e6707fa", }, .zmpl = .{ - .url = "https://github.com/jetzig-framework/zmpl/archive/9e2df115c9f17e92fb60a4d09bf55ea48d0388b0.tar.gz", - .hash = "1220820b7f5f3e01b7dc976d32cf9ff65d44dee2642533f4b8104e19a824e802d7e1", + .url = "https://github.com/jetzig-framework/zmpl/archive/ffdbd3767da28d5ab07c1a786ea778152d2f79f6.tar.gz", + .hash = "12202cf05fd4ba2482a9b4b89c632b435310a76ac501b7a3d87dfd41006748dd138d", }, .args = .{ .url = "https://github.com/MasterQ32/zig-args/archive/01d72b9a0128c474aeeb9019edd48605fa6d95f7.tar.gz", diff --git a/cli/commands/generate/secret.zig b/cli/commands/generate/secret.zig index 25da07f..6f9c9d5 100644 --- a/cli/commands/generate/secret.zig +++ b/cli/commands/generate/secret.zig @@ -6,9 +6,9 @@ pub fn run(allocator: std.mem.Allocator, cwd: std.fs.Dir, args: [][]const u8) !v _ = args; _ = cwd; const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; - var secret: [44]u8 = undefined; + var secret: [128]u8 = undefined; - for (0..44) |index| { + for (0..128) |index| { secret[index] = chars[std.crypto.random.intRangeAtMost(u8, 0, chars.len)]; } diff --git a/src/GenerateRoutes.zig b/src/GenerateRoutes.zig index 3198e2b..26dc339 100644 --- a/src/GenerateRoutes.zig +++ b/src/GenerateRoutes.zig @@ -271,7 +271,7 @@ fn generateRoutesForView(self: *Self, views_dir: std.fs.Dir, path: []const u8) ! var json_buf = std.ArrayList(u8).init(self.allocator); defer json_buf.deinit(); const json_writer = json_buf.writer(); - try item.toJson(json_writer); + try item.toJson(json_writer, false, 0); var encoded_buf = std.ArrayList(u8).init(self.allocator); defer encoded_buf.deinit(); const writer = encoded_buf.writer(); diff --git a/src/jetzig/Environment.zig b/src/jetzig/Environment.zig index a822573..85ac264 100644 --- a/src/jetzig/Environment.zig +++ b/src/jetzig/Environment.zig @@ -8,12 +8,13 @@ const Environment = @This(); allocator: std.mem.Allocator, +pub const EnvironmentName = enum { development, production }; + const Options = struct { help: bool = false, bind: []const u8 = "127.0.0.1", port: u16 = 8080, - // TODO: - // environment: []const u8 = "development", + environment: EnvironmentName = .development, log: []const u8 = "-", @"log-error": []const u8 = "-", @"log-level": jetzig.loggers.LogLevel = .INFO, @@ -24,8 +25,7 @@ const Options = struct { .h = "help", .b = "bind", .p = "port", - // TODO: - // .e = "environment", + .e = "environment", .d = "detach", }; @@ -35,8 +35,7 @@ const Options = struct { .option_docs = .{ .bind = "IP address/hostname to bind to (default: 127.0.0.1)", .port = "Port to listen on (default: 8080)", - // TODO: - // .environment = "Load an environment configuration from src/app/environments/.zig", + .environment = "Set the server environment. Must be one of: { development, production } (default: development)", .log = "Path to log file. Use '-' for stdout (default: '-')", .@"log-error" = \\Optional path to separate error log file. Use '-' for stderr. If omitted, errors are logged to the location specified by the `log` option (or stderr if `log` is '-'). @@ -94,9 +93,11 @@ pub fn getServerOptions(self: Environment) !jetzig.http.Server.ServerOptions { std.process.exit(1); } + const environment = options.options.environment; + // 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); + const secret = (try self.getSecret(&logger, secret_len, environment))[0..secret_len]; if (secret.len != secret_len) { try logger.ERROR("Expected secret length: {}, found: {}.", .{ secret_len, secret.len }); @@ -110,6 +111,7 @@ pub fn getServerOptions(self: Environment) !jetzig.http.Server.ServerOptions { .bind = try self.allocator.dupe(u8, options.options.bind), .port = options.options.port, .detach = options.options.detach, + .environment = environment, }; } @@ -132,11 +134,18 @@ fn getLogFile(stream: enum { stdout, stderr }, options: Options) !std.fs.File { 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| { +fn getSecret(self: Environment, logger: *jetzig.loggers.Logger, comptime len: u10, environment: EnvironmentName) ![]const u8 { + const env_var = "JETZIG_SECRET"; + + return std.process.getEnvVarOwned(self.allocator, env_var) catch |err| { switch (err) { error.EnvironmentVariableNotFound => { - // TODO: Make this a failure when running in non-development mode. + if (environment != .development) { + try logger.ERROR("Environment variable `{s}` must be defined in production mode.", .{env_var}); + try logger.ERROR("Run `jetzig generate secret` to generate an appropriate value.", .{}); + std.process.exit(1); + } + const secret = try jetzig.util.generateSecret(self.allocator, len); try logger.WARN( "Running in development mode, using auto-generated cookie encryption key: {s}", diff --git a/src/jetzig/http/Server.zig b/src/jetzig/http/Server.zig index 1192ff9..797e4f8 100644 --- a/src/jetzig/http/Server.zig +++ b/src/jetzig/http/Server.zig @@ -9,6 +9,7 @@ pub const ServerOptions = struct { port: u16, secret: []const u8, detach: bool, + environment: jetzig.Environment.EnvironmentName, }; allocator: std.mem.Allocator, @@ -48,7 +49,11 @@ pub fn listen(self: *Self) !void { self.initialized = true; - try self.logger.INFO("Listening on http://{s}:{}", .{ self.options.bind, self.options.port }); + try self.logger.INFO("Listening on http://{s}:{} [{s}]", .{ + self.options.bind, + self.options.port, + @tagName(self.options.environment), + }); try self.processRequests(); } @@ -167,7 +172,11 @@ fn renderJSON( if (data.value) |_| {} else _ = try data.object(); - rendered.content = try data.toJson(); + rendered.content = if (self.options.environment == .development) + try data.toPrettyJson() + else + try data.toJson(); + request.setResponse(rendered, .{}); } else { request.setResponse(try renderNotFound(request), .{});