diff --git a/API.md b/API.md index 3f2f2427..858aa480 100755 --- a/API.md +++ b/API.md @@ -3068,7 +3068,7 @@ Requires the string value to be a valid [RFC 3986](http://tools.ietf.org/html/rf - `allowRelative` - Allow relative URIs. Defaults to `false`. - `relativeOnly` - Restrict only relative URIs. Defaults to `false`. - `allowQuerySquareBrackets` - Allows unencoded square brackets inside the query string. This is **NOT** RFC 3986 compliant but query strings like `abc[]=123&abc[]=456` are very common these days. Defaults to `false`. - - `domain` - Validate the domain component using the options specified in [`string.domain()`](#stringdomainoptions). + - `domain` - Validate the domain component using the options specified in [`string.domain()`](#stringdomainoptions). An extra option `hostname` can be added to validate specific hostnames. It supports a string, a regular expression or an array containing multiples of them. ```js // Accept git or git http/https @@ -3080,7 +3080,7 @@ const schema = Joi.string().uri({ }); ``` -Possible validation errors: [`string.uri`](#stringuri), [`string.uriCustomScheme`](#stringuricustomscheme), [`string.uriRelativeOnly`](#stringurirelativeonly), [`string.domain`](#stringdomain) +Possible validation errors: [`string.uri`](#stringuri), [`string.uriCustomScheme`](#stringuricustomscheme), [`string.uriRelativeOnly`](#stringurirelativeonly), [`string.domain`](#stringdomain), [`string.uri.hostname`](#stringurihostname) ### `symbol` @@ -4395,6 +4395,10 @@ The string isn't all upper-cased. The string isn't a valid URI. +#### `string.uri.hostname` + +The hostname isn't a match for any of the given hostname strings or regular expressions. + #### `string.uriCustomScheme` The string isn't a valid URI considering the custom schemes. diff --git a/lib/types/string.js b/lib/types/string.js index cdaffc40..49103d85 100755 --- a/lib/types/string.js +++ b/lib/types/string.js @@ -639,7 +639,7 @@ module.exports = Any.extend({ Common.assertOptions(options, ['allowRelative', 'allowQuerySquareBrackets', 'domain', 'relativeOnly', 'scheme']); if (options.domain) { - Common.assertOptions(options.domain, ['allowFullyQualified', 'allowUnicode', 'maxDomainSegments', 'minDomainSegments', 'tlds']); + Common.assertOptions(options.domain, ['allowFullyQualified', 'allowUnicode', 'maxDomainSegments', 'minDomainSegments', 'tlds', 'hostname']); } const { regex, scheme } = Uri.regex(options); @@ -655,11 +655,40 @@ module.exports = Any.extend({ const match = regex.exec(value); if (match) { const matched = match[1] || match[2]; - if (domain && - (!options.allowRelative || matched) && - !Domain.isValid(matched, domain)) { - return helpers.error('string.domain', { value: matched }); + if (domain) { + + if ((!options.allowRelative || matched) && + !Domain.isValid(matched, domain)) { + return helpers.error('string.domain', { value: matched }); + } + + if (domain.hostname) { + + const isMatch = (matcher) => { + + if (matcher instanceof RegExp) { + return matcher.test(matched); + } + + return matcher === matched; + + }; + + if (Array.isArray(domain.hostname)) { + + if (!domain.hostname.some(isMatch)) { + return helpers.error('string.uri.hostname', { value: matched, expected: 'any of the hostname items' }); + } + } + else { + + if (!isMatch(domain.hostname)) { + return helpers.error('string.uri.hostname', { value: matched, expected: domain.hostname }); + } + + } + } } return value; @@ -721,6 +750,7 @@ module.exports = Any.extend({ 'string.pattern.invert.name': '{{#label}} with value {:[.]} matches the inverted {{#name}} pattern', 'string.trim': '{{#label}} must not have leading or trailing whitespace', 'string.uri': '{{#label}} must be a valid uri', + 'string.uri.hostname': '{{#label}} does not match {{#expected}}', 'string.uriCustomScheme': '{{#label}} must be a valid uri with a scheme matching the {{#scheme}} pattern', 'string.uriRelativeOnly': '{{#label}} must be a valid relative uri', 'string.uppercase': '{{#label}} must only contain uppercase characters' diff --git a/test/types/string.js b/test/types/string.js index fdc34744..c3dc8130 100755 --- a/test/types/string.js +++ b/test/types/string.js @@ -8606,6 +8606,32 @@ describe('string', () => { expect(() => Joi.string().uri({ foo: 'bar', baz: 'qux' })).to.throw('Options contain unknown keys: foo,baz'); }); + + it('validates domain.hostname', () => { + + const stringSchema = Joi.string().uri({ scheme: 'https', domain: { hostname: 'example.com' } }); + Helper.validate(stringSchema, [ + ['https://example.com', true], + ['https://example.com/test', true], + ['https://test.com', false, '"value" does not match example.com'], + ['https://test.com/example', false, '"value" does not match example.com'] + ]); + + const regexSchema = Joi.string().uri({ scheme: 'https', domain: { hostname: /example.com/ } }); + Helper.validate(regexSchema, [ + ['https://example.com', true], + ['https://dummy.com', false, '"value" does not match /example.com/'] + ]); + + const arraySchema = Joi.string().uri({ scheme: 'https', domain: { hostname: ['example.com', 'dummy.org', /dummy.com/] } }); + Helper.validate(arraySchema, [ + ['https://example.com/test', true], + ['https://test.com/example', false, '"value" does not match any of the hostname items'], + ['https://dummy.org/test', true], + ['https://dummy.com/test', true] + ]); + + }); }); describe('valid()', () => {