Files
2026-07-13 12:22:33 +08:00

998 lines
39 KiB
Swift

import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
public enum AlibabaTokenPlanUsageError: LocalizedError, Sendable, Equatable {
case loginRequired
case invalidCredentials
case apiError(String)
case networkError(String)
case parseFailed(String)
public var errorDescription: String? {
switch self {
case .loginRequired:
"Alibaba Token Plan login required."
case .invalidCredentials:
"Alibaba Token Plan credentials are invalid."
case let .apiError(message):
"Alibaba Token Plan API error: \(message)"
case let .networkError(message):
"Alibaba Token Plan network error: \(message)"
case let .parseFailed(message):
"Could not parse Alibaba Token Plan usage: \(message)"
}
}
}
// swiftlint:disable:next type_body_length
public struct AlibabaTokenPlanUsageFetcher: Sendable {
private static let log = CodexBarLog.logger("alibaba-token-plan")
private static let bssServiceCode = "BssOpenAPI-V3"
private static let subscriptionSummaryAction = "GetSubscriptionSummary"
private static let browserLikeUserAgent =
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) " +
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36"
private static let safariLikeUserAgent =
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) " +
"AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.3 Safari/605.1.15"
public static var dashboardURL: URL {
Self.dashboardURL(region: .international, environment: ProcessInfo.processInfo.environment)
}
public static func dashboardURL(
region: AlibabaTokenPlanAPIRegion,
environment: [String: String] = ProcessInfo.processInfo.environment) -> URL
{
if let host = AlibabaTokenPlanSettingsReader.hostOverride(environment: environment),
let base = ProviderEndpointOverrideValidator.normalizedHTTPSURL(from: host),
var components = URLComponents(url: base, resolvingAgainstBaseURL: false),
let dashboardComponents = URLComponents(url: region.dashboardURL, resolvingAgainstBaseURL: false)
{
components.path = dashboardComponents.path
components.percentEncodedQuery = dashboardComponents.percentEncodedQuery
components.fragment = dashboardComponents.fragment
return components.url ?? region.dashboardURL
}
return region.dashboardURL
}
public static func fetchUsage(
cookieHeader: String,
region: AlibabaTokenPlanAPIRegion = .international,
environment: [String: String] = ProcessInfo.processInfo.environment,
now: Date = Date()) async throws -> AlibabaTokenPlanUsageSnapshot
{
guard let headers = AlibabaTokenPlanCookieHeaders(singleHeader: cookieHeader) else {
throw AlibabaTokenPlanSettingsError.invalidCookie
}
return try await self.fetchUsage(
apiCookieHeader: headers.apiCookieHeader,
dashboardCookieHeader: headers.dashboardCookieHeader,
region: region,
environment: environment,
now: now)
}
static func fetchUsage(
apiCookieHeader: String,
dashboardCookieHeader: String,
region: AlibabaTokenPlanAPIRegion = .international,
environment: [String: String] = ProcessInfo.processInfo.environment,
now: Date = Date(),
session overrideSession: URLSession? = nil) async throws -> AlibabaTokenPlanUsageSnapshot
{
guard let normalizedAPIHeader = CookieHeaderNormalizer.normalize(apiCookieHeader),
let normalizedDashboardHeader = CookieHeaderNormalizer.normalize(dashboardCookieHeader)
else {
throw AlibabaTokenPlanSettingsError.invalidCookie
}
let url = self.resolveQuotaURL(region: region, environment: environment)
let apiRedirectDiagnostics = RedirectDiagnostics(cookieHeader: normalizedAPIHeader)
let dashboardRedirectDiagnostics: RedirectDiagnostics?
let apiSession: URLSession
let dashboardSession: URLSession
if let overrideSession {
apiSession = overrideSession
dashboardSession = overrideSession
dashboardRedirectDiagnostics = nil
} else {
let dashboardDiagnostics = RedirectDiagnostics(cookieHeader: normalizedDashboardHeader)
apiSession = URLSession(
configuration: .default,
delegate: apiRedirectDiagnostics,
delegateQueue: nil)
dashboardSession = URLSession(
configuration: .default,
delegate: dashboardDiagnostics,
delegateQueue: nil)
dashboardRedirectDiagnostics = dashboardDiagnostics
}
defer {
if overrideSession == nil {
apiSession.invalidateAndCancel()
dashboardSession.invalidateAndCancel()
}
}
let secToken = await self.resolveSECToken(
dashboardCookieHeader: normalizedDashboardHeader,
apiCookieHeader: normalizedAPIHeader,
region: region,
environment: environment,
session: dashboardSession)
Self.log.info(
"Fetching Alibaba Token Plan usage",
metadata: [
"apiHost": url.host ?? "unknown",
"region": region.rawValue,
"apiCookieNames": self.cookieNamesDescription(from: normalizedAPIHeader),
"dashboardCookieNames": self.cookieNamesDescription(from: normalizedDashboardHeader),
"hasCSRF": self.hasCSRF(in: normalizedAPIHeader) ? "1" : "0",
"secTokenSource": secToken == nil ? "missing" : "resolved",
])
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.timeoutInterval = 20
request.httpBody = self.subscriptionSummaryRequestBody(region: region, secToken: secToken)
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
request.setValue("*/*", forHTTPHeaderField: "Accept")
request.setValue(normalizedAPIHeader, forHTTPHeaderField: "Cookie")
if let csrf = self.extractCookieValue(name: "login_aliyunid_csrf", from: normalizedAPIHeader) ??
self.extractCookieValue(name: "csrf", from: normalizedAPIHeader)
{
request.setValue(csrf, forHTTPHeaderField: "x-xsrf-token")
request.setValue(csrf, forHTTPHeaderField: "x-csrf-token")
}
request.setValue("XMLHttpRequest", forHTTPHeaderField: "X-Requested-With")
request.setValue(Self.browserLikeUserAgent, forHTTPHeaderField: "User-Agent")
request.setValue(region.dashboardOriginURLString, forHTTPHeaderField: "Origin")
request.setValue(
Self.dashboardURL(region: region, environment: environment).absoluteString,
forHTTPHeaderField: "Referer")
let data: Data
let response: URLResponse
do {
(data, response) = try await apiSession.data(for: request)
} catch {
Self.log.error(
"Alibaba Token Plan request failed",
metadata: [
"apiHost": url.host ?? "unknown",
"error": error.localizedDescription,
])
throw AlibabaTokenPlanUsageError.networkError(error.localizedDescription)
}
if let dashboardRedirectDiagnostics, !dashboardRedirectDiagnostics.redirects.isEmpty {
Self.log.info(
"Alibaba Token Plan dashboard redirects",
metadata: [
"count": "\(dashboardRedirectDiagnostics.redirects.count)",
"items": dashboardRedirectDiagnostics.redirects.joined(separator: " | "),
])
}
if !apiRedirectDiagnostics.redirects.isEmpty {
Self.log.info(
"Alibaba Token Plan redirects",
metadata: [
"count": "\(apiRedirectDiagnostics.redirects.count)",
"items": apiRedirectDiagnostics.redirects.joined(separator: " | "),
])
}
guard let httpResponse = response as? HTTPURLResponse else {
Self.log.error("Alibaba Token Plan response was not HTTP")
throw AlibabaTokenPlanUsageError.networkError("Invalid response")
}
Self.log.info(
"Alibaba Token Plan HTTP response",
metadata: [
"status": "\(httpResponse.statusCode)",
"bodyBytes": "\(data.count)",
"contentType": httpResponse.value(forHTTPHeaderField: "Content-Type") ?? "none",
])
guard httpResponse.statusCode == 200 else {
if httpResponse.statusCode == 401 || httpResponse.statusCode == 403 {
throw AlibabaTokenPlanUsageError.loginRequired
}
Self.log.error("Alibaba Token Plan returned HTTP \(httpResponse.statusCode)")
throw AlibabaTokenPlanUsageError.apiError("HTTP \(httpResponse.statusCode)")
}
return try self.parseUsageSnapshot(from: data, now: now)
}
static func resolveQuotaURL(
region: AlibabaTokenPlanAPIRegion,
environment: [String: String]) -> URL
{
if let override = AlibabaTokenPlanSettingsReader.quotaURL(environment: environment) {
return override
}
if let host = AlibabaTokenPlanSettingsReader.hostOverride(environment: environment),
let hostURL = self.quotaURL(from: host)
{
return hostURL
}
return self.defaultQuotaURL(region: region)
}
static var defaultQuotaURL: URL {
self.defaultQuotaURL(region: .international)
}
static func defaultQuotaURL(region: AlibabaTokenPlanAPIRegion) -> URL {
var components = URLComponents(string: region.gatewayBaseURLString)!
components.path = "/data/api.json"
components.queryItems = [
URLQueryItem(name: "action", value: Self.subscriptionSummaryAction),
URLQueryItem(name: "product", value: Self.bssServiceCode),
URLQueryItem(name: "_tag", value: ""),
]
return components.url!
}
static func parseUsageSnapshot(from data: Data, now: Date = Date()) throws -> AlibabaTokenPlanUsageSnapshot {
guard !data.isEmpty else {
throw AlibabaTokenPlanUsageError.parseFailed("Empty response body")
}
let object: Any
do {
object = try JSONSerialization.jsonObject(with: data, options: [])
} catch {
if self.isLikelyLoginHTML(data) {
throw AlibabaTokenPlanUsageError.loginRequired
}
throw AlibabaTokenPlanUsageError.parseFailed("Invalid JSON response")
}
let expanded = self.expandedJSON(object)
guard let dictionary = expanded as? [String: Any] else {
throw AlibabaTokenPlanUsageError.parseFailed("Unexpected payload")
}
try self.throwIfErrorPayload(dictionary)
let summary = self.findSubscriptionSummary(in: dictionary) ?? dictionary
let total = self.anyDouble(for: Self.totalQuotaKeys, in: summary)
let remaining = self.anyDouble(for: Self.remainingQuotaKeys, in: summary)
let used = self.anyDouble(for: Self.usedQuotaKeys, in: summary) ??
total.flatMap { total in remaining.map { max(0, total - $0) } }
let resetsAt = self.findResetDate(in: summary) ?? self.findResetDate(in: dictionary)
let totalCount = self.anyDouble(for: Self.subscriptionCountKeys, in: summary)
let planName = self.findPlanName(in: summary) ?? ((totalCount ?? 0) > 0 || total != nil ? "TOKEN PLAN" : nil)
if planName == nil, total == nil, used == nil, remaining == nil, totalCount == nil {
let diagnostics = self.payloadDiagnostics(payload: dictionary)
Self.log.error("Alibaba Token Plan payload missing expected fields: \(diagnostics)")
throw AlibabaTokenPlanUsageError.parseFailed("Missing token plan data (\(diagnostics))")
}
return AlibabaTokenPlanUsageSnapshot(
planName: planName,
usedQuota: used,
totalQuota: total,
remainingQuota: remaining,
resetsAt: resetsAt,
updatedAt: now)
}
private static func subscriptionSummaryRequestBody(region: AlibabaTokenPlanAPIRegion, secToken: String?) -> Data {
let paramsObject = ["ProductCode": region.tokenPlanProductCode]
guard let paramsData = try? JSONSerialization.data(withJSONObject: paramsObject, options: []),
let paramsString = String(data: paramsData, encoding: .utf8)
else {
return Data()
}
var components = URLComponents()
var queryItems = [
URLQueryItem(name: "product", value: Self.bssServiceCode),
URLQueryItem(name: "action", value: Self.subscriptionSummaryAction),
URLQueryItem(name: "params", value: paramsString),
URLQueryItem(name: "region", value: region.currentRegionID),
]
if let secToken, !secToken.isEmpty {
queryItems.append(URLQueryItem(name: "sec_token", value: secToken))
}
components.queryItems = queryItems
return Data((components.percentEncodedQuery ?? "").utf8)
}
private static func resolveSECToken(
dashboardCookieHeader: String,
apiCookieHeader: String,
region: AlibabaTokenPlanAPIRegion,
environment: [String: String],
session: URLSession) async -> String?
{
let cookieSECToken = self.extractCookieValue(name: "sec_token", from: dashboardCookieHeader) ??
self.extractCookieValue(name: "sec_token", from: apiCookieHeader)
var request = URLRequest(url: self.dashboardURL(region: region, environment: environment))
request.httpMethod = "GET"
request.timeoutInterval = 10
request.setValue(dashboardCookieHeader, forHTTPHeaderField: "Cookie")
request.setValue(Self.safariLikeUserAgent, forHTTPHeaderField: "User-Agent")
request.setValue(
"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
forHTTPHeaderField: "Accept")
if let (data, response) = try? await session.data(for: request),
let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200,
let html = String(data: data, encoding: .utf8),
let token = self.extractSECToken(from: html)
{
Self.log.info(
"Resolved Alibaba Token Plan sec_token from dashboard HTML",
metadata: [
"dashboardHost": request.url?.host ?? "unknown",
"htmlBytes": "\(data.count)",
])
return token
}
if let token = await self.fetchSECTokenFromUserInfo(
cookieHeader: dashboardCookieHeader,
region: region,
environment: environment,
session: session)
{
return token
}
if let cookieSECToken, !cookieSECToken.isEmpty {
Self.log.info("Resolved Alibaba Token Plan sec_token from cookies")
return cookieSECToken
}
Self.log.info(
"Alibaba Token Plan sec_token missing; continuing with cookie-only request",
metadata: [
"dashboardCookieNames": self.cookieNamesDescription(from: dashboardCookieHeader),
"apiCookieNames": self.cookieNamesDescription(from: apiCookieHeader),
])
return nil
}
private static func fetchSECTokenFromUserInfo(
cookieHeader: String,
region: AlibabaTokenPlanAPIRegion,
environment: [String: String],
session: URLSession) async -> String?
{
let baseURL = self.consoleBaseURL(region: region, environment: environment)
let userInfoURL = baseURL.appendingPathComponent("tool/user/info.json")
var request = URLRequest(url: userInfoURL)
request.httpMethod = "GET"
request.timeoutInterval = 10
request.setValue(cookieHeader, forHTTPHeaderField: "Cookie")
request.setValue(Self.safariLikeUserAgent, forHTTPHeaderField: "User-Agent")
request.setValue("application/json, text/plain, */*", forHTTPHeaderField: "Accept")
let referer = baseURL.absoluteString.hasSuffix("/") ? baseURL.absoluteString : "\(baseURL.absoluteString)/"
request.setValue(referer, forHTTPHeaderField: "Referer")
guard let (data, response) = try? await session.data(for: request),
let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200,
let object = try? JSONSerialization.jsonObject(with: data, options: [])
else {
return nil
}
let expanded = self.expandedJSON(object)
guard let token = self.findFirstString(forKeys: ["secToken", "sec_token"], in: expanded),
!token.isEmpty
else {
return nil
}
Self.log.info(
"Resolved Alibaba Token Plan sec_token from user info",
metadata: [
"userInfoHost": userInfoURL.host ?? "unknown",
"bodyBytes": "\(data.count)",
])
return token
}
private static func consoleBaseURL(
region: AlibabaTokenPlanAPIRegion,
environment: [String: String]) -> URL
{
let dashboard = self.dashboardURL(region: region, environment: environment)
var components = URLComponents()
components.scheme = dashboard.scheme
components.host = dashboard.host
components.port = dashboard.port
return components.url ?? URL(string: region.dashboardOriginURLString)!
}
private static func quotaURL(from rawHost: String) -> URL? {
let cleaned = AlibabaTokenPlanSettingsReader.cleaned(rawHost)
guard let cleaned else { return nil }
guard let base = ProviderEndpointOverrideValidator.normalizedHTTPSURL(from: cleaned) else { return nil }
var components = URLComponents(url: base, resolvingAgainstBaseURL: false)
let defaultComponents = URLComponents(
url: Self.defaultQuotaURL(region: .international),
resolvingAgainstBaseURL: false)
components?.path = "/data/api.json"
components?.queryItems = defaultComponents?.queryItems
return components?.url
}
private final class RedirectDiagnostics: NSObject, URLSessionTaskDelegate, @unchecked Sendable {
private let cookieHeader: String
var redirects: [String] = []
init(cookieHeader: String) {
self.cookieHeader = cookieHeader
}
func urlSession(
_: URLSession,
task _: URLSessionTask,
willPerformHTTPRedirection response: HTTPURLResponse,
newRequest request: URLRequest,
completionHandler: @escaping (URLRequest?) -> Void)
{
let from = AlibabaTokenPlanUsageFetcher.redactedURLDescription(response.url)
let to = AlibabaTokenPlanUsageFetcher.redactedURLDescription(request.url)
self.redirects.append("\(response.statusCode) \(from) -> \(to)")
completionHandler(AlibabaTokenPlanUsageFetcher.redirectedRequest(
response: response,
request: request,
cookieHeader: self.cookieHeader))
}
}
private static func redactedURLDescription(_ url: URL?) -> String {
guard let url else { return "unknown" }
var components = URLComponents(url: url, resolvingAgainstBaseURL: false)
components?.query = nil
components?.fragment = nil
return components?.string ?? "\(url.scheme ?? "unknown")://\(url.host ?? "unknown")"
}
static func redirectedRequest(
response: HTTPURLResponse,
request: URLRequest,
cookieHeader: String) -> URLRequest?
{
guard request.url?.scheme?.lowercased() == "https" else {
return nil
}
var updated = request
if self.shouldForwardRedirectCookies(from: response.url, to: request.url) {
updated.setValue(cookieHeader, forHTTPHeaderField: "Cookie")
} else {
updated.setValue(nil, forHTTPHeaderField: "Cookie")
}
return updated
}
private static func shouldForwardRedirectCookies(from sourceURL: URL?, to targetURL: URL?) -> Bool {
guard let sourceHost = sourceURL?.host?.lowercased(),
let targetHost = targetURL?.host?.lowercased()
else {
return false
}
return sourceHost == targetHost
}
private static func throwIfErrorPayload(_ dictionary: [String: Any]) throws {
if self.parseBool(dictionary["successResponse"]) == false {
if let statusCode = self.findFirstInt(forKeys: ["statusCode", "status_code", "code"], in: dictionary),
statusCode == 401 || statusCode == 403
{
throw AlibabaTokenPlanUsageError.invalidCredentials
}
let code = self.findFirstString(forKeys: ["code", "status", "statusCode"], in: dictionary)
let message = self.findFirstString(forKeys: ["message", "msg", "statusMessage"], in: dictionary) ??
code ??
"request was not successful"
if self.isLoginOrTokenError(code: code, message: message) {
throw AlibabaTokenPlanUsageError.loginRequired
}
throw AlibabaTokenPlanUsageError.apiError(message)
}
if self.findBoolValues(forKeys: ["Success", "success"], in: dictionary).contains(false) {
let code = self.findFirstString(forKeys: ["Code", "code"], in: dictionary)
let message = self.findFirstString(forKeys: ["Message", "message", "msg", "Code", "code"], in: dictionary)
?? "request was not successful"
if self.isLoginOrTokenError(code: code, message: message) {
throw AlibabaTokenPlanUsageError.loginRequired
}
throw AlibabaTokenPlanUsageError.apiError(message)
}
if let statusCode = self.findFirstInt(forKeys: ["statusCode", "status_code", "code"], in: dictionary),
statusCode != 0,
statusCode != 200
{
let message = self.findFirstString(
forKeys: ["statusMessage", "status_msg", "message", "msg"],
in: dictionary)
?? "status code \(statusCode)"
if statusCode == 401 || statusCode == 403 {
throw AlibabaTokenPlanUsageError.invalidCredentials
}
throw AlibabaTokenPlanUsageError.apiError(message)
}
let codeText = self.findFirstString(forKeys: ["code", "status", "statusCode"], in: dictionary)?.lowercased()
let messageText = self.findFirstString(forKeys: ["message", "msg", "statusMessage"], in: dictionary)?
.lowercased()
if self.isLoginOrTokenError(code: codeText, message: messageText) {
throw AlibabaTokenPlanUsageError.loginRequired
}
}
private static func isLoginOrTokenError(code: String?, message: String?) -> Bool {
let combined = [code, message]
.compactMap { $0?.lowercased() }
.joined(separator: " ")
return combined.contains("needlogin") ||
combined.contains("login") ||
combined.contains("postonlyortokenerror") ||
combined.contains("tokenerror") ||
combined.contains("request has expired") ||
combined.contains("refresh page") ||
combined.contains("请求已经过期")
}
private static let planNameKeys = [
"planName",
"plan_name",
"packageName",
"package_name",
"commodityName",
"commodity_name",
"instanceName",
"instance_name",
"displayName",
"display_name",
"ProductName",
"productName",
"name",
"title",
"planType",
"plan_type",
]
private static let usedQuotaKeys = [
"usedQuota",
"used_quota",
"usedCredits",
"usedCredit",
"consumedCredits",
"usage",
"used",
"usedAmount",
"consumeAmount",
"usedValue",
"UsedValue",
"consumedValue",
"ConsumedValue",
]
private static let totalQuotaKeys = [
"totalQuota",
"total_quota",
"totalCredits",
"totalCredit",
"quota",
"creditLimit",
"creditsTotal",
"monthlyTotalQuota",
"amount",
"totalValue",
"TotalValue",
]
private static let remainingQuotaKeys = [
"remainingQuota",
"remainQuota",
"remainingCredits",
"remainingCredit",
"availableCredits",
"balance",
"remaining",
"availableAmount",
"remainAmount",
"totalSurplusValue",
"TotalSurplusValue",
"surplusValue",
"SurplusValue",
]
private static let subscriptionCountKeys = [
"totalCount",
"TotalCount",
"subscriptionTotalNumber",
"SubscriptionTotalNumber",
]
private static let resetDateKeys = [
"nextRefreshTime",
"resetTime",
"periodEndTime",
"billingCycleEnd",
"billCycleEndTime",
"expireTime",
"expirationTime",
"endTime",
"validEndTime",
"instanceEndTime",
"nearestExpireDate",
"NearestExpireDate",
]
private static func findSubscriptionSummary(in payload: [String: Any]) -> [String: Any]? {
if let data = self.findFirstDictionary(
forKeys: ["Data", "data", "successResponse", "success_response"],
in: payload),
self.containsSubscriptionSummaryFields(data)
{
return data
}
return self.findFirstDictionary(
matchingAnyKey: Self.usedQuotaKeys + Self.totalQuotaKeys + Self.remainingQuotaKeys +
Self.subscriptionCountKeys,
in: payload)
}
private static func containsSubscriptionSummaryFields(_ payload: [String: Any]) -> Bool {
let keys = self.usedQuotaKeys + self.totalQuotaKeys + self.remainingQuotaKeys + self.subscriptionCountKeys
return keys.contains { payload[$0] != nil }
}
private static func findPlanName(in payload: [String: Any]) -> String? {
self.anyString(for: self.planNameKeys, in: payload) ??
self.findFirstString(forKeys: self.planNameKeys, in: payload)
}
private static func findResetDate(in payload: [String: Any]) -> Date? {
self.anyDate(for: self.resetDateKeys, in: payload) ??
self.findFirstDate(forKeys: self.resetDateKeys, in: payload)
}
private static func payloadDiagnostics(payload: [String: Any]) -> String {
let topKeys = payload.keys.sorted()
let dataDict = self.findFirstDictionary(
forKeys: ["Data", "data", "successResponse", "success_response"],
in: payload)
let dataKeys = dataDict?.keys.sorted() ?? []
return "topKeys=\(topKeys.joined(separator: ",")) dataKeys=\(dataKeys.joined(separator: ","))"
}
private static func isLikelyLoginHTML(_ data: Data) -> Bool {
guard let text = String(data: data, encoding: .utf8)?.lowercased() else { return false }
return text.contains("<html") &&
(text.contains("login") || text.contains("sign in") || text.contains("signin"))
}
private static func findFirstDictionary(forKeys keys: [String], in value: Any) -> [String: Any]? {
if let dict = value as? [String: Any] {
for key in keys {
if let nested = dict[key] as? [String: Any] {
return nested
}
}
for nestedValue in dict.values {
if let nested = self.findFirstDictionary(forKeys: keys, in: nestedValue) {
return nested
}
}
return nil
}
if let array = value as? [Any] {
for item in array {
if let nested = self.findFirstDictionary(forKeys: keys, in: item) {
return nested
}
}
}
return nil
}
private static func findFirstDictionary(matchingAnyKey keys: [String], in value: Any) -> [String: Any]? {
if let dict = value as? [String: Any] {
if keys.contains(where: { dict[$0] != nil }) {
return dict
}
for nestedValue in dict.values {
if let nested = self.findFirstDictionary(matchingAnyKey: keys, in: nestedValue) {
return nested
}
}
return nil
}
if let array = value as? [Any] {
for item in array {
if let nested = self.findFirstDictionary(matchingAnyKey: keys, in: item) {
return nested
}
}
}
return nil
}
private static func findFirstString(forKeys keys: [String], in value: Any) -> String? {
if let dict = value as? [String: Any] {
for key in keys {
if let parsed = self.parseString(dict[key]) {
return parsed
}
}
for nestedValue in dict.values {
if let parsed = self.findFirstString(forKeys: keys, in: nestedValue) {
return parsed
}
}
return nil
}
if let array = value as? [Any] {
for item in array {
if let parsed = self.findFirstString(forKeys: keys, in: item) {
return parsed
}
}
}
return nil
}
private static func findBoolValues(forKeys keys: [String], in value: Any) -> [Bool] {
if let dict = value as? [String: Any] {
let directValues = keys.compactMap { self.parseBool(dict[$0]) }
let nestedValues = dict.values.flatMap { self.findBoolValues(forKeys: keys, in: $0) }
return directValues + nestedValues
}
if let array = value as? [Any] {
return array.flatMap { self.findBoolValues(forKeys: keys, in: $0) }
}
return []
}
private static func findFirstInt(forKeys keys: [String], in value: Any) -> Int? {
if let dict = value as? [String: Any] {
for key in keys {
if let parsed = self.parseInt(dict[key]) {
return parsed
}
}
for nestedValue in dict.values {
if let parsed = self.findFirstInt(forKeys: keys, in: nestedValue) {
return parsed
}
}
return nil
}
if let array = value as? [Any] {
for item in array {
if let parsed = self.findFirstInt(forKeys: keys, in: item) {
return parsed
}
}
}
return nil
}
private static func findFirstDate(forKeys keys: [String], in value: Any) -> Date? {
if let dict = value as? [String: Any] {
for key in keys {
if let parsed = self.parseDate(dict[key]) {
return parsed
}
}
for nestedValue in dict.values {
if let parsed = self.findFirstDate(forKeys: keys, in: nestedValue) {
return parsed
}
}
return nil
}
if let array = value as? [Any] {
for item in array {
if let parsed = self.findFirstDate(forKeys: keys, in: item) {
return parsed
}
}
}
return nil
}
private static func expandedJSON(_ value: Any) -> Any {
if let dict = value as? [String: Any] {
var expanded: [String: Any] = [:]
expanded.reserveCapacity(dict.count)
for (key, nested) in dict {
expanded[key] = self.expandedJSON(nested)
}
return expanded
}
if let array = value as? [Any] {
return array.map { self.expandedJSON($0) }
}
if let string = value as? String,
let data = string.data(using: .utf8),
let nested = try? JSONSerialization.jsonObject(with: data, options: []),
nested is [String: Any] || nested is [Any]
{
return self.expandedJSON(nested)
}
return value
}
private static func anyString(for keys: [String], in dict: [String: Any]) -> String? {
for key in keys {
if let value = self.parseString(dict[key]) {
return value
}
}
return nil
}
private static func anyDouble(for keys: [String], in dict: [String: Any]) -> Double? {
for key in keys {
if let value = self.parseDouble(dict[key]) {
return value
}
}
return nil
}
private static func anyDate(for keys: [String], in dict: [String: Any]) -> Date? {
for key in keys {
if let value = self.parseDate(dict[key]) {
return value
}
}
return nil
}
private static func anyBool(for keys: [String], in dict: [String: Any]) -> Bool? {
for key in keys {
if let value = self.parseBool(dict[key]) {
return value
}
}
return nil
}
private static func parseInt(_ raw: Any?) -> Int? {
if let value = raw as? Int { return value }
if let value = raw as? Int64 { return Int(value) }
if let value = raw as? Double { return Int(value) }
if let value = raw as? NSNumber { return value.intValue }
if let value = self.parseString(raw) { return Int(value) }
return nil
}
private static func parseDouble(_ raw: Any?) -> Double? {
if let value = raw as? Double { return value }
if let value = raw as? Int { return Double(value) }
if let value = raw as? Int64 { return Double(value) }
if let value = raw as? NSNumber { return value.doubleValue }
if let value = self.parseString(raw) {
let cleaned = value.replacingOccurrences(of: ",", with: "")
return Double(cleaned)
}
return nil
}
private static func parseString(_ raw: Any?) -> String? {
guard let value = raw as? String else { return nil }
let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
return trimmed.isEmpty ? nil : trimmed
}
private static func parseDate(_ raw: Any?) -> Date? {
if let intValue = self.parseInt(raw) {
if intValue > 1_000_000_000_000 {
return Date(timeIntervalSince1970: TimeInterval(intValue) / 1000)
}
if intValue > 1_000_000_000 {
return Date(timeIntervalSince1970: TimeInterval(intValue))
}
}
if let string = self.parseString(raw) {
let formatter = ISO8601DateFormatter()
if let date = formatter.date(from: string) {
return date
}
let dateFormatter = DateFormatter()
dateFormatter.locale = Locale(identifier: "en_US_POSIX")
for format in ["yyyy-MM-dd", "yyyy-MM-dd HH:mm", "yyyy-MM-dd HH:mm:ss"] {
dateFormatter.dateFormat = format
if let date = dateFormatter.date(from: string) {
return date
}
}
}
return nil
}
private static func parseBool(_ raw: Any?) -> Bool? {
if let value = raw as? Bool { return value }
if let number = raw as? NSNumber { return number.boolValue }
guard let string = self.parseString(raw)?.lowercased() else { return nil }
switch string {
case "true", "1", "yes", "active", "valid", "normal":
return true
case "false", "0", "no", "inactive", "invalid", "expired":
return false
default:
return nil
}
}
private static func extractCookieValue(name: String, from cookieHeader: String) -> String? {
cookieHeader
.split(separator: ";")
.compactMap { part -> (String, String)? in
let pieces = part.split(separator: "=", maxSplits: 1).map {
$0.trimmingCharacters(in: .whitespacesAndNewlines)
}
guard pieces.count == 2 else { return nil }
return (pieces[0], pieces[1])
}
.first { $0.0 == name }?
.1
}
private static func hasCSRF(in cookieHeader: String) -> Bool {
self.extractCookieValue(name: "login_aliyunid_csrf", from: cookieHeader) != nil ||
self.extractCookieValue(name: "csrf", from: cookieHeader) != nil
}
static func cookieNames(from cookieHeader: String) -> [String] {
CookieHeaderNormalizer.pairs(from: cookieHeader)
.map(\.name)
.filter { !$0.isEmpty }
.uniquedSorted()
}
static func cookieNamesDescription(from cookieHeader: String) -> String {
let names = self.cookieNames(from: cookieHeader)
return names.isEmpty ? "none" : names.joined(separator: ",")
}
private static func extractSECToken(from html: String) -> String? {
let patterns = [
#""secToken"\s*:\s*"([^"]+)""#,
#""sec_token"\s*:\s*"([^"]+)""#,
#"secToken['"]?\s*[:=]\s*['"]([^'"]+)['"]"#,
#"sec_token['"]?\s*[:=]\s*['"]([^'"]+)['"]"#,
]
for pattern in patterns {
if let token = self.matchFirstGroup(pattern: pattern, in: html), !token.isEmpty {
return token
}
}
return nil
}
private static func matchFirstGroup(pattern: String, in text: String) -> String? {
guard let regex = try? NSRegularExpression(pattern: pattern, options: []) else {
return nil
}
let range = NSRange(text.startIndex..<text.endIndex, in: text)
guard let match = regex.firstMatch(in: text, options: [], range: range),
match.numberOfRanges > 1,
let valueRange = Range(match.range(at: 1), in: text)
else {
return nil
}
let value = text[valueRange].trimmingCharacters(in: .whitespacesAndNewlines)
return value.isEmpty ? nil : String(value)
}
}
extension [String] {
fileprivate func uniquedSorted() -> [String] {
Array(Set(self)).sorted()
}
}