| |
| 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 './fetch-patch.js'; |
| import { serverDirectory } from './server-directory.js'; |
|
|
| 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 cacheBuster from './middleware/cacheBuster.js'; |
| import corsProxyMiddleware from './middleware/corsProxy.js'; |
| import hostWhitelistMiddleware from './middleware/hostWhitelist.js'; |
| import { |
| getVersion, |
| color, |
| removeColorFormatting, |
| getSeparator, |
| safeReadFileSync, |
| setupLogLevel, |
| setWindowTitle, |
| getConfigValue, |
| } from './util.js'; |
| import { UPLOADS_DIRECTORY } from './constants.js'; |
| import { ensureThumbnailCache } from './endpoints/thumbnails.js'; |
|
|
| import { router as secureGenerateRouter } from './endpoints/secure-generate.js'; |
|
|
| |
| 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'; |
| import { migrateFlatSecrets } from './endpoints/secrets.js'; |
| import { migrateGroupChatsMetadataFormat } from './endpoints/groups.js'; |
|
|
| |
| |
| |
| if (process.versions && process.versions.node && process.versions.node.match(/20\.[0-2]\.0/)) { |
| |
| if (net.setDefaultAutoSelectFamily) net.setDefaultAutoSelectFamily(false); |
| } |
|
|
| |
| 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); |
| } |
|
|
| const app = express(); |
| app.use(helmet({ |
| contentSecurityPolicy: false, |
| })); |
| app.use(compression()); |
| app.use(responseTime()); |
|
|
| app.use(bodyParser.json({ limit: '500mb' })); |
| app.use(bodyParser.urlencoded({ extended: true, limit: '500mb' })); |
|
|
| |
| 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); |
| } |
|
|
| app.use(hostWhitelistMiddleware); |
|
|
| 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); |
|
|
| |
| 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), |
| }); |
| }); |
|
|
| |
| 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', |
| }); |
| }); |
| } |
|
|
| |
| |
| app.get('/', cacheBuster.middleware, (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') }); |
| }); |
|
|
| |
| 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); |
| }); |
|
|
| |
| app.get('/login', loginPageMiddleware); |
|
|
| |
| const webpackMiddleware = getWebpackServeMiddleware(); |
| app.use(webpackMiddleware); |
| app.use(express.static(path.join(serverDirectory, 'public'), {})); |
|
|
| |
| app.use('/api/users', usersPublicRouter); |
|
|
| |
| app.use(requireLoginMiddleware); |
|
|
| |
| app.use('/api/secure-generate', secureGenerateRouter); |
|
|
| app.post('/api/ping', (request, response) => { |
| if (request.query.extend && request.session) { |
| request.session.touch = Date.now(); |
| } |
|
|
| response.sendStatus(204); |
| }); |
|
|
| |
| const uploadsPath = path.join(cliArgs.dataRoot, UPLOADS_DIRECTORY); |
| app.use(multer({ dest: uploadsPath, limits: { fieldSize: 500 * 1024 * 1024 } }).single('avatar')); |
| app.use(multerMonkeyPatch); |
|
|
| app.get('/version', async function (_, response) { |
| const data = await getVersion(); |
| response.send(data); |
| }); |
|
|
| redirectDeprecatedEndpoints(app); |
| setupPrivateEndpoints(app); |
|
|
| |
| |
| |
| |
| async function preSetupTasks() { |
| const version = await getVersion(); |
|
|
| |
| console.log(); |
| console.log(`TavernIntern ${version.pkgVersion}`); |
| if (version.gitBranch && version.commitDate) { |
| const date = new Date(version.commitDate); |
| const localDate = date.toLocaleString('en-US', { timeZoneName: 'short' }); |
| console.log(`Running '${version.gitBranch}' (${version.gitRevision}) - ${localDate}`); |
| 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 migrateGroupChatsMetadataFormat(directories); |
| await checkForNewContent(directories); |
| await ensureThumbnailCache(directories); |
| await diskCache.verify(directories); |
| migrateFlatSecrets(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(); |
| }; |
|
|
| |
| process.on('SIGINT', exitProcess); |
| process.on('SIGTERM', exitProcess); |
| process.on('uncaughtException', (err) => { |
| console.error('Uncaught exception:', err); |
| exitProcess(); |
| }); |
|
|
| |
| initRequestProxy({ enabled: cliArgs.requestProxyEnabled, url: cliArgs.requestProxyUrl, bypass: cliArgs.requestProxyBypass }); |
|
|
| |
| await webpackMiddleware.runWebpackCompiler(); |
| } |
|
|
| |
| |
| |
| |
| |
| async function postSetupTasks(result) { |
| const browserLaunchHostname = await cliArgs.getBrowserLaunchHostname(result); |
| const browserLaunchUrl = cliArgs.getBrowserLaunchUrl(browserLaunchHostname); |
| const browserLaunchApp = String(getConfigValue('browserLaunch.browser', 'default') ?? ''); |
|
|
| if (cliArgs.browserLaunchEnabled) { |
| try { |
| |
| const openModule = await import('open'); |
| const { default: open, apps } = openModule; |
|
|
| function getBrowsers() { |
| const isAndroid = process.platform === 'android'; |
| if (isAndroid) { |
| return {}; |
| } |
| return { |
| 'firefox': apps.firefox, |
| 'chrome': apps.chrome, |
| 'edge': apps.edge, |
| 'brave': apps.brave, |
| }; |
| } |
|
|
| const validBrowsers = getBrowsers(); |
| const appName = validBrowsers[browserLaunchApp.trim().toLowerCase()]; |
| const openOptions = appName ? { app: { name: appName } } : {}; |
|
|
| console.log(`Launching in a browser: ${browserLaunchApp}...`); |
| await open(browserLaunchUrl.toString(), openOptions); |
| } catch (error) { |
| console.error('Failed to launch the browser. Open the URL manually.', error); |
| } |
| } |
|
|
| setWindowTitle('TavernIntern WebServer'); |
|
|
| let logListen = 'TavernIntern 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(browserLaunchUrl)} to open TavernIntern`; |
| 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: browserLaunchUrl }); |
| } |
|
|
| |
| |
| |
| function apply404Middleware() { |
| const notFoundWebpage = safeReadFileSync(path.join(serverDirectory, 'public/error/url-not-found.html')) ?? ''; |
| app.use((req, res) => { |
| res.status(404).send(notFoundWebpage); |
| }); |
| } |
|
|
| |
| |
| |
| function setDnsResolutionOrder() { |
| 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.'); |
| } |
| } |
|
|
| |
| initUserStorage(globalThis.DATA_ROOT) |
| .then(setDnsResolutionOrder) |
| .then(ensurePublicDirectoriesExist) |
| .then(migrateUserData) |
| .then(migrateSystemPrompts) |
| .then(verifySecuritySettings) |
| .then(preSetupTasks) |
| .then(apply404Middleware) |
| .then(() => new ServerStartup(app, cliArgs).start()) |
| .then(postSetupTasks); |
|
|