107 lines
3.1 KiB
TypeScript
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)
|
|
}
|