Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions API.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand All @@ -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:
Expand Down
49 changes: 40 additions & 9 deletions src/domain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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.
*
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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');
}
}
Expand All @@ -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;
}
}

Expand Down
2 changes: 2 additions & 0 deletions test/domain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand All @@ -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],
Expand Down
1 change: 1 addition & 0 deletions test/email.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand Down