Unified server for docker

This commit is contained in:
2025-04-21 22:18:23 +02:00
parent 32dd6ff811
commit 2bfd1b100e
5 changed files with 233 additions and 14 deletions

View File

@@ -21,7 +21,7 @@ This guide explains how to use Docker to build and run the Svelte MUD client.
3. Access the application: 3. Access the application:
- Web interface: http://localhost:3000 - 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 ## Docker Commands
@@ -63,9 +63,8 @@ docker-compose logs -f svelte-mud
## Configuration ## Configuration
The Docker setup uses the following ports: The Docker setup uses a single port for both the web interface and WebSocket server:
- Port 3000: Web server - Port 3000: Unified server (Web + WebSocket)
- Port 3001: WebSocket server
You can modify these ports in the `docker-compose.yml` file if needed. You can modify these ports in the `docker-compose.yml` file if needed.
@@ -80,7 +79,7 @@ services:
svelte-mud: svelte-mud:
environment: environment:
- NODE_ENV=production - NODE_ENV=production
- WS_PORT=3001 - PORT=3000
# Add your custom environment variables here # Add your custom environment variables here
``` ```
@@ -93,8 +92,8 @@ The default configuration is optimized for production use. It:
## Troubleshooting ## 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. 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`. 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`.

View File

@@ -35,7 +35,7 @@ RUN npm ci --omit=dev
# Copy built application from the build stage # Copy built application from the build stage
COPY --from=build /app/build ./build COPY --from=build /app/build ./build
COPY --from=build /app/src/websocket-server.js ./src/ COPY --from=build /app/src/websocket-server.js ./src/
COPY production.js ./ COPY --from=build /app/unified-server.js ./
# Switch to non-root user # Switch to non-root user
USER nodejs USER nodejs
@@ -47,5 +47,5 @@ EXPOSE 3001
# Set environment variables # Set environment variables
ENV NODE_ENV=production ENV NODE_ENV=production
# Start the full application (web + websocket) # Start the unified server (web + websocket on the same port)
CMD ["node", "production.js"] CMD ["node", "unified-server.js"]

View File

@@ -8,12 +8,11 @@ services:
container_name: svelte-mud container_name: svelte-mud
restart: unless-stopped restart: unless-stopped
ports: ports:
- "3000:3000" # Web server - "3000:3000" # Unified server (web + WebSocket)
- "3001:3001" # WebSocket server
environment: environment:
- NODE_ENV=production - NODE_ENV=production
# Optional: You can override the WebSocket port if needed # Optional: You can set a custom port
# - WS_PORT=3001 # - PORT=3000
# Uncomment for adding custom healthcheck # Uncomment for adding custom healthcheck
# healthcheck: # healthcheck:
# test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3000/"] # test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3000/"]

View File

@@ -13,6 +13,7 @@
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"start": "node build/index.js", "start": "node build/index.js",
"start:full": "node production.js", "start:full": "node production.js",
"start:unified": "node unified-server.js",
"generate-icons": "node generate-icons.js" "generate-icons": "node generate-icons.js"
}, },
"dependencies": { "dependencies": {
@@ -20,6 +21,7 @@
"@types/ws": "^8.18.1", "@types/ws": "^8.18.1",
"ansi-to-html": "^0.7.2", "ansi-to-html": "^0.7.2",
"events": "^3.3.0", "events": "^3.3.0",
"express": "^4.18.2",
"howler": "^2.2.4", "howler": "^2.2.4",
"net": "^1.0.2", "net": "^1.0.2",
"split.js": "^1.6.5", "split.js": "^1.6.5",

219
unified-server.js Normal file
View File

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