Files
BYW-VoiceHub/server/utils/request-utils.ts
T

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 无效' })
}
}