Files
renovate/lib/util/http/host-rules.ts
Sebastian Poxhofer 853290db9c refactor(http): read platform families instead of the host type aliases (#46347)
`lib/util/http/auth.ts` and `lib/util/http/host-rules.ts` were the last
readers of the `*_API_USING_HOST_TYPES` constants, which the previous
commits left as aliases of `PLATFORM_FAMILIES`. Both now read the table
directly and the aliases are deleted, so each family's host types are
declared in exactly one place.

Also covers the `forgejo` fallback in `findMatchingRule()`, which had no
test: the shared setup only registers a `gitea` host rule, so the
forgejo branch was never entered.

Behaviour changes: none. The aliases pointed at the same arrays.
2026-09-21 15:27:53 +00:00

271 lines
6.7 KiB
TypeScript

import { isNonEmptyString } from '@sindresorhus/is';
import { GlobalConfig } from '../../config/global.ts';
import { PLATFORM_FAMILIES } from '../../constants/index.ts';
import { logger } from '../../logger/index.ts';
import { hasProxy } from '../../proxy.ts';
import type { CombinedHostRule, HostRule } from '../../types/index.ts';
import * as hostRules from '../host-rules.ts';
import { matchRegexOrGlobList } from '../string-match.ts';
import { parseUrl } from '../url.ts';
import type { InternalHttpOptions } from './http.ts';
import { keepAliveAgents } from './keep-alive.ts';
import type { GotOptions } from './types.ts';
export type HostRulesGotOptions = Pick<
GotOptions & InternalHttpOptions,
| 'hostType'
| 'noAuth'
| 'headers'
| 'token'
| 'username'
| 'password'
| 'context'
| 'enabled'
| 'abortOnError'
| 'abortIgnoreStatusCodes'
| 'timeout'
| 'agent'
| 'http2'
| 'https'
| 'readOnly'
>;
export function findMatchingRule<GotOptions extends HostRulesGotOptions>(
url: string,
options: GotOptions,
): CombinedHostRule {
const { hostType, readOnly } = options;
let res = hostRules.find({ hostType, url, readOnly });
if (
isNonEmptyString(res.token) ||
isNonEmptyString(res.username) ||
isNonEmptyString(res.password)
) {
// do not fallback if we already have auth infos
return res;
}
// Fallback to `github` hostType
if (
hostType &&
PLATFORM_FAMILIES.github.apiUsingHostTypes.includes(hostType) &&
hostType !== 'github'
) {
res = {
...hostRules.find({
hostType: 'github',
url,
}),
...res,
};
}
const platform = GlobalConfig.get('platform');
const platformEndpoint = GlobalConfig.get('endpoint');
// 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`)
if (url.startsWith('https://api.github.com/')) {
res = {
...hostRules.find({
hostType: 'github',
url,
}),
...res,
};
} else if (platform === 'github' && platformEndpoint) {
// Fallback to `github` hostType when the request URL targets the same host
// as the configured GitHub platform endpoint.
//
// This covers GitHub Enterprise Server without hardcoding URLs.
const requestHost = parseUrl(url)?.hostname;
const endpointHost = parseUrl(platformEndpoint)?.hostname;
if (requestHost && endpointHost && requestHost === endpointHost) {
res = {
...hostRules.find({ hostType: 'github', url }),
...res,
};
}
}
// Fallback to `gitlab` hostType
if (
hostType &&
PLATFORM_FAMILIES.gitlab.apiUsingHostTypes.includes(hostType) &&
hostType !== 'gitlab'
) {
res = {
...hostRules.find({
hostType: 'gitlab',
url,
}),
...res,
};
}
// Fallback to `bitbucket` hostType
if (
hostType &&
PLATFORM_FAMILIES.bitbucket.apiUsingHostTypes.includes(hostType) &&
hostType !== 'bitbucket'
) {
res = {
...hostRules.find({
hostType: 'bitbucket',
url,
}),
...res,
};
}
// Fallback to `bitbucket-server` hostType
if (
hostType &&
PLATFORM_FAMILIES['bitbucket-server'].apiUsingHostTypes.includes(
hostType,
) &&
hostType !== 'bitbucket-server'
) {
res = {
...hostRules.find({
hostType: 'bitbucket-server',
url,
}),
...res,
};
}
// Fallback to `forgejo` hostType
if (
hostType &&
PLATFORM_FAMILIES.forgejo.apiUsingHostTypes.includes(hostType) &&
hostType !== 'forgejo'
) {
res = {
...hostRules.find({
hostType: 'forgejo',
url,
}),
...res,
};
}
// Fallback to `gitea` hostType
if (
hostType &&
PLATFORM_FAMILIES.gitea.apiUsingHostTypes.includes(hostType) &&
hostType !== 'gitea'
) {
res = {
...hostRules.find({
hostType: 'gitea',
url,
}),
...res,
};
}
return res;
}
// Apply host rules to requests
export function applyHostRule<GotOptions extends HostRulesGotOptions>(
url: string,
options: GotOptions,
hostRule: HostRule,
): GotOptions {
if (hostRule.enabled === false) {
options.enabled = false;
return options;
}
const { username, password, token, authType } = hostRule;
const host = parseUrl(url)?.host;
if (options.noAuth) {
logger.trace({ url }, `Authorization disabled`);
} else if (
isNonEmptyString(options.headers?.authorization) ||
isNonEmptyString(options.password) ||
isNonEmptyString(options.token)
) {
logger.once.debug(`hostRules: authentication already set for ${host}`);
logger.trace({ url }, `Authorization already set`);
} else if (password !== undefined) {
logger.once.debug(`hostRules: applying Basic authentication for ${host}`);
logger.trace({ url }, `Applying Basic authentication`);
options.username = username;
options.password = password;
} else if (token) {
logger.once.debug(`hostRules: applying Bearer authentication for ${host}`);
logger.trace({ url }, `Applying Bearer authentication`);
options.token = token;
options.context = { ...options.context, authType };
} else {
logger.once.debug(`hostRules: no authentication for ${host}`);
}
// Apply optional params
if (hostRule.abortOnError) {
options.abortOnError = hostRule.abortOnError;
}
if (hostRule.abortIgnoreStatusCodes) {
options.abortIgnoreStatusCodes = hostRule.abortIgnoreStatusCodes;
}
if (hostRule.timeout) {
options.timeout = hostRule.timeout;
}
if (hostRule.headers) {
const allowedHeaders = GlobalConfig.get('allowedHeaders');
const filteredHeaders: Record<string, string> = {};
for (const [header, value] of Object.entries(hostRule.headers)) {
if (matchRegexOrGlobList(header, allowedHeaders)) {
filteredHeaders[header] = value;
} else {
logger.once.error(
{ allowedHeaders, header },
'Disallowed hostRules headers',
);
}
}
options.headers = {
...options.headers,
...filteredHeaders,
};
}
if (hostRule.keepAlive) {
options.agent = keepAliveAgents;
}
if (!hasProxy() && hostRule.enableHttp2 === true) {
options.http2 = true;
}
if (isNonEmptyString(hostRule.httpsCertificateAuthority)) {
options.https = {
...options.https,
certificateAuthority: hostRule.httpsCertificateAuthority,
};
}
if (isNonEmptyString(hostRule.httpsPrivateKey)) {
options.https = {
...options.https,
key: hostRule.httpsPrivateKey,
};
}
if (isNonEmptyString(hostRule.httpsCertificate)) {
options.https = {
...options.https,
certificate: hostRule.httpsCertificate,
};
}
return options;
}