Blame Raw
Cohee · 51ad27fb · · 87 lines (3.7 KB)
1 contributor
1/**
2 * When applied, this middleware will ensure the request contains the required header for basic authentication and only
3 * allow access to the endpoint after successful authentication.
4 */
5import { Buffer } from 'node:buffer';
6import path from 'node:path';
7import storage from 'node-persist';
8import { RateLimiterMemory, RateLimiterRes } from 'rate-limiter-flexible';
9import { getAllUserHandles, toKey, getPasswordHash } from '../users.js';
10import { getConfigValue, safeReadFileSync } from '../util.js';
11import { getIpAddress, retryAfter } from '../express-common.js';
12
13const PER_USER_BASIC_AUTH = !!getConfigValue('perUserBasicAuth', false, 'boolean');
14const ENABLE_ACCOUNTS = !!getConfigValue('enableUserAccounts', false, 'boolean');
15const PREFER_REAL_IP_HEADER = !!getConfigValue('rateLimiting.preferRealIpHeader', false, 'boolean');
16const BASIC_AUTH_ATTEMPTS = getConfigValue('rateLimiting.basicAuthMaxAttempts', 5, 'number');
17
18const basicAuthLimiter = new RateLimiterMemory({
19 points: BASIC_AUTH_ATTEMPTS > 0 ? BASIC_AUTH_ATTEMPTS : Number.MAX_SAFE_INTEGER,
20 duration: 60,
21});
22
23const basicAuthMiddleware = async function (request, response, callback) {
24 const unauthorizedResponse = (res) => {
25 const unauthorizedWebpage = safeReadFileSync(path.join(globalThis.DATA_ROOT, '_errors', 'unauthorized.html')) ?? '';
26 res.set('WWW-Authenticate', 'Basic realm="SillyTavern", charset="UTF-8"');
27 return res.status(401).send(unauthorizedWebpage);
28 };
29
30 try {
31 const ip = getIpAddress(request, PREFER_REAL_IP_HEADER);
32
33 const basicAuthUserName = getConfigValue('basicAuthUser.username');
34 const basicAuthUserPassword = getConfigValue('basicAuthUser.password');
35 const authHeader = request.headers.authorization;
36
37 if (!authHeader) {
38 return unauthorizedResponse(response);
39 }
40
41 const [scheme, credentials] = authHeader.split(' ');
42
43 if (scheme !== 'Basic' || !credentials) {
44 return unauthorizedResponse(response);
45 }
46
47 const rateLimit = await basicAuthLimiter.get(ip);
48
49 if (rateLimit !== null && rateLimit.consumedPoints > basicAuthLimiter.points) {
50 throw rateLimit;
51 }
52
53 const usePerUserAuth = PER_USER_BASIC_AUTH && ENABLE_ACCOUNTS;
54 const [username, ...passwordParts] = Buffer.from(credentials, 'base64')
55 .toString('utf8')
56 .split(':');
57 const password = passwordParts.join(':');
58
59 if (!usePerUserAuth && username === basicAuthUserName && password === basicAuthUserPassword) {
60 await basicAuthLimiter.delete(ip);
61 return callback();
62 } else if (usePerUserAuth) {
63 const userHandles = await getAllUserHandles();
64 for (const userHandle of userHandles) {
65 if (username === userHandle) {
66 const user = await storage.getItem(toKey(userHandle));
67 if (user && user.enabled && (user.password && user.password === getPasswordHash(password, user.salt))) {
68 await basicAuthLimiter.delete(ip);
69 return callback();
70 }
71 }
72 }
73 }
74
75 await basicAuthLimiter.consume(ip);
76 return unauthorizedResponse(response);
77 } catch (error) {
78 if (error instanceof RateLimiterRes) {
79 console.error('Basic auth failed: Rate limited from', getIpAddress(request, PREFER_REAL_IP_HEADER), request.method, request.originalUrl);
80 return retryAfter(response, error).sendStatus(429);
81 }
82 console.error('Basic auth error:', error);
83 return response.sendStatus(500);
84 }
85};
86
87export default basicAuthMiddleware;