workman / jiuguan2025cc /src /server-startup.js
stail00016's picture
Upload 888 files
4342d5f verified
raw
history blame contribute delete
16.5 kB
import https from 'node:https';
import http from 'node:http';
import fs from 'node:fs';
import { color, urlHostnameToIPv6, getHasIP } from './util.js';
// Express routers
import { router as userDataRouter } from './users.js';
import { router as usersPrivateRouter } from './endpoints/users-private.js';
import { router as usersAdminRouter } from './endpoints/users-admin.js';
import { router as movingUIRouter } from './endpoints/moving-ui.js';
import { router as imagesRouter } from './endpoints/images.js';
import { router as quickRepliesRouter } from './endpoints/quick-replies.js';
import { router as avatarsRouter } from './endpoints/avatars.js';
import { router as themesRouter } from './endpoints/themes.js';
import { router as openAiRouter } from './endpoints/openai.js';
import { router as googleRouter } from './endpoints/google.js';
import { router as anthropicRouter } from './endpoints/anthropic.js';
import { router as tokenizersRouter } from './endpoints/tokenizers.js';
import { router as presetsRouter } from './endpoints/presets.js';
import { router as secretsRouter } from './endpoints/secrets.js';
import { router as thumbnailRouter } from './endpoints/thumbnails.js';
import { router as novelAiRouter } from './endpoints/novelai.js';
import { router as extensionsRouter } from './endpoints/extensions.js';
import { router as assetsRouter } from './endpoints/assets.js';
import { router as filesRouter } from './endpoints/files.js';
import { router as charactersRouter } from './endpoints/characters.js';
import { router as chatsRouter } from './endpoints/chats.js';
import { router as groupsRouter } from './endpoints/groups.js';
import { router as worldInfoRouter } from './endpoints/worldinfo.js';
import { router as statsRouter } from './endpoints/stats.js';
import { router as contentManagerRouter } from './endpoints/content-manager.js';
import { router as settingsRouter } from './endpoints/settings.js';
import { router as backgroundsRouter } from './endpoints/backgrounds.js';
import { router as spritesRouter } from './endpoints/sprites.js';
import { router as stableDiffusionRouter } from './endpoints/stable-diffusion.js';
import { router as hordeRouter } from './endpoints/horde.js';
import { router as vectorsRouter } from './endpoints/vectors.js';
import { router as translateRouter } from './endpoints/translate.js';
import { router as classifyRouter } from './endpoints/classify.js';
import { router as captionRouter } from './endpoints/caption.js';
import { router as searchRouter } from './endpoints/search.js';
import { router as openRouterRouter } from './endpoints/openrouter.js';
import { router as chatCompletionsRouter } from './endpoints/backends/chat-completions.js';
import { router as koboldRouter } from './endpoints/backends/kobold.js';
import { router as textCompletionsRouter } from './endpoints/backends/text-completions.js';
import { router as scaleAltRouter } from './endpoints/backends/scale-alt.js';
import { router as speechRouter } from './endpoints/speech.js';
import { router as azureRouter } from './endpoints/azure.js';
/**
* @typedef {object} ServerStartupResult
* @property {boolean} v6Failed If the server failed to start on IPv6
* @property {boolean} v4Failed If the server failed to start on IPv4
* @property {boolean} useIPv6 If use IPv6
* @property {boolean} useIPv4 If use IPv4
*/
/**
* Redirect deprecated API endpoints to their replacements.
* @param {import('express').Express} app The Express app to use
*/
export function redirectDeprecatedEndpoints(app) {
/**
* Redirect a deprecated API endpoint URL to its replacement. Because fetch, form submissions, and $.ajax follow
* redirects, this is transparent to client-side code.
* @param {string} src The URL to redirect from.
* @param {string} destination The URL to redirect to.
*/
function redirect(src, destination) {
app.use(src, (req, res) => {
console.warn(`API endpoint ${src} is deprecated; use ${destination} instead`);
// HTTP 301 causes the request to become a GET. 308 preserves the request method.
res.redirect(308, destination);
});
}
redirect('/createcharacter', '/api/characters/create');
redirect('/renamecharacter', '/api/characters/rename');
redirect('/editcharacter', '/api/characters/edit');
redirect('/editcharacterattribute', '/api/characters/edit-attribute');
redirect('/v2/editcharacterattribute', '/api/characters/merge-attributes');
redirect('/deletecharacter', '/api/characters/delete');
redirect('/getcharacters', '/api/characters/all');
redirect('/getonecharacter', '/api/characters/get');
redirect('/getallchatsofcharacter', '/api/characters/chats');
redirect('/importcharacter', '/api/characters/import');
redirect('/dupecharacter', '/api/characters/duplicate');
redirect('/exportcharacter', '/api/characters/export');
redirect('/savechat', '/api/chats/save');
redirect('/getchat', '/api/chats/get');
redirect('/renamechat', '/api/chats/rename');
redirect('/delchat', '/api/chats/delete');
redirect('/exportchat', '/api/chats/export');
redirect('/importgroupchat', '/api/chats/group/import');
redirect('/importchat', '/api/chats/import');
redirect('/getgroupchat', '/api/chats/group/get');
redirect('/deletegroupchat', '/api/chats/group/delete');
redirect('/savegroupchat', '/api/chats/group/save');
redirect('/getgroups', '/api/groups/all');
redirect('/creategroup', '/api/groups/create');
redirect('/editgroup', '/api/groups/edit');
redirect('/deletegroup', '/api/groups/delete');
redirect('/getworldinfo', '/api/worldinfo/get');
redirect('/deleteworldinfo', '/api/worldinfo/delete');
redirect('/importworldinfo', '/api/worldinfo/import');
redirect('/editworldinfo', '/api/worldinfo/edit');
redirect('/getstats', '/api/stats/get');
redirect('/recreatestats', '/api/stats/recreate');
redirect('/updatestats', '/api/stats/update');
redirect('/getbackgrounds', '/api/backgrounds/all');
redirect('/delbackground', '/api/backgrounds/delete');
redirect('/renamebackground', '/api/backgrounds/rename');
redirect('/downloadbackground', '/api/backgrounds/upload'); // yes, the downloadbackground endpoint actually uploads one
redirect('/savetheme', '/api/themes/save');
redirect('/getuseravatars', '/api/avatars/get');
redirect('/deleteuseravatar', '/api/avatars/delete');
redirect('/uploaduseravatar', '/api/avatars/upload');
redirect('/deletequickreply', '/api/quick-replies/delete');
redirect('/savequickreply', '/api/quick-replies/save');
redirect('/uploadimage', '/api/images/upload');
redirect('/listimgfiles/:folder', '/api/images/list/:folder');
redirect('/api/content/import', '/api/content/importURL');
redirect('/savemovingui', '/api/moving-ui/save');
redirect('/api/serpapi/search', '/api/search/serpapi');
redirect('/api/serpapi/visit', '/api/search/visit');
redirect('/api/serpapi/transcript', '/api/search/transcript');
}
/**
* Setup the routers for the endpoints.
* @param {import('express').Express} app The Express app to use
*/
export function setupPrivateEndpoints(app) {
app.use('/', userDataRouter);
app.use('/api/users', usersPrivateRouter);
app.use('/api/users', usersAdminRouter);
app.use('/api/moving-ui', movingUIRouter);
app.use('/api/images', imagesRouter);
app.use('/api/quick-replies', quickRepliesRouter);
app.use('/api/avatars', avatarsRouter);
app.use('/api/themes', themesRouter);
app.use('/api/openai', openAiRouter);
app.use('/api/google', googleRouter);
app.use('/api/anthropic', anthropicRouter);
app.use('/api/tokenizers', tokenizersRouter);
app.use('/api/presets', presetsRouter);
app.use('/api/secrets', secretsRouter);
app.use('/thumbnail', thumbnailRouter);
app.use('/api/novelai', novelAiRouter);
app.use('/api/extensions', extensionsRouter);
app.use('/api/assets', assetsRouter);
app.use('/api/files', filesRouter);
app.use('/api/characters', charactersRouter);
app.use('/api/chats', chatsRouter);
app.use('/api/groups', groupsRouter);
app.use('/api/worldinfo', worldInfoRouter);
app.use('/api/stats', statsRouter);
app.use('/api/backgrounds', backgroundsRouter);
app.use('/api/sprites', spritesRouter);
app.use('/api/content', contentManagerRouter);
app.use('/api/settings', settingsRouter);
app.use('/api/sd', stableDiffusionRouter);
app.use('/api/horde', hordeRouter);
app.use('/api/vector', vectorsRouter);
app.use('/api/translate', translateRouter);
app.use('/api/extra/classify', classifyRouter);
app.use('/api/extra/caption', captionRouter);
app.use('/api/search', searchRouter);
app.use('/api/backends/text-completions', textCompletionsRouter);
app.use('/api/openrouter', openRouterRouter);
app.use('/api/backends/kobold', koboldRouter);
app.use('/api/backends/chat-completions', chatCompletionsRouter);
app.use('/api/backends/scale-alt', scaleAltRouter);
app.use('/api/speech', speechRouter);
app.use('/api/azure', azureRouter);
}
/**
* Utilities for starting the express server.
*/
export class ServerStartup {
/**
* Creates a new ServerStartup instance.
* @param {import('express').Express} app The Express app to use
* @param {import('./command-line.js').CommandLineArguments} cliArgs The command-line arguments
*/
constructor(app, cliArgs) {
this.app = app;
this.cliArgs = cliArgs;
}
/**
* Prints a fatal error message and exits the process.
* @param {string} message
*/
#fatal(message) {
console.error(color.red(message));
process.exit(1);
}
/**
* Checks if SSL options are valid. If not, it will print an error message and exit the process.
* @returns {void}
*/
#verifySslOptions() {
if (!this.cliArgs.ssl) return;
if (!this.cliArgs.certPath) {
this.#fatal('Error: SSL certificate path is required when using HTTPS. Check your config');
}
if (!this.cliArgs.keyPath) {
this.#fatal('Error: SSL key path is required when using HTTPS. Check your config');
}
if (!fs.existsSync(this.cliArgs.certPath)) {
this.#fatal('Error: SSL certificate path does not exist');
}
if (!fs.existsSync(this.cliArgs.keyPath)) {
this.#fatal('Error: SSL key path does not exist');
}
}
/**
* Creates an HTTPS server.
* @param {URL} url The URL to listen on
* @param {number} ipVersion the ip version to use
* @returns {Promise<void>} A promise that resolves when the server is listening
*/
#createHttpsServer(url, ipVersion) {
this.#verifySslOptions();
return new Promise((resolve, reject) => {
const sslOptions = {
cert: fs.readFileSync(this.cliArgs.certPath),
key: fs.readFileSync(this.cliArgs.keyPath),
};
const server = https.createServer(sslOptions, this.app);
server.on('error', reject);
server.on('listening', resolve);
let host = url.hostname;
if (ipVersion === 6) host = urlHostnameToIPv6(url.hostname);
server.listen({
host: host,
port: Number(url.port || 443),
// see https://nodejs.org/api/net.html#serverlisten for why ipv6Only is used
ipv6Only: true,
});
});
}
/**
* Creates an HTTP server.
* @param {URL} url The URL to listen on
* @param {number} ipVersion the ip version to use
* @returns {Promise<void>} A promise that resolves when the server is listening
*/
#createHttpServer(url, ipVersion) {
return new Promise((resolve, reject) => {
const server = http.createServer(this.app);
server.on('error', reject);
server.on('listening', resolve);
let host = url.hostname;
if (ipVersion === 6) host = urlHostnameToIPv6(url.hostname);
server.listen({
host: host,
port: Number(url.port || 80),
// see https://nodejs.org/api/net.html#serverlisten for why ipv6Only is used
ipv6Only: true,
});
});
}
/**
* Starts the server using http or https depending on config
* @param {boolean} useIPv6 If use IPv6
* @param {boolean} useIPv4 If use IPv4
* @returns {Promise<[boolean, boolean]>} A promise that resolves with an array of booleans indicating if the server failed to start on IPv6 and IPv4, respectively
*/
async #startHTTPorHTTPS(useIPv6, useIPv4) {
let v6Failed = false;
let v4Failed = false;
const createFunc = this.cliArgs.ssl ? this.#createHttpsServer.bind(this) : this.#createHttpServer.bind(this);
if (useIPv6) {
try {
await createFunc(this.cliArgs.getIPv6ListenUrl(), 6);
} catch (error) {
console.error('Warning: failed to start server on IPv6');
console.error(error);
v6Failed = true;
}
}
if (useIPv4) {
try {
await createFunc(this.cliArgs.getIPv4ListenUrl(), 4);
} catch (error) {
console.error('Warning: failed to start server on IPv4');
console.error(error);
v4Failed = true;
}
}
return [v6Failed, v4Failed];
}
/**
* Handles the case where the server failed to start on one or both protocols.
* @param {ServerStartupResult} result The results of the server startup
* @returns {void}
*/
#handleServerListenFail({ v6Failed, v4Failed, useIPv6, useIPv4 }) {
if (v6Failed && !useIPv4) {
this.#fatal('Error: Failed to start server on IPv6 and IPv4 disabled');
}
if (v4Failed && !useIPv6) {
this.#fatal('Error: Failed to start server on IPv4 and IPv6 disabled');
}
if (v6Failed && v4Failed) {
this.#fatal('Error: Failed to start server on both IPv6 and IPv4');
}
}
/**
* Performs the server startup.
* @returns {Promise<ServerStartupResult>} A promise that resolves with an object containing the results of the server startup
*/
async start() {
let useIPv6 = (this.cliArgs.enableIPv6 === true);
let useIPv4 = (this.cliArgs.enableIPv4 === true);
if (this.cliArgs.enableIPv6 === 'auto' || this.cliArgs.enableIPv4 === 'auto') {
const ipQuery = await getHasIP();
let hasIPv6 = false, hasIPv4 = false;
hasIPv6 = this.cliArgs.listen ? ipQuery.hasIPv6Any : ipQuery.hasIPv6Local;
if (this.cliArgs.enableIPv6 === 'auto') {
useIPv6 = hasIPv6;
}
if (hasIPv6) {
if (useIPv6) {
console.log(color.green('IPv6 support detected'));
} else {
console.log('IPv6 support detected (but disabled)');
}
}
hasIPv4 = this.cliArgs.listen ? ipQuery.hasIPv4Any : ipQuery.hasIPv4Local;
if (this.cliArgs.enableIPv4 === 'auto') {
useIPv4 = hasIPv4;
}
if (hasIPv4) {
if (useIPv4) {
console.log(color.green('IPv4 support detected'));
} else {
console.log('IPv4 support detected (but disabled)');
}
}
if (this.cliArgs.enableIPv6 === 'auto' && this.cliArgs.enableIPv4 === 'auto') {
if (!hasIPv6 && !hasIPv4) {
console.error('Both IPv6 and IPv4 are not detected');
process.exit(1);
}
}
}
if (!useIPv6 && !useIPv4) {
console.error('Both IPv6 and IPv4 are disabled or not detected');
process.exit(1);
}
const [v6Failed, v4Failed] = await this.#startHTTPorHTTPS(useIPv6, useIPv4);
const result = { v6Failed, v4Failed, useIPv6, useIPv4 };
this.#handleServerListenFail(result);
return result;
}
}