atproto relay implementation in zig
zlay.waow.tech
1//! HTTP response helpers and query string parsing.
2//!
3//! pure utility module — no domain dependencies. used by all API handler modules
4//! to write raw HTTP responses to websocket connections and parse query parameters.
5
6const std = @import("std");
7const http = std.http;
8const websocket = @import("websocket");
9
10pub const Conn = websocket.Conn;
11
12pub fn httpRespond(conn: *Conn, status: http.Status, content_type: []const u8, body: []const u8) void {
13 var buf: [512]u8 = undefined;
14 const header = std.fmt.bufPrint(&buf, "HTTP/1.1 {s}\r\nContent-Type: {s}\r\nContent-Length: {d}\r\nConnection: close\r\nServer: zlay\r\n\r\n", .{
15 httpStatusLine(status),
16 content_type,
17 body.len,
18 }) catch return;
19 conn.writeFramed(header) catch return;
20 if (body.len > 0) conn.writeFramed(body) catch return;
21}
22
23pub fn respondJson(conn: *Conn, status: http.Status, body: []const u8) void {
24 httpRespond(conn, status, "application/json", body);
25}
26
27pub fn respondText(conn: *Conn, status: http.Status, body: []const u8) void {
28 httpRespond(conn, status, "text/plain", body);
29}
30
31pub fn httpStatusLine(status: http.Status) []const u8 {
32 return switch (status) {
33 .ok => "200 OK",
34 .bad_request => "400 Bad Request",
35 .unauthorized => "401 Unauthorized",
36 .forbidden => "403 Forbidden",
37 .not_found => "404 Not Found",
38 .method_not_allowed => "405 Method Not Allowed",
39 .conflict => "409 Conflict",
40 .internal_server_error => "500 Internal Server Error",
41 else => "500 Internal Server Error",
42 };
43}
44
45// --- query string helpers ---
46
47pub fn queryParam(query: []const u8, name: []const u8) ?[]const u8 {
48 if (query.len == 0) return null;
49 var iter = std.mem.splitScalar(u8, query, '&');
50 while (iter.next()) |pair| {
51 const eq = std.mem.indexOfScalar(u8, pair, '=') orelse continue;
52 if (std.mem.eql(u8, pair[0..eq], name)) {
53 return pair[eq + 1 ..];
54 }
55 }
56 return null;
57}
58
59/// like queryParam but percent-decodes the value into buf.
60/// returns null if the param is missing, or a slice into buf with the decoded value.
61pub fn queryParamDecoded(query: []const u8, name: []const u8, buf: []u8) ?[]const u8 {
62 const raw = queryParam(query, name) orelse return null;
63 var i: usize = 0;
64 var out: usize = 0;
65 while (i < raw.len) {
66 if (raw[i] == '%' and i + 2 < raw.len) {
67 const hi = hexVal(raw[i + 1]) orelse {
68 if (out >= buf.len) return null;
69 buf[out] = raw[i];
70 out += 1;
71 i += 1;
72 continue;
73 };
74 const lo = hexVal(raw[i + 2]) orelse {
75 if (out >= buf.len) return null;
76 buf[out] = raw[i];
77 out += 1;
78 i += 1;
79 continue;
80 };
81 if (out >= buf.len) return null;
82 buf[out] = (@as(u8, hi) << 4) | @as(u8, lo);
83 out += 1;
84 i += 3;
85 } else if (raw[i] == '+') {
86 if (out >= buf.len) return null;
87 buf[out] = ' ';
88 out += 1;
89 i += 1;
90 } else {
91 if (out >= buf.len) return null;
92 buf[out] = raw[i];
93 out += 1;
94 i += 1;
95 }
96 }
97 return buf[0..out];
98}
99
100fn hexVal(c: u8) ?u4 {
101 return switch (c) {
102 '0'...'9' => @intCast(c - '0'),
103 'a'...'f' => @intCast(c - 'a' + 10),
104 'A'...'F' => @intCast(c - 'A' + 10),
105 else => null,
106 };
107}