UNPKG

haraka-tld

Version:
241 lines (200 loc) 6.46 kB
'use strict' const fs = require('fs') const path = require('path') const punycode = require('punycode.js') const update = require('./lib/update') const regex = { comment: /^\s*[;#].*$/, blank: /^\s*$/, line: /^\s*(.*?)\s*$/, } const logger = { log: (message) => { switch (process.env.HARAKA_LOGS_SUPPRESS) { case undefined: case 'false': case '0': console.log(message) } }, } module.exports = exports = { public_suffix_list: {}, top_level_tlds: {}, two_level_tlds: {}, three_level_tlds: {}, } function normalizeHost(host) { host = host.toLowerCase() if (/^xn--|\.xn--/.test(host)) { try { host = punycode.toUnicode(host) } catch (ignore) {} } return host } exports.is_public_suffix = function (host) { if (!host) return false host = normalizeHost(host) if (exports.public_suffix_list[host]) return true const up_one_level = host.split('.').slice(1).join('.') // co.uk -> uk if (!up_one_level) return false // no dot? const wildHost = `*.${up_one_level}` if (exports.public_suffix_list[wildHost]) { // check exception list if (exports.public_suffix_list[`!${host}`]) return false return true // matched a wildcard, ex: *.uk } return false } exports.get_organizational_domain = function (host) { // the domain that was registered with a domain name registrar. See // https://datatracker.ietf.org/doc/draft-kucherawy-dmarc-base/?include_text=1 // section 3.2 if (!host) return null host = normalizeHost(host) // www.example.com -> [ com, example, www ] const labels = host.split('.').reverse() // 4.3 Search the public suffix list for the name that matches the // largest number of labels found in the subject DNS domain. let greatest = 0 for (let i = 1; i <= labels.length; i++) { if (!labels[i - 1]) return null // dot w/o label const tld = labels.slice(0, i).reverse().join('.') if (exports.is_public_suffix(tld)) { greatest = +(i + 1) } else if (exports.public_suffix_list[`!${tld}`]) { greatest = i } } // 4.4 Construct a new DNS domain name using the name that matched // from the public suffix list and prefixing to it the "x+1"th // label from the subject domain. if (greatest === 0) return null // no valid TLD if (greatest > labels.length) return null // not enough labels if (greatest === labels.length) return host // same const orgName = labels.slice(0, greatest).reverse().join('.') return orgName } exports.split_hostname = function (host, level) { if (!level || (level && !(level >= 1 && level <= 3))) { level = 2 } const split = host.toLowerCase().split(/\./).reverse() let domain = '' // TLD if (level >= 1 && split[0] && exports.top_level_tlds[split[0]]) { domain = split.shift() + domain } // 2nd TLD if ( level >= 2 && split[0] && exports.two_level_tlds[`${split[0]}.${domain}`] ) { domain = `${split.shift()}.${domain}` } // 3rd TLD if ( level >= 3 && split[0] && exports.three_level_tlds[`${split[0]}.${domain}`] ) { domain = `${split.shift()}.${domain}` } // Domain if (split[0]) { domain = `${split.shift()}.${domain}` } return [split.reverse().join('.'), domain] } function load_public_suffix_list() { load_list_from_file('public-suffix-list').forEach((entry) => { // Parsing rules: http://publicsuffix.org/list/ // Each line is only read up to the first whitespace const suffix = entry.split(/\s/).shift() // Each line which is not entirely whitespace or begins with a comment // contains a rule. if (!suffix) return // empty string if ('/' === suffix.substring(0, 1)) return // comment // A rule may begin with a "!" (exclamation mark). If it does, it is // labelled as a "exception rule" and then treated as if the exclamation // mark is not present. if ('!' === suffix.substring(0, 1)) { const eName = suffix.substring(1) // remove ! prefix // bbc.co.uk -> co.uk const up_one = suffix.split('.').slice(1).join('.') if (exports.public_suffix_list[up_one]) { exports.public_suffix_list[up_one].push(eName) } else if (exports.public_suffix_list[`*.${up_one}`]) { exports.public_suffix_list[`*.${up_one}`].push(eName) } else { console.error(`unable to find parent for exception: ${eName}`) } } exports.public_suffix_list[suffix] = [] }) logger.log( `loaded ${Object.keys(exports.public_suffix_list).length} Public Suffixes`, ) } function load_tld_files() { load_list_from_file('top-level-tlds').forEach(function (tld) { exports.top_level_tlds[tld] = 1 }) load_list_from_file('two-level-tlds').forEach(function (tld) { exports.two_level_tlds[tld] = 1 }) load_list_from_file('three-level-tlds').forEach(function (tld) { exports.three_level_tlds[tld] = 1 }) load_list_from_file('extra-tlds').forEach(function (tld) { const s = tld.split(/\./) if (s.length === 2) { exports.two_level_tlds[tld] = 1 } else if (s.length === 3) { exports.three_level_tlds[tld] = 1 } }) logger.log(`loaded TLD files: 1=${Object.keys(exports.top_level_tlds).length} 2=${Object.keys(exports.two_level_tlds).length} 3=${Object.keys(exports.three_level_tlds).length}`) } function load_list_from_file(name) { const result = [] let filePath = path.resolve(__dirname, 'etc', name) if (!fs.existsSync(filePath)) { // not loaded by Haraka, use local path filePath = path.resolve('etc', name) } fs.readFileSync(filePath, 'UTF-8') .split(/\r\n|\r|\n/) .forEach((line) => { if (regex.comment.test(line)) return if (regex.blank.test(line)) return const line_data = regex.line.exec(line) if (!line_data) return result.push(line_data[1].trim().toLowerCase()) }) return result } load_tld_files() load_public_suffix_list() // every 15 days, check for an update. If updated, download, install, // and then read it into the exported object setInterval( () => { update .updatePSLfile() .then((updated) => { if (updated) load_public_suffix_list() }) .catch((err) => { console.error(err.message) }) }, 15 * 86400 * 1000, ).unref() // each 15 days // the .unref() on the interval tells node to ignore this // timer when deciding whether the process is done.