fix(subscription): auto fallback on unsupported app provider stubs
This commit is contained in:
@@ -7,8 +7,8 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: TACHYON\n"
|
"Project-Id-Version: TACHYON\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2026-09-17 10:43+0300\n"
|
"POT-Creation-Date: 2026-09-17 11:39+0300\n"
|
||||||
"PO-Revision-Date: 2026-09-17 10:43+0300\n"
|
"PO-Revision-Date: 2026-09-17 11:39+0300\n"
|
||||||
"Last-Translator: Dushnilin <dushnilin@gmail.com>\n"
|
"Last-Translator: Dushnilin <dushnilin@gmail.com>\n"
|
||||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||||
"Language: \n"
|
"Language: \n"
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: TACHYON\n"
|
"Project-Id-Version: TACHYON\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2026-09-17 13:43+0300\n"
|
"POT-Creation-Date: 2026-09-17 14:39+0300\n"
|
||||||
"PO-Revision-Date: 2026-09-17 13:43+0300\n"
|
"PO-Revision-Date: 2026-09-17 14:39+0300\n"
|
||||||
"Last-Translator: Dushnilin\n"
|
"Last-Translator: Dushnilin\n"
|
||||||
"Language-Team: none\n"
|
"Language-Team: none\n"
|
||||||
"Language: ru\n"
|
"Language: ru\n"
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: TACHYON\n"
|
"Project-Id-Version: TACHYON\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2026-09-17 13:43+0300\n"
|
"POT-Creation-Date: 2026-09-17 14:39+0300\n"
|
||||||
"PO-Revision-Date: 2026-09-17 13:43+0300\n"
|
"PO-Revision-Date: 2026-09-17 14:39+0300\n"
|
||||||
"Last-Translator: Dushnilin\n"
|
"Last-Translator: Dushnilin\n"
|
||||||
"Language-Team: none\n"
|
"Language-Team: none\n"
|
||||||
"Language: ru\n"
|
"Language: ru\n"
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: TACHYON\n"
|
"Project-Id-Version: TACHYON\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2026-09-17 10:43+0300\n"
|
"POT-Creation-Date: 2026-09-17 11:39+0300\n"
|
||||||
"PO-Revision-Date: 2026-09-17 10:43+0300\n"
|
"PO-Revision-Date: 2026-09-17 11:39+0300\n"
|
||||||
"Last-Translator: Dushnilin <dushnilin@gmail.com>\n"
|
"Last-Translator: Dushnilin <dushnilin@gmail.com>\n"
|
||||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||||
"Language: \n"
|
"Language: \n"
|
||||||
|
|||||||
@@ -346,19 +346,78 @@ function reality_public_key_valid(value) {
|
|||||||
return match(as_string(value), /^[A-Za-z0-9_-]{43}$/) != null;
|
return match(as_string(value), /^[A-Za-z0-9_-]{43}$/) != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function outbound_is_unsupported_stub(outbound) {
|
||||||
|
if (type(outbound) != "object")
|
||||||
|
return false;
|
||||||
|
|
||||||
|
let tag = lc(as_string(outbound.tag || outbound.name || outbound.remark || ""));
|
||||||
|
let server = lc(trim(as_string(outbound.server || "")));
|
||||||
|
|
||||||
|
if (index(tag, "не поддерживается") >= 0 ||
|
||||||
|
index(tag, "неподдерживаем") >= 0 ||
|
||||||
|
index(tag, "клиент не поддерживается") >= 0 ||
|
||||||
|
index(tag, "приложение не поддерживается") >= 0 ||
|
||||||
|
index(tag, "unsupported") >= 0 ||
|
||||||
|
index(tag, "not supported") >= 0 ||
|
||||||
|
index(tag, "update app") >= 0 ||
|
||||||
|
index(tag, "update client") >= 0 ||
|
||||||
|
index(tag, "обновите приложение") >= 0 ||
|
||||||
|
index(tag, "обновите клиент") >= 0 ||
|
||||||
|
index(tag, "скачайте happ") >= 0 ||
|
||||||
|
index(tag, "скачать happ") >= 0 ||
|
||||||
|
index(tag, "используйте happ") >= 0 ||
|
||||||
|
index(tag, "use happ") >= 0 ||
|
||||||
|
index(tag, "download happ") >= 0 ||
|
||||||
|
index(tag, "скачайте v2box") >= 0 ||
|
||||||
|
index(tag, "используйте v2box") >= 0 ||
|
||||||
|
index(tag, "скачайте v2rayn") >= 0 ||
|
||||||
|
index(tag, "используйте v2rayn") >= 0)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
let is_dummy_server = (
|
||||||
|
server == "" ||
|
||||||
|
server == "127.0.0.1" ||
|
||||||
|
server == "0.0.0.0" ||
|
||||||
|
server == "localhost" ||
|
||||||
|
server == "::1" ||
|
||||||
|
server == "0.0.0.1"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (is_dummy_server && (
|
||||||
|
index(tag, "⚠️") >= 0 ||
|
||||||
|
index(tag, "❗") >= 0 ||
|
||||||
|
index(tag, "❌") >= 0 ||
|
||||||
|
index(tag, "warning") >= 0 ||
|
||||||
|
index(tag, "error") >= 0 ||
|
||||||
|
index(tag, "notice") >= 0 ||
|
||||||
|
index(tag, "attention") >= 0 ||
|
||||||
|
index(tag, "внимание") >= 0 ||
|
||||||
|
index(tag, "ошибка") >= 0
|
||||||
|
))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
function validate_subscription(path) {
|
function validate_subscription(path) {
|
||||||
let value = read_json_file(path);
|
let value = read_json_file(path);
|
||||||
if (type(value) != "object" || type(value.outbounds) != "array" || length(value.outbounds) == 0)
|
if (type(value) != "object" || type(value.outbounds) != "array" || length(value.outbounds) == 0)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
let usable_count = 0;
|
||||||
for (let outbound in value.outbounds) {
|
for (let outbound in value.outbounds) {
|
||||||
|
if (outbound_is_unsupported_stub(outbound))
|
||||||
|
continue;
|
||||||
|
|
||||||
let tls = type(outbound) == "object" && type(outbound.tls) == "object" ? outbound.tls : {};
|
let tls = type(outbound) == "object" && type(outbound.tls) == "object" ? outbound.tls : {};
|
||||||
let reality = tls.reality;
|
let reality = tls.reality;
|
||||||
if (type(reality) == "object" && reality.enabled !== false && !reality_public_key_valid(reality.public_key))
|
if (type(reality) == "object" && reality.enabled !== false && !reality_public_key_valid(reality.public_key))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
usable_count++;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return usable_count > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
function write_json_file(path, value) {
|
function write_json_file(path, value) {
|
||||||
@@ -1685,8 +1744,13 @@ function normalize_clash_yaml(input_file, output_file) {
|
|||||||
|
|
||||||
for (let record in clash_yaml_records(input_file)) {
|
for (let record in clash_yaml_records(input_file)) {
|
||||||
let outbound = parse_clash_record(record);
|
let outbound = parse_clash_record(record);
|
||||||
if (outbound)
|
if (outbound) {
|
||||||
|
if (outbound_is_unsupported_stub(outbound)) {
|
||||||
|
skipped++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
push(outbounds, outbound);
|
push(outbounds, outbound);
|
||||||
|
}
|
||||||
else
|
else
|
||||||
skipped++;
|
skipped++;
|
||||||
}
|
}
|
||||||
@@ -1724,6 +1788,10 @@ function normalize_uri_list_data(data, output_file) {
|
|||||||
if (line != "" && !starts_with(line, "#")) {
|
if (line != "" && !starts_with(line, "#")) {
|
||||||
let outbound = parse_share_link(line);
|
let outbound = parse_share_link(line);
|
||||||
if (outbound) {
|
if (outbound) {
|
||||||
|
if (outbound_is_unsupported_stub(outbound)) {
|
||||||
|
skipped++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (added > 0)
|
if (added > 0)
|
||||||
output.write(",");
|
output.write(",");
|
||||||
output.write(sprintf("%J", outbound));
|
output.write(sprintf("%J", outbound));
|
||||||
@@ -2136,6 +2204,10 @@ function normalize_uri_list_stream(input, output_file, strip_metadata) {
|
|||||||
if (line != "" && !starts_with(line, "#")) {
|
if (line != "" && !starts_with(line, "#")) {
|
||||||
let outbound = parse_share_link(line);
|
let outbound = parse_share_link(line);
|
||||||
if (outbound) {
|
if (outbound) {
|
||||||
|
if (outbound_is_unsupported_stub(outbound)) {
|
||||||
|
skipped++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (added > 0)
|
if (added > 0)
|
||||||
output.write(",");
|
output.write(",");
|
||||||
output.write(sprintf("%J", outbound));
|
output.write(sprintf("%J", outbound));
|
||||||
@@ -2906,7 +2978,7 @@ function normalize_sing_box_json_outbounds(candidates) {
|
|||||||
let detour_refs = {};
|
let detour_refs = {};
|
||||||
|
|
||||||
for (let outbound in candidates) {
|
for (let outbound in candidates) {
|
||||||
if (type(outbound) != "object")
|
if (type(outbound) != "object" || outbound_is_unsupported_stub(outbound))
|
||||||
continue;
|
continue;
|
||||||
outbound = normalize_sing_box_hysteria2_outbound(outbound);
|
outbound = normalize_sing_box_hysteria2_outbound(outbound);
|
||||||
outbound = normalize_sing_box_tuic_outbound(outbound);
|
outbound = normalize_sing_box_tuic_outbound(outbound);
|
||||||
@@ -3177,7 +3249,8 @@ function module_exports() {
|
|||||||
normalized_skipped_message,
|
normalized_skipped_message,
|
||||||
try_decode_gzip_content_file,
|
try_decode_gzip_content_file,
|
||||||
extract_ui_metadata_file,
|
extract_ui_metadata_file,
|
||||||
runtime_outbounds_equal
|
runtime_outbounds_equal,
|
||||||
|
outbound_is_unsupported_stub
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,122 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -eo pipefail
|
||||||
|
|
||||||
|
TEST_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
ROOT_DIR="$(cd "$TEST_DIR/.." && pwd)"
|
||||||
|
TACHYON_LIB="$ROOT_DIR/tachyon/files/usr/lib"
|
||||||
|
PARSER_UC="$TACHYON_LIB/subscription/parser.uc"
|
||||||
|
CACHE_UC="$TACHYON_LIB/subscription/cache.uc"
|
||||||
|
|
||||||
|
WORK_DIR="$(mktemp -d)"
|
||||||
|
trap 'rm -rf "$WORK_DIR"' EXIT
|
||||||
|
|
||||||
|
fail() {
|
||||||
|
echo "FAIL: $*" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# 1. Test that a subscription with only "Данное приложение не поддерживается" is rejected
|
||||||
|
cat >"$WORK_DIR/stub_uri.txt" <<'EOF'
|
||||||
|
vless://00000000-0000-0000-0000-000000000000@127.0.0.1:443?security=none#%D0%94%D0%B0%D0%BD%D0%BD%D0%BE%D0%B5%20%D0%BF%D1%80%D0%B8%D0%BB%D0%BE%D0%B6%D0%B5%D0%BD%D0%B8%D0%B5%20%D0%BD%D0%B5%20%D0%BF%D0%BE%D0%B4%D0%B4%D0%B5%D1%80%D0%B6%D0%B8%D0%B2%D0%B0%D0%B5%D1%82%D1%81%D1%8F
|
||||||
|
EOF
|
||||||
|
|
||||||
|
if ucode -L "$TACHYON_LIB" "$PARSER_UC" normalize-content-validated "$WORK_DIR/stub_uri.txt" "$WORK_DIR/out_stub.json" >/dev/null 2>&1; then
|
||||||
|
fail "normalize-content-validated should reject subscription with only 'Данное приложение не поддерживается'"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 2. Test English variant: "This app is not supported. Use Happ"
|
||||||
|
cat >"$WORK_DIR/stub_en.txt" <<'EOF'
|
||||||
|
vless://00000000-0000-0000-0000-000000000000@127.0.0.1:443?security=none#Unsupported%20client%20-%20Use%20Happ
|
||||||
|
EOF
|
||||||
|
|
||||||
|
if ucode -L "$TACHYON_LIB" "$PARSER_UC" normalize-content-validated "$WORK_DIR/stub_en.txt" "$WORK_DIR/out_stub_en.json" >/dev/null 2>&1; then
|
||||||
|
fail "normalize-content-validated should reject English unsupported client notice"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 3. Test sing-box JSON with only stub outbound
|
||||||
|
cat >"$WORK_DIR/stub_singbox.json" <<'EOF'
|
||||||
|
{
|
||||||
|
"outbounds": [
|
||||||
|
{
|
||||||
|
"type": "vless",
|
||||||
|
"tag": "⚠️ Данное приложение не поддерживается",
|
||||||
|
"server": "127.0.0.1",
|
||||||
|
"server_port": 443,
|
||||||
|
"uuid": "00000000-0000-0000-0000-000000000000"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
if ucode -L "$TACHYON_LIB" "$PARSER_UC" normalize-content-validated "$WORK_DIR/stub_singbox.json" "$WORK_DIR/out_stub_sb.json" >/dev/null 2>&1; then
|
||||||
|
fail "normalize-content-validated should reject singbox JSON with stub outbound"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 4. Test Clash YAML with only stub outbound
|
||||||
|
cat >"$WORK_DIR/stub_clash.yaml" <<'EOF'
|
||||||
|
proxies:
|
||||||
|
- name: "Данное приложение не поддерживается"
|
||||||
|
type: vless
|
||||||
|
server: 127.0.0.1
|
||||||
|
port: 443
|
||||||
|
uuid: 00000000-0000-0000-0000-000000000000
|
||||||
|
EOF
|
||||||
|
|
||||||
|
if ucode -L "$TACHYON_LIB" "$PARSER_UC" normalize-content-validated "$WORK_DIR/stub_clash.yaml" "$WORK_DIR/out_stub_clash.json" >/dev/null 2>&1; then
|
||||||
|
fail "normalize-content-validated should reject Clash YAML with stub outbound"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 5. Test mixed subscription: 1 real node + 1 stub node
|
||||||
|
cat >"$WORK_DIR/mixed_uri.txt" <<'EOF'
|
||||||
|
vless://11111111-1111-1111-1111-111111111111@vpn.example.com:443?security=none#Valid-Server-DE
|
||||||
|
vless://00000000-0000-0000-0000-000000000000@127.0.0.1:443?security=none#Данное%20приложение%20не%20поддерживается
|
||||||
|
EOF
|
||||||
|
|
||||||
|
ucode -L "$TACHYON_LIB" "$PARSER_UC" normalize-content-validated "$WORK_DIR/mixed_uri.txt" "$WORK_DIR/out_mixed.json" ||
|
||||||
|
fail "normalize-content-validated should accept mixed subscription with valid nodes"
|
||||||
|
|
||||||
|
ucode -e '
|
||||||
|
let fs = require("fs");
|
||||||
|
let doc = json(fs.readfile("'"$WORK_DIR/out_mixed.json"'"));
|
||||||
|
let list = doc.outbounds || [];
|
||||||
|
if (length(list) != 1) {
|
||||||
|
print("Expected 1 valid outbound after filtering stub, got " + length(list) + "\n");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
if (list[0].tag != "Valid-Server-DE" || list[0].server != "vpn.example.com") {
|
||||||
|
print("Unexpected outbound content: " + sprintf("%J", list[0]) + "\n");
|
||||||
|
exit(2);
|
||||||
|
}
|
||||||
|
' || fail "Failed to filter out stub node from mixed subscription"
|
||||||
|
|
||||||
|
# 6. Test validate-subscription CLI mode directly
|
||||||
|
if ucode -L "$TACHYON_LIB" "$PARSER_UC" validate-subscription "$WORK_DIR/stub_singbox.json" >/dev/null 2>&1; then
|
||||||
|
fail "validate-subscription CLI should return error for stub-only JSON"
|
||||||
|
fi
|
||||||
|
|
||||||
|
ucode -L "$TACHYON_LIB" "$PARSER_UC" validate-subscription "$WORK_DIR/out_mixed.json" >/dev/null ||
|
||||||
|
fail "validate-subscription CLI should return success for valid JSON"
|
||||||
|
|
||||||
|
# 7. Test programmatic validate_subscription in parser module
|
||||||
|
cat >"$WORK_DIR/test_validate.uc" <<'EOF'
|
||||||
|
let parser = require("subscription.parser");
|
||||||
|
let stub_file = ARGV[0];
|
||||||
|
let valid_file = ARGV[1];
|
||||||
|
|
||||||
|
if (parser.validate_subscription(stub_file)) {
|
||||||
|
print("validate_subscription must return false for stub JSON\n");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
if (!parser.validate_subscription(valid_file)) {
|
||||||
|
print("validate_subscription must return true for valid JSON\n");
|
||||||
|
exit(2);
|
||||||
|
}
|
||||||
|
exit(0);
|
||||||
|
EOF
|
||||||
|
|
||||||
|
ucode -L "$TACHYON_LIB" "$WORK_DIR/test_validate.uc" "$WORK_DIR/stub_singbox.json" "$WORK_DIR/out_mixed.json" ||
|
||||||
|
fail "subscription validation cache checks failed"
|
||||||
|
|
||||||
|
echo "subscription unsupported stub checks passed"
|
||||||
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user