Merge pull request #3526 from Zhen-Bo/feature/access-log-middleware Add Separate Access Logging Middleware with Configuration Option

7188060ac8d2e6c5f4e529f480d444d301d37911

Cohee <18619528+Cohee1207@users.noreply.github.com>

Signed
6 files changed, +95 -46Showing whitespace changes
default/config.yaml+7 -2
@@ -71,8 +71,6 @@ autheliaAuth: false
7171# the username and passwords for basic auth are the same as those
7272# for the individual accounts
7373perUserBasicAuth: false
74-# Minimum log level to display in the terminal (DEBUG = 0, INFO = 1, WARN = 2, ERROR = 3)
75-minLogLevel: 0
7674
7775# User session timeout *in seconds* (defaults to 24 hours).
7876## Set to a positive number to expire session after a certain time of inactivity
@@ -85,6 +83,13 @@ cookieSecret: ''
8583disableCsrfProtection: false
8684# Disable startup security checks - NOT RECOMMENDED
8785securityOverride: false
86+# -- LOGGING CONFIGURATION --
87+logging:
88+ # Enable access logging to access.log file
89+ # Records new connections with timestamp, IP address and user agent
90+ enableAccessLog: true
91+ # Minimum log level to display in the terminal (DEBUG = 0, INFO = 1, WARN = 2, ERROR = 3)
92+ minLogLevel: 0
8893# -- RATE LIMITING CONFIGURATION --
8994rateLimiting:
9095 # Use X-Real-IP header instead of socket IP for rate limiting
post-install.js+5 -0
@@ -104,6 +104,11 @@ const keyMigrationMap = [
104104 newKey: 'extensions.models.textToSpeech',
105105 migrate: (value) => value,
106106 },
107+ {
108+ oldKey: 'minLogLevel',
109+ newKey: 'logging.minLogLevel',
110+ migrate: (value) => value,
111+ },
107112];
108113
109114/**
server.js+12 -3
@@ -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/accessLogWriter.js';
6162import multerMonkeyPatch from './src/middleware/multerMonkeyPatch.js';
6263import initRequestProxy from './src/request-proxy.js';
6364import getCacheBusterMiddleware from './src/middleware/cacheBuster.js';
@@ -339,9 +340,17 @@ const CORS = cors({
339340
340341app.use(CORS);
341342
342343if (listen && basicAuthMode) app.use(basicAuthMiddleware);{
344+ app.use(basicAuthMiddleware);
345+}
346+
347+if (enableWhitelist) {
348+ app.use(whitelistMiddleware());
349+}
343350
344-app.use(whitelistMiddleware(enableWhitelist, listen));
351+if (listen) {
352+ app.use(accessLoggerMiddleware());
353+}
345354
346355if (enableCorsProxy) {
347356 app.use(bodyParser.json({
src/middleware/accessLogWriter.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+11 -40
@@ -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,67 +45,41 @@ 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.
70- * @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
7250 * @returns {import('express').RequestHandler} The middleware function
7351 */
7452export default function whitelistMiddleware(whitelistMode, listen) {
7553 const forbiddenWebpage = Handlebars.compile(
7654 safeReadFileSync('./public/error/forbidden-by-whitelist.html') ?? '',
7755 );
7856
57+ const noLogPaths = [
58+ '/favicon.ico',
59+ ];
60+
7961 return function (req, res, next) {
8062 const clientIp = getIpFromRequest(req);
8163 const forwardedIp = getForwardedIp(req);
8264 const userAgent = req.headers['user-agent'];
8365
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-
9966 //clientIp = req.connection.remoteAddress.split(':').pop();
10067 if (whitelistMode === true && !whitelist.some(x => ipMatching.matches(clientIp, ipMatching.getMatch(x)))
10168 || forwardedIp && whitelistMode === true && !whitelist.some(x => ipMatching.matches(forwardedIp, ipMatching.getMatch(x)))
10269 ) {
10370 // Log the connection attempt with real IP address
10471 const ipDetails = forwardedIp
10572 ? `${clientIp} (forwarded from ${forwardedIp})`
10673 : clientIp;
74+
75+ if (!noLogPaths.includes(req.path)) {
10776 console.warn(
10877 color.red(
10978 `Blocked connection from ${clientIp}; User Agent: ${userAgent}\n\tTo allow this connection, add its IP address to the whitelist or disable whitelist mode by editing config.yaml in the root directory of your SillyTavern installation.\n`,
11079 ),
11180 );
81+ }
82+
11283 return res.status(403).send(forbiddenWebpage({ ipDetails }));
11384 }
11485 next();
src/util.js+1 -1
@@ -763,7 +763,7 @@ export function stringToBool(str) {
763763 * Setup the minimum log level
764764 */
765765export function setupLogLevel() {
766766 const logLevel = getConfigValue('logging.minLogLevel', LOG_LEVELS.DEBUG);
767767
768768 globalThis.console.debug = logLevel <= LOG_LEVELS.DEBUG ? console.debug : () => {};
769769 globalThis.console.info = logLevel <= LOG_LEVELS.INFO ? console.info : () => {};