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 respondRedirect(conn: *Conn, location: []const u8) void {
32 var buf: [1024]u8 = undefined;
33 const header = std.fmt.bufPrint(&buf, "HTTP/1.1 302 Found\r\nLocation: {s}\r\nContent-Length: 0\r\nConnection: close\r\nServer: zlay\r\n\r\n", .{location}) catch return;
34 conn.writeFramed(header) catch return;
35}
36
37pub fn httpStatusLine(status: http.Status) []const u8 {
38 return switch (status) {
39 .ok => "200 OK",
40 .bad_request => "400 Bad Request",
41 .unauthorized => "401 Unauthorized",
42 .forbidden => "403 Forbidden",
43 .not_found => "404 Not Found",
44 .method_not_allowed => "405 Method Not Allowed",
45 .conflict => "409 Conflict",
46 .internal_server_error => "500 Internal Server Error",
47 else => "500 Internal Server Error",
48 };
49}
50
51// --- query string helpers ---
52
53pub fn queryParam(query: []const u8, name: []const u8) ?[]const u8 {
54 if (query.len == 0) return null;
55 var iter = std.mem.splitScalar(u8, query, '&');
56 while (iter.next()) |pair| {
57 const eq = std.mem.indexOfScalar(u8, pair, '=') orelse continue;
58 if (std.mem.eql(u8, pair[0..eq], name)) {
59 return pair[eq + 1 ..];
60 }
61 }
62 return null;
63}
64
65/// like queryParam but percent-decodes the value into buf.
66/// returns null if the param is missing, or a slice into buf with the decoded value.
67pub fn queryParamDecoded(query: []const u8, name: []const u8, buf: []u8) ?[]const u8 {
68 const raw = queryParam(query, name) orelse return null;
69 var i: usize = 0;
70 var out: usize = 0;
71 while (i < raw.len) {
72 if (raw[i] == '%' and i + 2 < raw.len) {
73 const hi = hexVal(raw[i + 1]) orelse {
74 if (out >= buf.len) return null;
75 buf[out] = raw[i];
76 out += 1;
77 i += 1;
78 continue;
79 };
80 const lo = hexVal(raw[i + 2]) orelse {
81 if (out >= buf.len) return null;
82 buf[out] = raw[i];
83 out += 1;
84 i += 1;
85 continue;
86 };
87 if (out >= buf.len) return null;
88 buf[out] = (@as(u8, hi) << 4) | @as(u8, lo);
89 out += 1;
90 i += 3;
91 } else if (raw[i] == '+') {
92 if (out >= buf.len) return null;
93 buf[out] = ' ';
94 out += 1;
95 i += 1;
96 } else {
97 if (out >= buf.len) return null;
98 buf[out] = raw[i];
99 out += 1;
100 i += 1;
101 }
102 }
103 return buf[0..out];
104}
105
106fn hexVal(c: u8) ?u4 {
107 return switch (c) {
108 '0'...'9' => @intCast(c - '0'),
109 'a'...'f' => @intCast(c - 'a' + 10),
110 'A'...'F' => @intCast(c - 'A' + 10),
111 else => null,
112 };
113}