More attempts to fix docker

This commit is contained in:
2025-04-21 23:22:32 +02:00
parent 063877bc09
commit 2e265c08f1
17 changed files with 1567 additions and 116 deletions

27
.dockerignore Normal file
View File

@@ -0,0 +1,27 @@
# Node modules
node_modules
# Build artifacts
.svelte-kit
.npm
# Development files
.git
.github
.vscode
*.md
!README.md
*.log
# Temporary files
*.tmp
*.temp
# Specific configuration files
.env*
!.env.example
# Previous server implementations
unified-server.js
production.js
src/proxy-websocket-server.js

3
Caddyfile Normal file
View File

@@ -0,0 +1,3 @@
mud.iamtalon.me {
reverse_proxy svelte-mud:3000
}

View File

@@ -2,6 +2,10 @@
This guide explains how to use Docker to build and run the Svelte MUD client. This guide explains how to use Docker to build and run the Svelte MUD client.
## Solution Overview
This setup runs both the SvelteKit application and the WebSocket server in a single container, avoiding CORS issues. It follows the same approach used in development, where both servers run as separate processes but within the same context.
## Prerequisites ## Prerequisites
- [Docker](https://docs.docker.com/get-docker/) - [Docker](https://docs.docker.com/get-docker/)
@@ -14,14 +18,14 @@ This guide explains how to use Docker to build and run the Svelte MUD client.
cd path/to/svelte-mud cd path/to/svelte-mud
``` ```
2. Build and start the containers: 2. Build and start the container:
```bash ```bash
docker-compose up -d docker-compose up -d
``` ```
3. Access the application: 3. Access the application:
- Web interface: http://localhost:3000 - Web interface: http://localhost:3000
- WebSocket server: ws://localhost:3000/mud-ws (both services now run on the same port) - WebSocket server: ws://localhost:3001/mud-ws
## Docker Commands ## Docker Commands
@@ -43,57 +47,31 @@ docker-compose up --build
```bash ```bash
# Stop containers # Stop containers
docker-compose down docker-compose down
# Stop containers and remove volumes
docker-compose down -v
``` ```
### Viewing Logs ### Viewing Logs
```bash ```bash
# View all logs # View logs
docker-compose logs
# Follow logs
docker-compose logs -f docker-compose logs -f
# View logs for specific service
docker-compose logs -f svelte-mud
``` ```
## Configuration ## Caddy Configuration
The Docker setup uses a single port for both the web interface and WebSocket server: For use with Caddy as a reverse proxy, use this simple configuration:
- Port 3000: Unified server (Web + WebSocket)
You can modify these ports in the `docker-compose.yml` file if needed. ```
mud.example.com {
## Customization reverse_proxy svelte-mud:3000
}
### Environment Variables
You can add environment variables in the `docker-compose.yml` file:
```yaml
services:
svelte-mud:
environment:
- NODE_ENV=production
- PORT=3000
# Add your custom environment variables here
``` ```
### Building for Production Both the web interface and WebSocket connections will be routed correctly through this single reverse proxy rule.
The default configuration is optimized for production use. It:
- Uses a multi-stage build process to minimize image size
- Runs as a non-root user for better security
- Includes only production dependencies
## Troubleshooting ## Troubleshooting
1. **Port conflicts**: If port 3000 is already in use, modify the port mapping in `docker-compose.yml`. If you encounter any issues, check the container logs:
2. **Build failures**: Ensure that all dependencies are properly defined in your package.json. ```bash
docker-compose logs -f
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

@@ -34,18 +34,14 @@ RUN npm install --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 ./src
COPY --from=build /app/unified-server.js ./ COPY --from=build /app/run-production.js ./
# Switch to non-root user # Switch to non-root user
USER nodejs USER nodejs
# Expose ports for both web and websocket servers
EXPOSE 3000
EXPOSE 3001
# Set environment variables # Set environment variables
ENV NODE_ENV=production ENV NODE_ENV=production
# Start the unified server (web + websocket on the same port) # Start both servers using the production script
CMD ["node", "unified-server.js"] CMD ["node", "run-production.js"]

View File

@@ -7,26 +7,10 @@ services:
dockerfile: Dockerfile dockerfile: Dockerfile
container_name: svelte-mud container_name: svelte-mud
restart: unless-stopped restart: unless-stopped
# We don't need to publish ports to the host if we're using a reverse proxy
# ports:
# - "3005:3000" # Unified server (web + WebSocket)
networks: networks:
- revproxy - revproxy
environment: environment:
- NODE_ENV=production - NODE_ENV=production
# 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/"]
# interval: 30s
# timeout: 10s
# retries: 3
# start_period: 10s
volumes:
# Optional: Add persistent volumes if needed
# - ./logs:/app/logs
- /app/node_modules # Don't mount local node_modules
# Define networks to connect to external services # Define networks to connect to external services
networks: networks:

20
docker-production.js Normal file
View File

@@ -0,0 +1,20 @@
// Simple production entry point for Docker
// Runs the SvelteKit server only, WebSocket server is run separately
import { fileURLToPath } from 'url';
import { dirname } from 'path';
import { execFileSync } from 'child_process';
const __dirname = dirname(fileURLToPath(import.meta.url));
// Run the SvelteKit production server
try {
console.log('Starting SvelteKit production server');
// Execute the built SvelteKit server directly
execFileSync('node', ['build/index.js'], {
stdio: 'inherit',
cwd: __dirname
});
} catch (error) {
console.error('Error running SvelteKit server:', error);
process.exit(1);
}

47
docker/app.Dockerfile Normal file
View File

@@ -0,0 +1,47 @@
# Multi-stage build Dockerfile for Svelte MUD web application
# Build stage
FROM node:20-alpine AS build
# Set working directory
WORKDIR /app
# Copy package files
COPY package*.json ./
# Install dependencies
RUN npm install
# Copy source files
COPY . .
# Build the application
RUN npm run build
# Production stage
FROM node:20-alpine AS production
# Set working directory
WORKDIR /app
# Create a non-root user and group with the node user ID
RUN addgroup -g 1001 -S nodejs && \
adduser -S -u 1001 -G nodejs nodejs
# Install only production dependencies
COPY package*.json ./
RUN npm install --omit=dev
# Copy built application from the build stage
COPY --from=build /app/build ./build
COPY --from=build /app/docker-production.js ./
# Switch to non-root user
USER nodejs
# Set environment variables
ENV NODE_ENV=production
ENV PORT=3000
# Start the SvelteKit production server
CMD ["node", "docker-production.js"]

29
docker/ws.Dockerfile Normal file
View File

@@ -0,0 +1,29 @@
# Dockerfile for WebSocket server only
FROM node:20-alpine
# Set working directory
WORKDIR /app
# Copy package files
COPY package*.json ./
# Install only production dependencies
RUN npm install --omit=dev
# Copy only the websocket server code
COPY src/websocket-server.js ./src/
# Create a non-root user and group with the node user ID
RUN addgroup -g 1001 -S nodejs && \
adduser -S -u 1001 -G nodejs nodejs
# Switch to non-root user
USER nodejs
# Set environment variables
ENV NODE_ENV=production
ENV WS_PORT=3001
# Start the WebSocket server
CMD ["node", "src/websocket-server.js"]

209
integrated-server.js Normal file
View File

@@ -0,0 +1,209 @@
import { createServer } from 'http';
import { WebSocketServer } from 'ws';
import * as net from 'net';
import * as tls from 'tls';
import { parse } from 'url';
import { handler } from './build/handler.js';
// Create an HTTP server
const server = createServer(handler);
// Create WebSocket server
const wss = new WebSocketServer({ noServer: true });
// Active connections and their proxies
const connections = new Map();
// 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 integrated server
const PORT = process.env.PORT || 3000;
server.listen(PORT, () => {
console.log(`Integrated server (HTTP + WebSocket) is running on port ${PORT}`);
});

719
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -5,15 +5,12 @@
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite dev", "dev": "vite dev",
"ws": "node src/websocket-server.js",
"dev:full": "node start-server.js", "dev:full": "node start-server.js",
"build": "vite build", "build": "vite build",
"preview": "vite preview", "preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"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 run-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": {

View File

@@ -1,20 +1,22 @@
import { spawn } from 'child_process'; import { spawn } from 'child_process';
import { dirname } from 'path'; import { dirname } from 'path';
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url';
import { svelteKitPort } from './src/proxy-websocket-server.js';
const __dirname = dirname(fileURLToPath(import.meta.url)); const __dirname = dirname(fileURLToPath(import.meta.url));
// Start the WebSocket server // Start the SvelteKit production server on internal port
console.log('Starting WebSocket server'); console.log(`Starting SvelteKit production server on internal port ${svelteKitPort}`);
const wsServer = spawn('node', ['src/websocket-server.js'], { const sveltekit = spawn('node', ['build/index.js'], {
stdio: 'inherit', stdio: 'inherit',
shell: true, shell: true,
cwd: __dirname cwd: __dirname,
env: { ...process.env, PORT: svelteKitPort }
}); });
// Start the SvelteKit production server // Start the Proxy WebSocket server on the main port (3000)
console.log('Starting SvelteKit production server'); console.log('Starting Proxy WebSocket server on public port 3000');
const sveltekit = spawn('node', ['build/index.js'], { const proxyServer = spawn('node', ['src/proxy-websocket-server.js'], {
stdio: 'inherit', stdio: 'inherit',
shell: true, shell: true,
cwd: __dirname cwd: __dirname
@@ -23,14 +25,14 @@ const sveltekit = spawn('node', ['build/index.js'], {
// Handle process exit // Handle process exit
process.on('exit', () => { process.on('exit', () => {
console.log('Shutting down servers...'); console.log('Shutting down servers...');
wsServer.kill(); proxyServer.kill();
sveltekit.kill(); sveltekit.kill();
}); });
// Handle ctrl+c // Handle ctrl+c
process.on('SIGINT', () => { process.on('SIGINT', () => {
console.log('Received SIGINT, shutting down servers...'); console.log('Received SIGINT, shutting down servers...');
wsServer.kill('SIGINT'); proxyServer.kill('SIGINT');
sveltekit.kill('SIGINT'); sveltekit.kill('SIGINT');
process.exit(0); process.exit(0);
}); });
@@ -38,7 +40,7 @@ process.on('SIGINT', () => {
// Handle termination // Handle termination
process.on('SIGTERM', () => { process.on('SIGTERM', () => {
console.log('Received SIGTERM, shutting down servers...'); console.log('Received SIGTERM, shutting down servers...');
wsServer.kill('SIGTERM'); proxyServer.kill('SIGTERM');
sveltekit.kill('SIGTERM'); sveltekit.kill('SIGTERM');
process.exit(0); process.exit(0);
}); });

44
run-production.js Normal file
View File

@@ -0,0 +1,44 @@
import { spawn } from 'child_process';
import { dirname } from 'path';
import { fileURLToPath } from 'url';
const __dirname = dirname(fileURLToPath(import.meta.url));
// Start the WebSocket server
console.log('Starting WebSocket server');
const wsServer = spawn('node', ['src/websocket-server.js'], {
stdio: 'inherit',
shell: true,
cwd: __dirname
});
// Start the SvelteKit production server
console.log('Starting SvelteKit production server');
const sveltekit = spawn('node', ['build/index.js'], {
stdio: 'inherit',
shell: true,
cwd: __dirname
});
// Handle process exit
process.on('exit', () => {
console.log('Shutting down servers...');
wsServer.kill();
sveltekit.kill();
});
// Handle ctrl+c
process.on('SIGINT', () => {
console.log('Received SIGINT, shutting down servers...');
wsServer.kill('SIGINT');
sveltekit.kill('SIGINT');
process.exit(0);
});
// Handle termination
process.on('SIGTERM', () => {
console.log('Received SIGTERM, shutting down servers...');
wsServer.kill('SIGTERM');
sveltekit.kill('SIGTERM');
process.exit(0);
});

22
server.js Normal file
View File

@@ -0,0 +1,22 @@
import { createServer } from 'http';
import { handler } from './build/handler.js';
import express from 'express';
import { handleWebSocket } from './src/hooks.server.js';
// Create Express app
const app = express();
// Apply SvelteKit handler
app.use(handler);
// Create HTTP server
const server = createServer(app);
// Apply WebSocket handling
handleWebSocket(server);
// Start server
const PORT = process.env.PORT || 3000;
server.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});

View File

@@ -1,6 +1,205 @@
// This is a placeholder hooks.server.js file import { WebSocketServer } from 'ws';
// We're using a standalone WebSocket server instead of integrating with SvelteKit hooks import * as net from 'net';
import * as tls from 'tls';
import { parse } from 'url';
// Create WebSocket server instance
const wss = new WebSocketServer({ noServer: true });
// Active connections and their proxies
const connections = new Map();
// 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 WebSocket upgrades
export const handleWebSocket = (server) => {
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), let SvelteKit handle them
// Don't destroy the socket here
}
});
};
// Standard SvelteKit hook
export async function handle({ event, resolve }) { export async function handle({ event, resolve }) {
return await resolve(event); return await resolve(event);
} }

View File

@@ -0,0 +1,233 @@
import { WebSocketServer } from 'ws';
import * as net from 'net';
import * as tls from 'tls';
import { createProxyMiddleware } from 'http-proxy-middleware';
import express from 'express';
import { parse } from 'url';
import http from 'http';
// Define SvelteKit server port (internal only)
const SVELTEKIT_PORT = 3030;
// Create Express app for the proxy
const app = express();
// Create HTTP server
const server = http.createServer(app);
// Create WebSocket server
const wss = new WebSocketServer({ noServer: true });
// Active connections and their proxies
const connections = new Map();
// Proxy all regular HTTP requests to the SvelteKit server
app.use('/', createProxyMiddleware({
target: `http://localhost:${SVELTEKIT_PORT}`,
changeOrigin: true,
ws: false, // Don't proxy WebSockets here, we'll handle them separately
}));
// 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)' : ''}`);
}
});
}
// Socket error handler already defined above
// 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 server if this file is run directly
if (import.meta.url === `file://${process.argv[1]}`) {
const PORT = process.env.PORT || 3000;
server.listen(PORT, () => {
console.log(`Proxy WebSocket server is running on port ${PORT}, forwarding to SvelteKit on port ${SVELTEKIT_PORT}`);
});
}
// Export SVELTEKIT_PORT for use in other files
export const svelteKitPort = SVELTEKIT_PORT;
// Export the server for use in the main file
export default { server, SVELTEKIT_PORT };

View File

@@ -1,16 +1,16 @@
import { handler } from './build/handler.js'; import { createServer } from 'http';
import express from 'express'; import express from 'express';
import { WebSocketServer } from 'ws'; import { WebSocketServer } from 'ws';
import * as net from 'net'; import * as net from 'net';
import * as tls from 'tls'; import * as tls from 'tls';
import { parse } from 'url'; import { parse } from 'url';
import http from 'http'; import { handler } from './build/handler.js';
// Create an express app // Create Express app
const app = express(); const app = express();
// Create HTTP server from Express // Create HTTP server from Express
const server = http.createServer(app); const server = createServer(app);
// Create WebSocket server // Create WebSocket server
const wss = new WebSocketServer({ noServer: true }); const wss = new WebSocketServer({ noServer: true });
@@ -18,8 +18,8 @@ const wss = new WebSocketServer({ noServer: true });
// Active connections and their proxies // Active connections and their proxies
const connections = new Map(); const connections = new Map();
// Set up SvelteKit handler for all routes // Apply SvelteKit handler as middleware
// This must be the last middleware before error handlers // This needs to be before any other routes
app.use(handler); app.use(handler);
// Handle WebSocket connections // Handle WebSocket connections