Files
FileHost/server/utils/host-isolation.ts
2026-10-17 16:09:31 +08:00

264 lines
8.2 KiB
TypeScript

import type { H3Event } from 'h3'
import {
getImageBaseUrlConfigured,
getSiteBaseUrlConfigured,
getVideoBaseUrlConfigured,
getFileBaseUrlConfigured,
hostnameFromBaseUrl,
isFileDomainSeparated,
isImageDomainSeparated,
isDomainSeparationActive,
isHideFolderInUrl,
isVideoDomainSeparated
} from './env'
import { validateFileKey, validateImageKey, validateStorageKey, validateVideoKey } from './image-key'
import { requestPathToImageKey } from './serve-image'
const DEV_LOCAL_HOSTS = new Set(['localhost', '127.0.0.1'])
function getIsolationRequestHost(event: H3Event): string {
const requestUrl = getRequestURL(event, {
xForwardedHost: true,
xForwardedProto: true
})
return requestUrl.hostname.toLowerCase()
}
function isDevLocalHost(host: string): boolean {
return process.env.NODE_ENV === 'development' && DEV_LOCAL_HOSTS.has(host)
}
function configuredHostOrNull(loader: () => string | null): string | null {
try {
return loader()
} catch {
return null
}
}
export function getConfiguredSiteHost(event: H3Event): string | null {
return hostnameFromBaseUrl(getSiteBaseUrlConfigured(event))
}
export function getConfiguredImageHost(event: H3Event): string | null {
if (!isImageDomainSeparated(event)) return null
return hostnameFromBaseUrl(getImageBaseUrlConfigured(event))
}
export function getConfiguredVideoHost(event: H3Event): string | null {
if (!isVideoDomainSeparated(event)) return null
return hostnameFromBaseUrl(getVideoBaseUrlConfigured(event))
}
export function getConfiguredFileHost(event: H3Event): string | null {
if (!isFileDomainSeparated(event)) return null
return hostnameFromBaseUrl(getFileBaseUrlConfigured(event))
}
/** 域名分离开启时,拒绝未配置的 Host(开发环境 localhost 例外) */
export function shouldBlockUnknownHostRequest(
event: H3Event,
options?: {
requestHost?: string
separationActive?: boolean
siteHost?: string | null
imageHost?: string | null
videoHost?: string | null
fileHost?: string | null
}
): boolean {
const separationActive = options?.separationActive ?? isDomainSeparationActive(event)
if (!separationActive) return false
const siteHost = options?.siteHost ?? getConfiguredSiteHost(event)
const imageHost = options?.imageHost ?? getConfiguredImageHost(event)
const videoHost = options?.videoHost
?? configuredHostOrNull(() => getConfiguredVideoHost(event))
const fileHost = options?.fileHost
?? configuredHostOrNull(() => getConfiguredFileHost(event))
if (!siteHost || !imageHost) return false
const requestHost = (options?.requestHost ?? getIsolationRequestHost(event)).toLowerCase()
if (
requestHost === siteHost
|| requestHost === imageHost
|| (videoHost && requestHost === videoHost)
|| (fileHost && requestHost === fileHost)
) return false
if (isDevLocalHost(requestHost)) return false
return true
}
function requestKey(
event: H3Event,
options?: {
pathname?: string
hideFolder?: boolean
}
): string | null {
const pathname = options?.pathname ?? getRequestURL(event, {
xForwardedHost: true,
xForwardedProto: true
}).pathname
return requestPathToImageKey(pathname, {
hideFolder: options?.hideFolder ?? isHideFolderInUrl(event)
})
}
function mediaKindForKey(key: string | null): 'image' | 'video' | 'file' | null {
if (!key || !validateStorageKey(key)) return null
if (validateImageKey(key)) return 'image'
if (validateVideoKey(key)) return 'video'
if (validateFileKey(key)) return 'file'
return null
}
export function shouldBlockImageHostRequest(
event: H3Event,
options?: {
method?: string
pathname?: string
requestHost?: string
separationActive?: boolean
imageHost?: string | null
siteHost?: string | null
videoHost?: string | null
fileHost?: string | null
hideFolder?: boolean
}
): boolean {
const separationActive = options?.separationActive ?? isDomainSeparationActive(event)
if (!separationActive) return false
const imageHost = options?.imageHost ?? getConfiguredImageHost(event)
if (!imageHost) return false
const requestHost = (options?.requestHost ?? getIsolationRequestHost(event)).toLowerCase()
if (requestHost !== imageHost) return false
const siteHost = options?.siteHost ?? configuredHostOrNull(() => getConfiguredSiteHost(event))
if (siteHost === imageHost) return false
const method = (options?.method ?? getMethod(event)).toUpperCase()
if (method !== 'GET' && method !== 'HEAD') return true
const kind = mediaKindForKey(requestKey(event, options))
if (kind === 'image') return false
const videoHost = options?.videoHost
?? configuredHostOrNull(() => getConfiguredVideoHost(event))
const fileHost = options?.fileHost
?? configuredHostOrNull(() => getConfiguredFileHost(event))
if (kind === 'video' && (!videoHost || videoHost === imageHost)) return false
if (kind === 'file' && (!fileHost || fileHost === imageHost)) return false
return true
}
function shouldBlockMediaHostRequest(
event: H3Event,
expectedKind: 'video' | 'file',
configuredHost: string | null,
options?: {
method?: string
pathname?: string
requestHost?: string
separationActive?: boolean
hideFolder?: boolean
}
): boolean {
const separationActive = options?.separationActive ?? isDomainSeparationActive(event)
if (!separationActive || !configuredHost) return false
const siteHost = configuredHostOrNull(() => getConfiguredSiteHost(event))
if (siteHost === configuredHost) return false
const requestHost = (options?.requestHost ?? getIsolationRequestHost(event)).toLowerCase()
if (requestHost !== configuredHost) return false
const method = (options?.method ?? getMethod(event)).toUpperCase()
if (method !== 'GET' && method !== 'HEAD') return true
return mediaKindForKey(requestKey(event, options)) !== expectedKind
}
export function shouldBlockVideoHostRequest(
event: H3Event,
options?: {
method?: string
pathname?: string
requestHost?: string
separationActive?: boolean
videoHost?: string | null
hideFolder?: boolean
}
): boolean {
return shouldBlockMediaHostRequest(
event,
'video',
options?.videoHost ?? getConfiguredVideoHost(event),
options
)
}
export function shouldBlockFileHostRequest(
event: H3Event,
options?: {
method?: string
pathname?: string
requestHost?: string
separationActive?: boolean
fileHost?: string | null
hideFolder?: boolean
}
): boolean {
return shouldBlockMediaHostRequest(
event,
'file',
options?.fileHost ?? getConfiguredFileHost(event),
options
)
}
/** 网站域禁止直接访问任何媒体路径(仅对应媒体域可 GET/HEAD) */
export function shouldBlockSiteHostImageRequest(
event: H3Event,
options?: {
method?: string
pathname?: string
requestHost?: string
separationActive?: boolean
siteHost?: string | null
imageHost?: string | null
videoHost?: string | null
fileHost?: string | null
hideFolder?: boolean
}
): boolean {
const separationActive = options?.separationActive ?? isDomainSeparationActive(event)
if (!separationActive) return false
const siteHost = options?.siteHost ?? getConfiguredSiteHost(event)
if (!siteHost) return false
const requestHost = (options?.requestHost ?? getIsolationRequestHost(event)).toLowerCase()
if (requestHost !== siteHost) return false
const method = (options?.method ?? getMethod(event)).toUpperCase()
if (method !== 'GET' && method !== 'HEAD') return false
const kind = mediaKindForKey(requestKey(event, options))
if (!kind) return false
if (kind === 'image') {
const imageHost = options?.imageHost
?? configuredHostOrNull(() => getConfiguredImageHost(event))
return imageHost !== siteHost
}
if (kind === 'video') {
const imageHost = options?.imageHost
?? configuredHostOrNull(() => getConfiguredImageHost(event))
const videoHost = options?.videoHost
?? configuredHostOrNull(() => getConfiguredVideoHost(event))
return (videoHost || imageHost) !== siteHost
}
const imageHost = options?.imageHost
?? configuredHostOrNull(() => getConfiguredImageHost(event))
const fileHost = options?.fileHost
?? configuredHostOrNull(() => getConfiguredFileHost(event))
return (fileHost || imageHost) !== siteHost
}