╭ changed ──╮ │src/web.zig│ ╰───────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭──────────╮ │← previous│ ╰──────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │404 base: []const u8, │ │405 host: Host, │ │406 ) !void { │ │407 + var author_arena = std.heap.ArenaAllocator.init(allocator); │ │408 + defer author_arena.deinit(); │ │409 + const author_email = (try eventAuthorEmail(io, allocator, &author_arena, request, host)) orelse return respondLoginRequired(request);│ │410 + │ │411 var body_buf: [256]u8 = undefined; │ │412 const reader = request.readerExpectNone(&body_buf); │ │413 const body = reader.allocRemaining(allocator, .limited(65536)) catch &[_]u8{}; │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────╮ │437 var id_bytes: [evt.event_id_size]u8 = undefined; │ │438 io.random(&id_bytes); │ │439 const event_id_hex = std.fmt.bytesToHex(id_bytes, .lower); │ │410 - │ │411 - var author_arena = std.heap.ArenaAllocator.init(allocator);│ │412 - defer author_arena.deinit(); │ │440 │ │441 const event = evt.EventWithId{ │ │442 .id = event_id_hex, │ ╰────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │443 .timestamp = @intCast(std.Io.Timestamp.now(io, .real).toSeconds()), │ │417 - .author_email = if (try sessionUser(io, allocator, &author_arena, request, host)) |author| author.event.email else "user@haxy",│ │444 + .author_email = author_email, │ │445 .event = .{ .issue = .{ │ │446 .title = title, │ │447 .description = description, │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │589 │ │590 var author_arena = std.heap.ArenaAllocator.init(allocator); │ │591 defer author_arena.deinit(); │ │565 - const author_email = if (try sessionUser(io, allocator, &author_arena, request, host)) |author| author.event.email else "user@haxy"; │ │592 + const author_email = (try eventAuthorEmail(io, allocator, &author_arena, request, host)) orelse return respondLoginRequired(request);│ │593 │ │594 updateIssue(io, allocator, host, parts, .{ .status = status }, author_email) catch |err| switch (err) { │ │595 error.NotFound => { │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │622 base: []const u8, │ │623 host: Host, │ │624 ) !void { │ │625 + var author_arena = std.heap.ArenaAllocator.init(allocator); │ │626 + defer author_arena.deinit(); │ │627 + const author_email = (try eventAuthorEmail(io, allocator, &author_arena, request, host)) orelse return respondLoginRequired(request);│ │628 + │ │629 var body_buf: [256]u8 = undefined; │ │630 const reader = request.readerExpectNone(&body_buf); │ │631 const body = reader.allocRemaining(allocator, .limited(65536)) catch &[_]u8{}; │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │662 return; │ │663 }; │ │664 │ │634 - var author_arena = std.heap.ArenaAllocator.init(allocator); │ │635 - defer author_arena.deinit(); │ │636 - const author_email = if (try sessionUser(io, allocator, &author_arena, request, host)) |author| author.event.email else "user@haxy";│ │637 - │ │665 updateIssue(io, allocator, host, parts, .{ .fields = .{ │ │666 .title = title, │ │667 .tags = tags, │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1253 self.dir.deleteFile(self.io, token_hex) catch {}; │ │1254 } │ │1255 │ │1256 + // leave `token_hex`'s session for the next visitor arriving without one. │ │1257 + // only the `try` fixture writes one, and only a debug build honors it. the │ │1258 + // name isn't token-shaped, so it can't collide with a session file. │ │1259 + pub fn offerAutoLogin(self: SessionStore, token_hex: *const [token_hex_len]u8) !void { │ │1260 + const file = try self.dir.createFile(self.io, auto_login_name, .{}); │ │1261 + defer file.close(self.io); │ │1262 + try file.writeStreamingAll(self.io, token_hex); │ │1263 + } │ │1264 + │ │1265 + // claim the offered session, if one is there. the delete picks the winner │ │1266 + // of a race, so the offer is only ever taken once. a release build never │ │1267 + // looks, because that would be an extra syscall for every GET request. │ │1268 + pub fn claimAutoLogin(self: SessionStore, out: *[token_hex_len]u8) bool { │ │1269 + if (builtin.mode != .Debug) return false; │ │1270 + { │ │1271 + const file = self.dir.openFile(self.io, auto_login_name, .{ .mode = .read_only }) catch return false;│ │1272 + defer file.close(self.io); │ │1273 + var storage: [token_hex_len]u8 = undefined; │ │1274 + var file_reader = file.reader(self.io, &storage); │ │1275 + file_reader.interface.readSliceAll(out) catch return false; │ │1276 + } │ │1277 + self.dir.deleteFile(self.io, auto_login_name) catch return false; │ │1278 + return true; │ │1279 + } │ │1280 + │ │1281 + const auto_login_name = "auto-login"; │ │1282 + │ │1283 // a token is exactly token_hex_len hex chars. validating before using the │ │1284 // value as a path keeps attacker-supplied cookies from escaping the │ │1285 // sessions dir (e.g. via "/" or ".." bytes). │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯