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
|
FROM nginx:alpine
|
||||||
COPY --from=build /app/dist /usr/share/nginx/html
|
COPY --from=build /app/dist /usr/share/nginx/html
|
||||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
EXPOSE 80
|
EXPOSE 3000
|
||||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
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;"]
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
services:
|
services:
|
||||||
pinchchat:
|
pinchchat:
|
||||||
image: ghcr.io/marlburrow/pinchchat:latest
|
# image: ghcr.io/marlburrow/pinchchat:latest
|
||||||
# Or build locally:
|
build: .
|
||||||
# build: .
|
network_mode: host
|
||||||
ports:
|
|
||||||
- "3000:80"
|
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "wget", "-qO", "/dev/null", "http://localhost:80/"]
|
test: ["CMD", "wget", "-qO", "/dev/null", "http://localhost:3000/"]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
timeout: 3s
|
timeout: 3s
|
||||||
start_period: 5s
|
start_period: 5s
|
||||||
|
|||||||
19
nginx.conf
19
nginx.conf
@@ -1,5 +1,5 @@
|
|||||||
server {
|
server {
|
||||||
listen 80;
|
listen 3000;
|
||||||
server_name _;
|
server_name _;
|
||||||
root /usr/share/nginx/html;
|
root /usr/share/nginx/html;
|
||||||
index index.html;
|
index index.html;
|
||||||
@@ -10,24 +10,35 @@ server {
|
|||||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||||
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" 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 / {
|
location / {
|
||||||
try_files $uri $uri/ /index.html;
|
try_files $uri $uri/ /index.html;
|
||||||
}
|
}
|
||||||
|
|
||||||
# Cache static assets (hashed filenames — safe to cache forever)
|
|
||||||
location /assets/ {
|
location /assets/ {
|
||||||
expires 1y;
|
expires 1y;
|
||||||
add_header Cache-Control "public, immutable";
|
add_header Cache-Control "public, immutable";
|
||||||
}
|
}
|
||||||
|
|
||||||
# Don't cache index.html (SPA entry point must always be fresh)
|
|
||||||
location = /index.html {
|
location = /index.html {
|
||||||
add_header Cache-Control "no-cache, no-store, must-revalidate";
|
add_header Cache-Control "no-cache, no-store, must-revalidate";
|
||||||
add_header Pragma "no-cache";
|
add_header Pragma "no-cache";
|
||||||
add_header Expires "0";
|
add_header Expires "0";
|
||||||
}
|
}
|
||||||
|
|
||||||
# Gzip
|
|
||||||
gzip on;
|
gzip on;
|
||||||
gzip_types text/plain text/css application/json application/javascript text/xml application/xml text/javascript image/svg+xml;
|
gzip_types text/plain text/css application/json application/javascript text/xml application/xml text/javascript image/svg+xml;
|
||||||
gzip_min_length 256;
|
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;
|
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 protocol = window.location.protocol === 'https:' ? 'wss' : 'ws';
|
||||||
const host = window.location.hostname;
|
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)
|
// When served over HTTPS, assume gateway is on a sibling subdomain (e.g. marlbot-gw.example.com)
|
||||||
if (protocol === 'wss') {
|
if (protocol === 'wss') {
|
||||||
const parts = host.split('.');
|
const parts = host.split('.');
|
||||||
|
|||||||
@@ -79,7 +79,18 @@ export class GatewayClient {
|
|||||||
this.autoReconnect = true;
|
this.autoReconnect = true;
|
||||||
this.connectNonce = null;
|
this.connectNonce = null;
|
||||||
this._onStatus('connecting');
|
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'); };
|
this.ws.onopen = () => { log('WS open'); };
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user