feat(middleware): add separate access log middleware with config option
Signed| @@ -85,6 +85,11 @@ cookieSecret: '' | |||
| 85 | disableCsrfProtection: false | 85 | disableCsrfProtection: false |
| 86 | # Disable startup security checks - NOT RECOMMENDED | 86 | # Disable startup security checks - NOT RECOMMENDED |
| 87 | securityOverride: false | 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 | # -- RATE LIMITING CONFIGURATION -- | 93 | # -- RATE LIMITING CONFIGURATION -- |
| 89 | rateLimiting: | 94 | rateLimiting: |
| 90 | # Use X-Real-IP header instead of socket IP for rate limiting | 95 | # Use X-Real-IP header instead of socket IP for rate limiting |
| @@ -57,7 +57,8 @@ import { | |||
| 57 | 57 | ||
| 58 | import getWebpackServeMiddleware from './src/middleware/webpack-serve.js'; | 58 | import getWebpackServeMiddleware from './src/middleware/webpack-serve.js'; |
| 59 | import basicAuthMiddleware from './src/middleware/basicAuth.js'; | 59 | import basicAuthMiddleware from './src/middleware/basicAuth.js'; |
| 60 | import whitelistMiddleware, { getAccessLogPath, migrateAccessLog } from './src/middleware/whitelist.js'; | 60 | import whitelistMiddleware from './src/middleware/whitelist.js'; |
| 61 | import accessLoggerMiddleware, { getAccessLogPath, migrateAccessLog } from './src/middleware/accessLogger.js'; | ||
| 61 | import multerMonkeyPatch from './src/middleware/multerMonkeyPatch.js'; | 62 | import multerMonkeyPatch from './src/middleware/multerMonkeyPatch.js'; |
| 62 | import initRequestProxy from './src/request-proxy.js'; | 63 | import initRequestProxy from './src/request-proxy.js'; |
| 63 | import getCacheBusterMiddleware from './src/middleware/cacheBuster.js'; | 64 | import getCacheBusterMiddleware from './src/middleware/cacheBuster.js'; |
| @@ -342,7 +343,8 @@ app.use(CORS); | |||
| 342 | 343 | ||
| 343 | if (listen && basicAuthMode) app.use(basicAuthMiddleware); | 344 | if (listen && basicAuthMode) app.use(basicAuthMiddleware); |
| 344 | 345 | ||
| 345 | app.use(whitelistMiddleware(enableWhitelist, listen)); | 346 | app.use(whitelistMiddleware(enableWhitelist)); |
| 347 | app.use(accessLoggerMiddleware()); | ||
| 346 | 348 | ||
| 347 | if (enableCorsProxy) { | 349 | if (enableCorsProxy) { |
| 348 | app.use(bodyParser.json({ | 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 | const whitelistPath = path.join(process.cwd(), './whitelist.txt'); | 10 | const whitelistPath = path.join(process.cwd(), './whitelist.txt'); |
| 11 | const enableForwardedWhitelist = getConfigValue('enableForwardedWhitelist', false); | 11 | const enableForwardedWhitelist = getConfigValue('enableForwardedWhitelist', false); |
| 12 | let whitelist = getConfigValue('whitelist', []); | 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 | if (fs.existsSync(whitelistPath)) { | 14 | if (fs.existsSync(whitelistPath)) { |
| 18 | try { | 15 | try { |
| @@ -48,30 +45,12 @@ function getForwardedIp(req) { | |||
| 48 | return undefined; | 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 | * Returns a middleware function that checks if the client IP is in the whitelist. | 49 | * Returns a middleware function that checks if the client IP is in the whitelist. |
| 70 | * @param {boolean} whitelistMode If whitelist mode is enabled via config or command line | 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 | * @returns {import('express').RequestHandler} The middleware function | 51 | * @returns {import('express').RequestHandler} The middleware function |
| 73 | */ | 52 | */ |
| 74 | export default function whitelistMiddleware(whitelistMode, listen) { | 53 | export default function whitelistMiddleware(whitelistMode) { |
| 75 | const forbiddenWebpage = Handlebars.compile( | 54 | const forbiddenWebpage = Handlebars.compile( |
| 76 | safeReadFileSync('./public/error/forbidden-by-whitelist.html') ?? '', | 55 | safeReadFileSync('./public/error/forbidden-by-whitelist.html') ?? '', |
| 77 | ); | 56 | ); |
| @@ -81,21 +60,6 @@ export default function whitelistMiddleware(whitelistMode, listen) { | |||
| 81 | const forwardedIp = getForwardedIp(req); | 60 | const forwardedIp = getForwardedIp(req); |
| 82 | const userAgent = req.headers['user-agent']; | 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 | //clientIp = req.connection.remoteAddress.split(':').pop(); | 63 | //clientIp = req.connection.remoteAddress.split(':').pop(); |
| 100 | if (whitelistMode === true && !whitelist.some(x => ipMatching.matches(clientIp, ipMatching.getMatch(x))) | 64 | if (whitelistMode === true && !whitelist.some(x => ipMatching.matches(clientIp, ipMatching.getMatch(x))) |
| 101 | || forwardedIp && whitelistMode === true && !whitelist.some(x => ipMatching.matches(forwardedIp, ipMatching.getMatch(x))) | 65 | || forwardedIp && whitelistMode === true && !whitelist.some(x => ipMatching.matches(forwardedIp, ipMatching.getMatch(x))) |