fix: resolve crash on geoip_country selection and harden hosts list download (1.3.20)

This commit is contained in:
Dushnilin
2026-09-04 17:46:48 +03:00
parent 2f83954255
commit 9927c04a6f
7 changed files with 245 additions and 41 deletions
@@ -8214,13 +8214,21 @@ function createSectionContent(section) {
tdCheck.appendChild(cb);
tr.appendChild(tdCheck);
const tdUrl = E("td", { class: "cbi-section-table-cell" });
const tdUrl = E("td", {
class: "cbi-section-table-cell",
style: "word-break:break-all;overflow-wrap:anywhere;font-size:12px;line-height:1.3;max-width:320px;",
});
let displayUrl = url;
try {
displayUrl = decodeURIComponent(url);
} catch(e) {}
const urlSpan = E(
"span",
{
style: isOff ? "text-decoration:line-through;opacity:0.6;" : "",
title: url,
},
url,
displayUrl,
);
tdUrl.appendChild(urlSpan);
tr.appendChild(tdUrl);
@@ -8292,8 +8300,10 @@ function createSectionContent(section) {
class: "btn cbi-button cbi-button-action",
type: "button",
click: function () {
const val = (input.value || "").trim();
let val = (input.value || "").trim();
if (!val) return;
val = val.replace(/^https?:\/\/github\.com\/([^/]+)\/([^/]+)\/blob\/(.+)$/, "https://raw.githubusercontent.com/$1/$2/$3");
val = val.replace(/^https?:\/\/github\.com\/([^/]+)\/([^/]+)\/raw\/(.+)$/, "https://raw.githubusercontent.com/$1/$2/$3");
let u = getUrls();
if (u.includes(val)) {
input.value = "";
@@ -8316,6 +8326,54 @@ function createSectionContent(section) {
addRow.appendChild(input);
addRow.appendChild(addBtn);
container.appendChild(addRow);
if (urls.length > 0) {
const updateRow = E("div", {
style: "margin-top:8px;display:flex;justify-content:flex-end;",
});
const updateBtn = E(
"button",
{
class: "cbi-button cbi-button-action",
type: "button",
click: function () {
updateBtn.disabled = true;
updateBtn.textContent = _("Updating…");
fs.exec("/usr/bin/tachyon", ["hosts_list_update"])
.then(function (res) {
updateBtn.disabled = false;
updateBtn.textContent = _("Update Lists Now");
let ok = false;
let msg = "";
try {
const data = JSON.parse(res.stdout || "{}");
if (data.success) {
ok = true;
msg = _("Successfully updated %d hosts entries").format(data.entries);
} else {
msg = _("Update failed: ") + (data.error || res.stderr || "");
}
} catch (e) {
msg = res.stdout || res.stderr || _("Finished with code ") + res.code;
}
ui.addNotification(
null,
E("p", {}, msg),
ok ? "info" : "danger"
);
})
.catch(function (err) {
updateBtn.disabled = false;
updateBtn.textContent = _("Update Lists Now");
ui.addNotification(null, E("p", {}, err.message || err), "danger");
});
},
},
_("Update Lists Now"),
);
updateRow.appendChild(updateBtn);
container.appendChild(updateRow);
}
}
render();
+30 -5
View File
@@ -67,12 +67,33 @@ function enabled_hosts_sections() {
return result;
}
function command_exists(name) {
return system("command -v " + shell_quote(name) + " >/dev/null 2>&1") == 0;
}
function hash12(str) {
str = as_string(str);
let h1 = 5381;
let h2 = 52711;
for (let i = 0; i < length(str); i++) {
let code = ord(str, i);
h1 = ((h1 * 33) + code) % 2147483647;
h2 = ((h2 * 31) + code) % 2147483647;
}
return sprintf("%08x%04x", h1, h2 % 65536);
}
function http_get_to_file(url, output_path) {
run("mkdir -p " + shell_quote(HOSTS_TMP_DIR));
let cmd = "wget -q -O " + shell_quote(output_path) + " --timeout=" + CONNECT_TIMEOUT + " " + shell_quote(url) + " 2>&1";
mkdir_p(HOSTS_TMP_DIR);
let cmd;
if (command_exists("curl")) {
cmd = "curl -fsSL --connect-timeout 10 -m " + CONNECT_TIMEOUT + " -o " + shell_quote(output_path) + " " + shell_quote(url) + " 2>&1";
} else {
cmd = "wget -q -O " + shell_quote(output_path) + " --timeout=" + CONNECT_TIMEOUT + " " + shell_quote(url) + " 2>&1";
}
log("Running: " + cmd);
let result = run(cmd);
log("wget exit: " + as_string(result) + " file_exists: " + as_string(fs.stat(output_path) != null));
log("download exit: " + as_string(result) + " file_exists: " + as_string(fs.stat(output_path) != null));
return result;
}
@@ -139,7 +160,8 @@ function parse_hosts_file(path) {
function write_hosts_cache(entries, source_urls) {
mkdir_p(HOSTS_CACHE_DIR);
let tmp_path = HOSTS_CACHE_FILE + ".tmp." + as_string(getpid());
let pid = fs.readlink("/proc/self") || "0";
let tmp_path = HOSTS_CACHE_FILE + ".tmp." + pid;
let fh = fs.open(tmp_path, "w");
if (fh == null) {
log("Failed to open " + tmp_path + " for writing", "error");
@@ -217,7 +239,10 @@ function hosts_list_update(target_url) {
for (let url in urls) {
let label = "hosts list from " + url;
let safe_name = replace(replace(url, /:/g, "_"), /[^a-zA-Z0-9_]/g, "_");
let clean = replace(replace(url, /:/g, "_"), /[^a-zA-Z0-9_]/g, "_");
if (length(clean) > 40)
clean = substr(clean, 0, 40);
let safe_name = clean + "_" + hash12(url);
let tmp_file = HOSTS_TMP_DIR + "/list-" + safe_name + ".txt";
if (download_with_retry(url, tmp_file, label) != null) {
@@ -549,6 +549,14 @@ function dscp_value(section) {
return list_option_value_from_array(dscp_list(section));
}
function contains(arr, val) {
if (type(arr) != "array") return false;
for (let item in arr) {
if (item == val) return true;
}
return false;
}
function geoip_country_list(section) {
let raw = whitespace_list_value(section, "geoip_country");
let result = [];
+89 -28
View File
@@ -180,42 +180,99 @@ function query_params(value) {
return result;
}
function github_to_jsdelivr(url) {
function normalize_github_raw(url) {
url = as_string(url);
if (index(url, "github.com/") < 0)
return "";
let marker = "github.com/";
let marker_idx = index(url, marker);
if (marker_idx < 0)
return url;
let path = substr(url, index(url, "github.com/") + 11);
let slash1 = index(path, "/");
if (slash1 < 0) return "";
let owner = substr(path, 0, slash1);
let rest = substr(path, slash1 + 1);
let path_part = substr(url, marker_idx + length(marker));
let slash1 = index(path_part, "/");
if (slash1 < 0) return url;
let owner = substr(path_part, 0, slash1);
let rest = substr(path_part, slash1 + 1);
let slash2 = index(rest, "/");
if (slash2 < 0) return "";
if (slash2 < 0) return url;
let repo = substr(rest, 0, slash2);
let tail = substr(rest, slash2 + 1);
let slash3 = index(tail, "/");
if (slash3 < 0) return "";
let segment3 = substr(tail, 0, slash3);
let file = substr(tail, slash3 + 1);
if (file == "") return "";
let clean_tail = tail;
let q = index(clean_tail, "?");
if (q >= 0) clean_tail = substr(clean_tail, 0, q);
let h = index(clean_tail, "#");
if (h >= 0) clean_tail = substr(clean_tail, 0, h);
let ref = "";
if (segment3 == "releases" && index(tail, "releases/download/") == 0) {
let dl_path = substr(tail, 17);
let dl_slash = index(dl_path, "/");
if (dl_slash < 0) return "";
ref = substr(dl_path, 0, dl_slash);
file = substr(dl_path, dl_slash + 1);
} else if (segment3 == "raw") {
ref = substr(tail, slash3 + 1);
let raw_slash = index(ref, "/");
if (raw_slash < 0) return "";
file = substr(ref, raw_slash + 1);
ref = substr(ref, 0, raw_slash);
if (index(clean_tail, "blob/") == 0) {
let subpath = substr(clean_tail, 5);
return "https://raw.githubusercontent.com/" + owner + "/" + repo + "/" + subpath;
}
if (index(clean_tail, "raw/") == 0) {
let subpath = substr(clean_tail, 4);
return "https://raw.githubusercontent.com/" + owner + "/" + repo + "/" + subpath;
}
return url;
}
function github_to_jsdelivr(url) {
url = as_string(url);
let marker_gh = "github.com/";
let marker_raw = "raw.githubusercontent.com/";
let idx_gh = index(url, marker_gh);
let idx_raw = index(url, marker_raw);
if (idx_gh < 0 && idx_raw < 0)
return "";
let owner = "", repo = "", ref = "", file = "";
if (idx_raw >= 0) {
let path_part = substr(url, idx_raw + length(marker_raw));
let slash1 = index(path_part, "/");
if (slash1 < 0) return "";
owner = substr(path_part, 0, slash1);
let rest = substr(path_part, slash1 + 1);
let slash2 = index(rest, "/");
if (slash2 < 0) return "";
repo = substr(rest, 0, slash2);
let tail = substr(rest, slash2 + 1);
let slash3 = index(tail, "/");
if (slash3 < 0) return "";
ref = substr(tail, 0, slash3);
file = substr(tail, slash3 + 1);
} else {
ref = segment3;
let path_part = substr(url, idx_gh + length(marker_gh));
let slash1 = index(path_part, "/");
if (slash1 < 0) return "";
owner = substr(path_part, 0, slash1);
let rest = substr(path_part, slash1 + 1);
let slash2 = index(rest, "/");
if (slash2 < 0) return "";
repo = substr(rest, 0, slash2);
let tail = substr(rest, slash2 + 1);
let slash3 = index(tail, "/");
if (slash3 < 0) return "";
let segment3 = substr(tail, 0, slash3);
let remainder = substr(tail, slash3 + 1);
if (remainder == "") return "";
if (segment3 == "releases" && index(tail, "releases/download/") == 0) {
let dl_path = substr(tail, 17);
let dl_slash = index(dl_path, "/");
if (dl_slash < 0) return "";
ref = substr(dl_path, 0, dl_slash);
file = substr(dl_path, dl_slash + 1);
} else if (segment3 == "raw" || segment3 == "blob") {
let raw_slash = index(remainder, "/");
if (raw_slash < 0) return "";
ref = substr(remainder, 0, raw_slash);
file = substr(remainder, raw_slash + 1);
} else {
ref = segment3;
file = remainder;
}
}
if (ref == "" || file == "")
@@ -228,6 +285,8 @@ function download_candidates(value) {
if (value == "")
return [];
value = normalize_github_raw(value);
let candidates = [ value ];
let is_github = index(value, "github.com") >= 0 ||
index(value, "raw.githubusercontent.com") >= 0 ||
@@ -264,5 +323,7 @@ return {
userinfo,
path,
query_params,
normalize_github_raw,
github_to_jsdelivr,
download_candidates
};
+9 -1
View File
@@ -2,7 +2,7 @@
set -eo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
TACHYON_LIB="$ROOT_DIR/tachyon/files/usr/lib"
TACHYON_LIB="${TACHYON_LIB:-$ROOT_DIR/tachyon/files/usr/lib}"
WORK_DIR="$(mktemp -d)"
cleanup() {
@@ -66,6 +66,14 @@ function assert(val, msg) {
let index = connections.item_index_from_cursor(cursor, "tachyon");
assert(index.subscription_url.by_name.sub1.url == "https://example.com/sub.txt", "parse subscription URL");
assert(index.urltest.by_name.group1.ports[0] == "80", "parse urltest group ports");
// geoip_country_list and geoip_country_mode tests
let geo_res = connections.geoip_country_list({ geoip_country: [ "ru", "de", "ru" ] });
assert(geo_res[0] == "ru" && geo_res[1] == "de" && length(geo_res) == 2, "geoip list de-duplicate");
assert(connections.geoip_country_list({ geoip_country: "non-ru" })[0] == "ru", "geoip non-ru alias");
assert(connections.geoip_country_mode({ geoip_country: "non-ru" }) == "exclude", "geoip mode for non-ru");
assert(connections.geoip_country_mode({ geoip_mode: "exclude" }) == "exclude", "geoip mode exclude");
assert(connections.geoip_country_mode({ geoip_mode: "include" }) == "include", "geoip mode include");
'
printf 'config/connections checks passed\n'
+15 -1
View File
@@ -2,7 +2,7 @@
set -eo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
TACHYON_LIB="$ROOT_DIR/tachyon/files/usr/lib"
TACHYON_LIB="${TACHYON_LIB:-$ROOT_DIR/tachyon/files/usr/lib}"
ucode -L "$TACHYON_LIB" -e '
let url = require("core.url");
@@ -31,6 +31,20 @@ assert(url.strip_fragment("http://example.com") == "http://example.com", "strip
// strip_anchored_scheme
assert(url.strip_anchored_scheme("http://example.com") == "example.com", "strip anchored http");
assert(url.strip_anchored_scheme("example.com") == "example.com", "strip no scheme");
// normalize_github_raw
assert(url.normalize_github_raw("https://github.com/owner/repo/blob/main/path/to/hosts") == "https://raw.githubusercontent.com/owner/repo/main/path/to/hosts", "normalize blob url");
assert(url.normalize_github_raw("https://github.com/owner/repo/raw/main/path/to/hosts") == "https://raw.githubusercontent.com/owner/repo/main/path/to/hosts", "normalize raw url");
assert(url.normalize_github_raw("https://raw.githubusercontent.com/owner/repo/main/path/to/hosts") == "https://raw.githubusercontent.com/owner/repo/main/path/to/hosts", "keep raw url");
assert(url.normalize_github_raw("https://example.com/hosts.txt") == "https://example.com/hosts.txt", "keep non-github url");
// github_to_jsdelivr
assert(url.github_to_jsdelivr("https://github.com/owner/repo/blob/main/path/to/hosts") == "https://cdn.jsdelivr.net/gh/owner/repo@main/path/to/hosts", "jsdelivr from blob");
assert(url.github_to_jsdelivr("https://raw.githubusercontent.com/owner/repo/main/path/to/hosts") == "https://cdn.jsdelivr.net/gh/owner/repo@main/path/to/hosts", "jsdelivr from raw");
// download_candidates
let candidates = url.download_candidates("https://github.com/owner/repo/blob/main/hosts");
assert(candidates[0] == "https://raw.githubusercontent.com/owner/repo/main/hosts", "download_candidates first entry normalized");
'
printf 'core/url checks passed\n'
+33 -3
View File
@@ -2,14 +2,16 @@
set -eo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
HOSTS_UC="$ROOT_DIR/tachyon/files/usr/lib/components/hosts.uc"
TACHYON_LIB="$ROOT_DIR/tachyon/files/usr/lib"
HOSTS_UC="${HOSTS_UC:-$ROOT_DIR/tachyon/files/usr/lib/components/hosts.uc}"
TACHYON_LIB="${TACHYON_LIB:-$ROOT_DIR/tachyon/files/usr/lib}"
TMP_DIR="$(mktemp -d)"
PASS=0
FAIL=0
trap 'rm -rf "$TMP_DIR"' EXIT
mkdir -p "$TMP_DIR"
mkdir -p "$TMP_DIR/cache" "$TMP_DIR/tmp"
export TACHYON_HOSTS_CACHE_DIR="$TMP_DIR/cache"
export TACHYON_HOSTS_TMP_DIR="$TMP_DIR/tmp"
ucode() {
command ucode -L "$TACHYON_LIB" "$@"
@@ -72,6 +74,34 @@ echo "=== Test: hosts_list_status with no URLs ==="
output=$(ucode "$HOSTS_UC" list-status 2>/dev/null || true)
assert_eq "false" "$(echo "$output" | grep -o '"cache_exists":[a-z]*' | cut -d: -f2)" "cache_exists should be false initially"
# --- Test 5: Safe name length for long percent-encoded URLs ---
echo "=== Test: safe_name length for long URLs ==="
long_url_safe_len=$(ucode -e '
let common = require("core.common");
let as_string = common.as_string;
function hash12(str) {
str = as_string(str);
let h1 = 5381;
let h2 = 52711;
for (let i = 0; i < length(str); i++) {
let code = ord(str, i);
h1 = ((h1 * 33) + code) % 2147483647;
h2 = ((h2 * 31) + code) % 2147483647;
}
return sprintf("%08x%04x", h1, h2 % 65536);
}
let url = "https://raw.githubusercontent.com/V3nilla/IPSets-For-Bypass-in-Russia/main/%D0%A0%D0%B0%D0%B7%D0%B1%D0%BB%D0%BE%D0%BA%D0%B8%D1%80%D0%BE%D0%B2%D0%BA%D0%B0%20%D0%BC%D0%BD%D0%BE%D0%B6%D0%B5%D1%81%D1%82%D0%B2%D0%B0%20%D1%81%D0%B5%D1%80%D0%B2%D0%B8%D1%81%D0%BE%D0%B2(%D0%BF%D1%80%D0%B8%D0%BC%D0%B5%D1%80%20-%20ChatGPT)/hosts";
let clean = replace(replace(url, /:/g, "_"), /[^a-zA-Z0-9_]/g, "_");
if (length(clean) > 40) clean = substr(clean, 0, 40);
let safe_name = clean + "_" + hash12(url);
print(length(safe_name));
')
if [ "$long_url_safe_len" -le 60 ]; then
pass
else
fail "safe_name too long: $long_url_safe_len"
fi
echo ""
echo "=== Results: $PASS passed, $FAIL failed ==="
[ "$FAIL" -eq 0 ] || exit 1