feat: support cross-origin WebSocket proxy for remote gateway
Some checks failed
CI / build (20) (push) Has been cancelled
CI / build (22) (push) Has been cancelled
CI / notify-failure (push) Has been cancelled
Docker / build-and-push (push) Has been cancelled

- 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
This commit is contained in:
shiyue
2026-03-14 13:22:24 +08:00
parent 246953963b
commit a03e8db621
5 changed files with 41 additions and 13 deletions

View File

@@ -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;"]

View File

@@ -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

View File

@@ -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;

View File

@@ -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('.');

View File

@@ -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'); };