renovate 44.77.0 → 44.79.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/config/global.d.ts +1 -1
- package/dist/config/global.js +2 -0
- package/dist/config/global.js.map +1 -1
- package/dist/config/options/index.js +35 -0
- package/dist/config/options/index.js.map +1 -1
- package/dist/config/presets/http/index.js +3 -1
- package/dist/config/presets/http/index.js.map +1 -1
- package/dist/config/presets/index.js +7 -1
- package/dist/config/presets/index.js.map +1 -1
- package/dist/config/presets/npm/index.js +4 -2
- package/dist/config/presets/npm/index.js.map +1 -1
- package/dist/config/types.d.ts +4 -1
- package/dist/config/types.js.map +1 -1
- package/dist/config/validation.js +18 -0
- package/dist/config/validation.js.map +1 -1
- package/dist/constants/error-messages.d.ts +2 -1
- package/dist/constants/error-messages.js +2 -1
- package/dist/constants/error-messages.js.map +1 -1
- package/dist/global-config-option-defaults.generated.js +2 -0
- package/dist/global-config-option-defaults.generated.js.map +1 -1
- package/dist/modules/datasource/docker/common.js +3 -2
- package/dist/modules/datasource/docker/common.js.map +1 -1
- package/dist/modules/datasource/index.js +1 -1
- package/dist/modules/datasource/index.js.map +1 -1
- package/dist/modules/datasource/maven/util.js +3 -2
- package/dist/modules/datasource/maven/util.js.map +1 -1
- package/dist/modules/datasource/npm/get.js +1 -1
- package/dist/modules/datasource/npm/get.js.map +1 -1
- package/dist/modules/datasource/pod/index.js +2 -1
- package/dist/modules/datasource/pod/index.js.map +1 -1
- package/dist/modules/datasource/repology/index.js +3 -2
- package/dist/modules/datasource/repology/index.js.map +1 -1
- package/dist/modules/manager/fingerprint.generated.js +2 -2
- package/dist/modules/manager/fingerprint.generated.js.map +1 -1
- package/dist/modules/manager/github-actions/community.js +5 -0
- package/dist/modules/manager/github-actions/community.js.map +1 -1
- package/dist/modules/manager/npm/post-update/index.js +1 -1
- package/dist/modules/manager/npm/post-update/index.js.map +1 -1
- package/dist/modules/platform/utils/pr-body.js +1 -1
- package/dist/modules/platform/utils/pr-body.js.map +1 -1
- package/dist/types/host-rules.d.ts +35 -2
- package/dist/types/index.d.ts +2 -2
- package/dist/util/host-rules.d.ts +8 -2
- package/dist/util/host-rules.js +54 -6
- package/dist/util/host-rules.js.map +1 -1
- package/dist/util/http/host-guard.js +279 -0
- package/dist/util/http/host-guard.js.map +1 -0
- package/dist/util/http/host-rules.js.map +1 -1
- package/dist/util/http/http.d.ts +8 -0
- package/dist/util/http/http.js +37 -21
- package/dist/util/http/http.js.map +1 -1
- package/dist/util/http/types.d.ts +6 -0
- package/dist/util/http/types.js.map +1 -1
- package/dist/util/http/util.js +10 -1
- package/dist/util/http/util.js.map +1 -1
- package/dist/workers/repository/init/inherited.js +28 -4
- package/dist/workers/repository/init/inherited.js.map +1 -1
- package/dist/workers/repository/init/merge.js +28 -12
- package/dist/workers/repository/init/merge.js.map +1 -1
- package/dist/workers/repository/update/pr/changelog/index.js +7 -2
- package/dist/workers/repository/update/pr/changelog/index.js.map +1 -1
- package/dist/workers/repository/update/pr/index.js +14 -15
- package/dist/workers/repository/update/pr/index.js.map +1 -1
- package/package.json +1 -1
- package/renovate-schema.json +17 -2
package/dist/util/host-rules.js
CHANGED
|
@@ -5,7 +5,7 @@ import { addSecretForSanitizing, clearRepoSanitizedSecretsList } from "./sanitiz
|
|
|
5
5
|
import { logger } from "../logger/index.js";
|
|
6
6
|
import { clone } from "./clone.js";
|
|
7
7
|
import { isHttpUrl, massageHostUrl, parseUrl } from "./url.js";
|
|
8
|
-
import { isFalsy, isString, isTruthy, isUndefined } from "@sindresorhus/is";
|
|
8
|
+
import { isFalsy, isNonEmptyString, isString, isTruthy, isUndefined } from "@sindresorhus/is";
|
|
9
9
|
//#region lib/util/host-rules.ts
|
|
10
10
|
let hostRules = [];
|
|
11
11
|
/**
|
|
@@ -73,10 +73,26 @@ function filterAllowedHeaders(rules, allowedHeaders, warnOnDenied = true) {
|
|
|
73
73
|
if (denied.length && warnOnDenied) logger.warn({ denied }, "Ignoring hostRules headers not permitted by this Renovate instance's `allowedHeaders`");
|
|
74
74
|
return result;
|
|
75
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* The trust tier a caller asked for.
|
|
78
|
+
*
|
|
79
|
+
* Deliberately opt-in: a caller that says nothing registers into the untrusted tier, so a new call site cannot grant itself the administrator's precedence by omission.
|
|
80
|
+
*/
|
|
81
|
+
function trustTierFor(options) {
|
|
82
|
+
if (options?.trusted) return "admin";
|
|
83
|
+
if (options?.inherited) return "inherit";
|
|
84
|
+
return "untrusted";
|
|
85
|
+
}
|
|
76
86
|
function add(params, options) {
|
|
77
|
-
let rule =
|
|
87
|
+
let rule = {
|
|
88
|
+
...migrateRule(params),
|
|
89
|
+
trustTier: trustTierFor(options)
|
|
90
|
+
};
|
|
78
91
|
delete rule.trusted;
|
|
79
|
-
if (
|
|
92
|
+
if (!isUndefined(rule.allowInternal) && rule.trustTier === "untrusted") {
|
|
93
|
+
logger.debug(`Ignoring hostRules allowInternal for ${rule.matchHost ?? rule.hostType} from untrusted config`);
|
|
94
|
+
delete rule.allowInternal;
|
|
95
|
+
}
|
|
80
96
|
if (rule.headers) [rule] = filterAllowedHeaders([rule], options?.allowedHeaders);
|
|
81
97
|
if (rule.matchHost) {
|
|
82
98
|
rule.matchHost = massageHostUrl(rule.matchHost);
|
|
@@ -128,6 +144,18 @@ function fromLowerRankAndShorterMatchHost(a, b) {
|
|
|
128
144
|
function headersOfLastRuleToSetThem(rules) {
|
|
129
145
|
return rules.map((rule) => rule.headers).filter(isTruthy).pop();
|
|
130
146
|
}
|
|
147
|
+
/**
|
|
148
|
+
* The last defined value, following the same "most specific rule wins" ordering as {@link headersOfLastRuleToSetThem}.
|
|
149
|
+
*/
|
|
150
|
+
function lastDefined(values) {
|
|
151
|
+
return values.filter((value) => !isUndefined(value)).pop();
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* The rules whose grant is deliberately scoped, by a `hostType` or by a URL-prefix `matchHost` - see {@link InternalHostGrant}.
|
|
155
|
+
*/
|
|
156
|
+
function scopedRules(rules) {
|
|
157
|
+
return rules.filter((rule) => isNonEmptyString(rule.hostType) || isHttpUrl(rule.matchHost));
|
|
158
|
+
}
|
|
131
159
|
function find(search) {
|
|
132
160
|
if ([search.hostType, search.url].every(isFalsy)) {
|
|
133
161
|
logger.warn({ search }, "Invalid hostRules search");
|
|
@@ -159,17 +187,37 @@ function find(search) {
|
|
|
159
187
|
if (hostTypeMatch && readOnlyMatch && hostMatch) matchedRules.push(clone(rule));
|
|
160
188
|
}
|
|
161
189
|
const res = Object.assign({}, ...matchedRules);
|
|
162
|
-
const
|
|
163
|
-
const
|
|
164
|
-
|
|
190
|
+
const trustedRules = matchedRules.filter((rule) => rule.trustTier === "admin");
|
|
191
|
+
const inheritedRules = matchedRules.filter((rule) => rule.trustTier === "inherit");
|
|
192
|
+
const untrustedRules = matchedRules.filter((rule) => rule.trustTier === "untrusted");
|
|
193
|
+
const untrustedHeaders = headersOfLastRuleToSetThem(untrustedRules);
|
|
194
|
+
const inheritedHeaders = headersOfLastRuleToSetThem(inheritedRules);
|
|
195
|
+
const trustedHeaders = headersOfLastRuleToSetThem(trustedRules);
|
|
196
|
+
if (untrustedHeaders ?? inheritedHeaders ?? trustedHeaders) res.headers = {
|
|
165
197
|
...untrustedHeaders,
|
|
198
|
+
...inheritedHeaders,
|
|
166
199
|
...trustedHeaders
|
|
167
200
|
};
|
|
201
|
+
const enabled = lastDefined(trustedRules.map((rule) => rule.enabled)) ?? lastDefined(inheritedRules.map((rule) => rule.enabled)) ?? lastDefined(untrustedRules.map((rule) => rule.enabled));
|
|
202
|
+
if (!isUndefined(enabled)) res.enabled = enabled;
|
|
203
|
+
const trustedExplicit = lastDefined(trustedRules.map((rule) => rule.allowInternal));
|
|
204
|
+
const trustedScoped = lastDefined(scopedRules(trustedRules).map((rule) => rule.allowInternal));
|
|
205
|
+
const adminVeto = trustedExplicit === false || trustedScoped === false;
|
|
206
|
+
const inheritedExplicit = adminVeto ? false : lastDefined(inheritedRules.map((rule) => rule.allowInternal));
|
|
207
|
+
const inheritedScoped = adminVeto ? false : lastDefined(scopedRules(inheritedRules).map((rule) => rule.allowInternal));
|
|
208
|
+
const internalHostGrant = {
|
|
209
|
+
explicit: trustedExplicit ?? inheritedExplicit,
|
|
210
|
+
scoped: trustedScoped ?? inheritedScoped,
|
|
211
|
+
implicit: !adminVeto && [...trustedRules, ...inheritedRules].some((rule) => isNonEmptyString(rule.matchHost))
|
|
212
|
+
};
|
|
213
|
+
if (!isUndefined(internalHostGrant.explicit) || !isUndefined(internalHostGrant.scoped) || internalHostGrant.implicit) res.internalHostGrant = internalHostGrant;
|
|
168
214
|
delete res.hostType;
|
|
169
215
|
delete res.resolvedHost;
|
|
170
216
|
delete res.matchHost;
|
|
171
217
|
delete res.readOnly;
|
|
218
|
+
delete res.trustTier;
|
|
172
219
|
delete res.trusted;
|
|
220
|
+
delete res.allowInternal;
|
|
173
221
|
return res;
|
|
174
222
|
}
|
|
175
223
|
function hosts({ hostType }) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"host-rules.js","names":[],"sources":["../../lib/util/host-rules.ts"],"sourcesContent":["import { isFalsy, isString, isTruthy, isUndefined } from '@sindresorhus/is';\nimport { GlobalConfig } from '../config/global.ts';\nimport { logger } from '../logger/index.ts';\nimport type { CombinedHostRule, HostRule } from '../types/index.ts';\nimport { clone } from './clone.ts';\nimport * as sanitize from './sanitize.ts';\nimport { toBase64 } from './string.ts';\nimport { matchRegexOrGlobList } from './string-match.ts';\nimport { isHttpUrl, massageHostUrl, parseUrl } from './url.ts';\n\n/**\n * A host rule as registered through {@link add}.\n *\n * `trusted` is deliberately not a field of `HostRule`: it is set from {@link AddHostRuleOptions} at registration time and must never be settable through configuration.\n */\ninterface RegisteredHostRule extends HostRule {\n /** whether the rule came from the self-hosted administrator's own global config, rather than from repository or preset config */\n trusted?: boolean;\n}\n\nlet hostRules: RegisteredHostRule[] = [];\n\n/**\n * Fields within `HostRule`s that must have their value registered for sanitising through `sanitize.addSecretForSanitizing()`.\n *\n * Kept in sync with `redactedFields` through tests.\n */\nexport const confidentialFields: (keyof HostRule)[] = [\n 'password',\n 'token',\n 'httpsPrivateKey',\n /* not actually sensitive, but redacted nonetheless */\n 'httpsCertificate',\n /* not actually sensitive, but redacted nonetheless */\n 'httpsCertificateAuthority',\n];\n\nexport interface LegacyHostRule {\n hostName?: string;\n domainName?: string;\n baseUrl?: string;\n host?: string;\n endpoint?: string;\n}\n\nexport function migrateRule(rule: LegacyHostRule & HostRule): HostRule {\n const cloned: LegacyHostRule & HostRule = clone(rule);\n delete cloned.hostName;\n delete cloned.domainName;\n delete cloned.baseUrl;\n const result: HostRule = cloned;\n\n const { matchHost } = result;\n const { hostName, domainName, baseUrl } = rule;\n const hostValues = [matchHost, hostName, domainName, baseUrl].filter(\n isTruthy,\n );\n if (hostValues.length === 1) {\n const [matchHost] = hostValues;\n result.matchHost = matchHost;\n } else if (hostValues.length > 1) {\n throw new Error(\n `hostRules cannot contain more than one host-matching field - use \"matchHost\" only.`,\n );\n }\n\n return result;\n}\n\n/**\n * Enforce the `allowedHeaders` allowlist on a set of host rules.\n *\n * Loudly remove anything that's not permitted, logging a WARN.\n *\n * `add()` applies this to every rule it registers, so callers only need it themselves to pre-filter - e.g. to avoid repeating the WARN when the same rules are registered again and again.\n *\n * @param [allowedHeaders] the effective allowlist. Defaults to `GlobalConfig`, but must be passed explicitly when filtering before `GlobalConfig` reflects the repository being processed, i.e. for a `repositories[]` entry's own `allowedHeaders` override\n * @param [warnOnDenied=true] whether to log the WARN. Pass `false` where the very same rules are filtered again, so that it is logged once rather than repeated\n *\n * Headers that survive this allowlist are still subject to how {@link find} combines them: an admin's headers for a host are applied over those of any repository or preset rule matching the same request, so a repository can neither drop nor substitute them.\n */\nexport function filterAllowedHeaders(\n rules: HostRule[],\n allowedHeaders?: string[],\n warnOnDenied = true,\n): HostRule[] {\n // `??`, rather than a parameter default: a default only applies to `undefined`, and a `null` can reach us from user config\n const allowlist = allowedHeaders ?? GlobalConfig.get('allowedHeaders');\n const denied: string[] = [];\n\n const result = rules.map((rule) => {\n if (!rule.headers) {\n return rule;\n }\n\n const allowed: Record<string, string> = {};\n const ruleDenied: string[] = [];\n for (const [name, value] of Object.entries(rule.headers)) {\n if (matchRegexOrGlobList(name, allowlist)) {\n allowed[name] = value;\n } else {\n ruleDenied.push(name);\n }\n }\n\n if (!ruleDenied.length) {\n return rule;\n }\n denied.push(...ruleDenied);\n\n const filtered: HostRule = { ...rule, headers: allowed };\n if (!Object.keys(allowed).length) {\n // drop the key rather than leave an empty object behind: `find()` treats any rule that sets `headers` as replacing the headers of the broader rules of its own tier, so a rule left with none would suppress them\n delete filtered.headers;\n }\n return filtered;\n });\n\n if (denied.length && warnOnDenied) {\n logger.warn(\n { denied },\n \"Ignoring hostRules headers not permitted by this Renovate instance's `allowedHeaders`\",\n );\n }\n return result;\n}\n\nexport interface AddHostRuleOptions {\n /** the effective allowlist. Defaults to `GlobalConfig`; pass it explicitly when `GlobalConfig` does not yet reflect the repository the rule is registered for */\n allowedHeaders?: string[];\n\n /**\n * Whether this rule comes from the self-hosted administrator's own global config, rather than from repository or preset config.\n *\n * Only affects how `headers` are combined (see {@link find}), and is deliberately opt-in: a caller that says nothing registers into the untrusted tier, so a new call site cannot grant itself the administrator's precedence by omission.\n */\n trusted?: boolean;\n}\n\nexport function add(params: HostRule, options?: AddHostRuleOptions): void {\n let rule: RegisteredHostRule = migrateRule(params);\n\n // set only from `options`, and dropped first so that it cannot be carried over from `params`: `HostRule` has no `trusted` field, but configuration is parsed from JSON, so a repository could otherwise smuggle one in and have its headers treated as the administrator's\n delete rule.trusted;\n if (options?.trusted) {\n rule.trusted = true;\n }\n\n if (rule.headers) {\n // enforced here, at the single registration chokepoint, so that no current or future caller can register a rule whose headers bypass `allowedHeaders`; `applyHostRule` filters by header name again at request time as defence in depth\n [rule] = filterAllowedHeaders([rule], options?.allowedHeaders);\n }\n\n if (rule.matchHost) {\n rule.matchHost = massageHostUrl(rule.matchHost);\n const parsedUrl = parseUrl(rule.matchHost);\n rule.resolvedHost = parsedUrl?.hostname ?? rule.matchHost;\n confidentialFields.forEach((field) => {\n if (rule[field]) {\n logger.debug(\n // TODO: types (#22198)\n `Adding ${field} authentication for ${rule.matchHost!} (hostType=${\n rule.hostType\n }) to hostRules`,\n );\n }\n });\n }\n confidentialFields.forEach((field) => {\n const secret = rule[field];\n if (isString(secret) && secret.length > 3) {\n sanitize.addSecretForSanitizing(secret);\n }\n });\n if (rule.username && rule.password) {\n sanitize.addSecretForSanitizing(\n toBase64(`${rule.username}:${rule.password}`),\n );\n }\n hostRules.push(rule);\n}\n\nexport interface HostRuleSearch {\n hostType?: string;\n url?: string;\n readOnly?: boolean;\n}\n\nexport function matchesHost(url: string, matchHost: string): boolean {\n const parsedUrl = parseUrl(url);\n if (!parsedUrl) {\n return false;\n }\n\n const parsedMatchHost = parseUrl(matchHost);\n if (isHttpUrl(parsedUrl) && isHttpUrl(parsedMatchHost)) {\n return parsedUrl.href.startsWith(parsedMatchHost!.href);\n }\n\n const { hostname } = parsedUrl;\n if (!hostname) {\n return false;\n }\n\n if (hostname === matchHost) {\n return true;\n }\n\n const topLevelSuffix = matchHost.startsWith('.')\n ? matchHost\n : `.${matchHost}`;\n return hostname.endsWith(topLevelSuffix);\n}\n\nfunction fromShorterToLongerMatchHost(a: HostRule, b: HostRule): number {\n if (!a.matchHost || !b.matchHost) {\n return 0;\n }\n return a.matchHost.length - b.matchHost.length;\n}\n\nfunction hostRuleRank({ hostType, matchHost, readOnly }: HostRule): number {\n if ((hostType || readOnly) && matchHost) {\n return 3;\n }\n\n if (matchHost) {\n return 2;\n }\n\n if (hostType) {\n return 1;\n }\n\n return 0;\n}\n\nfunction fromLowerToHigherRank(a: HostRule, b: HostRule): number {\n return hostRuleRank(a) - hostRuleRank(b);\n}\n\n// A single comparator, rather than two consecutive sorts: `fromShorterToLongerMatchHost` returns 0 for rules without a `matchHost`, which makes it non-transitive on its own, so a standalone length sort could leave two `matchHost` rules unordered when a host-less rule sits between them\nfunction fromLowerRankAndShorterMatchHost(a: HostRule, b: HostRule): number {\n return fromLowerToHigherRank(a, b) || fromShorterToLongerMatchHost(a, b);\n}\n\n/**\n * The `headers` that apply from a set of matching rules of the same trust tier.\n *\n * The last rule to set any wins outright, as `find()`'s callers receive them sorted from least to most specific. That is the behaviour every matching rule had before `headers` were combined across tiers, and it is what lets a broad rule's headers be masked by a narrower rule from the same source.\n */\nfunction headersOfLastRuleToSetThem(\n rules: RegisteredHostRule[],\n): Record<string, string> | undefined {\n return rules\n .map((rule) => rule.headers)\n .filter(isTruthy)\n .pop();\n}\n\nexport function find(search: HostRuleSearch): CombinedHostRule {\n if ([search.hostType, search.url].every(isFalsy)) {\n logger.warn({ search }, 'Invalid hostRules search');\n return {};\n }\n\n // Sort primarily by rank, and secondarily by matchHost length\n const sortedRules = hostRules.sort(fromLowerRankAndShorterMatchHost);\n\n const matchedRules: RegisteredHostRule[] = [];\n for (const rule of sortedRules) {\n let hostTypeMatch = true;\n let hostMatch = true;\n let readOnlyMatch = true;\n\n if (rule.hostType) {\n hostTypeMatch = false;\n // v8 ignore else -- TODO: add test #40625\n if (search.hostType === rule.hostType) {\n hostTypeMatch = true;\n }\n }\n\n if (rule.matchHost && rule.resolvedHost) {\n hostMatch = false;\n if (search.url) {\n hostMatch = matchesHost(search.url, rule.matchHost);\n }\n }\n\n if (!isUndefined(rule.readOnly)) {\n readOnlyMatch = false;\n // v8 ignore else -- TODO: add test #40625\n if (search.readOnly === rule.readOnly) {\n readOnlyMatch = true;\n hostTypeMatch = true; // When we match `readOnly`, we don't care about `hostType`\n }\n }\n\n if (hostTypeMatch && readOnlyMatch && hostMatch) {\n matchedRules.push(clone(rule));\n }\n }\n\n const res: RegisteredHostRule = Object.assign({}, ...matchedRules);\n\n // `headers` are resolved per trust tier and then combined key by key, so that repository or preset config can no longer discard - or substitute - the headers a self-hosted admin configured for the same host\n // Within a tier nothing changes: the most specific rule's `headers` still replace those of the broader rules it is combined with, so an admin masking their own broad rule with a narrower one keeps working, as does a repository doing the same among its own rules\n // This is deliberately scoped to `headers` alone, as combining any more of the fields than we already do has caused authentication regressions before: an inherited `token` and a specific rule's `username`/`password` are not meant to be used together\n const untrustedHeaders = headersOfLastRuleToSetThem(\n matchedRules.filter((rule) => !rule.trusted),\n );\n const trustedHeaders = headersOfLastRuleToSetThem(\n matchedRules.filter((rule) => rule.trusted),\n );\n if (untrustedHeaders ?? trustedHeaders) {\n // the admin's own headers are applied last, so a repository cannot override one they set for this host either\n res.headers = { ...untrustedHeaders, ...trustedHeaders };\n }\n\n delete res.hostType;\n delete res.resolvedHost;\n delete res.matchHost;\n delete res.readOnly;\n delete res.trusted;\n return res;\n}\n\nexport function hosts({ hostType }: { hostType: string }): string[] {\n return hostRules\n .filter((rule) => rule.hostType === hostType)\n .map((rule) => rule.resolvedHost)\n .filter(isTruthy);\n}\n\nexport function hostType({ url }: { url: string }): string | null {\n return (\n hostRules\n .filter((rule) => rule.matchHost && matchesHost(url, rule.matchHost))\n .sort(fromShorterToLongerMatchHost)\n .map((rule) => rule.hostType)\n .filter(isTruthy)\n .pop() ?? null\n );\n}\n\nexport function findAll({ hostType }: { hostType: string }): HostRule[] {\n return hostRules.filter((rule) => rule.hostType === hostType);\n}\n\n/**\n * @returns a deep copy of all known host rules without any filtering\n */\nexport function getAll(): HostRule[] {\n return clone(hostRules);\n}\n\nexport function clear(): void {\n logger.debug('Clearing hostRules');\n hostRules = [];\n sanitize.clearRepoSanitizedSecretsList();\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"host-rules.js","names":[],"sources":["../../lib/util/host-rules.ts"],"sourcesContent":["import {\n isFalsy,\n isNonEmptyString,\n isString,\n isTruthy,\n isUndefined,\n} from '@sindresorhus/is';\nimport { GlobalConfig } from '../config/global.ts';\nimport { logger } from '../logger/index.ts';\nimport type {\n CombinedHostRule,\n HostRule,\n InternalHostGrant,\n} from '../types/index.ts';\nimport { clone } from './clone.ts';\nimport * as sanitize from './sanitize.ts';\nimport { toBase64 } from './string.ts';\nimport { matchRegexOrGlobList } from './string-match.ts';\nimport { isHttpUrl, massageHostUrl, parseUrl } from './url.ts';\n\n/**\n * How much a host rule is trusted, according to the configuration it came from, from most to least trusted:\n *\n * - `admin`: the self-hosted administrator's own configuration - their global config, or a `repositories[]` entry\n * - `inherit`: organization-inherited config, and only where the administrator has opted into trusting it through `inheritConfigTrusted`\n * - `untrusted`: repository configuration, and the presets it extends\n */\ntype HostRuleTrustTier = 'admin' | 'inherit' | 'untrusted';\n\n/**\n * A host rule as registered through {@link add}.\n *\n * `trustTier` is deliberately not a field of `HostRule`: it is set from {@link AddHostRuleOptions} at registration time and must never be settable through configuration.\n */\ninterface RegisteredHostRule extends HostRule {\n /** which configuration the rule came from - see {@link HostRuleTrustTier}. Always set by `add()`, but optional so that `find()` can delete it from its result */\n trustTier?: HostRuleTrustTier;\n\n /** never set by us, and declared only so that a `trusted` smuggled in through configuration can be deleted before the rule is registered. It was the boolean `trustTier` replaced, so a rule carrying one may well be an attempt at the administrator's precedence */\n trusted?: never;\n}\n\nlet hostRules: RegisteredHostRule[] = [];\n\n/**\n * Fields within `HostRule`s that must have their value registered for sanitising through `sanitize.addSecretForSanitizing()`.\n *\n * Kept in sync with `redactedFields` through tests.\n */\nexport const confidentialFields: (keyof HostRule)[] = [\n 'password',\n 'token',\n 'httpsPrivateKey',\n /* not actually sensitive, but redacted nonetheless */\n 'httpsCertificate',\n /* not actually sensitive, but redacted nonetheless */\n 'httpsCertificateAuthority',\n];\n\nexport interface LegacyHostRule {\n hostName?: string;\n domainName?: string;\n baseUrl?: string;\n host?: string;\n endpoint?: string;\n}\n\nexport function migrateRule(rule: LegacyHostRule & HostRule): HostRule {\n const cloned: LegacyHostRule & HostRule = clone(rule);\n delete cloned.hostName;\n delete cloned.domainName;\n delete cloned.baseUrl;\n const result: HostRule = cloned;\n\n const { matchHost } = result;\n const { hostName, domainName, baseUrl } = rule;\n const hostValues = [matchHost, hostName, domainName, baseUrl].filter(\n isTruthy,\n );\n if (hostValues.length === 1) {\n const [matchHost] = hostValues;\n result.matchHost = matchHost;\n } else if (hostValues.length > 1) {\n throw new Error(\n `hostRules cannot contain more than one host-matching field - use \"matchHost\" only.`,\n );\n }\n\n return result;\n}\n\n/**\n * Enforce the `allowedHeaders` allowlist on a set of host rules.\n *\n * Loudly remove anything that's not permitted, logging a WARN.\n *\n * `add()` applies this to every rule it registers, so callers only need it themselves to pre-filter - e.g. to avoid repeating the WARN when the same rules are registered again and again.\n *\n * @param [allowedHeaders] the effective allowlist. Defaults to `GlobalConfig`, but must be passed explicitly when filtering before `GlobalConfig` reflects the repository being processed, i.e. for a `repositories[]` entry's own `allowedHeaders` override\n * @param [warnOnDenied=true] whether to log the WARN. Pass `false` where the very same rules are filtered again, so that it is logged once rather than repeated\n *\n * Headers that survive this allowlist are still subject to how {@link find} combines them: an admin's headers for a host are applied over those of any repository or preset rule matching the same request, so a repository can neither drop nor substitute them.\n */\nexport function filterAllowedHeaders(\n rules: HostRule[],\n allowedHeaders?: string[],\n warnOnDenied = true,\n): HostRule[] {\n // `??`, rather than a parameter default: a default only applies to `undefined`, and a `null` can reach us from user config\n const allowlist = allowedHeaders ?? GlobalConfig.get('allowedHeaders');\n const denied: string[] = [];\n\n const result = rules.map((rule) => {\n if (!rule.headers) {\n return rule;\n }\n\n const allowed: Record<string, string> = {};\n const ruleDenied: string[] = [];\n for (const [name, value] of Object.entries(rule.headers)) {\n if (matchRegexOrGlobList(name, allowlist)) {\n allowed[name] = value;\n } else {\n ruleDenied.push(name);\n }\n }\n\n if (!ruleDenied.length) {\n return rule;\n }\n denied.push(...ruleDenied);\n\n const filtered: HostRule = { ...rule, headers: allowed };\n if (!Object.keys(allowed).length) {\n // drop the key rather than leave an empty object behind: `find()` treats any rule that sets `headers` as replacing the headers of the broader rules of its own tier, so a rule left with none would suppress them\n delete filtered.headers;\n }\n return filtered;\n });\n\n if (denied.length && warnOnDenied) {\n logger.warn(\n { denied },\n \"Ignoring hostRules headers not permitted by this Renovate instance's `allowedHeaders`\",\n );\n }\n return result;\n}\n\nexport interface AddHostRuleOptions {\n /** the effective allowlist. Defaults to `GlobalConfig`; pass it explicitly when `GlobalConfig` does not yet reflect the repository the rule is registered for */\n allowedHeaders?: string[];\n\n /**\n * Whether this rule comes from the self-hosted administrator's own configuration, rather than from repository or preset config.\n *\n * Registers the rule into the `admin` tier - see {@link HostRuleTrustTier}.\n */\n trusted?: boolean;\n\n /**\n * Whether this rule comes from organization-inherited config which the administrator has opted into trusting, through `inheritConfigTrusted`.\n *\n * Registers the rule into the `inherit` tier - see {@link HostRuleTrustTier}. Ignored when `trusted` is also set.\n */\n inherited?: boolean;\n}\n\n/**\n * The trust tier a caller asked for.\n *\n * Deliberately opt-in: a caller that says nothing registers into the untrusted tier, so a new call site cannot grant itself the administrator's precedence by omission.\n */\nfunction trustTierFor(options?: AddHostRuleOptions): HostRuleTrustTier {\n if (options?.trusted) {\n return 'admin';\n }\n if (options?.inherited) {\n return 'inherit';\n }\n return 'untrusted';\n}\n\nexport function add(params: HostRule, options?: AddHostRuleOptions): void {\n let rule: RegisteredHostRule = {\n ...migrateRule(params),\n // set only from `options`, and assigned unconditionally so that it cannot be carried over from `params`: `HostRule` has no `trustTier` field, but configuration is parsed from JSON, so a repository could otherwise smuggle one in and have its rules treated as the administrator's\n trustTier: trustTierFor(options),\n };\n // as above, for the boolean this tier replaced\n delete rule.trusted;\n\n // like the trust tier, `allowInternal` may only come from configuration the administrator trusts: a repository or preset rule must not be able to grant itself access to internal hosts\n if (!isUndefined(rule.allowInternal) && rule.trustTier === 'untrusted') {\n logger.debug(\n `Ignoring hostRules allowInternal for ${rule.matchHost ?? rule.hostType} from untrusted config`,\n );\n delete rule.allowInternal;\n }\n\n if (rule.headers) {\n // enforced here, at the single registration chokepoint, so that no current or future caller can register a rule whose headers bypass `allowedHeaders`; `applyHostRule` filters by header name again at request time as defence in depth\n [rule] = filterAllowedHeaders([rule], options?.allowedHeaders);\n }\n\n if (rule.matchHost) {\n rule.matchHost = massageHostUrl(rule.matchHost);\n const parsedUrl = parseUrl(rule.matchHost);\n rule.resolvedHost = parsedUrl?.hostname ?? rule.matchHost;\n confidentialFields.forEach((field) => {\n if (rule[field]) {\n logger.debug(\n // TODO: types (#22198)\n `Adding ${field} authentication for ${rule.matchHost!} (hostType=${\n rule.hostType\n }) to hostRules`,\n );\n }\n });\n }\n confidentialFields.forEach((field) => {\n const secret = rule[field];\n if (isString(secret) && secret.length > 3) {\n sanitize.addSecretForSanitizing(secret);\n }\n });\n if (rule.username && rule.password) {\n sanitize.addSecretForSanitizing(\n toBase64(`${rule.username}:${rule.password}`),\n );\n }\n hostRules.push(rule);\n}\n\nexport interface HostRuleSearch {\n hostType?: string;\n url?: string;\n readOnly?: boolean;\n}\n\nexport function matchesHost(url: string, matchHost: string): boolean {\n const parsedUrl = parseUrl(url);\n if (!parsedUrl) {\n return false;\n }\n\n const parsedMatchHost = parseUrl(matchHost);\n if (isHttpUrl(parsedUrl) && isHttpUrl(parsedMatchHost)) {\n return parsedUrl.href.startsWith(parsedMatchHost!.href);\n }\n\n const { hostname } = parsedUrl;\n if (!hostname) {\n return false;\n }\n\n if (hostname === matchHost) {\n return true;\n }\n\n const topLevelSuffix = matchHost.startsWith('.')\n ? matchHost\n : `.${matchHost}`;\n return hostname.endsWith(topLevelSuffix);\n}\n\nfunction fromShorterToLongerMatchHost(a: HostRule, b: HostRule): number {\n if (!a.matchHost || !b.matchHost) {\n return 0;\n }\n return a.matchHost.length - b.matchHost.length;\n}\n\nfunction hostRuleRank({ hostType, matchHost, readOnly }: HostRule): number {\n if ((hostType || readOnly) && matchHost) {\n return 3;\n }\n\n if (matchHost) {\n return 2;\n }\n\n if (hostType) {\n return 1;\n }\n\n return 0;\n}\n\nfunction fromLowerToHigherRank(a: HostRule, b: HostRule): number {\n return hostRuleRank(a) - hostRuleRank(b);\n}\n\n// A single comparator, rather than two consecutive sorts: `fromShorterToLongerMatchHost` returns 0 for rules without a `matchHost`, which makes it non-transitive on its own, so a standalone length sort could leave two `matchHost` rules unordered when a host-less rule sits between them\nfunction fromLowerRankAndShorterMatchHost(a: HostRule, b: HostRule): number {\n return fromLowerToHigherRank(a, b) || fromShorterToLongerMatchHost(a, b);\n}\n\n/**\n * The `headers` that apply from a set of matching rules of the same trust tier.\n *\n * The last rule to set any wins outright, as `find()`'s callers receive them sorted from least to most specific. That is the behaviour every matching rule had before `headers` were combined across tiers, and it is what lets a broad rule's headers be masked by a narrower rule from the same source.\n */\nfunction headersOfLastRuleToSetThem(\n rules: RegisteredHostRule[],\n): Record<string, string> | undefined {\n return rules\n .map((rule) => rule.headers)\n .filter(isTruthy)\n .pop();\n}\n\n/**\n * The last defined value, following the same \"most specific rule wins\" ordering as {@link headersOfLastRuleToSetThem}.\n */\nfunction lastDefined<T>(values: (T | undefined)[]): T | undefined {\n return values.filter((value) => !isUndefined(value)).pop();\n}\n\n/**\n * The rules whose grant is deliberately scoped, by a `hostType` or by a URL-prefix `matchHost` - see {@link InternalHostGrant}.\n */\nfunction scopedRules(rules: RegisteredHostRule[]): RegisteredHostRule[] {\n return rules.filter(\n (rule) => isNonEmptyString(rule.hostType) || isHttpUrl(rule.matchHost),\n );\n}\n\nexport function find(search: HostRuleSearch): CombinedHostRule {\n if ([search.hostType, search.url].every(isFalsy)) {\n logger.warn({ search }, 'Invalid hostRules search');\n return {};\n }\n\n // Sort primarily by rank, and secondarily by matchHost length\n const sortedRules = hostRules.sort(fromLowerRankAndShorterMatchHost);\n\n const matchedRules: RegisteredHostRule[] = [];\n for (const rule of sortedRules) {\n let hostTypeMatch = true;\n let hostMatch = true;\n let readOnlyMatch = true;\n\n if (rule.hostType) {\n hostTypeMatch = false;\n // v8 ignore else -- TODO: add test #40625\n if (search.hostType === rule.hostType) {\n hostTypeMatch = true;\n }\n }\n\n if (rule.matchHost && rule.resolvedHost) {\n hostMatch = false;\n if (search.url) {\n hostMatch = matchesHost(search.url, rule.matchHost);\n }\n }\n\n if (!isUndefined(rule.readOnly)) {\n readOnlyMatch = false;\n // v8 ignore else -- TODO: add test #40625\n if (search.readOnly === rule.readOnly) {\n readOnlyMatch = true;\n hostTypeMatch = true; // When we match `readOnly`, we don't care about `hostType`\n }\n }\n\n if (hostTypeMatch && readOnlyMatch && hostMatch) {\n matchedRules.push(clone(rule));\n }\n }\n\n const res: RegisteredHostRule & Pick<CombinedHostRule, 'internalHostGrant'> =\n Object.assign({}, ...matchedRules);\n\n // the sensitive fields below are resolved a trust tier at a time, so that the outcome cannot depend on how specific a rule from a less trusted tier makes itself\n const trustedRules = matchedRules.filter(\n (rule) => rule.trustTier === 'admin',\n );\n const inheritedRules = matchedRules.filter(\n (rule) => rule.trustTier === 'inherit',\n );\n const untrustedRules = matchedRules.filter(\n (rule) => rule.trustTier === 'untrusted',\n );\n\n // `headers` are resolved per trust tier and then combined key by key, so that repository or preset config can no longer discard - or substitute - the headers a self-hosted admin configured for the same host\n // Within a tier nothing changes: the most specific rule's `headers` still replace those of the broader rules it is combined with, so an admin masking their own broad rule with a narrower one keeps working, as does a repository doing the same among its own rules\n // This is deliberately scoped to `headers` alone, as combining any more of the fields than we already do has caused authentication regressions before: an inherited `token` and a specific rule's `username`/`password` are not meant to be used together\n const untrustedHeaders = headersOfLastRuleToSetThem(untrustedRules);\n const inheritedHeaders = headersOfLastRuleToSetThem(inheritedRules);\n const trustedHeaders = headersOfLastRuleToSetThem(trustedRules);\n if (untrustedHeaders ?? inheritedHeaders ?? trustedHeaders) {\n // the admin's own headers are applied last, so neither a repository nor inherited config can override one they set for this host\n res.headers = {\n ...untrustedHeaders,\n ...inheritedHeaders,\n ...trustedHeaders,\n };\n }\n\n // `enabled` is resolved per trust tier like `headers`: a repository or preset rule must not be able to re-enable a host the administrator's own rules disabled (or vice versa) by out-specifying them with a longer `matchHost`\n const enabled =\n lastDefined(trustedRules.map((rule) => rule.enabled)) ??\n lastDefined(inheritedRules.map((rule) => rule.enabled)) ??\n lastDefined(untrustedRules.map((rule) => rule.enabled));\n if (!isUndefined(enabled)) {\n res.enabled = enabled;\n }\n\n // computed here and never from configuration: `add()` strips `allowInternal` from untrusted registrations, so only rules from configuration the administrator trusts can contribute\n // the admin's own rules are resolved first, and an `allowInternal: false` of theirs is an absolute veto: inherited config cannot talk an organization's way back in through any grant shape, whether scoped, unscoped, or merely implicit in naming the host\n // each shape must therefore be resolved from the admin tier's verdict rather than falling through to the inherit tier on its own - a bare admin rule sets no scoped verdict, and an inherited scoped grant would otherwise fill the gap for exactly the requests which consult it, such as fetching config over HTTP\n const trustedExplicit = lastDefined(\n trustedRules.map((rule) => rule.allowInternal),\n );\n const trustedScoped = lastDefined(\n scopedRules(trustedRules).map((rule) => rule.allowInternal),\n );\n // derived from the resolved values, so an admin masking a broad `allowInternal: false` of their own with a narrower `true` is not a veto\n const adminVeto = trustedExplicit === false || trustedScoped === false;\n // a vetoed request has no inherit tier left to fall through to, and is clamped to `false` rather than to undefined so that a consumer's `explicit ?? implicit` still short-circuits\n const inheritedExplicit = adminVeto\n ? false\n : lastDefined(inheritedRules.map((rule) => rule.allowInternal));\n const inheritedScoped = adminVeto\n ? false\n : lastDefined(\n scopedRules(inheritedRules).map((rule) => rule.allowInternal),\n );\n const internalHostGrant: InternalHostGrant = {\n explicit: trustedExplicit ?? inheritedExplicit,\n scoped: trustedScoped ?? inheritedScoped,\n implicit:\n !adminVeto &&\n [...trustedRules, ...inheritedRules].some((rule) =>\n isNonEmptyString(rule.matchHost),\n ),\n };\n if (\n !isUndefined(internalHostGrant.explicit) ||\n !isUndefined(internalHostGrant.scoped) ||\n internalHostGrant.implicit\n ) {\n res.internalHostGrant = internalHostGrant;\n }\n\n delete res.hostType;\n delete res.resolvedHost;\n delete res.matchHost;\n delete res.readOnly;\n delete res.trustTier;\n delete res.trusted;\n delete res.allowInternal;\n return res;\n}\n\nexport function hosts({ hostType }: { hostType: string }): string[] {\n return hostRules\n .filter((rule) => rule.hostType === hostType)\n .map((rule) => rule.resolvedHost)\n .filter(isTruthy);\n}\n\nexport function hostType({ url }: { url: string }): string | null {\n return (\n hostRules\n .filter((rule) => rule.matchHost && matchesHost(url, rule.matchHost))\n .sort(fromShorterToLongerMatchHost)\n .map((rule) => rule.hostType)\n .filter(isTruthy)\n .pop() ?? null\n );\n}\n\nexport function findAll({ hostType }: { hostType: string }): HostRule[] {\n return hostRules.filter((rule) => rule.hostType === hostType);\n}\n\n/**\n * @returns a deep copy of all known host rules without any filtering\n */\nexport function getAll(): HostRule[] {\n return clone(hostRules);\n}\n\nexport function clear(): void {\n logger.debug('Clearing hostRules');\n hostRules = [];\n sanitize.clearRepoSanitizedSecretsList();\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import { HOST_BLOCKED } from "../../constants/error-messages.js";
|
|
2
|
+
import { coerceObject } from "../object.js";
|
|
3
|
+
import { regEx } from "../regex.js";
|
|
4
|
+
import { GlobalConfig } from "../../config/global.js";
|
|
5
|
+
import { logger } from "../../logger/index.js";
|
|
6
|
+
import { parseUrl } from "../url.js";
|
|
7
|
+
import { find } from "../host-rules.js";
|
|
8
|
+
import { hasProxy } from "../../proxy.js";
|
|
9
|
+
import { isString } from "@sindresorhus/is";
|
|
10
|
+
import dns from "node:dns";
|
|
11
|
+
import dnsPromises from "node:dns/promises";
|
|
12
|
+
import net from "node:net";
|
|
13
|
+
//#region lib/util/http/host-guard.ts
|
|
14
|
+
/**
|
|
15
|
+
* Instance-metadata hostnames, blocked regardless of what they resolve to.
|
|
16
|
+
*
|
|
17
|
+
* Only names which serve nothing but metadata belong here: a name in this set can never be permitted, whatever the configuration says. Providers whose metadata service is only reachable by IP (AWS, Azure, ...) are covered by {@link metadataBlockList} instead.
|
|
18
|
+
*/
|
|
19
|
+
const metadataHostnames = /* @__PURE__ */ new Set(["metadata.google.internal", "metadata.packet.net"]);
|
|
20
|
+
const metadataBlockList = new net.BlockList();
|
|
21
|
+
metadataBlockList.addAddress("169.254.169.254", "ipv4");
|
|
22
|
+
metadataBlockList.addAddress("fd00:ec2::254", "ipv6");
|
|
23
|
+
metadataBlockList.addAddress("169.254.170.2", "ipv4");
|
|
24
|
+
metadataBlockList.addAddress("169.254.170.23", "ipv4");
|
|
25
|
+
metadataBlockList.addAddress("100.100.100.200", "ipv4");
|
|
26
|
+
const internalBlockList = new net.BlockList();
|
|
27
|
+
internalBlockList.addSubnet("0.0.0.0", 8, "ipv4");
|
|
28
|
+
internalBlockList.addSubnet("10.0.0.0", 8, "ipv4");
|
|
29
|
+
internalBlockList.addSubnet("172.16.0.0", 12, "ipv4");
|
|
30
|
+
internalBlockList.addSubnet("192.168.0.0", 16, "ipv4");
|
|
31
|
+
internalBlockList.addSubnet("100.64.0.0", 10, "ipv4");
|
|
32
|
+
internalBlockList.addSubnet("127.0.0.0", 8, "ipv4");
|
|
33
|
+
internalBlockList.addSubnet("169.254.0.0", 16, "ipv4");
|
|
34
|
+
internalBlockList.addSubnet("224.0.0.0", 4, "ipv4");
|
|
35
|
+
internalBlockList.addAddress("::", "ipv6");
|
|
36
|
+
internalBlockList.addAddress("::1", "ipv6");
|
|
37
|
+
internalBlockList.addSubnet("fc00::", 7, "ipv6");
|
|
38
|
+
internalBlockList.addSubnet("fe80::", 10, "ipv6");
|
|
39
|
+
/**
|
|
40
|
+
* Classifies an IP address, returning the category it is blocked under, or `null` for a public address.
|
|
41
|
+
*
|
|
42
|
+
* A string that is not a valid IP address is classified as `internal` - we should fail closed instead of possibly connecting to something we shouldn't be.
|
|
43
|
+
*/
|
|
44
|
+
function classifyIpAddress(address) {
|
|
45
|
+
const version = net.isIP(address);
|
|
46
|
+
if (version === 0) return "internal";
|
|
47
|
+
const family = version === 4 ? "ipv4" : "ipv6";
|
|
48
|
+
if (metadataBlockList.check(address, family)) return "metadata";
|
|
49
|
+
if (internalBlockList.check(address, family)) return "internal";
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Classifies a hostname against the metadata-endpoint hostname list.
|
|
54
|
+
*
|
|
55
|
+
* Only metadata hostnames can be classified without DNS resolution - any other hostname needs its resolved addresses checked via {@link classifyIpAddress}.
|
|
56
|
+
*/
|
|
57
|
+
function classifyHostname(hostname) {
|
|
58
|
+
const normalized = hostname.toLowerCase().replace(regEx(/\.$/), "");
|
|
59
|
+
if (metadataHostnames.has(normalized)) return "metadata";
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
/** Strips the brackets `URL` keeps around IPv6 hosts, leaving other hosts as they are. */
|
|
63
|
+
function unbracketHostname(hostname) {
|
|
64
|
+
if (hostname.startsWith("[") && hostname.endsWith("]")) return hostname.slice(1, -1);
|
|
65
|
+
return hostname;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Classifies a URL from its host alone: IP-literal hosts are classified by address, and metadata hostnames by name.
|
|
69
|
+
*
|
|
70
|
+
* Returns `null` when nothing can be decided at the URL level - i.e. the host is a regular hostname, whose resolved addresses must be checked via {@link classifyIpAddress} at DNS lookup time.
|
|
71
|
+
*/
|
|
72
|
+
function classifyUrl(url) {
|
|
73
|
+
const hostname = unbracketHostname(url.hostname);
|
|
74
|
+
if (net.isIP(hostname) !== 0) return classifyIpAddress(hostname);
|
|
75
|
+
return classifyHostname(hostname);
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Returns the resolved address which decides the outcome for a hostname, or `null` when every one of them is a public address.
|
|
79
|
+
*
|
|
80
|
+
* Each resolved record is a potential connection target, so each must pass. A metadata address decides the outcome over an internal one, as it is blocked in every mode.
|
|
81
|
+
*/
|
|
82
|
+
function findBlockedAddress(addresses) {
|
|
83
|
+
let internal = null;
|
|
84
|
+
for (const address of addresses) {
|
|
85
|
+
const category = classifyIpAddress(address);
|
|
86
|
+
if (category === "metadata") return {
|
|
87
|
+
address,
|
|
88
|
+
category
|
|
89
|
+
};
|
|
90
|
+
if (category === "internal") internal ??= {
|
|
91
|
+
address,
|
|
92
|
+
category
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
return internal;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Applies `decision` to the addresses a hostname resolved to, logging the outcome, and returning `true` when the request must not be made.
|
|
99
|
+
*/
|
|
100
|
+
function isResolutionBlocked(hostname, addresses, decision, hostType, responseBecomesConfig) {
|
|
101
|
+
const blocked = findBlockedAddress(addresses);
|
|
102
|
+
if (!blocked) return false;
|
|
103
|
+
if (blocked.category === "internal") {
|
|
104
|
+
if (decision === "granted") return false;
|
|
105
|
+
if (decision === "warn-dns") {
|
|
106
|
+
warnInternalHost(hostname, hostType, responseBecomesConfig);
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
logger.warn({
|
|
111
|
+
hostname,
|
|
112
|
+
address: blocked.address
|
|
113
|
+
}, "Blocked HTTP request: hostname resolves to a blocked address");
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Validates every address a hostname resolves to, on every connection, which also catches a host whose answer changes between checks (DNS rebinding).
|
|
118
|
+
*
|
|
119
|
+
* The guard is built per request, as the policy it enforces is read from the configuration when the request is made.
|
|
120
|
+
*/
|
|
121
|
+
function makeGuardedLookup(decision, hostType, responseBecomesConfig) {
|
|
122
|
+
return function guardedLookup(hostname, options, callback) {
|
|
123
|
+
dns.lookup(hostname, options, (err, address, family) => {
|
|
124
|
+
if (err) {
|
|
125
|
+
callback(err, address, family);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
if (isResolutionBlocked(hostname, isString(address) ? [address] : address.map((entry) => entry.address), decision, hostType, responseBecomesConfig)) {
|
|
129
|
+
callback(new Error(HOST_BLOCKED), address, family);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
callback(err, address, family);
|
|
133
|
+
});
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
function isPlatformEndpoint(url) {
|
|
137
|
+
const endpoint = GlobalConfig.get("endpoint");
|
|
138
|
+
if (!endpoint) return false;
|
|
139
|
+
return parseUrl(endpoint)?.origin === url.origin;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Whether the administrator has permitted internal access for this request, leaving aside the `internalHostAccess` mode: a permitted request neither blocks nor warns, whatever the mode is.
|
|
143
|
+
*/
|
|
144
|
+
function isInternalGranted(url, grant, responseBecomesConfig) {
|
|
145
|
+
if (isPlatformEndpoint(url)) return true;
|
|
146
|
+
if (responseBecomesConfig) return grant?.scoped === true;
|
|
147
|
+
return grant?.explicit ?? grant?.implicit ?? false;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Warns about a request which `internalHostAccess=block` would refuse, once per host, naming the grant which would permit it.
|
|
151
|
+
*/
|
|
152
|
+
function warnInternalHost(hostname, hostType, responseBecomesConfig) {
|
|
153
|
+
if (responseBecomesConfig) {
|
|
154
|
+
logger.once.warn({
|
|
155
|
+
hostname,
|
|
156
|
+
hostType
|
|
157
|
+
}, "HTTP request to an internal host whose response becomes configuration, which `internalHostAccess=block` would refuse - permit it with a `hostRules` entry setting `allowInternal: true`, scoped with a `hostType` or a URL-prefix `matchHost`, or set `internalHostAccess=block` to enforce this now. The default will become `block` in a future major release.");
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
logger.once.warn({
|
|
161
|
+
hostname,
|
|
162
|
+
hostType
|
|
163
|
+
}, "HTTP request to an internal host, which `internalHostAccess=block` would refuse - permit it with a `hostRules` entry naming the host, or one setting `allowInternal: true`, or set `internalHostAccess=block` to enforce this now. The default will become `block` in a future major release.");
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Enforce the internal-host policy on a request URL, throwing `HOST_BLOCKED` when the request must not be made.
|
|
167
|
+
*
|
|
168
|
+
* Metadata endpoints are blocked no matter what the configuration says: Renovate's HTTP layer never has a legitimate reason to request them.
|
|
169
|
+
*/
|
|
170
|
+
function checkUrl(url, hostType, grant, responseBecomesConfig) {
|
|
171
|
+
const category = classifyUrl(url);
|
|
172
|
+
if (category === "metadata") {
|
|
173
|
+
logger.warn({
|
|
174
|
+
url: url.href,
|
|
175
|
+
hostType
|
|
176
|
+
}, "Blocked HTTP request to a cloud instance-metadata endpoint");
|
|
177
|
+
throw new Error(HOST_BLOCKED);
|
|
178
|
+
}
|
|
179
|
+
const mode = GlobalConfig.get("internalHostAccess");
|
|
180
|
+
if (mode === "allow" || isInternalGranted(url, grant, responseBecomesConfig)) {
|
|
181
|
+
if (category === "internal") logger.once.info(`Internal host ${url.hostname} permitted by configuration`);
|
|
182
|
+
return "granted";
|
|
183
|
+
}
|
|
184
|
+
if (mode === "warn") {
|
|
185
|
+
if (category === "internal") warnInternalHost(url.hostname, hostType, responseBecomesConfig);
|
|
186
|
+
return "warn-dns";
|
|
187
|
+
}
|
|
188
|
+
if (category === "internal") {
|
|
189
|
+
logger.warn({
|
|
190
|
+
url: url.href,
|
|
191
|
+
hostType
|
|
192
|
+
}, "Blocked HTTP request to an internal host - a self-hosted administrator can permit it via `hostRules`, or with `internalHostAccess=allow`");
|
|
193
|
+
throw new Error(HOST_BLOCKED);
|
|
194
|
+
}
|
|
195
|
+
return "check-dns";
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Validates what a request's hostname resolves to, before the request is made, for deployments where `dnsLookup` cannot do it.
|
|
199
|
+
*
|
|
200
|
+
* A proxy agent connects to the proxy and hands it the target hostname, so the target is resolved by the proxy and Renovate's `dnsLookup` guard never runs. Resolving here instead is best-effort only: the proxy re-resolves the hostname itself, so a DNS-rebinding or split-horizon answer can still differ from what we saw. Pair it with egress controls on the proxy.
|
|
201
|
+
*/
|
|
202
|
+
async function preflightHostname(url, decision, hostType, responseBecomesConfig) {
|
|
203
|
+
const hostname = unbracketHostname(url.hostname);
|
|
204
|
+
if (net.isIP(hostname) !== 0) return;
|
|
205
|
+
let addresses;
|
|
206
|
+
try {
|
|
207
|
+
addresses = await dnsPromises.lookup(hostname, { all: true });
|
|
208
|
+
} catch {
|
|
209
|
+
logger.once.debug(`Host guard could not resolve ${hostname} - relying on the proxy for this request`);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
if (isResolutionBlocked(hostname, addresses.map((entry) => entry.address), decision, hostType, responseBecomesConfig)) throw new Error(HOST_BLOCKED);
|
|
213
|
+
}
|
|
214
|
+
function makeBeforeRequest(hostType, responseBecomesConfig) {
|
|
215
|
+
return async function hostGuardBeforeRequest(options) {
|
|
216
|
+
const url = options.url instanceof URL ? options.url : null;
|
|
217
|
+
if (!url) throw new Error(HOST_BLOCKED);
|
|
218
|
+
const grant = find({
|
|
219
|
+
hostType,
|
|
220
|
+
url: url.toString()
|
|
221
|
+
}).internalHostGrant;
|
|
222
|
+
await preflightHostname(url, checkUrl(url, hostType, grant, responseBecomesConfig), hostType, responseBecomesConfig);
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Removes the credentials belonging to the host a request is being redirected away from, so that a redirect cannot hand them to another origin.
|
|
227
|
+
*
|
|
228
|
+
* got strips the credentials it knows about itself - `authorization`, `cookie` and any URL userinfo - before this hook runs, and treats a scheme downgrade as a different origin, so an `https:` to `http:` redirect is covered. What it does not know about survives: the `Private-token` header GitLab personal access tokens are sent in (see `applyAuthorization`), and any credential an administrator configured through a `hostRules` `headers` entry.
|
|
229
|
+
*
|
|
230
|
+
* Those headers are dropped rather than replaced with the target host's own: `hostRules` headers are resolved once, when the request is built, so there is nothing to re-apply here. Sending none is the safe outcome.
|
|
231
|
+
*/
|
|
232
|
+
function stripCrossOriginCredentials(options, fromUrl, toUrl, hostType) {
|
|
233
|
+
if (fromUrl?.origin === toUrl.origin) return;
|
|
234
|
+
if (fromUrl) {
|
|
235
|
+
const configured = find({
|
|
236
|
+
hostType,
|
|
237
|
+
url: fromUrl.toString()
|
|
238
|
+
}).headers;
|
|
239
|
+
for (const name of Object.keys(coerceObject(configured))) delete options.headers[name.toLowerCase()];
|
|
240
|
+
}
|
|
241
|
+
delete options.headers.authorization;
|
|
242
|
+
delete options.headers.cookie;
|
|
243
|
+
delete options.headers["private-token"];
|
|
244
|
+
}
|
|
245
|
+
function makeBeforeRedirect(hostType, responseBecomesConfig) {
|
|
246
|
+
return function hostGuardBeforeRedirect(options, response) {
|
|
247
|
+
const url = options.url instanceof URL ? options.url : null;
|
|
248
|
+
if (!url) throw new Error(HOST_BLOCKED);
|
|
249
|
+
stripCrossOriginCredentials(options, isString(response.url) ? parseUrl(response.url) : null, url, hostType);
|
|
250
|
+
const grant = find({
|
|
251
|
+
hostType,
|
|
252
|
+
url: url.toString()
|
|
253
|
+
}).internalHostGrant;
|
|
254
|
+
options.dnsLookup = makeGuardedLookup(checkUrl(url, hostType, grant, responseBecomesConfig), hostType, responseBecomesConfig);
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Enforce the internal-host policy for a request to `url`, throwing `HOST_BLOCKED` when the request must not be made at all.
|
|
259
|
+
*
|
|
260
|
+
* The returned got options carry the policy through the rest of the request's lifetime: `dnsLookup` validates what hostnames resolve to (on every connection, which also covers DNS rebinding), and `beforeRedirect` re-runs this check for every redirect target.
|
|
261
|
+
*
|
|
262
|
+
* When a proxy is configured, the proxy agent resolves the target hostname itself and `dnsLookup` is never called, so a `beforeRequest` hook resolves and validates the hostname up front instead - see {@link preflightHostname} for what that can and cannot catch.
|
|
263
|
+
*
|
|
264
|
+
* `responseBecomesConfig` marks a request whose response is interpreted as Renovate configuration, which an internal host may only serve under a deliberately-scoped `allowInternal` grant.
|
|
265
|
+
*
|
|
266
|
+
* Under the `internalHostAccess=warn` default, an internal host which no grant permits is logged rather than blocked, so that administrators can add the grants they need before the default becomes `block`.
|
|
267
|
+
*/
|
|
268
|
+
function applyHostGuard(url, hostType, grant, responseBecomesConfig) {
|
|
269
|
+
const guard = {
|
|
270
|
+
dnsLookup: makeGuardedLookup(checkUrl(url, hostType, grant, responseBecomesConfig), hostType, responseBecomesConfig),
|
|
271
|
+
beforeRedirect: makeBeforeRedirect(hostType, responseBecomesConfig)
|
|
272
|
+
};
|
|
273
|
+
if (hasProxy()) guard.beforeRequest = makeBeforeRequest(hostType, responseBecomesConfig);
|
|
274
|
+
return guard;
|
|
275
|
+
}
|
|
276
|
+
//#endregion
|
|
277
|
+
export { applyHostGuard, checkUrl, classifyHostname, classifyIpAddress, classifyUrl };
|
|
278
|
+
|
|
279
|
+
//# sourceMappingURL=host-guard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"host-guard.js","names":["hostRules.find"],"sources":["../../../lib/util/http/host-guard.ts"],"sourcesContent":["import dns from 'node:dns';\nimport dnsPromises from 'node:dns/promises';\nimport net from 'node:net';\nimport { isString } from '@sindresorhus/is';\nimport type {\n BeforeRedirectHook,\n BeforeRequestHook,\n NormalizedOptions,\n PlainResponse,\n} from 'got';\nimport { GlobalConfig } from '../../config/global.ts';\nimport { HOST_BLOCKED } from '../../constants/error-messages.ts';\nimport { logger } from '../../logger/index.ts';\nimport { hasProxy } from '../../proxy.ts';\nimport type { InternalHostGrant } from '../../types/index.ts';\nimport * as hostRules from '../host-rules.ts';\nimport { coerceObject } from '../object.ts';\nimport { regEx } from '../regex.ts';\nimport { parseUrl } from '../url.ts';\n\n/** Why a host must not be requested. */\nexport type BlockedHostCategory =\n /** loopback, RFC1918, link-local, and other special-use addresses that are never routable on the public Internet */\n | 'internal'\n /** Cloud provider instance-metadata services (IMDS and equivalents), which can potentially hand out credentials to anything that can reach them, and should never be accessible from an HTTP request from Renovate, regardless of any self-hosted administrator allowlisting */\n | 'metadata';\n\n/**\n * Instance-metadata hostnames, blocked regardless of what they resolve to.\n *\n * Only names which serve nothing but metadata belong here: a name in this set can never be permitted, whatever the configuration says. Providers whose metadata service is only reachable by IP (AWS, Azure, ...) are covered by {@link metadataBlockList} instead.\n */\nconst metadataHostnames = new Set([\n 'metadata.google.internal',\n // Equinix Metal (formerly Packet)\n 'metadata.packet.net',\n]);\n\n// `BlockList.check()` classifies IPv4-mapped IPv6 addresses (in dotted-quad, hexadecimal, and uncompressed spellings) against IPv4 rules, so e.g. `::ffff:169.254.169.254` matches `169.254.169.254` without any normalization on our side.\nconst metadataBlockList = new net.BlockList();\n// AWS/GCP/Azure/... IMDS\nmetadataBlockList.addAddress('169.254.169.254', 'ipv4');\n// AWS IMDS IPv6 endpoint\nmetadataBlockList.addAddress('fd00:ec2::254', 'ipv6');\n// AWS ECS task credentials endpoint\nmetadataBlockList.addAddress('169.254.170.2', 'ipv4');\n// AWS EKS Pod Identity endpoint\nmetadataBlockList.addAddress('169.254.170.23', 'ipv4');\n// Alibaba Cloud/Tencent Cloud metadata\nmetadataBlockList.addAddress('100.100.100.200', 'ipv4');\n\nconst internalBlockList = new net.BlockList();\n// \"this network\" (routes to localhost on Linux)\ninternalBlockList.addSubnet('0.0.0.0', 8, 'ipv4');\n// RFC1918 private ranges\ninternalBlockList.addSubnet('10.0.0.0', 8, 'ipv4');\ninternalBlockList.addSubnet('172.16.0.0', 12, 'ipv4');\ninternalBlockList.addSubnet('192.168.0.0', 16, 'ipv4');\n// carrier-grade NAT\ninternalBlockList.addSubnet('100.64.0.0', 10, 'ipv4');\n// loopback\ninternalBlockList.addSubnet('127.0.0.0', 8, 'ipv4');\n// link-local, including the cloud metadata endpoints\ninternalBlockList.addSubnet('169.254.0.0', 16, 'ipv4');\n// multicast\ninternalBlockList.addSubnet('224.0.0.0', 4, 'ipv4');\n// IPv6 unspecified and loopback\ninternalBlockList.addAddress('::', 'ipv6');\ninternalBlockList.addAddress('::1', 'ipv6');\n// IPv6 unique-local\ninternalBlockList.addSubnet('fc00::', 7, 'ipv6');\n// IPv6 link-local\ninternalBlockList.addSubnet('fe80::', 10, 'ipv6');\n\n/**\n * Classifies an IP address, returning the category it is blocked under, or `null` for a public address.\n *\n * A string that is not a valid IP address is classified as `internal` - we should fail closed instead of possibly connecting to something we shouldn't be.\n */\nexport function classifyIpAddress(address: string): BlockedHostCategory | null {\n const version = net.isIP(address);\n if (version === 0) {\n return 'internal';\n }\n\n const family = version === 4 ? 'ipv4' : 'ipv6';\n\n if (metadataBlockList.check(address, family)) {\n return 'metadata';\n }\n\n if (internalBlockList.check(address, family)) {\n return 'internal';\n }\n\n return null;\n}\n\n/**\n * Classifies a hostname against the metadata-endpoint hostname list.\n *\n * Only metadata hostnames can be classified without DNS resolution - any other hostname needs its resolved addresses checked via {@link classifyIpAddress}.\n */\nexport function classifyHostname(hostname: string): BlockedHostCategory | null {\n // handle trailing-dot FQDN forms like `metadata.google.internal.`\n const normalized = hostname.toLowerCase().replace(regEx(/\\.$/), '');\n if (metadataHostnames.has(normalized)) {\n return 'metadata';\n }\n return null;\n}\n\n/** Strips the brackets `URL` keeps around IPv6 hosts, leaving other hosts as they are. */\nfunction unbracketHostname(hostname: string): string {\n if (hostname.startsWith('[') && hostname.endsWith(']')) {\n return hostname.slice(1, -1);\n }\n return hostname;\n}\n\n/**\n * Classifies a URL from its host alone: IP-literal hosts are classified by address, and metadata hostnames by name.\n *\n * Returns `null` when nothing can be decided at the URL level - i.e. the host is a regular hostname, whose resolved addresses must be checked via {@link classifyIpAddress} at DNS lookup time.\n */\nexport function classifyUrl(url: URL): BlockedHostCategory | null {\n const hostname = unbracketHostname(url.hostname);\n\n if (net.isIP(hostname) !== 0) {\n return classifyIpAddress(hostname);\n }\n\n return classifyHostname(hostname);\n}\n\n/**\n * What the URL-level check decided for a request, and so how the addresses its host resolves to must be treated.\n */\nexport type HostGuardDecision =\n /** internal addresses are permitted for this request, and only metadata addresses need blocking at resolution time */\n | 'granted'\n /** every address the host resolves to must be validated at DNS lookup time, with internal ones blocking the request */\n | 'check-dns'\n /** as `check-dns`, but an internal address only warns - the `internalHostAccess=warn` default lets the request through */\n | 'warn-dns';\n\ninterface BlockedAddress {\n address: string;\n category: BlockedHostCategory;\n}\n\n/**\n * Returns the resolved address which decides the outcome for a hostname, or `null` when every one of them is a public address.\n *\n * Each resolved record is a potential connection target, so each must pass. A metadata address decides the outcome over an internal one, as it is blocked in every mode.\n */\nfunction findBlockedAddress(addresses: string[]): BlockedAddress | null {\n let internal: BlockedAddress | null = null;\n for (const address of addresses) {\n const category = classifyIpAddress(address);\n if (category === 'metadata') {\n return { address, category };\n }\n if (category === 'internal') {\n internal ??= { address, category };\n }\n }\n return internal;\n}\n\n/**\n * Applies `decision` to the addresses a hostname resolved to, logging the outcome, and returning `true` when the request must not be made.\n */\nfunction isResolutionBlocked(\n hostname: string,\n addresses: string[],\n decision: HostGuardDecision,\n hostType: string | undefined,\n responseBecomesConfig: boolean | undefined,\n): boolean {\n const blocked = findBlockedAddress(addresses);\n if (!blocked) {\n return false;\n }\n\n if (blocked.category === 'internal') {\n if (decision === 'granted') {\n return false;\n }\n if (decision === 'warn-dns') {\n warnInternalHost(hostname, hostType, responseBecomesConfig);\n return false;\n }\n }\n\n logger.warn(\n { hostname, address: blocked.address },\n 'Blocked HTTP request: hostname resolves to a blocked address',\n );\n return true;\n}\n\n/**\n * Validates every address a hostname resolves to, on every connection, which also catches a host whose answer changes between checks (DNS rebinding).\n *\n * The guard is built per request, as the policy it enforces is read from the configuration when the request is made.\n */\nfunction makeGuardedLookup(\n decision: HostGuardDecision,\n hostType: string | undefined,\n responseBecomesConfig: boolean | undefined,\n): typeof dns.lookup {\n return function guardedLookup(\n hostname: string,\n options: dns.LookupOptions,\n callback: (\n err: NodeJS.ErrnoException | null,\n address: string | dns.LookupAddress[],\n family?: number,\n ) => void,\n ): void {\n dns.lookup(hostname, options, (err, address, family) => {\n if (err) {\n callback(err, address, family);\n return;\n }\n\n // with `options.all`, the result is an array of every record\n const addresses = isString(address)\n ? [address]\n : address.map((entry) => entry.address);\n if (\n isResolutionBlocked(\n hostname,\n addresses,\n decision,\n hostType,\n responseBecomesConfig,\n )\n ) {\n callback(new Error(HOST_BLOCKED), address, family);\n return;\n }\n\n callback(err, address, family);\n });\n // `dns.lookup` is an overloaded function, so the single signature implemented here is not assignable to it without a cast - got only ever calls the `(hostname, options, callback)` overload\n } as typeof dns.lookup;\n}\n\nfunction isPlatformEndpoint(url: URL): boolean {\n const endpoint = GlobalConfig.get('endpoint');\n if (!endpoint) {\n return false;\n }\n // the whole origin must match: another port or scheme on the platform's host is a different service, which the administrator has not vouched for\n return parseUrl(endpoint)?.origin === url.origin;\n}\n\n/**\n * Whether the administrator has permitted internal access for this request, leaving aside the `internalHostAccess` mode: a permitted request neither blocks nor warns, whatever the mode is.\n */\nfunction isInternalGranted(\n url: URL,\n grant: InternalHostGrant | undefined,\n responseBecomesConfig: boolean | undefined,\n): boolean {\n // without its platform, Renovate cannot run at all - the endpoint is the administrator's own choice of host\n if (isPlatformEndpoint(url)) {\n return true;\n }\n\n // a response which becomes configuration can redirect Renovate at anything, so the administrator must have named this host for this purpose, not merely named it\n if (responseBecomesConfig) {\n return grant?.scoped === true;\n }\n\n return grant?.explicit ?? grant?.implicit ?? false;\n}\n\n/**\n * Warns about a request which `internalHostAccess=block` would refuse, once per host, naming the grant which would permit it.\n */\nfunction warnInternalHost(\n hostname: string,\n hostType: string | undefined,\n responseBecomesConfig: boolean | undefined,\n): void {\n // a response which becomes config needs the deliberately-scoped grant, so pointing at a plain host rule would be misleading advice\n if (responseBecomesConfig) {\n logger.once.warn(\n { hostname, hostType },\n 'HTTP request to an internal host whose response becomes configuration, which `internalHostAccess=block` would refuse - permit it with a `hostRules` entry setting `allowInternal: true`, scoped with a `hostType` or a URL-prefix `matchHost`, or set `internalHostAccess=block` to enforce this now. The default will become `block` in a future major release.',\n );\n return;\n }\n\n logger.once.warn(\n { hostname, hostType },\n 'HTTP request to an internal host, which `internalHostAccess=block` would refuse - permit it with a `hostRules` entry naming the host, or one setting `allowInternal: true`, or set `internalHostAccess=block` to enforce this now. The default will become `block` in a future major release.',\n );\n}\n\n/**\n * Enforce the internal-host policy on a request URL, throwing `HOST_BLOCKED` when the request must not be made.\n *\n * Metadata endpoints are blocked no matter what the configuration says: Renovate's HTTP layer never has a legitimate reason to request them.\n */\nexport function checkUrl(\n url: URL,\n hostType: string | undefined,\n grant: InternalHostGrant | undefined,\n responseBecomesConfig?: boolean,\n): HostGuardDecision {\n const category = classifyUrl(url);\n if (category === 'metadata') {\n logger.warn(\n { url: url.href, hostType },\n 'Blocked HTTP request to a cloud instance-metadata endpoint',\n );\n throw new Error(HOST_BLOCKED);\n }\n\n const mode = GlobalConfig.get('internalHostAccess');\n if (\n mode === 'allow' ||\n isInternalGranted(url, grant, responseBecomesConfig)\n ) {\n if (category === 'internal') {\n logger.once.info(\n `Internal host ${url.hostname} permitted by configuration`,\n );\n }\n return 'granted';\n }\n\n // `warn` reports what `block` would refuse, without refusing it, so that administrators can add the grants they need before the default changes\n if (mode === 'warn') {\n if (category === 'internal') {\n warnInternalHost(url.hostname, hostType, responseBecomesConfig);\n }\n return 'warn-dns';\n }\n\n if (category === 'internal') {\n logger.warn(\n { url: url.href, hostType },\n 'Blocked HTTP request to an internal host - a self-hosted administrator can permit it via `hostRules`, or with `internalHostAccess=allow`',\n );\n throw new Error(HOST_BLOCKED);\n }\n return 'check-dns';\n}\n\n/**\n * Validates what a request's hostname resolves to, before the request is made, for deployments where `dnsLookup` cannot do it.\n *\n * A proxy agent connects to the proxy and hands it the target hostname, so the target is resolved by the proxy and Renovate's `dnsLookup` guard never runs. Resolving here instead is best-effort only: the proxy re-resolves the hostname itself, so a DNS-rebinding or split-horizon answer can still differ from what we saw. Pair it with egress controls on the proxy.\n */\nasync function preflightHostname(\n url: URL,\n decision: HostGuardDecision,\n hostType: string | undefined,\n responseBecomesConfig: boolean | undefined,\n): Promise<void> {\n const hostname = unbracketHostname(url.hostname);\n if (net.isIP(hostname) !== 0) {\n // an IP-literal host needs no resolution - `checkUrl` has already classified it\n return;\n }\n\n let addresses: dns.LookupAddress[];\n try {\n addresses = await dnsPromises.lookup(hostname, { all: true });\n } catch {\n // fail open: a locked-down proxy environment often has no resolver which can answer for public names at all, and failing closed would break every such deployment\n logger.once.debug(\n `Host guard could not resolve ${hostname} - relying on the proxy for this request`,\n );\n return;\n }\n\n if (\n isResolutionBlocked(\n hostname,\n addresses.map((entry) => entry.address),\n decision,\n hostType,\n responseBecomesConfig,\n )\n ) {\n throw new Error(HOST_BLOCKED);\n }\n}\n\nfunction makeBeforeRequest(\n hostType: string | undefined,\n responseBecomesConfig: boolean | undefined,\n): BeforeRequestHook {\n return async function hostGuardBeforeRequest(\n options: NormalizedOptions,\n ): Promise<void> {\n const url = options.url instanceof URL ? options.url : null;\n if (!url) {\n // fail closed: without a parsed target there is nothing to validate\n throw new Error(HOST_BLOCKED);\n }\n\n // got runs this hook again for every redirect target, so the decision is re-made for whatever host is about to be requested rather than carried over\n const grant = hostRules.find({\n hostType,\n url: url.toString(),\n }).internalHostGrant;\n await preflightHostname(\n url,\n checkUrl(url, hostType, grant, responseBecomesConfig),\n hostType,\n responseBecomesConfig,\n );\n };\n}\n\n/**\n * Removes the credentials belonging to the host a request is being redirected away from, so that a redirect cannot hand them to another origin.\n *\n * got strips the credentials it knows about itself - `authorization`, `cookie` and any URL userinfo - before this hook runs, and treats a scheme downgrade as a different origin, so an `https:` to `http:` redirect is covered. What it does not know about survives: the `Private-token` header GitLab personal access tokens are sent in (see `applyAuthorization`), and any credential an administrator configured through a `hostRules` `headers` entry.\n *\n * Those headers are dropped rather than replaced with the target host's own: `hostRules` headers are resolved once, when the request is built, so there is nothing to re-apply here. Sending none is the safe outcome.\n */\nfunction stripCrossOriginCredentials(\n options: NormalizedOptions,\n fromUrl: URL | null,\n toUrl: URL,\n hostType: string | undefined,\n): void {\n if (fromUrl?.origin === toUrl.origin) {\n // the credentials are still going to the host they were configured for\n return;\n }\n\n // an unparseable source URL is treated as cross-origin: it cannot be shown to be the same origin, so nothing is carried over\n if (fromUrl) {\n const configured = hostRules.find({\n hostType,\n url: fromUrl.toString(),\n }).headers;\n for (const name of Object.keys(coerceObject(configured))) {\n // got normalizes header names to lower case\n delete options.headers[name.toLowerCase()];\n }\n }\n\n // defence in depth: got has removed these already, but they must never reach another origin\n delete options.headers.authorization;\n delete options.headers.cookie;\n delete options.headers['private-token'];\n}\n\nfunction makeBeforeRedirect(\n hostType: string | undefined,\n responseBecomesConfig: boolean | undefined,\n): BeforeRedirectHook {\n return function hostGuardBeforeRedirect(\n options: NormalizedOptions,\n response: PlainResponse,\n ): void {\n const url = options.url instanceof URL ? options.url : null;\n if (!url) {\n // fail closed: without a parsed target there is nothing to validate\n throw new Error(HOST_BLOCKED);\n }\n\n const fromUrl = isString(response.url) ? parseUrl(response.url) : null;\n stripCrossOriginCredentials(options, fromUrl, url, hostType);\n\n // the original host's verdict must not carry over: the new host gets its own grant lookup and decision\n const grant = hostRules.find({\n hostType,\n url: url.toString(),\n }).internalHostGrant;\n // a redirect target of a config-fetching request is still config-fetching, so the flag carries over even though the verdict does not\n options.dnsLookup = makeGuardedLookup(\n checkUrl(url, hostType, grant, responseBecomesConfig),\n hostType,\n responseBecomesConfig,\n );\n };\n}\n\nexport interface HostGuard {\n dnsLookup: typeof dns.lookup;\n beforeRedirect: BeforeRedirectHook;\n /** Only set when a proxy is configured, which makes `dnsLookup` inert. */\n beforeRequest?: BeforeRequestHook;\n}\n\n/**\n * Enforce the internal-host policy for a request to `url`, throwing `HOST_BLOCKED` when the request must not be made at all.\n *\n * The returned got options carry the policy through the rest of the request's lifetime: `dnsLookup` validates what hostnames resolve to (on every connection, which also covers DNS rebinding), and `beforeRedirect` re-runs this check for every redirect target.\n *\n * When a proxy is configured, the proxy agent resolves the target hostname itself and `dnsLookup` is never called, so a `beforeRequest` hook resolves and validates the hostname up front instead - see {@link preflightHostname} for what that can and cannot catch.\n *\n * `responseBecomesConfig` marks a request whose response is interpreted as Renovate configuration, which an internal host may only serve under a deliberately-scoped `allowInternal` grant.\n *\n * Under the `internalHostAccess=warn` default, an internal host which no grant permits is logged rather than blocked, so that administrators can add the grants they need before the default becomes `block`.\n */\nexport function applyHostGuard(\n url: URL,\n hostType: string | undefined,\n grant: InternalHostGrant | undefined,\n responseBecomesConfig?: boolean,\n): HostGuard {\n const decision = checkUrl(url, hostType, grant, responseBecomesConfig);\n const guard: HostGuard = {\n dnsLookup: makeGuardedLookup(decision, hostType, responseBecomesConfig),\n beforeRedirect: makeBeforeRedirect(hostType, responseBecomesConfig),\n };\n\n if (hasProxy()) {\n guard.beforeRequest = makeBeforeRequest(hostType, responseBecomesConfig);\n }\n\n return guard;\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"host-rules.js","names":["hostRules.find"],"sources":["../../../lib/util/http/host-rules.ts"],"sourcesContent":["import { isNonEmptyString } from '@sindresorhus/is';\nimport { GlobalConfig } from '../../config/global.ts';\nimport {\n BITBUCKET_API_USING_HOST_TYPES,\n BITBUCKET_SERVER_API_USING_HOST_TYPES,\n FORGEJO_API_USING_HOST_TYPES,\n GITEA_API_USING_HOST_TYPES,\n GITHUB_API_USING_HOST_TYPES,\n GITLAB_API_USING_HOST_TYPES,\n} from '../../constants/index.ts';\nimport { logger } from '../../logger/index.ts';\nimport { hasProxy } from '../../proxy.ts';\nimport type { HostRule } from '../../types/index.ts';\nimport * as hostRules from '../host-rules.ts';\nimport { matchRegexOrGlobList } from '../string-match.ts';\nimport { parseUrl } from '../url.ts';\nimport type { InternalHttpOptions } from './http.ts';\nimport { keepAliveAgents } from './keep-alive.ts';\nimport type { GotOptions } from './types.ts';\n\nexport type HostRulesGotOptions = Pick<\n GotOptions & InternalHttpOptions,\n | 'hostType'\n | 'noAuth'\n | 'headers'\n | 'token'\n | 'username'\n | 'password'\n | 'context'\n | 'enabled'\n | 'abortOnError'\n | 'abortIgnoreStatusCodes'\n | 'timeout'\n | 'agent'\n | 'http2'\n | 'https'\n | 'readOnly'\n>;\n\nexport function findMatchingRule<GotOptions extends HostRulesGotOptions>(\n url: string,\n options: GotOptions,\n): HostRule {\n const { hostType, readOnly } = options;\n let res = hostRules.find({ hostType, url, readOnly });\n\n if (\n isNonEmptyString(res.token) ||\n isNonEmptyString(res.username) ||\n isNonEmptyString(res.password)\n ) {\n // do not fallback if we already have auth infos\n return res;\n }\n\n // Fallback to `github` hostType\n if (\n hostType &&\n GITHUB_API_USING_HOST_TYPES.includes(hostType) &&\n hostType !== 'github'\n ) {\n res = {\n ...hostRules.find({\n hostType: 'github',\n url,\n }),\n ...res,\n };\n }\n\n const platform = GlobalConfig.get('platform');\n const platformEndpoint = GlobalConfig.get('endpoint');\n\n // in the case that an API URL is used for GitHub.com, fallback to `github` hostType, and use the `url`'s host to find a `matchHost: api.github.com` (or `matchHost: github.com`)\n if (url.startsWith('https://api.github.com/')) {\n res = {\n ...hostRules.find({\n hostType: 'github',\n url,\n }),\n ...res,\n };\n } else if (platform === 'github' && platformEndpoint) {\n // Fallback to `github` hostType when the request URL targets the same host\n // as the configured GitHub platform endpoint.\n //\n // This covers GitHub Enterprise Server without hardcoding URLs.\n const requestHost = parseUrl(url)?.hostname;\n const endpointHost = parseUrl(platformEndpoint)?.hostname;\n if (requestHost && endpointHost && requestHost === endpointHost) {\n res = {\n ...hostRules.find({ hostType: 'github', url }),\n ...res,\n };\n }\n }\n\n // Fallback to `gitlab` hostType\n if (\n hostType &&\n GITLAB_API_USING_HOST_TYPES.includes(hostType) &&\n hostType !== 'gitlab'\n ) {\n res = {\n ...hostRules.find({\n hostType: 'gitlab',\n url,\n }),\n ...res,\n };\n }\n\n // Fallback to `bitbucket` hostType\n if (\n hostType &&\n BITBUCKET_API_USING_HOST_TYPES.includes(hostType) &&\n hostType !== 'bitbucket'\n ) {\n res = {\n ...hostRules.find({\n hostType: 'bitbucket',\n url,\n }),\n ...res,\n };\n }\n\n // Fallback to `bitbucket-server` hostType\n if (\n hostType &&\n BITBUCKET_SERVER_API_USING_HOST_TYPES.includes(hostType) &&\n hostType !== 'bitbucket-server'\n ) {\n res = {\n ...hostRules.find({\n hostType: 'bitbucket-server',\n url,\n }),\n ...res,\n };\n }\n\n // Fallback to `forgejo` hostType\n if (\n hostType &&\n FORGEJO_API_USING_HOST_TYPES.includes(hostType) &&\n hostType !== 'forgejo'\n ) {\n res = {\n ...hostRules.find({\n hostType: 'forgejo',\n url,\n }),\n ...res,\n };\n }\n\n // Fallback to `gitea` hostType\n if (\n hostType &&\n GITEA_API_USING_HOST_TYPES.includes(hostType) &&\n hostType !== 'gitea'\n ) {\n res = {\n ...hostRules.find({\n hostType: 'gitea',\n url,\n }),\n ...res,\n };\n }\n\n return res;\n}\n\n// Apply host rules to requests\nexport function applyHostRule<GotOptions extends HostRulesGotOptions>(\n url: string,\n options: GotOptions,\n hostRule: HostRule,\n): GotOptions {\n if (hostRule.enabled === false) {\n options.enabled = false;\n return options;\n }\n\n const { username, password, token, authType } = hostRule;\n const host = parseUrl(url)?.host;\n if (options.noAuth) {\n logger.trace({ url }, `Authorization disabled`);\n } else if (\n isNonEmptyString(options.headers?.authorization) ||\n isNonEmptyString(options.password) ||\n isNonEmptyString(options.token)\n ) {\n logger.once.debug(`hostRules: authentication already set for ${host}`);\n logger.trace({ url }, `Authorization already set`);\n } else if (password !== undefined) {\n logger.once.debug(`hostRules: applying Basic authentication for ${host}`);\n logger.trace({ url }, `Applying Basic authentication`);\n options.username = username;\n options.password = password;\n } else if (token) {\n logger.once.debug(`hostRules: applying Bearer authentication for ${host}`);\n logger.trace({ url }, `Applying Bearer authentication`);\n options.token = token;\n options.context = { ...options.context, authType };\n } else {\n logger.once.debug(`hostRules: no authentication for ${host}`);\n }\n // Apply optional params\n if (hostRule.abortOnError) {\n options.abortOnError = hostRule.abortOnError;\n }\n\n if (hostRule.abortIgnoreStatusCodes) {\n options.abortIgnoreStatusCodes = hostRule.abortIgnoreStatusCodes;\n }\n\n if (hostRule.timeout) {\n options.timeout = hostRule.timeout;\n }\n\n if (hostRule.headers) {\n const allowedHeaders = GlobalConfig.get('allowedHeaders');\n const filteredHeaders: Record<string, string> = {};\n\n for (const [header, value] of Object.entries(hostRule.headers)) {\n if (matchRegexOrGlobList(header, allowedHeaders)) {\n filteredHeaders[header] = value;\n } else {\n logger.once.error(\n { allowedHeaders, header },\n 'Disallowed hostRules headers',\n );\n }\n }\n\n options.headers = {\n ...options.headers,\n ...filteredHeaders,\n };\n }\n\n if (hostRule.keepAlive) {\n options.agent = keepAliveAgents;\n }\n\n if (!hasProxy() && hostRule.enableHttp2 === true) {\n options.http2 = true;\n }\n\n if (isNonEmptyString(hostRule.httpsCertificateAuthority)) {\n options.https = {\n ...options.https,\n certificateAuthority: hostRule.httpsCertificateAuthority,\n };\n }\n\n if (isNonEmptyString(hostRule.httpsPrivateKey)) {\n options.https = {\n ...options.https,\n key: hostRule.httpsPrivateKey,\n };\n }\n\n if (isNonEmptyString(hostRule.httpsCertificate)) {\n options.https = {\n ...options.https,\n certificate: hostRule.httpsCertificate,\n };\n }\n\n return options;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"host-rules.js","names":["hostRules.find"],"sources":["../../../lib/util/http/host-rules.ts"],"sourcesContent":["import { isNonEmptyString } from '@sindresorhus/is';\nimport { GlobalConfig } from '../../config/global.ts';\nimport {\n BITBUCKET_API_USING_HOST_TYPES,\n BITBUCKET_SERVER_API_USING_HOST_TYPES,\n FORGEJO_API_USING_HOST_TYPES,\n GITEA_API_USING_HOST_TYPES,\n GITHUB_API_USING_HOST_TYPES,\n GITLAB_API_USING_HOST_TYPES,\n} from '../../constants/index.ts';\nimport { logger } from '../../logger/index.ts';\nimport { hasProxy } from '../../proxy.ts';\nimport type { CombinedHostRule, HostRule } from '../../types/index.ts';\nimport * as hostRules from '../host-rules.ts';\nimport { matchRegexOrGlobList } from '../string-match.ts';\nimport { parseUrl } from '../url.ts';\nimport type { InternalHttpOptions } from './http.ts';\nimport { keepAliveAgents } from './keep-alive.ts';\nimport type { GotOptions } from './types.ts';\n\nexport type HostRulesGotOptions = Pick<\n GotOptions & InternalHttpOptions,\n | 'hostType'\n | 'noAuth'\n | 'headers'\n | 'token'\n | 'username'\n | 'password'\n | 'context'\n | 'enabled'\n | 'abortOnError'\n | 'abortIgnoreStatusCodes'\n | 'timeout'\n | 'agent'\n | 'http2'\n | 'https'\n | 'readOnly'\n>;\n\nexport function findMatchingRule<GotOptions extends HostRulesGotOptions>(\n url: string,\n options: GotOptions,\n): CombinedHostRule {\n const { hostType, readOnly } = options;\n let res = hostRules.find({ hostType, url, readOnly });\n\n if (\n isNonEmptyString(res.token) ||\n isNonEmptyString(res.username) ||\n isNonEmptyString(res.password)\n ) {\n // do not fallback if we already have auth infos\n return res;\n }\n\n // Fallback to `github` hostType\n if (\n hostType &&\n GITHUB_API_USING_HOST_TYPES.includes(hostType) &&\n hostType !== 'github'\n ) {\n res = {\n ...hostRules.find({\n hostType: 'github',\n url,\n }),\n ...res,\n };\n }\n\n const platform = GlobalConfig.get('platform');\n const platformEndpoint = GlobalConfig.get('endpoint');\n\n // in the case that an API URL is used for GitHub.com, fallback to `github` hostType, and use the `url`'s host to find a `matchHost: api.github.com` (or `matchHost: github.com`)\n if (url.startsWith('https://api.github.com/')) {\n res = {\n ...hostRules.find({\n hostType: 'github',\n url,\n }),\n ...res,\n };\n } else if (platform === 'github' && platformEndpoint) {\n // Fallback to `github` hostType when the request URL targets the same host\n // as the configured GitHub platform endpoint.\n //\n // This covers GitHub Enterprise Server without hardcoding URLs.\n const requestHost = parseUrl(url)?.hostname;\n const endpointHost = parseUrl(platformEndpoint)?.hostname;\n if (requestHost && endpointHost && requestHost === endpointHost) {\n res = {\n ...hostRules.find({ hostType: 'github', url }),\n ...res,\n };\n }\n }\n\n // Fallback to `gitlab` hostType\n if (\n hostType &&\n GITLAB_API_USING_HOST_TYPES.includes(hostType) &&\n hostType !== 'gitlab'\n ) {\n res = {\n ...hostRules.find({\n hostType: 'gitlab',\n url,\n }),\n ...res,\n };\n }\n\n // Fallback to `bitbucket` hostType\n if (\n hostType &&\n BITBUCKET_API_USING_HOST_TYPES.includes(hostType) &&\n hostType !== 'bitbucket'\n ) {\n res = {\n ...hostRules.find({\n hostType: 'bitbucket',\n url,\n }),\n ...res,\n };\n }\n\n // Fallback to `bitbucket-server` hostType\n if (\n hostType &&\n BITBUCKET_SERVER_API_USING_HOST_TYPES.includes(hostType) &&\n hostType !== 'bitbucket-server'\n ) {\n res = {\n ...hostRules.find({\n hostType: 'bitbucket-server',\n url,\n }),\n ...res,\n };\n }\n\n // Fallback to `forgejo` hostType\n if (\n hostType &&\n FORGEJO_API_USING_HOST_TYPES.includes(hostType) &&\n hostType !== 'forgejo'\n ) {\n res = {\n ...hostRules.find({\n hostType: 'forgejo',\n url,\n }),\n ...res,\n };\n }\n\n // Fallback to `gitea` hostType\n if (\n hostType &&\n GITEA_API_USING_HOST_TYPES.includes(hostType) &&\n hostType !== 'gitea'\n ) {\n res = {\n ...hostRules.find({\n hostType: 'gitea',\n url,\n }),\n ...res,\n };\n }\n\n return res;\n}\n\n// Apply host rules to requests\nexport function applyHostRule<GotOptions extends HostRulesGotOptions>(\n url: string,\n options: GotOptions,\n hostRule: HostRule,\n): GotOptions {\n if (hostRule.enabled === false) {\n options.enabled = false;\n return options;\n }\n\n const { username, password, token, authType } = hostRule;\n const host = parseUrl(url)?.host;\n if (options.noAuth) {\n logger.trace({ url }, `Authorization disabled`);\n } else if (\n isNonEmptyString(options.headers?.authorization) ||\n isNonEmptyString(options.password) ||\n isNonEmptyString(options.token)\n ) {\n logger.once.debug(`hostRules: authentication already set for ${host}`);\n logger.trace({ url }, `Authorization already set`);\n } else if (password !== undefined) {\n logger.once.debug(`hostRules: applying Basic authentication for ${host}`);\n logger.trace({ url }, `Applying Basic authentication`);\n options.username = username;\n options.password = password;\n } else if (token) {\n logger.once.debug(`hostRules: applying Bearer authentication for ${host}`);\n logger.trace({ url }, `Applying Bearer authentication`);\n options.token = token;\n options.context = { ...options.context, authType };\n } else {\n logger.once.debug(`hostRules: no authentication for ${host}`);\n }\n // Apply optional params\n if (hostRule.abortOnError) {\n options.abortOnError = hostRule.abortOnError;\n }\n\n if (hostRule.abortIgnoreStatusCodes) {\n options.abortIgnoreStatusCodes = hostRule.abortIgnoreStatusCodes;\n }\n\n if (hostRule.timeout) {\n options.timeout = hostRule.timeout;\n }\n\n if (hostRule.headers) {\n const allowedHeaders = GlobalConfig.get('allowedHeaders');\n const filteredHeaders: Record<string, string> = {};\n\n for (const [header, value] of Object.entries(hostRule.headers)) {\n if (matchRegexOrGlobList(header, allowedHeaders)) {\n filteredHeaders[header] = value;\n } else {\n logger.once.error(\n { allowedHeaders, header },\n 'Disallowed hostRules headers',\n );\n }\n }\n\n options.headers = {\n ...options.headers,\n ...filteredHeaders,\n };\n }\n\n if (hostRule.keepAlive) {\n options.agent = keepAliveAgents;\n }\n\n if (!hasProxy() && hostRule.enableHttp2 === true) {\n options.http2 = true;\n }\n\n if (isNonEmptyString(hostRule.httpsCertificateAuthority)) {\n options.https = {\n ...options.https,\n certificateAuthority: hostRule.httpsCertificateAuthority,\n };\n }\n\n if (isNonEmptyString(hostRule.httpsPrivateKey)) {\n options.https = {\n ...options.https,\n key: hostRule.httpsPrivateKey,\n };\n }\n\n if (isNonEmptyString(hostRule.httpsCertificate)) {\n options.https = {\n ...options.https,\n certificate: hostRule.httpsCertificate,\n };\n }\n\n return options;\n}\n"],"mappings":";;;;;;;;;;AAuCA,SAAgB,iBACd,KACA,SACkB;CAClB,MAAM,EAAE,UAAU,aAAa;CAC/B,IAAI,MAAMA,KAAe;EAAE;EAAU;EAAK;CAAS,CAAC;CAEpD,IACE,iBAAiB,IAAI,KAAK,KAC1B,iBAAiB,IAAI,QAAQ,KAC7B,iBAAiB,IAAI,QAAQ,GAG7B,OAAO;CAIT,IACE,YACA,4BAA4B,SAAS,QAAQ,KAC7C,aAAa,UAEb,MAAM;EACJ,GAAGA,KAAe;GAChB,UAAU;GACV;EACF,CAAC;EACD,GAAG;CACL;CAGF,MAAM,WAAW,aAAa,IAAI,UAAU;CAC5C,MAAM,mBAAmB,aAAa,IAAI,UAAU;CAGpD,IAAI,IAAI,WAAW,yBAAyB,GAC1C,MAAM;EACJ,GAAGA,KAAe;GAChB,UAAU;GACV;EACF,CAAC;EACD,GAAG;CACL;MACK,IAAI,aAAa,YAAY,kBAAkB;EAKpD,MAAM,cAAc,SAAS,GAAG,CAAC,EAAE;EACnC,MAAM,eAAe,SAAS,gBAAgB,CAAC,EAAE;EACjD,IAAI,eAAe,gBAAgB,gBAAgB,cACjD,MAAM;GACJ,GAAGA,KAAe;IAAE,UAAU;IAAU;GAAI,CAAC;GAC7C,GAAG;EACL;CAEJ;CAGA,IACE,YACA,4BAA4B,SAAS,QAAQ,KAC7C,aAAa,UAEb,MAAM;EACJ,GAAGA,KAAe;GAChB,UAAU;GACV;EACF,CAAC;EACD,GAAG;CACL;CAIF,IACE,YACA,+BAA+B,SAAS,QAAQ,KAChD,aAAa,aAEb,MAAM;EACJ,GAAGA,KAAe;GAChB,UAAU;GACV;EACF,CAAC;EACD,GAAG;CACL;CAIF,IACE,YACA,sCAAsC,SAAS,QAAQ,KACvD,aAAa,oBAEb,MAAM;EACJ,GAAGA,KAAe;GAChB,UAAU;GACV;EACF,CAAC;EACD,GAAG;CACL;CAIF,IACE,YACA,6BAA6B,SAAS,QAAQ,KAC9C,aAAa,WAEb,MAAM;EACJ,GAAGA,KAAe;GAChB,UAAU;GACV;EACF,CAAC;EACD,GAAG;CACL;CAIF,IACE,YACA,2BAA2B,SAAS,QAAQ,KAC5C,aAAa,SAEb,MAAM;EACJ,GAAGA,KAAe;GAChB,UAAU;GACV;EACF,CAAC;EACD,GAAG;CACL;CAGF,OAAO;AACT;AAGA,SAAgB,cACd,KACA,SACA,UACY;CACZ,IAAI,SAAS,YAAY,OAAO;EAC9B,QAAQ,UAAU;EAClB,OAAO;CACT;CAEA,MAAM,EAAE,UAAU,UAAU,OAAO,aAAa;CAChD,MAAM,OAAO,SAAS,GAAG,CAAC,EAAE;CAC5B,IAAI,QAAQ,QACV,OAAO,MAAM,EAAE,IAAI,GAAG,wBAAwB;MACzC,IACL,iBAAiB,QAAQ,SAAS,aAAa,KAC/C,iBAAiB,QAAQ,QAAQ,KACjC,iBAAiB,QAAQ,KAAK,GAC9B;EACA,OAAO,KAAK,MAAM,6CAA6C,MAAM;EACrE,OAAO,MAAM,EAAE,IAAI,GAAG,2BAA2B;CACnD,OAAO,IAAI,aAAa,KAAA,GAAW;EACjC,OAAO,KAAK,MAAM,gDAAgD,MAAM;EACxE,OAAO,MAAM,EAAE,IAAI,GAAG,+BAA+B;EACrD,QAAQ,WAAW;EACnB,QAAQ,WAAW;CACrB,OAAO,IAAI,OAAO;EAChB,OAAO,KAAK,MAAM,iDAAiD,MAAM;EACzE,OAAO,MAAM,EAAE,IAAI,GAAG,gCAAgC;EACtD,QAAQ,QAAQ;EAChB,QAAQ,UAAU;GAAE,GAAG,QAAQ;GAAS;EAAS;CACnD,OACE,OAAO,KAAK,MAAM,oCAAoC,MAAM;CAG9D,IAAI,SAAS,cACX,QAAQ,eAAe,SAAS;CAGlC,IAAI,SAAS,wBACX,QAAQ,yBAAyB,SAAS;CAG5C,IAAI,SAAS,SACX,QAAQ,UAAU,SAAS;CAG7B,IAAI,SAAS,SAAS;EACpB,MAAM,iBAAiB,aAAa,IAAI,gBAAgB;EACxD,MAAM,kBAA0C,CAAC;EAEjD,KAAK,MAAM,CAAC,QAAQ,UAAU,OAAO,QAAQ,SAAS,OAAO,GAC3D,IAAI,qBAAqB,QAAQ,cAAc,GAC7C,gBAAgB,UAAU;OAE1B,OAAO,KAAK,MACV;GAAE;GAAgB;EAAO,GACzB,8BACF;EAIJ,QAAQ,UAAU;GAChB,GAAG,QAAQ;GACX,GAAG;EACL;CACF;CAEA,IAAI,SAAS,WACX,QAAQ,QAAQ;CAGlB,IAAI,CAAC,SAAS,KAAK,SAAS,gBAAgB,MAC1C,QAAQ,QAAQ;CAGlB,IAAI,iBAAiB,SAAS,yBAAyB,GACrD,QAAQ,QAAQ;EACd,GAAG,QAAQ;EACX,sBAAsB,SAAS;CACjC;CAGF,IAAI,iBAAiB,SAAS,eAAe,GAC3C,QAAQ,QAAQ;EACd,GAAG,QAAQ;EACX,KAAK,SAAS;CAChB;CAGF,IAAI,iBAAiB,SAAS,gBAAgB,GAC5C,QAAQ,QAAQ;EACd,GAAG,QAAQ;EACX,aAAa,SAAS;CACxB;CAGF,OAAO;AACT"}
|
package/dist/util/http/http.d.ts
CHANGED
|
@@ -18,6 +18,14 @@ declare abstract class HttpBase<JSONOpts extends HttpOptions = HttpOptions, Opts
|
|
|
18
18
|
protected get baseUrl(): string | undefined;
|
|
19
19
|
protected hostType: string;
|
|
20
20
|
constructor(hostType: string, options?: HttpOptions);
|
|
21
|
+
/**
|
|
22
|
+
* Builds the final got options for a request to a given `url`
|
|
23
|
+
*
|
|
24
|
+
* This merges instance options, applies default headers, host rules and authorization, and rejects requests to disabled hosts.
|
|
25
|
+
*
|
|
26
|
+
* Both the `request()` and `stream()` paths must use this single entry point so that policy applied here covers every outbound request.
|
|
27
|
+
*/
|
|
28
|
+
private prepareOptions;
|
|
21
29
|
private request;
|
|
22
30
|
private _normalizeOptions;
|
|
23
31
|
/**
|