Files
openclaw/apps/shared/OpenClawKit/Tests/OpenClawKitTests/GatewayCustomHeadersTests.swift
Peter Steinberger 7853aa35a4 feat(ios): per-gateway custom headers for gateways behind authenticating proxies (#100768)
* 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
2026-07-06 17:17:07 +01:00

54 lines
1.8 KiB
Swift

import Foundation
import Testing
@testable import OpenClawKit
struct GatewayCustomHeadersTests {
@Test func `sanitized keeps operator proxy credential headers`() {
let headers = GatewayCustomHeaders.sanitized([
"CF-Access-Client-Id": "client-id",
"CF-Access-Client-Secret": "client-secret",
"Authorization": "Basic dXNlcjpwYXNz",
])
#expect(headers == [
"CF-Access-Client-Id": "client-id",
"CF-Access-Client-Secret": "client-secret",
"Authorization": "Basic dXNlcjpwYXNz",
])
}
@Test func `sanitized drops reserved handshake header names`() {
let headers = GatewayCustomHeaders.sanitized([
"Host": "evil.example",
"connection": "close",
"Upgrade": "h2c",
"Sec-WebSocket-Protocol": "override",
"sec-websocket-key": "override",
"Content-Length": "0",
"Proxy-Connection": "keep-alive",
"X-Allowed": "yes",
])
#expect(headers == ["X-Allowed": "yes"])
}
@Test func `sanitized drops invalid names and control characters`() {
let headers = GatewayCustomHeaders.sanitized([
"": "value",
" ": "value",
"X Bad": "value",
"X:Bad": "value",
"X-Bad-é": "value",
"X-Split\r\nEvil": "value",
"X-Value-Split": "a\r\nEvil: b",
"X-Tab-Value": "a\tb",
"X-Fine": "value",
])
#expect(headers == ["X-Fine": "value"])
}
@Test func `reserved name check is case and whitespace insensitive`() {
#expect(GatewayCustomHeaders.isReservedName(" HOST "))
#expect(GatewayCustomHeaders.isReservedName("Sec-WebSocket-Extensions"))
#expect(!GatewayCustomHeaders.isReservedName("CF-Access-Client-Id"))
}
}