|
import https from 'node:https'; |
|
import http from 'node:http'; |
|
import fs from 'node:fs'; |
|
import { color, urlHostnameToIPv6, getHasIP } from './util.js'; |
|
|
|
|
|
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'; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export function redirectDeprecatedEndpoints(app) { |
|
|
|
|
|
|
|
|
|
|
|
|
|
function redirect(src, destination) { |
|
app.use(src, (req, res) => { |
|
console.warn(`API endpoint ${src} is deprecated; use ${destination} instead`); |
|
|
|
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'); |
|
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'); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
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); |
|
} |
|
|
|
|
|
|
|
|
|
export class ServerStartup { |
|
|
|
|
|
|
|
|
|
|
|
constructor(app, cliArgs) { |
|
this.app = app; |
|
this.cliArgs = cliArgs; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
#fatal(message) { |
|
console.error(color.red(message)); |
|
process.exit(1); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
#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'); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#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), |
|
|
|
ipv6Only: true, |
|
}); |
|
}); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#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), |
|
|
|
ipv6Only: true, |
|
}); |
|
}); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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]; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
#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'); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
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; |
|
} |
|
} |
|
|