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, +100 -51Ignore whitespace
default/config.yaml+7 -2
@@ -71,8 +71,6 @@ autheliaAuth: false
71# the username and passwords for basic auth are the same as those71# the username and passwords for basic auth are the same as those
72# for the individual accounts72# for the individual accounts
73perUserBasicAuth: false73perUserBasicAuth: false
74# Minimum log level to display in the terminal (DEBUG = 0, INFO = 1, WARN = 2, ERROR = 3)
75minLogLevel: 0
7674
77# User session timeout *in seconds* (defaults to 24 hours).75# User session timeout *in seconds* (defaults to 24 hours).
78## Set to a positive number to expire session after a certain time of inactivity76## Set to a positive number to expire session after a certain time of inactivity
@@ -85,6 +83,13 @@ cookieSecret: ''
85disableCsrfProtection: false83disableCsrfProtection: false
86# Disable startup security checks - NOT RECOMMENDED84# Disable startup security checks - NOT RECOMMENDED
87securityOverride: false85securityOverride: false
86# -- LOGGING CONFIGURATION --
87logging:
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
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
post-install.js+5 -0
@@ -104,6 +104,11 @@ const keyMigrationMap = [
104 newKey: 'extensions.models.textToSpeech',104 newKey: 'extensions.models.textToSpeech',
105 migrate: (value) => value,105 migrate: (value) => value,
106 },106 },
107 {
108 oldKey: 'minLogLevel',
109 newKey: 'logging.minLogLevel',
110 migrate: (value) => value,
111 },
107];112];
108113
109/**114/**
server.js+12 -3
@@ -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/accessLogWriter.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';
@@ -339,9 +340,17 @@ const CORS = cors({
339340
340app.use(CORS);341app.use(CORS);
341342
342if (listen && basicAuthMode) app.use(basicAuthMiddleware);343if (listen && basicAuthMode) {
344 app.use(basicAuthMiddleware);
345}
346
347if (enableWhitelist) {
348 app.use(whitelistMiddleware());
349}
343350
344app.use(whitelistMiddleware(enableWhitelist, listen));351if (listen) {
352 app.use(accessLoggerMiddleware());
353}
345354
346if (enableCorsProxy) {355if (enableCorsProxy) {
347 app.use(bodyParser.json({356 app.use(bodyParser.json({
src/middleware/accessLogWriter.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+16 -45
@@ -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,67 +45,41 @@ 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 line
71 * @param {boolean} listen If listen mode is enabled via config or command line
72 * @returns {import('express').RequestHandler} The middleware function50 * @returns {import('express').RequestHandler} The middleware function
73 */51 */
74export default function whitelistMiddleware(whitelistMode, listen) {52export default function whitelistMiddleware() {
75 const forbiddenWebpage = Handlebars.compile(53 const forbiddenWebpage = Handlebars.compile(
76 safeReadFileSync('./public/error/forbidden-by-whitelist.html') ?? '',54 safeReadFileSync('./public/error/forbidden-by-whitelist.html') ?? '',
77 );55 );
7856
57 const noLogPaths = [
58 '/favicon.ico',
59 ];
60
79 return function (req, res, next) {61 return function (req, res, next) {
80 const clientIp = getIpFromRequest(req);62 const clientIp = getIpFromRequest(req);
81 const forwardedIp = getForwardedIp(req);63 const forwardedIp = getForwardedIp(req);
82 const userAgent = req.headers['user-agent'];64 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
99 //clientIp = req.connection.remoteAddress.split(':').pop();66 //clientIp = req.connection.remoteAddress.split(':').pop();
100 if (whitelistMode === true && !whitelist.some(x => ipMatching.matches(clientIp, ipMatching.getMatch(x)))67 if (!whitelist.some(x => ipMatching.matches(clientIp, ipMatching.getMatch(x)))
101 || forwardedIp && whitelistMode === true && !whitelist.some(x => ipMatching.matches(forwardedIp, ipMatching.getMatch(x)))68 || forwardedIp && !whitelist.some(x => ipMatching.matches(forwardedIp, ipMatching.getMatch(x)))
102 ) {69 ) {
103 // Log the connection attempt with real IP address70 // Log the connection attempt with real IP address
104 const ipDetails = forwardedIp71 const ipDetails = forwardedIp
105 ? `${clientIp} (forwarded from ${forwardedIp})`72 ? `${clientIp} (forwarded from ${forwardedIp})`
106 : clientIp;73 : clientIp;
107 console.warn(74
108 color.red(75 if (!noLogPaths.includes(req.path)) {
109 `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`,76 console.warn(
110 ),77 color.red(
111 );78 `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`,
79 ),
80 );
81 }
82
112 return res.status(403).send(forbiddenWebpage({ ipDetails }));83 return res.status(403).send(forbiddenWebpage({ ipDetails }));
113 }84 }
114 next();85 next();
src/util.js+1 -1
@@ -763,7 +763,7 @@ export function stringToBool(str) {
763 * Setup the minimum log level763 * Setup the minimum log level
764 */764 */
765export function setupLogLevel() {765export function setupLogLevel() {
766 const logLevel = getConfigValue('minLogLevel', LOG_LEVELS.DEBUG);766 const logLevel = getConfigValue('logging.minLogLevel', LOG_LEVELS.DEBUG);
767767
768 globalThis.console.debug = logLevel <= LOG_LEVELS.DEBUG ? console.debug : () => {};768 globalThis.console.debug = logLevel <= LOG_LEVELS.DEBUG ? console.debug : () => {};
769 globalThis.console.info = logLevel <= LOG_LEVELS.INFO ? console.info : () => {};769 globalThis.console.info = logLevel <= LOG_LEVELS.INFO ? console.info : () => {};