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
This commit is contained in:
@@ -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;"]
|
||||
|
||||
@@ -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
|
||||
|
||||
19
nginx.conf
19
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;
|
||||
|
||||
@@ -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('.');
|
||||
|
||||
@@ -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'); };
|
||||
|
||||
|
||||
Reference in New Issue
Block a user