Skip to content

Commit

Permalink
fix: allow trailing dots in DNS targets (#262)
Browse files Browse the repository at this point in the history
  • Loading branch information
rdubrock committed Apr 26, 2021
1 parent 16743b8 commit 7eb3847
Show file tree
Hide file tree
Showing 2 changed files with 161 additions and 41 deletions.
110 changes: 70 additions & 40 deletions src/validation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ describe('trivial cases', () => {
});
});

describe('bad targets', () => {
describe('http', () => {
it('should reject non-http URLs', () => {
const testcases: string[] = ['ftp://example.org/', 'schema://example.org/'];
testcases.forEach((testcase: string) => {
Expand All @@ -116,57 +116,18 @@ describe('bad targets', () => {
expect(CheckValidation.target(CheckType.HTTP, 'https://hostname/')).toEqual('Target must have a valid hostname');
expect(CheckValidation.target(CheckType.HTTP, 'https://suraj/dev')).toEqual('Target must have a valid hostname');
});

it('should reject URLs without schema', () => {
const testcases: string[] = ['example.org'];
testcases.forEach((testcase: string) => {
expect(CheckValidation.target(CheckType.HTTP, testcase)).toEqual('Target must be a valid web URL');
});
});

it('should reject ping and dns targets without domains', () => {
const testcases: string[] = ['grafana'];
testcases.forEach((testcase: string) => {
expect(CheckValidation.target(CheckType.PING, testcase)).toBe('Target must be a valid hostname');
expect(CheckValidation.target(CheckType.DNS, testcase)).toBe('Target must be a valid hostname');
});
});

it('should reject malformed ipv6 https targets', () => {
const url = 'https://[2001:0db8:1001:1001:1001:1001:1001:1001/';
expect(CheckValidation.target(CheckType.HTTP, url)).toBe('Target must be a valid web URL');
});

it('should reject ping targets with invalid hostnames', () => {
const testcases: string[] = ['x.', '.y', 'x=y.org'];
testcases.forEach((testcase: string) => {
expect(CheckValidation.target(CheckType.PING, testcase)).toBe('Target must be a valid hostname');
expect(CheckValidation.target(CheckType.DNS, testcase)).toBe('Target must be a valid hostname');
});
});

it('should reject tcp targets without valid ports', () => {
expect(CheckValidation.target(CheckType.TCP, 'x:y')).toBe('Must be a valid host:port combination');
expect(CheckValidation.target(CheckType.TCP, 'x:y:')).toBe('Must be a valid host:port combination');
expect(CheckValidation.target(CheckType.TCP, 'x:y:0')).toBe('Must be a valid host:port combination');
expect(CheckValidation.target(CheckType.TCP, 'x:y:65536')).toBe('Must be a valid host:port combination');
expect(CheckValidation.target(CheckType.TCP, 'grafana.com:65536')).toBe('Port must be less than 65535');
expect(CheckValidation.target(CheckType.TCP, 'grafana.com:0')).toBe('Port must be greater than 0');
});

it('should reject invalid certificates', () => {
const invalidCert = 'not a legit cert';
expect(validateTLSCACert(invalidCert)).toBe('Certificate must be in the PEM format.');
expect(validateTLSClientCert(invalidCert)).toBe('Certificate must be in the PEM format.');
});

it('should reject invalid tls keys', () => {
const invalidKey = 'not a legit cert';
expect(validateTLSClientKey(invalidKey)).toBe('Key must be in the PEM format.');
});
});

describe('good targets', () => {
it('should accept http schema as HTTP target', () => {
const testcases: string[] = ['http://grafana.com/'];
testcases.forEach((testcase: string) => {
Expand Down Expand Up @@ -210,6 +171,18 @@ describe('good targets', () => {
expect(CheckValidation.target(CheckType.HTTP, testcase)).toBe(undefined);
});
});
});

describe('PING', () => {
it('should reject hostnames without domains', () => {
expect(CheckValidation.target(CheckType.PING, 'grafana')).toBe('Target must be a valid hostname');
});
it('should reject ping targets with invalid hostnames', () => {
const testcases: string[] = ['x.', '.y', 'x=y.org'];
testcases.forEach((testcase: string) => {
expect(CheckValidation.target(CheckType.PING, testcase)).toBe('Target must be a valid hostname');
});
});

it('should accept IPv4 as ping target', () => {
const testcases: string[] = [
Expand Down Expand Up @@ -240,6 +213,51 @@ describe('good targets', () => {
expect(CheckValidation.target(CheckType.PING, testcase)).toBe(undefined);
});
});
});

describe('DNS', () => {
it('should reject single element domains', () => {
expect(CheckValidation.target(CheckType.DNS, 'grafana')).toBe('Invalid number of elements in hostname');
});

it('should reject dns targets with invalid element length', () => {
expect(CheckValidation.target(CheckType.DNS, '.y')).toBe(
'Invalid domain element length. Each element must be between 1 and 62 characters'
);
});

it('should reject dns targets with invalid characters', () => {
expect(CheckValidation.target(CheckType.DNS, 'x=y.org')).toBe(
'Invalid character in domain name. Only letters, numbers and "-" are allowed'
);
});

it('should reject ip address', () => {
expect(CheckValidation.target(CheckType.DNS, '127.0.0.1')).toBe('IP addresses are not valid DNS targets');
});

it('IP address disguised as multi-label fully qualified dns name is invalid', () => {
expect(CheckValidation.target(CheckType.DNS, '127.0.0.1.')).toBe('A domain TLD cannot contain only numbers');
});

it('should accept dns targets with trailing .', () => {
expect(CheckValidation.target(CheckType.DNS, 'grafana.')).toBe(undefined);
});

it('should accept valid hostnames', () => {
expect(CheckValidation.target(CheckType.DNS, 'grafana.com')).toBe(undefined);
});
});

describe('tcp', () => {
it('should reject tcp targets without valid ports', () => {
expect(CheckValidation.target(CheckType.TCP, 'x:y')).toBe('Must be a valid host:port combination');
expect(CheckValidation.target(CheckType.TCP, 'x:y:')).toBe('Must be a valid host:port combination');
expect(CheckValidation.target(CheckType.TCP, 'x:y:0')).toBe('Must be a valid host:port combination');
expect(CheckValidation.target(CheckType.TCP, 'x:y:65536')).toBe('Must be a valid host:port combination');
expect(CheckValidation.target(CheckType.TCP, 'grafana.com:65536')).toBe('Port must be less than 65535');
expect(CheckValidation.target(CheckType.TCP, 'grafana.com:0')).toBe('Port must be greater than 0');
});

it('should accept tcp targets with host:port', () => {
const testcases: string[] = ['x.y:25', '1.2.3.4:25', '[2001:0db8:1001:1001:1001:1001:1001:1001]:8080'];
Expand All @@ -249,6 +267,18 @@ describe('good targets', () => {
});
});

describe('certificates', () => {
it('should reject invalid certificates', () => {
const invalidCert = 'not a legit cert';
expect(validateTLSCACert(invalidCert)).toBe('Certificate must be in the PEM format.');
expect(validateTLSClientCert(invalidCert)).toBe('Certificate must be in the PEM format.');
});
it('should reject invalid tls keys', () => {
const invalidKey = 'not a legit cert';
expect(validateTLSClientKey(invalidKey)).toBe('Key must be in the PEM format.');
});
});

describe('labels', () => {
it('rejects duplicate label names', () => {
const error = validateLabelName('a_name', [
Expand Down
92 changes: 91 additions & 1 deletion src/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export function validateTarget(typeOfCheck: CheckType, target: string): string |
return validateHostname(target);
}
case CheckType.DNS: {
return validateHostname(target);
return validateDomain(target);
}
case CheckType.TCP: {
return validateHostPort(target);
Expand Down Expand Up @@ -283,6 +283,96 @@ function validateHttpTarget(target: string): string | undefined {
}
}

function validateDomain(target: string): string | undefined {
const ipv4 = new Address4(target);
const ipv6 = new Address6(target);

if (ipv4.isValid() || ipv6.isValid()) {
return 'IP addresses are not valid DNS targets';
}

if (target.length === 0 || target.length > 255) {
return 'Hostname must be between 0 and 255 characters';
}

const rawElements = target.split('.');

if (rawElements.length < 2) {
return 'Invalid number of elements in hostname';
}

const filteredElements = rawElements.filter((element, index) => {
const isLast = index === rawElements.length - 1;
if (isLast && element === '') {
return false;
}
return true;
});

const errors = filteredElements
.map((element, index) => {
const isLast = index === filteredElements.length - 1;
const error = validateDomainElement(element, isLast);
if (error) {
return error;
}
return undefined;
})
.filter(Boolean);

return errors[0] ?? undefined;
}

function isCharacterNumber(character: string): boolean {
const numberRegex = new RegExp(/[0-9]/);
return Boolean(character.match(numberRegex)?.length);
}

function isCharacterLetter(character: string): boolean {
const letterRegex = new RegExp(/[a-zA-Z]/);
return Boolean(character.match(letterRegex)?.length);
}

function isValidDomainCharacter(character: string): boolean {
const regex = new RegExp(/[-A-Za-z0-9\.]/);
return Boolean(!character.match(regex)?.length);
}

function validateDomainElement(element: string, isLast: boolean): string | undefined {
if ((!isLast && element.length === 0) || element.length > 63) {
return 'Invalid domain element length. Each element must be between 1 and 62 characters';
}

// This is to allow trailing '.' characters in dns records
if (isLast && element.length === 0) {
return undefined;
}

const first = element[0];
const last = element[element.length - 1];
if (!isCharacterLetter(first) && !isCharacterNumber(first)) {
return 'A domain element must begin with a letter or number';
}

if (!isCharacterNumber(last) && !isCharacterLetter(last)) {
return 'A domain element must end with a letter or number';
}

if (isLast) {
const hasNonNumbers = element.split('').some((character) => !isCharacterNumber(character));
if (!hasNonNumbers) {
return 'A domain TLD cannot contain only numbers';
}
}

const hasInvalidCharacter = element.split('').some((character) => isValidDomainCharacter(character));
if (hasInvalidCharacter) {
return 'Invalid character in domain name. Only letters, numbers and "-" are allowed';
}

return undefined;
}

function validateHostname(target: string): string | undefined {
const ipv4 = new Address4(target);
const ipv6 = new Address6(target);
Expand Down

0 comments on commit 7eb3847

Please sign in to comment.