2021-11-28 15:31:11 +00:00
|
|
|
import is from '@sindresorhus/is';
|
2019-05-24 15:40:39 +00:00
|
|
|
import merge from 'deepmerge';
|
2019-07-15 09:04:05 +00:00
|
|
|
import { logger } from '../logger';
|
2022-04-06 14:56:40 +00:00
|
|
|
import type { HostRule, HostRuleSearchResult } from '../types';
|
2020-05-01 16:03:48 +00:00
|
|
|
import * as sanitize from './sanitize';
|
2022-02-26 09:16:54 +00:00
|
|
|
import { toBase64 } from './string';
|
2021-05-06 07:57:44 +00:00
|
|
|
import { parseUrl, validateUrl } from './url';
|
2019-05-24 15:40:39 +00:00
|
|
|
|
|
|
|
let hostRules: HostRule[] = [];
|
|
|
|
|
2021-11-28 15:31:11 +00:00
|
|
|
interface LegacyHostRule {
|
|
|
|
hostName?: string;
|
|
|
|
domainName?: string;
|
|
|
|
baseUrl?: string;
|
|
|
|
}
|
2021-05-01 21:18:14 +00:00
|
|
|
|
2021-11-28 15:31:11 +00:00
|
|
|
function migrateRule(rule: LegacyHostRule & HostRule): HostRule {
|
2023-04-27 05:06:22 +00:00
|
|
|
const cloned: LegacyHostRule & HostRule = structuredClone(rule);
|
2021-11-28 15:31:11 +00:00
|
|
|
delete cloned.hostName;
|
|
|
|
delete cloned.domainName;
|
|
|
|
delete cloned.baseUrl;
|
|
|
|
const result: HostRule = cloned;
|
|
|
|
|
|
|
|
const { matchHost } = result;
|
|
|
|
const { hostName, domainName, baseUrl } = rule;
|
|
|
|
const hostValues = [matchHost, hostName, domainName, baseUrl].filter(Boolean);
|
|
|
|
if (hostValues.length === 1) {
|
|
|
|
const [matchHost] = hostValues;
|
|
|
|
result.matchHost = matchHost;
|
|
|
|
} else if (hostValues.length > 1) {
|
|
|
|
throw new Error(
|
|
|
|
`hostRules cannot contain more than one host-matching field - use "matchHost" only.`
|
|
|
|
);
|
2018-07-06 05:26:36 +00:00
|
|
|
}
|
2021-05-13 20:53:18 +00:00
|
|
|
|
2021-11-28 15:31:11 +00:00
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
export function add(params: HostRule): void {
|
|
|
|
const rule = migrateRule(params);
|
|
|
|
|
|
|
|
const confidentialFields: (keyof HostRule)[] = ['password', 'token'];
|
2021-05-13 20:53:18 +00:00
|
|
|
if (rule.matchHost) {
|
|
|
|
const parsedUrl = parseUrl(rule.matchHost);
|
2021-11-28 15:31:11 +00:00
|
|
|
rule.resolvedHost = parsedUrl?.hostname ?? rule.matchHost;
|
2021-04-20 08:52:38 +00:00
|
|
|
confidentialFields.forEach((field) => {
|
2021-05-13 20:53:18 +00:00
|
|
|
if (rule[field]) {
|
2021-04-20 08:52:38 +00:00
|
|
|
logger.debug(
|
2022-07-26 08:32:12 +00:00
|
|
|
// TODO: types (#7154)
|
|
|
|
`Adding ${field} authentication for ${rule.matchHost!} to hostRules`
|
2021-04-20 08:52:38 +00:00
|
|
|
);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
2020-04-12 16:09:36 +00:00
|
|
|
confidentialFields.forEach((field) => {
|
2021-05-13 20:53:18 +00:00
|
|
|
const secret = rule[field];
|
2021-11-28 15:31:11 +00:00
|
|
|
if (is.string(secret) && secret.length > 3) {
|
2022-01-26 09:57:21 +00:00
|
|
|
sanitize.addSecretForSanitizing(secret);
|
2020-03-17 11:15:22 +00:00
|
|
|
}
|
2019-09-07 12:51:00 +00:00
|
|
|
});
|
2021-05-13 20:53:18 +00:00
|
|
|
if (rule.username && rule.password) {
|
2022-02-26 09:16:54 +00:00
|
|
|
sanitize.addSecretForSanitizing(
|
|
|
|
toBase64(`${rule.username}:${rule.password}`)
|
2021-05-13 20:53:18 +00:00
|
|
|
);
|
2019-09-07 12:51:00 +00:00
|
|
|
}
|
2021-05-13 20:53:18 +00:00
|
|
|
hostRules.push(rule);
|
2019-05-24 15:40:39 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
export interface HostRuleSearch {
|
|
|
|
hostType?: string;
|
2019-07-17 08:14:56 +00:00
|
|
|
url?: string;
|
2019-05-24 15:40:39 +00:00
|
|
|
}
|
|
|
|
|
2019-11-24 07:43:24 +00:00
|
|
|
function isEmptyRule(rule: HostRule): boolean {
|
2021-05-01 21:30:24 +00:00
|
|
|
return !rule.hostType && !rule.resolvedHost;
|
2019-05-24 15:40:39 +00:00
|
|
|
}
|
|
|
|
|
2019-11-24 07:43:24 +00:00
|
|
|
function isHostTypeRule(rule: HostRule): boolean {
|
2021-11-28 15:31:11 +00:00
|
|
|
return !!rule.hostType && !rule.resolvedHost;
|
2019-05-24 15:40:39 +00:00
|
|
|
}
|
|
|
|
|
2021-05-06 07:57:44 +00:00
|
|
|
function isHostOnlyRule(rule: HostRule): boolean {
|
|
|
|
return !rule.hostType && !!rule.matchHost;
|
|
|
|
}
|
|
|
|
|
2019-11-24 07:43:24 +00:00
|
|
|
function isMultiRule(rule: HostRule): boolean {
|
2021-11-28 15:31:11 +00:00
|
|
|
return !!rule.hostType && !!rule.resolvedHost;
|
2019-05-24 15:40:39 +00:00
|
|
|
}
|
|
|
|
|
2019-11-24 07:43:24 +00:00
|
|
|
function matchesHostType(rule: HostRule, search: HostRuleSearch): boolean {
|
2019-05-24 15:40:39 +00:00
|
|
|
return rule.hostType === search.hostType;
|
|
|
|
}
|
|
|
|
|
2021-05-06 07:57:44 +00:00
|
|
|
function matchesHost(rule: HostRule, search: HostRuleSearch): boolean {
|
2021-11-28 15:31:11 +00:00
|
|
|
// istanbul ignore if
|
|
|
|
if (!rule.matchHost) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
if (search.url && validateUrl(rule.matchHost)) {
|
2021-05-06 07:57:44 +00:00
|
|
|
return search.url.startsWith(rule.matchHost);
|
|
|
|
}
|
2021-11-28 15:31:11 +00:00
|
|
|
const parsedUrl = search.url ? parseUrl(search.url) : null;
|
2021-05-06 07:57:44 +00:00
|
|
|
if (!parsedUrl?.hostname) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
const { hostname } = parsedUrl;
|
2021-09-02 17:03:51 +00:00
|
|
|
const dotPrefixedMatchHost = rule.matchHost.startsWith('.')
|
|
|
|
? rule.matchHost
|
|
|
|
: `.${rule.matchHost}`;
|
|
|
|
return hostname === rule.matchHost || hostname.endsWith(dotPrefixedMatchHost);
|
2021-05-06 07:57:44 +00:00
|
|
|
}
|
|
|
|
|
2022-02-28 19:23:43 +00:00
|
|
|
function prioritizeLongestMatchHost(rule1: HostRule, rule2: HostRule): number {
|
|
|
|
// istanbul ignore if: won't happen in practice
|
|
|
|
if (!rule1.matchHost || !rule2.matchHost) {
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
return rule1.matchHost.length - rule2.matchHost.length;
|
|
|
|
}
|
|
|
|
|
2022-04-06 14:56:40 +00:00
|
|
|
export function find(search: HostRuleSearch): HostRuleSearchResult {
|
2019-05-24 15:40:39 +00:00
|
|
|
if (!(search.hostType || search.url)) {
|
|
|
|
logger.warn({ search }, 'Invalid hostRules search');
|
2019-05-25 05:49:26 +00:00
|
|
|
return {};
|
2018-09-13 04:48:08 +00:00
|
|
|
}
|
2021-05-17 08:06:24 +00:00
|
|
|
let res = {} as any as HostRule;
|
2019-05-24 15:40:39 +00:00
|
|
|
// First, apply empty rule matches
|
|
|
|
hostRules
|
2020-04-12 16:09:36 +00:00
|
|
|
.filter((rule) => isEmptyRule(rule))
|
|
|
|
.forEach((rule) => {
|
2019-05-24 15:40:39 +00:00
|
|
|
res = merge(res, rule);
|
|
|
|
});
|
|
|
|
// Next, find hostType-only matches
|
|
|
|
hostRules
|
2020-04-12 16:09:36 +00:00
|
|
|
.filter((rule) => isHostTypeRule(rule) && matchesHostType(rule, search))
|
|
|
|
.forEach((rule) => {
|
2019-05-24 15:40:39 +00:00
|
|
|
res = merge(res, rule);
|
|
|
|
});
|
2021-05-06 07:57:44 +00:00
|
|
|
hostRules
|
|
|
|
.filter((rule) => isHostOnlyRule(rule) && matchesHost(rule, search))
|
2022-02-28 19:23:43 +00:00
|
|
|
.sort(prioritizeLongestMatchHost)
|
2021-05-06 07:57:44 +00:00
|
|
|
.forEach((rule) => {
|
|
|
|
res = merge(res, rule);
|
|
|
|
});
|
2019-05-24 15:40:39 +00:00
|
|
|
// Finally, find combination matches
|
|
|
|
hostRules
|
|
|
|
.filter(
|
2020-04-12 16:09:36 +00:00
|
|
|
(rule) =>
|
2019-05-24 15:40:39 +00:00
|
|
|
isMultiRule(rule) &&
|
|
|
|
matchesHostType(rule, search) &&
|
2021-05-13 20:53:18 +00:00
|
|
|
matchesHost(rule, search)
|
2019-05-24 15:40:39 +00:00
|
|
|
)
|
2022-02-28 19:23:43 +00:00
|
|
|
.sort(prioritizeLongestMatchHost)
|
2020-04-12 16:09:36 +00:00
|
|
|
.forEach((rule) => {
|
2019-05-24 15:40:39 +00:00
|
|
|
res = merge(res, rule);
|
|
|
|
});
|
|
|
|
delete res.hostType;
|
2021-05-01 16:19:38 +00:00
|
|
|
delete res.resolvedHost;
|
2021-05-06 07:57:44 +00:00
|
|
|
delete res.matchHost;
|
2019-05-24 15:40:39 +00:00
|
|
|
return res;
|
2018-07-06 05:26:36 +00:00
|
|
|
}
|
|
|
|
|
2019-11-24 07:43:24 +00:00
|
|
|
export function hosts({ hostType }: { hostType: string }): string[] {
|
2022-02-06 16:30:53 +00:00
|
|
|
return hostRules
|
|
|
|
.filter((rule) => rule.hostType === hostType)
|
|
|
|
.map((rule) => rule.resolvedHost)
|
|
|
|
.filter(is.truthy);
|
2018-07-06 05:26:36 +00:00
|
|
|
}
|
|
|
|
|
2022-04-06 14:56:40 +00:00
|
|
|
export function hostType({ url }: { url: string }): string | null {
|
|
|
|
return (
|
|
|
|
hostRules
|
|
|
|
.filter((rule) => matchesHost(rule, { url }))
|
|
|
|
.sort(prioritizeLongestMatchHost)
|
|
|
|
.map((rule) => rule.hostType)
|
|
|
|
.filter(is.truthy)
|
|
|
|
.pop() ?? null
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-02-11 13:37:35 +00:00
|
|
|
export function findAll({ hostType }: { hostType: string }): HostRule[] {
|
2020-04-12 16:09:36 +00:00
|
|
|
return hostRules.filter((rule) => rule.hostType === hostType);
|
2020-02-11 13:37:35 +00:00
|
|
|
}
|
|
|
|
|
2021-10-31 07:06:59 +00:00
|
|
|
/**
|
|
|
|
* @returns a deep copy of all known host rules without any filtering
|
|
|
|
*/
|
|
|
|
export function getAll(): HostRule[] {
|
2023-04-27 05:06:22 +00:00
|
|
|
return structuredClone(hostRules);
|
2021-10-31 07:06:59 +00:00
|
|
|
}
|
|
|
|
|
2019-11-24 07:43:24 +00:00
|
|
|
export function clear(): void {
|
2021-04-20 08:52:38 +00:00
|
|
|
logger.debug('Clearing hostRules');
|
2019-05-24 15:40:39 +00:00
|
|
|
hostRules = [];
|
2022-01-26 09:57:21 +00:00
|
|
|
sanitize.clearSanitizedSecretsList();
|
2018-07-06 05:26:36 +00:00
|
|
|
}
|