| 1 | "use strict";
|
|---|
| 2 |
|
|---|
| 3 | const punycode = require("punycode");
|
|---|
| 4 | const regexes = require("./lib/regexes.js");
|
|---|
| 5 | const mappingTable = require("./lib/mappingTable.json");
|
|---|
| 6 | const { STATUS_MAPPING } = require("./lib/statusMapping.js");
|
|---|
| 7 |
|
|---|
| 8 | function containsNonASCII(str) {
|
|---|
| 9 | return /[^\x00-\x7F]/.test(str);
|
|---|
| 10 | }
|
|---|
| 11 |
|
|---|
| 12 | function findStatus(val, { useSTD3ASCIIRules }) {
|
|---|
| 13 | let start = 0;
|
|---|
| 14 | let end = mappingTable.length - 1;
|
|---|
| 15 |
|
|---|
| 16 | while (start <= end) {
|
|---|
| 17 | const mid = Math.floor((start + end) / 2);
|
|---|
| 18 |
|
|---|
| 19 | const target = mappingTable[mid];
|
|---|
| 20 | const min = Array.isArray(target[0]) ? target[0][0] : target[0];
|
|---|
| 21 | const max = Array.isArray(target[0]) ? target[0][1] : target[0];
|
|---|
| 22 |
|
|---|
| 23 | if (min <= val && max >= val) {
|
|---|
| 24 | if (useSTD3ASCIIRules &&
|
|---|
| 25 | (target[1] === STATUS_MAPPING.disallowed_STD3_valid || target[1] === STATUS_MAPPING.disallowed_STD3_mapped)) {
|
|---|
| 26 | return [STATUS_MAPPING.disallowed, ...target.slice(2)];
|
|---|
| 27 | } else if (target[1] === STATUS_MAPPING.disallowed_STD3_valid) {
|
|---|
| 28 | return [STATUS_MAPPING.valid, ...target.slice(2)];
|
|---|
| 29 | } else if (target[1] === STATUS_MAPPING.disallowed_STD3_mapped) {
|
|---|
| 30 | return [STATUS_MAPPING.mapped, ...target.slice(2)];
|
|---|
| 31 | }
|
|---|
| 32 |
|
|---|
| 33 | return target.slice(1);
|
|---|
| 34 | } else if (min > val) {
|
|---|
| 35 | end = mid - 1;
|
|---|
| 36 | } else {
|
|---|
| 37 | start = mid + 1;
|
|---|
| 38 | }
|
|---|
| 39 | }
|
|---|
| 40 |
|
|---|
| 41 | return null;
|
|---|
| 42 | }
|
|---|
| 43 |
|
|---|
| 44 | function mapChars(domainName, { useSTD3ASCIIRules, processingOption }) {
|
|---|
| 45 | let hasError = false;
|
|---|
| 46 | let processed = "";
|
|---|
| 47 |
|
|---|
| 48 | for (const ch of domainName) {
|
|---|
| 49 | const [status, mapping] = findStatus(ch.codePointAt(0), { useSTD3ASCIIRules });
|
|---|
| 50 |
|
|---|
| 51 | switch (status) {
|
|---|
| 52 | case STATUS_MAPPING.disallowed:
|
|---|
| 53 | hasError = true;
|
|---|
| 54 | processed += ch;
|
|---|
| 55 | break;
|
|---|
| 56 | case STATUS_MAPPING.ignored:
|
|---|
| 57 | break;
|
|---|
| 58 | case STATUS_MAPPING.mapped:
|
|---|
| 59 | processed += mapping;
|
|---|
| 60 | break;
|
|---|
| 61 | case STATUS_MAPPING.deviation:
|
|---|
| 62 | if (processingOption === "transitional") {
|
|---|
| 63 | processed += mapping;
|
|---|
| 64 | } else {
|
|---|
| 65 | processed += ch;
|
|---|
| 66 | }
|
|---|
| 67 | break;
|
|---|
| 68 | case STATUS_MAPPING.valid:
|
|---|
| 69 | processed += ch;
|
|---|
| 70 | break;
|
|---|
| 71 | }
|
|---|
| 72 | }
|
|---|
| 73 |
|
|---|
| 74 | return {
|
|---|
| 75 | string: processed,
|
|---|
| 76 | error: hasError
|
|---|
| 77 | };
|
|---|
| 78 | }
|
|---|
| 79 |
|
|---|
| 80 | function validateLabel(label, { checkHyphens, checkBidi, checkJoiners, processingOption, useSTD3ASCIIRules }) {
|
|---|
| 81 | if (label.normalize("NFC") !== label) {
|
|---|
| 82 | return false;
|
|---|
| 83 | }
|
|---|
| 84 |
|
|---|
| 85 | const codePoints = Array.from(label);
|
|---|
| 86 |
|
|---|
| 87 | if (checkHyphens) {
|
|---|
| 88 | if ((codePoints[2] === "-" && codePoints[3] === "-") ||
|
|---|
| 89 | (label.startsWith("-") || label.endsWith("-"))) {
|
|---|
| 90 | return false;
|
|---|
| 91 | }
|
|---|
| 92 | }
|
|---|
| 93 |
|
|---|
| 94 | if (label.includes(".") ||
|
|---|
| 95 | (codePoints.length > 0 && regexes.combiningMarks.test(codePoints[0]))) {
|
|---|
| 96 | return false;
|
|---|
| 97 | }
|
|---|
| 98 |
|
|---|
| 99 | for (const ch of codePoints) {
|
|---|
| 100 | const [status] = findStatus(ch.codePointAt(0), { useSTD3ASCIIRules });
|
|---|
| 101 | if ((processingOption === "transitional" && status !== STATUS_MAPPING.valid) ||
|
|---|
| 102 | (processingOption === "nontransitional" &&
|
|---|
| 103 | status !== STATUS_MAPPING.valid && status !== STATUS_MAPPING.deviation)) {
|
|---|
| 104 | return false;
|
|---|
| 105 | }
|
|---|
| 106 | }
|
|---|
| 107 |
|
|---|
| 108 | // https://tools.ietf.org/html/rfc5892#appendix-A
|
|---|
| 109 | if (checkJoiners) {
|
|---|
| 110 | let last = 0;
|
|---|
| 111 | for (const [i, ch] of codePoints.entries()) {
|
|---|
| 112 | if (ch === "\u200C" || ch === "\u200D") {
|
|---|
| 113 | if (i > 0) {
|
|---|
| 114 | if (regexes.combiningClassVirama.test(codePoints[i - 1])) {
|
|---|
| 115 | continue;
|
|---|
| 116 | }
|
|---|
| 117 | if (ch === "\u200C") {
|
|---|
| 118 | // TODO: make this more efficient
|
|---|
| 119 | const next = codePoints.indexOf("\u200C", i + 1);
|
|---|
| 120 | const test = next < 0 ? codePoints.slice(last) : codePoints.slice(last, next);
|
|---|
| 121 | if (regexes.validZWNJ.test(test.join(""))) {
|
|---|
| 122 | last = i + 1;
|
|---|
| 123 | continue;
|
|---|
| 124 | }
|
|---|
| 125 | }
|
|---|
| 126 | }
|
|---|
| 127 | return false;
|
|---|
| 128 | }
|
|---|
| 129 | }
|
|---|
| 130 | }
|
|---|
| 131 |
|
|---|
| 132 | // https://tools.ietf.org/html/rfc5893#section-2
|
|---|
| 133 | if (checkBidi) {
|
|---|
| 134 | let rtl;
|
|---|
| 135 |
|
|---|
| 136 | // 1
|
|---|
| 137 | if (regexes.bidiS1LTR.test(codePoints[0])) {
|
|---|
| 138 | rtl = false;
|
|---|
| 139 | } else if (regexes.bidiS1RTL.test(codePoints[0])) {
|
|---|
| 140 | rtl = true;
|
|---|
| 141 | } else {
|
|---|
| 142 | return false;
|
|---|
| 143 | }
|
|---|
| 144 |
|
|---|
| 145 | if (rtl) {
|
|---|
| 146 | // 2-4
|
|---|
| 147 | if (!regexes.bidiS2.test(label) ||
|
|---|
| 148 | !regexes.bidiS3.test(label) ||
|
|---|
| 149 | (regexes.bidiS4EN.test(label) && regexes.bidiS4AN.test(label))) {
|
|---|
| 150 | return false;
|
|---|
| 151 | }
|
|---|
| 152 | } else if (!regexes.bidiS5.test(label) ||
|
|---|
| 153 | !regexes.bidiS6.test(label)) { // 5-6
|
|---|
| 154 | return false;
|
|---|
| 155 | }
|
|---|
| 156 | }
|
|---|
| 157 |
|
|---|
| 158 | return true;
|
|---|
| 159 | }
|
|---|
| 160 |
|
|---|
| 161 | function isBidiDomain(labels) {
|
|---|
| 162 | const domain = labels.map(label => {
|
|---|
| 163 | if (label.startsWith("xn--")) {
|
|---|
| 164 | try {
|
|---|
| 165 | return punycode.decode(label.substring(4));
|
|---|
| 166 | } catch (err) {
|
|---|
| 167 | return "";
|
|---|
| 168 | }
|
|---|
| 169 | }
|
|---|
| 170 | return label;
|
|---|
| 171 | }).join(".");
|
|---|
| 172 | return regexes.bidiDomain.test(domain);
|
|---|
| 173 | }
|
|---|
| 174 |
|
|---|
| 175 | function processing(domainName, options) {
|
|---|
| 176 | const { processingOption } = options;
|
|---|
| 177 |
|
|---|
| 178 | // 1. Map.
|
|---|
| 179 | let { string, error } = mapChars(domainName, options);
|
|---|
| 180 |
|
|---|
| 181 | // 2. Normalize.
|
|---|
| 182 | string = string.normalize("NFC");
|
|---|
| 183 |
|
|---|
| 184 | // 3. Break.
|
|---|
| 185 | const labels = string.split(".");
|
|---|
| 186 | const isBidi = isBidiDomain(labels);
|
|---|
| 187 |
|
|---|
| 188 | // 4. Convert/Validate.
|
|---|
| 189 | for (const [i, origLabel] of labels.entries()) {
|
|---|
| 190 | let label = origLabel;
|
|---|
| 191 | let curProcessing = processingOption;
|
|---|
| 192 | if (label.startsWith("xn--")) {
|
|---|
| 193 | try {
|
|---|
| 194 | label = punycode.decode(label.substring(4));
|
|---|
| 195 | labels[i] = label;
|
|---|
| 196 | } catch (err) {
|
|---|
| 197 | error = true;
|
|---|
| 198 | continue;
|
|---|
| 199 | }
|
|---|
| 200 | curProcessing = "nontransitional";
|
|---|
| 201 | }
|
|---|
| 202 |
|
|---|
| 203 | // No need to validate if we already know there is an error.
|
|---|
| 204 | if (error) {
|
|---|
| 205 | continue;
|
|---|
| 206 | }
|
|---|
| 207 | const validation = validateLabel(label, Object.assign({}, options, {
|
|---|
| 208 | processingOption: curProcessing,
|
|---|
| 209 | checkBidi: options.checkBidi && isBidi
|
|---|
| 210 | }));
|
|---|
| 211 | if (!validation) {
|
|---|
| 212 | error = true;
|
|---|
| 213 | }
|
|---|
| 214 | }
|
|---|
| 215 |
|
|---|
| 216 | return {
|
|---|
| 217 | string: labels.join("."),
|
|---|
| 218 | error
|
|---|
| 219 | };
|
|---|
| 220 | }
|
|---|
| 221 |
|
|---|
| 222 | function toASCII(domainName, {
|
|---|
| 223 | checkHyphens = false,
|
|---|
| 224 | checkBidi = false,
|
|---|
| 225 | checkJoiners = false,
|
|---|
| 226 | useSTD3ASCIIRules = false,
|
|---|
| 227 | processingOption = "nontransitional",
|
|---|
| 228 | verifyDNSLength = false
|
|---|
| 229 | } = {}) {
|
|---|
| 230 | if (processingOption !== "transitional" && processingOption !== "nontransitional") {
|
|---|
| 231 | throw new RangeError("processingOption must be either transitional or nontransitional");
|
|---|
| 232 | }
|
|---|
| 233 |
|
|---|
| 234 | const result = processing(domainName, {
|
|---|
| 235 | processingOption,
|
|---|
| 236 | checkHyphens,
|
|---|
| 237 | checkBidi,
|
|---|
| 238 | checkJoiners,
|
|---|
| 239 | useSTD3ASCIIRules
|
|---|
| 240 | });
|
|---|
| 241 | let labels = result.string.split(".");
|
|---|
| 242 | labels = labels.map(l => {
|
|---|
| 243 | if (containsNonASCII(l)) {
|
|---|
| 244 | try {
|
|---|
| 245 | return "xn--" + punycode.encode(l);
|
|---|
| 246 | } catch (e) {
|
|---|
| 247 | result.error = true;
|
|---|
| 248 | }
|
|---|
| 249 | }
|
|---|
| 250 | return l;
|
|---|
| 251 | });
|
|---|
| 252 |
|
|---|
| 253 | if (verifyDNSLength) {
|
|---|
| 254 | const total = labels.join(".").length;
|
|---|
| 255 | if (total > 253 || total === 0) {
|
|---|
| 256 | result.error = true;
|
|---|
| 257 | }
|
|---|
| 258 |
|
|---|
| 259 | for (let i = 0; i < labels.length; ++i) {
|
|---|
| 260 | if (labels[i].length > 63 || labels[i].length === 0) {
|
|---|
| 261 | result.error = true;
|
|---|
| 262 | break;
|
|---|
| 263 | }
|
|---|
| 264 | }
|
|---|
| 265 | }
|
|---|
| 266 |
|
|---|
| 267 | if (result.error) {
|
|---|
| 268 | return null;
|
|---|
| 269 | }
|
|---|
| 270 | return labels.join(".");
|
|---|
| 271 | }
|
|---|
| 272 |
|
|---|
| 273 | function toUnicode(domainName, {
|
|---|
| 274 | checkHyphens = false,
|
|---|
| 275 | checkBidi = false,
|
|---|
| 276 | checkJoiners = false,
|
|---|
| 277 | useSTD3ASCIIRules = false,
|
|---|
| 278 | processingOption = "nontransitional"
|
|---|
| 279 | } = {}) {
|
|---|
| 280 | const result = processing(domainName, {
|
|---|
| 281 | processingOption,
|
|---|
| 282 | checkHyphens,
|
|---|
| 283 | checkBidi,
|
|---|
| 284 | checkJoiners,
|
|---|
| 285 | useSTD3ASCIIRules
|
|---|
| 286 | });
|
|---|
| 287 |
|
|---|
| 288 | return {
|
|---|
| 289 | domain: result.string,
|
|---|
| 290 | error: result.error
|
|---|
| 291 | };
|
|---|
| 292 | }
|
|---|
| 293 |
|
|---|
| 294 | module.exports = {
|
|---|
| 295 | toASCII,
|
|---|
| 296 | toUnicode
|
|---|
| 297 | };
|
|---|