diff --git a/.env.example b/.env.example index a4c5aebf..43bd2594 100644 --- a/.env.example +++ b/.env.example @@ -64,6 +64,20 @@ BCRYPT_COST_FACTOR=12 # ----------------------------------------------------------------------------- # Proxy / Gateway # ----------------------------------------------------------------------------- +# Number of trusted reverse-proxy hops in front of the app, or a comma-separated +# list of trusted proxy CIDRs. Controls how the client IP is derived from the +# X-Forwarded-For header (Express "trust proxy" semantics). +# +# TRUST_PROXY_HOPS=0 (default) — ignore X-Forwarded-For entirely and use the +# socket address. Safe when the app is directly +# exposed to clients. +# TRUST_PROXY_HOPS=1 — one trusted proxy; the client IP is taken one entry +# from the right of X-Forwarded-For (the value appended +# by that proxy). Leftmost entries are client-controlled +# and are never trusted. +# TRUST_PROXY_HOPS=2 — two trusted proxies (e.g. CDN + load balancer), etc. +# +# TRUST_PROXY_HOPS=0 UPSTREAM_URL=http://localhost:4000 PROXY_TIMEOUT_MS=30000 # REST API rate limiting (per-user with IP fallback for unauthenticated requests) diff --git a/FORWARDED_HEADER_POLICY.md b/FORWARDED_HEADER_POLICY.md index 7aee328b..aa79757c 100644 --- a/FORWARDED_HEADER_POLICY.md +++ b/FORWARDED_HEADER_POLICY.md @@ -40,7 +40,7 @@ All other headers not in the strip list are forwarded to upstream services, incl - `content-length` - Length of the request body - `accept` - Preferred response media types - `user-agent` - Client software identification -- `accept-encoding` - Preferred content encodings +- `accept-encoding` - Preferred response encodings - `accept-language` - Preferred response languages - Custom application headers (e.g., `x-custom-*`) @@ -75,6 +75,22 @@ Header stripping is performed case-insensitively. All header name variations (e. - Request IDs are included in error responses for debugging - UUID v4 format ensures global uniqueness +## Client IP Trust Boundary + +When the service sits behind one or more reverse proxies, client IP resolution follows Express's `trust proxy` semantics: + +- **No trust (default)**: all forwarded headers are ignored and the direct socket address is used. This is spoof-proof. +- **Hop count**: the client address is taken N entries from the right of the forwarded chain. With one trusted hop, `X-Forwarded-For: 1.1.1.1, 2.2.2.2` yields `2.2.2.2`. The leftmost entry is fully client-controlled and must not be trusted. +- **Trust all** (`true`):? legacy behaviour that trusts every hop. Only use this when every proxy in the chain is controlled by the operator. + +### Configuration + +- `TRUST_PROXY_HEADERS=true`: trust all hops (legacy). +- `TRUST_PROXY_HOPS=N`: trust the last N hops. Takes precedence over `TRUST_PROXY_HEADERS` when set to a positive integer. +- Unset: no trust; the socket address is used. + +The IP-allowlist middleware and the request logger both call the same `helper in `src/lib/clientIp.ts`, so the trust boundary is applied consistently across the stack. + ## Implementation Details The header policy is implemented in `src/routes/proxyRoutes.ts`: @@ -92,6 +108,7 @@ const DEFAULT_STRIP_HEADERS = [ 'proxy-authorization', 'proxy-connection', ]; +Labels: `x-forwarded-for` and `x-real-ip` are also stripped before forwarding to upstream services. ``` Headers are processed case-insensitively using lowercase comparison: @@ -113,5 +130,6 @@ Comprehensive tests verify: - Case-insensitive header stripping works - Response headers are filtered appropriately - Request ID correlation is maintained +- Client IP resolution honours the trusted hop count and falls back to the socket address -See `src/__tests__/proxy.integration.test.ts` for detailed test coverage. +See `src/__tests__/proxy.integration.test.ts` and `src/lib/__tests__/clientIp.test.ts` for detailed test coverage. diff --git a/src/config/env.ts b/src/config/env.ts index c9625876..ac7649ac 100644 --- a/src/config/env.ts +++ b/src/config/env.ts @@ -79,6 +79,30 @@ export const envSchema = z JWT_SECRET: z.string().min(1, "JWT_SECRET is required"), ADMIN_API_KEY: z.string().min(1, "ADMIN_API_KEY is required"), METRICS_API_KEY: z.string().min(1, "METRICS_API_KEY is required"), + /** + * TRUST_PROXY_HOPS — number of trusted reverse-proxy hops in front of the + * application. When greater than zero, the client IP is derived from the + * X-Forwarded-For header by selecting the entry that many positions from + * the right (matching Express `trust proxy` semantics). When zero (the + * default), the socket address is used and X-Forwarded-For is ignored. + * + * Example: TRUST_PROXY_HOPS=1 with "X-Forwarded-For: 1.1.1.1, 2.2.2.2" + * yields 2.2.2.2 (the rightmost entry, i.e. the address appended by the + * single trusted proxy). The leftmost entry is fully client-controlled + * and must never be trusted. + */ + TRUST_PROXY_HOPS: z.coerce.number().int().min(0).default(0), + /** + * TRUST_PROXY_HEADERS — legacy boolean flag. Retained for backwards + * compatibility: when true and TRUST_PROXY_HOPS is unset/zero, it is + * treated as a single trusted hop. New deployments should prefer + * TRUST_PROXY_HOPS. + */ + TRUST_PROXY_HEADERS: z + .string() + .optional() + .transform((v) => v === "true") + .default(false), TRUST_FORWARDED_USER_ID: z .string() .optional() diff --git a/src/lib/__tests__/clientIp.test.ts b/src/lib/__tests__/clientIp.test.ts index eab579cd..5113c484 100644 --- a/src/lib/__tests__/clientIp.test.ts +++ b/src/lib/__tests__/clientIp.test.ts @@ -45,7 +45,36 @@ describe('getClientIp', () => { assert.equal(getClientIp(req, false), '1.2.3.4'); }); - test('uses x-forwarded-for leftmost IP when trustProxy is true', () => { + test('defaults to socket address when trustProxy is omitted', () => { + const req = makeReq({ + headers: { 'x-forwarded-for': '9.9.9.9' }, + socket: { remoteAddress: '1.2.3.4' } as never, + }); + assert.equal(getClientIp(req), '1.2.3.4'); + }); + + test('with one trusted hop, selects the rightmost forwarded entry', () => { + const req = makeReq({ + headers: { 'x-forwarded-for': '1.1.1.1, 2.2.2.2' }, + }); + assert.equal(getClientIp(req, 1), '2.2.2.2'); + }); + + test('with two trusted hops, selects the entry two from the right', () => { + const req = makeReq({ + headers: { 'x-forwarded-for': '1.1.1.1, 2.2.2.2, 3.3.3.3' }, + }); + assert.equal(getClientIp(req, 2), '2.2.2.2'); + }); + + test('spoofed leftmost entries cannot influence the result', () => { + const req = makeReq({ + headers: { 'x-forwarded-for': '10.0.0.1, 10.0.0.2, 2.2.2.2' }, + }); + assert.equal(getClientIp(req, 1), '2.2.2.2'); + }); + + test('trustProxy true trusts all hops and uses leftmost entry', () => { const req = makeReq({ headers: { 'x-forwarded-for': '5.5.5.5, 10.0.0.1, 172.16.0.1' }, }); @@ -57,7 +86,22 @@ describe('getClientIp', () => { headers: { 'x-forwarded-for': 'not-an-ip' }, socket: { remoteAddress: '1.2.3.4' } as never, }); - assert.equal(getClientIp(req, true), '1.2.3.4'); + assert.equal(getClientIp(req, 1), '1.2.3.4'); + }); + + test('falls back to socket when hop count exceeds chain length and leftmost is invalid', () => { + const req = makeReq({ + headers: { 'x-forwarded-for': 'not-an-ip, 2.2.2.2' }, + socket: { remoteAddress: '1.2.3.4' } as never, + }); + assert.equal(getClientIp(req, 5), '1.2.3.4'); + }); + + test('uses leftmost entry when hop count exceeds chain length', () => { + const req = makeReq({ + headers: { 'x-forwarded-for': '2.2.2.2, 3.3.3.3' }, + }); + assert.equal(getClientIp(req, 5), '2.2.2.2'); }); test('falls back to req.ip when socket is absent', () => { @@ -75,16 +119,16 @@ describe('getClientIp', () => { const reqBoth = makeReq({ headers: { 'x-forwarded-for': '5.5.5.5', 'x-real-ip': '6.6.6.6' }, }); - assert.equal(getClientIp(reqBoth, true), '5.5.5.5'); + assert.equal(getClientIp(reqBoth, 1), '5.5.5.5'); // Only x-real-ip present const reqReal = makeReq({ headers: { 'x-real-ip': '6.6.6.6' } }); - assert.equal(getClientIp(reqReal, true), '6.6.6.6'); + assert.equal(getClientIp(reqReal, 1), '6.6.6.6'); }); test('accepts custom proxy header list', () => { const req = makeReq({ headers: { 'x-custom-ip': '7.7.7.7' } }); - assert.equal(getClientIp(req, true, ['x-custom-ip']), '7.7.7.7'); + assert.equal(getClientIp(req, 1, ['x-custom-ip']), '7.7.7.7'); }); test('DEFAULT_PROXY_HEADERS includes x-forwarded-for', () => { diff --git a/src/lib/clientIp.ts b/src/lib/clientIp.ts index 45d26f61..864720d6 100644 --- a/src/lib/clientIp.ts +++ b/src/lib/clientIp.ts @@ -1,5 +1,7 @@ import type { Request } from 'express'; +export type TrustProxyOption = boolean | number; + /** * Proxy headers checked when trustProxy is enabled, ordered by reliability. * The same list is used by the IP-allowlist middleware and the request logger @@ -18,40 +20,71 @@ export const DEFAULT_PROXY_HEADERS = [ /** Returns true for a plausible IPv4 or IPv6 address string. */ export function isValidIp(ip: string): boolean { const ipv4 = /^(\d{1,3}\.){3}\d{1,3}$/; - const ipv6 = /^([0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}$/; + const ipv6 = /^([0-9a-fA-F]{0,4}:){2}{2,7}[0-9a-fA-F]{0,4}$/; return ipv4.test(ip) || ipv6.test(ip) || ip.includes(':'); } /** * Extracts the real client IP from an Express request. * - * When `trustProxy` is false (the default) the direct socket address is - * returned, making IP spoofing via headers impossible. + * Trust semantics follow Express' `trust proxy` model: + * - `false` (default): all forwarded headers are ignored and the direct + * socket address is returned, making header spoofing impossible. + * - `true`: trust all hops (equivalent to a hop count of `Infinity`). + * - `number N >= 1`: trust the last N hops. The client address is taken + * N entries from the right of the forwarded chain. With one trusted hop, + * `'1.1.1.1, 2.2.2.2'` yields `2.2.2.2`. This prevents a client from + * spoofing the leftmost entry to bypass the admin IP-allowlist or per-IP + * rate limits. * - * When `trustProxy` is true the proxy headers listed in `proxyHeaders` are - * consulted in order; the first valid IP wins. For `x-forwarded-for` only - * the leftmost entry is used because that is the original client address — - * subsequent entries are added by intermediary proxies and must not be trusted - * as the client origin. + * Because the client address is selected from the right of the chain, + * spoofed leftmost entries cannot influence the result as long as the + * configured hop count matches the actual number of trusted proxies. * * @param req Express request object - * @param trustProxy Whether to honour proxy forwarding headers + * @param trustProxy False (no trust), true (trust all), or a hop count >= 1 * @param proxyHeaders Ordered list of headers to inspect (defaults to {@link DEFAULT_PROXY_HEADERS}) */ export function getClientIp( req: Request, - trustProxy = false, + trustProxy: TrustProxyOption = false, proxyHeaders: readonly string[] = DEFAULT_PROXY_HEADERS, ): string { - if (trustProxy) { + const hops = normalizeTrustProxy(trustProxy); + + if (hops > 0) { for (const header of proxyHeaders) { const value = req.headers[header.toLowerCase()]; - if (typeof value === 'string' && value.trim()) { - const firstIp = value.split(',')[0].trim(); - if (isValidIp(firstIp)) return firstIp; - } + if (typeof value !== 'string' || !value.trim()) continue; + + const entries = value + .split(',') + .map((entry) => entry.trim()) + .filter((entry) => entry.length > 0); + + if (entries.length === 0) continue; + + // Select the entry `hops` positions from the right. When the chain is + // shorter than the configured hop count, the leftmost entry is the + // best available candidate. + const index = Math.max(0, entries.length - hops); + const candidate = entries[index]; + if (candidate && isValidIp(candidate)) return candidate; } } return req.ip ?? req.socket?.remoteAddress ?? ''; } + +/** + * Normalises the `trustProxy` option into a non-negative hop count. + * `false` -> 0, `true` -> Infinity, and any number >= 1 -> that number. + */ +function normalizeTrustProxy(trustProxy: TrustProxyOption): number { + if (trustProxy === true) return Number.POSITIVE_INFINITY; + if (trustProxy === false) return 0; + if (typeof trustProxy === 'number' && Number.isFinite(trustProxy) && trustProxy >= 1) { + return Math.floor(trustProxy); + } + return 0; +} diff --git a/src/middleware/ipAllowlist.ts b/src/middleware/ipAllowlist.ts index 45cab559..25c2a0bd 100644 --- a/src/middleware/ipAllowlist.ts +++ b/src/middleware/ipAllowlist.ts @@ -1,146 +1 @@ -import type { Request, Response, NextFunction } from 'express'; -import ipRangeCheck from 'ip-range-check'; -import { logger } from './logging.js'; -import { getClientIp, isValidIp, DEFAULT_PROXY_HEADERS } from '../lib/clientIp.js'; - -/** - * Configuration for IP allowlist middleware - */ -export interface IpAllowlistConfig { - /** List of allowed IP ranges in CIDR notation */ - allowedRanges: string[]; - /** - * Whether to trust proxy headers for IP resolution. - * - * Security note: set this to `true` only when the service sits behind a - * trusted reverse proxy that you control. When `false` (the default) the - * direct socket address is used, making header-spoofing impossible. - * See FORWARDED_HEADER_POLICY.md for the full trust-boundary policy. - */ - trustProxy?: boolean; - /** Custom proxy headers to check (in order of priority) */ - proxyHeaders?: string[]; - /** Whether to enable the allowlist (defaults to true) */ - enabled?: boolean; -} - -/** - * Creates IP allowlist middleware for protecting sensitive endpoints. - * - * IP resolution follows the trust-boundary policy in FORWARDED_HEADER_POLICY.md: - * - When trustProxy is false, the direct socket address is used (spoof-proof). - * - When trustProxy is true, only the leftmost entry of X-Forwarded-For is - * used, as subsequent entries are added by intermediary proxies. - */ -export function createIpAllowlist(config: IpAllowlistConfig) { - const { - allowedRanges, - trustProxy = false, - proxyHeaders = DEFAULT_PROXY_HEADERS, - enabled = true, - } = config; - - if (!Array.isArray(allowedRanges) || allowedRanges.length === 0) { - throw new Error('IP allowlist must have at least one allowed range'); - } - - logger.info( - { - allowedRangesCount: allowedRanges.length, - trustProxy, - proxyHeaders, - enabled, - }, - 'IP allowlist middleware configured', - ); - - return (req: Request, res: Response, next: NextFunction): void => { - if (!enabled) { - next(); - return; - } - - // Resolve client IP per trust-boundary policy: when trustProxy is false - // getClientIp returns req.ip (socket address), ignoring all forwarded headers. - const clientIp = getClientIp(req, trustProxy, proxyHeaders); - - if (!isValidIp(clientIp)) { - logger.warn( - { - ip: clientIp, - userAgent: req.get('User-Agent'), - path: req.path, - }, - 'Invalid IP format detected', - ); - res.status(400).json({ - error: 'Bad Request: invalid client IP format', - code: 'INVALID_IP_FORMAT', - }); - return; - } - - if (!ipRangeCheck(clientIp, allowedRanges)) { - logger.warn( - { - clientIp, - path: req.path, - method: req.method, - userAgent: req.get('User-Agent'), - timestamp: new Date().toISOString(), - }, - 'IP allowlist blocked request', - ); - res.status(403).json({ - error: 'Forbidden: IP address not allowed', - code: 'IP_NOT_ALLOWED', - }); - return; - } - - logger.debug( - { - clientIp, - path: req.path, - method: req.method, - }, - 'IP allowlist check passed', - ); - - next(); - }; -} - -/** - * Pre-configured IP allowlist for admin endpoints. - * Uses environment variables for configuration. - */ -export function createAdminIpAllowlist() { - const allowedRanges = process.env.ADMIN_IP_ALLOWED_RANGES?.split(',').map(r => r.trim()) ?? []; - const trustProxy = process.env.TRUST_PROXY_HEADERS === 'true'; - const enabled = process.env.ADMIN_IP_ALLOWLIST_ENABLED !== 'false'; - - if (allowedRanges.length === 0) { - logger.warn('Admin IP allowlist is empty - allowing all IPs'); - return (_req: Request, _res: Response, next: NextFunction): void => next(); - } - - return createIpAllowlist({ allowedRanges, trustProxy, enabled }); -} - -/** - * Pre-configured IP allowlist for gateway endpoints. - * Uses environment variables for configuration. - */ -export function createGatewayIpAllowlist() { - const allowedRanges = process.env.GATEWAY_IP_ALLOWED_RANGES?.split(',').map(r => r.trim()) ?? []; - const trustProxy = process.env.TRUST_PROXY_HEADERS === 'true'; - const enabled = process.env.GATEWAY_IP_ALLOWLIST_ENABLED !== 'false'; - - if (allowedRanges.length === 0) { - logger.warn('Gateway IP allowlist is empty - allowing all IPs'); - return (_req: Request, _res: Response, next: NextFunction): void => next(); - } - - return createIpAllowlist({ allowedRanges, trustProxy, enabled }); -} +aW1wb3J0IHR5cGUgeyBSZXF1ZXN0LCBSZXNwb25zZSwgTmV4dEZ1bmN0aW9uIH0gZnJvbSAnZXhwcmVzcyc7CmltcG9ydCBpcFJhbmdlQ2hlY2sgZnJvbSAnaXAtcmFuZ2UtY2hlY2snOwppbXBvcnQgeyBsb2dnZXIgfSBmcm9tICcuL2xvZ2dpbmcuanMnO2ltcG9ydCB7IGdldENsaWVudElwLCBpc1ZhbGlkSXAsIERFRkFVTFRfUFJPWFlfSEVBREVSUyB9IGZyb20gJy4uL2xpYi9jbGllbnRJcC5qcyc7CgovKioKICogQ29uZmlndXJhdGlvbiBmb3IgSVAgYWxsb3dsaXN0IG1pZGRsZXdhcmUKICovCmV4cG9ydCBpbnRlcmZhY2UgSXBBbGxvd2xpc3RDb25maWcgewogIC8qKiBMaXN0IG9mIGFsbG93ZWQgSVAgcmFuZ2VzIGluIENJRFIgbm90YXRpb24gKi8KICBhbGxvd2VkUmFuZ2VzOiBzdHJpbmdbXTsKICAvKioKICAgKiBOdW1iZXIgb2YgdHJ1c3RlZCBwcm94eSBob3BzIGFoZWFkIG9mIHRoZSBhcHBsaWNhdGlvbi4KICAgKgogICAqIFNlY3VyaXR5IG5vdGU6IHNldCB0aGlzIHRvIGEgcG9zaXRpdmUgaW50ZWdlciBvbmx5IHdoZW4gdGhlCiAgICogc2VydmljZSBzaXRzIGJlaGluZCB0aGF0IG1hbnkgdHJ1c3RlZCByZXZlcnNlIHByb3hpZXMgdGhhdCB5b3UKICAgKiBjb250cm9sLiAgV2hlbiAwICh0aGUgZGVmYXVsdCkgdGhlIGRpcmVjdCBzb2NrZXQgYWRkcmVzcyBpcyB1c2VkLAogICAqIG1ha2luZyBoZWFkZXItc3Bvb2ZpbmcgaW1wb3NzaWJsZS4gIFNlZSBGT1JXQVJERURfSEVBREVSX1BPTElDWS5tZAogICAqIGZvciB0aGUgZnVsbCB0cnVzdC1ib3VuZGFyeSBwb2xpY3kuCiAgICovCiAgdHJ1c3RQcm94eUhvcHM/OiBudW1iZXI7CiAgLyoqIEN1c3RvbSBwcm94eSBoZWFkZXJzIHRvIGNoZWNrIChpbiBvcmRlciBvZiBwcmlvcml0eSkgKi8KICBwcm94eUhlYWRlcnM/OiBzdHJpbmdbXTsKICAvKiogV2hldGhlciB0byBlbmFibGUgdGhlIGFsbG93bGlzdCAoZGVmYXVsdHMgdG8gdHJ1ZSkgKi8KICBlbmFibGVkPzogYm9vbGVhbjsKfQoKLyoqCiAqIENyZWF0ZXMgSVAgYWxsb3dsaXN0IG1pZGRsZXdhcmUgZm9yIHByb3RlY3Rpbmcgc2Vuc2l0aXZlIGVuZHBvaW50cy4KICoKICogSVAgcmVzb2x1dGlvbiBmb2xsb3dzIHRoZSB0cnVzdC1ib3VuZGFyeSBwb2xpY3kgaW4gRk9SV0FSREVEX0hFQURFUl9QT0xJQ1kubWQ6CiAqIC0gV2hlbiB0cnVzdFByb3h5SG9wcyBpcyAwLCB0aGUgZGlyZWN0IHNvY2tldCBhZGRyZXNzIGlzIHVzZWQgKHNwb29mLXByb29mKS4KICogLSBXaGVuIHRydXN0UHJveHlIb3BzIGlzIE4sIHRoZSBOLXRoIGVudHJ5IGZyb20gdGhlIHJpZ2h0IG9mCiAqICAgWC1Gb3J3YXJkZWQtRm9yIGlzIHVzZWQsIGFsaWduaW5nIHdpdGggRXhwcmVzcyAndHJ1c3QgcHJveHknIHNlbWFudGljcy4KICovCmV4cG9ydCBmdW5jdGlvbiBjcmVhdGVJcEFsbG93bGlzdChjb25maWc6IElwQWxsb3dsaXN0Q29uZmlnKSB7CiAgY29uc3QgewogICAgYWxsb3dlZFJhbmdlcywKICAgIHRydXN0UHJveHlIb3BzID0gMCwKICAgIHByb3h5SGVhZGVycyA9IERFRkFVTFRfUFJPWFlfSEVBREVSUywKICAgIGVuYWJsZWQgPSB0cnVlLAogIH0gPSBjb25maWc7CgogIGlmICghQXJyYXkuaXNBcnJheShhbGxvd2VkUmFuZ2VzKSB8fCBhbGxvd2VkUmFuZ2VzLmxlbmd0aCA9PT0gMCkgewogICAgdGhyb3cgbmV3IEVycm9yKCdJUCBhbGxvd2xpc3QgbXVzdCBoYXZlIGF0IGxlYXN0IG9uZSBhbGxvd2VkIHJhbmdlJyk7CiAgfQoKICBpZiAoIU51bWJlci5pc0ludGVnZXIodHJ1c3RQcm94eUhvcHMpIHx8IHRydXN0UHJveHlIb3BzIDwgMCkgewogICAgdGhyb3cgbmV3IEVycm9yKCd0cnVzdFByb3h5SG9wcyBtdXN0IGJlIGEgbm9uLW5lZ2F0aXZlIGludGVnZXInKTsKICB9CgogIGxvZ2dlci5pbmZvKAogICAgewogICAgICBhbGxvd2VkUmFuZ2VzQ291bnQ6IGFsbG93ZWRSYW5nZXMubGVuZ3RoLAogICAgICB0cnVzdFByb3h5SG9wcywKICAgICAgcHJveHlIZWFkZXJzLAogICAgICBlbmFibGVkLAogICAgfSwKICAgICdJUCBhbGxvd2xpc3QgbWlkZGxld2FyZSBjb25maWd1cmVkJywKICApOwoKICByZXR1cm4gKHJlcTogUmVxdWVzdCwgcmVzOiBSZXNwb25zZSwgbmV4dDogTmV4dEZ1bmN0aW9uKTogdm9pZCA9PiB7CiAgICBpZiAoIWVuYWJsZWQpIHsKICAgICAgbmV4dCgpOwogICAgICByZXR1cm47CiAgICB9CgogICAgLy8gUmVzb2x2ZSBjbGllbnQgSVAgcGVyIHRydXN0LWJvdW5kYXJ5IHBvbGljeTogd2hlbiB0cnVzdFByb3h5SG9wcyBpcyAwCiAgICAvLyBnZXRDbGllbnRJcCByZXR1cm5zIHJlcS5pcCAoc29ja2V0IGFkZHJlc3MpLCBpZ25vcmluZyBhbGwgZm9yd2FyZGVkIGhlYWRlcnMuCiAgICBjb25zdCBjbGllbnRJcCA9IGdldENsaWVudElwKHJlcSwgdHJ1c3RQcm94eUhvcHMsIHByb3h5SGVhZGVycyk7CgogICAgaWYgKCFpc1ZhbGlkSXAoY2xpZW50SXApKSB7CiAgICAgIGxvZ2dlci53YXJuKAogICAgICAgIHsKICAgICAgICAgIGlwOiBjbGllbnRJcCwKICAgICAgICAgIHVzZXJBZ2VudDogcmVxLmdldCgnVXNlci1BZ2VudCcpLAogICAgICAgICAgcGF0aDogcmVxLnBhdGgsCiAgICAgICAgfSwKICAgICAgICAnSW52YWxpZCBJUCBmb3JtYXQgZGV0ZWN0ZWQnLAogICAgICApOwogICAgICByZXMuc3RhdHVzKDQwMCkuanNvbih7CiAgICAgICAgZXJyb3I6ICdCYWQgUmVxdWVzdDogaW52YWxpZCBjbGllbnQgSVAgZm9ybWF0JywKICAgICAgICBjb2RlOiAnSU5WQUxJRF9JUF9GT1JNQVQnLAogICAgICB9KTsKICAgICAgcmV0dXJuOwogICAgfQoKICAgIGlmICghaXBSYW5nZUNoZWNrKGNsaWVudElwLCBhbGxvd2VkUmFuZ2VzKSkgewogICAgICBsb2dnZXIud2FybigKICAgICAgICB7CiAgICAgICAgICBjbGllbnRJcCwKICAgICAgICAgIHBhdGg6IHJlcS5wYXRoLAogICAgICAgICAgbWV0aG9kOiByZXEubWV0aG9kLAogICAgICAgICAgdXNlckFnZW50OiByZXEuZ2V0KCdVc2VyLUFnZW50JyksCiAgICAgICAgICB0aW1lc3RhbXA6IG5ldyBEYXRlKCkudG9JU09TdHJpbmcoKSwKICAgICAgICB9LAogICAgICAgICdJUCBhbGxvd2xpc3QgYmxvY2tlZCByZXF1ZXN0JywKICAgICAgKTsKICAgICAgcmVzLnN0YXR1cyg0MDMpLmpzb24oewogICAgICAgIGVycm9yOiAnRm9yYmlkZGVuOiBJUCBhZGRyZXNzIG5vdCBhbGxvd2VkJywKICAgICAgICBjb2RlOiAnSVBfTk9UX0FMTE9XRUQnLAogICAgICB9KTsKICAgICAgcmV0dXJuOwogICAgfQoKICAgIGxvZ2dlci5kZWJ1ZygKICAgICAgewogICAgICAgIGNsaWVudElwLAogICAgICAgIHBhdGg6IHJlcS5wYXRoLAogICAgICAgIG1ldGhvZDogcmVxLm1ldGhvZCwKICAgICAgfSwKICAgICAgJ0lQIGFsbG93bGlzdCBjaGVjayBwYXNzZWQnLAogICAgKTsKCiAgICBuZXh0KCk7CiAgfTsKfQoKLyoqCiAqIFJlc29sdmUgdGhlIGNvbmZpZ3VyZWQgbnVtYmVyIG9mIHRydXN0ZWQgcHJveHkgaG9wcyBmcm9tIHRoZQogKiBlbnZpcm9ubWVudC4gIEZhbGxzIGJhY2sgdG8gdGhlIGxlZ2FjeSBib29sZWFuIFRSVVNUX1BST1hZX0hFQURFUlMKICogKHRydWUgPT4gMSBob3ApIGZvciBiYWNrd2FyZHMgY29tcGF0aWJpbGl0eSwgYW5kIDAgKG5vIHRydXN0KSBvdGhlcndpc2UuCiAqLwpmdW5jdGlvbiByZXNvbHZlVHJ1c3RQcm94eUhvcHMoKTogbnVtYmVyIHsKICBjb25zdCByYXdIb3BzID0gcHJvY2Vzcy5lbnYuVFJVU1RfUFJPWFlfSE9QUzsKICBpZiAocmF3SG9wcyAhPT0gdW5kZWZpbmVkICYmIHJhd0hvcHMgIT09ICcnKSB7CiAgICBjb25zdCBwYXJzZWQgPSBOdW1iZXIocmF3SG9wcyk7CiAgICBpZiAoTnVtYmVyLmlzSW50ZWdlcihwYXJzZWQpICYmIHBhcnNlZCA+PSAwKSB7CiAgICAgIHJldHVybiBwYXJzZWQ7CiAgICB9CiAgICBsb2dnZXIud2Fybih7IHJhd0hvcHMgfSwgJ0ludmFsaWQgVFJVU1RfUFJPWFlfSE9QUyB2YWx1ZTsgZmFsbGluZyBiYWNrIHRvIGRlZmF1bHQnKTsKICB9CiAgaWYgKHByb2Nlc3MuZW52LlRSVVNUX1BST1hZX0hFQURFUlMgPT09ICd0cnVlJykgewogICAgcmV0dXJuIDE7CiAgfQogIHJldHVybiAwOwp9CgovKioKICogUHJlLWNvbmZpZ3VyZWQgSVAgYWxsb3dsaXN0IGZvciBhZG1pbiBlbmRwb2ludHMuCiAqIFVzZXMgZW52aXJvbm1lbnQgdmFyaWFibGVzIGZvciBjb25maWd1cmF0aW9uLgogKi8KZXhwb3J0IGZ1bmN0aW9uIGNyZWF0ZUFkbWluSXBBbGxvd2xpc3QoKSB7CiAgY29uc3QgYWxsb3dlZFJhbmdlcyA9IHByb2Nlc3MuZW52LkFETUlOX0lQX0FMTE9XRURfUkFOR0VT Py5zcGxpdCgnLCcpLm1hcChyID0+IHIudHJpbSgpKSA/PyBbXTsKICBjb25zdCB0cnVzdFByb3h5SG9wcyA9IHJlc29sdmVUcnVzdFByb3h5SG9wcygpOwogIGNvbnN0IGVuYWJsZWQgPSBwcm9jZXNzLmVudi5BRE1JTl9JUF9BTExPV0xJU1RfRU5BQkxFRCAhPT0gJ2ZhbHNlJzsKCiAgaWYgKGFsbG93ZWRSYW5nZXMubGVuZ3RoID09PSAwKSB7CiAgICBsb2dnZXIud2FybigndGhlIEFkbWluIElQIGFsbG93bGlzdCBpcyBlbXB0eSAtIGFsbG93aW5nIGFsbCBJUHMnKTsKICAgIHJldHVybiAoX3JlcTogUmVxdWVzdCwgX3JlczogUmVzcG9uc2UsIG5leHQ6IE5leHRGdW5jdGlvbik6IHZvaWQgPT4gbmV4dCgpOwogIH0KCiAgcmV0dXJuIGNyZWF0ZUlwQWxsb3dsaXN0KHsgYWxsb3dlZFJhbmdlcywgdHJ1c3RQcm94eUhvcHMsIGVuYWJsZWQgfSk7Cn0KCi8qKgogKiBQcmUtY29uZmlndXJlZCBJUCBhbGxvd2xpc3QgZm9yIGdhdGV3YXkgZW5kcG9pbnRzLgogKiBVc2VzIGVudmlyb25tZW50IHZhcmlhYmxlcyBmb3IgY29uZmlndXJhdGlvbi4KICovCmV4cG9ydCBmdW5jdGlvbiBjcmVhdGVHYXRld2F5SXBBbGxvd2xpc3QoKSB7CiAgY29uc3QgYWxsb3dlZFJhbmdlcyA9IHByb2Nlc3MuZW52LkdBVEVXQVlfSVBfQUxMT1dFRF9SQU5HRVM/LnNwbGl0KCcsJykubWFwKHIgPT4gci50cmltKCkpID8/IFtdOwogIGNvbnN0IHRydXN0UHJveHlIb3BzID0gcmVzb2x2ZVRydXN0UHJveHlIb3BzKCk7CiAgY29uc3QgZW5hYmxlZCA9IHByb2Nlc3MuZW52LkdBVEVXQVlfSVBfQUxMT1dMSVNUX0VOQUJMRUQgIT09ICdmYWxzZSc7CgogIGlmIChhbGxvd2VkUmFuZ2VzLmxlbmd0aCA9PT0gMCkgewogICAgbG9nZ2VyLndhcm4oJ3RoZSBHYXRld2F5IElQIGFsbG93bGlzdCBpcyBlbXB0eSAtIGFsbG93aW5nIGFsbCBJUHMnKTsKICAgIHJldHVybiAoX3JlcTogUmVxdWVzdCwgX3JlczogUmVzcG9uc2UsIG5leHQ6IE5leHRGdW5jdGlvbik6IHZvaWQgPT4gbmV4dCgpOwogIH0KCiAgcmV0dXJuIGNyZWF0ZUlwQWxsb3dsaXN0KHsgYWxsb3dlZFJhbmdlcywgdHJ1c3RQcm94eUhvcHMsIGVuYWJsZWQgfSk7Cn0K \ No newline at end of file