feat(middleware): add separate access log middleware with config option
Signed| @@ -85,6 +85,11 @@ cookieSecret: '' | ||
| 85 | 85 | disableCsrfProtection: false |
| 86 | 86 | # Disable startup security checks - NOT RECOMMENDED |
| 87 | 87 | securityOverride: false |
| 88 | +# -- LOGGING CONFIGURATION -- | |
| 89 | +logging: | |
| 90 | + # Enable access logging to access.log file | |
| 91 | + # Records new connections with timestamp, IP address and user agent | |
| 92 | + enableAccessLog: true | |
| 88 | 93 | # -- RATE LIMITING CONFIGURATION -- |
| 89 | 94 | rateLimiting: |
| 90 | 95 | # Use X-Real-IP header instead of socket IP for rate limiting |
| @@ -57,7 +57,8 @@ import { | ||
| 57 | 57 | |
| 58 | 58 | import getWebpackServeMiddleware from './src/middleware/webpack-serve.js'; |
| 59 | 59 | import basicAuthMiddleware from './src/middleware/basicAuth.js'; |
| 60 | 60 | import whitelistMiddleware, { getAccessLogPath, migrateAccessLog } from './src/middleware/whitelist.js'; |
| 61 | +import accessLoggerMiddleware, { getAccessLogPath, migrateAccessLog } from './src/middleware/accessLogger.js'; | |
| 61 | 62 | import multerMonkeyPatch from './src/middleware/multerMonkeyPatch.js'; |
| 62 | 63 | import initRequestProxy from './src/request-proxy.js'; |
| 63 | 64 | import getCacheBusterMiddleware from './src/middleware/cacheBuster.js'; |
| @@ -342,7 +343,8 @@ app.use(CORS); | ||
| 342 | 343 | |
| 343 | 344 | if (listen && basicAuthMode) app.use(basicAuthMiddleware); |
| 344 | 345 | |
| 345 | 346 | app.use(whitelistMiddleware(enableWhitelist, listen)); |
| 347 | +app.use(accessLoggerMiddleware()); | |
| 346 | 348 | |
| 347 | 349 | if (enableCorsProxy) { |
| 348 | 350 | app.use(bodyParser.json({ |
| @@ -0,0 +1,59 @@ | ||
| 1 | +import path from 'node:path'; | |
| 2 | +import fs from 'node:fs'; | |
| 3 | +import { getRealIpFromHeader } from '../express-common.js'; | |
| 4 | +import { color, getConfigValue } from '../util.js'; | |
| 5 | + | |
| 6 | +const enableAccessLog = getConfigValue('logging.enableAccessLog', true); | |
| 7 | + | |
| 8 | +const knownIPs = new Set(); | |
| 9 | + | |
| 10 | +export const getAccessLogPath = () => path.join(globalThis.DATA_ROOT, 'access.log'); | |
| 11 | + | |
| 12 | +export function migrateAccessLog() { | |
| 13 | + try { | |
| 14 | + if (!fs.existsSync('access.log')) { | |
| 15 | + return; | |
| 16 | + } | |
| 17 | + const logPath = getAccessLogPath(); | |
| 18 | + if (fs.existsSync(logPath)) { | |
| 19 | + return; | |
| 20 | + } | |
| 21 | + fs.renameSync('access.log', logPath); | |
| 22 | + console.log(color.yellow('Migrated access.log to new location:'), logPath); | |
| 23 | + } catch (e) { | |
| 24 | + console.error('Failed to migrate access log:', e); | |
| 25 | + console.info('Please move access.log to the data directory manually.'); | |
| 26 | + } | |
| 27 | +} | |
| 28 | + | |
| 29 | +/** | |
| 30 | + * Creates middleware for logging access and new connections | |
| 31 | + * @returns {import('express').RequestHandler} | |
| 32 | + */ | |
| 33 | +export default function accessLoggerMiddleware() { | |
| 34 | + return function (req, res, next) { | |
| 35 | + const clientIp = getRealIpFromHeader(req); | |
| 36 | + const userAgent = req.headers['user-agent']; | |
| 37 | + | |
| 38 | + if (!knownIPs.has(clientIp)) { | |
| 39 | + // Log new connection | |
| 40 | + console.info(color.yellow(`New connection from ${clientIp}; User Agent: ${userAgent}\n`)); | |
| 41 | + knownIPs.add(clientIp); | |
| 42 | + | |
| 43 | + // Write to access log if enabled | |
| 44 | + if (enableAccessLog) { | |
| 45 | + const logPath = getAccessLogPath(); | |
| 46 | + const timestamp = new Date().toISOString(); | |
| 47 | + const log = `${timestamp} ${clientIp} ${userAgent}\n`; | |
| 48 | + | |
| 49 | + fs.appendFile(logPath, log, (err) => { | |
| 50 | + if (err) { | |
| 51 | + console.error('Failed to write access log:', err); | |
| 52 | + } | |
| 53 | + }); | |
| 54 | + } | |
| 55 | + } | |
| 56 | + | |
| 57 | + next(); | |
| 58 | + }; | |
| 59 | +} | |
| @@ -10,9 +10,6 @@ import { color, getConfigValue, safeReadFileSync } from '../util.js'; | ||
| 10 | 10 | const whitelistPath = path.join(process.cwd(), './whitelist.txt'); |
| 11 | 11 | const enableForwardedWhitelist = getConfigValue('enableForwardedWhitelist', false); |
| 12 | 12 | let whitelist = getConfigValue('whitelist', []); |
| 13 | -let knownIPs = new Set(); | |
| 14 | - | |
| 15 | -export const getAccessLogPath = () => path.join(globalThis.DATA_ROOT, 'access.log'); | |
| 16 | 13 | |
| 17 | 14 | if (fs.existsSync(whitelistPath)) { |
| 18 | 15 | try { |
| @@ -48,30 +45,12 @@ function getForwardedIp(req) { | ||
| 48 | 45 | return undefined; |
| 49 | 46 | } |
| 50 | 47 | |
| 51 | -export function migrateAccessLog() { | |
| 52 | - try { | |
| 53 | - if (!fs.existsSync('access.log')) { | |
| 54 | - return; | |
| 55 | - } | |
| 56 | - const logPath = getAccessLogPath(); | |
| 57 | - if (fs.existsSync(logPath)) { | |
| 58 | - return; | |
| 59 | - } | |
| 60 | - fs.renameSync('access.log', logPath); | |
| 61 | - console.log(color.yellow('Migrated access.log to new location:'), logPath); | |
| 62 | - } catch (e) { | |
| 63 | - console.error('Failed to migrate access log:', e); | |
| 64 | - console.info('Please move access.log to the data directory manually.'); | |
| 65 | - } | |
| 66 | -} | |
| 67 | - | |
| 68 | 48 | /** |
| 69 | 49 | * Returns a middleware function that checks if the client IP is in the whitelist. |
| 70 | 50 | * @param {boolean} whitelistMode If whitelist mode is enabled via config or command line |
| 71 | - * @param {boolean} listen If listen mode is enabled via config or command line | |
| 72 | 51 | * @returns {import('express').RequestHandler} The middleware function |
| 73 | 52 | */ |
| 74 | 53 | export default function whitelistMiddleware(whitelistMode, listen) { |
| 75 | 54 | const forbiddenWebpage = Handlebars.compile( |
| 76 | 55 | safeReadFileSync('./public/error/forbidden-by-whitelist.html') ?? '', |
| 77 | 56 | ); |
| @@ -81,21 +60,6 @@ export default function whitelistMiddleware(whitelistMode, listen) { | ||
| 81 | 60 | const forwardedIp = getForwardedIp(req); |
| 82 | 61 | const userAgent = req.headers['user-agent']; |
| 83 | 62 | |
| 84 | - if (listen && !knownIPs.has(clientIp)) { | |
| 85 | - console.info(color.yellow(`New connection from ${clientIp}; User Agent: ${userAgent}\n`)); | |
| 86 | - knownIPs.add(clientIp); | |
| 87 | - | |
| 88 | - // Write access log | |
| 89 | - const logPath = getAccessLogPath(); | |
| 90 | - const timestamp = new Date().toISOString(); | |
| 91 | - const log = `${timestamp} ${clientIp} ${userAgent}\n`; | |
| 92 | - fs.appendFile(logPath, log, (err) => { | |
| 93 | - if (err) { | |
| 94 | - console.error('Failed to write access log:', err); | |
| 95 | - } | |
| 96 | - }); | |
| 97 | - } | |
| 98 | - | |
| 99 | 63 | //clientIp = req.connection.remoteAddress.split(':').pop(); |
| 100 | 64 | if (whitelistMode === true && !whitelist.some(x => ipMatching.matches(clientIp, ipMatching.getMatch(x))) |
| 101 | 65 | || forwardedIp && whitelistMode === true && !whitelist.some(x => ipMatching.matches(forwardedIp, ipMatching.getMatch(x))) |