From a03e8db621045bccd1a237aea3771fcf4f5aad2c Mon Sep 17 00:00:00 2001 From: shiyue Date: Sat, 14 Mar 2026 13:22:24 +0800 Subject: [PATCH] feat: support cross-origin WebSocket proxy for remote gateway - Add nginx /gwproxy/ reverse proxy with correct Origin header - Auto-detect cross-origin gateway URL in GatewayClient.connect() - Detect path-based proxy deployment in LoginScreen getInitialUrl() - Change nginx/Docker to listen on port 3000, use host network mode --- Dockerfile | 4 ++-- docker-compose.yml | 10 ++++------ nginx.conf | 19 +++++++++++++++---- src/components/LoginScreen.tsx | 8 ++++++++ src/lib/gateway.ts | 13 ++++++++++++- 5 files changed, 41 insertions(+), 13 deletions(-) diff --git a/Dockerfile b/Dockerfile index f0d9405..7f8d375 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,7 +10,7 @@ RUN npm run build FROM nginx:alpine COPY --from=build /app/dist /usr/share/nginx/html COPY nginx.conf /etc/nginx/conf.d/default.conf -EXPOSE 80 +EXPOSE 3000 HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ - CMD wget -qO /dev/null http://localhost:80/ || exit 1 + CMD wget -qO /dev/null http://localhost:3000/ || exit 1 CMD ["nginx", "-g", "daemon off;"] diff --git a/docker-compose.yml b/docker-compose.yml index 2c0e923..1c93f73 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,13 +1,11 @@ services: pinchchat: - image: ghcr.io/marlburrow/pinchchat:latest - # Or build locally: - # build: . - ports: - - "3000:80" + # image: ghcr.io/marlburrow/pinchchat:latest + build: . + network_mode: host restart: unless-stopped healthcheck: - test: ["CMD", "wget", "-qO", "/dev/null", "http://localhost:80/"] + test: ["CMD", "wget", "-qO", "/dev/null", "http://localhost:3000/"] interval: 30s timeout: 3s start_period: 5s diff --git a/nginx.conf b/nginx.conf index 0356d96..4942f85 100644 --- a/nginx.conf +++ b/nginx.conf @@ -1,5 +1,5 @@ server { - listen 80; + listen 3000; server_name _; root /usr/share/nginx/html; index index.html; @@ -10,24 +10,35 @@ server { add_header Referrer-Policy "strict-origin-when-cross-origin" always; add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always; + # WebSocket reverse proxy — relay to remote gateway with correct Origin + location ~ ^/gwproxy/(.+) { + resolver 8.8.8.8 valid=30s; + proxy_pass https://api.routin.ai/$1; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host api.routin.ai; + proxy_set_header Origin "https://api.routin.ai"; + proxy_ssl_server_name on; + proxy_read_timeout 86400; + proxy_send_timeout 86400; + } + location / { try_files $uri $uri/ /index.html; } - # Cache static assets (hashed filenames — safe to cache forever) location /assets/ { expires 1y; add_header Cache-Control "public, immutable"; } - # Don't cache index.html (SPA entry point must always be fresh) location = /index.html { add_header Cache-Control "no-cache, no-store, must-revalidate"; add_header Pragma "no-cache"; add_header Expires "0"; } - # Gzip gzip on; gzip_types text/plain text/css application/json application/javascript text/xml application/xml text/javascript image/svg+xml; gzip_min_length 256; diff --git a/src/components/LoginScreen.tsx b/src/components/LoginScreen.tsx index 6ef1b10..f817936 100644 --- a/src/components/LoginScreen.tsx +++ b/src/components/LoginScreen.tsx @@ -15,6 +15,14 @@ function getInitialUrl(): string { if (import.meta.env.VITE_GATEWAY_WS_URL) return import.meta.env.VITE_GATEWAY_WS_URL; const protocol = window.location.protocol === 'https:' ? 'wss' : 'ws'; const host = window.location.hostname; + const pathname = window.location.pathname; + + // Detect path-based proxy deployment (e.g. routin.ai sandbox proxy) + // Browser auto-sends correct Origin (same domain), so direct connection works + const proxyMatch = pathname.match(/^(.*\/proxy\/)\d+(\/.*)?$/); + if (proxyMatch) { + return `${protocol}://${host}${proxyMatch[1]}18789`; + } // When served over HTTPS, assume gateway is on a sibling subdomain (e.g. marlbot-gw.example.com) if (protocol === 'wss') { const parts = host.split('.'); diff --git a/src/lib/gateway.ts b/src/lib/gateway.ts index b2256c1..4d1d03c 100644 --- a/src/lib/gateway.ts +++ b/src/lib/gateway.ts @@ -79,7 +79,18 @@ export class GatewayClient { this.autoReconnect = true; this.connectNonce = null; this._onStatus('connecting'); - this.ws = new WebSocket(this.wsUrl); + + // If wsUrl points to a different host, route through local /gwproxy/ to bypass Origin restriction + let url = this.wsUrl; + try { + const target = new URL(this.wsUrl); + if (target.hostname !== window.location.hostname) { + const proto = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; + url = `${proto}//${window.location.host}/gwproxy${target.pathname}`; + log('cross-origin detected, proxying via', url); + } + } catch { /* use original url */ } + this.ws = new WebSocket(url); this.ws.onopen = () => { log('WS open'); };