feat(middleware): add separate access log middleware with config option

db500188d87ab9fe376b7bb7aaff7d0b5baea267

KevinSun <paver.mails+github@gmail.com>

Signed
4 files changed, +69 -39Ignore whitespace
default/config.yaml+5 -0
@@ -85,6 +85,11 @@ cookieSecret: ''
85disableCsrfProtection: false85disableCsrfProtection: false
86# Disable startup security checks - NOT RECOMMENDED86# Disable startup security checks - NOT RECOMMENDED
87securityOverride: false87securityOverride: false
88# -- LOGGING CONFIGURATION --
89logging:
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 --
89rateLimiting:94rateLimiting:
90 # Use X-Real-IP header instead of socket IP for rate limiting95 # Use X-Real-IP header instead of socket IP for rate limiting
server.js+4 -2
@@ -57,7 +57,8 @@ import {
5757
58import getWebpackServeMiddleware from './src/middleware/webpack-serve.js';58import getWebpackServeMiddleware from './src/middleware/webpack-serve.js';
59import basicAuthMiddleware from './src/middleware/basicAuth.js';59import basicAuthMiddleware from './src/middleware/basicAuth.js';
60import whitelistMiddleware, { getAccessLogPath, migrateAccessLog } from './src/middleware/whitelist.js';60import whitelistMiddleware from './src/middleware/whitelist.js';
61import accessLoggerMiddleware, { getAccessLogPath, migrateAccessLog } from './src/middleware/accessLogger.js';
61import multerMonkeyPatch from './src/middleware/multerMonkeyPatch.js';62import multerMonkeyPatch from './src/middleware/multerMonkeyPatch.js';
62import initRequestProxy from './src/request-proxy.js';63import initRequestProxy from './src/request-proxy.js';
63import getCacheBusterMiddleware from './src/middleware/cacheBuster.js';64import getCacheBusterMiddleware from './src/middleware/cacheBuster.js';
@@ -342,7 +343,8 @@ app.use(CORS);
342343
343if (listen && basicAuthMode) app.use(basicAuthMiddleware);344if (listen && basicAuthMode) app.use(basicAuthMiddleware);
344345
345app.use(whitelistMiddleware(enableWhitelist, listen));346app.use(whitelistMiddleware(enableWhitelist));
347app.use(accessLoggerMiddleware());
346348
347if (enableCorsProxy) {349if (enableCorsProxy) {
348 app.use(bodyParser.json({350 app.use(bodyParser.json({
src/middleware/accessLogger.js+59 -0
@@ -0,0 +1,59 @@
1import path from 'node:path';
2import fs from 'node:fs';
3import { getRealIpFromHeader } from '../express-common.js';
4import { color, getConfigValue } from '../util.js';
5
6const enableAccessLog = getConfigValue('logging.enableAccessLog', true);
7
8const knownIPs = new Set();
9
10export const getAccessLogPath = () => path.join(globalThis.DATA_ROOT, 'access.log');
11
12export 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 */
33export 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}
src/middleware/whitelist.js+1 -37
@@ -10,9 +10,6 @@ import { color, getConfigValue, safeReadFileSync } from '../util.js';
10const whitelistPath = path.join(process.cwd(), './whitelist.txt');10const whitelistPath = path.join(process.cwd(), './whitelist.txt');
11const enableForwardedWhitelist = getConfigValue('enableForwardedWhitelist', false);11const enableForwardedWhitelist = getConfigValue('enableForwardedWhitelist', false);
12let whitelist = getConfigValue('whitelist', []);12let whitelist = getConfigValue('whitelist', []);
13let knownIPs = new Set();
14
15export const getAccessLogPath = () => path.join(globalThis.DATA_ROOT, 'access.log');
1613
17if (fs.existsSync(whitelistPath)) {14if (fs.existsSync(whitelistPath)) {
18 try {15 try {
@@ -48,30 +45,12 @@ function getForwardedIp(req) {
48 return undefined;45 return undefined;
49}46}
5047
51export 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 line50 * @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 function51 * @returns {import('express').RequestHandler} The middleware function
73 */52 */
74export default function whitelistMiddleware(whitelistMode, listen) {53export 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'];
8362
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)))