/* eslint-disable no-unused-vars */ 'use strict'; // Ref: http://javascript.tutorialhorizon.com/2014/09/20/organizing-your-expressjs-routes-in-separate-files/ const express = require('express'); require('express-async-errors'); const rateLimit = require('express-rate-limit') const debug = require('debug')('agm:server'), compression = require('compression'), path = require('path'), fs = require('fs-extra'), http = require('http'), https1 = require('https'), http2 = require('node:http2'), // Native HTTP/2 server (Express expects HTTP/1.1 req/res; do not advertise h2 unless using a compat layer) app = express(), env = require('./helpers/env'), { registerFatalHandlers, createServerIgnore } = require('./helpers/process_fatal_handlers'), dbConnect = require('./helpers/db/connect.js'), { checkUser } = require('./middlewares/app_validator.js'), { ErrorHandler } = require('./middlewares/error_handler.js'), errorHandler = require('error-handler').errorHandler; http.globalAgent.maxSockets = Infinity; const MAX_REQ_BDY_MB = env.MAX_REQ_BDY_MB || '150mb' // Load ENV vars specified in the .env file (within the same folder) debug("Is in Production: ", env.PRODUCTION); app.isProd = env.PRODUCTION; process.setMaxListeners(0); // DISABLED (intentionally): error-handler's process hooks use key-file-storage (read/modify/write) // which has produced corrupted *.rlog JSON under concurrent failures. It also calls process.exit(1) // for uncaughtException/unhandledRejection, which caused crash loops when the log file was malformed. // We rely on stdout/PM2 logs + our own guarded process handlers below. // errorHandler && (errorHandler.registerUnCaughtProcessErrorsHandler(process, errorLogPath)); // Register guarded process-level fatal handlers (atomic .rlog + optional email + optional exit) registerFatalHandlers(process, { env, debug, kindPrefix: 'server', reportFilePath: env.FATAL_REPORT_FILE, ignore: createServerIgnore(), }); app.set('trust proxy', env.APP_RATE_TRUST_PROXIES /* number of proxies between user and server */); if (!env.PRODUCTION && (env.INV_IMG_VIR_DIR && env.INV_UPLOAD_DIR)) { // Map static resources, cached for 8 hours app.use(env.INV_IMG_VIR_DIR, express.static(env.INV_UPLOAD_DIR, { maxAge: 31557600 })); } // Serve static files from public directory (e.g., DLQ monitor HTML) app.use(express.static(path.join(__dirname, 'public'))); // Also serve under "/public" prefix to match links like /public/dlq-monitor.html app.use('/public', express.static(path.join(__dirname, 'public'))); // Global middleware to handle request/response errors gracefully app.use((req, res, next) => { // Handle request errors (client disconnect, etc.) req.on('error', (err) => { debug('Request error (client disconnect):', err.message); }); // Explicitly handle aborted requests and tear down the socket req.on('aborted', () => { try { if (!res.headersSent) { res.status(499).end(); // 499 Client Closed Request (nginx convention) } } catch {} try { if (res && typeof res.destroy === 'function') res.destroy(); else if (req && typeof req.destroy === 'function') req.destroy(); } catch {} }); // Handle response errors res.on('error', (err) => { debug('Response error:', err.message); }); next(); }); // for parsing application/x-www-form-urlencoded app.use(express.urlencoded({ limit: MAX_REQ_BDY_MB, extended: true, parameterLimit: 100000 })); // Rate limiting middleware. Define a rate limiter const apiLimiter = rateLimit({ windowMs: (env.APP_RATE_MINS || 15) * 60 * 1000, // 15 minutes max: env.APP_RATE_REQS || 100, // Limit each IP to 100 requests per windowMs skipFailedRequests: env.APP_RATE_SKIPFAIL || true, // Skip failed requests message: { error: "Too many requests, please try again later." } }); // Apply the rate limiter to all routes in this router app.use(apiLimiter); // Handle webhook events from Stripe require('./routes/subscription_webhooks')(express, app); function shouldCompress(req, res) { if (req.headers['x-no-compression']) { return false; // don't compress responses with this request header } // fallback to standard filter function return compression.filter(req, res); } // Enable the compression middleware app.use(compression({ filter: shouldCompress })); // Parsers for POST JSON data app.use(express.json({ limit: MAX_REQ_BDY_MB })); // Allow all CORS requests // Ref at http://restlet.com/company/blog/2015/12/15/understanding-and-using-cors app.use(function (req, res, next) { res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Allow-Headers', 'Origin, Methods, X-Requested-With, Content-Type, Content-Disposition, Accept'); res.setHeader('Access-Control-Allow-Methods', 'POST, GET, PATCH, DELETE, OPTIONS'); next(); }); // Fast-path CORS preflight to avoid unnecessary fallthrough app.use(function (req, res, next) { if (req.method === 'OPTIONS') return res.status(204).end(); next(); }); async function setupRoutes() { // const resLogger = async (req, res, next) => { // debug('Request:', req.url); // next(); // }; // Handle routes with middelware functions app.use(checkUser/*, resLogger*/); // REGISTER OUR ROUTES require('./routes')(app); require('./routes/dlq')(app); // Global DLQ management routes (all queues) app.use(ErrorHandler); // Universal 404 for any method to avoid finalhandler on http2 app.use((req, res) => { if (res.headersSent) return; // For assets, send minimal text to ensure sockets close res.status(404).type('text/plain').end('Not Found'); }); // Avoid logging aborted requests // Ref: https://github.com/nodejs/help/issues/2155 app.use((err, req, res, next) => { if (err && err.code === 'ECONNABORTED') { res.status(400).end(); // Don't process this error any further to avoid its logging } else next(err); }); // Centralized error handler to always finalize a response app.use((err, req, res, next) => { debug('App error handled:', err && (err.message || err)); if (res.headersSent) return; try { res.status(500).send({ error: 'Internal Server Error' }); } catch { // Response failed, destroy socket try { if (res && typeof res.destroy === 'function') res.destroy(); else if (req && typeof req.destroy === 'function') req.destroy(); } catch { } } }); } /** * Preload some common libs, ussually written in ESM modules that require asynchronously loaded or import.. * instead of using requires in CommonJS ones. */ async function preloadLibs() { let mod = await import('@mickeyjohn/geodesy/utm.js'); app.locals.UTM = mod.default; app.locals.LatLonUTM = mod.LatLon; // More accuracy, for converting from ll to utm app.locals.Dms = mod.Dms; mod = await import('@mickeyjohn/geodesy/latlon-spherical.js'); app.locals.LatLonSP = mod.default; } async function ensureFolders() { try { await fs.ensureDir(env.INV_UPLOAD_DIR); await fs.ensureDir(env.UNZIP_DIR); await fs.ensureDir(env.REPORT_DIR); await fs.ensureDir(env.TEMP_DIR); await fs.ensureDir(env.AREAS_UPLOAD_DIR); } catch (error) { debug(error); } } const port = env.AGM_PORT || '4000'; const serverFactory = () => { const tlsOpts = { key: fs.readFileSync(env.SSL_KEY), cert: fs.readFileSync(env.SSL_CERT) }; if (env.HTTP2_ENABLED) { // WARNING: Express does not speak native HTTP/2 streams. // If you advertise h2, browsers may negotiate HTTP/2 and requests can hang. // Use a reverse proxy (nginx) to terminate HTTP/2 and proxy HTTP/1.1 upstream. const alpn = env.HTTP2_ADVERTISE_H2 ? ['h2', 'http/1.1'] : ['http/1.1']; return http2.createSecureServer({ ...tlsOpts, allowHTTP1: true, ALPNProtocols: alpn }, app); } // Default to stable HTTPS/1.1 to avoid Express/finalhandler issues under HTTP/2 return https1.createServer(tlsOpts, app); }; serverFactory() .listen(port, async (error) => { const onAppErr = (err) => { debug(err); process.exit(1); } if (error) return onAppErr(error); try { await ensureFolders(); await preloadLibs(); const conn = await dbConnect(); // Set up error handler for future connection issues conn.on('error', onAppErr); // Since connection is already established, execute the setup directly debug('✅-> MongoDB connected'); await setupRoutes(); // Start Job Importer queue consumer const jobQueuer = require('./helpers/job_queue').getInstance(); jobQueuer.start(); const protoLabel = env.HTTP2_ENABLED ? (env.HTTP2_ADVERTISE_H2 ? 'HTTPS-v2' : 'HTTPS (HTTP/1.1 only)') : 'HTTPS'; debug(`${protoLabel} AgMission Server is listening on port ${port}`); } catch (error) { onAppErr(error); } });