From 6f4f4625da8080e0513f57bec650b43372507205 Mon Sep 17 00:00:00 2001 From: Sai Sanjay Date: Sun, 12 Jul 2026 16:49:04 +0530 Subject: [PATCH] feat: Simplify Docker builds and add centralized frontend config for API URLs - Use distroless/static base images instead of Alpine - Remove self-signed certificate generation from Dockerfiles - Add centralized config.js for API_BASE, WS_BASE, HTTP_TUNNEL_BASE - Remove hardcoded API_BASE strings from frontend components - Remove unused GUAC_CLIENT_URL env var from Vite config - Update page title to KubeBrowse Signed-off-by: Sai Sanjay --- Dockerfile | 44 +--- Dockerfile.tilt | 27 +-- Makefile | 23 +- deployments/manifest.yml | 13 +- frontend/.env.example | 2 + frontend/Caddyfile | 20 -- frontend/index.html | 2 +- frontend/src/components/BrowserSession.jsx | 6 +- frontend/src/components/GuacClient.jsx | 229 +++++++++++-------- frontend/src/components/OfficeSession.jsx | 3 +- frontend/src/components/ShareWSSession.jsx | 142 ++++++------ frontend/src/components/WebSocketControl.jsx | 3 +- frontend/src/config.js | 18 ++ frontend/src/hooks/useGuacWebSocket.js | 5 +- frontend/vite.config.js | 160 ------------- 15 files changed, 238 insertions(+), 459 deletions(-) create mode 100644 frontend/.env.example create mode 100644 frontend/src/config.js diff --git a/Dockerfile b/Dockerfile index fa2819a..4f601d9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,24 +2,12 @@ FROM golang:1.24-alpine AS builder WORKDIR /app -# Install dependencies for builder including sqlc RUN apk add --no-cache git make bash curl -# Install sqlc -# RUN curl -L https://github.com/sqlc-dev/sqlc/releases/download/v1.29.0/sqlc_1.29.0_linux_amd64.tar.gz | tar -xz -C /usr/local/bin - COPY go.mod go.sum ./ RUN sed -i '/^tool github.com\/evilmartians\/lefthook/d' go.mod RUN go mod download -# Copy source files needed for sqlc generation -# COPY sqlc.yaml ./sqlc.yaml -# COPY db/ ./db/ - -# Generate sqlc code -# RUN sqlc generate - -# Copy remaining source files (excluding db/ since it's already copied) COPY cmd/ ./cmd/ COPY internal/ ./internal/ COPY api/ ./api/ @@ -27,35 +15,13 @@ COPY docs/ ./docs/ COPY templates/ ./templates/ COPY go.mod go.sum ./ -# Initial build of the application RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o guac cmd/guac/main.go -# Final stage for running the application -FROM alpine:3.20 - -# Install Go, bash, ca-certificates, and openssl. -# Go and bash are needed for Tilt's live_update run steps if they compile/run scripts. -RUN apk --no-cache add ca-certificates openssl go bash - +FROM gcr.io/distroless/static:nonroot WORKDIR /app - -# Copy the built binary from the builder stage. -COPY --from=builder /app/guac /app/guac -# Copy templates (if your application uses them from filesystem at runtime) -COPY --from=builder /app/templates /app/templates - -# Create and copy certificates as before -RUN mkdir -p /app/certs -# COPY --from=builder /app/certs/ /app/certs/ # This line might not be needed if certs are always generated -RUN echo "Generating self-signed certificates..." && \ - openssl req -x509 -nodes -days 365 -newkey rsa:2048 \ - -keyout /app/certs/private.key \ - -out /app/certs/certificate.crt \ - -subj "/C=US/ST=California/L=San Francisco/O=My Company/CN=mydomain.com" - -ENV CERT_PATH=/app/certs/certificate.crt -ENV CERT_KEY_PATH=/app/certs/private.key -ENV GUACD_ADDRESS=guacd:4822 - +COPY --from=builder --chown=nonroot:nonroot /app/guac /app/guac +COPY --from=builder --chown=nonroot:nonroot /app/templates /app/templates +ENV GUACD_ADDRESS=guacd:4822 EXPOSE 4567 +USER nonroot:nonroot CMD ["/app/guac"] diff --git a/Dockerfile.tilt b/Dockerfile.tilt index d37052c..57a69b1 100644 --- a/Dockerfile.tilt +++ b/Dockerfile.tilt @@ -1,27 +1,8 @@ -FROM alpine:3.20 - -# Install runtime dependencies only -RUN apk --no-cache add ca-certificates openssl bash - +FROM gcr.io/distroless/static:debug-nonroot WORKDIR /app - -# Copy pre-compiled binary from local build -COPY .tilt/guac /app/guac -RUN chmod +x /app/guac - -# Copy templates -COPY templates/ /app/templates/ - -# Generate self-signed certificates -RUN mkdir -p /app/certs && \ - openssl req -x509 -nodes -days 365 -newkey rsa:2048 \ - -keyout /app/certs/private.key \ - -out /app/certs/certificate.crt \ - -subj "/C=US/ST=California/L=San Francisco/O=My Company/CN=mydomain.com" - -ENV CERT_PATH=/app/certs/certificate.crt -ENV CERT_KEY_PATH=/app/certs/private.key +COPY --chmod=755 --chown=nonroot:nonroot .tilt/guac /app/guac +COPY --chown=nonroot:nonroot templates/ /app/templates/ ENV GUACD_ADDRESS=guacd:4822 - EXPOSE 4567 +USER nonroot:nonroot CMD ["/app/guac"] diff --git a/Makefile b/Makefile index 6e989f1..5a6f689 100644 --- a/Makefile +++ b/Makefile @@ -1,25 +1,5 @@ all: run -# check if the certs directory exists and .key and .crt files exist -ifneq ("$(wildcard certs)","") - ifneq ("$(wildcard certs/private.key)","") - ifneq ("$(wildcard certs/certificate.crt)","") - CERTS_EXIST := true - endif - endif -endif -# if certs exist, set the cert path and key path -ifeq ($(CERTS_EXIST), true) - CERT_PATH := "$(shell pwd)/certs/certificate.crt" - CERT_KEY_PATH := "$(shell pwd)/certs/private.key" -else - CERT_PATH := "$(shell pwd)/certs/certificate.crt" - CERT_KEY_PATH := "$(shell pwd)/certs/private.key" - bash ./certs/generate.sh -endif - - - # Install dependencies deps: go mod tidy @@ -52,9 +32,8 @@ lint: # run the server run: deps @echo "Running server..." - @echo "Using certs from $(CERT_PATH) and $(CERT_KEY_PATH)" @echo "Starting server..." - CERT_PATH=./certs/certificate.crt CERT_KEY_PATH=./certs/private.key go run cmd/guac/main.go + go run cmd/guac/main.go run_frontend: @echo "Running frontend..." diff --git a/deployments/manifest.yml b/deployments/manifest.yml index 270cddd..afb5569 100644 --- a/deployments/manifest.yml +++ b/deployments/manifest.yml @@ -255,7 +255,7 @@ spec: serviceAccountName: browser-sandbox-sa containers: - name: api - image: ghcr.io/browsersec/kubebrowse:sha-09dfa1f + image: ghcr.io/browsersec/kubebrowse:sha-09dfa1f imagePullPolicy: IfNotPresent ports: - containerPort: 4567 @@ -397,7 +397,6 @@ roleRef: name: pod-manager apiGroup: rbac.authorization.k8s.io --- - # --- # Cron Job to cleanup idle sessions # apiVersion: batch/v1 @@ -490,11 +489,11 @@ spec: cpu: "200m" env: - name: VITE_GUAC_CLIENT_URL - value: "https://browser-sandbox-api.browser-sandbox.svc.cluster.local:4567" + value: "http://browser-sandbox-api.browser-sandbox.svc.cluster.local:4567" - name: GUAC_CLIENT_URL - value: "https://browser-sandbox-api.browser-sandbox.svc.cluster.local:4567" + value: "http://browser-sandbox-api.browser-sandbox.svc.cluster.local:4567" - name: CADDY_GUAC_CLIENT_URL - value: "https://browser-sandbox-api.browser-sandbox.svc.cluster.local:4567" + value: "http://browser-sandbox-api.browser-sandbox.svc.cluster.local:4567" --- # Frontend Service apiVersion: v1 @@ -509,7 +508,7 @@ spec: ports: - port: 80 targetPort: 80 - # nodePort: 30007 + # nodePort: 30007 selector: app: "browser-sandbox-frontend" --- @@ -527,4 +526,4 @@ spec: - name: rdp port: 3389 targetPort: rdp # Matches the named port "rdp" (3389) in the pod spec ---- \ No newline at end of file +--- diff --git a/frontend/.env.example b/frontend/.env.example new file mode 100644 index 0000000..51884f4 --- /dev/null +++ b/frontend/.env.example @@ -0,0 +1,2 @@ +# Backend origin for local development (no TLS) +VITE_API_BASE_URL=http://localhost:4567 diff --git a/frontend/Caddyfile b/frontend/Caddyfile index 883dbcc..cb109a8 100644 --- a/frontend/Caddyfile +++ b/frontend/Caddyfile @@ -17,10 +17,6 @@ # Authentication endpoints - proxy to backend API handle /auth/* { reverse_proxy {$CADDY_GUAC_CLIENT_URL} { - transport http { - tls - tls_insecure_skip_verify - } header_up Host {http.reverse_proxy.upstream.hostport} header_up Connection {header.Connection} header_up Upgrade {header.Upgrade} @@ -34,10 +30,6 @@ # Session management endpoints handle /sessions/* { reverse_proxy {$CADDY_GUAC_CLIENT_URL} { - transport http { - tls - tls_insecure_skip_verify - } header_up Host {http.reverse_proxy.upstream.hostport} header_up Connection {header.Connection} header_up Upgrade {header.Upgrade} @@ -51,10 +43,6 @@ # API v1 endpoints handle /api/v1/* { reverse_proxy {$CADDY_GUAC_CLIENT_URL} { - transport http { - tls - tls_insecure_skip_verify - } header_up Host {http.reverse_proxy.upstream.hostport} header_up Connection {header.Connection} header_up Upgrade {header.Upgrade} @@ -68,10 +56,6 @@ # Tunnel endpoints handle /tunnel* { reverse_proxy {$CADDY_GUAC_CLIENT_URL} { - transport http { - tls - tls_insecure_skip_verify - } header_up Host {http.reverse_proxy.upstream.hostport} header_up Connection {header.Connection} header_up Upgrade {header.Upgrade} @@ -85,10 +69,6 @@ # WebSocket tunnel endpoints handle /websocket-tunnel* { reverse_proxy {$CADDY_GUAC_CLIENT_URL} { - transport http { - tls - tls_insecure_skip_verify - } header_up Host {http.reverse_proxy.upstream.hostport} header_up Connection {header.Connection} header_up Upgrade {header.Upgrade} diff --git a/frontend/index.html b/frontend/index.html index 0c589ec..47d9e28 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -4,7 +4,7 @@ - Vite + React + KubeBrowse
diff --git a/frontend/src/components/BrowserSession.jsx b/frontend/src/components/BrowserSession.jsx index 4eb945c..88caf6e 100644 --- a/frontend/src/components/BrowserSession.jsx +++ b/frontend/src/components/BrowserSession.jsx @@ -4,11 +4,7 @@ import SessionReconnectStatus from "./SessionReconnectStatus"; import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; - -// const API_BASE = import.meta.env.VITE_GUAC_CLIENT_URL || `${isSecure ? 'https' : 'http'}://${location.host}`; -// const API_BASE = 'https://152.53.244.80:30006' -// const API_BASE = 'http://localhost:4567' -const API_BASE = ""; // Use relative URLs to leverage Vite's proxy +import { API_BASE } from "../config"; // Session persistence keys const SESSION_STORAGE_KEY = "kubeBrowse_browserSession"; diff --git a/frontend/src/components/GuacClient.jsx b/frontend/src/components/GuacClient.jsx index 31e74ff..14b75bf 100644 --- a/frontend/src/components/GuacClient.jsx +++ b/frontend/src/components/GuacClient.jsx @@ -1,66 +1,83 @@ -import Guacamole from 'guacamole-common-js'; -import { useEffect, useRef, useState } from 'react'; -import clipboard from '../lib/clipboard'; -import GuacMouse from '../lib/GuacMouse'; -import states from '../lib/states'; -import Modal from './Modal'; -import WebSocketControl from './WebSocketControl'; -import useGuacWebSocket from '../hooks/useGuacWebSocket'; -import { Toaster } from 'react-hot-toast'; +import Guacamole from "guacamole-common-js"; +import { useEffect, useRef, useState } from "react"; +import clipboard from "../lib/clipboard"; +import GuacMouse from "../lib/GuacMouse"; +import states from "../lib/states"; +import Modal from "./Modal"; +import WebSocketControl from "./WebSocketControl"; +import useGuacWebSocket from "../hooks/useGuacWebSocket"; +import { Toaster } from "react-hot-toast"; +import { WS_BASE, HTTP_TUNNEL_BASE } from "../config"; // Set custom Mouse implementation Guacamole.Mouse = GuacMouse.mouse; // Define websocket and HTTP tunnel URLs -const isSecure = window.location.protocol === 'https:'; -const wsUrl = `${isSecure ? 'wss' : 'ws'}://${location.host}/websocket-tunnel`; -const wsSharedUrl = `${isSecure ? 'wss' : 'ws'}://${location.host}/websocket-tunnel/share`; -const httpUrl = `${isSecure ? 'https' : 'http'}://${location.host}/tunnel`; +const wsUrl = `${WS_BASE}/websocket-tunnel`; +const wsSharedUrl = `${WS_BASE}/websocket-tunnel/share`; +const httpUrl = `${HTTP_TUNNEL_BASE}/tunnel`; // Convert query object to query string const buildQueryString = (queryObj) => { - if (!queryObj || typeof queryObj !== 'object') return ''; - + if (!queryObj || typeof queryObj !== "object") return ""; + const params = new URLSearchParams(); - + for (const [key, value] of Object.entries(queryObj)) { if (value !== undefined && value !== null) { params.append(key, value.toString()); } } - + return params.toString(); }; -function GuacClient({ query, forceHttp = false, onDisconnect, connectionId , OfficeSession = true , sharing = false, sessionUUID = null, enableSharing = false, onConnectionStateChange }) { +function GuacClient({ + query, + forceHttp = false, + onDisconnect, + connectionId, + OfficeSession = true, + sharing = false, + sessionUUID = null, + enableSharing = false, + onConnectionStateChange, +}) { const [connected, setConnected] = useState(false); - + // Convert query object to proper query string const queryString = buildQueryString(query); - + // Check if we are sharing a session const wsUrlToUse = sharing ? wsSharedUrl : wsUrl; - + console.log("GuacClient queryString:", queryString); console.log("GuacClient wsUrlToUse:", wsUrlToUse); console.log("GuacClient sessionUUID:", sessionUUID); console.log("GuacClient enableSharing:", enableSharing); - + // Use our custom WebSocket hook for Guacamole - const { client, connectionState, errorMessage, isConnectionUnstable, reconnectAttempts, wsMetrics } = useGuacWebSocket( - wsUrlToUse, - httpUrl, - forceHttp, - connected ? queryString : '', + const { + client, + connectionState, + errorMessage, + isConnectionUnstable, + reconnectAttempts, + wsMetrics, + } = useGuacWebSocket( + wsUrlToUse, + httpUrl, + forceHttp, + connected ? queryString : "", sessionUUID, enableSharing, - sharing + sharing, ); - + const displayRef = useRef(null); const viewportRef = useRef(null); const modalRef = useRef(null); - + const clientRef = useRef(client); // Store client ref for access in other effects const displayObjRef = useRef(null); const keyboardRef = useRef(null); @@ -77,10 +94,10 @@ function GuacClient({ query, forceHttp = false, onDisconnect, connectionId , Off // Set up display and clipboard when client becomes available setupClientDisplay(); clipboard.install(client); - + // Set up clipboard events client.onclipboard = clipboard.onClipboard; - + // Test for argument mutability client.onargv = handleArgv; } @@ -91,7 +108,7 @@ function GuacClient({ query, forceHttp = false, onDisconnect, connectionId , Off if (queryString && !connected) { setConnected(true); } - + return () => { if (clientRef.current) { clientRef.current.disconnect(); @@ -109,7 +126,11 @@ function GuacClient({ query, forceHttp = false, onDisconnect, connectionId , Off // Track connection state changes and notify parent component when disconnected useEffect(() => { - if (connectionState === states.DISCONNECTED || connectionState === states.CLIENT_ERROR || connectionState === states.TUNNEL_ERROR) { + if ( + connectionState === states.DISCONNECTED || + connectionState === states.CLIENT_ERROR || + connectionState === states.TUNNEL_ERROR + ) { if (connected && onDisconnect) { // Delay to allow potential reconnect attempts to happen first const timeout = setTimeout(() => { @@ -129,13 +150,12 @@ function GuacClient({ query, forceHttp = false, onDisconnect, connectionId , Off // Handle argument value stream const handleArgv = (stream, mimetype, name) => { - if (mimetype !== 'text/plain') - return; + if (mimetype !== "text/plain") return; const reader = new Guacamole.StringReader(stream); // Assemble received data into a single string - let value = ''; + let value = ""; reader.ontext = (text) => { value += text; }; @@ -143,8 +163,8 @@ function GuacClient({ query, forceHttp = false, onDisconnect, connectionId , Off // Test mutability once stream is finished reader.onend = () => { if (!clientRef.current) return; - - const stream = clientRef.current.createArgumentValueStream('text/plain', name); + + const stream = clientRef.current.createArgumentValueStream("text/plain", name); stream.onack = (status) => { if (status.isError()) { return; @@ -157,34 +177,34 @@ function GuacClient({ query, forceHttp = false, onDisconnect, connectionId , Off // Set up the display element const setupClientDisplay = () => { if (!clientRef.current || !displayRef.current) return; - + const display = clientRef.current.getDisplay(); displayObjRef.current = display; - + const displayElement = display.getElement(); - + // Set the display element to fill the width - displayElement.style.width = '100%'; - displayElement.style.maxWidth = '100vw'; - + displayElement.style.width = "100%"; + displayElement.style.maxWidth = "100vw"; + displayRef.current.appendChild(displayElement); - displayRef.current.addEventListener('contextmenu', (e) => { + displayRef.current.addEventListener("contextmenu", (e) => { e.stopPropagation(); if (e.preventDefault) { e.preventDefault(); } e.returnValue = false; }); - + // Set up mouse and keyboard setupMouseAndKeyboard(); - + // Call resize immediately resize(); - + // Focus the display element displayRef.current.focus(); - + // Additional resize calls to handle delayed rendering setTimeout(resize, 100); setTimeout(resize, 500); @@ -194,10 +214,10 @@ function GuacClient({ query, forceHttp = false, onDisconnect, connectionId , Off // Set up mouse and keyboard handlers const setupMouseAndKeyboard = () => { if (!displayRef.current || !clientRef.current) return; - + const mouse = new Guacamole.Mouse(displayRef.current); mouseRef.current = mouse; - + // Hide software cursor when mouse leaves display mouse.onmouseout = () => { if (!displayObjRef.current) return; @@ -208,21 +228,21 @@ function GuacClient({ query, forceHttp = false, onDisconnect, connectionId , Off displayRef.current.onclick = () => { displayRef.current.focus(); }; - + displayRef.current.onfocus = () => { - displayRef.current.className = 'guac-display focus'; + displayRef.current.className = "guac-display focus"; }; - + displayRef.current.onblur = () => { - displayRef.current.className = 'guac-display'; + displayRef.current.className = "guac-display"; }; // Set up keyboard const keyboard = new Guacamole.Keyboard(displayRef.current); keyboardRef.current = keyboard; - + installKeyboard(); - + // Set up mouse event handlers mouse.onmousedown = mouse.onmouseup = mouse.onmousemove = handleMouseState; }; @@ -241,15 +261,15 @@ function GuacClient({ query, forceHttp = false, onDisconnect, connectionId , Off return; } clipboard.cache = { - type: 'text/plain', - data: cmd.data + type: "text/plain", + data: cmd.data, }; clipboard.setRemoteClipboard(clientRef.current); }; const handleMouseState = (mouseState) => { if (!displayObjRef.current || !clientRef.current) return; - + const scaledMouseState = { ...mouseState, x: mouseState.x / displayObjRef.current.getScale(), @@ -277,7 +297,7 @@ function GuacClient({ query, forceHttp = false, onDisconnect, connectionId , Off // Calculate both horizontal and vertical scale factors const scaleX = viewportWidth / remoteWidth; const scaleY = viewportHeight / remoteHeight; - + // Use the smaller scale to ensure everything fits within the viewport const scale = Math.min(scaleX, scaleY); @@ -286,36 +306,36 @@ function GuacClient({ query, forceHttp = false, onDisconnect, connectionId , Off // Get the actual pixel density for accurate resolution const pixelDensity = window.devicePixelRatio || 1; - + // Calculate the optimal resolution to send to the server const optimalWidth = Math.round(viewportWidth * pixelDensity); const optimalHeight = Math.round(viewportHeight * pixelDensity); - + // Send updated size to server clientRef.current.sendSize(optimalWidth, optimalHeight); - + // Center both horizontally and vertically if (displayRef.current) { const scaledWidth = remoteWidth * scale; const scaledHeight = remoteHeight * scale; - + // Center horizontally - displayRef.current.style.marginLeft = scaledWidth < viewportWidth ? - `${(viewportWidth - scaledWidth) / 2}px` : '0'; - - // Center vertically - displayRef.current.style.marginTop = scaledHeight < viewportHeight ? - `${(viewportHeight - scaledHeight) / 2}px` : '0'; + displayRef.current.style.marginLeft = + scaledWidth < viewportWidth ? `${(viewportWidth - scaledWidth) / 2}px` : "0"; + + // Center vertically + displayRef.current.style.marginTop = + scaledHeight < viewportHeight ? `${(viewportHeight - scaledHeight) / 2}px` : "0"; } }; const installKeyboard = () => { if (!keyboardRef.current || !clientRef.current) return; - + keyboardRef.current.onkeydown = (keysym) => { clientRef.current.sendKeyEvent(1, keysym); }; - + keyboardRef.current.onkeyup = (keysym) => { clientRef.current.sendKeyEvent(0, keysym); }; @@ -329,17 +349,17 @@ function GuacClient({ query, forceHttp = false, onDisconnect, connectionId , Off const handleReconnect = () => { // Reset connection state and reconnect setConnected(false); - + // Clean up any existing display elements if (displayRef.current) { while (displayRef.current.firstChild) { displayRef.current.removeChild(displayRef.current.firstChild); } } - + // Reset references displayObjRef.current = null; - + // Reconnect after a small delay setTimeout(() => setConnected(true), 500); }; @@ -348,24 +368,24 @@ function GuacClient({ query, forceHttp = false, onDisconnect, connectionId , Off if (clientRef.current) { // Properly clean up resources uninstallKeyboard(); - + // Clear the display area if (displayRef.current) { while (displayRef.current.firstChild) { displayRef.current.removeChild(displayRef.current.firstChild); } } - + // Reset necessary state displayObjRef.current = null; - + // Actually disconnect from the client clientRef.current.disconnect(); - - // Update connection state + + // Update connection state setTimeout(() => { setConnected(false); - + // Notify parent component that we've disconnected if (onDisconnect) { onDisconnect(); @@ -379,48 +399,55 @@ function GuacClient({ query, forceHttp = false, onDisconnect, connectionId , Off const handleWindowResize = () => { resize(); }; - + // Add event listener for window resize - window.addEventListener('resize', handleWindowResize); - + window.addEventListener("resize", handleWindowResize); + // Call resize immediately and then again after short delays handleWindowResize(); const timeouts = [ setTimeout(handleWindowResize, 100), setTimeout(handleWindowResize, 300), setTimeout(handleWindowResize, 500), - setTimeout(handleWindowResize, 1000) + setTimeout(handleWindowResize, 1000), ]; - + // Cleanup return () => { - window.removeEventListener('resize', handleWindowResize); - timeouts.forEach(timeout => clearTimeout(timeout)); + window.removeEventListener("resize", handleWindowResize); + timeouts.forEach((timeout) => clearTimeout(timeout)); }; - }, []); - + }, []); + return (
-
-
+
+
{/* The Guacamole display will be inserted here */}
- + - - - +
); } -export default GuacClient; \ No newline at end of file +export default GuacClient; diff --git a/frontend/src/components/OfficeSession.jsx b/frontend/src/components/OfficeSession.jsx index 8c01b6d..5b1c825 100644 --- a/frontend/src/components/OfficeSession.jsx +++ b/frontend/src/components/OfficeSession.jsx @@ -8,8 +8,7 @@ import { Alert, AlertDescription } from "@/components/ui/alert"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Copy, Share2, Users, ExternalLink, Check } from "lucide-react"; - -const API_BASE = ""; // Use relative URLs to leverage Vite's proxy +import { API_BASE } from "../config"; // Session persistence keys const SESSION_STORAGE_KEY = "kubeBrowse_officeSession"; diff --git a/frontend/src/components/ShareWSSession.jsx b/frontend/src/components/ShareWSSession.jsx index b878cb3..5383588 100644 --- a/frontend/src/components/ShareWSSession.jsx +++ b/frontend/src/components/ShareWSSession.jsx @@ -1,35 +1,33 @@ -import { useState, useEffect, useCallback, useRef } from 'react'; -import GuacClient from './GuacClient'; -import SessionReconnectStatus from './SessionReconnectStatus'; -import { Button } from '@/components/ui/button'; -import { Input } from '@/components/ui/input'; -import { Label } from '@/components/ui/label'; -import { Card, CardContent } from '@/components/ui/card'; -import { Badge } from '@/components/ui/badge'; -import { Alert, AlertDescription } from '@/components/ui/alert'; -import { Loader2, AlertTriangle } from 'lucide-react'; - -const API_BASE = '' // Use relative URLs to leverage Vite's proxy +import { useState, useEffect, useCallback, useRef } from "react"; +import GuacClient from "./GuacClient"; +import SessionReconnectStatus from "./SessionReconnectStatus"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Card, CardContent } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { Alert, AlertDescription } from "@/components/ui/alert"; +import { Loader2, AlertTriangle } from "lucide-react"; // Session persistence keys -const SESSION_STORAGE_KEY = 'kubeBrowse_sharedSession'; +const SESSION_STORAGE_KEY = "kubeBrowse_sharedSession"; const SESSION_TIMEOUT = 24 * 60 * 60 * 1000; // 24 hours const ShareWSSession = () => { const [sessionState, setSessionState] = useState({ connectionId: null, - status: 'idle', + status: "idle", error: null, - name: '' + name: "", }); - const [inputUuid, setInputUuid] = useState(''); - const [sessionName, setSessionName] = useState(''); + const [inputUuid, setInputUuid] = useState(""); + const [sessionName, setSessionName] = useState(""); const hasRestored = useRef(false); const [reconnectStatus, setReconnectStatus] = useState({ isReconnecting: false, attempts: 0, - connectionState: 'IDLE' + connectionState: "IDLE", }); // Session persistence functions @@ -37,7 +35,7 @@ const ShareWSSession = () => { const sessionInfo = { ...sessionData, timestamp: Date.now(), - expiresAt: Date.now() + SESSION_TIMEOUT + expiresAt: Date.now() + SESSION_TIMEOUT, }; localStorage.setItem(SESSION_STORAGE_KEY, JSON.stringify(sessionInfo)); }, []); @@ -48,7 +46,7 @@ const ShareWSSession = () => { if (!stored) return null; const sessionInfo = JSON.parse(stored); - + if (Date.now() > sessionInfo.expiresAt) { localStorage.removeItem(SESSION_STORAGE_KEY); return null; @@ -56,7 +54,7 @@ const ShareWSSession = () => { return sessionInfo; } catch (error) { - console.error('Error loading session from storage:', error); + console.error("Error loading session from storage:", error); localStorage.removeItem(SESSION_STORAGE_KEY); return null; } @@ -72,18 +70,18 @@ const ShareWSSession = () => { hasRestored.current = true; const urlParams = new URLSearchParams(window.location.search); - const uuidFromUrl = urlParams.get('uuid'); + const uuidFromUrl = urlParams.get("uuid"); const restoreSession = (sessionData) => { if (sessionState.connectionId !== sessionData.connectionId) { const newUrl = new URL(window.location); - newUrl.searchParams.set('uuid', sessionData.connectionId); - window.history.replaceState({}, '', newUrl); + newUrl.searchParams.set("uuid", sessionData.connectionId); + window.history.replaceState({}, "", newUrl); setSessionState({ connectionId: sessionData.connectionId, name: sessionData.name || `Shared Session: ${sessionData.connectionId.substring(0, 8)}`, - status: 'ready', - error: null + status: "ready", + error: null, }); } }; @@ -91,7 +89,8 @@ const ShareWSSession = () => { if (uuidFromUrl) { const storedSession = loadSessionFromStorage(); // Restore from URL, use stored name if available for the same session - const name = (storedSession && storedSession.connectionId === uuidFromUrl) ? storedSession.name : ''; + const name = + storedSession && storedSession.connectionId === uuidFromUrl ? storedSession.name : ""; restoreSession({ connectionId: uuidFromUrl, name }); } else { const storedSession = loadSessionFromStorage(); @@ -104,27 +103,26 @@ const ShareWSSession = () => { // Save session to storage when it becomes ready useEffect(() => { - if (sessionState.status === 'ready' && sessionState.connectionId) { + if (sessionState.status === "ready" && sessionState.connectionId) { saveSessionToStorage(sessionState); } }, [sessionState, saveSessionToStorage]); - const handleDisconnect = useCallback(() => { clearSessionFromStorage(); const newUrl = new URL(window.location); - newUrl.searchParams.delete('uuid'); - window.history.replaceState({}, '', newUrl); + newUrl.searchParams.delete("uuid"); + window.history.replaceState({}, "", newUrl); console.log("Disconnected from shared session"); setSessionState({ connectionId: null, - status: 'idle', + status: "idle", error: null, - name: '' + name: "", }); - setInputUuid(''); - setSessionName(''); + setInputUuid(""); + setSessionName(""); }, [clearSessionFromStorage]); const handleInputChange = (e) => { @@ -138,36 +136,36 @@ const ShareWSSession = () => { const handleSubmit = async (e) => { e.preventDefault(); try { - setSessionState(prev => ({ ...prev, status: 'creating', error: null })); - + setSessionState((prev) => ({ ...prev, status: "creating", error: null })); + // Just take the UUID directly, no parsing const uuid = inputUuid.trim(); - + // Validate UUID format if (!uuid.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i)) { - throw new Error('Invalid UUID format. Please enter a valid connection ID.'); + throw new Error("Invalid UUID format. Please enter a valid connection ID."); } - + const newSessionName = sessionName || `Shared Session: ${uuid.substring(0, 8)}`; // Set the connection state with the UUID setSessionState({ connectionId: uuid, - status: 'ready', + status: "ready", error: null, - name: newSessionName + name: newSessionName, }); // Update URL const newUrl = new URL(window.location); - newUrl.searchParams.set('uuid', uuid); - window.history.replaceState({}, '', newUrl); + newUrl.searchParams.set("uuid", uuid); + window.history.replaceState({}, "", newUrl); console.log("Ready to connect with UUID:", uuid); } catch (error) { - setSessionState(prev => ({ + setSessionState((prev) => ({ ...prev, - status: 'error', - error: error.message + status: "error", + error: error.message, })); } }; @@ -175,8 +173,8 @@ const ShareWSSession = () => { const handleConnectionStateChange = useCallback((state, attempts) => { setReconnectStatus({ connectionState: state, - isReconnecting: state === 'CONNECTING' && attempts > 0, - attempts: attempts || 0 + isReconnecting: state === "CONNECTING" && attempts > 0, + attempts: attempts || 0, }); }, []); @@ -192,8 +190,8 @@ const ShareWSSession = () => { Enter the connection ID to join an existing session.

- - {sessionState.status === 'idle' && ( + + {sessionState.status === "idle" && (
@@ -211,7 +209,7 @@ const ShareWSSession = () => { Enter the UUID of the shared session

- +
{ placeholder="My Shared Session" />
- + @@ -230,27 +228,27 @@ const ShareWSSession = () => { )} - - {sessionState.status === 'creating' && ( + + {sessionState.status === "creating" && (
Connecting to shared session...
)} - - {sessionState.status === 'error' && ( + + {sessionState.status === "error" && (

{sessionState.error}

-
@@ -296,7 +286,7 @@ const ShareWSSession = () => { query={{ uuid: sessionState.connectionId, width: Math.round(window.innerWidth * (window.devicePixelRatio || 1)), - height: Math.round(window.innerHeight * (window.devicePixelRatio || 1)) + height: Math.round(window.innerHeight * (window.devicePixelRatio || 1)), }} connectionId={sessionState.connectionId} onDisconnect={handleDisconnect} diff --git a/frontend/src/components/WebSocketControl.jsx b/frontend/src/components/WebSocketControl.jsx index 4d45f47..0ba1964 100644 --- a/frontend/src/components/WebSocketControl.jsx +++ b/frontend/src/components/WebSocketControl.jsx @@ -21,6 +21,7 @@ import { DialogTrigger, } from "@/components/ui/dialog"; import WebSocketMetricsDisplay from "./WebSocketMetricsDisplay"; +import { API_BASE } from "../config"; /** * A collapsible control panel for WebSocket connection management @@ -132,7 +133,7 @@ function WebSocketControl({ const formData = new FormData(); formData.append("file", file); const xhr = new window.XMLHttpRequest(); - xhr.open("POST", `/sessions/${connectionId}/upload`, true); + xhr.open("POST", `${API_BASE}/sessions/${connectionId}/upload`, true); xhr.withCredentials = false; xhr.upload.onprogress = (event) => { if (event.lengthComputable) { diff --git a/frontend/src/config.js b/frontend/src/config.js new file mode 100644 index 0000000..7a78a7f --- /dev/null +++ b/frontend/src/config.js @@ -0,0 +1,18 @@ +// Base URL for backend REST API calls. +// In production (e.g. Cloudflare Pages) set VITE_API_BASE_URL to the backend origin, +// e.g. https://api.kubebrowse.example.com or https://kubebrowse-tunnel.pages.dev +// +// For local dev with an HTTP backend, set VITE_API_BASE_URL=http://localhost:4567 +// and run `npm run dev` (HTTP dev server). +export const API_BASE = (import.meta.env.VITE_API_BASE_URL || "").replace(/\/$/, ""); + +// Derive WebSocket / HTTP tunnel bases from the same origin so they stay in sync. +const apiUrl = API_BASE ? new URL(API_BASE) : null; + +export const WS_BASE = apiUrl + ? `${apiUrl.protocol === "https:" ? "wss" : "ws"}://${apiUrl.host}` + : `${window.location.protocol === "https:" ? "wss" : "ws"}://${location.host}`; + +export const HTTP_TUNNEL_BASE = apiUrl + ? `${apiUrl.protocol}//${apiUrl.host}` + : `${window.location.protocol}//${location.host}`; diff --git a/frontend/src/hooks/useGuacWebSocket.js b/frontend/src/hooks/useGuacWebSocket.js index d6659d3..cccbc96 100644 --- a/frontend/src/hooks/useGuacWebSocket.js +++ b/frontend/src/hooks/useGuacWebSocket.js @@ -4,6 +4,7 @@ import Guacamole from "guacamole-common-js"; import states from "../lib/states"; import sessionDuplicator from "../lib/websocketSessionDuplicator"; import useWebSocketMetrics from "./useWebSocketMetrics"; +import { API_BASE } from "../config"; // Session persistence keys const SESSION_STORAGE_KEY = "kubeBrowse_sessionConnection"; @@ -120,7 +121,7 @@ const useGuacWebSocket = ( if (!isSharedSession) { try { console.log(`Cleaning up session ${sessionUUID} after failed reconnection attempts`); - await fetch(`/sessions/${sessionUUID}/stop`, { + await fetch(`${API_BASE}/sessions/${sessionUUID}/stop`, { method: "DELETE", }); console.log(`Session ${sessionUUID} cleanup request sent`); @@ -180,7 +181,7 @@ const useGuacWebSocket = ( console.log( `Cleaning up session ${sessionUUID} after failed reconnection attempts`, ); - fetch(`/sessions/${sessionUUID}/stop`, { + fetch(`${API_BASE}/sessions/${sessionUUID}/stop`, { method: "DELETE", }).catch((err) => console.error("Failed to cleanup session on backend:", err)); } else { diff --git a/frontend/vite.config.js b/frontend/vite.config.js index 2e0e460..bff74f7 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -3,9 +3,6 @@ import { defineConfig } from "vite"; import react from "@vitejs/plugin-react"; import path from "path"; -const guacClient = process.env.GUAC_CLIENT_URL || "https://localhost:4567"; -console.log(`Using Guacamole client URL: ${guacClient}`); - export default defineConfig({ plugins: [react()], resolve: { @@ -13,161 +10,4 @@ export default defineConfig({ "@": path.resolve(__dirname, "./src"), }, }, - server: { - proxy: { - "/tunnel": { - target: guacClient, - changeOrigin: true, - ws: false, - secure: false, // Ignore certificate validation - timeout: 60000, // Increase timeout to 60 seconds - proxyTimeout: 60000, - cookieDomainRewrite: "localhost", - cookiePathRewrite: "/", - }, - "/api/v1": { - target: guacClient, - changeOrigin: true, - ws: false, - secure: false, // Ignore certificate validation - timeout: 60000, // Increase timeout to 60 seconds - proxyTimeout: 60000, - cookieDomainRewrite: "localhost", - cookiePathRewrite: "/", - }, - "/sessions": { - target: guacClient, - changeOrigin: true, - ws: false, - secure: false, // Ignore certificate validation - timeout: 60000, // Increase timeout to 60 seconds - proxyTimeout: 60000, - cookieDomainRewrite: "localhost", - cookiePathRewrite: "/", - }, - "/auth": { - target: guacClient, - changeOrigin: true, - ws: false, - secure: false, // Ignore certificate validation - timeout: 60000, // Increase timeout to 60 seconds - proxyTimeout: 60000, - cookieDomainRewrite: "localhost", - cookiePathRewrite: "/", - onProxyReq: (proxyReq, req, res) => { - // Ensure cookies are forwarded properly - if (req.headers.cookie) { - proxyReq.setHeader("Cookie", req.headers.cookie); - } - }, - onProxyRes: (proxyRes, req, res) => { - // Handle Set-Cookie headers from the backend - if (proxyRes.headers["set-cookie"]) { - const cookies = proxyRes.headers["set-cookie"]; - // Rewrite cookie domain and path for frontend - const rewrittenCookies = cookies.map( - (cookie) => - cookie - .replace(/Domain=[^;]+;?/g, "Domain=localhost;") - .replace(/Path=[^;]+;?/g, "Path=/;") - .replace(/Secure;?/g, ""), // Remove Secure flag for local development - ); - proxyRes.headers["set-cookie"] = rewrittenCookies; - } - }, - }, - "/websocket-tunnel": { - target: guacClient, - changeOrigin: true, - ws: true, - secure: false, // Ignore certificate validation - timeout: 120000, // Increase timeout to 120 seconds - proxyTimeout: 120000, - configure: (proxy, _options) => { - // Increase buffer size to handle larger WebSocket frames - proxy.options.buffer = Buffer.alloc(1024 * 1024); - - proxy.on("error", (err, req, res) => { - console.log("Proxy error:", err); - - // Prevent additional writes to broken connections - if (err.code === "EPIPE" || err.code === "ECONNRESET") { - console.log("Connection closed by remote host. Preventing further writes."); - if (res && !res.headersSent) { - res.writeHead(502, { "Content-Type": "text/plain" }); - res.end("WebSocket connection error. Please refresh the page to reconnect."); - } - } - }); - - proxy.on("proxyReq", (proxyReq, req, _res) => { - // Ensure WebSocket headers are preserved - if (req.headers["sec-websocket-key"]) { - if (!proxyReq.getHeader("connection")) { - proxyReq.setHeader("connection", "upgrade"); - } - if (!proxyReq.getHeader("upgrade")) { - proxyReq.setHeader("upgrade", "websocket"); - } - - // Add additional headers that might help with connection stability - proxyReq.setHeader("pragma", "no-cache"); - proxyReq.setHeader("cache-control", "no-cache"); - } - - // Log outgoing proxy requests - console.log(`Proxying WebSocket request to: ${req.url}`); - }); - - proxy.on("proxyRes", (proxyRes, req, res) => { - // Log successful responses - console.log(`Proxy response: ${proxyRes.statusCode} for ${req.url}`); - }); - - proxy.on("upgrade", (req, socket, head) => { - console.log("WebSocket upgrade initiated for:", req.url); - - // Add error handler to the socket - socket.on("error", (err) => { - console.error("WebSocket socket error:", err); - // Close the socket gracefully to prevent EPIPE errors - try { - if (!socket.destroyed) { - socket.end(); - } - } catch (e) { - console.error("Error while closing socket:", e); - } - }); - - // Add close handler - socket.on("close", () => { - console.log("WebSocket connection closed for:", req.url); - }); - - // Keep socket alive - socket.setKeepAlive(true); - - // Increase socket timeout - socket.setTimeout(120000); - }); - - // Add general proxy error handling - proxy.on("econnreset", (err, req, res, target) => { - console.warn("Connection reset by peer:", err); - - // Try to gracefully handle the reset - if (res && !res.headersSent) { - res.writeHead(502, { "Content-Type": "text/plain" }); - res.end("Connection reset by server. Please refresh the page to reconnect."); - } - }); - - proxy.on("end", () => { - console.log("Proxy connection ended"); - }); - }, - }, - }, - }, });