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: ''
8585disableCsrfProtection: false
8686# Disable startup security checks - NOT RECOMMENDED
8787securityOverride: 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
8893# -- RATE LIMITING CONFIGURATION --
8994rateLimiting:
9095 # Use X-Real-IP header instead of socket IP for rate limiting
server.js+4 -2
@@ -57,7 +57,8 @@ import {
5757
5858import getWebpackServeMiddleware from './src/middleware/webpack-serve.js';
5959import basicAuthMiddleware from './src/middleware/basicAuth.js';
6060import whitelistMiddleware, { getAccessLogPath, migrateAccessLog } from './src/middleware/whitelist.js';
61+import accessLoggerMiddleware, { getAccessLogPath, migrateAccessLog } from './src/middleware/accessLogger.js';
6162import multerMonkeyPatch from './src/middleware/multerMonkeyPatch.js';
6263import initRequestProxy from './src/request-proxy.js';
6364import getCacheBusterMiddleware from './src/middleware/cacheBuster.js';
@@ -342,7 +343,8 @@ app.use(CORS);
342343
343344if (listen && basicAuthMode) app.use(basicAuthMiddleware);
344345
345346app.use(whitelistMiddleware(enableWhitelist, listen));
347+app.use(accessLoggerMiddleware());
346348
347349if (enableCorsProxy) {
348350 app.use(bodyParser.json({
src/middleware/accessLogger.js+59 -0
@@ -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+}
src/middleware/whitelist.js+1 -37
@@ -10,9 +10,6 @@ import { color, getConfigValue, safeReadFileSync } from '../util.js';
1010const whitelistPath = path.join(process.cwd(), './whitelist.txt');
1111const enableForwardedWhitelist = getConfigValue('enableForwardedWhitelist', false);
1212let whitelist = getConfigValue('whitelist', []);
13-let knownIPs = new Set();
14-
15-export const getAccessLogPath = () => path.join(globalThis.DATA_ROOT, 'access.log');
1613
1714if (fs.existsSync(whitelistPath)) {
1815 try {
@@ -48,30 +45,12 @@ function getForwardedIp(req) {
4845 return undefined;
4946}
5047
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-
6848/**
6949 * Returns a middleware function that checks if the client IP is in the whitelist.
7050 * @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
7251 * @returns {import('express').RequestHandler} The middleware function
7352 */
7453export default function whitelistMiddleware(whitelistMode, listen) {
7554 const forbiddenWebpage = Handlebars.compile(
7655 safeReadFileSync('./public/error/forbidden-by-whitelist.html') ?? '',
7756 );
@@ -81,21 +60,6 @@ export default function whitelistMiddleware(whitelistMode, listen) {
8160 const forwardedIp = getForwardedIp(req);
8261 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-
9963 //clientIp = req.connection.remoteAddress.split(':').pop();
10064 if (whitelistMode === true && !whitelist.some(x => ipMatching.matches(clientIp, ipMatching.getMatch(x)))
10165 || forwardedIp && whitelistMode === true && !whitelist.some(x => ipMatching.matches(forwardedIp, ipMatching.getMatch(x)))