diff --git a/server.js b/server.js index e43c0baf0..1a40b4fcc 100644 --- a/server.js +++ b/server.js @@ -1,393 +1,15 @@ #!/usr/bin/env node - -// native node modules -import path from 'node:path'; -import util from 'node:util'; -import net from 'node:net'; -import dns from 'node:dns'; -import process from 'node:process'; - -import cors from 'cors'; -import { csrfSync } from 'csrf-sync'; -import express from 'express'; -import compression from 'compression'; -import cookieSession from 'cookie-session'; -import multer from 'multer'; -import responseTime from 'response-time'; -import helmet from 'helmet'; -import bodyParser from 'body-parser'; -import open from 'open'; - -// local library imports -import './src/fetch-patch.js'; import { CommandLineParser } from './src/command-line.js'; import { serverDirectory } from './src/server-directory.js'; -console.log(`Node version: ${process.version}. Running in ${process.env.NODE_ENV} environment. Server directory: ${serverDirectory}`); - -// Work around a node v20.0.0, v20.1.0, and v20.2.0 bug. The issue was fixed in v20.3.0. -// https://github.com/nodejs/node/issues/47822#issuecomment-1564708870 -// Safe to remove once support for Node v20 is dropped. -if (process.versions && process.versions.node && process.versions.node.match(/20\.[0-2]\.0/)) { - // @ts-ignore - if (net.setDefaultAutoSelectFamily) net.setDefaultAutoSelectFamily(false); -} - // config.yaml will be set when parsing command line arguments const cliArgs = new CommandLineParser().parse(process.argv); globalThis.DATA_ROOT = cliArgs.dataRoot; globalThis.COMMAND_LINE_ARGS = cliArgs; process.chdir(serverDirectory); -const { serverEvents, EVENT_NAMES } = await import('./src/server-events.js'); -const { loadPlugins } = await import('./src/plugin-loader.js'); -const { - initUserStorage, - getCookieSecret, - getCookieSessionName, - ensurePublicDirectoriesExist, - getUserDirectoriesList, - migrateSystemPrompts, - migrateUserData, - requireLoginMiddleware, - setUserDataMiddleware, - shouldRedirectToLogin, - cleanUploads, - getSessionCookieAge, - verifySecuritySettings, - loginPageMiddleware, -} = await import('./src/users.js'); - -const { default: getWebpackServeMiddleware } = await import('./src/middleware/webpack-serve.js'); -const { default: basicAuthMiddleware } = await import('./src/middleware/basicAuth.js'); -const { default: getWhitelistMiddleware } = await import('./src/middleware/whitelist.js'); -const { default: accessLoggerMiddleware, getAccessLogPath, migrateAccessLog } = await import('./src/middleware/accessLogWriter.js'); -const { default: multerMonkeyPatch } = await import('./src/middleware/multerMonkeyPatch.js'); -const { default: initRequestProxy } = await import('./src/request-proxy.js'); -const { default: getCacheBusterMiddleware } = await import('./src/middleware/cacheBuster.js'); -const { default: corsProxyMiddleware } = await import('./src/middleware/corsProxy.js'); -const { - getVersion, - color, - removeColorFormatting, - getSeparator, - safeReadFileSync, - setupLogLevel, - setWindowTitle, -} = await import('./src/util.js'); -const { UPLOADS_DIRECTORY } = await import('./src/constants.js'); -const { ensureThumbnailCache } = await import('./src/endpoints/thumbnails.js'); - -// Routers -const { router : usersPublicRouter } = await import('./src/endpoints/users-public.js'); -const { init : statsInit, onExit : statsOnExit } = await import('./src/endpoints/stats.js'); -const { checkForNewContent } = await import('./src/endpoints/content-manager.js'); -const { init : settingsInit } = await import('./src/endpoints/settings.js'); -const { redirectDeprecatedEndpoints, ServerStartup, setupPrivateEndpoints } = await import('./src/server-startup.js'); -const { diskCache } = await import('./src/endpoints/characters.js'); - -// Unrestrict console logs display limit -util.inspect.defaultOptions.maxArrayLength = null; -util.inspect.defaultOptions.maxStringLength = null; -util.inspect.defaultOptions.depth = 4; - -if (!cliArgs.enableIPv6 && !cliArgs.enableIPv4) { - console.error('error: You can\'t disable all internet protocols: at least IPv6 or IPv4 must be enabled.'); - process.exit(1); -} - try { - if (cliArgs.dnsPreferIPv6) { - dns.setDefaultResultOrder('ipv6first'); - console.log('Preferring IPv6 for DNS resolution'); - } else { - dns.setDefaultResultOrder('ipv4first'); - console.log('Preferring IPv4 for DNS resolution'); - } + await import('./src/server-main.js'); } catch (error) { - console.warn('Failed to set DNS resolution order. Possibly unsupported in this Node version.'); + console.error('A critical error has occurred while starting the server:', error); } - -const app = express(); -app.use(helmet({ - contentSecurityPolicy: false, -})); -app.use(compression()); -app.use(responseTime()); - -app.use(bodyParser.json({ limit: '200mb' })); -app.use(bodyParser.urlencoded({ extended: true, limit: '200mb' })); - -// CORS Settings // -const CORS = cors({ - origin: 'null', - methods: ['OPTIONS'], -}); - -app.use(CORS); - -if (cliArgs.listen && cliArgs.basicAuthMode) { - app.use(basicAuthMiddleware); -} - -if (cliArgs.whitelistMode) { - const whitelistMiddleware = await getWhitelistMiddleware(); - app.use(whitelistMiddleware); -} - -if (cliArgs.listen) { - app.use(accessLoggerMiddleware()); -} - -if (cliArgs.enableCorsProxy) { - app.use('/proxy/:url(*)', corsProxyMiddleware); -} else { - app.use('/proxy/:url(*)', async (_, res) => { - const message = 'CORS proxy is disabled. Enable it in config.yaml or use the --corsProxy flag.'; - console.log(message); - res.status(404).send(message); - }); -} - -app.use(cookieSession({ - name: getCookieSessionName(), - sameSite: 'lax', - httpOnly: true, - maxAge: getSessionCookieAge(), - secret: getCookieSecret(globalThis.DATA_ROOT), -})); - -app.use(setUserDataMiddleware); - -// CSRF Protection // -if (!cliArgs.disableCsrf) { - const csrfSyncProtection = csrfSync({ - getTokenFromState: (req) => { - if (!req.session) { - console.error('(CSRF error) getTokenFromState: Session object not initialized'); - return; - } - return req.session.csrfToken; - }, - getTokenFromRequest: (req) => { - return req.headers['x-csrf-token']?.toString(); - }, - storeTokenInState: (req, token) => { - if (!req.session) { - console.error('(CSRF error) storeTokenInState: Session object not initialized'); - return; - } - req.session.csrfToken = token; - }, - size: 32, - }); - - app.get('/csrf-token', (req, res) => { - res.json({ - 'token': csrfSyncProtection.generateToken(req), - }); - }); - - // Customize the error message - csrfSyncProtection.invalidCsrfTokenError.message = color.red('Invalid CSRF token. Please refresh the page and try again.'); - csrfSyncProtection.invalidCsrfTokenError.stack = undefined; - - app.use(csrfSyncProtection.csrfSynchronisedProtection); -} else { - console.warn('\nCSRF protection is disabled. This will make your server vulnerable to CSRF attacks.\n'); - app.get('/csrf-token', (req, res) => { - res.json({ - 'token': 'disabled', - }); - }); -} - -// Static files -// Host index page -app.get('/', getCacheBusterMiddleware(), (request, response) => { - if (shouldRedirectToLogin(request)) { - const query = request.url.split('?')[1]; - const redirectUrl = query ? `/login?${query}` : '/login'; - return response.redirect(redirectUrl); - } - - return response.sendFile('index.html', { root: path.join(serverDirectory, 'public') }); -}); - -// Callback endpoint for OAuth PKCE flows (e.g. OpenRouter) -app.get('/callback/:source?', (request, response) => { - const source = request.params.source; - const query = request.url.split('?')[1]; - const searchParams = new URLSearchParams(); - source && searchParams.set('source', source); - query && searchParams.set('query', query); - const path = `/?${searchParams.toString()}`; - return response.redirect(307, path); -}); - -// Host login page -app.get('/login', loginPageMiddleware); - -// Host frontend assets -const webpackMiddleware = getWebpackServeMiddleware(); -app.use(webpackMiddleware); -app.use(express.static(path.join(serverDirectory, 'public'), {})); - -// Public API -app.use('/api/users', usersPublicRouter); - -// Everything below this line requires authentication -app.use(requireLoginMiddleware); -app.get('/api/ping', (request, response) => { - if (request.query.extend && request.session) { - request.session.touch = Date.now(); - } - - response.sendStatus(204); -}); - -// File uploads -const uploadsPath = path.join(cliArgs.dataRoot, UPLOADS_DIRECTORY); -app.use(multer({ dest: uploadsPath, limits: { fieldSize: 10 * 1024 * 1024 } }).single('avatar')); -app.use(multerMonkeyPatch); - -app.get('/version', async function (_, response) { - const data = await getVersion(); - response.send(data); -}); - -redirectDeprecatedEndpoints(app); -setupPrivateEndpoints(app); - -/** - * Tasks that need to be run before the server starts listening. - * @returns {Promise} - */ -async function preSetupTasks() { - const version = await getVersion(); - - // Print formatted header - console.log(); - console.log(`SillyTavern ${version.pkgVersion}`); - if (version.gitBranch) { - console.log(`Running '${version.gitBranch}' (${version.gitRevision}) - ${version.commitDate}`); - if (!version.isLatest && ['staging', 'release'].includes(version.gitBranch)) { - console.log('INFO: Currently not on the latest commit.'); - console.log(' Run \'git pull\' to update. If you have any merge conflicts, run \'git reset --hard\' and \'git pull\' to reset your branch.'); - } - } - console.log(); - - const directories = await getUserDirectoriesList(); - await checkForNewContent(directories); - await ensureThumbnailCache(directories); - await diskCache.verify(directories); - cleanUploads(); - migrateAccessLog(); - - await settingsInit(); - await statsInit(); - - const pluginsDirectory = path.join(serverDirectory, 'plugins'); - const cleanupPlugins = await loadPlugins(app, pluginsDirectory); - const consoleTitle = process.title; - - let isExiting = false; - const exitProcess = async () => { - if (isExiting) return; - isExiting = true; - await statsOnExit(); - if (typeof cleanupPlugins === 'function') { - await cleanupPlugins(); - } - diskCache.dispose(); - setWindowTitle(consoleTitle); - process.exit(); - }; - - // Set up event listeners for a graceful shutdown - process.on('SIGINT', exitProcess); - process.on('SIGTERM', exitProcess); - process.on('uncaughtException', (err) => { - console.error('Uncaught exception:', err); - exitProcess(); - }); - - // Add request proxy. - initRequestProxy({ enabled: cliArgs.requestProxyEnabled, url: cliArgs.requestProxyUrl, bypass: cliArgs.requestProxyBypass }); - - // Wait for frontend libs to compile - await webpackMiddleware.runWebpackCompiler(); -} - -/** - * Tasks that need to be run after the server starts listening. - * @param {import('./src/server-startup.js').ServerStartupResult} result The result of the server startup - * @returns {Promise} - */ -async function postSetupTasks(result) { - const autorunHostname = await cliArgs.getAutorunHostname(result); - const autorunUrl = cliArgs.getAutorunUrl(autorunHostname); - - if (cliArgs.autorun) { - try { - console.log('Launching in a browser...'); - await open(autorunUrl.toString()); - } catch (error) { - console.error('Failed to launch the browser. Open the URL manually.'); - } - } - - setWindowTitle('SillyTavern WebServer'); - - let logListen = 'SillyTavern is listening on'; - - if (result.useIPv6 && !result.v6Failed) { - logListen += color.green( - ' IPv6: ' + cliArgs.getIPv6ListenUrl().host, - ); - } - - if (result.useIPv4 && !result.v4Failed) { - logListen += color.green( - ' IPv4: ' + cliArgs.getIPv4ListenUrl().host, - ); - } - - const goToLog = 'Go to: ' + color.blue(autorunUrl) + ' to open SillyTavern'; - const plainGoToLog = removeColorFormatting(goToLog); - - console.log(logListen); - if (cliArgs.listen) { - console.log(); - console.log('To limit connections to internal localhost only ([::1] or 127.0.0.1), change the setting in config.yaml to "listen: false".'); - console.log('Check the "access.log" file in the data directory to inspect incoming connections:', color.green(getAccessLogPath())); - } - console.log('\n' + getSeparator(plainGoToLog.length) + '\n'); - console.log(goToLog); - console.log('\n' + getSeparator(plainGoToLog.length) + '\n'); - - setupLogLevel(); - serverEvents.emit(EVENT_NAMES.SERVER_STARTED, { url: autorunUrl }); -} - -/** - * Registers a not-found error response if a not-found error page exists. Should only be called after all other middlewares have been registered. - */ -function apply404Middleware() { - const notFoundWebpage = safeReadFileSync(path.join(serverDirectory, 'public/error/url-not-found.html')) ?? ''; - app.use((req, res) => { - res.status(404).send(notFoundWebpage); - }); -} - -// User storage module needs to be initialized before starting the server -initUserStorage(globalThis.DATA_ROOT) - .then(ensurePublicDirectoriesExist) - .then(migrateUserData) - .then(migrateSystemPrompts) - .then(verifySecuritySettings) - .then(preSetupTasks) - .then(apply404Middleware) - .then(() => new ServerStartup(app, cliArgs).start()) - .then(postSetupTasks); diff --git a/src/server-main.js b/src/server-main.js new file mode 100644 index 000000000..4e5344482 --- /dev/null +++ b/src/server-main.js @@ -0,0 +1,386 @@ +// native node modules +import path from 'node:path'; +import util from 'node:util'; +import net from 'node:net'; +import dns from 'node:dns'; +import process from 'node:process'; + +import cors from 'cors'; +import { csrfSync } from 'csrf-sync'; +import express from 'express'; +import compression from 'compression'; +import cookieSession from 'cookie-session'; +import multer from 'multer'; +import responseTime from 'response-time'; +import helmet from 'helmet'; +import bodyParser from 'body-parser'; +import open from 'open'; + +// local library imports +import './fetch-patch.js'; +import { serverDirectory } from './server-directory.js'; + +console.log(`Node version: ${process.version}. Running in ${process.env.NODE_ENV} environment. Server directory: ${serverDirectory}`); + +// Work around a node v20.0.0, v20.1.0, and v20.2.0 bug. The issue was fixed in v20.3.0. +// https://github.com/nodejs/node/issues/47822#issuecomment-1564708870 +// Safe to remove once support for Node v20 is dropped. +if (process.versions && process.versions.node && process.versions.node.match(/20\.[0-2]\.0/)) { + // @ts-ignore + if (net.setDefaultAutoSelectFamily) net.setDefaultAutoSelectFamily(false); +} + +import { serverEvents, EVENT_NAMES } from './server-events.js'; +import { loadPlugins } from './plugin-loader.js'; +import { + initUserStorage, + getCookieSecret, + getCookieSessionName, + ensurePublicDirectoriesExist, + getUserDirectoriesList, + migrateSystemPrompts, + migrateUserData, + requireLoginMiddleware, + setUserDataMiddleware, + shouldRedirectToLogin, + cleanUploads, + getSessionCookieAge, + verifySecuritySettings, + loginPageMiddleware, +} from './users.js'; + +import getWebpackServeMiddleware from './middleware/webpack-serve.js'; +import basicAuthMiddleware from './middleware/basicAuth.js'; +import getWhitelistMiddleware from './middleware/whitelist.js'; +import accessLoggerMiddleware, { getAccessLogPath, migrateAccessLog } from './middleware/accessLogWriter.js'; +import multerMonkeyPatch from './middleware/multerMonkeyPatch.js'; +import initRequestProxy from './request-proxy.js'; +import getCacheBusterMiddleware from './middleware/cacheBuster.js'; +import corsProxyMiddleware from './middleware/corsProxy.js'; +import { + getVersion, + color, + removeColorFormatting, + getSeparator, + safeReadFileSync, + setupLogLevel, + setWindowTitle, +} from './util.js'; +import { UPLOADS_DIRECTORY } from './constants.js'; +import { ensureThumbnailCache } from './endpoints/thumbnails.js'; + +// Routers +import { router as usersPublicRouter } from './endpoints/users-public.js'; +import { init as statsInit, onExit as statsOnExit } from './endpoints/stats.js'; +import { checkForNewContent } from './endpoints/content-manager.js'; +import { init as settingsInit } from './endpoints/settings.js'; +import { redirectDeprecatedEndpoints, ServerStartup, setupPrivateEndpoints } from './server-startup.js'; +import { diskCache } from './endpoints/characters.js'; + +// Unrestrict console logs display limit +util.inspect.defaultOptions.maxArrayLength = null; +util.inspect.defaultOptions.maxStringLength = null; +util.inspect.defaultOptions.depth = 4; + +const cliArgs = globalThis.COMMAND_LINE_ARGS; + +if (!cliArgs.enableIPv6 && !cliArgs.enableIPv4) { + console.error('error: You can\'t disable all internet protocols: at least IPv6 or IPv4 must be enabled.'); + process.exit(1); +} + +try { + if (cliArgs.dnsPreferIPv6) { + dns.setDefaultResultOrder('ipv6first'); + console.log('Preferring IPv6 for DNS resolution'); + } else { + dns.setDefaultResultOrder('ipv4first'); + console.log('Preferring IPv4 for DNS resolution'); + } +} catch (error) { + console.warn('Failed to set DNS resolution order. Possibly unsupported in this Node version.'); +} + +const app = express(); +app.use(helmet({ + contentSecurityPolicy: false, +})); +app.use(compression()); +app.use(responseTime()); + +app.use(bodyParser.json({ limit: '200mb' })); +app.use(bodyParser.urlencoded({ extended: true, limit: '200mb' })); + +// CORS Settings // +const CORS = cors({ + origin: 'null', + methods: ['OPTIONS'], +}); + +app.use(CORS); + +if (cliArgs.listen && cliArgs.basicAuthMode) { + app.use(basicAuthMiddleware); +} + +if (cliArgs.whitelistMode) { + const whitelistMiddleware = await getWhitelistMiddleware(); + app.use(whitelistMiddleware); +} + +if (cliArgs.listen) { + app.use(accessLoggerMiddleware()); +} + +if (cliArgs.enableCorsProxy) { + app.use('/proxy/:url(*)', corsProxyMiddleware); +} else { + app.use('/proxy/:url(*)', async (_, res) => { + const message = 'CORS proxy is disabled. Enable it in config.yaml or use the --corsProxy flag.'; + console.log(message); + res.status(404).send(message); + }); +} + +app.use(cookieSession({ + name: getCookieSessionName(), + sameSite: 'lax', + httpOnly: true, + maxAge: getSessionCookieAge(), + secret: getCookieSecret(globalThis.DATA_ROOT), +})); + +app.use(setUserDataMiddleware); + +// CSRF Protection // +if (!cliArgs.disableCsrf) { + const csrfSyncProtection = csrfSync({ + getTokenFromState: (req) => { + if (!req.session) { + console.error('(CSRF error) getTokenFromState: Session object not initialized'); + return; + } + return req.session.csrfToken; + }, + getTokenFromRequest: (req) => { + return req.headers['x-csrf-token']?.toString(); + }, + storeTokenInState: (req, token) => { + if (!req.session) { + console.error('(CSRF error) storeTokenInState: Session object not initialized'); + return; + } + req.session.csrfToken = token; + }, + size: 32, + }); + + app.get('/csrf-token', (req, res) => { + res.json({ + 'token': csrfSyncProtection.generateToken(req), + }); + }); + + // Customize the error message + csrfSyncProtection.invalidCsrfTokenError.message = color.red('Invalid CSRF token. Please refresh the page and try again.'); + csrfSyncProtection.invalidCsrfTokenError.stack = undefined; + + app.use(csrfSyncProtection.csrfSynchronisedProtection); +} else { + console.warn('\nCSRF protection is disabled. This will make your server vulnerable to CSRF attacks.\n'); + app.get('/csrf-token', (req, res) => { + res.json({ + 'token': 'disabled', + }); + }); +} + +// Static files +// Host index page +app.get('/', getCacheBusterMiddleware(), (request, response) => { + if (shouldRedirectToLogin(request)) { + const query = request.url.split('?')[1]; + const redirectUrl = query ? `/login?${query}` : '/login'; + return response.redirect(redirectUrl); + } + + return response.sendFile('index.html', { root: path.join(serverDirectory, 'public') }); +}); + +// Callback endpoint for OAuth PKCE flows (e.g. OpenRouter) +app.get('/callback/:source?', (request, response) => { + const source = request.params.source; + const query = request.url.split('?')[1]; + const searchParams = new URLSearchParams(); + source && searchParams.set('source', source); + query && searchParams.set('query', query); + const path = `/?${searchParams.toString()}`; + return response.redirect(307, path); +}); + +// Host login page +app.get('/login', loginPageMiddleware); + +// Host frontend assets +const webpackMiddleware = getWebpackServeMiddleware(); +app.use(webpackMiddleware); +app.use(express.static(path.join(serverDirectory, 'public'), {})); + +// Public API +app.use('/api/users', usersPublicRouter); + +// Everything below this line requires authentication +app.use(requireLoginMiddleware); +app.get('/api/ping', (request, response) => { + if (request.query.extend && request.session) { + request.session.touch = Date.now(); + } + + response.sendStatus(204); +}); + +// File uploads +const uploadsPath = path.join(cliArgs.dataRoot, UPLOADS_DIRECTORY); +app.use(multer({ dest: uploadsPath, limits: { fieldSize: 10 * 1024 * 1024 } }).single('avatar')); +app.use(multerMonkeyPatch); + +app.get('/version', async function (_, response) { + const data = await getVersion(); + response.send(data); +}); + +redirectDeprecatedEndpoints(app); +setupPrivateEndpoints(app); + +/** + * Tasks that need to be run before the server starts listening. + * @returns {Promise} + */ +async function preSetupTasks() { + const version = await getVersion(); + + // Print formatted header + console.log(); + console.log(`SillyTavern ${version.pkgVersion}`); + if (version.gitBranch) { + console.log(`Running '${version.gitBranch}' (${version.gitRevision}) - ${version.commitDate}`); + if (!version.isLatest && ['staging', 'release'].includes(version.gitBranch)) { + console.log('INFO: Currently not on the latest commit.'); + console.log(' Run \'git pull\' to update. If you have any merge conflicts, run \'git reset --hard\' and \'git pull\' to reset your branch.'); + } + } + console.log(); + + const directories = await getUserDirectoriesList(); + await checkForNewContent(directories); + await ensureThumbnailCache(directories); + await diskCache.verify(directories); + cleanUploads(); + migrateAccessLog(); + + await settingsInit(); + await statsInit(); + + const pluginsDirectory = path.join(serverDirectory, 'plugins'); + const cleanupPlugins = await loadPlugins(app, pluginsDirectory); + const consoleTitle = process.title; + + let isExiting = false; + const exitProcess = async () => { + if (isExiting) return; + isExiting = true; + await statsOnExit(); + if (typeof cleanupPlugins === 'function') { + await cleanupPlugins(); + } + diskCache.dispose(); + setWindowTitle(consoleTitle); + process.exit(); + }; + + // Set up event listeners for a graceful shutdown + process.on('SIGINT', exitProcess); + process.on('SIGTERM', exitProcess); + process.on('uncaughtException', (err) => { + console.error('Uncaught exception:', err); + exitProcess(); + }); + + // Add request proxy. + initRequestProxy({ enabled: cliArgs.requestProxyEnabled, url: cliArgs.requestProxyUrl, bypass: cliArgs.requestProxyBypass }); + + // Wait for frontend libs to compile + await webpackMiddleware.runWebpackCompiler(); +} + +/** + * Tasks that need to be run after the server starts listening. + * @param {import('./server-startup.js').ServerStartupResult} result The result of the server startup + * @returns {Promise} + */ +async function postSetupTasks(result) { + const autorunHostname = await cliArgs.getAutorunHostname(result); + const autorunUrl = cliArgs.getAutorunUrl(autorunHostname); + + if (cliArgs.autorun) { + try { + console.log('Launching in a browser...'); + await open(autorunUrl.toString()); + } catch (error) { + console.error('Failed to launch the browser. Open the URL manually.'); + } + } + + setWindowTitle('SillyTavern WebServer'); + + let logListen = 'SillyTavern is listening on'; + + if (result.useIPv6 && !result.v6Failed) { + logListen += color.green( + ' IPv6: ' + cliArgs.getIPv6ListenUrl().host, + ); + } + + if (result.useIPv4 && !result.v4Failed) { + logListen += color.green( + ' IPv4: ' + cliArgs.getIPv4ListenUrl().host, + ); + } + + const goToLog = 'Go to: ' + color.blue(autorunUrl) + ' to open SillyTavern'; + const plainGoToLog = removeColorFormatting(goToLog); + + console.log(logListen); + if (cliArgs.listen) { + console.log(); + console.log('To limit connections to internal localhost only ([::1] or 127.0.0.1), change the setting in config.yaml to "listen: false".'); + console.log('Check the "access.log" file in the data directory to inspect incoming connections:', color.green(getAccessLogPath())); + } + console.log('\n' + getSeparator(plainGoToLog.length) + '\n'); + console.log(goToLog); + console.log('\n' + getSeparator(plainGoToLog.length) + '\n'); + + setupLogLevel(); + serverEvents.emit(EVENT_NAMES.SERVER_STARTED, { url: autorunUrl }); +} + +/** + * Registers a not-found error response if a not-found error page exists. Should only be called after all other middlewares have been registered. + */ +function apply404Middleware() { + const notFoundWebpage = safeReadFileSync(path.join(serverDirectory, 'public/error/url-not-found.html')) ?? ''; + app.use((req, res) => { + res.status(404).send(notFoundWebpage); + }); +} + +// User storage module needs to be initialized before starting the server +initUserStorage(globalThis.DATA_ROOT) + .then(ensurePublicDirectoriesExist) + .then(migrateUserData) + .then(migrateSystemPrompts) + .then(verifySecuritySettings) + .then(preSetupTasks) + .then(apply404Middleware) + .then(() => new ServerStartup(app, cliArgs).start()) + .then(postSetupTasks);