fix: resolve crash on geoip_country selection and harden hosts list download (1.3.20)
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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 = [];
|
||||
|
||||
@@ -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
|
||||
};
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user