190 lines
6.6 KiB
TypeScript
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 }
|
|
);
|
|
}
|
|
}
|