Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e760db1192 | ||
|
|
3b03850a3b | ||
|
|
705c19f7c8 |
No files matched your search
@@ -32,3 +32,24 @@ ORIGIN=localhost
|
||||
# Set to "false" to disable the selectable theme feature on status pages.
|
||||
# When disabled, all status pages render with the default "refined" theme.
|
||||
STATUS_PAGE_THEMES_ENABLED=true
|
||||
|
||||
# Outbound Proxy
|
||||
# --------------
|
||||
# Route monitor checks through an HTTP proxy, for deployments whose egress is
|
||||
# restricted. Credentials may be embedded: http://user:pass@proxy:3128
|
||||
# The lowercase forms (http_proxy, https_proxy, no_proxy) are also honoured and
|
||||
# take precedence over these, which matters on base images that already set them.
|
||||
# HTTP_PROXY=http://proxy.corp.example:3128
|
||||
# HTTPS_PROXY=http://proxy.corp.example:3128
|
||||
#
|
||||
# Hosts that must be reached directly. Supports wildcards and host lists, with
|
||||
# the same semantics curl uses.
|
||||
# NO_PROXY=localhost,127.0.0.1,*.internal,10.0.0.5
|
||||
#
|
||||
# Monitor types to route through the proxy, comma-separated. Defaults to
|
||||
# "http,pagespeed". Infrastructure ("hardware") monitors are excluded by
|
||||
# default because Capture agents normally sit on the internal network — add it
|
||||
# here if yours are reachable only through the proxy. Use "none" to disable
|
||||
# proxying without unsetting the variables above. Types that open raw sockets
|
||||
# (ping, port, docker, grpc, websocket, dns, game) cannot be proxied.
|
||||
# PROXY_MONITOR_TYPES=http,pagespeed
|
||||
Generated
+142
-30
@@ -26,6 +26,8 @@
|
||||
"got": "14.4.7",
|
||||
"handlebars": "^4.7.8",
|
||||
"helmet": "^8.0.0",
|
||||
"http-proxy-agent": "^9.1.0",
|
||||
"https-proxy-agent": "^9.1.0",
|
||||
"isomorphic-dompurify": "^2.26.0",
|
||||
"jmespath": "^0.16.0",
|
||||
"jsdom": "^26.1.0",
|
||||
@@ -35,6 +37,7 @@
|
||||
"multer": "^1.4.5-lts.2",
|
||||
"nodemailer": "9.0.5",
|
||||
"ping": "0.4.4",
|
||||
"proxy-from-env": "^2.1.0",
|
||||
"re2": "^1.24.1",
|
||||
"sharp": "0.35.3",
|
||||
"ssl-checker": "2.0.10",
|
||||
@@ -172,7 +175,6 @@
|
||||
"integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.29.7",
|
||||
"@babel/generator": "^7.29.7",
|
||||
@@ -805,7 +807,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
@@ -850,7 +851,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
@@ -866,6 +866,29 @@
|
||||
"kuler": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/core": {
|
||||
"version": "1.11.3",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.3.tgz",
|
||||
"integrity": "sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@emnapi/wasi-threads": "1.2.3",
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/runtime": {
|
||||
"version": "1.11.3",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz",
|
||||
"integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/wasi-threads": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.3.tgz",
|
||||
@@ -873,6 +896,7 @@
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
@@ -3050,7 +3074,6 @@
|
||||
"integrity": "sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/body-parser": "*",
|
||||
"@types/express-serve-static-core": "^5.0.0",
|
||||
@@ -3211,7 +3234,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.1.tgz",
|
||||
"integrity": "sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~7.16.0"
|
||||
}
|
||||
@@ -3448,7 +3470,6 @@
|
||||
"integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.56.1",
|
||||
"@typescript-eslint/types": "8.56.1",
|
||||
@@ -4149,7 +4170,6 @@
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -4557,7 +4577,6 @@
|
||||
"integrity": "sha512-Z0oHEHAFDZkffN8Qc39zNZjQlMDkPJRyyyZieU1VH7u8c5S+qHZ2S8ixdKIAxEjfHO7FJxXmJWgteOghVanIsg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"peerDependencies": {
|
||||
"bare-abort-controller": "*"
|
||||
},
|
||||
@@ -4876,7 +4895,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.10.42",
|
||||
"caniuse-lite": "^1.0.30001800",
|
||||
@@ -5739,7 +5757,6 @@
|
||||
"resolved": "https://registry.npmjs.org/cssnano/-/cssnano-7.1.9.tgz",
|
||||
"integrity": "sha512-uPR75+5Dk/WJ/YSPR1/YDHdwMM9c5FsaARljfKWgeCKLKOtJ0we21xy/RcCjn53fZnD/f6yYEIZ8pu18+GnbNQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"cssnano-preset-default": "^7.0.17",
|
||||
"lilconfig": "^3.1.3"
|
||||
@@ -6517,7 +6534,6 @@
|
||||
"integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.8.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
@@ -6895,7 +6911,6 @@
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz",
|
||||
"integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"accepts": "~1.3.8",
|
||||
"array-flatten": "1.1.1",
|
||||
@@ -7890,16 +7905,26 @@
|
||||
}
|
||||
},
|
||||
"node_modules/http-proxy-agent": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
|
||||
"integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
|
||||
"version": "9.1.0",
|
||||
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-9.1.0.tgz",
|
||||
"integrity": "sha512-2NxoveTT58mjYT4n3RPTEfCZGLMbidoO8XEieXfpSYxu+PQJ1qpx4ypwH6N+uF9twBPIvRRgvkvW5HUTYWENig==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"agent-base": "^7.1.0",
|
||||
"debug": "^4.3.4"
|
||||
"agent-base": "9.0.0",
|
||||
"debug": "^4.3.4",
|
||||
"proxy-agent-negotiate": "1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
"node": ">= 20"
|
||||
}
|
||||
},
|
||||
"node_modules/http-proxy-agent/node_modules/agent-base": {
|
||||
"version": "9.0.0",
|
||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-9.0.0.tgz",
|
||||
"integrity": "sha512-TQf59BsZnytt8GdJKLPfUZ54g/iaUL2OWDSFCCvMOhsHduDQxO8xC4PNeyIkVcA5KwL2phPSv0douC0fgWzmnA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 20"
|
||||
}
|
||||
},
|
||||
"node_modules/http2-wrapper": {
|
||||
@@ -7916,16 +7941,26 @@
|
||||
}
|
||||
},
|
||||
"node_modules/https-proxy-agent": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
|
||||
"integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
|
||||
"version": "9.1.0",
|
||||
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-9.1.0.tgz",
|
||||
"integrity": "sha512-ag87y7cJJ9/3+GxFr8Oy4O5faDsGRGnBGsJj/YjOSsSx/5eadKLYTMPlzuR6obgoCDDm0abAAZitXXQkMOPSpA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"agent-base": "^7.1.2",
|
||||
"debug": "4"
|
||||
"agent-base": "9.0.0",
|
||||
"debug": "^4.3.4",
|
||||
"proxy-agent-negotiate": "1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
"node": ">= 20"
|
||||
}
|
||||
},
|
||||
"node_modules/https-proxy-agent/node_modules/agent-base": {
|
||||
"version": "9.0.0",
|
||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-9.0.0.tgz",
|
||||
"integrity": "sha512-TQf59BsZnytt8GdJKLPfUZ54g/iaUL2OWDSFCCvMOhsHduDQxO8xC4PNeyIkVcA5KwL2phPSv0douC0fgWzmnA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 20"
|
||||
}
|
||||
},
|
||||
"node_modules/human-signals": {
|
||||
@@ -8265,6 +8300,32 @@
|
||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/isomorphic-dompurify/node_modules/http-proxy-agent": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
|
||||
"integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"agent-base": "^7.1.0",
|
||||
"debug": "^4.3.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/isomorphic-dompurify/node_modules/https-proxy-agent": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
|
||||
"integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"agent-base": "^7.1.2",
|
||||
"debug": "4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/isomorphic-dompurify/node_modules/jsdom": {
|
||||
"version": "27.0.1",
|
||||
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-27.0.1.tgz",
|
||||
@@ -8463,7 +8524,6 @@
|
||||
"integrity": "sha512-Yi1jqNC/Oq0N4hBgNH/YvBpP1P57QqundgytzYqy3yqAa7NZPNjSoi4SGbRAXDMdBzNE6xBCi5U7RgfrvMEUVQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jest/core": "30.4.2",
|
||||
"@jest/types": "30.4.1",
|
||||
@@ -9204,6 +9264,32 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/jsdom/node_modules/http-proxy-agent": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
|
||||
"integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"agent-base": "^7.1.0",
|
||||
"debug": "^4.3.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/jsdom/node_modules/https-proxy-agent": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
|
||||
"integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"agent-base": "^7.1.2",
|
||||
"debug": "4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/jsesc": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
|
||||
@@ -10773,6 +10859,20 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/mongodb-memory-server-core/node_modules/https-proxy-agent": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
|
||||
"integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"agent-base": "^7.1.2",
|
||||
"debug": "4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/mongodb-memory-server-core/node_modules/mongodb": {
|
||||
"version": "7.3.0",
|
||||
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-7.3.0.tgz",
|
||||
@@ -11688,7 +11788,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.17",
|
||||
"picocolors": "^1.1.1",
|
||||
@@ -12372,6 +12471,23 @@
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-agent-negotiate": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-agent-negotiate/-/proxy-agent-negotiate-1.1.0.tgz",
|
||||
"integrity": "sha512-N8IBcM3UgCVzz2L2Lqv8DVntDnnC8/hiV4nEDUPkqq72TPUgYWjQc+bdZlBPZK9LzPAvOY//gAt0S0DApoOXWQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 20"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"kerberos": "^2.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"kerberos": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-from-env": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
|
||||
@@ -13763,7 +13879,6 @@
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
|
||||
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -13951,7 +14066,6 @@
|
||||
"integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@cspotcode/source-map-support": "^0.8.0",
|
||||
"@tsconfig/node10": "^1.0.7",
|
||||
@@ -14115,7 +14229,6 @@
|
||||
"integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -14869,7 +14982,6 @@
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
|
||||
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
|
||||
@@ -49,6 +49,8 @@
|
||||
"got": "14.4.7",
|
||||
"handlebars": "^4.7.8",
|
||||
"helmet": "^8.0.0",
|
||||
"http-proxy-agent": "^9.1.0",
|
||||
"https-proxy-agent": "^9.1.0",
|
||||
"isomorphic-dompurify": "^2.26.0",
|
||||
"jmespath": "^0.16.0",
|
||||
"jsdom": "^26.1.0",
|
||||
@@ -58,6 +60,7 @@
|
||||
"multer": "^1.4.5-lts.2",
|
||||
"nodemailer": "9.0.5",
|
||||
"ping": "0.4.4",
|
||||
"proxy-from-env": "^2.1.0",
|
||||
"re2": "^1.24.1",
|
||||
"sharp": "0.35.3",
|
||||
"ssl-checker": "2.0.10",
|
||||
|
||||
@@ -34,6 +34,17 @@ const envSchema = z.object({
|
||||
// Optional
|
||||
ORIGIN: z.string().optional(),
|
||||
|
||||
// Outbound proxy for monitor checks. HTTP_PROXY/HTTPS_PROXY/NO_PROXY follow
|
||||
// the conventional semantics (NO_PROXY supports wildcards and host lists) and
|
||||
// are read by proxy-from-env directly from the environment.
|
||||
HTTP_PROXY: z.string().optional(),
|
||||
HTTPS_PROXY: z.string().optional(),
|
||||
NO_PROXY: z.string().optional(),
|
||||
// Comma-separated monitor types to route through the proxy. Defaults to
|
||||
// "http,pagespeed"; "hardware" is excluded because Capture agents normally
|
||||
// live on the internal network. Set explicitly to override.
|
||||
PROXY_MONITOR_TYPES: z.string().optional(),
|
||||
|
||||
// Feature flags
|
||||
STATUS_PAGE_THEMES_ENABLED: booleanCoercion.default(true),
|
||||
});
|
||||
|
||||
@@ -70,7 +70,7 @@ export const buildWorker = async (shared: SharedServices, envSettings: EnvConfig
|
||||
// Network providers
|
||||
// ***********************
|
||||
const pingProvider = new PingProvider(ping);
|
||||
const httpProvider = new HttpProvider(got, new AdvancedMatcher(jmespath));
|
||||
const httpProvider = new HttpProvider(got, new AdvancedMatcher(jmespath), envSettings.proxyMonitorTypes, logger);
|
||||
const pageSpeedProvider = new PageSpeedProvider(httpProvider, settingsService, logger);
|
||||
const hardwareProvider = new HardwareProvider(httpProvider);
|
||||
const dockerProvider = new DockerProvider(logger, Docker);
|
||||
|
||||
@@ -16,6 +16,8 @@ export type EnvConfig = {
|
||||
queueMode: QueueMode;
|
||||
queuePrimaryProcesses: boolean;
|
||||
statusPageThemesEnabled: boolean;
|
||||
/** Comma-separated monitor types routed through the outbound proxy, if set. */
|
||||
proxyMonitorTypes?: string;
|
||||
clientConfig: ClientRuntimeConfig;
|
||||
};
|
||||
|
||||
@@ -43,6 +45,7 @@ export class SettingsService implements ISettingsService {
|
||||
queueMode: env.QUEUE_MODE,
|
||||
queuePrimaryProcesses: env.QUEUE_PRIMARY_PROCESSES,
|
||||
statusPageThemesEnabled: env.STATUS_PAGE_THEMES_ENABLED,
|
||||
proxyMonitorTypes: env.PROXY_MONITOR_TYPES,
|
||||
clientConfig: {
|
||||
...(env.CLIENT_CONFIG_API_BASE_URL && { apiBaseUrl: env.CLIENT_CONFIG_API_BASE_URL }),
|
||||
...(env.CLIENT_CONFIG_CLIENT_HOST && { clientHost: env.CLIENT_CONFIG_CLIENT_HOST }),
|
||||
|
||||
@@ -9,6 +9,7 @@ import { Monitor, MonitorType } from "@/domain/monitors/monitor.type.js";
|
||||
import { isStatusUp } from "@/service/network/utils.js";
|
||||
import { NETWORK_ERROR } from "@/types/network.js";
|
||||
import CacheableLookup from "cacheable-lookup";
|
||||
import { ProxyAgentCache, parseProxyMonitorTypes, resolveProxyForCheck } from "@/service/network/ProxyPolicy.js";
|
||||
|
||||
export class HttpProvider implements IStatusProvider<HttpStatusPayload> {
|
||||
readonly type = "http";
|
||||
@@ -18,9 +19,15 @@ export class HttpProvider implements IStatusProvider<HttpStatusPayload> {
|
||||
private readonly httpsAgent: HttpsAgent;
|
||||
private readonly httpsAgentInsecure: HttpsAgent;
|
||||
|
||||
// Outbound proxy, resolved once at startup from the environment
|
||||
private readonly proxyableTypes: Set<string>;
|
||||
private readonly proxyAgents = new ProxyAgentCache();
|
||||
|
||||
constructor(
|
||||
private got: Got,
|
||||
private advancedMatcher: IAdvancedMatcher
|
||||
private advancedMatcher: IAdvancedMatcher,
|
||||
proxyMonitorTypes?: string,
|
||||
logger?: { warn: (payload: { message: string; service: string; method: string }) => void }
|
||||
) {
|
||||
const cacheable = new CacheableLookup({ maxTtl: 300, errorTtl: 30 });
|
||||
this.got = got.extend({
|
||||
@@ -35,6 +42,12 @@ export class HttpProvider implements IStatusProvider<HttpStatusPayload> {
|
||||
this.httpAgent = new HttpAgent(agentOptions);
|
||||
this.httpsAgent = new HttpsAgent({ ...agentOptions, rejectUnauthorized: true });
|
||||
this.httpsAgentInsecure = new HttpsAgent({ ...agentOptions, rejectUnauthorized: false });
|
||||
|
||||
const { types, warning } = parseProxyMonitorTypes(proxyMonitorTypes);
|
||||
this.proxyableTypes = types;
|
||||
if (warning) {
|
||||
logger?.warn({ message: warning, service: "HttpProvider", method: "constructor" });
|
||||
}
|
||||
}
|
||||
|
||||
supports(type: MonitorType) {
|
||||
@@ -166,11 +179,37 @@ export class HttpProvider implements IStatusProvider<HttpStatusPayload> {
|
||||
const options: Record<string, unknown> = {
|
||||
headers: monitor.secret ? { Authorization: `Bearer ${secret}` } : undefined,
|
||||
};
|
||||
let cleanupProxyListener: (() => void) | undefined;
|
||||
|
||||
options.agent = {
|
||||
http: this.httpAgent,
|
||||
https: ignoreTlsErrors ? this.httpsAgentInsecure : this.httpsAgent,
|
||||
};
|
||||
// Route through an outbound proxy when the environment configures one and
|
||||
// this monitor type is eligible; otherwise use the shared direct agents.
|
||||
const { proxyUrl } = resolveProxyForCheck(monitor.type, url, this.proxyableTypes);
|
||||
// Set when the proxy itself refuses the tunnel, so a proxy ACL denial is not
|
||||
// reported as the target's own status code.
|
||||
let tunnelFailure: { statusCode: number } | null = null;
|
||||
if (proxyUrl) {
|
||||
const proxyAgent = this.proxyAgents.get(proxyUrl, url);
|
||||
options.agent = { http: proxyAgent, https: proxyAgent };
|
||||
// The agent's rejectUnauthorized governs the socket to the proxy, not the
|
||||
// tunnelled handshake with the target, so ignoreTlsErrors has to be stated
|
||||
// per request or it would be silently dropped for proxied monitors.
|
||||
options.https = { rejectUnauthorized: !ignoreTlsErrors };
|
||||
|
||||
const emitter = proxyAgent as unknown as {
|
||||
once?: (event: string, cb: (connect: { statusCode: number }) => void) => void;
|
||||
off?: (event: string, cb: (connect: { statusCode: number }) => void) => void;
|
||||
};
|
||||
const onProxyConnect = (connect: { statusCode: number }) => {
|
||||
if (connect.statusCode >= 400) tunnelFailure = { statusCode: connect.statusCode };
|
||||
};
|
||||
emitter.once?.("proxyConnect", onProxyConnect);
|
||||
cleanupProxyListener = () => emitter.off?.("proxyConnect", onProxyConnect);
|
||||
} else {
|
||||
options.agent = {
|
||||
http: this.httpAgent,
|
||||
https: ignoreTlsErrors ? this.httpsAgentInsecure : this.httpsAgent,
|
||||
};
|
||||
}
|
||||
|
||||
options.method = monitor.method;
|
||||
|
||||
@@ -188,7 +227,24 @@ export class HttpProvider implements IStatusProvider<HttpStatusPayload> {
|
||||
timings: response.timings,
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
if (tunnelFailure) {
|
||||
// The proxy rejected the tunnel, so the target was never reached and
|
||||
// its status is unknown. Reporting NETWORK_ERROR keeps customUpCodes
|
||||
// from marking the monitor up on the proxy's 403/407.
|
||||
return {
|
||||
monitorId: monitor.id,
|
||||
teamId: monitor.teamId,
|
||||
type: monitor.type,
|
||||
status: false,
|
||||
code: NETWORK_ERROR,
|
||||
message: `Proxy refused the connection (HTTP ${(tunnelFailure as { statusCode: number }).statusCode})`,
|
||||
responseTime: 0,
|
||||
payload: null as T,
|
||||
};
|
||||
}
|
||||
return this.handleHttpError(error, monitor);
|
||||
} finally {
|
||||
cleanupProxyListener?.();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
import { HttpProxyAgent } from "http-proxy-agent";
|
||||
import { HttpsProxyAgent } from "https-proxy-agent";
|
||||
import { getProxyForUrl } from "proxy-from-env";
|
||||
import type { Agent as HttpAgent } from "http";
|
||||
import type { Agent as HttpsAgent } from "https";
|
||||
import { MonitorTypes, type MonitorType } from "@/domain/monitors/monitor.type.js";
|
||||
|
||||
/**
|
||||
* Monitor types whose checks may be routed through an outbound HTTP proxy.
|
||||
*
|
||||
* Only types served by HttpProvider can be proxied at all — ping, port, docker,
|
||||
* grpc, websocket, dns and game open raw sockets, which an HTTP proxy cannot
|
||||
* carry.
|
||||
*
|
||||
* "hardware" is deliberately excluded by default: infrastructure monitors talk
|
||||
* to Capture agents, which almost always sit on the internal network. Sending
|
||||
* those through a corporate proxy fails or hairpins, so operators must opt in
|
||||
* explicitly via PROXY_MONITOR_TYPES.
|
||||
*/
|
||||
export const PROXYABLE_MONITOR_TYPES = ["http", "pagespeed"] as const;
|
||||
export type ProxyableMonitorType = (typeof PROXYABLE_MONITOR_TYPES)[number];
|
||||
|
||||
/** Types that reach the network through HttpProvider, and so could be proxied. */
|
||||
const HTTP_BACKED_MONITOR_TYPES = ["http", "pagespeed", "hardware"] as const;
|
||||
|
||||
export interface ProxyResolution {
|
||||
/** Proxy URL to dial through, or null to connect directly. */
|
||||
proxyUrl: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the PROXY_MONITOR_TYPES escape hatch.
|
||||
*
|
||||
* Unset falls back to PROXYABLE_MONITOR_TYPES, and the literal "none" disables
|
||||
* proxying entirely. Entries that are not real monitor types, or that never
|
||||
* reach the network through HttpProvider, are dropped and reported so a typo
|
||||
* cannot silently disable proxying.
|
||||
*/
|
||||
export const parseProxyMonitorTypes = (raw: string | undefined): { types: Set<string>; warning?: string } => {
|
||||
if (raw === undefined || raw.trim() === "") {
|
||||
return { types: new Set<string>(PROXYABLE_MONITOR_TYPES) };
|
||||
}
|
||||
|
||||
const requested = raw
|
||||
.split(",")
|
||||
.map((entry) => entry.trim().toLowerCase())
|
||||
.filter((entry) => entry !== "");
|
||||
|
||||
// Explicit opt-out, so proxying can be disabled without unsetting the proxy
|
||||
// environment variables the rest of the container may rely on. Honoured
|
||||
// wherever it appears, so "none,http" disables rather than half-enabling.
|
||||
if (requested.includes("none")) {
|
||||
return { types: new Set<string>() };
|
||||
}
|
||||
|
||||
const types = new Set<string>();
|
||||
const unknown: string[] = [];
|
||||
const unproxyable: string[] = [];
|
||||
|
||||
for (const entry of requested) {
|
||||
// "unknown" is a real MonitorType but never a deliberate choice here.
|
||||
if (entry === "unknown" || !(MonitorTypes as readonly string[]).includes(entry)) {
|
||||
unknown.push(entry);
|
||||
} else if (!(HTTP_BACKED_MONITOR_TYPES as readonly string[]).includes(entry)) {
|
||||
unproxyable.push(entry);
|
||||
} else {
|
||||
types.add(entry);
|
||||
}
|
||||
}
|
||||
|
||||
const problems: string[] = [];
|
||||
if (unknown.length > 0) {
|
||||
problems.push(`unknown monitor type(s): ${unknown.join(", ")}`);
|
||||
}
|
||||
if (unproxyable.length > 0) {
|
||||
problems.push(`monitor type(s) that do not use HTTP and cannot be proxied: ${unproxyable.join(", ")}`);
|
||||
}
|
||||
|
||||
// Every entry was a typo. Disabling all proxying here would take every HTTP
|
||||
// monitor down in a restricted-egress deployment, so fall back to the defaults
|
||||
// and make the misconfiguration loud instead.
|
||||
//
|
||||
// An all-unproxyable list (e.g. "ping,port") is left empty on purpose: those
|
||||
// are deliberate choices, and silently proxying http instead would route
|
||||
// traffic the operator never nominated.
|
||||
if (types.size === 0 && unproxyable.length === 0) {
|
||||
return {
|
||||
types: new Set<string>(PROXYABLE_MONITOR_TYPES),
|
||||
warning: `PROXY_MONITOR_TYPES matched no proxyable monitor type — ${problems.join("; ")}. Falling back to: ${PROXYABLE_MONITOR_TYPES.join(", ")}. Use "none" to disable proxying.`,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
types,
|
||||
warning: problems.length > 0 ? `PROXY_MONITOR_TYPES ignored ${problems.join("; ")}. Proxying: ${[...types].join(", ")}` : undefined,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Decides whether a single check should be proxied.
|
||||
*
|
||||
* Honours NO_PROXY (including wildcards and port-qualified hosts) by delegating
|
||||
* to proxy-from-env, so the same rules operators already use for curl apply
|
||||
* here rather than a hand-rolled reimplementation.
|
||||
*
|
||||
* proxy-from-env resolves against process.env, which is read once at startup
|
||||
* (see createProxySettings). Checks run concurrently, so the environment is
|
||||
* never mutated per-check.
|
||||
*/
|
||||
export const resolveProxyForCheck = (monitorType: MonitorType, url: string, proxyableTypes: Set<string>): ProxyResolution => {
|
||||
if (!proxyableTypes.has(monitorType)) {
|
||||
return { proxyUrl: null };
|
||||
}
|
||||
|
||||
const proxyUrl = getProxyForUrl(url);
|
||||
return { proxyUrl: proxyUrl === "" ? null : proxyUrl };
|
||||
};
|
||||
|
||||
/**
|
||||
* Builds proxy agents on demand and reuses them.
|
||||
*
|
||||
* The agent must match the *target* scheme, not the proxy's. HttpsProxyAgent
|
||||
* always issues CONNECT, which hardened proxies (Squid's stock ACL among them)
|
||||
* permit only to 443 — so a plain-http target needs HttpProxyAgent, which sends
|
||||
* an absolute-URI request instead.
|
||||
*
|
||||
* Caching preserves connection pooling: without it every check would open a
|
||||
* fresh connection to the proxy. Node keys its own socket pools on the TLS
|
||||
* options it receives per request, so agents are shared across monitors
|
||||
* regardless of ignoreTlsErrors — that setting is applied per request in
|
||||
* HttpProvider, not baked into the agent.
|
||||
*/
|
||||
export class ProxyAgentCache {
|
||||
private readonly agents = new Map<string, HttpAgent | HttpsAgent>();
|
||||
|
||||
get(proxyUrl: string, targetUrl: string): HttpAgent | HttpsAgent {
|
||||
const targetIsSecure = safeProtocol(targetUrl) !== "http:";
|
||||
const key = `${proxyUrl}::${targetIsSecure ? "https" : "http"}`;
|
||||
const existing = this.agents.get(key);
|
||||
if (existing) return existing;
|
||||
|
||||
const options = { keepAlive: true, maxSockets: 256, maxFreeSockets: 256 };
|
||||
const agent = (targetIsSecure ? new HttpsProxyAgent(proxyUrl, options) : new HttpProxyAgent(proxyUrl, options)) as unknown as HttpsAgent;
|
||||
|
||||
this.agents.set(key, agent);
|
||||
return agent;
|
||||
}
|
||||
|
||||
/** Number of distinct agents held; used in tests to prove pooling. */
|
||||
get size(): number {
|
||||
return this.agents.size;
|
||||
}
|
||||
}
|
||||
|
||||
/** Target URLs are validated upstream; fall back to https on anything odd. */
|
||||
const safeProtocol = (url: string): string => {
|
||||
try {
|
||||
return new URL(url).protocol;
|
||||
} catch {
|
||||
return "https:";
|
||||
}
|
||||
};
|
||||
Vendored
+8
@@ -0,0 +1,8 @@
|
||||
declare module "proxy-from-env" {
|
||||
/**
|
||||
* Resolves the proxy that should handle a request to the given URL, reading
|
||||
* HTTP_PROXY / HTTPS_PROXY / NO_PROXY (and their lowercase forms) from
|
||||
* process.env. Returns an empty string when the request should go direct.
|
||||
*/
|
||||
export function getProxyForUrl(url: string): string;
|
||||
}
|
||||
@@ -74,9 +74,9 @@ const makeGotResponse = (overrides?: Record<string, any>) => ({
|
||||
...overrides,
|
||||
});
|
||||
|
||||
const createProvider = (matcher?: any) => {
|
||||
const createProvider = (matcher?: any, proxyMonitorTypes?: string) => {
|
||||
const advancedMatcher = matcher ?? createMockMatcher();
|
||||
const provider = new HttpProvider(mockGot as any, advancedMatcher);
|
||||
const provider = new HttpProvider(mockGot as any, advancedMatcher, proxyMonitorTypes);
|
||||
return { provider, advancedMatcher };
|
||||
};
|
||||
|
||||
@@ -494,4 +494,128 @@ describe("HttpProvider", () => {
|
||||
expect(advancedMatcher.validate).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
// ── Outbound proxy ───────────────────────────────────────────────────
|
||||
|
||||
describe("outbound proxy routing", () => {
|
||||
const PROXY_KEYS = ["HTTP_PROXY", "HTTPS_PROXY", "NO_PROXY", "http_proxy", "https_proxy", "no_proxy"];
|
||||
const clearProxyEnv = () => {
|
||||
for (const key of PROXY_KEYS) delete process.env[key];
|
||||
};
|
||||
|
||||
const agentOf = () => (mockGot.mock.calls[0]?.[1] as any)?.agent;
|
||||
const httpsOptionOf = () => (mockGot.mock.calls[0]?.[1] as any)?.https;
|
||||
const isProxyAgent = (agent: any) => Boolean(agent?.https?.constructor?.name?.includes("Proxy"));
|
||||
|
||||
it("routes an http monitor through the configured proxy", async () => {
|
||||
clearProxyEnv();
|
||||
process.env.HTTPS_PROXY = "http://corp:3128";
|
||||
mockGot.mockResolvedValue(makeGotResponse());
|
||||
const { provider } = createProvider();
|
||||
|
||||
await provider.handle(makeMonitor({ url: "https://example.com" }));
|
||||
|
||||
expect(isProxyAgent(agentOf())).toBe(true);
|
||||
clearProxyEnv();
|
||||
});
|
||||
|
||||
it("leaves infrastructure monitors direct by default", async () => {
|
||||
clearProxyEnv();
|
||||
process.env.HTTPS_PROXY = "http://corp:3128";
|
||||
mockGot.mockResolvedValue(makeGotResponse());
|
||||
const { provider } = createProvider();
|
||||
|
||||
// Capture agents live on the internal network; proxying them breaks them.
|
||||
await provider.handle(makeMonitor({ type: "hardware", url: "https://10.0.0.5:59232" }));
|
||||
|
||||
expect(isProxyAgent(agentOf())).toBe(false);
|
||||
clearProxyEnv();
|
||||
});
|
||||
|
||||
it("routes infrastructure monitors once opted in", async () => {
|
||||
clearProxyEnv();
|
||||
process.env.HTTPS_PROXY = "http://corp:3128";
|
||||
mockGot.mockResolvedValue(makeGotResponse());
|
||||
const { provider } = createProvider(undefined, "http,hardware");
|
||||
|
||||
await provider.handle(makeMonitor({ type: "hardware", url: "https://10.0.0.5:59232" }));
|
||||
|
||||
expect(isProxyAgent(agentOf())).toBe(true);
|
||||
clearProxyEnv();
|
||||
});
|
||||
|
||||
it("keeps ignoreTlsErrors effective for proxied monitors", async () => {
|
||||
clearProxyEnv();
|
||||
process.env.HTTPS_PROXY = "http://corp:3128";
|
||||
mockGot.mockResolvedValue(makeGotResponse());
|
||||
const { provider } = createProvider();
|
||||
|
||||
// The proxy agent's rejectUnauthorized applies to the socket to the proxy,
|
||||
// not the tunnelled handshake, so this must be stated per request or a
|
||||
// self-signed target would start failing the moment a proxy is configured.
|
||||
await provider.handle(makeMonitor({ url: "https://self-signed.example.com", ignoreTlsErrors: true }));
|
||||
|
||||
expect(httpsOptionOf()).toEqual({ rejectUnauthorized: false });
|
||||
clearProxyEnv();
|
||||
});
|
||||
|
||||
it("keeps TLS strict for proxied monitors that did not opt out", async () => {
|
||||
clearProxyEnv();
|
||||
process.env.HTTPS_PROXY = "http://corp:3128";
|
||||
mockGot.mockResolvedValue(makeGotResponse());
|
||||
const { provider } = createProvider();
|
||||
|
||||
await provider.handle(makeMonitor({ url: "https://example.com", ignoreTlsErrors: false }));
|
||||
|
||||
expect(httpsOptionOf()).toEqual({ rejectUnauthorized: true });
|
||||
clearProxyEnv();
|
||||
});
|
||||
|
||||
it("does not set a proxy agent when no proxy is configured", async () => {
|
||||
clearProxyEnv();
|
||||
mockGot.mockResolvedValue(makeGotResponse());
|
||||
const { provider } = createProvider();
|
||||
|
||||
await provider.handle(makeMonitor({ url: "https://example.com" }));
|
||||
|
||||
expect(isProxyAgent(agentOf())).toBe(false);
|
||||
});
|
||||
|
||||
it("reports a refused proxy tunnel as a network error, not the target's status", async () => {
|
||||
clearProxyEnv();
|
||||
process.env.HTTPS_PROXY = "http://corp:3128";
|
||||
const { provider } = createProvider();
|
||||
// The agent replays a refused CONNECT onto the socket, so the proxy's own
|
||||
// status would otherwise be parsed as the target's.
|
||||
const agent: any = (provider as any).proxyAgents.get("http://corp:3128", "https://example.com");
|
||||
const err = new HTTPError("Response code 403 (Forbidden)");
|
||||
(err as any).response = { statusCode: 403, body: "", headers: {} };
|
||||
(err as any).timings = { phases: { total: 5 } };
|
||||
mockGot.mockImplementation(() => {
|
||||
agent.emit("proxyConnect", { statusCode: 403 });
|
||||
return Promise.reject(err);
|
||||
});
|
||||
|
||||
const result = await provider.handle(makeMonitor({ url: "https://example.com", customUpCodes: [403] }));
|
||||
|
||||
// customUpCodes must not mark the monitor up on the proxy's status.
|
||||
expect(result.status).toBe(false);
|
||||
expect(result.code).toBe(NETWORK_ERROR);
|
||||
expect(result.message).toContain("Proxy refused");
|
||||
clearProxyEnv();
|
||||
});
|
||||
|
||||
it("honours NO_PROXY", async () => {
|
||||
clearProxyEnv();
|
||||
process.env.HTTPS_PROXY = "http://corp:3128";
|
||||
process.env.NO_PROXY = "example.com";
|
||||
mockGot.mockResolvedValue(makeGotResponse());
|
||||
const { provider } = createProvider();
|
||||
|
||||
await provider.handle(makeMonitor({ url: "https://example.com" }));
|
||||
|
||||
expect(isProxyAgent(agentOf())).toBe(false);
|
||||
clearProxyEnv();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,218 @@
|
||||
import { describe, expect, it, afterEach } from "@jest/globals";
|
||||
import {
|
||||
PROXYABLE_MONITOR_TYPES,
|
||||
ProxyAgentCache,
|
||||
parseProxyMonitorTypes,
|
||||
resolveProxyForCheck,
|
||||
} from "../../../../src/service/network/ProxyPolicy.ts";
|
||||
|
||||
// proxy-from-env reads process.env directly, so these tests set and restore it.
|
||||
const PROXY_KEYS = ["HTTP_PROXY", "HTTPS_PROXY", "NO_PROXY", "http_proxy", "https_proxy", "no_proxy"];
|
||||
|
||||
const withEnv = (vars: Record<string, string>) => {
|
||||
for (const key of PROXY_KEYS) delete process.env[key];
|
||||
for (const [key, value] of Object.entries(vars)) process.env[key] = value;
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
for (const key of PROXY_KEYS) delete process.env[key];
|
||||
});
|
||||
|
||||
const defaults = () => parseProxyMonitorTypes(undefined).types;
|
||||
|
||||
describe("parseProxyMonitorTypes", () => {
|
||||
it("defaults to http and pagespeed, excluding infrastructure", () => {
|
||||
const { types, warning } = parseProxyMonitorTypes(undefined);
|
||||
|
||||
expect([...types].sort()).toEqual([...PROXYABLE_MONITOR_TYPES].sort());
|
||||
expect(types.has("hardware")).toBe(false);
|
||||
expect(warning).toBeUndefined();
|
||||
});
|
||||
|
||||
it("treats an empty value as unset", () => {
|
||||
expect([...parseProxyMonitorTypes(" ").types].sort()).toEqual([...PROXYABLE_MONITOR_TYPES].sort());
|
||||
});
|
||||
|
||||
it("lets operators opt infrastructure in", () => {
|
||||
const { types, warning } = parseProxyMonitorTypes("http,hardware");
|
||||
|
||||
expect(types.has("hardware")).toBe(true);
|
||||
expect(types.has("pagespeed")).toBe(false);
|
||||
expect(warning).toBeUndefined();
|
||||
});
|
||||
|
||||
it("tolerates whitespace and mixed case", () => {
|
||||
const { types } = parseProxyMonitorTypes(" HTTP , Hardware ");
|
||||
|
||||
expect([...types].sort()).toEqual(["hardware", "http"]);
|
||||
});
|
||||
|
||||
it("allows proxying to be disabled entirely with an explicit sentinel", () => {
|
||||
const { types, warning } = parseProxyMonitorTypes("none");
|
||||
|
||||
expect(types.size).toBe(0);
|
||||
// "none" is a supported value, not a typo, so it must not warn.
|
||||
expect(warning).toBeUndefined();
|
||||
});
|
||||
|
||||
it("honours the none sentinel wherever it appears", () => {
|
||||
expect(parseProxyMonitorTypes("none,http").types.size).toBe(0);
|
||||
});
|
||||
|
||||
it("leaves an all-unproxyable list empty rather than proxying types nobody named", () => {
|
||||
// "ping,port" is a misunderstanding, but a deliberate one — silently
|
||||
// proxying http instead would route traffic the operator never nominated.
|
||||
const { types, warning } = parseProxyMonitorTypes("ping,port");
|
||||
|
||||
expect(types.size).toBe(0);
|
||||
expect(warning).toContain("cannot be proxied");
|
||||
});
|
||||
|
||||
it("falls back to defaults when every entry is invalid, rather than disabling proxying", () => {
|
||||
// Silently proxying nothing would take every HTTP monitor down in a
|
||||
// restricted-egress deployment, which is worse than ignoring the typo.
|
||||
const { types, warning } = parseProxyMonitorTypes("htpp");
|
||||
|
||||
expect([...types].sort()).toEqual([...PROXYABLE_MONITOR_TYPES].sort());
|
||||
expect(warning).toContain("Falling back to");
|
||||
});
|
||||
|
||||
it("treats the internal unknown type as a typo", () => {
|
||||
const { types, warning } = parseProxyMonitorTypes("unknown");
|
||||
|
||||
expect([...types].sort()).toEqual([...PROXYABLE_MONITOR_TYPES].sort());
|
||||
expect(warning).toContain("unknown monitor type");
|
||||
});
|
||||
|
||||
it("drops socket-based types that a proxy cannot carry, and says so", () => {
|
||||
const { types, warning } = parseProxyMonitorTypes("http,ping,port,dns");
|
||||
|
||||
expect([...types]).toEqual(["http"]);
|
||||
expect(warning).toContain("cannot be proxied");
|
||||
expect(warning).toContain("ping");
|
||||
});
|
||||
|
||||
it("reports unknown types rather than silently ignoring them", () => {
|
||||
const { types, warning } = parseProxyMonitorTypes("http,htpp");
|
||||
|
||||
expect([...types]).toEqual(["http"]);
|
||||
expect(warning).toContain("unknown monitor type");
|
||||
expect(warning).toContain("htpp");
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveProxyForCheck", () => {
|
||||
it("proxies a public HTTP monitor", () => {
|
||||
withEnv({ HTTPS_PROXY: "http://corp:3128" });
|
||||
|
||||
expect(resolveProxyForCheck("http", "https://example.com", defaults()).proxyUrl).toBe("http://corp:3128");
|
||||
});
|
||||
|
||||
it("leaves infrastructure checks direct by default", () => {
|
||||
withEnv({ HTTPS_PROXY: "http://corp:3128" });
|
||||
|
||||
// Capture agents sit on the internal network; proxying them would break them.
|
||||
expect(resolveProxyForCheck("hardware", "https://10.0.0.5:59232", defaults()).proxyUrl).toBeNull();
|
||||
});
|
||||
|
||||
it("proxies infrastructure once opted in", () => {
|
||||
withEnv({ HTTPS_PROXY: "http://corp:3128" });
|
||||
const types = parseProxyMonitorTypes("http,hardware").types;
|
||||
|
||||
expect(resolveProxyForCheck("hardware", "https://10.0.0.5:59232", types).proxyUrl).toBe("http://corp:3128");
|
||||
});
|
||||
|
||||
it("returns null when no proxy is configured", () => {
|
||||
withEnv({});
|
||||
|
||||
expect(resolveProxyForCheck("http", "https://example.com", defaults()).proxyUrl).toBeNull();
|
||||
});
|
||||
|
||||
it("honours NO_PROXY host entries", () => {
|
||||
withEnv({ HTTPS_PROXY: "http://corp:3128", NO_PROXY: "internal.example.com" });
|
||||
|
||||
expect(resolveProxyForCheck("http", "https://internal.example.com", defaults()).proxyUrl).toBeNull();
|
||||
expect(resolveProxyForCheck("http", "https://public.example.com", defaults()).proxyUrl).toBe("http://corp:3128");
|
||||
});
|
||||
|
||||
it("honours NO_PROXY wildcards", () => {
|
||||
withEnv({ HTTPS_PROXY: "http://corp:3128", NO_PROXY: "*.internal" });
|
||||
|
||||
expect(resolveProxyForCheck("http", "https://box.internal", defaults()).proxyUrl).toBeNull();
|
||||
});
|
||||
|
||||
it("keeps credentials in an authenticated proxy URL", () => {
|
||||
withEnv({ HTTPS_PROXY: "http://enki:s3cr3t@corp:3128" });
|
||||
|
||||
expect(resolveProxyForCheck("http", "https://example.com", defaults()).proxyUrl).toBe("http://enki:s3cr3t@corp:3128");
|
||||
});
|
||||
|
||||
it("selects the proxy matching the target scheme", () => {
|
||||
withEnv({ HTTP_PROXY: "http://plain:3128", HTTPS_PROXY: "http://secure:3129" });
|
||||
|
||||
expect(resolveProxyForCheck("http", "http://example.com", defaults()).proxyUrl).toBe("http://plain:3128");
|
||||
expect(resolveProxyForCheck("http", "https://example.com", defaults()).proxyUrl).toBe("http://secure:3129");
|
||||
});
|
||||
|
||||
it("never proxies socket-based monitor types", () => {
|
||||
withEnv({ HTTPS_PROXY: "http://corp:3128" });
|
||||
|
||||
for (const type of ["ping", "port", "docker", "grpc", "websocket", "dns", "game"] as const) {
|
||||
expect(resolveProxyForCheck(type, "https://example.com", defaults()).proxyUrl).toBeNull();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("ProxyAgentCache", () => {
|
||||
it("reuses one agent per proxy so connections stay pooled", () => {
|
||||
const cache = new ProxyAgentCache();
|
||||
|
||||
const first = cache.get("http://corp:3128", "https://example.com");
|
||||
const second = cache.get("http://corp:3128", "https://other.example.com");
|
||||
|
||||
expect(second).toBe(first);
|
||||
expect(cache.size).toBe(1);
|
||||
});
|
||||
|
||||
it("uses a CONNECT-based agent for https targets", () => {
|
||||
const cache = new ProxyAgentCache();
|
||||
|
||||
const agent = cache.get("http://corp:3128", "https://example.com");
|
||||
|
||||
expect(agent.constructor.name).toBe("HttpsProxyAgent");
|
||||
});
|
||||
|
||||
it("uses an absolute-URI agent for plain http targets", () => {
|
||||
const cache = new ProxyAgentCache();
|
||||
|
||||
// HttpsProxyAgent would issue CONNECT, which hardened proxies allow only
|
||||
// to 443, breaking every http:// monitor.
|
||||
const agent = cache.get("http://corp:3128", "http://example.com");
|
||||
|
||||
expect(agent.constructor.name).toBe("HttpProxyAgent");
|
||||
});
|
||||
|
||||
it("keeps http and https target agents separate", () => {
|
||||
const cache = new ProxyAgentCache();
|
||||
|
||||
cache.get("http://corp:3128", "https://example.com");
|
||||
cache.get("http://corp:3128", "http://example.com");
|
||||
|
||||
expect(cache.size).toBe(2);
|
||||
});
|
||||
|
||||
it("keeps a separate agent per proxy URL", () => {
|
||||
const cache = new ProxyAgentCache();
|
||||
|
||||
cache.get("http://a:3128", "https://example.com");
|
||||
cache.get("http://b:3128", "https://example.com");
|
||||
|
||||
expect(cache.size).toBe(2);
|
||||
});
|
||||
|
||||
it("falls back to a CONNECT agent for an unparseable target", () => {
|
||||
const cache = new ProxyAgentCache();
|
||||
|
||||
expect(cache.get("http://corp:3128", "not-a-url").constructor.name).toBe("HttpsProxyAgent");
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user