"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.startProxy = startProxy; exports.stopProxy = stopProxy; const node_http_1 = __importDefault(require("node:http")); const node_crypto_1 = __importDefault(require("node:crypto")); const jsonwebtoken_1 = __importDefault(require("jsonwebtoken")); const client_1 = require("@prisma/client"); const env_1 = require("../config/env"); const logger_1 = require("../utils/logger"); // HMAC secret — regenerated each server start (cookie sessions reset on restart) const HMAC_SECRET = node_crypto_1.default.randomBytes(32); const COOKIE_NAME = '_lmp_auth'; const COOKIE_MAX_AGE_S = 4 * 60 * 60; // 4 hours // Extract Listmonk internal host/port from LISTMONK_URL (e.g. "http://listmonk-app:9000") function parseListmonkTarget() { const url = new URL(env_1.env.LISTMONK_URL); return { hostname: url.hostname, port: parseInt(url.port || '9000', 10) }; } // Basic auth header for Listmonk API user function listmonkAuthHeader() { const creds = Buffer.from(`${env_1.env.LISTMONK_ADMIN_USER}:${env_1.env.LISTMONK_ADMIN_PASSWORD}`).toString('base64'); return `Basic ${creds}`; } function signCookie(payload) { const data = Buffer.from(JSON.stringify(payload)).toString('base64url'); const sig = node_crypto_1.default.createHmac('sha256', HMAC_SECRET).update(data).digest('base64url'); return `${data}.${sig}`; } function verifyCookie(cookie) { const dot = cookie.indexOf('.'); if (dot === -1) return null; const data = cookie.slice(0, dot); const sig = cookie.slice(dot + 1); const expected = node_crypto_1.default.createHmac('sha256', HMAC_SECRET).update(data).digest('base64url'); if (!node_crypto_1.default.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) return null; try { const payload = JSON.parse(Buffer.from(data, 'base64url').toString()); if (payload.exp < Date.now() / 1000) return null; return payload; } catch { return null; } } function parseCookies(header) { const cookies = {}; if (!header) return cookies; for (const pair of header.split(';')) { const eq = pair.indexOf('='); if (eq === -1) continue; const key = pair.slice(0, eq).trim(); const val = pair.slice(eq + 1).trim(); cookies[key] = val; } return cookies; } let server = null; function startProxy() { const target = parseListmonkTarget(); server = node_http_1.default.createServer((req, res) => { const url = new URL(req.url || '/', `http://${req.headers.host}`); // --- /auth?token= — exchange JWT for proxy cookie --- if (url.pathname === '/auth') { const token = url.searchParams.get('token'); if (!token) { res.writeHead(400, { 'Content-Type': 'text/plain' }); res.end('Missing token parameter'); return; } try { const payload = jsonwebtoken_1.default.verify(token, env_1.env.JWT_ACCESS_SECRET, { algorithms: ['HS256'] }); const payloadRoles = payload.roles || [payload.role]; if (!payloadRoles.includes(client_1.UserRole.SUPER_ADMIN)) { res.writeHead(403, { 'Content-Type': 'text/plain' }); res.end('Forbidden — SUPER_ADMIN role required'); return; } const cookieVal = signCookie({ uid: payload.id, exp: Math.floor(Date.now() / 1000) + COOKIE_MAX_AGE_S, }); res.writeHead(302, { Location: '/admin/', 'Set-Cookie': `${COOKIE_NAME}=${cookieVal}; HttpOnly; SameSite=Lax; Path=/; Max-Age=${COOKIE_MAX_AGE_S}`, }); res.end(); } catch { res.writeHead(401, { 'Content-Type': 'text/plain' }); res.end('Invalid or expired token'); } return; } // --- All other routes: verify cookie, proxy to Listmonk --- const cookies = parseCookies(req.headers.cookie); const cookieVal = cookies[COOKIE_NAME]; if (!cookieVal || !verifyCookie(cookieVal)) { res.writeHead(403, { 'Content-Type': 'text/plain' }); res.end('Forbidden — authenticate at /auth?token= first'); return; } // Build proxy request headers (copy originals, inject auth, remove cookie) const proxyHeaders = { ...req.headers }; delete proxyHeaders.host; delete proxyHeaders.cookie; proxyHeaders.authorization = listmonkAuthHeader(); const proxyReq = node_http_1.default.request({ hostname: target.hostname, port: target.port, path: req.url, method: req.method, headers: proxyHeaders, }, (proxyRes) => { // Strip frame-busting headers from Listmonk responses const responseHeaders = { ...proxyRes.headers }; delete responseHeaders['x-frame-options']; delete responseHeaders['content-security-policy']; res.writeHead(proxyRes.statusCode || 502, responseHeaders); proxyRes.pipe(res); }); proxyReq.on('error', (err) => { logger_1.logger.error('Listmonk proxy error:', err); if (!res.headersSent) { res.writeHead(502, { 'Content-Type': 'text/plain' }); } res.end('Listmonk is unreachable'); }); // Pipe request body for POST/PUT/PATCH req.pipe(proxyReq); }); server.listen(env_1.env.LISTMONK_PROXY_PORT, () => { logger_1.logger.info(`Listmonk proxy listening on port ${env_1.env.LISTMONK_PROXY_PORT}`); }); return server; } function stopProxy() { return new Promise((resolve) => { if (server) { server.close(() => resolve()); } else { resolve(); } }); } //# sourceMappingURL=listmonk-proxy.service.js.map