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.

269 lines
5.9 KiB

  1. /*eslint no-var:0, prefer-arrow-callback: 0, object-shorthand: 0 */
  2. 'use strict';
  3. var Punycode = require('punycode');
  4. var internals = {};
  5. //
  6. // Read rules from file.
  7. //
  8. internals.rules = require('./data/rules.json').map(function (rule) {
  9. return {
  10. rule: rule,
  11. suffix: rule.replace(/^(\*\.|\!)/, ''),
  12. punySuffix: -1,
  13. wildcard: rule.charAt(0) === '*',
  14. exception: rule.charAt(0) === '!'
  15. };
  16. });
  17. //
  18. // Check is given string ends with `suffix`.
  19. //
  20. internals.endsWith = function (str, suffix) {
  21. return str.indexOf(suffix, str.length - suffix.length) !== -1;
  22. };
  23. //
  24. // Find rule for a given domain.
  25. //
  26. internals.findRule = function (domain) {
  27. var punyDomain = Punycode.toASCII(domain);
  28. return internals.rules.reduce(function (memo, rule) {
  29. if (rule.punySuffix === -1){
  30. rule.punySuffix = Punycode.toASCII(rule.suffix);
  31. }
  32. if (!internals.endsWith(punyDomain, '.' + rule.punySuffix) && punyDomain !== rule.punySuffix) {
  33. return memo;
  34. }
  35. // This has been commented out as it never seems to run. This is because
  36. // sub tlds always appear after their parents and we never find a shorter
  37. // match.
  38. //if (memo) {
  39. // var memoSuffix = Punycode.toASCII(memo.suffix);
  40. // if (memoSuffix.length >= punySuffix.length) {
  41. // return memo;
  42. // }
  43. //}
  44. return rule;
  45. }, null);
  46. };
  47. //
  48. // Error codes and messages.
  49. //
  50. exports.errorCodes = {
  51. DOMAIN_TOO_SHORT: 'Domain name too short.',
  52. DOMAIN_TOO_LONG: 'Domain name too long. It should be no more than 255 chars.',
  53. LABEL_STARTS_WITH_DASH: 'Domain name label can not start with a dash.',
  54. LABEL_ENDS_WITH_DASH: 'Domain name label can not end with a dash.',
  55. LABEL_TOO_LONG: 'Domain name label should be at most 63 chars long.',
  56. LABEL_TOO_SHORT: 'Domain name label should be at least 1 character long.',
  57. LABEL_INVALID_CHARS: 'Domain name label can only contain alphanumeric characters or dashes.'
  58. };
  59. //
  60. // Validate domain name and throw if not valid.
  61. //
  62. // From wikipedia:
  63. //
  64. // Hostnames are composed of series of labels concatenated with dots, as are all
  65. // domain names. Each label must be between 1 and 63 characters long, and the
  66. // entire hostname (including the delimiting dots) has a maximum of 255 chars.
  67. //
  68. // Allowed chars:
  69. //
  70. // * `a-z`
  71. // * `0-9`
  72. // * `-` but not as a starting or ending character
  73. // * `.` as a separator for the textual portions of a domain name
  74. //
  75. // * http://en.wikipedia.org/wiki/Domain_name
  76. // * http://en.wikipedia.org/wiki/Hostname
  77. //
  78. internals.validate = function (input) {
  79. // Before we can validate we need to take care of IDNs with unicode chars.
  80. var ascii = Punycode.toASCII(input);
  81. if (ascii.length < 1) {
  82. return 'DOMAIN_TOO_SHORT';
  83. }
  84. if (ascii.length > 255) {
  85. return 'DOMAIN_TOO_LONG';
  86. }
  87. // Check each part's length and allowed chars.
  88. var labels = ascii.split('.');
  89. var label;
  90. for (var i = 0; i < labels.length; ++i) {
  91. label = labels[i];
  92. if (!label.length) {
  93. return 'LABEL_TOO_SHORT';
  94. }
  95. if (label.length > 63) {
  96. return 'LABEL_TOO_LONG';
  97. }
  98. if (label.charAt(0) === '-') {
  99. return 'LABEL_STARTS_WITH_DASH';
  100. }
  101. if (label.charAt(label.length - 1) === '-') {
  102. return 'LABEL_ENDS_WITH_DASH';
  103. }
  104. if (!/^[a-z0-9\-]+$/.test(label)) {
  105. return 'LABEL_INVALID_CHARS';
  106. }
  107. }
  108. };
  109. //
  110. // Public API
  111. //
  112. //
  113. // Parse domain.
  114. //
  115. exports.parse = function (input) {
  116. if (typeof input !== 'string') {
  117. throw new TypeError('Domain name must be a string.');
  118. }
  119. // Force domain to lowercase.
  120. var domain = input.slice(0).toLowerCase();
  121. // Handle FQDN.
  122. // TODO: Simply remove trailing dot?
  123. if (domain.charAt(domain.length - 1) === '.') {
  124. domain = domain.slice(0, domain.length - 1);
  125. }
  126. // Validate and sanitise input.
  127. var error = internals.validate(domain);
  128. if (error) {
  129. return {
  130. input: input,
  131. error: {
  132. message: exports.errorCodes[error],
  133. code: error
  134. }
  135. };
  136. }
  137. var parsed = {
  138. input: input,
  139. tld: null,
  140. sld: null,
  141. domain: null,
  142. subdomain: null,
  143. listed: false
  144. };
  145. var domainParts = domain.split('.');
  146. // Non-Internet TLD
  147. if (domainParts[domainParts.length - 1] === 'local') {
  148. return parsed;
  149. }
  150. var handlePunycode = function () {
  151. if (!/xn--/.test(domain)) {
  152. return parsed;
  153. }
  154. if (parsed.domain) {
  155. parsed.domain = Punycode.toASCII(parsed.domain);
  156. }
  157. if (parsed.subdomain) {
  158. parsed.subdomain = Punycode.toASCII(parsed.subdomain);
  159. }
  160. return parsed;
  161. };
  162. var rule = internals.findRule(domain);
  163. // Unlisted tld.
  164. if (!rule) {
  165. if (domainParts.length < 2) {
  166. return parsed;
  167. }
  168. parsed.tld = domainParts.pop();
  169. parsed.sld = domainParts.pop();
  170. parsed.domain = [parsed.sld, parsed.tld].join('.');
  171. if (domainParts.length) {
  172. parsed.subdomain = domainParts.pop();
  173. }
  174. return handlePunycode();
  175. }
  176. // At this point we know the public suffix is listed.
  177. parsed.listed = true;
  178. var tldParts = rule.suffix.split('.');
  179. var privateParts = domainParts.slice(0, domainParts.length - tldParts.length);
  180. if (rule.exception) {
  181. privateParts.push(tldParts.shift());
  182. }
  183. parsed.tld = tldParts.join('.');
  184. if (!privateParts.length) {
  185. return handlePunycode();
  186. }
  187. if (rule.wildcard) {
  188. tldParts.unshift(privateParts.pop());
  189. parsed.tld = tldParts.join('.');
  190. }
  191. if (!privateParts.length) {
  192. return handlePunycode();
  193. }
  194. parsed.sld = privateParts.pop();
  195. parsed.domain = [parsed.sld, parsed.tld].join('.');
  196. if (privateParts.length) {
  197. parsed.subdomain = privateParts.join('.');
  198. }
  199. return handlePunycode();
  200. };
  201. //
  202. // Get domain.
  203. //
  204. exports.get = function (domain) {
  205. if (!domain) {
  206. return null;
  207. }
  208. return exports.parse(domain).domain || null;
  209. };
  210. //
  211. // Check whether domain belongs to a known public suffix.
  212. //
  213. exports.isValid = function (domain) {
  214. var parsed = exports.parse(domain);
  215. return Boolean(parsed.domain && parsed.listed);
  216. };