mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-07 19:48:31 +08:00
* feat(ios): support per-gateway custom headers for proxied gateway connections Per-gateway custom request headers (Cloudflare Access-style service tokens) for gateways behind authenticating reverse proxies. Headers are credentials: Keychain-stored per gateway stableID, masked in settings UI, injected on the WebSocket upgrade at connect time via a provider so edits apply on the next reconnect without re-pairing. Reserved handshake headers and control chars are sanitized out. TLS pinning and pairing flows unchanged. Push relay and other non-gateway destinations proven header-free by regression test. Related: #100698 * chore(ios): sync native i18n inventory for custom header strings * fix(ios): harden gateway custom headers * chore(ios): resync native i18n inventory * fix(ios): validate custom header names
89 lines
3.3 KiB
Swift
89 lines
3.3 KiB
Swift
import Foundation
|
|
import Testing
|
|
@testable import OpenClaw
|
|
|
|
/// Gateway custom headers are proxy credentials scoped to one gateway. They ride only the
|
|
/// gateway WebSocket upgrade; the push relay is a different trust domain and must never
|
|
/// observe them, even while they sit in the store.
|
|
private final class CapturingURLProtocol: URLProtocol {
|
|
private static let lock = NSLock()
|
|
private nonisolated(unsafe) static var requests: [URLRequest] = []
|
|
|
|
static func drain() -> [URLRequest] {
|
|
self.lock.lock()
|
|
defer { self.lock.unlock() }
|
|
let drained = self.requests
|
|
self.requests = []
|
|
return drained
|
|
}
|
|
|
|
private static func record(_ request: URLRequest) {
|
|
self.lock.lock()
|
|
self.requests.append(request)
|
|
self.lock.unlock()
|
|
}
|
|
|
|
override class func canInit(with request: URLRequest) -> Bool {
|
|
true
|
|
}
|
|
|
|
override class func canonicalRequest(for request: URLRequest) -> URLRequest {
|
|
request
|
|
}
|
|
|
|
override func startLoading() {
|
|
Self.record(self.request)
|
|
guard let url = self.request.url,
|
|
let response = HTTPURLResponse(
|
|
url: url,
|
|
statusCode: 503,
|
|
httpVersion: nil,
|
|
headerFields: ["Content-Type": "application/json"])
|
|
else {
|
|
self.client?.urlProtocol(self, didFailWithError: URLError(.badURL))
|
|
return
|
|
}
|
|
self.client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
|
|
self.client?.urlProtocol(self, didLoad: Data("{}".utf8))
|
|
self.client?.urlProtocolDidFinishLoading(self)
|
|
}
|
|
|
|
override func stopLoading() {}
|
|
}
|
|
|
|
struct PushRelayHeaderIsolationTests {
|
|
@Test func `push relay requests never carry gateway custom headers`() async throws {
|
|
let gatewayID = "manual|proxied.example.com|443|\(UUID().uuidString)"
|
|
defer { GatewaySettingsStore.saveGatewayCustomHeaders([:], gatewayStableID: gatewayID) }
|
|
#expect(GatewaySettingsStore.saveGatewayCustomHeaders(
|
|
["CF-Access-Client-Id": "client-id", "CF-Access-Client-Secret": "client-secret"],
|
|
gatewayStableID: gatewayID))
|
|
|
|
let configuration = URLSessionConfiguration.ephemeral
|
|
configuration.protocolClasses = [CapturingURLProtocol.self]
|
|
let client = try PushRelayClient(
|
|
baseURL: #require(URL(string: "https://relay.example.invalid")),
|
|
session: URLSession(configuration: configuration))
|
|
let input = PushRelayRegistrationInput(
|
|
installationId: "test-install",
|
|
bundleId: "ai.openclaw.tests",
|
|
appVersion: "1.0",
|
|
environment: .sandbox,
|
|
relayProfile: .deviceSandbox,
|
|
proofPolicy: .appleDevelopment,
|
|
distribution: .local,
|
|
apnsTokenHex: "00",
|
|
gatewayIdentity: PushRelayGatewayIdentity(deviceId: "device", publicKey: "key"))
|
|
|
|
// Registration fails at the stubbed challenge; the captured request is what matters.
|
|
_ = try? await client.register(input)
|
|
|
|
let requests = CapturingURLProtocol.drain()
|
|
#expect(!requests.isEmpty)
|
|
for request in requests {
|
|
#expect(request.value(forHTTPHeaderField: "CF-Access-Client-Id") == nil)
|
|
#expect(request.value(forHTTPHeaderField: "CF-Access-Client-Secret") == nil)
|
|
}
|
|
}
|
|
}
|