parse-domain
Version:
Splits a hostname into subdomains, domain and (effective) top-level domains
193 lines • 7.68 kB
JavaScript
import { ipVersion } from "is-ip";
// See https://en.wikipedia.org/wiki/Domain_name
// See https://tools.ietf.org/html/rfc1034
const LABEL_SEPARATOR = ".";
const LABEL_LENGTH_MIN = 1;
const LABEL_LENGTH_MAX = 63;
/**
* 255 octets - 2 octets if you remove the last dot
* @see https://devblogs.microsoft.com/oldnewthing/20120412-00/?p=7873
*/
const DOMAIN_LENGTH_MAX = 253;
const textEncoder = new TextEncoder();
export var Validation;
(function (Validation) {
/**
* Allows any octets as labels
* but still restricts the length of labels and the overall domain.
*
* @see https://www.rfc-editor.org/rfc/rfc2181#section-11
**/
Validation["Lax"] = "LAX";
/**
* Only allows ASCII letters, digits and hyphens (aka LDH),
* forbids hyphens at the beginning or end of a label
* and requires top-level domain names not to be all-numeric.
*
* This is the default if no validation is configured.
*
* @see https://datatracker.ietf.org/doc/html/rfc3696#section-2
*/
Validation["Strict"] = "STRICT";
})(Validation || (Validation = {}));
export var ValidationErrorType;
(function (ValidationErrorType) {
ValidationErrorType["NoHostname"] = "NO_HOSTNAME";
ValidationErrorType["DomainMaxLength"] = "DOMAIN_MAX_LENGTH";
ValidationErrorType["LabelMinLength"] = "LABEL_MIN_LENGTH";
ValidationErrorType["LabelMaxLength"] = "LABEL_MAX_LENGTH";
ValidationErrorType["LabelInvalidCharacter"] = "LABEL_INVALID_CHARACTER";
ValidationErrorType["LastLabelInvalid"] = "LAST_LABEL_INVALID";
})(ValidationErrorType || (ValidationErrorType = {}));
export var SanitizationResultType;
(function (SanitizationResultType) {
SanitizationResultType["ValidIp"] = "VALID_IP";
SanitizationResultType["ValidDomain"] = "VALID_DOMAIN";
SanitizationResultType["Error"] = "ERROR";
})(SanitizationResultType || (SanitizationResultType = {}));
const createNoHostnameError = (input) => {
return {
type: ValidationErrorType.NoHostname,
message: `The given input ${String(input)} does not look like a hostname.`,
column: 1,
};
};
const createDomainMaxLengthError = (domain, length) => {
return {
type: ValidationErrorType.DomainMaxLength,
message: `Domain "${domain}" is too long. Domain is ${length} octets long but should not be longer than ${DOMAIN_LENGTH_MAX}.`,
column: length,
};
};
const createLabelMinLengthError = (label, column) => {
const length = label.length;
return {
type: ValidationErrorType.LabelMinLength,
message: `Label "${label}" is too short. Label is ${length} octets long but should be at least ${LABEL_LENGTH_MIN}.`,
column,
};
};
const createLabelMaxLengthError = (label, column) => {
const length = label.length;
return {
type: ValidationErrorType.LabelMaxLength,
message: `Label "${label}" is too long. Label is ${length} octets long but should not be longer than ${LABEL_LENGTH_MAX}.`,
column,
};
};
const createLabelInvalidCharacterError = (label, invalidCharacter, column) => {
return {
type: ValidationErrorType.LabelInvalidCharacter,
message: `Label "${label}" contains invalid character "${invalidCharacter}" at column ${column}.`,
column,
};
};
const createLastLabelInvalidError = (label, column) => {
return {
type: ValidationErrorType.LabelInvalidCharacter,
message: `Last label "${label}" must not be all-numeric.`,
column,
};
};
export const sanitize = (input, options = {}) => {
// Extra check for non-TypeScript users
if (typeof input !== "string") {
return {
type: SanitizationResultType.Error,
errors: [createNoHostnameError(input)],
};
}
if (input === "") {
return {
type: SanitizationResultType.ValidDomain,
domain: input,
labels: [],
};
}
// IPv6 addresses are surrounded by square brackets in URLs
// See https://tools.ietf.org/html/rfc3986#section-3.2.2
const inputTrimmedAsIp = input.replace(/^\[|]$/g, "");
const ipVersionOfInput = ipVersion(inputTrimmedAsIp);
if (ipVersionOfInput !== undefined) {
return {
type: SanitizationResultType.ValidIp,
ip: inputTrimmedAsIp,
ipVersion: ipVersionOfInput,
};
}
const lastChar = input.charAt(input.length - 1);
const canonicalInput = lastChar === LABEL_SEPARATOR ? input.slice(0, -1) : input;
const octets = new TextEncoder().encode(canonicalInput);
if (octets.length > DOMAIN_LENGTH_MAX) {
return {
type: SanitizationResultType.Error,
errors: [createDomainMaxLengthError(input, octets.length)],
};
}
const labels = canonicalInput.split(LABEL_SEPARATOR);
const { validation = Validation.Strict } = options;
const labelValidationErrors = validateLabels[validation](labels);
if (labelValidationErrors.length > 0) {
return {
type: SanitizationResultType.Error,
errors: labelValidationErrors,
};
}
return {
type: SanitizationResultType.ValidDomain,
domain: input,
labels,
};
};
const validateLabels = {
[Validation.Lax]: (labels) => {
const labelValidationErrors = [];
let column = 1;
for (const label of labels) {
const octets = textEncoder.encode(label);
if (octets.length < LABEL_LENGTH_MIN) {
labelValidationErrors.push(createLabelMinLengthError(label, column));
}
else if (octets.length > LABEL_LENGTH_MAX) {
labelValidationErrors.push(createLabelMaxLengthError(label, column));
}
column += label.length + LABEL_SEPARATOR.length;
}
return labelValidationErrors;
},
[Validation.Strict]: (labels) => {
const labelValidationErrors = [];
let column = 1;
let lastLabel;
for (const label of labels) {
// According to https://tools.ietf.org/html/rfc6761 labels should
// only contain ASCII letters, digits and hyphens (LDH).
const invalidCharacter = /[^\da-z-]/i.exec(label);
if (invalidCharacter) {
labelValidationErrors.push(createLabelInvalidCharacterError(label, invalidCharacter[0], invalidCharacter.index + 1));
}
if (label.startsWith("-")) {
labelValidationErrors.push(createLabelInvalidCharacterError(label, "-", column));
}
else if (label.endsWith("-")) {
labelValidationErrors.push(createLabelInvalidCharacterError(label, "-", column + label.length - 1));
}
if (
// We can use .length here to check for the octet size because
// label can only contain ASCII LDH characters at this point.
label.length < LABEL_LENGTH_MIN) {
labelValidationErrors.push(createLabelMinLengthError(label, column));
}
else if (label.length > LABEL_LENGTH_MAX) {
labelValidationErrors.push(createLabelMaxLengthError(label, column));
}
column += label.length + LABEL_SEPARATOR.length;
lastLabel = label;
}
if (lastLabel !== undefined && /[a-z-]/iu.test(lastLabel) === false) {
labelValidationErrors.push(createLastLabelInvalidError(lastLabel, column - lastLabel.length - LABEL_SEPARATOR.length));
}
return labelValidationErrors;
},
};
//# sourceMappingURL=sanitize.js.map