193 lines · 7.4 KB · Swift
RawDownload
import Foundation #if os(macOS)import os.lockimport SweetCookieKit enum BrowserCookieStoreAccessDecision: Equatable {    case allowed    case suppressed} struct BrowserCookieStoreAccessSuppressedError: LocalizedError {    var errorDescription: String? {        "Browser cookie store access is suppressed for this process."    }} public enum BrowserCookieAccessGate {    private struct State {        var loaded = false        var deniedUntilByBrowser: [String: Date] = [:]    }     private static let lock = OSAllocatedUnfairLock<State>(initialState: State())    private static let defaultsKey = "browserCookieAccessDeniedUntil"    private static let cooldownInterval: TimeInterval = 60 * 60 * 6    private static let log = CodexBarLog.logger(LogCategories.browserCookieGate)     static let allowTestCookieAccessEnvironmentKey = "CODEXBAR_ALLOW_TEST_BROWSER_COOKIE_ACCESS"     static func cookieStoreAccessDecision(        homeDirectories: [URL],        processName: String = ProcessInfo.processInfo.processName,        environment: [String: String] = ProcessInfo.processInfo.environment) -> BrowserCookieStoreAccessDecision    {        guard self.isRunningUnderTests(processName: processName, environment: environment),              environment[self.allowTestCookieAccessEnvironmentKey] != "1"        else {            return .allowed        }         let defaultHomes = Set(BrowserCookieClient.defaultHomeDirectories().map(Self.normalizedPath))        let usesDefaultHome = homeDirectories.contains { defaultHomes.contains(Self.normalizedPath($0)) }        return usesDefaultHome ? .suppressed : .allowed    }     public static func shouldAttempt(_ browser: Browser, now: Date = Date()) -> Bool {        guard browser.usesKeychainForCookieDecryption else { return true }        guard !KeychainAccessGate.isDisabled else { return false }        let shouldCheckKeychain = self.lock.withLock { state in            self.loadIfNeeded(&state)            if let blockedUntil = state.deniedUntilByBrowser[browser.rawValue] {                if blockedUntil > now {                    self.log.debug(                        "Cookie access blocked",                        metadata: ["browser": browser.displayName, "until": "\(blockedUntil.timeIntervalSince1970)"])                    return false                }                state.deniedUntilByBrowser.removeValue(forKey: browser.rawValue)                self.persist(state)            }            return true        }        guard shouldCheckKeychain else { return false }         let requiresInteraction = self.chromiumKeychainRequiresInteraction()        return self.lock.withLock { state in            self.loadIfNeeded(&state)            if requiresInteraction {                state.deniedUntilByBrowser[browser.rawValue] = now.addingTimeInterval(self.cooldownInterval)                self.persist(state)                self.log.info(                    "Cookie access requires keychain interaction; suppressing",                    metadata: ["browser": browser.displayName])                return false            }            self.log.debug("Cookie access allowed", metadata: ["browser": browser.displayName])            return true        }    }     public static func recordIfNeeded(_ error: Error, now: Date = Date()) {        guard let error = error as? BrowserCookieError else { return }        guard case .accessDenied = error else { return }        self.recordDenied(for: error.browser, now: now)    }     public static func recordDenied(for browser: Browser, now: Date = Date()) {        guard browser.usesKeychainForCookieDecryption else { return }        let blockedUntil = now.addingTimeInterval(self.cooldownInterval)        self.lock.withLock { state in            self.loadIfNeeded(&state)            state.deniedUntilByBrowser[browser.rawValue] = blockedUntil            self.persist(state)        }        self.log            .info(                "Browser cookie access denied; suppressing prompts",                metadata: [                    "browser": browser.displayName,                    "until": "\(blockedUntil.timeIntervalSince1970)",                ])    }     public static func resetForTesting() {        self.lock.withLock { state in            state.loaded = true            state.deniedUntilByBrowser.removeAll()            UserDefaults.standard.removeObject(forKey: self.defaultsKey)        }    }     private static func chromiumKeychainRequiresInteraction() -> Bool {        for label in self.safeStorageLabels {            switch KeychainAccessPreflight.checkGenericPassword(service: label.service, account: label.account) {            case .allowed:                return false            case .interactionRequired:                return true            case .notFound, .failure:                continue            }        }        return false    }     private static let safeStorageLabels: [(service: String, account: String)] = Browser.safeStorageLabels     private static func isRunningUnderTests(        processName: String,        environment: [String: String]) -> Bool    {        processName == "swiftpm-testing-helper"            || processName.hasSuffix("PackageTests")            || environment["XCTestConfigurationFilePath"] != nil            || environment["SWIFT_TESTING"] != nil    }     private static func normalizedPath(_ url: URL) -> String {        url.standardizedFileURL.resolvingSymlinksInPath().path    }     private static func loadIfNeeded(_ state: inout State) {        guard !state.loaded else { return }        state.loaded = true        guard let raw = UserDefaults.standard.dictionary(forKey: self.defaultsKey) as? [String: Double] else {            return        }        state.deniedUntilByBrowser = raw.compactMapValues { Date(timeIntervalSince1970: $0) }    }     private static func persist(_ state: State) {        let raw = state.deniedUntilByBrowser.mapValues { $0.timeIntervalSince1970 }        UserDefaults.standard.set(raw, forKey: self.defaultsKey)    }} extension BrowserCookieClient {    public func codexBarStores(for browser: Browser) throws -> [BrowserCookieStore] {        guard BrowserCookieAccessGate.cookieStoreAccessDecision(            homeDirectories: self.configuration.homeDirectories) == .allowed        else {            throw BrowserCookieStoreAccessSuppressedError()        }        guard BrowserCookieAccessGate.shouldAttempt(browser) else { return [] }        return self.stores(for: browser)    }     public func codexBarRecords(        matching query: BrowserCookieQuery,        in browser: Browser,        logger: ((String) -> Void)? = nil) throws -> [BrowserCookieStoreRecords]    {        guard BrowserCookieAccessGate.cookieStoreAccessDecision(            homeDirectories: self.configuration.homeDirectories) == .allowed        else {            throw BrowserCookieStoreAccessSuppressedError()        }        guard BrowserCookieAccessGate.shouldAttempt(browser) else { return [] }        return try self.records(matching: query, in: browser, logger: logger)    }}#elsepublic enum BrowserCookieAccessGate {    public static func shouldAttempt(_ browser: Browser, now: Date = Date()) -> Bool {        true    }     public static func recordIfNeeded(_ error: Error, now: Date = Date()) {}    public static func recordDenied(for browser: Browser, now: Date = Date()) {}    public static func resetForTesting() {}}#endif