import { networkInterfaces } from 'node:os'; import { Hono } from 'hono'; import { getConnInfo, serveStatic } from 'hono/bun'; import { compress } from 'hono/compress'; import { secureHeaders } from 'hono/secure-headers'; import { timeout } from 'hono/timeout'; import { rateLimiter } from 'hono-rate-limiter'; import { ALLOW_INSECURE_HTTP, API_KEY, PORT, PROTON_IMAP_PASSWORD, PROTON_IMAP_USERNAME, RATE_LIMIT, } from '@/constants/config'; import { PUBLIC_DIR } from '@/constants/paths'; import { cleanupDaemon, initSignal } from '@/modules/signal'; import admin from '@/routes/admin'; import ntfy from '@/routes/ntfy'; import unifiedpush from '@/routes/unified-push'; import { isLocalIP } from '@/utils/auth'; import { logError, logInfo, logVerbose, logWarn } from '@/utils/log'; initSignal(); if (!API_KEY) { logWarn('Server running without API_KEY'); } if (PROTON_IMAP_USERNAME && PROTON_IMAP_PASSWORD) { try { const { startProtonMonitor } = await import('./modules/proton-mail'); await startProtonMonitor(); } catch (err) { logError('Failed to start Proton Mail monitor:', err); logWarn('Continuing without Proton Mail integration'); } } const getLanIP = () => { const nets = networkInterfaces(); for (const name of Object.keys(nets)) { const interfaces = nets[name]; if (!interfaces) continue; for (const iface of interfaces) { if (iface.family === 'IPv4' && !iface.internal) { return iface.address; } } } return null; }; const app = new Hono(); const cspConfig = { defaultSrc: ["'self'"], scriptSrc: ["'self'"], styleSrc: ["'self'", "'unsafe-inline'"], imgSrc: ["'self'", 'data:'], formAction: ["'self'"], frameAncestors: ["'none'"], objectSrc: ["'none'"], }; const cspString = Object.entries(cspConfig) .map(([key, values]) => { const directive = key.replace(/([A-Z])/g, '-$1').toLowerCase(); return `${directive} ${values.join(' ')}`; }) .join('; '); app.use('*', (c, next) => { const proto = c.req.header('x-forwarded-proto') || 'http'; const addr = getConnInfo(c).remote.address; if (proto === 'https' || isLocalIP(addr)) { return secureHeaders({ contentSecurityPolicy: cspConfig, })(c, next); } c.header('Content-Security-Policy', cspString); return next(); }); app.use('*', (c, next) => { if (c.req.path === '/link/status-check') { return next(); } return timeout(5000)(c, next); }); app.use('*', compress()); app.use( '*', rateLimiter({ limit: RATE_LIMIT, keyGenerator: (c) => getConnInfo(c).remote.address || 'unknown', skip: (c) => ALLOW_INSECURE_HTTP || isLocalIP(getConnInfo(c).remote.address), }), ); app.use('*', serveStatic({ root: PUBLIC_DIR })); app.route('/', unifiedpush); app.route('/', ntfy); app.route('/', admin); app.notFound((c) => c.text('Not Found', 404)); const server = Bun.serve({ port: PORT, fetch: app.fetch, maxRequestBodySize: 1024 * 1024, idleTimeout: 30, }); logInfo(`\nSUP running on:`); logInfo(` Local: http://localhost:${server.port}`); const lanIP = getLanIP(); if (lanIP) { logVerbose(` Network: http://${lanIP}:${server.port}\n`); } process.on('SIGINT', () => { cleanupDaemon(); process.exit(0); }); process.on('SIGTERM', () => { cleanupDaemon(); process.exit(0); });