| |
| import type { IncomingMessage } from "node:http"; |
| import { AUTH_RATE_LIMIT_SCOPE_SHARED_SECRET, type AuthRateLimiter } from "../auth-rate-limit.js"; |
| import { |
| authorizeHttpGatewayConnect, |
| type GatewayAuthResult, |
| type ResolvedGatewayAuth, |
| } from "../auth.js"; |
| import { getBearerToken, resolveHttpBrowserOriginPolicy } from "../http-auth-utils.js"; |
| import { |
| prepareGatewayIngressAttribution, |
| PROXY_ATTRIBUTION_REQUIRED_REASON, |
| } from "../ingress-attribution.js"; |
| import { |
| hasAuthorizedPluginNodeCapability, |
| type PluginNodeCapabilitySurface, |
| } from "../plugin-node-capability.js"; |
| import { withSerializedCredentialFallbackAttempt } from "../rate-limit-attempt-serialization.js"; |
| import type { GatewayWsClient } from "./ws-types.js"; |
|
|
| |
| |
| |
| export async function authorizePluginNodeCapabilityRequest(params: { |
| req: IncomingMessage; |
| auth: ResolvedGatewayAuth; |
| trustedProxies: string[]; |
| allowRealIpFallback: boolean; |
| clients: Set<GatewayWsClient>; |
| nodeCapability: PluginNodeCapabilitySurface; |
| capability?: string; |
| malformedScopedPath?: boolean; |
| rateLimiter?: AuthRateLimiter; |
| }): Promise<GatewayAuthResult> { |
| const { |
| req, |
| auth, |
| trustedProxies, |
| allowRealIpFallback, |
| clients, |
| nodeCapability, |
| capability, |
| malformedScopedPath, |
| rateLimiter, |
| } = params; |
| if (malformedScopedPath) { |
| return { ok: false, reason: "unauthorized" }; |
| } |
|
|
| const attribution = prepareGatewayIngressAttribution({ |
| req, |
| trustedProxies, |
| allowRealIpFallback, |
| }); |
| if (attribution.kind === "unattributable-proxy") { |
| return { ok: false, reason: PROXY_ATTRIBUTION_REQUIRED_REASON }; |
| } |
| const token = getBearerToken(req); |
| const run = async (): Promise<GatewayAuthResult> => { |
| let lastAuthFailure: GatewayAuthResult | null = null; |
| if (token) { |
| |
| |
| const authResult = await authorizeHttpGatewayConnect({ |
| auth: { ...auth, allowTailscale: false }, |
| connectAuth: { token, password: token }, |
| req, |
| trustedProxies, |
| allowRealIpFallback, |
| rateLimiter, |
| |
| |
| deferRateLimitFailure: Boolean(capability), |
| browserOriginPolicy: resolveHttpBrowserOriginPolicy(req), |
| }); |
| if (authResult.ok) { |
| return authResult; |
| } |
| lastAuthFailure = authResult; |
| } |
|
|
| if ( |
| capability && |
| hasAuthorizedPluginNodeCapability({ clients, surface: nodeCapability, capability }) |
| ) { |
| return { ok: true }; |
| } |
|
|
| if ( |
| capability && |
| (lastAuthFailure?.reason === "token_mismatch" || |
| lastAuthFailure?.reason === "password_mismatch") |
| ) { |
| await rateLimiter?.recordFailureAndDelay( |
| attribution.rateLimit.subject.key, |
| AUTH_RATE_LIMIT_SCOPE_SHARED_SECRET, |
| ); |
| } |
|
|
| return lastAuthFailure ?? { ok: false, reason: "unauthorized" }; |
| }; |
|
|
| return token && capability && rateLimiter |
| ? await withSerializedCredentialFallbackAttempt({ |
| limiter: rateLimiter, |
| ip: attribution.rateLimit.subject.key, |
| run, |
| }) |
| : await run(); |
| } |
|
|