diff --git a/Dockerfile b/Dockerfile index 5e75ccb..405a299 100644 --- a/Dockerfile +++ b/Dockerfile @@ -24,9 +24,6 @@ RUN set -eux; \ ENV PATH="/opt/zig:${PATH}" ENV ZIG_GLOBAL_CACHE_DIR=/root/.cache/zig -WORKDIR /work -RUN git clone --depth=1 https://tangled.org/zat.dev/zat /work/zat.dev/zat - WORKDIR /work/zat.dev/zds COPY . . RUN --mount=type=cache,target=/root/.cache/zig \ diff --git a/README.md b/README.md index f629fa5..9e66b7b 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,25 @@ protocol primitives: syntax, tids, did resolution, jwt helpers, dag-cbor, car, mst, repo verification, and key encoding. WebAuthn support comes from [`webauthn`](https://tangled.org/zzstoatzz.io/webauthn). +## dependency posture + +ZDS is an application. Dependencies are chosen around that boundary: + +- `zat` is the sibling library we maintain for reusable AT Protocol primitives. + If ZDS grows a generally useful atproto parser, codec, verifier, resolver, or + client helper, expect it to move upstream into `zat`. +- `httpz` is the HTTP/1.1 server boundary. ZDS should consume it directly from + Tangled, not vendor it. HTTP server behavior should stay local to ZDS unless + it is a focused compatibility fix or improvement for `httpz` itself. +- `zat` and `httpz` both rely on Karl Seguin's `websocket.zig`. Keep the graph + on one websocket implementation; fix the upstream package we control rather + than copying it into this repo. +- Vendoring is a last resort for short-lived debugging only. Do not leave + `vendor/` as the dependency strategy. + +See [development](docs/development.md#dependency-boundary) for the longer +decision rules. + ## docs - [architecture](docs/architecture.md) diff --git a/build.zig b/build.zig index c349859..e04df00 100644 --- a/build.zig +++ b/build.zig @@ -17,6 +17,10 @@ pub fn build(b: *std.Build) void { .target = target, .optimize = optimize, }); + const httpz = b.dependency("httpz", .{ + .target = target, + .optimize = optimize, + }); const mod = b.addModule("zds", .{ .root_source_file = b.path("src/root.zig"), @@ -27,6 +31,7 @@ pub fn build(b: *std.Build) void { .{ .name = "zat", .module = zat.module("zat") }, .{ .name = "zqlite", .module = zqlite.module("zqlite") }, .{ .name = "webauthn", .module = webauthn.module("webauthn") }, + .{ .name = "httpz", .module = httpz.module("httpz") }, }, }); diff --git a/build.zig.zon b/build.zig.zon index b197566..5e3bc80 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -5,8 +5,8 @@ .minimum_zig_version = "0.16.0", .dependencies = .{ .zat = .{ - .url = "git+https://tangled.org/zat.dev/zat?ref=v0.3.3#6260230c64ecd8c9758d56cc0489db31da63861f", - .hash = "zat-0.3.3-5PuC7l8GCQC1-gNvfy8dt_1yxlQA1mK3aHUj9VzcgtnS", + .url = "git+https://tangled.org/zat.dev/zat?ref=main#22bc51bd82f63d5464f0aa7386dd1a47e6bb0927", + .hash = "zat-0.3.4-5PuC7ngGCQAsTo-8n0Hw14y7ii3kiyjI15AmhuANLnFz", }, .zqlite = .{ .url = "git+https://github.com/karlseguin/zqlite.zig?ref=master#05a88d6758753e1c63fdd45b211dde2057094b0c", @@ -16,6 +16,10 @@ .url = "git+https://tangled.org/zzstoatzz.io/webauthn?ref=main#3815415a0aa4fc280411ef0b9a795a35c02d3b05", .hash = "webauthn-0.0.1--JitMv-LAACZoE0TF1Y4poG3EUvlk8q_qbz8NbLMPXAI", }, + .httpz = .{ + .url = "git+https://tangled.org/karlseguin.tngl.sh/http.zig?ref=master#744c588608d2b13e6dba2013577b2e0928877f19", + .hash = "httpz-0.0.0-PNVzrCzKCAAKOXAKsuPzDZYzstlou0FLBp3HlDoRgmUa", + }, }, .paths = .{ "build.zig", diff --git a/docs/architecture.md b/docs/architecture.md index 2ec3290..040ec06 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -12,9 +12,15 @@ path when clients send the proxy header. - `zat` owns AT Protocol primitives: syntax, TIDs, DID/handle resolution, JWT helpers, DAG-CBOR, CAR, MST, repo verification, OAuth helpers, and streaming clients. +- `httpz` owns the HTTP/1.1 server mechanics: request parsing, response + serialization, connection handling, upgrades, and server-side websocket + plumbing. ZDS adapts AT Protocol handlers onto that server boundary. - `zds` owns PDS policy and persistence: accounts, repo writes, sessions, app passwords, passkeys, storage, server routes, sync production, account migration, and the local login/security surface. +- Dependency fixes should happen at the owning layer. Since this project also + maintains `zat`, ZDS should consume a fixed `zat` commit rather than vendoring + or patching a private copy. - SQLite stores account, repo, commit, token, OAuth, blob metadata, and identity state. - Password sessions are durable rows keyed by access and refresh JWT IDs. Refresh diff --git a/docs/development.md b/docs/development.md index 1c066c0..914f151 100644 --- a/docs/development.md +++ b/docs/development.md @@ -24,14 +24,38 @@ quickly. ## dependency boundary -Use `zat` for atproto primitives: syntax, TIDs, DID and handle resolution, JWT -helpers, DAG-CBOR, CAR, MST, repo verification, OAuth helpers, and JSON helpers. -Do not edit `zat` from this repo unless the task explicitly includes upstream -work. +Treat ZDS as an application and `zat` as the sibling library we maintain for +reusable AT Protocol code. ZDS should depend on `zat`; it should not copy, +vendor, or fork `zat` to make local package graph problems disappear. + +- Use `zat` for atproto primitives: syntax, TIDs, DID and handle resolution, + JWT helpers, DAG-CBOR, CAR, MST, repo verification, OAuth helpers, JSON + helpers, and streaming clients. +- Use `httpz` for the PDS HTTP/1.1 server boundary: accept loops, request + parsing, response writing, upgrades, and server-side websocket integration. +- Use `webauthn` for browser credential ceremony verification. ZDS owns the + account policy, challenge storage, and UI that surround it. +- Use `zqlite` for SQLite access. ZDS owns the schema and transaction policy. When ZDS needs a generally useful primitive that `zat` does not expose yet, build the smallest local version under `src/internal`, use it from ZDS, and -document why it is likely upstream material. +document why it is likely upstream material. Once the shape is clear, upstream +it into `zat` and repin ZDS to the new commit or release. + +When dependency packages disagree, fix the package that owns the disagreement. +Because we maintain `zat`, dependency alignment problems in `zat` should be +fixed in the `zat` repo and consumed here by pinning the resulting commit. Do +not vendor `zat` into ZDS. Do not vendor `httpz` either; consume it directly +from Tangled and send focused fixes upstream when its HTTP or websocket behavior +needs to change. + +Keep the websocket graph singular. `httpz` uses Karl Seguin's `websocket.zig`, +and `zat` should use the same package so ZDS does not pull two websocket +implementations into one build. + +Vendoring is only acceptable as a temporary local debugging move. Before a +change is considered ready, remove `vendor/`, express dependencies in +`build.zig.zon`, and verify the project from a clean package graph. For generated JSON, prefer typed structs plus `std.json.Stringify.valueAlloc` or a narrow helper over hand-written object strings. `std.json.fmt` is still diff --git a/src/atproto/identity.zig b/src/atproto/identity.zig index e18a68a..f0ca81c 100644 --- a/src/atproto/identity.zig +++ b/src/atproto/identity.zig @@ -10,7 +10,7 @@ const zat = @import("zat"); const http = std.http; -pub fn getRecommendedDidCredentials(request: *http.Server.Request) !void { +pub fn getRecommendedDidCredentials(request: *http_api.Request) !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const allocator = arena.allocator(); @@ -42,7 +42,7 @@ pub fn getRecommendedDidCredentials(request: *http.Server.Request) !void { return http_api.json(request, .ok, body); } -pub fn requestPlcOperationSignature(request: *http.Server.Request) !void { +pub fn requestPlcOperationSignature(request: *http_api.Request) !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const allocator = arena.allocator(); @@ -59,7 +59,7 @@ pub fn requestPlcOperationSignature(request: *http.Server.Request) !void { return http_api.json(request, .ok, "{}"); } -pub fn signPlcOperation(request: *http.Server.Request) !void { +pub fn signPlcOperation(request: *http_api.Request) !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const allocator = arena.allocator(); @@ -109,7 +109,7 @@ pub fn signPlcOperation(request: *http.Server.Request) !void { return http_api.json(request, .ok, response); } -pub fn submitPlcOperation(request: *http.Server.Request) !void { +pub fn submitPlcOperation(request: *http_api.Request) !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const allocator = arena.allocator(); @@ -148,13 +148,13 @@ pub fn submitPlcOperation(request: *http.Server.Request) !void { return http_api.json(request, .ok, "{}"); } -pub fn resolveHandle(request: *http.Server.Request) !void { +pub fn resolveHandle(request: *http_api.Request) !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const allocator = arena.allocator(); var handle_buf: [256]u8 = undefined; - const handle_param = http_api.queryParam(request.head.target, "handle", &handle_buf) orelse { + const handle_param = http_api.queryParam(request.url.raw, "handle", &handle_buf) orelse { return http_api.xrpcError(request, .bad_request, "InvalidRequest", "Missing handle"); }; const handle = std.mem.trim(u8, handle_param, &std.ascii.whitespace); @@ -181,7 +181,7 @@ pub fn resolveHandle(request: *http.Server.Request) !void { return http_api.json(request, .ok, body); } -fn requireAccount(request: *http.Server.Request, allocator: std.mem.Allocator) !http_api.BearerAccount { +fn requireAccount(request: *http_api.Request, allocator: std.mem.Allocator) !http_api.BearerAccount { return http_api.requireBearerAccess(request, allocator) catch |err| { switch (err) { error.AuthRequired => try http_api.xrpcError(request, .unauthorized, "AuthenticationRequired", "Authentication required"), @@ -191,14 +191,14 @@ fn requireAccount(request: *http.Server.Request, allocator: std.mem.Allocator) ! }; } -fn requireIdentityScope(request: *http.Server.Request, maybe_scope: ?[]const u8, attr: scopes.IdentityAttr) !void { +fn requireIdentityScope(request: *http_api.Request, maybe_scope: ?[]const u8, attr: scopes.IdentityAttr) !void { if (scopes.identityAllows(maybe_scope, attr)) return; try http_api.xrpcError(request, .forbidden, "InsufficientScope", "Insufficient scope"); return error.HandledResponse; } fn plcOperationSigningKeypair( - request: *http.Server.Request, + request: *http_api.Request, allocator: std.mem.Allocator, _: auth.Account, last_op: std.json.Value, @@ -222,7 +222,7 @@ fn plcOperationSigningKeypair( } fn validatePlcOperation( - request: *http.Server.Request, + request: *http_api.Request, allocator: std.mem.Allocator, account: auth.Account, operation: std.json.Value, diff --git a/src/atproto/oauth.zig b/src/atproto/oauth.zig index a18bae1..4081c81 100644 --- a/src/atproto/oauth.zig +++ b/src/atproto/oauth.zig @@ -39,11 +39,11 @@ const ClientMetadata = struct { } }; -pub fn protectedResource(request: *http.Server.Request) !void { +pub fn protectedResource(request: *http_api.Request) !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); var resource_buf: [512]u8 = undefined; - const requested_resource = http_api.queryParam(request.head.target, "resource", &resource_buf); + const requested_resource = http_api.queryParam(request.url.raw, "resource", &resource_buf); const resource = if (requested_resource) |requested| if (std.mem.eql(u8, trimTrailingSlash(requested), config.publicUrl())) requested else config.publicUrl() else @@ -54,7 +54,7 @@ pub fn protectedResource(request: *http.Server.Request) !void { try http_api.json(request, .ok, body); } -pub fn authorizationServer(request: *http.Server.Request) !void { +pub fn authorizationServer(request: *http_api.Request) !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const allocator = arena.allocator(); @@ -74,11 +74,11 @@ pub fn authorizationServer(request: *http.Server.Request) !void { try http_api.json(request, .ok, body); } -pub fn jwks(request: *http.Server.Request) !void { +pub fn jwks(request: *http_api.Request) !void { try http_api.json(request, .ok, "{\"keys\":[]}"); } -pub fn par(request: *http.Server.Request) !void { +pub fn par(request: *http_api.Request) !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const allocator = arena.allocator(); @@ -91,7 +91,7 @@ pub fn par(request: *http.Server.Request) !void { return parWithParams(request, allocator, Form{ .body = body }); } -fn parWithParams(request: *http.Server.Request, allocator: std.mem.Allocator, params: anytype) !void { +fn parWithParams(request: *http_api.Request, allocator: std.mem.Allocator, params: anytype) !void { const response_type = try requireParam(request, params, allocator, "response_type"); if (!std.mem.eql(u8, response_type, "code")) return oauthError(request, .bad_request, "unsupported_response_type", "Only response_type=code is supported"); const client_id = try requireParam(request, params, allocator, "client_id"); @@ -127,12 +127,12 @@ fn parWithParams(request: *http.Server.Request, allocator: std.mem.Allocator, pa try http_api.json(request, .created, response); } -pub fn authorizeGet(request: *http.Server.Request) !void { +pub fn authorizeGet(request: *http_api.Request) !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const allocator = arena.allocator(); var request_id_buf: [512]u8 = undefined; - const request_id = requestIdFromTarget(request.head.target, &request_id_buf) orelse return oauthError(request, .bad_request, "invalid_request", "Missing request_uri"); + const request_id = requestIdFromTarget(request.url.raw, &request_id_buf) orelse return oauthError(request, .bad_request, "invalid_request", "Missing request_uri"); const oauth_request = (try store.getOAuthRequest(allocator, request_id)) orelse return oauthError(request, .bad_request, "invalid_request_uri", "Unknown request_uri"); if (oauth_request.expires_at < now()) return oauthError(request, .bad_request, "invalid_request_uri", "Expired request_uri"); const client = clientDisplayInfo(allocator, oauth_request.client_id) catch ClientDisplayInfo{ .name = oauth_request.client_id, .uri = oauth_request.client_id }; @@ -295,7 +295,7 @@ fn passkeyLoginAvailable(allocator: std.mem.Allocator, login_hint: ?[]const u8) return true; } -pub fn authorizePost(request: *http.Server.Request) !void { +pub fn authorizePost(request: *http_api.Request) !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const allocator = arena.allocator(); @@ -344,7 +344,7 @@ pub fn authorizePost(request: *http.Server.Request) !void { try redirectTo(request, redirect_uri); } -pub fn token(request: *http.Server.Request) !void { +pub fn token(request: *http_api.Request) !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const allocator = arena.allocator(); @@ -357,14 +357,14 @@ pub fn token(request: *http.Server.Request) !void { return tokenWithParams(request, allocator, Form{ .body = body }); } -fn tokenWithParams(request: *http.Server.Request, allocator: std.mem.Allocator, params: anytype) !void { +fn tokenWithParams(request: *http_api.Request, allocator: std.mem.Allocator, params: anytype) !void { const grant_type = try requireParam(request, params, allocator, "grant_type"); if (std.mem.eql(u8, grant_type, "authorization_code")) return authorizationCodeToken(request, allocator, params); if (std.mem.eql(u8, grant_type, "refresh_token")) return refreshToken(request, allocator, params); return oauthError(request, .bad_request, "unsupported_grant_type", "Unsupported grant_type"); } -pub fn introspect(request: *http.Server.Request) !void { +pub fn introspect(request: *http_api.Request) !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const allocator = arena.allocator(); @@ -377,7 +377,7 @@ pub fn introspect(request: *http.Server.Request) !void { return introspectWithParams(request, allocator, Form{ .body = body }); } -fn introspectWithParams(request: *http.Server.Request, allocator: std.mem.Allocator, params: anytype) !void { +fn introspectWithParams(request: *http_api.Request, allocator: std.mem.Allocator, params: anytype) !void { const token_text = try requireParam(request, params, allocator, "token"); const row = try store.getOAuthToken(allocator, token_text); if (row == null or row.?.revoked or row.?.expires_at < now()) return http_api.json(request, .ok, "{\"active\":false}"); @@ -390,7 +390,7 @@ fn introspectWithParams(request: *http.Server.Request, allocator: std.mem.Alloca try http_api.json(request, .ok, response); } -pub fn revoke(request: *http.Server.Request) !void { +pub fn revoke(request: *http_api.Request) !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const allocator = arena.allocator(); @@ -403,13 +403,13 @@ pub fn revoke(request: *http.Server.Request) !void { return revokeWithParams(request, allocator, Form{ .body = body }); } -fn revokeWithParams(request: *http.Server.Request, allocator: std.mem.Allocator, params: anytype) !void { +fn revokeWithParams(request: *http_api.Request, allocator: std.mem.Allocator, params: anytype) !void { const token_text = try requireParam(request, params, allocator, "token"); try store.revokeOAuthToken(token_text); try http_api.json(request, .ok, "{}"); } -fn authorizationCodeToken(request: *http.Server.Request, allocator: std.mem.Allocator, params: anytype) !void { +fn authorizationCodeToken(request: *http_api.Request, allocator: std.mem.Allocator, params: anytype) !void { const code = try requireParam(request, params, allocator, "code"); const redirect_uri = try requireParam(request, params, allocator, "redirect_uri"); const code_verifier = try requireParam(request, params, allocator, "code_verifier"); @@ -428,7 +428,7 @@ fn authorizationCodeToken(request: *http.Server.Request, allocator: std.mem.Allo try issueTokenResponse(request, allocator, account, oauth_request.client_id, oauth_request.scope, null); } -fn refreshToken(request: *http.Server.Request, allocator: std.mem.Allocator, params: anytype) !void { +fn refreshToken(request: *http_api.Request, allocator: std.mem.Allocator, params: anytype) !void { const refresh = try requireParam(request, params, allocator, "refresh_token"); const client_id = try requireParam(request, params, allocator, "client_id"); const client_metadata = try fetchClientMetadataForAuth(request, allocator, client_id); @@ -441,7 +441,7 @@ fn refreshToken(request: *http.Server.Request, allocator: std.mem.Allocator, par try issueTokenResponse(request, allocator, account, token_row.client_id, token_row.scope, refresh); } -fn issueTokenResponse(request: *http.Server.Request, allocator: std.mem.Allocator, account: auth.Account, client_id: []const u8, scope: []const u8, revoke_old: ?[]const u8) !void { +fn issueTokenResponse(request: *http_api.Request, allocator: std.mem.Allocator, account: auth.Account, client_id: []const u8, scope: []const u8, revoke_old: ?[]const u8) !void { if (revoke_old) |old| try store.revokeOAuthToken(old); const access = try auth.createSessionJwt(allocator, "access", account); const refresh = try auth.createSessionJwt(allocator, "refresh", account); @@ -460,7 +460,7 @@ fn issueTokenResponse(request: *http.Server.Request, allocator: std.mem.Allocato try http_api.json(request, .ok, body); } -fn requireClientAuth(request: *http.Server.Request, allocator: std.mem.Allocator, params: anytype, client_id: []const u8, metadata: std.json.Value) !void { +fn requireClientAuth(request: *http_api.Request, allocator: std.mem.Allocator, params: anytype, client_id: []const u8, metadata: std.json.Value) !void { const method = zat.json.getString(metadata, "token_endpoint_auth_method") orelse "none"; if (std.mem.eql(u8, method, "none")) return; if (!std.mem.eql(u8, method, "private_key_jwt")) { @@ -486,7 +486,7 @@ fn requireClientAuth(request: *http.Server.Request, allocator: std.mem.Allocator }; } -fn fetchClientMetadataForAuth(request: *http.Server.Request, allocator: std.mem.Allocator, client_id: []const u8) !std.json.Parsed(std.json.Value) { +fn fetchClientMetadataForAuth(request: *http_api.Request, allocator: std.mem.Allocator, client_id: []const u8) !std.json.Parsed(std.json.Value) { return fetchJson(allocator, client_id, 256 * 1024) catch |err| { log.err("oauth client_auth rejected metadata_fetch_failed client={s} err={s}\n", .{ client_id, @errorName(err) }); try oauthError(request, .bad_request, "invalid_client", "Could not fetch client metadata"); @@ -605,11 +605,11 @@ const JsonParams = struct { } }; -fn requireForm(request: *http.Server.Request, form: Form, allocator: std.mem.Allocator, name: []const u8) ![]const u8 { +fn requireForm(request: *http_api.Request, form: Form, allocator: std.mem.Allocator, name: []const u8) ![]const u8 { return requireParam(request, form, allocator, name); } -fn requireParam(request: *http.Server.Request, params: anytype, allocator: std.mem.Allocator, name: []const u8) ![]const u8 { +fn requireParam(request: *http_api.Request, params: anytype, allocator: std.mem.Allocator, name: []const u8) ![]const u8 { return (try params.value(allocator, name)) orelse { try oauthError(request, .bad_request, "invalid_request", "Missing required field"); return error.MissingFormField; @@ -657,15 +657,13 @@ fn authorizationResponseSeparator(redirect_uri: []const u8, response_mode: []con return if (std.mem.indexOfScalar(u8, redirect_uri, '?') == null) "?" else "&"; } -fn redirectTo(request: *http.Server.Request, location: []const u8) !void { - try request.respond("", .{ - .status = .see_other, - .extra_headers = &[_]http.Header{ - .{ .name = "location", .value = location }, - .{ .name = "access-control-allow-origin", .value = "*" }, - .{ .name = "connection", .value = "close" }, - }, - }); +fn redirectTo(request: *http_api.Request, location: []const u8) !void { + const headers = [_]http.Header{ + .{ .name = "location", .value = location }, + .{ .name = "access-control-allow-origin", .value = "*" }, + .{ .name = "connection", .value = "close" }, + }; + try http_api.respond(request, .see_other, "", &headers); } fn clientDisplayInfo(allocator: std.mem.Allocator, client_id: []const u8) !ClientDisplayInfo { @@ -947,7 +945,7 @@ fn validResponseMode(response_mode: []const u8) bool { std.mem.eql(u8, response_mode, "fragment"); } -fn validateScope(request: *http.Server.Request, allocator: std.mem.Allocator, scope_text: []const u8, client_scope_text: ?[]const u8) !void { +fn validateScope(request: *http_api.Request, allocator: std.mem.Allocator, scope_text: []const u8, client_scope_text: ?[]const u8) !void { var scopes = std.mem.splitScalar(u8, scope_text, ' '); var saw_scope = false; var has_transition = false; @@ -1044,7 +1042,7 @@ fn trimTrailingSlash(value: []const u8) []const u8 { return value; } -fn acceptsJson(request: *const http.Server.Request) bool { +fn acceptsJson(request: *const http_api.Request) bool { const accept = http_api.headerValue(request, "accept") orelse return false; return std.mem.indexOf(u8, accept, "application/json") != null; } @@ -1057,21 +1055,19 @@ fn now() i64 { }; } -fn oauthError(request: *http.Server.Request, status: http.Status, err: []const u8, description: []const u8) !void { +fn oauthError(request: *http_api.Request, status: http.Status, err: []const u8, description: []const u8) !void { var buf: [512]u8 = undefined; const body = try std.fmt.bufPrint(&buf, "{{\"error\":{f},\"error_description\":{f}}}", .{ std.json.fmt(err, .{}), std.json.fmt(description, .{}) }); try http_api.json(request, status, body); } -fn respondHtml(request: *http.Server.Request, status: http.Status, body: []const u8) !void { - try request.respond(body, .{ - .status = status, - .extra_headers = &[_]http.Header{ - .{ .name = "content-type", .value = "text/html; charset=utf-8" }, - .{ .name = "access-control-allow-origin", .value = "*" }, - .{ .name = "connection", .value = "close" }, - }, - }); +fn respondHtml(request: *http_api.Request, status: http.Status, body: []const u8) !void { + const headers = [_]http.Header{ + .{ .name = "content-type", .value = "text/html; charset=utf-8" }, + .{ .name = "access-control-allow-origin", .value = "*" }, + .{ .name = "connection", .value = "close" }, + }; + try http_api.respond(request, status, body, &headers); } test "client assertion accepts exp or recent iat" { diff --git a/src/atproto/preferences.zig b/src/atproto/preferences.zig index f31641e..97a621b 100644 --- a/src/atproto/preferences.zig +++ b/src/atproto/preferences.zig @@ -10,7 +10,7 @@ const max_preference_size = 10_000; const personal_details_pref = "app.bsky.actor.defs#personalDetailsPref"; const declared_age_pref = "app.bsky.actor.defs#declaredAgePref"; -pub fn getPreferences(request: *http.Server.Request) !void { +pub fn getPreferences(request: *http_api.Request) !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const allocator = arena.allocator(); @@ -40,7 +40,7 @@ pub fn getPreferences(request: *http.Server.Request) !void { return http_api.json(request, .ok, body); } -pub fn putPreferences(request: *http.Server.Request) !void { +pub fn putPreferences(request: *http_api.Request) !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const allocator = arena.allocator(); @@ -141,7 +141,7 @@ fn civilFromDays(days_since_unix_epoch: i64) CivilDate { return .{ .year = year, .month = month, .day = day }; } -fn requireAccount(request: *http.Server.Request, allocator: std.mem.Allocator) !auth.Account { +fn requireAccount(request: *http_api.Request, allocator: std.mem.Allocator) !auth.Account { return http_api.requireBearerAccount(request, allocator) catch |err| { switch (err) { error.AuthRequired => try http_api.xrpcError(request, .unauthorized, "AuthenticationRequired", "Authentication required"), diff --git a/src/atproto/proxy.zig b/src/atproto/proxy.zig index 1376be2..7cd66f1 100644 --- a/src/atproto/proxy.zig +++ b/src/atproto/proxy.zig @@ -8,16 +8,16 @@ const zat = @import("zat"); const http = std.http; -pub fn xrpcProxy(request: *http.Server.Request) !void { +pub fn xrpcProxy(request: *http_api.Request) !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const allocator = arena.allocator(); - if (request.head.method != .GET and request.head.method != .POST) { + if (request.method != .GET and request.method != .POST) { return http_api.xrpcError(request, .bad_request, "InvalidRequest", "XRPC proxy supports GET and POST"); } - const method = xrpcMethod(request.head.target) orelse { + const method = xrpcMethod(request.url.raw) orelse { return http_api.xrpcError(request, .bad_request, "InvalidRequest", "Invalid XRPC path"); }; if (isProtectedMethod(method)) { @@ -26,7 +26,7 @@ pub fn xrpcProxy(request: *http.Server.Request) !void { const proxy_to = http_api.headerValue(request, "atproto-proxy") orelse { return http_api.xrpcError(request, .bad_request, "InvalidRequest", "Missing atproto-proxy header"); }; - log.debug("xrpc proxy start method={s} target={s} proxy_to={s}\n", .{ method, request.head.target, proxy_to }); + log.debug("xrpc proxy start method={s} target={s} proxy_to={s}\n", .{ method, request.url.raw, proxy_to }); const service = resolveProxyService(allocator, proxy_to) catch |err| switch (err) { error.InvalidProxyHeader => { log.err("xrpc proxy invalid header method={s} proxy_to={s}\n", .{ method, proxy_to }); @@ -67,7 +67,7 @@ pub fn xrpcProxy(request: *http.Server.Request) !void { } const account = auth_ctx.account; log.debug("xrpc proxy auth ok method={s} account={s} service_did={s} endpoint={s}\n", .{ method, account.did, service.did, service.endpoint }); - const upstream_url = try std.fmt.allocPrint(allocator, "{s}{s}", .{ service.endpoint, request.head.target }); + const upstream_url = try std.fmt.allocPrint(allocator, "{s}{s}", .{ service.endpoint, request.url.raw }); var extra_buf: [6]std.http.Header = undefined; var extra_count: usize = 0; @@ -78,7 +78,7 @@ pub fn xrpcProxy(request: *http.Server.Request) !void { } } - const payload = if (request.head.method == .POST) + const payload = if (request.method == .POST) try http_api.readBodyAlloc(request, allocator, 1024 * 1024) else null; @@ -95,7 +95,7 @@ pub fn xrpcProxy(request: *http.Server.Request) !void { log.debug("xrpc proxy fetch method={s} upstream={s} extra_headers={d}\n", .{ method, upstream_url, extra_count }); const result = transport.fetch(.{ .url = upstream_url, - .method = request.head.method, + .method = http_api.toStdMethod(request.method), .payload = payload, .authorization = authorization, .accept = "application/json", @@ -110,10 +110,10 @@ pub fn xrpcProxy(request: *http.Server.Request) !void { return http_api.json(request, result.status, result.body); } -pub fn shouldProxy(request: *const http.Server.Request) bool { - if (request.head.method != .GET and request.head.method != .POST) return false; +pub fn shouldProxy(request: *const http_api.Request) bool { + if (request.method != .GET and request.method != .POST) return false; if (http_api.headerValue(request, "atproto-proxy") == null) return false; - const method = xrpcMethod(request.head.target) orelse return false; + const method = xrpcMethod(request.url.raw) orelse return false; return !isProtectedMethod(method); } diff --git a/src/atproto/repo.zig b/src/atproto/repo.zig index 402940f..6f7117c 100644 --- a/src/atproto/repo.zig +++ b/src/atproto/repo.zig @@ -9,7 +9,7 @@ const zat = @import("zat"); const http = std.http; -pub fn createRecord(request: *http.Server.Request) !void { +pub fn createRecord(request: *http_api.Request) !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const allocator = arena.allocator(); @@ -44,7 +44,7 @@ pub fn createRecord(request: *http.Server.Request) !void { return writeRecordRef(request, allocator, result.records[0], result.commit); } -pub fn putRecord(request: *http.Server.Request) !void { +pub fn putRecord(request: *http_api.Request) !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const allocator = arena.allocator(); @@ -81,13 +81,13 @@ pub fn putRecord(request: *http.Server.Request) !void { return writeRecordRef(request, allocator, result.records[0], result.commit); } -pub fn describeRepo(request: *http.Server.Request) !void { +pub fn describeRepo(request: *http_api.Request) !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const allocator = arena.allocator(); var repo_buf: [256]u8 = undefined; - const repo = http_api.queryParam(request.head.target, "repo", &repo_buf) orelse { + const repo = http_api.queryParam(request.url.raw, "repo", &repo_buf) orelse { return http_api.xrpcError(request, .bad_request, "InvalidRequest", "Missing repo"); }; const account = store.resolveRepo(repo) orelse { @@ -109,24 +109,24 @@ pub fn describeRepo(request: *http.Server.Request) !void { return http_api.json(request, .ok, body); } -pub fn getRecord(request: *http.Server.Request) !void { +pub fn getRecord(request: *http_api.Request) !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const allocator = arena.allocator(); var repo_buf: [256]u8 = undefined; - const repo = http_api.queryParam(request.head.target, "repo", &repo_buf) orelse { + const repo = http_api.queryParam(request.url.raw, "repo", &repo_buf) orelse { return http_api.xrpcError(request, .bad_request, "InvalidRequest", "Missing repo"); }; const account = store.resolveRepo(repo) orelse { return http_api.xrpcError(request, .not_found, "RepoNotFound", "Repo not found"); }; var collection_buf: [256]u8 = undefined; - const collection = http_api.queryParam(request.head.target, "collection", &collection_buf) orelse { + const collection = http_api.queryParam(request.url.raw, "collection", &collection_buf) orelse { return http_api.xrpcError(request, .bad_request, "InvalidRequest", "Missing collection"); }; var rkey_buf: [512]u8 = undefined; - const rkey = http_api.queryParam(request.head.target, "rkey", &rkey_buf) orelse { + const rkey = http_api.queryParam(request.url.raw, "rkey", &rkey_buf) orelse { return http_api.xrpcError(request, .bad_request, "InvalidRequest", "Missing rkey"); }; @@ -137,27 +137,27 @@ pub fn getRecord(request: *http.Server.Request) !void { return http_api.json(request, .ok, body); } -pub fn listRecords(request: *http.Server.Request) !void { +pub fn listRecords(request: *http_api.Request) !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const allocator = arena.allocator(); var repo_buf: [256]u8 = undefined; - const repo = http_api.queryParam(request.head.target, "repo", &repo_buf) orelse { + const repo = http_api.queryParam(request.url.raw, "repo", &repo_buf) orelse { return http_api.xrpcError(request, .bad_request, "InvalidRequest", "Missing repo"); }; const account = store.resolveRepo(repo) orelse { return http_api.xrpcError(request, .not_found, "RepoNotFound", "Repo not found"); }; var collection_buf: [256]u8 = undefined; - const collection = http_api.queryParam(request.head.target, "collection", &collection_buf) orelse { + const collection = http_api.queryParam(request.url.raw, "collection", &collection_buf) orelse { return http_api.xrpcError(request, .bad_request, "InvalidRequest", "Missing collection"); }; - const body = try store.writeListJson(allocator, account.did, collection, @min(http_api.queryLimit(request.head.target, 50), 100)); + const body = try store.writeListJson(allocator, account.did, collection, @min(http_api.queryLimit(request.url.raw, 50), 100)); return http_api.json(request, .ok, body); } -pub fn deleteRecord(request: *http.Server.Request) !void { +pub fn deleteRecord(request: *http_api.Request) !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const allocator = arena.allocator(); @@ -183,7 +183,7 @@ pub fn deleteRecord(request: *http.Server.Request) !void { return http_api.json(request, .ok, "{}"); } -pub fn applyWrites(request: *http.Server.Request) !void { +pub fn applyWrites(request: *http_api.Request) !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const allocator = arena.allocator(); @@ -293,7 +293,7 @@ pub fn applyWrites(request: *http.Server.Request) !void { return http_api.json(request, .ok, body_out); } -pub fn importRepo(request: *http.Server.Request) !void { +pub fn importRepo(request: *http_api.Request) !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const allocator = arena.allocator(); @@ -342,7 +342,7 @@ pub fn importRepo(request: *http.Server.Request) !void { return http_api.json(request, .ok, "{}"); } -fn verifyImportedRepoCar(request: *http.Server.Request, allocator: std.mem.Allocator, did: []const u8, body: []const u8) !void { +fn verifyImportedRepoCar(request: *http_api.Request, allocator: std.mem.Allocator, did: []const u8, body: []const u8) !void { var resolver = zat.DidResolver.init(store.currentIo(), allocator); defer resolver.deinit(); var doc = resolver.resolve(zat.Did.parse(did).?) catch { @@ -367,7 +367,7 @@ fn verifyImportedRepoCar(request: *http.Server.Request, allocator: std.mem.Alloc }; } -pub fn uploadBlob(io: std.Io, request: *http.Server.Request) !void { +pub fn uploadBlob(io: std.Io, request: *http_api.Request) !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const allocator = arena.allocator(); @@ -394,7 +394,7 @@ const BlobUploadAuth = struct { oauth_scope: ?[]const u8, }; -fn requireBlobUploadAuth(request: *http.Server.Request, allocator: std.mem.Allocator) !BlobUploadAuth { +fn requireBlobUploadAuth(request: *http_api.Request, allocator: std.mem.Allocator) !BlobUploadAuth { if (http_api.requireBearerAccess(request, allocator)) |ctx| { return .{ .account = ctx.account, .oauth_scope = ctx.oauth_scope }; } else |err| switch (err) { @@ -406,7 +406,7 @@ fn requireBlobUploadAuth(request: *http.Server.Request, allocator: std.mem.Alloc } } -fn requireBlobUploadServiceAuth(request: *http.Server.Request, allocator: std.mem.Allocator) !BlobUploadAuth { +fn requireBlobUploadServiceAuth(request: *http_api.Request, allocator: std.mem.Allocator) !BlobUploadAuth { const raw_header = http_api.headerValue(request, "authorization") orelse { try http_api.xrpcError(request, .unauthorized, "AuthenticationRequired", "Authentication required"); return error.HandledResponse; @@ -506,18 +506,18 @@ fn uploadBlobResponseJson( return out.toOwnedSlice(); } -pub fn listMissingBlobs(request: *http.Server.Request) !void { +pub fn listMissingBlobs(request: *http_api.Request) !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const allocator = arena.allocator(); const auth_ctx = requireAccount(request, allocator) catch return; const account = auth_ctx.account; - const body = try store.writeMissingBlobsJson(allocator, account.did, @min(http_api.queryLimit(request.head.target, 500), 1000)); + const body = try store.writeMissingBlobsJson(allocator, account.did, @min(http_api.queryLimit(request.url.raw, 500), 1000)); return http_api.json(request, .ok, body); } -fn requireAccount(request: *http.Server.Request, allocator: std.mem.Allocator) !http_api.BearerAccount { +fn requireAccount(request: *http_api.Request, allocator: std.mem.Allocator) !http_api.BearerAccount { return http_api.requireBearerAccess(request, allocator) catch |err| { switch (err) { error.AuthRequired => try http_api.xrpcError(request, .unauthorized, "AuthenticationRequired", "Authentication required"), @@ -527,17 +527,17 @@ fn requireAccount(request: *http.Server.Request, allocator: std.mem.Allocator) ! }; } -fn requireRepoScope(request: *http.Server.Request, maybe_scope: ?[]const u8, action: scopes.RepoAction, collection: []const u8) !void { +fn requireRepoScope(request: *http_api.Request, maybe_scope: ?[]const u8, action: scopes.RepoAction, collection: []const u8) !void { if (scopes.repoAllows(maybe_scope, action, collection)) return; return http_api.xrpcError(request, .forbidden, "InsufficientScope", "Insufficient scope"); } -fn requireBlobScope(request: *http.Server.Request, maybe_scope: ?[]const u8, mime_type: []const u8) !void { +fn requireBlobScope(request: *http_api.Request, maybe_scope: ?[]const u8, mime_type: []const u8) !void { if (scopes.blobAllows(maybe_scope, mime_type)) return; return http_api.xrpcError(request, .forbidden, "InsufficientScope", "Insufficient scope"); } -fn requireRepoMatches(request: *http.Server.Request, account: auth.Account, value: std.json.Value) !void { +fn requireRepoMatches(request: *http_api.Request, account: auth.Account, value: std.json.Value) !void { const repo = zat.json.getString(value, "repo") orelse { return http_api.xrpcError(request, .bad_request, "InvalidRequest", "Missing repo"); }; @@ -552,7 +552,7 @@ fn recordBodyValue(value: std.json.Value) ?std.json.Value { }; } -fn writeRecordRef(request: *http.Server.Request, allocator: std.mem.Allocator, record: store.Record, commit: store.CommitInfo) !void { +fn writeRecordRef(request: *http_api.Request, allocator: std.mem.Allocator, record: store.Record, commit: store.CommitInfo) !void { const uri = try record.uri(allocator); const body_out = try std.fmt.allocPrint( allocator, diff --git a/src/atproto/server.zig b/src/atproto/server.zig index 7ee9098..2705049 100644 --- a/src/atproto/server.zig +++ b/src/atproto/server.zig @@ -12,7 +12,7 @@ const zat = @import("zat"); const http = std.http; -pub fn describeServer(request: *http.Server.Request) !void { +pub fn describeServer(request: *http_api.Request) !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const domains = try jsonStringArray(arena.allocator(), config.handleDomains()); @@ -24,7 +24,7 @@ pub fn describeServer(request: *http.Server.Request) !void { return http_api.json(request, .ok, body); } -pub fn didJson(request: *http.Server.Request) !void { +pub fn didJson(request: *http_api.Request) !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const body = try std.fmt.allocPrint( @@ -35,7 +35,7 @@ pub fn didJson(request: *http.Server.Request) !void { return http_api.json(request, .ok, body); } -pub fn atprotoDid(request: *http.Server.Request) !void { +pub fn atprotoDid(request: *http_api.Request) !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const allocator = arena.allocator(); @@ -49,7 +49,7 @@ pub fn atprotoDid(request: *http.Server.Request) !void { return plain(request, .ok, account.did); } -pub fn reserveSigningKey(request: *http.Server.Request) !void { +pub fn reserveSigningKey(request: *http_api.Request) !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const allocator = arena.allocator(); @@ -69,7 +69,7 @@ pub fn reserveSigningKey(request: *http.Server.Request) !void { return http_api.json(request, .ok, body_out); } -pub fn createAccount(request: *http.Server.Request) !void { +pub fn createAccount(request: *http_api.Request) !void { const authorization = http_api.headerValue(request, "authorization"); var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); @@ -167,7 +167,7 @@ pub fn createAccount(request: *http.Server.Request) !void { try http_api.json(request, .ok, body_out); } -pub fn createInviteCode(request: *http.Server.Request) !void { +pub fn createInviteCode(request: *http_api.Request) !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const allocator = arena.allocator(); @@ -187,7 +187,7 @@ pub fn createInviteCode(request: *http.Server.Request) !void { return http_api.json(request, .ok, body_out); } -pub fn createInviteCodes(request: *http.Server.Request) !void { +pub fn createInviteCodes(request: *http_api.Request) !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const allocator = arena.allocator(); @@ -229,13 +229,13 @@ pub fn createInviteCodes(request: *http.Server.Request) !void { return http_api.json(request, .ok, try out.toOwnedSlice()); } -pub fn getAccountInviteCodes(request: *http.Server.Request) !void { +pub fn getAccountInviteCodes(request: *http_api.Request) !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const allocator = arena.allocator(); const account = requireAccount(request, allocator) catch return; var include_buf: [8]u8 = undefined; - const include_used = if (http_api.queryParam(request.head.target, "includeUsed", &include_buf)) |value| + const include_used = if (http_api.queryParam(request.url.raw, "includeUsed", &include_buf)) |value| std.ascii.eqlIgnoreCase(value, "true") else true; @@ -252,7 +252,7 @@ pub fn getAccountInviteCodes(request: *http.Server.Request) !void { return http_api.json(request, .ok, try out.toOwnedSlice()); } -pub fn listAppPasswords(request: *http.Server.Request) !void { +pub fn listAppPasswords(request: *http_api.Request) !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const allocator = arena.allocator(); @@ -274,7 +274,7 @@ pub fn listAppPasswords(request: *http.Server.Request) !void { return http_api.json(request, .ok, try out.toOwnedSlice()); } -pub fn createAppPassword(request: *http.Server.Request) !void { +pub fn createAppPassword(request: *http_api.Request) !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const allocator = arena.allocator(); @@ -310,7 +310,7 @@ pub fn createAppPassword(request: *http.Server.Request) !void { return http_api.json(request, .ok, body_out); } -pub fn revokeAppPassword(request: *http.Server.Request) !void { +pub fn revokeAppPassword(request: *http_api.Request) !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const allocator = arena.allocator(); @@ -386,7 +386,7 @@ fn jsonObjectField(value: std.json.Value, key: []const u8) ?std.json.Value { } fn validateCreateAccountPlcOperation( - request: *http.Server.Request, + request: *http_api.Request, allocator: std.mem.Allocator, handle: []const u8, operation: std.json.Value, @@ -521,7 +521,7 @@ fn isoTimestamp(allocator: std.mem.Allocator, seconds: i64) ![]const u8 { ); } -fn requestHost(request: *const http.Server.Request) ?[]const u8 { +fn requestHost(request: *const http_api.Request) ?[]const u8 { return http_api.headerValue(request, "host") orelse http_api.headerValue(request, ":authority"); } @@ -531,11 +531,8 @@ fn stripPort(host: []const u8) []const u8 { return host; } -fn plain(request: *http.Server.Request, status: http.Status, body: []const u8) !void { - try request.respond(body, .{ - .status = status, - .extra_headers = &plain_headers, - }); +fn plain(request: *http_api.Request, status: http.Status, body: []const u8) !void { + try http_api.respond(request, status, body, &plain_headers); } const plain_headers = [_]http.Header{ @@ -544,7 +541,7 @@ const plain_headers = [_]http.Header{ .{ .name = "connection", .value = "close" }, }; -fn verifyCreateAccountServiceAuth(request: *http.Server.Request, allocator: std.mem.Allocator, maybe_authorization: ?[]const u8, did: []const u8) !void { +fn verifyCreateAccountServiceAuth(request: *http_api.Request, allocator: std.mem.Allocator, maybe_authorization: ?[]const u8, did: []const u8) !void { const raw_header = maybe_authorization orelse { log.debug("xrpc createAccount service_auth missing did={s}\n", .{did}); return http_api.xrpcError(request, .unauthorized, "AuthenticationRequired", "service auth required to migrate an existing did"); @@ -605,7 +602,7 @@ fn issuerDid(iss: []const u8) []const u8 { return if (std.mem.indexOfScalar(u8, iss, '#')) |idx| iss[0..idx] else iss; } -pub fn createSession(request: *http.Server.Request) !void { +pub fn createSession(request: *http_api.Request) !void { var body_buf: [4096]u8 = undefined; const body = try http_api.readBody(request, &body_buf); @@ -652,7 +649,7 @@ pub fn createSession(request: *http.Server.Request) !void { try http_api.json(request, .ok, body_out); } -pub fn refreshSession(request: *http.Server.Request) !void { +pub fn refreshSession(request: *http_api.Request) !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const allocator = arena.allocator(); @@ -682,7 +679,7 @@ pub fn refreshSession(request: *http.Server.Request) !void { try http_api.json(request, .ok, body_out); } -pub fn getSession(request: *http.Server.Request) !void { +pub fn getSession(request: *http_api.Request) !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const allocator = arena.allocator(); @@ -733,7 +730,7 @@ fn accessScopeForAuthMethod(auth_method: []const u8) []const u8 { return "com.atproto.access"; } -fn currentBearerClaims(request: *http.Server.Request, allocator: std.mem.Allocator) !auth.TokenClaims { +fn currentBearerClaims(request: *http_api.Request, allocator: std.mem.Allocator) !auth.TokenClaims { const auth_header = http_api.headerValue(request, "authorization") orelse return error.AuthRequired; if (!std.ascii.startsWithIgnoreCase(auth_header, "bearer ")) return error.AuthRequired; const token = std.mem.trim(u8, auth_header["bearer ".len..], " \t"); @@ -788,7 +785,7 @@ fn sessionDidDocJson(allocator: std.mem.Allocator, account: auth.Account) ![]con ); } -pub fn activateAccount(request: *http.Server.Request) !void { +pub fn activateAccount(request: *http_api.Request) !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const allocator = arena.allocator(); @@ -801,7 +798,7 @@ pub fn activateAccount(request: *http.Server.Request) !void { return http_api.json(request, .ok, "{}"); } -pub fn deactivateAccount(request: *http.Server.Request) !void { +pub fn deactivateAccount(request: *http_api.Request) !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const allocator = arena.allocator(); @@ -812,7 +809,7 @@ pub fn deactivateAccount(request: *http.Server.Request) !void { return http_api.json(request, .ok, "{}"); } -pub fn getServiceAuth(request: *http.Server.Request) !void { +pub fn getServiceAuth(request: *http_api.Request) !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const allocator = arena.allocator(); @@ -824,7 +821,7 @@ pub fn getServiceAuth(request: *http.Server.Request) !void { const account = auth_ctx.account; var aud_buf: [256]u8 = undefined; - const audience = http_api.queryParam(request.head.target, "aud", &aud_buf) orelse { + const audience = http_api.queryParam(request.url.raw, "aud", &aud_buf) orelse { return http_api.xrpcError(request, .bad_request, "InvalidRequest", "Missing aud"); }; if (!validServiceAuthAudience(audience)) { @@ -832,7 +829,7 @@ pub fn getServiceAuth(request: *http.Server.Request) !void { } var lxm_buf: [256]u8 = undefined; - const lxm = http_api.queryParam(request.head.target, "lxm", &lxm_buf); + const lxm = http_api.queryParam(request.url.raw, "lxm", &lxm_buf); if (lxm) |method| { if (zat.Nsid.parse(method) == null) { return http_api.xrpcError(request, .bad_request, "InvalidRequest", "Invalid lxm"); @@ -863,9 +860,9 @@ fn validServiceAuthAudience(audience: []const u8) bool { return zat.Did.parse(audience[0..hash]) != null; } -fn requestedServiceAuthExpiration(request: *http.Server.Request, has_lxm: bool) !?i64 { +fn requestedServiceAuthExpiration(request: *http_api.Request, has_lxm: bool) !?i64 { var exp_buf: [32]u8 = undefined; - const raw = http_api.queryParam(request.head.target, "exp", &exp_buf) orelse return null; + const raw = http_api.queryParam(request.url.raw, "exp", &exp_buf) orelse return null; const exp = std.fmt.parseInt(i64, raw, 10) catch { try http_api.xrpcError(request, .bad_request, "InvalidRequest", "Invalid exp"); return error.HandledResponse; @@ -920,7 +917,7 @@ fn serviceAuthProtectedMethod(method: []const u8) bool { return false; } -pub fn requestEmailConfirmation(request: *http.Server.Request) !void { +pub fn requestEmailConfirmation(request: *http_api.Request) !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const allocator = arena.allocator(); @@ -940,7 +937,7 @@ pub fn requestEmailConfirmation(request: *http.Server.Request) !void { return http_api.json(request, .ok, "{}"); } -pub fn confirmEmail(request: *http.Server.Request) !void { +pub fn confirmEmail(request: *http_api.Request) !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const allocator = arena.allocator(); @@ -970,7 +967,7 @@ pub fn confirmEmail(request: *http.Server.Request) !void { return http_api.json(request, .ok, "{}"); } -pub fn requestEmailUpdate(request: *http.Server.Request) !void { +pub fn requestEmailUpdate(request: *http_api.Request) !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const allocator = arena.allocator(); @@ -990,7 +987,7 @@ pub fn requestEmailUpdate(request: *http.Server.Request) !void { return http_api.json(request, .ok, "{\"tokenRequired\":true}"); } -pub fn updateEmail(request: *http.Server.Request) !void { +pub fn updateEmail(request: *http_api.Request) !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const allocator = arena.allocator(); @@ -1023,7 +1020,7 @@ pub fn updateEmail(request: *http.Server.Request) !void { return http_api.json(request, .ok, "{}"); } -pub fn checkAccountStatus(request: *http.Server.Request) !void { +pub fn checkAccountStatus(request: *http_api.Request) !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const allocator = arena.allocator(); @@ -1032,11 +1029,11 @@ pub fn checkAccountStatus(request: *http.Server.Request) !void { return http_api.json(request, .ok, body); } -fn requireAccount(request: *http.Server.Request, allocator: std.mem.Allocator) !auth.Account { +fn requireAccount(request: *http_api.Request, allocator: std.mem.Allocator) !auth.Account { return (try requireAccountAccess(request, allocator)).account; } -fn requireAccountAccess(request: *http.Server.Request, allocator: std.mem.Allocator) !http_api.BearerAccount { +fn requireAccountAccess(request: *http_api.Request, allocator: std.mem.Allocator) !http_api.BearerAccount { return http_api.requireBearerAccess(request, allocator) catch |err| { switch (err) { error.AuthRequired => try http_api.xrpcError(request, .unauthorized, "AuthenticationRequired", "Authentication required"), @@ -1046,13 +1043,13 @@ fn requireAccountAccess(request: *http.Server.Request, allocator: std.mem.Alloca }; } -fn requireAccountScope(request: *http.Server.Request, maybe_scope: ?[]const u8, attr: scopes.AccountAttr, action: scopes.AccountAction) !void { +fn requireAccountScope(request: *http_api.Request, maybe_scope: ?[]const u8, attr: scopes.AccountAttr, action: scopes.AccountAction) !void { if (scopes.accountAllows(maybe_scope, attr, action)) return; try http_api.xrpcError(request, .forbidden, "InsufficientScope", "Insufficient scope"); return error.HandledResponse; } -fn requirePasswordSession(request: *http.Server.Request, allocator: std.mem.Allocator, did: []const u8) !void { +fn requirePasswordSession(request: *http_api.Request, allocator: std.mem.Allocator, did: []const u8) !void { const claims = currentBearerClaims(request, allocator) catch { try http_api.xrpcError(request, .forbidden, "AuthFactorTokenRequired", "Password session required"); return error.HandledResponse; @@ -1074,7 +1071,7 @@ fn requirePasswordSession(request: *http.Server.Request, allocator: std.mem.Allo } } -fn requireAdminToken(request: *http.Server.Request) !void { +fn requireAdminToken(request: *http_api.Request) !void { const expected = config.adminToken() orelse { try http_api.xrpcError(request, .forbidden, "AdminRequired", "Admin token not configured"); return error.HandledResponse; diff --git a/src/atproto/sync.zig b/src/atproto/sync.zig index 04da676..1338e8e 100644 --- a/src/atproto/sync.zig +++ b/src/atproto/sync.zig @@ -5,23 +5,112 @@ const http_api = @import("../http/api.zig"); const eventlog = @import("../storage/eventlog.zig"); const store = @import("../storage/store.zig"); const zat = @import("zat"); +const httpz = @import("httpz"); const http = std.http; const max_subscribe_repos_connections = 32; var subscribe_repos_connections: usize = 0; -pub fn getBlob(request: *http.Server.Request) !void { +pub const SubscribeReposClient = struct { + state: *StreamState, + + pub const Context = struct { + cursor: u64, + }; + + pub fn init(conn: *httpz.websocket.Conn, ctx: *const Context) !SubscribeReposClient { + const state = try std.heap.smp_allocator.create(StreamState); + state.* = .{ .conn = conn, .cursor = ctx.cursor }; + return .{ .state = state }; + } + + pub fn afterInit(self: *SubscribeReposClient) !void { + self.state.retain(); + const thread = std.Thread.spawn(.{}, streamEvents, .{self.state}) catch |err| { + self.state.release(); + return err; + }; + thread.detach(); + } + + pub fn clientMessage(_: *SubscribeReposClient, _: []const u8) !void {} + + pub fn close(self: *SubscribeReposClient) void { + @atomicStore(bool, &self.state.closed, true, .release); + self.state.finish(); + self.state.release(); + } + + const StreamState = struct { + conn: *httpz.websocket.Conn, + cursor: u64, + closed: bool = false, + counted: bool = true, + refs: usize = 1, + + fn retain(self: *StreamState) void { + _ = @atomicRmw(usize, &self.refs, .Add, 1, .monotonic); + } + + fn release(self: *StreamState) void { + if (@atomicRmw(usize, &self.refs, .Sub, 1, .acq_rel) == 1) { + std.heap.smp_allocator.destroy(self); + } + } + + fn finish(self: *StreamState) void { + if (@atomicRmw(bool, &self.counted, .Xchg, false, .acq_rel)) { + _ = @atomicRmw(usize, &subscribe_repos_connections, .Sub, 1, .monotonic); + } + } + + fn isClosed(self: *const StreamState) bool { + return @atomicLoad(bool, &self.closed, .acquire); + } + }; + + fn streamEvents(state: *StreamState) void { + defer state.release(); + defer state.finish(); + + var observed = eventlog.snapshot().generation; + while (!state.isClosed()) { + var sent = false; + { + var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); + defer arena.deinit(); + const events = store.listSeqEvents(arena.allocator(), state.cursor, 100) catch return; + for (events) |event| { + if (state.isClosed()) return; + state.conn.writeBin(event.frame) catch return; + state.cursor = event.seq; + sent = true; + } + } + if (sent) { + observed = eventlog.snapshot().generation; + continue; + } + const next = eventlog.waitForChange(observed) catch return; + if (next.generation != observed or next.latest_seq > state.cursor) { + observed = next.generation; + } + } + } +}; + +pub fn getBlob(request: *http_api.Request) !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const allocator = arena.allocator(); var did_buf: [256]u8 = undefined; - const did = http_api.queryParam(request.head.target, "did", &did_buf) orelse { + const did = http_api.queryParam(request.url.raw, "did", &did_buf) orelse { return http_api.xrpcError(request, .bad_request, "InvalidRequest", "Missing did"); }; var cid_buf: [256]u8 = undefined; - const cid = http_api.queryParam(request.head.target, "cid", &cid_buf) orelse { + const cid = http_api.queryParam(request.url.raw, "cid", &cid_buf) orelse { return http_api.xrpcError(request, .bad_request, "InvalidRequest", "Missing cid"); }; const blob = store.getBlob(allocator, did, cid) orelse { @@ -40,19 +129,16 @@ pub fn getBlob(request: *http.Server.Request) !void { .{ .name = "access-control-allow-private-network", .value = "true" }, .{ .name = "connection", .value = "close" }, }; - try request.respond(if (request.head.method == .HEAD) "" else blob.data, .{ - .status = .ok, - .extra_headers = &headers, - }); + try http_api.respond(request, .ok, if (request.method == .HEAD) "" else blob.data, &headers); } -pub fn getRepo(request: *http.Server.Request) !void { +pub fn getRepo(request: *http_api.Request) !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const allocator = arena.allocator(); var did_buf: [256]u8 = undefined; - const did = http_api.queryParam(request.head.target, "did", &did_buf) orelse { + const did = http_api.queryParam(request.url.raw, "did", &did_buf) orelse { return http_api.xrpcError(request, .bad_request, "InvalidRequest", "Missing did"); }; const body = store.writeRepoCar(allocator, did) catch { @@ -64,32 +150,29 @@ pub fn getRepo(request: *http.Server.Request) !void { .{ .name = "access-control-allow-private-network", .value = "true" }, .{ .name = "connection", .value = "close" }, }; - try request.respond(if (request.head.method == .HEAD) "" else body, .{ - .status = .ok, - .extra_headers = &headers, - }); + try http_api.respond(request, .ok, if (request.method == .HEAD) "" else body, &headers); } -pub fn listBlobs(request: *http.Server.Request) !void { +pub fn listBlobs(request: *http_api.Request) !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const allocator = arena.allocator(); var did_buf: [256]u8 = undefined; - const did = http_api.queryParam(request.head.target, "did", &did_buf) orelse { + const did = http_api.queryParam(request.url.raw, "did", &did_buf) orelse { return http_api.xrpcError(request, .bad_request, "InvalidRequest", "Missing did"); }; - const body = try store.writeBlobListJson(allocator, did, @min(http_api.queryLimit(request.head.target, 500), 1000)); + const body = try store.writeBlobListJson(allocator, did, @min(http_api.queryLimit(request.url.raw, 500), 1000)); return http_api.json(request, .ok, body); } -pub fn getLatestCommit(request: *http.Server.Request) !void { +pub fn getLatestCommit(request: *http_api.Request) !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const allocator = arena.allocator(); var did_buf: [256]u8 = undefined; - const did = http_api.queryParam(request.head.target, "did", &did_buf) orelse { + const did = http_api.queryParam(request.url.raw, "did", &did_buf) orelse { return http_api.xrpcError(request, .bad_request, "InvalidRequest", "Missing did"); }; const body = store.writeLatestCommitJson(allocator, did) catch |err| switch (err) { @@ -99,76 +182,47 @@ pub fn getLatestCommit(request: *http.Server.Request) !void { return http_api.json(request, .ok, body); } -pub fn listRepos(request: *http.Server.Request) !void { +pub fn listRepos(request: *http_api.Request) !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const allocator = arena.allocator(); - const body = try store.writeRepoListJson(allocator, http_api.queryLimit(request.head.target, 500)); + const body = try store.writeRepoListJson(allocator, http_api.queryLimit(request.url.raw, 500)); return http_api.json(request, .ok, body); } -pub fn subscribeRepos(request: *http.Server.Request) !void { - var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); - defer arena.deinit(); - const allocator = arena.allocator(); - - const key = switch (request.upgradeRequested()) { - .websocket => |maybe_key| maybe_key orelse { - return http_api.xrpcError(request, .bad_request, "InvalidRequest", "Missing Sec-WebSocket-Key"); - }, - else => return http_api.xrpcError(request, .upgrade_required, "InvalidRequest", "Expected WebSocket upgrade"), - }; - +pub fn subscribeRepos(request: *http_api.Request) !void { const active = @atomicRmw(usize, &subscribe_repos_connections, .Add, 1, .monotonic); if (active >= max_subscribe_repos_connections) { _ = @atomicRmw(usize, &subscribe_repos_connections, .Sub, 1, .monotonic); log.debug("sync subscribeRepos rejected too_many_connections active={d} max={d}\n", .{ active + 1, max_subscribe_repos_connections }); return http_api.xrpcError(request, .too_many_requests, "RateLimitExceeded", "too many subscribeRepos connections"); } - defer _ = @atomicRmw(usize, &subscribe_repos_connections, .Sub, 1, .monotonic); - - var ws = try request.respondWebSocket(.{ .key = key }); - try ws.flush(); + var upgraded = false; + defer if (!upgraded) { + _ = @atomicRmw(usize, &subscribe_repos_connections, .Sub, 1, .monotonic); + }; var cursor_buf: [32]u8 = undefined; - var cursor: u64 = if (http_api.queryParam(request.head.target, "cursor", &cursor_buf)) |raw| + const cursor: u64 = if (http_api.queryParam(request.url.raw, "cursor", &cursor_buf)) |raw| std.fmt.parseInt(u64, raw, 10) catch 0 else 0; - var observed = eventlog.snapshot().generation; - while (true) { - var sent = false; - { - var batch_arena = std.heap.ArenaAllocator.init(allocator); - defer batch_arena.deinit(); - const events = try store.listSeqEvents(batch_arena.allocator(), cursor, 100); - for (events) |event| { - ws.writeMessage(event.frame, .binary) catch return; - cursor = event.seq; - sent = true; - } - } - if (sent) { - observed = eventlog.snapshot().generation; - continue; - } - const next = try eventlog.waitForChange(observed); - if (next.generation != observed or next.latest_seq > cursor) { - observed = next.generation; - continue; - } + const ctx = SubscribeReposClient.Context{ .cursor = cursor }; + upgraded = try http_api.upgradeWebsocket(SubscribeReposClient, request, &ctx); + if (!upgraded) { + return http_api.xrpcError(request, .upgrade_required, "InvalidRequest", "Expected WebSocket upgrade"); } } -pub fn getRepoStatus(request: *http.Server.Request) !void { +pub fn getRepoStatus(request: *http_api.Request) !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const allocator = arena.allocator(); var did_buf: [256]u8 = undefined; - const did = http_api.queryParam(request.head.target, "did", &did_buf) orelse { + const did = http_api.queryParam(request.url.raw, "did", &did_buf) orelse { return http_api.xrpcError(request, .bad_request, "InvalidRequest", "Missing did"); }; const body = store.writeRepoStatusJson(allocator, did) catch |err| switch (err) { @@ -178,11 +232,11 @@ pub fn getRepoStatus(request: *http.Server.Request) !void { return http_api.json(request, .ok, body); } -pub fn notifyOfUpdate(request: *http.Server.Request) !void { +pub fn notifyOfUpdate(request: *http_api.Request) !void { return http_api.json(request, .ok, "{}"); } -pub fn requestCrawl(request: *http.Server.Request) !void { +pub fn requestCrawl(request: *http_api.Request) !void { return http_api.json(request, .ok, "{}"); } diff --git a/src/http/api.zig b/src/http/api.zig index 33ca2da..2d90cb8 100644 --- a/src/http/api.zig +++ b/src/http/api.zig @@ -1,23 +1,40 @@ const std = @import("std"); const auth = @import("../auth/tokens.zig"); const store = @import("../storage/store.zig"); +const httpz = @import("httpz"); const http = std.http; +pub const Request = httpz.Request; +pub const Response = httpz.Response; + +threadlocal var active_response: ?*Response = null; + +pub fn bindResponse(res: *Response) void { + active_response = res; +} + +pub fn unbindResponse() void { + active_response = null; +} + +fn response() *Response { + return active_response orelse @panic("http response not bound"); +} pub const BearerAccount = struct { account: auth.Account, oauth_scope: ?[]const u8, }; -pub fn requireBearerAccount(request: *const http.Server.Request, allocator: std.mem.Allocator) !auth.Account { +pub fn requireBearerAccount(request: *const Request, allocator: std.mem.Allocator) !auth.Account { return (try requireBearerAccountWithScope(request, allocator, "com.atproto.access")).account; } -pub fn requireBearerAccess(request: *const http.Server.Request, allocator: std.mem.Allocator) !BearerAccount { +pub fn requireBearerAccess(request: *const Request, allocator: std.mem.Allocator) !BearerAccount { return requireBearerAccountWithScope(request, allocator, "com.atproto.access"); } -pub fn requireBearerAccountWithScope(request: *const http.Server.Request, allocator: std.mem.Allocator, scope: []const u8) !BearerAccount { +pub fn requireBearerAccountWithScope(request: *const Request, allocator: std.mem.Allocator, scope: []const u8) !BearerAccount { const auth_header = headerValue(request, "authorization") orelse { return error.AuthRequired; }; @@ -56,7 +73,7 @@ fn scopeAllows(actual: []const u8, required: []const u8) bool { std.mem.eql(u8, actual, "com.atproto.appPassPrivileged"); } -pub fn optionalBearerAccount(request: *const http.Server.Request, allocator: std.mem.Allocator) ?auth.Account { +pub fn optionalBearerAccount(request: *const Request, allocator: std.mem.Allocator) ?auth.Account { return requireBearerAccount(request, allocator) catch null; } @@ -68,7 +85,7 @@ fn now() i64 { }; } -pub fn parseJsonBody(request: *http.Server.Request, allocator: std.mem.Allocator, body: []const u8) !std.json.Parsed(std.json.Value) { +pub fn parseJsonBody(request: *Request, allocator: std.mem.Allocator, body: []const u8) !std.json.Parsed(std.json.Value) { return std.json.parseFromSlice(std.json.Value, allocator, body, .{}) catch { try xrpcError(request, .bad_request, "InvalidRequest", "Expected JSON body"); return error.InvalidJson; @@ -85,73 +102,26 @@ pub fn valueString(value: std.json.Value, key: []const u8) ?[]const u8 { }; } -pub fn readBody(request: *http.Server.Request, buf: []u8) ![]const u8 { - var reader_buf: [1024]u8 = undefined; - const reader = try request.readerExpectContinue(&reader_buf); - if (request.head.content_length) |len| { - if (len > buf.len) return error.BodyTooLarge; - var total: usize = 0; - const expected: usize = @intCast(len); - var chunk: [8192]u8 = undefined; - while (total < expected) { - const remaining = expected - total; - const n = try reader.readSliceShort(chunk[0..@min(chunk.len, remaining)]); - if (n == 0) return error.UnexpectedEof; - @memcpy(buf[total..][0..n], chunk[0..n]); - total += n; - } - return buf[0..expected]; - } - - var total: usize = 0; - while (true) { - if (total == buf.len) { - var extra: [1]u8 = undefined; - const n = try reader.readSliceShort(&extra); - if (n == 0) return buf[0..total]; - return error.BodyTooLarge; - } - - const n = try reader.readSliceShort(buf[total..]); - if (n == 0) return buf[0..total]; - total += n; - } +pub fn readBody(request: *Request, buf: []u8) ![]const u8 { + const body = request.body() orelse ""; + if (body.len > buf.len) return error.BodyTooLarge; + @memcpy(buf[0..body.len], body); + return buf[0..body.len]; } -pub fn readBodyAlloc(request: *http.Server.Request, allocator: std.mem.Allocator, max_len: usize) ![]const u8 { - var reader_buf: [4096]u8 = undefined; - const reader = try request.readerExpectContinue(&reader_buf); - if (request.head.content_length) |len| { - if (len > max_len) return error.BodyTooLarge; - const body = try allocator.alloc(u8, @intCast(len)); - var total: usize = 0; - var chunk: [8192]u8 = undefined; - while (total < body.len) { - const remaining = body.len - total; - const n = try reader.readSliceShort(chunk[0..@min(chunk.len, remaining)]); - if (n == 0) return error.UnexpectedEof; - @memcpy(body[total..][0..n], chunk[0..n]); - total += n; - } - return body; - } - - var body: std.ArrayList(u8) = .empty; - var chunk: [8192]u8 = undefined; - while (true) { - const n = try reader.readSliceShort(&chunk); - if (n == 0) return try body.toOwnedSlice(allocator); - if (body.items.len + n > max_len) return error.BodyTooLarge; - try body.appendSlice(allocator, chunk[0..n]); - } +pub fn readBodyAlloc(request: *Request, allocator: std.mem.Allocator, max_len: usize) ![]const u8 { + const body = request.body() orelse ""; + if (body.len > max_len) return error.BodyTooLarge; + return try allocator.dupe(u8, body); } -pub fn headerValue(request: *const http.Server.Request, name: []const u8) ?[]const u8 { - var it = request.iterateHeaders(); - while (it.next()) |header| { - if (std.ascii.eqlIgnoreCase(header.name, name)) return header.value; - } - return null; +pub fn headerValue(request: *const Request, name: []const u8) ?[]const u8 { + var lower_buf: [128]u8 = undefined; + const lower = if (name.len <= lower_buf.len) blk: { + for (name, 0..) |c, i| lower_buf[i] = std.ascii.toLower(c); + break :blk lower_buf[0..name.len]; + } else name; + return request.header(lower); } pub fn queryParam(target: []const u8, name: []const u8, out: []u8) ?[]const u8 { @@ -196,48 +166,59 @@ pub fn percentDecode(input: []const u8, out: []u8) ![]const u8 { return out[0..write]; } -pub fn corsPreflight(request: *http.Server.Request) !void { - var headers = cors_headers; - var allow_headers_buf: [1024]u8 = undefined; +pub fn corsPreflight(request: *Request) !void { + const res = response(); if (headerValue(request, "access-control-request-headers")) |requested| { const trimmed = std.mem.trim(u8, requested, " \t"); - if (trimmed.len > 0 and trimmed.len < allow_headers_buf.len) { - @memcpy(allow_headers_buf[0..trimmed.len], trimmed); - headers[3].value = allow_headers_buf[0..trimmed.len]; - } + if (trimmed.len > 0) try addHeader(res, "access-control-allow-headers", trimmed); } - try request.respond("", .{ - .status = .no_content, - .extra_headers = &headers, - }); + try setHeaders(res, &cors_headers); + res.setStatus(.no_content); +} + +pub fn toStdMethod(method: httpz.Method) http.Method { + return switch (method) { + .GET => .GET, + .HEAD => .HEAD, + .POST => .POST, + .PUT => .PUT, + .PATCH => .PATCH, + .DELETE => .DELETE, + .OPTIONS => .OPTIONS, + else => .GET, + }; } -pub fn json(request: *http.Server.Request, status: http.Status, body: []const u8) !void { - normalizeBodylessRequest(request); - try request.respond(body, .{ - .status = status, - .extra_headers = &json_headers, - }); +pub fn upgradeWebsocket(comptime Handler: type, request: *Request, ctx: anytype) !bool { + return httpz.upgradeWebsocket(Handler, request, response(), ctx); } -pub fn empty(request: *http.Server.Request, status: http.Status) !void { - normalizeBodylessRequest(request); - try request.respond("", .{ - .status = status, - .extra_headers = &empty_headers, - }); +pub fn json(request: *Request, status: http.Status, body: []const u8) !void { + _ = request; + const res = response(); + try setHeaders(res, &json_headers); + res.setStatus(status); + res.body = try res.arena.dupe(u8, body); } -pub fn text(request: *http.Server.Request, status: http.Status, body: []const u8) !void { - normalizeBodylessRequest(request); - try request.respond(body, .{ - .status = status, - .extra_headers = &text_headers, - }); +pub fn empty(request: *Request, status: http.Status) !void { + _ = request; + const res = response(); + try setHeaders(res, &empty_headers); + res.setStatus(status); + res.body = ""; +} + +pub fn text(request: *Request, status: http.Status, body: []const u8) !void { + _ = request; + const res = response(); + try setHeaders(res, &text_headers); + res.setStatus(status); + res.body = try res.arena.dupe(u8, body); } pub fn xrpcError( - request: *http.Server.Request, + request: *Request, status: http.Status, error_name: []const u8, message: []const u8, @@ -277,13 +258,20 @@ const cors_headers = [_]http.Header{ .{ .name = "connection", .value = "close" }, }; -fn normalizeBodylessRequest(request: *http.Server.Request) void { - if (request.head.method == .POST and - request.head.content_length == null and - request.head.transfer_encoding == .none) - { - request.head.content_length = 0; - } +pub fn respond(request: *Request, status: http.Status, body: []const u8, headers: []const http.Header) !void { + _ = request; + const res = response(); + try setHeaders(res, headers); + res.setStatus(status); + res.body = try res.arena.dupe(u8, body); +} + +fn setHeaders(res: *Response, headers: []const http.Header) !void { + for (headers) |header| try addHeader(res, header.name, header.value); +} + +fn addHeader(res: *Response, name: []const u8, value: []const u8) !void { + try res.headerOpts(name, value, .{ .dupe_name = true, .dupe_value = true }); } test "decodes query params" { diff --git a/src/http/landing/mod.zig b/src/http/landing/mod.zig index b903fda..001a972 100644 --- a/src/http/landing/mod.zig +++ b/src/http/landing/mod.zig @@ -2,11 +2,12 @@ const std = @import("std"); const assets = @import("assets.zig"); const config = @import("../../core/config.zig"); const entropy = @import("entropy.zig"); +const http_api = @import("../api.zig"); const store = @import("../../storage/store.zig"); const http = std.http; -pub fn serve(request: *http.Server.Request) !void { +pub fn serve(request: *http_api.Request) !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const allocator = arena.allocator(); @@ -19,27 +20,27 @@ pub fn serve(request: *http.Server.Request) !void { .{ .name = "access-control-allow-private-network", .value = "true" }, .{ .name = "connection", .value = "close" }, }; - try request.respond(body, .{ .status = .ok, .extra_headers = &headers }); + try http_api.respond(request, .ok, body, &headers); } -pub fn serveFavicon(request: *http.Server.Request) !void { +pub fn serveFavicon(request: *http_api.Request) !void { const headers = [_]http.Header{ .{ .name = "content-type", .value = "image/svg+xml; charset=utf-8" }, .{ .name = "cache-control", .value = "public, max-age=86400" }, .{ .name = "access-control-allow-origin", .value = "*" }, .{ .name = "connection", .value = "close" }, }; - try request.respond(assets.favicon, .{ .status = .ok, .extra_headers = &headers }); + try http_api.respond(request, .ok, assets.favicon, &headers); } -pub fn serveOgImage(request: *http.Server.Request) !void { +pub fn serveOgImage(request: *http_api.Request) !void { const headers = [_]http.Header{ .{ .name = "content-type", .value = "image/png" }, .{ .name = "cache-control", .value = "public, max-age=86400" }, .{ .name = "access-control-allow-origin", .value = "*" }, .{ .name = "connection", .value = "close" }, }; - try request.respond(assets.og_image, .{ .status = .ok, .extra_headers = &headers }); + try http_api.respond(request, .ok, assets.og_image, &headers); } fn render(allocator: std.mem.Allocator) ![]const u8 { diff --git a/src/http/router.zig b/src/http/router.zig index 2cc4be2..7fdea82 100644 --- a/src/http/router.zig +++ b/src/http/router.zig @@ -1,8 +1,7 @@ const std = @import("std"); +const httpz = @import("httpz"); const xrpc = @import("../core/xrpc.zig"); -const http = std.http; - pub const Route = enum { cors_preflight, root, @@ -77,7 +76,7 @@ pub const Route = enum { not_found, }; -pub fn route(method: http.Method, target: []const u8) Route { +pub fn route(method: httpz.Method, target: []const u8) Route { if (method == .OPTIONS) return .cors_preflight; const path = stripQuery(target); diff --git a/src/http/server.zig b/src/http/server.zig index 7812e04..c8ff54a 100644 --- a/src/http/server.zig +++ b/src/http/server.zig @@ -7,6 +7,7 @@ const atproto_repo = @import("../atproto/repo.zig"); const atproto_server = @import("../atproto/server.zig"); const atproto_sync = @import("../atproto/sync.zig"); const build_options = @import("build_options"); +const httpz = @import("httpz"); const log = @import("../core/log.zig"); const http_api = @import("api.zig"); const passkeys = @import("../internal/passkeys.zig"); @@ -14,7 +15,6 @@ const landing = @import("landing/mod.zig"); const router = @import("router.zig"); const http = std.http; -const net = std.Io.net; const corsPreflight = http_api.corsPreflight; const json = http_api.json; const xrpcError = http_api.xrpcError; @@ -24,147 +24,127 @@ pub const Options = struct { port: u16 = 2583, }; -pub fn listen(io: std.Io, options: Options) !void { - const ip4 = try net.Ip4Address.parse(options.host, options.port); - const address: net.IpAddress = .{ .ip4 = ip4 }; - var tcp_server = try address.listen(io, .{ .reuse_address = true }); - defer tcp_server.deinit(io); - - var connections: std.Io.Group = .init; - defer connections.cancel(io); +const App = struct { + io: std.Io, + pub const WebsocketHandler = atproto_sync.SubscribeReposClient; - log.info("zds listening on http://{s}:{d}\n", .{ options.host, options.port }); - while (true) { - const stream = try tcp_server.accept(io); - connections.concurrent(io, serveConnection, .{ io, stream }) catch |err| { - log.err("failed to start connection handler: {s}\n", .{@errorName(err)}); - var copy = stream; - copy.close(io); - continue; + pub fn handle(app: *App, request: *httpz.Request, response: *httpz.Response) void { + http_api.bindResponse(response); + defer http_api.unbindResponse(); + app.serveRequest(request) catch |err| { + log.err("failed to serve {s}: {s}\n", .{ request.url.raw, @errorName(err) }); + response.setStatus(.internal_server_error); + response.body = "Internal Server Error"; }; } -} -fn serveConnection(io: std.Io, stream: net.Stream) void { - defer { - var copy = stream; - copy.close(io); - } + fn serveRequest(app: *App, request: *httpz.Request) !void { + const route = router.route(request.method, request.url.raw); + log.debug("http {s} {s} route={s} start\n", .{ methodName(request), request.url.raw, @tagName(route) }); - var send_buffer: [4096]u8 = undefined; - var recv_buffer: [4096]u8 = undefined; - var connection_reader = stream.reader(io, &recv_buffer); - var connection_writer = stream.writer(io, &send_buffer); - var http_server: http.Server = .init(&connection_reader.interface, &connection_writer.interface); - - var request = http_server.receiveHead() catch |err| switch (err) { - error.HttpConnectionClosing => return, - else => { - log.err("failed to receive request: {s}\n", .{@errorName(err)}); - return; - }, - }; - var target_buf: [512]u8 = undefined; - const target_len = @min(request.head.target.len, target_buf.len); - @memcpy(target_buf[0..target_len], request.head.target[0..target_len]); - const target = target_buf[0..target_len]; - serveRequest(io, &request) catch |err| { - log.err("failed to serve {s}: {s}\n", .{ target, @errorName(err) }); - return; - }; -} + if (atproto_proxy.shouldProxy(request)) { + return atproto_proxy.xrpcProxy(request); + } -fn serveRequest(io: std.Io, request: *http.Server.Request) !void { - const route = router.route(request.head.method, request.head.target); - const method = request.head.method; - var target_buf: [512]u8 = undefined; - const target_len = @min(request.head.target.len, target_buf.len); - @memcpy(target_buf[0..target_len], request.head.target[0..target_len]); - const target = target_buf[0..target_len]; - log.debug("http {s} {s} route={s} start\n", .{ @tagName(method), target, @tagName(route) }); - - if (atproto_proxy.shouldProxy(request)) { - return atproto_proxy.xrpcProxy(request); + switch (route) { + .cors_preflight => try corsPreflight(request), + .root => try landing.serve(request), + .favicon => try landing.serveFavicon(request), + .og_image => try landing.serveOgImage(request), + .health => try health(request), + .did_json => try atproto_server.didJson(request), + .oauth_protected_resource => try atproto_oauth.protectedResource(request), + .oauth_authorization_server => try atproto_oauth.authorizationServer(request), + .oauth_jwks => try atproto_oauth.jwks(request), + .oauth_par => try atproto_oauth.par(request), + .oauth_authorize => if (request.method == .GET) try atproto_oauth.authorizeGet(request) else try atproto_oauth.authorizePost(request), + .oauth_token => try atproto_oauth.token(request), + .oauth_introspect => try atproto_oauth.introspect(request), + .oauth_revoke => try atproto_oauth.revoke(request), + .oauth_passkey_options => try passkeys.loginStart(request), + .oauth_passkey_finish => try passkeys.loginFinish(request), + .passkeys_page => try passkeys.redirectToSecurity(request), + .security_page => try passkeys.securityPage(request), + .atproto_did => try atproto_server.atprotoDid(request), + .describe_server => try atproto_server.describeServer(request), + .reserve_signing_key => try atproto_server.reserveSigningKey(request), + .create_account => try atproto_server.createAccount(request), + .create_invite_code => try atproto_server.createInviteCode(request), + .create_invite_codes => try atproto_server.createInviteCodes(request), + .get_account_invite_codes => try atproto_server.getAccountInviteCodes(request), + .list_app_passwords => try atproto_server.listAppPasswords(request), + .create_app_password => try atproto_server.createAppPassword(request), + .revoke_app_password => try atproto_server.revokeAppPassword(request), + .start_passkey_registration => try passkeys.xrpcStartRegistration(request), + .finish_passkey_registration => try passkeys.xrpcFinishRegistration(request), + .list_passkeys => try passkeys.xrpcList(request), + .delete_passkey => try passkeys.xrpcDelete(request), + .update_passkey => try passkeys.xrpcUpdate(request), + .create_session => try atproto_server.createSession(request), + .refresh_session => try atproto_server.refreshSession(request), + .get_session => try atproto_server.getSession(request), + .get_service_auth => try atproto_server.getServiceAuth(request), + .activate_account => try atproto_server.activateAccount(request), + .deactivate_account => try atproto_server.deactivateAccount(request), + .request_email_confirmation => try atproto_server.requestEmailConfirmation(request), + .confirm_email => try atproto_server.confirmEmail(request), + .request_email_update => try atproto_server.requestEmailUpdate(request), + .update_email => try atproto_server.updateEmail(request), + .check_account_status => try atproto_server.checkAccountStatus(request), + .app_preferences_get => try atproto_preferences.getPreferences(request), + .app_preferences_put => try atproto_preferences.putPreferences(request), + .repo_create_record => try atproto_repo.createRecord(request), + .repo_put_record => try atproto_repo.putRecord(request), + .repo_describe_repo => try atproto_repo.describeRepo(request), + .repo_get_record => try atproto_repo.getRecord(request), + .repo_list_records => try atproto_repo.listRecords(request), + .repo_delete_record => try atproto_repo.deleteRecord(request), + .repo_apply_writes => try atproto_repo.applyWrites(request), + .repo_import_repo => try atproto_repo.importRepo(request), + .repo_upload_blob => try atproto_repo.uploadBlob(app.io, request), + .repo_list_missing_blobs => try atproto_repo.listMissingBlobs(request), + .sync_get_blob => try atproto_sync.getBlob(request), + .sync_get_repo => try atproto_sync.getRepo(request), + .sync_get_latest_commit => try atproto_sync.getLatestCommit(request), + .sync_list_repos => try atproto_sync.listRepos(request), + .sync_list_blobs => try atproto_sync.listBlobs(request), + .sync_subscribe_repos => try atproto_sync.subscribeRepos(request), + .sync_get_repo_status => try atproto_sync.getRepoStatus(request), + .sync_notify_of_update => try atproto_sync.notifyOfUpdate(request), + .sync_request_crawl => try atproto_sync.requestCrawl(request), + .identity_get_recommended_did_credentials => try atproto_identity.getRecommendedDidCredentials(request), + .identity_request_plc_operation_signature => try atproto_identity.requestPlcOperationSignature(request), + .identity_sign_plc_operation => try atproto_identity.signPlcOperation(request), + .identity_submit_plc_operation => try atproto_identity.submitPlcOperation(request), + .identity_resolve_handle => try atproto_identity.resolveHandle(request), + .not_found => try xrpcError(request, .not_found, "UnknownMethod", "Unknown XRPC method"), + } } +}; - switch (route) { - .cors_preflight => try corsPreflight(request), - .root => try landing.serve(request), - .favicon => try landing.serveFavicon(request), - .og_image => try landing.serveOgImage(request), - .health => try health(request), - .did_json => try atproto_server.didJson(request), - .oauth_protected_resource => try atproto_oauth.protectedResource(request), - .oauth_authorization_server => try atproto_oauth.authorizationServer(request), - .oauth_jwks => try atproto_oauth.jwks(request), - .oauth_par => try atproto_oauth.par(request), - .oauth_authorize => if (request.head.method == .GET) try atproto_oauth.authorizeGet(request) else try atproto_oauth.authorizePost(request), - .oauth_token => try atproto_oauth.token(request), - .oauth_introspect => try atproto_oauth.introspect(request), - .oauth_revoke => try atproto_oauth.revoke(request), - .oauth_passkey_options => try passkeys.loginStart(request), - .oauth_passkey_finish => try passkeys.loginFinish(request), - .passkeys_page => try passkeys.redirectToSecurity(request), - .security_page => try passkeys.securityPage(request), - .atproto_did => try atproto_server.atprotoDid(request), - .describe_server => try atproto_server.describeServer(request), - .reserve_signing_key => try atproto_server.reserveSigningKey(request), - .create_account => try atproto_server.createAccount(request), - .create_invite_code => try atproto_server.createInviteCode(request), - .create_invite_codes => try atproto_server.createInviteCodes(request), - .get_account_invite_codes => try atproto_server.getAccountInviteCodes(request), - .list_app_passwords => try atproto_server.listAppPasswords(request), - .create_app_password => try atproto_server.createAppPassword(request), - .revoke_app_password => try atproto_server.revokeAppPassword(request), - .start_passkey_registration => try passkeys.xrpcStartRegistration(request), - .finish_passkey_registration => try passkeys.xrpcFinishRegistration(request), - .list_passkeys => try passkeys.xrpcList(request), - .delete_passkey => try passkeys.xrpcDelete(request), - .update_passkey => try passkeys.xrpcUpdate(request), - .create_session => try atproto_server.createSession(request), - .refresh_session => try atproto_server.refreshSession(request), - .get_session => try atproto_server.getSession(request), - .get_service_auth => try atproto_server.getServiceAuth(request), - .activate_account => try atproto_server.activateAccount(request), - .deactivate_account => try atproto_server.deactivateAccount(request), - .request_email_confirmation => try atproto_server.requestEmailConfirmation(request), - .confirm_email => try atproto_server.confirmEmail(request), - .request_email_update => try atproto_server.requestEmailUpdate(request), - .update_email => try atproto_server.updateEmail(request), - .check_account_status => try atproto_server.checkAccountStatus(request), - .app_preferences_get => try atproto_preferences.getPreferences(request), - .app_preferences_put => try atproto_preferences.putPreferences(request), - .repo_create_record => try atproto_repo.createRecord(request), - .repo_put_record => try atproto_repo.putRecord(request), - .repo_describe_repo => try atproto_repo.describeRepo(request), - .repo_get_record => try atproto_repo.getRecord(request), - .repo_list_records => try atproto_repo.listRecords(request), - .repo_delete_record => try atproto_repo.deleteRecord(request), - .repo_apply_writes => try atproto_repo.applyWrites(request), - .repo_import_repo => try atproto_repo.importRepo(request), - .repo_upload_blob => try atproto_repo.uploadBlob(io, request), - .repo_list_missing_blobs => try atproto_repo.listMissingBlobs(request), - .sync_get_blob => try atproto_sync.getBlob(request), - .sync_get_repo => try atproto_sync.getRepo(request), - .sync_get_latest_commit => try atproto_sync.getLatestCommit(request), - .sync_list_repos => try atproto_sync.listRepos(request), - .sync_list_blobs => try atproto_sync.listBlobs(request), - .sync_subscribe_repos => try atproto_sync.subscribeRepos(request), - .sync_get_repo_status => try atproto_sync.getRepoStatus(request), - .sync_notify_of_update => try atproto_sync.notifyOfUpdate(request), - .sync_request_crawl => try atproto_sync.requestCrawl(request), - .identity_get_recommended_did_credentials => try atproto_identity.getRecommendedDidCredentials(request), - .identity_request_plc_operation_signature => try atproto_identity.requestPlcOperationSignature(request), - .identity_sign_plc_operation => try atproto_identity.signPlcOperation(request), - .identity_submit_plc_operation => try atproto_identity.submitPlcOperation(request), - .identity_resolve_handle => try atproto_identity.resolveHandle(request), - .not_found => { - try xrpcError(request, .not_found, "UnknownMethod", "Unknown XRPC method"); +pub fn listen(io: std.Io, options: Options) !void { + var app: App = .{ .io = io }; + var server = try httpz.Server(*App).init(io, std.heap.smp_allocator, .{ + .address = .{ .ip = .{ .ip4 = try std.Io.net.Ip4Address.parse(options.host, options.port) } }, + .request = .{ + .max_body_size = 128 * 1024 * 1024, + .lazy_read_size = 16 * 1024 * 1024, }, + .workers = .{ + .large_buffer_count = 4, + .large_buffer_size = 64 * 1024, + }, + }, &app); + defer { + server.stop(); + server.deinit(); } + + log.info("zds listening on http://{s}:{d}\n", .{ options.host, options.port }); + try server.listen(); } -fn health(request: *http.Server.Request) !void { +fn health(request: *httpz.Request) !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const body = try std.fmt.allocPrint( @@ -174,3 +154,17 @@ fn health(request: *http.Server.Request) !void { ); try json(request, .ok, body); } + +fn methodName(request: *const httpz.Request) []const u8 { + return switch (request.method) { + .GET => "GET", + .HEAD => "HEAD", + .POST => "POST", + .PUT => "PUT", + .PATCH => "PATCH", + .DELETE => "DELETE", + .OPTIONS => "OPTIONS", + .CONNECT => "CONNECT", + .OTHER => request.method_string, + }; +} diff --git a/src/internal/passkeys.zig b/src/internal/passkeys.zig index ef8c60f..02f3817 100644 --- a/src/internal/passkeys.zig +++ b/src/internal/passkeys.zig @@ -41,17 +41,15 @@ pub fn verifyAssertionSignature(credential_public_key: []const u8, signature_der try webauthn.crypto.verifyWebAuthnEs256(credential_public_key, signature_der, message); } -pub fn redirectToSecurity(request: *http.Server.Request) !void { - try request.respond("", .{ - .status = .see_other, - .extra_headers = &[_]http.Header{ - .{ .name = "location", .value = "/security" }, - .{ .name = "connection", .value = "close" }, - }, - }); +pub fn redirectToSecurity(request: *http_api.Request) !void { + const headers = [_]http.Header{ + .{ .name = "location", .value = "/security" }, + .{ .name = "connection", .value = "close" }, + }; + try http_api.respond(request, .see_other, "", &headers); } -pub fn securityPage(request: *http.Server.Request) !void { +pub fn securityPage(request: *http_api.Request) !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const allocator = arena.allocator(); @@ -155,7 +153,7 @@ pub fn securityPage(request: *http.Server.Request) !void { try respondHtml(request, .ok, body); } -pub fn xrpcStartRegistration(request: *http.Server.Request) !void { +pub fn xrpcStartRegistration(request: *http_api.Request) !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const allocator = arena.allocator(); @@ -182,7 +180,7 @@ pub fn xrpcStartRegistration(request: *http.Server.Request) !void { try http_api.json(request, .ok, body); } -pub fn xrpcFinishRegistration(request: *http.Server.Request) !void { +pub fn xrpcFinishRegistration(request: *http_api.Request) !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const allocator = arena.allocator(); @@ -206,7 +204,7 @@ pub fn xrpcFinishRegistration(request: *http.Server.Request) !void { try http_api.json(request, .ok, body); } -pub fn xrpcList(request: *http.Server.Request) !void { +pub fn xrpcList(request: *http_api.Request) !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const allocator = arena.allocator(); @@ -216,7 +214,7 @@ pub fn xrpcList(request: *http.Server.Request) !void { try http_api.json(request, .ok, body); } -pub fn xrpcDelete(request: *http.Server.Request) !void { +pub fn xrpcDelete(request: *http_api.Request) !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const allocator = arena.allocator(); @@ -227,7 +225,7 @@ pub fn xrpcDelete(request: *http.Server.Request) !void { try http_api.json(request, .ok, "{}"); } -pub fn xrpcUpdate(request: *http.Server.Request) !void { +pub fn xrpcUpdate(request: *http_api.Request) !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const allocator = arena.allocator(); @@ -239,7 +237,7 @@ pub fn xrpcUpdate(request: *http.Server.Request) !void { try http_api.json(request, .ok, "{}"); } -pub fn loginStart(request: *http.Server.Request) !void { +pub fn loginStart(request: *http_api.Request) !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const allocator = arena.allocator(); @@ -270,7 +268,7 @@ pub fn loginStart(request: *http.Server.Request) !void { try http_api.json(request, .ok, body); } -pub fn loginFinish(request: *http.Server.Request) !void { +pub fn loginFinish(request: *http_api.Request) !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const allocator = arena.allocator(); @@ -311,7 +309,7 @@ pub fn loginFinish(request: *http.Server.Request) !void { try http_api.json(request, .ok, body); } -fn readJson(request: *http.Server.Request, allocator: std.mem.Allocator) !std.json.Parsed(std.json.Value) { +fn readJson(request: *http_api.Request, allocator: std.mem.Allocator) !std.json.Parsed(std.json.Value) { const body = try http_api.readBodyAlloc(request, allocator, 64 * 1024); return std.json.parseFromSlice(std.json.Value, allocator, body, .{}) catch { try jsonError(request, .bad_request, "Expected JSON body"); @@ -494,13 +492,13 @@ fn now() i64 { }; } -fn jsonError(request: *http.Server.Request, status: http.Status, message: []const u8) !void { +fn jsonError(request: *http_api.Request, status: http.Status, message: []const u8) !void { var buf: [512]u8 = undefined; const body = try std.fmt.bufPrint(&buf, "{{\"error\":{f}}}", .{std.json.fmt(message, .{})}); try http_api.json(request, status, body); } -fn requireBearerAccount(request: *http.Server.Request, allocator: std.mem.Allocator) !auth.Account { +fn requireBearerAccount(request: *http_api.Request, allocator: std.mem.Allocator) !auth.Account { return http_api.requireBearerAccount(request, allocator) catch |err| { switch (err) { error.AuthRequired => try http_api.xrpcError(request, .unauthorized, "AuthenticationRequired", "Authentication required"), @@ -510,15 +508,13 @@ fn requireBearerAccount(request: *http.Server.Request, allocator: std.mem.Alloca }; } -fn respondHtml(request: *http.Server.Request, status: http.Status, body: []const u8) !void { - try request.respond(body, .{ - .status = status, - .extra_headers = &[_]http.Header{ - .{ .name = "content-type", .value = "text/html; charset=utf-8" }, - .{ .name = "access-control-allow-origin", .value = "*" }, - .{ .name = "connection", .value = "close" }, - }, - }); +fn respondHtml(request: *http_api.Request, status: http.Status, body: []const u8) !void { + const headers = [_]http.Header{ + .{ .name = "content-type", .value = "text/html; charset=utf-8" }, + .{ .name = "access-control-allow-origin", .value = "*" }, + .{ .name = "connection", .value = "close" }, + }; + try http_api.respond(request, status, body, &headers); } fn htmlEscape(allocator: std.mem.Allocator, value: []const u8) ![]const u8 {