import { createServer } from 'node:http'; import crypto from 'node:crypto'; import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import express from 'express'; import { Server } from 'socket.io'; import winston from 'winston'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); // *************************************************************************** // Config // *************************************************************************** // Everything comes from the environment -- see .env.example. `npm run dev` // loads .env; in production the service manager passes real env vars. This // replaces the old client-side ENV switch in public/javascripts/app.js, which // hardcoded chess.davidawindham.com:8181 and had to be hand-edited per host. const config = { port: Number(process.env.PORT ?? 8181), // '' when served at the domain root, '/chess' when mounted at a subpath. basePath: normalizeBasePath(process.env.BASE_PATH ?? ''), // Number of reverse proxies in front of us, or a preset like 'loopback'. trustProxy: process.env.TRUST_PROXY ?? '', logFile: process.env.LOG_FILE ?? path.join(__dirname, 'logs/games.log'), // Local dev only. See the /embed proxy below. dawOrigin: process.env.DAW_ORIGIN ?? '', }; function normalizeBasePath(value) { const trimmed = value.trim().replace(/\/+$/, ''); if (!trimmed) return ''; return trimmed.startsWith('/') ? trimmed : `/${trimmed}`; } // A game link is the only thing gating access to a game, so tokens are the // closest thing this app has to a credential. Shape is fixed by makeToken: // 15 random bytes as base64url = 20 chars from [A-Za-z0-9_-]. const TOKEN_RE = /^[A-Za-z0-9_-]{20}$/; // Mirrors the bounds the form enforces client-side (views/index.pug). const MIN_MINUTES = 1; const MAX_MINUTES = 50; const MAX_INCREMENT = 50; const TOKEN_TTL_MS = 5 * 60 * 1000; // *************************************************************************** // Logging // *************************************************************************** fs.mkdirSync(path.dirname(config.logFile), { recursive: true }); const format = winston.format.combine(winston.format.timestamp(), winston.format.simple()); // Operational messages -- boot, config, the embed proxy. Console only, so the // service manager collects them. Deliberately kept out of games.log: that file // is world-readable over GET /logs, and this is where the port and DAW_ORIGIN // would otherwise leak into it. const logger = winston.createLogger({ level: 'info', format, transports: [new winston.transports.Console()], }); // games.log, which GET /logs serves. Winston had no file transport at all // before, so the file was never written and that route could only ever reach // its error path. Game counts only -- see the note on the route. const gameLogger = winston.createLogger({ level: 'info', format, transports: [ new winston.transports.File({ filename: config.logFile }), new winston.transports.Console(), ], }); // *************************************************************************** // App // *************************************************************************** const app = express(); const server = createServer(app); if (config.trustProxy) { // Needed for correct client IPs / protocol when Apache fronts us at /chess. const asNumber = Number(config.trustProxy); app.set('trust proxy', Number.isNaN(asNumber) ? config.trustProxy : asNumber); } app.set('views', path.join(__dirname, 'views')); app.set('view engine', 'pug'); // Templates build every asset URL from this, so the same views work whether the // app is at the domain root or under /chess. app.locals.basePath = config.basePath; const router = express.Router(); router.use(express.static(path.join(__dirname, 'public'))); router.get('/', (req, res) => { res.render('index'); }); router.get('/about', (req, res) => { res.render('about'); }); // The three params land in a