@undefineds.co/xpod 0.3.31 → 0.3.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/auth/AuthContext.d.ts +3 -2
- package/dist/api/auth/AuthContext.js +2 -1
- package/dist/api/auth/AuthContext.js.map +1 -1
- package/dist/api/auth/ClientCredentialsAuthenticator.d.ts +2 -12
- package/dist/api/auth/ClientCredentialsAuthenticator.js +4 -4
- package/dist/api/auth/ClientCredentialsAuthenticator.js.map +1 -1
- package/dist/api/auth/ServiceTokenAuthenticator.d.ts +2 -2
- package/dist/api/auth/ServiceTokenAuthenticator.js.map +1 -1
- package/dist/api/container/business-token.d.ts +1 -1
- package/dist/api/container/business-token.js +5 -1
- package/dist/api/container/business-token.js.map +1 -1
- package/dist/api/container/common.js +14 -10
- package/dist/api/container/common.js.map +1 -1
- package/dist/api/container/routes.js +16 -3
- package/dist/api/container/routes.js.map +1 -1
- package/dist/api/container/types.d.ts +2 -4
- package/dist/api/container/types.js.map +1 -1
- package/dist/api/handlers/ChatHandler.d.ts +1 -1
- package/dist/api/handlers/ChatHandler.js +1 -1
- package/dist/api/handlers/ChatHandler.js.map +1 -1
- package/dist/api/handlers/EdgeNodeSignalHandler.js +3 -1
- package/dist/api/handlers/EdgeNodeSignalHandler.js.map +1 -1
- package/dist/api/handlers/PodManagementHandler.d.ts +2 -0
- package/dist/api/handlers/PodManagementHandler.js +114 -12
- package/dist/api/handlers/PodManagementHandler.js.map +1 -1
- package/dist/api/handlers/ProvisionHandler.d.ts +27 -0
- package/dist/api/handlers/ProvisionHandler.js +339 -32
- package/dist/api/handlers/ProvisionHandler.js.map +1 -1
- package/dist/api/handlers/QuotaHandler.js +0 -12
- package/dist/api/handlers/QuotaHandler.js.map +1 -1
- package/dist/api/handlers/index.d.ts +0 -1
- package/dist/api/handlers/index.js +0 -1
- package/dist/api/handlers/index.js.map +1 -1
- package/dist/api/runtime.js +3 -3
- package/dist/api/runtime.js.map +1 -1
- package/dist/components/context.jsonld +12 -0
- package/dist/edge/EdgeNodeAgent.d.ts +1 -1
- package/dist/edge/EdgeNodeAgent.js +1 -1
- package/dist/edge/EdgeNodeAgent.js.map +1 -1
- package/dist/edge/EdgeNodeDnsCoordinator.d.ts +1 -0
- package/dist/edge/EdgeNodeDnsCoordinator.js +9 -3
- package/dist/edge/EdgeNodeDnsCoordinator.js.map +1 -1
- package/dist/edge/EdgeNodeDnsCoordinator.jsonld +4 -0
- package/dist/edge/EdgeNodeHealthProbeService.d.ts +3 -0
- package/dist/edge/EdgeNodeHealthProbeService.js +22 -2
- package/dist/edge/EdgeNodeHealthProbeService.js.map +1 -1
- package/dist/edge/EdgeNodeHealthProbeService.jsonld +12 -0
- package/dist/http/ClusterIngressRouter.js +6 -3
- package/dist/http/ClusterIngressRouter.js.map +1 -1
- package/dist/http/ClusterWebSocketConfigurator.js +6 -2
- package/dist/http/ClusterWebSocketConfigurator.js.map +1 -1
- package/dist/http/EdgeNodeDirectDebugHttpHandler.d.ts +2 -0
- package/dist/http/EdgeNodeDirectDebugHttpHandler.js +18 -3
- package/dist/http/EdgeNodeDirectDebugHttpHandler.js.map +1 -1
- package/dist/http/EdgeNodeDirectDebugHttpHandler.jsonld +8 -0
- package/dist/http/EdgeNodeProxyHttpHandler.js +6 -2
- package/dist/http/EdgeNodeProxyHttpHandler.js.map +1 -1
- package/dist/http/cluster/PodMigrationHttpHandler.d.ts +2 -2
- package/dist/http/cluster/PodMigrationHttpHandler.js +2 -2
- package/dist/http/cluster/PodMigrationHttpHandler.js.map +1 -1
- package/dist/http/quota/QuotaAdminHttpHandler.js +27 -21
- package/dist/http/quota/QuotaAdminHttpHandler.js.map +1 -1
- package/dist/identity/drizzle/AccountRepository.d.ts +4 -22
- package/dist/identity/drizzle/AccountRepository.js +9 -113
- package/dist/identity/drizzle/AccountRepository.js.map +1 -1
- package/dist/identity/drizzle/AccountRoleRepository.d.ts +5 -5
- package/dist/identity/drizzle/AccountRoleRepository.js +204 -97
- package/dist/identity/drizzle/AccountRoleRepository.js.map +1 -1
- package/dist/identity/drizzle/DdnsRepository.d.ts +5 -20
- package/dist/identity/drizzle/DdnsRepository.js +13 -49
- package/dist/identity/drizzle/DdnsRepository.js.map +1 -1
- package/dist/identity/drizzle/EdgeNodeRepository.d.ts +13 -6
- package/dist/identity/drizzle/EdgeNodeRepository.js +167 -66
- package/dist/identity/drizzle/EdgeNodeRepository.js.map +1 -1
- package/dist/identity/drizzle/PodLookupRepository.d.ts +7 -36
- package/dist/identity/drizzle/PodLookupRepository.js +103 -126
- package/dist/identity/drizzle/PodLookupRepository.js.map +1 -1
- package/dist/identity/drizzle/ServiceTokenRepository.d.ts +13 -1
- package/dist/identity/drizzle/ServiceTokenRepository.js +7 -0
- package/dist/identity/drizzle/ServiceTokenRepository.js.map +1 -1
- package/dist/identity/drizzle/db.d.ts +2 -1
- package/dist/identity/drizzle/db.js +173 -297
- package/dist/identity/drizzle/db.js.map +1 -1
- package/dist/identity/drizzle/schema.pg.d.ts +3 -11
- package/dist/identity/drizzle/schema.pg.js +10 -45
- package/dist/identity/drizzle/schema.pg.js.map +1 -1
- package/dist/identity/drizzle/schema.sqlite.d.ts +88 -531
- package/dist/identity/drizzle/schema.sqlite.js +13 -46
- package/dist/identity/drizzle/schema.sqlite.js.map +1 -1
- package/dist/identity/oidc/ScopedPickWebIdHandler.d.ts +3 -0
- package/dist/identity/oidc/ScopedPickWebIdHandler.js +18 -6
- package/dist/identity/oidc/ScopedPickWebIdHandler.js.map +1 -1
- package/dist/identity/oidc/ScopedPickWebIdHandler.jsonld +22 -0
- package/dist/provision/ProvisionCodeCodec.js +10 -1
- package/dist/provision/ProvisionCodeCodec.js.map +1 -1
- package/dist/provision/ProvisionPodCreator.d.ts +8 -2
- package/dist/provision/ProvisionPodCreator.js +134 -41
- package/dist/provision/ProvisionPodCreator.js.map +1 -1
- package/dist/provision/ProvisionPodCreator.jsonld +38 -3
- package/dist/quota/DrizzleQuotaService.d.ts +0 -4
- package/dist/quota/DrizzleQuotaService.js +1 -21
- package/dist/quota/DrizzleQuotaService.js.map +1 -1
- package/dist/quota/DrizzleQuotaService.jsonld +0 -16
- package/dist/quota/NoopQuotaService.d.ts +0 -4
- package/dist/quota/NoopQuotaService.js +0 -8
- package/dist/quota/NoopQuotaService.js.map +1 -1
- package/dist/quota/NoopQuotaService.jsonld +0 -16
- package/dist/quota/QuotaService.d.ts +0 -4
- package/dist/quota/QuotaService.js.map +1 -1
- package/dist/quota/QuotaService.jsonld +0 -16
- package/dist/service/EdgeNodeSignalClient.d.ts +0 -2
- package/dist/service/EdgeNodeSignalClient.js +0 -4
- package/dist/service/EdgeNodeSignalClient.js.map +1 -1
- package/dist/service/PodMigrationService.d.ts +2 -2
- package/dist/service/PodMigrationService.js +4 -4
- package/dist/service/PodMigrationService.js.map +1 -1
- package/dist/setup/LocalSetupServiceTokenRepository.d.ts +22 -0
- package/dist/setup/LocalSetupServiceTokenRepository.js +68 -0
- package/dist/setup/LocalSetupServiceTokenRepository.js.map +1 -0
- package/dist/storage/quota/PerAccountQuotaStrategy.js +2 -2
- package/dist/storage/quota/PerAccountQuotaStrategy.js.map +1 -1
- package/dist/storage/quota/UsageRepository.d.ts +10 -32
- package/dist/storage/quota/UsageRepository.js +84 -281
- package/dist/storage/quota/UsageRepository.js.map +1 -1
- package/dist/subdomain/SubdomainService.d.ts +1 -1
- package/dist/subdomain/SubdomainService.js +1 -1
- package/dist/subdomain/SubdomainService.js.map +1 -1
- package/dist/subdomain/SubdomainService.jsonld +1 -1
- package/package.json +1 -1
- package/dist/api/handlers/ApiKeyHandler.d.ts +0 -15
- package/dist/api/handlers/ApiKeyHandler.js +0 -153
- package/dist/api/handlers/ApiKeyHandler.js.map +0 -1
- package/dist/api/store/DrizzleClientCredentialsStore.d.ts +0 -51
- package/dist/api/store/DrizzleClientCredentialsStore.js +0 -115
- package/dist/api/store/DrizzleClientCredentialsStore.js.map +0 -1
|
@@ -26,7 +26,7 @@ class EdgeNodeDnsCoordinator {
|
|
|
26
26
|
let recordType;
|
|
27
27
|
const ipv6 = this.extractString(metadata.ipv6);
|
|
28
28
|
const ipv4 = this.extractString(metadata.ipv4);
|
|
29
|
-
const
|
|
29
|
+
const tunnelEntrypoint = this.extractTunnelEntrypoint(metadata.tunnel);
|
|
30
30
|
// IPv6 优先
|
|
31
31
|
if (ipv6 && this.isIpv6(ipv6)) {
|
|
32
32
|
target = ipv6;
|
|
@@ -34,7 +34,7 @@ class EdgeNodeDnsCoordinator {
|
|
|
34
34
|
this.logger.debug(`Node ${nodeId} 使用 IPv6 地址: ${ipv6}`);
|
|
35
35
|
}
|
|
36
36
|
else {
|
|
37
|
-
target = ipv4 ??
|
|
37
|
+
target = ipv4 ?? tunnelEntrypoint;
|
|
38
38
|
}
|
|
39
39
|
if (!target && hints?.target) {
|
|
40
40
|
target = hints.target;
|
|
@@ -90,13 +90,19 @@ class EdgeNodeDnsCoordinator {
|
|
|
90
90
|
return undefined;
|
|
91
91
|
}
|
|
92
92
|
const target = this.extractString(dns.target)
|
|
93
|
-
?? this.
|
|
93
|
+
?? this.extractTunnelEntrypoint(metadata.tunnel)
|
|
94
94
|
?? this.extractString(metadata.baseUrl);
|
|
95
95
|
if (!target) {
|
|
96
96
|
return undefined;
|
|
97
97
|
}
|
|
98
98
|
return { subdomain, target };
|
|
99
99
|
}
|
|
100
|
+
extractTunnelEntrypoint(value) {
|
|
101
|
+
if (!value || typeof value !== 'object') {
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
return this.extractString(value.entrypoint);
|
|
105
|
+
}
|
|
100
106
|
extractString(value) {
|
|
101
107
|
if (typeof value !== 'string') {
|
|
102
108
|
return undefined;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EdgeNodeDnsCoordinator.js","sourceRoot":"","sources":["../../src/edge/EdgeNodeDnsCoordinator.ts"],"names":[],"mappings":"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","sourcesContent":["import { getLoggerFor } from 'global-logger-factory';\nimport type { DnsProvider, DnsRecordTypeValue } from '../dns/DnsProvider';\n\nexport interface EdgeNodeDnsCoordinatorOptions {\n provider: DnsProvider;\n /** 顶级域名,例如 `undefineds.site`。 */\n rootDomain?: string | null;\n /**\n * 默认记录类型,当目标地址无法识别时回退使用。\n * 一般为 `A`(IPv4)或 `AAAA`(IPv6)。\n */\n defaultRecordType?: DnsRecordTypeValue;\n /** TTL 秒数,缺省按供应商默认。 */\n ttl?: number | string | null;\n}\n\nexport class EdgeNodeDnsCoordinator {\n private readonly logger = getLoggerFor(this);\n private readonly provider: DnsProvider;\n private readonly rootDomain?: string;\n private readonly defaultRecordType: DnsRecordTypeValue;\n private readonly ttl?: number;\n private readonly enabled: boolean;\n\n public constructor(options: EdgeNodeDnsCoordinatorOptions) {\n this.provider = options.provider;\n this.rootDomain = this.normalizeRootDomain(options.rootDomain);\n this.defaultRecordType = options.defaultRecordType ?? 'A';\n this.ttl = this.normalizeTtl(options.ttl);\n this.enabled = Boolean(this.rootDomain);\n }\n\n public async synchronize(nodeId: string, metadata: Record<string, unknown>): Promise<void> {\n if (!this.enabled) {\n return;\n }\n\n const hints = this.extractDnsHints(metadata);\n\n const subdomain = this.extractString(metadata.subdomain) ?? hints?.subdomain;\n if (!subdomain) {\n this.logger.debug(`Node ${nodeId} 未提供 subdomain,跳过 DNS 同步。`);\n return;\n }\n\n // 用节点上报的地址(公网 IP 或隧道入口,由节点自行决定)\n let target: string | undefined;\n let recordType: DnsRecordTypeValue | undefined;\n\n const ipv6 = this.extractString(metadata.ipv6);\n const ipv4 = this.extractString(metadata.ipv4);\n const publicAddress = this.extractString(metadata.publicAddress);\n\n // IPv6 优先\n if (ipv6 && this.isIpv6(ipv6)) {\n target = ipv6;\n recordType = 'AAAA';\n this.logger.debug(`Node ${nodeId} 使用 IPv6 地址: ${ipv6}`);\n } else {\n target = ipv4 ?? publicAddress;\n }\n\n if (!target && hints?.target) {\n target = hints.target;\n }\n\n if (!target) {\n this.logger.debug(`Node ${nodeId} 未提供可用地址,跳过 DNS 同步。`);\n return;\n }\n\n // 健康检查未通过时删除 DNS 记录\n const connectivityStatus = this.extractString(metadata.connectivityStatus);\n if (connectivityStatus === 'unreachable') {\n this.logger.info(`节点 ${nodeId} 不可达,删除 DNS 记录 ${subdomain}.${this.rootDomain}`);\n try {\n await this.provider.deleteRecord({\n domain: this.rootDomain!,\n subdomain,\n type: recordType ?? this.defaultRecordType,\n });\n } catch (error: unknown) {\n this.logger.error(`删除节点 ${nodeId} DNS 记录失败: ${(error as Error).message}`);\n }\n return;\n }\n\n const type = recordType ?? this.detectRecordType(target) ?? this.defaultRecordType;\n const value = this.normalizeRecordValue(target, type);\n\n if (!value) {\n this.logger.warn(`Edge node ${nodeId} DNS 目标解析失败,跳过同步。`);\n return;\n }\n\n try {\n await this.provider.upsertRecord({\n domain: this.rootDomain!,\n subdomain,\n type,\n value,\n ttl: this.ttl,\n });\n this.logger.info(`已同步节点 ${nodeId} 的 DNS: ${subdomain}.${this.rootDomain} -> ${value}`);\n } catch (error: unknown) {\n this.logger.error(`同步节点 ${nodeId} DNS 记录失败: ${(error as Error).message}`);\n throw error;\n }\n }\n\n private extractDnsHints(metadata: Record<string, unknown>): { subdomain: string; target: string } | undefined {\n const dns = metadata?.dns;\n if (!dns || typeof dns !== 'object') {\n return undefined;\n }\n const subdomain = this.extractString((dns as Record<string, unknown>).subdomain);\n if (!subdomain) {\n return undefined;\n }\n\n const target = this.extractString((dns as Record<string, unknown>).target)\n ?? this.extractString(metadata.publicAddress)\n ?? this.extractString(metadata.baseUrl);\n\n if (!target) {\n return undefined;\n }\n return { subdomain, target };\n }\n\n private extractString(value: unknown): string | undefined {\n if (typeof value !== 'string') {\n return undefined;\n }\n const trimmed = value.trim();\n return trimmed.length > 0 ? trimmed : undefined;\n }\n\n private detectRecordType(target: string): DnsRecordTypeValue | undefined {\n const host = this.extractHost(target) ?? target;\n if (this.isIpv4(host)) {\n return 'A';\n }\n if (this.isIpv6(host)) {\n return 'AAAA';\n }\n if (host.includes('.')) {\n return 'CNAME';\n }\n return undefined;\n }\n\n private normalizeRecordValue(target: string, type: DnsRecordTypeValue): string | undefined {\n const host = this.extractHost(target) ?? target;\n if (type === 'A' && this.isIpv4(host)) {\n return host;\n }\n if (type === 'AAAA' && this.isIpv6(host)) {\n return host;\n }\n if (type === 'CNAME') {\n return host.endsWith('.') ? host : `${host}.`;\n }\n if (type === 'TXT') {\n return host;\n }\n return undefined;\n }\n\n private extractHost(input: string): string | undefined {\n try {\n const url = new URL(input);\n return url.hostname;\n } catch {\n return input;\n }\n }\n\n private isIpv4(value: string): boolean {\n const parts = value.split('.');\n if (parts.length !== 4) {\n return false;\n }\n return parts.every((part) => {\n if (!/^[0-9]{1,3}$/.test(part)) {\n return false;\n }\n const num = Number(part);\n return num >= 0 && num <= 255;\n });\n }\n\n private isIpv6(value: string): boolean {\n return /^[0-9a-fA-F:]+$/.test(value) && value.includes(':');\n }\n\n private normalizeRootDomain(value?: string | null): string | undefined {\n if (typeof value !== 'string') {\n return undefined;\n }\n let trimmed = value.trim();\n if (trimmed.includes('://')) {\n try {\n trimmed = new URL(trimmed).hostname;\n } catch {\n // ignore\n }\n }\n trimmed = trimmed.replace(/\\.$/, '');\n return trimmed.length > 0 ? trimmed : undefined;\n }\n\n private normalizeTtl(value?: number | string | null): number | undefined {\n if (typeof value === 'number' && Number.isFinite(value) && value > 0) {\n return Math.trunc(value);\n }\n if (typeof value === 'string') {\n const parsed = Number(value.trim());\n if (Number.isFinite(parsed) && parsed > 0) {\n return Math.trunc(parsed);\n }\n }\n return undefined;\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"EdgeNodeDnsCoordinator.js","sourceRoot":"","sources":["../../src/edge/EdgeNodeDnsCoordinator.ts"],"names":[],"mappings":"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","sourcesContent":["import { getLoggerFor } from 'global-logger-factory';\nimport type { DnsProvider, DnsRecordTypeValue } from '../dns/DnsProvider';\n\nexport interface EdgeNodeDnsCoordinatorOptions {\n provider: DnsProvider;\n /** 顶级域名,例如 `undefineds.site`。 */\n rootDomain?: string | null;\n /**\n * 默认记录类型,当目标地址无法识别时回退使用。\n * 一般为 `A`(IPv4)或 `AAAA`(IPv6)。\n */\n defaultRecordType?: DnsRecordTypeValue;\n /** TTL 秒数,缺省按供应商默认。 */\n ttl?: number | string | null;\n}\n\nexport class EdgeNodeDnsCoordinator {\n private readonly logger = getLoggerFor(this);\n private readonly provider: DnsProvider;\n private readonly rootDomain?: string;\n private readonly defaultRecordType: DnsRecordTypeValue;\n private readonly ttl?: number;\n private readonly enabled: boolean;\n\n public constructor(options: EdgeNodeDnsCoordinatorOptions) {\n this.provider = options.provider;\n this.rootDomain = this.normalizeRootDomain(options.rootDomain);\n this.defaultRecordType = options.defaultRecordType ?? 'A';\n this.ttl = this.normalizeTtl(options.ttl);\n this.enabled = Boolean(this.rootDomain);\n }\n\n public async synchronize(nodeId: string, metadata: Record<string, unknown>): Promise<void> {\n if (!this.enabled) {\n return;\n }\n\n const hints = this.extractDnsHints(metadata);\n\n const subdomain = this.extractString(metadata.subdomain) ?? hints?.subdomain;\n if (!subdomain) {\n this.logger.debug(`Node ${nodeId} 未提供 subdomain,跳过 DNS 同步。`);\n return;\n }\n\n // 用节点上报的地址(公网 IP 或隧道入口,由节点自行决定)\n let target: string | undefined;\n let recordType: DnsRecordTypeValue | undefined;\n\n const ipv6 = this.extractString(metadata.ipv6);\n const ipv4 = this.extractString(metadata.ipv4);\n const tunnelEntrypoint = this.extractTunnelEntrypoint(metadata.tunnel);\n\n // IPv6 优先\n if (ipv6 && this.isIpv6(ipv6)) {\n target = ipv6;\n recordType = 'AAAA';\n this.logger.debug(`Node ${nodeId} 使用 IPv6 地址: ${ipv6}`);\n } else {\n target = ipv4 ?? tunnelEntrypoint;\n }\n\n if (!target && hints?.target) {\n target = hints.target;\n }\n\n if (!target) {\n this.logger.debug(`Node ${nodeId} 未提供可用地址,跳过 DNS 同步。`);\n return;\n }\n\n // 健康检查未通过时删除 DNS 记录\n const connectivityStatus = this.extractString(metadata.connectivityStatus);\n if (connectivityStatus === 'unreachable') {\n this.logger.info(`节点 ${nodeId} 不可达,删除 DNS 记录 ${subdomain}.${this.rootDomain}`);\n try {\n await this.provider.deleteRecord({\n domain: this.rootDomain!,\n subdomain,\n type: recordType ?? this.defaultRecordType,\n });\n } catch (error: unknown) {\n this.logger.error(`删除节点 ${nodeId} DNS 记录失败: ${(error as Error).message}`);\n }\n return;\n }\n\n const type = recordType ?? this.detectRecordType(target) ?? this.defaultRecordType;\n const value = this.normalizeRecordValue(target, type);\n\n if (!value) {\n this.logger.warn(`Edge node ${nodeId} DNS 目标解析失败,跳过同步。`);\n return;\n }\n\n try {\n await this.provider.upsertRecord({\n domain: this.rootDomain!,\n subdomain,\n type,\n value,\n ttl: this.ttl,\n });\n this.logger.info(`已同步节点 ${nodeId} 的 DNS: ${subdomain}.${this.rootDomain} -> ${value}`);\n } catch (error: unknown) {\n this.logger.error(`同步节点 ${nodeId} DNS 记录失败: ${(error as Error).message}`);\n throw error;\n }\n }\n\n private extractDnsHints(metadata: Record<string, unknown>): { subdomain: string; target: string } | undefined {\n const dns = metadata?.dns;\n if (!dns || typeof dns !== 'object') {\n return undefined;\n }\n const subdomain = this.extractString((dns as Record<string, unknown>).subdomain);\n if (!subdomain) {\n return undefined;\n }\n\n const target = this.extractString((dns as Record<string, unknown>).target)\n ?? this.extractTunnelEntrypoint(metadata.tunnel)\n ?? this.extractString(metadata.baseUrl);\n\n if (!target) {\n return undefined;\n }\n return { subdomain, target };\n }\n\n private extractTunnelEntrypoint(value: unknown): string | undefined {\n if (!value || typeof value !== 'object') {\n return undefined;\n }\n return this.extractString((value as Record<string, unknown>).entrypoint);\n }\n\n private extractString(value: unknown): string | undefined {\n if (typeof value !== 'string') {\n return undefined;\n }\n const trimmed = value.trim();\n return trimmed.length > 0 ? trimmed : undefined;\n }\n\n private detectRecordType(target: string): DnsRecordTypeValue | undefined {\n const host = this.extractHost(target) ?? target;\n if (this.isIpv4(host)) {\n return 'A';\n }\n if (this.isIpv6(host)) {\n return 'AAAA';\n }\n if (host.includes('.')) {\n return 'CNAME';\n }\n return undefined;\n }\n\n private normalizeRecordValue(target: string, type: DnsRecordTypeValue): string | undefined {\n const host = this.extractHost(target) ?? target;\n if (type === 'A' && this.isIpv4(host)) {\n return host;\n }\n if (type === 'AAAA' && this.isIpv6(host)) {\n return host;\n }\n if (type === 'CNAME') {\n return host.endsWith('.') ? host : `${host}.`;\n }\n if (type === 'TXT') {\n return host;\n }\n return undefined;\n }\n\n private extractHost(input: string): string | undefined {\n try {\n const url = new URL(input);\n return url.hostname;\n } catch {\n return input;\n }\n }\n\n private isIpv4(value: string): boolean {\n const parts = value.split('.');\n if (parts.length !== 4) {\n return false;\n }\n return parts.every((part) => {\n if (!/^[0-9]{1,3}$/.test(part)) {\n return false;\n }\n const num = Number(part);\n return num >= 0 && num <= 255;\n });\n }\n\n private isIpv6(value: string): boolean {\n return /^[0-9a-fA-F:]+$/.test(value) && value.includes(':');\n }\n\n private normalizeRootDomain(value?: string | null): string | undefined {\n if (typeof value !== 'string') {\n return undefined;\n }\n let trimmed = value.trim();\n if (trimmed.includes('://')) {\n try {\n trimmed = new URL(trimmed).hostname;\n } catch {\n // ignore\n }\n }\n trimmed = trimmed.replace(/\\.$/, '');\n return trimmed.length > 0 ? trimmed : undefined;\n }\n\n private normalizeTtl(value?: number | string | null): number | undefined {\n if (typeof value === 'number' && Number.isFinite(value) && value > 0) {\n return Math.trunc(value);\n }\n if (typeof value === 'string') {\n const parsed = Number(value.trim());\n if (Number.isFinite(parsed) && parsed > 0) {\n return Math.trunc(parsed);\n }\n }\n return undefined;\n }\n}\n"]}
|
|
@@ -117,6 +117,10 @@
|
|
|
117
117
|
"@id": "undefineds:dist/edge/EdgeNodeDnsCoordinator.jsonld#EdgeNodeDnsCoordinator__member_extractDnsHints",
|
|
118
118
|
"memberFieldName": "extractDnsHints"
|
|
119
119
|
},
|
|
120
|
+
{
|
|
121
|
+
"@id": "undefineds:dist/edge/EdgeNodeDnsCoordinator.jsonld#EdgeNodeDnsCoordinator__member_extractTunnelEntrypoint",
|
|
122
|
+
"memberFieldName": "extractTunnelEntrypoint"
|
|
123
|
+
},
|
|
120
124
|
{
|
|
121
125
|
"@id": "undefineds:dist/edge/EdgeNodeDnsCoordinator.jsonld#EdgeNodeDnsCoordinator__member_extractString",
|
|
122
126
|
"memberFieldName": "extractString"
|
|
@@ -15,6 +15,9 @@ export declare class EdgeNodeHealthProbeService {
|
|
|
15
15
|
constructor(options: EdgeNodeHealthProbeServiceOptions);
|
|
16
16
|
probeNode(nodeId: string): Promise<void>;
|
|
17
17
|
private collectCandidates;
|
|
18
|
+
private extractTunnelEntrypoint;
|
|
19
|
+
private extractManagedTunnelEndpoint;
|
|
20
|
+
private extractNonEmptyString;
|
|
18
21
|
private ping;
|
|
19
22
|
private toUrl;
|
|
20
23
|
private createRepository;
|
|
@@ -54,14 +54,34 @@ class EdgeNodeHealthProbeService {
|
|
|
54
54
|
candidates.add(candidate.trim());
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
|
-
|
|
58
|
-
|
|
57
|
+
const tunnelEntrypoint = this.extractTunnelEntrypoint(metadata.tunnel) ?? this.extractManagedTunnelEndpoint(metadata.managedTunnel);
|
|
58
|
+
if (tunnelEntrypoint) {
|
|
59
|
+
candidates.add(tunnelEntrypoint);
|
|
59
60
|
}
|
|
60
61
|
if (typeof metadata.baseUrl === 'string') {
|
|
61
62
|
candidates.add(metadata.baseUrl.trim());
|
|
62
63
|
}
|
|
63
64
|
return Array.from(candidates);
|
|
64
65
|
}
|
|
66
|
+
extractTunnelEntrypoint(value) {
|
|
67
|
+
if (!value || typeof value !== 'object') {
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
return this.extractNonEmptyString(value.entrypoint);
|
|
71
|
+
}
|
|
72
|
+
extractManagedTunnelEndpoint(value) {
|
|
73
|
+
if (!value || typeof value !== 'object') {
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
return this.extractNonEmptyString(value.endpoint);
|
|
77
|
+
}
|
|
78
|
+
extractNonEmptyString(value) {
|
|
79
|
+
if (typeof value !== 'string') {
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
const trimmed = value.trim();
|
|
83
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
84
|
+
}
|
|
65
85
|
async ping(candidate, location) {
|
|
66
86
|
const url = this.toUrl(candidate);
|
|
67
87
|
if (!url) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EdgeNodeHealthProbeService.js","sourceRoot":"","sources":["../../src/edge/EdgeNodeHealthProbeService.ts"],"names":[],"mappings":"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","sourcesContent":["import { getLoggerFor } from 'global-logger-factory';\nimport { getIdentityDatabase } from '../identity/drizzle/db';\nimport { EdgeNodeRepository } from '../identity/drizzle/EdgeNodeRepository';\n\ninterface EdgeNodeHealthProbeServiceOptions {\n repository?: EdgeNodeRepository;\n identityDbUrl?: string;\n enabled?: boolean | string;\n timeoutMs?: number | string;\n locations?: string | string[];\n}\n\ninterface ProbeResult {\n location: string;\n candidate: string;\n success: boolean;\n latencyMs?: number;\n error?: string;\n checkedAt: string;\n}\n\ninterface ProbeLocation {\n name: string;\n endpoint?: string;\n}\n\nexport class EdgeNodeHealthProbeService {\n private readonly logger = getLoggerFor(this);\n private readonly repository?: EdgeNodeRepository;\n private readonly enabled: boolean;\n private readonly timeoutMs: number;\n private readonly locations: ProbeLocation[];\n\n public constructor(options: EdgeNodeHealthProbeServiceOptions) {\n this.repository = options.repository ?? this.createRepository(options.identityDbUrl);\n this.enabled = this.normalizeBoolean(options.enabled) && Boolean(this.repository);\n this.timeoutMs = this.normalizeTimeout(options.timeoutMs) ?? 3_000;\n this.locations = this.normalizeLocations(options.locations);\n }\n\n public async probeNode(nodeId: string): Promise<void> {\n if (!this.enabled) {\n return;\n }\n const node = await this.repository!.getNodeMetadata(nodeId);\n if (!node?.metadata) {\n this.logger.debug(`节点 ${nodeId} 无 metadata,跳过探测。`);\n return;\n }\n const metadata = node.metadata as Record<string, unknown>;\n const candidates = this.collectCandidates(metadata);\n if (candidates.length === 0) {\n this.logger.debug(`节点 ${nodeId} 没有可探测的候选地址。`);\n return;\n }\n\n const results: ProbeResult[] = [];\n for (const candidate of candidates) {\n for (const location of this.locations) {\n const result = await this.ping(candidate, location);\n results.push(result);\n }\n }\n\n const successful = results.find((item) => item.success);\n const clusterSuccess = results.some((item) => item.location === 'cluster' && item.success);\n const status = clusterSuccess ? 'direct' : successful ? 'degraded' : 'unreachable';\n const now = new Date();\n const reachability = {\n status,\n lastProbeAt: now.toISOString(),\n lastSuccessAt: successful ? successful.checkedAt : undefined,\n samples: results,\n };\n\n await this.repository!.mergeNodeMetadata(nodeId, { reachability });\n }\n\n private collectCandidates(metadata: Record<string, unknown>): string[] {\n const candidates = new Set<string>();\n const direct = (metadata.directCandidates as string[] | undefined) ?? [];\n for (const candidate of direct) {\n if (typeof candidate === 'string' && candidate.trim().length > 0) {\n candidates.add(candidate.trim());\n }\n }\n if (typeof metadata.publicAddress === 'string') {\n candidates.add(metadata.publicAddress.trim());\n }\n if (typeof metadata.baseUrl === 'string') {\n candidates.add(metadata.baseUrl.trim());\n }\n return Array.from(candidates);\n }\n\n private async ping(candidate: string, location: ProbeLocation): Promise<ProbeResult> {\n const url = this.toUrl(candidate);\n if (!url) {\n return { candidate, success: false, error: 'invalid-url', location: location.name, checkedAt: new Date().toISOString() };\n }\n\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), this.timeoutMs);\n const started = Date.now();\n try {\n if (location.endpoint) {\n const probeUrl = new URL(location.endpoint);\n probeUrl.searchParams.set('target', url.toString());\n const response = await fetch(probeUrl.toString(), {\n method: 'GET',\n signal: controller.signal,\n headers: { 'accept': 'application/json' },\n });\n clearTimeout(timer);\n const latencyMs = Date.now() - started;\n if (!response.ok) {\n return {\n candidate,\n location: location.name,\n success: false,\n latencyMs,\n error: `status:${response.status}`,\n checkedAt: new Date().toISOString(),\n };\n }\n try {\n const data = await response.json() as Partial<ProbeResult>;\n return {\n candidate,\n location: location.name,\n success: Boolean(data.success),\n latencyMs: typeof data.latencyMs === 'number' ? data.latencyMs : latencyMs,\n error: data.error,\n checkedAt: data.checkedAt ?? new Date().toISOString(),\n };\n } catch (error: unknown) {\n return {\n candidate,\n location: location.name,\n success: false,\n latencyMs,\n error: `invalid-json:${(error as Error).message}`,\n checkedAt: new Date().toISOString(),\n };\n }\n }\n const response = await fetch(url.toString(), {\n method: 'HEAD',\n signal: controller.signal,\n });\n clearTimeout(timer);\n const latencyMs = Date.now() - started;\n return {\n candidate,\n location: location.name,\n success: response.ok,\n latencyMs,\n error: response.ok ? undefined : `status:${response.status}`,\n checkedAt: new Date().toISOString(),\n };\n } catch (error: unknown) {\n clearTimeout(timer);\n return {\n candidate,\n location: location.name,\n success: false,\n error: (error as Error).message,\n checkedAt: new Date().toISOString(),\n };\n }\n }\n\n private toUrl(value: string): URL | undefined {\n try {\n return new URL(value);\n } catch {\n try {\n return new URL(`https://${value}`);\n } catch {\n return undefined;\n }\n }\n }\n\n private createRepository(identityDbUrl?: string): EdgeNodeRepository | undefined {\n if (!identityDbUrl) {\n return undefined;\n }\n const db = getIdentityDatabase(identityDbUrl);\n return new EdgeNodeRepository(db);\n }\n\n private normalizeBoolean(value?: boolean | string): boolean {\n if (typeof value === 'boolean') {\n return value;\n }\n if (typeof value === 'string') {\n const normalized = value.trim().toLowerCase();\n return normalized === 'true' || normalized === '1' || normalized === 'yes' || normalized === 'on';\n }\n return false;\n }\n\n private normalizeTimeout(value?: number | string): number | undefined {\n if (typeof value === 'number' && Number.isFinite(value) && value > 0) {\n return Math.trunc(value);\n }\n if (typeof value === 'string') {\n const parsed = Number(value.trim());\n if (Number.isFinite(parsed) && parsed > 0) {\n return Math.trunc(parsed);\n }\n }\n return undefined;\n }\n\n private normalizeLocations(value?: string | string[]): ProbeLocation[] {\n const defaultLocation: ProbeLocation = { name: 'cluster' };\n if (value === undefined) {\n return [ defaultLocation ];\n }\n const input = Array.isArray(value) ? value : value.split(/[,;\\n]+/u);\n const result: ProbeLocation[] = [];\n for (const entry of input) {\n const trimmed = entry.trim();\n if (trimmed.length === 0) {\n continue;\n }\n const [ namePart, endpointPart ] = trimmed.split('@', 2);\n const name = namePart.trim() || 'cluster';\n const endpoint = endpointPart?.trim();\n result.push({\n name,\n endpoint: endpoint?.length ? endpoint : undefined,\n });\n }\n return result.length > 0 ? result : [ defaultLocation ];\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"EdgeNodeHealthProbeService.js","sourceRoot":"","sources":["../../src/edge/EdgeNodeHealthProbeService.ts"],"names":[],"mappings":"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","sourcesContent":["import { getLoggerFor } from 'global-logger-factory';\nimport { getIdentityDatabase } from '../identity/drizzle/db';\nimport { EdgeNodeRepository } from '../identity/drizzle/EdgeNodeRepository';\n\ninterface EdgeNodeHealthProbeServiceOptions {\n repository?: EdgeNodeRepository;\n identityDbUrl?: string;\n enabled?: boolean | string;\n timeoutMs?: number | string;\n locations?: string | string[];\n}\n\ninterface ProbeResult {\n location: string;\n candidate: string;\n success: boolean;\n latencyMs?: number;\n error?: string;\n checkedAt: string;\n}\n\ninterface ProbeLocation {\n name: string;\n endpoint?: string;\n}\n\nexport class EdgeNodeHealthProbeService {\n private readonly logger = getLoggerFor(this);\n private readonly repository?: EdgeNodeRepository;\n private readonly enabled: boolean;\n private readonly timeoutMs: number;\n private readonly locations: ProbeLocation[];\n\n public constructor(options: EdgeNodeHealthProbeServiceOptions) {\n this.repository = options.repository ?? this.createRepository(options.identityDbUrl);\n this.enabled = this.normalizeBoolean(options.enabled) && Boolean(this.repository);\n this.timeoutMs = this.normalizeTimeout(options.timeoutMs) ?? 3_000;\n this.locations = this.normalizeLocations(options.locations);\n }\n\n public async probeNode(nodeId: string): Promise<void> {\n if (!this.enabled) {\n return;\n }\n const node = await this.repository!.getNodeMetadata(nodeId);\n if (!node?.metadata) {\n this.logger.debug(`节点 ${nodeId} 无 metadata,跳过探测。`);\n return;\n }\n const metadata = node.metadata as Record<string, unknown>;\n const candidates = this.collectCandidates(metadata);\n if (candidates.length === 0) {\n this.logger.debug(`节点 ${nodeId} 没有可探测的候选地址。`);\n return;\n }\n\n const results: ProbeResult[] = [];\n for (const candidate of candidates) {\n for (const location of this.locations) {\n const result = await this.ping(candidate, location);\n results.push(result);\n }\n }\n\n const successful = results.find((item) => item.success);\n const clusterSuccess = results.some((item) => item.location === 'cluster' && item.success);\n const status = clusterSuccess ? 'direct' : successful ? 'degraded' : 'unreachable';\n const now = new Date();\n const reachability = {\n status,\n lastProbeAt: now.toISOString(),\n lastSuccessAt: successful ? successful.checkedAt : undefined,\n samples: results,\n };\n\n await this.repository!.mergeNodeMetadata(nodeId, { reachability });\n }\n\n private collectCandidates(metadata: Record<string, unknown>): string[] {\n const candidates = new Set<string>();\n const direct = (metadata.directCandidates as string[] | undefined) ?? [];\n for (const candidate of direct) {\n if (typeof candidate === 'string' && candidate.trim().length > 0) {\n candidates.add(candidate.trim());\n }\n }\n const tunnelEntrypoint = this.extractTunnelEntrypoint(metadata.tunnel) ?? this.extractManagedTunnelEndpoint(metadata.managedTunnel);\n if (tunnelEntrypoint) {\n candidates.add(tunnelEntrypoint);\n }\n if (typeof metadata.baseUrl === 'string') {\n candidates.add(metadata.baseUrl.trim());\n }\n return Array.from(candidates);\n }\n\n private extractTunnelEntrypoint(value: unknown): string | undefined {\n if (!value || typeof value !== 'object') {\n return undefined;\n }\n return this.extractNonEmptyString((value as Record<string, unknown>).entrypoint);\n }\n\n private extractManagedTunnelEndpoint(value: unknown): string | undefined {\n if (!value || typeof value !== 'object') {\n return undefined;\n }\n return this.extractNonEmptyString((value as Record<string, unknown>).endpoint);\n }\n\n private extractNonEmptyString(value: unknown): string | undefined {\n if (typeof value !== 'string') {\n return undefined;\n }\n const trimmed = value.trim();\n return trimmed.length > 0 ? trimmed : undefined;\n }\n\n private async ping(candidate: string, location: ProbeLocation): Promise<ProbeResult> {\n const url = this.toUrl(candidate);\n if (!url) {\n return { candidate, success: false, error: 'invalid-url', location: location.name, checkedAt: new Date().toISOString() };\n }\n\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), this.timeoutMs);\n const started = Date.now();\n try {\n if (location.endpoint) {\n const probeUrl = new URL(location.endpoint);\n probeUrl.searchParams.set('target', url.toString());\n const response = await fetch(probeUrl.toString(), {\n method: 'GET',\n signal: controller.signal,\n headers: { 'accept': 'application/json' },\n });\n clearTimeout(timer);\n const latencyMs = Date.now() - started;\n if (!response.ok) {\n return {\n candidate,\n location: location.name,\n success: false,\n latencyMs,\n error: `status:${response.status}`,\n checkedAt: new Date().toISOString(),\n };\n }\n try {\n const data = await response.json() as Partial<ProbeResult>;\n return {\n candidate,\n location: location.name,\n success: Boolean(data.success),\n latencyMs: typeof data.latencyMs === 'number' ? data.latencyMs : latencyMs,\n error: data.error,\n checkedAt: data.checkedAt ?? new Date().toISOString(),\n };\n } catch (error: unknown) {\n return {\n candidate,\n location: location.name,\n success: false,\n latencyMs,\n error: `invalid-json:${(error as Error).message}`,\n checkedAt: new Date().toISOString(),\n };\n }\n }\n const response = await fetch(url.toString(), {\n method: 'HEAD',\n signal: controller.signal,\n });\n clearTimeout(timer);\n const latencyMs = Date.now() - started;\n return {\n candidate,\n location: location.name,\n success: response.ok,\n latencyMs,\n error: response.ok ? undefined : `status:${response.status}`,\n checkedAt: new Date().toISOString(),\n };\n } catch (error: unknown) {\n clearTimeout(timer);\n return {\n candidate,\n location: location.name,\n success: false,\n error: (error as Error).message,\n checkedAt: new Date().toISOString(),\n };\n }\n }\n\n private toUrl(value: string): URL | undefined {\n try {\n return new URL(value);\n } catch {\n try {\n return new URL(`https://${value}`);\n } catch {\n return undefined;\n }\n }\n }\n\n private createRepository(identityDbUrl?: string): EdgeNodeRepository | undefined {\n if (!identityDbUrl) {\n return undefined;\n }\n const db = getIdentityDatabase(identityDbUrl);\n return new EdgeNodeRepository(db);\n }\n\n private normalizeBoolean(value?: boolean | string): boolean {\n if (typeof value === 'boolean') {\n return value;\n }\n if (typeof value === 'string') {\n const normalized = value.trim().toLowerCase();\n return normalized === 'true' || normalized === '1' || normalized === 'yes' || normalized === 'on';\n }\n return false;\n }\n\n private normalizeTimeout(value?: number | string): number | undefined {\n if (typeof value === 'number' && Number.isFinite(value) && value > 0) {\n return Math.trunc(value);\n }\n if (typeof value === 'string') {\n const parsed = Number(value.trim());\n if (Number.isFinite(parsed) && parsed > 0) {\n return Math.trunc(parsed);\n }\n }\n return undefined;\n }\n\n private normalizeLocations(value?: string | string[]): ProbeLocation[] {\n const defaultLocation: ProbeLocation = { name: 'cluster' };\n if (value === undefined) {\n return [ defaultLocation ];\n }\n const input = Array.isArray(value) ? value : value.split(/[,;\\n]+/u);\n const result: ProbeLocation[] = [];\n for (const entry of input) {\n const trimmed = entry.trim();\n if (trimmed.length === 0) {\n continue;\n }\n const [ namePart, endpointPart ] = trimmed.split('@', 2);\n const name = namePart.trim() || 'cluster';\n const endpoint = endpointPart?.trim();\n result.push({\n name,\n endpoint: endpoint?.length ? endpoint : undefined,\n });\n }\n return result.length > 0 ? result : [ defaultLocation ];\n }\n}\n"]}
|
|
@@ -109,6 +109,18 @@
|
|
|
109
109
|
"@id": "undefineds:dist/edge/EdgeNodeHealthProbeService.jsonld#EdgeNodeHealthProbeService__member_collectCandidates",
|
|
110
110
|
"memberFieldName": "collectCandidates"
|
|
111
111
|
},
|
|
112
|
+
{
|
|
113
|
+
"@id": "undefineds:dist/edge/EdgeNodeHealthProbeService.jsonld#EdgeNodeHealthProbeService__member_extractTunnelEntrypoint",
|
|
114
|
+
"memberFieldName": "extractTunnelEntrypoint"
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
"@id": "undefineds:dist/edge/EdgeNodeHealthProbeService.jsonld#EdgeNodeHealthProbeService__member_extractManagedTunnelEndpoint",
|
|
118
|
+
"memberFieldName": "extractManagedTunnelEndpoint"
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
"@id": "undefineds:dist/edge/EdgeNodeHealthProbeService.jsonld#EdgeNodeHealthProbeService__member_extractNonEmptyString",
|
|
122
|
+
"memberFieldName": "extractNonEmptyString"
|
|
123
|
+
},
|
|
112
124
|
{
|
|
113
125
|
"@id": "undefineds:dist/edge/EdgeNodeHealthProbeService.jsonld#EdgeNodeHealthProbeService__member_ping",
|
|
114
126
|
"memberFieldName": "ping"
|
|
@@ -194,9 +194,12 @@ class ClusterIngressRouter extends community_server_1.HttpHandler {
|
|
|
194
194
|
return entrypoint;
|
|
195
195
|
}
|
|
196
196
|
}
|
|
197
|
-
|
|
198
|
-
if (typeof
|
|
199
|
-
|
|
197
|
+
const managedTunnel = metadata?.managedTunnel;
|
|
198
|
+
if (managedTunnel && typeof managedTunnel === 'object') {
|
|
199
|
+
const endpoint = managedTunnel.endpoint;
|
|
200
|
+
if (typeof endpoint === 'string' && endpoint.trim().length > 0) {
|
|
201
|
+
return endpoint;
|
|
202
|
+
}
|
|
200
203
|
}
|
|
201
204
|
return undefined;
|
|
202
205
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ClusterIngressRouter.js","sourceRoot":"","sources":["../../src/http/ClusterIngressRouter.ts"],"names":[],"mappings":";;;AACA,iEAAqD;AACrD,6CAAuC;AACvC,8DAAsD;AAEtD,8DAIiC;AACjC,+CAA6D;AAC7D,+EAA4E;AAW5E;;;;;;;;;GASG;AACH,MAAa,oBAAqB,SAAQ,8BAAW;IAiBnD,YAAmB,OAAoC;QACrD,KAAK,EAAE,CAAC;QAjBS,WAAM,GAAG,IAAA,oCAAY,EAAC,IAAI,CAAC,CAAC;QAO/C,+DAA+D;QAC9C,cAAS,GAAG;YAC3B,OAAO;YACP,mCAAmC;YACnC,yCAAyC;YACzC,QAAQ;YACR,SAAS;SACV,CAAC;QAIA,IAAI,CAAC,UAAU,GAAG,OAAO,CAAC,UAAU,IAAI,IAAI,uCAAkB,CAAC,IAAA,wBAAmB,EAAC,OAAO,CAAC,aAAa,CAAC,CAAC,CAAC;QAC3G,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,gBAAgB,CAAC,OAAO,CAAC,gBAAgB,CAAC,CAAC;QAC/D,IAAI,CAAC,oBAAoB,GAAG,IAAI,CAAC,eAAe,CAAC,OAAO,CAAC,oBAAoB,CAAC,CAAC;QAC/E,IAAI,CAAC,gBAAgB,GAAG,OAAO,CAAC,gBAAgB,IAAI,KAAK,CAAC;QAC1D,IAAI,CAAC,SAAS,GAAG,OAAO,CAAC,SAAS,IAAI,KAAK,CAAC;IAC9C,CAAC;IAEe,KAAK,CAAC,SAAS,CAAC,EAAE,OAAO,EAAoB;QAC3D,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,uCAAuC,CAAC,CAAC;QAC3D,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC;YAClB,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,+BAA+B,CAAC,CAAC;YACnD,MAAM,IAAI,0CAAuB,CAAC,kCAAkC,CAAC,CAAC;QACxE,CAAC;QAED,MAAM,QAAQ,GAAG,IAAI,CAAC,eAAe,CAAC,OAAO,CAAC,CAAC;QAC/C,IAAI,CAAC,QAAQ,EAAE,CAAC;YACd,MAAM,IAAI,0CAAuB,CAAC,sBAAsB,CAAC,CAAC;QAC5D,CAAC;QAED,yEAAyE;QACzE,IAAI,QAAQ,KAAK,IAAI,CAAC,oBAAoB,EAAE,CAAC;YAC3C,MAAM,IAAI,0CAAuB,CAAC,kDAAkD,CAAC,CAAC;QACxF,CAAC;QAED,0CAA0C;QAC1C,MAAM,MAAM,GAAG,IAAI,CAAC,yBAAyB,CAAC,QAAQ,CAAC,CAAC;QACxD,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,MAAM,IAAI,0CAAuB,CAAC,+BAA+B,CAAC,CAAC;QACrE,CAAC;QAED,yBAAyB;QACzB,MAAM,UAAU,GAAG,MAAM,IAAI,CAAC,UAAU,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC;QAC/D,IAAI,CAAC,UAAU,EAAE,CAAC;YAChB,MAAM,IAAI,0CAAuB,CAAC,QAAQ,MAAM,kBAAkB,CAAC,CAAC;QACtE,CAAC;QAED,MAAM,GAAG,GAAG,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;QACnC,IAAI,IAAI,CAAC,uBAAuB,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC/C,IAAI,CAAC,2BAA2B,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;YACpD,MAAM,IAAI,0CAAuB,CAAC,uCAAuC,CAAC,CAAC;QAC7E,CAAC;IACH,CAAC;IAEe,KAAK,CAAC,MAAM,CAAC,EAAE,OAAO,EAAE,QAAQ,EAAoB;QAClE,MAAM,QAAQ,GAAG,IAAI,CAAC,eAAe,CAAC,OAAO,CAAE,CAAC;QAChD,MAAM,GAAG,GAAG,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;QACnC,MAAM,MAAM,GAAG,IAAI,CAAC,yBAAyB,CAAC,QAAQ,CAAE,CAAC;QAEzD,6CAA6C;QAC7C,IAAI,IAAI,CAAC,uBAAuB,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC/C,MAAM,IAAI,CAAC,2BAA2B,CAAC,OAAO,EAAE,QAAQ,EAAE,GAAG,CAAC,CAAC;YAC/D,OAAO;QACT,CAAC;QAED,iDAAiD;QACjD,MAAM,IAAI,CAAC,iBAAiB,CAAC,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,CAAC,CAAC;IAC/D,CAAC;IAED;;OAEG;IACK,KAAK,CAAC,2BAA2B,CACvC,OAAwB,EACxB,QAAsB,EACtB,GAAQ;QAER,IAAI,IAAI,CAAC,gBAAgB,EAAE,CAAC;YAC1B,MAAM,IAAI,0CAAuB,CAAC,mCAAmC,CAAC,CAAC;QACzE,CAAC;QAED,kDAAkD;QAClD,MAAM,cAAc,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,QAAQ,GAAG,GAAG,CAAC,MAAM,GAAG,GAAG,CAAC,IAAI,EAAE,WAAW,IAAI,CAAC,oBAAoB,EAAE,CAAC,CAAC;QAE7G,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,wCAAwC,cAAc,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;QAEvF,QAAQ,CAAC,UAAU,GAAG,GAAG,CAAC;QAC1B,QAAQ,CAAC,SAAS,CAAC,UAAU,EAAE,cAAc,CAAC,QAAQ,EAAE,CAAC,CAAC;QAC1D,QAAQ,CAAC,SAAS,CAAC,eAAe,EAAE,UAAU,CAAC,CAAC;QAChD,QAAQ,CAAC,SAAS,CAAC,sBAAsB,EAAE,aAAa,CAAC,CAAC;QAC1D,QAAQ,CAAC,GAAG,EAAE,CAAC;IACjB,CAAC;IAED;;OAEG;IACK,KAAK,CAAC,iBAAiB,CAC7B,OAAwB,EACxB,QAAsB,EACtB,MAAc,EACd,GAAQ;QAER,IAAI,CAAC;YACH,MAAM,CAAC,QAAQ,EAAE,YAAY,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC;gBACjD,IAAI,CAAC,UAAU,CAAC,uBAAuB,CAAC,MAAM,CAAC;gBAC/C,IAAI,CAAC,UAAU,CAAC,eAAe,CAAC,MAAM,CAAC;aACxC,CAAC,CAAC;YAEH,IAAI,CAAC,QAAQ,EAAE,CAAC;gBACd,MAAM,IAAI,sCAAmB,CAAC,QAAQ,MAAM,+BAA+B,CAAC,CAAC;YAC/E,CAAC;YAED,MAAM,IAAI,GAAG,IAAI,CAAC,aAAa,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC;YACrD,IAAI,IAAI,KAAK,QAAQ,IAAI,QAAQ,CAAC,IAAI,EAAE,CAAC;gBACvC,MAAM,IAAI,CAAC,wBAAwB,CAAC,QAAQ,EAAE,QAAQ,EAAE,GAAG,CAAC,CAAC;YAC/D,CAAC;iBAAM,IAAI,IAAI,KAAK,OAAO,EAAE,CAAC;gBAC5B,MAAM,IAAI,CAAC,sBAAsB,CAAC,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,YAAY,EAAE,QAAQ,IAAI,IAAI,EAAE,GAAG,CAAC,CAAC;YAC9G,CAAC;iBAAM,CAAC;gBACN,MAAM,IAAI,sCAAmB,CAAC,QAAQ,MAAM,+BAA+B,QAAQ,CAAC,UAAU,IAAI,SAAS,GAAG,CAAC,CAAC;YAClH,CAAC;QACH,CAAC;QAAC,OAAO,KAAc,EAAE,CAAC;YACxB,IAAI,KAAK,YAAY,0CAAuB,EAAE,CAAC;gBAC7C,MAAM,KAAK,CAAC;YACd,CAAC;YACD,MAAM,IAAI,sCAAmB,CAAC,+BAA+B,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC;QACnF,CAAC;IACH,CAAC;IAED;;OAEG;IACK,KAAK,CAAC,wBAAwB,CACpC,QAAsB,EACtB,QAAyF,EACzF,GAAQ;QAER,MAAM,IAAI,GAAG,QAAQ,CAAC,UAAU,IAAI,QAAQ,CAAC,UAAU,KAAK,GAAG,CAAC,CAAC,CAAC,IAAI,QAAQ,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACjG,MAAM,aAAa,GAAG,WAAW,QAAQ,CAAC,IAAI,GAAG,IAAI,GAAG,GAAG,CAAC,QAAQ,GAAG,GAAG,CAAC,MAAM,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC;QAE/F,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,2CAA2C,aAAa,EAAE,CAAC,CAAC;QAE9E,QAAQ,CAAC,UAAU,GAAG,GAAG,CAAC;QAC1B,QAAQ,CAAC,SAAS,CAAC,UAAU,EAAE,aAAa,CAAC,CAAC;QAC9C,QAAQ,CAAC,SAAS,CAAC,eAAe,EAAE,UAAU,CAAC,CAAC;QAChD,QAAQ,CAAC,SAAS,CAAC,oBAAoB,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;QAC1D,QAAQ,CAAC,SAAS,CAAC,kBAAkB,EAAE,QAAQ,CAAC,IAAK,CAAC,CAAC;QACvD,QAAQ,CAAC,GAAG,EAAE,CAAC;IACjB,CAAC;IAED;;OAEG;IACK,KAAK,CAAC,sBAAsB,CAClC,OAAwB,EACxB,QAAsB,EACtB,MAAc,EACd,QAAyF,EACzF,QAAwC,EACxC,GAAQ;QAER,2CAA2C;QAC3C,MAAM,QAAQ,GAAG,IAAI,CAAC,eAAe,CAAC,QAAQ,CAAC,CAAC;QAChD,IAAI,CAAC,QAAQ,EAAE,CAAC;YACd,MAAM,IAAI,sCAAmB,CAAC,QAAQ,MAAM,6BAA6B,CAAC,CAAC;QAC7E,CAAC;QAED,MAAM,YAAY,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,CAAC;QACvC,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,QAAQ,GAAG,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;QAChE,MAAM,CAAC,IAAI,GAAG,GAAG,CAAC,IAAI,CAAC;QAEvB,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,eAAe,CAAC,OAAO,CAAC,CAAC;QACjD,MAAM,OAAO,GAAG,IAAI,CAAC,iBAAiB,CAAC,OAAO,EAAE,GAAG,EAAE,YAAY,CAAC,CAAC;QAEnE,IAAI,gBAA0B,CAAC;QAC/B,IAAI,CAAC;YACH,gBAAgB,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,QAAQ,EAAE,EAAE;gBACzD,MAAM,EAAE,CAAC,OAAO,CAAC,MAAM,IAAI,KAAK,CAAC,CAAC,WAAW,EAAE;gBAC/C,OAAO;gBACP,IAAI,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS;aACtC,CAAC,CAAC;QACL,CAAC;QAAC,OAAO,KAAc,EAAE,CAAC;YACxB,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,oBAAoB,MAAM,CAAC,QAAQ,EAAE,YAAY,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;YACpF,MAAM,IAAI,sCAAmB,CAAC,uCAAuC,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC;QAC3F,CAAC;QAED,mBAAmB;QACnB,QAAQ,CAAC,UAAU,GAAG,gBAAgB,CAAC,MAAM,CAAC;QAC9C,QAAQ,CAAC,SAAS,CAAC,mBAAmB,EAAE,MAAM,CAAC,CAAC;QAEhD,gBAAgB,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,GAAG,EAAE,EAAE;YAC9C,IAAI,GAAG,CAAC,WAAW,EAAE,KAAK,mBAAmB,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;gBACrE,OAAO,CAAC,8BAA8B;YACxC,CAAC;YACD,QAAQ,CAAC,SAAS,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;QACjC,CAAC,CAAC,CAAC;QAEH,IAAI,CAAC,gBAAgB,CAAC,IAAI,EAAE,CAAC;YAC3B,QAAQ,CAAC,GAAG,EAAE,CAAC;YACf,OAAO;QACT,CAAC;QAED,MAAM,QAAQ,GAAG,sBAAQ,CAAC,IAAI,CAAC,gBAAgB,CAAC,IAAW,CAAC,CAAC;QAC7D,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,EAAE;YAC7B,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,uBAAuB,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;YAC1D,QAAQ,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;QAC1B,CAAC,CAAC,CAAC;QACH,QAAQ,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IAC1B,CAAC;IAED;;OAEG;IACK,eAAe,CAAC,QAAyC;QAC/D,MAAM,MAAM,GAAG,QAAQ,EAAE,MAAM,CAAC;QAChC,IAAI,MAAM,IAAI,OAAO,MAAM,KAAK,QAAQ,EAAE,CAAC;YACzC,MAAM,UAAU,GAAI,MAAkC,CAAC,UAAU,CAAC;YAClE,IAAI,OAAO,UAAU,KAAK,QAAQ,IAAI,UAAU,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBACnE,OAAO,UAAU,CAAC;YACpB,CAAC;QACH,CAAC;QAED,yCAAyC;QACzC,IAAI,OAAO,QAAQ,EAAE,aAAa,KAAK,QAAQ,EAAE,CAAC;YAChD,OAAO,QAAQ,CAAC,aAAa,CAAC;QAChC,CAAC;QAED,OAAO,SAAS,CAAC;IACnB,CAAC;IAED;;OAEG;IACK,iBAAiB,CAAC,OAAwB,EAAE,QAAa,EAAE,QAAa;QAC9E,MAAM,OAAO,GAAG,IAAI,OAAO,EAAE,CAAC;QAE9B,uCAAuC;QACvC,KAAK,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;YAC5D,IAAI,KAAK,KAAK,SAAS,IAAI,IAAI,CAAC,WAAW,EAAE,KAAK,MAAM,EAAE,CAAC;gBACzD,SAAS;YACX,CAAC;YACD,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;gBACzB,OAAO,CAAC,GAAG,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;YACrC,CAAC;iBAAM,CAAC;gBACN,OAAO,CAAC,GAAG,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;YAC3B,CAAC;QACH,CAAC;QAED,yBAAyB;QACzB,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,QAAQ,CAAC,IAAI,CAAC,CAAC;QAEnC,yCAAyC;QACzC,OAAO,CAAC,GAAG,CAAC,kBAAkB,EAAE,QAAQ,CAAC,IAAI,CAAC,CAAC;QAC/C,OAAO,CAAC,GAAG,CAAC,mBAAmB,EAAE,QAAQ,CAAC,QAAQ,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,CAAC;QAEvE,MAAM,IAAI,GAAG,QAAQ,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;QAC9E,OAAO,CAAC,GAAG,CAAC,kBAAkB,EAAE,IAAI,CAAC,CAAC;QAEtC,6BAA6B;QAC7B,MAAM,aAAa,GAAI,OAAO,CAAC,MAAc,EAAE,aAAa,CAAC;QAC7D,IAAI,aAAa,EAAE,CAAC;YAClB,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,iBAAiB,CAAC,CAAC;YAChD,OAAO,CAAC,GAAG,CAAC,iBAAiB,EAAE,QAAQ,CAAC,CAAC,CAAC,GAAG,QAAQ,KAAK,aAAa,EAAE,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC;QAC7F,CAAC;QAED,OAAO,OAAO,CAAC;IACjB,CAAC;IAED;;OAEG;IACK,eAAe,CAAC,OAAwB;QAC9C,MAAM,MAAM,GAAG,CAAC,OAAO,CAAC,MAAM,IAAI,KAAK,CAAC,CAAC,WAAW,EAAE,CAAC;QACvD,IAAI,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;YACrC,OAAO,OAAO,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;QACpC,CAAC;QAED,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;YACrC,MAAM,MAAM,GAAa,EAAE,CAAC;YAC5B,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,KAAa,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC;YAC1D,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;YACxD,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;QAC9B,CAAC,CAAC,CAAC;IACL,CAAC;IAED;;OAEG;IACK,uBAAuB,CAAC,QAAgB;QAC9C,OAAO,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC,QAAQ,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,CAAC;IACxE,CAAC;IAED;;OAEG;IACK,2BAA2B,CAAC,OAAwB,EAAE,YAAoB;QAChF,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,iBAAiB,CAAC,EAAE,CAAC;YACxC,OAAO,CAAC,OAAO,CAAC,iBAAiB,CAAC,GAAG,YAAY,CAAC;QACpD,CAAC;QAED,OAAO,CAAC,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC,oBAAoB,CAAC;QACjD,OAAO,CAAC,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC,oBAAoB,CAAC;QAEjD,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,kBAAkB,CAAC,EAAE,CAAC;YACzC,OAAO,CAAC,OAAO,CAAC,kBAAkB,CAAC,GAAG,YAAY,CAAC;QACrD,CAAC;IACH,CAAC;IAED;;;OAGG;IACK,yBAAyB,CAAC,QAAgB;QAChD,MAAM,aAAa,GAAG,IAAI,IAAI,CAAC,oBAAoB,EAAE,CAAC;QACtD,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,aAAa,CAAC,EAAE,CAAC;YACtC,OAAO,SAAS,CAAC;QACnB,CAAC;QAED,MAAM,MAAM,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC;QACxD,8CAA8C;QAC9C,IAAI,CAAC,MAAM,IAAI,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC3D,OAAO,SAAS,CAAC;QACnB,CAAC;QAED,OAAO,MAAM,CAAC;IAChB,CAAC;IAED;;;OAGG;IACK,eAAe,CAAC,OAAwB;QAC9C,yEAAyE;QACzE,MAAM,YAAY,GAAG,OAAO,CAAC,OAAO,CAAC,iBAAiB,CAAC,CAAC;QACxD,IAAI,YAAY,IAAI,OAAO,YAAY,KAAK,QAAQ,EAAE,CAAC;YACrD,OAAO,YAAY,CAAC,WAAW,EAAE,CAAC;QACpC,CAAC;QAED,MAAM,UAAU,GAAG,OAAO,CAAC,OAAO,CAAC,IAAI,IAAI,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC;QAChE,IAAI,KAAK,CAAC,OAAO,CAAC,UAAU,CAAC,EAAE,CAAC;YAC9B,OAAO,UAAU,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,CAAC;QACtC,CAAC;QACD,OAAO,OAAO,UAAU,KAAK,QAAQ,CAAC,CAAC,CAAC,UAAU,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;IAC/E,CAAC;IAED;;OAEG;IACK,QAAQ,CAAC,OAAwB;QACvC,MAAM,UAAU,GAAG,OAAO,CAAC,OAAO,CAAC,IAAI,IAAI,OAAO,CAAC,OAAO,CAAC,IAAI,IAAI,WAAW,CAAC;QAC/E,MAAM,WAAW,GAAG,OAAO,CAAC,OAAO,CAAC,mBAAmB,CAAC,IAAI,OAAO,CAAC,OAAO,CAAC,mBAAmB,CAAC,CAAC;QACjG,MAAM,QAAQ,GAAG,KAAK,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC;QAC3E,MAAM,MAAM,GAAG,OAAO,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC;QAClG,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,IAAI,GAAG,CAAC;QAClC,OAAO,IAAI,GAAG,CAAC,MAAM,EAAE,GAAG,MAAM,MAAM,UAAU,EAAE,CAAC,CAAC;IACtD,CAAC;IAED;;OAEG;IACK,eAAe,CAAC,MAAc;QACpC,IAAI,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;YAC3B,IAAI,CAAC;gBACH,OAAO,IAAI,GAAG,CAAC,MAAM,CAAC,CAAC,QAAQ,CAAC,WAAW,EAAE,CAAC;YAChD,CAAC;YAAC,MAAM,CAAC;gBACP,OAAO,MAAM,CAAC,WAAW,EAAE,CAAC;YAC9B,CAAC;QACH,CAAC;QACD,OAAO,MAAM,CAAC,WAAW,EAAE,CAAC;IAC9B,CAAC;IAED;;OAEG;IACK,gBAAgB,CAAC,KAAwB;QAC/C,IAAI,OAAO,KAAK,KAAK,SAAS,EAAE,CAAC;YAC/B,OAAO,KAAK,CAAC;QACf,CAAC;QACD,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;YAC9B,MAAM,UAAU,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;YAC9C,OAAO,UAAU,KAAK,MAAM,IAAI,UAAU,KAAK,GAAG,IAAI,UAAU,KAAK,KAAK,IAAI,UAAU,KAAK,IAAI,CAAC;QACpG,CAAC;QACD,OAAO,KAAK,CAAC;IACf,CAAC;IAEO,aAAa,CAAC,IAAwB;QAC5C,IAAI,CAAC,IAAI,EAAE,CAAC;YACV,OAAO,SAAS,CAAC;QACnB,CAAC;QACD,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;QAC7C,oDAAoD;QACpD,IAAI,UAAU,KAAK,UAAU,IAAI,UAAU,KAAK,QAAQ,EAAE,CAAC;YACzD,OAAO,QAAQ,CAAC;QAClB,CAAC;QACD,IAAI,UAAU,KAAK,OAAO,EAAE,CAAC;YAC3B,OAAO,OAAO,CAAC;QACjB,CAAC;QACD,OAAO,SAAS,CAAC;IACnB,CAAC;CACF;AAtZD,oDAsZC","sourcesContent":["import type { IncomingMessage } from 'node:http';\nimport { getLoggerFor } from 'global-logger-factory';\nimport { Readable } from 'node:stream';\nimport { HttpHandler } from '@solid/community-server';\nimport type { HttpHandlerInput, HttpResponse } from '@solid/community-server';\nimport {\n NotImplementedHttpError,\n InternalServerError,\n \n} from '@solid/community-server';\nimport { getIdentityDatabase } from '../identity/drizzle/db';\nimport { EdgeNodeRepository } from '../identity/drizzle/EdgeNodeRepository';\n\ninterface ClusterIngressRouterOptions {\n identityDbUrl: string;\n edgeNodesEnabled?: string | boolean;\n repository?: EdgeNodeRepository;\n clusterIngressDomain: string; // cluster.example.com\n skipAuthRedirect?: boolean; // For testing\n fetchImpl?: any;\n}\n\n/**\n * Cluster Ingress Router - 集群统一入口路由器\n * \n * 实现我们设计的混合路由策略:\n * 1. 所有节点子域名DNS都指向集群入口\n * 2. 认证请求路由到集群IDP \n * 3. 数据请求根据节点模式智能路由(307重定向 vs 代理)\n * \n * Note: WebSocket 代理由 ClusterWebSocketConfigurator 处理\n */\nexport class ClusterIngressRouter extends HttpHandler {\n protected readonly logger = getLoggerFor(this);\n private readonly repository: EdgeNodeRepository;\n private readonly enabled: boolean;\n private readonly clusterIngressDomain: string;\n private readonly skipAuthRedirect: boolean;\n private readonly fetchImpl: any;\n\n // Authentication paths that should always route to cluster IDP\n private readonly authPaths = [\n '/idp/',\n '/.well-known/openid-configuration',\n '/.well-known/oauth-authorization-server', \n '/login',\n '/logout'\n ];\n\n public constructor(options: ClusterIngressRouterOptions) {\n super();\n this.repository = options.repository ?? new EdgeNodeRepository(getIdentityDatabase(options.identityDbUrl));\n this.enabled = this.normalizeBoolean(options.edgeNodesEnabled);\n this.clusterIngressDomain = this.normalizeDomain(options.clusterIngressDomain);\n this.skipAuthRedirect = options.skipAuthRedirect ?? false;\n this.fetchImpl = options.fetchImpl ?? fetch;\n }\n\n public override async canHandle({ request }: HttpHandlerInput): Promise<void> {\n this.logger.debug('ClusterIngressRouter.canHandle called');\n if (!this.enabled) {\n this.logger.debug('ClusterIngressRouter disabled');\n throw new NotImplementedHttpError('Cluster ingress router disabled.');\n }\n\n const hostname = this.extractHostname(request);\n if (!hostname) {\n throw new NotImplementedHttpError('Missing Host header.');\n }\n\n // Only handle requests to node subdomains, not the cluster domain itself\n if (hostname === this.clusterIngressDomain) {\n throw new NotImplementedHttpError('Request to cluster domain, not a node subdomain.');\n }\n\n // Check if this is a valid node subdomain\n const nodeId = this.extractNodeIdFromHostname(hostname);\n if (!nodeId) {\n throw new NotImplementedHttpError('Not a node subdomain pattern.');\n }\n\n // Verify the node exists\n const nodeSecret = await this.repository.getNodeSecret(nodeId);\n if (!nodeSecret) {\n throw new NotImplementedHttpError(`Node ${nodeId} not registered.`);\n }\n\n const url = this.parseUrl(request);\n if (this.isAuthenticationRequest(url.pathname)) {\n this.rewriteRequestForClusterIdp(request, hostname);\n throw new NotImplementedHttpError('Authentication routed to cluster IDP.');\n }\n }\n\n public override async handle({ request, response }: HttpHandlerInput): Promise<void> {\n const hostname = this.extractHostname(request)!;\n const url = this.parseUrl(request);\n const nodeId = this.extractNodeIdFromHostname(hostname)!;\n \n // Check if this is an authentication request\n if (this.isAuthenticationRequest(url.pathname)) {\n await this.handleAuthenticationRequest(request, response, url);\n return;\n }\n\n // Data request - route based on node access mode\n await this.handleDataRequest(request, response, nodeId, url);\n }\n\n /**\n * Handle authentication requests - always route to cluster IDP\n */\n private async handleAuthenticationRequest(\n request: IncomingMessage, \n response: HttpResponse, \n url: URL\n ): Promise<void> {\n if (this.skipAuthRedirect) {\n throw new NotImplementedHttpError('Auth routing skipped for testing.');\n }\n\n // Redirect authentication requests to cluster IDP\n const clusterAuthUrl = new URL(url.pathname + url.search + url.hash, `https://${this.clusterIngressDomain}`);\n \n this.logger.debug(`Routing auth request to cluster IDP: ${clusterAuthUrl.toString()}`);\n \n response.statusCode = 307;\n response.setHeader('Location', clusterAuthUrl.toString());\n response.setHeader('Cache-Control', 'no-cache');\n response.setHeader('X-Xpod-Auth-Redirect', 'cluster-idp');\n response.end();\n }\n\n /**\n * Handle data requests - route based on node access mode\n */\n private async handleDataRequest(\n request: IncomingMessage,\n response: HttpResponse, \n nodeId: string,\n url: URL\n ): Promise<void> {\n try {\n const [nodeInfo, nodeMetadata] = await Promise.all([\n this.repository.getNodeConnectivityInfo(nodeId),\n this.repository.getNodeMetadata(nodeId),\n ]);\n \n if (!nodeInfo) {\n throw new InternalServerError(`Node ${nodeId} connectivity info not found.`);\n }\n\n const mode = this.normalizeMode(nodeInfo.accessMode);\n if (mode === 'direct' && nodeInfo.ipv4) {\n await this.handleDirectModeRedirect(response, nodeInfo, url);\n } else if (mode === 'proxy') {\n await this.handleProxyModeRequest(request, response, nodeId, nodeInfo, nodeMetadata?.metadata || null, url);\n } else {\n throw new InternalServerError(`Node ${nodeId} has unsupported accessMode ${nodeInfo.accessMode ?? 'unknown'}.`);\n }\n } catch (error: unknown) {\n if (error instanceof NotImplementedHttpError) {\n throw error;\n }\n throw new InternalServerError('Failed to route data request.', { cause: error });\n }\n }\n\n /**\n * Handle direct mode - redirect to node's public IP\n */\n private async handleDirectModeRedirect(\n response: HttpResponse,\n nodeInfo: NonNullable<Awaited<ReturnType<EdgeNodeRepository['getNodeConnectivityInfo']>>>,\n url: URL\n ): Promise<void> {\n const port = nodeInfo.publicPort && nodeInfo.publicPort !== 443 ? `:${nodeInfo.publicPort}` : '';\n const nodeDirectUrl = `https://${nodeInfo.ipv4}${port}${url.pathname}${url.search}${url.hash}`;\n \n this.logger.debug(`Redirecting to edge node (direct mode): ${nodeDirectUrl}`);\n \n response.statusCode = 307;\n response.setHeader('Location', nodeDirectUrl);\n response.setHeader('Cache-Control', 'no-cache');\n response.setHeader('X-Xpod-Direct-Node', nodeInfo.nodeId);\n response.setHeader('X-Xpod-Target-IP', nodeInfo.ipv4!);\n response.end();\n }\n\n /**\n * Handle proxy mode - proxy the request through tunnel\n */\n private async handleProxyModeRequest(\n request: IncomingMessage,\n response: HttpResponse,\n nodeId: string,\n nodeInfo: NonNullable<Awaited<ReturnType<EdgeNodeRepository['getNodeConnectivityInfo']>>>,\n metadata: Record<string, unknown> | null,\n url: URL\n ): Promise<void> {\n // Get tunnel entrypoint from node metadata\n const upstream = this.resolveUpstream(metadata);\n if (!upstream) {\n throw new InternalServerError(`Node ${nodeId} tunnel endpoint not ready.`);\n }\n\n const upstreamBase = new URL(upstream);\n const target = new URL(url.pathname + url.search, upstreamBase);\n target.hash = url.hash;\n\n const body = await this.readRequestBody(request);\n const headers = this.buildProxyHeaders(request, url, upstreamBase);\n\n let upstreamResponse: Response;\n try {\n upstreamResponse = await this.fetchImpl(target.toString(), {\n method: (request.method ?? 'GET').toUpperCase(),\n headers,\n body: body?.length ? body : undefined,\n });\n } catch (error: unknown) {\n this.logger.error(`Proxy request to ${target.toString()} failed: ${String(error)}`);\n throw new InternalServerError('Failed to proxy request to edge node.', { cause: error });\n }\n\n // Forward response\n response.statusCode = upstreamResponse.status;\n response.setHeader('X-Xpod-Proxy-Node', nodeId);\n \n upstreamResponse.headers.forEach((value, key) => {\n if (key.toLowerCase() === 'transfer-encoding' && value === 'chunked') {\n return; // Let Node.js handle chunking\n }\n response.setHeader(key, value);\n });\n\n if (!upstreamResponse.body) {\n response.end();\n return;\n }\n\n const readable = Readable.from(upstreamResponse.body as any);\n readable.on('error', (error) => {\n this.logger.error(`Proxy stream error: ${String(error)}`);\n response.destroy(error);\n });\n readable.pipe(response);\n }\n\n /**\n * Resolve upstream endpoint from node metadata\n */\n private resolveUpstream(metadata?: Record<string, unknown> | null): string | undefined {\n const tunnel = metadata?.tunnel;\n if (tunnel && typeof tunnel === 'object') {\n const entrypoint = (tunnel as Record<string, unknown>).entrypoint;\n if (typeof entrypoint === 'string' && entrypoint.trim().length > 0) {\n return entrypoint;\n }\n }\n \n // Fallback to publicAddress if available\n if (typeof metadata?.publicAddress === 'string') {\n return metadata.publicAddress;\n }\n \n return undefined;\n }\n\n /**\n * Build headers for proxy request\n */\n private buildProxyHeaders(request: IncomingMessage, original: URL, upstream: URL): Headers {\n const headers = new Headers();\n \n // Forward original headers except host\n for (const [name, value] of Object.entries(request.headers)) {\n if (value === undefined || name.toLowerCase() === 'host') {\n continue;\n }\n if (Array.isArray(value)) {\n headers.set(name, value.join(','));\n } else {\n headers.set(name, value);\n }\n }\n \n // Set proper target host\n headers.set('host', upstream.host);\n \n // Add forwarded headers for transparency\n headers.set('x-forwarded-host', original.host);\n headers.set('x-forwarded-proto', original.protocol.replace(/:$/u, ''));\n \n const port = original.port || (original.protocol === 'https:' ? '443' : '80');\n headers.set('x-forwarded-port', port);\n \n // Add client IP if available\n const remoteAddress = (request.socket as any)?.remoteAddress;\n if (remoteAddress) {\n const existing = headers.get('x-forwarded-for');\n headers.set('x-forwarded-for', existing ? `${existing}, ${remoteAddress}` : remoteAddress);\n }\n \n return headers;\n }\n\n /**\n * Read request body for proxy forwarding\n */\n private readRequestBody(request: IncomingMessage): Promise<Buffer | undefined> {\n const method = (request.method ?? 'GET').toUpperCase();\n if (['GET', 'HEAD'].includes(method)) {\n return Promise.resolve(undefined);\n }\n \n return new Promise((resolve, reject) => {\n const chunks: Buffer[] = [];\n request.on('data', (chunk: Buffer) => chunks.push(chunk));\n request.on('end', () => resolve(Buffer.concat(chunks)));\n request.on('error', reject);\n });\n }\n\n /**\n * Check if request path is for authentication\n */\n private isAuthenticationRequest(pathname: string): boolean {\n return this.authPaths.some(authPath => pathname.startsWith(authPath));\n }\n\n /**\n * Rewrite the incoming request so downstream handlers treat it as cluster IDP traffic.\n */\n private rewriteRequestForClusterIdp(request: IncomingMessage, originalHost: string): void {\n if (!request.headers['x-original-host']) {\n request.headers['x-original-host'] = originalHost;\n }\n\n request.headers.host = this.clusterIngressDomain;\n request.headers.Host = this.clusterIngressDomain;\n\n if (!request.headers['x-forwarded-host']) {\n request.headers['x-forwarded-host'] = originalHost;\n }\n }\n\n /**\n * Extract node ID from hostname\n * e.g., \"node1.cluster.example.com\" -> \"node1\"\n */\n private extractNodeIdFromHostname(hostname: string): string | undefined {\n const clusterSuffix = `.${this.clusterIngressDomain}`;\n if (!hostname.endsWith(clusterSuffix)) {\n return undefined;\n }\n \n const nodeId = hostname.slice(0, -clusterSuffix.length);\n // Validate node ID format (simple validation)\n if (!nodeId || nodeId.includes('.') || nodeId.length === 0) {\n return undefined;\n }\n \n return nodeId;\n }\n\n /**\n * Extract hostname from request headers\n * Check for original host header first (set by ClusterHttpServerFactory)\n */\n private extractHostname(request: IncomingMessage): string | undefined {\n // Check for original host header first (set by ClusterHttpServerFactory)\n const originalHost = request.headers['x-original-host'];\n if (originalHost && typeof originalHost === 'string') {\n return originalHost.toLowerCase();\n }\n \n const hostHeader = request.headers.host || request.headers.Host;\n if (Array.isArray(hostHeader)) {\n return hostHeader[0]?.toLowerCase();\n }\n return typeof hostHeader === 'string' ? hostHeader.toLowerCase() : undefined;\n }\n\n /**\n * Parse request URL\n */\n private parseUrl(request: IncomingMessage): URL {\n const hostHeader = request.headers.host ?? request.headers.Host ?? 'localhost';\n const protoHeader = request.headers['x-forwarded-proto'] ?? request.headers['X-Forwarded-Proto'];\n const protocol = Array.isArray(protoHeader) ? protoHeader[0] : protoHeader;\n const scheme = typeof protocol === 'string' ? protocol.split(',')[0]?.trim() ?? 'https' : 'https';\n const rawUrl = request.url ?? '/';\n return new URL(rawUrl, `${scheme}://${hostHeader}`);\n }\n\n /**\n * Normalize domain input; accept bare host or full URL.\n */\n private normalizeDomain(domain: string): string {\n if (domain.includes('://')) {\n try {\n return new URL(domain).hostname.toLowerCase();\n } catch {\n return domain.toLowerCase();\n }\n }\n return domain.toLowerCase();\n }\n\n /**\n * Normalize boolean values from string/boolean\n */\n private normalizeBoolean(value?: string | boolean): boolean {\n if (typeof value === 'boolean') {\n return value;\n }\n if (typeof value === 'string') {\n const normalized = value.trim().toLowerCase();\n return normalized === 'true' || normalized === '1' || normalized === 'yes' || normalized === 'on';\n }\n return false;\n }\n\n private normalizeMode(mode: string | undefined): 'direct' | 'proxy' | undefined {\n if (!mode) {\n return undefined;\n }\n const normalized = mode.trim().toLowerCase();\n // Backward compatibility for 'redirect' -> 'direct'\n if (normalized === 'redirect' || normalized === 'direct') {\n return 'direct';\n }\n if (normalized === 'proxy') {\n return 'proxy';\n }\n return undefined;\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"ClusterIngressRouter.js","sourceRoot":"","sources":["../../src/http/ClusterIngressRouter.ts"],"names":[],"mappings":"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","sourcesContent":["import type { IncomingMessage } from 'node:http';\nimport { getLoggerFor } from 'global-logger-factory';\nimport { Readable } from 'node:stream';\nimport { HttpHandler } from '@solid/community-server';\nimport type { HttpHandlerInput, HttpResponse } from '@solid/community-server';\nimport {\n NotImplementedHttpError,\n InternalServerError,\n \n} from '@solid/community-server';\nimport { getIdentityDatabase } from '../identity/drizzle/db';\nimport { EdgeNodeRepository } from '../identity/drizzle/EdgeNodeRepository';\n\ninterface ClusterIngressRouterOptions {\n identityDbUrl: string;\n edgeNodesEnabled?: string | boolean;\n repository?: EdgeNodeRepository;\n clusterIngressDomain: string; // cluster.example.com\n skipAuthRedirect?: boolean; // For testing\n fetchImpl?: any;\n}\n\n/**\n * Cluster Ingress Router - 集群统一入口路由器\n * \n * 实现我们设计的混合路由策略:\n * 1. 所有节点子域名DNS都指向集群入口\n * 2. 认证请求路由到集群IDP \n * 3. 数据请求根据节点模式智能路由(307重定向 vs 代理)\n * \n * Note: WebSocket 代理由 ClusterWebSocketConfigurator 处理\n */\nexport class ClusterIngressRouter extends HttpHandler {\n protected readonly logger = getLoggerFor(this);\n private readonly repository: EdgeNodeRepository;\n private readonly enabled: boolean;\n private readonly clusterIngressDomain: string;\n private readonly skipAuthRedirect: boolean;\n private readonly fetchImpl: any;\n\n // Authentication paths that should always route to cluster IDP\n private readonly authPaths = [\n '/idp/',\n '/.well-known/openid-configuration',\n '/.well-known/oauth-authorization-server', \n '/login',\n '/logout'\n ];\n\n public constructor(options: ClusterIngressRouterOptions) {\n super();\n this.repository = options.repository ?? new EdgeNodeRepository(getIdentityDatabase(options.identityDbUrl));\n this.enabled = this.normalizeBoolean(options.edgeNodesEnabled);\n this.clusterIngressDomain = this.normalizeDomain(options.clusterIngressDomain);\n this.skipAuthRedirect = options.skipAuthRedirect ?? false;\n this.fetchImpl = options.fetchImpl ?? fetch;\n }\n\n public override async canHandle({ request }: HttpHandlerInput): Promise<void> {\n this.logger.debug('ClusterIngressRouter.canHandle called');\n if (!this.enabled) {\n this.logger.debug('ClusterIngressRouter disabled');\n throw new NotImplementedHttpError('Cluster ingress router disabled.');\n }\n\n const hostname = this.extractHostname(request);\n if (!hostname) {\n throw new NotImplementedHttpError('Missing Host header.');\n }\n\n // Only handle requests to node subdomains, not the cluster domain itself\n if (hostname === this.clusterIngressDomain) {\n throw new NotImplementedHttpError('Request to cluster domain, not a node subdomain.');\n }\n\n // Check if this is a valid node subdomain\n const nodeId = this.extractNodeIdFromHostname(hostname);\n if (!nodeId) {\n throw new NotImplementedHttpError('Not a node subdomain pattern.');\n }\n\n // Verify the node exists\n const nodeSecret = await this.repository.getNodeSecret(nodeId);\n if (!nodeSecret) {\n throw new NotImplementedHttpError(`Node ${nodeId} not registered.`);\n }\n\n const url = this.parseUrl(request);\n if (this.isAuthenticationRequest(url.pathname)) {\n this.rewriteRequestForClusterIdp(request, hostname);\n throw new NotImplementedHttpError('Authentication routed to cluster IDP.');\n }\n }\n\n public override async handle({ request, response }: HttpHandlerInput): Promise<void> {\n const hostname = this.extractHostname(request)!;\n const url = this.parseUrl(request);\n const nodeId = this.extractNodeIdFromHostname(hostname)!;\n \n // Check if this is an authentication request\n if (this.isAuthenticationRequest(url.pathname)) {\n await this.handleAuthenticationRequest(request, response, url);\n return;\n }\n\n // Data request - route based on node access mode\n await this.handleDataRequest(request, response, nodeId, url);\n }\n\n /**\n * Handle authentication requests - always route to cluster IDP\n */\n private async handleAuthenticationRequest(\n request: IncomingMessage, \n response: HttpResponse, \n url: URL\n ): Promise<void> {\n if (this.skipAuthRedirect) {\n throw new NotImplementedHttpError('Auth routing skipped for testing.');\n }\n\n // Redirect authentication requests to cluster IDP\n const clusterAuthUrl = new URL(url.pathname + url.search + url.hash, `https://${this.clusterIngressDomain}`);\n \n this.logger.debug(`Routing auth request to cluster IDP: ${clusterAuthUrl.toString()}`);\n \n response.statusCode = 307;\n response.setHeader('Location', clusterAuthUrl.toString());\n response.setHeader('Cache-Control', 'no-cache');\n response.setHeader('X-Xpod-Auth-Redirect', 'cluster-idp');\n response.end();\n }\n\n /**\n * Handle data requests - route based on node access mode\n */\n private async handleDataRequest(\n request: IncomingMessage,\n response: HttpResponse, \n nodeId: string,\n url: URL\n ): Promise<void> {\n try {\n const [nodeInfo, nodeMetadata] = await Promise.all([\n this.repository.getNodeConnectivityInfo(nodeId),\n this.repository.getNodeMetadata(nodeId),\n ]);\n \n if (!nodeInfo) {\n throw new InternalServerError(`Node ${nodeId} connectivity info not found.`);\n }\n\n const mode = this.normalizeMode(nodeInfo.accessMode);\n if (mode === 'direct' && nodeInfo.ipv4) {\n await this.handleDirectModeRedirect(response, nodeInfo, url);\n } else if (mode === 'proxy') {\n await this.handleProxyModeRequest(request, response, nodeId, nodeInfo, nodeMetadata?.metadata || null, url);\n } else {\n throw new InternalServerError(`Node ${nodeId} has unsupported accessMode ${nodeInfo.accessMode ?? 'unknown'}.`);\n }\n } catch (error: unknown) {\n if (error instanceof NotImplementedHttpError) {\n throw error;\n }\n throw new InternalServerError('Failed to route data request.', { cause: error });\n }\n }\n\n /**\n * Handle direct mode - redirect to node's public IP\n */\n private async handleDirectModeRedirect(\n response: HttpResponse,\n nodeInfo: NonNullable<Awaited<ReturnType<EdgeNodeRepository['getNodeConnectivityInfo']>>>,\n url: URL\n ): Promise<void> {\n const port = nodeInfo.publicPort && nodeInfo.publicPort !== 443 ? `:${nodeInfo.publicPort}` : '';\n const nodeDirectUrl = `https://${nodeInfo.ipv4}${port}${url.pathname}${url.search}${url.hash}`;\n \n this.logger.debug(`Redirecting to edge node (direct mode): ${nodeDirectUrl}`);\n \n response.statusCode = 307;\n response.setHeader('Location', nodeDirectUrl);\n response.setHeader('Cache-Control', 'no-cache');\n response.setHeader('X-Xpod-Direct-Node', nodeInfo.nodeId);\n response.setHeader('X-Xpod-Target-IP', nodeInfo.ipv4!);\n response.end();\n }\n\n /**\n * Handle proxy mode - proxy the request through tunnel\n */\n private async handleProxyModeRequest(\n request: IncomingMessage,\n response: HttpResponse,\n nodeId: string,\n nodeInfo: NonNullable<Awaited<ReturnType<EdgeNodeRepository['getNodeConnectivityInfo']>>>,\n metadata: Record<string, unknown> | null,\n url: URL\n ): Promise<void> {\n // Get tunnel entrypoint from node metadata\n const upstream = this.resolveUpstream(metadata);\n if (!upstream) {\n throw new InternalServerError(`Node ${nodeId} tunnel endpoint not ready.`);\n }\n\n const upstreamBase = new URL(upstream);\n const target = new URL(url.pathname + url.search, upstreamBase);\n target.hash = url.hash;\n\n const body = await this.readRequestBody(request);\n const headers = this.buildProxyHeaders(request, url, upstreamBase);\n\n let upstreamResponse: Response;\n try {\n upstreamResponse = await this.fetchImpl(target.toString(), {\n method: (request.method ?? 'GET').toUpperCase(),\n headers,\n body: body?.length ? body : undefined,\n });\n } catch (error: unknown) {\n this.logger.error(`Proxy request to ${target.toString()} failed: ${String(error)}`);\n throw new InternalServerError('Failed to proxy request to edge node.', { cause: error });\n }\n\n // Forward response\n response.statusCode = upstreamResponse.status;\n response.setHeader('X-Xpod-Proxy-Node', nodeId);\n \n upstreamResponse.headers.forEach((value, key) => {\n if (key.toLowerCase() === 'transfer-encoding' && value === 'chunked') {\n return; // Let Node.js handle chunking\n }\n response.setHeader(key, value);\n });\n\n if (!upstreamResponse.body) {\n response.end();\n return;\n }\n\n const readable = Readable.from(upstreamResponse.body as any);\n readable.on('error', (error) => {\n this.logger.error(`Proxy stream error: ${String(error)}`);\n response.destroy(error);\n });\n readable.pipe(response);\n }\n\n /**\n * Resolve upstream endpoint from node metadata\n */\n private resolveUpstream(metadata?: Record<string, unknown> | null): string | undefined {\n const tunnel = metadata?.tunnel;\n if (tunnel && typeof tunnel === 'object') {\n const entrypoint = (tunnel as Record<string, unknown>).entrypoint;\n if (typeof entrypoint === 'string' && entrypoint.trim().length > 0) {\n return entrypoint;\n }\n }\n \n const managedTunnel = metadata?.managedTunnel;\n if (managedTunnel && typeof managedTunnel === 'object') {\n const endpoint = (managedTunnel as Record<string, unknown>).endpoint;\n if (typeof endpoint === 'string' && endpoint.trim().length > 0) {\n return endpoint;\n }\n }\n return undefined;\n }\n\n /**\n * Build headers for proxy request\n */\n private buildProxyHeaders(request: IncomingMessage, original: URL, upstream: URL): Headers {\n const headers = new Headers();\n \n // Forward original headers except host\n for (const [name, value] of Object.entries(request.headers)) {\n if (value === undefined || name.toLowerCase() === 'host') {\n continue;\n }\n if (Array.isArray(value)) {\n headers.set(name, value.join(','));\n } else {\n headers.set(name, value);\n }\n }\n \n // Set proper target host\n headers.set('host', upstream.host);\n \n // Add forwarded headers for transparency\n headers.set('x-forwarded-host', original.host);\n headers.set('x-forwarded-proto', original.protocol.replace(/:$/u, ''));\n \n const port = original.port || (original.protocol === 'https:' ? '443' : '80');\n headers.set('x-forwarded-port', port);\n \n // Add client IP if available\n const remoteAddress = (request.socket as any)?.remoteAddress;\n if (remoteAddress) {\n const existing = headers.get('x-forwarded-for');\n headers.set('x-forwarded-for', existing ? `${existing}, ${remoteAddress}` : remoteAddress);\n }\n \n return headers;\n }\n\n /**\n * Read request body for proxy forwarding\n */\n private readRequestBody(request: IncomingMessage): Promise<Buffer | undefined> {\n const method = (request.method ?? 'GET').toUpperCase();\n if (['GET', 'HEAD'].includes(method)) {\n return Promise.resolve(undefined);\n }\n \n return new Promise((resolve, reject) => {\n const chunks: Buffer[] = [];\n request.on('data', (chunk: Buffer) => chunks.push(chunk));\n request.on('end', () => resolve(Buffer.concat(chunks)));\n request.on('error', reject);\n });\n }\n\n /**\n * Check if request path is for authentication\n */\n private isAuthenticationRequest(pathname: string): boolean {\n return this.authPaths.some(authPath => pathname.startsWith(authPath));\n }\n\n /**\n * Rewrite the incoming request so downstream handlers treat it as cluster IDP traffic.\n */\n private rewriteRequestForClusterIdp(request: IncomingMessage, originalHost: string): void {\n if (!request.headers['x-original-host']) {\n request.headers['x-original-host'] = originalHost;\n }\n\n request.headers.host = this.clusterIngressDomain;\n request.headers.Host = this.clusterIngressDomain;\n\n if (!request.headers['x-forwarded-host']) {\n request.headers['x-forwarded-host'] = originalHost;\n }\n }\n\n /**\n * Extract node ID from hostname\n * e.g., \"node1.cluster.example.com\" -> \"node1\"\n */\n private extractNodeIdFromHostname(hostname: string): string | undefined {\n const clusterSuffix = `.${this.clusterIngressDomain}`;\n if (!hostname.endsWith(clusterSuffix)) {\n return undefined;\n }\n \n const nodeId = hostname.slice(0, -clusterSuffix.length);\n // Validate node ID format (simple validation)\n if (!nodeId || nodeId.includes('.') || nodeId.length === 0) {\n return undefined;\n }\n \n return nodeId;\n }\n\n /**\n * Extract hostname from request headers\n * Check for original host header first (set by ClusterHttpServerFactory)\n */\n private extractHostname(request: IncomingMessage): string | undefined {\n // Check for original host header first (set by ClusterHttpServerFactory)\n const originalHost = request.headers['x-original-host'];\n if (originalHost && typeof originalHost === 'string') {\n return originalHost.toLowerCase();\n }\n \n const hostHeader = request.headers.host || request.headers.Host;\n if (Array.isArray(hostHeader)) {\n return hostHeader[0]?.toLowerCase();\n }\n return typeof hostHeader === 'string' ? hostHeader.toLowerCase() : undefined;\n }\n\n /**\n * Parse request URL\n */\n private parseUrl(request: IncomingMessage): URL {\n const hostHeader = request.headers.host ?? request.headers.Host ?? 'localhost';\n const protoHeader = request.headers['x-forwarded-proto'] ?? request.headers['X-Forwarded-Proto'];\n const protocol = Array.isArray(protoHeader) ? protoHeader[0] : protoHeader;\n const scheme = typeof protocol === 'string' ? protocol.split(',')[0]?.trim() ?? 'https' : 'https';\n const rawUrl = request.url ?? '/';\n return new URL(rawUrl, `${scheme}://${hostHeader}`);\n }\n\n /**\n * Normalize domain input; accept bare host or full URL.\n */\n private normalizeDomain(domain: string): string {\n if (domain.includes('://')) {\n try {\n return new URL(domain).hostname.toLowerCase();\n } catch {\n return domain.toLowerCase();\n }\n }\n return domain.toLowerCase();\n }\n\n /**\n * Normalize boolean values from string/boolean\n */\n private normalizeBoolean(value?: string | boolean): boolean {\n if (typeof value === 'boolean') {\n return value;\n }\n if (typeof value === 'string') {\n const normalized = value.trim().toLowerCase();\n return normalized === 'true' || normalized === '1' || normalized === 'yes' || normalized === 'on';\n }\n return false;\n }\n\n private normalizeMode(mode: string | undefined): 'direct' | 'proxy' | undefined {\n if (!mode) {\n return undefined;\n }\n const normalized = mode.trim().toLowerCase();\n // Backward compatibility for 'redirect' -> 'direct'\n if (normalized === 'redirect' || normalized === 'direct') {\n return 'direct';\n }\n if (normalized === 'proxy') {\n return 'proxy';\n }\n return undefined;\n }\n}\n"]}
|
|
@@ -145,8 +145,12 @@ class ClusterWebSocketConfigurator {
|
|
|
145
145
|
return entrypoint;
|
|
146
146
|
}
|
|
147
147
|
}
|
|
148
|
-
|
|
149
|
-
|
|
148
|
+
const managedTunnel = metadata?.managedTunnel;
|
|
149
|
+
if (managedTunnel && typeof managedTunnel === 'object') {
|
|
150
|
+
const endpoint = managedTunnel.endpoint;
|
|
151
|
+
if (typeof endpoint === 'string' && endpoint.trim().length > 0) {
|
|
152
|
+
return endpoint;
|
|
153
|
+
}
|
|
150
154
|
}
|
|
151
155
|
return undefined;
|
|
152
156
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ClusterWebSocketConfigurator.js","sourceRoot":"","sources":["../../src/http/ClusterWebSocketConfigurator.ts"],"names":[],"mappings":"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","sourcesContent":["import type { IncomingMessage, Server } from 'node:http';\nimport type { Duplex } from 'node:stream';\nimport httpProxy from 'http-proxy';\nimport { getLoggerFor } from 'global-logger-factory';\nimport { getIdentityDatabase } from '../identity/drizzle/db';\nimport { EdgeNodeRepository } from '../identity/drizzle/EdgeNodeRepository';\n\ninterface ClusterWebSocketConfiguratorOptions {\n identityDbUrl: string;\n edgeNodesEnabled?: string | boolean;\n repository?: EdgeNodeRepository;\n clusterIngressDomain: string;\n}\n\n/**\n * ServerConfigurator that handles WebSocket upgrade requests for edge nodes.\n * \n * For proxy mode: proxies WebSocket connections through FRP tunnel\n * For direct mode: sends 307 redirect to edge node's public IP\n */\nexport class ClusterWebSocketConfigurator {\n protected readonly logger = getLoggerFor(this);\n private readonly repository: EdgeNodeRepository;\n private readonly enabled: boolean;\n private readonly clusterIngressDomain: string;\n private readonly wsProxy: httpProxy;\n\n public constructor(options: ClusterWebSocketConfiguratorOptions) {\n this.repository = options.repository ?? new EdgeNodeRepository(getIdentityDatabase(options.identityDbUrl));\n this.enabled = this.normalizeBoolean(options.edgeNodesEnabled);\n this.clusterIngressDomain = this.normalizeDomain(options.clusterIngressDomain);\n \n // Create WebSocket proxy instance\n this.wsProxy = httpProxy.createProxyServer({\n ws: true,\n changeOrigin: true,\n xfwd: true,\n });\n \n this.wsProxy.on('error', (err, req, res) => {\n this.logger.error(`WebSocket proxy error: ${String(err)}`);\n if (res && 'end' in res && typeof res.end === 'function') {\n res.end();\n }\n });\n }\n\n /**\n * Attach to HTTP server's upgrade event\n */\n public async handle(server: Server): Promise<void> {\n if (!this.enabled) {\n this.logger.info('ClusterWebSocketConfigurator disabled');\n return;\n }\n\n // Prepend our handler to run before CSS's WebSocketServerConfigurator\n server.prependListener('upgrade', (request: IncomingMessage, socket: Duplex, head: Buffer) => {\n this.handleUpgrade(request, socket, head).catch((error) => {\n this.logger.error(`WebSocket upgrade error: ${String(error)}`);\n // Don't destroy socket here - let other handlers try\n });\n });\n\n this.logger.info('ClusterWebSocketConfigurator attached to server');\n }\n\n /**\n * Handle WebSocket upgrade request\n */\n private async handleUpgrade(\n request: IncomingMessage,\n socket: Duplex,\n head: Buffer\n ): Promise<boolean> {\n const hostname = this.extractHostname(request);\n if (!hostname) {\n return false; // Let other handlers deal with it\n }\n\n // Only handle requests to node subdomains\n if (hostname === this.clusterIngressDomain) {\n return false; // Cluster domain - let CSS handle it\n }\n\n const nodeId = this.extractNodeIdFromHostname(hostname);\n if (!nodeId) {\n return false; // Not a node subdomain\n }\n\n // Verify node exists\n const nodeSecret = await this.repository.getNodeSecret(nodeId);\n if (!nodeSecret) {\n this.logger.warn(`WebSocket upgrade: Node ${nodeId} not registered`);\n this.sendUpgradeError(socket, 404, `Node ${nodeId} not found`);\n return true; // We handled it (with error)\n }\n\n // Get node info\n const [nodeInfo, nodeMetadata] = await Promise.all([\n this.repository.getNodeConnectivityInfo(nodeId),\n this.repository.getNodeMetadata(nodeId),\n ]);\n\n if (!nodeInfo) {\n this.logger.error(`WebSocket upgrade: Node ${nodeId} connectivity info not found`);\n this.sendUpgradeError(socket, 502, 'Node connectivity info not found');\n return true;\n }\n\n const mode = this.normalizeMode(nodeInfo.accessMode);\n\n if (mode === 'direct' && nodeInfo.ipv4) {\n // Direct mode: redirect client to connect directly\n const port = nodeInfo.publicPort && nodeInfo.publicPort !== 443 ? `:${nodeInfo.publicPort}` : '';\n const directUrl = `wss://${nodeInfo.ipv4}${port}${request.url ?? '/'}`;\n\n this.logger.info(`WebSocket direct mode: redirecting to ${directUrl}`);\n \n socket.write(\n `HTTP/1.1 307 Temporary Redirect\\r\\n` +\n `Location: ${directUrl}\\r\\n` +\n `X-Xpod-Direct-Node: ${nodeId}\\r\\n` +\n `Connection: close\\r\\n` +\n `\\r\\n`\n );\n socket.end();\n return true;\n }\n\n if (mode === 'proxy') {\n const upstream = this.resolveUpstream(nodeMetadata?.metadata || null);\n if (!upstream) {\n this.logger.error(`WebSocket upgrade: Node ${nodeId} tunnel endpoint not ready`);\n this.sendUpgradeError(socket, 502, 'Node tunnel not ready');\n return true;\n }\n\n const upstreamUrl = new URL(upstream);\n const wsProtocol = upstreamUrl.protocol === 'https:' ? 'wss:' : 'ws:';\n const target = `${wsProtocol}//${upstreamUrl.host}`;\n\n this.logger.info(`WebSocket proxy: ${hostname} -> ${target}${request.url}`);\n\n // Add forwarded headers\n request.headers['x-forwarded-host'] = hostname;\n request.headers['x-forwarded-proto'] = 'wss';\n request.headers['x-xpod-proxy-node'] = nodeId;\n\n // Proxy the WebSocket connection\n this.wsProxy.ws(request, socket, head, {\n target,\n secure: true,\n });\n return true;\n }\n\n this.logger.warn(`WebSocket upgrade: Unsupported mode ${mode} for node ${nodeId}`);\n this.sendUpgradeError(socket, 400, `Unsupported access mode: ${mode}`);\n return true;\n }\n\n /**\n * Send HTTP error response for WebSocket upgrade failure\n */\n private sendUpgradeError(socket: Duplex, statusCode: number, message: string): void {\n socket.write(\n `HTTP/1.1 ${statusCode} ${message}\\r\\n` +\n `Content-Type: text/plain\\r\\n` +\n `Connection: close\\r\\n` +\n `\\r\\n` +\n message\n );\n socket.end();\n }\n\n /**\n * Resolve upstream endpoint from node metadata\n */\n private resolveUpstream(metadata?: Record<string, unknown> | null): string | undefined {\n const tunnel = metadata?.tunnel;\n if (tunnel && typeof tunnel === 'object') {\n const entrypoint = (tunnel as Record<string, unknown>).entrypoint;\n if (typeof entrypoint === 'string' && entrypoint.trim().length > 0) {\n return entrypoint;\n }\n }\n if (typeof metadata?.publicAddress === 'string') {\n return metadata.publicAddress;\n }\n return undefined;\n }\n\n /**\n * Extract hostname from request headers\n */\n private extractHostname(request: IncomingMessage): string | undefined {\n const originalHost = request.headers['x-original-host'];\n if (originalHost && typeof originalHost === 'string') {\n return originalHost.toLowerCase();\n }\n const hostHeader = request.headers.host || request.headers.Host;\n if (Array.isArray(hostHeader)) {\n return hostHeader[0]?.toLowerCase();\n }\n return typeof hostHeader === 'string' ? hostHeader.toLowerCase() : undefined;\n }\n\n /**\n * Extract node ID from hostname\n */\n private extractNodeIdFromHostname(hostname: string): string | undefined {\n const clusterSuffix = `.${this.clusterIngressDomain}`;\n if (!hostname.endsWith(clusterSuffix)) {\n return undefined;\n }\n const nodeId = hostname.slice(0, -clusterSuffix.length);\n if (!nodeId || nodeId.includes('.') || nodeId.length === 0) {\n return undefined;\n }\n return nodeId;\n }\n\n /**\n * Normalize domain input\n */\n private normalizeDomain(domain: string): string {\n if (domain.includes('://')) {\n try {\n return new URL(domain).hostname.toLowerCase();\n } catch {\n return domain.toLowerCase();\n }\n }\n return domain.toLowerCase();\n }\n\n /**\n * Normalize boolean values\n */\n private normalizeBoolean(value?: string | boolean): boolean {\n if (typeof value === 'boolean') {\n return value;\n }\n if (typeof value === 'string') {\n const normalized = value.trim().toLowerCase();\n return normalized === 'true' || normalized === '1' || normalized === 'yes' || normalized === 'on';\n }\n return false;\n }\n\n /**\n * Normalize access mode\n */\n private normalizeMode(mode: string | undefined): 'direct' | 'proxy' | undefined {\n if (!mode) {\n return undefined;\n }\n const normalized = mode.trim().toLowerCase();\n if (normalized === 'redirect' || normalized === 'direct') {\n return 'direct';\n }\n if (normalized === 'proxy') {\n return 'proxy';\n }\n return undefined;\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"ClusterWebSocketConfigurator.js","sourceRoot":"","sources":["../../src/http/ClusterWebSocketConfigurator.ts"],"names":[],"mappings":"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","sourcesContent":["import type { IncomingMessage, Server } from 'node:http';\nimport type { Duplex } from 'node:stream';\nimport httpProxy from 'http-proxy';\nimport { getLoggerFor } from 'global-logger-factory';\nimport { getIdentityDatabase } from '../identity/drizzle/db';\nimport { EdgeNodeRepository } from '../identity/drizzle/EdgeNodeRepository';\n\ninterface ClusterWebSocketConfiguratorOptions {\n identityDbUrl: string;\n edgeNodesEnabled?: string | boolean;\n repository?: EdgeNodeRepository;\n clusterIngressDomain: string;\n}\n\n/**\n * ServerConfigurator that handles WebSocket upgrade requests for edge nodes.\n * \n * For proxy mode: proxies WebSocket connections through FRP tunnel\n * For direct mode: sends 307 redirect to edge node's public IP\n */\nexport class ClusterWebSocketConfigurator {\n protected readonly logger = getLoggerFor(this);\n private readonly repository: EdgeNodeRepository;\n private readonly enabled: boolean;\n private readonly clusterIngressDomain: string;\n private readonly wsProxy: httpProxy;\n\n public constructor(options: ClusterWebSocketConfiguratorOptions) {\n this.repository = options.repository ?? new EdgeNodeRepository(getIdentityDatabase(options.identityDbUrl));\n this.enabled = this.normalizeBoolean(options.edgeNodesEnabled);\n this.clusterIngressDomain = this.normalizeDomain(options.clusterIngressDomain);\n \n // Create WebSocket proxy instance\n this.wsProxy = httpProxy.createProxyServer({\n ws: true,\n changeOrigin: true,\n xfwd: true,\n });\n \n this.wsProxy.on('error', (err, req, res) => {\n this.logger.error(`WebSocket proxy error: ${String(err)}`);\n if (res && 'end' in res && typeof res.end === 'function') {\n res.end();\n }\n });\n }\n\n /**\n * Attach to HTTP server's upgrade event\n */\n public async handle(server: Server): Promise<void> {\n if (!this.enabled) {\n this.logger.info('ClusterWebSocketConfigurator disabled');\n return;\n }\n\n // Prepend our handler to run before CSS's WebSocketServerConfigurator\n server.prependListener('upgrade', (request: IncomingMessage, socket: Duplex, head: Buffer) => {\n this.handleUpgrade(request, socket, head).catch((error) => {\n this.logger.error(`WebSocket upgrade error: ${String(error)}`);\n // Don't destroy socket here - let other handlers try\n });\n });\n\n this.logger.info('ClusterWebSocketConfigurator attached to server');\n }\n\n /**\n * Handle WebSocket upgrade request\n */\n private async handleUpgrade(\n request: IncomingMessage,\n socket: Duplex,\n head: Buffer\n ): Promise<boolean> {\n const hostname = this.extractHostname(request);\n if (!hostname) {\n return false; // Let other handlers deal with it\n }\n\n // Only handle requests to node subdomains\n if (hostname === this.clusterIngressDomain) {\n return false; // Cluster domain - let CSS handle it\n }\n\n const nodeId = this.extractNodeIdFromHostname(hostname);\n if (!nodeId) {\n return false; // Not a node subdomain\n }\n\n // Verify node exists\n const nodeSecret = await this.repository.getNodeSecret(nodeId);\n if (!nodeSecret) {\n this.logger.warn(`WebSocket upgrade: Node ${nodeId} not registered`);\n this.sendUpgradeError(socket, 404, `Node ${nodeId} not found`);\n return true; // We handled it (with error)\n }\n\n // Get node info\n const [nodeInfo, nodeMetadata] = await Promise.all([\n this.repository.getNodeConnectivityInfo(nodeId),\n this.repository.getNodeMetadata(nodeId),\n ]);\n\n if (!nodeInfo) {\n this.logger.error(`WebSocket upgrade: Node ${nodeId} connectivity info not found`);\n this.sendUpgradeError(socket, 502, 'Node connectivity info not found');\n return true;\n }\n\n const mode = this.normalizeMode(nodeInfo.accessMode);\n\n if (mode === 'direct' && nodeInfo.ipv4) {\n // Direct mode: redirect client to connect directly\n const port = nodeInfo.publicPort && nodeInfo.publicPort !== 443 ? `:${nodeInfo.publicPort}` : '';\n const directUrl = `wss://${nodeInfo.ipv4}${port}${request.url ?? '/'}`;\n\n this.logger.info(`WebSocket direct mode: redirecting to ${directUrl}`);\n \n socket.write(\n `HTTP/1.1 307 Temporary Redirect\\r\\n` +\n `Location: ${directUrl}\\r\\n` +\n `X-Xpod-Direct-Node: ${nodeId}\\r\\n` +\n `Connection: close\\r\\n` +\n `\\r\\n`\n );\n socket.end();\n return true;\n }\n\n if (mode === 'proxy') {\n const upstream = this.resolveUpstream(nodeMetadata?.metadata || null);\n if (!upstream) {\n this.logger.error(`WebSocket upgrade: Node ${nodeId} tunnel endpoint not ready`);\n this.sendUpgradeError(socket, 502, 'Node tunnel not ready');\n return true;\n }\n\n const upstreamUrl = new URL(upstream);\n const wsProtocol = upstreamUrl.protocol === 'https:' ? 'wss:' : 'ws:';\n const target = `${wsProtocol}//${upstreamUrl.host}`;\n\n this.logger.info(`WebSocket proxy: ${hostname} -> ${target}${request.url}`);\n\n // Add forwarded headers\n request.headers['x-forwarded-host'] = hostname;\n request.headers['x-forwarded-proto'] = 'wss';\n request.headers['x-xpod-proxy-node'] = nodeId;\n\n // Proxy the WebSocket connection\n this.wsProxy.ws(request, socket, head, {\n target,\n secure: true,\n });\n return true;\n }\n\n this.logger.warn(`WebSocket upgrade: Unsupported mode ${mode} for node ${nodeId}`);\n this.sendUpgradeError(socket, 400, `Unsupported access mode: ${mode}`);\n return true;\n }\n\n /**\n * Send HTTP error response for WebSocket upgrade failure\n */\n private sendUpgradeError(socket: Duplex, statusCode: number, message: string): void {\n socket.write(\n `HTTP/1.1 ${statusCode} ${message}\\r\\n` +\n `Content-Type: text/plain\\r\\n` +\n `Connection: close\\r\\n` +\n `\\r\\n` +\n message\n );\n socket.end();\n }\n\n /**\n * Resolve upstream endpoint from node metadata\n */\n private resolveUpstream(metadata?: Record<string, unknown> | null): string | undefined {\n const tunnel = metadata?.tunnel;\n if (tunnel && typeof tunnel === 'object') {\n const entrypoint = (tunnel as Record<string, unknown>).entrypoint;\n if (typeof entrypoint === 'string' && entrypoint.trim().length > 0) {\n return entrypoint;\n }\n }\n const managedTunnel = metadata?.managedTunnel;\n if (managedTunnel && typeof managedTunnel === 'object') {\n const endpoint = (managedTunnel as Record<string, unknown>).endpoint;\n if (typeof endpoint === 'string' && endpoint.trim().length > 0) {\n return endpoint;\n }\n }\n return undefined;\n }\n\n /**\n * Extract hostname from request headers\n */\n private extractHostname(request: IncomingMessage): string | undefined {\n const originalHost = request.headers['x-original-host'];\n if (originalHost && typeof originalHost === 'string') {\n return originalHost.toLowerCase();\n }\n const hostHeader = request.headers.host || request.headers.Host;\n if (Array.isArray(hostHeader)) {\n return hostHeader[0]?.toLowerCase();\n }\n return typeof hostHeader === 'string' ? hostHeader.toLowerCase() : undefined;\n }\n\n /**\n * Extract node ID from hostname\n */\n private extractNodeIdFromHostname(hostname: string): string | undefined {\n const clusterSuffix = `.${this.clusterIngressDomain}`;\n if (!hostname.endsWith(clusterSuffix)) {\n return undefined;\n }\n const nodeId = hostname.slice(0, -clusterSuffix.length);\n if (!nodeId || nodeId.includes('.') || nodeId.length === 0) {\n return undefined;\n }\n return nodeId;\n }\n\n /**\n * Normalize domain input\n */\n private normalizeDomain(domain: string): string {\n if (domain.includes('://')) {\n try {\n return new URL(domain).hostname.toLowerCase();\n } catch {\n return domain.toLowerCase();\n }\n }\n return domain.toLowerCase();\n }\n\n /**\n * Normalize boolean values\n */\n private normalizeBoolean(value?: string | boolean): boolean {\n if (typeof value === 'boolean') {\n return value;\n }\n if (typeof value === 'string') {\n const normalized = value.trim().toLowerCase();\n return normalized === 'true' || normalized === '1' || normalized === 'yes' || normalized === 'on';\n }\n return false;\n }\n\n /**\n * Normalize access mode\n */\n private normalizeMode(mode: string | undefined): 'direct' | 'proxy' | undefined {\n if (!mode) {\n return undefined;\n }\n const normalized = mode.trim().toLowerCase();\n if (normalized === 'redirect' || normalized === 'direct') {\n return 'direct';\n }\n if (normalized === 'proxy') {\n return 'proxy';\n }\n return undefined;\n }\n}\n"]}
|
|
@@ -18,6 +18,8 @@ export declare class EdgeNodeDirectDebugHttpHandler extends HttpHandler {
|
|
|
18
18
|
private resolve;
|
|
19
19
|
private shouldSkip;
|
|
20
20
|
private resolveTarget;
|
|
21
|
+
private extractTunnelEntrypoint;
|
|
22
|
+
private extractManagedTunnelEndpoint;
|
|
21
23
|
private extractUrl;
|
|
22
24
|
private parseUrl;
|
|
23
25
|
private normalizeBoolean;
|
|
@@ -59,7 +59,7 @@ class EdgeNodeDirectDebugHttpHandler extends community_server_1.HttpHandler {
|
|
|
59
59
|
this.logger.warn(`Request reached cluster for proxy-mode node ${record.nodeId}, should be handled by L4 SNI proxy`);
|
|
60
60
|
return undefined;
|
|
61
61
|
}
|
|
62
|
-
const target = this.resolveTarget(record.metadata ?? {}, original, record.baseUrl);
|
|
62
|
+
const target = this.resolveTarget(record.metadata ?? {}, original, record.baseUrl, record.publicUrl);
|
|
63
63
|
if (!target) {
|
|
64
64
|
return undefined;
|
|
65
65
|
}
|
|
@@ -68,8 +68,11 @@ class EdgeNodeDirectDebugHttpHandler extends community_server_1.HttpHandler {
|
|
|
68
68
|
shouldSkip(pathname) {
|
|
69
69
|
return this.skipPrefixes.some((prefix) => pathname.startsWith(prefix));
|
|
70
70
|
}
|
|
71
|
-
resolveTarget(metadata, original, podBaseUrl) {
|
|
72
|
-
const baseUrl = this.extractUrl(
|
|
71
|
+
resolveTarget(metadata, original, podBaseUrl, publicUrl) {
|
|
72
|
+
const baseUrl = this.extractUrl(publicUrl)
|
|
73
|
+
?? this.extractUrl(this.extractTunnelEntrypoint(metadata.tunnel))
|
|
74
|
+
?? this.extractUrl(this.extractManagedTunnelEndpoint(metadata.managedTunnel))
|
|
75
|
+
?? this.extractUrl(metadata.baseUrl);
|
|
73
76
|
if (!baseUrl) {
|
|
74
77
|
return undefined;
|
|
75
78
|
}
|
|
@@ -88,6 +91,18 @@ class EdgeNodeDirectDebugHttpHandler extends community_server_1.HttpHandler {
|
|
|
88
91
|
target.hash = original.hash;
|
|
89
92
|
return target;
|
|
90
93
|
}
|
|
94
|
+
extractTunnelEntrypoint(value) {
|
|
95
|
+
if (!value || typeof value !== 'object') {
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
return value.entrypoint;
|
|
99
|
+
}
|
|
100
|
+
extractManagedTunnelEndpoint(value) {
|
|
101
|
+
if (!value || typeof value !== 'object') {
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
return value.endpoint;
|
|
105
|
+
}
|
|
91
106
|
extractUrl(value) {
|
|
92
107
|
if (typeof value !== 'string') {
|
|
93
108
|
return undefined;
|