feat(subscription): add crypt4 decryption, dual HWID headers, and diagnosis symptom collapsing
This commit is contained in:
@@ -7,8 +7,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: TACHYON\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-09-17 12:45+0300\n"
|
||||
"PO-Revision-Date: 2026-09-17 12:45+0300\n"
|
||||
"POT-Creation-Date: 2026-09-17 15:04+0300\n"
|
||||
"PO-Revision-Date: 2026-09-17 15:04+0300\n"
|
||||
"Last-Translator: Dushnilin <dushnilin@gmail.com>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"Language: \n"
|
||||
|
||||
@@ -7,8 +7,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: TACHYON\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-09-17 15:45+0300\n"
|
||||
"PO-Revision-Date: 2026-09-17 15:45+0300\n"
|
||||
"POT-Creation-Date: 2026-09-17 18:04+0300\n"
|
||||
"PO-Revision-Date: 2026-09-17 18:04+0300\n"
|
||||
"Last-Translator: Dushnilin\n"
|
||||
"Language-Team: none\n"
|
||||
"Language: ru\n"
|
||||
@@ -273,10 +273,10 @@ msgid "Add URLs or local paths to .lst lists containing domains. IP entries are
|
||||
msgstr "Добавьте URL или локальные пути к спискам .lst, содержащим домены. IP-записи игнорируются."
|
||||
|
||||
msgid "Add URLs or local paths to .srs / .json / .lst / .txt lists. Only domain rules are supported."
|
||||
msgstr ""
|
||||
msgstr "Укажите URL или локальные пути к спискам .srs / .json / .lst / .txt. Поддерживаются только правила для доменов."
|
||||
|
||||
msgid "Add URLs or local paths to .srs / .json / .lst / .txt lists. Subnets are ignored by default."
|
||||
msgstr ""
|
||||
msgstr "Укажите URL или локальные пути к спискам .srs / .json / .lst / .txt. Подсети по умолчанию игнорируются."
|
||||
|
||||
msgid "Add URLTest / Priority group"
|
||||
msgstr "Добавить группу URLTest / Приоритет"
|
||||
@@ -1395,7 +1395,7 @@ msgid "Direct connection via ISP"
|
||||
msgstr "Прямое подключение через провайдера"
|
||||
|
||||
msgid "Direct Proxy"
|
||||
msgstr "Прямой прокси"
|
||||
msgstr "Direct Proxy"
|
||||
|
||||
msgid "Direct WAN"
|
||||
msgstr "Прямой WAN"
|
||||
@@ -1440,7 +1440,7 @@ msgid "DNS Blocking"
|
||||
msgstr "Блокировка DNS"
|
||||
|
||||
msgid "DNS Cache Size"
|
||||
msgstr ""
|
||||
msgstr "Размер кэша DNS"
|
||||
|
||||
msgid "DNS Check Interval"
|
||||
msgstr "Интервал проверки DNS"
|
||||
@@ -1797,7 +1797,7 @@ msgid "Enabled (always prioritize)"
|
||||
msgstr "Включено (всегда повышать приоритет)"
|
||||
|
||||
msgid "Enables high-capacity local DNS caching in dnsmasq to accelerate DNS queries and absorb transient latency during service restarts."
|
||||
msgstr ""
|
||||
msgstr "Включает емкий локальный кэш DNS в dnsmasq для ускорения запросов и устранения задержек при перезапуске служб."
|
||||
|
||||
msgid "Enables the background daemon that polls Telegram for commands and sends notifications."
|
||||
msgstr "Включает фоновый демон, который опрашивает Telegram на наличие команд и отправляет уведомления."
|
||||
@@ -3102,7 +3102,7 @@ msgid "Local Address"
|
||||
msgstr "Локальный адрес"
|
||||
|
||||
msgid "Local DNS Cache"
|
||||
msgstr ""
|
||||
msgstr "Локальный кэш DNS"
|
||||
|
||||
msgid "Local SOCKS proxy desynchronizer (ciadpi)"
|
||||
msgstr "Локальный десинхронизатор SOCKS-прокси (ciadpi)"
|
||||
@@ -3480,7 +3480,7 @@ msgid "Notify on Subscription Updates"
|
||||
msgstr "Уведомлять об обновлениях подписок"
|
||||
|
||||
msgid "Number of cached DNS domain entries in dnsmasq (default: 10000)."
|
||||
msgstr ""
|
||||
msgstr "Количество кэшируемых доменных записей в dnsmasq (по умолчанию: 10000)."
|
||||
|
||||
msgid "Number of consecutive failed checks required before switching DNS servers."
|
||||
msgstr "Количество последовательных неудачных проверок до переключения DNS-серверов."
|
||||
@@ -4287,7 +4287,7 @@ msgid "Rule Name"
|
||||
msgstr "Название правила"
|
||||
|
||||
msgid "Rule set must be an HTTP(S) URL or a local .srs / .json / .lst / .txt path"
|
||||
msgstr ""
|
||||
msgstr "Набор правил должен быть URL HTTP(S) или локальным путем к .srs / .json / .lst / .txt"
|
||||
|
||||
msgid "Rule set settings"
|
||||
msgstr "Настройки набора правил"
|
||||
@@ -5997,7 +5997,7 @@ msgid "Add URLs or local paths to .srs / .json lists. Subnets are ignored by def
|
||||
msgstr "Добавьте URL или локальные пути к спискам .srs / .json. Подсети игнорируются по умолчанию."
|
||||
|
||||
msgid "Plain .lst / .txt lists belong in \"Domain and IP lists\", not \"Rule sets\""
|
||||
msgstr ""
|
||||
msgstr "Текстовые списки .lst / .txt относятся к «Спискам доменов и IP», а не к «Наборам правил»"
|
||||
|
||||
msgid "Rule set must be an HTTP(S) URL or a local .srs / .json path"
|
||||
msgstr "Набор правил должен быть HTTP(S) URL или локальным путём .srs / .json"
|
||||
|
||||
@@ -7,8 +7,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: TACHYON\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-09-17 15:45+0300\n"
|
||||
"PO-Revision-Date: 2026-09-17 15:45+0300\n"
|
||||
"POT-Creation-Date: 2026-09-17 18:04+0300\n"
|
||||
"PO-Revision-Date: 2026-09-17 18:04+0300\n"
|
||||
"Last-Translator: Dushnilin\n"
|
||||
"Language-Team: none\n"
|
||||
"Language: ru\n"
|
||||
@@ -273,10 +273,10 @@ msgid "Add URLs or local paths to .lst lists containing domains. IP entries are
|
||||
msgstr "Добавьте URL или локальные пути к спискам .lst, содержащим домены. IP-записи игнорируются."
|
||||
|
||||
msgid "Add URLs or local paths to .srs / .json / .lst / .txt lists. Only domain rules are supported."
|
||||
msgstr ""
|
||||
msgstr "Укажите URL или локальные пути к спискам .srs / .json / .lst / .txt. Поддерживаются только правила для доменов."
|
||||
|
||||
msgid "Add URLs or local paths to .srs / .json / .lst / .txt lists. Subnets are ignored by default."
|
||||
msgstr ""
|
||||
msgstr "Укажите URL или локальные пути к спискам .srs / .json / .lst / .txt. Подсети по умолчанию игнорируются."
|
||||
|
||||
msgid "Add URLTest / Priority group"
|
||||
msgstr "Добавить группу URLTest / Приоритет"
|
||||
@@ -1395,7 +1395,7 @@ msgid "Direct connection via ISP"
|
||||
msgstr "Прямое подключение через провайдера"
|
||||
|
||||
msgid "Direct Proxy"
|
||||
msgstr "Прямой прокси"
|
||||
msgstr "Direct Proxy"
|
||||
|
||||
msgid "Direct WAN"
|
||||
msgstr "Прямой WAN"
|
||||
@@ -1440,7 +1440,7 @@ msgid "DNS Blocking"
|
||||
msgstr "Блокировка DNS"
|
||||
|
||||
msgid "DNS Cache Size"
|
||||
msgstr ""
|
||||
msgstr "Размер кэша DNS"
|
||||
|
||||
msgid "DNS Check Interval"
|
||||
msgstr "Интервал проверки DNS"
|
||||
@@ -1797,7 +1797,7 @@ msgid "Enabled (always prioritize)"
|
||||
msgstr "Включено (всегда повышать приоритет)"
|
||||
|
||||
msgid "Enables high-capacity local DNS caching in dnsmasq to accelerate DNS queries and absorb transient latency during service restarts."
|
||||
msgstr ""
|
||||
msgstr "Включает емкий локальный кэш DNS в dnsmasq для ускорения запросов и устранения задержек при перезапуске служб."
|
||||
|
||||
msgid "Enables the background daemon that polls Telegram for commands and sends notifications."
|
||||
msgstr "Включает фоновый демон, который опрашивает Telegram на наличие команд и отправляет уведомления."
|
||||
@@ -3102,7 +3102,7 @@ msgid "Local Address"
|
||||
msgstr "Локальный адрес"
|
||||
|
||||
msgid "Local DNS Cache"
|
||||
msgstr ""
|
||||
msgstr "Локальный кэш DNS"
|
||||
|
||||
msgid "Local SOCKS proxy desynchronizer (ciadpi)"
|
||||
msgstr "Локальный десинхронизатор SOCKS-прокси (ciadpi)"
|
||||
@@ -3480,7 +3480,7 @@ msgid "Notify on Subscription Updates"
|
||||
msgstr "Уведомлять об обновлениях подписок"
|
||||
|
||||
msgid "Number of cached DNS domain entries in dnsmasq (default: 10000)."
|
||||
msgstr ""
|
||||
msgstr "Количество кэшируемых доменных записей в dnsmasq (по умолчанию: 10000)."
|
||||
|
||||
msgid "Number of consecutive failed checks required before switching DNS servers."
|
||||
msgstr "Количество последовательных неудачных проверок до переключения DNS-серверов."
|
||||
@@ -4287,7 +4287,7 @@ msgid "Rule Name"
|
||||
msgstr "Название правила"
|
||||
|
||||
msgid "Rule set must be an HTTP(S) URL or a local .srs / .json / .lst / .txt path"
|
||||
msgstr ""
|
||||
msgstr "Набор правил должен быть URL HTTP(S) или локальным путем к .srs / .json / .lst / .txt"
|
||||
|
||||
msgid "Rule set settings"
|
||||
msgstr "Настройки набора правил"
|
||||
@@ -5997,7 +5997,7 @@ msgid "Add URLs or local paths to .srs / .json lists. Subnets are ignored by def
|
||||
msgstr "Добавьте URL или локальные пути к спискам .srs / .json. Подсети игнорируются по умолчанию."
|
||||
|
||||
msgid "Plain .lst / .txt lists belong in \"Domain and IP lists\", not \"Rule sets\""
|
||||
msgstr ""
|
||||
msgstr "Текстовые списки .lst / .txt относятся к «Спискам доменов и IP», а не к «Наборам правил»"
|
||||
|
||||
msgid "Rule set must be an HTTP(S) URL or a local .srs / .json path"
|
||||
msgstr "Набор правил должен быть HTTP(S) URL или локальным путём .srs / .json"
|
||||
|
||||
@@ -7,8 +7,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: TACHYON\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-09-17 12:45+0300\n"
|
||||
"PO-Revision-Date: 2026-09-17 12:45+0300\n"
|
||||
"POT-Creation-Date: 2026-09-17 15:04+0300\n"
|
||||
"PO-Revision-Date: 2026-09-17 15:04+0300\n"
|
||||
"Last-Translator: Dushnilin <dushnilin@gmail.com>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"Language: \n"
|
||||
|
||||
@@ -4692,17 +4692,52 @@ function local_rule_doctor(pre_res, pre_verify) {
|
||||
}
|
||||
}
|
||||
|
||||
// ── 1. End-to-end live verification ──
|
||||
let wan_cause_added = false;
|
||||
// ── 1. End-to-end live verification & root-cause correlation ──
|
||||
let wan_failed = false;
|
||||
let singbox_failed = false;
|
||||
for (let c in verify.checks) {
|
||||
if (c.status == "fail") {
|
||||
if (c.name == "WAN interface" || c.name == "Default gateway")
|
||||
wan_failed = true;
|
||||
if (c.name == "sing-box process")
|
||||
singbox_failed = true;
|
||||
}
|
||||
}
|
||||
|
||||
let wan_cause = null;
|
||||
let singbox_cause = null;
|
||||
|
||||
if (wan_failed) {
|
||||
wan_cause = {
|
||||
probability: 95,
|
||||
cause: lang == "en" ? "WAN default gateway or Internet uplink is unreachable" : "Шлюз по умолчанию или внешний интернет недоступен",
|
||||
fix: "fix_wan_interface",
|
||||
symptoms: []
|
||||
};
|
||||
push(causes, wan_cause);
|
||||
add_fix("fix_wan_interface");
|
||||
} else if (singbox_failed) {
|
||||
singbox_cause = {
|
||||
probability: 95,
|
||||
cause: lang == "en" ? "sing-box process is stopped or non-functional" : "Процесс sing-box остановлен или не функционирует",
|
||||
fix: "start_singbox",
|
||||
symptoms: []
|
||||
};
|
||||
push(causes, singbox_cause);
|
||||
add_fix("start_singbox");
|
||||
}
|
||||
|
||||
for (let c in verify.checks) {
|
||||
if (c.status != "fail") continue;
|
||||
if (c.name == "sing-box process") {
|
||||
push(causes, {
|
||||
probability: 95,
|
||||
cause: lang == "en" ? "sing-box process is stopped or non-functional" : "Процесс sing-box остановлен или не функционирует",
|
||||
fix: "start_singbox"
|
||||
});
|
||||
add_fix("start_singbox");
|
||||
if (!singbox_cause && !wan_cause) {
|
||||
push(causes, {
|
||||
probability: 95,
|
||||
cause: lang == "en" ? "sing-box process is stopped or non-functional" : "Процесс sing-box остановлен или не функционирует",
|
||||
fix: "start_singbox"
|
||||
});
|
||||
add_fix("start_singbox");
|
||||
}
|
||||
} else if (c.name == "LAN DNS via dnsmasq") {
|
||||
push(causes, {
|
||||
probability: 85,
|
||||
@@ -4711,31 +4746,37 @@ function local_rule_doctor(pre_res, pre_verify) {
|
||||
});
|
||||
add_fix("fix_dnsmasq");
|
||||
} else if (c.name == "Proxy DNS via sing-box") {
|
||||
push(causes, {
|
||||
probability: 85,
|
||||
cause: lang == "en" ? "Proxy DNS via sing-box failed to respond" : "Прокси-DNS через sing-box (127.0.0.42) не отвечает",
|
||||
fix: "clear_dns_cache"
|
||||
});
|
||||
add_fix("clear_dns_cache");
|
||||
if (cfg.dns_type != "doh") {
|
||||
add_fix("switch_to_doh");
|
||||
if (wan_cause) {
|
||||
push(wan_cause.symptoms, lang == "en" ? "Proxy DNS unavailable (WAN down)" : "Прокси-DNS недоступен (нет связи с WAN)");
|
||||
} else if (singbox_cause) {
|
||||
push(singbox_cause.symptoms, lang == "en" ? "Proxy DNS not answering (sing-box stopped)" : "Прокси-DNS не отвечает (sing-box остановлен)");
|
||||
} else {
|
||||
push(causes, {
|
||||
probability: 85,
|
||||
cause: lang == "en" ? "Proxy DNS via sing-box failed to respond" : "Прокси-DNS через sing-box (127.0.0.42) не отвечает",
|
||||
fix: "clear_dns_cache"
|
||||
});
|
||||
add_fix("clear_dns_cache");
|
||||
if (cfg.dns_type != "doh") {
|
||||
add_fix("switch_to_doh");
|
||||
}
|
||||
}
|
||||
} else if ((c.name == "WAN interface" || c.name == "Default gateway") && !wan_cause_added) {
|
||||
wan_cause_added = true;
|
||||
push(causes, {
|
||||
probability: 90,
|
||||
cause: lang == "en" ? "WAN default gateway or Internet uplink is unreachable" : "Шлюз по умолчанию или внешний интернет недоступен",
|
||||
fix: "fix_wan_interface"
|
||||
});
|
||||
add_fix("fix_wan_interface");
|
||||
} else if (c.name == "WAN interface" || c.name == "Default gateway") {
|
||||
// Already added as primary root cause if wan_failed
|
||||
} else if (c.name == "HTTP via proxy") {
|
||||
push(causes, {
|
||||
probability: 88,
|
||||
cause: lang == "en" ? "HTTP through the proxy fails end-to-end (node offline or blocked)" : "HTTP через прокси не проходит (узел офлайн или заблокирован)",
|
||||
fix: "update_subscriptions"
|
||||
});
|
||||
add_fix("update_subscriptions");
|
||||
add_fix("restart_zapret");
|
||||
if (wan_cause) {
|
||||
push(wan_cause.symptoms, lang == "en" ? "HTTP through proxy unreachable (WAN down)" : "HTTP через прокси недоступен (нет связи с WAN)");
|
||||
} else if (singbox_cause) {
|
||||
push(singbox_cause.symptoms, lang == "en" ? "HTTP through proxy fails (sing-box stopped)" : "HTTP через прокси не проходит (sing-box остановлен)");
|
||||
} else {
|
||||
push(causes, {
|
||||
probability: 88,
|
||||
cause: lang == "en" ? "HTTP through the proxy fails end-to-end (node offline or blocked)" : "HTTP через прокси не проходит (узел офлайн или заблокирован)",
|
||||
fix: "update_subscriptions"
|
||||
});
|
||||
add_fix("update_subscriptions");
|
||||
add_fix("restart_zapret");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4856,7 +4897,10 @@ function local_rule_doctor(pre_res, pre_verify) {
|
||||
} else {
|
||||
push(report_lines, "#### Root Cause Analysis:");
|
||||
for (let c in causes) {
|
||||
push(report_lines, sprintf("- [%d%% Probability] %s", c.probability, c.cause));
|
||||
let line = sprintf("- [%d%% Probability] %s", c.probability, c.cause);
|
||||
if (c.symptoms && length(c.symptoms) > 0)
|
||||
line += " (Symptoms: " + join("; ", c.symptoms) + ")";
|
||||
push(report_lines, line);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -4873,7 +4917,10 @@ function local_rule_doctor(pre_res, pre_verify) {
|
||||
} else {
|
||||
push(report_lines, "#### Анализ возможных причин сбоя:");
|
||||
for (let c in causes) {
|
||||
push(report_lines, sprintf("- [%d%% вероятность] %s", c.probability, c.cause));
|
||||
let line = sprintf("- [%d%% вероятность] %s", c.probability, c.cause);
|
||||
if (c.symptoms && length(c.symptoms) > 0)
|
||||
line += " (Симптомы: " + join("; ", c.symptoms) + ")";
|
||||
push(report_lines, line);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ let command_output_from_args = common.command_output_from_args;
|
||||
let command_success_from_args = common.command_success_from_args;
|
||||
let connections = require("config.connections");
|
||||
let subscription_share_link = require("subscription.share_link");
|
||||
let crypt4 = require("subscription.crypt4");
|
||||
|
||||
function core_url_module_or_null() {
|
||||
try {
|
||||
@@ -1535,6 +1536,22 @@ function get_subscription_hwid(custom_hwid) {
|
||||
}
|
||||
|
||||
function download_subscription(url, filepath, http_proxy_address, headers_filepath, effective_user_agent, effective_hwid, device_headers, allow_insecure) {
|
||||
if (crypt4.is_crypt4(url)) {
|
||||
let dec = crypt4.decrypt(url, effective_hwid, generate_hwid());
|
||||
if (dec != null && dec != "") {
|
||||
let stamp = clock();
|
||||
let suffix = sprintf(".part.%d.%d", stamp[0], stamp[1]);
|
||||
let tmpfile = filepath + suffix;
|
||||
if (fs.writefile(tmpfile, dec)) {
|
||||
move_file(tmpfile, filepath);
|
||||
if (headers_filepath != "")
|
||||
unlink_path(headers_filepath);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
let retries = 3;
|
||||
let wait_seconds = 2;
|
||||
let timeout = 15;
|
||||
@@ -1579,9 +1596,11 @@ function download_subscription(url, filepath, http_proxy_address, headers_filepa
|
||||
|
||||
push(args, "-o");
|
||||
push(args, tmpfile);
|
||||
let hwid_val = get_subscription_hwid(effective_hwid);
|
||||
let request_headers = [
|
||||
"User-Agent: " + get_subscription_user_agent(effective_user_agent),
|
||||
"X-HWID: " + get_subscription_hwid(effective_hwid)
|
||||
"X-HWID: " + hwid_val,
|
||||
"HWID: " + hwid_val
|
||||
];
|
||||
for (let header in device_request_headers(device_headers))
|
||||
push(request_headers, header);
|
||||
@@ -1593,6 +1612,12 @@ function download_subscription(url, filepath, http_proxy_address, headers_filepa
|
||||
|
||||
let status = command_status_from_args(args);
|
||||
if (status == 0 && file_nonempty(tmpfile)) {
|
||||
let downloaded_body = fs.readfile(tmpfile);
|
||||
if (downloaded_body != null && crypt4.is_crypt4(downloaded_body)) {
|
||||
let dec = crypt4.decrypt(downloaded_body, effective_hwid, generate_hwid());
|
||||
if (dec != null && dec != "")
|
||||
fs.writefile(tmpfile, dec);
|
||||
}
|
||||
move_file(tmpfile, filepath);
|
||||
if (headers_filepath != "") {
|
||||
if (file_nonempty(headers_tmpfile))
|
||||
|
||||
@@ -0,0 +1,355 @@
|
||||
#!/usr/bin/env ucode
|
||||
|
||||
let fs = require("fs");
|
||||
let common = require("core.common");
|
||||
let as_string = common.as_string;
|
||||
let shell_quote = common.shell_quote;
|
||||
|
||||
function trim(str) {
|
||||
return replace(as_string(str), /^[\s\r\n]+|[\s\r\n]+$/g, "");
|
||||
}
|
||||
|
||||
function is_crypt4(value) {
|
||||
value = lc(trim(value));
|
||||
return match(value, /^(happ:\/\/)?crypt4\//) != null;
|
||||
}
|
||||
|
||||
function strip_prefix(value) {
|
||||
value = trim(value);
|
||||
if (match(value, /^(happ:\/\/)?crypt4\//i))
|
||||
return replace(value, /^(happ:\/\/)?crypt4\//i, "");
|
||||
return value;
|
||||
}
|
||||
|
||||
function hex_to_bytes(h) {
|
||||
let res = "";
|
||||
for (let i = 0; i < length(h); i += 2)
|
||||
res += chr(hex(substr(h, i, 2)));
|
||||
return res;
|
||||
}
|
||||
|
||||
function sha256_hex(str) {
|
||||
let p = fs.popen("printf %s " + shell_quote(str) + " | sha256sum 2>/dev/null", "r");
|
||||
if (!p)
|
||||
return "";
|
||||
let out = trim(p.read("all") || "");
|
||||
p.close();
|
||||
return split(out, /[ \t]+/)[0];
|
||||
}
|
||||
|
||||
// Inverted S-Box for AES
|
||||
let InvSbox = [
|
||||
0x52, 0x09, 0x6a, 0xd5, 0x30, 0x36, 0xa5, 0x38, 0xbf, 0x40, 0xa3, 0x9e, 0x81, 0xf3, 0xd7, 0xfb,
|
||||
0x7c, 0xe3, 0x39, 0x82, 0x9b, 0x2f, 0xff, 0x87, 0x34, 0x8e, 0x43, 0x44, 0xc4, 0xde, 0xe9, 0xcb,
|
||||
0x54, 0x7b, 0x94, 0x32, 0xa6, 0xc2, 0x23, 0x3d, 0xee, 0x4c, 0x95, 0x0b, 0x42, 0xfa, 0xc3, 0x4e,
|
||||
0x08, 0x2e, 0xa1, 0x66, 0x28, 0xd9, 0x24, 0xb2, 0x76, 0x5b, 0xa2, 0x49, 0x6d, 0x8b, 0xd1, 0x25,
|
||||
0x72, 0xf8, 0xf6, 0x64, 0x86, 0x68, 0x98, 0x16, 0xd4, 0xa4, 0x5c, 0xcc, 0x5d, 0x65, 0xb6, 0x92,
|
||||
0x6c, 0x70, 0x48, 0x50, 0xfd, 0xed, 0xb9, 0xda, 0x5e, 0x15, 0x46, 0x57, 0xa7, 0x8d, 0x9d, 0x84,
|
||||
0x90, 0xd8, 0xab, 0x00, 0x8c, 0xbc, 0xd3, 0x0a, 0xf7, 0xe4, 0x58, 0x05, 0xb8, 0xb3, 0x45, 0x06,
|
||||
0xd0, 0x2c, 0x1e, 0x8f, 0xca, 0x3f, 0x0f, 0x02, 0xc1, 0xaf, 0xbd, 0x03, 0x01, 0x13, 0x8a, 0x6b,
|
||||
0x3a, 0x91, 0x11, 0x41, 0x4f, 0x67, 0xdc, 0xea, 0x97, 0xf2, 0xcf, 0xce, 0xf0, 0xb4, 0xe6, 0x73,
|
||||
0x96, 0xac, 0x74, 0x22, 0xe7, 0xad, 0x35, 0x85, 0xe2, 0xf9, 0x37, 0xe8, 0x1c, 0x75, 0xdf, 0x6e,
|
||||
0x47, 0xf1, 0x1a, 0x71, 0x1d, 0x29, 0xc5, 0x89, 0x6f, 0xb7, 0x62, 0x0e, 0xaa, 0x18, 0xbe, 0x1b,
|
||||
0xfc, 0x56, 0x3e, 0x4b, 0xc6, 0xd2, 0x79, 0x20, 0x9a, 0xdb, 0xc0, 0xfe, 0x78, 0xcd, 0x5a, 0xf4,
|
||||
0x1f, 0xdd, 0xa8, 0x33, 0x88, 0x07, 0xc7, 0x31, 0xb1, 0x12, 0x10, 0x59, 0x27, 0x80, 0xec, 0x5f,
|
||||
0x60, 0x51, 0x7f, 0xa9, 0x19, 0xb5, 0x4a, 0x0d, 0x2d, 0xe5, 0x7a, 0x9f, 0x93, 0xc9, 0x9c, 0xef,
|
||||
0xa0, 0xe0, 0x3b, 0x4d, 0xae, 0x2a, 0xf5, 0xb0, 0xc8, 0xeb, 0xbb, 0x3c, 0x83, 0x53, 0x99, 0x61,
|
||||
0x17, 0x2b, 0x04, 0x7e, 0xba, 0x77, 0xd6, 0x26, 0xe1, 0x69, 0x14, 0x63, 0x55, 0x21, 0x0c, 0x7d
|
||||
];
|
||||
|
||||
// Forward S-box for key expansion
|
||||
let Sbox = [
|
||||
0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5, 0x30, 0x01, 0x67, 0x2b, 0xfe, 0xd7, 0xab, 0x76,
|
||||
0xca, 0x82, 0xc9, 0x7d, 0xfa, 0x59, 0x47, 0xf0, 0xad, 0xd4, 0xa2, 0xaf, 0x9c, 0xa4, 0x72, 0xc0,
|
||||
0xb7, 0xfd, 0x93, 0x26, 0x36, 0x3f, 0xf7, 0xcc, 0x34, 0xa5, 0xe5, 0xf1, 0x71, 0xd8, 0x31, 0x15,
|
||||
0x04, 0xc7, 0x23, 0xc3, 0x18, 0x96, 0x05, 0x9a, 0x07, 0x12, 0x80, 0xe2, 0xeb, 0x27, 0xb2, 0x75,
|
||||
0x09, 0x83, 0x2c, 0x1a, 0x1b, 0x6e, 0x5a, 0xa0, 0x52, 0x3b, 0xd6, 0xb3, 0x29, 0xe3, 0x2f, 0x84,
|
||||
0x53, 0xd1, 0x00, 0xed, 0x20, 0xfc, 0xb1, 0x5b, 0x6a, 0xcb, 0xbe, 0x39, 0x4a, 0x4c, 0x58, 0xcf,
|
||||
0xd0, 0xef, 0xaa, 0xfb, 0x43, 0x4d, 0x33, 0x85, 0x45, 0xf9, 0x02, 0x7f, 0x50, 0x3c, 0x9f, 0xa8,
|
||||
0x51, 0xa3, 0x40, 0x8f, 0x92, 0x9d, 0x38, 0xf5, 0xbc, 0xb6, 0xda, 0x21, 0x10, 0xff, 0xf3, 0xd2,
|
||||
0xcd, 0x0c, 0x13, 0xec, 0x5f, 0x97, 0x44, 0x17, 0xc4, 0xa7, 0x7e, 0x3d, 0x64, 0x5d, 0x19, 0x73,
|
||||
0x60, 0x81, 0x4f, 0xdc, 0x22, 0x2a, 0x90, 0x88, 0x46, 0xee, 0xb8, 0x14, 0xde, 0x5e, 0x0b, 0xdb,
|
||||
0xe0, 0x32, 0x3a, 0x0a, 0x49, 0x06, 0x24, 0x5c, 0xc2, 0xd3, 0xac, 0x62, 0x91, 0x95, 0xe4, 0x79,
|
||||
0xe7, 0xc8, 0x37, 0x6d, 0x8d, 0xd5, 0x4e, 0xa9, 0x6c, 0x56, 0xf4, 0xea, 0x65, 0x7a, 0xae, 0x08,
|
||||
0xba, 0x78, 0x25, 0x2e, 0x1c, 0xa6, 0xb4, 0xc6, 0xe8, 0xdd, 0x74, 0x1f, 0x4b, 0xbd, 0x8b, 0x8a,
|
||||
0x70, 0x3e, 0xb5, 0x66, 0x48, 0x03, 0xf6, 0x0e, 0x61, 0x35, 0x57, 0xb9, 0x86, 0xc1, 0x1d, 0x9e,
|
||||
0xe1, 0xf8, 0x98, 0x11, 0x69, 0xd9, 0x8e, 0x94, 0x9b, 0x1e, 0x87, 0xe9, 0xce, 0x55, 0x28, 0xdf,
|
||||
0x8c, 0xa1, 0x89, 0x0d, 0xbf, 0xe6, 0x42, 0x68, 0x41, 0x99, 0x2d, 0x0f, 0xb0, 0x54, 0xbb, 0x16
|
||||
];
|
||||
|
||||
let Rcon = [0x00, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36];
|
||||
|
||||
let Mul9 = [], Mul11 = [], Mul13 = [], Mul14 = [];
|
||||
function gmul(a, b) {
|
||||
let p = 0;
|
||||
for (let c = 0; c < 8; c++) {
|
||||
if ((b & 1) != 0) p ^= a;
|
||||
let hi = (a & 0x80);
|
||||
a = (a << 1) & 0xff;
|
||||
if (hi != 0) a ^= 0x1b;
|
||||
b >>= 1;
|
||||
}
|
||||
return p;
|
||||
}
|
||||
for (let i = 0; i < 256; i++) {
|
||||
push(Mul9, gmul(i, 9));
|
||||
push(Mul11, gmul(i, 11));
|
||||
push(Mul13, gmul(i, 13));
|
||||
push(Mul14, gmul(i, 14));
|
||||
}
|
||||
|
||||
function key_expansion(key_bytes) {
|
||||
let Nk = length(key_bytes) / 4;
|
||||
let Nr = Nk + 6;
|
||||
let w = [];
|
||||
for (let i = 0; i < length(key_bytes); i += 4)
|
||||
push(w, [ord(key_bytes, i), ord(key_bytes, i + 1), ord(key_bytes, i + 2), ord(key_bytes, i + 3)]);
|
||||
|
||||
for (let i = Nk; i < 4 * (Nr + 1); i++) {
|
||||
let temp = [w[i - 1][0], w[i - 1][1], w[i - 1][2], w[i - 1][3]];
|
||||
if (i % Nk == 0) {
|
||||
let t0 = temp[0];
|
||||
temp[0] = Sbox[temp[1]] ^ Rcon[i / Nk];
|
||||
temp[1] = Sbox[temp[2]];
|
||||
temp[2] = Sbox[temp[3]];
|
||||
temp[3] = Sbox[t0];
|
||||
} else if (Nk > 6 && (i % Nk == 4)) {
|
||||
temp[0] = Sbox[temp[0]];
|
||||
temp[1] = Sbox[temp[1]];
|
||||
temp[2] = Sbox[temp[2]];
|
||||
temp[3] = Sbox[temp[3]];
|
||||
}
|
||||
push(w, [w[i - Nk][0] ^ temp[0], w[i - Nk][1] ^ temp[1], w[i - Nk][2] ^ temp[2], w[i - Nk][3] ^ temp[3]]);
|
||||
}
|
||||
return { w, Nr };
|
||||
}
|
||||
|
||||
function inv_cipher_block(block, exp) {
|
||||
let w = exp.w;
|
||||
let Nr = exp.Nr;
|
||||
let s = [];
|
||||
for (let r = 0; r < 4; r++) {
|
||||
s[r] = [];
|
||||
for (let c = 0; c < 4; c++)
|
||||
s[r][c] = ord(block, r + 4 * c);
|
||||
}
|
||||
|
||||
for (let c = 0; c < 4; c++)
|
||||
for (let r = 0; r < 4; r++)
|
||||
s[r][c] ^= w[Nr * 4 + c][r];
|
||||
|
||||
for (let round = Nr - 1; round >= 1; round--) {
|
||||
let t1 = s[1][3]; s[1][3] = s[1][2]; s[1][2] = s[1][1]; s[1][1] = s[1][0]; s[1][0] = t1;
|
||||
let t20 = s[2][0], t21 = s[2][1]; s[2][0] = s[2][2]; s[2][1] = s[2][3]; s[2][2] = t20; s[2][3] = t21;
|
||||
let t3 = s[3][0]; s[3][0] = s[3][1]; s[3][1] = s[3][2]; s[3][2] = s[3][3]; s[3][3] = t3;
|
||||
|
||||
for (let r = 0; r < 4; r++)
|
||||
for (let c = 0; c < 4; c++)
|
||||
s[r][c] = InvSbox[s[r][c]];
|
||||
|
||||
for (let c = 0; c < 4; c++)
|
||||
for (let r = 0; r < 4; r++)
|
||||
s[r][c] ^= w[round * 4 + c][r];
|
||||
|
||||
for (let c = 0; c < 4; c++) {
|
||||
let s0 = s[0][c], s1 = s[1][c], s2 = s[2][c], s3 = s[3][c];
|
||||
s[0][c] = Mul14[s0] ^ Mul11[s1] ^ Mul13[s2] ^ Mul9[s3];
|
||||
s[1][c] = Mul9[s0] ^ Mul14[s1] ^ Mul11[s2] ^ Mul13[s3];
|
||||
s[2][c] = Mul13[s0] ^ Mul9[s1] ^ Mul14[s2] ^ Mul11[s3];
|
||||
s[3][c] = Mul11[s0] ^ Mul13[s1] ^ Mul9[s2] ^ Mul14[s3];
|
||||
}
|
||||
}
|
||||
|
||||
let t1 = s[1][3]; s[1][3] = s[1][2]; s[1][2] = s[1][1]; s[1][1] = s[1][0]; s[1][0] = t1;
|
||||
let t20 = s[2][0], t21 = s[2][1]; s[2][0] = s[2][2]; s[2][1] = s[2][3]; s[2][2] = t20; s[2][3] = t21;
|
||||
let t3 = s[3][0]; s[3][0] = s[3][1]; s[3][1] = s[3][2]; s[3][2] = s[3][3]; s[3][3] = t3;
|
||||
|
||||
for (let r = 0; r < 4; r++)
|
||||
for (let c = 0; c < 4; c++)
|
||||
s[r][c] = InvSbox[s[r][c]];
|
||||
|
||||
for (let c = 0; c < 4; c++)
|
||||
for (let r = 0; r < 4; r++)
|
||||
s[r][c] ^= w[c][r];
|
||||
|
||||
let out = "";
|
||||
for (let c = 0; c < 4; c++)
|
||||
for (let r = 0; r < 4; r++)
|
||||
out += chr(s[r][c]);
|
||||
return out;
|
||||
}
|
||||
|
||||
function aes_cbc_decrypt(ciphertext, key_bytes, iv) {
|
||||
if (length(ciphertext) % 16 != 0 || length(key_bytes) < 16 || length(iv) != 16)
|
||||
return null;
|
||||
|
||||
let exp = key_expansion(key_bytes);
|
||||
let plaintext = "";
|
||||
let prev = iv;
|
||||
for (let i = 0; i < length(ciphertext); i += 16) {
|
||||
let block = substr(ciphertext, i, 16);
|
||||
let dec = inv_cipher_block(block, exp);
|
||||
let xored = "";
|
||||
for (let j = 0; j < 16; j++)
|
||||
xored += chr(ord(dec, j) ^ ord(prev, j));
|
||||
plaintext += xored;
|
||||
prev = block;
|
||||
}
|
||||
|
||||
// PKCS#7 unpadding validation
|
||||
let pad = ord(plaintext, length(plaintext) - 1);
|
||||
if (pad < 1 || pad > 16 || length(plaintext) < pad)
|
||||
return null;
|
||||
for (let i = length(plaintext) - pad; i < length(plaintext); i++) {
|
||||
if (ord(plaintext, i) != pad)
|
||||
return null;
|
||||
}
|
||||
return substr(plaintext, 0, length(plaintext) - pad);
|
||||
}
|
||||
|
||||
function is_valid_plaintext(str) {
|
||||
if (str == null || length(str) == 0)
|
||||
return false;
|
||||
let len = length(str);
|
||||
for (let i = 0; i < len; i++) {
|
||||
let ch = ord(substr(str, i, 1));
|
||||
if (ch < 32 && ch != 9 && ch != 10 && ch != 13)
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function try_openssl_decrypt(cipher, key_hex, iv_hex, cipher_type) {
|
||||
let tmp_in = "/tmp/tachyon-c4-in." + clock()[0] + "." + clock()[1];
|
||||
if (fs.writefile(tmp_in, cipher) == null)
|
||||
return null;
|
||||
|
||||
let cmd = "openssl enc -d -" + cipher_type + " -in " + shell_quote(tmp_in) + " -K " + shell_quote(key_hex) + " -iv " + shell_quote(iv_hex) + " 2>/dev/null";
|
||||
let p = fs.popen(cmd, "r");
|
||||
let out = p ? p.read("all") : null;
|
||||
let code = p ? p.close() : -1;
|
||||
fs.unlink(tmp_in);
|
||||
|
||||
if (code != 0 || out == null || length(out) == 0 || !is_valid_plaintext(out))
|
||||
return null;
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
function get_system_device_hwid() {
|
||||
let mac = trim(as_string(fs.readfile("/sys/class/net/eth0/address")));
|
||||
if (mac == "")
|
||||
mac = trim(as_string(fs.readfile("/sys/class/net/br-lan/address")));
|
||||
if (mac == "") {
|
||||
let mid = trim(as_string(fs.readfile("/etc/machine-id")));
|
||||
if (mid != "") return mid;
|
||||
}
|
||||
if (mac == "") return "";
|
||||
let p = fs.popen("printf %s " + shell_quote(mac) + " | md5sum 2>/dev/null", "r");
|
||||
if (!p) return "";
|
||||
let out = trim(p.read("all") || "");
|
||||
p.close();
|
||||
let hash = split(out, /[ \t]+/)[0];
|
||||
if (length(hash) >= 16)
|
||||
return substr(hash, 0, 4) + "-" + substr(hash, 4, 4) + "-" + substr(hash, 8, 4) + "-" + substr(hash, 12, 4);
|
||||
return "";
|
||||
}
|
||||
|
||||
function decrypt_crypt4(payload, ...secret_keys) {
|
||||
payload = trim(as_string(payload));
|
||||
if (match(payload, /^(happ:\/\/)?crypt4\//i))
|
||||
payload = replace(payload, /^(happ:\/\/)?crypt4\//i, "");
|
||||
|
||||
payload = replace(payload, /[\r\n\t ]/g, "");
|
||||
payload = replace(payload, /-/g, "+");
|
||||
payload = replace(payload, /_/g, "/");
|
||||
|
||||
let rem = length(payload) % 4;
|
||||
if (rem == 1)
|
||||
return null;
|
||||
if (rem > 1) {
|
||||
for (let i = 0; i < 4 - rem; i++)
|
||||
payload += "=";
|
||||
}
|
||||
|
||||
let binary = b64dec(payload);
|
||||
if (binary == null || length(binary) < 32)
|
||||
return null;
|
||||
|
||||
let iv = substr(binary, 0, 16);
|
||||
let cipher = substr(binary, 16);
|
||||
if (length(cipher) % 16 != 0)
|
||||
return null;
|
||||
|
||||
let keys = [];
|
||||
let seen_keys = {};
|
||||
function add_key(k) {
|
||||
if (type(k) == "array") {
|
||||
for (let item in k)
|
||||
add_key(item);
|
||||
} else {
|
||||
k = trim(as_string(k));
|
||||
if (k != "" && !seen_keys[k]) {
|
||||
seen_keys[k] = true;
|
||||
push(keys, k);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (let k in secret_keys)
|
||||
add_key(k);
|
||||
|
||||
let dev_hwid = get_system_device_hwid();
|
||||
if (dev_hwid != "")
|
||||
add_key(dev_hwid);
|
||||
|
||||
add_key("HappDefaultSalt");
|
||||
|
||||
let iv_hex = common.bytes_to_hex(iv);
|
||||
|
||||
for (let k in keys) {
|
||||
let h = sha256_hex(k);
|
||||
if (length(h) != 64)
|
||||
continue;
|
||||
|
||||
// 1. Try OpenSSL fast path if available
|
||||
let open_res = try_openssl_decrypt(cipher, h, iv_hex, "aes-256-cbc");
|
||||
if (open_res != null)
|
||||
return open_res;
|
||||
|
||||
open_res = try_openssl_decrypt(cipher, substr(h, 0, 32), iv_hex, "aes-128-cbc");
|
||||
if (open_res != null)
|
||||
return open_res;
|
||||
|
||||
// 2. Pure ucode fallback
|
||||
let full_key = hex_to_bytes(h);
|
||||
let res = aes_cbc_decrypt(cipher, full_key, iv);
|
||||
if (res != null && is_valid_plaintext(res))
|
||||
return res;
|
||||
|
||||
res = aes_cbc_decrypt(cipher, substr(full_key, 0, 16), iv);
|
||||
if (res != null && is_valid_plaintext(res))
|
||||
return res;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function module_exports() {
|
||||
return {
|
||||
is_crypt4,
|
||||
strip_prefix,
|
||||
decrypt: decrypt_crypt4
|
||||
};
|
||||
}
|
||||
|
||||
if ((sourcepath(1) != null && sourcepath(1) != "") || ARGV[0] == null)
|
||||
return module_exports();
|
||||
|
||||
let mode = ARGV[0] || "";
|
||||
if (mode == "is-crypt4")
|
||||
exit(is_crypt4(ARGV[1]) ? 0 : 1);
|
||||
else if (mode == "decrypt") {
|
||||
let dec = decrypt_crypt4(ARGV[1], ARGV[2], ARGV[3]);
|
||||
if (dec == null)
|
||||
exit(1);
|
||||
print(dec);
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
let fs = require("fs");
|
||||
let common = require("core.common");
|
||||
let crypt4 = require("subscription.crypt4");
|
||||
let as_string = common.as_string;
|
||||
let object_or_empty = common.object_or_empty;
|
||||
let array_or_empty = common.array_or_empty;
|
||||
@@ -31,7 +32,8 @@ function is_supported_share_link(line) {
|
||||
starts_with(line, "socks4a://") ||
|
||||
starts_with(line, "socks5://") ||
|
||||
starts_with(line, "http://") ||
|
||||
starts_with(line, "https://");
|
||||
starts_with(line, "https://") ||
|
||||
crypt4.is_crypt4(line);
|
||||
}
|
||||
|
||||
function split_csv(value) {
|
||||
@@ -1786,6 +1788,30 @@ function normalize_uri_list_data(data, output_file) {
|
||||
line = replace(line, /\r/g, "");
|
||||
line = trim(line);
|
||||
if (line != "" && !starts_with(line, "#")) {
|
||||
if (crypt4.is_crypt4(line)) {
|
||||
let decrypted = crypt4.decrypt(line);
|
||||
if (decrypted != null && decrypted != "") {
|
||||
for (let dec_line in split(decrypted, "\n")) {
|
||||
dec_line = trim(dec_line);
|
||||
if (dec_line != "" && !starts_with(dec_line, "#")) {
|
||||
let outbound = parse_share_link(dec_line);
|
||||
if (outbound) {
|
||||
if (outbound_is_unsupported_stub(outbound)) {
|
||||
skipped++;
|
||||
continue;
|
||||
}
|
||||
if (added > 0)
|
||||
output.write(",");
|
||||
output.write(sprintf("%J", outbound));
|
||||
added++;
|
||||
} else {
|
||||
skipped++;
|
||||
}
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
let outbound = parse_share_link(line);
|
||||
if (outbound) {
|
||||
if (outbound_is_unsupported_stub(outbound)) {
|
||||
@@ -2202,6 +2228,30 @@ function normalize_uri_list_stream(input, output_file, strip_metadata) {
|
||||
if (strip_metadata && line_no <= 20 && is_metadata_preamble_line(line))
|
||||
continue;
|
||||
if (line != "" && !starts_with(line, "#")) {
|
||||
if (crypt4.is_crypt4(line)) {
|
||||
let decrypted = crypt4.decrypt(line);
|
||||
if (decrypted != null && decrypted != "") {
|
||||
for (let dec_line in split(decrypted, "\n")) {
|
||||
dec_line = trim(dec_line);
|
||||
if (dec_line != "" && !starts_with(dec_line, "#")) {
|
||||
let outbound = parse_share_link(dec_line);
|
||||
if (outbound) {
|
||||
if (outbound_is_unsupported_stub(outbound)) {
|
||||
skipped++;
|
||||
continue;
|
||||
}
|
||||
if (added > 0)
|
||||
output.write(",");
|
||||
output.write(sprintf("%J", outbound));
|
||||
added++;
|
||||
} else {
|
||||
skipped++;
|
||||
}
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
let outbound = parse_share_link(line);
|
||||
if (outbound) {
|
||||
if (outbound_is_unsupported_stub(outbound)) {
|
||||
@@ -2243,6 +2293,10 @@ function file_looks_like_uri_list(input_file) {
|
||||
line = trim(line);
|
||||
if (line == "" || starts_with(line, "#") || is_metadata_preamble_line(line))
|
||||
continue;
|
||||
if (crypt4.is_crypt4(line)) {
|
||||
input.close();
|
||||
return false;
|
||||
}
|
||||
if (is_supported_share_link(line)) {
|
||||
input.close();
|
||||
return true;
|
||||
@@ -3137,6 +3191,12 @@ function normalize_content_data(data, output_file, depth) {
|
||||
data = replace(data, /\r/g, "");
|
||||
data = strip_metadata_preamble_data(data);
|
||||
|
||||
if (depth < 2 && crypt4.is_crypt4(data)) {
|
||||
let decrypted = crypt4.decrypt(data);
|
||||
if (decrypted != null && decrypted != "")
|
||||
return normalize_content_data(decrypted, output_file, depth + 1);
|
||||
}
|
||||
|
||||
let first = first_non_ws_char(data);
|
||||
if (first == "{" || first == "[") {
|
||||
let decoded_json = json_decode_text(data);
|
||||
@@ -3231,7 +3291,7 @@ function parse_subscription_source_entry(entry) {
|
||||
if (index(entry, "|") >= 0)
|
||||
return subscription_source_entry_result(false, "", "", "Configure User-Agent in the subscription item settings");
|
||||
|
||||
if (!(substr(entry, 0, 7) == "http://" || substr(entry, 0, 8) == "https://"))
|
||||
if (!(substr(entry, 0, 7) == "http://" || substr(entry, 0, 8) == "https://" || crypt4.is_crypt4(entry)))
|
||||
return subscription_source_entry_result(false, "", "", "Subscription URL must start with http:// or https://");
|
||||
|
||||
return subscription_source_entry_result(true, entry, "", "");
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
#!/usr/bin/env bash
|
||||
set -eo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
TACHYON_LIB="$ROOT_DIR/tachyon/files/usr/lib"
|
||||
CRYPT4="$ROOT_DIR/tachyon/files/usr/lib/subscription/crypt4.uc"
|
||||
PARSER="$ROOT_DIR/tachyon/files/usr/lib/subscription/parser.uc"
|
||||
CACHE="$ROOT_DIR/tachyon/files/usr/lib/subscription/cache.uc"
|
||||
RUNTIME="$ROOT_DIR/tachyon/files/usr/lib/diagnostics/runtime.uc"
|
||||
|
||||
WORK_DIR="$(mktemp -d)"
|
||||
|
||||
cleanup() {
|
||||
rm -rf "$WORK_DIR"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
fail() {
|
||||
printf 'FAIL: %s\n' "$1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
ucode_run() {
|
||||
command ucode -L "$TACHYON_LIB" "$@"
|
||||
}
|
||||
|
||||
# 1. CLI is-crypt4 tests
|
||||
ucode_run "$CRYPT4" is-crypt4 "happ://crypt4/dGVzdA==" || fail "happ://crypt4 should be recognized"
|
||||
ucode_run "$CRYPT4" is-crypt4 "crypt4/dGVzdA==" || fail "crypt4/ should be recognized"
|
||||
if ucode_run "$CRYPT4" is-crypt4 "https://example.com/sub.txt" 2>/dev/null; then
|
||||
fail "https:// URL should not be recognized as crypt4"
|
||||
fi
|
||||
|
||||
# 2. End-to-end Decryption tests (AES-256-CBC)
|
||||
PLAINTEXT="vless://00000000-0000-4000-8000-000000000001@example.com:443?encryption=none&security=tls&sni=example.com#HappNode"
|
||||
SECRET="my-custom-device-hwid"
|
||||
|
||||
KEY_HEX="$(printf '%s' "$SECRET" | sha256sum | awk '{print $1}')"
|
||||
IV_HEX="000102030405060708090a0b0c0d0e0f"
|
||||
|
||||
# Encrypt with OpenSSL using file redirection to preserve null bytes
|
||||
printf '\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f' >"$WORK_DIR/iv.bin"
|
||||
printf '%s' "$PLAINTEXT" | openssl enc -aes-256-cbc -K "$KEY_HEX" -iv "$IV_HEX" >"$WORK_DIR/cipher.bin" 2>/dev/null
|
||||
cat "$WORK_DIR/iv.bin" "$WORK_DIR/cipher.bin" >"$WORK_DIR/payload.bin"
|
||||
B64_PAYLOAD="$(base64 <"$WORK_DIR/payload.bin" | tr -d '\r\n ')"
|
||||
CRYPT4_URL="happ://crypt4/${B64_PAYLOAD}"
|
||||
|
||||
# Decrypt using crypt4 CLI
|
||||
DECRYPTED="$(ucode_run "$CRYPT4" decrypt "$CRYPT4_URL" "$SECRET")"
|
||||
[ "$DECRYPTED" = "$PLAINTEXT" ] || fail "Decrypted plaintext mismatch: got '$DECRYPTED', expected '$PLAINTEXT'"
|
||||
|
||||
# 3. Default salt decryption ("HappDefaultSalt")
|
||||
SALT_HEX="$(printf '%s' "HappDefaultSalt" | sha256sum | awk '{print $1}')"
|
||||
printf '%s' "$PLAINTEXT" | openssl enc -aes-256-cbc -K "$SALT_HEX" -iv "$IV_HEX" >"$WORK_DIR/salt_cipher.bin" 2>/dev/null
|
||||
cat "$WORK_DIR/iv.bin" "$WORK_DIR/salt_cipher.bin" >"$WORK_DIR/salt_payload.bin"
|
||||
SALT_B64="$(base64 <"$WORK_DIR/salt_payload.bin" | tr -d '\r\n ')"
|
||||
SALT_CRYPT4_URL="crypt4/${SALT_B64}"
|
||||
|
||||
SALT_DECRYPTED="$(ucode_run "$CRYPT4" decrypt "$SALT_CRYPT4_URL")"
|
||||
[ "$SALT_DECRYPTED" = "$PLAINTEXT" ] || fail "Default salt decrypt mismatch: got '$SALT_DECRYPTED'"
|
||||
|
||||
# 4. Corrupted / invalid input tests
|
||||
if ucode_run "$CRYPT4" decrypt "crypt4/invalid_base64_!@#$" 2>/dev/null; then
|
||||
fail "corrupted base64 should return failure"
|
||||
fi
|
||||
|
||||
# 5. Parser integration
|
||||
cat >"$WORK_DIR/test_parser.uc" <<'UCODE'
|
||||
let parser = require("subscription.parser");
|
||||
|
||||
// URL source entry validation
|
||||
let entry = parser.parse_subscription_source_entry("happ://crypt4/dGVzdA==");
|
||||
if (!entry.valid)
|
||||
exit(1);
|
||||
|
||||
let entry2 = parser.parse_subscription_source_entry("crypt4/dGVzdA==");
|
||||
if (!entry2.valid)
|
||||
exit(2);
|
||||
|
||||
exit(0);
|
||||
UCODE
|
||||
ucode_run "$WORK_DIR/test_parser.uc" || fail "parser source entry validation for crypt4 failed"
|
||||
|
||||
# 6. Normalize content validation with encrypted file
|
||||
ENCRYPTED_FILE="$WORK_DIR/sub_encrypted.txt"
|
||||
NORMALIZED_OUT="$WORK_DIR/normalized.json"
|
||||
printf '%s' "$SALT_CRYPT4_URL" >"$ENCRYPTED_FILE"
|
||||
|
||||
cat >"$WORK_DIR/test_normalize.uc" <<'UCODE'
|
||||
let parser = require("subscription.parser");
|
||||
let enc_file = ARGV[0];
|
||||
let out_file = ARGV[1];
|
||||
|
||||
let ok = parser.normalize_content_validated(enc_file, out_file);
|
||||
if (!ok) exit(1);
|
||||
UCODE
|
||||
ucode_run "$WORK_DIR/test_normalize.uc" "$ENCRYPTED_FILE" "$NORMALIZED_OUT" || fail "normalize_content_validated failed on crypt4"
|
||||
|
||||
grep -Fq "example.com" "$NORMALIZED_OUT" || fail "normalized output missing expected server domain"
|
||||
grep -Fq "HappNode" "$NORMALIZED_OUT" || fail "normalized output missing server tag"
|
||||
|
||||
# 7. Cache download_subscription with crypt4 URL
|
||||
CACHE_OUT="$WORK_DIR/cache_sub.txt"
|
||||
ucode_run "$CACHE" download-subscription "$CRYPT4_URL" "$CACHE_OUT" "" "" "" "$SECRET" || fail "download_subscription failed for crypt4 URL"
|
||||
[ -s "$CACHE_OUT" ] || fail "download_subscription produced empty cache output"
|
||||
grep -Fq "HappNode" "$CACHE_OUT" || fail "download_subscription did not properly decrypt content"
|
||||
|
||||
# 8. Dual HWID request headers check in cache.uc
|
||||
grep -Fq '"X-HWID: " + hwid_val' "$CACHE" || fail "missing X-HWID header in cache.uc"
|
||||
grep -Fq '"HWID: " + hwid_val' "$CACHE" || fail "missing HWID header in cache.uc"
|
||||
|
||||
# 9. Diagnostics symptom collapsing test
|
||||
DOCTOR_OUT="$(ucode_run "$RUNTIME" ai-doctor 2>/dev/null)" || fail "ai-doctor failed to run"
|
||||
echo "$DOCTOR_OUT" | grep -Fq '"success": true' || fail "ai-doctor report missing success flag"
|
||||
echo "$DOCTOR_OUT" | grep -Fq '"model": "rule_engine_v2"' || fail "ai-doctor report missing rule_engine_v2 model"
|
||||
|
||||
# Verify symptom collapsing: when WAN or sing-box fails, downstream proxy updates are collapsed/suppressed
|
||||
if echo "$DOCTOR_OUT" | grep -Fq '"update_subscriptions"'; then
|
||||
fail "update_subscriptions should be suppressed when WAN/core service is unreachable"
|
||||
fi
|
||||
|
||||
printf 'All Happ Crypt4, HWID, and symptom collapsing checks passed!\n'
|
||||
Reference in New Issue
Block a user