mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-16 13:03:31 +08:00
177 lines
4.2 KiB
TypeScript
177 lines
4.2 KiB
TypeScript
export type H5RequestKind = 'local-trusted' | 'internal-sdk' | 'h5-browser'
|
|
export type H5RequestContext = {
|
|
clientAddress: string | null
|
|
localAccessTokenConfigured?: boolean
|
|
localAccessAuthorized?: boolean
|
|
internalSdkAuthorized?: boolean
|
|
}
|
|
|
|
const LOCAL_DESKTOP_ORIGINS = new Set(['file://'])
|
|
const PROXY_TRACE_HEADERS = [
|
|
'forwarded',
|
|
'x-forwarded-for',
|
|
'x-forwarded-host',
|
|
'x-forwarded-proto',
|
|
'x-real-ip',
|
|
'via',
|
|
] as const
|
|
|
|
export function normalizeHostname(hostname: string): string {
|
|
return hostname.trim().replace(/^\[/, '').replace(/\]$/, '').toLowerCase()
|
|
}
|
|
|
|
export function isLoopbackHost(hostname: string): boolean {
|
|
const normalized = normalizeHostname(hostname)
|
|
if (normalized.startsWith('::ffff:')) {
|
|
return isLoopbackHost(normalized.slice('::ffff:'.length))
|
|
}
|
|
return normalized === 'localhost' || normalized === '::1' || isLoopbackIPv4(normalized)
|
|
}
|
|
|
|
function isLoopbackIPv4(hostname: string): boolean {
|
|
const parts = hostname.split('.')
|
|
if (parts.length !== 4 || parts[0] !== '127') {
|
|
return false
|
|
}
|
|
|
|
return parts.every((part) => {
|
|
if (!/^\d+$/.test(part)) {
|
|
return false
|
|
}
|
|
|
|
const value = Number(part)
|
|
return value >= 0 && value <= 255
|
|
})
|
|
}
|
|
|
|
function isLoopbackBrowserOrigin(origin: string): boolean {
|
|
let parsed: URL
|
|
try {
|
|
parsed = new URL(origin)
|
|
} catch {
|
|
return false
|
|
}
|
|
|
|
if (!['http:', 'https:'].includes(parsed.protocol)) {
|
|
return false
|
|
}
|
|
|
|
return isLoopbackHost(parsed.hostname)
|
|
}
|
|
|
|
function isLocalDesktopOrNavigationOrigin(origin: string | null): boolean {
|
|
if (!origin) return true
|
|
return LOCAL_DESKTOP_ORIGINS.has(origin) || isLoopbackBrowserOrigin(origin)
|
|
}
|
|
|
|
function hasProxyTraceHeaders(headers: Headers): boolean {
|
|
return PROXY_TRACE_HEADERS.some((header) => headers.has(header))
|
|
}
|
|
|
|
function isLocalTrustedRequest(
|
|
request: Request,
|
|
url: URL,
|
|
context: H5RequestContext,
|
|
origin: string | null,
|
|
): boolean {
|
|
if (context.localAccessTokenConfigured) {
|
|
return context.localAccessAuthorized === true
|
|
}
|
|
|
|
const clientAddress = context.clientAddress
|
|
if (!clientAddress) return false
|
|
if (hasProxyTraceHeaders(request.headers)) return false
|
|
|
|
return isLoopbackHost(clientAddress) &&
|
|
isLoopbackHost(url.hostname) &&
|
|
isLocalDesktopOrNavigationOrigin(origin)
|
|
}
|
|
|
|
function isFilesystemCapabilityPath(pathname: string): boolean {
|
|
return pathname.startsWith('/local-file/') ||
|
|
pathname.startsWith('/preview-fs/')
|
|
}
|
|
|
|
export function classifyH5Request(
|
|
request: Request,
|
|
url: URL,
|
|
context: H5RequestContext,
|
|
): H5RequestKind {
|
|
const origin = request.headers.get('Origin')
|
|
const localTrusted = isLocalTrustedRequest(request, url, context, origin)
|
|
if (isFilesystemCapabilityPath(url.pathname)) {
|
|
return localTrusted ? 'local-trusted' : 'h5-browser'
|
|
}
|
|
|
|
if (url.pathname.startsWith('/sdk/') && (localTrusted || context.internalSdkAuthorized)) {
|
|
return 'internal-sdk'
|
|
}
|
|
|
|
if (localTrusted) {
|
|
return 'local-trusted'
|
|
}
|
|
|
|
return 'h5-browser'
|
|
}
|
|
|
|
export function shouldRequireH5Token({
|
|
request,
|
|
url,
|
|
h5Enabled,
|
|
context,
|
|
}: {
|
|
request: Request
|
|
url: URL
|
|
h5Enabled: boolean
|
|
context: H5RequestContext
|
|
}): boolean {
|
|
if (!h5Enabled) {
|
|
return false
|
|
}
|
|
|
|
if (!isH5BrowserCapabilityPath(url.pathname)) {
|
|
return false
|
|
}
|
|
|
|
return classifyH5Request(request, url, context) === 'h5-browser'
|
|
}
|
|
|
|
export function shouldBlockDisabledH5Access({
|
|
request,
|
|
url,
|
|
h5Enabled,
|
|
explicitAuthRequired,
|
|
context,
|
|
}: {
|
|
request: Request
|
|
url: URL
|
|
h5Enabled: boolean
|
|
explicitAuthRequired: boolean
|
|
context: H5RequestContext
|
|
}): boolean {
|
|
if (h5Enabled || explicitAuthRequired) {
|
|
return false
|
|
}
|
|
|
|
if (!isH5ProtectedCapabilityPath(url.pathname)) {
|
|
return false
|
|
}
|
|
|
|
return classifyH5Request(request, url, context) === 'h5-browser'
|
|
}
|
|
|
|
function isH5ProtectedCapabilityPath(pathname: string): boolean {
|
|
return pathname.startsWith('/api/') ||
|
|
isFilesystemCapabilityPath(pathname) ||
|
|
pathname.startsWith('/proxy/') ||
|
|
pathname.startsWith('/ws/') ||
|
|
pathname.startsWith('/sdk/')
|
|
}
|
|
|
|
function isH5BrowserCapabilityPath(pathname: string): boolean {
|
|
return pathname.startsWith('/api/') ||
|
|
isFilesystemCapabilityPath(pathname) ||
|
|
pathname.startsWith('/proxy/') ||
|
|
pathname.startsWith('/ws/')
|
|
}
|