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:
Thomas Kosiewski
2026-05-07 09:08:12 -05:00
committed by GitHub
parent 3bb1d43f8b
commit d33bdb36f3
10 changed files with 776 additions and 54 deletions
@@ -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}`);
}
}
+9 -7
View File
@@ -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) {
+3 -2
View File
@@ -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);