summaryrefslogtreecommitdiff
path: root/libpanto/src/http_helper.zig
diff options
context:
space:
mode:
Diffstat (limited to 'libpanto/src/http_helper.zig')
-rw-r--r--libpanto/src/http_helper.zig160
1 files changed, 0 insertions, 160 deletions
diff --git a/libpanto/src/http_helper.zig b/libpanto/src/http_helper.zig
deleted file mode 100644
index a075d72..0000000
--- a/libpanto/src/http_helper.zig
+++ /dev/null
@@ -1,160 +0,0 @@
-//! Small non-streaming HTTP request/response helper over the process-global
-//! `std.http.Client`. The streaming providers deliberately avoid `fetch()`
-//! (it buffers); auth flows want the opposite — request a URL, read the whole
-//! response body into one buffer, inspect status + JSON. This is that.
-//!
-//! Used by `auth.zig` for OAuth device flows and token exchanges. Not part of
-//! the streaming hot path, so simplicity wins: one request, full-body read,
-//! owned result.
-
-const std = @import("std");
-const config_mod = @import("config.zig");
-
-pub const Header = config_mod.Header;
-pub const Method = std.http.Method;
-
-/// A fully-read HTTP response. `body` is owned by `allocator`.
-pub const Response = struct {
- allocator: std.mem.Allocator,
- status: u16,
- body: []u8,
-
- pub fn deinit(self: Response) void {
- self.allocator.free(self.body);
- }
-
- /// True for a 2xx status.
- pub fn ok(self: Response) bool {
- return self.status >= 200 and self.status < 300;
- }
-};
-
-pub const Options = struct {
- /// Caller headers (auth bearer, identity headers, …). `accept` and
- /// `content-type` are added separately from the fields below.
- headers: []const Header = &.{},
- /// Request body, or null for a bodiless request (e.g. a GET).
- body: ?[]const u8 = null,
- /// `content-type` header value when `body` is present.
- content_type: ?[]const u8 = null,
- /// `accept` header value. Defaults to JSON.
- accept: ?[]const u8 = "application/json",
- /// Upper bound on the response body read into memory.
- max_body_bytes: usize = 1 << 20,
-};
-
-/// Perform one request and return the fully-read response. The caller owns
-/// `Response.body` and must `deinit` it. Redirects are surfaced as their 3xx
-/// status (not followed) so auth credentials never leak across a redirect.
-pub fn request(
- allocator: std.mem.Allocator,
- client: *std.http.Client,
- method: Method,
- url: []const u8,
- opts: Options,
-) !Response {
- const uri = try std.Uri.parse(url);
-
- var hdrs: std.ArrayList(std.http.Header) = .empty;
- defer hdrs.deinit(allocator);
- if (opts.accept) |a| try hdrs.append(allocator, .{ .name = "accept", .value = a });
- if (opts.content_type) |ct| {
- if (opts.body != null) try hdrs.append(allocator, .{ .name = "content-type", .value = ct });
- }
- for (opts.headers) |h| try hdrs.append(allocator, .{ .name = h.name, .value = h.value });
-
- var req = try client.request(method, uri, .{
- .extra_headers = hdrs.items,
- // Disable compression so we read the body without a decompressor.
- .headers = .{ .accept_encoding = .{ .override = "identity" } },
- .keep_alive = false,
- // Surface 3xx to us rather than following with auth headers attached.
- .redirect_behavior = .unhandled,
- });
- defer req.deinit();
-
- if (opts.body) |body| {
- req.transfer_encoding = .{ .content_length = body.len };
- var send_buf: [4096]u8 = undefined;
- var bw = try req.sendBodyUnflushed(&send_buf);
- try bw.writer.writeAll(body);
- try bw.end();
- try req.connection.?.flush();
- } else {
- try req.sendBodiless();
- }
-
- var redirect_buf: [2048]u8 = undefined;
- var response = try req.receiveHead(&redirect_buf);
- const status: u16 = @intFromEnum(response.head.status);
-
- var transfer_buf: [4096]u8 = undefined;
- const body_reader = response.reader(&transfer_buf);
- var out: std.ArrayList(u8) = .empty;
- errdefer out.deinit(allocator);
- var tmp: [4096]u8 = undefined;
- while (true) {
- const n = body_reader.readSliceShort(&tmp) catch break;
- if (n == 0) break;
- try out.appendSlice(allocator, tmp[0..n]);
- if (out.items.len > opts.max_body_bytes) break;
- }
-
- return .{ .allocator = allocator, .status = status, .body = try out.toOwnedSlice(allocator) };
-}
-
-// ===========================================================================
-// JSON helpers (used by auth flows to read fields out of a response body)
-// ===========================================================================
-
-/// Read a string at a dotted JSON path (e.g. `endpoints.api`) from a parsed
-/// JSON value. Returns null if any segment is missing or the leaf is not a
-/// string. Borrows from `root`.
-pub fn jsonStringAtPath(root: std.json.Value, path: []const u8) ?[]const u8 {
- const leaf = jsonAtPath(root, path) orelse return null;
- return switch (leaf) {
- .string => |s| s,
- else => null,
- };
-}
-
-/// Read an integer (unix-seconds expiry, etc.) at a dotted JSON path. Accepts
-/// JSON integers and integer-valued floats. Returns null otherwise.
-pub fn jsonIntAtPath(root: std.json.Value, path: []const u8) ?i64 {
- const leaf = jsonAtPath(root, path) orelse return null;
- return switch (leaf) {
- .integer => |i| i,
- .float => |f| @intFromFloat(f),
- .number_string => |s| std.fmt.parseInt(i64, s, 10) catch null,
- else => null,
- };
-}
-
-/// Walk a dotted path through nested JSON objects. Returns the leaf value or
-/// null if any object segment is missing.
-pub fn jsonAtPath(root: std.json.Value, path: []const u8) ?std.json.Value {
- var cur = root;
- var it = std.mem.splitScalar(u8, path, '.');
- while (it.next()) |seg| {
- switch (cur) {
- .object => |o| cur = o.get(seg) orelse return null,
- else => return null,
- }
- }
- return cur;
-}
-
-const t = std.testing;
-
-test "jsonAtPath: nested object string + int" {
- const src =
- \\{"token":"abc","expires_at":1700000000,"endpoints":{"api":"https://x"}}
- ;
- var parsed = try std.json.parseFromSlice(std.json.Value, t.allocator, src, .{});
- defer parsed.deinit();
- try t.expectEqualStrings("abc", jsonStringAtPath(parsed.value, "token").?);
- try t.expectEqual(@as(i64, 1700000000), jsonIntAtPath(parsed.value, "expires_at").?);
- try t.expectEqualStrings("https://x", jsonStringAtPath(parsed.value, "endpoints.api").?);
- try t.expect(jsonStringAtPath(parsed.value, "endpoints.missing") == null);
- try t.expect(jsonStringAtPath(parsed.value, "nope.deep") == null);
-}