Download src/server/services/h5AccessService.ts from chenbhao/codev: direct link, hf CLI and curl.
- Browser
- Download file 12.2 kB
-
https://huggingface.co/chenbhao/codev/resolve/afbc7edaae4e6bb0102ba024b49ce8be89d2264c/src/server/services/h5AccessService.ts
- Command line
-
hf download hf://chenbhao/codev@afbc7edaae4e6bb0102ba024b49ce8be89d2264c/src/server/services/h5AccessService.ts
-
curl -L -o h5AccessService.ts https://huggingface.co/chenbhao/codev/resolve/afbc7edaae4e6bb0102ba024b49ce8be89d2264c/src/server/services/h5AccessService.ts
12.2 kB
| import { createHash, randomBytes } from 'node:crypto' | |
| import os from 'node:os' | |
| import { ApiError } from '../middleware/errorHandler.js' | |
| import { ManagedSettingsService } from './managedSettingsService.js' | |
| import { ProviderService } from './providerService.js' | |
| export type H5AccessSettings = { | |
| enabled: boolean | |
| tokenPreview: string | null | |
| allowedOrigins: string[] | |
| publicBaseUrl: string | null | |
| } | |
| export type H5AccessEnableResult = { | |
| settings: H5AccessSettings | |
| token: string | |
| } | |
| type StoredH5AccessSettings = H5AccessSettings & { | |
| tokenHash: string | null | |
| } | |
| const DEFAULT_STORED_SETTINGS: StoredH5AccessSettings = { | |
| enabled: false, | |
| tokenHash: null, | |
| tokenPreview: null, | |
| allowedOrigins: [], | |
| publicBaseUrl: null, | |
| } | |
| const TOKEN_HASH_RE = /^[a-f0-9]{64}$/ | |
| function isRecord(value: unknown): value is Record<string, unknown> { | |
| return !!value && typeof value === 'object' && !Array.isArray(value) | |
| } | |
| function toPublicSettings(settings: StoredH5AccessSettings): H5AccessSettings { | |
| return { | |
| enabled: settings.enabled, | |
| tokenPreview: settings.tokenPreview, | |
| allowedOrigins: settings.allowedOrigins, | |
| publicBaseUrl: resolveEffectiveH5PublicBaseUrl({ | |
| enabled: settings.enabled, | |
| storedPublicBaseUrl: settings.publicBaseUrl, | |
| configuredPublicBaseUrl: resolveConfiguredPublicBaseUrl(), | |
| autoPublicBaseUrl: resolveAutoLanPublicBaseUrl(), | |
| }), | |
| } | |
| } | |
| function hashToken(token: string): string { | |
| return createHash('sha256').update(token).digest('hex') | |
| } | |
| function createToken(): string { | |
| return `h5_${randomBytes(32).toString('base64url')}` | |
| } | |
| function createTokenPreview(token: string): string { | |
| return `${token.slice(0, 7)}...${token.slice(-4)}` | |
| } | |
| function normalizeOriginInput(origin: string, fieldName = 'allowedOrigins'): string { | |
| if (origin.includes('*')) { | |
| throw ApiError.badRequest(`${fieldName} must not contain wildcard origins`) | |
| } | |
| let parsed: URL | |
| try { | |
| parsed = new URL(origin) | |
| } catch { | |
| throw ApiError.badRequest(`Invalid origin: ${origin}`) | |
| } | |
| if (!['http:', 'https:'].includes(parsed.protocol)) { | |
| throw ApiError.badRequest(`Invalid origin protocol: ${origin}`) | |
| } | |
| if (parsed.username || parsed.password) { | |
| throw ApiError.badRequest(`Invalid origin credentials: ${origin}`) | |
| } | |
| return parsed.origin | |
| } | |
| function normalizeAllowedOrigins(input: unknown): string[] { | |
| if (!Array.isArray(input)) { | |
| throw ApiError.badRequest('allowedOrigins must be an array of strings') | |
| } | |
| const normalized = input.map((origin) => { | |
| if (typeof origin !== 'string') { | |
| throw ApiError.badRequest('allowedOrigins must be an array of strings') | |
| } | |
| return normalizeOriginInput(origin) | |
| }) | |
| return [...new Set(normalized)] | |
| } | |
| function normalizePublicBaseUrl(input: unknown): string | null { | |
| if (input === null || input === undefined || input === '') { | |
| return null | |
| } | |
| if (typeof input !== 'string') { | |
| throw ApiError.badRequest('publicBaseUrl must be a string or null') | |
| } | |
| let parsed: URL | |
| try { | |
| parsed = new URL(input) | |
| } catch { | |
| throw ApiError.badRequest(`Invalid publicBaseUrl: ${input}`) | |
| } | |
| if (!['http:', 'https:'].includes(parsed.protocol)) { | |
| throw ApiError.badRequest(`Invalid publicBaseUrl protocol: ${input}`) | |
| } | |
| if (parsed.username || parsed.password) { | |
| throw ApiError.badRequest(`Invalid publicBaseUrl credentials: ${input}`) | |
| } | |
| const normalizedPath = parsed.pathname.replace(/\/+$/, '') | |
| return `${parsed.origin}${normalizedPath === '/' ? '' : normalizedPath}` | |
| } | |
| function resolveConfiguredPublicBaseUrl(): string | null { | |
| const configured = process.env.CLAUDE_H5_PUBLIC_BASE_URL | |
| if (configured) { | |
| try { | |
| return normalizePublicBaseUrl(configured) | |
| } catch { | |
| return null | |
| } | |
| } | |
| return null | |
| } | |
| function resolveAutoLanPublicBaseUrl(): string | null { | |
| if (process.env.CLAUDE_H5_AUTO_PUBLIC_URL !== '1') { | |
| return null | |
| } | |
| const host = findPrivateLanAddress() | |
| if (!host) { | |
| return null | |
| } | |
| return `http://${host}:${ProviderService.getServerPort()}` | |
| } | |
| export function resolveEffectiveH5PublicBaseUrl({ | |
| enabled, | |
| storedPublicBaseUrl, | |
| configuredPublicBaseUrl, | |
| autoPublicBaseUrl, | |
| }: { | |
| enabled: boolean | |
| storedPublicBaseUrl: string | null | |
| configuredPublicBaseUrl: string | null | |
| autoPublicBaseUrl: string | null | |
| }): string | null { | |
| if (!enabled) { | |
| return storedPublicBaseUrl | |
| } | |
| if (configuredPublicBaseUrl) { | |
| return configuredPublicBaseUrl | |
| } | |
| if (!autoPublicBaseUrl) { | |
| return storedPublicBaseUrl | |
| } | |
| if (!storedPublicBaseUrl || isLocalPublicBaseUrl(storedPublicBaseUrl)) { | |
| return autoPublicBaseUrl | |
| } | |
| return storedPublicBaseUrl | |
| } | |
| function isLocalPublicBaseUrl(value: string): boolean { | |
| try { | |
| const hostname = new URL(value).hostname | |
| .trim() | |
| .replace(/^\[/, '') | |
| .replace(/\]$/, '') | |
| .toLowerCase() | |
| return isLocalHost(hostname) | |
| } catch { | |
| return false | |
| } | |
| } | |
| function isLocalHost(hostname: string): boolean { | |
| return hostname === 'localhost' || | |
| hostname === '127.0.0.1' || | |
| hostname === '::1' || | |
| hostname === '0.0.0.0' || | |
| hostname === '::' | |
| } | |
| type NetworkInterfaces = ReturnType<typeof os.networkInterfaces> | |
| const PHYSICAL_INTERFACE_RE = /\b(wi-?fi|wlan|wireless|ethernet|lan|en\d+|eth\d+)\b/i | |
| const VIRTUAL_INTERFACE_RE = /\b(wsl|docker|hyper-?v|veth|vethernet|virtual|virtualbox|vmware|podman|container|bridge|br-|tailscale|zerotier|utun|vpn)\b/i | |
| export function findPrivateLanAddress(networkInterfaces: NetworkInterfaces = os.networkInterfaces()): string | null { | |
| const candidates: Array<{ | |
| address: string | |
| interfaceName: string | |
| index: number | |
| score: number | |
| }> = [] | |
| let index = 0 | |
| for (const [interfaceName, entries] of Object.entries(networkInterfaces)) { | |
| for (const entry of entries ?? []) { | |
| if (entry.family !== 'IPv4' || entry.internal || !isPrivateIPv4(entry.address)) { | |
| continue | |
| } | |
| candidates.push({ | |
| address: entry.address, | |
| interfaceName, | |
| index, | |
| score: scoreLanAddressCandidate(interfaceName, entry.address), | |
| }) | |
| index += 1 | |
| } | |
| } | |
| candidates.sort((a, b) => b.score - a.score || a.index - b.index) | |
| return candidates[0]?.address ?? null | |
| } | |
| function scoreLanAddressCandidate(interfaceName: string, address: string): number { | |
| let score = 0 | |
| if (PHYSICAL_INTERFACE_RE.test(interfaceName)) { | |
| score += 100 | |
| } | |
| if (VIRTUAL_INTERFACE_RE.test(interfaceName)) { | |
| score -= 200 | |
| } | |
| if (address.startsWith('192.168.')) { | |
| score += 30 | |
| } else if (address.startsWith('10.')) { | |
| score += 20 | |
| } else if (is172PrivateIPv4(address)) { | |
| score += 10 | |
| } else if (address.startsWith('169.254.')) { | |
| score -= 100 | |
| } | |
| return score | |
| } | |
| function isPrivateIPv4(address: string): boolean { | |
| const parts = address.split('.') | |
| if (parts.length !== 4 || !parts.every((part) => /^\d+$/.test(part))) { | |
| return false | |
| } | |
| const [a = -1, b = -1] = parts.map((part) => Number(part)) | |
| return ( | |
| a === 10 || | |
| is172PrivateIPv4(address) || | |
| (a === 192 && b === 168) || | |
| (a === 169 && b === 254) | |
| ) | |
| } | |
| function is172PrivateIPv4(address: string): boolean { | |
| const parts = address.split('.') | |
| if (parts.length !== 4 || !parts.every((part) => /^\d+$/.test(part))) { | |
| return false | |
| } | |
| const [a = -1, b = -1] = parts.map((part) => Number(part)) | |
| return a === 172 && b >= 16 && b <= 31 | |
| } | |
| function normalizeStoredSettings(value: unknown): StoredH5AccessSettings { | |
| if (!isRecord(value)) { | |
| return { ...DEFAULT_STORED_SETTINGS } | |
| } | |
| const allowedOrigins = Array.isArray(value.allowedOrigins) | |
| ? [...new Set(value.allowedOrigins.flatMap((origin) => { | |
| if (typeof origin !== 'string') { | |
| return [] | |
| } | |
| try { | |
| return [normalizeOriginInput(origin)] | |
| } catch { | |
| return [] | |
| } | |
| }))] | |
| : [] | |
| let publicBaseUrl: string | null = null | |
| if (typeof value.publicBaseUrl === 'string') { | |
| try { | |
| publicBaseUrl = normalizePublicBaseUrl(value.publicBaseUrl) | |
| } catch { | |
| publicBaseUrl = null | |
| } | |
| } | |
| const tokenHash = typeof value.tokenHash === 'string' && TOKEN_HASH_RE.test(value.tokenHash) | |
| ? value.tokenHash | |
| : null | |
| return { | |
| enabled: value.enabled === true && tokenHash !== null, | |
| tokenHash, | |
| tokenPreview: tokenHash && typeof value.tokenPreview === 'string' ? value.tokenPreview : null, | |
| allowedOrigins, | |
| publicBaseUrl, | |
| } | |
| } | |
| export class H5AccessService { | |
| private managedSettingsService = new ManagedSettingsService() | |
| private async readStoredSettings(): Promise<{ | |
| managedSettings: Record<string, unknown> | |
| h5Access: StoredH5AccessSettings | |
| }> { | |
| const managedSettings = await this.managedSettingsService.readSettings() | |
| return { | |
| managedSettings, | |
| h5Access: normalizeStoredSettings(managedSettings.h5Access), | |
| } | |
| } | |
| private async setToken( | |
| managedSettings: Record<string, unknown>, | |
| current: StoredH5AccessSettings, | |
| ): Promise<{ | |
| settings: Record<string, unknown> | |
| result: H5AccessEnableResult | |
| }> { | |
| const token = createToken() | |
| const nextSettings: StoredH5AccessSettings = { | |
| ...current, | |
| enabled: true, | |
| tokenHash: hashToken(token), | |
| tokenPreview: createTokenPreview(token), | |
| } | |
| return { | |
| settings: { | |
| ...managedSettings, | |
| h5Access: nextSettings, | |
| }, | |
| result: { | |
| settings: toPublicSettings(nextSettings), | |
| token, | |
| }, | |
| } | |
| } | |
| async getSettings(): Promise<H5AccessSettings> { | |
| const { h5Access } = await this.readStoredSettings() | |
| return toPublicSettings(h5Access) | |
| } | |
| async enable(): Promise<H5AccessEnableResult> { | |
| return this.managedSettingsService.updateSettings(async (current) => { | |
| return this.setToken(current, normalizeStoredSettings(current.h5Access)) | |
| }) | |
| } | |
| async disable(): Promise<H5AccessSettings> { | |
| return this.managedSettingsService.updateSettings(async (current) => { | |
| const h5Access = normalizeStoredSettings(current.h5Access) | |
| const nextSettings: StoredH5AccessSettings = { | |
| ...h5Access, | |
| enabled: false, | |
| tokenHash: null, | |
| tokenPreview: null, | |
| } | |
| return { | |
| settings: { | |
| ...current, | |
| h5Access: nextSettings, | |
| }, | |
| result: toPublicSettings(nextSettings), | |
| } | |
| }) | |
| } | |
| async regenerateToken(): Promise<H5AccessEnableResult> { | |
| return this.managedSettingsService.updateSettings(async (current) => { | |
| return this.setToken(current, normalizeStoredSettings(current.h5Access)) | |
| }) | |
| } | |
| async updateSettings(input: { | |
| allowedOrigins?: string[] | |
| publicBaseUrl?: string | null | |
| }): Promise<H5AccessSettings> { | |
| return this.managedSettingsService.updateSettings(async (current) => { | |
| const h5Access = normalizeStoredSettings(current.h5Access) | |
| const nextSettings: StoredH5AccessSettings = { | |
| ...h5Access, | |
| allowedOrigins: input.allowedOrigins === undefined | |
| ? h5Access.allowedOrigins | |
| : normalizeAllowedOrigins(input.allowedOrigins), | |
| publicBaseUrl: input.publicBaseUrl === undefined | |
| ? h5Access.publicBaseUrl | |
| : normalizePublicBaseUrl(input.publicBaseUrl), | |
| } | |
| return { | |
| settings: { | |
| ...current, | |
| h5Access: nextSettings, | |
| }, | |
| result: toPublicSettings(nextSettings), | |
| } | |
| }) | |
| } | |
| async validateToken(token: string | null | undefined): Promise<boolean> { | |
| if (!token) { | |
| return false | |
| } | |
| const { h5Access } = await this.readStoredSettings() | |
| if (!h5Access.enabled || !h5Access.tokenHash) { | |
| return false | |
| } | |
| return hashToken(token) === h5Access.tokenHash | |
| } | |
| async isOriginAllowed(origin: string | null | undefined): Promise<boolean> { | |
| if (!origin) { | |
| return false | |
| } | |
| const { h5Access } = await this.readStoredSettings() | |
| if (!h5Access.enabled) { | |
| return false | |
| } | |
| try { | |
| const normalizedOrigin = normalizeOriginInput(origin, 'origin') | |
| return h5Access.allowedOrigins.includes(normalizedOrigin) | |
| } catch { | |
| return false | |
| } | |
| } | |
| } | |