mirror of
https://github.com/bunny-lab-io/Borealis.git
synced 2026-02-06 14:40:32 -07:00
Additional VNC WebUI Changes
This commit is contained in:
@@ -61,12 +61,44 @@ def _find_project_root() -> Optional[Path]:
|
|||||||
|
|
||||||
|
|
||||||
def _resolve_vnc_root() -> Optional[Path]:
|
def _resolve_vnc_root() -> Optional[Path]:
|
||||||
|
override = os.environ.get("BOREALIS_VNC_ROOT") or os.environ.get("BOREALIS_ULTRAVNC_ROOT")
|
||||||
|
if override:
|
||||||
|
try:
|
||||||
|
override_path = Path(override).expanduser().resolve()
|
||||||
|
if override_path.is_dir():
|
||||||
|
return override_path
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
root = _find_project_root()
|
root = _find_project_root()
|
||||||
if not root:
|
candidates: list[Path] = []
|
||||||
return None
|
if root:
|
||||||
candidate = root / "Dependencies" / "UltraVNC_Server"
|
candidates.append(root / "Dependencies" / "UltraVNC_Server")
|
||||||
if candidate.is_dir():
|
candidates.append(root / "UltraVNC_Server")
|
||||||
return candidate
|
try:
|
||||||
|
current = Path(__file__).resolve()
|
||||||
|
for parent in (current, *current.parents):
|
||||||
|
candidates.append(parent / "Dependencies" / "UltraVNC_Server")
|
||||||
|
candidates.append(parent / "UltraVNC_Server")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
cwd = Path.cwd().resolve()
|
||||||
|
for parent in (cwd, *cwd.parents):
|
||||||
|
candidates.append(parent / "Dependencies" / "UltraVNC_Server")
|
||||||
|
candidates.append(parent / "UltraVNC_Server")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
seen = set()
|
||||||
|
for candidate in candidates:
|
||||||
|
try:
|
||||||
|
resolved = candidate.resolve()
|
||||||
|
except Exception:
|
||||||
|
resolved = candidate
|
||||||
|
if resolved in seen:
|
||||||
|
continue
|
||||||
|
seen.add(resolved)
|
||||||
|
if candidate.is_dir():
|
||||||
|
return candidate
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@@ -137,20 +169,46 @@ def _resolve_vnc_password_tool(root: Optional[Path]) -> Optional[str]:
|
|||||||
vnc_root / "tools" / "createpassword.exe",
|
vnc_root / "tools" / "createpassword.exe",
|
||||||
vnc_root / "createpassword64.exe",
|
vnc_root / "createpassword64.exe",
|
||||||
vnc_root / "tools" / "createpassword64.exe",
|
vnc_root / "tools" / "createpassword64.exe",
|
||||||
|
vnc_root / "payload" / "x64" / "createpassword.exe",
|
||||||
|
vnc_root / "payload" / "x64" / "createpassword64.exe",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
for candidate in candidates:
|
for candidate in candidates:
|
||||||
if candidate.is_file():
|
if candidate.is_file():
|
||||||
return str(candidate)
|
return str(candidate)
|
||||||
try:
|
try:
|
||||||
for candidate in root.rglob("createpassword.exe"):
|
if root:
|
||||||
if candidate.is_file():
|
for candidate in root.rglob("createpassword.exe"):
|
||||||
return str(candidate)
|
if candidate.is_file():
|
||||||
|
return str(candidate)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _discover_ultravnc_service_name() -> Optional[str]:
|
||||||
|
if os.name != "nt":
|
||||||
|
return None
|
||||||
|
command = (
|
||||||
|
"Get-Service -ErrorAction SilentlyContinue | "
|
||||||
|
"Where-Object { $_.Name -like '*uvnc*' -or $_.DisplayName -like '*UltraVNC*' } | "
|
||||||
|
"Select-Object -First 1 -ExpandProperty Name"
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["powershell.exe", "-NoProfile", "-Command", command],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
check=False,
|
||||||
|
)
|
||||||
|
output = (result.stdout or "").strip()
|
||||||
|
if output:
|
||||||
|
return output.splitlines()[0].strip()
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _ensure_ultravnc_ini(config_dir: Path, port: int) -> Optional[Path]:
|
def _ensure_ultravnc_ini(config_dir: Path, port: int) -> Optional[Path]:
|
||||||
try:
|
try:
|
||||||
config_dir.mkdir(parents=True, exist_ok=True)
|
config_dir.mkdir(parents=True, exist_ok=True)
|
||||||
@@ -195,13 +253,14 @@ class VncManager:
|
|||||||
self._last_password: Optional[str] = None
|
self._last_password: Optional[str] = None
|
||||||
self._vnc_exe = _resolve_vnc_exe()
|
self._vnc_exe = _resolve_vnc_exe()
|
||||||
self._password_tool: Optional[str] = None
|
self._password_tool: Optional[str] = None
|
||||||
|
self._service_name: Optional[str] = None
|
||||||
|
|
||||||
def _service_state(self) -> Optional[str]:
|
def _service_state_by_name(self, service_name: str) -> Optional[str]:
|
||||||
if os.name != "nt":
|
if os.name != "nt":
|
||||||
return None
|
return None
|
||||||
try:
|
try:
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
["sc.exe", "query", ULTRAVNC_SERVICE_NAME],
|
["sc.exe", "query", service_name],
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
text=True,
|
text=True,
|
||||||
check=False,
|
check=False,
|
||||||
@@ -217,25 +276,62 @@ class VncManager:
|
|||||||
return None
|
return None
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def _resolve_service_name(self, *, refresh: bool = False) -> Optional[str]:
|
||||||
|
if self._service_name and not refresh:
|
||||||
|
return self._service_name
|
||||||
|
candidate = ULTRAVNC_SERVICE_NAME
|
||||||
|
if candidate:
|
||||||
|
state = self._service_state_by_name(candidate)
|
||||||
|
if state is not None:
|
||||||
|
self._service_name = candidate
|
||||||
|
return candidate
|
||||||
|
discovered = _discover_ultravnc_service_name()
|
||||||
|
if discovered:
|
||||||
|
self._service_name = discovered
|
||||||
|
return discovered
|
||||||
|
if candidate:
|
||||||
|
self._service_name = candidate
|
||||||
|
return candidate
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _wait_for_service(self, service_name: str, timeout: float = 5.0) -> bool:
|
||||||
|
deadline = time.time() + max(1.0, timeout)
|
||||||
|
while time.time() < deadline:
|
||||||
|
state = self._service_state_by_name(service_name)
|
||||||
|
if state == "RUNNING":
|
||||||
|
return True
|
||||||
|
if state is None:
|
||||||
|
return False
|
||||||
|
time.sleep(0.5)
|
||||||
|
return False
|
||||||
|
|
||||||
def _restart_service(self) -> None:
|
def _restart_service(self) -> None:
|
||||||
if os.name != "nt":
|
if os.name != "nt":
|
||||||
return
|
return
|
||||||
state = self._service_state()
|
service_name = self._resolve_service_name()
|
||||||
|
if not service_name:
|
||||||
|
return
|
||||||
|
state = self._service_state_by_name(service_name)
|
||||||
if state != "RUNNING":
|
if state != "RUNNING":
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
subprocess.run(["sc.exe", "stop", ULTRAVNC_SERVICE_NAME], capture_output=True, text=True, check=False)
|
subprocess.run(["sc.exe", "stop", service_name], capture_output=True, text=True, check=False)
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
subprocess.run(["sc.exe", "start", ULTRAVNC_SERVICE_NAME], capture_output=True, text=True, check=False)
|
subprocess.run(["sc.exe", "start", service_name], capture_output=True, text=True, check=False)
|
||||||
|
if not self._wait_for_service(service_name, timeout=8.0):
|
||||||
|
_write_log(f"UltraVNC service restart timed out (service={service_name}).")
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
_write_log(f"Failed to restart UltraVNC service: {exc}")
|
_write_log(f"Failed to restart UltraVNC service: {exc}")
|
||||||
|
|
||||||
def _ensure_service_running(self) -> bool:
|
def _ensure_service_running(self) -> bool:
|
||||||
if os.name != "nt":
|
if os.name != "nt":
|
||||||
return False
|
return False
|
||||||
state = self._service_state()
|
service_name = self._resolve_service_name()
|
||||||
|
state = self._service_state_by_name(service_name) if service_name else None
|
||||||
if state == "RUNNING":
|
if state == "RUNNING":
|
||||||
return True
|
return True
|
||||||
|
if state == "START_PENDING" and service_name:
|
||||||
|
return self._wait_for_service(service_name, timeout=10.0)
|
||||||
if not self._vnc_exe:
|
if not self._vnc_exe:
|
||||||
self._vnc_exe = _resolve_vnc_exe()
|
self._vnc_exe = _resolve_vnc_exe()
|
||||||
if not self._vnc_exe:
|
if not self._vnc_exe:
|
||||||
@@ -243,17 +339,28 @@ class VncManager:
|
|||||||
try:
|
try:
|
||||||
if state is None:
|
if state is None:
|
||||||
subprocess.run([self._vnc_exe, "-install"], capture_output=True, text=True, check=False)
|
subprocess.run([self._vnc_exe, "-install"], capture_output=True, text=True, check=False)
|
||||||
|
service_name = self._resolve_service_name(refresh=True)
|
||||||
|
if not service_name:
|
||||||
|
service_name = ULTRAVNC_SERVICE_NAME
|
||||||
subprocess.run(
|
subprocess.run(
|
||||||
["sc.exe", "config", ULTRAVNC_SERVICE_NAME, "start=", "auto"],
|
["sc.exe", "config", service_name, "start=", "auto"],
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
text=True,
|
text=True,
|
||||||
check=False,
|
check=False,
|
||||||
)
|
)
|
||||||
subprocess.run(["sc.exe", "start", ULTRAVNC_SERVICE_NAME], capture_output=True, text=True, check=False)
|
start_result = subprocess.run(
|
||||||
|
["sc.exe", "start", service_name],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
check=False,
|
||||||
|
)
|
||||||
|
start_output = (start_result.stdout or "") + (start_result.stderr or "")
|
||||||
|
if "SERVICE_ALREADY_RUNNING" in start_output:
|
||||||
|
return True
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
_write_log(f"Failed to ensure UltraVNC service running: {exc}")
|
_write_log(f"Failed to ensure UltraVNC service running: {exc}")
|
||||||
return False
|
return False
|
||||||
return self._service_state() == "RUNNING"
|
return self._wait_for_service(service_name, timeout=10.0)
|
||||||
|
|
||||||
def _normalize_firewall_remote(self, allowed_ips: Optional[str]) -> Optional[str]:
|
def _normalize_firewall_remote(self, allowed_ips: Optional[str]) -> Optional[str]:
|
||||||
if not allowed_ips:
|
if not allowed_ips:
|
||||||
@@ -321,7 +428,10 @@ class VncManager:
|
|||||||
if not self._password_tool:
|
if not self._password_tool:
|
||||||
self._password_tool = _resolve_vnc_password_tool(config_dir)
|
self._password_tool = _resolve_vnc_password_tool(config_dir)
|
||||||
if not self._password_tool:
|
if not self._password_tool:
|
||||||
_write_log("VNC password tool not found; expected createpassword.exe under Dependencies/UltraVNC_Server.")
|
_write_log(
|
||||||
|
"VNC password tool not found; expected createpassword.exe under "
|
||||||
|
"Dependencies/UltraVNC_Server/tools or payload."
|
||||||
|
)
|
||||||
return None
|
return None
|
||||||
try:
|
try:
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
@@ -354,7 +464,10 @@ class VncManager:
|
|||||||
if not self._vnc_exe:
|
if not self._vnc_exe:
|
||||||
self._vnc_exe = _resolve_vnc_exe()
|
self._vnc_exe = _resolve_vnc_exe()
|
||||||
if not self._vnc_exe:
|
if not self._vnc_exe:
|
||||||
_write_log("UltraVNC server binary not found; expected under Dependencies/UltraVNC_Server.")
|
_write_log(
|
||||||
|
"UltraVNC server binary not found; expected under "
|
||||||
|
"Dependencies/UltraVNC_Server/payload (or set BOREALIS_VNC_SERVER_BIN)."
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
exe_path = Path(self._vnc_exe)
|
exe_path = Path(self._vnc_exe)
|
||||||
|
|||||||
@@ -4,16 +4,19 @@
|
|||||||
#
|
#
|
||||||
# API Endpoints (if applicable):
|
# API Endpoints (if applicable):
|
||||||
# - GET /api/server/time (Operator Session) - Returns the server clock in multiple formats.
|
# - GET /api/server/time (Operator Session) - Returns the server clock in multiple formats.
|
||||||
|
# - GET /api/server/certificates/root (Operator Session) - Downloads the Borealis root CA certificate.
|
||||||
# ======================================================
|
# ======================================================
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from typing import TYPE_CHECKING, Any, Dict
|
from pathlib import Path
|
||||||
|
from typing import TYPE_CHECKING, Any, Dict, Optional
|
||||||
|
|
||||||
from flask import Blueprint, Flask, jsonify
|
from flask import Blueprint, Flask, jsonify, send_file
|
||||||
|
|
||||||
from ...auth import RequestAuthContext
|
from ...auth import RequestAuthContext
|
||||||
|
from ....security import certificates
|
||||||
|
|
||||||
if TYPE_CHECKING: # pragma: no cover - typing aide
|
if TYPE_CHECKING: # pragma: no cover - typing aide
|
||||||
from .. import EngineServiceAdapters
|
from .. import EngineServiceAdapters
|
||||||
@@ -33,6 +36,29 @@ def _serialize_time(now_local: datetime, now_utc: datetime) -> Dict[str, Any]:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_root_ca_path(adapters: "EngineServiceAdapters") -> Optional[Path]:
|
||||||
|
candidates = []
|
||||||
|
try:
|
||||||
|
candidates.append(certificates.engine_certificates_root() / "borealis-root-ca.pem")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
cert_path = getattr(adapters.context, "tls_cert_path", None)
|
||||||
|
if cert_path:
|
||||||
|
try:
|
||||||
|
candidates.append(Path(str(cert_path)).expanduser().resolve().parent / "borealis-root-ca.pem")
|
||||||
|
except Exception:
|
||||||
|
candidates.append(Path(str(cert_path)).parent / "borealis-root-ca.pem")
|
||||||
|
|
||||||
|
for candidate in candidates:
|
||||||
|
try:
|
||||||
|
if candidate and candidate.is_file():
|
||||||
|
return candidate
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def register_info(app: Flask, adapters: "EngineServiceAdapters") -> None:
|
def register_info(app: Flask, adapters: "EngineServiceAdapters") -> None:
|
||||||
"""Expose server telemetry endpoints used by the admin interface."""
|
"""Expose server telemetry endpoints used by the admin interface."""
|
||||||
|
|
||||||
@@ -54,4 +80,20 @@ def register_info(app: Flask, adapters: "EngineServiceAdapters") -> None:
|
|||||||
payload = _serialize_time(now_local, now_utc)
|
payload = _serialize_time(now_local, now_utc)
|
||||||
return jsonify(payload)
|
return jsonify(payload)
|
||||||
|
|
||||||
|
@blueprint.route("/api/server/certificates/root", methods=["GET"])
|
||||||
|
def server_root_ca() -> Any:
|
||||||
|
_, error = auth.require_user()
|
||||||
|
if error:
|
||||||
|
return jsonify(error[0]), error[1]
|
||||||
|
path = _resolve_root_ca_path(adapters)
|
||||||
|
if not path:
|
||||||
|
return jsonify({"error": "root_ca_missing"}), 404
|
||||||
|
return send_file(
|
||||||
|
str(path),
|
||||||
|
mimetype="application/x-pem-file",
|
||||||
|
as_attachment=True,
|
||||||
|
download_name="borealis-root-ca.pem",
|
||||||
|
max_age=0,
|
||||||
|
)
|
||||||
|
|
||||||
app.register_blueprint(blueprint)
|
app.register_blueprint(blueprint)
|
||||||
|
|||||||
@@ -152,15 +152,19 @@ class VncProxyServer:
|
|||||||
self._ready.set()
|
self._ready.set()
|
||||||
await server.wait_closed()
|
await server.wait_closed()
|
||||||
|
|
||||||
async def _handle_client(self, websocket, path: str) -> None:
|
async def _handle_client(self, websocket, path: Optional[str] = None) -> None:
|
||||||
parsed = urlsplit(path)
|
raw_path = path or getattr(websocket, "path", "") or ""
|
||||||
|
parsed = urlsplit(raw_path)
|
||||||
if parsed.path != VNC_WS_PATH:
|
if parsed.path != VNC_WS_PATH:
|
||||||
|
self.logger.warning("VNC proxy rejected request with invalid path: %s", raw_path)
|
||||||
await websocket.close(code=1008, reason="invalid_path")
|
await websocket.close(code=1008, reason="invalid_path")
|
||||||
return
|
return
|
||||||
query = parse_qs(parsed.query or "")
|
query = parse_qs(parsed.query or "")
|
||||||
token = (query.get("token") or [""])[0]
|
token = (query.get("token") or [""])[0]
|
||||||
session = self.registry.consume(token)
|
session = self.registry.consume(token)
|
||||||
if not session:
|
if not session:
|
||||||
|
token_hint = token[:8] if token else "-"
|
||||||
|
self.logger.warning("VNC proxy rejected session (token=%s)", token_hint)
|
||||||
await websocket.close(code=1008, reason="invalid_session")
|
await websocket.close(code=1008, reason="invalid_session")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ import {
|
|||||||
StopRounded as StopIcon,
|
StopRounded as StopIcon,
|
||||||
LinkRounded as LinkIcon,
|
LinkRounded as LinkIcon,
|
||||||
LanRounded as IpIcon,
|
LanRounded as IpIcon,
|
||||||
|
SecurityRounded as SecurityIcon,
|
||||||
|
OpenInNewRounded as OpenIcon,
|
||||||
|
DownloadRounded as DownloadIcon,
|
||||||
} from "@mui/icons-material";
|
} from "@mui/icons-material";
|
||||||
import RFB from "@novnc/novnc/lib/rfb";
|
import RFB from "@novnc/novnc/lib/rfb";
|
||||||
|
|
||||||
@@ -46,15 +49,36 @@ function normalizeText(value) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildCertHelp(wsUrl) {
|
||||||
|
if (!wsUrl) return null;
|
||||||
|
try {
|
||||||
|
const parsed = new URL(wsUrl);
|
||||||
|
const isSecure = parsed.protocol === "wss:";
|
||||||
|
const trustScheme = isSecure ? "https:" : "http:";
|
||||||
|
return {
|
||||||
|
wsUrl,
|
||||||
|
isSecure,
|
||||||
|
host: parsed.host,
|
||||||
|
trustUrl: `${trustScheme}//${parsed.host}/`,
|
||||||
|
trustCheck: "unknown",
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default function ReverseTunnelVnc({ device }) {
|
export default function ReverseTunnelVnc({ device }) {
|
||||||
const [sessionState, setSessionState] = useState("idle");
|
const [sessionState, setSessionState] = useState("idle");
|
||||||
const [statusMessage, setStatusMessage] = useState("");
|
const [statusMessage, setStatusMessage] = useState("");
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [tunnel, setTunnel] = useState(null);
|
const [tunnel, setTunnel] = useState(null);
|
||||||
|
const [certHelp, setCertHelp] = useState(null);
|
||||||
const containerRef = useRef(null);
|
const containerRef = useRef(null);
|
||||||
const displayRef = useRef(null);
|
const displayRef = useRef(null);
|
||||||
const rfbRef = useRef(null);
|
const rfbRef = useRef(null);
|
||||||
const agentIdRef = useRef("");
|
const agentIdRef = useRef("");
|
||||||
|
const certProbeRef = useRef("");
|
||||||
|
const certDownloadUrl = "/api/server/certificates/root";
|
||||||
|
|
||||||
const agentId = useMemo(() => {
|
const agentId = useMemo(() => {
|
||||||
return (
|
return (
|
||||||
@@ -73,6 +97,17 @@ export default function ReverseTunnelVnc({ device }) {
|
|||||||
agentIdRef.current = agentId;
|
agentIdRef.current = agentId;
|
||||||
}, [agentId]);
|
}, [agentId]);
|
||||||
|
|
||||||
|
const probeCertificateTrust = useCallback(async (info) => {
|
||||||
|
if (!info || !info.isSecure || !info.trustUrl) return;
|
||||||
|
if (certProbeRef.current === info.trustUrl) return;
|
||||||
|
certProbeRef.current = info.trustUrl;
|
||||||
|
try {
|
||||||
|
await fetch(info.trustUrl, { mode: "no-cors", cache: "no-store" });
|
||||||
|
setCertHelp((prev) => (prev ? { ...prev, trustCheck: "ok" } : prev));
|
||||||
|
} catch {
|
||||||
|
setCertHelp((prev) => (prev ? { ...prev, trustCheck: "blocked" } : prev));
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
const notifyAgentOnboarding = useCallback(async () => {
|
const notifyAgentOnboarding = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
@@ -138,6 +173,7 @@ export default function ReverseTunnelVnc({ device }) {
|
|||||||
await disconnectVnc("operator_disconnect");
|
await disconnectVnc("operator_disconnect");
|
||||||
} finally {
|
} finally {
|
||||||
setTunnel(null);
|
setTunnel(null);
|
||||||
|
setCertHelp(null);
|
||||||
setSessionState("idle");
|
setSessionState("idle");
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -194,6 +230,9 @@ export default function ReverseTunnelVnc({ device }) {
|
|||||||
if (!token || !wsUrl) {
|
if (!token || !wsUrl) {
|
||||||
throw new Error("VNC session unavailable.");
|
throw new Error("VNC session unavailable.");
|
||||||
}
|
}
|
||||||
|
const help = buildCertHelp(wsUrl);
|
||||||
|
setCertHelp(help);
|
||||||
|
probeCertificateTrust(help);
|
||||||
const tunnelUrl = `${wsUrl}?token=${encodeURIComponent(token)}`;
|
const tunnelUrl = `${wsUrl}?token=${encodeURIComponent(token)}`;
|
||||||
const displayHost = displayRef.current;
|
const displayHost = displayRef.current;
|
||||||
if (!displayHost) {
|
if (!displayHost) {
|
||||||
@@ -231,12 +270,13 @@ export default function ReverseTunnelVnc({ device }) {
|
|||||||
|
|
||||||
rfbRef.current = rfb;
|
rfbRef.current = rfb;
|
||||||
setStatusMessage("Establishing VNC...");
|
setStatusMessage("Establishing VNC...");
|
||||||
}, []);
|
}, [probeCertificateTrust]);
|
||||||
|
|
||||||
const handleConnect = useCallback(async () => {
|
const handleConnect = useCallback(async () => {
|
||||||
if (sessionState === "connected") return;
|
if (sessionState === "connected") return;
|
||||||
setStatusMessage("");
|
setStatusMessage("");
|
||||||
setSessionState("connecting");
|
setSessionState("connecting");
|
||||||
|
certProbeRef.current = "";
|
||||||
try {
|
try {
|
||||||
const sessionData = await requestTunnel();
|
const sessionData = await requestTunnel();
|
||||||
if (!sessionData) {
|
if (!sessionData) {
|
||||||
@@ -250,6 +290,8 @@ export default function ReverseTunnelVnc({ device }) {
|
|||||||
}, [openVncSession, requestTunnel, sessionState]);
|
}, [openVncSession, requestTunnel, sessionState]);
|
||||||
|
|
||||||
const isConnected = sessionState === "connected";
|
const isConnected = sessionState === "connected";
|
||||||
|
const showCertHelp =
|
||||||
|
certHelp?.isSecure && (certHelp.trustCheck === "blocked" || sessionState === "error");
|
||||||
const sessionChips = [
|
const sessionChips = [
|
||||||
tunnel?.tunnel_id
|
tunnel?.tunnel_id
|
||||||
? {
|
? {
|
||||||
@@ -336,6 +378,62 @@ export default function ReverseTunnelVnc({ device }) {
|
|||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
{showCertHelp ? (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
borderRadius: 2,
|
||||||
|
border: `1px solid ${MAGIC_UI.panelBorder}`,
|
||||||
|
backgroundColor: "rgba(10,16,30,0.75)",
|
||||||
|
p: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stack spacing={1}>
|
||||||
|
<Stack direction="row" spacing={1} alignItems="center">
|
||||||
|
<SecurityIcon sx={{ color: MAGIC_UI.accentC, fontSize: 20 }} />
|
||||||
|
<Typography variant="body2" sx={{ color: MAGIC_UI.textBright, fontWeight: 600 }}>
|
||||||
|
VNC proxy certificate not trusted
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
<Typography variant="body2" sx={{ color: MAGIC_UI.textMuted }}>
|
||||||
|
Your browser blocked the secure VNC WebSocket. Trust the Borealis root CA (or use a
|
||||||
|
publicly trusted certificate) so the VNC proxy can connect.
|
||||||
|
</Typography>
|
||||||
|
<Stack direction={{ xs: "column", sm: "row" }} spacing={1}>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
startIcon={<OpenIcon />}
|
||||||
|
sx={gradientButtonSx}
|
||||||
|
onClick={() => {
|
||||||
|
if (certHelp?.trustUrl) {
|
||||||
|
window.open(certHelp.trustUrl, "_blank", "noreferrer");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Open VNC Proxy
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
startIcon={<DownloadIcon />}
|
||||||
|
sx={{
|
||||||
|
...gradientButtonSx,
|
||||||
|
backgroundImage: "linear-gradient(135deg,#34d399,#7dd3fc)",
|
||||||
|
}}
|
||||||
|
component="a"
|
||||||
|
href={certDownloadUrl}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
Download Root CA
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
<Typography variant="caption" sx={{ color: MAGIC_UI.textMuted }}>
|
||||||
|
After downloading, install the root CA into the OS trusted root store and refresh the
|
||||||
|
page. If you are behind a corporate CA, install that CA instead.
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<Stack spacing={0.3} sx={{ mt: 1 }}>
|
<Stack spacing={0.3} sx={{ mt: 1 }}>
|
||||||
<Typography variant="body2" sx={{ color: MAGIC_UI.textMuted }}>
|
<Typography variant="body2" sx={{ color: MAGIC_UI.textMuted }}>
|
||||||
Session: {isConnected ? "Active" : sessionState}
|
Session: {isConnected ? "Active" : sessionState}
|
||||||
|
|||||||
@@ -118,6 +118,7 @@ Provide a consolidated, human-readable list of Borealis Engine API endpoints gro
|
|||||||
|
|
||||||
### Server Info and Logs
|
### Server Info and Logs
|
||||||
- `GET /api/server/time` (Operator Session) - server clock.
|
- `GET /api/server/time` (Operator Session) - server clock.
|
||||||
|
- `GET /api/server/certificates/root` (Operator Session) - download Borealis root CA certificate.
|
||||||
- `GET /api/server/logs` (Admin) - list logs and retention.
|
- `GET /api/server/logs` (Admin) - list logs and retention.
|
||||||
- `GET /api/server/logs/<log_name>/entries` (Admin) - tail log lines.
|
- `GET /api/server/logs/<log_name>/entries` (Admin) - tail log lines.
|
||||||
- `PUT /api/server/logs/retention` (Admin) - update retention policies.
|
- `PUT /api/server/logs/retention` (Admin) - update retention policies.
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ Explain the Borealis trust model, enrollment security, token handling, and code
|
|||||||
### Overall
|
### Overall
|
||||||
- Borealis enforces mutual trust: each agent presents a unique Ed25519 identity to the server, the server issues EdDSA-signed (Ed25519) access tokens bound to that fingerprint, and both sides pin the generated Borealis root CA.
|
- Borealis enforces mutual trust: each agent presents a unique Ed25519 identity to the server, the server issues EdDSA-signed (Ed25519) access tokens bound to that fingerprint, and both sides pin the generated Borealis root CA.
|
||||||
- End-to-end TLS everywhere: the Engine auto-provisions an ECDSA P-384 root + leaf chain under `Engine/Certificates` and serves TLS using Python defaults (TLS 1.2+); agents pin the delivered bundle for both REST and WebSocket traffic to eliminate man-in-the-middle avenues.
|
- End-to-end TLS everywhere: the Engine auto-provisions an ECDSA P-384 root + leaf chain under `Engine/Certificates` and serves TLS using Python defaults (TLS 1.2+); agents pin the delivered bundle for both REST and WebSocket traffic to eliminate man-in-the-middle avenues.
|
||||||
|
- Operators can download the Borealis root CA via `GET /api/server/certificates/root` to trust the WebUI and VNC proxy in browsers.
|
||||||
- Device enrollment is gated by enrollment and installer codes (configurable expiration and usage limits) and an operator approval queue; replay-resistant nonces plus rate limits (40 req/min/IP, 12 req/min/fingerprint) prevent brute force or code reuse.
|
- Device enrollment is gated by enrollment and installer codes (configurable expiration and usage limits) and an operator approval queue; replay-resistant nonces plus rate limits (40 req/min/IP, 12 req/min/fingerprint) prevent brute force or code reuse.
|
||||||
- All device APIs require Authorization: Bearer headers and a service-context marker (SYSTEM or CURRENTUSER); missing, expired, mismatched, or revoked credentials are rejected before any business logic runs. Operator-driven revoking and device quarantining are not yet implemented.
|
- All device APIs require Authorization: Bearer headers and a service-context marker (SYSTEM or CURRENTUSER); missing, expired, mismatched, or revoked credentials are rejected before any business logic runs. Operator-driven revoking and device quarantining are not yet implemented.
|
||||||
- Replay and credential theft defenses layer in DPoP proof validation (thumbprint binding) on the server side and short-lived access tokens (about 15 minutes) with 90-day refresh tokens hashed via SHA-256.
|
- Replay and credential theft defenses layer in DPoP proof validation (thumbprint binding) on the server side and short-lived access tokens (about 15 minutes) with 90-day refresh tokens hashed via SHA-256.
|
||||||
|
|||||||
Reference in New Issue
Block a user