import { mkdir, readFile } from 'fs/promises'; import { writeFileSync, readFileSync, unlinkSync, existsSync } from 'fs'; import { join, dirname } from 'path'; import { fileURLToPath } from 'url'; import { secureImportModule } from './utils/plugins.js'; import * as logs from './utils/logs.js'; import express from 'express'; import { createServer } from 'http'; import { WebSocketServer } from 'ws'; import { spawn } from 'child_process'; // Load environment variables from .env file import dotenv from 'dotenv'; dotenv.config(); // Stop daemon: if run with -k, kill the running process and exit. if (process.argv.includes('-k')) { const pidFile = join(dirname(fileURLToPath(import.meta.url)), 'checkpoint.pid'); if (existsSync(pidFile)) { const pid = parseInt(readFileSync(pidFile, 'utf8'), 10); try { process.kill(pid); unlinkSync(pidFile); console.log(`Stopped daemon (pid ${pid})`); } catch (err) { console.error(`Failed to stop pid ${pid}: ${err}`); } } else { console.error(`No pid file found at ${pidFile}`); } process.exit(0); } // Daemonize: if run with -d, kill any existing daemon, then re-spawn detached, write pid file, and exit parent. if (process.argv.includes('-d')) { const pidFile = join(dirname(fileURLToPath(import.meta.url)), 'checkpoint.pid'); // If already running, stop the old daemon if (existsSync(pidFile)) { const oldPid = parseInt(readFileSync(pidFile, 'utf8'), 10); try { process.kill(oldPid); console.log(`Stopped old daemon (pid ${oldPid})`); } catch (e) { console.error(`Failed to stop old daemon (pid ${oldPid}): ${e}`); } try { unlinkSync(pidFile); } catch {} } // Spawn new background process const args = process.argv.slice(1).filter((arg) => arg !== '-d'); const cp = spawn(process.argv[0], args, { detached: true, stdio: 'ignore' }); cp.unref(); writeFileSync(pidFile, cp.pid.toString(), 'utf8'); console.log(`Daemonized (pid ${cp.pid}), pid stored in ${pidFile}`); process.exit(0); } const pluginRegistry = []; export function registerPlugin(pluginName, handler) { pluginRegistry.push({ name: pluginName, handler }); } /** * Return the array of middleware handlers in registration order. */ export function loadPlugins() { return pluginRegistry.map((item) => item.handler); } /** * Return the names of all registered plugins. */ export function getRegisteredPluginNames() { return pluginRegistry.map((item) => item.name); } /** * Freeze plugin registry to prevent further registration and log the final set. */ export function freezePlugins() { Object.freeze(pluginRegistry); pluginRegistry.forEach((item) => Object.freeze(item)); logs.msg('Plugin registration frozen'); } // Determine root directory for config loading const __dirname = dirname(fileURLToPath(import.meta.url)); export const rootDir = __dirname; export async function loadConfig(name, target) { const configPath = join(rootDir, 'config', `${name}.toml`); const txt = await readFile(configPath, 'utf8'); const { default: toml } = await import('@iarna/toml'); Object.assign(target, toml.parse(txt)); logs.config(name, 'loaded'); } async function initDataDirectories() { logs.section('INIT'); const directories = [join(rootDir, 'data'), join(rootDir, 'db'), join(rootDir, 'config')]; for (const dirPath of directories) { try { await mkdir(dirPath, { recursive: true }); } catch {} } logs.init('Data directories are now in place'); } function staticFileMiddleware() { const router = express.Router(); router.use('/webfont', express.static(join(rootDir, 'pages/interstitial/webfont'), { maxAge: '7d' })); router.use('/js', express.static(join(rootDir, 'pages/interstitial/js'), { maxAge: '7d' })); return router; } async function main() { await initDataDirectories(); logs.section('CONFIG'); logs.config('checkpoint', 'loaded'); logs.config('ipfilter', 'loaded'); logs.config('proxy', 'loaded'); logs.config('stats', 'loaded'); logs.section('OPERATIONS'); const app = express(); const server = createServer(app); // Trust proxy headers (important for proper protocol detection) app.set('trust proxy', true); // Initialize WebSocket server const wss = new WebSocketServer({ noServer: true }); try { await secureImportModule('checkpoint.js'); } catch (e) { logs.error('checkpoint', `Failed to load checkpoint plugin: ${e}`); } try { await secureImportModule('plugins/ipfilter.js'); } catch (e) { logs.error('ipfilter', `Failed to load IP filter plugin: ${e}`); } try { await secureImportModule('plugins/proxy.js'); } catch (e) { logs.error('proxy', `Failed to load proxy plugin: ${e}`); } try { await secureImportModule('plugins/stats.js'); } catch (e) { logs.error('stats', `Failed to load stats plugin: ${e}`); } // Register static middleware app.use(staticFileMiddleware()); logs.section('PLUGINS'); // Ensure ipfilter runs first by moving it to front of the registry const ipIndex = pluginRegistry.findIndex((item) => item.name === 'ipfilter'); if (ipIndex > 0) { const [ipEntry] = pluginRegistry.splice(ipIndex, 1); pluginRegistry.unshift(ipEntry); } pluginRegistry.forEach((item) => logs.msg(item.name)); logs.section('SYSTEM'); freezePlugins(); // Apply all plugin middlewares to Express const middlewareHandlers = loadPlugins(); middlewareHandlers.forEach(handler => { if (handler && handler.middleware) { // If plugin exports an object with middleware property if (Array.isArray(handler.middleware)) { // If middleware is an array, apply each one handler.middleware.forEach(mw => app.use(mw)); } else { // Single middleware app.use(handler.middleware); } } else if (typeof handler === 'function') { // Legacy function-style handlers (shouldn't exist anymore) logs.warn('server', 'Found legacy function-style plugin handler'); app.use(handler); } }); // 404 handler app.use((req, res) => { res.status(404).send('Not Found'); }); // Error handler app.use((err, req, res, next) => { logs.error('server', `Server error: ${err.message}`); res.status(500).send(`Server Error: ${err.message}`); }); // Handle WebSocket upgrades for http-proxy-middleware server.on('upgrade', (request, socket, head) => { // http-proxy-middleware handles WebSocket upgrades internally // This is just a placeholder for any custom WebSocket handling if needed wss.handleUpgrade(request, socket, head, (ws) => { wss.emit('connection', ws, request); }); }); logs.section('SERVER'); const portNumber = Number(process.env.PORT || 3000); server.listen(portNumber, () => { logs.server(`🚀 Server is up and running on port ${portNumber}...`); logs.section('REQ LOGS'); }); } main();