atproto relay implementation in zig zlay.waow.tech
at main 107 lines 3.6 kB view raw
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}