From bff207400dfdfea9a3eb869498156e58439a997a Mon Sep 17 00:00:00 2001 From: NobyDa <53217160+NobyDa@users.noreply.github.com> Date: Sat, 24 Feb 2024 09:46:43 +0800 Subject: [PATCH] Add rule storage script. --- Rule-Storage/Include-Domain.txt | 26 ++++++ Rule-Storage/Rule-Storage.js | 139 ++++++++++++++++++++++++++++++++ 2 files changed, 165 insertions(+) create mode 100644 Rule-Storage/Include-Domain.txt create mode 100644 Rule-Storage/Rule-Storage.js diff --git a/Rule-Storage/Include-Domain.txt b/Rule-Storage/Include-Domain.txt new file mode 100644 index 0000000..e458138 --- /dev/null +++ b/Rule-Storage/Include-Domain.txt @@ -0,0 +1,26 @@ +DOMAIN-KEYWORD,.a +DOMAIN-KEYWORD,.b +DOMAIN-KEYWORD,.c +DOMAIN-KEYWORD,.d +DOMAIN-KEYWORD,.e +DOMAIN-KEYWORD,.f +DOMAIN-KEYWORD,.g +DOMAIN-KEYWORD,.h +DOMAIN-KEYWORD,.i +DOMAIN-KEYWORD,.j +DOMAIN-KEYWORD,.k +DOMAIN-KEYWORD,.l +DOMAIN-KEYWORD,.m +DOMAIN-KEYWORD,.n +DOMAIN-KEYWORD,.o +DOMAIN-KEYWORD,.p +DOMAIN-KEYWORD,.q +DOMAIN-KEYWORD,.r +DOMAIN-KEYWORD,.s +DOMAIN-KEYWORD,.t +DOMAIN-KEYWORD,.u +DOMAIN-KEYWORD,.v +DOMAIN-KEYWORD,.w +DOMAIN-KEYWORD,.x +DOMAIN-KEYWORD,.y +DOMAIN-KEYWORD,.z \ No newline at end of file diff --git a/Rule-Storage/Rule-Storage.js b/Rule-Storage/Rule-Storage.js new file mode 100644 index 0000000..297ed92 --- /dev/null +++ b/Rule-Storage/Rule-Storage.js @@ -0,0 +1,139 @@ +/* +Surge规则自动生成脚本 +更新时间:2024/02/24 + +需按照博客内教程配合使用: +https://nobyda.github.io/2024/02/24/Surge_Rule_Storage + +*/ + +const args = argsList(typeof $argument == "string" && $argument || 'region=debug'); +/* +When matching whitelist rules, skip generating suffix domain. Three ways to write: +Domain: example.com +Domain suffix: .example.com +Domain keyword: .example. +*/ +args.whitelist = args.whitelist || `[".mwcname.com", ".akadns.", ".akamai.", ".cloud.", ".cdn.", ".yun."]`; +args.key = args.key || 'Rule-Storage'; + +(async () => { + const host = $request.hostname.toLowerCase(); + if (['127.0.0.1', '0.0.0.0'].filter((v) => [...$request.dnsResult.v4Addresses].includes(v)).length) { + // DNS poisoning + args.matched = false; + args.region = 'global'; + } + if (!/\d$|:/.test(host) && host.includes('.')) { + const data = JSON.parse($persistentStore.read(args.key) || '{}'); + const saved_rules = $persistentStore.read(`${args.key}-${args.region}`); + if (!evalRules(host, saved_rules)) { + data[args.region] = saveDecision(host, data[args.region]); + if (data[args.region][host].quantity >= (args.quantity || 10)) { + data.eTLD = await eTLD(data.eTLD); + const suffix = shortenDomain(host, data.eTLD.public_suffix); + const domain = evalRules(host, JSON.parse(args.whitelist)) ? host : suffix; + const text = [...formatRules(saved_rules), ...formatRules(domain)].join('\n'); + delete data[args.region][host]; + $persistentStore.write(text, `${args.key}-${args.region}`) + } + } + return $persistentStore.write(JSON.stringify(data, null, 2), args.key) + } +})().catch((e) => $notification.post(args.key, ``, e.message || e)) + .finally(() => $done({ matched: Boolean(args.matched) })); + +function saveDecision(host_name, content = {}) { + for (const i in content) { + if (Date.now() - content[i].update_time > 86400000 * 30) { + delete content[i]; + } + } + if (content[host_name]) { + if (Date.now() - content[host_name].update_time > ((args.interval || 30) * 1000)) { + content[host_name].update_time = Date.now(); + content[host_name].quantity++; + } + } else { + content[host_name] = { update_time: Date.now(), quantity: 1 } + } + return content +} + +function evalRules(host_name, rule_list) { + const host_suffix = host_name.split('.').reverse(); + rule_list = typeof rule_list == 'object' ? rule_list : formatRules(rule_list, 1); + for (const i in rule_list) { + if (rule_list[i].startsWith('.') && !rule_list[i].endsWith('.')) { + const rule_host_suffix = rule_list[i].split('.').reverse().filter((v) => v); + if (rule_host_suffix.filter((v, i) => host_suffix[i] === v).length === rule_host_suffix.length) { + return true + } + } else if (rule_list[i].startsWith('.') && rule_list[i].endsWith('.')) { + if (host_name.includes(rule_list[i].slice(1, -1))) { + return true + } + } else if (rule_list[i] === host_name) { + return true + } + } + return false +} + +function formatRules(list, type) { + return (list || '').replace(/\r|\ |(\/\/|#|;).*/g, '').split('\n').map((v) => { + if (v.startsWith('DOMAIN,')) { return type ? v.split(",")[1] : v } + if (v.startsWith('DOMAIN-SUFFIX,')) { return type ? `.${v.split(",")[1]}` : v } + if (v.startsWith('.')) { return type ? v : `DOMAIN-SUFFIX,${v.slice(1)}` } + if (v.includes('.')) { return type ? v : `DOMAIN,${v}` } + }).filter((v) => v); +} + +async function eTLD(content = {}) { + if (!content.update_time || (Date.now() - content.update_time > 86400000 * 30)) { + await new Promise(resolve => { + $httpClient.get({ + url: 'https://publicsuffix.org/list/public_suffix_list.dat' + }, (error, resp, body) => { + if (resp.status == 200 && !error && body) { + content.update_time = Date.now(); + content.public_suffix = body.replace(/\r|.*(\/\/|#|;).*|\n(\!|\*\.)/g, '\n').split('\n').filter((t) => t); + resolve() + } else if (content.update_time) { + console.log(`Update eTLD list failed: ${error}`); + resolve() + } else { + throw new Error(`Download eTLD list failed: ${error}`) + } + }) + }) + } + return content +} + +/* +Shorten multi level domain: non-eTLD, full eTLD, second level domain will return original +Basic logic: www.abc.com -> .abc.com +*/ +function shortenDomain(host_name, eTLD_list) { + return host_name.split('.').reverse().reduce((t, v, i, c) => { + if (t === host_name || c.length == 2) { return host_name } + if (t.startsWith('.')) { return t } + const host_suffix = v + (t && `.${t}` || ''); + for (const ix in eTLD_list) { + if (eTLD_list[ix] === host_suffix) { + return host_suffix + } + } + return !i && host_name || `.${host_suffix}` + }, '') +} + +function argsList(data) { + return Array.from( + data.split("&") + .map((i) => i.split("=")) + .map(([k, v]) => [k, decodeURIComponent(v)]) + ) + .reduce((a, [k, v]) => Object.assign(a, { [k]: v }), {}) +} \ No newline at end of file