PranitInitial commit439f83c3 months agoHistory
790 lines · 29.1 KB · Zig
RawDownload
const std = @import("std");const debug_trace = @import("../shared/debug_trace.zig");const host_target = @import("../hosts/target.zig");const io_mod = @import("../shared/io.zig");const profile_paths = @import("../shared/profile_paths.zig");const js_host_auth = @import("js_host_auth.zig");const secret = @import("secret.zig"); const Allocator = std.mem.Allocator; pub const issuer = "https://vercel.com";pub const client_id_env = "FX_OAUTH_CLIENT_ID";pub const default_client_id = "cl_zzh5hiOZbwJ9bfqEcYqPIJv3TaPaEYL0";const e2e_issuer_url_env = "FX_E2E_OAUTH_ISSUER_URL";pub const auth_file_name = profile_paths.auth_file_name;const schema_version: i64 = 1;const max_auth_file_bytes: usize = 64 * 1024;const expiry_skew_ms: i64 = 60 * 1000;const mutation_lock_file_name = "auth.lock";const mutation_lock_deadline_ms: u64 = 2000;const e2e_lock_contention_file_name = "auth-lock-contention";const LoadMode = enum { tolerate_open_failure, report_open_failure }; pub fn refresh_deadline_ms(expires_at_ms: i64) i64 {    return expires_at_ms -| expiry_skew_ms;} const MutationLockProbe = struct {    fx_dir: std.Io.Dir,    signaled: bool = false,     fn tryLock(raw_ctx: ?*anyopaque, file: std.Io.File) anyerror!bool {        const locked = try file.tryLock(io_mod.getIo(), .exclusive);        const self: *MutationLockProbe = @ptrCast(@alignCast(raw_ctx.?));        if (!locked and !self.signaled) {            signalE2ELockContention(self.fx_dir);            self.signaled = true;        }        return locked;    }}; fn signalE2ELockContention(fx_dir: std.Io.Dir) void {    const enabled = io_mod.getenv("FX_E2E_AUTH_LOCK_CONTENTION") orelse return;    if (!std.mem.eql(u8, enabled, "1")) return;    var file = fx_dir.createFile(io_mod.getIo(), e2e_lock_contention_file_name, .{        .truncate = true,        .permissions = std.Io.File.Permissions.fromMode(0o600),    }) catch return;    defer file.close(io_mod.getIo());    file.writeStreamingAll(io_mod.getIo(), "contended\n") catch {};} pub const DeleteOutcome = enum {    deleted,    missing,    deleted_not_durable,}; pub const Session = struct {    issuer: []u8,    client_id: []u8,    access_token: []u8,    refresh_token: []u8,    expires_at_ms: i64,    scope: []u8,    token_type: []u8,    team_slug: ?[]u8 = null,    team_id: ?[]u8 = null,     pub fn deinit(self: *Session, alloc: Allocator) void {        alloc.free(self.issuer);        alloc.free(self.client_id);        secret.zeroAndFree(alloc, self.access_token);        secret.zeroAndFree(alloc, self.refresh_token);        alloc.free(self.scope);        alloc.free(self.token_type);        if (self.team_slug) |value| alloc.free(value);        if (self.team_id) |value| alloc.free(value);        self.* = undefined;    }     pub fn expired(self: Session, now_ms: i64) bool {        return refresh_deadline_ms(self.expires_at_ms) <= now_ms;    }}; pub const Mutation = if (host_target.is_wasm) HostMutation else NativeMutation; const NativeMutation = struct {    fx_dir: io_mod.VerifiedDir,    lock: io_mod.TimedAdvisoryLock,     pub fn deinit(self: *Mutation) void {        self.lock.release();        self.fx_dir.close();        self.* = undefined;    }     pub fn load(self: *Mutation, alloc: Allocator) !?Session {        return loadFromDir(alloc, &self.fx_dir.dir, .report_open_failure);    }     pub fn save(self: *Mutation, alloc: Allocator, session: Session) !void {        const text = try stringify(alloc, session);        defer secret.zeroAndFree(alloc, text);        try io_mod.durableReplaceVerified(alloc, &self.fx_dir, auth_file_name, text);    }     pub fn delete(self: *Mutation) !DeleteOutcome {        return deleteAuthFile(&self.fx_dir.dir, .{});    }}; const HostMutation = struct {    store: js_host_auth.SessionStore = js_host_auth.oauth_session_store,    revision: [js_host_auth.max_revision_bytes]u8 = undefined,    revision_len: usize = 0,    exists: bool = false,     fn init(store: js_host_auth.SessionStore) HostMutation {        return .{ .store = store };    }     pub fn deinit(self: *HostMutation) void {        @memset(&self.revision, 0);        self.* = undefined;    }     pub fn load(self: *HostMutation, alloc: Allocator) !?Session {        var stored = (try self.store.load(alloc)) orelse {            self.exists = false;            self.revision_len = 0;            return null;        };        defer stored.deinit(alloc);        try self.captureStoredRevision(stored.revision);        return @as(?Session, try parse(alloc, stored.bytes));    }     pub fn save(self: *HostMutation, alloc: Allocator, session: Session) !void {        const text = try stringify(alloc, session);        defer secret.zeroAndFree(alloc, text);        const expected = if (self.exists) self.revision[0..self.revision_len] else null;        const revision = try self.store.commit(alloc, text, expected);        defer alloc.free(revision);        try self.captureStoredRevision(revision);    }     pub fn delete(self: *HostMutation) !DeleteOutcome {        const expected = if (self.exists) self.revision[0..self.revision_len] else null;        return switch (try self.store.remove(expected)) {            .deleted => .deleted,            .missing => .missing,        };    }     fn captureRevision(self: *HostMutation, alloc: Allocator) !void {        var stored = (try self.store.load(alloc)) orelse {            self.exists = false;            self.revision_len = 0;            return;        };        defer stored.deinit(alloc);        try self.captureStoredRevision(stored.revision);    }     fn captureStoredRevision(self: *HostMutation, revision: []const u8) !void {        if (revision.len > self.revision.len) return error.OAuthSessionRevisionTooLarge;        @memcpy(self.revision[0..revision.len], revision);        self.revision_len = revision.len;        self.exists = true;    }}; pub fn configuredClientId() ?[]const u8 {    if (io_mod.getenv(client_id_env)) |value| {        if (std.mem.trim(u8, value, " \t\r\n").len > 0) return value;    }    return if (default_client_id.len == 0) null else default_client_id;} pub fn configuredIssuerUrl() ![]const u8 {    return selectIssuerUrl(io_mod.getenv(e2e_issuer_url_env));} pub fn isLoopbackE2EIssuer(url: []const u8) bool {    return !std.mem.eql(u8, url, issuer) and isLoopbackHttpUrl(url, true);} pub fn validateE2EEndpoint(issuer_url: []const u8, endpoint: []const u8) !void {    if (isLoopbackE2EIssuer(issuer_url) and !isLoopbackHttpUrl(endpoint, false)) {        return error.InvalidE2EOAuthEndpoint;    }} fn selectIssuerUrl(override: ?[]const u8) ![]const u8 {    const raw = override orelse return issuer;    const candidate = std.mem.trimEnd(u8, raw, "/");    if (!isLoopbackHttpUrl(candidate, true)) return error.InvalidE2EOAuthIssuer;    return candidate;} fn isLoopbackHttpUrl(url: []const u8, require_origin: bool) bool {    const uri = std.Uri.parse(url) catch return false;    if (!std.ascii.eqlIgnoreCase(uri.scheme, "http") or        uri.user != null or        uri.password != null or        uri.port == null or        (require_origin and (!uri.path.isEmpty() or uri.query != null or uri.fragment != null)))    {        return false;    }     const host_component = uri.host orelse return false;    var host_buf: [std.Io.net.HostName.max_len]u8 = undefined;    const host = host_component.toRaw(&host_buf) catch return false;    return std.mem.eql(u8, host, "127.0.0.1") or        std.ascii.eqlIgnoreCase(host, "localhost") or        std.mem.eql(u8, host, "[::1]");} pub fn load(alloc: Allocator) !?Session {    if (comptime host_target.is_wasm) return loadFromHost(alloc, js_host_auth.oauth_session_store);    const home = io_mod.getenv("HOME") orelse {        debug_trace.logf("auth", "session load skipped step=home err=HomeNotSet", .{});        return null;    };    var home_dir = std.Io.Dir.openDirAbsolute(io_mod.getIo(), home, .{ .iterate = true }) catch |err| {        debug_trace.logf("auth", "session load failed step=open_home err={s}", .{@errorName(err)});        return null;    };    defer home_dir.close(io_mod.getIo());     var fx_dir = home_dir.openDir(io_mod.getIo(), profile_paths.root_dir_name, .{        .iterate = true,        .follow_symlinks = false,    }) catch |err| {        debug_trace.logf("auth", "session load failed step=open_profile err={s}", .{@errorName(err)});        return null;    };    defer fx_dir.close(io_mod.getIo());     return loadFromDir(alloc, &fx_dir, .tolerate_open_failure);} fn loadFromHost(alloc: Allocator, store: js_host_auth.SessionStore) !?Session {    var stored = (try store.load(alloc)) orelse return null;    defer stored.deinit(alloc);    return parse(alloc, stored.bytes) catch |err| switch (err) {        error.OutOfMemory => return err,        else => {            debug_trace.logf("auth", "session load failed step=parse err={s}", .{@errorName(err)});            return null;        },    };} fn loadFromDir(alloc: Allocator, fx_dir: *std.Io.Dir, mode: LoadMode) !?Session {    var file = fx_dir.openFile(io_mod.getIo(), auth_file_name, .{        .mode = .read_only,        .allow_directory = false,        .follow_symlinks = false,        .resolve_beneath = true,    }) catch |err| switch (err) {        error.FileNotFound => return null,        else => {            debug_trace.logf("auth", "session load failed step=open_file err={s}", .{@errorName(err)});            if (mode == .tolerate_open_failure) return null else return err;        },    };    defer file.close(io_mod.getIo());     const stat = try file.stat(io_mod.getIo());    if (stat.kind != .file or stat.permissions.toMode() & 0o077 != 0) {        debug_trace.logf("auth", "session load failed step=permissions err=InsecureAuthFile", .{});        return null;    }     const bytes = try io_mod.readFileToEnd(alloc, &file, max_auth_file_bytes);    defer secret.zeroAndFree(alloc, bytes);    return parse(alloc, bytes) catch |err| switch (err) {        error.OutOfMemory => return err,        else => {            debug_trace.logf("auth", "session load failed step=parse err={s}", .{@errorName(err)});            return null;        },    };} pub fn saveNewSession(alloc: Allocator, session: Session) !void {    if (comptime host_target.is_wasm) {        var mutation = HostMutation.init(js_host_auth.oauth_session_store);        defer mutation.deinit();        try mutation.captureRevision(alloc);        return mutation.save(alloc, session);    }    var mutation = try beginMutation();    defer mutation.deinit();    try mutation.save(alloc, session);} pub fn beginExistingMutation() !?Mutation {    if (comptime host_target.is_wasm) {        return @as(?Mutation, HostMutation.init(js_host_auth.oauth_session_store));    }    const home = io_mod.getenv("HOME") orelse return error.HomeNotSet;    var home_dir = io_mod.VerifiedDir{        .dir = try std.Io.Dir.openDirAbsolute(io_mod.getIo(), home, .{ .iterate = true }),    };    defer home_dir.close();     const fx_dir = openExistingPrivateFxDir(&home_dir) catch |err| switch (err) {        error.FileNotFound => return null,        else => return err,    };    return @as(?Mutation, try lockMutation(fx_dir));} fn beginMutation() !Mutation {    const home = io_mod.getenv("HOME") orelse return error.HomeNotSet;    var home_dir = io_mod.VerifiedDir{        .dir = try std.Io.Dir.openDirAbsolute(io_mod.getIo(), home, .{ .iterate = true }),    };    defer home_dir.close();     const fx_dir = try io_mod.openOrCreateVerifiedPrivateDir(&home_dir, profile_paths.root_dir_name);    return lockMutation(fx_dir);} fn lockMutation(open_fx_dir: io_mod.VerifiedDir) !Mutation {    var probe = MutationLockProbe{ .fx_dir = open_fx_dir.dir };    return lockMutationWithOps(open_fx_dir, mutation_lock_deadline_ms, .{        .ctx = &probe,        .try_lock = MutationLockProbe.tryLock,    });} fn lockMutationWithOps(    open_fx_dir: io_mod.VerifiedDir,    deadline_ms: u64,    ops: io_mod.LockOps,) !Mutation {    var fx_dir = open_fx_dir;    errdefer fx_dir.close();     var lock = try io_mod.acquireTimedAdvisoryLockWithOps(        &fx_dir,        mutation_lock_file_name,        deadline_ms,        ops,    );    errdefer lock.release();     return .{        .fx_dir = fx_dir,        .lock = lock,    };} fn openExistingPrivateFxDir(home_dir: *io_mod.VerifiedDir) !io_mod.VerifiedDir {    var dir = try home_dir.dir.openDir(io_mod.getIo(), profile_paths.root_dir_name, .{        .iterate = true,        .follow_symlinks = false,    });    errdefer dir.close(io_mod.getIo());     const initial_stat = try dir.stat(io_mod.getIo());    if (initial_stat.kind != .directory) return error.DurablePathUnsafe;    if (initial_stat.permissions.toMode() & 0o200 == 0) {        return error.PrivateStatePermissionsUnsupported;    }    dir.setPermissions(io_mod.getIo(), std.Io.File.Permissions.fromMode(0o700)) catch {        return error.PrivateStatePermissionsUnsupported;    };    const stat = try dir.stat(io_mod.getIo());    if (stat.kind != .directory) return error.DurablePathUnsafe;    if (stat.permissions.toMode() & 0o777 != 0o700) {        return error.PrivateStatePermissionsUnsupported;    }    return .{ .dir = dir };} fn deleteAuthFile(fx_dir: *std.Io.Dir, ops: io_mod.DurableOps) !DeleteOutcome {    fx_dir.deleteFile(io_mod.getIo(), auth_file_name) catch |err| switch (err) {        error.FileNotFound => return .missing,        else => return err,    };    ops.sync_dir(ops.ctx, fx_dir.*) catch return .deleted_not_durable;    return .deleted;} pub fn parse(alloc: Allocator, bytes: []const u8) !Session {    var parsed = try std.json.parseFromSlice(std.json.Value, alloc, bytes, .{});    defer parsed.deinit();    if (parsed.value != .object) return error.InvalidAuthSession;    const object = parsed.value.object;    const version = object.get("version") orelse return error.InvalidAuthSession;    if (version != .integer or version.integer != schema_version) return error.InvalidAuthSession;    const saved_issuer = try requiredString(object, "issuer");    if (!std.mem.eql(u8, saved_issuer, issuer) and !isLoopbackE2EIssuer(saved_issuer)) {        return error.InvalidAuthSession;    }     const expires_at_ms = try requiredInteger(object, "expires_at_ms");    const owned_issuer = try alloc.dupe(u8, saved_issuer);    errdefer alloc.free(owned_issuer);    const client_id = try dupeRequiredString(alloc, object, "client_id");    errdefer alloc.free(client_id);    const access_token = try dupeRequiredString(alloc, object, "access_token");    errdefer secret.zeroAndFree(alloc, access_token);    const refresh_token = try dupeRequiredString(alloc, object, "refresh_token");    errdefer secret.zeroAndFree(alloc, refresh_token);    const scope = try dupeRequiredString(alloc, object, "scope");    errdefer alloc.free(scope);    const token_type = try dupeRequiredString(alloc, object, "token_type");    errdefer alloc.free(token_type);    const team_slug = try dupeOptionalString(alloc, object, "team_slug");    errdefer if (team_slug) |value| alloc.free(value);    const team_id = try dupeOptionalString(alloc, object, "team_id");    errdefer if (team_id) |value| alloc.free(value);     return .{        .issuer = owned_issuer,        .client_id = client_id,        .access_token = access_token,        .refresh_token = refresh_token,        .expires_at_ms = expires_at_ms,        .scope = scope,        .token_type = token_type,        .team_slug = team_slug,        .team_id = team_id,    };} pub fn stringify(alloc: Allocator, session: Session) ![]u8 {    var out: std.Io.Writer.Allocating = .init(alloc);    errdefer out.deinit();    const writer = &out.writer;    try writer.writeAll("{\"version\":1");    try writeField(writer, "issuer", session.issuer);    try writeField(writer, "client_id", session.client_id);    try writeField(writer, "access_token", session.access_token);    try writeField(writer, "refresh_token", session.refresh_token);    try writer.print(",\"expires_at_ms\":{d}", .{session.expires_at_ms});    try writeField(writer, "scope", session.scope);    try writeField(writer, "token_type", session.token_type);    if (session.team_slug) |value| try writeField(writer, "team_slug", value);    if (session.team_id) |value| try writeField(writer, "team_id", value);    try writer.writeAll("}\n");    return out.toOwnedSlice();} fn writeField(writer: *std.Io.Writer, name: []const u8, value: []const u8) !void {    try writer.writeAll(",");    try std.json.Stringify.value(name, .{}, writer);    try writer.writeAll(":");    try std.json.Stringify.value(value, .{}, writer);} fn dupeRequiredString(alloc: Allocator, object: std.json.ObjectMap, key: []const u8) ![]u8 {    return alloc.dupe(u8, try requiredString(object, key));} fn requiredString(object: std.json.ObjectMap, key: []const u8) ![]const u8 {    const value = object.get(key) orelse return error.InvalidAuthSession;    if (value != .string or value.string.len == 0) return error.InvalidAuthSession;    return value.string;} fn dupeOptionalString(alloc: Allocator, object: std.json.ObjectMap, key: []const u8) !?[]u8 {    const value = object.get(key) orelse return null;    if (value == .null) return null;    if (value != .string or value.string.len == 0) return error.InvalidAuthSession;    return try alloc.dupe(u8, value.string);} fn requiredInteger(object: std.json.ObjectMap, key: []const u8) !i64 {    const value = object.get(key) orelse return error.InvalidAuthSession;    if (value != .integer) return error.InvalidAuthSession;    return value.integer;} const test_session_json = "{\"version\":1,\"issuer\":\"https://vercel.com\",\"client_id\":\"client\",\"access_token\":\"access\",\"refresh_token\":\"refresh\",\"expires_at_ms\":1,\"scope\":\"openid offline_access\",\"token_type\":\"Bearer\",\"team_slug\":\"team-slug\",\"team_id\":\"team-id\"}"; const HostStoreTestState = struct {    record: ?[]const u8 = test_session_json,    revision: []const u8 = "7",    next_revision: []const u8 = "8",    force_conflict: bool = false,    commit_count: usize = 0,    remove_count: usize = 0,    expected_revision_matched: bool = false,    committed_format_matched: bool = false,     fn provider(self: *@This()) js_host_auth.SessionStore {        return .{            .context = self,            .load_fn = HostStoreTestState.load,            .commit_fn = HostStoreTestState.commit,

Lines 1–500 of 790.

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t