diff --git a/DOCKER-README.md b/DOCKER-README.md index 143ef3b..4763cba 100644 --- a/DOCKER-README.md +++ b/DOCKER-README.md @@ -21,7 +21,7 @@ This guide explains how to use Docker to build and run the Svelte MUD client. 3. Access the application: - Web interface: http://localhost:3000 - - WebSocket server: ws://localhost:3001/mud-ws + - WebSocket server: ws://localhost:3000/mud-ws (both services now run on the same port) ## Docker Commands @@ -63,9 +63,8 @@ docker-compose logs -f svelte-mud ## Configuration -The Docker setup uses the following ports: -- Port 3000: Web server -- Port 3001: WebSocket server +The Docker setup uses a single port for both the web interface and WebSocket server: +- Port 3000: Unified server (Web + WebSocket) You can modify these ports in the `docker-compose.yml` file if needed. @@ -80,7 +79,7 @@ services: svelte-mud: environment: - NODE_ENV=production - - WS_PORT=3001 + - PORT=3000 # Add your custom environment variables here ``` @@ -93,8 +92,8 @@ The default configuration is optimized for production use. It: ## Troubleshooting -1. **Port conflicts**: If ports 3000 or 3001 are already in use, modify the port mappings in `docker-compose.yml`. +1. **Port conflicts**: If port 3000 is already in use, modify the port mapping in `docker-compose.yml`. 2. **Build failures**: Ensure that all dependencies are properly defined in your package.json. -3. **Connection issues**: If you can't connect to the WebSocket server, verify that your client is using the correct URL format: `ws://localhost:3001/mud-ws?host=YOUR_MUD_HOST&port=YOUR_MUD_PORT`. \ No newline at end of file +3. **Connection issues**: If you can't connect to the WebSocket server, verify that your client is using the correct URL format: `ws://localhost:3000/mud-ws?host=YOUR_MUD_HOST&port=YOUR_MUD_PORT`. \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 48b286e..6dd448e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -35,7 +35,7 @@ RUN npm ci --omit=dev # Copy built application from the build stage COPY --from=build /app/build ./build COPY --from=build /app/src/websocket-server.js ./src/ -COPY production.js ./ +COPY --from=build /app/unified-server.js ./ # Switch to non-root user USER nodejs @@ -47,5 +47,5 @@ EXPOSE 3001 # Set environment variables ENV NODE_ENV=production -# Start the full application (web + websocket) -CMD ["node", "production.js"] \ No newline at end of file +# Start the unified server (web + websocket on the same port) +CMD ["node", "unified-server.js"] \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 1ba0503..14256e5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,12 +8,11 @@ services: container_name: svelte-mud restart: unless-stopped ports: - - "3000:3000" # Web server - - "3001:3001" # WebSocket server + - "3000:3000" # Unified server (web + WebSocket) environment: - NODE_ENV=production - # Optional: You can override the WebSocket port if needed - # - WS_PORT=3001 + # Optional: You can set a custom port + # - PORT=3000 # Uncomment for adding custom healthcheck # healthcheck: # test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3000/"] diff --git a/package.json b/package.json index edeeb6d..8056ec4 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", "start": "node build/index.js", "start:full": "node production.js", + "start:unified": "node unified-server.js", "generate-icons": "node generate-icons.js" }, "dependencies": { @@ -20,6 +21,7 @@ "@types/ws": "^8.18.1", "ansi-to-html": "^0.7.2", "events": "^3.3.0", + "express": "^4.18.2", "howler": "^2.2.4", "net": "^1.0.2", "split.js": "^1.6.5", diff --git a/unified-server.js b/unified-server.js new file mode 100644 index 0000000..eda1db1 --- /dev/null +++ b/unified-server.js @@ -0,0 +1,219 @@ +import { handler } from './build/handler.js'; +import express from 'express'; +import { WebSocketServer } from 'ws'; +import * as net from 'net'; +import * as tls from 'tls'; +import { parse } from 'url'; +import http from 'http'; + +// Create an express app +const app = express(); + +// Create HTTP server from Express +const server = http.createServer(app); + +// Create WebSocket server +const wss = new WebSocketServer({ noServer: true }); + +// Active connections and their proxies +const connections = new Map(); + +// Set up SvelteKit handler for all routes +// This must be the last middleware before error handlers +app.use(handler); + +// Handle WebSocket connections +wss.on('connection', (ws, req, mudHost, mudPort, useSSL) => { + console.log(`WebSocket connection established for ${mudHost}:${mudPort} (SSL: ${useSSL})`); + + // Create a unique ID for this connection + const connectionId = `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; + + // Special handling for test connections + if (mudHost === 'example.com' && mudPort === '23') { + console.log('Test connection detected - using echo server mode'); + + // Send welcome message + ws.send('Hello from WebSocket test server! This is an echo server.'); + + // Echo back messages + ws.on('message', (message) => { + console.log('Test server received:', message.toString()); + ws.send(`Echo: ${message.toString()}`); + }); + + // Handle close + ws.on('close', () => { + console.log('Test connection closed'); + connections.delete(connectionId); + }); + + // Store the connection (without a socket) + connections.set(connectionId, { ws, testMode: true }); + return; + } + + let socket; + try { + // Create a TCP socket connection to the MUD server + // Use tls for SSL connections, net for regular connections + socket = useSSL + ? tls.connect({ host: mudHost, port: parseInt(mudPort), rejectUnauthorized: false }) + : net.createConnection({ host: mudHost, port: parseInt(mudPort) }); + + // Add error handler + socket.on('error', (error) => { + console.error(`Socket error for ${mudHost}:${mudPort}:`, error.message); + // Send error to client + if (ws.readyState === 1) { + ws.send(Buffer.from(`ERROR: Connection to MUD server failed: ${error.message}\r\n`)); + setTimeout(() => { + if (ws.readyState === 1) ws.close(); + }, 1000); + } + // Remove from connections map + connections.delete(connectionId); + }); + + // Store the connection + connections.set(connectionId, { ws, socket }); + } catch (error) { + console.error(`Error creating socket connection: ${error.message}`); + if (ws.readyState === 1) { + ws.send(Buffer.from(`ERROR: Failed to connect to MUD server: ${error.message}\r\n`)); + ws.close(); + } + return; + } + + // Handle data from the MUD server - only in regular mode, not test mode + if (socket) { + socket.on('data', (data) => { + // Check for GMCP data (IAC SB GMCP) - very basic check for debugging + // IAC = 255, SB = 250, GMCP = 201 + let isGmcp = false; + for (let i = 0; i < data.length - 2; i++) { + if (data[i] === 255 && data[i+1] === 250 && data[i+2] === 201) { + isGmcp = true; + console.log('WebSocket server: Detected GMCP data in server response'); + break; + } + } + + // Forward data to the WebSocket client if it's still open + if (ws.readyState === 1) { // WebSocket.OPEN + ws.send(data); + console.log(`WebSocket server: Sent ${data.length} bytes to client${isGmcp ? ' (contains GMCP data)' : ''}`); + } + }); + } + + // Handle socket close + if (socket) { + socket.on('close', () => { + console.log(`MUD connection closed for ${mudHost}:${mudPort}`); + // Close WebSocket if it's still open + if (ws.readyState === 1) { + ws.close(); + } + // Remove from connections map + connections.delete(connectionId); + }); + } + + // Handle WebSocket messages (data from client to server) + ws.on('message', (message) => { + try { + // Skip if this is a test connection (already handled in the test mode section) + const conn = connections.get(connectionId); + if (conn.testMode) return; + + // Check for GMCP data (IAC SB GMCP) in client messages + let isGmcp = false; + if (message instanceof Buffer || message instanceof Uint8Array) { + for (let i = 0; i < message.length - 2; i++) { + if (message[i] === 255 && message[i+1] === 250 && message[i+2] === 201) { + isGmcp = true; + console.log('WebSocket server: Detected GMCP data in client message'); + break; + } + } + } + + // Forward data to the MUD server + // The message might be Buffer, ArrayBuffer, or string + if (conn.socket && conn.socket.writable) { + conn.socket.write(message); + console.log(`WebSocket server: Sent ${message.length} bytes to MUD server${isGmcp ? ' (contains GMCP data)' : ''}`); + } else { + console.error('Socket not writable, cannot send data to MUD server'); + if (ws.readyState === 1) { // WebSocket.OPEN + ws.send(Buffer.from(`ERROR: Cannot send data to MUD server: Socket not connected\r\n`)); + } + } + } catch (error) { + console.error('Error forwarding message to MUD server:', error); + if (ws.readyState === 1) { // WebSocket.OPEN + ws.send(Buffer.from(`ERROR: Failed to send data to MUD server: ${error.message}\r\n`)); + } + } + }); + + // Handle WebSocket close + ws.on('close', () => { + console.log(`WebSocket closed for ${mudHost}:${mudPort}`); + // Close socket if it's still open + const conn = connections.get(connectionId); + if (conn && conn.socket) { + conn.socket.end(); + } + // Remove from connections map + connections.delete(connectionId); + }); + + // Handle WebSocket errors + ws.on('error', (error) => { + console.error(`WebSocket error for ${mudHost}:${mudPort}:`, error.message); + // Close socket on error + const conn = connections.get(connectionId); + if (conn && conn.socket) { + conn.socket.end(); + } + // Remove from connections map + connections.delete(connectionId); + }); +}); + +// Handle HTTP server upgrade (WebSocket handshake) +server.on('upgrade', (request, socket, head) => { + // Parse URL to get query parameters + const { pathname, query } = parse(request.url, true); + + // Only handle WebSocket connections to /mud-ws + if (pathname === '/mud-ws') { + // Extract MUD server details from query parameters + const { host, port, useSSL } = query; + + if (!host || !port) { + socket.write('HTTP/1.1 400 Bad Request\r\n\r\n'); + socket.destroy(); + return; + } + + // Handle WebSocket upgrade + wss.handleUpgrade(request, socket, head, (ws) => { + wss.emit('connection', ws, request, host, port, useSSL === 'true'); + }); + } else { + // For other upgrades (not to /mud-ws), close the connection + socket.destroy(); + } +}); + +// Start the unified server +const PORT = process.env.PORT || 3000; +server.listen(PORT, () => { + console.log(`Unified server (HTTP + WebSocket) is running on port ${PORT}`); +}); + +export default server; \ No newline at end of file