From b8b760768d9538c224633c0d828183e9e9855b59 Mon Sep 17 00:00:00 2001 From: RahulGautamSingh Date: Fri, 28 Jun 2024 12:50:00 +0530 Subject: [PATCH] feat(hostRules/matchHost): massage and validate (#29487) Co-authored-by: Rhys Arkins --- .../custom/host-rules-migration.spec.ts | 2 + .../migrations/custom/host-rules-migration.ts | 13 ++---- lib/config/validation.spec.ts | 42 +++++++++++++++++++ lib/config/validation.ts | 18 ++++++++ lib/util/host-rules.spec.ts | 33 ++++++++++----- lib/util/host-rules.ts | 3 +- lib/util/url.spec.ts | 10 +++++ lib/util/url.ts | 13 ++++++ 8 files changed, 112 insertions(+), 22 deletions(-) diff --git a/lib/config/migrations/custom/host-rules-migration.spec.ts b/lib/config/migrations/custom/host-rules-migration.spec.ts index fdc77fa14086cd..67c1331db2f487 100644 --- a/lib/config/migrations/custom/host-rules-migration.spec.ts +++ b/lib/config/migrations/custom/host-rules-migration.spec.ts @@ -27,6 +27,7 @@ describe('config/migrations/custom/host-rules-migration', () => { { hostName: 'some.domain.com', token: '123test' }, { endpoint: 'domain.com/', token: '123test' }, { host: 'some.domain.com', token: '123test' }, + { matchHost: 'some.domain.com:8080', token: '123test' }, ], } as any, { @@ -58,6 +59,7 @@ describe('config/migrations/custom/host-rules-migration', () => { { matchHost: 'some.domain.com', token: '123test' }, { matchHost: 'https://domain.com/', token: '123test' }, { matchHost: 'some.domain.com', token: '123test' }, + { matchHost: 'https://some.domain.com:8080', token: '123test' }, ], }, ); diff --git a/lib/config/migrations/custom/host-rules-migration.ts b/lib/config/migrations/custom/host-rules-migration.ts index b9d1d5af86b49f..838cf93d48f175 100644 --- a/lib/config/migrations/custom/host-rules-migration.ts +++ b/lib/config/migrations/custom/host-rules-migration.ts @@ -3,6 +3,7 @@ import { CONFIG_VALIDATION } from '../../../constants/error-messages'; import { logger } from '../../../logger'; import type { HostRule } from '../../../types'; import type { LegacyHostRule } from '../../../util/host-rules'; +import { massageHostUrl } from '../../../util/url'; import { AbstractMigration } from '../base/abstract-migration'; import { migrateDatasource } from './datasource-migration'; @@ -25,7 +26,7 @@ export class HostRulesMigration extends AbstractMigration { if (key === 'matchHost') { if (is.string(value)) { - newRule.matchHost ??= massageUrl(value); + newRule.matchHost ??= massageHostUrl(value); } continue; } @@ -45,7 +46,7 @@ export class HostRulesMigration extends AbstractMigration { key === 'domainName' ) { if (is.string(value)) { - newRule.matchHost ??= massageUrl(value); + newRule.matchHost ??= massageHostUrl(value); } continue; } @@ -91,14 +92,6 @@ function validateHostRule(rule: LegacyHostRule & HostRule): void { } } -function massageUrl(url: string): string { - if (!url.includes('://') && url.includes('/')) { - return 'https://' + url; - } else { - return url; - } -} - function removeUndefinedFields( obj: Record, ): Record { diff --git a/lib/config/validation.spec.ts b/lib/config/validation.spec.ts index ee8e4d5a5fa040..976c86b8c80f40 100644 --- a/lib/config/validation.spec.ts +++ b/lib/config/validation.spec.ts @@ -1134,6 +1134,48 @@ describe('config/validation', () => { ]); }); + it('errors if invalid matchHost values in hostRules', async () => { + GlobalConfig.set({ allowedHeaders: ['X-*'] }); + + const config = { + hostRules: [ + { + matchHost: '://', + token: 'token', + }, + { + matchHost: '', + token: 'token', + }, + { + matchHost: undefined, + token: 'token', + }, + { + hostType: 'github', + token: 'token', + }, + ], + }; + const { errors } = await configValidation.validateConfig('repo', config); + expect(errors).toMatchObject([ + { + topic: 'Configuration Error', + message: + 'Configuration option `hostRules[2].matchHost` should be a string', + }, + { + topic: 'Configuration Error', + message: + 'Invalid value for hostRules matchHost. It cannot be an empty string.', + }, + { + topic: 'Configuration Error', + message: 'hostRules matchHost `://` is not a valid URL.', + }, + ]); + }); + it('errors if forbidden header in hostRules', async () => { GlobalConfig.set({ allowedHeaders: ['X-*'] }); diff --git a/lib/config/validation.ts b/lib/config/validation.ts index d21e9812ddbedd..cec5ce191845fd 100644 --- a/lib/config/validation.ts +++ b/lib/config/validation.ts @@ -15,6 +15,7 @@ import { matchRegexOrGlobList, } from '../util/string-match'; import * as template from '../util/template'; +import { parseUrl } from '../util/url'; import { hasValidSchedule, hasValidTimezone, @@ -813,6 +814,23 @@ export async function validateConfig( ? (config.allowedHeaders as string[]) ?? [] : GlobalConfig.get('allowedHeaders', []); for (const rule of val as HostRule[]) { + if (is.nonEmptyString(rule.matchHost)) { + if (rule.matchHost.includes('://')) { + if (parseUrl(rule.matchHost) === null) { + errors.push({ + topic: 'Configuration Error', + message: `hostRules matchHost \`${rule.matchHost}\` is not a valid URL.`, + }); + } + } + } else if (is.emptyString(rule.matchHost)) { + errors.push({ + topic: 'Configuration Error', + message: + 'Invalid value for hostRules matchHost. It cannot be an empty string.', + }); + } + if (!rule.headers) { continue; } diff --git a/lib/util/host-rules.spec.ts b/lib/util/host-rules.spec.ts index f5fa0cbf239a53..9caa4a3657051a 100644 --- a/lib/util/host-rules.spec.ts +++ b/lib/util/host-rules.spec.ts @@ -56,6 +56,27 @@ describe('util/host-rules', () => { username: 'user1', }); }); + + it('massages host url', () => { + add({ + matchHost: 'some.domain.com:8080', + username: 'user1', + password: 'pass1', + }); + add({ + matchHost: 'domain.com/', + username: 'user2', + password: 'pass2', + }); + expect(find({ url: 'https://some.domain.com:8080' })).toEqual({ + password: 'pass1', + username: 'user1', + }); + expect(find({ url: 'https://domain.com/' })).toEqual({ + password: 'pass2', + username: 'user2', + }); + }); }); describe('find()', () => { @@ -125,7 +146,7 @@ describe('util/host-rules', () => { }); it('matches on specific path', () => { - // Initialized platform holst rule + // Initialized platform host rule add({ hostType: 'github', matchHost: 'https://api.github.com', @@ -244,16 +265,6 @@ describe('util/host-rules', () => { expect(find({ url: 'httpsdomain.com' }).token).toBeUndefined(); }); - it('host with port is interpreted as empty', () => { - add({ - matchHost: 'domain.com:9118', - token: 'def', - }); - expect(find({ url: 'https://domain.com:9118' }).token).toBe('def'); - expect(find({ url: 'https://domain.com' }).token).toBe('def'); - expect(find({ url: 'httpsdomain.com' }).token).toBe('def'); - }); - it('matches on hostType and endpoint', () => { add({ hostType: NugetDatasource.id, diff --git a/lib/util/host-rules.ts b/lib/util/host-rules.ts index 1da59fcd9e3c49..55d5c4d85a7e87 100644 --- a/lib/util/host-rules.ts +++ b/lib/util/host-rules.ts @@ -4,7 +4,7 @@ import type { CombinedHostRule, HostRule } from '../types'; import { clone } from './clone'; import * as sanitize from './sanitize'; import { toBase64 } from './string'; -import { isHttpUrl, parseUrl } from './url'; +import { isHttpUrl, massageHostUrl, parseUrl } from './url'; let hostRules: HostRule[] = []; @@ -43,6 +43,7 @@ export function add(params: HostRule): void { const confidentialFields: (keyof HostRule)[] = ['password', 'token']; if (rule.matchHost) { + rule.matchHost = massageHostUrl(rule.matchHost); const parsedUrl = parseUrl(rule.matchHost); rule.resolvedHost = parsedUrl?.hostname ?? rule.matchHost; confidentialFields.forEach((field) => { diff --git a/lib/util/url.spec.ts b/lib/util/url.spec.ts index a0c65cb7ae27b1..0ed4143cb69c55 100644 --- a/lib/util/url.spec.ts +++ b/lib/util/url.spec.ts @@ -5,6 +5,7 @@ import { getQueryString, isHttpUrl, joinUrlParts, + massageHostUrl, parseLinkHeader, parseUrl, replaceUrlPath, @@ -214,4 +215,13 @@ describe('util/url', () => { }, }); }); + + it('massageHostUrl', () => { + expect(massageHostUrl('domain.com')).toBe('domain.com'); + expect(massageHostUrl('domain.com:8080')).toBe('https://domain.com:8080'); + expect(massageHostUrl('domain.com/some/path')).toBe( + 'https://domain.com/some/path', + ); + expect(massageHostUrl('https://domain.com')).toBe('https://domain.com'); + }); }); diff --git a/lib/util/url.ts b/lib/util/url.ts index 9ac957233127ff..26853a013e061b 100644 --- a/lib/util/url.ts +++ b/lib/util/url.ts @@ -132,3 +132,16 @@ export function parseLinkHeader( } return _parseLinkHeader(linkHeader); } + +/** + * prefix https:// to hosts with port or path + */ +export function massageHostUrl(url: string): string { + if (!url.includes('://') && url.includes('/')) { + return 'https://' + url; + } else if (!url.includes('://') && url.includes(':')) { + return 'https://' + url; + } else { + return url; + } +}