feat: add support for allowing self-signed TLS certificates in gateway configuration
This commit is contained in:
@@ -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,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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" });
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
Reference in New Issue
Block a user