╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ──╮ │src/web.zig│ ╰───────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────╮ │154 if (std.mem.eql(u8, path, "/favicon.ico")) { │ │151 - if (http_server.reader.state == .received_head) { │ │152 - http_server.reader.state = .ready; │ │153 - } │ │154 - try writeStaticResponse(http_server, 204, "No Content", "image/x-icon", "", true);│ │155 + try request.respond("", .{ │ │156 + .status = .no_content, │ │157 + .extra_headers = &.{.{ .name = "content-type", .value = "image/x-icon" }}, │ │158 + }); │ │159 return; │ │160 } │ │161 │ ╰───────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────╮ │158 - // for the index page we need to consult the session cookie before │ │159 - // consuming the request, so we don't dispatch through findEmbed here. │ │162 if (std.mem.eql(u8, path, "/") or std.mem.eql(u8, path, "/index.html")) {│ │163 const user_id_hex = getCookieValue(request, cookie_name); │ │164 const login_failure: ?ui.Home.Auth.Login.Failure = │ ╰──────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │171 null │ │172 else │ │173 null; │ │172 - if (http_server.reader.state == .received_head) { │ │173 - http_server.reader.state = .ready; │ │174 - } │ │174 const html = try renderIndexHtml(io, allocator, admin_repo_path, user_id_hex, login_failure); │ │175 defer allocator.free(html); │ │177 - // expire the flash cookie on the way out — it's been consumed by │ │178 - // this render and shouldn't survive a refresh. │ │179 - const clear_flash: []const u8 = if (login_failure != null) │ │180 - "Set-Cookie: " ++ login_failure_cookie ++ "=; Path=/; Max-Age=0\r\n" │ │181 - else │ │182 - ""; │ │183 - try http_server.out.print( │ │184 - "HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=utf-8\r\n{s}Content-Length: {d}\r\n\r\n", │ │185 - .{ clear_flash, html.len }, │ │186 - ); │ │187 - if (method != .HEAD) try http_server.out.writeAll(html); │ │176 + // expire the flash cookie on the way out so a refresh doesn't keep │ │177 + // showing the failure label. │ │178 + var headers: [2]std.http.Header = undefined; │ │179 + headers[0] = .{ .name = "content-type", .value = "text/html; charset=utf-8" }; │ │180 + var headers_slice: []const std.http.Header = headers[0..1]; │ │181 + if (login_failure != null) { │ │182 + headers[1] = .{ .name = "set-cookie", .value = login_failure_cookie ++ "=; Path=/; Max-Age=0" };│ │183 + headers_slice = headers[0..2]; │ │184 + } │ │185 + try request.respond(html, .{ .extra_headers = headers_slice }); │ │186 return; │ │187 } │ │188 │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────╮ │189 const embed = findEmbed(path) orelse { │ │192 - if (http_server.reader.state == .received_head) { │ │193 - http_server.reader.state = .ready; │ │194 - } │ │195 - try writeSimpleResponse(http_server, 404, "Not Found", "text/plain", "not found");│ │190 + try request.respond("not found", .{ │ │191 + .status = .not_found, │ │192 + .extra_headers = &.{.{ .name = "content-type", .value = "text/plain" }}, │ │193 + }); │ │194 return; │ │195 }; │ │196 │ ╰───────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │199 - if (http_server.reader.state == .received_head) { │ │200 - http_server.reader.state = .ready; │ │201 - } │ │202 - try writeStaticResponse(http_server, 200, "OK", embed.content_type, embed.body, method == .HEAD);│ │197 + try request.respond(embed.body, .{ │ │198 + .extra_headers = &.{.{ .name = "content-type", .value = embed.content_type }}, │ │199 + }); │ │200 } │ │201 │ │202 fn handleLogin( │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────╮ │203 io: std.Io, │ │207 - http_server: *std.http.Server, │ │204 request: *std.http.Server.Request,│ │205 allocator: std.mem.Allocator, │ │206 admin_repo_path: []const u8, │ ╰───────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │229 switch (result) { │ │230 .success => |user_id| { │ │231 const hex = std.fmt.bytesToHex(user_id, .lower); │ │236 - try http_server.out.print( │ │237 - "HTTP/1.1 303 See Other\r\nLocation: /\r\nSet-Cookie: " ++ cookie_name ++ "={s}; Path=/; HttpOnly; SameSite=Strict\r\nContent-Length: 0\r\n\r\n", │ │238 - .{hex}, │ │239 - ); │ │232 + var cookie_buf: [256]u8 = undefined; │ │233 + const cookie = try std.fmt.bufPrint(&cookie_buf, cookie_name ++ "={s}; Path=/; HttpOnly; SameSite=Strict", .{hex}); │ │234 + try request.respond("", .{ │ │235 + .status = .see_other, │ │236 + .extra_headers = &.{ │ │237 + .{ .name = "location", .value = "/" }, │ │238 + .{ .name = "set-cookie", .value = cookie }, │ │239 + }, │ │240 + }); │ │241 }, │ │242 .unknown_user => { │ │242 - try http_server.out.print( │ │243 - "HTTP/1.1 303 See Other\r\nLocation: /\r\nSet-Cookie: " ++ login_failure_cookie ++ "=unknown_user; Path=/; HttpOnly; SameSite=Strict\r\nContent-Length: 0\r\n\r\n", │ │244 - .{}, │ │245 - ); │ │243 + try request.respond("", .{ │ │244 + .status = .see_other, │ │245 + .extra_headers = &.{ │ │246 + .{ .name = "location", .value = "/" }, │ │247 + .{ .name = "set-cookie", .value = login_failure_cookie ++ "=unknown_user; Path=/; HttpOnly; SameSite=Strict" }, │ │248 + }, │ │249 + }); │ │250 }, │ │251 .wrong_password => { │ │248 - try http_server.out.print( │ │249 - "HTTP/1.1 303 See Other\r\nLocation: /\r\nSet-Cookie: " ++ login_failure_cookie ++ "=wrong_password; Path=/; HttpOnly; SameSite=Strict\r\nContent-Length: 0\r\n\r\n",│ │250 - .{}, │ │251 - ); │ │252 + try request.respond("", .{ │ │253 + .status = .see_other, │ │254 + .extra_headers = &.{ │ │255 + .{ .name = "location", .value = "/" }, │ │256 + .{ .name = "set-cookie", .value = login_failure_cookie ++ "=wrong_password; Path=/; HttpOnly; SameSite=Strict" }, │ │257 + }, │ │258 + }); │ │259 }, │ │260 } │ │261 } │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │262 │ │256 -fn handleLogout(http_server: *std.http.Server, request: *std.http.Server.Request) !void { │ │257 - // we don't need the body, but the http server wants the request consumed │ │258 - // before the next one (and before we write the response). │ │259 - var sink_buf: [64]u8 = undefined; │ │260 - _ = request.readerExpectNone(&sink_buf).discardRemaining() catch {}; │ │261 - │ │262 - try http_server.out.print( │ │263 - "HTTP/1.1 303 See Other\r\nLocation: /\r\nSet-Cookie: " ++ cookie_name ++ "=; Path=/; HttpOnly; SameSite=Strict; Max-Age=0\r\nContent-Length: 0\r\n\r\n",│ │264 - .{}, │ │265 - ); │ │263 +fn handleLogout(request: *std.http.Server.Request) !void { │ │264 + // request.respond's discardBody reads any unused body for us and lands │ │265 + // the connection back in the .ready state for the next request on the │ │266 + // same connection (keep_alive defaults to true). │ │267 + try request.respond("", .{ │ │268 + .status = .see_other, │ │269 + .extra_headers = &.{ │ │270 + .{ .name = "location", .value = "/" }, │ │271 + .{ .name = "set-cookie", .value = cookie_name ++ "=; Path=/; HttpOnly; SameSite=Strict; Max-Age=0" }, │ │272 + }, │ │273 + }); │ │274 } │ │275 │ │276 fn renderIndexHtml( │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────╮ │307 │ │308 const content = try generateHtml(allocator, &root, &session); │ │309 defer allocator.free(content); │ │310 + const overlay = try generateOverlay(allocator, &root, &session); │ │311 + defer allocator.free(overlay); │ │312 │ │313 // serialize the snapshot so the wasm side can parse it back without │ │314 // making a second request. base64-encoded so the json can be embedded│ ╰───────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │328 var cursor: usize = 0; │ │329 for (&[_]struct { needle: []const u8, replacement: []const u8 }{ │ │330 .{ .needle = "{{{ HAXY_HTML }}}", .replacement = content }, │ │331 + .{ .needle = "{{{ HAXY_OVERLAY }}}", .replacement = overlay }, │ │332 .{ .needle = "{{{ HAXY_JSON }}}", .replacement = json_b64 }, │ │333 }) |sub| { │ │334 const idx = std.mem.indexOfPos(u8, template, cursor, sub.needle) orelse return error.MissingTemplateToken;│ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯