2024-02-24 09:46:43 +08:00
|
|
|
|
/*
|
|
|
|
|
Surge规则自动生成脚本
|
2024-08-11 22:01:41 +08:00
|
|
|
|
更新时间:2024/08/11
|
2024-02-24 09:46:43 +08:00
|
|
|
|
|
|
|
|
|
需按照博客内教程配合使用:
|
|
|
|
|
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();
|
2024-05-07 19:47:22 +08:00
|
|
|
|
const inHost = $request.listenPort == 6152 && !$request.sourcePort && !$request.processPath && /^[a-z0-9]{10}\.[a-z]+$/.test(host); //Prevent benchmark
|
|
|
|
|
if (['127.0.0.1', '0.0.0.0'].filter((v) => [...($request.dnsResult || {}).v4Addresses || []].includes(v)).length) {
|
2024-02-24 09:46:43 +08:00
|
|
|
|
// DNS poisoning
|
|
|
|
|
args.matched = false;
|
|
|
|
|
args.region = 'global';
|
|
|
|
|
}
|
2024-05-07 19:47:22 +08:00
|
|
|
|
if (!/\d$|:/.test(host) && host.includes('.') && !inHost) {
|
2024-02-24 09:46:43 +08:00
|
|
|
|
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)) {
|
2024-08-11 22:01:41 +08:00
|
|
|
|
const eTLDs = await eTLD(data.eTLD || JSON.parse($persistentStore.read(`${args.key}-eTLD`) || '{}'));
|
|
|
|
|
if (data.eTLD) { // legacy
|
|
|
|
|
$persistentStore.write(JSON.stringify(data.eTLD), `${args.key}-eTLD`);
|
|
|
|
|
delete data.eTLD;
|
|
|
|
|
}
|
|
|
|
|
const suffix = shortenDomain(host, eTLDs.public_suffix);
|
2024-02-24 09:46:43 +08:00
|
|
|
|
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}`)
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-05-07 19:47:22 +08:00
|
|
|
|
return $persistentStore.write(JSON.stringify(data), args.key)
|
2024-02-24 09:46:43 +08:00
|
|
|
|
}
|
|
|
|
|
})().catch((e) => $notification.post(args.key, ``, e.message || e))
|
|
|
|
|
.finally(() => $done({ matched: Boolean(args.matched) }));
|
|
|
|
|
|
|
|
|
|
function saveDecision(host_name, content = {}) {
|
2024-08-11 22:01:41 +08:00
|
|
|
|
const count = [];
|
2024-02-24 09:46:43 +08:00
|
|
|
|
for (const i in content) {
|
2024-05-07 19:47:22 +08:00
|
|
|
|
if (Date.now() - content[i].update_time > 86400000 * (args.cacheDays || 30)) {
|
2024-02-24 09:46:43 +08:00
|
|
|
|
delete content[i];
|
2024-08-11 22:01:41 +08:00
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
count.push(content[i].update_time);
|
|
|
|
|
}
|
|
|
|
|
if (count.length > (args.cacheNumber || 1000)) { // limit amount to prevent NE memory issues.
|
|
|
|
|
const spill = count.sort((x, y) => x - y).slice(0, count.length - (args.cacheNumber || 1000));
|
|
|
|
|
for (const is of spill) {
|
|
|
|
|
for (const ic in content) {
|
|
|
|
|
if (content[ic].update_time === is) {
|
|
|
|
|
delete content[ic];
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-02-24 09:46:43 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
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);
|
2024-08-11 22:01:41 +08:00
|
|
|
|
$persistentStore.write(JSON.stringify(content), `${args.key}-eTLD`);
|
2024-02-24 09:46:43 +08:00
|
|
|
|
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 }), {})
|
|
|
|
|
}
|