Ported only the dual-stack listen from PR #291 (the change accepted as #188). With HOST unset the server binds :: and, if that fails with EAFNOSUPPORT, EADDRNOTAVAIL or EINVAL, retries 0.0.0.0. An explicit HOST, including 0.0.0.0 and 127.0.0.1, is tried once. The boot banner and doctor list shareable IPv6 addresses in brackets, skip fe80::, and keep one URL per /64. An explicit IPv6 literal in the local URL is bracketed. The Windows service listing no longer drops those bracketed lines. Not ported: /dev/grant, the settings-default migration, and the firewall scripts. The Docker image stays on 0.0.0.0; the alpine fallback was not booted. Tests: node --test test/ipv6-bind.test.js test/doctor.test.js test/ci.test.js — 20 pass, 0 fail (8 bind, 4 doctor, 8 ci).
104 lines
4.6 KiB
JavaScript
104 lines
4.6 KiB
JavaScript
// server/http/config.js — where the server's settings come from. startServer() options win over the environment:
|
|
//
|
|
// * PORT (default 3000), HOST (default '::', one dual-stack socket for IPv6 and IPv4);
|
|
// * TRUST_PROXY ('auto' default: honour CF-Connecting-IP / X-Real-IP / X-Forwarded-For only from loopback/private
|
|
// peers such as a local cloudflared; '1' always; '0' never) → net.js trustProxy;
|
|
// * DEBUG → the console logger's debug level;
|
|
// * the served directories (public/, data/, shared/ and the content packs' packs/ of this repository unless the
|
|
// options name others), and which startServer() options are handed on to net.js Network and lobby.js Lobby.
|
|
|
|
import path from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
/** Repository root. */
|
|
export const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
|
|
export const noopLog = { info() {}, warn() {}, error() {}, debug() {} };
|
|
|
|
/** startServer() options handed on to net.js Network / lobby.js Lobby (an absent one keeps that module's default). */
|
|
const NET_OPTION_KEYS = ['reconnectWindowMs', 'heartbeatMs', 'helloTimeoutMs', 'ratePerSec', 'rateBurst', 'maxConnections', 'abuseDropsPerSec',
|
|
'maxConnectionsPerAddr', 'heavyPerSec', 'heavyBurst', 'trustProxy'];
|
|
const LOBBY_OPTION_KEYS = ['lobbyGraceMs', 'maxRooms', 'maxRoomsPerAddr', 'maxMatchesPerAddr', 'resyncMinGapMs', 'soloReconnectWindowMs'];
|
|
|
|
/**
|
|
* Bind address used when neither the `host` option nor `HOST` says otherwise: one dual-stack socket, so the server
|
|
* answers IPv6 and IPv4 alike without a second listener (Node keeps `ipv6Only` off for `::`).
|
|
* `HOST=0.0.0.0` still means IPv4 only, `HOST=127.0.0.1` still means loopback only (a reverse proxy in front).
|
|
*/
|
|
export const DEFAULT_BIND_HOST = '::';
|
|
|
|
/**
|
|
* Where to listen: the `port` / `host` options, else PORT / HOST, else port 3000 on DEFAULT_BIND_HOST.
|
|
* An empty host is treated as unset.
|
|
* @param {{ port?: number, host?: string }} opts
|
|
* @returns {{ port: number, host: string }}
|
|
* @throws {RangeError} when the port is not an integer in 0…65535
|
|
*/
|
|
export function listenAddress(opts) {
|
|
const port = opts.port ?? (process.env.PORT != null && process.env.PORT !== '' ? Number(process.env.PORT) : 3000);
|
|
const host = (opts.host || process.env.HOST) || DEFAULT_BIND_HOST;
|
|
if (!Number.isInteger(port) || port < 0 || port > 65535) throw new RangeError(`invalid PORT ${port}`);
|
|
return { port, host };
|
|
}
|
|
|
|
/**
|
|
* The hosts to try, in order, for a bind. Only the default is retried: a host with IPv6 switched off refuses `::`
|
|
* with EAFNOSUPPORT / EADDRNOTAVAIL / EINVAL, and falling back to IPv4 beats not booting. An explicit HOST is literal.
|
|
* @param {string} host a host out of listenAddress()
|
|
* @returns {string[]}
|
|
*/
|
|
export function bindCandidates(host) {
|
|
return host === DEFAULT_BIND_HOST ? [DEFAULT_BIND_HOST, '0.0.0.0'] : [host];
|
|
}
|
|
|
|
/**
|
|
* The directories the static server reads (packsDir: the pack folders, server/packs.js).
|
|
* @param {{ publicDir?: string, dataDir?: string, sharedDir?: string, packsDir?: string }} opts
|
|
*/
|
|
export function serveDirs(opts) {
|
|
return {
|
|
publicDir: opts.publicDir || path.join(ROOT, 'public'),
|
|
dataDir: opts.dataDir || path.join(ROOT, 'data'),
|
|
sharedDir: opts.sharedDir || path.join(ROOT, 'shared'),
|
|
packsDir: opts.packsDir || path.join(ROOT, 'packs'),
|
|
};
|
|
}
|
|
|
|
/** net.js Network options out of the startServer() options; `trustProxy` falls back to TRUST_PROXY. */
|
|
export function netOptionsFrom(opts) {
|
|
const netOptions = {};
|
|
for (const k of NET_OPTION_KEYS) {
|
|
if (opts[k] != null) netOptions[k] = opts[k];
|
|
}
|
|
if (netOptions.trustProxy == null) netOptions.trustProxy = parseTrustProxy(process.env.TRUST_PROXY);
|
|
return netOptions;
|
|
}
|
|
|
|
/** lobby.js Lobby options out of the startServer() options. */
|
|
export function lobbyOptionsFrom(opts) {
|
|
const lobbyOptions = {};
|
|
for (const k of LOBBY_OPTION_KEYS) {
|
|
if (opts[k] != null) lobbyOptions[k] = opts[k];
|
|
}
|
|
return lobbyOptions;
|
|
}
|
|
|
|
/** TRUST_PROXY env → net.js trustProxy ('auto' unless explicitly on/off). @param {string | undefined} v */
|
|
export function parseTrustProxy(v) {
|
|
const s = String(v ?? '').trim().toLowerCase();
|
|
if (['1', 'true', 'yes', 'on', 'always'].includes(s)) return true;
|
|
if (['0', 'false', 'no', 'off', 'never'].includes(s)) return false;
|
|
return 'auto';
|
|
}
|
|
|
|
/** The console logger (`quiet` → silent; debug lines only with DEBUG set). */
|
|
export function makeLogger(quiet) {
|
|
if (quiet) return noopLog;
|
|
return {
|
|
info: (...a) => console.log(...a),
|
|
warn: (...a) => console.warn(...a),
|
|
error: (...a) => console.error(...a),
|
|
debug: process.env.DEBUG ? (...a) => console.debug(...a) : () => {},
|
|
};
|
|
}
|