import { validateResourceSessionToken } from "@server/auth/sessions/resource"; import { verifyResourceAccessToken } from "@server/auth/verifyResourceAccessToken"; import { getResourceByDomain, getResourceRules, getRoleResourceAccess, getUserOrgRole, getUserResourceAccess, getOrgLoginPage, getUserSessionWithUser } from "@server/db/queries/verifySessionQueries"; import { LoginPage, Org, Resource, ResourceHeaderAuth, ResourceHeaderAuthExtendedCompatibility, ResourcePassword, ResourcePincode, ResourceRule, resourceSessions } from "@server/db"; import config from "@server/lib/config"; import { isIpInCidr, stripPortFromHost } from "@server/lib/ip"; import { response } from "@server/lib/response"; import logger from "@server/logger"; import HttpCode from "@server/types/HttpCode"; import { NextFunction, Request, Response } from "express"; import createHttpError from "http-errors"; import { z } from "zod"; import { fromError } from "zod-validation-error"; import { getCountryCodeForIp } from "@server/lib/geoip"; import { getAsnForIp } from "@server/lib/asn"; import { getOrgTierData } from "#dynamic/lib/billing"; import { TierId } from "@server/lib/billing/tiers"; import { verifyPassword } from "@server/auth/password"; import { checkOrgAccessPolicy, enforceResourceSessionLength } from "#dynamic/lib/checkOrgAccessPolicy"; import { logRequestAudit } from "./logRequestAudit"; import cache from "@server/lib/cache"; import semver from "semver"; import { APP_VERSION } from "@server/lib/consts"; const verifyResourceSessionSchema = z.object({ sessions: z.record(z.string(), z.string()).optional(), headers: z.record(z.string(), z.string()).optional(), query: z.record(z.string(), z.string()).optional(), originalRequestURL: z.url(), scheme: z.string(), host: z.string(), path: z.string(), method: z.string(), tls: z.boolean(), requestIp: z.string().optional(), badgerVersion: z.string().optional() }); export type VerifyResourceSessionSchema = z.infer< typeof verifyResourceSessionSchema >; type BasicUserData = { username: string; email: string | null; name: string | null; role: string | null; }; export type VerifyUserResponse = { valid: boolean; headerAuthChallenged?: boolean; redirectUrl?: string; userData?: BasicUserData; pangolinVersion?: string; }; export async function verifyResourceSession( req: Request, res: Response, next: NextFunction ): Promise { logger.debug("Verify session: Badger sent", req.body); // remove when done testing const parsedBody = verifyResourceSessionSchema.safeParse(req.body); if (!parsedBody.success) { return next( createHttpError( HttpCode.BAD_REQUEST, fromError(parsedBody.error).toString() ) ); } try { const { sessions, host, originalRequestURL, requestIp, path, headers, query, badgerVersion } = parsedBody.data; // Extract HTTP Basic Auth credentials if present const clientHeaderAuth = extractBasicAuth(headers); const clientIp = requestIp ? stripPortFromHost(requestIp, badgerVersion) : undefined; logger.debug("Client IP:", { clientIp }); const ipCC = clientIp ? await getCountryCodeFromIp(clientIp) : undefined; const ipAsn = clientIp ? await getAsnFromIp(clientIp) : undefined; let cleanHost = host; // if the host ends with :port, strip it if (cleanHost.match(/:[0-9]{1,5}$/)) { const matched = "" + cleanHost.match(/:[0-9]{1,5}$/); cleanHost = cleanHost.slice(0, -1 * matched.length); } const resourceCacheKey = `resource:${cleanHost}`; let resourceData: | { resource: Resource | null; pincode: ResourcePincode | null; password: ResourcePassword | null; headerAuth: ResourceHeaderAuth | null; headerAuthExtendedCompatibility: ResourceHeaderAuthExtendedCompatibility | null; org: Org; } | undefined = cache.get(resourceCacheKey); if (!resourceData) { const result = await getResourceByDomain(cleanHost); if (!result) { logger.debug(`Resource not found ${cleanHost}`); // TODO: we cant log this for now because we dont know the org // eventually it would be cool to show this for the server admin // logRequestAudit( // { // action: false, // reason: 201, //resource not found // location: ipCC // }, // parsedBody.data // ); return notAllowed(res); } resourceData = result; cache.set(resourceCacheKey, resourceData, 5); } const { resource, pincode, password, headerAuth, headerAuthExtendedCompatibility } = resourceData; if (!resource) { logger.debug(`Resource not found ${cleanHost}`); // TODO: we cant log this for now because we dont know the org // eventually it would be cool to show this for the server admin // logRequestAudit( // { // action: false, // reason: 201, //resource not found // location: ipCC // }, // parsedBody.data // ); return notAllowed(res); } const { sso, blockAccess } = resource; if (blockAccess) { logger.debug("Resource blocked", host); logRequestAudit( { action: false, reason: 202, //resource blocked resourceId: resource.resourceId, orgId: resource.orgId, location: ipCC }, parsedBody.data ); return notAllowed(res); } // check the rules if (resource.applyRules) { const action = await checkRules( resource.resourceId, clientIp, path, ipCC, ipAsn ); if (action == "ACCEPT") { logger.debug("Resource allowed by rule"); logRequestAudit( { action: true, reason: 100, // allowed by rule resourceId: resource.resourceId, orgId: resource.orgId, location: ipCC }, parsedBody.data ); return allowed(res); } else if (action == "DROP") { logger.debug("Resource denied by rule"); // TODO: add rules type logRequestAudit( { action: false, reason: 203, // dropped by rules resourceId: resource.resourceId, orgId: resource.orgId, location: ipCC }, parsedBody.data ); return notAllowed(res); } else if (action == "PASS") { logger.debug( "Resource passed by rule, continuing to auth checks" ); // Continue to authentication checks below } // otherwise its undefined and we pass } // IMPORTANT: ADD NEW AUTH CHECKS HERE OR WHEN TURNING OFF ALL OTHER AUTH METHODS IT WILL JUST PASS if ( !sso && !pincode && !password && !resource.emailWhitelistEnabled && !headerAuth ) { logger.debug("Resource allowed because no auth"); logRequestAudit( { action: true, reason: 101, // allowed no auth resourceId: resource.resourceId, orgId: resource.orgId, location: ipCC }, parsedBody.data ); return allowed(res); } const redirectPath = `/auth/resource/${encodeURIComponent( resource.resourceGuid )}?redirect=${encodeURIComponent(originalRequestURL)}`; // check for access token in headers if ( headers && headers[ config.getRawConfig().server.resource_access_token_headers.id ] && headers[ config.getRawConfig().server.resource_access_token_headers.token ] ) { const accessTokenId = headers[ config.getRawConfig().server.resource_access_token_headers .id ]; const accessToken = headers[ config.getRawConfig().server.resource_access_token_headers .token ]; const { valid, error, tokenItem } = await verifyResourceAccessToken( { accessToken, accessTokenId, resourceId: resource.resourceId } ); if (error) { logger.debug("Access token invalid: " + error); } if (!valid) { if (config.getRawConfig().app.log_failed_attempts) { logger.info( `Resource access token is invalid. Resource ID: ${ resource.resourceId }. IP: ${clientIp}.` ); } } if (valid && tokenItem) { logRequestAudit( { action: true, reason: 102, // valid access token resourceId: resource.resourceId, orgId: resource.orgId, location: ipCC, apiKey: { name: tokenItem.title, apiKeyId: tokenItem.accessTokenId } }, parsedBody.data ); return allowed(res); } } if ( query && query[config.getRawConfig().server.resource_access_token_param] ) { const token = query[config.getRawConfig().server.resource_access_token_param]; const [accessTokenId, accessToken] = token.split("."); const { valid, error, tokenItem } = await verifyResourceAccessToken( { accessToken, accessTokenId, resourceId: resource.resourceId } ); if (error) { logger.debug("Access token invalid: " + error); } if (!valid) { if (config.getRawConfig().app.log_failed_attempts) { logger.info( `Resource access token is invalid. Resource ID: ${ resource.resourceId }. IP: ${clientIp}.` ); } } if (valid && tokenItem) { logRequestAudit( { action: true, reason: 102, // valid access token resourceId: resource.resourceId, orgId: resource.orgId, location: ipCC, apiKey: { name: tokenItem.title, apiKeyId: tokenItem.accessTokenId } }, parsedBody.data ); return allowed(res); } } // check for HTTP Basic Auth header const clientHeaderAuthKey = `headerAuth:${clientHeaderAuth}`; if (headerAuth && clientHeaderAuth) { if (cache.get(clientHeaderAuthKey)) { logger.debug( "Resource allowed because header auth is valid (cached)" ); logRequestAudit( { action: true, reason: 103, // valid header auth resourceId: resource.resourceId, orgId: resource.orgId, location: ipCC }, parsedBody.data ); return allowed(res); } else if ( await verifyPassword( clientHeaderAuth, headerAuth.headerAuthHash ) ) { cache.set(clientHeaderAuthKey, clientHeaderAuth, 5); logger.debug("Resource allowed because header auth is valid"); logRequestAudit( { action: true, reason: 103, // valid header auth resourceId: resource.resourceId, orgId: resource.orgId, location: ipCC }, parsedBody.data ); return allowed(res); } if ( // we dont want to redirect if this is the only auth method and we did not pass here !sso && !pincode && !password && !resource.emailWhitelistEnabled && !headerAuthExtendedCompatibility?.extendedCompatibilityIsActivated ) { logRequestAudit( { action: false, reason: 299, // no more auth methods resourceId: resource.resourceId, orgId: resource.orgId, location: ipCC }, parsedBody.data ); return notAllowed(res); } } else if (headerAuth) { // if there are no other auth methods we need to return unauthorized if nothing is provided if ( !sso && !pincode && !password && !resource.emailWhitelistEnabled && !headerAuthExtendedCompatibility?.extendedCompatibilityIsActivated ) { logRequestAudit( { action: false, reason: 299, // no more auth methods resourceId: resource.resourceId, orgId: resource.orgId, location: ipCC }, parsedBody.data ); return notAllowed(res); } } if (!sessions) { if (config.getRawConfig().app.log_failed_attempts) { logger.info( `Missing resource sessions. Resource ID: ${ resource.resourceId }. IP: ${clientIp}.` ); } logRequestAudit( { action: false, reason: 204, // no sessions resourceId: resource.resourceId, orgId: resource.orgId, location: ipCC }, parsedBody.data ); return notAllowed(res); } const resourceSessionToken = extractResourceSessionToken( sessions, resource.ssl ); if (resourceSessionToken) { const sessionCacheKey = `session:${resourceSessionToken}`; let resourceSession: any = cache.get(sessionCacheKey); if (!resourceSession) { const result = await validateResourceSessionToken( resourceSessionToken, resource.resourceId ); resourceSession = result?.resourceSession; cache.set(sessionCacheKey, resourceSession, 5); } if (resourceSession?.isRequestToken) { logger.debug( "Resource not allowed because session is a temporary request token" ); if (config.getRawConfig().app.log_failed_attempts) { logger.info( `Resource session is an exchange token. Resource ID: ${ resource.resourceId }. IP: ${clientIp}.` ); } logRequestAudit( { action: false, reason: 205, // temporary request token resourceId: resource.resourceId, orgId: resource.orgId, location: ipCC }, parsedBody.data ); return notAllowed(res); } if (resourceSession) { // only run this check if not SSO session; SSO session length is checked later const accessPolicy = await enforceResourceSessionLength( resourceSession, resourceData.org ); if (!accessPolicy.valid) { logger.debug( "Resource session invalid due to org policy:", accessPolicy.error ); return notAllowed(res, redirectPath, resource.orgId); } if (pincode && resourceSession.pincodeId) { logger.debug( "Resource allowed because pincode session is valid" ); logRequestAudit( { action: true, reason: 104, // valid pincode resourceId: resource.resourceId, orgId: resource.orgId, location: ipCC }, parsedBody.data ); return allowed(res); } if (password && resourceSession.passwordId) { logger.debug( "Resource allowed because password session is valid" ); logRequestAudit( { action: true, reason: 105, // valid password resourceId: resource.resourceId, orgId: resource.orgId, location: ipCC }, parsedBody.data ); return allowed(res); } if ( resource.emailWhitelistEnabled && resourceSession.whitelistId ) { logger.debug( "Resource allowed because whitelist session is valid" ); logRequestAudit( { action: true, reason: 106, // valid email resourceId: resource.resourceId, orgId: resource.orgId, location: ipCC }, parsedBody.data ); return allowed(res); } if (resourceSession.accessTokenId) { logger.debug( "Resource allowed because access token session is valid" ); logRequestAudit( { action: true, reason: 102, // valid access token resourceId: resource.resourceId, orgId: resource.orgId, location: ipCC, apiKey: { name: resourceSession.accessTokenTitle, apiKeyId: resourceSession.accessTokenId } }, parsedBody.data ); return allowed(res); } if (resourceSession.userSessionId && sso) { const userAccessCacheKey = `userAccess:${ resourceSession.userSessionId }:${resource.resourceId}`; let allowedUserData: BasicUserData | null | undefined = cache.get(userAccessCacheKey); if (allowedUserData === undefined) { allowedUserData = await isUserAllowedToAccessResource( resourceSession.userSessionId, resource, resourceData.org ); cache.set(userAccessCacheKey, allowedUserData, 5); } if ( allowedUserData !== null && allowedUserData !== undefined ) { logger.debug( "Resource allowed because user session is valid" ); logRequestAudit( { action: true, reason: 107, // valid sso resourceId: resource.resourceId, orgId: resource.orgId, location: ipCC, user: { username: allowedUserData.username, userId: resourceSession.userId } }, parsedBody.data ); return allowed(res, allowedUserData); } } } } // If headerAuthExtendedCompatibility is activated but no clientHeaderAuth provided, force client to challenge if ( headerAuthExtendedCompatibility && headerAuthExtendedCompatibility.extendedCompatibilityIsActivated && !clientHeaderAuth ) { return headerAuthChallenged(res, redirectPath, resource.orgId); } logger.debug("No more auth to check, resource not allowed"); if (config.getRawConfig().app.log_failed_attempts) { logger.info( `Resource access not allowed. Resource ID: ${ resource.resourceId }. IP: ${clientIp}.` ); } logger.debug(`Redirecting to login at ${redirectPath}`); logRequestAudit( { action: false, reason: 299, // no more auth methods resourceId: resource.resourceId, orgId: resource.orgId, location: ipCC }, parsedBody.data ); return notAllowed(res, redirectPath, resource.orgId); } catch (e) { console.error(e); return next( createHttpError( HttpCode.INTERNAL_SERVER_ERROR, "Failed to verify session" ) ); } } function extractResourceSessionToken( sessions: Record, ssl: boolean ) { const prefix = `${config.getRawConfig().server.session_cookie_name}${ ssl ? "_s" : "" }`; const all: { cookieName: string; token: string; priority: number }[] = []; for (const [key, value] of Object.entries(sessions)) { const parts = key.split("."); const timestamp = parts[parts.length - 1]; // check if string is only numbers if (!/^\d+$/.test(timestamp)) { continue; } // cookie name is the key without the timestamp const cookieName = key.slice(0, -timestamp.length - 1); if (cookieName === prefix) { all.push({ cookieName, token: value, priority: parseInt(timestamp) }); } } // sort by priority in desc order all.sort((a, b) => b.priority - a.priority); const latest = all[0]; if (!latest) { return; } return latest.token; } async function notAllowed( res: Response, redirectPath?: string, orgId?: string ) { let loginPage: LoginPage | null = null; if (orgId) { const { tier } = await getOrgTierData(orgId); // returns null in oss if (tier === TierId.STANDARD) { loginPage = await getOrgLoginPage(orgId); } } let redirectUrl: string | undefined = undefined; if (redirectPath) { let endpoint: string; if (loginPage && loginPage.domainId && loginPage.fullDomain) { const secure = config .getRawConfig() .app.dashboard_url?.startsWith("https"); const method = secure ? "https" : "http"; endpoint = `${method}://${loginPage.fullDomain}`; } else { endpoint = config.getRawConfig().app.dashboard_url!; } redirectUrl = `${endpoint}${redirectPath}`; } const data = { data: { valid: false, redirectUrl, pangolinVersion: APP_VERSION }, success: true, error: false, message: "Access denied", status: HttpCode.OK }; logger.debug(JSON.stringify(data)); return response(res, data); } function allowed(res: Response, userData?: BasicUserData) { const data = { data: userData !== undefined && userData !== null ? { valid: true, ...userData, pangolinVersion: APP_VERSION } : { valid: true, pangolinVersion: APP_VERSION }, success: true, error: false, message: "Access allowed", status: HttpCode.OK }; return response(res, data); } async function headerAuthChallenged( res: Response, redirectPath?: string, orgId?: string ) { let loginPage: LoginPage | null = null; if (orgId) { const { tier } = await getOrgTierData(orgId); // returns null in oss if (tier === TierId.STANDARD) { loginPage = await getOrgLoginPage(orgId); } } let redirectUrl: string | undefined = undefined; if (redirectPath) { let endpoint: string; if (loginPage && loginPage.domainId && loginPage.fullDomain) { const secure = config .getRawConfig() .app.dashboard_url?.startsWith("https"); const method = secure ? "https" : "http"; endpoint = `${method}://${loginPage.fullDomain}`; } else { endpoint = config.getRawConfig().app.dashboard_url!; } redirectUrl = `${endpoint}${redirectPath}`; } const data = { data: { headerAuthChallenged: true, valid: false, redirectUrl, pangolinVersion: APP_VERSION }, success: true, error: false, message: "Access denied", status: HttpCode.OK }; logger.debug(JSON.stringify(data)); return response(res, data); } async function isUserAllowedToAccessResource( userSessionId: string, resource: Resource, org: Org ): Promise { const result = await getUserSessionWithUser(userSessionId); if (!result) { return null; } const { user, session } = result; if (!user || !session) { return null; } if ( config.getRawConfig().flags?.require_email_verification && !user.emailVerified ) { return null; } const userOrgRole = await getUserOrgRole(user.userId, resource.orgId); if (!userOrgRole) { return null; } const accessPolicy = await checkOrgAccessPolicy({ org, user, session }); if (!accessPolicy.allowed || accessPolicy.error) { logger.debug(`User not allowed by org access policy because`, { accessPolicy }); return null; } const roleResourceAccess = await getRoleResourceAccess( resource.resourceId, userOrgRole.roleId ); if (roleResourceAccess) { return { username: user.username, email: user.email, name: user.name, role: userOrgRole.roleName }; } const userResourceAccess = await getUserResourceAccess( user.userId, resource.resourceId ); if (userResourceAccess) { return { username: user.username, email: user.email, name: user.name, role: userOrgRole.roleName }; } return null; } async function checkRules( resourceId: number, clientIp: string | undefined, path: string | undefined, ipCC?: string, ipAsn?: number ): Promise<"ACCEPT" | "DROP" | "PASS" | undefined> { const ruleCacheKey = `rules:${resourceId}`; let rules: ResourceRule[] | undefined = cache.get(ruleCacheKey); if (!rules) { rules = await getResourceRules(resourceId); cache.set(ruleCacheKey, rules, 5); } if (rules.length === 0) { logger.debug("No rules found for resource", resourceId); return; } // sort rules by priority in ascending order rules = rules.sort((a, b) => a.priority - b.priority); for (const rule of rules) { if (!rule.enabled) { continue; } if ( clientIp && rule.match == "CIDR" && isIpInCidr(clientIp, rule.value) ) { return rule.action as any; } else if (clientIp && rule.match == "IP" && clientIp == rule.value) { return rule.action as any; } else if ( path && rule.match == "PATH" && isPathAllowed(rule.value, path) ) { return rule.action as any; } else if ( clientIp && rule.match == "COUNTRY" && (await isIpInGeoIP(ipCC, rule.value)) ) { return rule.action as any; } else if ( clientIp && rule.match == "ASN" && (await isIpInAsn(ipAsn, rule.value)) ) { return rule.action as any; } } return; } export function isPathAllowed(pattern: string, path: string): boolean { logger.debug(`\nMatching path "${path}" against pattern "${pattern}"`); // Normalize and split paths into segments const normalize = (p: string) => p.split("/").filter(Boolean); const patternParts = normalize(pattern); const pathParts = normalize(path); logger.debug(`Normalized pattern parts: [${patternParts.join(", ")}]`); logger.debug(`Normalized path parts: [${pathParts.join(", ")}]`); // Maximum recursion depth to prevent stack overflow and memory issues const MAX_RECURSION_DEPTH = 100; // Recursive function to try different wildcard matches function matchSegments(patternIndex: number, pathIndex: number, depth: number = 0): boolean { // Check recursion depth limit if (depth > MAX_RECURSION_DEPTH) { logger.warn( `Path matching exceeded maximum recursion depth (${MAX_RECURSION_DEPTH}) for pattern "${pattern}" and path "${path}"` ); return false; } const indent = " ".repeat(depth); // Indent based on recursion depth const currentPatternPart = patternParts[patternIndex]; const currentPathPart = pathParts[pathIndex]; logger.debug( `${indent}Checking patternIndex=${patternIndex} (${currentPatternPart || "END"}) vs pathIndex=${pathIndex} (${currentPathPart || "END"}) [depth=${depth}]` ); // If we've consumed all pattern parts, we should have consumed all path parts if (patternIndex >= patternParts.length) { const result = pathIndex >= pathParts.length; logger.debug( `${indent}Reached end of pattern, remaining path: ${pathParts.slice(pathIndex).join("/")} -> ${result}` ); return result; } // If we've consumed all path parts but still have pattern parts if (pathIndex >= pathParts.length) { // The only way this can match is if all remaining pattern parts are wildcards const remainingPattern = patternParts.slice(patternIndex); const result = remainingPattern.every((p) => p === "*"); logger.debug( `${indent}Reached end of path, remaining pattern: ${remainingPattern.join("/")} -> ${result}` ); return result; } // For full segment wildcards, try consuming different numbers of path segments if (currentPatternPart === "*") { logger.debug( `${indent}Found wildcard at pattern index ${patternIndex}` ); // Try consuming 0 segments (skip the wildcard) logger.debug( `${indent}Trying to skip wildcard (consume 0 segments)` ); if (matchSegments(patternIndex + 1, pathIndex, depth + 1)) { logger.debug( `${indent}Successfully matched by skipping wildcard` ); return true; } // Try consuming current segment and recursively try rest logger.debug( `${indent}Trying to consume segment "${currentPathPart}" for wildcard` ); if (matchSegments(patternIndex, pathIndex + 1, depth + 1)) { logger.debug( `${indent}Successfully matched by consuming segment for wildcard` ); return true; } logger.debug(`${indent}Failed to match wildcard`); return false; } // Check for in-segment wildcard (e.g., "prefix*" or "prefix*suffix") if (currentPatternPart.includes("*")) { logger.debug( `${indent}Found in-segment wildcard in "${currentPatternPart}"` ); // Convert the pattern segment to a regex pattern const regexPattern = currentPatternPart .replace(/\*/g, ".*") // Replace * with .* for regex wildcard .replace(/\?/g, "."); // Replace ? with . for single character wildcard if needed const regex = new RegExp(`^${regexPattern}$`); if (regex.test(currentPathPart)) { logger.debug( `${indent}Segment with wildcard matches: "${currentPatternPart}" matches "${currentPathPart}"` ); return matchSegments(patternIndex + 1, pathIndex + 1, depth + 1); } logger.debug( `${indent}Segment with wildcard mismatch: "${currentPatternPart}" doesn't match "${currentPathPart}"` ); return false; } // For regular segments, they must match exactly if (currentPatternPart !== currentPathPart) { logger.debug( `${indent}Segment mismatch: "${currentPatternPart}" != "${currentPathPart}"` ); return false; } logger.debug( `${indent}Segments match: "${currentPatternPart}" = "${currentPathPart}"` ); // Move to next segments in both pattern and path return matchSegments(patternIndex + 1, pathIndex + 1, depth + 1); } const result = matchSegments(0, 0, 0); logger.debug(`Final result: ${result}`); return result; } async function isIpInGeoIP( ipCountryCode: string | undefined, checkCountryCode: string ): Promise { if (checkCountryCode == "ALL") { return true; } return ipCountryCode?.toUpperCase() === checkCountryCode.toUpperCase(); } async function isIpInAsn( ipAsn: number | undefined, checkAsn: string ): Promise { // Handle "ALL" special case if (checkAsn === "ALL" || checkAsn === "AS0") { return true; } if (!ipAsn) { return false; } // Normalize the check ASN - remove "AS" prefix if present and convert to number const normalizedCheckAsn = checkAsn.toUpperCase().replace(/^AS/, ""); const checkAsnNumber = parseInt(normalizedCheckAsn, 10); if (isNaN(checkAsnNumber)) { logger.warn(`Invalid ASN format in rule: ${checkAsn}`); return false; } const match = ipAsn === checkAsnNumber; logger.debug( `ASN check: IP ASN ${ipAsn} ${match ? "matches" : "does not match"} rule ASN ${checkAsnNumber}` ); return match; } async function getAsnFromIp(ip: string): Promise { const asnCacheKey = `asn:${ip}`; let cachedAsn: number | undefined = cache.get(asnCacheKey); if (!cachedAsn) { cachedAsn = await getAsnForIp(ip); // do it locally // Cache for longer since IP ASN doesn't change frequently if (cachedAsn) { cache.set(asnCacheKey, cachedAsn, 300); // 5 minutes } } return cachedAsn; } async function getCountryCodeFromIp(ip: string): Promise { const geoIpCacheKey = `geoip:${ip}`; let cachedCountryCode: string | undefined = cache.get(geoIpCacheKey); if (!cachedCountryCode) { cachedCountryCode = await getCountryCodeForIp(ip); // do it locally // Only cache successful lookups to avoid filling cache with undefined values if (cachedCountryCode) { // Cache for longer since IP geolocation doesn't change frequently cache.set(geoIpCacheKey, cachedCountryCode, 300); // 5 minutes } } return cachedCountryCode; } function extractBasicAuth( headers: Record | undefined ): string | undefined { if (!headers || (!headers.authorization && !headers.Authorization)) { return; } const authHeader = headers.authorization || headers.Authorization; // Check if it's Basic Auth if (!authHeader.startsWith("Basic ")) { logger.debug("Authorization header is not Basic Auth"); return; } try { // Extract the base64 encoded credentials return authHeader.slice("Basic ".length); } catch (error) { logger.debug("Basic Auth: Failed to decode credentials", { error: error instanceof Error ? error.message : "Unknown error" }); } }