Files
CloudFlare-ImgBed/functions/api/manage/_middleware.js
2025-12-25 19:51:36 +08:00

168 lines
5.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { fetchSecurityConfig } from "../../utils/sysConfig";
import { checkDatabaseConfig } from "../../utils/middleware";
import { validateApiToken } from "../../utils/tokenValidator";
import { getDatabase } from "../../utils/databaseAdapter.js";
let securityConfig = {}
let basicUser = ""
let basicPass = ""
async function errorHandling(context) {
try {
return await context.next();
} catch (err) {
return new Response(`${err.message}\n${err.stack}`, { status: 500 });
}
}
function basicAuthentication(request) {
const Authorization = request.headers.get('Authorization');
const [scheme, encoded] = Authorization.split(' ');
// The Authorization header must start with Basic, followed by a space.
if (!encoded || scheme !== 'Basic') {
return BadRequestException('Malformed authorization header.');
}
// Decodes the base64 value and performs unicode normalization.
// @see https://datatracker.ietf.org/doc/html/rfc7613#section-3.3.2 (and #section-4.2.2)
// @see https://dev.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String/normalize
const buffer = Uint8Array.from(atob(encoded), character => character.charCodeAt(0));
const decoded = new TextDecoder().decode(buffer).normalize();
// The username & password are split by the first colon.
//=> example: "username:password"
const index = decoded.indexOf(':');
// The user & password are split by the first colon and MUST NOT contain control characters.
// @see https://tools.ietf.org/html/rfc5234#appendix-B.1 (=> "CTL = %x00-1F / %x7F")
if (index === -1 || /[\0-\x1F\x7F]/.test(decoded)) {
return BadRequestException('Invalid authorization value.');
}
return {
user: decoded.substring(0, index),
pass: decoded.substring(index + 1),
};
}
function UnauthorizedException(reason) {
return new Response(reason, {
status: 401,
statusText: 'Unauthorized',
headers: {
'Content-Type': 'text/plain;charset=UTF-8',
// Disables caching by default.
'Cache-Control': 'no-store',
// Returns the "Content-Length" header for HTTP HEAD requests.
'Content-Length': reason.length,
},
});
}
function BadRequestException(reason) {
return new Response(reason, {
status: 400,
statusText: 'Bad Request',
headers: {
'Content-Type': 'text/plain;charset=UTF-8',
// Disables caching by default.
'Cache-Control': 'no-store',
// Returns the "Content-Length" header for HTTP HEAD requests.
'Content-Length': reason.length,
},
});
}
/**
* 根据请求路径提取所需权限
* @param {string} pathname - 请求路径
* @returns {string|null} 需要的权限类型或null
*/
function extractRequiredPermission(pathname) {
// 提取路径中的关键部分
const pathParts = pathname.toLowerCase().split('/');
// 检查是否包含delete路径
if (pathParts.includes('delete')) {
return 'delete';
}
// 检查是否包含list路径
if (pathParts.includes('list')) {
return 'list';
}
// 其他情况返回null
return null;
}
// CORS 跨域响应头
const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, DELETE, PUT, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
'Access-Control-Max-Age': '86400',
};
async function authentication(context) {
// OPTIONS 预检请求不需要鉴权,直接返回 CORS 响应
// 这是安全的,因为 OPTIONS 请求只是预检请求,不会执行任何实际操作
if (context.request.method === 'OPTIONS') {
return new Response(null, {
status: 204,
headers: corsHeaders
});
}
// 读取安全配置
securityConfig = await fetchSecurityConfig(context.env);
basicUser = securityConfig.auth.admin.adminUsername
basicPass = securityConfig.auth.admin.adminPassword
if (typeof basicUser == "undefined" || basicUser == null || basicUser == "") {
// 无需身份验证
return context.next();
} else {
if (context.request.headers.has('Authorization')) {
// 首先尝试使用API Token验证
// 根据请求的 url 判断所需权限
const pathname = new URL(context.request.url).pathname;
const requiredPermission = extractRequiredPermission(pathname);
const db = getDatabase(context.env);
const tokenValidation = await validateApiToken(context.request, db, requiredPermission);
if (tokenValidation.valid) {
// Token验证通过继续处理请求
return context.next();
}
// 回退到使用传统身份认证方式
const { user, pass } = basicAuthentication(context.request);
if (basicUser !== user || basicPass !== pass) {
return UnauthorizedException('Invalid credentials.');
} else {
return context.next();
}
} else {
// 要求客户端进行基本认证
return new Response('You need to login.', {
status: 401,
headers: {
// Prompts the user for credentials.
'WWW-Authenticate': 'Basic realm="my scope", charset="UTF-8"',
// 'WWW-Authenticate': 'None',
},
});
}
}
}
export const onRequest = [checkDatabaseConfig, errorHandling, authentication];