You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
			
				
					288 lines
				
				6.6 KiB
			
		
		
			
		
	
	
					288 lines
				
				6.6 KiB
			| 
								 
											4 years ago
										 
									 | 
							
								"use strict";
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								const punycode = require("punycode");
							 | 
						||
| 
								 | 
							
								const regexes = require("./lib/regexes.js");
							 | 
						||
| 
								 | 
							
								const mappingTable = require("./lib/mappingTable.json");
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								function containsNonASCII(str) {
							 | 
						||
| 
								 | 
							
								  return /[^\x00-\x7F]/.test(str);
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								function findStatus(val, { useSTD3ASCIIRules }) {
							 | 
						||
| 
								 | 
							
								  let start = 0;
							 | 
						||
| 
								 | 
							
								  let end = mappingTable.length - 1;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  while (start <= end) {
							 | 
						||
| 
								 | 
							
								    const mid = Math.floor((start + end) / 2);
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    const target = mappingTable[mid];
							 | 
						||
| 
								 | 
							
								    if (target[0][0] <= val && target[0][1] >= val) {
							 | 
						||
| 
								 | 
							
								      if (target[1].startsWith("disallowed_STD3_")) {
							 | 
						||
| 
								 | 
							
								        const newStatus = useSTD3ASCIIRules ? "disallowed" : target[1].slice(16);
							 | 
						||
| 
								 | 
							
								        return [newStatus, ...target.slice(2)];
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								      return target.slice(1);
							 | 
						||
| 
								 | 
							
								    } else if (target[0][0] > val) {
							 | 
						||
| 
								 | 
							
								      end = mid - 1;
							 | 
						||
| 
								 | 
							
								    } else {
							 | 
						||
| 
								 | 
							
								      start = mid + 1;
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  return null;
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								function mapChars(domainName, { useSTD3ASCIIRules, processingOption }) {
							 | 
						||
| 
								 | 
							
								  let hasError = false;
							 | 
						||
| 
								 | 
							
								  let processed = "";
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  for (const ch of domainName) {
							 | 
						||
| 
								 | 
							
								    const [status, mapping] = findStatus(ch.codePointAt(0), { useSTD3ASCIIRules });
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    switch (status) {
							 | 
						||
| 
								 | 
							
								      case "disallowed":
							 | 
						||
| 
								 | 
							
								        hasError = true;
							 | 
						||
| 
								 | 
							
								        processed += ch;
							 | 
						||
| 
								 | 
							
								        break;
							 | 
						||
| 
								 | 
							
								      case "ignored":
							 | 
						||
| 
								 | 
							
								        break;
							 | 
						||
| 
								 | 
							
								      case "mapped":
							 | 
						||
| 
								 | 
							
								        processed += mapping;
							 | 
						||
| 
								 | 
							
								        break;
							 | 
						||
| 
								 | 
							
								      case "deviation":
							 | 
						||
| 
								 | 
							
								        if (processingOption === "transitional") {
							 | 
						||
| 
								 | 
							
								          processed += mapping;
							 | 
						||
| 
								 | 
							
								        } else {
							 | 
						||
| 
								 | 
							
								          processed += ch;
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								        break;
							 | 
						||
| 
								 | 
							
								      case "valid":
							 | 
						||
| 
								 | 
							
								        processed += ch;
							 | 
						||
| 
								 | 
							
								        break;
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  return {
							 | 
						||
| 
								 | 
							
								    string: processed,
							 | 
						||
| 
								 | 
							
								    error: hasError
							 | 
						||
| 
								 | 
							
								  };
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								function validateLabel(label, { checkHyphens, checkBidi, checkJoiners, processingOption, useSTD3ASCIIRules }) {
							 | 
						||
| 
								 | 
							
								  if (label.normalize("NFC") !== label) {
							 | 
						||
| 
								 | 
							
								    return false;
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  const codePoints = Array.from(label);
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  if (checkHyphens) {
							 | 
						||
| 
								 | 
							
								    if ((codePoints[2] === "-" && codePoints[3] === "-") ||
							 | 
						||
| 
								 | 
							
								        (label.startsWith("-") || label.endsWith("-"))) {
							 | 
						||
| 
								 | 
							
								      return false;
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  if (label.includes(".") ||
							 | 
						||
| 
								 | 
							
								      (codePoints.length > 0 && regexes.combiningMarks.test(codePoints[0]))) {
							 | 
						||
| 
								 | 
							
								    return false;
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  for (const ch of codePoints) {
							 | 
						||
| 
								 | 
							
								    const [status] = findStatus(ch.codePointAt(0), { useSTD3ASCIIRules });
							 | 
						||
| 
								 | 
							
								    if ((processingOption === "transitional" && status !== "valid") ||
							 | 
						||
| 
								 | 
							
								        (processingOption === "nontransitional" &&
							 | 
						||
| 
								 | 
							
								         status !== "valid" && status !== "deviation")) {
							 | 
						||
| 
								 | 
							
								      return false;
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  // https://tools.ietf.org/html/rfc5892#appendix-A
							 | 
						||
| 
								 | 
							
								  if (checkJoiners) {
							 | 
						||
| 
								 | 
							
								    let last = 0;
							 | 
						||
| 
								 | 
							
								    for (const [i, ch] of codePoints.entries()) {
							 | 
						||
| 
								 | 
							
								      if (ch === "\u200C" || ch === "\u200D") {
							 | 
						||
| 
								 | 
							
								        if (i > 0) {
							 | 
						||
| 
								 | 
							
								          if (regexes.combiningClassVirama.test(codePoints[i - 1])) {
							 | 
						||
| 
								 | 
							
								            continue;
							 | 
						||
| 
								 | 
							
								          }
							 | 
						||
| 
								 | 
							
								          if (ch === "\u200C") {
							 | 
						||
| 
								 | 
							
								            // TODO: make this more efficient
							 | 
						||
| 
								 | 
							
								            const next = codePoints.indexOf("\u200C", i + 1);
							 | 
						||
| 
								 | 
							
								            const test = next < 0 ? codePoints.slice(last) : codePoints.slice(last, next);
							 | 
						||
| 
								 | 
							
								            if (regexes.validZWNJ.test(test.join(""))) {
							 | 
						||
| 
								 | 
							
								              last = i + 1;
							 | 
						||
| 
								 | 
							
								              continue;
							 | 
						||
| 
								 | 
							
								            }
							 | 
						||
| 
								 | 
							
								          }
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								        return false;
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  // https://tools.ietf.org/html/rfc5893#section-2
							 | 
						||
| 
								 | 
							
								  if (checkBidi) {
							 | 
						||
| 
								 | 
							
								    let rtl;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    // 1
							 | 
						||
| 
								 | 
							
								    if (regexes.bidiS1LTR.test(codePoints[0])) {
							 | 
						||
| 
								 | 
							
								      rtl = false;
							 | 
						||
| 
								 | 
							
								    } else if (regexes.bidiS1RTL.test(codePoints[0])) {
							 | 
						||
| 
								 | 
							
								      rtl = true;
							 | 
						||
| 
								 | 
							
								    } else {
							 | 
						||
| 
								 | 
							
								      return false;
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    if (rtl) {
							 | 
						||
| 
								 | 
							
								      // 2-4
							 | 
						||
| 
								 | 
							
								      if (!regexes.bidiS2.test(label) ||
							 | 
						||
| 
								 | 
							
								          !regexes.bidiS3.test(label) ||
							 | 
						||
| 
								 | 
							
								          (regexes.bidiS4EN.test(label) && regexes.bidiS4AN.test(label))) {
							 | 
						||
| 
								 | 
							
								        return false;
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								    } else if (!regexes.bidiS5.test(label) ||
							 | 
						||
| 
								 | 
							
								               !regexes.bidiS6.test(label)) { // 5-6
							 | 
						||
| 
								 | 
							
								      return false;
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  return true;
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								function isBidiDomain(labels) {
							 | 
						||
| 
								 | 
							
								  const domain = labels.map(label => {
							 | 
						||
| 
								 | 
							
								    if (label.startsWith("xn--")) {
							 | 
						||
| 
								 | 
							
								      try {
							 | 
						||
| 
								 | 
							
								        return punycode.decode(label.substring(4));
							 | 
						||
| 
								 | 
							
								      } catch (err) {
							 | 
						||
| 
								 | 
							
								        return "";
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								    return label;
							 | 
						||
| 
								 | 
							
								  }).join(".");
							 | 
						||
| 
								 | 
							
								  return regexes.bidiDomain.test(domain);
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								function processing(domainName, options) {
							 | 
						||
| 
								 | 
							
								  const { processingOption } = options;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  // 1. Map.
							 | 
						||
| 
								 | 
							
								  let { string, error } = mapChars(domainName, options);
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  // 2. Normalize.
							 | 
						||
| 
								 | 
							
								  string = string.normalize("NFC");
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  // 3. Break.
							 | 
						||
| 
								 | 
							
								  const labels = string.split(".");
							 | 
						||
| 
								 | 
							
								  const isBidi = isBidiDomain(labels);
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  // 4. Convert/Validate.
							 | 
						||
| 
								 | 
							
								  for (const [i, origLabel] of labels.entries()) {
							 | 
						||
| 
								 | 
							
								    let label = origLabel;
							 | 
						||
| 
								 | 
							
								    let curProcessing = processingOption;
							 | 
						||
| 
								 | 
							
								    if (label.startsWith("xn--")) {
							 | 
						||
| 
								 | 
							
								      try {
							 | 
						||
| 
								 | 
							
								        label = punycode.decode(label.substring(4));
							 | 
						||
| 
								 | 
							
								        labels[i] = label;
							 | 
						||
| 
								 | 
							
								      } catch (err) {
							 | 
						||
| 
								 | 
							
								        error = true;
							 | 
						||
| 
								 | 
							
								        continue;
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								      curProcessing = "nontransitional";
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    // No need to validate if we already know there is an error.
							 | 
						||
| 
								 | 
							
								    if (error) {
							 | 
						||
| 
								 | 
							
								      continue;
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								    const validation = validateLabel(label, Object.assign({}, options, {
							 | 
						||
| 
								 | 
							
								      processingOption: curProcessing,
							 | 
						||
| 
								 | 
							
								      checkBidi: options.checkBidi && isBidi
							 | 
						||
| 
								 | 
							
								    }));
							 | 
						||
| 
								 | 
							
								    if (!validation) {
							 | 
						||
| 
								 | 
							
								      error = true;
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  return {
							 | 
						||
| 
								 | 
							
								    string: labels.join("."),
							 | 
						||
| 
								 | 
							
								    error
							 | 
						||
| 
								 | 
							
								  };
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								function toASCII(domainName, {
							 | 
						||
| 
								 | 
							
								  checkHyphens = false,
							 | 
						||
| 
								 | 
							
								  checkBidi = false,
							 | 
						||
| 
								 | 
							
								  checkJoiners = false,
							 | 
						||
| 
								 | 
							
								  useSTD3ASCIIRules = false,
							 | 
						||
| 
								 | 
							
								  processingOption = "nontransitional",
							 | 
						||
| 
								 | 
							
								  verifyDNSLength = false
							 | 
						||
| 
								 | 
							
								} = {}) {
							 | 
						||
| 
								 | 
							
								  if (processingOption !== "transitional" && processingOption !== "nontransitional") {
							 | 
						||
| 
								 | 
							
								    throw new RangeError("processingOption must be either transitional or nontransitional");
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  const result = processing(domainName, {
							 | 
						||
| 
								 | 
							
								    processingOption,
							 | 
						||
| 
								 | 
							
								    checkHyphens,
							 | 
						||
| 
								 | 
							
								    checkBidi,
							 | 
						||
| 
								 | 
							
								    checkJoiners,
							 | 
						||
| 
								 | 
							
								    useSTD3ASCIIRules
							 | 
						||
| 
								 | 
							
								  });
							 | 
						||
| 
								 | 
							
								  let labels = result.string.split(".");
							 | 
						||
| 
								 | 
							
								  labels = labels.map(l => {
							 | 
						||
| 
								 | 
							
								    if (containsNonASCII(l)) {
							 | 
						||
| 
								 | 
							
								      try {
							 | 
						||
| 
								 | 
							
								        return "xn--" + punycode.encode(l);
							 | 
						||
| 
								 | 
							
								      } catch (e) {
							 | 
						||
| 
								 | 
							
								        result.error = true;
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								    return l;
							 | 
						||
| 
								 | 
							
								  });
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  if (verifyDNSLength) {
							 | 
						||
| 
								 | 
							
								    const total = labels.join(".").length;
							 | 
						||
| 
								 | 
							
								    if (total > 253 || total === 0) {
							 | 
						||
| 
								 | 
							
								      result.error = true;
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    for (let i = 0; i < labels.length; ++i) {
							 | 
						||
| 
								 | 
							
								      if (labels[i].length > 63 || labels[i].length === 0) {
							 | 
						||
| 
								 | 
							
								        result.error = true;
							 | 
						||
| 
								 | 
							
								        break;
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  if (result.error) {
							 | 
						||
| 
								 | 
							
								    return null;
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								  return labels.join(".");
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								function toUnicode(domainName, {
							 | 
						||
| 
								 | 
							
								  checkHyphens = false,
							 | 
						||
| 
								 | 
							
								  checkBidi = false,
							 | 
						||
| 
								 | 
							
								  checkJoiners = false,
							 | 
						||
| 
								 | 
							
								  useSTD3ASCIIRules = false
							 | 
						||
| 
								 | 
							
								} = {}) {
							 | 
						||
| 
								 | 
							
								  const result = processing(domainName, {
							 | 
						||
| 
								 | 
							
								    processingOption: "nontransitional",
							 | 
						||
| 
								 | 
							
								    checkHyphens,
							 | 
						||
| 
								 | 
							
								    checkBidi,
							 | 
						||
| 
								 | 
							
								    checkJoiners,
							 | 
						||
| 
								 | 
							
								    useSTD3ASCIIRules
							 | 
						||
| 
								 | 
							
								  });
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  return {
							 | 
						||
| 
								 | 
							
								    domain: result.string,
							 | 
						||
| 
								 | 
							
								    error: result.error
							 | 
						||
| 
								 | 
							
								  };
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								module.exports = {
							 | 
						||
| 
								 | 
							
								  toASCII,
							 | 
						||
| 
								 | 
							
								  toUnicode
							 | 
						||
| 
								 | 
							
								};
							 |