265 lines · 10.2 KB · Swift
RawDownload
import Foundation#if canImport(FoundationNetworking)import FoundationNetworking#endif enum MiMoCookieHeader {    static let requiredCookieNames: Set<String> = [        "api-platform_serviceToken",        "userId",    ]    static let knownCookieNames: Set<String> = requiredCookieNames.union([        "api-platform_ph",        "api-platform_slh",    ])     static func normalizedHeader(from raw: String?) -> String? {        guard let normalized = CookieHeaderNormalizer.normalize(raw) else { return nil }        let pairs = CookieHeaderNormalizer.pairs(from: normalized)        guard !pairs.isEmpty else { return nil }         var byName: [String: String] = [:]        for pair in pairs {            let name = pair.name.trimmingCharacters(in: .whitespacesAndNewlines)            let value = pair.value.trimmingCharacters(in: .whitespacesAndNewlines)            guard self.knownCookieNames.contains(name), !value.isEmpty else { continue }            byName[name] = value        }         guard self.requiredCookieNames.isSubset(of: Set(byName.keys)) else { return nil }        return byName.keys.sorted().compactMap { name in            guard let value = byName[name] else { return nil }            return "\(name)=\(value)"        }.joined(separator: "; ")    }     static func header(from cookies: [HTTPCookie]) -> String? {        let requestURL = URL(string: "https://platform.xiaomimimo.com/api/v1/balance")!        var byName: [String: HTTPCookie] = [:]        for cookie in cookies {            guard self.knownCookieNames.contains(cookie.name) else { continue }            guard !cookie.value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { continue }            if let expiry = cookie.expiresDate, expiry < Date() { continue }            guard Self.matchesRequestURL(cookie: cookie, url: requestURL) else { continue }             if let existing = byName[cookie.name] {                if Self.cookieSortKey(for: cookie) >= Self.cookieSortKey(for: existing) {                    byName[cookie.name] = cookie                }            } else {                byName[cookie.name] = cookie            }        }         guard self.requiredCookieNames.isSubset(of: Set(byName.keys)) else { return nil }        return byName.keys.sorted().compactMap { name in            guard let cookie = byName[name] else { return nil }            return "\(cookie.name)=\(cookie.value)"        }.joined(separator: "; ")    }     private static func matchesRequestURL(cookie: HTTPCookie, url: URL) -> Bool {        guard let host = url.host else { return false }        let normalizedDomain = cookie.domain.lowercased().trimmingCharacters(in: CharacterSet(charactersIn: "."))        guard !normalizedDomain.isEmpty else { return false }        guard host == normalizedDomain || host.hasSuffix(".\(normalizedDomain)") else { return false }         let cookiePath = cookie.path.isEmpty ? "/" : cookie.path        let requestPath = url.path.isEmpty ? "/" : url.path        if requestPath == cookiePath {            return true        }        guard requestPath.hasPrefix(cookiePath) else { return false }        guard cookiePath != "/" else { return true }        if cookiePath.hasSuffix("/") {            return true        }        guard            let boundaryIndex = requestPath.index(                cookiePath.startIndex,                offsetBy: cookiePath.count,                limitedBy: requestPath.endIndex),            boundaryIndex < requestPath.endIndex        else {            return true        }        return requestPath[boundaryIndex] == "/"    }     private static func cookieSortKey(for cookie: HTTPCookie) -> (Int, Int, Date) {        let pathLength = cookie.path.count        let normalizedDomain = cookie.domain.lowercased().trimmingCharacters(in: CharacterSet(charactersIn: "."))        let domainLength = normalizedDomain.count        let expiry = cookie.expiresDate ?? .distantPast        return (pathLength, domainLength, expiry)    }} #if os(macOS)import SweetCookieKit private let miMoCookieImportOrder: BrowserCookieImportOrder =    ProviderDefaults.metadata[.mimo]?.browserCookieOrder ?? Browser.defaultImportOrder public enum MiMoCookieImporter {    private static let cookieClient = BrowserCookieClient()    private static let cookieDomains = [        "platform.xiaomimimo.com",        "xiaomimimo.com",    ]     public struct SessionInfo: Sendable {        public let cookieHeader: String        public let sourceLabel: String         public init(cookieHeader: String, sourceLabel: String) {            self.cookieHeader = cookieHeader            self.sourceLabel = sourceLabel        }    }     nonisolated(unsafe) static var importSessionsOverrideForTesting:        ((BrowserDetection, ((String) -> Void)?) throws -> [SessionInfo])?     public static func importSessions(        browserDetection: BrowserDetection,        logger: ((String) -> Void)? = nil) throws -> [SessionInfo]    {        if let override = self.importSessionsOverrideForTesting {            return try override(browserDetection, logger)        }         return try self.importSessions(            browserDetection: browserDetection,            logger: logger,            loadRecords: { browserSource, query, log in                try Self.cookieClient.codexBarRecords(                    matching: query,                    in: browserSource,                    logger: log)            })    }     static func importSessions(        browserDetection: BrowserDetection,        logger: ((String) -> Void)? = nil,        loadRecords: (Browser, BrowserCookieQuery, ((String) -> Void)?) throws            -> [BrowserCookieStoreRecords]) throws -> [SessionInfo]    {        let log: (String) -> Void = { msg in logger?("[mimo-cookie] \(msg)") }        var sessions: [SessionInfo] = []        var accessDeniedHints: [String] = []        let installed = miMoCookieImportOrder.cookieImportCandidates(using: browserDetection)        let labels = installed.map(\.displayName).joined(separator: ", ")        log("Cookie import candidates: \(labels)")         for browserSource in installed {            do {                let query = BrowserCookieQuery(domains: self.cookieDomains)                let sources = try loadRecords(browserSource, query, log)                sessions.append(contentsOf: self.sessionInfos(from: sources, origin: query.origin))            } catch let error as BrowserCookieError {                BrowserCookieAccessGate.recordIfNeeded(error)                if let hint = error.accessDeniedHint {                    accessDeniedHints.append(hint)                }                log("\(browserSource.displayName) cookie import failed: \(error.localizedDescription)")            } catch {                BrowserCookieAccessGate.recordIfNeeded(error)                log("\(browserSource.displayName) cookie import failed: \(error.localizedDescription)")            }        }         if sessions.isEmpty, !accessDeniedHints.isEmpty {            let details = Array(Set(accessDeniedHints)).sorted().joined(separator: " ")            throw MiMoSettingsError.missingCookie(details: details)        }        return sessions    }     public static func hasSession(        browserDetection: BrowserDetection,        logger: ((String) -> Void)? = nil) -> Bool    {        (try? self.importSessions(browserDetection: browserDetection, logger: logger).isEmpty == false) ?? false    }     static func sessionInfos(        from sources: [BrowserCookieStoreRecords],        origin: BrowserCookieOriginStrategy = .domainBased) -> [SessionInfo]    {        let grouped = Dictionary(grouping: sources, by: { $0.store.profile.id })        let sortedGroups = grouped.values.sorted { lhs, rhs in            self.mergedLabel(for: lhs) < self.mergedLabel(for: rhs)        }         var sessions: [SessionInfo] = []        for group in sortedGroups where !group.isEmpty {            let label = self.mergedLabel(for: group)            let mergedRecords = self.mergeRecords(group)            guard !mergedRecords.isEmpty else { continue }            let cookies = BrowserCookieClient.makeHTTPCookies(mergedRecords, origin: origin)            guard let cookieHeader = MiMoCookieHeader.header(from: cookies) else {                continue            }            sessions.append(SessionInfo(cookieHeader: cookieHeader, sourceLabel: label))        }        return sessions    }     private static func mergedLabel(for sources: [BrowserCookieStoreRecords]) -> String {        guard let base = sources.map(\.label).min() else {            return "Unknown"        }        if base.hasSuffix(" (Network)") {            return String(base.dropLast(" (Network)".count))        }        return base    }     private static func mergeRecords(_ sources: [BrowserCookieStoreRecords]) -> [BrowserCookieRecord] {        let sortedSources = sources.sorted { lhs, rhs in            self.storePriority(lhs.store.kind) < self.storePriority(rhs.store.kind)        }        var mergedByKey: [String: BrowserCookieRecord] = [:]        for source in sortedSources {            for record in source.records {                let key = self.recordKey(record)                if let existing = mergedByKey[key] {                    if self.shouldReplace(existing: existing, candidate: record) {                        mergedByKey[key] = record                    }                } else {                    mergedByKey[key] = record                }            }        }        return Array(mergedByKey.values)    }     private static func storePriority(_ kind: BrowserCookieStoreKind) -> Int {        switch kind {        case .network: 0        case .primary: 1        case .safari: 2        }    }     private static func recordKey(_ record: BrowserCookieRecord) -> String {        "\(record.name)|\(record.domain)|\(record.path)"    }     private static func shouldReplace(existing: BrowserCookieRecord, candidate: BrowserCookieRecord) -> Bool {        switch (existing.expires, candidate.expires) {        case let (lhs?, rhs?):            rhs > lhs        case (nil, .some):            true        case (.some, nil):            false        case (nil, nil):            false        }    }}#endif