mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-02-18 21:20:39 +01:00
Merge pull request #3342 from Spappz/staging
Allow customisation of the 403 page
This commit is contained in:
commit
44ad69ceca
1
.gitignore
vendored
1
.gitignore
vendored
@ -45,6 +45,7 @@ access.log
|
||||
/vectors/
|
||||
/cache/
|
||||
public/css/user.css
|
||||
public/error/
|
||||
/plugins/
|
||||
/data
|
||||
/default/scaffold
|
||||
|
22
default/public/error/forbidden-by-whitelist.html
Normal file
22
default/public/error/forbidden-by-whitelist.html
Normal file
@ -0,0 +1,22 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>Forbidden</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>Forbidden</h1>
|
||||
<p>
|
||||
If you are the system administrator, add your IP address to the
|
||||
whitelist or disable whitelist mode by editing
|
||||
<code>config.yaml</code> in the root directory of your installation.
|
||||
</p>
|
||||
<hr />
|
||||
<p>
|
||||
<em>Connection from {{ipDetails}} has been blocked. This attempt
|
||||
has been logged.</em>
|
||||
</p>
|
||||
</body>
|
||||
|
||||
</html>
|
17
default/public/error/unauthorized.html
Normal file
17
default/public/error/unauthorized.html
Normal file
@ -0,0 +1,17 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>Unauthorized</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>Unauthorized</h1>
|
||||
<p>
|
||||
If you are the system administrator, you can configure the
|
||||
<code>basicAuthUser</code> credentials by editing
|
||||
<code>config.yaml</code> in the root directory of your installation.
|
||||
</p>
|
||||
</body>
|
||||
|
||||
</html>
|
15
default/public/error/url-not-found.html
Normal file
15
default/public/error/url-not-found.html
Normal file
@ -0,0 +1,15 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>Not found</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>Not found</h1>
|
||||
<p>
|
||||
The requested URL was not found on this server.
|
||||
</p>
|
||||
</body>
|
||||
|
||||
</html>
|
@ -213,20 +213,60 @@ function addMissingConfigValues() {
|
||||
* Creates the default config files if they don't exist yet.
|
||||
*/
|
||||
function createDefaultFiles() {
|
||||
const files = {
|
||||
config: './config.yaml',
|
||||
user: './public/css/user.css',
|
||||
};
|
||||
/**
|
||||
* @typedef DefaultItem
|
||||
* @type {object}
|
||||
* @property {'file' | 'directory'} type - Whether the item should be copied as a single file or merged into a directory structure.
|
||||
* @property {string} defaultPath - The path to the default item (typically in `default/`).
|
||||
* @property {string} productionPath - The path to the copied item for production use.
|
||||
*/
|
||||
|
||||
for (const file of Object.values(files)) {
|
||||
/** @type {DefaultItem[]} */
|
||||
const defaultItems = [
|
||||
{
|
||||
type: 'file',
|
||||
defaultPath: './default/config.yaml',
|
||||
productionPath: './config.yaml',
|
||||
},
|
||||
{
|
||||
type: 'directory',
|
||||
defaultPath: './default/public/',
|
||||
productionPath: './public/',
|
||||
},
|
||||
];
|
||||
|
||||
for (const defaultItem of defaultItems) {
|
||||
try {
|
||||
if (!fs.existsSync(file)) {
|
||||
const defaultFilePath = path.join('./default', path.parse(file).base);
|
||||
fs.copyFileSync(defaultFilePath, file);
|
||||
console.log(color.green(`Created default file: ${file}`));
|
||||
if (defaultItem.type === 'file') {
|
||||
if (!fs.existsSync(defaultItem.productionPath)) {
|
||||
fs.copyFileSync(
|
||||
defaultItem.defaultPath,
|
||||
defaultItem.productionPath,
|
||||
);
|
||||
console.log(
|
||||
color.green(`Created default file: ${defaultItem.productionPath}`),
|
||||
);
|
||||
}
|
||||
} else if (defaultItem.type === 'directory') {
|
||||
fs.cpSync(defaultItem.defaultPath, defaultItem.productionPath, {
|
||||
force: false, // Don't overwrite existing files!
|
||||
recursive: true,
|
||||
});
|
||||
console.log(
|
||||
color.green(`Synchronized missing files: ${defaultItem.productionPath}`),
|
||||
);
|
||||
} else {
|
||||
throw new Error(
|
||||
'FATAL: Unexpected default file format in `post-install.js#createDefaultFiles()`.',
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(color.red(`FATAL: Could not write default file: ${file}`), error);
|
||||
console.error(
|
||||
color.red(
|
||||
`FATAL: Could not write default ${defaultItem.type}: ${defaultItem.productionPath}`,
|
||||
),
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
12
server.js
12
server.js
@ -67,6 +67,7 @@ import {
|
||||
forwardFetchResponse,
|
||||
removeColorFormatting,
|
||||
getSeparator,
|
||||
safeReadFileSync,
|
||||
} from './src/util.js';
|
||||
import { UPLOADS_DIRECTORY } from './src/constants.js';
|
||||
import { ensureThumbnailCache } from './src/endpoints/thumbnails.js';
|
||||
@ -921,6 +922,16 @@ async function verifySecuritySettings() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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('./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(dataRoot)
|
||||
.then(ensurePublicDirectoriesExist)
|
||||
@ -928,4 +939,5 @@ initUserStorage(dataRoot)
|
||||
.then(migrateSystemPrompts)
|
||||
.then(verifySecuritySettings)
|
||||
.then(preSetupTasks)
|
||||
.then(apply404Middleware)
|
||||
.finally(startServer);
|
||||
|
@ -5,14 +5,15 @@
|
||||
import { Buffer } from 'node:buffer';
|
||||
import storage from 'node-persist';
|
||||
import { getAllUserHandles, toKey, getPasswordHash } from '../users.js';
|
||||
import { getConfig, getConfigValue } from '../util.js';
|
||||
import { getConfig, getConfigValue, safeReadFileSync } from '../util.js';
|
||||
|
||||
const PER_USER_BASIC_AUTH = getConfigValue('perUserBasicAuth', false);
|
||||
const ENABLE_ACCOUNTS = getConfigValue('enableUserAccounts', false);
|
||||
|
||||
const unauthorizedWebpage = safeReadFileSync('./public/error/unauthorized.html') ?? '';
|
||||
const unauthorizedResponse = (res) => {
|
||||
res.set('WWW-Authenticate', 'Basic realm="SillyTavern", charset="UTF-8"');
|
||||
return res.status(401).send('Authentication required');
|
||||
return res.status(401).send(unauthorizedWebpage);
|
||||
};
|
||||
|
||||
const basicAuthMiddleware = async function (request, response, callback) {
|
||||
|
@ -1,15 +1,19 @@
|
||||
import path from 'node:path';
|
||||
import fs from 'node:fs';
|
||||
import process from 'node:process';
|
||||
import Handlebars from 'handlebars';
|
||||
import ipMatching from 'ip-matching';
|
||||
|
||||
import { getIpFromRequest } from '../express-common.js';
|
||||
import { color, getConfigValue } from '../util.js';
|
||||
import { color, getConfigValue, safeReadFileSync } from '../util.js';
|
||||
|
||||
const whitelistPath = path.join(process.cwd(), './whitelist.txt');
|
||||
const enableForwardedWhitelist = getConfigValue('enableForwardedWhitelist', false);
|
||||
let whitelist = getConfigValue('whitelist', []);
|
||||
let knownIPs = new Set();
|
||||
const forbiddenWebpage = Handlebars.compile(
|
||||
safeReadFileSync('./public/error/forbidden-by-whitelist.html') ?? '',
|
||||
);
|
||||
|
||||
if (fs.existsSync(whitelistPath)) {
|
||||
try {
|
||||
@ -55,9 +59,9 @@ export default function whitelistMiddleware(whitelistMode, listen) {
|
||||
return function (req, res, next) {
|
||||
const clientIp = getIpFromRequest(req);
|
||||
const forwardedIp = getForwardedIp(req);
|
||||
const userAgent = req.headers['user-agent'];
|
||||
|
||||
if (listen && !knownIPs.has(clientIp)) {
|
||||
const userAgent = req.headers['user-agent'];
|
||||
console.log(color.yellow(`New connection from ${clientIp}; User Agent: ${userAgent}\n`));
|
||||
knownIPs.add(clientIp);
|
||||
|
||||
@ -76,9 +80,15 @@ export default function whitelistMiddleware(whitelistMode, listen) {
|
||||
|| forwardedIp && whitelistMode === true && !whitelist.some(x => ipMatching.matches(forwardedIp, ipMatching.getMatch(x)))
|
||||
) {
|
||||
// Log the connection attempt with real IP address
|
||||
const ipDetails = forwardedIp ? `${clientIp} (forwarded from ${forwardedIp})` : clientIp;
|
||||
console.log(color.red('Forbidden: Connection attempt from ' + ipDetails + '. If you are attempting to connect, please add your IP address in whitelist or disable whitelist mode in config.yaml in root of SillyTavern folder.\n'));
|
||||
return res.status(403).send('<b>Forbidden</b>: Connection attempt from <b>' + ipDetails + '</b>. If you are attempting to connect, please add your IP address in whitelist or disable whitelist mode in config.yaml in root of SillyTavern folder.');
|
||||
const ipDetails = forwardedIp
|
||||
? `${clientIp} (forwarded from ${forwardedIp})`
|
||||
: clientIp;
|
||||
console.log(
|
||||
color.red(
|
||||
`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`,
|
||||
),
|
||||
);
|
||||
return res.status(403).send(forbiddenWebpage({ ipDetails }));
|
||||
}
|
||||
next();
|
||||
};
|
||||
|
11
src/util.js
11
src/util.js
@ -871,3 +871,14 @@ export class MemoryLimitedMap {
|
||||
return this.map[Symbol.iterator]();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A 'safe' version of `fs.readFileSync()`. Returns the contents of a file if it exists, falling back to a default value if not.
|
||||
* @param {string} filePath Path of the file to be read.
|
||||
* @param {Parameters<typeof fs.readFileSync>[1]} options Options object to pass through to `fs.readFileSync()` (default: `{ encoding: 'utf-8' }`).
|
||||
* @returns The contents at `filePath` if it exists, or `null` if not.
|
||||
*/
|
||||
export function safeReadFileSync(filePath, options = { encoding: 'utf-8' }) {
|
||||
if (fs.existsSync(filePath)) return fs.readFileSync(filePath, options);
|
||||
return null;
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user