╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ──╮ │src/web.zig│ ╰───────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │381 var repo = try rp.Repo(.xit, evt.admin_repo_opts).open(io, allocator, .{ .path = remote.admin_repo_path }); │ │382 defer repo.deinit(io, allocator); │ │383 const moment = try evt.currentMoment(evt.admin_repo_opts, &repo); │ │369 - return (try evt.User.readById(evt.AdminDB, evt.admin_repo_opts.hash, moment, arena, &user_id)) orelse unreachable; │ │384 + const user = (try evt.User.readById(evt.AdminDB, evt.admin_repo_opts.hash, moment, arena, &user_id)) orelse return null;│ │385 + return user.event.email; │ │386 +} │ │387 + │ │388 +// refuse a write from a request with no logged-in user. the body goes unread, │ │389 +// so the connection closes. │ │390 +fn respondLoginRequired(request: *std.http.Server.Request) !void { │ │391 + try request.respond("log in to make changes", .{ │ │392 + .status = .forbidden, │ │393 + .keep_alive = false, │ │394 + .extra_headers = &.{.{ .name = "content-type", .value = "text/plain" }}, │ │395 + }); │ │396 } │ │397 │ │398 // create an issue in the repo the form's page names (base is │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │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). │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯