2026-06-03 18:41:52 +08:00

190 lines
6.6 KiB
TypeScript

import { NextRequest, NextResponse } from 'next/server';
import { getPublicProjectDb } from '../../../../lib/project-auth';
import {
MODULE_PULL_TOKEN_USERNAME,
hashModulePullToken,
verifyModulePullToken,
} from '../../../../lib/module-pull-token';
import { getAuthorizedModuleGrant } from '../../../../lib/module-grants';
export const dynamic = 'force-dynamic';
function extractToken(request: NextRequest): string | null {
const queryToken = request.nextUrl.searchParams.get('token');
if (queryToken) return queryToken;
const authorization = request.headers.get('authorization') || '';
if (authorization.startsWith('Bearer ')) {
return authorization.slice('Bearer '.length).trim();
}
if (authorization.startsWith('Basic ')) {
try {
const decoded = Buffer.from(authorization.slice('Basic '.length), 'base64').toString('utf8');
const separatorIndex = decoded.indexOf(':');
if (separatorIndex < 0) return null;
const username = decoded.slice(0, separatorIndex);
const password = decoded.slice(separatorIndex + 1);
if (username !== MODULE_PULL_TOKEN_USERNAME) return null;
return password || null;
} catch {
return null;
}
}
return null;
}
function extractRepository(request: NextRequest): string | null {
const direct =
request.nextUrl.searchParams.get('repository') ||
request.nextUrl.searchParams.get('name') ||
request.headers.get('x-registry-repository');
if (direct) return direct;
const scope = request.nextUrl.searchParams.get('scope') || request.headers.get('x-registry-scope');
const match = scope?.match(/^repository:([^:]+):pull/);
return match?.[1] || null;
}
function extractRegistryHost(request: NextRequest): string | null {
return request.nextUrl.searchParams.get('registryHost') || request.headers.get('x-registry-host');
}
function isManifestDigestRequest(request: NextRequest, requestedDigest: string | null): boolean {
const referenceType =
request.nextUrl.searchParams.get('referenceType') ||
request.headers.get('x-registry-reference-type') ||
'';
return referenceType === 'manifests' && !!requestedDigest;
}
function extractReferenceType(request: NextRequest): string {
return (
request.nextUrl.searchParams.get('referenceType') ||
request.headers.get('x-registry-reference-type') ||
''
);
}
function extractReference(request: NextRequest): string | null {
return request.nextUrl.searchParams.get('reference') || request.headers.get('x-registry-reference');
}
function wasTokenRecentlyValidated(tokenAudit: { lastValidatedAt?: Date | string | null }, now: Date): boolean {
if (!tokenAudit.lastValidatedAt) return false;
const validatedAt = new Date(tokenAudit.lastValidatedAt).getTime();
return !Number.isNaN(validatedAt) && now.getTime() - validatedAt <= 5 * 60 * 1000;
}
export async function GET(request: NextRequest) {
try {
const token = extractToken(request);
if (!token) {
return NextResponse.json({ success: false, error: '缺少模块拉取凭证' }, { status: 401 });
}
const payload = verifyModulePullToken(token);
if (!payload) {
return NextResponse.json({ success: false, error: '模块拉取凭证无效或已过期' }, { status: 401 });
}
const requestedRepository = extractRepository(request);
if (!requestedRepository || requestedRepository !== payload.repository) {
return NextResponse.json({ success: false, error: '模块仓库不匹配' }, { status: 403 });
}
const requestedRegistryHost = extractRegistryHost(request);
if (requestedRegistryHost && requestedRegistryHost !== payload.registryHost) {
return NextResponse.json({ success: false, error: 'Registry 主机不匹配' }, { status: 403 });
}
const requestedReferenceType = extractReferenceType(request);
const requestedReference = extractReference(request);
if (
requestedReferenceType === 'manifests' &&
requestedReference &&
!requestedReference.startsWith('sha256:') &&
requestedReference !== payload.tag
) {
return NextResponse.json({ success: false, error: '模块 tag 不匹配' }, { status: 403 });
}
const projectDbResult = await getPublicProjectDb(payload.projectCode);
if (!projectDbResult.success) {
return projectDbResult.response;
}
const now = new Date();
const tokenHash = hashModulePullToken(token);
const tokenAudit = await projectDbResult.prisma.modulePullTokenAudit.findFirst({
where: {
tokenHash,
licenseId: payload.licenseId,
activationId: payload.activationId,
moduleId: payload.moduleId,
repository: payload.repository,
digest: payload.digest,
expiresAt: { gt: now },
},
});
if (!tokenAudit) {
return NextResponse.json({ success: false, error: '模块拉取凭证未登记或已过期' }, { status: 403 });
}
const requestedDigest = request.nextUrl.searchParams.get('digest') || request.headers.get('x-registry-digest');
if (
isManifestDigestRequest(request, requestedDigest) &&
payload.digest &&
requestedDigest !== payload.digest &&
!wasTokenRecentlyValidated(tokenAudit, now)
) {
return NextResponse.json({ success: false, error: '模块 digest 不匹配' }, { status: 403 });
}
const activation = await projectDbResult.prisma.activation.findFirst({
where: {
id: payload.activationId,
licenseId: payload.licenseId,
instanceId: payload.instanceId,
isActive: true,
},
include: { license: true },
});
const license = activation?.license;
if (!activation || !license || license.status !== 'active' || (license.expiresAt && license.expiresAt < now)) {
return NextResponse.json({ success: false, error: '许可证不可用' }, { status: 403 });
}
const grant = await getAuthorizedModuleGrant(projectDbResult.prisma, {
id: license.id,
licenseType: license.licenseType,
expiresAt: license.expiresAt,
}, payload.moduleId);
if (!grant) {
return NextResponse.json({ success: false, error: '模块授权已失效' }, { status: 403 });
}
await projectDbResult.prisma.modulePullTokenAudit.updateMany({
where: { tokenHash },
data: { lastValidatedAt: now },
});
return NextResponse.json({
success: true,
moduleId: payload.moduleId,
repository: payload.repository,
digest: payload.digest,
});
} catch (error) {
console.error('Registry 模块拉取鉴权失败:', error);
return NextResponse.json(
{ success: false, error: '服务器内部错误' },
{ status: 500 }
);
}
}