mirror of
https://github.com/MarSeventh/CloudFlare-ImgBed.git
synced 2026-02-01 22:47:37 +00:00
Feat:后端接口跨域请求处理逻辑优化
This commit is contained in:
@@ -84,31 +84,48 @@ function BadRequestException(reason) {
|
||||
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 == ""){
|
||||
if (typeof basicUser == "undefined" || basicUser == null || basicUser == "") {
|
||||
// 无需身份验证
|
||||
return context.next();
|
||||
}else{
|
||||
} else {
|
||||
|
||||
if (context.request.headers.has('Authorization')) {
|
||||
// 首先尝试使用API Token验证
|
||||
@@ -123,29 +140,29 @@ async function authentication(context) {
|
||||
// Token验证通过,继续处理请求
|
||||
return context.next();
|
||||
}
|
||||
|
||||
|
||||
// 回退到使用传统身份认证方式
|
||||
const { user, pass } = basicAuthentication(context.request);
|
||||
const { user, pass } = basicAuthentication(context.request);
|
||||
if (basicUser !== user || basicPass !== pass) {
|
||||
return UnauthorizedException('Invalid credentials.');
|
||||
}else{
|
||||
} 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',
|
||||
// Prompts the user for credentials.
|
||||
'WWW-Authenticate': 'Basic realm="my scope", charset="UTF-8"',
|
||||
// 'WWW-Authenticate': 'None',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export const onRequest = [checkDatabaseConfig, errorHandling, authentication];
|
||||
@@ -3,6 +3,14 @@ import { purgeCFCache } from "../../../utils/purgeCache";
|
||||
import { removeFileFromIndex, batchRemoveFilesFromIndex } from "../../../utils/indexManager.js";
|
||||
import { getDatabase } from '../../../utils/databaseAdapter.js';
|
||||
|
||||
// CORS 跨域响应头
|
||||
const corsHeaders = {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'GET, DELETE, OPTIONS',
|
||||
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
|
||||
'Access-Control-Max-Age': '86400',
|
||||
};
|
||||
|
||||
export async function onRequest(context) {
|
||||
const { request, env, params, waitUntil } = context;
|
||||
|
||||
@@ -23,7 +31,7 @@ export async function onRequest(context) {
|
||||
|
||||
while (folderQueue.length > 0) {
|
||||
const currentFolder = folderQueue.shift();
|
||||
|
||||
|
||||
// 获取指定目录下的所有文件
|
||||
const listUrl = new URL(`${url.origin}/api/manage/list?count=-1&dir=${currentFolder.path}`);
|
||||
const listRequest = new Request(listUrl, request);
|
||||
@@ -59,18 +67,22 @@ export async function onRequest(context) {
|
||||
waitUntil(batchRemoveFilesFromIndex(context, deletedFiles));
|
||||
}
|
||||
|
||||
// 返回处理结果
|
||||
return new Response(JSON.stringify({
|
||||
success: true,
|
||||
deleted: deletedFiles,
|
||||
failed: failedFiles
|
||||
}));
|
||||
}), {
|
||||
headers: { 'Content-Type': 'application/json', ...corsHeaders }
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
return new Response(JSON.stringify({
|
||||
success: false,
|
||||
error: e.message
|
||||
}), { status: 400 });
|
||||
}), {
|
||||
status: 400,
|
||||
headers: { 'Content-Type': 'application/json', ...corsHeaders }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,12 +104,17 @@ export async function onRequest(context) {
|
||||
return new Response(JSON.stringify({
|
||||
success: true,
|
||||
fileId: fileId
|
||||
}));
|
||||
}), {
|
||||
headers: { 'Content-Type': 'application/json', ...corsHeaders }
|
||||
});
|
||||
} catch (e) {
|
||||
return new Response(JSON.stringify({
|
||||
success: false,
|
||||
error: e.message
|
||||
}), { status: 400 });
|
||||
}), {
|
||||
status: 400,
|
||||
headers: { 'Content-Type': 'application/json', ...corsHeaders }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,7 +148,7 @@ async function deleteFile(env, fileId, cdnUrl, url) {
|
||||
const nullResponse = new Response(null, {
|
||||
headers: { 'Cache-Control': 'max-age=0' },
|
||||
});
|
||||
|
||||
|
||||
const normalizedFolder = fileId.split('/').slice(0, -1).join('/');
|
||||
await cache.put(`${url.origin}/api/randomFileList?dir=${normalizedFolder}`, nullResponse);
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,7 +1,17 @@
|
||||
import { readIndex, mergeOperationsToIndex, deleteAllOperations, rebuildIndex,
|
||||
getIndexInfo, getIndexStorageStats } from '../../utils/indexManager.js';
|
||||
import {
|
||||
readIndex, mergeOperationsToIndex, deleteAllOperations, rebuildIndex,
|
||||
getIndexInfo, getIndexStorageStats
|
||||
} from '../../utils/indexManager.js';
|
||||
import { getDatabase } from '../../utils/databaseAdapter.js';
|
||||
|
||||
// CORS 跨域响应头
|
||||
const corsHeaders = {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'GET, OPTIONS',
|
||||
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
|
||||
'Access-Control-Max-Age': '86400',
|
||||
};
|
||||
|
||||
export async function onRequest(context) {
|
||||
const { request, waitUntil } = context;
|
||||
const url = new URL(request.url);
|
||||
@@ -44,7 +54,7 @@ export async function onRequest(context) {
|
||||
}));
|
||||
|
||||
return new Response('Index rebuilt asynchronously', {
|
||||
headers: { "Content-Type": "text/plain" }
|
||||
headers: { "Content-Type": "text/plain", ...corsHeaders }
|
||||
});
|
||||
}
|
||||
|
||||
@@ -53,7 +63,7 @@ export async function onRequest(context) {
|
||||
waitUntil(mergeOperationsToIndex(context));
|
||||
|
||||
return new Response('Operations merged into index asynchronously', {
|
||||
headers: { "Content-Type": "text/plain" }
|
||||
headers: { "Content-Type": "text/plain", ...corsHeaders }
|
||||
});
|
||||
}
|
||||
|
||||
@@ -62,7 +72,7 @@ export async function onRequest(context) {
|
||||
waitUntil(deleteAllOperations(context));
|
||||
|
||||
return new Response('All operations deleted asynchronously', {
|
||||
headers: { "Content-Type": "text/plain" }
|
||||
headers: { "Content-Type": "text/plain", ...corsHeaders }
|
||||
});
|
||||
}
|
||||
|
||||
@@ -70,7 +80,7 @@ export async function onRequest(context) {
|
||||
if (action === 'index-storage-stats') {
|
||||
const stats = await getIndexStorageStats(context);
|
||||
return new Response(JSON.stringify(stats), {
|
||||
headers: { "Content-Type": "application/json" }
|
||||
headers: { "Content-Type": "application/json", ...corsHeaders }
|
||||
});
|
||||
}
|
||||
|
||||
@@ -78,7 +88,7 @@ export async function onRequest(context) {
|
||||
if (action === 'info') {
|
||||
const info = await getIndexInfo(context);
|
||||
return new Response(JSON.stringify(info), {
|
||||
headers: { "Content-Type": "application/json" }
|
||||
headers: { "Content-Type": "application/json", ...corsHeaders }
|
||||
});
|
||||
}
|
||||
|
||||
@@ -93,12 +103,12 @@ export async function onRequest(context) {
|
||||
excludeTags: excludeTagsArray,
|
||||
countOnly: true
|
||||
});
|
||||
|
||||
return new Response(JSON.stringify({
|
||||
|
||||
return new Response(JSON.stringify({
|
||||
sum: result.totalCount,
|
||||
indexLastUpdated: result.indexLastUpdated
|
||||
indexLastUpdated: result.indexLastUpdated
|
||||
}), {
|
||||
headers: { "Content-Type": "application/json" }
|
||||
headers: { "Content-Type": "application/json", ...corsHeaders }
|
||||
});
|
||||
}
|
||||
|
||||
@@ -118,7 +128,7 @@ export async function onRequest(context) {
|
||||
// 索引读取失败,直接从 KV 中获取所有文件记录
|
||||
if (!result.success) {
|
||||
const dbRecords = await getAllFileRecords(context.env, dir);
|
||||
|
||||
|
||||
return new Response(JSON.stringify({
|
||||
files: dbRecords.files,
|
||||
directories: dbRecords.directories,
|
||||
@@ -127,7 +137,7 @@ export async function onRequest(context) {
|
||||
indexLastUpdated: Date.now(),
|
||||
isIndexedResponse: false // 标记这是来自 KV 的响应
|
||||
}), {
|
||||
headers: { "Content-Type": "application/json" }
|
||||
headers: { "Content-Type": "application/json", ...corsHeaders }
|
||||
});
|
||||
}
|
||||
|
||||
@@ -145,7 +155,7 @@ export async function onRequest(context) {
|
||||
indexLastUpdated: result.indexLastUpdated,
|
||||
isIndexedResponse: true // 标记这是来自索引的响应
|
||||
}), {
|
||||
headers: { "Content-Type": "application/json" }
|
||||
headers: { "Content-Type": "application/json", ...corsHeaders }
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
@@ -155,7 +165,7 @@ export async function onRequest(context) {
|
||||
message: error.message
|
||||
}), {
|
||||
status: 500,
|
||||
headers: { "Content-Type": "application/json" }
|
||||
headers: { "Content-Type": "application/json", ...corsHeaders }
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -197,7 +207,7 @@ async function getAllFileRecords(env, dir) {
|
||||
}
|
||||
|
||||
if (!cursor) break;
|
||||
|
||||
|
||||
// 添加协作点
|
||||
await new Promise(resolve => setTimeout(resolve, 10));
|
||||
}
|
||||
|
||||
@@ -1,3 +1,22 @@
|
||||
import { errorHandling, telemetryData, checkDatabaseConfig } from '../utils/middleware';
|
||||
|
||||
export const onRequest = [checkDatabaseConfig, errorHandling, telemetryData];
|
||||
// CORS 跨域响应头
|
||||
const corsHeaders = {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
||||
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
|
||||
'Access-Control-Max-Age': '86400',
|
||||
};
|
||||
|
||||
// OPTIONS 预检请求处理
|
||||
async function handleOptions(context) {
|
||||
if (context.request.method === 'OPTIONS') {
|
||||
return new Response(null, {
|
||||
status: 204,
|
||||
headers: corsHeaders
|
||||
});
|
||||
}
|
||||
return context.next();
|
||||
}
|
||||
|
||||
export const onRequest = [checkDatabaseConfig, handleOptions, errorHandling, telemetryData];
|
||||
@@ -7,10 +7,11 @@ import { getDatabase } from '../utils/databaseAdapter.js';
|
||||
export function createResponse(body, options = {}) {
|
||||
const defaultHeaders = {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'POST, GET',
|
||||
'Access-Control-Allow-Methods': 'POST, GET, OPTIONS',
|
||||
'Access-Control-Allow-Headers': 'Content-Type, Authorization, authCode',
|
||||
'Access-Control-Max-Age': '86400',
|
||||
};
|
||||
|
||||
|
||||
return new Response(body, {
|
||||
...options,
|
||||
headers: {
|
||||
@@ -36,11 +37,11 @@ export async function getIPAddress(ip) {
|
||||
try {
|
||||
const ipInfo = await fetch(`https://apimobile.meituan.com/locate/v2/ip/loc?rgeo=true&ip=${ip}`);
|
||||
const ipData = await ipInfo.json();
|
||||
|
||||
|
||||
if (ipInfo.ok && ipData.data) {
|
||||
const lng = ipData.data?.lng || 0;
|
||||
const lat = ipData.data?.lat || 0;
|
||||
|
||||
|
||||
// 读取具体地址
|
||||
const addressInfo = await fetch(`https://apimobile.meituan.com/group/v1/city/latlng/${lat},${lng}?tag=0`);
|
||||
const addressData = await addressInfo.json();
|
||||
@@ -72,11 +73,11 @@ export function sanitizeFileName(fileName) {
|
||||
|
||||
// 检查文件扩展名是否有效
|
||||
export function isExtValid(fileExt) {
|
||||
return ['jpeg', 'jpg', 'png', 'gif', 'webp',
|
||||
'mp4', 'mp3', 'ogg',
|
||||
'mp3', 'wav', 'flac', 'aac', 'opus',
|
||||
'doc', 'docx', 'ppt', 'pptx', 'xls', 'xlsx', 'pdf',
|
||||
'txt', 'md', 'json', 'xml', 'html', 'css', 'js', 'ts', 'go', 'java', 'php', 'py', 'rb', 'sh', 'bat', 'cmd', 'ps1', 'psm1', 'psd', 'ai', 'sketch', 'fig', 'svg', 'eps', 'zip', 'rar', '7z', 'tar', 'gz', 'bz2', 'xz', 'apk', 'exe', 'msi', 'dmg', 'iso', 'torrent', 'webp', 'ico', 'svg', 'ttf', 'otf', 'woff', 'woff2', 'eot', 'apk', 'crx', 'xpi', 'deb', 'rpm', 'jar', 'war', 'ear', 'img', 'iso', 'vdi', 'ova', 'ovf', 'qcow2', 'vmdk', 'vhd', 'vhdx', 'pvm', 'dsk', 'hdd', 'bin', 'cue', 'mds', 'mdf', 'nrg', 'ccd', 'cif', 'c2d', 'daa', 'b6t', 'b5t', 'bwt', 'isz', 'isz', 'cdi', 'flp', 'uif', 'xdi', 'sdi'
|
||||
return ['jpeg', 'jpg', 'png', 'gif', 'webp',
|
||||
'mp4', 'mp3', 'ogg',
|
||||
'mp3', 'wav', 'flac', 'aac', 'opus',
|
||||
'doc', 'docx', 'ppt', 'pptx', 'xls', 'xlsx', 'pdf',
|
||||
'txt', 'md', 'json', 'xml', 'html', 'css', 'js', 'ts', 'go', 'java', 'php', 'py', 'rb', 'sh', 'bat', 'cmd', 'ps1', 'psm1', 'psd', 'ai', 'sketch', 'fig', 'svg', 'eps', 'zip', 'rar', '7z', 'tar', 'gz', 'bz2', 'xz', 'apk', 'exe', 'msi', 'dmg', 'iso', 'torrent', 'webp', 'ico', 'svg', 'ttf', 'otf', 'woff', 'woff2', 'eot', 'apk', 'crx', 'xpi', 'deb', 'rpm', 'jar', 'war', 'ear', 'img', 'iso', 'vdi', 'ova', 'ovf', 'qcow2', 'vmdk', 'vhd', 'vhdx', 'pvm', 'dsk', 'hdd', 'bin', 'cue', 'mds', 'mdf', 'nrg', 'ccd', 'cif', 'c2d', 'daa', 'b6t', 'b5t', 'bwt', 'isz', 'isz', 'cdi', 'flp', 'uif', 'xdi', 'sdi'
|
||||
].includes(fileExt);
|
||||
}
|
||||
|
||||
@@ -141,7 +142,7 @@ export async function moderateContent(env, url) {
|
||||
console.error('Moderate Error:', error);
|
||||
// 将不带审查的图片写入数据库
|
||||
label = "None";
|
||||
}
|
||||
}
|
||||
|
||||
return label;
|
||||
}
|
||||
@@ -184,7 +185,7 @@ export async function endUpload(context, fileId, metadata) {
|
||||
const cdnUrl = `https://${url.hostname}/file/${fileId}`;
|
||||
const normalizedFolder = (url.searchParams.get('uploadFolder') || '').replace(/^\/+/, '').replace(/\/{2,}/g, '/').replace(/\/$/, '');
|
||||
await purgeCDNCache(env, cdnUrl, url, normalizedFolder);
|
||||
|
||||
|
||||
// 更新文件索引
|
||||
await addFileToIndex(context, fileId, metadata);
|
||||
}
|
||||
@@ -196,7 +197,7 @@ export function getUploadIp(request) {
|
||||
if (!ip) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
// 处理多个IP地址的情况
|
||||
const ips = ip.split(',').map(i => i.trim());
|
||||
|
||||
@@ -238,10 +239,10 @@ export async function buildUniqueFileId(context, fileName, fileType = 'applicati
|
||||
|
||||
const nameType = url.searchParams.get('uploadNameType') || 'default';
|
||||
const uploadFolder = url.searchParams.get('uploadFolder') || '';
|
||||
const normalizedFolder = uploadFolder
|
||||
? uploadFolder.replace(/^\/+/, '').replace(/\/{2,}/g, '/').replace(/\/$/, '')
|
||||
const normalizedFolder = uploadFolder
|
||||
? uploadFolder.replace(/^\/+/, '').replace(/\/{2,}/g, '/').replace(/\/$/, '')
|
||||
: '';
|
||||
|
||||
|
||||
if (!isExtValid(fileExt)) {
|
||||
fileExt = fileType.split('/').pop();
|
||||
if (fileExt === fileType || fileExt === '' || fileExt === null || fileExt === undefined) {
|
||||
@@ -254,7 +255,7 @@ export async function buildUniqueFileId(context, fileName, fileType = 'applicati
|
||||
|
||||
const unique_index = Date.now() + Math.floor(Math.random() * 10000);
|
||||
let baseId = '';
|
||||
|
||||
|
||||
// 根据命名方式构建基础ID
|
||||
if (nameType === 'index') {
|
||||
baseId = normalizedFolder ? `${normalizedFolder}/${unique_index}.${fileExt}` : `${unique_index}.${fileExt}`;
|
||||
@@ -272,44 +273,44 @@ export async function buildUniqueFileId(context, fileName, fileType = 'applicati
|
||||
} else {
|
||||
baseId = normalizedFolder ? `${normalizedFolder}/${unique_index}_${fileName}` : `${unique_index}_${fileName}`;
|
||||
}
|
||||
|
||||
|
||||
// 检查基础ID是否已存在
|
||||
if (await db.get(baseId) === null) {
|
||||
return baseId;
|
||||
}
|
||||
|
||||
|
||||
// 如果已存在,在文件名后面加上递增编号
|
||||
let counter = 1;
|
||||
while (true) {
|
||||
let duplicateId;
|
||||
|
||||
|
||||
if (nameType === 'index') {
|
||||
const baseName = unique_index;
|
||||
duplicateId = normalizedFolder ?
|
||||
`${normalizedFolder}/${baseName}(${counter}).${fileExt}` :
|
||||
duplicateId = normalizedFolder ?
|
||||
`${normalizedFolder}/${baseName}(${counter}).${fileExt}` :
|
||||
`${baseName}(${counter}).${fileExt}`;
|
||||
} else if (nameType === 'origin') {
|
||||
const nameWithoutExt = fileName.substring(0, fileName.lastIndexOf('.'));
|
||||
const ext = fileName.substring(fileName.lastIndexOf('.'));
|
||||
duplicateId = normalizedFolder ?
|
||||
`${normalizedFolder}/${nameWithoutExt}(${counter})${ext}` :
|
||||
duplicateId = normalizedFolder ?
|
||||
`${normalizedFolder}/${nameWithoutExt}(${counter})${ext}` :
|
||||
`${nameWithoutExt}(${counter})${ext}`;
|
||||
} else {
|
||||
const baseName = `${unique_index}_${fileName}`;
|
||||
const nameWithoutExt = baseName.substring(0, baseName.lastIndexOf('.'));
|
||||
const ext = baseName.substring(baseName.lastIndexOf('.'));
|
||||
duplicateId = normalizedFolder ?
|
||||
`${normalizedFolder}/${nameWithoutExt}(${counter})${ext}` :
|
||||
duplicateId = normalizedFolder ?
|
||||
`${normalizedFolder}/${nameWithoutExt}(${counter})${ext}` :
|
||||
`${nameWithoutExt}(${counter})${ext}`;
|
||||
}
|
||||
|
||||
|
||||
// 检查新ID是否已存在
|
||||
if (await db.get(duplicateId) === null) {
|
||||
return duplicateId;
|
||||
}
|
||||
|
||||
|
||||
counter++;
|
||||
|
||||
|
||||
// 防止无限循环,最多尝试1000次
|
||||
if (counter > 1000) {
|
||||
throw new Error('无法生成唯一的文件ID');
|
||||
@@ -322,7 +323,7 @@ export function selectConsistentChannel(channels, uploadId, loadBalanceEnabled)
|
||||
if (!loadBalanceEnabled || !channels || channels.length === 0) {
|
||||
return channels[0];
|
||||
}
|
||||
|
||||
|
||||
// 使用uploadId的哈希值来选择渠道,确保相同uploadId总是选择相同渠道
|
||||
let hash = 0;
|
||||
for (let i = 0; i < uploadId.length; i++) {
|
||||
@@ -330,7 +331,7 @@ export function selectConsistentChannel(channels, uploadId, loadBalanceEnabled)
|
||||
hash = ((hash << 5) - hash) + char;
|
||||
hash = hash & hash; // 转换为32位整数
|
||||
}
|
||||
|
||||
|
||||
const index = Math.abs(hash) % channels.length;
|
||||
return channels[index];
|
||||
}
|
||||
Reference in New Issue
Block a user