diff --git a/API.md b/API.md index b8b7819..c735016 100755 --- a/API.md +++ b/API.md @@ -7,7 +7,8 @@ Analyzes a string to verify it is a valid domain name where: - `domain` - the domain name string being verified. - `options` - optional settings: - `allowUnicode` - if `false`, Unicode characters are not allowed in domain names. Defaults to `true`. - - `allowUnderscore` - if `false`, underscore (`_`) characters will not be allowed in the domain name. Defaults to `false`. + - `allowUnderscore` - if `true`, underscore (`_`) characters are allowed in the domain name. Defaults to `false`. + - `allowForwardSlash` - if `true`, forward slash (`/`) characters are allowed in the domain name. Defaults to `false`. - `minDomainSegments` - the minimum number of domain segments (e.g. `x.y.z` has 3 segments) required. Defaults to `2`. - `tlds` - options to validate the top-level-domain segment (e.g. `com` in `example.com`) where: - `deny` - a `Set` with strings matching forbidden TLD values (all non-matching values are allowed). @@ -31,7 +32,8 @@ Analyzes a string to verify it is a valid email address where: - `email` - the email address string being verified. - `options` - optional settings: - `allowUnicode` - if `false`, Unicode characters are not allowed in the email address local and domain parts. Defaults to `true`. - - `allowUnderscore` - if `false`, underscore (`_`) characters will not be allowed in the domain name. Defaults to `false`. + - `allowUnderscore` - if `true`, underscore (`_`) characters are allowed in the domain name. Defaults to `false`. + - `allowForwardSlash` - if `true`, forward slash (`/`) characters are allowed in the domain name. Defaults to `false`. - `ignoreLength` - if `true`, the standards email maximum length limit is ignored. Defaults to `true`. - `minDomainSegments` - the minimum number of domain segments (e.g. `x.y.z` has 3 segments) required in the domain part. Defaults to `2`. - `tlds` - options to validate the top-level-domain segment (e.g. `com` in `example.com`) where: diff --git a/src/domain.ts b/src/domain.ts index 2529fe9..8d528a4 100755 --- a/src/domain.ts +++ b/src/domain.ts @@ -5,9 +5,12 @@ import { errorCode } from './errors'; const MIN_DOMAIN_SEGMENTS = 2; const NON_ASCII_RX = /[^\x00-\x7f]/; const DOMAIN_CONTROL_RX = /[\x00-\x20@\:\/\\#!\$&\'\(\)\*\+,;=\?]/; // Control + space + separators +const DOMAIN_CONTROL_NO_FORWARD_SLASH_RX = /[\x00-\x20@\:\\#!\$&\'\(\)\*\+,;=\?]/; // Control + space + separators without / const TLD_SEGMENT_RX = /^[a-zA-Z](?:[a-zA-Z0-9\-]*[a-zA-Z0-9])?$/; const DOMAIN_SEGMENT_RX = /^[a-zA-Z0-9](?:[a-zA-Z0-9\-]*[a-zA-Z0-9])?$/; const DOMAIN_UNDERSCORE_SEGMENT_RX = /^[a-zA-Z0-9_](?:[a-zA-Z0-9\-]*[a-zA-Z0-9])?$/; +const DOMAIN_FORWARD_SLASH_SEGMENT_RX = /^[a-zA-Z0-9](?:[a-zA-Z0-9\-\/]*[a-zA-Z0-9])?$/; +const DOMAIN_UNDERSCORE_FORWARD_SLASH_SEGMENT_RX = /^[a-zA-Z0-9_](?:[a-zA-Z0-9\-\/]*[a-zA-Z0-9])?$/; const URL_IMPL = Url.URL || URL; // $lab:coverage:ignore$ interface TldsAllow { @@ -37,6 +40,13 @@ export interface DomainOptions { */ readonly allowUnderscore?: boolean; + /** + * Determines whether forward slash (/) characters are allowed. + * + * @default false + */ + readonly allowForwardSlash?: boolean; + /** * The maximum number of domain segments (e.g. `x.y.z` has 3 segments) allowed. * @@ -110,11 +120,12 @@ export function analyzeDomain(domain: string, options: DomainOptions = {}): Anal domain = domain.normalize('NFC'); } - if (DOMAIN_CONTROL_RX.test(domain)) { + const controlRx = options.allowForwardSlash ? DOMAIN_CONTROL_NO_FORWARD_SLASH_RX : DOMAIN_CONTROL_RX; + if (controlRx.test(domain)) { return errorCode('DOMAIN_INVALID_CHARS'); } - domain = punycode(domain); + domain = punycode(domain, options.allowForwardSlash); // https://tools.ietf.org/html/rfc1035 section 2.3.1 @@ -160,11 +171,15 @@ export function analyzeDomain(domain: string, options: DomainOptions = {}): Anal if (i < segments.length - 1) { if (options.allowUnderscore) { - if (!DOMAIN_UNDERSCORE_SEGMENT_RX.test(segment)) { + const segmentRx = options.allowForwardSlash + ? DOMAIN_UNDERSCORE_FORWARD_SLASH_SEGMENT_RX + : DOMAIN_UNDERSCORE_SEGMENT_RX; + if (!segmentRx.test(segment)) { return errorCode('DOMAIN_INVALID_CHARS'); } } else { - if (!DOMAIN_SEGMENT_RX.test(segment)) { + const segmentRx = options.allowForwardSlash ? DOMAIN_FORWARD_SLASH_SEGMENT_RX : DOMAIN_SEGMENT_RX; + if (!segmentRx.test(segment)) { return errorCode('DOMAIN_INVALID_CHARS'); } } @@ -190,15 +205,31 @@ export function isDomainValid(domain: string, options?: DomainOptions) { return !analyzeDomain(domain, options); } -function punycode(domain: string) { - if (domain.includes('%')) { - domain = domain.replace(/%/g, '%25'); +function punycode(domain: string, allowForwardSlash?: boolean) { + if (allowForwardSlash) { + return domain + .split('.') + .map((segment) => + segment + .split('/') + .map((part) => punycodePart(part)) + .join('/') + ) + .join('.'); + } + + return punycodePart(domain); +} + +function punycodePart(part: string) { + if (part.includes('%')) { + part = part.replace(/%/g, '%25'); } try { - return new URL_IMPL(`http://${domain}`).host; + return new URL_IMPL(`http://${part}`).host; } catch (err) { - return domain; + return part; } } diff --git a/test/domain.ts b/test/domain.ts index 7bea23c..c8e9d05 100755 --- a/test/domain.ts +++ b/test/domain.ts @@ -89,6 +89,7 @@ describe('domain', () => { ['test:example.com', false], ['example.com:123', false], ['example.com/path', false], + ['128/26.2.0.192.in-addr.arpa', true, { allowForwardSlash: true }], ['x.example.com', false, { maxDomainSegments: 2 }], ['x.example.com', true, { maxDomainSegments: 3 }], ['example.com/', false], @@ -104,6 +105,7 @@ describe('domain', () => { ['example.com', true, { allowFullyQualified: true }], ['_acme-challenge.example.com', false], ['_acme-challenge.example.com', true, { allowUnderscore: true }], + ['_acme/challenge.example.com', true, { allowUnderscore: true, allowForwardSlash: true }], ['_acme-challenge.example.com', false, { allowUnderscore: false }], ['_abc.example.com', true, { allowUnderscore: true }], ['_abc.example.com', false], diff --git a/test/email.ts b/test/email.ts index f67b3e6..be8f39f 100755 --- a/test/email.ts +++ b/test/email.ts @@ -357,6 +357,7 @@ describe('email', () => { ], ['test@example.com@example.com', false], ['test@example.com/path', false], + ['test/path@example.com', true, { allowForwardSlash: true }], ['test@example.com:123', false], ['test@example.com_', false], ['test@example.com\\', false],