mirror of
https://github.com/lone-cloud/prism
synced 2026-06-03 08:43:10 -07:00
143 lines
3.4 KiB
TypeScript
143 lines
3.4 KiB
TypeScript
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/unifiedpush';
|
|
import { isLocalIP } from '@/utils/auth';
|
|
import { logError, logInfo, logVerbose, logWarn } from '@/utils/log';
|
|
|
|
try {
|
|
await initSignal();
|
|
} catch (error) {
|
|
logError(`${error instanceof Error ? error.message : String(error)}`);
|
|
}
|
|
|
|
if (!API_KEY) {
|
|
logWarn('Server running without API_KEY');
|
|
}
|
|
|
|
if (PROTON_IMAP_USERNAME && PROTON_IMAP_PASSWORD) {
|
|
try {
|
|
const { startProtonMonitor } = await import('./modules/protonmail');
|
|
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:', 'https://api.qrserver.com'],
|
|
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);
|
|
});
|