feat: add support for allowing self-signed TLS certificates in gateway configuration

This commit is contained in:
Abhimanyu Saharan
2026-02-22 20:20:19 +05:30
parent d37f230eb3
commit 56f4964332
11 changed files with 89 additions and 19 deletions

View File

@@ -38,12 +38,14 @@ def _query_to_resolve_input(
gateway_url: str | None = Query(default=None), gateway_url: str | None = Query(default=None),
gateway_token: str | None = Query(default=None), gateway_token: str | None = Query(default=None),
gateway_disable_device_pairing: bool = Query(default=False), gateway_disable_device_pairing: bool = Query(default=False),
gateway_allow_insecure_tls: bool = Query(default=False),
) -> GatewayResolveQuery: ) -> GatewayResolveQuery:
return GatewaySessionService.to_resolve_query( return GatewaySessionService.to_resolve_query(
board_id=board_id, board_id=board_id,
gateway_url=gateway_url, gateway_url=gateway_url,
gateway_token=gateway_token, gateway_token=gateway_token,
gateway_disable_device_pairing=gateway_disable_device_pairing, gateway_disable_device_pairing=gateway_disable_device_pairing,
gateway_allow_insecure_tls=gateway_allow_insecure_tls,
) )

View File

@@ -22,6 +22,7 @@ class GatewayResolveQuery(SQLModel):
gateway_url: str | None = None gateway_url: str | None = None
gateway_token: str | None = None gateway_token: str | None = None
gateway_disable_device_pairing: bool = False gateway_disable_device_pairing: bool = False
gateway_allow_insecure_tls: bool = False
class GatewaysStatusResponse(SQLModel): class GatewaysStatusResponse(SQLModel):

View File

@@ -66,12 +66,14 @@ class GatewaySessionService(OpenClawDBService):
gateway_url: str | None, gateway_url: str | None,
gateway_token: str | None, gateway_token: str | None,
gateway_disable_device_pairing: bool = False, gateway_disable_device_pairing: bool = False,
gateway_allow_insecure_tls: bool = False,
) -> GatewayResolveQuery: ) -> GatewayResolveQuery:
return GatewayResolveQuery( return GatewayResolveQuery(
board_id=board_id, board_id=board_id,
gateway_url=gateway_url, gateway_url=gateway_url,
gateway_token=gateway_token, gateway_token=gateway_token,
gateway_disable_device_pairing=gateway_disable_device_pairing, gateway_disable_device_pairing=gateway_disable_device_pairing,
gateway_allow_insecure_tls=gateway_allow_insecure_tls,
) )
@staticmethod @staticmethod
@@ -112,6 +114,7 @@ class GatewaySessionService(OpenClawDBService):
GatewayClientConfig( GatewayClientConfig(
url=raw_url, url=raw_url,
token=(params.gateway_token or "").strip() or None, token=(params.gateway_token or "").strip() or None,
allow_insecure_tls=params.gateway_allow_insecure_tls,
disable_device_pairing=params.gateway_disable_device_pairing, disable_device_pairing=params.gateway_disable_device_pairing,
), ),
None, None,

View File

@@ -3,7 +3,10 @@ from __future__ import annotations
from uuid import uuid4 from uuid import uuid4
import pytest
from app.models.gateways import Gateway from app.models.gateways import Gateway
from app.schemas.gateway_api import GatewayResolveQuery
from app.services.openclaw.gateway_resolver import ( from app.services.openclaw.gateway_resolver import (
gateway_client_config, gateway_client_config,
optional_gateway_client_config, optional_gateway_client_config,
@@ -14,6 +17,7 @@ from app.services.openclaw.session_service import GatewaySessionService
def _gateway( def _gateway(
*, *,
disable_device_pairing: bool, disable_device_pairing: bool,
allow_insecure_tls: bool = False,
url: str = "ws://gateway.example:18789/ws", url: str = "ws://gateway.example:18789/ws",
token: str | None = " secret-token ", token: str | None = " secret-token ",
) -> Gateway: ) -> Gateway:
@@ -25,6 +29,7 @@ def _gateway(
token=token, token=token,
workspace_root="~/.openclaw", workspace_root="~/.openclaw",
disable_device_pairing=disable_device_pairing, disable_device_pairing=disable_device_pairing,
allow_insecure_tls=allow_insecure_tls,
) )
@@ -43,6 +48,14 @@ def test_optional_gateway_client_config_maps_disable_device_pairing() -> None:
assert config.disable_device_pairing is False assert config.disable_device_pairing is False
def test_gateway_client_config_maps_allow_insecure_tls() -> None:
config = gateway_client_config(
_gateway(disable_device_pairing=False, allow_insecure_tls=True),
)
assert config.allow_insecure_tls is True
def test_optional_gateway_client_config_returns_none_for_missing_or_blank_url() -> None: def test_optional_gateway_client_config_returns_none_for_missing_or_blank_url() -> None:
assert optional_gateway_client_config(None) is None assert optional_gateway_client_config(None) is None
assert ( assert (
@@ -62,3 +75,28 @@ def test_to_resolve_query_keeps_gateway_disable_device_pairing_value() -> None:
) )
assert resolved.gateway_disable_device_pairing is True assert resolved.gateway_disable_device_pairing is True
def test_to_resolve_query_keeps_gateway_allow_insecure_tls_value() -> None:
resolved = GatewaySessionService.to_resolve_query(
board_id=None,
gateway_url="wss://gateway.example:18789/ws",
gateway_token="secret-token",
gateway_allow_insecure_tls=True,
)
assert resolved.gateway_allow_insecure_tls is True
@pytest.mark.asyncio
async def test_resolve_gateway_keeps_gateway_allow_insecure_tls_for_direct_url() -> None:
service = GatewaySessionService(session=object()) # type: ignore[arg-type]
_, config, _ = await service.resolve_gateway(
GatewayResolveQuery(
gateway_url="wss://gateway.example:18789/ws",
gateway_allow_insecure_tls=True,
),
user=None,
)
assert config.allow_insecure_tls is True

View File

@@ -12,7 +12,7 @@ export interface GatewayRead {
name: string; name: string;
url: string; url: string;
workspace_root: string; workspace_root: string;
allow_insecure_tls: boolean; allow_insecure_tls?: boolean;
disable_device_pairing?: boolean; disable_device_pairing?: boolean;
id: string; id: string;
organization_id: string; organization_id: string;

View File

@@ -10,4 +10,5 @@ export type GatewaysStatusApiV1GatewaysStatusGetParams = {
gateway_url?: string | null; gateway_url?: string | null;
gateway_token?: string | null; gateway_token?: string | null;
gateway_disable_device_pairing?: boolean; gateway_disable_device_pairing?: boolean;
gateway_allow_insecure_tls?: boolean;
}; };

View File

@@ -132,6 +132,7 @@ export default function EditGatewayPage() {
gatewayUrl: resolvedGatewayUrl, gatewayUrl: resolvedGatewayUrl,
gatewayToken: resolvedGatewayToken, gatewayToken: resolvedGatewayToken,
gatewayDisableDevicePairing: resolvedDisableDevicePairing, gatewayDisableDevicePairing: resolvedDisableDevicePairing,
gatewayAllowInsecureTls: resolvedAllowInsecureTls,
}); });
setGatewayCheckStatus(ok ? "success" : "error"); setGatewayCheckStatus(ok ? "success" : "error");
setGatewayCheckMessage(message); setGatewayCheckMessage(message);
@@ -205,7 +206,11 @@ export default function EditGatewayPage() {
setGatewayCheckMessage(null); setGatewayCheckMessage(null);
}} }}
onWorkspaceRootChange={setWorkspaceRoot} onWorkspaceRootChange={setWorkspaceRoot}
onAllowInsecureTlsChange={setAllowInsecureTls} onAllowInsecureTlsChange={(next) => {
setAllowInsecureTls(next);
setGatewayCheckStatus("idle");
setGatewayCheckMessage(null);
}}
/> />
</DashboardPageLayout> </DashboardPageLayout>
); );

View File

@@ -88,6 +88,7 @@ export default function NewGatewayPage() {
gatewayUrl, gatewayUrl,
gatewayToken, gatewayToken,
gatewayDisableDevicePairing: disableDevicePairing, gatewayDisableDevicePairing: disableDevicePairing,
gatewayAllowInsecureTls: allowInsecureTls,
}); });
setGatewayCheckStatus(ok ? "success" : "error"); setGatewayCheckStatus(ok ? "success" : "error");
setGatewayCheckMessage(message); setGatewayCheckMessage(message);
@@ -156,7 +157,11 @@ export default function NewGatewayPage() {
setGatewayCheckMessage(null); setGatewayCheckMessage(null);
}} }}
onWorkspaceRootChange={setWorkspaceRoot} onWorkspaceRootChange={setWorkspaceRoot}
onAllowInsecureTlsChange={setAllowInsecureTls} onAllowInsecureTlsChange={(next) => {
setAllowInsecureTls(next);
setGatewayCheckStatus("idle");
setGatewayCheckMessage(null);
}}
/> />
</DashboardPageLayout> </DashboardPageLayout>
); );

View File

@@ -150,21 +150,30 @@ export function GatewayForm({
</div> </div>
</div> </div>
<div className="flex items-center gap-2"> <div className="space-y-2">
<input <label className="text-sm font-medium text-slate-900">
type="checkbox" Allow self-signed TLS certificates
id="allow-insecure-tls" </label>
className="h-4 w-4 rounded border-slate-300 text-blue-600" <label className="flex h-10 items-center gap-3 px-1 text-sm text-slate-900">
checked={allowInsecureTls} <button
onChange={(event) => onAllowInsecureTlsChange(event.target.checked)} type="button"
disabled={isLoading} role="switch"
/> aria-checked={allowInsecureTls}
<label aria-label="Allow self-signed TLS certificates"
htmlFor="allow-insecure-tls" onClick={() => onAllowInsecureTlsChange(!allowInsecureTls)}
className="text-sm text-slate-700 cursor-pointer" disabled={isLoading}
> className={`inline-flex h-6 w-11 shrink-0 items-center rounded-full border transition ${
Allow self-signed TLS certificates (for localhost or trusted local allowInsecureTls
networks) ? "border-emerald-600 bg-emerald-600"
: "border-slate-300 bg-slate-200"
} ${isLoading ? "cursor-not-allowed opacity-60" : "cursor-pointer"}`}
>
<span
className={`inline-block h-5 w-5 rounded-full bg-white shadow-sm transition ${
allowInsecureTls ? "translate-x-5" : "translate-x-0.5"
}`}
/>
</button>
</label> </label>
</div> </div>

View File

@@ -29,7 +29,7 @@ describe("checkGatewayConnection", () => {
mockedGatewaysStatusApiV1GatewaysStatusGet.mockReset(); mockedGatewaysStatusApiV1GatewaysStatusGet.mockReset();
}); });
it("passes pairing toggle to gateway status API", async () => { it("passes pairing and TLS toggles to gateway status API", async () => {
mockedGatewaysStatusApiV1GatewaysStatusGet.mockResolvedValue({ mockedGatewaysStatusApiV1GatewaysStatusGet.mockResolvedValue({
status: 200, status: 200,
data: { connected: true }, data: { connected: true },
@@ -39,12 +39,14 @@ describe("checkGatewayConnection", () => {
gatewayUrl: "ws://gateway.example:18789", gatewayUrl: "ws://gateway.example:18789",
gatewayToken: "secret-token", gatewayToken: "secret-token",
gatewayDisableDevicePairing: true, gatewayDisableDevicePairing: true,
gatewayAllowInsecureTls: true,
}); });
expect(mockedGatewaysStatusApiV1GatewaysStatusGet).toHaveBeenCalledWith({ expect(mockedGatewaysStatusApiV1GatewaysStatusGet).toHaveBeenCalledWith({
gateway_url: "ws://gateway.example:18789", gateway_url: "ws://gateway.example:18789",
gateway_token: "secret-token", gateway_token: "secret-token",
gateway_disable_device_pairing: true, gateway_disable_device_pairing: true,
gateway_allow_insecure_tls: true,
}); });
expect(result).toEqual({ ok: true, message: "Gateway reachable." }); expect(result).toEqual({ ok: true, message: "Gateway reachable." });
}); });
@@ -62,6 +64,7 @@ describe("checkGatewayConnection", () => {
gatewayUrl: "ws://gateway.example:18789", gatewayUrl: "ws://gateway.example:18789",
gatewayToken: "", gatewayToken: "",
gatewayDisableDevicePairing: false, gatewayDisableDevicePairing: false,
gatewayAllowInsecureTls: false,
}); });
expect(result).toEqual({ ok: false, message: "missing required scope" }); expect(result).toEqual({ ok: false, message: "missing required scope" });

View File

@@ -25,15 +25,18 @@ export async function checkGatewayConnection(params: {
gatewayUrl: string; gatewayUrl: string;
gatewayToken: string; gatewayToken: string;
gatewayDisableDevicePairing: boolean; gatewayDisableDevicePairing: boolean;
gatewayAllowInsecureTls: boolean;
}): Promise<{ ok: boolean; message: string }> { }): Promise<{ ok: boolean; message: string }> {
try { try {
const requestParams: { const requestParams: {
gateway_url: string; gateway_url: string;
gateway_token?: string; gateway_token?: string;
gateway_disable_device_pairing: boolean; gateway_disable_device_pairing: boolean;
gateway_allow_insecure_tls: boolean;
} = { } = {
gateway_url: params.gatewayUrl.trim(), gateway_url: params.gatewayUrl.trim(),
gateway_disable_device_pairing: params.gatewayDisableDevicePairing, gateway_disable_device_pairing: params.gatewayDisableDevicePairing,
gateway_allow_insecure_tls: params.gatewayAllowInsecureTls,
}; };
if (params.gatewayToken.trim()) { if (params.gatewayToken.trim()) {
requestParams.gateway_token = params.gatewayToken.trim(); requestParams.gateway_token = params.gatewayToken.trim();