Files
BYW-VoiceHub/server/middleware/api-cors.ts
T

136 lines
5.3 KiB
TypeScript

import { isTrustedOrigin, normalizeOrigin } from '~~/server/utils/request-utils'
export default defineEventHandler((event) => {
const requestUrl = getRequestURL(event)
const pathname = requestUrl.pathname
const method = getMethod(event)
// 只处理特定的内部API路由,防止站外调用
const isProtectedApi = pathname.startsWith('/api/api-enhanced/netease') ||
pathname.startsWith('/api/native-api')
if (!isProtectedApi) {
return
}
// 内部受信客户端绕过 CORS 校验
const requestedFrom = getHeader(event, 'x-requested-from')
if (requestedFrom === 'ClassIslandPlugin') {
return
}
// 显式配置 NUXT_PUBLIC_HOST 时使用配置来源校验。
// 未配置时仍使用 Host 头;云平台等公开 Host 继续校验,仅当反向代理把 Host 改写为回环地址时跳过。
const config = useRuntimeConfig(event)
let configuredHost = typeof config.public?.host === 'string' ? config.public.host.trim() : ''
if (!configuredHost) {
const hostHeader = getHeader(event, 'x-forwarded-host') || getHeader(event, 'host')
if (!hostHeader) {
throw createError({ statusCode: 400, message: 'Bad Request: 缺少Host请求头' })
}
configuredHost = (hostHeader.split(',')[0] || '').trim()
try {
const hostOrigin = normalizeOrigin(configuredHost, requestUrl.protocol)
if (isLoopbackHostname(hostOrigin.hostname)) {
return
}
} catch {
throw createError({ statusCode: 400, message: 'Bad Request: Host请求头无效' })
}
// 如果 x-forwarded-proto 存在,可以带上协议,使校验更精准
const forwardedProto = getHeader(event, 'x-forwarded-proto')
if (forwardedProto && !configuredHost.includes('://')) {
const proto = (forwardedProto.split(',')[0] || '').trim()
configuredHost = `${proto}://${configuredHost}`
}
}
// CORS 限制:禁止站外网站调用
const origin = getHeader(event, 'origin')
const referer = getHeader(event, 'referer')
const secFetchSite = getHeader(event, 'sec-fetch-site')
const secFetchMode = getHeader(event, 'sec-fetch-mode')
const sourceUrl = origin || referer
// Fetch Metadata 由浏览器生成且脚本无法修改,同源请求不应被部署平台改写的 Host 误伤。
if (secFetchSite === 'same-origin') return
if (sourceUrl) {
try {
const sourceOrigin = normalizeOrigin(sourceUrl, requestUrl.protocol)
const trustedOrigin = normalizeOrigin(configuredHost, requestUrl.protocol)
const isSameLoopbackOrigin =
isLoopbackHostname(sourceOrigin.hostname) &&
isLoopbackHostname(trustedOrigin.hostname) &&
sourceOrigin.protocol === trustedOrigin.protocol &&
sourceOrigin.port === trustedOrigin.port
const matchesConfiguredOrigin = isTrustedOrigin(sourceOrigin, trustedOrigin)
const requestHost = getHeader(event, 'x-forwarded-host') || getHeader(event, 'host')
const requestProto = getHeader(event, 'x-forwarded-proto')?.split(',')[0]?.trim() || requestUrl.protocol.replace(':', '')
const hostOrigin = requestHost
? normalizeOrigin(`${requestProto}://${requestHost.split(',')[0].trim()}`, requestUrl.protocol)
: null
const matchesRequestHost = hostOrigin && isTrustedOrigin(sourceOrigin, hostOrigin)
if (!matchesConfiguredOrigin && !matchesRequestHost && !isSameLoopbackOrigin) {
console.warn(`[CORS Middleware] 拦截跨域请求: 来源 ${sourceOrigin.origin}, 期望 ${trustedOrigin.origin}, 路径 ${pathname}`)
throw createError({
statusCode: 403,
message: 'Forbidden: 内部API不允许跨域请求'
})
}
} catch (e: unknown) {
if (
typeof e === 'object' &&
e !== null &&
'statusCode' in e &&
typeof e.statusCode === 'number'
) {
throw e
}
console.warn(`[CORS Middleware] 无效的来源 URL: ${sourceUrl}`, e)
throw createError({
statusCode: 400,
message: 'Bad Request: Origin或Referer头无效'
})
}
} else if (isTrustedFetchMetadata(secFetchSite, secFetchMode, method)) {
// 浏览器隐私策略可能移除来源头,此时 Fetch Metadata 是更稳定的同源信号。
return
} else {
// 没有来源信息且缺少可信 Fetch Metadata 时,仍按站外请求处理。
console.warn(`[CORS Middleware] 拦截无Origin/Referer头的请求: 路径 ${pathname}, sec-fetch-site: ${secFetchSite || 'missing'}`)
throw createError({
statusCode: 403,
message: 'Forbidden: 访问此API必须提供Origin或Referer头'
})
}
})
function isLoopbackHostname(hostname: string): boolean {
return hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '[::1]'
}
function isTrustedFetchMetadata(
secFetchSite: string | undefined,
secFetchMode: string | undefined,
method: string
): boolean {
const normalizedMethod = method.toUpperCase()
const safeMethod = normalizedMethod === 'GET' || normalizedMethod === 'HEAD'
if (secFetchSite === 'same-origin') return true
if (secFetchSite === 'none') {
const isNavigation = !secFetchMode || secFetchMode === 'navigate'
return safeMethod && isNavigation
}
// 兼容不发送 Fetch Metadata 的老浏览器、WebView 或反向代理;跨站请求有明确标记时仍会被拒绝。
return !secFetchSite && safeMethod
}