mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-16 17:01:45 +00:00
* style: apply SwiftFormat 0.62.1 rules Refs #103202 * ci: enforce deterministic Swift lint Refs #103202 * refactor: keep gateway connect lint-clean Refs #103202 * style: keep iOS typography checks warning-free * ci: route MLX Swift changes through pre-push * fix: preserve native i18n extraction after Swift cleanup * refactor: keep rebased Swift surfaces lint-clean * style: format latest Swift additions * chore: refresh native i18n inventory * style: keep generated Swift formatter-clean * fix: preserve node route invalidation callbacks * fix: keep native translation IDs stable * fix: retain native translation identifiers * fix: preserve translations across Swift source moves
162 lines
5.4 KiB
Swift
162 lines
5.4 KiB
Swift
import CryptoKit
|
|
import Foundation
|
|
import os
|
|
import Security
|
|
|
|
enum GatewayTLSFingerprintProbeFailure: Equatable {
|
|
case endpointUnreachable
|
|
case tlsHandshakeTimeout
|
|
case tlsUnavailable
|
|
case certificateUnavailable
|
|
}
|
|
|
|
enum GatewayTLSFingerprintProbeResult: Equatable {
|
|
case fingerprint(String)
|
|
case failure(GatewayTLSFingerprintProbeFailure)
|
|
}
|
|
|
|
typealias GatewayTLSFingerprintProbeFunction = @Sendable (URL) async -> GatewayTLSFingerprintProbeResult
|
|
|
|
enum GatewayTLSFingerprintProbeBudget {
|
|
static let tcpConnectTimeoutSeconds = 3.0
|
|
fileprivate static let tlsHandshakeTimeoutSeconds = 10.0
|
|
}
|
|
|
|
func defaultGatewayTLSFingerprintProbe(url: URL) async -> GatewayTLSFingerprintProbeResult {
|
|
await withCheckedContinuation { continuation in
|
|
let probe = GatewayTLSFingerprintProbe(
|
|
url: url,
|
|
timeoutSeconds: GatewayTLSFingerprintProbeBudget.tlsHandshakeTimeoutSeconds)
|
|
{ result in
|
|
continuation.resume(returning: result)
|
|
}
|
|
probe.start()
|
|
}
|
|
}
|
|
|
|
private final class GatewayTLSFingerprintProbe: NSObject, URLSessionDelegate, URLSessionTaskDelegate,
|
|
@unchecked Sendable
|
|
{
|
|
private struct ProbeState {
|
|
var didFinish = false
|
|
var session: URLSession?
|
|
var task: URLSessionWebSocketTask?
|
|
}
|
|
|
|
private let url: URL
|
|
private let timeoutSeconds: Double
|
|
private let onComplete: (GatewayTLSFingerprintProbeResult) -> Void
|
|
private let state = OSAllocatedUnfairLock(initialState: ProbeState())
|
|
|
|
init(
|
|
url: URL,
|
|
timeoutSeconds: Double,
|
|
onComplete: @escaping (GatewayTLSFingerprintProbeResult) -> Void)
|
|
{
|
|
self.url = url
|
|
self.timeoutSeconds = timeoutSeconds
|
|
self.onComplete = onComplete
|
|
}
|
|
|
|
func start() {
|
|
let config = URLSessionConfiguration.ephemeral
|
|
config.timeoutIntervalForRequest = self.timeoutSeconds
|
|
config.timeoutIntervalForResource = self.timeoutSeconds
|
|
let session = URLSession(configuration: config, delegate: self, delegateQueue: nil)
|
|
let task = session.webSocketTask(with: self.url)
|
|
self.state.withLock { s in
|
|
s.session = session
|
|
s.task = task
|
|
}
|
|
task.resume()
|
|
|
|
DispatchQueue.global(qos: .utility).asyncAfter(deadline: .now() + self.timeoutSeconds) { [weak self] in
|
|
self?.finish(.failure(.tlsHandshakeTimeout))
|
|
}
|
|
}
|
|
|
|
func urlSession(
|
|
_ session: URLSession,
|
|
didReceive challenge: URLAuthenticationChallenge,
|
|
completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void)
|
|
{
|
|
guard challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust,
|
|
let trust = challenge.protectionSpace.serverTrust
|
|
else {
|
|
completionHandler(.performDefaultHandling, nil)
|
|
return
|
|
}
|
|
|
|
let fp = GatewayTLSFingerprintProbe.certificateFingerprint(trust)
|
|
completionHandler(.cancelAuthenticationChallenge, nil)
|
|
if let fp {
|
|
self.finish(.fingerprint(fp))
|
|
} else {
|
|
self.finish(.failure(.certificateUnavailable))
|
|
}
|
|
}
|
|
|
|
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
|
|
guard let error else {
|
|
self.finish(.failure(.tlsUnavailable))
|
|
return
|
|
}
|
|
self.finish(.failure(Self.failure(for: error)))
|
|
}
|
|
|
|
private func finish(_ result: GatewayTLSFingerprintProbeResult) {
|
|
typealias FinishState = (Bool, URLSessionWebSocketTask?, URLSession?)
|
|
let (shouldComplete, taskToCancel, sessionToInvalidate) = self.state.withLock { s -> FinishState in
|
|
guard !s.didFinish else { return (false, nil, nil) }
|
|
s.didFinish = true
|
|
let task = s.task
|
|
let session = s.session
|
|
s.task = nil
|
|
s.session = nil
|
|
return (true, task, session)
|
|
}
|
|
guard shouldComplete else { return }
|
|
taskToCancel?.cancel(with: .goingAway, reason: nil)
|
|
sessionToInvalidate?.invalidateAndCancel()
|
|
self.onComplete(result)
|
|
}
|
|
|
|
private static func failure(for error: Error) -> GatewayTLSFingerprintProbeFailure {
|
|
let nsError = error as NSError
|
|
guard nsError.domain == URLError.errorDomain else {
|
|
return .tlsUnavailable
|
|
}
|
|
|
|
switch URLError.Code(rawValue: nsError.code) {
|
|
case .timedOut:
|
|
return .tlsHandshakeTimeout
|
|
case .cannotFindHost,
|
|
.dnsLookupFailed,
|
|
.cannotConnectToHost,
|
|
.notConnectedToInternet,
|
|
.internationalRoamingOff,
|
|
.callIsActive,
|
|
.dataNotAllowed:
|
|
return .endpointUnreachable
|
|
case .networkConnectionLost,
|
|
.secureConnectionFailed,
|
|
.cannotParseResponse,
|
|
.badServerResponse:
|
|
return .tlsUnavailable
|
|
default:
|
|
return .tlsUnavailable
|
|
}
|
|
}
|
|
|
|
private static func certificateFingerprint(_ trust: SecTrust) -> String? {
|
|
guard let chain = SecTrustCopyCertificateChain(trust) as? [SecCertificate],
|
|
let cert = chain.first
|
|
else {
|
|
return nil
|
|
}
|
|
let data = SecCertificateCopyData(cert) as Data
|
|
let digest = SHA256.hash(data: data)
|
|
return digest.map { String(format: "%02x", $0) }.joined()
|
|
}
|
|
}
|