222 lines
5.5 KiB
JavaScript
222 lines
5.5 KiB
JavaScript
"use strict";
|
|
|
|
const tr46 = require("tr46");
|
|
|
|
const infra = require("./infra");
|
|
|
|
function p(char) {
|
|
return char.codePointAt(0);
|
|
}
|
|
|
|
const failure = Symbol("failure");
|
|
|
|
const specialSchemes = {
|
|
ftp: 21,
|
|
file: null,
|
|
http: 80,
|
|
https: 443,
|
|
ws: 80,
|
|
wss: 443
|
|
};
|
|
|
|
const urlCodePoints = new Set([
|
|
p("!"), p("$"), p("&"), p("'"), p("("), p(")"), p("*"), p("+"), p(","), p("-"), p("."), p("/"),
|
|
p(":"), p(";"), p("="), p("?"), p("@"), p("_"), p("~")
|
|
]);
|
|
|
|
const forbiddenHostCodePoints = new Set([
|
|
0x00, 0x09, 0x0A, 0x0D, 0x20, p("#"), p("/"), p(":"), p("<"), p(">"), p("?"), p("@"), p("["),
|
|
p("\\"), p("]"), p("^"), p("|")
|
|
]);
|
|
|
|
function isURLCodePoint(c) {
|
|
return infra.isASCIIAlphanumeric(c) ||
|
|
urlCodePoints.has(c) ||
|
|
(c >= 0xA0 && c <= 0x10FFFD && (c < 0xD800 || c > 0xDFFF) && !infra.isNoncharacter(c));
|
|
}
|
|
|
|
function isInvalidURLCodePoint(c) {
|
|
return !isURLCodePoint(c) && c !== p("%");
|
|
}
|
|
|
|
function isPercentEncodedByteAt(input, index) {
|
|
return index + 2 < input.length &&
|
|
input[index] === "%" &&
|
|
infra.isASCIIHex(input.charCodeAt(index + 1)) &&
|
|
infra.isASCIIHex(input.charCodeAt(index + 2));
|
|
}
|
|
|
|
function containsPercentEncodedByte(input) {
|
|
return /%[0-9A-Fa-f]{2}/u.test(input);
|
|
}
|
|
|
|
function containsForbiddenHostCodePoint(string) {
|
|
return [...string].some(c => forbiddenHostCodePoints.has(c.codePointAt(0)));
|
|
}
|
|
|
|
function containsForbiddenDomainCodePoint(string) {
|
|
return [...string].some(c => {
|
|
const cp = c.codePointAt(0);
|
|
return forbiddenHostCodePoints.has(cp) || (cp >= 0x00 && cp <= 0x1F) || cp === p("%") || cp === 0x7F;
|
|
});
|
|
}
|
|
|
|
function domainParserToASCII(domain, beStrict) {
|
|
return tr46.toASCII(domain, {
|
|
checkHyphens: beStrict,
|
|
checkBidi: true,
|
|
checkJoiners: true,
|
|
useSTD3ASCIIRules: beStrict,
|
|
transitionalProcessing: false,
|
|
verifyDNSLength: beStrict,
|
|
ignoreInvalidPunycode: false
|
|
});
|
|
}
|
|
|
|
function parseIPv4Number(input, validationErrors = null) {
|
|
if (input === "") {
|
|
return failure;
|
|
}
|
|
|
|
let validationErrorSeen = false;
|
|
let R = 10;
|
|
|
|
if (input.length >= 2 && input.charAt(0) === "0" && input.charAt(1).toLowerCase() === "x") {
|
|
validationErrorSeen = true;
|
|
input = input.substring(2);
|
|
R = 16;
|
|
} else if (input.length >= 2 && input.charAt(0) === "0") {
|
|
validationErrorSeen = true;
|
|
input = input.substring(1);
|
|
R = 8;
|
|
}
|
|
|
|
if (input === "") {
|
|
validationErrors?.push("IPv4-non-decimal-part");
|
|
return 0;
|
|
}
|
|
|
|
let regex = /[^0-7]/u;
|
|
if (R === 10) {
|
|
regex = /[^0-9]/u;
|
|
}
|
|
if (R === 16) {
|
|
regex = /[^0-9A-Fa-f]/u;
|
|
}
|
|
|
|
if (regex.test(input)) {
|
|
return failure;
|
|
}
|
|
|
|
if (validationErrorSeen) {
|
|
validationErrors?.push("IPv4-non-decimal-part");
|
|
}
|
|
|
|
return parseInt(input, R);
|
|
}
|
|
|
|
function endsInANumber(input) {
|
|
const parts = input.split(".");
|
|
if (parts[parts.length - 1] === "") {
|
|
if (parts.length === 1) {
|
|
return false;
|
|
}
|
|
parts.pop();
|
|
}
|
|
|
|
const last = parts[parts.length - 1];
|
|
if (/^[0-9]+$/u.test(last)) {
|
|
return true;
|
|
}
|
|
|
|
if (parseIPv4Number(last) !== failure) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
function domainParser(domain, validationErrors = null, beStrict = false) {
|
|
// A domain-to-ASCII validation error is reported whenever the strict Unicode ToASCII (CheckHyphens,
|
|
// UseSTD3ASCIIRules, and VerifyDnsLength all true) fails, even when the relaxed parameters used
|
|
// for non-strict parsing succeed. This step does not itself fail the algorithm.
|
|
//
|
|
// Spec divergence (performance): the URL Standard runs this strict pass unconditionally, but when
|
|
// beStrict is false its only effect is that validation error, so we skip it when we are neither
|
|
// being strict nor collecting validation errors.
|
|
if (beStrict || validationErrors) {
|
|
const strictResult = domainParserToASCII(domain, true);
|
|
if (strictResult === null) {
|
|
validationErrors?.push("domain-to-ASCII");
|
|
}
|
|
|
|
if (beStrict) {
|
|
return strictResult === null ? failure : strictResult;
|
|
}
|
|
}
|
|
|
|
let result;
|
|
if (infra.isASCIIString(domain)) {
|
|
// For web compatibility an ASCII domain is returned lowercased regardless of ToASCII's outcome.
|
|
result = domain.toLowerCase();
|
|
} else {
|
|
result = domainParserToASCII(domain, false);
|
|
if (result === null) {
|
|
return failure;
|
|
}
|
|
}
|
|
|
|
if (result === "") {
|
|
return failure;
|
|
}
|
|
if (containsForbiddenDomainCodePoint(result)) {
|
|
return failure;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
function isSpecialScheme(scheme) {
|
|
return specialSchemes[scheme.toLowerCase()] !== undefined;
|
|
}
|
|
|
|
function isSpecialSchemeExceptFile(scheme) {
|
|
return isSpecialScheme(scheme) && scheme.toLowerCase() !== "file";
|
|
}
|
|
|
|
function defaultPort(scheme) {
|
|
return specialSchemes[scheme.toLowerCase()];
|
|
}
|
|
|
|
function isWindowsDriveLetterCodePoints(cp1, cp2) {
|
|
return infra.isASCIIAlpha(cp1) && (cp2 === p(":") || cp2 === p("|"));
|
|
}
|
|
|
|
function isWindowsDriveLetterString(string) {
|
|
return string.length === 2 && infra.isASCIIAlpha(string.codePointAt(0)) && (string[1] === ":" || string[1] === "|");
|
|
}
|
|
|
|
function isNormalizedWindowsDriveLetterString(string) {
|
|
return string.length === 2 && infra.isASCIIAlpha(string.codePointAt(0)) && string[1] === ":";
|
|
}
|
|
|
|
module.exports = {
|
|
containsForbiddenHostCodePoint,
|
|
containsPercentEncodedByte,
|
|
defaultPort,
|
|
domainParser,
|
|
endsInANumber,
|
|
failure,
|
|
forbiddenHostCodePoints,
|
|
isInvalidURLCodePoint,
|
|
isNormalizedWindowsDriveLetterString,
|
|
isPercentEncodedByteAt,
|
|
parseIPv4Number,
|
|
isSpecialScheme,
|
|
isSpecialSchemeExceptFile,
|
|
isURLCodePoint,
|
|
isWindowsDriveLetterCodePoints,
|
|
isWindowsDriveLetterString,
|
|
p
|
|
};
|