Merge pull request #3526 from Zhen-Bo/feature/access-log-middleware Add Separate Access Logging Middleware with Configuration Option
Signed| @@ -71,8 +71,6 @@ autheliaAuth: false | ||
| 71 | 71 | # the username and passwords for basic auth are the same as those |
| 72 | 72 | # for the individual accounts |
| 73 | 73 | perUserBasicAuth: false |
| 74 | -# Minimum log level to display in the terminal (DEBUG = 0, INFO = 1, WARN = 2, ERROR = 3) | |
| 75 | -minLogLevel: 0 | |
| 76 | 74 | |
| 77 | 75 | # User session timeout *in seconds* (defaults to 24 hours). |
| 78 | 76 | ## Set to a positive number to expire session after a certain time of inactivity |
| @@ -85,6 +83,13 @@ cookieSecret: '' | ||
| 85 | 83 | disableCsrfProtection: false |
| 86 | 84 | # Disable startup security checks - NOT RECOMMENDED |
| 87 | 85 | securityOverride: 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 | |
| 88 | 93 | # -- RATE LIMITING CONFIGURATION -- |
| 89 | 94 | rateLimiting: |
| 90 | 95 | # Use X-Real-IP header instead of socket IP for rate limiting |
| @@ -104,6 +104,11 @@ const keyMigrationMap = [ | ||
| 104 | 104 | newKey: 'extensions.models.textToSpeech', |
| 105 | 105 | migrate: (value) => value, |
| 106 | 106 | }, |
| 107 | + { | |
| 108 | + oldKey: 'minLogLevel', | |
| 109 | + newKey: 'logging.minLogLevel', | |
| 110 | + migrate: (value) => value, | |
| 111 | + }, | |
| 107 | 112 | ]; |
| 108 | 113 | |
| 109 | 114 | /** |
| @@ -57,7 +57,8 @@ import { | ||
| 57 | 57 | |
| 58 | 58 | import getWebpackServeMiddleware from './src/middleware/webpack-serve.js'; |
| 59 | 59 | import basicAuthMiddleware from './src/middleware/basicAuth.js'; |
| 60 | 60 | import whitelistMiddleware, { getAccessLogPath, migrateAccessLog } from './src/middleware/whitelist.js'; |
| 61 | +import accessLoggerMiddleware, { getAccessLogPath, migrateAccessLog } from './src/middleware/accessLogWriter.js'; | |
| 61 | 62 | import multerMonkeyPatch from './src/middleware/multerMonkeyPatch.js'; |
| 62 | 63 | import initRequestProxy from './src/request-proxy.js'; |
| 63 | 64 | import getCacheBusterMiddleware from './src/middleware/cacheBuster.js'; |
| @@ -339,9 +340,17 @@ const CORS = cors({ | ||
| 339 | 340 | |
| 340 | 341 | app.use(CORS); |
| 341 | 342 | |
| 342 | 343 | if (listen && basicAuthMode) app.use(basicAuthMiddleware);{ |
| 344 | + app.use(basicAuthMiddleware); | |
| 345 | +} | |
| 346 | + | |
| 347 | +if (enableWhitelist) { | |
| 348 | + app.use(whitelistMiddleware()); | |
| 349 | +} | |
| 343 | 350 | |
| 344 | -app.use(whitelistMiddleware(enableWhitelist, listen)); | |
| 351 | +if (listen) { | |
| 352 | + app.use(accessLoggerMiddleware()); | |
| 353 | +} | |
| 345 | 354 | |
| 346 | 355 | if (enableCorsProxy) { |
| 347 | 356 | 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 | 10 | const whitelistPath = path.join(process.cwd(), './whitelist.txt'); |
| 11 | 11 | const enableForwardedWhitelist = getConfigValue('enableForwardedWhitelist', false); |
| 12 | 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 | 14 | if (fs.existsSync(whitelistPath)) { |
| 18 | 15 | try { |
| @@ -48,67 +45,41 @@ function getForwardedIp(req) { | ||
| 48 | 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 | 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 | 50 | * @returns {import('express').RequestHandler} The middleware function |
| 73 | 51 | */ |
| 74 | 52 | export default function whitelistMiddleware(whitelistMode, listen) { |
| 75 | 53 | const forbiddenWebpage = Handlebars.compile( |
| 76 | 54 | safeReadFileSync('./public/error/forbidden-by-whitelist.html') ?? '', |
| 77 | 55 | ); |
| 78 | 56 | |
| 57 | + const noLogPaths = [ | |
| 58 | + '/favicon.ico', | |
| 59 | + ]; | |
| 60 | + | |
| 79 | 61 | return function (req, res, next) { |
| 80 | 62 | const clientIp = getIpFromRequest(req); |
| 81 | 63 | const forwardedIp = getForwardedIp(req); |
| 82 | 64 | const userAgent = req.headers['user-agent']; |
| 83 | 65 | |
| 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 | 66 | //clientIp = req.connection.remoteAddress.split(':').pop(); |
| 100 | 67 | if (whitelistMode === true && !whitelist.some(x => ipMatching.matches(clientIp, ipMatching.getMatch(x))) |
| 101 | 68 | || forwardedIp && whitelistMode === true && !whitelist.some(x => ipMatching.matches(forwardedIp, ipMatching.getMatch(x))) |
| 102 | 69 | ) { |
| 103 | 70 | // Log the connection attempt with real IP address |
| 104 | 71 | const ipDetails = forwardedIp |
| 105 | 72 | ? `${clientIp} (forwarded from ${forwardedIp})` |
| 106 | 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 | 83 | return res.status(403).send(forbiddenWebpage({ ipDetails })); |
| 113 | 84 | } |
| 114 | 85 | next(); |
| @@ -763,7 +763,7 @@ export function stringToBool(str) { | ||
| 763 | 763 | * Setup the minimum log level |
| 764 | 764 | */ |
| 765 | 765 | export function setupLogLevel() { |
| 766 | 766 | const logLevel = getConfigValue('logging.minLogLevel', LOG_LEVELS.DEBUG); |
| 767 | 767 | |
| 768 | 768 | globalThis.console.debug = logLevel <= LOG_LEVELS.DEBUG ? console.debug : () => {}; |
| 769 | 769 | globalThis.console.info = logLevel <= LOG_LEVELS.INFO ? console.info : () => {}; |