Files
adolf/apps/macos/Sources/OpenClaw/GatewayRemoteConfig.swift
alvis bedb527145
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Vendor OpenClaw source as Adolf fork baseline
Adolf is a fork/vendored clone of github.com/openclaw/openclaw (v2026.6.11),
free to diverge. Tree copied sans upstream .git; upstream remote added for
future syncs. Node pinned to 24 (.nvmrc); engines already require >=22.19.
Preserves docs/ARCHITECTURE.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LeqyaxJF2nbRXJtae2kNB2
2026-07-05 09:36:54 +00:00

263 lines
8.5 KiB
Swift

import Foundation
import OpenClawKit
#if canImport(Darwin)
import Darwin
#endif
enum GatewayRemoteConfig {
enum TransportSource: Equatable {
case explicit
case inferredRemoteURL
case legacySSH
}
struct TransportResolution: Equatable {
let transport: AppState.RemoteTransport
let source: TransportSource
let directURL: URL?
}
enum TokenValue: Equatable {
case missing
case plaintext(String)
case unsupportedNonString
var textFieldValue: String {
switch self {
case let .plaintext(token):
token
case .missing, .unsupportedNonString:
""
}
}
var isUnsupportedNonString: Bool {
if case .unsupportedNonString = self {
return true
}
return false
}
}
static func resolveTransport(root: [String: Any]) -> AppState.RemoteTransport {
self.resolveTransportResolution(root: root).transport
}
static func resolveTransportResolution(root: [String: Any]) -> TransportResolution {
let explicit = self.resolveExplicitTransport(root: root)
switch explicit {
case .direct:
return TransportResolution(
transport: .direct,
source: .explicit,
directURL: self.resolveGatewayUrl(root: root))
case .ssh:
return TransportResolution(transport: .ssh, source: .explicit, directURL: nil)
case nil:
break
}
if let url = self.resolveGatewayUrl(root: root),
let host = url.host,
!LoopbackHost.isLoopbackHost(host)
{
return TransportResolution(transport: .direct, source: .inferredRemoteURL, directURL: url)
}
return TransportResolution(transport: .ssh, source: .legacySSH, directURL: nil)
}
private static func resolveExplicitTransport(root: [String: Any]) -> AppState.RemoteTransport? {
guard let gateway = root["gateway"] as? [String: Any],
let remote = gateway["remote"] as? [String: Any],
let raw = remote["transport"] as? String
else {
return nil
}
let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
switch trimmed {
case AppState.RemoteTransport.direct.rawValue:
return .direct
case AppState.RemoteTransport.ssh.rawValue:
return .ssh
default:
return .ssh
}
}
static func resolveUrlString(root: [String: Any]) -> String? {
guard let gateway = root["gateway"] as? [String: Any],
let remote = gateway["remote"] as? [String: Any],
let urlRaw = remote["url"] as? String
else {
return nil
}
let trimmed = urlRaw.trimmingCharacters(in: .whitespacesAndNewlines)
return trimmed.isEmpty ? nil : trimmed
}
static func resolveTokenValue(root: [String: Any]) -> TokenValue {
guard let gateway = root["gateway"] as? [String: Any],
let remote = gateway["remote"] as? [String: Any],
let tokenRaw = remote["token"]
else {
return .missing
}
guard let tokenString = tokenRaw as? String else {
return .unsupportedNonString
}
let trimmed = tokenString.trimmingCharacters(in: .whitespacesAndNewlines)
return trimmed.isEmpty ? .missing : .plaintext(trimmed)
}
static func resolveTokenString(root: [String: Any]) -> String? {
switch self.resolveTokenValue(root: root) {
case let .plaintext(token):
token
case .missing, .unsupportedNonString:
nil
}
}
static func resolvePasswordString(root: [String: Any]) -> String? {
guard let gateway = root["gateway"] as? [String: Any],
let remote = gateway["remote"] as? [String: Any],
let raw = remote["password"] as? String
else {
return nil
}
let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines)
return trimmed.isEmpty ? nil : trimmed
}
static func resolveTLSFingerprint(root: [String: Any]) -> String? {
guard let gateway = root["gateway"] as? [String: Any],
let remote = gateway["remote"] as? [String: Any],
let raw = remote["tlsFingerprint"] as? String
else {
return nil
}
let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines)
return trimmed.isEmpty ? nil : trimmed
}
static func resolveGatewayUrl(root: [String: Any]) -> URL? {
guard let raw = self.resolveUrlString(root: root) else { return nil }
return self.normalizeGatewayUrl(raw)
}
static func resolveRemotePort(root: [String: Any]) -> Int? {
guard let gateway = root["gateway"] as? [String: Any],
let remote = gateway["remote"] as? [String: Any]
else {
return nil
}
let value = remote["remotePort"]
let port: Int? = switch value {
case let raw as Int:
raw
case let raw as NSNumber:
raw.intValue
case let raw as String:
Int(raw.trimmingCharacters(in: .whitespacesAndNewlines))
default:
nil
}
guard let port, port > 0, port <= 65535 else { return nil }
return port
}
static func normalizeGatewayUrlString(_ raw: String) -> String? {
self.normalizeGatewayUrl(raw)?.absoluteString
}
static func normalizeGatewayUrl(_ raw: String) -> URL? {
let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty, let url = URL(string: trimmed) else { return nil }
let scheme = url.scheme?.lowercased() ?? ""
guard scheme == "ws" || scheme == "wss" else { return nil }
let host = url.host?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
guard !host.isEmpty else { return nil }
if scheme == "ws",
!LoopbackHost.isLoopbackHost(host),
!self.isTrustedPlaintextRemoteHost(host)
{
return nil
}
if scheme == "ws", url.port == nil {
guard var components = URLComponents(url: url, resolvingAgainstBaseURL: false) else {
return url
}
components.port = 18789
return components.url
}
return url
}
static func isTrustedPlaintextRemoteHost(_ host: String) -> Bool {
let lower = host.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
guard !lower.isEmpty else { return false }
if lower == "localhost" || lower.hasSuffix(".local") || lower.hasSuffix(".ts.net") {
return true
}
if self.isPrivateIPv6Literal(lower) {
return true
}
guard let parts = self.ipv4Parts(lower) else { return false }
switch (parts[0], parts[1]) {
case (10, _), (192, 168), (169, 254):
return true
case (172, 16...31):
return true
case (100, 64...127):
return true
default:
return false
}
}
private static func ipv4Parts(_ value: String) -> [Int]? {
let labels = value.split(separator: ".", omittingEmptySubsequences: false)
guard labels.count == 4 else { return nil }
var parts: [Int] = []
parts.reserveCapacity(4)
for label in labels {
guard !label.isEmpty,
label.allSatisfy(\.isNumber),
let part = Int(label),
part >= 0,
part <= 255
else {
return nil
}
parts.append(part)
}
return parts
}
private static func isPrivateIPv6Literal(_ value: String) -> Bool {
#if canImport(Darwin)
var addr = in6_addr()
guard value.withCString({ inet_pton(AF_INET6, $0, &addr) }) == 1 else {
return false
}
return value.hasPrefix("fc") || value.hasPrefix("fd") || value.hasPrefix("fe80:")
#else
return false
#endif
}
static func defaultPort(for url: URL) -> Int? {
if let port = url.port { return port }
let scheme = url.scheme?.lowercased() ?? ""
switch scheme {
case "wss":
return 443
case "ws":
return 18789
default:
return nil
}
}
}