gongxiangp2/lib/cors-origin.ts

107 lines
3.1 KiB
TypeScript

import { getPublicAppOrigin } from '@/lib/public-app-url'
type HeaderAccessor = {
get?: (name: string) => string | null | undefined
}
type RequestLike = {
headers?: HeaderAccessor
nextUrl?: {
origin?: string
protocol?: string
}
url?: string
}
function getFirstHeaderValue(headersLike: HeaderAccessor | undefined, name: string): string {
const raw = headersLike?.get?.(name) || ''
return raw.split(',')[0].trim()
}
function normalizeProtocol(value: string | null | undefined): 'http' | 'https' | null {
const normalized = String(value || '').trim().replace(/:$/, '').toLowerCase()
if (normalized === 'http' || normalized === 'https') {
return normalized
}
return null
}
function resolveForwardedProtocol(headersLike: HeaderAccessor | undefined): 'http' | 'https' | null {
const forwardedProto = normalizeProtocol(getFirstHeaderValue(headersLike, 'x-forwarded-proto'))
if (forwardedProto) {
return forwardedProto
}
const forwardedSsl = getFirstHeaderValue(headersLike, 'x-forwarded-ssl').toLowerCase()
if (forwardedSsl === 'on') {
return 'https'
}
const forwardedPort = getFirstHeaderValue(headersLike, 'x-forwarded-port')
if (forwardedPort === '443') {
return 'https'
}
return null
}
export function normalizeHttpOrigin(value: unknown): string | null {
const raw = String(value ?? '').trim()
if (!raw) return null
try {
const parsed = new URL(raw)
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
return null
}
if (parsed.username || parsed.password || parsed.pathname !== '/' || parsed.search || parsed.hash) {
return null
}
return parsed.origin
} catch {
return null
}
}
export function getRequestHostOrigin(request: RequestLike): string | null {
const host =
getFirstHeaderValue(request.headers, 'host') ||
getFirstHeaderValue(request.headers, 'x-forwarded-host')
if (!host) {
return null
}
const protocol =
resolveForwardedProtocol(request.headers) ||
normalizeProtocol(request.nextUrl?.protocol) ||
normalizeProtocol(request.url ? new URL(request.url).protocol : null) ||
'https'
return normalizeHttpOrigin(`${protocol}://${host}`)
}
export function getAllowedCorsOrigins(request: RequestLike): Set<string> {
const configuredOrigins = String(process.env.CORS_ALLOWED_ORIGINS || '')
.split(',')
.map((item) => normalizeHttpOrigin(item))
.filter((item): item is string => Boolean(item))
const values = [
getPublicAppOrigin(),
getRequestHostOrigin(request),
normalizeHttpOrigin(request.nextUrl?.origin),
normalizeHttpOrigin(process.env.APP_PUBLIC_URL),
normalizeHttpOrigin(process.env.NEXTAUTH_URL),
...configuredOrigins,
]
return new Set(values.filter((item): item is string => Boolean(item)))
}
export function isAllowedCorsOrigin(request: RequestLike, requestOrigin: string | null): boolean {
if (!requestOrigin) return true
const normalizedRequestOrigin = normalizeHttpOrigin(requestOrigin)
if (!normalizedRequestOrigin) return false
return getAllowedCorsOrigins(request).has(normalizedRequestOrigin)
}