const std = @import("std"); const host_target = @import("../hosts/target.zig"); const io_mod = @import("../shared/io.zig"); const oauth_transport = @import("oauth_transport.zig"); const secret = @import("secret.zig"); const Allocator = std.mem.Allocator; const max_response_bytes: usize = 64 * 1024; pub const max_session_bytes: usize = 64 * 1024; pub const max_revision_bytes: usize = 1024; extern "fx" fn fx_http_request( method_ptr: [*]const u8, method_len: usize, url_ptr: [*]const u8, url_len: usize, headers_ptr: [*]const u8, headers_len: usize, body_ptr: [*]const u8, body_len: usize, status_out: *u16, response_ptr: [*]u8, response_cap: usize, ) i32; extern "fx" fn fx_oauth_session_load( bytes_ptr: [*]u8, bytes_cap: usize, revision_ptr: [*]u8, revision_cap: usize, revision_len_out: *usize, ) i32; extern "fx" fn fx_oauth_session_commit( bytes_ptr: [*]const u8, bytes_len: usize, expected_revision_ptr: [*]const u8, expected_revision_len: usize, revision_ptr: [*]u8, revision_cap: usize, revision_len_out: *usize, ) i32; extern "fx" fn fx_oauth_session_remove( expected_revision_ptr: [*]const u8, expected_revision_len: usize, ) i32; pub const oauth_provider: oauth_transport.Provider = if (host_target.is_wasm) .{ .execute_fn = executeOAuthRequest } else oauth_transport.unavailable_provider; fn executeOAuthRequest( _: ?*anyopaque, alloc: Allocator, request: oauth_transport.Request, ) !oauth_transport.Response { try checkRequestBounds(request); const Header = struct { name: []const u8, value: []const u8 }; const headers: []const Header = switch (request.method) { .get => &.{}, .post_form => &.{.{ .name = "content-type", .value = "application/x-www-form-urlencoded", }}, }; var response = try executeRequest( alloc, switch (request.method) { .get => "GET", .post_form => "POST", }, request.url, headers, request.payload orelse "", ); errdefer response.deinit(alloc); try checkRequestBounds(request); return response; } pub fn executeBearerGet( alloc: Allocator, url: []const u8, access_token: []const u8, ) !oauth_transport.Response { const authorization = try std.fmt.allocPrint(alloc, "Bearer {s}", .{access_token}); defer secret.zeroAndFree(alloc, authorization); return executeRequest(alloc, "GET", url, &.{.{ .name = "authorization", .value = authorization, }}, ""); } fn executeRequest( alloc: Allocator, method: []const u8, url: []const u8, headers: anytype, body: []const u8, ) !oauth_transport.Response { var headers_json: std.Io.Writer.Allocating = .init(alloc); defer headers_json.deinit(); try std.json.Stringify.value(headers, .{}, &headers_json.writer); const response_buffer = try alloc.alloc(u8, max_response_bytes); defer secret.zeroAndFree(alloc, response_buffer); var status: u16 = 0; const response_len = fx_http_request( method.ptr, method.len, url.ptr, url.len, headers_json.written().ptr, headers_json.written().len, body.ptr, body.len, &status, response_buffer.ptr, response_buffer.len, ); if (response_len == -2) return error.OAuthResponseTooLarge; if (response_len < 0 or @as(usize, @intCast(response_len)) > response_buffer.len) { return error.OAuthRequestFailed; } return .{ .disposition = if (status == 200) .accepted else .rejected, .body = try alloc.dupe(u8, response_buffer[0..@intCast(response_len)]), }; } fn checkRequestBounds(request: oauth_transport.Request) !void { if (request.cancel_flag) |flag| { if (flag.load(.seq_cst)) return error.Cancelled; } if (request.deadline) |deadline| { const now = std.Io.Clock.Timestamp.now(io_mod.getIo(), .awake); if (!std.Io.Clock.Timestamp.compare(now, .lt, deadline)) return error.Timeout; } } pub const StoredSession = struct { bytes: []u8, revision: []u8, pub fn deinit(self: *StoredSession, alloc: Allocator) void { secret.zeroAndFree(alloc, self.bytes); alloc.free(self.revision); self.* = undefined; } }; pub const RemoveOutcome = enum { deleted, missing }; pub const SessionStore = struct { context: ?*anyopaque = null, load_fn: *const fn (?*anyopaque, Allocator) anyerror!?StoredSession, commit_fn: *const fn (?*anyopaque, Allocator, []const u8, ?[]const u8) anyerror![]u8, remove_fn: *const fn (?*anyopaque, ?[]const u8) anyerror!RemoveOutcome, pub fn load(self: SessionStore, alloc: Allocator) !?StoredSession { return self.load_fn(self.context, alloc); } pub fn commit( self: SessionStore, alloc: Allocator, bytes: []const u8, expected_revision: ?[]const u8, ) ![]u8 { return self.commit_fn(self.context, alloc, bytes, expected_revision); } pub fn remove(self: SessionStore, expected_revision: ?[]const u8) !RemoveOutcome { return self.remove_fn(self.context, expected_revision); } }; pub const oauth_session_store: SessionStore = if (host_target.is_wasm) .{ .load_fn = loadSession, .commit_fn = commitSession, .remove_fn = removeSession, } else .{ .load_fn = unavailableSessionLoad, .commit_fn = unavailableSessionCommit, .remove_fn = unavailableSessionRemove, }; fn unavailableSessionLoad(_: ?*anyopaque, _: Allocator) !?StoredSession { return error.OAuthSessionStoreUnavailable; } fn unavailableSessionCommit(_: ?*anyopaque, _: Allocator, _: []const u8, _: ?[]const u8) ![]u8 { return error.OAuthSessionStoreUnavailable; } fn unavailableSessionRemove(_: ?*anyopaque, _: ?[]const u8) !RemoveOutcome { return error.OAuthSessionStoreUnavailable; } fn loadSession(_: ?*anyopaque, alloc: Allocator) !?StoredSession { const session_buffer = try alloc.alloc(u8, max_session_bytes); defer secret.zeroAndFree(alloc, session_buffer); const revision_buffer = try alloc.alloc(u8, max_revision_bytes); defer alloc.free(revision_buffer); var revision_len: usize = 0; const session_len = fx_oauth_session_load( session_buffer.ptr, session_buffer.len, revision_buffer.ptr, revision_buffer.len, &revision_len, ); if (session_len == -2) return null; if (session_len == -3) return error.OAuthSessionTooLarge; if (session_len < 0 or @as(usize, @intCast(session_len)) > session_buffer.len or revision_len > revision_buffer.len) { return error.OAuthSessionStoreUnavailable; } const session_bytes = try alloc.dupe(u8, session_buffer[0..@intCast(session_len)]); errdefer secret.zeroAndFree(alloc, session_bytes); return .{ .bytes = session_bytes, .revision = try alloc.dupe(u8, revision_buffer[0..revision_len]), }; } fn commitSession( _: ?*anyopaque, alloc: Allocator, bytes: []const u8, expected_revision: ?[]const u8, ) ![]u8 { if (bytes.len > max_session_bytes) return error.OAuthSessionTooLarge; const revision_buffer = try alloc.alloc(u8, max_revision_bytes); defer alloc.free(revision_buffer); var revision_len: usize = 0; const expected = expected_revision orelse ""; const status = fx_oauth_session_commit( bytes.ptr, bytes.len, expected.ptr, expected.len, revision_buffer.ptr, revision_buffer.len, &revision_len, ); if (status == -2) return error.OAuthSessionRevisionConflict; if (status < 0 or revision_len > revision_buffer.len) { return error.OAuthSessionStoreUnavailable; } return alloc.dupe(u8, revision_buffer[0..revision_len]); } fn removeSession(_: ?*anyopaque, expected_revision: ?[]const u8) !RemoveOutcome { const expected = expected_revision orelse ""; return switch (fx_oauth_session_remove(expected.ptr, expected.len)) { 0 => .deleted, 1 => .missing, -2 => error.OAuthSessionRevisionConflict, else => error.OAuthSessionStoreUnavailable, }; } test "request bounds reject cancellation before touching the JS host" { var cancelled = std.atomic.Value(bool).init(true); try std.testing.expectError(error.Cancelled, checkRequestBounds(.{ .method = .get, .url = "https://vercel.test", .cancel_flag = &cancelled, })); }