Skip to content
68 changes: 68 additions & 0 deletions Sources/CodexBarCore/ProviderEndpointOverrideValidator.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import Foundation

struct ProviderEndpointOverrideValidator: Sendable {
enum HostPolicy: Sendable {
case allowAnyHTTPSHost
case providerOwnedOnly
}

private let allowedDomainSuffixes: Set<String>

init(allowedDomainSuffixes: [String]) {
self.allowedDomainSuffixes = Set(allowedDomainSuffixes.map { $0.lowercased() })
}

func validatedHost(_ raw: String?, policy: HostPolicy = .allowAnyHTTPSHost) -> String? {
guard let raw,
let url = self.url(from: raw),
let host = self.validatedDecodedHost(for: url, policy: policy)
else { return nil }
return host
}

func validatedURL(_ raw: String?, policy: HostPolicy = .allowAnyHTTPSHost) -> URL? {
guard let raw,
let url = self.url(from: raw),
self.validatedDecodedHost(for: url, policy: policy) != nil
else { return nil }
return url
}

private func url(from raw: String) -> URL? {
let url = if let parsed = URL(string: raw), parsed.scheme != nil {
parsed
} else {
URL(string: "https://\(raw)")
}
guard let url else { return nil }
guard let scheme = url.scheme?.lowercased(), scheme == "https" else { return nil }
guard url.user == nil, url.password == nil else { return nil }
guard url.host(percentEncoded: false) != nil else { return nil }
return url
}

private func validatedDecodedHost(for url: URL, policy: HostPolicy) -> String? {
guard let decodedHost = url.host(percentEncoded: false)?.lowercased(),
let encodedHost = url.host(percentEncoded: true)?.lowercased(),
self.hostHasNoEncodedDelimiters(encodedHost, decodedHost: decodedHost)
else { return nil }

switch policy {
case .allowAnyHTTPSHost:
return decodedHost
case .providerOwnedOnly:
guard self.allowedDomainSuffixes.contains(where: { suffix in
decodedHost == suffix || decodedHost.hasSuffix(".\(suffix)")
}) else { return nil }
return decodedHost
}
}

private func hostHasNoEncodedDelimiters(_ encodedHost: String, decodedHost: String) -> Bool {
let decodedDelimiters = CharacterSet(charactersIn: "/\\?#@:")
guard decodedHost.rangeOfCharacter(from: decodedDelimiters) == nil else { return false }

let encodedDelimiters = ["%2f", "%5c", "%3f", "%23", "%40", "%3a"]
return !encodedDelimiters.contains { encodedHost.contains($0) }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ struct AlibabaCodingPlanWebFetchStrategy: ProviderFetchStrategy {
return message.contains("HTTP 404") || message.contains("HTTP 403")
case .networkError:
return true
case .parseFailed:
case .parseFailed, .invalidEndpointOverride:
return false
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import Foundation

public struct AlibabaCodingPlanSettingsReader: Sendable {
private static let endpointValidator = ProviderEndpointOverrideValidator(
allowedDomainSuffixes: ["alibabacloud.com", "aliyun.com", "aliyuncs.com"])

public static let apiTokenKey = "ALIBABA_CODING_PLAN_API_KEY"
public static let qwenAPITokenKey = "ALIBABA_QWEN_API_KEY"
public static let dashScopeAPITokenKey = "DASHSCOPE_API_KEY"
Expand All @@ -12,6 +15,11 @@ public struct AlibabaCodingPlanSettingsReader: Sendable {
public static let cookieHeaderKey = "ALIBABA_CODING_PLAN_COOKIE"
public static let hostKey = "ALIBABA_CODING_PLAN_HOST"
public static let quotaURLKey = "ALIBABA_CODING_PLAN_QUOTA_URL"
public static let requireProviderEndpointOverridesKey = "ALIBABA_CODING_PLAN_REQUIRE_PROVIDER_ENDPOINT_OVERRIDES"
private static let endpointOverrideKeys = [
Self.hostKey,
Self.quotaURLKey,
]

public static func apiToken(
environment: [String: String] = ProcessInfo.processInfo.environment) -> String?
Expand All @@ -25,7 +33,22 @@ public struct AlibabaCodingPlanSettingsReader: Sendable {
public static func hostOverride(
environment: [String: String] = ProcessInfo.processInfo.environment) -> String?
{
self.cleaned(environment[self.hostKey])
self.endpointValidator.validatedHost(
self.cleaned(environment[self.hostKey]),
policy: self.endpointOverrideHostPolicy(environment: environment))
}

public static func rejectedEndpointOverrideKey(
environment: [String: String] = ProcessInfo.processInfo.environment) -> String?
{
let policy = self.endpointOverrideHostPolicy(environment: environment)
return self.endpointOverrideKeys.first { key in
guard let value = self.cleaned(environment[key]) else { return false }
if key == Self.hostKey {
return self.endpointValidator.validatedHost(value, policy: policy) == nil
}
return self.endpointValidator.validatedURL(value, policy: policy) == nil
}
}

public static func cookieHeader(
Expand All @@ -37,11 +60,16 @@ public struct AlibabaCodingPlanSettingsReader: Sendable {
public static func quotaURL(
environment: [String: String] = ProcessInfo.processInfo.environment) -> URL?
{
guard let raw = self.cleaned(environment[self.quotaURLKey]) else { return nil }
if let url = URL(string: raw), url.scheme != nil {
return url
}
return URL(string: "https://\(raw)")
self.endpointValidator.validatedURL(
self.cleaned(environment[self.quotaURLKey]),
policy: self.endpointOverrideHostPolicy(environment: environment))
}

static func endpointOverrideHostPolicy(environment: [String: String]) -> ProviderEndpointOverrideValidator.HostPolicy {
guard let value = self.cleaned(environment[self.requireProviderEndpointOverridesKey])?.lowercased(),
["1", "true", "yes", "on"].contains(value)
else { return .allowAnyHTTPSHost }
return .providerOwnedOnly
}

static func cleaned(_ raw: String?) -> String? {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,78 +22,93 @@ public struct AlibabaCodingPlanUsageFetcher: Sendable {
apiKey: String,
region: AlibabaCodingPlanAPIRegion = .international,
environment: [String: String] = ProcessInfo.processInfo.environment,
now: Date = Date()) async throws -> AlibabaCodingPlanUsageSnapshot
now: Date = Date(),
transport: any ProviderHTTPTransport = ProviderHTTPClient.shared) async throws -> AlibabaCodingPlanUsageSnapshot
{
let cleanedKey = apiKey.trimmingCharacters(in: .whitespacesAndNewlines)
guard !cleanedKey.isEmpty else {
throw AlibabaCodingPlanUsageError.invalidCredentials
}
if let rejectedKey = AlibabaCodingPlanSettingsReader.rejectedEndpointOverrideKey(environment: environment) {
throw AlibabaCodingPlanUsageError.invalidEndpointOverride(rejectedKey)
}

if region != .international {
return try await self.fetchUsageOnce(
apiKey: cleanedKey,
region: region,
environment: environment,
now: now)
now: now,
transport: transport)
}

do {
return try await self.fetchUsageOnce(
apiKey: cleanedKey,
region: .international,
environment: environment,
now: now)
now: now,
transport: transport)
} catch let error as AlibabaCodingPlanUsageError {
guard error.shouldRetryOnAlternateRegion else { throw error }
Self.log.debug("Alibaba Coding Plan request failed on intl host; retrying cn host")
return try await self.fetchUsageOnce(
apiKey: cleanedKey,
region: .chinaMainland,
environment: environment,
now: now)
now: now,
transport: transport)
}
}

public static func fetchUsage(
cookieHeader: String,
region: AlibabaCodingPlanAPIRegion = .international,
environment: [String: String] = ProcessInfo.processInfo.environment,
now: Date = Date()) async throws -> AlibabaCodingPlanUsageSnapshot
now: Date = Date(),
transport: any ProviderHTTPTransport = ProviderHTTPClient.shared) async throws -> AlibabaCodingPlanUsageSnapshot
{
guard let normalizedCookie = CookieHeaderNormalizer.normalize(cookieHeader) else {
throw AlibabaCodingPlanSettingsError.invalidCookie
}
if let rejectedKey = AlibabaCodingPlanSettingsReader.rejectedEndpointOverrideKey(environment: environment) {
throw AlibabaCodingPlanUsageError.invalidEndpointOverride(rejectedKey)
}

if region != .international {
return try await self.fetchUsageOnce(
cookieHeader: normalizedCookie,
region: region,
environment: environment,
now: now)
now: now,
transport: transport)
}

do {
return try await self.fetchUsageOnce(
cookieHeader: normalizedCookie,
region: .international,
environment: environment,
now: now)
now: now,
transport: transport)
} catch let error as AlibabaCodingPlanUsageError {
guard error.shouldRetryOnAlternateRegion else { throw error }
Self.log.debug("Alibaba Coding Plan cookie request failed on intl host; retrying cn host")
return try await self.fetchUsageOnce(
cookieHeader: normalizedCookie,
region: .chinaMainland,
environment: environment,
now: now)
now: now,
transport: transport)
}
}

private static func fetchUsageOnce(
apiKey: String,
region: AlibabaCodingPlanAPIRegion,
environment: [String: String],
now: Date) async throws -> AlibabaCodingPlanUsageSnapshot
now: Date,
transport: any ProviderHTTPTransport) async throws -> AlibabaCodingPlanUsageSnapshot
{
let url = self.resolveQuotaURL(region: region, environment: environment)
var request = URLRequest(url: url)
Expand All @@ -108,7 +123,7 @@ public struct AlibabaCodingPlanUsageFetcher: Sendable {
request.setValue(region.gatewayBaseURLString, forHTTPHeaderField: "Origin")
request.setValue(region.dashboardURL.absoluteString, forHTTPHeaderField: "Referer")

let response = try await ProviderHTTPClient.shared.response(for: request)
let response = try await transport.response(for: request)
let data = response.data
guard response.statusCode == 200 else {
if response.statusCode == 401 || response.statusCode == 403 {
Expand All @@ -126,13 +141,15 @@ public struct AlibabaCodingPlanUsageFetcher: Sendable {
cookieHeader: String,
region: AlibabaCodingPlanAPIRegion,
environment: [String: String],
now: Date) async throws -> AlibabaCodingPlanUsageSnapshot
now: Date,
transport: any ProviderHTTPTransport) async throws -> AlibabaCodingPlanUsageSnapshot
{
let url = self.resolveConsoleQuotaURL(region: region, environment: environment)
let secToken = try await self.resolveConsoleSECToken(
cookieHeader: cookieHeader,
region: region,
environment: environment)
environment: environment,
transport: transport)
let anonymousID = self.extractCookieValue(name: "cna", from: cookieHeader)

var request = URLRequest(url: url)
Expand All @@ -155,7 +172,7 @@ public struct AlibabaCodingPlanUsageFetcher: Sendable {
request.setValue(region.gatewayBaseURLString, forHTTPHeaderField: "Origin")
request.setValue(region.consoleRefererURL.absoluteString, forHTTPHeaderField: "Referer")

let response = try await ProviderHTTPClient.shared.response(for: request)
let response = try await transport.response(for: request)
let data = response.data
guard response.statusCode == 200 else {
if response.statusCode == 401 || response.statusCode == 403 {
Expand Down Expand Up @@ -318,7 +335,8 @@ public struct AlibabaCodingPlanUsageFetcher: Sendable {
private static func resolveConsoleSECToken(
cookieHeader: String,
region: AlibabaCodingPlanAPIRegion,
environment: [String: String]) async throws -> String
environment: [String: String],
transport: any ProviderHTTPTransport) async throws -> String
{
let cookieSECToken = self.extractCookieValue(name: "sec_token", from: cookieHeader)

Expand All @@ -332,7 +350,7 @@ public struct AlibabaCodingPlanUsageFetcher: Sendable {
"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
forHTTPHeaderField: "Accept")

if let response = try? await ProviderHTTPClient.shared.response(for: request),
if let response = try? await transport.response(for: request),
response.statusCode == 200,
let html = String(data: response.data, encoding: .utf8),
let token = self.extractConsoleSECToken(from: html),
Expand All @@ -344,7 +362,8 @@ public struct AlibabaCodingPlanUsageFetcher: Sendable {
if let token = try? await self.fetchSECTokenFromUserInfo(
cookieHeader: cookieHeader,
region: region,
environment: environment)
environment: environment,
transport: transport)
{
return token
}
Expand Down Expand Up @@ -384,7 +403,8 @@ public struct AlibabaCodingPlanUsageFetcher: Sendable {
private static func fetchSECTokenFromUserInfo(
cookieHeader: String,
region: AlibabaCodingPlanAPIRegion,
environment: [String: String]) async throws -> String?
environment: [String: String],
transport: any ProviderHTTPTransport) async throws -> String?
{
let gatewayBaseURL = self.resolveConsoleGatewayBaseURL(region: region, environment: environment)
let userInfoURL = gatewayBaseURL.appendingPathComponent("tool/user/info.json")
Expand All @@ -397,7 +417,7 @@ public struct AlibabaCodingPlanUsageFetcher: Sendable {
.absoluteString + "/"
request.setValue(referer, forHTTPHeaderField: "Referer")

let response = try await ProviderHTTPClient.shared.response(for: request)
let response = try await transport.response(for: request)
guard response.statusCode == 200 else {
return nil
}
Expand Down Expand Up @@ -1075,6 +1095,7 @@ public enum AlibabaCodingPlanUsageError: LocalizedError, Sendable, Equatable {
case apiError(String)
case parseFailed(String)
case apiKeyUnavailableInRegion
case invalidEndpointOverride(String)

var shouldRetryOnAlternateRegion: Bool {
switch self {
Expand All @@ -1090,6 +1111,8 @@ public enum AlibabaCodingPlanUsageError: LocalizedError, Sendable, Equatable {
message.contains("Missing coding plan quota data") || message.contains("No quota windows found")
case .networkError:
false
case .invalidEndpointOverride:
false
}
}

Expand All @@ -1110,6 +1133,10 @@ public enum AlibabaCodingPlanUsageError: LocalizedError, Sendable, Equatable {
"Alibaba Coding Plan API error: \(message)"
case let .parseFailed(message):
"Failed to parse Alibaba Coding Plan response: \(message)"
case let .invalidEndpointOverride(key):
"Alibaba Coding Plan endpoint override \(key) is not allowed. " +
"Use an HTTPS endpoint without user info or encoded host tricks. " +
"If ALIBABA_CODING_PLAN_REQUIRE_PROVIDER_ENDPOINT_OVERRIDES=true is set, the endpoint must also be Alibaba-owned."
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ struct MiniMaxAPIFetchStrategy: ProviderFetchStrategy {
return true
case let .apiError(message):
return message.contains("HTTP 404")
case .networkError, .parseFailed:
case .networkError, .parseFailed, .invalidEndpointOverride:
return false
}
}
Expand Down
Loading
Loading