继续完善规则引擎

This commit is contained in:
mtvpls
2026-05-19 22:36:56 +08:00
parent 11f603a10c
commit 8c86243d32
5 changed files with 487 additions and 552 deletions
+39 -3
View File
@@ -1,12 +1,15 @@
import { NextRequest, NextResponse } from 'next/server';
import { legadoClient } from '@/lib/legado.client';
import crypto from 'crypto';
import { validateProxyUrlServerSide } from '@/lib/server/ssrf';
import { getAuthorizedBooksUsername } from '../_utils';
export const runtime = 'nodejs';
const imageCache = new Map<string, { expiresAt: number; contentType: string; data: Uint8Array }>();
function asObjectHeader(value?: string | Record<string, string>): Record<string, string> {
if (!value) return {};
if (typeof value === 'object') return value;
@@ -33,19 +36,52 @@ export async function GET(request: NextRequest) {
if (!sourceId || !url) return NextResponse.json({ error: '缺少 sourceId 或 url' }, { status: 400 });
if (!(await validateProxyUrlServerSide(url))) return NextResponse.json({ error: '图片地址未通过安全校验' }, { status: 400 });
const source = await legadoClient.getSourceById(sourceId);
let imageOptions: any = {};
try {
const rawOptions = searchParams.get('options') || '';
imageOptions = rawOptions ? JSON.parse(rawOptions) : {};
} catch {
imageOptions = {};
}
const headers: Record<string, string> = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0 Safari/537.36',
Referer: source.legado?.bookSourceUrl || source.url,
...asObjectHeader(source.legado?.header),
...(imageOptions.headers && typeof imageOptions.headers === 'object' ? imageOptions.headers : {}),
};
delete headers.Host;
delete headers.host;
const res = await fetch(url, { headers, cache: 'no-store' });
const cacheKey = crypto.createHash('sha1').update(`${sourceId}|${url}|${JSON.stringify(imageOptions)}`).digest('hex');
const cached = imageCache.get(cacheKey);
if (cached && cached.expiresAt > Date.now() && !request.headers.get('range')) {
return new NextResponse(cached.data, { headers: { 'Content-Type': cached.contentType, 'Cache-Control': 'public, max-age=86400' } });
}
const range = request.headers.get('range');
if (range) headers.Range = range;
const res = await fetch(url, { method: imageOptions.method || 'GET', headers, body: imageOptions.body, cache: 'no-store', redirect: 'manual' });
if ([301, 302, 303, 307, 308].includes(res.status)) {
const location = res.headers.get('location');
if (!location) return NextResponse.json({ error: '图片重定向缺少地址' }, { status: 502 });
const redirected = new URL(location, url).toString();
if (!(await validateProxyUrlServerSide(redirected))) return NextResponse.json({ error: '图片重定向地址未通过安全校验' }, { status: 400 });
const second = await fetch(redirected, { method: imageOptions.method || 'GET', headers, body: imageOptions.body, cache: 'no-store' });
if (!second.ok) return NextResponse.json({ error: `图片请求失败: ${second.status}` }, { status: second.status });
const data = new Uint8Array(await second.arrayBuffer());
const contentType = second.headers.get('content-type') || 'image/jpeg';
if (!range) imageCache.set(cacheKey, { data, contentType, expiresAt: Date.now() + 86400_000 });
return new NextResponse(data, { status: second.status, headers: { 'Content-Type': contentType, 'Cache-Control': 'public, max-age=86400' } });
}
if (!res.ok) return NextResponse.json({ error: `图片请求失败: ${res.status}` }, { status: res.status });
return new NextResponse(res.body, {
const data = new Uint8Array(await res.arrayBuffer());
const contentType = res.headers.get('content-type') || 'image/jpeg';
if (!range) imageCache.set(cacheKey, { data, contentType, expiresAt: Date.now() + 86400_000 });
return new NextResponse(data, {
status: res.status,
headers: {
'Content-Type': res.headers.get('content-type') || 'image/jpeg',
'Content-Type': contentType,
'Cache-Control': 'public, max-age=86400',
...(res.headers.get('content-range') ? { 'Content-Range': res.headers.get('content-range') as string } : {}),
...(res.headers.get('accept-ranges') ? { 'Accept-Ranges': res.headers.get('accept-ranges') as string } : {}),
},
});
} catch (error) {