mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-23 09:07:46 +02:00
fix: avoid wildcard websocket hosts
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
3463456bee
commit
996c0e95e0
7 files changed
+104
-8
No files matched your search
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { resolveViteHmrPort } from "../app.ts";
|
||||
import { resolveViteHmrHost, resolveViteHmrPort } from "../app.ts";
|
||||
|
||||
describe("resolveViteHmrPort", () => {
|
||||
it("uses serverPort + 10000 when the result stays in range", () => {
|
||||
@@ -17,3 +17,15 @@ describe("resolveViteHmrPort", () => {
|
||||
expect(resolveViteHmrPort(9_000)).toBe(19_000);
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveViteHmrHost", () => {
|
||||
it("omits wildcard bind hosts so Vite uses the browser hostname", () => {
|
||||
expect(resolveViteHmrHost("0.0.0.0")).toBeUndefined();
|
||||
expect(resolveViteHmrHost("::")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("keeps concrete bind hosts", () => {
|
||||
expect(resolveViteHmrHost("127.0.0.1")).toBe("127.0.0.1");
|
||||
expect(resolveViteHmrHost("paperclip-dev")).toBe("paperclip-dev");
|
||||
});
|
||||
});
|
||||
+8
-1
@@ -96,6 +96,12 @@ export function resolveViteHmrPort(serverPort: number): number {
|
||||
return Math.max(1_024, serverPort - 10_000);
|
||||
}
|
||||
|
||||
export function resolveViteHmrHost(bindHost: string): string | undefined {
|
||||
const normalized = bindHost.trim().toLowerCase();
|
||||
if (normalized === "0.0.0.0" || normalized === "::") return undefined;
|
||||
return bindHost;
|
||||
}
|
||||
|
||||
export function shouldServeViteDevHtml(req: ExpressRequest): boolean {
|
||||
const pathname = req.path;
|
||||
if (VITE_DEV_STATIC_PATHS.has(pathname)) return false;
|
||||
@@ -373,6 +379,7 @@ export async function createApp(
|
||||
const uiRoot = path.resolve(__dirname, "../../ui");
|
||||
const publicUiRoot = path.resolve(uiRoot, "public");
|
||||
const hmrPort = resolveViteHmrPort(opts.serverPort);
|
||||
const hmrHost = resolveViteHmrHost(opts.bindHost);
|
||||
const { createServer: createViteServer } = await import("vite");
|
||||
const vite = await createViteServer({
|
||||
root: uiRoot,
|
||||
@@ -380,7 +387,7 @@ export async function createApp(
|
||||
server: {
|
||||
middlewareMode: true,
|
||||
hmr: {
|
||||
host: opts.bindHost,
|
||||
...(hmrHost ? { host: hmrHost } : {}),
|
||||
port: hmrPort,
|
||||
clientPort: hmrPort,
|
||||
},
|
||||
|
||||
@@ -6,6 +6,7 @@ import { instanceSettingsApi } from "../../api/instanceSettings";
|
||||
import { heartbeatsApi } from "../../api/heartbeats";
|
||||
import { buildTranscript, getUIAdapter, onAdapterChange, type RunLogChunk, type TranscriptEntry } from "../../adapters";
|
||||
import { queryKeys } from "../../lib/queryKeys";
|
||||
import { buildSameOriginWebSocketUrl } from "../../lib/websocket-url";
|
||||
|
||||
const LOG_POLL_INTERVAL_MS = 2000;
|
||||
const LOG_READ_LIMIT_BYTES = 256_000;
|
||||
@@ -279,8 +280,9 @@ export function useLiveRunTranscripts({
|
||||
|
||||
const connect = () => {
|
||||
if (closed) return;
|
||||
const protocol = window.location.protocol === "https:" ? "wss" : "ws";
|
||||
const url = `${protocol}://${window.location.host}/api/companies/${encodeURIComponent(companyId)}/events/ws`;
|
||||
const url = buildSameOriginWebSocketUrl(
|
||||
`/api/companies/${encodeURIComponent(companyId)}/events/ws`,
|
||||
);
|
||||
socket = new WebSocket(url);
|
||||
|
||||
socket.onmessage = (message) => {
|
||||
|
||||
@@ -14,6 +14,7 @@ import { clearIssueExecutionRun, removeLiveRunById } from "../lib/optimistic-iss
|
||||
import { queryKeys } from "../lib/queryKeys";
|
||||
import { toCompanyRelativePath } from "../lib/company-routes";
|
||||
import { useLocation } from "../lib/router";
|
||||
import { buildSameOriginWebSocketUrl } from "../lib/websocket-url";
|
||||
|
||||
const TOAST_COOLDOWN_WINDOW_MS = 10_000;
|
||||
const TOAST_COOLDOWN_MAX = 3;
|
||||
@@ -979,8 +980,9 @@ export function LiveUpdatesProvider({ children }: { children: ReactNode }) {
|
||||
|
||||
const connect = () => {
|
||||
if (closed) return;
|
||||
const protocol = window.location.protocol === "https:" ? "wss" : "ws";
|
||||
const url = `${protocol}://${window.location.host}/api/companies/${encodeURIComponent(liveCompanyId)}/events/ws`;
|
||||
const url = buildSameOriginWebSocketUrl(
|
||||
`/api/companies/${encodeURIComponent(liveCompanyId)}/events/ws`,
|
||||
);
|
||||
const nextSocket = new WebSocket(url);
|
||||
socket = nextSocket;
|
||||
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { browserReachableHost, buildSameOriginWebSocketUrl } from "./websocket-url";
|
||||
|
||||
describe("browserReachableHost", () => {
|
||||
it("keeps concrete browser hosts unchanged", () => {
|
||||
expect(browserReachableHost({
|
||||
protocol: "http:",
|
||||
hostname: "paperclip-dev",
|
||||
host: "paperclip-dev:46259",
|
||||
port: "46259",
|
||||
})).toBe("paperclip-dev:46259");
|
||||
});
|
||||
|
||||
it("rewrites wildcard IPv4 bind hosts to localhost", () => {
|
||||
expect(browserReachableHost({
|
||||
protocol: "http:",
|
||||
hostname: "0.0.0.0",
|
||||
host: "0.0.0.0:46259",
|
||||
port: "46259",
|
||||
})).toBe("localhost:46259");
|
||||
});
|
||||
|
||||
it("rewrites wildcard IPv6 bind hosts to localhost", () => {
|
||||
expect(browserReachableHost({
|
||||
protocol: "http:",
|
||||
hostname: "::",
|
||||
host: "[::]:46259",
|
||||
port: "46259",
|
||||
})).toBe("localhost:46259");
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildSameOriginWebSocketUrl", () => {
|
||||
it("uses wss for https pages", () => {
|
||||
expect(buildSameOriginWebSocketUrl("/api/events/ws", {
|
||||
protocol: "https:",
|
||||
hostname: "example.com",
|
||||
host: "example.com",
|
||||
port: "",
|
||||
})).toBe("wss://example.com/api/events/ws");
|
||||
});
|
||||
|
||||
it("does not emit 0.0.0.0 websocket URLs", () => {
|
||||
expect(buildSameOriginWebSocketUrl("api/events/ws", {
|
||||
protocol: "http:",
|
||||
hostname: "0.0.0.0",
|
||||
host: "0.0.0.0:46259",
|
||||
port: "46259",
|
||||
})).toBe("ws://localhost:46259/api/events/ws");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,20 @@
|
||||
type BrowserLocationLike = Pick<Location, "host" | "hostname" | "port" | "protocol">;
|
||||
|
||||
function isWildcardHost(hostname: string): boolean {
|
||||
const normalized = hostname.trim().toLowerCase();
|
||||
return normalized === "0.0.0.0" || normalized === "::" || normalized === "[::]";
|
||||
}
|
||||
|
||||
export function browserReachableHost(location: BrowserLocationLike = window.location): string {
|
||||
if (!isWildcardHost(location.hostname)) return location.host;
|
||||
return location.port ? `localhost:${location.port}` : "localhost";
|
||||
}
|
||||
|
||||
export function buildSameOriginWebSocketUrl(
|
||||
path: string,
|
||||
location: BrowserLocationLike = window.location,
|
||||
): string {
|
||||
const protocol = location.protocol === "https:" ? "wss" : "ws";
|
||||
const normalizedPath = path.startsWith("/") ? path : `/${path}`;
|
||||
return `${protocol}://${browserReachableHost(location)}${normalizedPath}`;
|
||||
}
|
||||
@@ -45,6 +45,7 @@ import { ScrollToBottom } from "../components/ScrollToBottom";
|
||||
import { SourceResolvedFoldCallout } from "../components/SourceResolvedFoldCallout";
|
||||
import { SourceResolvedFoldBadge } from "../components/SourceResolvedFoldBadge";
|
||||
import { readSourceResolvedWatchdogFold } from "../lib/source-resolved-watchdog-fold";
|
||||
import { buildSameOriginWebSocketUrl } from "../lib/websocket-url";
|
||||
import { formatCents, formatDate, relativeTime, formatTokens, visibleRunCostUsd } from "../lib/utils";
|
||||
import { cn } from "../lib/utils";
|
||||
import { describeRunRetryState } from "../lib/runRetryState";
|
||||
@@ -3865,8 +3866,9 @@ function LogViewer({ run, adapterType }: { run: HeartbeatRun; adapterType: strin
|
||||
|
||||
const connect = () => {
|
||||
if (closed) return;
|
||||
const protocol = window.location.protocol === "https:" ? "wss" : "ws";
|
||||
const url = `${protocol}://${window.location.host}/api/companies/${encodeURIComponent(run.companyId)}/events/ws`;
|
||||
const url = buildSameOriginWebSocketUrl(
|
||||
`/api/companies/${encodeURIComponent(run.companyId)}/events/ws`,
|
||||
);
|
||||
socket = new WebSocket(url);
|
||||
|
||||
socket.onopen = () => {
|
||||
|
||||
Reference in new issue
Block a user