diff --git a/demo/README.md b/demo/README.md index 9f0897f..14193d8 100644 --- a/demo/README.md +++ b/demo/README.md @@ -14,9 +14,10 @@ Works on **Linux** and **macOS** (no Windows support yet). ## What it does - Starts an HTTP server on port 8080 (configurable via `PORT` env var) -- Starts a WebSocket server on port 3001 for PTY communication +- Serves WebSocket PTY on the same port at `/ws` endpoint - Opens a real shell session (bash, zsh, etc.) - Provides full PTY support (colors, cursor positioning, resize, etc.) +- Supports reverse proxies (ngrok, nginx, etc.) via X-Forwarded-\* headers ## Usage @@ -30,6 +31,49 @@ PORT=3000 npx @ghostty-web/demo@next Then open http://localhost:8080 in your browser. +## Reverse Proxy Support + +The server now supports reverse proxies like ngrok, nginx, and others by: + +- Serving WebSocket on the same HTTP port (no separate port needed) +- Using relative WebSocket URLs on the client side +- Automatic protocol detection (HTTP/HTTPS, WS/WSS) + +This means the WebSocket connection automatically adapts to use the same protocol and host as the HTTP connection, making it work seamlessly through any reverse proxy. + +### Example with ngrok + +```bash +# Start the demo server +npx @ghostty-web/demo@next + +# In another terminal, expose it via ngrok +ngrok http 8080 +``` + +The terminal will work seamlessly through the ngrok URL! Both HTTP and WebSocket traffic will be properly proxied. + +### Example with nginx + +```nginx +server { + listen 80; + server_name example.com; + + location / { + proxy_pass http://localhost:8080; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + } +} +``` + ## Security Warning ⚠️ **This server provides full shell access.** diff --git a/demo/bin/demo.js b/demo/bin/demo.js index d652e68..936b237 100644 --- a/demo/bin/demo.js +++ b/demo/bin/demo.js @@ -23,7 +23,6 @@ const __dirname = path.dirname(__filename); const DEV_MODE = process.argv.includes('--dev'); const HTTP_PORT = process.env.PORT || (DEV_MODE ? 8000 : 8080); -const WS_PORT = 3001; // ============================================================================ // Locate ghostty-web assets @@ -239,8 +238,9 @@ const HTML_TEMPLATE = ` statusText.textContent = text; } - // Connect to WebSocket PTY server - const wsUrl = 'ws://' + window.location.hostname + ':${WS_PORT}/ws?cols=' + term.cols + '&rows=' + term.rows; + // Connect to WebSocket PTY server (use same origin as HTTP server) + const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; + const wsUrl = protocol + '//' + window.location.host + '/ws?cols=' + term.cols + '&rows=' + term.rows; let ws; function connect() { @@ -386,8 +386,23 @@ function createPtySession(cols, rows) { return ptyProcess; } -// WebSocket server using ws package -const wss = new WebSocketServer({ port: WS_PORT, path: '/ws' }); +// WebSocket server attached to HTTP server (same port) +const wss = new WebSocketServer({ noServer: true }); + +// Handle HTTP upgrade for WebSocket connections +httpServer.on('upgrade', (req, socket, head) => { + const url = new URL(req.url, `http://${req.headers.host}`); + + if (url.pathname === '/ws') { + // In production, consider validating req.headers.origin to prevent CSRF + // For development/demo purposes, we allow all origins + wss.handleUpgrade(req, socket, head, (ws) => { + wss.emit('connection', ws, req); + }); + } else { + socket.destroy(); + } +}); wss.on('connection', (ws, req) => { const url = new URL(req.url, `http://${req.headers.host}`); @@ -474,7 +489,7 @@ function printBanner(url) { console.log(' 🚀 ghostty-web demo server' + (DEV_MODE ? ' (dev mode)' : '')); console.log('═'.repeat(60)); console.log(`\n 📺 Open: ${url}`); - console.log(` 📡 WebSocket PTY: ws://localhost:${WS_PORT}/ws`); + console.log(` 📡 WebSocket PTY: same endpoint /ws`); console.log(` 🐚 Shell: ${getShell()}`); console.log(` 📁 Home: ${homedir()}`); if (DEV_MODE) { @@ -510,7 +525,34 @@ if (DEV_MODE) { strictPort: true, }, }); + await vite.listen(); + + // Attach WebSocket handler AFTER Vite has fully initialized + // Use prependListener (not prependOnceListener) so it runs for every request + // This ensures our handler runs BEFORE Vite's handlers + if (vite.httpServer) { + vite.httpServer.prependListener('upgrade', (req, socket, head) => { + const pathname = req.url?.split('?')[0] || req.url || ''; + + // ONLY handle /ws - everything else passes through unchanged to Vite + if (pathname === '/ws') { + if (!socket.destroyed && !socket.readableEnded) { + wss.handleUpgrade(req, socket, head, (ws) => { + wss.emit('connection', ws, req); + }); + } + // Stop here - we handled it, socket is consumed + // Don't call other listeners + return; + } + + // For non-/ws paths, explicitly do nothing and let the event propagate + // The key is: don't return, don't touch the socket, just let it pass through + // Vite's handlers (which were added before ours via prependListener) will process it + }); + } + printBanner(`http://localhost:${HTTP_PORT}/demo/`); } else { // Production mode: static file server diff --git a/demo/index.html b/demo/index.html index 8de6938..2a0d323 100644 --- a/demo/index.html +++ b/demo/index.html @@ -193,7 +193,7 @@ function connectWebSocket() { const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; - const wsUrl = `${protocol}//${window.location.hostname}:3001/ws?cols=${term.cols}&rows=${term.rows}`; + const wsUrl = `${protocol}//${window.location.host}/ws?cols=${term.cols}&rows=${term.rows}`; ws = new WebSocket(wsUrl);