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'; // 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 = Bun.spawn({ cmd: [process.argv[0], ...args], detached: true, stdio: ['ignore', 'ignore', '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() { return async (request) => { const url = new URL(request.url); const pathname = url.pathname; if (pathname.startsWith('/webfont/') || pathname.startsWith('/js/')) { const filePath = join(rootDir, 'pages/interstitial', pathname.slice(1)); try { return new Response(Bun.file(filePath), { headers: { 'Cache-Control': 'public, max-age=604800' }, }); } catch { return new Response('Not Found', { status: 404 }); } } return undefined; }; } 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'); let wsHandler; 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'); const mod = await import('./plugins/proxy.js'); wsHandler = mod.proxyWebSocketHandler; } 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}`); } registerPlugin('static', 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(); logs.section('SERVER'); const portNumber = Number(process.env.PORT || 3000); const middlewareHandlers = loadPlugins(); logs.server(`🚀 Server is up and running on port ${portNumber}...`); logs.section('REQ LOGS'); Bun.serve({ port: portNumber, async fetch(request, server) { for (const handler of middlewareHandlers) { try { const resp = await handler(request, server); if (resp instanceof Response) return resp; } catch (err) { logs.error('server', `Handler error: ${err}`); } } return new Response('Not Found', { status: 404 }); }, websocket: wsHandler, error(err) { return new Response(`Server Error: ${err.message}`, { status: 500 }); }, }); } main();