milestone 19: hygiene sweep - dead ecs surface, single-source constants, tls classification, frontend state hazards, docker smoke network fix
This commit is contained in:
@@ -116,13 +116,13 @@ pub fn login(state: *server.WebState, io: std.Io, request: *Request) HandlerErro
|
||||
// Ruling 18: a refused login is a 401, not the 400 an invalid value
|
||||
// would earn elsewhere. Only the address and the outcome are logged.
|
||||
if (failure == .invalid) {
|
||||
log.warn("web login refused for {f}", .{request.peer});
|
||||
log.warn("web login refused for {f}", .{request.client_addr});
|
||||
return http_util.respondError(request, .unauthorized, "invalid password");
|
||||
}
|
||||
return mutations.respondFailure(request, failure, "verifying the web password");
|
||||
},
|
||||
.cookie => |cookie| {
|
||||
log.info("web login accepted for {f}", .{request.peer});
|
||||
log.info("web login accepted for {f}", .{request.client_addr});
|
||||
var buf: [cookie_buf_len]u8 = undefined;
|
||||
const header = http_util.formatSetCookie(
|
||||
&buf,
|
||||
|
||||
@@ -259,6 +259,20 @@ fn expectType(comptime name: []const u8, comptime Actual: type, comptime Expecte
|
||||
///
|
||||
/// A state with no `reload_fn` has nothing to reload — that is the shape of a
|
||||
/// web layer under test, and of one whose composition root wired no manager.
|
||||
///
|
||||
/// Locking contract, and it is the reason every one of the fourteen call sites
|
||||
/// in `groups.zig`, `rules.zig`, `clients.zig` and `blocklists.zig` may call
|
||||
/// this *after* releasing `state.config_lock`: `reload_fn` must re-read all of
|
||||
/// the state it publishes from the database itself, under the manager's own
|
||||
/// writer lock. It must never accept rows the caller read. Rows read under
|
||||
/// `config_lock` and passed across its release are already stale, so a
|
||||
/// signature change that adds a row parameter here silently breaks the
|
||||
/// correctness of all fourteen sites — every one of them would have to move
|
||||
/// the call back inside the lock.
|
||||
///
|
||||
/// `swapLocalTables` below is the pre-read shape and is exactly the contrast:
|
||||
/// it takes the rows, so `local.zig`'s `publish` calls it while it still holds
|
||||
/// `config_lock`, and the ordering rationale lives on that function.
|
||||
pub fn reload(state: *server.WebState, io: std.Io) ?Failure {
|
||||
const reload_fn = state.reload_fn orelse return null;
|
||||
reload_fn(state, io) catch |err| {
|
||||
|
||||
@@ -15,6 +15,7 @@ const Allocator = std.mem.Allocator;
|
||||
|
||||
const db = @import("../../storage/db.zig");
|
||||
const http_util = @import("../http_util.zig");
|
||||
const logger = @import("../../storage/logger.zig");
|
||||
const queries_repo = @import("../../storage/repositories/queries_repo.zig");
|
||||
const server = @import("../server.zig");
|
||||
|
||||
@@ -23,11 +24,10 @@ const log = std.log.scoped(.web_queries);
|
||||
pub const default_limit: u32 = 100;
|
||||
pub const max_limit: u32 = queries_repo.max_limit;
|
||||
|
||||
/// A domain filter longer than the longest legal domain name matches nothing.
|
||||
pub const max_domain_len = 253;
|
||||
|
||||
/// Long enough for an IPv6 address with a zone identifier.
|
||||
pub const max_client_len = 64;
|
||||
/// The filter widths are the stored widths: a filter wider than the column it
|
||||
/// compares against could only match a row the query log cannot hold.
|
||||
pub const max_domain_len = logger.max_domain_len;
|
||||
pub const max_client_len = logger.max_client_len;
|
||||
|
||||
/// Where the two string filters are copied to. The parsed filter borrows them,
|
||||
/// so it must not outlive the buffers — in the handler both live in the same
|
||||
|
||||
@@ -332,10 +332,10 @@ pub fn applyPut(
|
||||
const hash_changed = password != null and !std.mem.eql(u8, previous_hash, cfg.web.password_hash);
|
||||
const replacement: ?[]u8 = if (hash_changed) try state.gpa.dupe(u8, cfg.web.password_hash) else null;
|
||||
|
||||
if (writeSettings(arena, database, cfg)) |err| {
|
||||
writeSettings(arena, database, cfg) catch |err| {
|
||||
if (replacement) |hash| state.gpa.free(hash);
|
||||
return .{ .fail = .{ .internal = err } };
|
||||
}
|
||||
};
|
||||
|
||||
if (replacement) |hash| {
|
||||
// Ruling 17, both halves: the running server must verify against the
|
||||
@@ -354,26 +354,36 @@ pub fn applyPut(
|
||||
/// costs a few dozen upserts and buys the guarantee that the table is exactly
|
||||
/// what `model.toSettings` says the merged configuration is — no key can be
|
||||
/// missed and none can be left behind.
|
||||
fn writeSettings(arena: Allocator, database: *db.Db, cfg: model.Config) ?db.Error {
|
||||
fn writeSettings(arena: Allocator, database: *db.Db, cfg: model.Config) db.Error!void {
|
||||
var pairs: std.ArrayList(model.SettingPair) = .empty;
|
||||
model.toSettings(cfg, arena, &pairs) catch return error.OutOfMemory;
|
||||
try model.toSettings(cfg, arena, &pairs);
|
||||
|
||||
var tx = db.Tx.begin(database) catch |err| return err;
|
||||
var tx = try db.Tx.begin(database);
|
||||
errdefer tx.rollback();
|
||||
|
||||
try write_fault.check();
|
||||
for (pairs.items) |pair| {
|
||||
settings_repo.putSetting(database, pair.key, pair.value) catch |err| {
|
||||
tx.rollback();
|
||||
return err;
|
||||
};
|
||||
try settings_repo.putSetting(database, pair.key, pair.value);
|
||||
}
|
||||
tx.commit() catch |err| {
|
||||
tx.rollback();
|
||||
return err;
|
||||
};
|
||||
return null;
|
||||
try tx.commit();
|
||||
}
|
||||
|
||||
/// Fails one `writeSettings` after its transaction has begun, so a test can
|
||||
/// prove the `errdefer` above rolls that transaction back rather than leaving
|
||||
/// the shared connection inside it. Test builds only, and it reduces to nothing
|
||||
/// everywhere else — the rotation seam's shape (logging.zig).
|
||||
const write_fault = if (builtin.is_test) struct {
|
||||
var armed: bool = false;
|
||||
|
||||
fn check() db.Error!void {
|
||||
if (!armed) return;
|
||||
armed = false;
|
||||
return error.Internal;
|
||||
}
|
||||
} else struct {
|
||||
fn check() db.Error!void {}
|
||||
};
|
||||
|
||||
fn problem(arena: Allocator, cfg: model.Config) error{OutOfMemory}!?[]const u8 {
|
||||
return mutations.firstProblem(arena, cfg);
|
||||
}
|
||||
@@ -581,6 +591,28 @@ test "a put that would not validate writes nothing" {
|
||||
try testing.expectEqual(@as(u16, 53), stored.dns.port);
|
||||
}
|
||||
|
||||
test "a write that fails after the transaction begins rolls it back" {
|
||||
var bench: mutations.Bench = undefined;
|
||||
try bench.init(testing.allocator);
|
||||
defer bench.deinit(testing.allocator);
|
||||
try seeded(&bench);
|
||||
|
||||
var patch: Patch = .{};
|
||||
patch.dns = .{ .port = 5353 };
|
||||
|
||||
write_fault.armed = true;
|
||||
const outcome = try applyPut(&bench.state, bench.io(), bench.arena(), patch);
|
||||
try testing.expect(outcome.fail == .internal);
|
||||
|
||||
// BEGIN IMMEDIATE inside an open transaction is an error, so a second
|
||||
// `begin` succeeding is what proves the errdefer ran.
|
||||
var tx = try db.Tx.begin(&bench.database);
|
||||
tx.rollback();
|
||||
|
||||
const stored = try mutations.loadConfig(bench.arena(), &bench.database);
|
||||
try testing.expectEqual(@as(u16, 53), stored.dns.port);
|
||||
}
|
||||
|
||||
test "an enum value the model does not know names the key it came from" {
|
||||
var bench: mutations.Bench = undefined;
|
||||
try bench.init(testing.allocator);
|
||||
|
||||
Reference in New Issue
Block a user