100 lines
3.3 KiB
TypeScript
100 lines
3.3 KiB
TypeScript
import type { H3Event } from 'h3'
|
|
import { createError, getRequestHeaders, getRequestURL } from 'h3'
|
|
|
|
const getFirstForwardedValue = (value: string | undefined): string =>
|
|
value?.split(',')[0]?.trim() || ''
|
|
|
|
/**
|
|
* 安全地获取请求的协议(http 或 https)
|
|
* 正确处理 x-forwarded-proto 可能包含多个值的情况
|
|
* @param event H3Event
|
|
* @returns 'http' | 'https'
|
|
*/
|
|
export function getSafeRequestProtocol(event: H3Event): 'http' | 'https' {
|
|
const headers = getRequestHeaders(event)
|
|
const forwardedProto = getFirstForwardedValue(headers['x-forwarded-proto']?.toString())
|
|
const requestProto = getRequestURL(event).protocol.replace(/:$/, '').toLowerCase()
|
|
|
|
const normalizedForwardedProto = forwardedProto.toLowerCase().replace(/:$/, '')
|
|
// 非法的转发协议值不可信,忽略并回退到请求自身协议,避免误伤全局认证等调用方
|
|
if (normalizedForwardedProto && !['http', 'https'].includes(normalizedForwardedProto)) {
|
|
return requestProto === 'https' ? 'https' : 'http'
|
|
}
|
|
|
|
return (normalizedForwardedProto || requestProto) === 'https' ? 'https' : 'http'
|
|
}
|
|
|
|
/**
|
|
* 判断请求是否安全(即是否为 https)
|
|
* @param event H3Event
|
|
* @returns boolean
|
|
*/
|
|
export function isSecureRequest(event: H3Event): boolean {
|
|
return getSafeRequestProtocol(event) === 'https'
|
|
}
|
|
|
|
export interface OriginInfo {
|
|
origin: string
|
|
protocol: string
|
|
hostname: string
|
|
port: string
|
|
explicitPort: string
|
|
}
|
|
|
|
export function normalizeOrigin(value: string, fallbackProtocol: string): OriginInfo {
|
|
const normalizedValue = value.includes('://') ? value : `${fallbackProtocol}//${value}`
|
|
const url = new URL(normalizedValue)
|
|
const protocol = url.protocol
|
|
|
|
return {
|
|
origin: url.origin,
|
|
protocol,
|
|
hostname: url.hostname,
|
|
port: url.port || (protocol === 'https:' ? '443' : '80'),
|
|
explicitPort: url.port
|
|
}
|
|
}
|
|
|
|
export function isTrustedOrigin(source: OriginInfo, trusted: OriginInfo): boolean {
|
|
// 反代未透传外部协议时,允许同主机 HTTPS 访问按 HTTP 配置的来源
|
|
return (
|
|
source.hostname === trusted.hostname &&
|
|
source.explicitPort === trusted.explicitPort &&
|
|
(source.protocol === trusted.protocol ||
|
|
(source.protocol === 'https:' && trusted.protocol === 'http:'))
|
|
)
|
|
}
|
|
|
|
// 不带方括号的裸 IPv6 地址需补齐方括号才能通过 URL 解析
|
|
const normalizeHostForUrl = (host: string): string =>
|
|
host.includes(':') && !host.startsWith('[') && /^[0-9a-f:]+$/i.test(host) ? `[${host}]` : host
|
|
|
|
/**
|
|
* 获取请求的 Origin (包含协议和主机名)
|
|
* @param event H3Event
|
|
* @returns string 例如: https://example.com
|
|
*/
|
|
export function getRequestOrigin(event: H3Event): string {
|
|
const protocol = getSafeRequestProtocol(event)
|
|
const headers = getRequestHeaders(event)
|
|
const forwardedHost = getFirstForwardedValue(headers['x-forwarded-host']?.toString())
|
|
const host = forwardedHost || headers['host']?.toString() || getRequestURL(event).host
|
|
|
|
try {
|
|
const requestUrl = new URL(`${protocol}://${normalizeHostForUrl(host)}`)
|
|
if (
|
|
requestUrl.origin === 'null' ||
|
|
requestUrl.username ||
|
|
requestUrl.password ||
|
|
requestUrl.pathname !== '/' ||
|
|
requestUrl.search ||
|
|
requestUrl.hash
|
|
) {
|
|
throw new Error('invalid host')
|
|
}
|
|
return requestUrl.origin
|
|
} catch {
|
|
throw createError({ statusCode: 400, message: '请求 Host 无效' })
|
|
}
|
|
}
|