Make dashboard work from proxied origins via same-origin proxy (#1111)
* Restore dashboard session proxy routes Change-Id: I36ffc3727ce44100121bc94a81510a5f009ee0bc Signed-off-by: Thomas Kosiewski <tk@coder.com> * Port dashboard frontend and docs Change-Id: I80356f64d618dab9d07b610ba67def14539f98ac Signed-off-by: Thomas Kosiewski <tk@coder.com> * docs: restore dashboard note in skill Change-Id: Id0913c64e7a6f2cbbfc429ef03b34dae185d8487 Signed-off-by: Thomas Kosiewski <tk@coder.com> * fix: tighten dashboard proxy same-origin checks Change-Id: I792bc859a24cd47314bd46c94344ef3dfb7d6db5 Signed-off-by: Thomas Kosiewski <tk@coder.com> --------- Signed-off-by: Thomas Kosiewski <tk@coder.com>
This commit is contained in:
@@ -5,6 +5,7 @@ import { useAtomValue, useSetAtom } from "jotai/react";
|
||||
import { ArrowLeft, ArrowRight, Camera, Circle, FileCode, Maximize, Moon, RotateCw, Smartphone, Square, Sun, Wifi, WifiOff } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { execCommand, sessionArgs } from "@/lib/exec";
|
||||
import { getSessionStreamUrl } from "@/lib/dashboard-routes";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import {
|
||||
@@ -550,7 +551,7 @@ export function Viewport() {
|
||||
</span>
|
||||
{browserConnected && (
|
||||
<span className="text-xs text-muted-foreground/60 font-mono">
|
||||
ws://localhost:{streamPort}
|
||||
{getSessionStreamUrl(streamPort)}
|
||||
</span>
|
||||
)}
|
||||
<div className="ml-auto flex items-center gap-2">
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* Centralized route building for dashboard API calls.
|
||||
* All routes stay on the current dashboard origin so the UI also works
|
||||
* behind forwarded or reverse-proxied URLs.
|
||||
*/
|
||||
|
||||
/** Build a dashboard API path such as "/api/sessions". */
|
||||
export function getDashboardApiPath(path: string): string {
|
||||
const normalizedPath = path.startsWith("/") ? path : `/${path}`;
|
||||
assertDashboardApiPath(normalizedPath);
|
||||
return normalizedPath;
|
||||
}
|
||||
|
||||
/** Build the same-origin per-session tabs endpoint proxied through the dashboard. */
|
||||
export function getSessionTabsPath(port: number): string {
|
||||
assertValidPort(port);
|
||||
return `/api/session/${port}/tabs`;
|
||||
}
|
||||
|
||||
/** Build the same-origin WebSocket URL for a session stream. */
|
||||
export function getSessionStreamUrl(port: number): string {
|
||||
assertValidPort(port);
|
||||
const streamPath = `/api/session/${port}/stream`;
|
||||
if (typeof window === "undefined") {
|
||||
return streamPath;
|
||||
}
|
||||
|
||||
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
||||
return `${protocol}//${window.location.host}${streamPath}`;
|
||||
}
|
||||
|
||||
function assertDashboardApiPath(path: string): asserts path is string {
|
||||
if (!path.startsWith("/api/")) {
|
||||
throw new Error(`Assertion failed: Expected dashboard API path, got: ${path}`);
|
||||
}
|
||||
}
|
||||
|
||||
function assertValidPort(port: number): asserts port is number {
|
||||
if (!Number.isInteger(port) || port <= 0 || port > 65535) {
|
||||
throw new Error(`Assertion failed: Invalid session port: ${port}`);
|
||||
}
|
||||
}
|
||||
@@ -5,20 +5,22 @@ import { useCallback, useEffect, useRef } from "react";
|
||||
import { useAtomCallback } from "jotai/utils";
|
||||
import type { SessionInfo } from "@/types";
|
||||
import { type ExecResult, execCommand, killSession, sessionArgs } from "@/lib/exec";
|
||||
import { getDashboardApiPath, getSessionTabsPath } from "@/lib/dashboard-routes";
|
||||
import { tabCacheAtom, engineCacheAtom } from "@/store/tabs";
|
||||
import { streamTabsAtom, streamEngineAtom } from "@/store/stream";
|
||||
|
||||
function getPort(): number {
|
||||
if (typeof window === "undefined") return 9223;
|
||||
if (typeof window === "undefined") return 0;
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const p = params.get("port");
|
||||
return p ? parseInt(p, 10) || 9223 : 9223;
|
||||
const portParam = params.get("port");
|
||||
const port = portParam ? Number.parseInt(portParam, 10) : 0;
|
||||
return Number.isInteger(port) && port > 0 ? port : 0;
|
||||
}
|
||||
|
||||
export const newSessionDialogAtom = atom(false);
|
||||
|
||||
function getSessionsUrl(): string {
|
||||
return "/api/sessions";
|
||||
return getDashboardApiPath("/api/sessions");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -254,9 +256,9 @@ export function useSessionsSync(pollInterval = 5000) {
|
||||
// Poll tabs for all sessions
|
||||
for (const s of data) {
|
||||
try {
|
||||
const tabsResp = await fetch(
|
||||
`http://localhost:${s.port}/api/tabs`,
|
||||
).catch(() => null);
|
||||
const tabsResp = await fetch(getSessionTabsPath(s.port)).catch(
|
||||
() => null,
|
||||
);
|
||||
if (tabsResp?.ok) {
|
||||
const tabs = await tabsResp.json();
|
||||
if (tabs.length > 0) {
|
||||
|
||||
@@ -9,7 +9,7 @@ import type {
|
||||
StreamMessage,
|
||||
TabInfo,
|
||||
} from "@/types";
|
||||
import { activePortAtom } from "@/store/sessions";
|
||||
import { getSessionStreamUrl } from "@/lib/dashboard-routes";
|
||||
import { tabCacheAtom, engineCacheAtom } from "@/store/tabs";
|
||||
|
||||
const MAX_EVENTS = 500;
|
||||
@@ -117,9 +117,10 @@ export function useStreamSync(port: number) {
|
||||
}, [port, setConnected, setBrowserConnected, setScreencasting, setRecording, setVpWidth, setVpHeight, setFrame, setEvents, setConsoleLogs, setTabs, setEngine]);
|
||||
|
||||
const connect = useCallback(() => {
|
||||
if (port <= 0) return;
|
||||
if (wsRef.current?.readyState === WebSocket.OPEN) return;
|
||||
|
||||
const ws = new WebSocket(`ws://localhost:${port}`);
|
||||
const ws = new WebSocket(getSessionStreamUrl(port));
|
||||
wsRef.current = ws;
|
||||
setWsRef(ws);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user