// server.js // Load .env variables FIRST! require('dotenv').config(); // --- Sentry Initialisierung (GANZ OBEN, nach dotenv) --- const Sentry = require("@sentry/node"); // Initialize Sentry BEFORE requiring any other modules! Sentry.init({ // DSN should now be available from process.env if set in .env dsn: process.env.SENTRY_DSN || "https://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa@oooooooooooooooo.ingest.sentry.io/123456", // Enable tracing - Adjust sample rate as needed tracesSampleRate: process.env.NODE_ENV === 'production' ? 0.1 : 1.0, }); // DEBUG: Check Sentry object after init console.log("Sentry object after init:", typeof Sentry, Sentry ? Object.keys(Sentry) : 'Sentry is undefined/null'); // --- Ende Sentry Initialisierung --- // Require necessary core modules AFTER Sentry is initialized const express = require('express'); const cors = require('cors'); const pino = require('pino'); // Logging library const rateLimit = require('express-rate-limit'); // Rate limiting middleware // Import local modules const { initializeMaxMind } = require('./maxmind'); // MaxMind DB initialization const ipinfoRoutes = require('./routes/ipinfo'); const pingRoutes = require('./routes/ping'); const tracerouteRoutes = require('./routes/traceroute'); const lookupRoutes = require('./routes/lookup'); const dnsLookupRoutes = require('./routes/dnsLookup'); const whoisLookupRoutes = require('./routes/whoisLookup'); const versionRoutes = require('./routes/version'); const sslCheckRoutes = require('./routes/sslCheck'); // <-- NEUE ROUTE IMPORTIERT // --- Logger Initialisierung --- const logger = pino({ level: process.env.LOG_LEVEL || 'info', transport: process.env.NODE_ENV !== 'production' ? { target: 'pino-pretty', options: { colorize: true, translateTime: 'SYS:standard', ignore: 'pid,hostname' } } : undefined, }); // Create Express app instance const app = express(); const PORT = process.env.PORT || 3000; // --- Sentry Middleware (Request Handler & Tracing) --- // Must be the first middleware if (Sentry.Handlers && Sentry.Handlers.requestHandler) { app.use(Sentry.Handlers.requestHandler()); } else { logger.error("Sentry.Handlers.requestHandler is not available!"); } // Must be after requestHandler, before routes if (Sentry.Handlers && Sentry.Handlers.tracingHandler) { app.use(Sentry.Handlers.tracingHandler()); } else { logger.error("Sentry.Handlers.tracingHandler is not available!"); } // --- Ende Sentry Middleware --- // --- Core Middleware --- app.use(cors()); // Enable CORS app.use(express.json()); // Parse JSON bodies app.set('trust proxy', parseInt(process.env.TRUST_PROXY_COUNT || '2', 10)); // Adjust based on your proxy setup, ensure integer // --- Rate Limiter --- // Apply a general limiter to most routes const generalLimiter = rateLimit({ windowMs: 5 * 60 * 1000, // 5 minutes max: parseInt(process.env.RATE_LIMIT_MAX || (process.env.NODE_ENV === 'production' ? '20' : '200'), 10), // Requests per window per IP, ensure integer standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers legacyHeaders: false, // Disable the `X-RateLimit-*` headers message: { success: false, error: 'Too many requests from this IP, please try again after 5 minutes' }, keyGenerator: (req, res) => req.ip, // Use client IP address from Express handler: (req, res, next, options) => { logger.warn({ ip: req.ip, route: req.originalUrl }, 'Rate limit exceeded'); Sentry.captureMessage('Rate limit exceeded', { level: 'warning', extra: { ip: req.ip, route: req.originalUrl } }); res.status(options.statusCode).send(options.message); } }); // Apply the limiter to specific API routes that perform external actions // Note: /api/ipinfo and /api/version are often excluded as they are less resource-intensive app.use('/api/ping', generalLimiter); app.use('/api/traceroute', generalLimiter); app.use('/api/lookup', generalLimiter); app.use('/api/dns-lookup', generalLimiter); app.use('/api/whois-lookup', generalLimiter); app.use('/api/ssl-check', generalLimiter); // <-- RATE LIMITER FÜR NEUE ROUTE // --- API Routes --- // Mount the imported route handlers app.use('/api/ipinfo', ipinfoRoutes); app.use('/api/ping', pingRoutes); app.use('/api/traceroute', tracerouteRoutes); app.use('/api/lookup', lookupRoutes); app.use('/api/dns-lookup', dnsLookupRoutes); app.use('/api/whois-lookup', whoisLookupRoutes); app.use('/api/version', versionRoutes); app.use('/api/ssl-check', sslCheckRoutes); // <-- NEUE ROUTE REGISTRIERT // --- Sentry Error Handler --- // Must be AFTER all controllers and BEFORE any other error handling middleware if (Sentry.Handlers && Sentry.Handlers.errorHandler) { app.use(Sentry.Handlers.errorHandler({ shouldHandleError(error) { // Capture all 500 errors if (error.status === 500) return true; // Capture specific client errors if needed, e.g., 403 // if (error.status === 403) return true; // By default, capture only server errors (5xx) return error.status >= 500; }, })); } else { logger.error("Sentry.Handlers.errorHandler is not available!"); } // --- Ende Sentry Error Handler --- // --- Fallback Error Handler --- // Optional: Catches errors not handled by Sentry or passed via next(err) app.use((err, req, res, next) => { logger.error({ error: err.message, stack: err.stack, url: req.originalUrl, method: req.method, status: err.status, sentryId: res.sentry // Sentry ID if available }, 'Unhandled error caught by fallback handler'); // Avoid sending stack trace in production const errorResponse = { error: err.message || 'Internal Server Error', ...(res.sentry && { sentryId: res.sentry }) // Include Sentry ID if available }; res.status(err.status || 500).json(errorResponse); }); // --- Server Start --- let server; // Variable to hold the server instance for graceful shutdown // Initialize external resources (like MaxMind DBs) then start the server initializeMaxMind().then(() => { server = app.listen(PORT, () => { logger.info({ port: PORT, node_env: process.env.NODE_ENV || 'development' }, `Server listening`); // Log available routes (optional) logger.info(`API base URL: http://localhost:${PORT}/api`); }); }).catch(error => { logger.fatal({ error: error.message, stack: error.stack }, "Server could not start due to initialization errors."); Sentry.captureException(error); // Capture initialization errors process.exit(1); // Exit if initialization fails }); // --- Graceful Shutdown --- const signals = { 'SIGINT': 2, 'SIGTERM': 15 }; async function gracefulShutdown(signal) { logger.info(`Received ${signal}, shutting down gracefully...`); if (server) { server.close(async () => { logger.info('HTTP server closed.'); // Close Sentry to allow time for events to be sent try { await Sentry.close(2000); // 2 second timeout logger.info('Sentry closed.'); } catch (e) { logger.error({ error: e.message }, 'Error closing Sentry'); } finally { process.exit(128 + signals[signal]); // Standard exit code for signals } }); } else { // If server never started, still try to close Sentry and exit logger.warn('Server was not running, attempting to close Sentry and exit.'); try { await Sentry.close(2000); logger.info('Sentry closed (server never started).'); } catch (e) { logger.error({ error: e.message }, 'Error closing Sentry (server never started)'); } finally { process.exit(128 + signals[signal]); } } // Force exit after a timeout if graceful shutdown hangs setTimeout(() => { logger.warn('Graceful shutdown timed out, forcing exit.'); process.exit(1); }, 5000); // 5 seconds } // Register signal handlers Object.keys(signals).forEach((signal) => { process.on(signal, () => gracefulShutdown(signal)); });